diff --git a/.config/example.yml b/.config/example.yml
index cd08f76d61a2685027e62255477961f0df3ff77a..91580d9ecbe459505b02df704b3f007ab2cd4bdf 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -111,10 +111,6 @@ id: 'aid'
 #   ┌─────────────────────┐
 #───┘ Other configuration └─────────────────────────────────────
 
-# If enabled:
-#  The first account created is automatically marked as Admin.
-autoAdmin: true
-
 # Whether disable HSTS
 #disableHsts: true
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1a35dc5481a69b1b8b26120a977fc54351d05db6..3b6e2c59a615e0f7a774e4503faec378e9b9732c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,56 @@
 ChangeLog
 =========
 
+12.0.0 indigo (unreleased)
+--------------------
+### Breaking Chnages
+* お知らせがリセットされます。
+* 通知がリセットされます。
+* モデレーターがインスタンス設定を閲覧したり変更したりできなくなります(それらができるのはAdminのみになります)。
+	* モデレーターが出来るのは、ユーザーのサイレンス/凍結などに限られます。
+	* 従来と同じ権限を与えたい場合、モデレーターをAdminに設定することを検討してください(Adminは複数人設定可能です)。
+* notes/search APIのページングがoffsetではなくuntilId方式に
+* クライアントのテーマのフォーマットが調整されました。
+	* 旧テーマを変換してインポートする機能が予定されています
+* ノートに位置情報を添付できる機能を廃止
+* ノートに何のアプリから投稿したかという情報を含めるのを廃止
+
+### ✨Improvements
+* Webクライアントを一新
+	* Syuilo Design System (仮称)を採用し、各コンポーネントが統一され一貫したデザインに
+	* レスポンシブデザインになり、デスクトップ/タブレット/スマートフォンで同じ機能が使えるように
+	* 複数アカウントに対応し、簡単に別のアカウントに切り替えられるように
+	* 通知から直接フォローリクエストを許可/拒否できるように
+	* ユーザーの登録日を表示するように
+	* タイムラインウィジェットを追加
+	* ユーザーを選択する操作が便利に
+	* ユーザーページからユーザーにメッセージを送れるように
+	* ユーザーページからユーザーとトークを開始できるように
+	* 「戻る」ボタンを追加し、PWAフレンドリーに
+	* 軽量化
+* お知らせ機能の強化
+	* お知らせが未読か既読か管理されるようになり、未読のお知らせがあると分かりやすく表示されるように
+	* 何人がお知らせを読んだか分かるように
+* アンテナ機能
+	* 指定した条件(キーワード、ファイル添付の有無など)にマッチする投稿のタイムラインを見れる機能
+	* 新しい投稿があったとき通知するようにもできる
+	* ウィジェットとしても表示可能
+* Elasticsearchをインストールしなくても全文検索できるように
+* リモートのカスタム絵文字をコピーしてくる機能を追加
+* 自分の送ったフォローリクエストが承認されたときの通知を追加
+* 他多数
+
+### 🐛Fixes
+* ミュートしている人からのリアクション通知があると、通知があると表示される問題を修正
+* 投稿メニューを開いて操作した後にもう一度メニューを開こうとしてもできない問題を修正
+* リモートのノートのURLが書かれていた場合、動作がおかしい問題を修正
+* リストTLだとTでのTLフォーカスが効かない問題を修正
+* OAuth認証画面の配色がおかしい問題を修正
+* 設定画面で、アバターを更新してもアバターの画像がその場で更新されない問題を修正
+* 投稿詳細/ユーザー詳細 画面でadminや公式アカウントマークが表示されない問題を修正
+* APIのリクエスト方法(websocket/HTTP)によって返ってくるエラーの内容に違いがある問題を修正
+* Pages: VERSION 変数が常に null な問題を修正
+
 11.37.1 (2020/01/07)
 --------------------
 ### 🐛Fixes
diff --git a/gulpfile.ts b/gulpfile.ts
index 2ba30aace19947f6ec5dc972023979130c300a83..274f05a5a84d9e0e943df5336f6ee8e2d5b1884a 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -2,38 +2,25 @@
  * Gulp tasks
  */
 
+import * as fs from 'fs';
 import * as gulp from 'gulp';
 import * as ts from 'gulp-typescript';
-const sourcemaps = require('gulp-sourcemaps');
-import tslint from 'gulp-tslint';
-const stylus = require('gulp-stylus');
 import * as rimraf from 'rimraf';
-import * as chalk from 'chalk';
 import * as rename from 'gulp-rename';
-import * as mocha from 'gulp-mocha';
-import * as replace from 'gulp-replace';
 const cleanCSS = require('gulp-clean-css');
-const terser = require('gulp-terser');
+const sass = require('gulp-dart-sass');
+const fiber = require('fibers');
 
 const locales = require('./locales');
-
-const env = process.env.NODE_ENV || 'development';
-const isDebug = env !== 'production';
-
-if (isDebug) {
-	console.warn(chalk.yellow.bold('WARNING! NODE_ENV is not "production".'));
-	console.warn(chalk.yellow.bold('         built script will not be compressed.'));
-}
+const meta = require('./package.json');
 
 gulp.task('build:ts', () => {
 	const tsProject = ts.createProject('./tsconfig.json');
 
 	return tsProject
 		.src()
-		.pipe(sourcemaps.init())
 		.pipe(tsProject())
 		.on('error', () => {})
-		.pipe(sourcemaps.write('.', { includeContent: false, sourceRoot: '../built' }))
 		.pipe(gulp.dest('./built/'));
 });
 
@@ -41,47 +28,25 @@ gulp.task('build:copy:views', () =>
 	gulp.src('./src/server/web/views/**/*').pipe(gulp.dest('./built/server/web/views'))
 );
 
-gulp.task('build:copy:fonts', () =>
-	gulp.src('./node_modules/three/examples/fonts/**/*').pipe(gulp.dest('./built/client/assets/fonts/'))
-);
+gulp.task('build:copy:locales', cb => {
+	fs.mkdirSync('./built/client/assets/locales', { recursive: true });
 
-gulp.task('build:copy', gulp.parallel('build:copy:views', 'build:copy:fonts', () =>
+	for (const [lang, locale] of Object.entries(locales)) {
+		fs.writeFileSync(`./built/client/assets/locales/${lang}.${meta.version}.json`, JSON.stringify(locale), 'utf-8');
+	}
+
+	cb();
+});
+
+gulp.task('build:copy', gulp.parallel('build:copy:views', 'build:copy:locales', () =>
 	gulp.src([
-		'./src/const.json',
 		'./src/emojilist.json',
 		'./src/server/web/views/**/*',
 		'./src/**/assets/**/*',
-		'!./src/client/app/**/assets/**/*'
+		'!./src/client/assets/**/*'
 	]).pipe(gulp.dest('./built/'))
 ));
 
-gulp.task('lint', () =>
-	gulp.src('./src/**/*.ts')
-		.pipe(tslint({
-			formatter: 'verbose'
-		}))
-		.pipe(tslint.report())
-);
-
-gulp.task('format', () =>
-	gulp.src('./src/**/*.ts')
-		.pipe(tslint({
-			formatter: 'verbose',
-			fix: true
-		}))
-		.pipe(tslint.report())
-);
-
-gulp.task('mocha', () =>
-	gulp.src('./test/**/*.ts')
-		.pipe(mocha({
-			exit: true,
-			require: 'ts-node/register'
-		} as any))
-);
-
-gulp.task('test', gulp.task('mocha'));
-
 gulp.task('clean', cb =>
 	rimraf('./built', cb)
 );
@@ -90,20 +55,9 @@ gulp.task('cleanall', gulp.parallel('clean', cb =>
 	rimraf('./node_modules', cb)
 ));
 
-gulp.task('build:client:script', () => {
-	const client = require('./built/meta.json');
-	return gulp.src(['./src/client/app/boot.js', './src/client/app/safe.js'])
-		.pipe(replace('VERSION', JSON.stringify(client.version)))
-		.pipe(replace('ENV', JSON.stringify(env)))
-		.pipe(replace('LANGS', JSON.stringify(Object.keys(locales))))
-		.pipe(terser({
-			toplevel: true
-		}))
-		.pipe(gulp.dest('./built/client/assets/'));
-});
-
 gulp.task('build:client:styles', () =>
-	gulp.src('./src/client/app/init.css')
+	gulp.src('./src/client/style.scss')
+		.pipe(sass({ fiber }))
 		.pipe(cleanCSS())
 		.pipe(gulp.dest('./built/client/assets/'))
 );
@@ -112,7 +66,6 @@ gulp.task('copy:client', () =>
 		gulp.src([
 			'./assets/**/*',
 			'./src/client/assets/**/*',
-			'./src/client/app/*/assets/**/*'
 		])
 			.pipe(rename(path => {
 				path.dirname = path.dirname!.replace('assets', '.');
@@ -120,15 +73,7 @@ gulp.task('copy:client', () =>
 			.pipe(gulp.dest('./built/client/assets/'))
 );
 
-gulp.task('doc', () =>
-	gulp.src('./src/docs/**/*.styl')
-		.pipe(stylus())
-		.pipe(cleanCSS())
-		.pipe(gulp.dest('./built/docs/assets/'))
-);
-
 gulp.task('build:client', gulp.parallel(
-	'build:client:script',
 	'build:client:styles',
 	'copy:client'
 ));
@@ -137,7 +82,6 @@ gulp.task('build', gulp.parallel(
 	'build:ts',
 	'build:copy',
 	'build:client',
-	'doc'
 ));
 
 gulp.task('default', gulp.task('build'));
diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
deleted file mode 100644
index ed97d539c095cf1413af30cc23dea272095b97dd..0000000000000000000000000000000000000000
--- a/locales/ca-ES.yml
+++ /dev/null
@@ -1 +0,0 @@
----
diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml
deleted file mode 100644
index 96135478c11d0ff9a173cddc606d9db11ed11fa2..0000000000000000000000000000000000000000
--- a/locales/cs-CZ.yml
+++ /dev/null
@@ -1,1481 +0,0 @@
----
-meta:
-  lang: "Čeština"
-common:
-  misskey: "⭐ ve fedivesmíru"
-  about-title: "⭐ ve fedivesmíru."
-  about: "Děkujeme, že jste našli Misskey. Misskey je <b>decentralizovaná mikroblogovací platforma</b> zrozená na Zemi. Neboť existuje ve fedivesmíru (vesmíru, kde jsou organizovány různé sociální sítě), je vzájemně propojena s jinými sociálními sítěmi. Co takhle si chvilku odpočinout od ruchu a shonu města a ponořit se do nového internetu?"
-  intro:
-    title: "Co je Misskey?"
-    about: "Misskey je open-source <b>decentralizovaný mikroblogovací software</b>. Má sofistikované, zcela přizpůsobitelné uživatelské rozhraní, různé způsoby reagování na příspěvky, bezplatné úložiště souborů nabízející integrovaný management system, a další pokročilé vlastnosti. Misskey je navíc připojeno k systému sítí zvanému „fedivesmír“ nebo „fediverse“, který nám dovoluje komunikovat s uživateli na jiných sociálních sítí. Pokud například něco napíšete, nebude to posláno pouze uživatelů Misskey, ale také lidem na sítích Mastodon a Pleroma. Jen si představte, že planeta posílá jiné planetě rádiový signál, aby s ní komunikovala."
-    features: "Vlastnosti"
-    rich-contents: "Příspěvky"
-    rich-contents-desc: "Pouze napište svoje nápady, žhavá témata a cokoliv, co chcete sdílet. Můžete ozdobit svá slova, připojit vaše oblíbené obrázky, posílat soubory včetně videí či vytvořit hlasování – to je jen několik věcí, co můžete dělat s Misskey!"
-    reaction: "Reakce"
-    reaction-desc: "Nejsnadnější způsob, jak vyjádřit své emoce. Misskey vám dovoluje přidávat k příspěvkům ostatních lidí různé reakce. Emoční zážitek na Misskey nebude nikdy na jiných sociálních sítích, které mohou dávat pouze „lajky“."
-    ui: "Rozhraní"
-    ui-desc: "Jediné rozhraní není pro všechny. Misskey má proto vysoce přizpůsobitelné rozhraní pro váš vkus. Můžete si vytvořit svou vlastní originální domovskou stránku upravením vzhledu vaší časové osy a posunováním widgetů, které můžete snadno upravit, a učinit toto místo svým vlastním."
-    drive: "Disk"
-    drive-desc: "Chcete sdílet obrázek, který jste již nahráli? Chcete vaše nahrané soubory organizovat, pojmenovávat a vytvářet pro ně složky? Misskey Disk je pro vás nejlepší řešení. Je velmi snadné sdílet vaše soubory online."
-    outro: "Podívejte se na unikátní vlastnosti Misskey vlastníma očima! Pokud si myslíte, že tato instance není pro vás, zkuste jiné instance, neboť Misskey je decentralizovaná sociální síť, takže můžete snadno najít své přátele. Hodně štěstí a zábavy!"
-  application-authorization: "Autorizované aplikace"
-  close: "Zavřít"
-  do-not-copy-paste: "Prosím nezadávejte ani nevkládejte sem kód. Váš účet může být kompromitován."
-  load-more: "Načíst více"
-  enter-password: "Prosím zadejte heslo"
-  2fa: "Dvoufaktorová autentikace"
-  customize-home: "Přizpůsobit vzhled domovské stránky"
-  featured-notes: "Oblíbené poznámky"
-  dark-mode: "Tmavý režim"
-  signin: "Přihlásit"
-  signup: "Registrovat"
-  signout: "Odhlásit"
-  reload-to-apply-the-setting: "Pro uplatnění tohoto nastavení musíte znovu načíst tuto stránku. Chcete ji načíst teď?"
-  fetching-as-ap-object: "Načítám data z Fediversu..."
-  delete-confirm: "Opravdu chcete smazat tento příspěvek?"
-  signin-required: "Přihlašte se, prosím"
-  notification-type: "Typy oznámení"
-  notification-types:
-    all: "VÅ¡echny"
-    pollVote: "Hlasy"
-    follow: "Sledovaní"
-    receiveFollowRequest: "Žádost o sledování"
-    reply: "Odpovědi"
-    quote: "Citace"
-    renote: "Renotovat"
-    mention: "Zmínky"
-    reaction: "Reakce"
-  got-it: "Rozumím!"
-  customization-tips:
-    title: "Tipy pro přizpůsobení"
-    paragraph: "<p>Přizpůsobování domovské stránky vám dovoluje přidávat/odstraňovat, přetahovat a a uspořádat widgety.</p><p>Můžete změnit zobrazení <strong><strong>pravým</strong> kliknutím</strong> na některé widgety.</p><p>Můžete widgety smazat jejich přetažením do <strong>prostoru označeného jako „Koš“</strong> v záhlaví stránky.</p><p>Přizpůsobování dokončíte kliknutím na tlačítko „Hotovo“ v pravéh horním rohu.</p>"
-    gotit: "Rozumím!"
-  notification:
-    file-uploaded: "Soubor nahrán!"
-    message-from: "Zpráva od uživatele {}:"
-    reversi-invited: "Pozván/a ke hře"
-    reversi-invited-by: "Pozván/a uživatelem {}:"
-    notified-by: "Oznámení od uživatele {}:"
-    reply-from: "Odpověď uživatele {}:"
-    quoted-by: "Citováno uživatelem {}:"
-  time:
-    unknown: "neznámý čas"
-    future: "budoucí"
-    just_now: "teď"
-    seconds_ago: "před {} s"
-    minutes_ago: "před {} min"
-    hours_ago: "před {} h"
-    days_ago: "před {} d"
-    weeks_ago: "před {} týd"
-    months_ago: "před {} měs"
-    years_ago: "před {} lety"
-  month-and-day: "{day}. {month}."
-  trash: "Koš"
-  drive: "Disk"
-  pages: "Stránky"
-  messaging: "Konverzace"
-  home: "Domů"
-  deck: "Deck"
-  timeline: "Časová osa"
-  explore: "Objevovat"
-  following: "Sledovaní"
-  followers: "Sledující"
-  favorites: "Oblíbené"
-  permissions:
-    "read:account": "Zobrazit informace o účtu"
-    "write:account": "Narábět s účtem"
-    "read:blocks": "Prohlížet blokování"
-    "write:blocks": "Narábět s blokováním"
-    "read:drive": "Prohlížet Disk"
-    "write:drive": "Pracovat s Diskem"
-    "read:favorites": "Prohlížet oblíbené"
-    "write:favorites": "Narábět s oblíbeními"
-    "read:following": "Prohlížet následování"
-    "write:following": "Pracovat s následováním"
-    "read:messaging": "Prohlížet konverzaci"
-    "write:messaging": "Pracovat s konverzaci"
-    "read:mutes": "Prohlížet ztlumené"
-    "write:mutes": "Narábět s utíšeními"
-    "write:notes": "Narábět s poznámkami"
-    "read:notifications": "Prohlížet oznámení"
-    "write:notifications": "Pracovat s oznámeními"
-    "read:reactions": "Prohlížet reakce"
-    "write:reactions": "Narabět s reakcemi"
-    "write:votes": "Hlasovat"
-    "read:pages": "Zhlédnutí stránky"
-    "write:pages": "Upravit stránky"
-    "write:user-groups": "Upravit uživatelskou skupinu"
-  empty-timeline-info:
-    follow-users-to-make-your-timeline: "Poznámky sledujících se zobrazí ve vaší časové ose"
-    explore: "Najít uživatele"
-  post-form:
-    attach-location-information: "Přidat informace o lokaci"
-    hide-contents: "Schovat obsah"
-    reply-placeholder: "Odpovědět na tento příspěvek"
-    quote-placeholder: "Citovat tento příspěvek"
-    quote-attached: "Přiložit citaci"
-    submit: "Odeslat"
-    reply: "Odpovědět"
-    renote: "Renotovat"
-    posting: "Posílání"
-    attach-media-from-local: "Nahrát soubor z vašeho zařízení"
-    attach-media-from-drive: "Přiložit soubory z vašeho Drivu"
-    insert-a-kao: "v('ω')v"
-    create-poll: "Vytvořit anketu"
-    text-remain: "zbývá ještě {} znaků"
-    recent-tags: "Nejnovější"
-    local-only-message: "Publikovat zprávu pouze lokálně"
-    click-to-tagging: "Klikni pro otágování"
-    visibility: "Viditelnost"
-    geolocation-alert: "Vaše zařízení nedalo k dispozici lokaci"
-    error: "Chyba"
-    enter-username: "Zadejte své uživatelské jméno"
-    specified-recipient: "Pro"
-    add-visible-user: "Přidat uživatele"
-    username-prompt: "Zadejte své uživatelské jméno"
-    enter-file-name: "Upravit název souboru"
-  weekday-short:
-    sunday: "Ne"
-    monday: "Po"
-    tuesday: "Út"
-    wednesday: "St"
-    thursday: "ÄŒt"
-    friday: "Pá"
-    saturday: "So"
-  weekday:
-    sunday: "Neděle"
-    monday: "Pondělí"
-    tuesday: "Úterý"
-    wednesday: "Středa"
-    thursday: "ÄŒtvrtek"
-    friday: "Pátek"
-    saturday: "Sobota"
-  reactions:
-    like: "Lajk"
-    love: "Super"
-    laugh: "Smích"
-    hmm: "Hmm...?"
-    surprise: "Překvapení"
-    congrats: "Gratuluji!"
-    angry: "Naštvaný"
-    confused: "Zmatený"
-    rip: "RIP"
-    pudding: "Pudink"
-  note-visibility:
-    public: "Veřejná"
-    home: "Domovská"
-    home-desc: "Poslat pouze na domovskou časovou osu"
-    followers: "Pro sledující"
-    followers-desc: "Poslat pouze sledujícím"
-    specified: "Přímá"
-    specified-desc: "Poslat pouze zmíněným uživatelům"
-    local-public: "Veřejná (pouze místní)"
-    local-home: "Domovská (pouze místní)"
-    local-followers: "Pro sledující (pouze místní)"
-  note-placeholders:
-    a: "Co právě děláte?"
-    b: "Co se děje?"
-    c: "Co se vám honí hlavou?"
-    d: "Napíšete pár slov?"
-    e: "Pište sem"
-    f: "Čekám, až něco napíšete..."
-  settings: "Nastavení"
-  _settings:
-    profile: "Profil"
-    notification: "Oznámení"
-    apps: "Aplikace"
-    tags: "Hashtagy"
-    mute-and-block: "Ztlumit/blokovat"
-    blocking: "Blokování"
-    security: "Zabezpečení"
-    signin: "Historie přihlášení"
-    password: "Heslo"
-    other: "Ostatní"
-    appearance: "Vzhled"
-    behavior: "Chování"
-    reactions: "Reakce"
-    fetch-on-scroll: "Nekonečné načítaní posuvem"
-    fetch-on-scroll-desc: "Pokud budete rolovat dolů po stránce, automaticky bude načten další obsah."
-    note-visibility: "Viditelnost příspěvku"
-    default-note-visibility: "Výchozí viditelnost příspěvku"
-    remember-note-visibility: "Zapamatovat viditelnost příspěvků"
-    web-search-engine: "Webové vyhledávače"
-    web-search-engine-desc: "Například: https://www.google.com/?#q={{query}}"
-    paste: "Vložit"
-    pasted-file-name-desc: "Například: \"rrrr-MM-dd HH-mm-ss [{{number}}]\" → \"2018-03-20 21-30-24 1\""
-    paste-dialog: "Upravit název vloženého souboru"
-    keep-cw: "Zachovat varování o obsahu"
-    keep-cw-desc: "Při odpovědi na příspěvek bude varování o obsahu nastaveno stejně jako původní příspěvek."
-    i-like-sushi: "Mam radši sushi (než puding)"
-    show-reversi-board-labels: "Zobrazit označení řad a sloupců v Reversi"
-    use-avatar-reversi-stones: "Použít avatar jako figurku v Reversi"
-    disable-animated-mfm: "Vypnout pohyblivé texty v příspěvku"
-    disable-showing-animated-images: "Nepřehrávat animované obrázky"
-    suggest-recent-hashtags: "Navrhovat nedávné hashtagy v rámci psacího pole"
-    always-show-nsfw: "Vždycky ukázat NSFW obsah"
-    always-mark-nsfw: "Označovat všechny příspěvky za delikátní"
-    show-full-acct: "Zaradit hostovací server jako součast přezdívky"
-    show-via: "zobrazit přes"
-    reduce-motion: "Snížit pohyb v rozhraní"
-    this-setting-is-this-device-only: "Pouze pro toto zařízení"
-    use-os-default-emojis: "Použít výchozí emoji systému"
-    line-width: "Hrubka línie"
-    line-width-thin: "Úzka"
-    line-width-normal: "Běžná"
-    line-width-thick: "Tlustá"
-    font-size: "Velikost písma"
-    font-size-x-small: "Malé"
-    font-size-small: "Dost malé"
-    font-size-medium: "Průměrné"
-    font-size-large: "Dost velké"
-    font-size-x-large: "Velké"
-    deck-column-align: "Zarovnání sloupců v Decku"
-    deck-column-align-center: "Na střed"
-    deck-column-align-left: "Vlevo"
-    deck-column-align-flexible: "Flexibilní"
-    deck-column-width: "Šířka sloupců v Decku"
-    deck-column-width-narrow: "Úzké"
-    deck-column-width-narrower: "Poněkud úzké"
-    deck-column-width-normal: "Normální"
-    deck-column-width-wider: "Poněkud široké"
-    deck-column-width-wide: "Široké"
-    use-shadow: "Používat v rozhraní stíny"
-    rounded-corners: "Zakulatit rohy v rozhraní"
-    circle-icons: "Používat kulaté avatary"
-    contrasted-acct: "Přidat uživatelskému účtu kontrast"
-    wallpaper: "Obrázek na pozadí"
-    choose-wallpaper: "Zvolit pozadí"
-    delete-wallpaper: "Odstranit pozadí"
-    post-form-on-timeline: "Zobrazit formulář pro nové příspěvky nad časovou osou"
-    show-clock-on-header: "Zobrazit hodiny v pravém horním rohu"
-    show-reply-target: "Zobrazit cíl odpovědi"
-    timeline: "Časová osa"
-    show-my-renotes: "Zobrazit moje renoty v časové ose"
-    show-renoted-my-notes: "Zobrazit renoty vašich vlastních příspěvků v časové ose"
-    show-local-renotes: "Zobrazit renoty místních příspěvků v časové ose"
-    remain-deleted-note: "I nadále zobrazovat odstraněné příspěvky"
-    sound: "Zvuk"
-    enable-sounds: "Povolit zvuk"
-    enable-sounds-desc: "Přehrát zvuk, například při odeslání nebo přijetí příspěvku, či zprávy. Toto nastavení je uloženo v prohlížeči."
-    volume: "Hlasitost"
-    test: "Test"
-    update: "Aktualizace Misskey"
-    version: "Verze:"
-    latest-version: "Nejnovější verze:"
-    update-checking: "Kontroluji aktualizace"
-    do-update: "Zkontrolovat aktualizace"
-    update-settings: "Pokročilá nastavení"
-    no-updates: "Nejsou dostupné žádné aktualizace"
-    no-updates-desc: "Váš server Misskey je aktuální."
-    update-available: "Je dostupná nová verze"
-    update-available-desc: "Aktualizace budou aplikovány po znovunačtení stránky."
-    advanced-settings: "Pokročilá nastavení"
-    debug-mode: "Povolit režim ladění"
-    debug-mode-desc: "Toto nastavení je uloženo v prohlížeči."
-    navbar-position: "Poloha navigačního panelu"
-    navbar-position-top: "Nahoře"
-    navbar-position-left: "Vlevo"
-    navbar-position-right: "Vpravo"
-    i-am-under-limited-internet: "Mam omezený (pomalý) internet"
-    post-style: "Styl zobrazení poznámek"
-    post-style-standard: "Standardní"
-    post-style-smart: "Chytrý"
-    notification-position: "Poloha oznámení"
-    notification-position-bottom: "Dole"
-    notification-position-top: "Nahoře"
-    disable-via-mobile: "Neoznačovat příspěvky jako „z mobilu“"
-    load-raw-images: "Zobrazovat obrázky v původní kvalitě"
-    load-remote-media: "Zobrazovat média ze vzdáleného serveru"
-    sync: "Synchronizace"
-    save: "Uložit"
-    saved: "Uloženo"
-    preview: "Náhled"
-    room: "Místnost"
-    _room:
-      graphicsQuality: "Kvalita grafiky"
-      _graphicsQuality:
-        ultra: "Nejvyšší"
-        high: "Vysoká"
-        medium: "Střední"
-        low: "Nízká"
-        cheep: "Nejnižší"
-  search: "Hledání"
-  delete: "Odstranit"
-  loading: "Načítám..."
-  ok: "OK"
-  cancel: "Zrušit"
-  update-available-title: "Aktualizace k dispozici"
-  update-available: "Je k dispozici nová verze Misskey ({newer},vaše verze je {current}). Pro aplikování nové verze znovunačtěte stránku."
-  my-token-regenerated: "Váš token byl regenerován, proto budete odhlášen/a."
-  hide-password: "Skrýt heslo"
-  show-password: "Zobrazit heslo"
-  enter-username: "Zadejte své uživatelské jméno"
-  do-not-use-in-production: "Tohle je vývojářský build. Nepoužívejte v produkci."
-  user-suspended: "Tomuto uživateli byl pozastaven účet."
-  is-remote-user: "Informace o tomto uživateli nemusí být kompletní."
-  is-remote-post: "Obsah tohoto příspěvku je zrcadlen."
-  view-on-remote: "Pro kompletnost jej zobrazte vzdáleně."
-  renoted-by: "{user} renotoval/a"
-  no-notes: "Bez poznámek"
-  turn-on-darkmode: "Přepnout na tmavý režim"
-  turn-off-darkmode: "Světlý režim"
-  error:
-    title: "Něco se stalo :("
-    retry: "Zkusit znovu"
-  reversi:
-    drawn: "Remíza"
-    my-turn: "Váš tah"
-    opponent-turn: "Je řada na protivníkovi"
-    turn-of: "{name} je na tahu"
-    past-turn-of: "{name} byl/a na tahu"
-    won: "{name} vyhrál/a"
-    black: "Černá"
-    white: "Bílá"
-    total: "Celkem"
-    this-turn: "{count}. kolo"
-  widgets:
-    analog-clock: "Analogové hodiny"
-    profile: "Profil"
-    calendar: "Kalendář"
-    timemachine: "Kalendář (Stroj času)"
-    activity: "Aktivita"
-    rss: "RSS čtečka"
-    memo: "Rychlé poznámky"
-    trends: "Trendy"
-    photo-stream: "Proud fotek"
-    posts-monitor: "Grafy příspěvků"
-    slideshow: "Prezentace"
-    version: "Verze"
-    broadcast: "Rozhlas"
-    notifications: "Oznámení"
-    users: "Doporučení uživatelé"
-    polls: "Ankety"
-    post-form: "Formulář pro psaní"
-    server: "Informace o serveru"
-    nav: "Navigace"
-    tips: "Tipy"
-    hashtags: "Hashtagy"
-    queue: "Ve frontÄ›"
-  dev: "Nepodařilo se vytvořit aplikace. Prosím zkuste to znovu."
-  ai-chan-kawaii: "Ai-chan kawaii!"
-  you: "Vy"
-auth/views/form.vue:
-  share-access: "Chcete dovolit aplikaci <i>{name}</i> přístup k vašemu účtu?"
-  permission-ask: "Tato aplikace vyžaduje následující oprávnění:"
-  cancel: "Zrušit"
-  accept: "Povolit přístup"
-auth/views/index.vue:
-  loading: "Načítám..."
-  denied-paragraph: "Tato aplikace nebude mít přístup k Vašemu účtu."
-  already-authorized: "Tato aplikace byla již autorizována."
-  callback-url: "Zpátky do aplikace."
-  please-go-back: "Prosím vraťte se zpátky do aplikace."
-  error: "Taková relace neexistuje."
-  sign-in: "Prosím přihlaste se."
-common/views/pages/explore.vue:
-  popular-users: "Populární uživatelé"
-  recently-updated-users: "Nedávno aktívni uživatelé"
-  recently-registered-users: "Nedávno registrovaní uživatelé"
-  popular-tags: "Populární tagy"
-  federated: "Z fedivesmíru"
-  explore: "Prozkoumat {host}"
-  users-info: "Aktuálně je zde registrováno {users} uživatelů"
-common/views/components/url-preview.vue:
-  enable-player: "Otevřít v přehrávači"
-  disable-player: "Zavřít přehrávač"
-common/views/components/user-list.vue:
-  no-users: "Žádní uživatelé"
-common/views/components/games/reversi/reversi.vue:
-  matching:
-    waiting-for: "Čeká se na {}"
-    cancel: "Zrušit"
-common/views/components/games/reversi/reversi.game.vue:
-  surrender: "Vzdát se"
-  surrendered: "Vzdaním se"
-  looped-map: "Zacyklená mapa"
-  can-put-everywhere: "Lze položit kamkoliv"
-common/views/components/games/reversi/reversi.index.vue:
-  title: "Misskey Reversi"
-  sub-title: "Hrajte Reversi s Vašimi kamarády!"
-  invite: "Pozvat"
-  rule: "Jak hrát"
-  mode-invite: "Pozvat"
-  invitations: "Jste pozvaní ke hře!"
-  my-games: "Moje hra"
-  all-games: "VÅ¡echny hry"
-  enter-username: "Zadejte své uživatelské jméno"
-  game-state:
-    ended: "Ukončené"
-    playing: "Probíhají"
-common/views/components/games/reversi/reversi.room.vue:
-  settings-of-the-game: "Nastavení hry"
-  choose-map: "Vybrat mapu"
-  random: "Náhodně"
-  black-or-white: "Černé/bílé"
-  black-is: "Černá je {}"
-  rules: "Pravidla"
-  looped-map: "Zacyklená mapa"
-  settings-of-the-bot: "Nastavení Botu"
-  this-game-is-started-soon: "Hra začne za pár vteřin"
-  waiting-for-other: "Čeká se na protivníka"
-  waiting-for-me: "Čeká se na Vás"
-  waiting-for-both: "Připravuji"
-  cancel: "Zrušit"
-  ready: "Připraveno"
-  cancel-ready: "Pokračovat v přípravě"
-common/views/components/connect-failed.vue:
-  title: "Nelze se připojit k serveru"
-  description: "Nastal problém s Vaším připojením k internetu, nebo server není dostupný nebo zrovna probíhá údržba. Prosím {zkuste to znova} za pár minut."
-  thanks: "Děkujeme že jste použili Misskey."
-  troubleshoot: "Odstranění problémů"
-common/views/components/connect-failed.troubleshooter.vue:
-  title: "Poradce při potížích"
-  network: "Síťové připojení"
-  checking-network: "Prověřit síťové připojení"
-  internet: "Připojení k internetu"
-  checking-internet: "Ověřuji připojení k internetu."
-  server: "Připojení k serveru"
-  checking-server: "Spojuji se se serverem"
-  finding: "Vyšetřování problému"
-  no-network: "Žádné připojení k síti"
-  no-network-desc: "Ujistěte se že jste připojeni k Internetu."
-  no-internet: "Nejste připojeni k internetu"
-  no-internet-desc: "Jste připojen k síti, ale zdá se že stále chybí připojení k Internetu. Prosím zkontrolujte Vaše připojení k Internetu."
-  no-server: "Nelze se připojit k serveru Misskey"
-  success: "Úspěšně se podařilo spojit s Misskey serverem"
-  flush: "Vyčistit mezipaměť"
-  set-version: "Vyberte verzi"
-common/views/components/media-banner.vue:
-  sensitive: "Choulostivý obsah"
-  click-to-show: "Klikněte pro zobrazení"
-common/views/components/theme.vue:
-  theme: "Vzhled"
-  light-theme: "Motiv pro použití ve světlém vzhledu"
-  dark-theme: "Motiv pro použití v tmavém vzhledu"
-  light-themes: "Světlý vzhled"
-  dark-themes: "Tmavý vzhled"
-  install-a-theme: "Nainstalovat motiv"
-  theme-code: "Kód motivu"
-  install: "Nainstalovat"
-  installed: "\"{}\" byl nainstalován"
-  create-a-theme: "Vytvořit motiv"
-  save-created-theme: "Uložit motiv"
-  primary-color: "Základní barva"
-  text-color: "Barva textu"
-  base-theme: "Základní vzhled"
-  base-theme-light: "Světlý"
-  base-theme-dark: "Tmavý"
-  find-more-theme: "Najít další vzhledy"
-  theme-name: "Jméno vzhledu"
-  preview-created-theme: "Náhled"
-  invalid-theme: "Vzhled není validní"
-  already-installed: "Tento vzhled je již nainstalován."
-  saved: "Uloženo"
-  manage-themes: "Správa vzhledů"
-  builtin-themes: "Standardní vzhledy"
-  my-themes: "Moje vzhledy"
-  installed-themes: "Nainstalované vzhledy"
-  select-theme: "Zvolte vzhled"
-  uninstall: "Odinstalovat"
-  uninstalled: "\"{}\" byl odinstalován"
-  author: "Autor"
-  desc: "Popis"
-  export: "Exportovat"
-  import: "Importovat"
-  import-by-code: "nebo zkopírujte kód"
-  theme-name-required: "Jméno vzhledu je povinné"
-common/views/components/cw-button.vue:
-  hide: "Skrýt"
-  show: "Více"
-  chars: "{count} znaků"
-  files: "{count} souborů"
-  poll: "Anketa"
-common/views/components/messaging.vue:
-  search-user: "Najít uživatele"
-  you: "Vy"
-  no-history: "Žádná historie"
-  user: "Uživatel"
-  group: "Skupina"
-  start-with-user: "Zahájit konverzaci s uživatelem"
-  start-with-group: "Zahájit skupinovou konverzaci"
-  select-group: "Vybrat skupinu"
-common/views/components/messaging-room.vue:
-  new-message: "Máte novou zprávu"
-common/views/components/messaging-room.form.vue:
-  input-message-here: "Sem zadejte zprávu"
-  send: "Odeslat"
-  attach-from-local: "Přiložit soubory z Vašeho zařízení"
-common/views/components/messaging-room.message.vue:
-  is-read: "Přečtené"
-  deleted: "Tato zpráva byla odstraněna"
-common/views/components/nav.vue:
-  about: "O Misskey"
-  stats: "Statistiky"
-  status: "Status"
-  wiki: "Wiki"
-  donors: "Dárci"
-  repository: "Úložiště"
-  develop: "Vývojáři"
-  feedback: "Zpětná vazba"
-  tos: "Podmínky užívání"
-common/views/components/note-menu.vue:
-  mention: "Zmínění"
-  detail: "Více"
-  copy-content: "Zkopírovat obsah"
-  copy-link: "Zkopírovat odkaz"
-  favorite: "Přidat do oblíbených"
-  unfavorite: "Odebrat z oblízených"
-  watch: "Sledovat"
-  unwatch: "Přestat sledovat"
-  pin: "Připnout"
-  unpin: "Odepnout"
-  delete: "Odstranit"
-  delete-confirm: "Opravdu chcete smazat tento příspěvek?"
-  delete-and-edit: "Smazat a upravit"
-  remote: "Ukázat originální poznámku"
-common/views/components/user-menu.vue:
-  mention: "Zmínění"
-  mute: "Umlčet"
-  unmute: "Zrušit umlčení"
-  block: "Blokován"
-  unblock: "Odblokovat"
-  push-to-list: "Přidat do seznamu"
-  select-list: "Vyberte seznam"
-  report-abuse: "Nahlásit spam"
-  report-abuse-reported: "Problém byl nahlášen administrátorovi. Děkujeme za Vaší kooperaci."
-  silence: "Ztlumit"
-  suspend: "Zmrazit"
-common/views/components/poll.vue:
-  vote-count: "{} hlasů"
-  total-votes: "{} hlasů celkem"
-  vote: "Hlasovat"
-  show-result: "Podívat se na výsledky"
-  voted: "Už jste hlasovaly"
-  closed: "Ukončeno"
-  remaining-days: "zbývá {d} dnů, {h} hodin"
-  remaining-hours: "zbývá {h} hodin, a {m} minut"
-  remaining-minutes: "zbývá {m} minut, a {s} sekund"
-  remaining-seconds: "zbývá {s} sekund"
-common/views/components/poll-editor.vue:
-  no-only-one-choice: "Musíte vybrat alespoň dvě možnosti"
-  choice-n: "Volba {}"
-  remove: "Odstranit tuto možnost"
-  add: "+ Přidat možnost"
-  destroy: "Zahodit dotazník"
-  multiple: "Více odpovědí je povoleno"
-  expiration: "Termín"
-  infinite: "Nekonečne"
-  at: "Výběr data a času"
-  no-more: "Více už přidat nemůžete"
-  deadline-date: "Termín ukončení"
-  deadline-time: "Doba trvání"
-  interval: "Trvání"
-  unit: "Jednotka"
-  second: "Sekunda"
-  minute: "Minuta"
-  hour: "Hodina"
-  day: "Ne"
-common/views/components/reaction-picker.vue:
-  choose-reaction: "Vyberte svoji reakci"
-  input-reaction-placeholder: "nebo vložte Emoji"
-common/views/components/emoji-picker.vue:
-  custom-emoji: "Emoji"
-  people: "Lidé"
-  animals-and-nature: "Zvířata a příroda"
-  food-and-drink: "Jídlo a pití"
-  activity: "Aktivita"
-  travel-and-places: "Místa a cestování"
-  objects: "Objekty"
-  symbols: "Symboly"
-  flags: "Vlajky"
-common/views/components/settings/app-type.vue:
-  title: "Režim"
-  intro: "Můžete vybrat zdali chcete použít stolní, nebo mobilní vzhled."
-  choices:
-    auto: "Vybrat vzhled automaticky"
-    desktop: "Vždy použít stolní vzhled"
-    mobile: "Vždy použít mobilní vzhled"
-  info: "Pro aktivování změn musíte znovu načíst stránky."
-common/views/components/signin.vue:
-  username: "Přezdívka"
-  password: "Heslo"
-  token: "Token"
-  signing-in: "Přihlašování..."
-  or: "Nebo"
-  signin-with-twitter: "Přihlásit se pomocí účtu Twitter"
-  signin-with-github: "Přihlásit se pomocí účtu GitHub"
-  signin-with-discord: "Přihlásit se pomocí účtu Discord"
-  login-failed: "Nelze se přihlásit. Zkontrolujte prosím své uživatelské jméno a heslo."
-  enter-2fa-code: "Vložte Váš verifikační kód"
-common/views/components/signup.vue:
-  invitation-code: "Kód pozvánky"
-  invitation-info: "Pokud máte pozvánku, prosím kontaktujte <a href=\"{}\">administrátora</a>."
-  username: "Přezdívka"
-  checking: "Kontroluji..."
-  available: "Dostupná"
-  unavailable: "Obsazená"
-  error: "Chyba připojení"
-  invalid-format: "Písmena, čísla a _ jsou povolená."
-  too-short: "Nesmí být prázdné!"
-  too-long: "Do 20 znaků."
-  password: "Heslo"
-  password-placeholder: "Více jak 8 znaků je doporučováno."
-  weak-password: "Slabé heslo"
-  normal-password: "Průměrné heslo"
-  strong-password: "Silné heslo"
-  retype: "Zadejte znovu"
-  retype-placeholder: "Zadejte znovu pro kontrolu"
-  password-matched: "OK"
-  password-not-matched: "Neshodují se"
-  recaptcha: "Potvrzení"
-  agree-to: "Souhlasím s {0}."
-  tos: "Podmínky užívání"
-  create: "Vytvořit účet"
-  some-error: "Pokus o vytvoření účtu selhal. Prosím zkuste to znovu."
-common/views/components/special-message.vue:
-  new-year: "Šťastný nový rok!"
-  christmas: "Šťastné a veselé vánoce!"
-common/views/components/stream-indicator.vue:
-  connecting: "Připojování"
-  reconnecting: "Připojuji se znovu"
-  connected: "Připojení navázáno"
-common/views/components/notification-settings.vue:
-  title: "Oznámení"
-  mark-as-read-all-notifications: "Označit všechna oznámení za přečtená"
-  mark-as-read-all-unread-notes: "Označit všechny příspěvky za přečtené"
-  mark-as-read-all-talk-messages: "Označit všechny zprávy za přečtené"
-common/views/components/integration-settings.vue:
-  title: "Integrace"
-  connect: "Připojit"
-  disconnect: "Odpojit"
-  connected-to: "Jste připojen k tomuto GitHub účtu"
-common/views/components/github-setting.vue:
-  description: "Jakmile spojíte Váš GitHub účet s Vaším Misskey účtem, uvidíte informace o Vašem GitHub účtu na Vašem profilu a budete se moci přihlásit skrze GitHub."
-  connected-to: "Je připojen k tomuto GitHub účtu"
-  detail: "Více…"
-  reconnect: "Znovu připojit"
-  connect: "Připojit Váš GitHub účet"
-  disconnect: "Odpojit"
-common/views/components/discord-setting.vue:
-  description: "Jakmile spojíte Váš Discord účet s Vaším Misskey účtem, uvidíte informace o Vašem Discord účtu na Vašem profilu a budete se moci přihlásit skrze Discord."
-  connected-to: "Je připojen k tomuto Discord účtu"
-  detail: "Více…"
-  reconnect: "Znovu připojit"
-  connect: "Připojit Váš Discord účet"
-  disconnect: "Odpojit"
-common/views/components/uploader.vue:
-  waiting: "Čekáme"
-common/views/components/visibility-chooser.vue:
-  public: "Veřejné"
-  home: "Domů"
-  home-desc: "Poslat pouze na domovskou časovou osu"
-  specified: "Přímá"
-  specified-desc: "Poslat pouze zmíněným uživatelům"
-  local-public: "Veřejná (pouze místní)"
-  local-public-desc: "Nepublikovat na vzdálených serverech"
-  local-home: "Domovská (pouze místní)"
-  local-followers: "Pro sledující (pouze místní)"
-common/views/components/trends.vue:
-  count: "{} zmíněných uživatelů"
-  empty: "Žádný trend"
-common/views/components/language-settings.vue:
-  title: "Zobrazit jazyky"
-  pick-language: "Zvolte jazyk"
-  recommended: "Doporučené"
-  auto: "Automaticky"
-  specify-language: "Vyberte jazyk"
-  info: "Pro aktivování změn musíte znovu načíst stránky."
-common/views/components/profile-editor.vue:
-  title: "Profil"
-  name: "Jméno"
-  account: "Účet"
-  location: "Lokace"
-  description: "O mnÄ›"
-  you-can-include-hashtags: "V popisku o Vás můžete použít i hastagy."
-  language: "Jazyk"
-  birthday: "Datum narození"
-  avatar: "Avatar"
-  banner: "Baner"
-  is-cat: "Tento účet je kočka"
-  is-bot: "Tento účet je Bot"
-  advanced: "Ostatní"
-  privacy: "Osobní údaje"
-  save: "Uložit"
-  saved: "Profil byl úspěšně aktualizován"
-  uploading: "Nahrávám"
-  upload-failed: "Nahrávání selhalo"
-  unable-to-process: "Operace nemohla být dokončena."
-  email: "Nastavení e-mailů"
-  email-address: "Emailová adresa"
-  email-verified: "Váš e-mail byl ověřen"
-  email-not-verified: "Váš email není potvrzen. Prosím zkontrolujte si svou schránku."
-  export: "Exportovat"
-  import: "Importovat"
-  export-and-import: "Import / Export"
-  export-targets:
-    following-list: "Seznam sledujících"
-    mute-list: "Seznam ztlumených uživatelů"
-    blocking-list: "Seznam blokovaných uživatelů"
-    user-lists: "Seznamy"
-  enter-password: "Prosím, zadejte Vaše heslo"
-  danger-zone: "Nebezpečná zóna"
-  delete-account: "Smazat účet"
-  account-deleted: "Váš účet byl smazán. Může chvilku trvat než zmizí všechna data."
-  profile-metadata: "Metadata profilu"
-  metadata-label: "Popis"
-  metadata-content: "Obsah"
-common/views/components/user-list-editor.vue:
-  users: "Uživatel"
-  rename: "Přejmenovat seznam"
-  delete: "Smazat seznam"
-  remove-user: "Odebrat z tohoto seznamu"
-  delete-are-you-sure: "Smazat seznam \"$1\"?"
-  deleted: "Smazáno"
-  add-user: "Přidat uživatele"
-common/views/components/user-group-editor.vue:
-  users: "Členové"
-  rename: "Přejmenovat skupinu"
-  delete: "Odstranit skupinu"
-  transfer: "Přesunout skupinu"
-  transfer-are-you-sure: "Jste si jistí že chcete přidat @$2 do skupiny: $1?"
-  transferred: "Skupina přesunuta"
-  remove-user: "Odebrat uživatele z této skupiny"
-  delete-are-you-sure: "Jste si jistí že chcete smazat skupinu \"$1\"?"
-  deleted: "Smazáno"
-  invite: "Pozvat"
-  invited: "Pozvánka byla úspěšně odeslána"
-common/views/components/user-lists.vue:
-  user-lists: "Seznamy"
-  create-list: "Vytvořit seznam"
-  list-name: "Název seznamu"
-common/views/components/user-groups.vue:
-  user-groups: "Skupiny"
-  create-group: "Vytvořit skupinu"
-  group-name: "Název skupiny"
-  owned-groups: "Moje skupiny"
-  invites: "Pozvat"
-  accept-invite: "Přidat se"
-  reject-invite: "Odmítnout"
-common/views/widgets/broadcast.vue:
-  fetching: "Načítám"
-  no-broadcasts: "Žádná nová oznámení"
-  have-a-nice-day: "Přejeme Vám příjemný den!"
-  next: "Další"
-  prev: "Předchozí"
-common/views/widgets/calendar.vue:
-  year: "Rok {}"
-  month: "{},"
-  day: "{}"
-  today: "Dneska: "
-  this-month: "Měsíc:"
-  this-year: "Rok:"
-common/views/widgets/photo-stream.vue:
-  title: "Foto stream"
-  no-photos: "Žádné obrázky"
-common/views/widgets/posts-monitor.vue:
-  title: "Grafy příspěvků"
-  toggle: "Přepnout zobrazení"
-common/views/widgets/hashtags.vue:
-  title: "Hashtagy"
-common/views/widgets/server.vue:
-  title: "Informace o serveru"
-  toggle: "Přepnout zobrazení"
-common/views/widgets/memo.vue:
-  title: "Poznámky"
-  memo: "Pište sem!"
-  save: "Uložit"
-common/views/widgets/slideshow.vue:
-  no-image: "V této složce nebyly nalezeny žádné fotky."
-common/views/widgets/tips.vue:
-  tips-line23: "Ai-chan kawaii!"
-common/views/pages/not-found.vue:
-  page-not-found: "Stránka nenalezena"
-common/views/pages/follow.vue:
-  following: "Sledování"
-  follow: "Sledovat"
-common/views/pages/follow-requests.vue:
-  accept: "Přijmout"
-  reject: "Odmítnout"
-desktop:
-  banner: "Baner"
-  avatar-crop-title: "Vyberte část, která se zobrazí jako avatar"
-  avatar: "Avatar"
-  uploading-avatar: "Nahrál nový avatar"
-  avatar-updated: "Vaše avatar byl aktualizován"
-  unable-to-process: "Operace nemohla být dokončena."
-  invalid-filetype: "Tento formát souboru není podporován"
-desktop/views/components/activity.chart.vue:
-  total: "Černá ... Celkem"
-  notes: "Modrá ... Poznámky"
-  replies: "Červená ... Odpovědi"
-  renotes: "Zelená ... Renoty"
-desktop/views/components/activity.vue:
-  title: "Aktivita"
-  toggle: "Přepnout zobrazení"
-desktop/views/components/calendar.vue:
-  title: "{month}. {year}"
-  prev: "Předchozí měsíc"
-  next: "Následující měsíc"
-desktop/views/components/choose-file-from-drive-window.vue:
-  chosen-files: "{count} souborů vybráno"
-  upload: "Nahrajte soubory z vašeho zařízení"
-  cancel: "Zrušit"
-  ok: "OK"
-  choose-prompt: "Vybrat soubory"
-desktop/views/components/choose-folder-from-drive-window.vue:
-  cancel: "Zrušit"
-  ok: "OK"
-  choose-prompt: "Zvolte adresář"
-desktop/views/components/crop-window.vue:
-  cancel: "Zrušit"
-  ok: "OK"
-desktop/views/components/drive-window.vue:
-  used: "využito"
-desktop/views/components/drive.file.vue:
-  avatar: "Avatar"
-  banner: "Baner"
-  nsfw: "NSFW"
-  contextmenu:
-    rename: "Přejmenovat"
-    copy-url: "Kopírovat URL"
-    download: "Stáhnout"
-    else-files: "Ostatní"
-    set-as-avatar: "Nastavit jako avatar"
-    set-as-banner: "Nastavit jako baner"
-    open-in-app: "Otevřít v aplikaci"
-    add-app: "Přidat aplikaci"
-    rename-file: "Přejmenovat soubor"
-    input-new-file-name: "Zadejte nový název"
-    copied: "Kopírování dokončeno"
-    copied-url-to-clipboard: "URL zkopírována do schránky"
-desktop/views/components/drive.folder.vue:
-  unable-to-process: "Operace nemohla být dokončena."
-  unhandled-error: "Neznámá chyba"
-  contextmenu:
-    move-to-this-folder: "Přesunout do této složky"
-    show-in-new-window: "Otevřít v novém okně"
-    rename: "Přejmenovat"
-    rename-folder: "Přejmenovat složku"
-    input-new-folder-name: "Zadejte nové jméno"
-    else-folders: "Ostatní"
-desktop/views/components/drive.vue:
-  search: "Vyhledávání"
-  empty-drive-description: "Klikněte pravým tlačítkem myši pro otevření menu, nebo sem přetáhněte soubor pro nahrání."
-  empty-folder: "Tato složka je prázdná"
-  unable-to-process: "Operace nemohla být dokončena."
-  unhandled-error: "Neznámá chyba"
-  url-upload: "Nahrát z URL adresy"
-  url-of-file: "URL adresa souboru, který chcete nahrát"
-  may-take-time: "Může trvat nějakou dobu, dokud nebude dokončeno nahrávání."
-  create-folder: "Vytvořit složku"
-  folder-name: "Název složky"
-  contextmenu:
-    create-folder: "Vytvořit složku"
-    upload: "Nahrát soubor"
-    url-upload: "Nahrát z URL"
-desktop/views/components/media-video.vue:
-  click-to-show: "Klikněte pro zobrazení"
-desktop/views/components/followers.vue:
-  empty: "Vypadá to že Vás nikdo nesleduje."
-desktop/views/components/game-window.vue:
-  game: "Reversi"
-desktop/views/components/home.vue:
-  done: "Hotovo"
-  add-widget: "Přidat widget:"
-  add: "Přidat"
-desktop/views/input-dialog.vue:
-  cancel: "Zrušit"
-  ok: "OK"
-desktop/views/components/note-detail.vue:
-  private: "Tento příspěvek je soukromý"
-  deleted: "Tento příspěvek byl odstraněn"
-  location: "Lokace"
-  renote: "Renotovat"
-  add-reaction: "Přidat reakci"
-  undo-reaction: "Odebrat reakci"
-desktop/views/components/note.vue:
-  reply: "Odpovědět"
-  renote: "Renote"
-  add-reaction: "Přidat reakci"
-  undo-reaction: "Odebrat reakci"
-  detail: "Více"
-  private: "Tento příspěvek je soukromý"
-  deleted: "Tento příspěvek byl odstraněn"
-desktop/views/components/notes.vue:
-  error: "Načítání selhalo."
-  retry: "Opakovat"
-desktop/views/components/notifications.vue:
-  empty: "Žádné nové notifikace!"
-desktop/views/components/post-form.vue:
-  posted: "Odesláno!"
-  replied: "Odpověděno!"
-  reposted: "Renotováno!"
-  note-failed: "Nepodařilo se přidat příspěvek"
-  renote-failed: "Renotování neuspělo"
-desktop/views/components/post-form-window.vue:
-  note: "Nový příspěvek"
-  reply: "Odpovědět"
-desktop/views/components/progress-dialog.vue:
-  waiting: "Čekáme"
-desktop/views/components/renote-form.vue:
-  quote: "Citovat..."
-  cancel: "Zrušit"
-  renote: "Renotovat"
-  renote-home: "Renote (domů)"
-  reposting: "Renotuji..."
-  success: "Renotováno!"
-  failure: "Renotování neuspělo"
-desktop/views/components/renote-form-window.vue:
-  title: "Chcete tohle renotovat?"
-desktop/views/components/settings.2fa.vue:
-  detail: "Více…"
-  url: "https://www.google.cz/landing/2step/"
-  register: "Přidat zařízení"
-  already-registered: "Toto zařízení je již připojené"
-  unregister: "Odebrat"
-  enter-password: "Prosím zadejte heslo"
-  authenticator: "Nejprve musíte nainstalovat Google Authenticator na Vašem zařízení:"
-  howtoinstall: "Jak nainstalovat"
-  token: "Token"
-  scan: "Poté naskenujte QR kód:"
-  done: "Prosím vložte kód zobrazený na Vašem zařízení:"
-  submit: "Uložit"
-  success: "Nastavení uloženo!"
-  failed: "Nepodařilo se spárovat. Prosím zkontrolujte správnost bezpečnostního kódu."
-  totp-header: "Ověřovací aplikace"
-  security-key-header: "Bezpečnostní klíč"
-  last-used: "Naposledy použito:"
-  activate-key: "Klikněte pro aktivaci bezpečnostního klíče"
-  security-key-name: "Název klíče"
-  key-unregistered: "Bezpečnostní klíč byl odstraněn"
-common/views/components/media-image.vue:
-  sensitive: "NSFW"
-  click-to-show: "Klikněte pro zobrazení"
-common/views/components/api-settings.vue:
-  caution: "Nepoužívejte tento kód v žádné jiné aplikace nebo ho sdílejte s ostatními (jinak můžete ohrozit svojí bezpečnost)."
-  token: "Token:"
-  enter-password: "Prosím zadejte heslo"
-  console:
-    title: "API konzole"
-    endpoint: "Endpoint"
-    parameter: "Parametry"
-    send: "Odeslat"
-    sending: "Odesílám"
-    response: "Výsledek"
-desktop/views/components/settings.apps.vue:
-  no-apps: "Žádné připojené aplikace"
-common/views/components/drive-settings.vue:
-  max: "Velikost úložiště"
-  in-use: "využito"
-  stats: "Statistiky"
-common/views/components/mute-and-block.vue:
-  mute-and-block: "Umlčet/blokovat"
-  mute: "Umlčet"
-  block: "Blokován"
-  no-muted-users: "Žádný uživatel nebyl umlčen"
-  no-blocked-users: "Žádný uživatel není blokován"
-  save: "Uložit"
-common/views/components/password-settings.vue:
-  reset: "Změnit heslo"
-  enter-current-password: "Prosím, vložte své současné heslo"
-  enter-new-password: "Zadejte své nové heslo"
-  enter-new-password-again: "Znovu zadejte své nové heslo"
-  not-match: "Nová hesla se neshodují"
-  changed: "Heslo bylo úspěšně změněno"
-  failed: "Nepodařilo se změnit heslo"
-desktop/views/components/sub-note-content.vue:
-  private: "Tento příspěvek je soukromý"
-  deleted: "Tento příspěvek byl odstraněn"
-  poll: "Anketa"
-desktop/views/components/settings.tags.vue:
-  title: "Tagy"
-  add: "Přidat"
-  save: "Uložit"
-desktop/views/components/timeline.vue:
-  home: "Domů"
-  local: "Lokální"
-  global: "Globální"
-  mentions: "Zmínění"
-  list: "Seznamy"
-  hashtag: "Hashtag"
-  add-list: "Přidat do seznamu"
-  list-name: "Název seznamu"
-desktop/views/components/ui.header.vue:
-  welcome-back: "Vítejte zpátky,"
-  adjective: "Pán"
-desktop/views/components/ui.header.account.vue:
-  profile: "Váš profil"
-  lists: "Seznamy"
-  groups: "Skupiny"
-  admin: "Administrace"
-  room: "Místnost"
-desktop/views/components/ui.header.nav.vue:
-  game: "Hry"
-desktop/views/components/ui.header.notifications.vue:
-  title: "Oznámení"
-desktop/views/components/ui.header.post.vue:
-  post: "Nový příspěvek"
-desktop/views/components/ui.header.search.vue:
-  placeholder: "Vyhledávání"
-desktop/views/components/user-preview.vue:
-  notes: "Příspěvky"
-desktop/views/components/users-list.vue:
-  all: "VÅ¡echny"
-  iknow: "Znáte"
-  fetching: "Načítám…"
-desktop/views/components/window.vue:
-  close: "Zavřít"
-admin/views/index.vue:
-  instance: "Instance"
-  emoji: "Emoji"
-  moderators: "Moderátoři"
-  users: "Uživatelé"
-  federation: "Z fedivesmíru"
-  announcements: "Oznámení"
-  queue: "Fronta úloh"
-  logs: "Logy"
-  db: "Databáze"
-  back-to-misskey: "Zpět na Misskey"
-admin/views/db.vue:
-  tables: "Tabulky"
-  vacuum: "Vysavač"
-  vacuum-info: "Uklidí databázi. Neohrozí data a sníží využití disku. Tohle se děje automaticky a opakovaně."
-  vacuum-exclamation: "Vysavač může dočasně přetížit databázi a dočasně omezit akce uživatelů."
-admin/views/dashboard.vue:
-  dashboard: "Kontrolní panel"
-  accounts: "Účty"
-  notes: "Poznámky"
-  drive: "Disk"
-  instances: "Instance"
-  this-instance: "Tato instance"
-  federated: "Z fedivesmíru"
-admin/views/queue.vue:
-  title: "Ve frontÄ›"
-  remove-all-jobs: "Vyčistit frontu"
-  jobs: "Úkoly"
-  queue: "Ve frontÄ›"
-  domains:
-    inbox: "Obdržené"
-    db: "Databáze"
-  states:
-    active: "V procesu"
-    delayed: "Naplánováno"
-    waiting: "Ve frontÄ›"
-  result-is-truncated: "Zkrácený výsledek"
-admin/views/logs.vue:
-  logs: "Logy"
-  domain: "Doména"
-  level: "Úroveň"
-  levels:
-    all: "VÅ¡e"
-    info: "Informace"
-    success: "Podařilo se"
-    warning: "Varování"
-    error: "Chyba"
-    debug: "Debug"
-  delete-all: "Smazat vše"
-admin/views/abuse.vue:
-  target: "Cíl"
-  details: "Popis"
-  remove-report: "Odstranit"
-admin/views/instance.vue:
-  instance: "Instance"
-  instance-name: "Název instance"
-  instance-description: "Popis instance"
-  host: "Hostitel"
-  icon-url: "URL ikonky"
-  logo-url: "URL loga"
-  banner-url: "URL pro baner"
-  error-image-url: "URL pro chybový obrázek"
-  languages: "Jazyk této instance"
-  languages-desc: "Můžete nastavit více než jeden, oddělte mezerami."
-  tos-url: "URL pro smluvní podmínky"
-  repository-url: "URL adresa repositáře"
-  feedback-url: "URL pro zpětnou vazbu"
-  maintainer-config: "Informace o administrátorovi"
-  maintainer-name: "Jméno administrátora"
-  maintainer-email: "Kontakt na administrátora"
-  advanced-config: "Další nastavení"
-  object-storage-base-url: "URL"
-  object-storage-prefix: "Předpona"
-  object-storage-endpoint: "Endpoint"
-  object-storage-region: "Region"
-  object-storage-port: "Port"
-  object-storage-access-key: "Přístupový klíč"
-  object-storage-secret-key: "Tajný Klíč (Secret Key)"
-  object-storage-use-ssl: "Použít SSL"
-  object-storage-s3-info-here: "zde"
-  mb: "V megabajtech"
-  recaptcha-config: "nastavení služby reCAPTCHA"
-  recaptcha-info: "reCAPTCHA token je povinný. Můžete jej získat na https://www.google.com/recaptcha/intro/"
-  enable-recaptcha: "povolit reCAPTCHA"
-  recaptcha-secret-key: "Tajný Klíč (Secret Key)"
-  recaptcha-preview: "Náhled"
-  twitter-integration-config: "Nastavení spojení s Twitterem"
-  twitter-integration-info: "The callback URL is set on {url}."
-  enable-twitter-integration: "Povolit připojení k Twitteru"
-  twitter-integration-consumer-key: "Consumer key"
-  twitter-integration-consumer-secret: "Consumer Secret"
-  github-integration-config: "Nastavení spojení s GitHubem"
-  github-integration-info: "The callback URL is set on {url}."
-  enable-github-integration: "Povolit připojení ke GitHubu"
-  github-integration-client-id: "Client ID"
-  github-integration-client-secret: "Client Secret"
-  discord-integration-config: "Nastavení spojení s Discordem"
-  discord-integration-info: "The callback URL is set to {url}."
-  enable-discord-integration: "Povolit připojení ke Discordu"
-  discord-integration-client-id: "Client ID"
-  discord-integration-client-secret: "Client Secret"
-  invite: "Pozvat"
-  save: "Uložit"
-  saved: "Uloženo"
-  email: "Emailová adresa"
-  smtp-port: "SMTP Port"
-  smtp-auth: "Provést SMTP autentikaci"
-  smtp-user: "SMTP uživatel"
-  smtp-pass: "SMTP heslo"
-  test-email: "Test"
-  serviceworker-config: "ServiceWorker"
-  enable-serviceworker: "Povolit ServiceWorker"
-  vapid-publickey: "VAPID veřejný klíč"
-  vapid-privatekey: "VAPID osobní klíč"
-admin/views/charts.vue:
-  title: "Graf"
-  per-day: "za den"
-  per-hour: "za hodinu"
-  federation: "Federace"
-  notes: "Příspěvky"
-  users: "Uživatelé"
-  drive: "Disk"
-  network: "Síť"
-  charts:
-    federation-instances: "Počet instancí: zvýšení/snížení"
-    federation-instances-total: "Celkový počet instancí"
-    notes-total: "Celkem příspěvků"
-    users-total: "Celkem uživatelů"
-    active-users: "Aktivní uživatelé"
-    network-requests: "Požadavek"
-    network-time: "Doba odezvy"
-    network-usage: "Síťový provoz"
-admin/views/drive.vue:
-  operation: "Operace"
-  fileid-or-url: "ID nebo URL souboru"
-  file-not-found: "Soubor nebyl nalezen"
-  sort:
-    title: "Seřadit"
-    createdAtAsc: "Věk - od nejstaršího"
-    createdAtDesc: "Věk - od nejmladšího"
-    sizeAsc: "Velikost - od nejmenších"
-    sizeDesc: "Velikost – od největších"
-  origin:
-    title: "Původ"
-    combined: "Lokální + Vzdálené"
-    local: "Lokální"
-    remote: "Vzdálené"
-  delete: "Smazat"
-  deleted: "Smazáno"
-admin/views/users.vue:
-  operation: "Operace"
-  username-or-userid: "Uživatelské jméno nebo ID uživatele"
-  user-not-found: "Uživatel nebyl nalezen"
-  reset-password: "Resetovat heslo"
-  reset-password-confirm: "Opravdu chcete resetovat Vaše heslo?"
-  password-updated: "Heslo je nyní \"{password}\""
-  update-remote-user: "Aktualizovat informace o vzdáleném účtu"
-  username: "Přezdívka"
-  host: "Hostitel"
-  users:
-    title: "Uživatel"
-    state:
-      all: "VÅ¡echny"
-      moderator: "Moderátor"
-      adminOrModerator: "Admin/Moderátor"
-    origin:
-      title: "Původ"
-      combined: "Lokální + Vzdálené"
-      local: "Lokální"
-      remote: "Vzdálené"
-    createdAt: "Vytvořeno"
-    updatedAt: "Aktualizováno"
-admin/views/moderators.vue:
-  add-moderator:
-    title: "Vytvořit moderátora"
-  logs:
-    title: "Logy"
-    moderator: "Moderátoři"
-    type: "Operace"
-    info: "Informace"
-admin/views/emoji.vue:
-  add-emoji:
-    title: "Přidat emoji"
-    name: "Jméno emoji"
-    name-desc: "Můžete použít následující znaky a~z 0~9 _"
-    aliases: "Aliasy"
-    aliases-desc: "Můžete nastavit více než jeden, oddělte mezerami."
-    url: "URL obrázku"
-    add: "Přidat"
-    info: "Doporučujeme obrázky ve formátu PNG pod 50 kB."
-    added: "Emoji bylo přidáno"
-  emojis:
-    title: "Seznam smajlíků"
-    update: "Aktualizovat"
-    remove: "Odstranit"
-  remove-emoji:
-    are-you-sure: "Odstranit „$1“?"
-    removed: "Smazáno"
-admin/views/announcements.vue:
-  announcements: "Oznámení"
-  save: "Uložit"
-  remove: "Odstranit"
-  add: "Přidat"
-  title: "Titulek"
-  text: "Obsah"
-  saved: "Uloženo"
-  _remove:
-    are-you-sure: "Odstranit \"$1\"?"
-    removed: "Smazáno"
-admin/views/hashtags.vue:
-  hided-tags: "Skryté tagy"
-admin/views/federation.vue:
-  instance: "Instance"
-  host: "Hostitel"
-  notes: "Poznámky"
-  users: "Uživatelé"
-  following: "Sledování"
-  caught-at: "Vytvořeno"
-  status: "Status"
-  latest-request-received-at: "Poslední požadavek přijat"
-  block: "Blokován"
-  instances: "Z fedivesmíru"
-  states:
-    all: "VÅ¡echny"
-    blocked: "Blokován"
-    not-responding: "Bez odpovědi"
-    marked-as-closed: "Označeno jako uzavřené"
-  charts: "Graf"
-  chart-srcs:
-    requests: "Požadavek"
-    users-total: "Celkem uživatelů"
-    notes-total: "Celkem příspěvků"
-  chart-spans:
-    hour: "za hodinu"
-    day: "za den"
-  blocked-hosts: "Blokován"
-  save: "Uložit"
-desktop/views/pages/welcome.vue:
-  about: "O Misskey"
-  timeline: "Časová osa"
-  announcements: "Oznámení"
-  photos: "Nedávné obrázky"
-  powered-by-misskey: "Běží na <b>Misskey</b>."
-  info: "Informace"
-desktop/views/pages/drive.vue:
-  title: "Misskey Disk"
-desktop/views/pages/note.vue:
-  prev: "Předchozí příspěvěk"
-  next: "Následující příspěvek"
-desktop/views/pages/selectdrive.vue:
-  title: "Vyberte soubor(y)"
-  ok: "OK"
-  cancel: "Zrušit"
-  upload: "Nahrajte soubory z vašeho zařízení"
-desktop/views/pages/search.vue:
-  not-available: "Vyhledávání je vypnuté pro tuto instanci."
-  not-found: "Pro '{q}' nebyly nalezeny žádné příspěvky."
-desktop/views/pages/tag.vue:
-  no-posts-found: "Nebyly nalezeny žádné příspěvky s \"{q}\"."
-desktop/views/pages/user-list.users.vue:
-  users: "Uživatel"
-  add-user: "Přidat uživatele"
-  username: "Přezdívka"
-desktop/views/pages/user/user.followers-you-know.vue:
-  loading: "Načítám..."
-desktop/views/pages/user/user.friends.vue:
-  title: "Častá zmínění"
-  loading: "Načítám..."
-  no-users: "Žádná častá zmínění"
-desktop/views/pages/user/user.photos.vue:
-  title: "Fotky"
-  loading: "Načítám..."
-  no-photos: "Žádné obrázky"
-desktop/views/pages/user/user.header.vue:
-  posts: "Poznámky"
-  following: "Sledovaní"
-  followers: "Sledující"
-  month: "Po"
-  day: "Ne"
-desktop/views/widgets/notifications.vue:
-  title: "Oznámení"
-desktop/views/widgets/polls.vue:
-  title: "Ankety"
-  nothing: "Žádné nové notifikace!"
-desktop/views/widgets/trends.vue:
-  nothing: "Žádné nové notifikace!"
-desktop/views/widgets/users.vue:
-  title: "Doporučení uživatelé"
-mobile/views/components/drive.vue:
-  used: "využito"
-  file-count: "Soubor(ů)"
-  folder-is-empty: "Tato složka je prázdná"
-  folder-name: "Název složky"
-  url-prompt: "URL adresa souboru, který chcete nahrát"
-  uploading: "Byl zahájen upload. Může chvilku trvat než bude dokončen."
-mobile/views/components/drive-file-chooser.vue:
-  select-file: "Vybrat soubory"
-mobile/views/components/drive-folder-chooser.vue:
-  select-folder: "Vyberte složku"
-mobile/views/components/drive.file-detail.vue:
-  download: "Stáhnout"
-  rename: "Přejmenovat"
-  move: "Přesunout"
-  hash: "Hash (md5)"
-  exif: "EXIF"
-mobile/views/components/media-video.vue:
-  click-to-show: "Klikněte pro zobrazení"
-common/views/components/follow-button.vue:
-  following: "Sledování"
-  follow-processing: "Zpracovávám"
-mobile/views/components/note.vue:
-  private: "Tento příspěvek je soukromý"
-  deleted: "Tento příspěvek byl odstraněn"
-  location: "Lokace"
-mobile/views/components/note-detail.vue:
-  reply: "Odpovědět"
-  reaction: "Reakce"
-  private: "Tento příspěvek je soukromý"
-  deleted: "Tento příspěvek byl odstraněn"
-  location: "Lokace"
-mobile/views/components/note-preview.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "kočka"
-mobile/views/components/note-sub.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "kočka"
-mobile/views/components/notifications.vue:
-  empty: "Žádné nové notifikace!"
-mobile/views/components/sub-note-content.vue:
-  private: "Tento příspěvek je soukromý"
-  deleted: "Tento příspěvek byl odstraněn"
-  poll: "Ankety"
-mobile/views/components/ui.header.vue:
-  welcome-back: "Vítejte zpátky,"
-  adjective: "Pán"
-mobile/views/components/ui.nav.vue:
-  timeline: "Časová osa"
-  notifications: "Oznámení"
-  search: "Vyhledávání"
-  user-lists: "Seznamy"
-  user-groups: "Skupiny"
-  widgets: "Widgety"
-  game: "Hry"
-  admin: "Administrace"
-  about: "O Misskey"
-mobile/views/pages/drive.vue:
-  contextmenu:
-    upload: "Nahrát soubor"
-    create-folder: "Vytvořit složku"
-mobile/views/pages/signup.vue:
-  lets-start: "Váš účet je připraven! 📦"
-mobile/views/pages/home.vue:
-  home: "Domů"
-  local: "Lokální"
-  global: "Globální"
-  mentions: "Zmínění"
-mobile/views/pages/tag.vue:
-  no-posts-found: "Nebyly nalezeny žádné příspěvky s \"{q}\"."
-mobile/views/pages/widgets.vue:
-  add-widget: "Přidat"
-  customization-tips: "Tipy pro přizpůsobení"
-mobile/views/pages/widgets/activity.vue:
-  activity: "Aktivita"
-mobile/views/pages/share.vue:
-  share-with: "Sdílet na {name}"
-mobile/views/pages/note.vue:
-  prev: "Předchozí příspěvěk"
-  next: "Následující příspěvek"
-mobile/views/pages/games/reversi.vue:
-  reversi: "Reversi"
-mobile/views/pages/search.vue:
-  search: "Vyhledávání"
-  not-found: "Pro '{q}' nebyly nalezeny žádné příspěvky."
-mobile/views/pages/selectdrive.vue:
-  select-file: "Vybrat soubory"
-mobile/views/pages/notifications.vue:
-  notifications: "Oznámení"
-mobile/views/pages/user/home.vue:
-  activity: "Aktivita"
-  frequently-replied-users: "Častá zmínění"
-mobile/views/pages/user/home.photos.vue:
-  no-photos: "Žádné obrázky"
-deck:
-  widgets: "Widgety"
-  home: "Domů"
-  local: "Lokální"
-  hashtag: "Hashtagy"
-  global: "Globální"
-  mentions: "Zmínění"
-  notifications: "Oznámení"
-  list: "Seznamy"
-  select-list: "Vyberte seznam"
-  swap-left: "Posunout doleva"
-  swap-right: "Posunout doprava"
-  rename: "Přejmenovat"
-deck/deck.user-column.vue:
-  activity: "Aktivita"
-dev/views/new-app.vue:
-  app-name-desc: "Jméno vaší aplikace"
-pages:
-  pin-this-page: "Připnout"
-  unpin-this-page: "Odepnout"
-  like: "Lajk"
-  title: "Titulek"
-  blocks:
-    post: "Formulář pro psaní"
-    _post:
-      text: "Obsah"
-    _textInput:
-      text: "Titulek"
-    _textareaInput:
-      text: "Titulek"
-    _numberInput:
-      text: "Titulek"
-    _switch:
-      text: "Titulek"
-    _counter:
-      text: "Titulek"
-    _button:
-      text: "Titulek"
-      _action:
-        _dialog:
-          content: "Obsah"
-    _radioButton:
-      title: "Titulek"
-  script:
-    categories:
-      random: "Náhodně"
-      list: "Seznamy"
-    blocks:
-      _join:
-        arg1: "Seznamy"
-      random: "Náhodně"
-      _randomPick:
-        arg1: "Seznamy"
-      _dailyRandomPick:
-        arg1: "Seznamy"
-      _seedRandomPick:
-        arg2: "Seznamy"
-      _pick:
-        arg1: "Seznamy"
-      _listLen:
-        arg1: "Seznamy"
-    types:
-      array: "Seznamy"
-room:
-  translate: "Přesunout"
-  save: "Uložit"
-  saved: "Uloženo"
-  furnitures:
-    moon: "Měsíc"
-    bin: "Koš"
diff --git a/locales/da-DK.yml b/locales/da-DK.yml
deleted file mode 100644
index de326526a09393756e552f0515efaff76c765ea7..0000000000000000000000000000000000000000
--- a/locales/da-DK.yml
+++ /dev/null
@@ -1,1921 +0,0 @@
----
-meta:
-  lang: "Dansk"
-common:
-  misskey: "En ⭐ i fediverset"
-  about-title: "En ⭐ i fediverset."
-  about: "Tak, fordi du fandt Misskey. Misskey er en <b> decentral mikroblog platform </b> født på Jorden. Den findes i Fediverset (et univers med forskellige sociale medieplatforme). Den er tæt integreret med andre sociale medier platforme. Hvorfor ikke tage en pause fra trængsel og travlhed i storbyen og hoppe ind i en ny type internet?"
-  intro:
-    title: "Hvad er Misskey?"
-    about: "Misskey er en open-source, <b>decentraliseret microblogging platform</b>. Den har en sofistikeret brugerflade, som kan tilpasses fuldstændigt. Den giver mulighed for at udtrykke mange forskellige reaktioner på poster. Desuden tilbyder den gratis opbevaring af filer med et integreret håndteringssystem samt andre avancerede funktioner. Oven i dette er Misskey tilknyttet et netværk ved navn “Fediverse”, som gør os i stand til at kommunikere med brugere på andre SNS'er. For eksempel vil en post, som du har skrevet, ikke kun blive sendt til brugere af Misskey men også til brugere af Mastodon og Pleroma. Det svarer lidt til at sende radio transmissioner mellem planeter for at etablere en kommunikation."
-    features: "Funktioner"
-    rich-contents: "Post"
-    rich-contents-desc: "Bare skriv løs om dine ideer, aktuelle emner eller alt muligt andet, som du gerne vil dele med andre. Det kan være, at du gerne vil udsmykke dine ord, vedhæfte dine yndlingsbilleder, sende filer, tilføje videoer eller oprette en afstemning. Alle de nævnte ting er muligt med Misskey!"
-    reaction: "Reaktioner"
-    reaction-desc: "Den nemmeste måde at udtrykke dine reaktioner på. Misskey giver mulighed for at tilføje forskellige reaktioner på andres poster. Reaktionerne vil aldrig blive vist på andre SNS'er, som kun er i stand til at udveksle \"likes\"."
-    ui: "Brugerflade"
-    ui-desc: "En enkelt brugerflade vil aldrig passe helt for alle. Derfor er Misskey's brugerflade gennemført justerbar, så den kan ramme dine ønsker helt præcist. Du kan designe dit helt eget personlige udtryk ved at rette layoutet af din tidslinje og tilpasse udvalgte widgets, som desuden kan flyttes frit rundt."
-    drive: "Drev"
-    drive-desc: "Vil du poste et billede, som du tidligere har uploadet? Har du brug for at navngive filer og organisere dem i mapper, som du selv har navngivet? Så er Misskey Drev den bedste løsning for dig. Den gør det så let som ingenting at dele dine filer online."
-    outro: "Tjek Misskey's unikke funktioner ved at se dem med dine egne øjne. Hvis du kommer frem til, at den ene server ikke er noget for dig, så kan du prøve en anden. Misskey er et decentraliseret SNS, så du kan lettere finde frem til brugere, som du klikker med. God fornøjelse!"
-  application-authorization: "Adgangsstyring"
-  close: "Luk"
-  do-not-copy-paste: "Undgå venligst at skrive eller klistre kode ind her. I modsat fald kan din konto blive kompromitteret."
-  load-more: "Læs mere"
-  enter-password: "Skriv din adgangskode"
-  2fa: "To-faktor adgangsstyring"
-  customize-home: "Tilpas dit layout"
-  featured-notes: "Fremhævede poster"
-  dark-mode: "Nat design"
-  signin: "Log ind"
-  signup: "Bliv bruger"
-  signout: "Log ud"
-  reload-to-apply-the-setting: "Denne indstilling slår først igennem, når du har genindlæst siden. Vil du genindlæse siden nu?"
-  fetching-as-ap-object: "Tilladelse til sammenkobling"
-  delete-confirm: "Er du helt sikker på, at du vil slette denne post?"
-  notification-types:
-    all: "Alle"
-    follow: "Følger"
-    reply: "Svar"
-    renote: "Gen-postering"
-    reaction: "Reaktion"
-  got-it: "Det er OK"
-  customization-tips:
-    title: "Tips om tilpasning"
-    paragraph: "<p>Tilpasning giver mulighed for at tilføje, slette og flytte rundt på widgets med træk-og-slip.</p><p>Du kan ændre visningen af visse widgets ved at <strong>højre-klikke</strong> på dem.</p><p>En widget slettes ved at trække den med musen hen til <strong>skaldespanden</strong> i toppen af siden.</p><p>Du afslutter tilpasningen ved at klikke på \"Færdig\" øverst til højre.</p>"
-    gotit: "Det er OK"
-  notification:
-    file-uploaded: "Filen er overført!"
-    message-from: "Besked fra {}:"
-    reversi-invited: "Invitation til spil"
-    reversi-invited-by: "Inviteret af {}:"
-    notified-by: "Besked fra {}:"
-    reply-from: "Svar fra {}:"
-    quoted-by: "Citeret af {}:"
-  time:
-    unknown: "ukendt"
-    future: "fremtidig"
-    just_now: "nu"
-    seconds_ago: "{} sekund(er) siden"
-    minutes_ago: "{} minut(ter) siden"
-    hours_ago: "{} time(r) siden"
-    days_ago: "{} dag(e) siden"
-    weeks_ago: "{} uge(r) siden"
-    months_ago: "{} måned(er) siden"
-    years_ago: "{} år siden"
-  month-and-day: "{day}-{month}"
-  trash: "Skraldespand"
-  drive: "Drev"
-  pages: "Sider"
-  messaging: "Konversationer"
-  home: "Startside"
-  deck: "Stabel"
-  timeline: "Tidslinje"
-  explore: "Udforsk"
-  following: "Følger"
-  followers: "Følgere"
-  favorites: "Favoritter"
-  permissions:
-    "read:account": "Se konto indstillinger"
-    "write:account": "Opdater dine konto informationer"
-    "read:blocks": "Vis blokke"
-    "write:blocks": "Rediger blokke"
-    "read:drive": "Gennemse drevet"
-    "write:drive": "Rediger drevet"
-    "read:favorites": "Mine favoritter"
-    "write:favorites": "Rediger favoritterne"
-    "read:following": "Vis info om følgere"
-    "write:following": "Rediger info om følgere"
-    "read:messaging": "Se meddelelser"
-    "write:messaging": "Rediger meddelelser"
-    "read:mutes": "Se annullerede poster"
-    "write:mutes": "Rediger annullerede poster"
-    "write:notes": "Opret og slet poster"
-    "read:notifications": "Vis notifikationer"
-    "write:notifications": "Rediger notifikationer"
-    "read:reactions": "Vis reaktioner"
-    "write:reactions": "Rediger reaktioner"
-    "write:votes": "Stem"
-  empty-timeline-info:
-    follow-users-to-make-your-timeline: "Følgende brugere vil få vist deres poster på tidslinjen."
-    explore: "Find brugere"
-  post-form:
-    submit: "Post"
-    reply: "Svar"
-    renote: "Gen-postering"
-    error: "Fejl"
-    enter-username: "Angiv brugernavn"
-    add-visible-user: "Tilføj en bruger"
-    username-prompt: "Angiv brugernavn"
-  weekday-short:
-    sunday: "Søn"
-    monday: "Man"
-    tuesday: "Tirs"
-    wednesday: "Ons"
-    thursday: "Tors"
-    friday: "Fre"
-    saturday: "Lør"
-  weekday:
-    sunday: "Søndag"
-    monday: "Mandag"
-    tuesday: "Tirsdag"
-    wednesday: "Onsdag"
-    thursday: "Torsdag"
-    friday: "Fredag"
-    saturday: "Lørdag"
-  reactions:
-    like: "Synes om"
-    love: "Elsker"
-    laugh: "Ler"
-    hmm: "Hmm...?"
-    surprise: "Wauw"
-    congrats: "Tillykke"
-    angry: "Vred"
-    confused: "Forvirret"
-    rip: "Hvil i fred"
-    pudding: "Budding"
-  note-visibility:
-    public: "Offentlig"
-    home: "Startside"
-    home-desc: "Post udelukkende til tidslinjen"
-    followers: "Følgere"
-    followers-desc: "Skriv kun til dine følgere"
-    specified: "Direkte"
-    specified-desc: "Skriv kun til udvalgte brugere"
-    local-public: "Offentlig (på den lokale server)"
-    local-home: "Startside (på den lokale server)"
-    local-followers: "Følgere (på den lokale server)"
-  note-placeholders:
-    a: "Hvad laver du?"
-    b: "Hvad sker der?"
-    c: "Hvad har du i tankerne?"
-    d: "Hvad vil du gerne sige?"
-    e: "Skriv her"
-    f: "Venter på din indtastning."
-  settings: "Indstillinger"
-  _settings:
-    profile: "Profil"
-    notification: "Notifikation"
-    apps: "Apps"
-    tags: "Hashtag"
-    mute-and-block: "Sluk / Blokér"
-    blocking: "Blokér"
-    security: "Sikkerhed"
-    signin: "Login historik"
-    password: "Adgangskode"
-    other: "Andet"
-    appearance: "Udseende"
-    behavior: "Opførsel"
-    reactions: "Reaktion"
-    fetch-on-scroll: "Uendeligt scroll"
-    fetch-on-scroll-desc: "NÃ¥r du scroller ned ad siden, hentes der automatisk nyt indhold ind"
-    note-visibility: "Post synlighed"
-    default-note-visibility: "Standard synlighed"
-    remember-note-visibility: "Husk post synlighed"
-    web-search-engine: "Søgemaskine"
-    web-search-engine-desc: "Eksempel: https://www.google.com/?#q={{query}}"
-    keep-cw: "Bevar indholdsvarsel"
-    keep-cw-desc: "Det indholdsvarsel, som står på det oprindelige indlæg, vil som standard blive overført til eventuelle svar på indlægget."
-    i-like-sushi: "Jeg foretrækker sushi frem for budding"
-    show-reversi-board-labels: "Vis række- og kolonne-etiketter i Reversi"
-    use-avatar-reversi-stones: "Anvend avatar som en sten i Reversi"
-    disable-animated-mfm: "Deaktiver animeret tekst i en post"
-    disable-showing-animated-images: "Afspil ikke animerede billeder"
-    suggest-recent-hashtags: "Vis de seneste populære hashtags på post formularen"
-    always-show-nsfw: "Vis altid indhold, der er markeret som Upassende PÃ¥ Jobbet"
-    always-mark-nsfw: "Marker altid poster med medie bilag som Upassende PÃ¥ Jobbet"
-    show-full-acct: "Vis aldrig værtsnavnet på brugernavnet"
-    show-via: "vis via"
-    reduce-motion: "Reducer bevægelser"
-    this-setting-is-this-device-only: "Indstillingen gælder kun for denne enhed"
-    use-os-default-emojis: "Anvend standard emojis fra operativsystemet"
-    line-width: "Linjebredde"
-    line-width-thin: "Tynd linje"
-    line-width-normal: "Normal"
-    line-width-thick: "Tyk linje"
-    font-size: "Tekst størrelse"
-    font-size-x-small: "Meget lille"
-    font-size-small: "Lille"
-    font-size-medium: "Normal"
-    font-size-large: "Stor"
-    font-size-x-large: "Meget stor"
-    deck-column-align: "Justering af kolonner"
-    deck-column-align-center: "Midten"
-    deck-column-align-left: "Venstre"
-    deck-column-align-flexible: "Højre"
-    deck-column-width: "Kolonne bredde"
-    deck-column-width-narrow: "Smal"
-    deck-column-width-narrower: "Smallere"
-    deck-column-width-normal: "Normal"
-    deck-column-width-wider: "Lidt bredere"
-    deck-column-width-wide: "Bred"
-    use-shadow: "Vis skygger"
-    rounded-corners: "Vis afrundede hjørner"
-    circle-icons: "Anvend cykliske avatar"
-    contrasted-acct: "Tilføj kontrast til brugerkontoen"
-    wallpaper: "Baggrundsbillede"
-    choose-wallpaper: "Vælg en baggrund"
-    delete-wallpaper: "Fjern baggrund"
-    post-form-on-timeline: "Vis post formularen oven over tidslinjen"
-    show-clock-on-header: "Vis uret i øverste højre hjørne"
-    show-reply-target: "Vis hvad der svares på"
-    timeline: "Tidslinje"
-    show-my-renotes: "Vis mine gen-posteringer på tidslinjen"
-    show-renoted-my-notes: "Vis gen-posteringer af dine egne poster på tidslinjen"
-    show-local-renotes: "Vis gen-posteringer af lokale poster på tidslinjen"
-    remain-deleted-note: "Fortsæt med at vise slettede poster"
-    sound: "Lyd"
-    enable-sounds: "Aktiver lyd"
-    enable-sounds-desc: "Afspil en lyd, når du modtager en post/besked. Denne indstilling gemmes i browseren."
-    volume: "Volumen"
-    test: "Test"
-    update: "Misskey opdatering"
-    version: "Aktuel version:"
-    latest-version: "Seneste version:"
-    update-checking: "Kikker efter opdateringer"
-    do-update: "Kikker efter opdateringer"
-    update-settings: "Avancerede indstillinger"
-    no-updates: "Der er ikke kommet nogen opdateringer"
-    no-updates-desc: "Din Misskey er opdateret"
-    update-available: "Der er kommet en ny version"
-    update-available-desc: "Opdateringer vil slå igennem efter genindlæsning af siden."
-    advanced-settings: "Avancerede indstillinger"
-    debug-mode: "Aktiver debug"
-    debug-mode-desc: "Denne indstilling er gemt i browseren"
-    navbar-position: "Placering af navigationsbaren"
-    navbar-position-top: "Top"
-    navbar-position-left: "Venstre"
-    navbar-position-right: "Højre"
-    i-am-under-limited-internet: "Mit internet kører med lav hastighed"
-    post-style: "Stil for visning af poster"
-    post-style-standard: "Standard"
-    post-style-smart: "Smart"
-    notification-position: "Vis notifikationer"
-    notification-position-bottom: "Bund"
-    notification-position-top: "Top"
-    disable-via-mobile: "Marker aldrig posten som \"fra mobil\""
-    load-raw-images: "Vis vedhæftede bilag i original kvalitet"
-    load-remote-media: "Vis medie-materiale fra en ekstern server"
-    save: "Gem"
-    saved: "Gemt"
-    preview: "Før-visning"
-  search: "Søg"
-  delete: "Slet"
-  loading: "Henter"
-  ok: "Bekræft"
-  cancel: "Afbryd"
-  update-available-title: "Opdatering tilgængelig"
-  update-available: "En ny version af Misskey er nu tilgængelig ({newer}, den aktuelle version er {current}). Genindlæs siden for at få opdateringerne til at slå igennem."
-  my-token-regenerated: "Din nøgle er blevet genopbygget, så du bliver logget ud."
-  hide-password: "Skjul adgangskoden"
-  show-password: "Vis adgangskoden"
-  enter-username: "Indtast brugernavn"
-  do-not-use-in-production: "Dette er en instans til udvikling. Bør ikke benyttes til produktion."
-  user-suspended: "Denne bruger er blevet udelukket."
-  is-remote-user: "Oplysningerne om denne bruger er muligvis ikke fyldestgørende"
-  is-remote-post: "Indholdet af denne post er spejlet fra andetsteds"
-  view-on-remote: "Se den fulde version eksternt"
-  renoted-by: "Gen-posteret af {user}"
-  no-notes: "Uden poster"
-  turn-on-darkmode: "Skift til mørk baggrund"
-  turn-off-darkmode: "Lys baggrund"
-  error:
-    title: "Noget gik galt :("
-    retry: "Prøv igen"
-  reversi:
-    drawn: "Tegn"
-    my-turn: "Din tur"
-    opponent-turn: "Modstanderens tur"
-    turn-of: "{name}s tur"
-    past-turn-of: "{name}s tur forinden"
-    won: "{name} vandt"
-    black: "Sort"
-    white: "Hvid"
-    total: "I alt"
-    this-turn: "Runde {count}"
-  widgets:
-    analog-clock: "Analogt ur"
-    profile: "Profil"
-    calendar: "Kalender"
-    timemachine: "Kalender (tidsmaskine)"
-    activity: "Aktivitet"
-    rss: "RSS læser"
-    memo: "Selvklæbende noter"
-    trends: "Tendenser"
-    photo-stream: "Billedkavalkade"
-    posts-monitor: "Graf over poster"
-    slideshow: "Billedkarrusel"
-    version: "Version"
-    broadcast: "Offentliggør"
-    notifications: "Notifikation"
-    users: "Anbefalede brugere"
-    polls: "Afstemninger"
-    post-form: "Post formular"
-    server: "Server info"
-    nav: "Navigation"
-    tips: "Tips og tricks"
-    hashtags: "Hashtags"
-    queue: "Kø"
-  dev: "Fejl under oprettelse af app. Prøv igen."
-  ai-chan-kawaii: "Ai Chan Kawaii!"
-  you: "Du"
-auth/views/form.vue:
-  share-access: "Vil du tillade, at <i>{name}</i> får adgang til din konto?"
-  permission-ask: "Denne app kræver følgende tilladelser:"
-  cancel: "Annuller"
-  accept: "Ã…bn for adgang."
-auth/views/index.vue:
-  loading: "Henter"
-  denied: "Adgang til app er blevet afvist."
-  denied-paragraph: "Denne app vil ikke give adgang for din konto."
-  already-authorized: "Der er allerede adgang til denne app."
-  allowed: "Der er adgang til app."
-  callback-url: "Hopper tilbage til app."
-  please-go-back: "Hop tilbage til app."
-  error: "Sessionen eksisterer ikke."
-  sign-in: "Log ind."
-common/views/pages/explore.vue:
-  pinned-users: "Fremhævede brugere"
-  popular-users: "Populære brugere"
-  recently-updated-users: "Senest aktive brugere"
-  recently-registered-users: "Brugere som er kommet til for nyligt"
-  popular-tags: "Populære tags"
-  federated: "Fra Fediverset"
-  explore: "Udforsk {host}"
-  users-info: "Lige nu er {users} brugere registreret her"
-common/views/components/url-preview.vue:
-  enable-player: "Aktiver afspilning"
-  disable-player: "Stop afspilning"
-common/views/components/user-list.vue:
-  no-users: "Der er ingen brugere"
-common/views/components/games/reversi/reversi.vue:
-  matching:
-    waiting-for: "Venter på {}"
-    cancel: "Annuller"
-common/views/components/games/reversi/reversi.game.vue:
-  surrender: "Giv op"
-  surrendered: "Af taberen"
-  is-llotheo: "Den med færrest vinder (Llotheo)"
-  looped-map: "Vendebrikker"
-  can-put-everywhere: "Kan placeres hvorsomhelst"
-common/views/components/games/reversi/reversi.index.vue:
-  title: "Misskey Reversi"
-  sub-title: "Spil Reversi med dine venner!"
-  invite: "Inviter"
-  rule: "Spilleregler"
-  rule-desc: "Reversi er et strategi spil for to deltagere, og det spilles på et bræt med 8 gange 8 felter. På felterne skal placeres 64 ens brikker, som er sorte på den ene side og hvide på den anden. Deltagerne vælger hver sin farve og placerer på skift en brik med deres egen farve opad. Det gælder om at placere brikker med sin egen farve i hver sin ende af en stribe brikker med modstanderens farve, for det giver ret til at vende de mellemliggende brikker rundt, så de får ens egen farve. Vinderen er den, som til sidst har erobret flest felter på brættet."
-  mode-invite: "Inviter"
-  mode-invite-desc: "Spil med en udvalgt bruger"
-  invitations: "Du har fået en invitation!"
-  my-games: "Mine spil"
-  all-games: "Alle spil"
-  enter-username: "Angiv brugernavn"
-  game-state:
-    ended: "Slut"
-    playing: "I gang"
-common/views/components/games/reversi/reversi.room.vue:
-  settings-of-the-game: "Spilleindstillinger"
-  choose-map: "Vælg en brikfarve"
-  random: "Tilfældig"
-  black-or-white: "Sort/hvid"
-  black-is: "Sort er {}"
-  rules: "Regler"
-  is-llotheo: "Den med færrest vinder (Llotheo)"
-  looped-map: "Vendebrikker"
-  can-put-everywhere: "Kan placeres hvorsomhelst"
-  settings-of-the-bot: "Bot indstillinger"
-  this-game-is-started-soon: "Spillet begynder lige om lidt"
-  waiting-for-other: "Venter på modstanderen"
-  waiting-for-me: "Venter på, at du bliver klar"
-  waiting-for-both: "Venter på, at spillerne er klar"
-  cancel: "Annuller"
-  ready: "Klar"
-  cancel-ready: "Fortryd din klar-melding"
-common/views/components/connect-failed.vue:
-  title: "Ingen kontakt med serveren"
-  description: "Der er et problem med din internet forbindelse, eller så er serveren nede eller under vedligeholdelse. Tag og {try again} senere."
-  thanks: "Tak, fordi du bruger Misskey."
-  troubleshoot: "Fejlfinding"
-common/views/components/connect-failed.troubleshooter.vue:
-  title: "Fejlfinding"
-  network: "Netværksforbindelse"
-  checking-network: "Tjekker netværksforbindelsen"
-  internet: "Internetforbindelse"
-  checking-internet: "Tjekker internetforbindelse"
-  server: "Forbindelse til server"
-  checking-server: "Tjekker forbindelsen til server"
-  finding: "Prøver at finde problemet"
-  no-network: "Ingen forbindelse"
-  no-network-desc: "Tjek en ekstra gang, om du har netværksforbindelse."
-  no-internet: "Det er ingen internetforbindelse"
-  no-internet-desc: "Tjek en ekstra gang, at du har forbindelse til internettet"
-  no-server: "Ude af stand til at skabe forbindelse til Misskey serveren"
-  no-server-desc: "Netværksforbindelsen på din enhed er normal, men du kunne ikke koble dig på Misskey serveren. Årsagen kan være, at serveren er nede, eller at den er under vedligeholdelse. Prøv igen senere."
-  success: "Du er nu blevet koblet til Misskey serveren"
-  success-desc: "Det ser ud til, at der er forbindelse. Genindlæs siden."
-  flush: "Ryd cachen"
-  set-version: "Angiv version"
-common/views/components/media-banner.vue:
-  sensitive: "Upassende PÃ¥ Jobbet"
-  click-to-show: "Klik for at se"
-common/views/components/theme.vue:
-  theme: "Tema"
-  light-theme: "Tema i tilknytning til lys baggrund"
-  dark-theme: "Tema i tilknytning til mørk baggrund"
-  light-themes: "Lyst tema"
-  dark-themes: "Mørkt tema"
-  install-a-theme: "Installer et tema"
-  theme-code: "Tema kode"
-  install: "Installer"
-  installed: "\"{}\" er blevet installeret"
-  create-a-theme: "Opret et tema"
-  save-created-theme: "Gem tema"
-  primary-color: "Primær farve"
-  secondary-color: "Sekundær farve"
-  text-color: "Tekst farve"
-  base-theme: "Grundtema"
-  base-theme-light: "Lyst"
-  base-theme-dark: "Mørkt"
-  find-more-theme: "Find flere temaer"
-  theme-name: "Tema navn"
-  preview-created-theme: "Før-visning"
-  invalid-theme: "Temaet er ikke gyldigt"
-  already-installed: "Teamet er allerede installeret"
-  saved: "Gemt"
-  manage-themes: "Administrer temaer"
-  builtin-themes: "Standard temaer"
-  my-themes: "Mine temaer"
-  installed-themes: "Installerede temaer"
-  select-theme: "Vælg dit tema"
-  uninstall: "Afinstaller"
-  uninstalled: "\"{}\" er blevet afinstalleret"
-  author: "Skribent"
-  desc: "Beskrivelse"
-  export: "Eksport"
-  import: "Import"
-  import-by-code: "eller indsæt kode"
-  theme-name-required: "Temaet skal have et navn"
-common/views/components/cw-button.vue:
-  hide: "Skjul"
-  show: "Se mere"
-  chars: "{count} tegn"
-  files: "{count} filer"
-  poll: "Afstemninger"
-common/views/components/messaging.vue:
-  search-user: "Find en bruger"
-  you: "Du"
-  no-history: "Uden historik"
-  user: "Bruger"
-  group: "Gruppe"
-  start-with-user: "Start chat med bruger"
-  start-with-group: "Start chat med gruppe"
-  select-group: "Vælg gruppe"
-common/views/components/messaging-room.vue:
-  not-talked-user: "Ingen bruger sessionshistorik"
-  not-talked-group: "Intet gruppesessions dokument"
-  no-history: "Der er ingen yderligere historik"
-  new-message: "Ny besked"
-  only-one-file-attached: "Kan kun indeholde én vedhæftning"
-common/views/components/messaging-room.form.vue:
-  input-message-here: "Skriv meddelelsen her"
-  send: "Send"
-  attach-from-local: "Vedhæft filen fra din enhed"
-  attach-from-drive: "Vedhæft filen fra dit drev"
-  only-one-file-attached: "Kan kun indeholde én vedhæftning"
-common/views/components/messaging-room.message.vue:
-  is-read: "Læst"
-  deleted: "Denne meddelelse er slettet"
-common/views/components/nav.vue:
-  about: "Om"
-  stats: "Statistik"
-  status: "Status"
-  wiki: "Wiki"
-  donors: "Donatorer"
-  repository: "Systemets kode-repo"
-  develop: "Udviklere"
-  feedback: "Tilbagemeldinger"
-  tos: "Brugerbetingelser"
-common/views/components/note-menu.vue:
-  mention: "Omtale"
-  detail: "Detaljer"
-  copy-content: "Kopier indholdet"
-  copy-link: "Kopier link"
-  favorite: "Marker denne post som favorit"
-  unfavorite: "Fjern favorit-markering"
-  watch: "Hold øje med"
-  unwatch: "Hold ikke længere øje med"
-  pin: "Tilknyt til din profil"
-  unpin: "Fjern tilknytning til din profil"
-  delete: "Slet"
-  delete-confirm: "Er du helt sikker på, at du vil slette denne post?"
-  remote: "Vis den oprindelige post"
-common/views/components/user-menu.vue:
-  mention: "Omtale"
-  mute: "Annuller"
-  unmute: "Ophæv annullering"
-  mute-confirm: "Er du sikker på, at du vil annullere denne bruger?"
-  unmute-confirm: "Er du sikker på, at du vil fjerne annulleringen af denne bruger?"
-  block: "Bloker"
-  unblock: "Fjern blokering"
-  block-confirm: "Er du sikker på, at du vil blokere denne bruger?"
-  unblock-confirm: "Er du sikker på, at du vil fjerne blokeringen af denne bruger?"
-  push-to-list: "Tilføj til liste"
-  select-list: "Vælg liste"
-  report-abuse: "Meld misbrug"
-  report-abuse-detail: "Hvilken form for misbrug har du været ude for?"
-  report-abuse-reported: "Denne hændelse er nu videresendt til administratoren. Mange tak for hjælpen."
-  silence: "Gør tavs"
-  unsilence: "Fortryd at du har gjort tavs"
-  silence-confirm: "Er du sikker på, at du vil gøre denne bruger tavs?"
-  unsilence-confirm: "Er du sikker på, at du har fortrudt, at du har gjort denne bruger tavs?"
-  suspend: "Udeluk"
-  unsuspend: "Ophæv udelukkelse"
-  suspend-confirm: "Er du sikker på, at du vil udelukke denne bruger?"
-  unsuspend-confirm: "Er du sikker på, at du vil ophæve udelukkelsen af denne bruger?"
-common/views/components/poll.vue:
-  vote-to: "Stem på '{}'"
-  vote-count: "{} stemmer"
-  total-votes: "{} stemmer i alt"
-  vote: "Stem"
-  show-result: "Vis resultatet"
-  voted: "Stemt"
-  closed: "Afsluttet"
-  remaining-days: "{d} dage og {h} timer tilbage"
-  remaining-hours: "{h} timer og {m} minutter tilbage"
-  remaining-minutes: "{m} minutter og {s} sekunder tilbage"
-  remaining-seconds: "{s} sekunder tilbage"
-common/views/components/poll-editor.vue:
-  no-only-one-choice: "Der skal vælges mindst to muligheder"
-  choice-n: "Valgmulighed {}"
-  remove: "Slet valgmulighed"
-  add: "Tilføj valgmulighed"
-  destroy: "Drop afstemningen"
-  multiple: "Mere end et svar er tilladt"
-  expiration: "Udløber"
-  infinite: "Uendelig"
-  at: "Dato- og tidsvælger"
-  after: "Angivet tid"
-  no-more: "Du kan ikke tilføje flere svar"
-  deadline-date: "Slutdato"
-  deadline-time: "Varighed"
-  interval: "Varighed"
-  unit: "Enhed"
-  second: "Sekunder"
-  minute: "Minutter"
-  hour: "Time"
-  day: "Søn"
-common/views/components/reaction-picker.vue:
-  choose-reaction: "Vælg reaktion"
-common/views/components/emoji-picker.vue:
-  custom-emoji: "Brugerdefineret emoji"
-  people: "Personer"
-  animals-and-nature: "Dyr og natur"
-  food-and-drink: "Mad og drikke"
-  activity: "Aktivitet"
-  travel-and-places: "Rejser og steder"
-  objects: "Objekt"
-  symbols: "Symboler"
-  flags: "Flag"
-common/views/components/settings/app-type.vue:
-  info: "Du er nødt til at genindlæse siden, før ændringerne slår igennem."
-common/views/components/signin.vue:
-  username: "Brugernavn"
-  password: "Adgangskode"
-  token: "Nøgle"
-  signing-in: "Log ind"
-  or: "Eller"
-  signin-with-twitter: "Log ind med Twitter"
-  signin-with-github: "Log ind med GitHub"
-  signin-with-discord: "Log ind med Discord"
-  login-failed: "Fejl ved log ind. Sørg for, at du har skrevet korrekt brugernavn og adgangskode."
-common/views/components/signup.vue:
-  invitation-code: "Invitationskode"
-  invitation-info: "Kontakt en <a href=\"{}\">administrator</a>, hvis du ikke har en invitationskode."
-  username: "Brugernavn"
-  checking: "Tjekker"
-  available: "Tilgængelig"
-  unavailable: "Ikke tilgængelig"
-  error: "Netværksfejl"
-  invalid-format: "bogstaver, tal og \"_\" er tilladt."
-  too-short: "Må ikke være tom!"
-  too-long: "Brug højst 20 tegn."
-  password: "Adgangskode"
-  password-placeholder: "Det anbefales at skrive mere end otte tegn"
-  weak-password: "Svag adgangskode"
-  normal-password: "Rimelig adgangskode"
-  strong-password: "Stærk adgangskode"
-  retype: "Skriv igen"
-  retype-placeholder: "Bekræft din adgangskode"
-  password-matched: "Godkendt"
-  password-not-matched: "Ikke godkendt"
-  recaptcha: "Verificering"
-  agree-to: "Enig {0}"
-  tos: "Brugerbetingelser"
-  create: "Opret en konto"
-  some-error: "Af en eller anden grund mislykkedes forsøget på at oprette en konto. Prøv igen."
-common/views/components/special-message.vue:
-  new-year: "Godt nytår!"
-  christmas: "Glædelig jul!"
-common/views/components/stream-indicator.vue:
-  connecting: "Tilslutter"
-  reconnecting: "Tilslutter igen"
-  connected: "Tilsluttet"
-common/views/components/notification-settings.vue:
-  title: "Notifikationer"
-  mark-as-read-all-notifications: "Marker alle notifikationer som læste"
-  mark-as-read-all-unread-notes: "Marker alle poster som læste"
-  mark-as-read-all-talk-messages: "Marker alle samtaler som læste"
-  auto-watch: "Automatisk visning af poster"
-  auto-watch-desc: "Modtag automatisk notifikationer om poster, som du har reageret eller svaret på."
-common/views/components/integration-settings.vue:
-  title: "Service samarbejde"
-  connect: "Tilslut"
-  disconnect: "Frakobl"
-  connected-to: "Du er tilsluttet næste konto"
-common/views/components/github-setting.vue:
-  description: "Når du tilslutter din GitHub konto til din Misskey konto, bliver du i stand til at se info om din GitHub konto på din profil, og du vil få mulighed for at logge ind via GitHub."
-  connected-to: "Du er tilsluttet denne GitHub konto"
-  detail: "Flere detaljer"
-  reconnect: "Tilslut igen"
-  connect: "Tilslut til din GitHub konto"
-  disconnect: "Frakobl"
-common/views/components/discord-setting.vue:
-  description: "Når du tilslutter din Discord konto til din Misskey konto, bliver du i stand til at se info om din Discord konto på din profil, og du vil få mulighed for at logge ind via Discord."
-  connected-to: "Du er tilsluttet denne Discord konto"
-  detail: "Detaljer..."
-  reconnect: "Tilslut igen"
-  connect: "Tilslut din Discord konto"
-  disconnect: "Frakobl"
-common/views/components/uploader.vue:
-  waiting: "Venter"
-common/views/components/visibility-chooser.vue:
-  public: "Offentlig"
-  home: "Startside"
-  home-desc: "Post kun til startsiden"
-  followers: "Følgere"
-  followers-desc: "Post kun til følgere"
-  specified: "Direkte"
-  specified-desc: "Post kun til udvalgte brugere"
-  local-public: "Offentlig (på den lokale server)"
-  local-public-desc: "Offentliggør ikke til eksterne"
-  local-home: "Startside (på den lokale server)"
-  local-followers: "Følgere (på den lokale server)"
-common/views/components/trends.vue:
-  count: "{} brugere nævnt"
-  empty: "Ingen tendenser"
-common/views/components/language-settings.vue:
-  title: "Vis sprog"
-  pick-language: "Vælg sprog"
-  recommended: "Anbefalet"
-  auto: "Automatisk"
-  specify-language: "Angiv sprog"
-  info: "Du er nødt til at genindlæse siden, før ændringerne slår igennem."
-common/views/components/profile-editor.vue:
-  title: "Profil"
-  name: "Navn"
-  account: "Konto"
-  location: "Placering"
-  description: "Om mig"
-  you-can-include-hashtags: "Du må gerne bruge hashtags i din profil beskrivelse"
-  language: "Sprog"
-  birthday: "Fødselsdag"
-  avatar: "Avatar"
-  banner: "Banner"
-  is-cat: "Denne konto er en Kat"
-  is-bot: "Denne konto er en Bot"
-  is-locked: "Anmodning fra følgere kræver godkendelse"
-  careful-bot: "Følger anmodninger fra bots kræver godkendelse"
-  auto-accept-followed: "Accepter automatisk følgere af personer, som du selv følger"
-  advanced: "Avanceret"
-  privacy: "Privatliv"
-  save: "Gem"
-  saved: "Profil er opdateret med succes"
-  uploading: "Overfører"
-  upload-failed: "Fejl ved overførsel"
-  unable-to-process: "Handlingen kunne ikke gennemføres."
-  email: "Email indstillinger"
-  email-address: "Email adresse"
-  email-verified: "Din email er blevet bekræftet"
-  email-not-verified: "Email adresse er ikke bekræftet. Tjek indbakken i din mailboks."
-  export: "Eksport"
-  import: "Import"
-  export-and-import: "Eksport og import"
-  export-targets:
-    all-notes: "Alle poster"
-    following-list: "Liste over følgere"
-    mute-list: "Liste over annullerede konti"
-    blocking-list: "Liste over blokerede konti"
-    user-lists: "Lister"
-  export-requested: "Du har bedt om en eksport. Det kan tage et stykke tid. Når eksporten er gennemført, vil eksport-filen blive lagt på dit drev."
-  import-requested: "Du har sat en import i gang. Det kan tage et stykke tid."
-  enter-password: "Angiv din adgangskode"
-  danger-zone: "Risici"
-  delete-account: "Slet kontoen"
-  account-deleted: "Kontoen er slettet. Det kan vare lidt, inden alle data forsvinder."
-  metadata-content: "Indhold"
-common/views/components/user-list-editor.vue:
-  users: "Bruger"
-  rename: "Omdøb listen"
-  delete: "Slet liste"
-  remove-user: "Fjern fra denne liste"
-  delete-are-you-sure: "Slet liste \"$1\"?"
-  deleted: "Slettet med succes"
-  add-user: "Tilføj en bruger"
-common/views/components/user-group-editor.vue:
-  users: "Brugere"
-  rename: "Omdøb gruppe"
-  delete: "Slet gruppe"
-  remove-user: "Fjern bruger fra denne gruppe"
-  delete-are-you-sure: "Er du sikker på, at du vil slette gruppen \"$ 1\"?"
-  deleted: "Slettet"
-  invite: "Inviter"
-  invited: "Inviterede"
-common/views/components/user-lists.vue:
-  user-lists: "Lister"
-  create-list: "Opret en liste"
-  list-name: "Liste navn"
-common/views/components/user-groups.vue:
-  user-groups: "Gruppe"
-  create-group: "Opret gruppe"
-  group-name: "Gruppenavn"
-  owned-groups: "Egne grupper"
-  joined-groups: "Tilsluttede grupper"
-  invites: "Inviter"
-  accept-invite: "Tag imod invitation"
-  reject-invite: "Afvis"
-common/views/widgets/broadcast.vue:
-  fetching: "Tjekker"
-  no-broadcasts: "Ingen meddelelser"
-  have-a-nice-day: "Hav en god dag!"
-  next: "Næste"
-common/views/widgets/calendar.vue:
-  year: "Ã…r {}"
-  month: "{},"
-  day: "{}"
-  today: "I dag:"
-  this-month: "MÃ¥ned:"
-  this-year: "Ã…r:"
-common/views/widgets/photo-stream.vue:
-  title: "Billedkavalkade"
-  no-photos: "Ingen billeder"
-common/views/widgets/posts-monitor.vue:
-  title: "Graf over poster"
-  toggle: "Skift mellem visninger"
-common/views/widgets/hashtags.vue:
-  title: "Hashtags"
-common/views/widgets/server.vue:
-  title: "Server info"
-  toggle: "Skift mellem visninger"
-common/views/widgets/memo.vue:
-  title: "Selvklæbende noter"
-  memo: "Skriv her!"
-  save: "Gem"
-common/views/widgets/slideshow.vue:
-  folder-customize-mode: "For at kunne angive en mappe er du nødt til at gå ud af tilpasnings indstillingerne"
-  folder: "Klik og angiv en mappe"
-  no-image: "Der er ikke noget billede i denne mappe"
-common/views/widgets/tips.vue:
-  tips-line1: "Du kan fokusere på tidslinjen med <kbd>t</kbd>"
-  tips-line2: "Ã…bn post formularen med <kbd>p</kbd> eller <kbd>n</kbd>."
-  tips-line3: "Du kan trække og slippe filer på post formularen"
-  tips-line4: "Du kan indsætte et billede fra klippebordet på afsendelses formularen"
-  tips-line5: "Du kan overføre filer ved at trække og slippe dem på dit drev"
-  tips-line6: "Du kan flytte en mappe ved at trække den inden for dit drev"
-  tips-line7: "Du kan flytte mapper ved at trække dem inden for dit drev"
-  tips-line8: "Startsidens layout kan tilpasses fra indstillingerne"
-  tips-line9: "Misskey er licenseret under AGPLv3."
-  tips-line10: "Widgeten med tidsmaskinen gør det let at \"spole\" tilbage til den tidligere tidslinje."
-  tips-line11: "Du kan sende poster til bruger siden ved at klikke på \"...\""
-  tips-line13: "Alle filer tilknyttet en post gemmes på dit drev."
-  tips-line14: "Når du tilpasser layoutet på startsiden, kan du højreklikke på en widget for at ændre dens design."
-  tips-line17: "Du kan fremhæve en tekstbid ved at omgive den med ** **."
-  tips-line19: "Flere vinduer kan kobles af og vises uden for browseren."
-  tips-line20: "Procentdelen af kalender-widgeten viser procentdelen af den tid, der er gået."
-  tips-line21: "Du kan også bruge Misskey's API til at udvikle bots."
-  tips-line23: "Ai Chan Kawaii!"
-  tips-line24: "Misskey har været i drift siden 2014."
-  tips-line25: "Du kan modtage notifikationer, selv om Misskey ikke er åben, hvis du anvender en browser, der er i stand til at håndtere notifikationer."
-common/views/pages/not-found.vue:
-  page-not-found: "Siden kan ikke findes"
-common/views/pages/follow.vue:
-  signed-in-as: "Logget ind som {}"
-  following: "Følger"
-  follow: "Følg"
-  request-pending: "Ventende anmodninger om at blive følger"
-  follow-processing: "Anmoder om behandling"
-  follow-request: "Anmodning om at blive følger"
-common/views/pages/follow-requests.vue:
-  received-follow-requests: "Anmodninger om at blive følgere"
-  accept: "Accepter"
-  reject: "Afvis"
-desktop:
-  banner-crop-title: "Beskær den del, der vises som et banner"
-  banner: "Banner"
-  uploading-banner: "Overfør et nyt banner"
-  banner-updated: "Banner er overført med succes"
-  choose-banner: "Vælg banner"
-  avatar-crop-title: "Beskær den del, der vises som en avatar"
-  avatar: "Avatar"
-  uploading-avatar: "Overfør en ny avatar"
-  avatar-updated: "Avatar er overført med succes"
-  choose-avatar: "Vælg et billede til din avatar"
-  unable-to-process: "Handlingen kunne ikke gennemføres."
-  invalid-filetype: "Denne filtype kan ikke benyttes her"
-desktop/views/components/activity.chart.vue:
-  total: "Sort ... Total"
-  notes: "Blå ... Noter"
-  replies: "Rød ... Svar"
-  renotes: "Grøn ... Gen-postering"
-desktop/views/components/activity.vue:
-  title: "Aktivitet"
-  toggle: "Skift mellem visninger"
-desktop/views/components/calendar.vue:
-  title: "{year} / {month}"
-  prev: "Forrige måned"
-  next: "Næste måned"
-  go: "Klik for at navigere"
-desktop/views/components/choose-file-from-drive-window.vue:
-  chosen-files: "{count} fil(er) er valgt"
-  upload: "Overfør filer fra din enhed"
-  cancel: "Annuller"
-  ok: "OK"
-  choose-prompt: "Vælg filer"
-desktop/views/components/choose-folder-from-drive-window.vue:
-  cancel: "Annuller"
-  ok: "OK"
-  choose-prompt: "Vælg en mappe"
-desktop/views/components/crop-window.vue:
-  skip: "Afbryd beskæring"
-  cancel: "Annuller"
-  ok: "OK"
-desktop/views/components/drive-window.vue:
-  used: "I brug"
-desktop/views/components/drive.file.vue:
-  avatar: "Avatar"
-  banner: "Banner"
-  nsfw: "Upassende PÃ¥ Jobbet"
-  contextmenu:
-    rename: "Omdøb"
-    mark-as-sensitive: "Marker som 'følsom'"
-    unmark-as-sensitive: "Fjern markering som 'følsom'"
-    copy-url: "Kopier webadresse"
-    download: "Download"
-    else-files: "Avanceret"
-    set-as-avatar: "Vælg som avatar"
-    set-as-banner: "Vælg som banner"
-    open-in-app: "Ã…bn i app"
-    add-app: "Tilføj app"
-    rename-file: "Omdøb fil"
-    input-new-file-name: "Angiv nyt navn"
-    copied: "Kopieret"
-    copied-url-to-clipboard: "Webadressen er kopieret til klippebordet"
-desktop/views/components/drive.folder.vue:
-  unable-to-process: "Handlingen kunne ikke gennemføres."
-  circular-reference-detected: "Destinationsmappen er en undermappe til den mappe, som du forsøger at flytte."
-  unhandled-error: "Ukendt fejl"
-  contextmenu:
-    move-to-this-folder: "Flyt til denne mappe"
-    show-in-new-window: "Ã…bn i nyt vindue"
-    rename: "Omdøb"
-    rename-folder: "Omdøb mappe"
-    input-new-folder-name: "Angiv nyt navn"
-    else-folders: "Avanceret"
-desktop/views/components/drive.vue:
-  search: "Søg"
-  empty-draghover: "Smid det her! Fordi du ved, at jeg er meget sød, ikke?"
-  empty-drive: "Dit medielager er tomt"
-  empty-drive-description: "Højreklik for at åbne menuen, eller træk og slip filer her for at overføre."
-  empty-folder: "Denne mappe er tom"
-  unable-to-process: "Handlingen kunne ikke gennemføres."
-  circular-reference-detected: "Destinationsmappen er en undermappe til den mappe, som du forsøger at flytte."
-  unhandled-error: "Ukendt fejl"
-  url-upload: "Overfør fra webadresse"
-  url-of-file: "Webadresse på filen, som du vil overføre"
-  url-upload-requested: "Der er anmodet om overførsel"
-  may-take-time: "Det kan tage noget tid at gennemføre overførslen."
-  create-folder: "Opret en mappe"
-  folder-name: "Mappenavn"
-  contextmenu:
-    create-folder: "Opret en mappe"
-    upload: "Overfør en fil"
-    url-upload: "Overfør fra webadresse"
-desktop/views/components/media-video.vue:
-  sensitive: "Indholdet er Upassende PÃ¥ Jobbet"
-  click-to-show: "Klik for at vise"
-desktop/views/components/followers-window.vue:
-  followers: "{}s følgere"
-desktop/views/components/followers.vue:
-  empty: "Det ser ikke ud til, at du har nogen følgere."
-desktop/views/components/following-window.vue:
-  following: "Følger {}"
-desktop/views/components/following.vue:
-  empty: "Det ser ikke ud til, at du følger brugeren..."
-desktop/views/components/game-window.vue:
-  game: "Reversi"
-desktop/views/components/home.vue:
-  done: "Send"
-  add-widget: "Tilføj widget:"
-  add: "Tilføj"
-desktop/views/input-dialog.vue:
-  cancel: "Annuller"
-  ok: "OK"
-desktop/views/components/note-detail.vue:
-  private: "Posten er privat"
-  deleted: "Posten er blevet fjernet"
-  location: "Placering"
-  renote: "Gen-postering"
-  add-reaction: "Tilføj en reaktion"
-  undo-reaction: "Fortryd reaktion"
-desktop/views/components/note.vue:
-  reply: "Svar"
-  renote: "Gen-postering"
-  add-reaction: "Tilføj en reaktion"
-  undo-reaction: "Fortryd reaktion"
-  detail: "Detaljer"
-  private: "Posten er privat"
-  deleted: "Posten er blevet fjernet"
-desktop/views/components/notes.vue:
-  error: "Fejl ved indlæsning"
-  retry: "Prøv igen"
-desktop/views/components/notifications.vue:
-  empty: "Der er ingen notifikationer!"
-desktop/views/components/post-form.vue:
-  posted: "Afsendt!"
-  replied: "Besvaret!"
-  reposted: "Gen-posteret!"
-  note-failed: "Fejl under afsendelse"
-  reply-failed: "Fejl under besvarelse"
-  renote-failed: "Fejl under gen-postering"
-desktop/views/components/post-form-window.vue:
-  note: "Ny post"
-  reply: "Svar"
-  attaches: "{} medier er vedhæftet"
-  uploading-media: "Har overført {} medier"
-desktop/views/components/progress-dialog.vue:
-  waiting: "Venter"
-desktop/views/components/renote-form.vue:
-  quote: "Citat"
-  cancel: "Annuller"
-  renote: "Gen-postering"
-  renote-home: "Gen-postering (på startsiden)"
-  reposting: "Gen-posterer"
-  success: "Gen-posteret!"
-  failure: "Fejl under gen-postering"
-desktop/views/components/renote-form-window.vue:
-  title: "Ønsker du at gen-postere den?"
-desktop/views/pages/user-following-or-followers.vue:
-  following: "{user} følger"
-  followers: "{user}s følger"
-desktop/views/components/settings.2fa.vue:
-  intro: "Du kan forbedre sikkerheden ved at indføre to-faktor godkendelse. I så fald vil du både få brug for en adgangskode til log ind og en fysisk enhed (f.eks. en smartphone), som er registreret på forhånd."
-  detail: "Mere info..."
-  url: "https://www.google.com/landing/2step/"
-  caution: "Hvis du mister adgangen til din registrerede enhed, vil du ikke længere være i stand til at koble dig på Misskey!"
-  register: "Registrer en enhed"
-  already-registered: "Denne enhed er allerede registreret"
-  unregister: "Af-registrer"
-  unregistered: "To-faktor godkendelse er de-aktiveret."
-  enter-password: "Angiv adgangskoden"
-  authenticator: "Allerførst skal du installere Google Authenticator på din enhed:"
-  howtoinstall: "SÃ¥dan installerer du"
-  token: "Nøgle"
-  scan: "Og derefter, scan QR koden:"
-  done: "Angiv den nøgle, som vises på din enhed:"
-  submit: "Send"
-  success: "Indstillingerne er gemt!"
-  failed: "Fejl ved opsætningen. Tjek at din nøgle er korrekt."
-  info: "Næste gang du logger på Misskey, vil den nøgle og adgangskode, der vises på din enhed, være obligatorisk."
-common/views/components/media-image.vue:
-  sensitive: "Indholdet er Upassende PÃ¥ Jobbet"
-  click-to-show: "Klik for at vise"
-common/views/components/api-settings.vue:
-  intro: "For at få adgang til API'en skal du sætte denne nøgle til at være søgeordet \"i\" i anmodningsparameteren."
-  caution: "Indtast ikke denne nøgle i nogen app, og fortæl heller ikke andre om den. I modsat fald kan din konto blive kompromitteret."
-  regeneration-of-token: "Hvis din nøgle er kompromitteret, kan du genskabe den."
-  regenerate-token: "Genskab nøgle"
-  token: "Nøgle:"
-  enter-password: "Angiv adgangskoden"
-  console:
-    title: "API konsol"
-    endpoint: "Endpoint"
-    parameter: "Parametre"
-    credential-info: "Denne konsol kræver ikke parameteren \"i\"."
-    send: "Send"
-    sending: "Sender"
-    response: "Svar"
-desktop/views/components/settings.apps.vue:
-  no-apps: "Der er ingen tilsluttede apps"
-common/views/components/drive-settings.vue:
-  max: "Kapacitet"
-  in-use: "I brug"
-  stats: "Statistik"
-  default-upload-folder-name: "Mappe(r)"
-common/views/components/mute-and-block.vue:
-  mute-and-block: "Annuller / Bloker"
-  mute: "Annuller"
-  block: "Bloker"
-  no-muted-users: "Ingen annullerede brugere"
-  no-blocked-users: "Ingen blokerede brugere"
-  word-mute: "Ordfilter"
-  muted-words: "Frafiltrerede ord"
-  muted-words-description: "Mellemrum mellem ord vil blive håndteret, som om der står AND i mellem ordene i søgningen (dvs. alle ord skal være til stede). Linjeskift mellem ord vil føre til, at der søges med OR mellem ordene (dvs. kun det ene af ordene behøver være til stede)."
-  unmute-confirm: "Er du sikker på, at du vil fjerne annulleringen af denne bruger?"
-  unblock-confirm: "Er du sikker på, at du vil fjerne blokeringen af denne bruger?"
-  save: "Gem"
-common/views/components/password-settings.vue:
-  reset: "Skift adgangskode"
-  enter-current-password: "Angiv den nuværende adgangskode"
-  enter-new-password: "Angiv den nye adgangskode"
-  enter-new-password-again: "Skriv den nye adgangskode igen"
-  not-match: "Du har skrevet den nye adgangskode på to forskellige måder"
-  changed: "Adgangskoden er ændret"
-  failed: "Fejl ved ændring af adgangskode"
-common/views/components/post-form-attaches.vue:
-  attach-cancel: "Fjern den vedhæftede fil"
-  mark-as-sensitive: "Marker som 'følsom'"
-  unmark-as-sensitive: "Fjern markering som 'følsom'"
-desktop/views/components/sub-note-content.vue:
-  private: "Posten er privat"
-  deleted: "Posten er blevet fjernet"
-  media-count: "{} medie(r) er vedhæftet"
-  poll: "Afstemninger"
-desktop/views/components/settings.tags.vue:
-  title: "Tags"
-  query: "Søgning (valgfri)"
-  add: "Tilføj"
-  save: "Gem"
-desktop/views/components/timeline.vue:
-  home: "Startside"
-  local: "Lokal"
-  hybrid: "Social"
-  global: "Global"
-  mentions: "Omtaler"
-  messages: "Direkte poster"
-  list: "Liste"
-  hashtag: "Hashtags"
-  add-tag-timeline: "Tilføj hashtag sky"
-  add-list: "Tilføj liste"
-  list-name: "Navn på liste"
-desktop/views/components/ui.header.vue:
-  welcome-back: "Velkommen tilbage!"
-  adjective: "Hr."
-desktop/views/components/ui.header.account.vue:
-  profile: "Din profil"
-  lists: "Lister"
-  groups: "Gruppe"
-  follow-requests: "Anmodninger om at blive følger"
-  admin: "Administration"
-desktop/views/components/ui.header.nav.vue:
-  game: "Spil"
-desktop/views/components/ui.header.notifications.vue:
-  title: "Notifikationer"
-desktop/views/components/ui.header.post.vue:
-  post: "Ny post"
-desktop/views/components/ui.header.search.vue:
-  placeholder: "Søg"
-desktop/views/components/user-preview.vue:
-  notes: "Poster"
-  following: "Følger"
-  followers: "Følgere"
-desktop/views/components/users-list.vue:
-  all: "Alle"
-  iknow: "Som du ved"
-  fetching: "Indlæser..."
-desktop/views/components/users-list-item.vue:
-  followed: "Følger dig"
-desktop/views/components/window.vue:
-  popout: "Pop op vindue"
-  close: "Luk"
-admin/views/index.vue:
-  dashboard: "Kontrolpanel"
-  instance: "Instans"
-  emoji: "Emoji"
-  moderators: "Redaktører"
-  users: "Brugere"
-  federation: "Forening"
-  announcements: "Annonceringer"
-  abuse: "Misbrug"
-  queue: "Job kø"
-  logs: "Logs"
-  back-to-misskey: "Tilbage til Misskey"
-admin/views/dashboard.vue:
-  dashboard: "Kontrolpanel"
-  accounts: "Konto"
-  notes: "Poster"
-  drive: "Drev"
-  instances: "Instans"
-  this-instance: "Denne instans"
-  federated: "Forenede"
-admin/views/queue.vue:
-  title: "Kø"
-  remove-all-jobs: "Ryd alle job køer"
-  queue: "Kø"
-  state: "Sorter efter"
-admin/views/logs.vue:
-  logs: "Logs"
-  levels:
-    info: "Information"
-    error: "Fejl"
-admin/views/abuse.vue:
-  title: "Misbrug"
-  target: "MÃ¥l"
-  reporter: "Kilde"
-  details: "Detaljer"
-  remove-report: "Slet"
-admin/views/instance.vue:
-  instance: "Instans"
-  instance-name: "Instans navn"
-  instance-description: "Beskrivelse af instans"
-  host: "Vært"
-  icon-url: "Ikonets webadresse"
-  logo-url: "Logoets webadresse"
-  banner-url: "Banner billedets webadresse"
-  error-image-url: "Fejl billedets webadresse"
-  languages: "Sproget på denne instans"
-  languages-desc: "Du kan angive flere ved at adskille med mellemrum"
-  tos-url: "Webadresse for brugerbetingelser"
-  repository-url: "Webadresse for systemets kode-repo"
-  feedback-url: "Webadresse for tilbagemeldinger om systemet"
-  maintainer-config: "Administrator info"
-  maintainer-name: "Administrator navn"
-  maintainer-email: "Kontakt administrator"
-  advanced-config: "Andre indstillinger"
-  note-and-tl: "Poster og tidslinje"
-  drive-config: "Indstillinger for drev"
-  use-object-storage: "Brug af eksternt lager"
-  object-storage-base-url: "Webadresse"
-  object-storage-bucket: "Navn på eksternt lager"
-  object-storage-prefix: "Præfiks"
-  object-storage-endpoint: "Endpoint"
-  object-storage-region: "Region"
-  object-storage-port: "Port"
-  object-storage-access-key: "Genvejstast"
-  object-storage-secret-key: "Nøgle"
-  object-storage-use-ssl: "Brug SSL"
-  object-storage-s3-info: "Når du bruger Amazon S3 som eksternt lager, skal du bekræfte indstillingerne for {0} samt den dertil hørende \"Terminal\" og \"Region\"."
-  object-storage-s3-info-here: "Her"
-  object-storage-gcs-info: "NÃ¥r du bruger Google Cloud Storage som eksternt lager, skal du indstille \"Terminal\" til storage.googleapis.com og forlade feltet \"Region\"."
-  cache-remote-files: "Cache eksterne filer"
-  local-drive-capacity-mb: "Kapacitet på hver lokal brugers drev"
-  remote-drive-capacity-mb: "Kapacitet på hver ekstern brugers drev"
-  mb: "I megabytes (MB)"
-  recaptcha-config: "Indstillinger for verificering"
-  recaptcha-info: "Du skal bruge en verificeringsnøgle for at aktivere verificering. Nøglen fås på https://www.google.com/recaptcha/intro/"
-  enable-recaptcha: "Aktiver verificering"
-  recaptcha-site-key: "Nøgle for webstedet"
-  recaptcha-secret-key: "Verificeringsnøgle"
-  recaptcha-preview: "Før-visning"
-  hidden-tags: "Skjulte hashtags"
-  hidden-tags-info: "Brug linjeskift til at adskille de hashtags, som du vil skjule."
-  external-service-integration-config: "Opret forbindelse til eksterne tjenester"
-  twitter-integration-config: "Indstillinger for forbindelse til Twitter"
-  twitter-integration-info: "Webadressen for callback er sat til {url}."
-  enable-twitter-integration: "Aktiver forbindelsen til Twitter"
-  twitter-integration-consumer-key: "Brugernøgle"
-  twitter-integration-consumer-secret: "Brugerhemmelighed"
-  github-integration-config: "Indstillinger for forbindelse til GitHub"
-  github-integration-info: "Webadressen for callback er sat til {url}."
-  enable-github-integration: "Aktiver forbindelsen til GitHub"
-  github-integration-client-id: "Bruger ID"
-  github-integration-client-secret: "Brugerhemmelighed"
-  discord-integration-config: "Indstillinger for forbindelse til Discord"
-  discord-integration-info: "Webadressen for callback er sat til {url}."
-  enable-discord-integration: "Aktiver forbindelsen til Discord"
-  discord-integration-client-id: "Bruger ID"
-  discord-integration-client-secret: "Brugerhemmelighed"
-  proxy-account-config: "Indstillinger for proxy-konto"
-  proxy-account-info: "En proxy-konto kan følge en ekstern brugers aktivitet og hente data fra vedkommende. Når du tilføjer en ekstern bruger, som ikke følges af nogen på denne instans, til din liste, vil proxy-kontoen følge ham eller hende i stedet for dine følgere."
-  proxy-account-username: "Brugernavn for proxy konto"
-  proxy-account-username-desc: "Angiv brugernavnet på den konto, der bliver brugt som proxy."
-  proxy-account-warn: "Du er nødt til at oprette en konto med dette brugernavn først."
-  max-note-text-length: "Det højeste antal tegn pr. post"
-  disable-registration: "Deaktiver oprettelse af nye brugere"
-  disable-local-timeline: "Deaktiver den lokale tidslinje"
-  disable-global-timeline: "Deaktiver den globale tidslinje"
-  disabling-timelines-info: "Administratorer og redaktører kan stadig bruge en tidslinje, selv om den er deaktiveret."
-  enable-emoji-reaction: "Aktiver piktogram med reaktioner"
-  use-star-for-reaction-fallback: "Fald tilbage på en stjerne, hvis der kommer en ukendt reaktion."
-  invite: "Inviter"
-  save: "Gem"
-  saved: "Gemt"
-  pinned-users: "Fremhæv bruger"
-  pinned-users-info: "Angiv brugere, du vil fremhæve, adskilt af linjeskift."
-  email-config: "Indstillinger for email server"
-  email-config-info: "Bruges til bekræftelse af email adresse og nulstilling af adgangskode."
-  enable-email: "Aktiver levering af emails"
-  email: "Email adresse"
-  smtp-secure: "Brug implicit SSL / TLS til SMTP-forbindelsen"
-  smtp-secure-info: "Sluk STARTTLS, når du bruger den."
-  smtp-host: "SMTP vært"
-  smtp-port: "SMTP port"
-  smtp-auth: "Udfør autentifikation af SMTP"
-  smtp-user: "SMTP bruger"
-  smtp-pass: "SMTP adgangskode"
-  test-email: "Test"
-  serviceworker-config: "ServiceWorker"
-  enable-serviceworker: "Aktiver ServiceWorker"
-  serviceworker-info: "Skal være aktiveret for at give push notifikationer."
-  vapid-publickey: "Offentlig nøgle til VAPID"
-  vapid-privatekey: "Privat nøgle til VAPID"
-  vapid-info: "Hvis du vil aktivere ServiceWorker, skal du generere en VAPID-nøgle. Medmindre du har angivet den globale node_modules placering andetsteds, skal du køre den som root:"
-admin/views/charts.vue:
-  title: "Graf"
-  per-day: "pr. dag"
-  per-hour: "pr. time"
-  federation: "Forening"
-  notes: "Poster"
-  users: "Brugere"
-  drive: "Drev"
-  network: "Netværk"
-  charts:
-    federation-instances: "Sæt antallet af instanser op eller ned"
-    federation-instances-total: "Antal instanser i alt"
-    notes: "Fremgang eller tilbagegang i antal poster (lokale + eksterne)"
-    local-notes: "Fremgang eller tilbagegang i antal poster (lokale)"
-    remote-notes: "Fremgang eller tilbagegang i antal poster (eksterne)"
-    notes-total: "Antal poster i alt"
-    users: "Fremgang eller tilbagegang i antal brugere"
-    users-total: "Antal brugere i alt"
-    active-users: "Aktive brugere"
-    drive: "Fremgang eller tilbagegang i brugen af drevet"
-    drive-total: "Forbrugt plads på drevet i alt"
-    drive-files: "Fremgang eller tilbagegang i antal filer på drevet"
-    drive-files-total: "Antal filer i alt på drevet"
-    network-requests: "Netværkskald"
-    network-time: "Svartid"
-    network-usage: "Trafik"
-admin/views/drive.vue:
-  operation: "Drift"
-  fileid-or-url: "Fil ID eller webadresse"
-  file-not-found: "Filen kunne ikke findes"
-  lookup: "Forespørgsel"
-  sort:
-    title: "Sorter efter"
-    createdAtAsc: "Alder - ældste først"
-    createdAtDesc: "Alder - seneste først"
-    sizeAsc: "Størrelse - mindste først"
-    sizeDesc: "Størrelse - største først"
-  origin:
-    title: "Oprindelse"
-    combined: "Lokal + ekstern"
-    local: "Lokal"
-    remote: "Ekstern"
-  delete: "Slet"
-  deleted: "Slettet"
-  mark-as-sensitive: "Marker som 'følsom'"
-  unmark-as-sensitive: "Fjern markering som 'følsom'"
-  marked-as-sensitive: "Marker som 'følsom'"
-  unmarked-as-sensitive: "Fjern markering som 'følsom'"
-admin/views/users.vue:
-  operation: "Drift"
-  username-or-userid: "Brugernavn eller bruger ID"
-  user-not-found: "Bruger kunne ikke findes"
-  lookup: "Opslag"
-  reset-password: "Nulstil adgangskode"
-  reset-password-confirm: "Er du sikker på, at du vil nulstille din adgangskode?"
-  password-updated: "Adgangskoden er nu \"{password}\""
-  suspend: "Udeluk"
-  suspend-confirm: "Er du sikker på, at du vil udelukke denne konto?"
-  suspended: "Udelukket med succes"
-  unsuspend: "Annuller udelukkelse"
-  unsuspend-confirm: "Er du sikker på, at du vil annullere udelukkelsen på denne konto?"
-  unsuspended: "Brugerens udelukkelse er annulleret med succes"
-  make-silence: "Gør tavs"
-  silence-confirm: "Vil du gøre brugeren tavs?"
-  unmake-silence: "Vil du annullere, at brugeren er gjort tavs?"
-  unsilence-confirm: "Er du sikker på, at du vil omgøre, at brugeren er gjort tavs?"
-  update-remote-user: "Opdater informationen om den eksterne bruger"
-  remote-user-updated: "Informationen om den eksterne bruger er nu blevet opdateret."
-  delete-all-files: "Slet alle filer"
-  delete-all-files-confirm: "Er du sikker på, at alle filerne skal slettes?"
-  username: "Brugernavn"
-  host: "Vært"
-  users:
-    title: "Bruger"
-    sort:
-      title: "Sorter efter"
-      createdAtAsc: "Tidspunkt for oprettelse (ældste først)"
-      createdAtDesc: "Tidspunkt for oprettelse (seneste først)"
-      updatedAtAsc: "Tidspunkt for seneste opdatering (ældste først)"
-      updatedAtDesc: "Tidspunkt for seneste opdatering (seneste først)"
-    state:
-      title: "Sorter efter"
-      all: "Alle"
-      admin: "Administrator"
-      moderator: "Redaktører"
-      adminOrModerator: "Administrator/Redaktør"
-      silenced: "Brugeren er i forvejen gjort tavs"
-      suspended: "Udelukket"
-    origin:
-      title: "Oprindelse"
-      combined: "Lokal + Ekstern"
-      local: "Lokal"
-      remote: "Ekstern"
-    createdAt: "Oprettet den"
-    updatedAt: "Opdateret den"
-admin/views/moderators.vue:
-  add-moderator:
-    title: "Opret redaktør"
-    add: "Opret"
-    added: "Redaktør er oprettet"
-    remove: "Fjern"
-    removed: "Redaktøren er nu fjernet"
-  logs:
-    title: "Logs"
-    moderator: "Redaktører"
-    type: "Drift"
-    info: "Information"
-admin/views/emoji.vue:
-  add-emoji:
-    title: "Tilføj emoji"
-    name: "Navn på emoji"
-    name-desc: "Du kan bruge tegnene fra a til z, 0 til 9 og \"_\""
-    aliases: "Aliasser"
-    aliases-desc: "Du kan tilføje flere med mellemrum imellem"
-    url: "Webadresse for billede"
-    add: "Tilføj"
-    info: "Vi anbefaler PNG billeder under 50 KB."
-    added: "Emoji er tilføjet"
-  emojis:
-    title: "Emojis"
-    update: "Opdatering"
-    remove: "Slet"
-  updated: "Opdateret"
-  remove-emoji:
-    are-you-sure: "Er du sikker på, at du vil slette \"$1\"?"
-    removed: "Slettet"
-admin/views/announcements.vue:
-  announcements: "Annonceringer"
-  save: "Gem"
-  remove: "Slet"
-  add: "Tilføj"
-  title: "Titel"
-  text: "Indhold"
-  saved: "Gemt"
-  _remove:
-    are-you-sure: "Er du sikker på, at du vil slette \"$1\"?"
-    removed: "Slettet"
-admin/views/hashtags.vue:
-  hided-tags: "Skjulte tags"
-admin/views/federation.vue:
-  instance: "Instans"
-  host: "Vært"
-  notes: "Poster"
-  users: "Brugere"
-  following: "Følger"
-  followers: "Følgere"
-  caught-at: "Oprettet den"
-  status: "Status"
-  latest-request-sent-at: "Tidspunkt for afsendelse af seneste forespørgsel"
-  latest-request-received-at: "Seneste forespørgsel blev modtaget den"
-  remove-all-following: "Annuller alle følgere"
-  remove-all-following-info: "Annuller alle konti fra værten {host}. Udføres når værten ikke længere eksisterer."
-  delete-all-files: "Slet alle filer"
-  block: "Bloker"
-  marked-as-closed: "Marker som lukket"
-  lookup: "Opslag"
-  instances: "Forenede"
-  instance-not-registered: "Instansen kan ikke findes"
-  sort: "Sorter efter"
-  sorts:
-    caughtAtAsc: "Tidspunkt for oprettelse (ældste først)"
-    caughtAtDesc: "Tidspunkt for oprettelse (seneste først)"
-    lastCommunicatedAtAsc: "Tidspunktet for den tidligere interaktion"
-    lastCommunicatedAtDesc: "Tidspunktet for den senere interaktion"
-    notesAsc: "Poster (mindste først)"
-    notesDesc: "Poster (største først)"
-    usersAsc: "Antal brugere (færrest først)"
-    usersDesc: "Antal brugere (flest først)"
-    followingAsc: "Antal følgere (færrest først)"
-    followingDesc: "Antal følgere (flest først)"
-    followersAsc: "Antal følgere (færrest først)"
-    followersDesc: "Antal følgere (flest først)"
-    driveUsageAsc: "Diskforbrug (mindst først)"
-    driveUsageDesc: "Diskforbrug (størst først)"
-    driveFilesAsc: "Antal filer på drev (færrest først)"
-    driveFilesDesc: "Antal filer på drev (flest først)"
-  state: "Sorter efter"
-  states:
-    all: "Alle"
-    blocked: "Bloker"
-    not-responding: "Uden svar"
-    marked-as-closed: "Markeret som lukkede"
-  result-is-truncated: "Vis de øverste {n} elementer."
-  charts: "Graf"
-  chart-srcs:
-    requests: "Netværkskald"
-    users: "Fremgang eller tilbagegang i antal brugere"
-    users-total: "Antal brugere i alt"
-    notes: "Sæt antallet af poster op eller ned"
-    notes-total: "Antal poster i alt"
-    ff: "Stigning i antal følgere"
-    ff-total: "Det samlede antal følgere"
-    drive-usage: "Fremgang eller tilbagegang i brugen af drevet"
-    drive-usage-total: "Forbrugt plads på drevet i alt"
-    drive-files: "Sæt antallet af filer på drevet op eller ned"
-    drive-files-total: "Samlet antal filer på drev"
-  chart-spans:
-    hour: "pr. time"
-    day: "pr. dag"
-  blocked-hosts: "Bloker"
-  blocked-hosts-info: "Beskrivelse af værterne du vil blokere, adskilt af linjeskift."
-  save: "Gem"
-desktop/views/pages/welcome.vue:
-  about: "Mere info..."
-  timeline: "Tidslinje"
-  announcements: "Annonceringer"
-  photos: "Seneste billeder"
-  powered-by-misskey: "Leveret af <b>Misskey</b>."
-  info: "Information"
-desktop/views/pages/drive.vue:
-  title: "Misskey drev"
-desktop/views/pages/note.vue:
-  prev: "Forrige post"
-  next: "Næste post"
-desktop/views/pages/selectdrive.vue:
-  title: "Vælg fil(er)"
-  ok: "OK"
-  cancel: "Annuller"
-  upload: "Overfør filer fra din enhed"
-desktop/views/pages/search.vue:
-  not-available: "Søgefunktionen er deaktiverede i indstillingerne for denne instans."
-  not-found: "Ingen poster fundet for \"{q}\""
-desktop/views/pages/tag.vue:
-  no-posts-found: "Ingen poster fundet med \"{q}\"."
-desktop/views/pages/user-list.users.vue:
-  users: "Bruger"
-  add-user: "Tilføj en bruger"
-  username: "Brugernavn"
-desktop/views/pages/user/user.followers-you-know.vue:
-  title: "Følgere som du måske kender"
-  loading: "Henter"
-  no-users: "Ingen følgere du kender"
-desktop/views/pages/user/user.friends.vue:
-  title: "Hyppige omtaler"
-  loading: "Henter"
-  no-users: "Ingen hyppige omtaler"
-desktop/views/pages/user/user.photos.vue:
-  title: "Billeder"
-  loading: "Henter"
-  no-photos: "Ingen billeder"
-desktop/views/pages/user/user.header.vue:
-  posts: "Poster"
-  following: "Følger"
-  followers: "Følgere"
-  is-bot: "Denne konto er en bot"
-  no-description: "Ingen brugerbeskrivelse"
-  years-old: "{age} år"
-  year: "-"
-  month: "Man"
-  day: "Søn"
-  follows-you: "Følger dig"
-desktop/views/pages/user/user.timeline.vue:
-  default: "Poster"
-  with-replies: "Poster og svar"
-  with-media: "Medier"
-  my-posts: "Mine poster"
-desktop/views/widgets/notifications.vue:
-  title: "Notifikationer"
-desktop/views/widgets/polls.vue:
-  title: "Afstemninger"
-  refresh: "Genopfrisk"
-  nothing: "Der er ingen notifikationer!"
-desktop/views/widgets/post-form.vue:
-  title: "Post"
-  note: "Post"
-  something-happened: "Kunne af uvisse årsager ikke postes"
-desktop/views/widgets/profile.vue:
-  update-banner: "Klik for at redigere dit banner"
-  update-avatar: "Klik for at redigere din avatar"
-desktop/views/widgets/trends.vue:
-  title: "Tendenser"
-  refresh: "Genopfrisk"
-  nothing: "Der er ingen notifikationer!"
-desktop/views/widgets/users.vue:
-  title: "Anbefalede brugere"
-  refresh: "Genopfrisk"
-  no-one: "Hvad synes du?"
-mobile/views/components/drive.vue:
-  used: "I brug"
-  folder-count: "Mappe(r)"
-  count-separator: ","
-  file-count: "Fil(er)"
-  nothing-in-drive: "Der er ikke gemt noget"
-  folder-is-empty: "Denne mappe er tom"
-  folder-name: "Mappenavn"
-  here-is-root: "Lige nu befinder du dig i bunden - ikke i en mappe."
-  url-prompt: "Webadresse på filen, som du vil overføre"
-  uploading: "Overførslen er sat i gang. Det kan tage et stykke tid."
-  folder-name-cannot-empty: "Et mappe er nødt til at have et navn"
-mobile/views/components/drive-file-chooser.vue:
-  select-file: "Vælg fil"
-mobile/views/components/drive-folder-chooser.vue:
-  select-folder: "Vælg mappe"
-mobile/views/components/drive.file.vue:
-  nsfw: "Indholdet er Upassende PÃ¥ Jobbet"
-mobile/views/components/drive.file-detail.vue:
-  download: "Download"
-  rename: "Omdøb"
-  move: "Flyt"
-  hash: "Hashtag (md5)"
-  exif: "EXIF"
-  nsfw: "Indholdet er Upassende PÃ¥ Jobbet"
-  mark-as-sensitive: "Marker som 'følsom'"
-  unmark-as-sensitive: "Fjern markering som 'følsom'"
-mobile/views/components/media-video.vue:
-  sensitive: "Indholdet er Upassende PÃ¥ Jobbet"
-  click-to-show: "Klik for at vise"
-common/views/components/follow-button.vue:
-  following: "Følger"
-  follow: "Følger"
-  request-pending: "Ventende anmodninger om at blive følger"
-  follow-processing: "Anmoder om behandling"
-  follow-request: "Anmodninger om at blive følger"
-mobile/views/components/note.vue:
-  private: "Posten er privat"
-  deleted: "Posten er blevet fjernet"
-  location: "Placering"
-mobile/views/components/note-detail.vue:
-  reply: "Svar"
-  reaction: "Reaktion"
-  private: "Posten er privat"
-  deleted: "Posten er blevet fjernet"
-  location: "Placering"
-mobile/views/components/note-preview.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "cat"
-mobile/views/components/note-sub.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "cat"
-mobile/views/components/notifications.vue:
-  empty: "Der er ingen notifikationer!"
-mobile/views/components/sub-note-content.vue:
-  private: "Posten er privat"
-  deleted: "Posten er blevet fjernet"
-  media-count: "{} medie(r) er vedhæftet"
-  poll: "Afstemninger"
-mobile/views/components/ui.header.vue:
-  welcome-back: "Velkommen tilbage!"
-  adjective: "Hr."
-mobile/views/components/ui.nav.vue:
-  timeline: "Tidslinje"
-  notifications: "Notifikationer"
-  follow-requests: "Anmodninger om at blive følger"
-  search: "Søg"
-  user-lists: "Lister"
-  user-groups: "Gruppe"
-  widgets: "Widgets"
-  game: "Spil"
-  admin: "Administration"
-  about: "Om"
-mobile/views/pages/drive.vue:
-  contextmenu:
-    upload: "Overfør en fil"
-    url-upload: "Overfør fil fra webadresse"
-    create-folder: "Opret en mappe"
-    rename-folder: "Omdøb mappe"
-    move-folder: "Flyt denne mappe"
-    delete-folder: "Slet denne mappe"
-mobile/views/pages/signup.vue:
-  lets-start: "Din konto er nu klar! 📦"
-mobile/views/pages/followers.vue:
-  followers-of: "{name}s følgere"
-mobile/views/pages/following.vue:
-  following-of: "{name}s følger"
-mobile/views/pages/home.vue:
-  home: "Startside"
-  local: "Lokal"
-  hybrid: "Social"
-  global: "Global"
-  mentions: "Omtaler"
-  messages: "Direkte poster"
-mobile/views/pages/tag.vue:
-  no-posts-found: "Ingen poster fundet med \"{q}\"."
-mobile/views/pages/widgets.vue:
-  dashboard: "Kontrolpanel"
-  widgets-hints: "Du kan tilføje / slette / flytte rundt på widgets. For at flytte widgeten skal du trække “三”. Klik på \"×\" for at slette widgeten. Nogle widgets kan tilpasses direkte ved at klikke på dem."
-  add-widget: "Tilføj"
-  customization-tips: "Tips om tilpasning"
-mobile/views/pages/widgets/activity.vue:
-  activity: "Aktivitet"
-mobile/views/pages/share.vue:
-  share-with: "Del med {name}"
-mobile/views/pages/note.vue:
-  title: "Post"
-  prev: "Forrige post"
-  next: "Næste post"
-mobile/views/pages/games/reversi.vue:
-  reversi: "Reversi"
-mobile/views/pages/search.vue:
-  search: "Søg"
-  not-found: "Ingen poster fundet for \"{q}\""
-mobile/views/pages/selectdrive.vue:
-  select-file: "Vælg fil(er)"
-mobile/views/pages/notifications.vue:
-  notifications: "Notifikationer"
-mobile/views/pages/settings.vue:
-  signed-in-as: "Logget ind som {}"
-mobile/views/pages/user.vue:
-  follows-you: "Følger dig"
-  following: "Følger"
-  followers: "Følgere"
-  notes: "Poster"
-  overview: "Oversigt"
-  timeline: "Tidslinje"
-  media: "Medier"
-  years-old: "{age} år"
-mobile/views/pages/user/home.vue:
-  recent-notes: "Seneste poster"
-  images: "Billeder"
-  activity: "Aktivitet"
-  keywords: "Nøgleord"
-  domains: "Domæner"
-  frequently-replied-users: "Hyppige omtaler"
-  followers-you-know: "Følgere som du måske kender"
-  last-used-at: "Senest aktiv:"
-mobile/views/pages/user/home.photos.vue:
-  no-photos: "Ingen billeder"
-deck:
-  widgets: "Widgets"
-  home: "Startside"
-  local: "Lokal"
-  hybrid: "Social"
-  hashtag: "Hashtags"
-  global: "Global"
-  mentions: "Omtaler"
-  direct: "Direkte poster"
-  notifications: "Notifikationer"
-  list: "Lister"
-  select-list: "Vælg liste"
-  swap-left: "Flyt til venstre"
-  swap-right: "Flyt til højre"
-  swap-up: "Flyt op"
-  swap-down: "Flyt ned"
-  remove: "Fjern"
-  add-column: "Tilføj en kolonne"
-  rename: "Omdøb"
-  stack-left: "Fold sammen til venstre"
-  pop-right: "Parker til højre"
-  disabled-timeline:
-    title: "Tidslinjen er deaktiveret"
-    description: "Tidslinjen er deaktiveret af serverens administrator"
-deck/deck.tl-column.vue:
-  is-media-only: "Poster med medieindhold"
-  edit: "Valgmuligheder"
-deck/deck.user-column.vue:
-  follows-you: "Følger dig"
-  posts: "Poster"
-  following: "Følger"
-  followers: "Følgere"
-  images: "Billeder"
-  activity: "Aktivitet"
-  timeline: "Tidslinje"
-  pinned-notes: "Fremhævede poster"
-docs:
-  edit-this-page-on-github: "Har du fundet en fejl, eller ønsker at bidrage til dokumentationen?"
-  edit-this-page-on-github-link: "Rediger denne side på GitHub"
-dev/views/index.vue:
-  manage-apps: "Administrer apps"
-dev/views/apps.vue:
-  manage-apps: "Administrer apps"
-  create-app: "Opret app"
-  app-missing: "Ingen apps"
-dev/views/new-app.vue:
-  new-app: "Ny app"
-  new-app-info: "Du kan også oprette en app fra API'en. (app / opret)"
-  create-app: "Opretter app"
-  app-name: "Navn på app"
-  app-name-placeholder: "F.eks. Misskey for iOS"
-pages:
-  pin-this-page: "Tilknyt til din profil"
-  unpin-this-page: "Fjern tilknytning til din profil"
-  like: "Synes om"
-  title: "Titel"
-  blocks:
-    section: "Sektion"
-    image: "Billeder"
-    button: "Knap"
-    if: "Hvis"
-    _if:
-      variable: "Variabel"
-    post: "Post formular"
-    _post:
-      text: "Indhold"
-    textInput: "Indtastet tekst"
-    _textInput:
-      name: "Variabel navn"
-      text: "Titel"
-      default: "Standard værdi"
-    textareaInput: "Tekst med flere linjer"
-    _textareaInput:
-      name: "Variabel navn"
-      text: "Titel"
-      default: "Standard værdi"
-    numberInput: "Indtastet tal"
-    _numberInput:
-      name: "Variabel navn"
-      text: "Titel"
-      default: "Standard værdi"
-    switch: "Kontakt"
-    _switch:
-      name: "Variabel navn"
-      text: "Titel"
-      default: "Standard værdi"
-    counter: "Tæller"
-    _counter:
-      name: "Variabel navn"
-      text: "Titel"
-      inc: "Forhøj tallet med en"
-    _button:
-      text: "Titel"
-      action: "Handling når der trykkes på knappen"
-      _action:
-        dialog: "Vis dialogboks"
-        _dialog:
-          content: "Indhold"
-        resetRandom: "Nulstil tilfældigt tal"
-    _radioButton:
-      name: "Variabel navn"
-      title: "Titel"
-      default: "Standard værdi"
-  script:
-    categories:
-      flow: "Kontrol"
-      logical: "Logisk handling"
-      operation: "Eksekver"
-      comparison: "Sammenlign"
-      random: "Tilfældig"
-      value: "Værdi"
-      fn: "Funktion"
-      text: "Tekstmanipulation"
-      convert: "Konverter"
-      list: "Lister"
-    blocks:
-      text: "Text"
-      multiLineText: "Tekst med linjeskift"
-      textList: "Punktliste"
-      _textList:
-        info: "Brug linjeskift til at adskille linjerne"
-      strLen: "Længde af tekststrengen"
-      _strLen:
-        arg1: "Tekst"
-      strPick: "Udtræk et tegn"
-      _strPick:
-        arg1: "Text"
-        arg2: "Tegnets position"
-      strReplace: "Erstat tekststreng(e)"
-      _strReplace:
-        arg1: "Tekst"
-        arg2: "Før søg-og-erstat"
-        arg3: "Efter søg-og-erstat"
-      strReverse: "Vend teksten rundt"
-      _strReverse:
-        arg1: "Tekst"
-      join: "Sammenflet tekst"
-      _join:
-        arg1: "Lister"
-        arg2: "Skilletegn"
-      add: "+ Plus"
-      _add:
-        arg1: "A"
-        arg2: "B"
-      subtract: "- Minus"
-      _subtract:
-        arg1: "A"
-        arg2: "B"
-      multiply: "× Multiplicer"
-      _multiply:
-        arg1: "A"
-        arg2: "B"
-      divide: "÷ Divider"
-      _divide:
-        arg1: "A"
-        arg2: "B"
-      _mod:
-        arg1: "A"
-        arg2: "B"
-      _round:
-        arg1: "Tal"
-      eq: "A og B er ens"
-      _eq:
-        arg1: "A"
-        arg2: "B"
-      notEq: "A og B er forskellige"
-      _notEq:
-        arg1: "A"
-        arg2: "B"
-      and: "A og B"
-      _and:
-        arg1: "A"
-        arg2: "B"
-      or: "A eller B"
-      _or:
-        arg1: "A"
-        arg2: "B"
-      lt: "A er mindre end B"
-      _lt:
-        arg1: "A"
-        arg2: "B"
-      gt: "A er større end B"
-      _gt:
-        arg1: "A"
-        arg2: "B"
-      ltEq: "A er mindre end eller lig med B"
-      _ltEq:
-        arg1: "A"
-        arg2: "B"
-      gtEq: "A er større end eller lig med B"
-      _gtEq:
-        arg1: "A"
-        arg2: "B"
-      if: "Branch"
-      _if:
-        arg1: "Hvis"
-        arg2: "så"
-        arg3: "ellers"
-      not: "negation"
-      _not:
-        arg1: "negation"
-      random: "Tilfældig"
-      _random:
-        arg1: "Sandsynlighed"
-      rannum: "Tilfældigt tal"
-      _rannum:
-        arg1: "Minimum"
-        arg2: "Maximum"
-      randomPick: "Vælg tilfældig fra listen"
-      _randomPick:
-        arg1: "Lister"
-      dailyRandom: "Tilfældigt (pr. bruger pr. dag)"
-      _dailyRandom:
-        arg1: "Sandsynlighed"
-      dailyRannum: "Tilfældigt antal (pr. bruger pr. dag)"
-      _dailyRannum:
-        arg1: "Minimum"
-        arg2: "Maximum"
-      dailyRandomPick: "Tilfældigt valgt fra listen (pr. bruger pr. dag)"
-      _dailyRandomPick:
-        arg1: "Lister"
-      seedRandom: "Tilfældig (frø)"
-      _seedRandom:
-        arg1: "Frø"
-        arg2: "Sandsynlighed"
-      seedRannum: "Tilfældigt antal (frø)"
-      _seedRannum:
-        arg1: "Frø"
-        arg2: "Minimum"
-        arg3: "Maximum"
-      seedRandomPick: "Vælg tilfældigt (frø) fra listen"
-      _seedRandomPick:
-        arg1: "Frø"
-        arg2: "Lister"
-      DRPWPM: "Tilfældigt valgt fra sandsynlighedslisten (pr. bruger pr. dag)"
-      _DRPWPM:
-        arg1: "Punktliste"
-      pick: "Vælg fra listen"
-      _pick:
-        arg1: "Lister"
-        arg2: "Position"
-      _listLen:
-        arg1: "Lister"
-      number: "Tal"
-      stringToNumber: "Tekst til tal"
-      _stringToNumber:
-        arg1: "Tekst"
-      numberToString: "Tal til tekst"
-      _numberToString:
-        arg1: "Tal"
-      splitStrByLine: "Del teksten op i linjer"
-      _splitStrByLine:
-        arg1: "Tekst"
-      ref: "Variabel"
-      fn: "Funktion"
-      _fn:
-        slots: "Slot-funktion"
-        slots-info: "Brug linjeskift til at adskille slot-funktionerne"
-        arg1: "Output"
-      for: "Gentag"
-      _for:
-        arg1: "Antal gange"
-        arg2: "Proces"
-    typeError: "Slot-funktionen {slot} skal gennemløbe i \"{expect}\", men den faktiske indtastning er \"{actual}\"!"
-    thereIsEmptySlot: "Slot-funktionen {slot} er tom!"
-    types:
-      string: "Tekst"
-      number: "Tal"
-      boolean: "Sand/falsk"
-      array: "Lister"
-      stringArray: "Punktliste"
-    emptySlot: "Tomt slot"
-    enviromentVariables: "Miljø variabel"
-    pageVariables: "Side element"
-    argVariables: "Input slot"
-room:
-  translate: "Flyt"
-  save: "Gem"
-  saved: "Gemt"
-  furnitures:
-    moon: "MÃ¥ne"
-    bin: "Skraldespand"
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
deleted file mode 100644
index db3daf5246716b874da341167180eb4a66cb832e..0000000000000000000000000000000000000000
--- a/locales/de-DE.yml
+++ /dev/null
@@ -1,954 +0,0 @@
----
-meta:
-  lang: "Deutsch"
-common:
-  misskey: "Ein ⭐ des Fediversums"
-  about-title: "Ein ⭐ des Fediversums."
-  about: "Danke, dass Du Misskey gefunden hast. Misskey ist eine <b>dezentralisierte Microblogging-Plattform</b>, welche auf der ganzen Welt verteilt ist. Da es innerhalb es Fediversums existiert (ein Universum, in dem verschiedene Soziale Netzwerke organisiert sind), ist es unmittelbar mit anderen sozialen Netzwerken verbunden. Warum nimmst du dir nicht einmal eine Auszeit von dem Trubel der Stadt und tauchst in das neue Internet hinein?"
-  intro:
-    title: "Was ist Misskey?"
-    about: "Misskey ist eine Quelloffene, <b>dezentralisierte microblogging Software</b>. Es bietet eine erweiterbare Benutzeroberfläche, verschiedenste Möglichkeiten auf Beiträge zu reagieren, kostenlosen Datenspeicher, und andere fortschrittliche Funktionen. Zusätzlich ist Misskey dazu in der Lage, sich mittels des Fediverse mit beliebig vielen anderen ActivityPub-kompatiblen Diensten zu verbinden. Wenn du zum Beispiel einen Betrag mit Misskey abschickst, wird dieser auch für Nutzer von Mastodon oder Pleroma sichtbar sein. So ähnlich wie eine Radioübertragung zwischen Planeten."
-    features: "Funktionen"
-    rich-contents: "Notizen"
-    rich-contents-desc: "Poste einfach deine Ideen, Interessen und alles, was du teilen möchtest. Gestalte deine Nachrichten, teile deine Lieblingsbilder, sende Dateien und Videos und erstelle Umfragen – das und mehr kann Misskey!"
-    reaction: "Reaktionen"
-    reaction-desc: "Der einfachste Weg, deine Gefühle mit anderen zu teilen. Mit Misskey kannst du auf verschiedenste Arten auf Beiträge reagieren, statt nur zu „liken“."
-    ui: "Benutzeroberfläche"
-    ui-desc: "Geschmäcker sind verschieden. Deswegen ist Misskeys Oberfläche hochanpassbar und modular. Mache die Startseite zu deiner Startseite, indem du das Layout deiner Timeline änderst und mit Widgets staffierst."
-    drive: "Drive"
-    drive-desc: "Du willst ein hochgeladenes Foto nochmal posten? Deine Dateien benennen und in Ordnern sortieren? Misskeys Drive ist der beste Ort dafür. Damit wird das Teilen zum Kinderspiel!"
-    outro: "Probiere Misskey aus und entdecke Misskeys einzigartige Funktionen. Wenn dir diese Instanz nicht zusagt, nimm einfach eine andere. Dank Misskeys dezentralem System kannst du dich überall mit deinen Freunden verbinden. Also dann, GLHF!"
-  application-authorization: "Autorisierte Anwendungen"
-  close: "Schließen"
-  do-not-copy-paste: "Bitte keinen Code einfügen. Ihr Account könnte gefährdet werden."
-  load-more: "Mehr laden"
-  enter-password: "Bitte Passwort eingeben"
-  2fa: "Zwei-Faktor-Authentifizierung"
-  customize-home: "Layout Anpassen"
-  featured-notes: "Beliebt"
-  dark-mode: "Dunkler Modus"
-  signin: "Einloggen"
-  signup: "Registrieren"
-  signout: "Ausloggen"
-  reload-to-apply-the-setting: "Die Seite muss zum Ãœbernehmen dieser Einstellung aktualisiert werden. Soll die Seite jetzt neu geladen werden?"
-  fetching-as-ap-object: "Hole Daten…"
-  delete-confirm: "Diesen Beitrag löschen?"
-  notification-types:
-    reply: "Antworten"
-    renote: "Anmerkung"
-  got-it: "Verstanden!"
-  customization-tips:
-    title: "Anpassung-Tipps"
-    paragraph: "<p>Du kannst deine Startseite anpassen, indem du Widgets hinzufügst und verschiebst.</p><p><strong>Klicke <strong>rechts</strong></strong> auf ein Widget, um dessen Aussehen zu verändern.</p><p>Um ein Widget zu löschen, klicke und ziehe es auf den <strong>Papierkorb</strong> am Kopfende der Seite.</p><p>Wenn du fertig bist, drücke auf den Beenden-Knopf oben rechts.</p>"
-    gotit: "Verstanden!"
-  notification:
-    file-uploaded: "Datei hochgeladen!"
-    message-from: "Nachricht von {}:"
-    reversi-invited: "Zu einem Spiel eingeladen"
-    reversi-invited-by: "Eingeladen von {}:"
-    notified-by: "Benachrichtigt von {}:"
-    reply-from: "Antwort von {}:"
-    quoted-by: "Zitiert von {}:"
-  time:
-    unknown: "Unbekannt"
-    future: "Zukunft"
-    just_now: "Gerade eben"
-    seconds_ago: "vor {} Sekunde(n)"
-    minutes_ago: "vor {} Minute(n)"
-    hours_ago: "vor {} Stunde(n)"
-    days_ago: "vor {} Tag(en)"
-    weeks_ago: "vor {} Woche(n)"
-    months_ago: "vor {} Monat(en)"
-    years_ago: "vor {} Jahr(en)"
-  month-and-day: "{day}/{month}"
-  trash: "Papierkorb"
-  drive: "Drive"
-  pages: "Seite"
-  messaging: "Unterhaltungen"
-  home: "Home"
-  deck: "Deck"
-  timeline: "Zeitleiste"
-  explore: "Entdecken"
-  following: "Folgt"
-  followers: "Folgende"
-  favorites: "Favoriten"
-  permissions:
-    "read:account": "Accountinformationen anzeigen."
-    "write:account": "Accountinformationen bearbeiten."
-    "read:blocks": "Blöcke anzeigen"
-    "write:blocks": "Auf Sperrungen zugreifen"
-    "read:drive": "Dateien anzeigen"
-    "write:drive": "Dateien bearbeiten"
-    "read:favorites": "Favoriten anzeigen"
-    "write:favorites": "Auf Favoriten zugreifen"
-    "read:following": "Follower-Daten lesen"
-    "write:following": "Folgestatus bearbeiten"
-    "read:messaging": "Unterhaltung anzeigen"
-    "write:messaging": "Unterhaltung bearbeiten"
-    "read:mutes": "Stummschaltungen lesen"
-    "write:mutes": "Stummschaltungen bearbeiten"
-    "write:notes": "Beiträge löschen und verfassen"
-    "read:notifications": "Benachrichtigungen lesen"
-    "write:notifications": "Benachrichtigungen bearbeiten"
-    "read:reactions": "Reaktionen sehen"
-    "write:reactions": "Reaktionen hinzufügen und bearbeiten"
-    "write:votes": "Abstimmen"
-  empty-timeline-info:
-    follow-users-to-make-your-timeline: "Beiträge von Benutzern, denen du folgst, werden in der Zeitleiste angezeigt."
-    explore: "Benutzer finden"
-  post-form:
-    reply: "Antworten"
-    renote: "Anmerkung"
-    enter-username: "Bitte gib einen Benutzernamen ein"
-    username-prompt: "Bitte gib einen Benutzernamen ein"
-  weekday-short:
-    sunday: "So"
-    monday: "Mo"
-    tuesday: "Di"
-    wednesday: "Mi"
-    thursday: "Do"
-    friday: "Fr"
-    saturday: "Sa"
-  weekday:
-    sunday: "Sonntag"
-    monday: "Montag"
-    tuesday: "Dienstag"
-    wednesday: "Mittwoch"
-    thursday: "Donnerstag"
-    friday: "Freitag"
-    saturday: "Samstag"
-  reactions:
-    like: "Gefällt mir"
-    love: "Lieben"
-    laugh: "Lachen"
-    hmm: "Hmm...?"
-    surprise: "Wow"
-    congrats: "Glückwunsch!"
-    angry: "Wütend"
-    confused: "Verwirrt"
-    rip: "RIP"
-    pudding: "Pudding"
-  note-visibility:
-    public: "Öffentlich"
-    home: "Startseite"
-    home-desc: "Auf die Startseite posten"
-    followers: "Abonnenten"
-    followers-desc: "Nur für diejenigen sichtbar, die dir folgen"
-    specified: "Direkt"
-    specified-desc: "Nur für bestimmte Benutzer sichtbar"
-    local-public: "Öffentlich (nur lokal)"
-    local-home: "Home (nur lokal)"
-    local-followers: "Follower (nur lokal)"
-  note-placeholders:
-    a: "Was machst du gerade?"
-    b: "Was ist so passiert?"
-    c: "Was geht dir durch den Kopf?"
-    d: "Willst du etwas sagen?"
-    e: "Schreib hier etwas!"
-    f: "Warte darauf, das du schreibst..."
-  settings: "Einstellungen"
-  _settings:
-    profile: "Dein Profil"
-    notification: "Benachrichtigungen"
-    apps: "Anwendungen"
-    tags: "Hashtags"
-    mute-and-block: "Stummschalten/Blocken"
-    blocking: "Blocken"
-    security: "Sicherheit"
-    signin: "Login-Verlauf"
-    password: "Passwort"
-    other: "Mehr"
-    appearance: "Designs"
-    behavior: "Verhalten"
-    fetch-on-scroll: "Unendliches laden beim scrollen"
-    fetch-on-scroll-desc: "Wenn beim scrollen das Ende erreicht wird, lädt die Anwendung automatisch neue Inhalte nach."
-    note-visibility: "Sichtbarkeit von Beiträgen"
-    default-note-visibility: "Die Standardsichtbarkeit"
-    remember-note-visibility: "Erinnerung an Sichtbarkeit von Beiträgen"
-    web-search-engine: "Web-Suchmaschine"
-    web-search-engine-desc: "Beispiel: https://www.google.de/search?q={{query}}"
-    keep-cw: "Inhaltswarnung beibehalten"
-    keep-cw-desc: "Wenn auf einen Beitrag geantwortet wird, wird die Inhaltswarnung des Originalbeitrags übernommen."
-    i-like-sushi: "Ich bevorzuge Sushi anstelle von Pudding"
-    show-reversi-board-labels: "Zeige Reihen- und Spaltenbeschreibungen in Reversi an"
-    use-avatar-reversi-stones: "Avatar als Stein in Reversi anzeigen"
-    disable-animated-mfm: "Animierten Text in Beiträgen deaktivieren"
-    disable-showing-animated-images: "Animierte Grafiken deaktivieren"
-    suggest-recent-hashtags: "Beim Verfassen von Beiträgen letzte Hashtags anzeigen"
-    always-show-nsfw: "Sensible Inhalte (NSFW) immer anzeigen"
-    always-mark-nsfw: "Meine Anhänge immer als NSFW markieren"
-    show-full-acct: "Servername bei Benutzernamen immer anzeigen"
-    show-via: "„via“ anzeigen"
-    reduce-motion: "Animationen der Benutzeroberfläche reduzieren"
-    this-setting-is-this-device-only: "Nur auf diesem Gerät"
-    use-os-default-emojis: "Betriebssystem-Emojis nutzen"
-    line-width: "Linienstärke"
-    line-width-thin: "Dünn"
-    line-width-normal: "Normal"
-    line-width-thick: "Dick"
-    font-size: "Schriftgröße"
-    font-size-x-small: "Sehr klein"
-    font-size-small: "Klein"
-    font-size-medium: "Normal"
-    font-size-large: "Groß"
-    font-size-x-large: "Sehr groß"
-    deck-column-align: "Spaltenaufteilung der Deck-Ansicht"
-    deck-column-align-center: "Mitte"
-    deck-column-align-left: "Links"
-    deck-column-align-flexible: "Flexibel"
-    deck-column-width: "Spaltenbreite des Decks"
-    deck-column-width-narrow: "Sehr eng"
-    deck-column-width-narrower: "Eng"
-    deck-column-width-normal: "Normal"
-    deck-column-width-wider: "Breit"
-    deck-column-width-wide: "Sehr breit"
-    use-shadow: "Nutze Schatten"
-    rounded-corners: "Abgerundete Ecken"
-    circle-icons: "Kreisförmige Avatar"
-    contrasted-acct: "Nutzernamen kontrastreicher darstellen"
-    wallpaper: "Hintergrund"
-    choose-wallpaper: "Hintergrund auswählen"
-    delete-wallpaper: "Hintergrund entfernen"
-    post-form-on-timeline: "Beitragsformular über Timeline anzeigen"
-    show-clock-on-header: "Uhr am oberen rechten Rand anzeigen"
-    show-reply-target: "Zeige bei einer Antwort die ursprüngliche Nachricht"
-    timeline: "Timeline"
-    show-my-renotes: "Zeige eigene Renotes in der Timeline"
-    show-renoted-my-notes: "Zeige Renotes deiner Posts in der Timeline"
-    show-local-renotes: "Zeige Renotes lokaler Posts in der Timeline"
-    remain-deleted-note: "Gelöschte Beiträge weiterhin anzeigen"
-    sound: "Töne"
-    enable-sounds: "Töne aktivieren"
-    enable-sounds-desc: "Spiel einen Ton ab beim Erhalten eines Beitrags bzw. einer Nachricht. Diese Einstellung wird im Browser gespeichert."
-    volume: "Lautstärke"
-    test: "Test"
-    update: "Misskey-Update"
-    version: "Version:"
-    latest-version: "Neuste Version:"
-    update-checking: "Suche nach Updates"
-    do-update: "Nach Updates suchen"
-    update-settings: "Erweiterte Einstellungen"
-    no-updates: "Kein Update verfügbar"
-    no-updates-desc: "Misskey ist aktuell."
-    update-available: "Eine neue Version ist verfügbar!"
-    update-available-desc: "Änderungen werden beim Neuladen der Seite angewendet."
-    advanced-settings: "Erweiterte Einstellungen"
-    debug-mode: "Debug-Modus einschalten"
-    debug-mode-desc: "Diese Einstellung wird im Browser gespeichert."
-    navbar-position: "Postion der Navigationsleiste"
-    navbar-position-top: "Oben"
-    navbar-position-left: "Links"
-    navbar-position-right: "Rechts"
-    i-am-under-limited-internet: "Ich möchte Datenvolumen sparen"
-    post-style: "Beitrags-Anzeigestil"
-    post-style-standard: "Standard"
-    post-style-smart: "Smart"
-    notification-position: "Benachrichtigungen anzeigen"
-    notification-position-bottom: "Unten"
-    notification-position-top: "Oben"
-    disable-via-mobile: "Beitrag nicht als „vom Handy“ markieren"
-    load-raw-images: "Anhänge in voller Größe laden"
-    load-remote-media: "Zeige Inhalte von fremden Servern"
-    save: "Speichern"
-    saved: "Gespeichert"
-    preview: "Vorschau"
-  search: "Suche"
-  delete: "Löschen"
-  loading: "Laden"
-  ok: "Okay"
-  cancel: "Abbrechen"
-  update-available-title: "Aktualisierung verfügbar"
-  update-available: "Eine neue Version von Misskey ist verfügbar ({newer}, aktuell ist {current}). Lade die Seite neu um die aktuelle Version zu laden"
-  my-token-regenerated: "Dein Token wurde generiert. Du wirst jetzt abgemeldet."
-  hide-password: "Passwort verbergen"
-  show-password: "Passwort zeigen"
-  enter-username: "Kontonamen eingeben"
-  do-not-use-in-production: "Dies ist eine Entwicklungsversion. Nicht in einer Produktivumgebung verwenden."
-  user-suspended: "Dieser Nutzer wurde gesperrt."
-  is-remote-user: "Diese Nutzerinformationen können unvollständig sein."
-  is-remote-post: "Dies ist ein entfernter Post."
-  view-on-remote: "Vollständige Infos auf Ursprungsserver anzeigen"
-  renoted-by: "Renote von {user}"
-  no-notes: "Keine Beiträge"
-  turn-on-darkmode: "Dunkles Design"
-  turn-off-darkmode: "Helles Design"
-  error:
-    title: "Allgemeiner Fehler"
-    retry: "Erneut versuchen"
-  reversi:
-    drawn: "Unentschieden"
-    my-turn: "Du bist am Zug"
-    opponent-turn: "Dein Gegner ist an der Reihe"
-    turn-of: "{name}s Zug"
-    past-turn-of: "Zug von {name}"
-    won: "{name} hat gewonnen"
-    black: "Schwarz"
-    white: "Weiß"
-    total: "Gesamt"
-    this-turn: "{count}. Zug"
-  widgets:
-    analog-clock: "Analoge Uhr"
-    profile: "Profil"
-    calendar: "Kalender"
-    timemachine: "Kalender (Zeitmaschine)"
-    activity: "Aktivitäten"
-    rss: "RSS Leser"
-    memo: "Notizen"
-    trends: "Trends"
-    photo-stream: "Bilder"
-    posts-monitor: "Beitrags-Aktivität"
-    slideshow: "Diashow"
-    version: "Version"
-    broadcast: "Ankündigungen"
-    notifications: "Benachrichtigungen"
-    users: "Empfohlene Benutzer"
-    polls: "Umfrage"
-    post-form: "\"Neuer Beitrag\"-Formular"
-    server: "Server-Info"
-    nav: "Navigation"
-    tips: "Tipps"
-    hashtags: "Hashtags"
-    queue: "Warteschlange"
-  dev: "Fehler beim Erstellen der Applikation. Bitte versuche es erneut."
-  ai-chan-kawaii: "Ai-chan kawaii!"
-  you: "Du"
-auth/views/form.vue:
-  share-access: "Erlaubst Du <i>{name}</i> auf deinen Account zuzugreifen?"
-  permission-ask: "Diese Applikation benötigt folgende Berechtigungen:"
-  cancel: "Abbrechen"
-  accept: "Zugriff erlauben."
-auth/views/index.vue:
-  loading: "Lädt"
-  denied: "Autorisierung der Anwendung wurde verweigert."
-  denied-paragraph: "Diese App kann nicht auf deinen Account zugreifen."
-  already-authorized: "Diese Anwendung ist bereits autorisiert."
-  allowed: "Autorisierung der Anwendung wurde erlaubt."
-  callback-url: "Zur App zurückkehren"
-  please-go-back: "Bitte gehe zurück zur Anwendung."
-  error: "Sitzung ist nicht vorhanden."
-  sign-in: "Bitte melde dich an."
-common/views/pages/explore.vue:
-  pinned-users: "Vorgeschlagen"
-  popular-users: "Beliebt"
-  recently-updated-users: "Kürzlich aktiv"
-  recently-registered-users: "Neue Benutzer"
-  popular-tags: "Beliebte Tags"
-  federated: "Aus dem Fediverse"
-  explore: "{host} erkunden"
-  users-info: "Momentan sind {users} Nutzer hier registriert"
-common/views/components/url-preview.vue:
-  enable-player: "Player öffnen"
-  disable-player: "Player schließen"
-common/views/components/user-list.vue:
-  no-users: "Keine Benutzer"
-common/views/components/games/reversi/reversi.vue:
-  matching:
-    waiting-for: "Warten auf {}"
-    cancel: "Abbrechen"
-common/views/components/games/reversi/reversi.game.vue:
-  surrender: "Aufgeben"
-  surrendered: "durch Aufgabe"
-  is-llotheo: "Der niedrigere gewinnt (Llotheo)"
-  looped-map: "Spielbrettenden verbinden"
-  can-put-everywhere: "Setzen ist überall erlaubt"
-common/views/components/games/reversi/reversi.index.vue:
-  title: "Misskey Reversi"
-  sub-title: "Spiele Reversi mit deinen Freunden!"
-  invite: "Einladen"
-  rule: "Spielanleitung"
-  mode-invite: "Einladen"
-  all-games: "Alle Spiele"
-  enter-username: "Bitte gib einen Benutzernamen ein"
-  game-state:
-    ended: "Fertig"
-common/views/components/games/reversi/reversi.room.vue:
-  settings-of-the-game: "Spieleinstellungen"
-  choose-map: "Wähle eine Karte"
-  random: "Zufällige Auswahl"
-  black-or-white: "Schwarz/Weiß"
-  black-is: "Schwarz ist {}"
-  rules: "Regeln"
-  is-llotheo: "Der niedrigere gewinnt (Llotheo)"
-  looped-map: "Spielbrettenden verbinden"
-  can-put-everywhere: "Setzen ist überall erlaubt"
-  settings-of-the-bot: "Bot-Einstellungen"
-  this-game-is-started-soon: "Spiel beginnt gleich"
-  waiting-for-other: "Warte auf den Gegner"
-  waiting-for-me: "Warten, bis du bereit bist"
-  waiting-for-both: "Vorbereiten…"
-  cancel: "Abbrechen"
-  ready: "Bereit"
-common/views/components/connect-failed.vue:
-  title: "Verbindung zum Server ist fehlgeschlagen"
-  description: "Entweder gibt es ein Problem mit deiner Internetverbindung oder der Server ist zur Zeit nicht erreichbar oder wird gewartet. Bitte versuche es später noch einmal."
-  thanks: "Vielen Dank für das nutzen von Misskey."
-  troubleshoot: "Problembehandlung"
-common/views/components/connect-failed.troubleshooter.vue:
-  title: "Problembehandlung"
-  network: "Netzwerkverbindung"
-  checking-network: "Prüfen der Netzwerkverbindung"
-  internet: "Internetverbindung"
-  checking-internet: "Internetverbindung wird getestet"
-  server: "Serververbindung"
-  checking-server: "Überprüfung der Server-Verbindung"
-  finding: "Nach dem Problem suchen"
-  no-network: "Keine Netzwerkverbindung"
-  no-network-desc: "Bitte stelle sicher, dass du mit dem Internet verbunden bist."
-  no-internet: "Keine Internetverbindung"
-  no-internet-desc: "Bitte vergewissere dich, dass du mit dem Internet verbunden bist."
-  no-server: "Verbindung mit dem Server nicht möglich"
-  no-server-desc: "Die Internetverbindung scheint in Ordnung zu sein, aber eine Verbindung mit dem Misskey-Server konnte nicht hergestellt werden. Möglicherweise ist dieser zur Zeit offline oder wird gewartet. Bitte versuche es später noch einmal."
-  success: "Erfolgreich mit dem Misskey-Server verbunden"
-  success-desc: "Die Verbindung scheint zu funktionieren. Bitte lade die Seite neu."
-  flush: "Cache leeren"
-  set-version: "Version angeben"
-common/views/components/media-banner.vue:
-  sensitive: "Dieser Inhalt ist NSFW"
-  click-to-show: "Klicke zum den Inhalt anzusehen"
-common/views/components/theme.vue:
-  theme: "Design"
-  light-theme: "Thema"
-  dark-theme: "Thema während des Nachtmodus"
-  light-themes: "Helles Thema"
-  dark-themes: "Dunkles Thema"
-  install-a-theme: "Design wird installiert"
-  theme-code: "Design-Quelltext"
-  install: "Anwenden"
-  installed: "\"{}\" wurde installiert"
-  create-a-theme: "Thema erstellen"
-  save-created-theme: "Thema speichern"
-  primary-color: "Primäre Farbe"
-  secondary-color: "Sekundäre Farbe"
-  text-color: "Textfarbe"
-  base-theme: "Basisthema"
-  base-theme-light: "Hell"
-  base-theme-dark: "Dunkel"
-  find-more-theme: "Mehr Designs finden"
-  theme-name: "Name des Themas"
-  preview-created-theme: "Vorschau"
-  invalid-theme: "Thema ist ungültig"
-  already-installed: "Thema ist bereits installiert"
-  saved: "Gespeichert"
-  manage-themes: "Designs verwalten"
-  builtin-themes: "Standard-Designs"
-  my-themes: "Meine Designs"
-  installed-themes: "Installierte Designs"
-  select-theme: "Design wählen"
-  uninstall: "Deinstallieren"
-  uninstalled: "„{}“ wurde deinstalliert"
-  author: "Autor"
-  desc: "Beschreibung"
-  export: "Exportieren"
-  import: "Importieren"
-  import-by-code: "oder Quelltext einfügen"
-  theme-name-required: "Design-Name ist erforderlich"
-common/views/components/cw-button.vue:
-  hide: "Ausblenden"
-  show: "Mehr"
-  chars: "{count} Zeichen"
-  files: "{count} Dateien"
-  poll: "Umfrage"
-common/views/components/messaging.vue:
-  search-user: "Einen Nutzer suchen"
-  you: "Du"
-  no-history: "Keine Chronik"
-  user: "Benutzer"
-  group: "Gruppen"
-common/views/components/messaging-room.vue:
-  no-history: "Keine weitere Chronik vorhanden"
-  new-message: "Neue Nachricht"
-common/views/components/messaging-room.form.vue:
-  input-message-here: "Nachricht hier eingeben"
-  send: "Senden"
-  attach-from-local: "Wähle Dateien von deinem PC aus"
-  attach-from-drive: "Wähle Dateien von deinem Speicher aus"
-common/views/components/messaging-room.message.vue:
-  is-read: "Gelesen"
-  deleted: "Diese Nachricht wurde gelöscht"
-common/views/components/nav.vue:
-  about: "Ãœber"
-  stats: "Statistiken"
-  status: "Status"
-  wiki: "Wiki"
-  donors: "Spender"
-  repository: "Quellcode"
-  develop: "Entwickler"
-  feedback: "Feedback"
-common/views/components/note-menu.vue:
-  mention: "Erwähnungen"
-  detail: "Details"
-  copy-content: "Inhalt kopieren"
-  copy-link: "Link kopieren"
-  favorite: "Diesen Beitrag favorisieren"
-  unfavorite: "Aus Favoriten entfernen"
-  watch: "Beobachten"
-  unwatch: "Nicht mehr beobachten"
-  pin: "An die Profilseite pinnen"
-  unpin: "Lösen"
-  delete: "Löschen"
-  delete-confirm: "Diesen Beitrag löschen?"
-  remote: "Auf Quelle anzeigen"
-common/views/components/user-menu.vue:
-  mention: "Erwähnungen"
-  mute: "Stummschalten"
-  unmute: "Stummschaltung aufheben"
-  mute-confirm: "Bist du sicher, dass du diesen Nutzer stummschalten möchtest?"
-  unmute-confirm: "Stummschaltung für diesen Nutzer aufheben?"
-  block: "Sperren"
-  unblock: "Sperrung aufheben"
-  block-confirm: "Diesen Nutzer wirklich sperren?"
-common/views/components/poll.vue:
-  vote-to: "Stimme für '{}'"
-  vote-count: "{} Stimmen"
-  vote: "Abstimmen"
-  show-result: "Zeige Ergebnis"
-  voted: "Abgestimmt"
-common/views/components/poll-editor.vue:
-  no-only-one-choice: "Du musst zwei oder mehr Entscheidungen angeben"
-  choice-n: "Auswahl {}"
-  remove: "Diese Auswahl entfernen"
-  add: "+ Eine Auswahl hinzufügen"
-  destroy: "Diese Abstimmung löschen"
-  day: "So"
-common/views/components/reaction-picker.vue:
-  choose-reaction: "Wähle eine Reaktion aus"
-common/views/components/emoji-picker.vue:
-  activity: "Aktivität"
-common/views/components/signin.vue:
-  username: "Benutzername"
-  password: "Passwort"
-  token: "Token"
-  signing-in: "Melde an..."
-  or: "Oder"
-common/views/components/signup.vue:
-  username: "Benutzername"
-  checking: "Überprüfung..."
-  available: "Verfügbar"
-  unavailable: "Nicht verfügbar"
-  error: "Verbindungsfehler"
-  invalid-format: "Benutze nur Buchstaben, Zahlen und _"
-  too-short: "Bitte mindestens ein Zeichen eingeben"
-  too-long: "Bitte maximal 20 Zeichen verwenden"
-  password: "Passwort"
-  password-placeholder: "Wir empfehlen mindestens 8 Zeichen"
-  weak-password: "Schwaches Passwort"
-  normal-password: "Normales Passwort"
-  strong-password: "Starkes Passwort"
-  retype: "Wiederholen"
-  retype-placeholder: "Bitte das Passwort erneut eingeben"
-  password-matched: "OK"
-  password-not-matched: "Stimmt nicht überein"
-  recaptcha: "Captcha"
-  create: "Account erstellen"
-  some-error: "Die Anmeldung konnte aufgrund eines Fehler nicht abgeschlossen werden. Bitte versuche es erneut."
-common/views/components/special-message.vue:
-  new-year: "Frohes neues Jahr!"
-  christmas: "Frohe Weihnachten!"
-common/views/components/stream-indicator.vue:
-  connecting: "Verbindung wird hergestellt"
-  reconnecting: "Erneut verbinden"
-  connected: "Verbindung hergestellt"
-common/views/components/notification-settings.vue:
-  title: "Benachrichtigungen"
-common/views/components/uploader.vue:
-  waiting: "Warten"
-common/views/components/visibility-chooser.vue:
-  public: "Öffentlich"
-  home: "Home"
-  home-desc: "Auf die Startseite posten"
-  followers: "Folgende"
-  followers-desc: "Nur für diejenigen sichtbar, die dir folgen"
-  specified: "Direkt"
-  specified-desc: "Nur für bestimmte Benutzer sichtbar"
-  local-public: "Öffentlich (nur lokal)"
-  local-home: "Home (nur lokal)"
-  local-followers: "Follower (nur lokal)"
-common/views/components/profile-editor.vue:
-  title: "Dein Profil"
-  name: "Name"
-  avatar: "Avatar"
-  banner: "Banner"
-  save: "Speichern"
-  unable-to-process: "Der Vorgang konnte nicht abgeschlossen werden"
-  export: "Exportieren"
-  import: "Importieren"
-  export-targets:
-    user-lists: "Listen"
-  enter-password: "Bitte Passwort eingeben"
-common/views/components/user-group-editor.vue:
-  invite: "Einladen"
-common/views/components/user-lists.vue:
-  user-lists: "Listen"
-common/views/components/user-groups.vue:
-  user-groups: "Gruppen"
-  owned-groups: "Meine Gruppen"
-  invites: "Einladen"
-common/views/widgets/broadcast.vue:
-  fetching: "Laden"
-  no-broadcasts: "Keine Broadcasts"
-  have-a-nice-day: "Schönen Tag!"
-  next: "Nächster"
-common/views/widgets/photo-stream.vue:
-  title: "Fotostream"
-  no-photos: "Keine Fotos"
-common/views/widgets/posts-monitor.vue:
-  title: "Beitrags-Aktivität"
-  toggle: "Sicht umschalten"
-common/views/widgets/server.vue:
-  title: "Serverinformationen"
-  toggle: "Sicht umschalten"
-common/views/widgets/memo.vue:
-  title: "Notizen"
-  memo: "Schreib hier!"
-  save: "Speichern"
-desktop:
-  banner: "Banner"
-  avatar: "Avatar"
-  unable-to-process: "Der Vorgang konnte nicht abgeschlossen werden"
-desktop/views/components/activity.chart.vue:
-  total: "Schwarz ... komplett"
-  notes: "Blau ... Hinweise"
-  replies: "Rot ... Antworten"
-  renotes: "Grün ... Anmerkungen"
-desktop/views/components/activity.vue:
-  title: "Aktivität"
-  toggle: "Sichten umschalten"
-desktop/views/components/calendar.vue:
-  prev: "Vorheriger Monat"
-  next: "Nächster Monat"
-  go: "Klicke zur Navigation"
-desktop/views/components/choose-file-from-drive-window.vue:
-  upload: "Dateien von deinem PC hochladen"
-  cancel: "Abbrechen"
-  ok: "OK"
-  choose-prompt: "Wähle eine Datei aus"
-desktop/views/components/choose-folder-from-drive-window.vue:
-  cancel: "Abbrechen"
-  ok: "OK"
-  choose-prompt: "Wähle einen Ordner"
-desktop/views/components/crop-window.vue:
-  skip: "Zuschneiden überspringen"
-  cancel: "Abbrechen"
-  ok: "OK"
-desktop/views/components/drive-window.vue:
-  used: "benutzt"
-desktop/views/components/drive.file.vue:
-  avatar: "Avatar"
-  banner: "Banner"
-  contextmenu:
-    rename: "Umbenennen"
-    copy-url: "URL kopieren"
-    download: "Download"
-    set-as-avatar: "Als Avatar festlegen"
-    set-as-banner: "Setze als Banner"
-    open-in-app: "In der App öffnen"
-    add-app: "App hinzufügen"
-    rename-file: "Datei umbennen"
-    input-new-file-name: "Gib den neuen Dateinamen an"
-    copied: "Kopieren erfolgreich"
-    copied-url-to-clipboard: "URL wurde in die Zwischenablage kopiert"
-desktop/views/components/drive.folder.vue:
-  unable-to-process: "Der Vorgang konnte nicht abgeschlossen werden"
-  circular-reference-detected: "Das Zielverzeichnis ist innerhalb des Verzeichnisses, dass du verschieben möchtest"
-  unhandled-error: "Unbekannter Fehler"
-  contextmenu:
-    move-to-this-folder: "Verschiebe in diesen Ordner"
-    show-in-new-window: "In einem neuen Fenster anzeigen"
-    rename: "Umbenennen"
-    rename-folder: "Ordner umbenennen"
-    input-new-folder-name: "Namen für neuen Ordner eingeben"
-desktop/views/components/drive.vue:
-  search: "Suchen"
-  empty-draghover: "Herzlich Willkommen!"
-  empty-drive: "Dein Speicher ist leer"
-  empty-drive-description: "Du kannst rechts klicken und \"Datei hochladen\" auswählen oder eine Datei per Drag and Drop auf das Fenster ziehen."
-  empty-folder: "Dieser Ordner ist leer"
-  unable-to-process: "Der Vorgang konnte nicht beendet werden"
-  circular-reference-detected: "Das Zielverzeichnis ist innerhalb des Verzeichnisses, dass du verschieben möchtest"
-  unhandled-error: "Unbekannter Fehler"
-  url-upload: "Von einer URL hochladen"
-  url-of-file: "URL der Datei, welche du hochladen möchtest"
-  url-upload-requested: "Upload angefordert"
-  may-take-time: "Es kann eine Weile dauern, bis der Upload fertiggestellt ist."
-  create-folder: "Ein Verzeichnis erstellen"
-  folder-name: "Ordnername"
-  contextmenu:
-    create-folder: "Ein Verzeichnis erstellen"
-    upload: "Eine Datei hochladen"
-    url-upload: "Von einer URL hochladen"
-desktop/views/components/followers.vue:
-  empty: "Dir scheint niemand zu folgen."
-desktop/views/components/following.vue:
-  empty: "Du folgst niemanden"
-desktop/views/components/home.vue:
-  done: "Beenden"
-  add-widget: "Widget hinzufügen:"
-  add: "Hinzufügen"
-desktop/views/input-dialog.vue:
-  cancel: "Abbrechen"
-  ok: "OK"
-desktop/views/components/note-detail.vue:
-  private: "Dieser Beitrag ist privat"
-  deleted: "Dieser Beitrag wurde entfernt"
-  location: "Ort"
-  renote: "Anmerkung"
-  add-reaction: "Reaktion hinzufügen"
-desktop/views/components/note.vue:
-  reply: "Antworten"
-  renote: "Anmerkung"
-  detail: "Details"
-  private: "Dieser Beitrag ist privat"
-  deleted: "Dieser Beitrag wurde entfernt"
-desktop/views/components/notes.vue:
-  error: "Laden fehlgeschlagen."
-  retry: "Erneut versuchen"
-desktop/views/components/notifications.vue:
-  empty: "Keine Benachrichtigungen"
-desktop/views/components/post-form.vue:
-  posted: "Gepostet!"
-  replied: "Geantwortet!"
-  reposted: "Weitergesagt!"
-  note-failed: "Anmerkung fehlgeschlagen"
-  reply-failed: "Antwort fehlgeschlagen"
-  renote-failed: "Anmerkung fehlgeschlagen"
-desktop/views/components/post-form-window.vue:
-  note: "Neuer Beitrag"
-  reply: "Antworten"
-  attaches: "{} Medien hinzugefügt"
-  uploading-media: "Lade {} Medien hoch"
-desktop/views/components/progress-dialog.vue:
-  waiting: "Warten"
-desktop/views/components/renote-form.vue:
-  quote: "Zitieren..."
-  cancel: "Abbrechen"
-  renote: "Anmerkung"
-  reposting: "Weitersagen..."
-  success: "Weitergesagt!"
-  failure: "Weitersagen fehlgeschlagen"
-desktop/views/components/renote-form-window.vue:
-  title: "Bist du dir sicher, dass du das weitersagen willst?"
-desktop/views/components/settings.2fa.vue:
-  url: "https://www.google.de/intl/de/landing/2step/"
-  register: "Ein Gerät registrieren"
-  already-registered: "Das Gerät wurde bereits registriert"
-  unregister: "Abschalten"
-  unregistered: "Zwei-Faktor-Authentifizierung wurde deaktiviert."
-  enter-password: "Bitte Passwort eingeben"
-  token: "Token"
-common/views/components/api-settings.vue:
-  enter-password: "Bitte Passwort eingeben"
-  console:
-    parameter: "Parameter"
-    send: "Senden"
-common/views/components/drive-settings.vue:
-  in-use: "benutzt"
-  stats: "Statistiken"
-common/views/components/mute-and-block.vue:
-  unmute-confirm: "Stummschaltung für diesen Nutzer aufheben?"
-  save: "Speichern"
-desktop/views/components/sub-note-content.vue:
-  private: "Dieser Beitrag ist privat"
-  deleted: "Dieser Beitrag wurde entfernt"
-  poll: "Umfrage"
-desktop/views/components/settings.tags.vue:
-  add: "Hinzufügen"
-  save: "Speichern"
-desktop/views/components/timeline.vue:
-  home: "Home"
-  local: "Lokal"
-  global: "Global"
-  list: "Listen"
-desktop/views/components/ui.header.account.vue:
-  profile: "Dein Profil"
-  lists: "Listen"
-  groups: "Gruppen"
-desktop/views/components/ui.header.nav.vue:
-  game: "Spielen"
-desktop/views/components/ui.header.notifications.vue:
-  title: "Benachrichtigungen"
-desktop/views/components/ui.header.post.vue:
-  post: "Einen neuen Post erstellen"
-desktop/views/components/ui.header.search.vue:
-  placeholder: "Suchen"
-desktop/views/components/users-list.vue:
-  fetching: "Lade…"
-admin/views/dashboard.vue:
-  drive: "Drive"
-admin/views/abuse.vue:
-  details: "Details"
-  remove-report: "Löschen"
-admin/views/instance.vue:
-  recaptcha-preview: "Vorschau"
-  invite: "Einladen"
-  save: "Speichern"
-  saved: "Gespeichert"
-  test-email: "Test"
-admin/views/charts.vue:
-  drive: "Drive"
-admin/views/drive.vue:
-  origin:
-    local: "Lokal"
-  delete: "Löschen"
-admin/views/users.vue:
-  username: "Benutzername"
-  users:
-    origin:
-      local: "Lokal"
-admin/views/emoji.vue:
-  add-emoji:
-    add: "Hinzufügen"
-  emojis:
-    remove: "Löschen"
-admin/views/announcements.vue:
-  save: "Speichern"
-  remove: "Löschen"
-  add: "Hinzufügen"
-  saved: "Gespeichert"
-admin/views/federation.vue:
-  status: "Status"
-  save: "Speichern"
-desktop/views/pages/note.vue:
-  prev: "Vorheriger Kommentar"
-  next: "Nächster Kommentar"
-desktop/views/pages/selectdrive.vue:
-  title: "Wähle Datei(en) aus"
-  ok: "OK"
-  cancel: "Abbrechen"
-desktop/views/pages/user-list.users.vue:
-  username: "Benutzername"
-desktop/views/pages/user/user.followers-you-know.vue:
-  loading: "Laden"
-desktop/views/pages/user/user.friends.vue:
-  loading: "Laden"
-desktop/views/pages/user/user.photos.vue:
-  loading: "Laden"
-  no-photos: "Keine Fotos"
-desktop/views/pages/user/user.header.vue:
-  month: "Mo"
-  day: "So"
-desktop/views/widgets/notifications.vue:
-  title: "Benachrichtigungen"
-desktop/views/widgets/polls.vue:
-  title: "Umfrage"
-  nothing: "Keine Benachrichtigungen"
-desktop/views/widgets/trends.vue:
-  nothing: "Keine Benachrichtigungen"
-mobile/views/components/drive.vue:
-  used: "benutzt"
-  folder-name: "Ordnername"
-  url-prompt: "URL der Datei, welche du hochladen möchtest"
-mobile/views/components/drive.file-detail.vue:
-  download: "Download"
-  rename: "Umbenennen"
-mobile/views/components/note.vue:
-  private: "Dieser Beitrag ist privat"
-  deleted: "Dieser Beitrag wurde entfernt"
-  location: "Ort"
-mobile/views/components/note-detail.vue:
-  reply: "Antworten"
-  private: "Dieser Beitrag ist privat"
-  deleted: "Dieser Beitrag wurde entfernt"
-  location: "Ort"
-mobile/views/components/notifications.vue:
-  empty: "Keine Benachrichtigungen"
-mobile/views/components/sub-note-content.vue:
-  private: "Dieser Beitrag ist privat"
-  deleted: "Dieser Beitrag wurde entfernt"
-  poll: "Umfrage"
-mobile/views/components/ui.nav.vue:
-  notifications: "Benachrichtigungen"
-  search: "Suchen"
-  user-lists: "Listen"
-  user-groups: "Gruppen"
-  game: "Spielen"
-  about: "Ãœber"
-mobile/views/pages/drive.vue:
-  contextmenu:
-    upload: "Eine Datei hochladen"
-    create-folder: "Ein Verzeichnis erstellen"
-mobile/views/pages/home.vue:
-  home: "Home"
-  local: "Lokal"
-  global: "Global"
-mobile/views/pages/widgets.vue:
-  add-widget: "Hinzufügen"
-  customization-tips: "Anpassungs-Tipps"
-mobile/views/pages/widgets/activity.vue:
-  activity: "Aktivität"
-mobile/views/pages/note.vue:
-  prev: "Vorheriger Kommentar"
-  next: "Nächster Kommentar"
-mobile/views/pages/search.vue:
-  search: "Suchen"
-mobile/views/pages/notifications.vue:
-  notifications: "Benachrichtigungen"
-mobile/views/pages/user/home.vue:
-  activity: "Aktivität"
-  keywords: "Schlagwörter"
-mobile/views/pages/user/home.photos.vue:
-  no-photos: "Keine Fotos"
-deck:
-  home: "Home"
-  local: "Lokal"
-  global: "Global"
-  notifications: "Benachrichtigungen"
-  list: "Listen"
-  rename: "Umbenennen"
-deck/deck.user-column.vue:
-  following: "Folgen"
-  followers: "Folgende"
-  images: "Bilder"
-  activity: "Aktivität"
-  timeline: "Zeitleiste"
-  pinned-notes: "Angeheftete Beiträge"
-docs:
-  edit-this-page-on-github: "Hast Du einen Fehler gefunden oder Lust, diese Dokumentation zu verbessern?"
-  edit-this-page-on-github-link: "Seite auf GitHub bearbeiten!"
-dev/views/index.vue:
-  manage-apps: "Anwendungen verwalten"
-dev/views/apps.vue:
-  manage-apps: "Anwendungen verwalten"
-  create-app: "Anwendung erstellen"
-  app-missing: "Keine Anwendungen"
-dev/views/new-app.vue:
-  create-app: "Erstelle Anwendung"
-  app-name: "Name der Anwendung"
-  app-name-desc: "Der Name der Anwendung"
-  app-overview: "Beschreibung der Anwendung"
-  callback-url: "Callback-URL (optional)"
-  callback-url-desc: "Die URL, auf die nach erfolgreicher Authentifizierung umgeleitet werden soll."
-  authority: "Berechtigungen"
-  authority-desc: "Nur die hier eingetragenen Berechtigungen, werden per API zur Verfügung stehen."
-  authority-warning: "Dies kann auch nach dem erstellen der Anwendung geändert werden, allerdings werden dann alle bisher generierten Token ungültig."
-pages:
-  pin-this-page: "An die Profilseite pinnen"
-  unpin-this-page: "Lösen"
-  like: "Gefällt mir"
-  blocks:
-    post: "\"Neuer Beitrag\"-Formular"
-  script:
-    categories:
-      random: "Zufällige Auswahl"
-      list: "Listen"
-    blocks:
-      _join:
-        arg1: "Listen"
-      random: "Zufällige Auswahl"
-      _randomPick:
-        arg1: "Listen"
-      _dailyRandomPick:
-        arg1: "Listen"
-      _seedRandomPick:
-        arg2: "Listen"
-      _pick:
-        arg1: "Listen"
-      _listLen:
-        arg1: "Listen"
-    types:
-      array: "Listen"
-room:
-  save: "Speichern"
-  saved: "Gespeichert"
-  furnitures:
-    moon: "Mond"
-    bin: "Papierkorb"
diff --git a/locales/en-US.yml b/locales/en-US.yml
deleted file mode 100644
index 94f728b4767111a16c5a1168bc66fbb36a329926..0000000000000000000000000000000000000000
--- a/locales/en-US.yml
+++ /dev/null
@@ -1,2175 +0,0 @@
----
-meta:
-  lang: "English"
-common:
-  misskey: "A ⭐ of the fediverse"
-  about-title: "A ⭐ of the fediverse."
-  about: "Thank you for finding Misskey. Misskey is a <b>decentralized microblogging platform</b> born on Earth. Since it exists within the Fediverse (a universe where various social media platforms are organized), it is mutually linked with other social media platforms. Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet?"
-  intro:
-    title: "What is Misskey?"
-    about: "Misskey is an open-source, <b>decentralized microblogging software</b>. It has a sophisticated, fully customizable user interface, a variety of ways for expressing a reaction to posts, free file storage providing an integrated management system, and other advanced features are available. In addition, Misskey connects to a network system called the “Fediverse”, which enables us to communicate with users on other SNSs. For example, when you post something, it will be sent not only to Misskey users, but also those on Mastodon and Pleroma. Just imagine that the planet is sending a radio transmission to another planet, in order to communicate."
-    features: "Features"
-    rich-contents: "Post"
-    rich-contents-desc: "Just post your idea, hot topics, and anything you want to share. You may want to decorate your words, attach your favorite pictures, send files, including videos, or create a poll - those are some of the things you can do with Misskey!"
-    reaction: "Reactions"
-    reaction-desc: "The easiest way to express your emotions. Misskey allows you to add various reactions to posts. Other SNSs only have a \"like\" reaction."
-    ui: "Interface"
-    ui-desc: "No single UI can suit everyone. Therefore, Misskey has a highly customizable UI for your tastes. You can make your home original by editing the layout of your timeline, and moving around selectable widgets that you can easily adjust to make this place your own."
-    drive: "Drive"
-    drive-desc: "Wanna post a picture you have already uploaded? Wish to organize, name and create a folder for your uploaded files? Misskey Drive is the best solution for you. Very easy to share your files online."
-    outro: "Check Misskey-unique features by seeing them with your own eyes! If you feel like this instance is not for you, try other instances, as Misskey is a decentralized SNS, so that you can easily find your mates. Then, GLHF!"
-  application-authorization: "Application authorizations"
-  close: "Close"
-  do-not-copy-paste: "Please do not enter or paste the code here. Account may be compromised."
-  load-more: "Read more"
-  enter-password: "Enter your password"
-  2fa: "Two-factor authentication"
-  customize-home: "Customize home layout"
-  featured-notes: "Featured notes"
-  dark-mode: "Dark Mode"
-  signin: "Login"
-  signup: "Sign up"
-  signout: "Logout"
-  reload-to-apply-the-setting: "You'll need to reload the page to reflect this setting. Do you want to reload it now?"
-  fetching-as-ap-object: "Inquiring to fediverse"
-  unfollow-confirm: "Do you want to unfollow {name}?"
-  delete-confirm: "Are you sure you want to delete this post?"
-  signin-required: "Please login"
-  notification-type: "Notification Type"
-  notification-types:
-    all: "All"
-    pollVote: "Votes"
-    follow: "Following"
-    receiveFollowRequest: "Follow requests"
-    reply: "Reply"
-    quote: "Quote"
-    renote: "Renote"
-    mention: "Mentions"
-    reaction: "Reaction"
-  got-it: "Got it!"
-  customization-tips:
-    title: "Customization tips"
-    paragraph: "<p>Home customization allows you to add/delete, drag and drop and rearrange widgets.</p><p>You can change the display by <strong><strong>right</strong> clicking</strong> on some widgets.</p><p>To delete a widget, drag and drop the widget onto <strong>the area labeled \"Trash\"</strong> in the header.</p><p>To finish the customization, click \"Done\" on the upper right.</p>"
-    gotit: "Got it!"
-  notification:
-    file-uploaded: "File uploaded!"
-    message-from: "Message from {}:"
-    reversi-invited: "Invited to a game"
-    reversi-invited-by: "Invited by {}:"
-    notified-by: "Notified by {}:"
-    reply-from: "Reply from {}:"
-    quoted-by: "Quoted by {}:"
-  time:
-    unknown: "unknown"
-    future: "future"
-    just_now: "now"
-    seconds_ago: "{}s ago"
-    minutes_ago: "{}m ago"
-    hours_ago: "{}h ago"
-    days_ago: "{}d ago"
-    weeks_ago: "{}week(s) ago"
-    months_ago: "{}month(s) ago"
-    years_ago: "{}year(s) ago"
-  month-and-day: "{month}/{day}"
-  trash: "Trash"
-  drive: "Drive"
-  pages: "Pages"
-  messaging: "Talk"
-  home: "Home"
-  deck: "Deck"
-  timeline: "Timeline"
-  explore: "Explore"
-  following: "Following"
-  followers: "Followers"
-  favorites: "Favorites"
-  permissions:
-    "read:account": "View account information"
-    "write:account": "Update your account information"
-    "read:blocks": "View Blocks"
-    "write:blocks": "Work with Blocks"
-    "read:drive": "Browse the Drive"
-    "write:drive": "Work with the Drive"
-    "read:favorites": "View Favorites"
-    "write:favorites": "Work with Favorites"
-    "read:following": "View your Follow info"
-    "write:following": "Work with Follow info"
-    "read:messaging": "View Messaging"
-    "write:messaging": "Work with Messaging"
-    "read:mutes": "View Muted"
-    "write:mutes": "Work with Muted"
-    "write:notes": "Create and delete posts"
-    "read:notifications": "View notifications"
-    "write:notifications": "Work with notifications"
-    "read:reactions": "View reactions"
-    "write:reactions": "Work with reactions"
-    "write:votes": "Vote"
-    "read:pages": "View Pages"
-    "write:pages": "Update Pages"
-    "read:page-likes": "View Likes on Pages"
-    "write:page-likes": "Update Likes on Pages"
-    "read:user-groups": "View User groups"
-    "write:user-groups": "Edit User groups"
-  empty-timeline-info:
-    follow-users-to-make-your-timeline: "Following users will show their posts in your timeline."
-    explore: "Find users"
-  post-form:
-    attach-location-information: "Attach location information"
-    hide-contents: "Hide contents"
-    reply-placeholder: "Reply to this post..."
-    quote-placeholder: "Quote this Post..."
-    option-quote-placeholder: "Quote this post... (optional)"
-    quote-attached: "Quoted"
-    quote-question: "Do you want to append a quote?"
-    submit: "Post"
-    reply: "Reply"
-    renote: "Renote"
-    posting: "Posting"
-    attach-media-from-local: "Attach media from your device"
-    attach-media-from-drive: "Attach media from your Drive"
-    insert-a-kao: "v('ω')v"
-    create-poll: "Create a poll"
-    text-remain: "{} characters remaining"
-    recent-tags: "Recent"
-    local-only-message: "This post will only be published locally"
-    click-to-tagging: "Click to tagging"
-    visibility: "Visibility"
-    geolocation-alert: "Your device does not provide location services"
-    error: "Error"
-    enter-username: "Please enter username"
-    specified-recipient: "Recipient"
-    add-visible-user: "Add a user"
-    cw-placeholder: "Comments for the post (optional)"
-    username-prompt: "Please enter username"
-    enter-file-name: "Edit file name"
-  weekday-short:
-    sunday: "S"
-    monday: "M"
-    tuesday: "T"
-    wednesday: "W"
-    thursday: "T"
-    friday: "F"
-    saturday: "S"
-  weekday:
-    sunday: "Sunday"
-    monday: "Monday"
-    tuesday: "Tuesday"
-    wednesday: "Wednesday"
-    thursday: "Thursday"
-    friday: "Friday"
-    saturday: "Saturday"
-  reactions:
-    like: "Like"
-    love: "Love"
-    laugh: "Laugh"
-    hmm: "Hmm...?"
-    surprise: "Wow"
-    congrats: "Congrats!"
-    angry: "Angry"
-    confused: "Confused"
-    rip: "RIP"
-    pudding: "Pudding"
-  note-visibility:
-    public: "Public"
-    home: "Home"
-    home-desc: "Post to the home timeline only"
-    followers: "Followers"
-    followers-desc: "Post to followers only"
-    specified: "Direct"
-    specified-desc: "Post to specified users only"
-    local-public: "Public (Only local)"
-    local-home: "Home (Only local)"
-    local-followers: "Followers (Only local)"
-  note-placeholders:
-    a: "What are you doing?"
-    b: "What's happening?"
-    c: "What’s on your mind?"
-    d: "What do you want to say?"
-    e: "Write here"
-    f: "Waiting for your writing."
-  settings: "Settings"
-  _settings:
-    profile: "Profile"
-    notification: "Notification"
-    apps: "Apps"
-    tags: "Hashtag"
-    mute-and-block: "Mute / Block"
-    blocking: "Block"
-    security: "Security"
-    signin: "Login History"
-    password: "Password"
-    other: "Other"
-    appearance: "Appearance"
-    behavior: "Behavior"
-    reactions: "Reaction"
-    reactions-description: "Customize Emojis of Reaction picker delimited by line breaks"
-    fetch-on-scroll: "Endless loading on scroll"
-    fetch-on-scroll-desc: "When you scroll down the page, it automatically fetches additional content."
-    note-visibility: "Post visibility"
-    default-note-visibility: "Default visibility"
-    remember-note-visibility: "Remember post visibility"
-    web-search-engine: "Web search engine"
-    web-search-engine-desc: "Example: https://www.google.com/?#q={{query}}"
-    paste: "Paste"
-    pasted-file-name: "Template for pasted file name"
-    pasted-file-name-desc: "Example: \"yyyy-MM-dd HH-mm-ss [{{number}}]\" → \"2018-03-20 21-30-24 1\""
-    paste-dialog: "Edit the pasted file name"
-    paste-dialog-desc: "Display a dialog to edit the file name when you paste a file."
-    keep-cw: "Preserve content warning"
-    keep-cw-desc: "When replying to a post, the same content warning is set by default to the reply, as has been set by the original post."
-    i-like-sushi: "I prefer sushi rather than pudding"
-    show-reversi-board-labels: "Show row and column labels in Reversi"
-    use-avatar-reversi-stones: "Use avatar as a stone in reversi"
-    disable-animated-mfm: "Disable animated texts in a post"
-    disable-showing-animated-images: "Do not play animated images"
-    enable-quick-notification-view: "Enable Quick Notification View"
-    suggest-recent-hashtags: "Show recent popular hashtags on the post form"
-    always-show-nsfw: "Always show NSFW contents"
-    always-mark-nsfw: "Always mark posts with media attachments as NSFW"
-    show-full-acct: "Do not omit the hostname from the username"
-    show-via: "Show via"
-    reduce-motion: "Reduce motion in UI"
-    this-setting-is-this-device-only: "Only for this device"
-    use-os-default-emojis: "Use the OS default Emojis"
-    line-width: "Line thickness"
-    line-width-thin: "Thin"
-    line-width-normal: "Regular"
-    line-width-thick: "Thick"
-    font-size: "Text size"
-    font-size-x-small: "Very small"
-    font-size-small: "Small"
-    font-size-medium: "Medium"
-    font-size-large: "Big"
-    font-size-x-large: "Very big"
-    deck-column-align: "Deck column alignment"
-    deck-column-align-center: "Center"
-    deck-column-align-left: "Left"
-    deck-column-align-flexible: "Flexible"
-    deck-column-width: "Deck column width"
-    deck-column-width-narrow: "Narrow"
-    deck-column-width-narrower: "Narrower"
-    deck-column-width-normal: "Regular"
-    deck-column-width-wider: "Slightly wide"
-    deck-column-width-wide: "Wide"
-    use-shadow: "Use shadows in the UI"
-    rounded-corners: "Round the corners of the UI"
-    circle-icons: "Use circular avatar icon"
-    contrasted-acct: "Add contrast to user account"
-    wallpaper: "Background image"
-    choose-wallpaper: "Choose a background"
-    delete-wallpaper: "Remove background"
-    post-form-on-timeline: "Display the posting form at the top of the timeline"
-    show-clock-on-header: "Show clock on the upper-right"
-    show-reply-target: "Show reply target"
-    timeline: "Timeline"
-    show-my-renotes: "Show my renotes in the timeline"
-    show-renoted-my-notes: "Show renotes of your own posts in the timeline"
-    show-local-renotes: "Show renotes of local posts on the timeline"
-    remain-deleted-note: "Continue to show deleted notes"
-    sound: "Sound"
-    enable-sounds: "Enable sounds"
-    enable-sounds-desc: "Play a sound when you receive a post/message. This setting is stored in the browser."
-    volume: "Volume"
-    test: "Test"
-    update: "Misskey Update"
-    version: "Current version:"
-    latest-version: "Latest version:"
-    update-checking: "Checking for updates"
-    do-update: "Check for updates"
-    update-settings: "Advanced settings"
-    no-updates: "No updates are available"
-    no-updates-desc: "Your Misskey is up to date."
-    update-available: "A new version is available"
-    update-available-desc: "Updates will be applied after reloading the page."
-    advanced-settings: "Advanced Settings"
-    debug-mode: "Enable debug mode"
-    debug-mode-desc: "This setting is stored in the browser."
-    navbar-position: "Navbar position"
-    navbar-position-top: "Top"
-    navbar-position-left: "Left"
-    navbar-position-right: "Right"
-    i-am-under-limited-internet: "I have limited bandwidth"
-    post-style: "Note display style"
-    post-style-standard: "Standard"
-    post-style-smart: "Smart"
-    notification-position: "Show notifications"
-    notification-position-bottom: "Bottom"
-    notification-position-top: "Top"
-    disable-via-mobile: "Don't mark the post as 'from mobile'"
-    load-raw-images: "Show attached images in original quality"
-    load-remote-media: "Show media from a remote server"
-    sync: "Sync"
-    save: "Save"
-    saved: "Saved"
-    preview: "Preview"
-    home-profile: "Home profile"
-    deck-profile: "Deck profile"
-    room: "Room"
-    _room:
-      graphicsQuality: "Graphics Quality"
-      _graphicsQuality:
-        ultra: "Ultra"
-        high: "High"
-        medium: "Medium"
-        low: "Low"
-        cheep: "Cheep"
-      useOrthographicCamera: "Use Orthographic Camera"
-  search: "Search"
-  delete: "Delete"
-  loading: "Loading"
-  ok: "Confirm"
-  cancel: "Cancel"
-  update-available-title: "Update available"
-  update-available: "A new version of Misskey is now available({newer}, the current version is {current}). Reload the page to apply updates."
-  my-token-regenerated: "Your token has been regenerated, so you will be signed out."
-  hide-password: "Hide Password"
-  show-password: "Show Password"
-  enter-username: "Please enter username"
-  do-not-use-in-production: "This is a development build. Do not use in production."
-  user-suspended: "This user has been suspended."
-  is-remote-user: "The information about this user may not be entirely complete."
-  is-remote-post: "These post contents are mirrored."
-  view-on-remote: "For completion, view it remotely."
-  renoted-by: "Renoted by {user}"
-  no-notes: "Without any notes"
-  turn-on-darkmode: "Switch to Dark mode"
-  turn-off-darkmode: "Light mode"
-  error:
-    title: "Something happened :("
-    retry: "Retry"
-  reversi:
-    drawn: "Draw"
-    my-turn: "Your turn"
-    opponent-turn: "Opponent's turn"
-    turn-of: "{name}'s turn"
-    past-turn-of: "{name}'s turn"
-    won: "{name} won"
-    black: "Black"
-    white: "White"
-    total: "Total"
-    this-turn: "Turn {count}"
-  widgets:
-    analog-clock: "Analog clock"
-    profile: "Profile"
-    calendar: "Calendar"
-    timemachine: "Calendar (Time Machine)"
-    activity: "Activity"
-    rss: "RSS reader"
-    memo: "Sticky note"
-    trends: "Trends"
-    photo-stream: "Photostream"
-    posts-monitor: "Chart of posts"
-    slideshow: "Slideshow"
-    version: "Version"
-    broadcast: "Broadcast"
-    notifications: "Notifications"
-    users: "Recommended users"
-    polls: "Polls"
-    post-form: "Post form"
-    server: "Server info"
-    nav: "Navigation"
-    tips: "Tips"
-    hashtags: "Hashtags"
-    queue: "Queue"
-  dev: "Failed to create the application. Please try again."
-  ai-chan-kawaii: "Ai-chan kawaii!"
-  you: "You"
-auth/views/form.vue:
-  share-access: "Would you allow <i>{name}</i> to access your account?"
-  permission-ask: "This application requires the following permissions:"
-  cancel: "Cancel"
-  accept: "Allow access."
-auth/views/index.vue:
-  loading: "Loading"
-  denied: "Application authorization has been denied."
-  denied-paragraph: "This application will not access your account."
-  already-authorized: "This application has already been authorized."
-  allowed: "Application authorizations allowed."
-  callback-url: "Going back to the application."
-  please-go-back: "Please go back to the application."
-  error: "Session does not exist."
-  sign-in: "Please sign in."
-common/views/pages/explore.vue:
-  pinned-users: "Pinned users"
-  popular-users: "Popular users"
-  recently-updated-users: "Recently active users"
-  recently-registered-users: "Users who joined recently"
-  recently-discovered-users: "Recently Discovered Users"
-  popular-tags: "Popular Tags"
-  federated: "From the fediverse"
-  explore: "Explore {host}"
-  explore-fediverse: "Explore Fediverse"
-  users-info: "Currently, {users} users are registered here"
-common/views/components/reactions-viewer.details.vue:
-  few-users: "{users} reacted with {reaction}"
-  many-users: "{users}, and {omitted} more reacted with {reaction}"
-common/views/components/url-preview.vue:
-  enable-player: "Enable playback"
-  disable-player: "Close the player"
-common/views/components/user-list.vue:
-  no-users: "There are no users."
-common/views/components/games/reversi/reversi.vue:
-  matching:
-    waiting-for: "Waiting for {}"
-    cancel: "Cancel"
-common/views/components/games/reversi/reversi.game.vue:
-  surrender: "Surrender"
-  surrendered: "By surrender"
-  is-llotheo: "The lesser one wins(Llotheo)"
-  looped-map: "Looped map"
-  can-put-everywhere: "Can put everywhere"
-common/views/components/games/reversi/reversi.index.vue:
-  title: "Misskey Reversi"
-  sub-title: "Play reversi with your friends!"
-  invite: "Invite"
-  rule: "How to play"
-  rule-desc: "Reversi is a strategy board game for two players, played on an 8×8 uncheckered board. There are sixty-four identical game pieces called disks (often spelled \"discs\"), which are light on one side and dark on the other. Players take turns placing disks on the board with their assigned color facing up. During a play, any disks of the opponent's color that are in a straight line and bounded by the disk just placed and another disk of the current player's color are turned over to the current player's color. The object of the game is to have the majority of disks turned to display your color when the last playable empty square is filled."
-  mode-invite: "Invite"
-  mode-invite-desc: "Game with a specified user."
-  invitations: "You’ve got an invitation!"
-  my-games: "My game"
-  all-games: "All games"
-  enter-username: "Please enter username"
-  game-state:
-    ended: "Finished"
-    playing: "In Progress"
-common/views/components/games/reversi/reversi.room.vue:
-  settings-of-the-game: "Game settings"
-  choose-map: "Choose a map"
-  random: "Random"
-  black-or-white: "Black/White"
-  black-is: "Black is {}"
-  rules: "Rules"
-  is-llotheo: "The lesser one wins(Llotheo)"
-  looped-map: "Looped map"
-  can-put-everywhere: "Can put everywhere"
-  settings-of-the-bot: "Bot settings"
-  this-game-is-started-soon: "The game will begin in seconds"
-  waiting-for-other: "Waiting for the opponent"
-  waiting-for-me: "Waiting for the your preparation"
-  waiting-for-both: "Preparing"
-  cancel: "Cancel"
-  ready: "Ready"
-  cancel-ready: "Cancel \"Ready\""
-common/views/components/connect-failed.vue:
-  title: "Unable to connect to the server"
-  description: "There is a problem with your Internet connection, or the server may be down or under maintenance. Please {try again} later."
-  thanks: "Thank you for using Misskey."
-  troubleshoot: "Troubleshoot"
-common/views/components/connect-failed.troubleshooter.vue:
-  title: "Troubleshooting"
-  network: "Network connection"
-  checking-network: "Checking network connection"
-  internet: "Internet connection"
-  checking-internet: "Checking Internet connection"
-  server: "Server connection"
-  checking-server: "Checking server connection"
-  finding: "Searching for issues"
-  no-network: "No connection"
-  no-network-desc: "Please make sure that you have a network connection."
-  no-internet: "There is no Internet connection"
-  no-internet-desc: "Please make sure you are connected to the Internet."
-  no-server: "Unable to connect to the Misskey server"
-  no-server-desc: "The network connection of your device is normal, but you could not connect to the Misskey server. There is a possibility that the server is either down, or under maintenance, please try again later."
-  success: "Successfully connected to the Misskey server"
-  success-desc: "Looks like we have a connection. Please reload the page."
-  flush: "Clean cache"
-  set-version: "Specify version"
-common/views/components/media-banner.vue:
-  sensitive: "NSFW"
-  click-to-show: "Click to show"
-common/views/components/theme.vue:
-  theme: "Theme"
-  light-theme: "Theme to use in Light mode"
-  dark-theme: "Theme during dark mode"
-  light-themes: "Light theme"
-  dark-themes: "Dark theme"
-  install-a-theme: "Install a theme"
-  theme-code: "Theme code"
-  install: "Install"
-  installed: "\"{}\" has been installed"
-  create-a-theme: "Create a theme"
-  save-created-theme: "Save theme"
-  primary-color: "Primary color"
-  secondary-color: "Secondary color"
-  text-color: "Text color"
-  base-theme: "Base theme"
-  base-theme-light: "Light"
-  base-theme-dark: "Dark"
-  find-more-theme: "Find more themes"
-  theme-name: "Theme name"
-  preview-created-theme: "Preview"
-  invalid-theme: "Not valid theme"
-  already-installed: "This theme is already installed."
-  saved: "Saved"
-  manage-themes: "Themes manager"
-  builtin-themes: "Standard themes"
-  my-themes: "My themes"
-  installed-themes: "Installed themes"
-  select-theme: "Select your theme"
-  uninstall: "Uninstall"
-  uninstalled: "\"{}\" has been uninstalled"
-  author: "Author"
-  desc: "Description"
-  export: "Export"
-  import: "Import"
-  import-by-code: "or paste code"
-  theme-name-required: "Theme name is required"
-common/views/components/cw-button.vue:
-  hide: "Hide"
-  show: "See more"
-  chars: "{count} chars"
-  files: "{count} files"
-  poll: "Poll"
-common/views/components/messaging.vue:
-  search-user: "Find a user"
-  you: "You"
-  no-history: "Without history"
-  user: "User"
-  group: "Group"
-  start-with-user: "Start chatting with a user"
-  start-with-group: "Start a group and chat"
-  select-group: "Select a group"
-common/views/components/messaging-room.vue:
-  not-talked-user: "You have not talked to this user yet"
-  not-talked-group: "There is no conversation in this group"
-  no-history: "There is no further history"
-  new-message: "New message"
-  only-one-file-attached: "You can only attach one file to a message"
-common/views/components/messaging-room.form.vue:
-  input-message-here: "Enter message here"
-  send: "Send"
-  attach-from-local: "Attach files from your device"
-  attach-from-drive: "Attach files from your Drive"
-  only-one-file-attached: "You can only attach one file to a message"
-common/views/components/messaging-room.message.vue:
-  is-read: "Read"
-  deleted: "This message has been deleted"
-common/views/components/nav.vue:
-  about: "About"
-  stats: "Stats"
-  status: "Status"
-  wiki: "Wiki"
-  donors: "Donators"
-  repository: "Repository"
-  develop: "Developers"
-  feedback: "Feedback"
-  tos: "Terms Of Service"
-common/views/components/note-menu.vue:
-  mention: "Mention"
-  detail: "Details"
-  copy-content: "Copy the contents"
-  copy-link: "Copy link"
-  favorite: "Favorite this note"
-  unfavorite: "Unfavorite"
-  watch: "Watch"
-  unwatch: "Unwatch"
-  pin: "Pin to your profile"
-  unpin: "Unpin"
-  delete: "Delete"
-  delete-confirm: "Are you sure you want to delete this post?"
-  delete-and-edit: "Delete and Edit"
-  delete-and-edit-confirm: "Are you sure you want to delete this note and edit it? You will lose all reactions, renotes and replies to it."
-  remote: "Show original note"
-  pin-limit-exceeded: "You can't pin any more posts."
-common/views/components/user-menu.vue:
-  mention: "Mention"
-  mute: "Mute"
-  unmute: "Unmute"
-  mute-confirm: "Are you sure you want to mute this user?"
-  unmute-confirm: "Are you certain that you want to unmute this user?"
-  block: "Block"
-  unblock: "Unblock"
-  block-confirm: "Are you sure you want to block this user?"
-  unblock-confirm: "Are you certain that you want to unblock this user?"
-  push-to-list: "Add to list"
-  select-list: "Select a list"
-  report-abuse: "Report abuse"
-  report-abuse-detail: "What kind of nuisance did you encounter?"
-  report-abuse-reported: "The issue has been reported to the administrator. Your cooperation is much appreciated."
-  silence: "Silence"
-  unsilence: "Unsilence"
-  silence-confirm: "Are you sure that you want to silence this user?"
-  unsilence-confirm: "Are you sure that you want to stop silencing this user?"
-  suspend: "Suspend"
-  unsuspend: "Unsuspend"
-  suspend-confirm: "Are you sure that you want to suspend this user?"
-  unsuspend-confirm: "Are you sure that you want to unsuspend this user?"
-common/views/components/poll.vue:
-  vote-to: "Vote for '{}'"
-  vote-count: "{} votes"
-  total-votes: "{} votes in total"
-  vote: "Vote"
-  show-result: "Show results"
-  voted: "Voted"
-  closed: "Ended"
-  remaining-days: "{d} days, {h} hours remain"
-  remaining-hours: "{h} hours, and {m} minutes remain"
-  remaining-minutes: "{m} minutes, and {s} seconds remaining"
-  remaining-seconds: "{s} seconds remaining"
-common/views/components/poll-editor.vue:
-  no-only-one-choice: "At least two choices are required"
-  choice-n: "Choice {}"
-  remove: "Delete the choice"
-  add: "+ Add a choice"
-  destroy: "Discard the poll"
-  multiple: "More than one answer is allowed"
-  expiration: "Valid until"
-  infinite: "Indefinitely"
-  at: "Date and time pick"
-  after: "Progression specifics"
-  no-more: "You cannot add any more"
-  deadline-date: "Finish date"
-  deadline-time: "Time duration"
-  interval: "Duration"
-  unit: "Unit"
-  second: "Seconds"
-  minute: "Minutes"
-  hour: "Hours"
-  day: "S"
-common/views/components/reaction-picker.vue:
-  choose-reaction: "Send a reaction"
-  input-reaction-placeholder: "or input Emoji"
-common/views/components/emoji-picker.vue:
-  recent-emoji: "Recently used"
-  custom-emoji: "Custom Emoji"
-  no-category: "Uncategorized"
-  people: "People"
-  animals-and-nature: "Animals & Nature"
-  food-and-drink: "Food & drink"
-  activity: "Activity"
-  travel-and-places: "Travel & Places"
-  objects: "Objects"
-  symbols: "Symbols"
-  flags: "Flags"
-common/views/components/settings/app-type.vue:
-  title: "Mode"
-  intro: "You can specify whether you want to use the desktop, or the mobile layout."
-  choices:
-    auto: "Choose layout automatically"
-    desktop: "Always use the desktop layout"
-    mobile: "Always use the mobile layout"
-  info: "You need to reload the page for the changes to take effect."
-common/views/components/signin.vue:
-  username: "Username"
-  password: "Password"
-  token: "Token"
-  signing-in: "Signing in..."
-  or: "Or"
-  signin-with-twitter: "Log in with Twitter"
-  signin-with-github: "Sign in with GitHub"
-  signin-with-discord: "Sign in with Discord"
-  login-failed: "Unable to log in. The username or password you entered is incorrect."
-  tap-key: "Click on the Security Key to log in"
-  enter-2fa-code: "Enter your verification code"
-common/views/components/signup.vue:
-  invitation-code: "Invitation code"
-  invitation-info: "If you do not have an invitation code, please contact an <a href=\"{}\">administrator</a>."
-  username: "Username"
-  checking: "Confirming..."
-  available: "Available"
-  unavailable: "Unavailable"
-  error: "Network error"
-  invalid-format: "letters, numbers and _ are acceptable."
-  too-short: "Should not be blank!"
-  too-long: "Enter within 20 characters."
-  password: "Password"
-  password-placeholder: "More than 8 characters are recommended."
-  weak-password: "Weak password"
-  normal-password: "Fair password"
-  strong-password: "Strong password"
-  retype: "Re-enter"
-  retype-placeholder: "Confirm your password"
-  password-matched: "OK"
-  password-not-matched: "Doesn't match"
-  recaptcha: "Verification"
-  agree-to: "Accept {0}."
-  tos: "Terms Of Service"
-  create: "Create an Account"
-  some-error: "An attempt at account creation has failed for some reason. Please try again."
-common/views/components/special-message.vue:
-  new-year: "Happy New Year!"
-  christmas: "Merry Christmas!"
-common/views/components/stream-indicator.vue:
-  connecting: "Connecting"
-  reconnecting: "Reconnecting"
-  connected: "Connected"
-common/views/components/notification-settings.vue:
-  title: "Notifications"
-  mark-as-read-all-notifications: "Mark all notifications as read"
-  mark-as-read-all-unread-notes: "Mark all posts as read"
-  mark-as-read-all-talk-messages: "Mark all conversations as read"
-  auto-watch: "Automatically watch out for posts"
-  auto-watch-desc: "Automatically receive notifications about posts you react to, or respond to."
-common/views/components/integration-settings.vue:
-  title: "Service cooperation"
-  connect: "Connect"
-  disconnect: "Disconnect"
-  connected-to: "You are connected to this account"
-common/views/components/github-setting.vue:
-  description: "Once you connect your GitHub account to your Misskey account, you will be able to see information about your GitHub account on your profile, and you will be able to sign-in via GitHub."
-  connected-to: "You are connected to this GitHub account"
-  detail: "More..."
-  reconnect: "Reconnect"
-  connect: "Link your GitHub account"
-  disconnect: "Disconnect"
-common/views/components/discord-setting.vue:
-  description: "Once you connect your Discord account to your Misskey account, you will be able to see information from your Discord account on your profile, and you will be able to sign-in using Discord."
-  connected-to: "You are connected to this Discord account"
-  detail: "Details…"
-  reconnect: "Reconnect"
-  connect: "Link your Discord account"
-  disconnect: "Disconnect"
-common/views/components/uploader.vue:
-  waiting: "Waiting"
-common/views/components/visibility-chooser.vue:
-  public: "Public"
-  home: "Home"
-  home-desc: "Post to Home only"
-  followers: "Followers"
-  followers-desc: "Post to Followers only"
-  specified: "Direct"
-  specified-desc: "Post to specified users only"
-  local-public: "Local (Public)"
-  local-public-desc: "Do not publish to remote"
-  local-home: "Home (Only local)"
-  local-followers: "Followers (Only local)"
-common/views/components/trends.vue:
-  count: "{} users mentioned"
-  empty: "No popular hashtag trends"
-common/views/components/language-settings.vue:
-  title: "Display Language"
-  pick-language: "Select a language"
-  recommended: "Recommended"
-  auto: "Auto"
-  specify-language: "Specify language"
-  info: "You need to reload the page for the changes to take effect."
-common/views/components/profile-editor.vue:
-  title: "Profile"
-  name: "Name"
-  account: "Account"
-  location: "Location"
-  description: "About me"
-  you-can-include-hashtags: "You can also include hashtags in your profile description."
-  language: "Language"
-  birthday: "Birthday"
-  avatar: "Avatar"
-  banner: "Banner"
-  is-cat: "This account is a Cat"
-  is-bot: "This account is a Bot"
-  is-locked: "Follower requests require approval"
-  careful-bot: "Follower requests from bots require approval"
-  auto-accept-followed: "Automatically approve follows from the people you follow"
-  advanced: "Other"
-  privacy: "Privacy"
-  save: "Save"
-  saved: "Profile updated successfully"
-  uploading: "Uploading"
-  upload-failed: "Failed to upload"
-  unable-to-process: "The operation could not be completed."
-  avatar-not-an-image: "The file you specified as an avatar is not an image"
-  banner-not-an-image: "The file you specified as a banner is not an image"
-  email: "Email settings"
-  email-address: "Email Address"
-  email-verified: "Your email has been verified."
-  email-not-verified: "Email address is not confirmed. Please check your inbox."
-  export: "Export"
-  import: "Import"
-  export-and-import: "Export and Import"
-  export-targets:
-    all-notes: "All posted Notes"
-    following-list: "List of followers"
-    mute-list: "List of muted accounts"
-    blocking-list: "List of blocked accounts"
-    user-lists: "Lists"
-  export-requested: "You have requested an export. This may take a while. After the export is complete, the resulting file will be added to the drive."
-  import-requested: "You have initiated an import. This may take quite some time."
-  enter-password: "Please enter your password"
-  danger-zone: "Cautious options"
-  delete-account: "Remove the account"
-  account-deleted: "The account has been deleted. It may take some time until all of the data disappears."
-  profile-metadata: "Profile metadata"
-  metadata-label: "Label"
-  metadata-content: "Content"
-common/views/components/user-list-editor.vue:
-  users: "User"
-  rename: "Rename list"
-  delete: "Delete list"
-  remove-user: "Remove from this list"
-  delete-are-you-sure: "Delete list \"$1\"?"
-  deleted: "Deleted successfully"
-  add-user: "Add a user"
-common/views/components/user-group-editor.vue:
-  users: "Members"
-  rename: "Rename group"
-  delete: "Delete group"
-  transfer: "transfer group"
-  transfer-are-you-sure: "Are you sure you want to add @$2 to the group $1?"
-  transferred: "Group transferred"
-  remove-user: "Remove a user from this group"
-  delete-are-you-sure: "Are you sure to delete group \"$1\"?"
-  deleted: "Deleted"
-  invite: "Invite"
-  invited: "The invitation was successfully sent"
-common/views/components/user-lists.vue:
-  user-lists: "Lists"
-  create-list: "Create a list"
-  list-name: "List name"
-common/views/components/user-groups.vue:
-  user-groups: "Groups"
-  create-group: "Create a group"
-  group-name: "Group name"
-  owned-groups: "My groups"
-  joined-groups: "Membership in groups"
-  invites: "Invite"
-  accept-invite: "Join"
-  reject-invite: "Decline"
-common/views/widgets/broadcast.vue:
-  fetching: "Checking"
-  no-broadcasts: "No announcements"
-  have-a-nice-day: "Have a nice day!"
-  next: "Next"
-  prev: "Previous"
-common/views/widgets/calendar.vue:
-  year: "Year {}"
-  month: "{},"
-  day: "{}"
-  today: "Today: "
-  this-month: "Month:"
-  this-year: "Year:"
-common/views/widgets/photo-stream.vue:
-  title: "Photo stream"
-  no-photos: "No photos"
-common/views/widgets/posts-monitor.vue:
-  title: "Chart of posts"
-  toggle: "Toggle views"
-common/views/widgets/hashtags.vue:
-  title: "Hashtags"
-common/views/widgets/server.vue:
-  title: "Server info"
-  toggle: "Toggle views"
-common/views/widgets/memo.vue:
-  title: "Sticky note"
-  memo: "Write here!"
-  save: "Save"
-common/views/widgets/slideshow.vue:
-  folder-customize-mode: "To specify a folder, please exit customization mode"
-  folder: "Please click and specify a folder"
-  no-image: "There is no image in this folder"
-common/views/widgets/tips.vue:
-  tips-line1: "You can focus on the timeline with <kbd>t</kbd>."
-  tips-line2: "Open posting form with <kbd>p</kbd> or <kbd>n</kbd>."
-  tips-line3: "You can drag and drop files on the post form."
-  tips-line4: "You can paste an image from the clipboard into the submission form."
-  tips-line5: "You can upload files by dragging and dropping them to Drive."
-  tips-line6: "You can move a folder by dragging it within the Drive."
-  tips-line7: "You can move folders by dragging them within the Drive."
-  tips-line8: "The Home layout can be customized from the settings."
-  tips-line9: "Misskey is licensed under AGPLv3."
-  tips-line10: "Using the Time Machine widget makes it easy to trace back to the past timeline."
-  tips-line11: "You can pin posts to user page by clicking on \"...\""
-  tips-line13: "All the files attached to the post are saved to Drive."
-  tips-line14: "While customizing your home layout, you can right click on a widget to change its design."
-  tips-line17: "Surrounding the text with ** ** will highlight it."
-  tips-line19: "Several windows can be detached outside the browser."
-  tips-line20: "The percentage of the calendar widget shows the percentage of time elapsed."
-  tips-line21: "You can also use the API to develop bots."
-  tips-line23: "Ai-chan kawaii!"
-  tips-line24: "Misskey has been running since 2014."
-  tips-line25: "In a browser compatible with notification features, you can receive notifications in case Misskey is not open"
-common/views/pages/not-found.vue:
-  page-not-found: "The page has not been found"
-common/views/pages/follow.vue:
-  signed-in-as: "Signed in as {}"
-  following: "Following"
-  follow: "Follow"
-  request-pending: "Pending follow request"
-  follow-processing: "Processing follow"
-  follow-request: "Follow request"
-common/views/pages/follow-requests.vue:
-  received-follow-requests: "Follow requests"
-  accept: "Accept"
-  reject: "Reject"
-desktop:
-  banner-crop-title: "Crop the part that appears as a banner"
-  banner: "Banner"
-  uploading-banner: "Uploading a new banner"
-  banner-updated: "Successfully updated the banner"
-  choose-banner: "Choose the banner"
-  avatar-crop-title: "Crop the part that appears as an avatar"
-  avatar: "Avatar"
-  uploading-avatar: "Uploading a new avatar"
-  avatar-updated: "Successfully updated the avatar"
-  choose-avatar: "Select an image for the avatar"
-  unable-to-process: "The operation could not be completed."
-  invalid-filetype: "This filetype is not acceptable here"
-desktop/views/components/activity.chart.vue:
-  total: "Black ... Total"
-  notes: "Blue ... Notes"
-  replies: "Red ... Replies"
-  renotes: "Green ... Renotes"
-desktop/views/components/activity.vue:
-  title: "Activity"
-  toggle: "Toggle views"
-desktop/views/components/calendar.vue:
-  title: "{year} / {month}"
-  prev: "Previous month"
-  next: "Next month"
-  go: "Click to navigate"
-desktop/views/components/choose-file-from-drive-window.vue:
-  chosen-files: "{count} File(s) selected"
-  upload: "Upload files from your device"
-  cancel: "Cancel"
-  ok: "OK"
-  choose-prompt: "Choose files"
-desktop/views/components/choose-folder-from-drive-window.vue:
-  cancel: "Cancel"
-  ok: "OK"
-  choose-prompt: "Choose a folder"
-desktop/views/components/crop-window.vue:
-  skip: "Skip cropping"
-  cancel: "Cancel"
-  ok: "OK"
-desktop/views/components/drive-window.vue:
-  used: "used"
-desktop/views/components/drive.file.vue:
-  avatar: "Avatar"
-  banner: "Banner"
-  nsfw: "NSFW"
-  contextmenu:
-    rename: "Rename"
-    mark-as-sensitive: "Mark as 'sensitive'"
-    unmark-as-sensitive: "Unmark as 'sensitive'"
-    copy-url: "Copy URL"
-    download: "Download"
-    else-files: "Other"
-    set-as-avatar: "Set as avatar"
-    set-as-banner: "Set as a banner"
-    open-in-app: "Open in app"
-    add-app: "Add app"
-    rename-file: "Rename file"
-    input-new-file-name: "Enter new name"
-    copied: "Copied"
-    copied-url-to-clipboard: "URL has been copied to clipboard"
-desktop/views/components/drive.folder.vue:
-  upload-folder: "Default Upload location"
-  unable-to-process: "The operation could not be completed."
-  circular-reference-detected: "The destination folder is a subfolder of the folder you wish to move."
-  unhandled-error: "Unknown error"
-  unable-to-delete: "Unable to delete"
-  has-child-files-or-folders: "Since this folder is not empty, it can not be deleted."
-  contextmenu:
-    move-to-this-folder: "Move to this folder"
-    show-in-new-window: "Open in new window"
-    rename: "Rename"
-    rename-folder: "Rename folder"
-    input-new-folder-name: "Enter new name"
-    else-folders: "Other"
-    set-as-upload-folder: "Set as default upload folder"
-desktop/views/components/drive.vue:
-  search: "Search"
-  empty-draghover: "Drop it here! Yep, cuz you know I'm cute, right?"
-  empty-drive: "Your media storage is empty"
-  empty-drive-description: "Right-click to open the menu, or drag and drop a file onto here for uploading."
-  empty-folder: "This folder is empty"
-  unable-to-process: "The operation could not be completed."
-  circular-reference-detected: "The destination folder is a subfolder of the folder you wish to move."
-  unhandled-error: "Unknown error"
-  url-upload: "Upload from a URL"
-  url-of-file: "URL of file you want to upload"
-  url-upload-requested: "Upload requested"
-  may-take-time: "It may take some time until the upload is complete."
-  create-folder: "Create a folder"
-  folder-name: "Folder name"
-  contextmenu:
-    create-folder: "Create a folder"
-    upload: "Upload a file"
-    url-upload: "Upload from a URL"
-desktop/views/components/media-video.vue:
-  sensitive: "The content is NSFW"
-  click-to-show: "Click to show"
-desktop/views/components/followers-window.vue:
-  followers: "{}'s followers"
-desktop/views/components/followers.vue:
-  empty: "Seems like you don’t have any followers."
-desktop/views/components/following-window.vue:
-  following: "Following {}"
-desktop/views/components/following.vue:
-  empty: "It seems you don't have any following users…"
-desktop/views/components/game-window.vue:
-  game: "Reversi"
-desktop/views/components/home.vue:
-  done: "Done"
-  add-widget: "Add widget:"
-  add: "Add"
-desktop/views/input-dialog.vue:
-  cancel: "Cancel"
-  ok: "OK"
-desktop/views/components/note-detail.vue:
-  private: "Post is private"
-  deleted: "Post has been removed"
-  location: "Location"
-  renote: "Renote"
-  add-reaction: "Add a reaction"
-  undo-reaction: "Reverse reaction"
-desktop/views/components/note.vue:
-  reply: "Reply"
-  renote: "Renote"
-  add-reaction: "Add a reaction"
-  undo-reaction: "Reverse reaction"
-  detail: "Details"
-  private: "This post is private"
-  deleted: "This post has been deleted"
-desktop/views/components/notes.vue:
-  error: "Loading failed."
-  retry: "Retry"
-desktop/views/components/notifications.vue:
-  empty: "No notifications!"
-desktop/views/components/post-form.vue:
-  posted: "Posted!"
-  replied: "Replied!"
-  reposted: "Renoted!"
-  note-failed: "Failed to post"
-  reply-failed: "Failed to reply"
-  renote-failed: "Failed to Renote"
-desktop/views/components/post-form-window.vue:
-  note: "New Post"
-  reply: "Reply"
-  attaches: "{} media attached"
-  uploading-media: "Uploading {} media"
-desktop/views/components/progress-dialog.vue:
-  waiting: "Waiting"
-desktop/views/components/renote-form.vue:
-  quote: "Quote..."
-  cancel: "Cancel"
-  renote: "Renote"
-  renote-home: "Renote (Home)"
-  reposting: "Renoting..."
-  success: "Renoted!"
-  failure: "Failed to Renote"
-desktop/views/components/renote-form-window.vue:
-  title: "Do you want to renote it?"
-desktop/views/pages/user-following-or-followers.vue:
-  following: "{user}'s following"
-  followers: "{user}'s follower"
-desktop/views/components/settings.2fa.vue:
-  intro: "If you set up 2-step verification, you will not only need a password at sign-in, but also a pre-registered physical device (such as your smartphone), which will improve security."
-  detail: "Details…"
-  url: "https://www.google.com/landing/2step/"
-  caution: "If you lose access to your registered device, you won't be able to connect to Misskey anymore!"
-  register: "Register a device"
-  already-registered: "This device is already registered"
-  unregister: "Unregister"
-  unregistered: "Two-factor authentication has been disabled."
-  enter-password: "Enter the password"
-  authenticator: "First, you need to install Google Authenticator on your device:"
-  howtoinstall: "How to install"
-  token: "Token"
-  scan: "And then, scan the QR code:"
-  done: "Please enter the token displayed on your device:"
-  submit: "Submit"
-  success: "Settings saved!"
-  failed: "Failed to setup. Please ensure that the token is correct."
-  info: "From the next time you sign in to Misskey, the token displayed on your device will be necessary too, as well as the password."
-  totp-header: "Authenticator App"
-  security-key-header: "Security Key"
-  security-key: "For additional security, you can log in to your account using a hardware Security Key that supports FIDO2. When you then sign in, you'll need the registered Security Key, or an authenticator app with you."
-  last-used: "Last used:"
-  activate-key: "Click to activate the Security Key"
-  security-key-name: "Name the Key"
-  register-security-key: "Complete Key registration"
-  something-went-wrong: "Wow! There was a problem registering the Key:"
-  key-unregistered: "The Key has been deleted"
-  use-password-less-login: "Use Password-less login"
-common/views/components/media-image.vue:
-  sensitive: "NSFW"
-  click-to-show: "Click to show"
-common/views/components/api-settings.vue:
-  intro: "To access the API, set this token as the key 'i' of request parameters."
-  caution: "Do not enter this token to any apps nor tell this token to others otherwise your account may get compromised."
-  regeneration-of-token: "If your token gets leaked, you can regenerate it."
-  regenerate-token: "Regenerate the token"
-  token: "Token:"
-  enter-password: "Enter the password"
-  console:
-    title: "API console"
-    endpoint: "Endpoint"
-    parameter: "Parameters"
-    credential-info: "Parameter \"i\" is not required at this console."
-    send: "Send"
-    sending: "Sending"
-    response: "Result"
-desktop/views/components/settings.apps.vue:
-  no-apps: "No linked applications"
-common/views/components/drive-settings.vue:
-  max: "Max"
-  in-use: "In use"
-  stats: "Statistics"
-  default-upload-folder: "Default upload folder location"
-  default-upload-folder-name: "Folder(s)"
-  change-default-upload-folder: "Change folder"
-common/views/components/mute-and-block.vue:
-  mute-and-block: "Mute / Block"
-  mute: "Mute"
-  block: "Blocking"
-  no-muted-users: "No muted users"
-  no-blocked-users: "No blocked users"
-  word-mute: "Word mute"
-  muted-words: "Muted keywords"
-  muted-words-description: "Separating with spaces results in AND specifications, and delimiting with line breaks results in OR specifications"
-  unmute-confirm: "Are you certain that you want to unmute this user?"
-  unblock-confirm: "Are you certain that you want to unblock this user?"
-  save: "Save"
-common/views/components/password-settings.vue:
-  reset: "Change password"
-  enter-current-password: "Enter the current password"
-  enter-new-password: "Enter the new password"
-  enter-new-password-again: "Enter the new password again"
-  not-match: "The new passwords do not match"
-  changed: "Password changed"
-  failed: "Failed to change password"
-common/views/components/post-form-attaches.vue:
-  attach-cancel: "Remove Attachment"
-  mark-as-sensitive: "Mark as 'sensitive'"
-  unmark-as-sensitive: "Unmark as 'sensitive'"
-desktop/views/components/sub-note-content.vue:
-  private: "This post is private"
-  deleted: "This post has been deleted"
-  media-count: "{} media attached"
-  poll: "Poll"
-desktop/views/components/settings.tags.vue:
-  title: "Tags"
-  query: "Query (optional)"
-  add: "Add"
-  save: "Save"
-desktop/views/components/timeline.vue:
-  home: "Home"
-  local: "Local"
-  hybrid: "Social"
-  global: "Global"
-  mentions: "Mentions"
-  messages: "Direct posts"
-  list: "Lists"
-  hashtag: "Hashtag"
-  add-tag-timeline: "Add hashtag cloud"
-  add-list: "Add list"
-  list-name: "List name"
-desktop/views/components/ui.header.vue:
-  welcome-back: "Welcome back,"
-  adjective: "-san"
-desktop/views/components/ui.header.account.vue:
-  profile: "Your profile"
-  lists: "Lists"
-  groups: "Groups"
-  follow-requests: "Follow requests"
-  admin: "Admin"
-  room: "Room"
-desktop/views/components/ui.header.nav.vue:
-  game: "Games"
-desktop/views/components/ui.header.notifications.vue:
-  title: "Notifications"
-desktop/views/components/ui.header.post.vue:
-  post: "Compose new Post"
-desktop/views/components/ui.header.search.vue:
-  placeholder: "Search"
-desktop/views/components/user-preview.vue:
-  notes: "Posts"
-  following: "Following"
-  followers: "Followers"
-desktop/views/components/users-list.vue:
-  all: "All"
-  iknow: "You know"
-  fetching: "Loading…"
-desktop/views/components/users-list-item.vue:
-  followed: "Follows you"
-desktop/views/components/window.vue:
-  popout: "Pop-out"
-  close: "Close"
-admin/views/index.vue:
-  dashboard: "Dashboard"
-  instance: "Instance"
-  emoji: "Emoji"
-  moderators: "Moderators"
-  users: "Users"
-  federation: "Federation"
-  announcements: "Announcements"
-  abuse: "Abuse"
-  queue: "Job Queue"
-  logs: "Logs"
-  db: "Database"
-  back-to-misskey: "Back to Misskey"
-admin/views/db.vue:
-  tables: "Tables"
-  vacuum: "Vacuum"
-  vacuum-info: "Tidies up the database. Keeps the data intact and reduces disk usage. This is usually done automatically and periodically."
-  vacuum-exclamation: "Vacuuming can overload the database for a while, and cause users not to be able to participate in interactions."
-admin/views/dashboard.vue:
-  dashboard: "Dashboard"
-  accounts: "Accounts"
-  notes: "Notes"
-  drive: "Drive"
-  instances: "Instances"
-  this-instance: "This instance"
-  federated: "Federated"
-admin/views/queue.vue:
-  title: "Queue"
-  remove-all-jobs: "Clear all queued jobs"
-  jobs: "Jobs"
-  queue: "Queue"
-  domains:
-    deliver: "Delivers"
-    inbox: "Received"
-    db: "Database"
-    objectStorage: "Object Storage"
-  state: "Sort"
-  states:
-    active: "Running"
-    delayed: "Scheduled"
-    waiting: "Queued"
-  result-is-truncated: "Result is truncated"
-  other-queues: "Other queues"
-admin/views/logs.vue:
-  logs: "Logs"
-  domain: "Domain"
-  level: "Level"
-  levels:
-    all: "All"
-    info: "Information"
-    success: "Success"
-    warning: "Warning"
-    error: "Error"
-    debug: "Debug"
-  delete-all: "Remove All"
-admin/views/abuse.vue:
-  title: "Abuse"
-  target: "Target"
-  reporter: "Reporter"
-  details: "Details"
-  remove-report: "Remove"
-admin/views/instance.vue:
-  instance: "Instance"
-  instance-name: "Instance name"
-  instance-description: "Instance description"
-  host: "Host"
-  icon-url: "URL of the icon"
-  logo-url: "URL of the logo"
-  banner-url: "Banner image URL"
-  error-image-url: "Error image URL"
-  languages: "Language of this instance"
-  languages-desc: "You can add more than one, separated by spaces."
-  tos-url: "Terms of Service URL"
-  repository-url: "Repository URL"
-  feedback-url: "URL for feedback"
-  maintainer-config: "Administrator information"
-  maintainer-name: "Administrator name"
-  maintainer-email: "Contact Administrator"
-  advanced-config: "Other settings"
-  note-and-tl: "Notes and timelines"
-  drive-config: "Drive settings"
-  use-object-storage: "Use Object Storage"
-  object-storage-base-url: "URL"
-  object-storage-bucket: "Bucket Name"
-  object-storage-prefix: "Prefix"
-  object-storage-endpoint: "Endpoint"
-  object-storage-region: "Region"
-  object-storage-port: "Port"
-  object-storage-access-key: "Access Key"
-  object-storage-secret-key: "Secret Key"
-  object-storage-use-ssl: "Use SSL"
-  object-storage-s3-info: "If you are going to use Amazon S3 as Object Storage, Please refer {0} to configure 'Endpoint' and 'Region'."
-  object-storage-s3-info-here: "here"
-  object-storage-gcs-info: "If you are going to use Google Cloud Storage as Object Storage, Set the 'Endpoint' as storage.googleapis.com, and keep the 'Region' is blank."
-  cache-remote-files: "Cache remote files"
-  cache-remote-files-desc: "If disabled, All remote files going to be linked to their origin server directly. This will be an effective solution to save your server storage. However, Since no thumbnail will be generated, It will make increasing data usage, and also may remote files are invisible to users who set direct-link disabled. It is recommended that this config set enabled or enabling the next config, 'Proxy remote files'."
-  proxy-remote-files: "Proxy remote files"
-  proxy-remote-files-desc: "If enabled, Remote files that not stored locally or deleted by storage overusage will be proxied locally and also thumbnails will be generated."
-  local-drive-capacity-mb: "Volume of Drive per user"
-  remote-drive-capacity-mb: "Volume of Drive per remote user"
-  mb: "In megabytes"
-  recaptcha-config: "the reCAPTCHA settings"
-  recaptcha-info: "reCAPTCHA token is required. Please get it on https://www.google.com/recaptcha/intro/"
-  recaptcha-info2: "v3 is not supported. Please use v2."
-  enable-recaptcha: "enable reCAPTCHA"
-  recaptcha-site-key: "Site key"
-  recaptcha-secret-key: "Secret Key"
-  recaptcha-preview: "Preview"
-  hidden-tags: "Hidden hashtags"
-  hidden-tags-info: "List up the hashtags delimited by line breaks that you want exclude from statistics."
-  external-service-integration-config: "Connect an external service"
-  twitter-integration-config: "Settings of connecting to Twitter"
-  twitter-integration-info: "The callback URL is set on {url}."
-  enable-twitter-integration: "Enable connection to Twitter"
-  twitter-integration-consumer-key: "Consumer key"
-  twitter-integration-consumer-secret: "Consumer Secret"
-  github-integration-config: "Setting of connecting to GitHub"
-  github-integration-info: "The callback URL is set on {url}."
-  enable-github-integration: "Enable connection to GitHub"
-  github-integration-client-id: "Client ID"
-  github-integration-client-secret: "Client Secret"
-  discord-integration-config: "Discord Integration settings"
-  discord-integration-info: "The callback URL is set to {url}."
-  enable-discord-integration: "Enable Discord connection"
-  discord-integration-client-id: "Client ID"
-  discord-integration-client-secret: "Client Secret"
-  proxy-account-config: "Proxy account"
-  proxy-account-info: "A proxy account is an account that acts as a remote follower for users under certain conditions. For example, when a user adds a remote user to the list, the activity will not be delivered to the server if no one is following that user, so the proxy account will follow instead."
-  proxy-account-username: "Proxy account user name"
-  proxy-account-username-desc: "Specify the user name of the account that is used as a proxy."
-  proxy-account-warn: "You must make an account having this username before this action."
-  max-note-text-length: "Maximum numbers of post characters"
-  disable-registration: "Disable new user registration"
-  disable-local-timeline: "Disable the Local Timeline"
-  disable-global-timeline: "Disable global timeline"
-  disabling-timelines-info: "Even if you disable these timelines, the administrator as well as moderators can use them continually."
-  enable-emoji-reaction: "Enable pictograms for reactions"
-  use-star-for-reaction-fallback: "Use the star as fallback for unknown reaction"
-  invite: "Invite"
-  save: "Save"
-  saved: "Saved"
-  pinned-users: "Pinned user"
-  pinned-users-info: "List up the users delimited by line breaks that you want to show as 'Pinned Users'."
-  email-config: "Email server settings"
-  email-config-info: "Used to confirm email and password reset etc."
-  enable-email: "Enable email delivery"
-  email: "Email Address"
-  smtp-secure: "Use implicit SSL/TLS in the SMTP connection"
-  smtp-secure-info: "Turn off STARTTLS when used that."
-  smtp-host: "SMTP Host"
-  smtp-port: "SMTP Port"
-  smtp-auth: "Perform SMTP authentication"
-  smtp-user: "SMTP User"
-  smtp-pass: "SMTP Password"
-  test-email: "Test"
-  serviceworker-config: "ServiceWorker"
-  enable-serviceworker: "Enable ServiceWorker"
-  serviceworker-info: "Must be enabled for push notifications."
-  vapid-publickey: "VAPID public key"
-  vapid-privatekey: "VAPID private key"
-  vapid-info: "If you want to enable ServiceWorker, you need to generate VAPID keys. Unless you have set your global node_modules location elsewhere, you need to run this as root:"
-admin/views/charts.vue:
-  title: "Chart"
-  per-day: "per Day"
-  per-hour: "per Hour"
-  federation: "Federation"
-  notes: "Posts"
-  users: "Users"
-  drive: "Media storage"
-  network: "Network"
-  charts:
-    federation-instances: "The number of instances: increase/decrease"
-    federation-instances-total: "Total number of instances"
-    notes: "Increase, or decrease in the number of posts (Combined)"
-    local-notes: "Increase, or decrease in the number of posts (Local)"
-    remote-notes: "Increase, or decrease in the number of posts (Remote)"
-    notes-total: "Total posts"
-    users: "The number of users: increase/decrease"
-    users-total: "Total users"
-    active-users: "Active users"
-    drive: "Increase and decrease in storage capacity use"
-    drive-total: "Total usage of Drive"
-    drive-files: "The number of files on the storage: increase/decrease"
-    drive-files-total: "Total number of files on Drive"
-    network-requests: "Requests"
-    network-time: "Response time"
-    network-usage: "Traffic"
-admin/views/drive.vue:
-  operation: "Operations"
-  fileid-or-url: "File ID or URL"
-  file-not-found: "File not found"
-  lookup: "Look up"
-  sort:
-    title: "Sort"
-    createdAtAsc: "Age - Oldest First"
-    createdAtDesc: "Age - Newest First"
-    sizeAsc: "Size - Smallest First"
-    sizeDesc: "Size - Largest First"
-  origin:
-    title: "Origin"
-    combined: "Local + Remote"
-    local: "Local"
-    remote: "Remote"
-  delete: "Delete"
-  deleted: "Deleted successfully"
-  mark-as-sensitive: "Mark as 'sensitive'"
-  unmark-as-sensitive: "Unmark as 'sensitive'"
-  marked-as-sensitive: "Set a sensitive content notice"
-  unmarked-as-sensitive: "Remove the sensitive content notice"
-  clean-remote-files: "Clear the remote files cache"
-  clean-remote-files-are-you-sure: "Are you sure you want to remove all cached files from remote?"
-  clean-up: "Clean up"
-admin/views/users.vue:
-  operation: "Operations"
-  username-or-userid: "Username or user ID"
-  user-not-found: "User not found"
-  lookup: "Look up"
-  reset-password: "Reset password"
-  reset-password-confirm: "Do you want to reset your password?"
-  password-updated: "The password is now \"{password}\""
-  suspend: "Suspend"
-  suspend-confirm: "Do you want to suspend this account?"
-  suspended: "Successfully suspended."
-  unsuspend: "Unsuspend"
-  unsuspend-confirm: "Are you sure you want to unsuspend this account?"
-  unsuspended: "The user has successfully unsuspended."
-  make-silence: "Silence"
-  silence-confirm: "Silence user?"
-  unmake-silence: "Unsilence"
-  unsilence-confirm: "Are you certain that you want to stop silencing this user?"
-  update-remote-user: "Update information about remote user"
-  remote-user-updated: "The information regarding the remote user has been updated."
-  delete-all-files: "Delete all files"
-  delete-all-files-confirm: "Are you sure that you want to delete all files?"
-  username: "Username"
-  host: "Host"
-  users:
-    title: "Users"
-    sort:
-      title: "Sort"
-      createdAtAsc: "Date Registered (Ascending)"
-      createdAtDesc: "Date Registered (Descending)"
-      updatedAtAsc: "Last Updated (Ascending)"
-      updatedAtDesc: "Last Updated (Descending)"
-    state:
-      title: "Sort"
-      all: "All"
-      available: "Available"
-      admin: "Administrator"
-      moderator: "Moderator"
-      adminOrModerator: "Admin/Moderator"
-      silenced: "Already silenced"
-      suspended: "Suspended"
-    origin:
-      title: "Origin"
-      combined: "Local + Remote"
-      local: "Local"
-      remote: "Remote"
-    createdAt: "Created at"
-    updatedAt: "Updated at"
-admin/views/moderators.vue:
-  add-moderator:
-    title: "Register Moderator"
-    add: "Register"
-    added: "Registered a Moderator."
-    remove: "Discharge"
-    removed: "The moderator has been discharged"
-  logs:
-    title: "Logs"
-    moderator: "Moderators"
-    type: "Operations"
-    at: "Timestamp"
-    info: "Information"
-admin/views/emoji.vue:
-  add-emoji:
-    title: "Add emoji"
-    name: "Emoji name"
-    name-desc: "You can use the characters a~z 0~9 _"
-    category: "Categories"
-    aliases: "Aliases"
-    aliases-desc: "You can add more than one, separated by spaces."
-    url: "Image URL"
-    add: "Add"
-    info: "We recommend PNG images under 50KB."
-    added: "Emoji was added"
-  emojis:
-    title: "Emojis"
-    update: "Update"
-    remove: "Remove"
-  updated: "Updated"
-  remove-emoji:
-    are-you-sure: "Delete \"$1\"?"
-    removed: "Deleted"
-admin/views/announcements.vue:
-  announcements: "Announcements"
-  save: "Save"
-  remove: "Remove"
-  add: "Add"
-  title: "Title"
-  text: "Content"
-  saved: "Saved"
-  _remove:
-    are-you-sure: "Delete \"$1\"?"
-    removed: "Deleted"
-admin/views/hashtags.vue:
-  hided-tags: "Hidden Tags"
-admin/views/federation.vue:
-  instance: "Instance"
-  host: "Host"
-  notes: "Notes"
-  users: "Users"
-  following: "Following"
-  followers: "Followers"
-  caught-at: "Created at"
-  status: "Statuses"
-  latest-request-sent-at: "Time of last request sent"
-  latest-request-received-at: "Last request received at"
-  remove-all-following: "Withold all followers"
-  remove-all-following-info: "Unfollow all accounts from {host}. Please run this if the instance no longer exists."
-  delete-all-files: "Remove all files"
-  block: "Block"
-  marked-as-closed: "Marked as closed"
-  lookup: "Look up"
-  instances: "Federated"
-  instance-not-registered: "The instance has not been discovered"
-  sort: "Sort by"
-  sorts:
-    caughtAtAsc: "Date of discovery (Ascending)"
-    caughtAtDesc: "Date of discovery (Descending)"
-    lastCommunicatedAtAsc: "The date and time of the older interactions"
-    lastCommunicatedAtDesc: "The date and time of the newer interactions"
-    notesAsc: "Least Notes posted"
-    notesDesc: "Most Notes posted"
-    usersAsc: "Less followers"
-    usersDesc: "More followers"
-    followingAsc: "Least followed"
-    followingDesc: "Most followed"
-    followersAsc: "Having less followers"
-    followersDesc: "The largest number of followers"
-    driveUsageAsc: "Least storage used"
-    driveUsageDesc: "Most storage used"
-    driveFilesAsc: "Least files stored on Drive"
-    driveFilesDesc: "The largest number of files stored on Drive"
-  state: "Sort"
-  states:
-    all: "All"
-    blocked: "Blocked"
-    not-responding: "Without response"
-    marked-as-closed: "Marked as closed"
-  result-is-truncated: "Displaying the top {n} items."
-  charts: "Charts"
-  chart-srcs:
-    requests: "Requests"
-    users: "Increase, or decrease in the number of users"
-    users-total: "Users in total"
-    notes: "Increase, or decrease in the number of notes"
-    notes-total: "Total number of notes"
-    ff: "Increase of followers"
-    ff-total: "Total number of follows accumulated"
-    drive-usage: "Increase and decrease in storage use"
-    drive-usage-total: "Total usage of the Drive"
-    drive-files: "Increase, or decrease in the number of files stored on Drive"
-    drive-files-total: "The number of files accumulated on Drive"
-  chart-spans:
-    hour: "Hourly"
-    day: "Daily"
-  blocked-hosts: "Blocking"
-  blocked-hosts-info: "List up the hosts delimited by line breaks that you want to block."
-  save: "Save"
-desktop/views/pages/welcome.vue:
-  about: "More details..."
-  timeline: "Timeline"
-  announcements: "Announcements"
-  photos: "Recent Images"
-  powered-by-misskey: "Powered by <b>Misskey</b>."
-  info: "Information"
-desktop/views/pages/drive.vue:
-  title: "Misskey storage"
-desktop/views/pages/note.vue:
-  prev: "Previous post"
-  next: "Next post"
-desktop/views/pages/selectdrive.vue:
-  title: "Choose file(s)"
-  ok: "OK"
-  cancel: "Cancel"
-  upload: "Upload files from your device"
-desktop/views/pages/search.vue:
-  not-available: "Search feature is turned off in the settings for this instance."
-  not-found: "No posts were found for '{q}'"
-desktop/views/pages/tag.vue:
-  no-posts-found: "No posts contains \"{q}\" found."
-desktop/views/pages/user-list.users.vue:
-  users: "User"
-  add-user: "Add a user"
-  username: "Username"
-desktop/views/pages/user/user.followers-you-know.vue:
-  title: "Followers you may know"
-  loading: "Loading"
-  no-users: "No followers you know"
-desktop/views/pages/user/user.friends.vue:
-  title: "Frequent mentions"
-  loading: "Loading"
-  no-users: "No frequent mentions"
-desktop/views/pages/user/user.photos.vue:
-  title: "Photos"
-  loading: "Loading"
-  no-photos: "No photos"
-desktop/views/pages/user/user.header.vue:
-  posts: "Notes"
-  following: "Following"
-  followers: "Followers"
-  is-bot: "This account is a Bot"
-  no-description: "This user has not written their profile introduction"
-  years-old: "{age} years old"
-  year: "/"
-  month: "/"
-  day: "-"
-  follows-you: "Follows you"
-desktop/views/pages/user/user.timeline.vue:
-  default: "Posts"
-  with-replies: "Posts and replies"
-  with-media: "Media"
-  my-posts: "My posts"
-desktop/views/widgets/notifications.vue:
-  title: "Notifications"
-desktop/views/widgets/polls.vue:
-  title: "Polls"
-  refresh: "refresh"
-  nothing: "No polls found!"
-desktop/views/widgets/post-form.vue:
-  title: "Post"
-  note: "Post"
-  something-happened: "Could not be posted in this circumstance."
-desktop/views/widgets/profile.vue:
-  update-banner: "Click to edit your banner"
-  update-avatar: "Click to edit your avatar"
-desktop/views/widgets/trends.vue:
-  title: "Trend"
-  refresh: "refresh"
-  nothing: "No trends found!"
-desktop/views/widgets/users.vue:
-  title: "Recommended users"
-  refresh: "refresh"
-  no-one: "Anyone!"
-mobile/views/components/drive.vue:
-  used: "used"
-  folder-count: "Folder(s)"
-  count-separator: ", "
-  file-count: "File(s)"
-  nothing-in-drive: "There's nothing stored."
-  folder-is-empty: "This folder is empty"
-  folder-name: "Folder name"
-  here-is-root: "Currently, you are on the root, not inside of any folder."
-  url-prompt: "URL of the file you want to upload"
-  uploading: "Upload requested. It may take a while for the upload to finish."
-  folder-name-cannot-empty: "Folder name cannot be blank."
-mobile/views/components/drive-file-chooser.vue:
-  select-file: "Choose files"
-mobile/views/components/drive-folder-chooser.vue:
-  select-folder: "Choose a folder"
-mobile/views/components/drive.file.vue:
-  nsfw: "NSFW"
-mobile/views/components/drive.file-detail.vue:
-  download: "Download"
-  rename: "Rename"
-  move: "Move"
-  hash: "Hash (md5)"
-  exif: "EXIF"
-  nsfw: "NSFW"
-  mark-as-sensitive: "Mark as 'sensitive'"
-  unmark-as-sensitive: "Unmark as 'sensitive'"
-mobile/views/components/media-video.vue:
-  sensitive: "The content is NSFW"
-  click-to-show: "Click to show"
-common/views/components/follow-button.vue:
-  following: "Following"
-  follow: "Follow"
-  request-pending: "Pending"
-  follow-processing: "Processing"
-  follow-request: "Follow request"
-mobile/views/components/note.vue:
-  private: "This post is private"
-  deleted: "This post has been deleted"
-  location: "Location"
-mobile/views/components/note-detail.vue:
-  reply: "Reply"
-  reaction: "Reaction"
-  private: "This post is private"
-  deleted: "This post has been deleted"
-  location: "Location"
-mobile/views/components/note-preview.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "cat"
-mobile/views/components/note-sub.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "cat"
-mobile/views/components/notifications.vue:
-  empty: "No notifications"
-mobile/views/components/sub-note-content.vue:
-  private: "This post is private"
-  deleted: "This post has been deleted"
-  media-count: "{} media attached"
-  poll: "Poll"
-mobile/views/components/ui.header.vue:
-  welcome-back: "Welcome back, "
-  adjective: "Sir"
-mobile/views/components/ui.nav.vue:
-  timeline: "Timeline"
-  notifications: "Notifications"
-  follow-requests: "Follow requests"
-  search: "Search"
-  user-lists: "Lists"
-  user-groups: "Groups"
-  widgets: "Widgets"
-  game: "Games"
-  admin: "Admin"
-  about: "About Misskey"
-mobile/views/pages/drive.vue:
-  contextmenu:
-    upload: "Upload a file"
-    url-upload: "Upload file from a URL"
-    create-folder: "Create a folder"
-    rename-folder: "Rename folder"
-    move-folder: "Move this folder"
-    delete-folder: "Delete this folder"
-mobile/views/pages/signup.vue:
-  lets-start: "Your account is now ready! 📦"
-mobile/views/pages/followers.vue:
-  followers-of: "{name}'s followers"
-mobile/views/pages/following.vue:
-  following-of: "{name}'s following"
-mobile/views/pages/home.vue:
-  home: "Home"
-  local: "Local"
-  hybrid: "Social"
-  global: "Global"
-  mentions: "Mentions"
-  messages: "Direct posts"
-mobile/views/pages/tag.vue:
-  no-posts-found: "No posts contains \"{q}\" found."
-mobile/views/pages/widgets.vue:
-  dashboard: "Dashboard"
-  widgets-hints: "You can add/delete/rearrange widgets. To move the widget, drag \"三\". Tap \"x\" to delete the widget. Some widgets can change display by tapping."
-  add-widget: "Add"
-  customization-tips: "Customization tips"
-mobile/views/pages/widgets/activity.vue:
-  activity: "Activity"
-mobile/views/pages/share.vue:
-  share-with: "Share on {name}"
-mobile/views/pages/note.vue:
-  title: "Post"
-  prev: "Previous note"
-  next: "Next note"
-mobile/views/pages/games/reversi.vue:
-  reversi: "Reversi"
-mobile/views/pages/search.vue:
-  search: "Search"
-  not-found: "No posts were found for '{q}'"
-mobile/views/pages/selectdrive.vue:
-  select-file: "Choose files"
-mobile/views/pages/notifications.vue:
-  notifications: "Notifications"
-mobile/views/pages/settings.vue:
-  signed-in-as: "Signed in as {}"
-mobile/views/pages/user.vue:
-  follows-you: "Follows you"
-  following: "Following"
-  followers: "Followers"
-  notes: "Posts"
-  overview: "Overview"
-  timeline: "Timeline"
-  media: "Media"
-  years-old: "{age} years old"
-mobile/views/pages/user/home.vue:
-  recent-notes: "Recent notes"
-  images: "Images"
-  activity: "Activity"
-  keywords: "Keywords"
-  domains: "Domains"
-  frequently-replied-users: "Frequent mentions"
-  followers-you-know: "Followers you know"
-  last-used-at: "Last active:"
-mobile/views/pages/user/home.photos.vue:
-  no-photos: "No photos"
-deck:
-  widgets: "Widgets"
-  home: "Home"
-  local: "Local"
-  hybrid: "Social"
-  hashtag: "Hashtag"
-  global: "Global"
-  mentions: "Mentions"
-  direct: "Direct posts"
-  notifications: "Notifications"
-  list: "List"
-  select-list: "Select a list"
-  swap-left: "Move left"
-  swap-right: "Move right"
-  swap-up: "Move up"
-  swap-down: "Move down"
-  remove: "Remove"
-  add-column: "Add a column"
-  rename: "Rename"
-  stack-left: "Stack to the left"
-  pop-right: "Dock on the right"
-  disabled-timeline:
-    title: "The timeline has been disabled"
-    description: "This timeline has been disabled by the server's administrator."
-deck/deck.tl-column.vue:
-  is-media-only: "Only media posts"
-  edit: "Options"
-deck/deck.user-column.vue:
-  follows-you: "Follows you"
-  posts: "Posts"
-  following: "Following"
-  followers: "Followers"
-  images: "Images"
-  activity: "Activity"
-  timeline: "Timeline"
-  pinned-notes: "Pinned posts"
-  pinned-page: "Pinned page"
-docs:
-  edit-this-page-on-github: "Found an error, or do you want to contribute to the documentation?"
-  edit-this-page-on-github-link: "Edit this page at GitHub!"
-dev/views/index.vue:
-  manage-apps: "Manage apps"
-dev/views/apps.vue:
-  manage-apps: "Manage apps"
-  create-app: "Create app"
-  app-missing: "No apps"
-dev/views/new-app.vue:
-  new-app: "New Application"
-  new-app-info: "You can also create an application with the API. (app/create)"
-  create-app: "Creating application"
-  app-name: "Application name"
-  app-name-placeholder: "ex) Misskey for iOS"
-  app-name-desc: "The name of your app"
-  app-overview: "Application summary"
-  app-overview-placeholder: " ex) Misskey iOS Client."
-  app-overview-desc: "A brief description, or an introduction of your app."
-  callback-url: "The callback URL (optional)"
-  callback-url-placeholder: "ex) https://your.app.example.com/callback.php"
-  callback-url-desc: "The URL to redirect to after the user is authenticated via the authentication form."
-  authority: "Permissions"
-  authority-desc: "Only the functions requested here can be accessed via the API."
-  authority-warning: "You can change it even after creating the application, but if you give different permissions, all user keys associated at that time will be invalidated."
-pages:
-  new-page: "Create a page"
-  edit-page: "Edit a page"
-  read-page: "Viewing the source"
-  page-created: "Created the page!"
-  page-updated: "Updated the page"
-  name-already-exists: "The specified page name already exists"
-  title-invalid-name: "The specified page URL is invalid"
-  text-invalid-name: "Check whether that is not a blank"
-  are-you-sure-delete: "Do you want to delete this page?"
-  page-deleted: "The page has been deleted"
-  edit-this-page: "Edit this page"
-  pin-this-page: "Pin to your profile"
-  unpin-this-page: "Unpin"
-  view-source: "View Source"
-  view-page: "View page"
-  like: "Like"
-  unlike: "Unlike"
-  liked-pages: "Favorite pages"
-  my-pages: "My pages"
-  inspector: "Inspector"
-  content: "Page block"
-  variables: "Variables"
-  variables-info: "You can make your page more dynamic by using variables. If you write down <b>{ variable name }</b> in the text, you can embed the value of the variable there. For example, if the source text is <b>Hello { thing } world!</b> and the value of variable 'thing' is <b> ai </b>, that text becomes to <b>Hello ai world!</b>."
-  variables-info2: "Because the evaluation(=calculating) of variables are performed from top to bottom, the variable cannot refer another variable which exists on later line. For example, when defining three variables <b>A</b>, <b>B</b> and <b>C</b>, variable <b>C</b> <i>can</i> refer the variable <b>A</b> and <b>B</b> in its expression, but variable <b>A</b> <i>cannot</i> refer the variable <b>B</b> or <b>C</b> in its expression."
-  variables-info3: "If you want to get some input from the user, place a 'User Input' block on the page and set the variable name as which you want to store that input in 'variable name' (variables are created automatically). You can use that variable to perform actions in response to user's input."
-  variables-info4: "Function allows make your processing logic as group in a reusable way. To create a function, create a variable of type 'Function'. A function can have a slot (Argument) whose value is available as a variable in the function. There are also functions that take functions as arguments in the AiScript standard (called the higher-order function.). In addition to the predefined functions, you can also set them in the slots of such higher-order functions on the fly."
-  more-details: "Description"
-  title: "Title"
-  url: "Page URL"
-  summary: "Summary of page"
-  align-center: "Center align"
-  hide-title-when-pinned: "Hide page title when pinned to profile"
-  font: "Font"
-  fontSerif: "Serif"
-  fontSansSerif: "Sans Serif"
-  set-eye-catching-image: "Set an eye-catching image"
-  remove-eye-catching-image: "Delete an eye-catching image"
-  choose-block: "Add a block"
-  select-type: "Select a type"
-  enter-variable-name: "Please choose a variable name"
-  the-variable-name-is-already-used: "This variable name is already used"
-  content-blocks: "Content"
-  input-blocks: "Input"
-  special-blocks: "Special"
-  post-from-post-form: "Post this content"
-  posted-from-post-form: "Posted!"
-  blocks:
-    text: "Text"
-    textarea: "Text area"
-    section: "Section"
-    image: "Images"
-    button: "Button"
-    if: "If"
-    _if:
-      variable: "Variables"
-    post: "Post form"
-    _post:
-      text: "Content"
-    textInput: "Text input"
-    _textInput:
-      name: "Variable name"
-      text: "Title"
-      default: "Default value"
-    textareaInput: "Multiple type text input"
-    _textareaInput:
-      name: "Variable name"
-      text: "Title"
-      default: "Default value"
-    numberInput: "Numeric input"
-    _numberInput:
-      name: "Variable name"
-      text: "Title"
-      default: "Default value"
-    switch: "Switch"
-    _switch:
-      name: "Variable name"
-      text: "Title"
-      default: "Default value"
-    counter: "Counter"
-    _counter:
-      name: "Variable name"
-      text: "Title"
-      inc: "Increase number"
-    _button:
-      text: "Title"
-      colored: "Color"
-      action: "Operation when the button pressed"
-      _action:
-        dialog: "Show a dialog"
-        _dialog:
-          content: "Content"
-        resetRandom: "Reset a random number"
-        pushEvent: "Send an event"
-        _pushEvent:
-          event: "Name of the event"
-          message: "Message to display when pressed"
-          variable: "Variable to send"
-          no-variable: "None"
-    radioButton: "Choices"
-    _radioButton:
-      name: "Variable name"
-      title: "Title"
-      values: "Item of choices that delimited by line breaks"
-      default: "Default value"
-  script:
-    categories:
-      flow: "Control"
-      logical: "Logical operation"
-      operation: "Compute"
-      comparison: "Compare"
-      random: "Random"
-      value: "Value"
-      fn: "Function"
-      text: "Text operation"
-      convert: "Variable"
-      list: "Lists"
-    blocks:
-      text: "Text"
-      multiLineText: "Text (Multiple lines)"
-      textList: "List of text"
-      _textList:
-        info: "Separate each one with a newline"
-      strLen: "Length of text"
-      _strLen:
-        arg1: "Text"
-      strPick: "Extract character"
-      _strPick:
-        arg1: "Text"
-        arg2: "Position of character"
-      strReplace: "Replace string(s)"
-      _strReplace:
-        arg1: "Text"
-        arg2: "Before replacement"
-        arg3: "After replacement"
-      strReverse: "Flip text"
-      _strReverse:
-        arg1: "Text"
-      join: "Text Concatenation"
-      _join:
-        arg1: "Lists"
-        arg2: "Separator"
-      add: "+ Plus"
-      _add:
-        arg1: "A"
-        arg2: "B"
-      subtract: "- Minus"
-      _subtract:
-        arg1: "A"
-        arg2: "B"
-      multiply: "× Multiply"
-      _multiply:
-        arg1: "A"
-        arg2: "B"
-      divide: "÷ Divide"
-      _divide:
-        arg1: "A"
-        arg2: "B"
-      mod: "÷ Remaindering"
-      _mod:
-        arg1: "A"
-        arg2: "B"
-      round: "Round decimal"
-      _round:
-        arg1: "Number"
-      eq: "A and B are equal"
-      _eq:
-        arg1: "A"
-        arg2: "B"
-      notEq: "A and B are different"
-      _notEq:
-        arg1: "A"
-        arg2: "B"
-      and: "A and B"
-      _and:
-        arg1: "A"
-        arg2: "B"
-      or: "A or B"
-      _or:
-        arg1: "A"
-        arg2: "B"
-      lt: "A is smaller than B"
-      _lt:
-        arg1: "A"
-        arg2: "B"
-      gt: "A is bigger than B"
-      _gt:
-        arg1: "A"
-        arg2: "B"
-      ltEq: "A is smaller or same than B"
-      _ltEq:
-        arg1: "A"
-        arg2: "B"
-      gtEq: "A is bigger or same than B"
-      _gtEq:
-        arg1: "A"
-        arg2: "B"
-      if: "Branch"
-      _if:
-        arg1: "If"
-        arg2: "then"
-        arg3: "else"
-      not: "denial"
-      _not:
-        arg1: "denial"
-      random: "Random"
-      _random:
-        arg1: "Probability"
-      rannum: "Random number"
-      _rannum:
-        arg1: "Minimum"
-        arg2: "Maximum"
-      randomPick: "Choose at random from the list"
-      _randomPick:
-        arg1: "Lists"
-      dailyRandom: "Random (Daily for each user)"
-      _dailyRandom:
-        arg1: "Probability"
-      dailyRannum: "Random number (Daily for each user)"
-      _dailyRannum:
-        arg1: "Minimum"
-        arg2: "Maximum"
-      dailyRandomPick: "Choose at random from the list (Daily for each user)"
-      _dailyRandomPick:
-        arg1: "Lists"
-      seedRandom: "Random (Seed)"
-      _seedRandom:
-        arg1: "Seed"
-        arg2: "Probability"
-      seedRannum: "Random number (Seed)"
-      _seedRannum:
-        arg1: "Seed"
-        arg2: "Minimum"
-        arg3: "Maximum"
-      seedRandomPick: "Randomly selected from list (Seed)"
-      _seedRandomPick:
-        arg1: "Seed"
-        arg2: "Lists"
-      DRPWPM: "Randomly selected from weighted list (Daily updated per user)"
-      _DRPWPM:
-        arg1: "List of text"
-      pick: "Select from list"
-      _pick:
-        arg1: "Lists"
-        arg2: "Position"
-      listLen: "Get length of list"
-      _listLen:
-        arg1: "Lists"
-      number: "Number"
-      stringToNumber: "Text to number"
-      _stringToNumber:
-        arg1: "Text"
-      numberToString: "Number to text"
-      _numberToString:
-        arg1: "Number"
-      splitStrByLine: "Split the text by lines"
-      _splitStrByLine:
-        arg1: "Text"
-      ref: "Variables"
-      fn: "Function"
-      _fn:
-        slots: "Slots"
-        slots-info: "Please delimit each slot with a line break"
-        arg1: "Output"
-      for: "Repeat"
-      _for:
-        arg1: "Count"
-        arg2: "Action"
-    typeError: "Slot {slot} accepts \"{expect}\" type, but It actually contains \"{actual}\" type!"
-    thereIsEmptySlot: "Slot {slot} is empty!"
-    types:
-      string: "Text"
-      number: "Number"
-      boolean: "Flag"
-      array: "Lists"
-      stringArray: "List of text"
-    emptySlot: "Empty slot"
-    enviromentVariables: "Environment variable"
-    pageVariables: "Page element"
-    argVariables: "Input slot"
-room:
-  add-furniture: "Place furniture"
-  translate: "Move"
-  rotate: "Rotate"
-  exit: "Deselect"
-  remove: "Remove"
-  save: "Save"
-  saved: "Saved"
-  clear: "Remove All"
-  clear-confirm: "Are you sure to remove all furnitures in your room?"
-  leave-confirm: "There are unsaved changes. Do you really want to leave?"
-  chooseImage: "Select an image"
-  room-type: "Room type"
-  carpet-color: "Color of carpet"
-  rooms:
-    default: "Default"
-    washitsu: "Japanese-style"
-  furnitures:
-    milk: "Milk carton"
-    bed: "Bed"
-    low-table: "Low Table"
-    desk: "Desk"
-    chair: "Chair"
-    chair2: "Chair 2"
-    fan: "Fan"
-    pc: "Computer"
-    plant: "Houseplant"
-    plant2: "Houseplant 2"
-    eraser: "Eraser"
-    pencil: "Pencil"
-    pudding: "Pudding"
-    cardboard-box: "Cardboard Box"
-    cardboard-box2: "Cardboard Box 2"
-    cardboard-box3: "Cardboard Box 3"
-    book: "Book"
-    book2: "Book 2"
-    piano: "Piano"
-    facial-tissue: "Facial tissue"
-    server: "Servers"
-    moon: "Moon"
-    corkboard: "Cork board"
-    mousepad: "Mousepad"
-    monitor: "Monitor"
-    keyboard: "Keyboard"
-    carpet-stripe: "Carpet (stripe)"
-    mat: "Mat"
-    color-box: "Bookshelf"
-    wall-clock: "Wall clock"
-    photoframe: "Picture frame"
-    cube: "Cube"
-    tv: "TV"
-    pinguin: "Penguin"
-    rubik-cube: "Rubik's Cube"
-    poster-h: "Poster (Horizontal)"
-    poster-v: "Poster (Vertical)"
-    sofa: "Sofa"
-    spiral: "Spiral Staircase"
-    bin: "Waste bin"
-    cup-noodle: "Cup noodle"
-    holo-display: "Holographic display"
-    energy-drink: "Energy drink"
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
deleted file mode 100644
index 67a546b86ccbba0d9194d415a25f7ccffffad2ea..0000000000000000000000000000000000000000
--- a/locales/es-ES.yml
+++ /dev/null
@@ -1,1194 +0,0 @@
----
-meta:
-  lang: "Español"
-common:
-  misskey: "Una ⭐️ del fediverso"
-  about-title: "Una ⭐️ del fediverso"
-  about: "Gracias por encontrar Misskey. Misskey es una <b>plataforma descentralizada de microblogging</b> nacida en la Tierra. Porque el servicio existe dentro del Fediverso (un universo donde se organizan varias plataformas sociales), se encuentra enlazado mutuamente con otras plataformas sociales. ¿Por qué no te tomas un respiro del caos de la ciudad y te sumerges es una nueva manera de entender Internet?"
-  intro:
-    title: "¿Misskey?"
-    about: "Misskey es un <b>Servicio de red social descentralizada de microblogging</b> de código abierto. Contiene una interfaz de usuario altamente personalizable, reacciones a posts, almacenamiento para poder manejar archivos y otras funciones avanzadas. Además de conectarse con la red llamada Fediverso, puede intercambiar mensajes con otras redes sociales. Por ejemplo, si contribuyes con algo, esa contribución es transmitida no sólo a Misskey sino a otras redes sociales. Imagina que se parece a transmitir una onda de radio de un planeta a otro."
-    features: "Características"
-    rich-contents: "Posts"
-    rich-contents-desc: "Escribe sobre tus pensamientos, eventos, todo lo que quieras compartir. Si es necesario, puedes usar varias sintaxis, decorar tus posts y añadir tus imágenes favoritas, archivos de viddeo y encuestas."
-    reaction: "Reacciones"
-    reaction-desc: "La forma mas facil de expresar tus emociones. Misskey te permite añadir varios tipos de reacciones a los posts de otros usuarios. La emperiencia emocional en Misskey nunca será igual que en otra red social, donde solo puedes poner \"likes\"."
-    ui: "Interfaz"
-    ui-desc: "No hay ninguna interfaz que le vaya bien a todos. Por eso, Misskey tiene una interfaz altamente personalizable para tus gustos. Puedes hacer tu página principal única editando la interfaz de tu timeline y moviendo varios widgets para conseguir hacer de este lugar uno propio."
-    drive: "Drive"
-    drive-desc: "¿Quieres postear de nuevo la imagen que has posteado antes? Si es así, ¿Quieres separar y ordenar en carpetas los archivos que has subido? La característica Drive incorporada en la base de Misskey es la solución. Compartir archivos es simple."
-    outro: "Aún hay características que solamente están en Misskey, asegúrate de eso con tus propios ojos. Misskey es un servicio de red social distribuida, si no te gusta esta instancia, puedes probar otra instancia. Así que, ¡buena suerte!"
-  application-authorization: "Autorizaciones de la aplicación."
-  close: "Cerrar"
-  do-not-copy-paste: "Por favor no copies código aquí. Tu cuenta puede resultar comprometida."
-  load-more: "Leer más"
-  enter-password: "Escribe una contraseña"
-  2fa: "Autenticación de dos factores"
-  customize-home: "Personaliza la página principal"
-  featured-notes: "Destacados"
-  dark-mode: "Modo oscuro"
-  signin: "Iniciar sesión"
-  signup: "¡Regístrate!"
-  signout: "Cerrar sesión"
-  reload-to-apply-the-setting: "Para aplicar esta configuración, hay que recargar la página. ¿Quiere recargar ahora?"
-  fetching-as-ap-object: "Consultar en el fediverso"
-  unfollow-confirm: "¿Quiere dejar de seguir a {name}?"
-  delete-confirm: "¿Seguro que quieres borrar la publicación?"
-  signin-required: "Inicie sesion"
-  notification-type: "Tipo de notificación"
-  notification-types:
-    all: "Todo"
-    pollVote: "Encuestas"
-    follow: "Seguimientos"
-    receiveFollowRequest: "Solicitudes de seguimiento"
-    reply: "Responder"
-    quote: "Citas"
-    renote: "Volver a publicar"
-    mention: "Menciones"
-    reaction: "Reacciones"
-  got-it: "¡Listo!"
-  customization-tips:
-    title: "Consejos de personalización"
-    paragraph: "<p>Se puede personalizar el inicio agregando/quitando widgets, arrastrarlos, soltarlos y ordenarlos.</p><p>Haciendo <strong>Click <strong>derecho</strong></strong>, se puede modificar la muestra de un widget</p><p>Para quitar un widget, arrastre y suelte el widget en el area que dice <strong>\"Papelera\"</strong> en el cabezal</p><p>Para acabar de personalizar, haga click en \"Listo\" arriba a la derecha</p>"
-    gotit: "¡Comprendido!"
-  notification:
-    file-uploaded: "Archivo cargado."
-    message-from: "Mensaje de {}:"
-    reversi-invited: "Invitado a un juego"
-    reversi-invited-by: "Invitado por {}:"
-    notified-by: "Notificado por {}:"
-    reply-from: "Respuesta de {}:"
-    quoted-by: "Citado por {}:"
-  time:
-    unknown: "Desconocido"
-    future: "Futuro"
-    just_now: "Ahora mismo"
-    seconds_ago: "Hace {}"
-    minutes_ago: "Hace {} minuto(s)"
-    hours_ago: "Hace {} hora(s)"
-    days_ago: "Hace {} dia(s)"
-    weeks_ago: "Hace {} semana(s)"
-    months_ago: "Hace {} mes(es)"
-    years_ago: "Hace {} año(s)"
-  month-and-day: "{day} de {month}"
-  trash: "Papelera"
-  drive: "Drive"
-  pages: "Páginas"
-  messaging: "Conversación"
-  home: "Inicio"
-  deck: "Deck"
-  timeline: "Timeline"
-  explore: "Explorar"
-  following: "Siguiendo"
-  followers: "Seguidores"
-  favorites: "Me gusta esta nota"
-  permissions:
-    "read:account": "Ver información de la cuenta"
-    "write:account": "Editar información de la cuenta"
-    "read:blocks": "Ver bloques"
-    "write:blocks": "Editar bloques"
-    "read:drive": "Explorar el drive"
-    "write:drive": "Administrar el drive"
-    "read:favorites": "Ver favoritos"
-    "write:favorites": "Editar favoritos"
-    "read:following": "Ver información de seguidor"
-    "write:following": "Seguir/Dejar de seguir"
-    "read:messaging": "Ver conversación"
-    "write:messaging": "Administrar coversación"
-    "read:mutes": "Ver silenciados"
-    "write:mutes": "Administrar silenciados"
-    "write:notes": "Crear y eliminar articulos"
-    "read:notifications": "Ver notificaciones"
-    "write:notifications": "Administrar notificaciones"
-    "read:reactions": "Ver reacciones"
-    "write:reactions": "Administrar reacciones"
-    "write:votes": "Vota"
-    "read:pages": "Ver páginas"
-    "write:pages": "Administrar páginas"
-    "read:page-likes": "Ver páginas que te gustan"
-    "write:page-likes": "Administrar páginas que te gustan"
-    "read:user-groups": "Ver grupos de usuarios"
-    "write:user-groups": "Administrar grupos de usuarios"
-  empty-timeline-info:
-    follow-users-to-make-your-timeline: "Seguir al usuario mostrará sus posts en la linea de tiempo"
-    explore: "Explorar usuarios"
-  post-form:
-    reply: "Responder"
-    renote: "Volver a publicar"
-    attach-media-from-local: "Agregar medios de tu dispositivo"
-    insert-a-kao: "v('ω')v"
-    recent-tags: "Reciente"
-    error: "Error"
-    enter-username: "Ingresar nombre de usuario"
-    add-visible-user: "Agregar usuario"
-    username-prompt: "Ingresar nombre de usuario"
-    enter-file-name: "Editar nombre del archivo"
-  weekday-short:
-    sunday: "domingo"
-    monday: "lunes"
-    tuesday: "martes"
-    wednesday: "miércoles"
-    thursday: "jueves"
-    friday: "viernes"
-    saturday: "sábado"
-  weekday:
-    sunday: "Domingo"
-    monday: "Lunes"
-    tuesday: "Martes"
-    wednesday: "Miércoles"
-    thursday: "Jueves"
-    friday: "Viernes"
-    saturday: "Sábado"
-  reactions:
-    like: "Me gusta"
-    love: "amor"
-    laugh: "risa"
-    hmm: "hmm"
-    surprise: "sorpresa"
-    congrats: "felicidades"
-    angry: "enfadado"
-    confused: "confundido"
-    rip: "RIP"
-    pudding: "Chafado"
-  note-visibility:
-    public: "Público"
-    home: "Inicio"
-    home-desc: "Sólo en el timeline de inicio"
-    followers: "Seguidores"
-    followers-desc: "Sólo para tus seguidores"
-    specified: "Mensaje directo"
-    specified-desc: "Sólo para ciertos usuarios"
-    local-public: "Público (sólo local)"
-    local-home: "Inicio (sólo local)"
-    local-followers: "Seguidores (sólo local)"
-  note-placeholders:
-    a: "¿Qué haces?"
-    b: "¿Qué está pasando?"
-    c: "¿Qué te pasa por la cabeza?"
-    d: "¿Quieres decir algo?"
-    e: "¡Escribe aquí!"
-    f: "Esperando a que escribas algo..."
-  settings: "Configuración"
-  _settings:
-    profile: "Tu perfil"
-    notification: "Notificaciones"
-    apps: "Aplicaciones"
-    tags: "Etiquetas"
-    mute-and-block: "Silenciar/Bloquear"
-    blocking: "Bloquear"
-    security: "Seguridad"
-    signin: "Historial de ingresos"
-    password: "Contraseña"
-    other: "Otros"
-    appearance: "Diseño"
-    behavior: "Comportamiento"
-    reactions: "Reacciones"
-    fetch-on-scroll-desc: "Cuando te deslizas al final de la página nuevo contenido se carga automáticamente."
-    note-visibility: "Visibilidad de la publicación"
-    default-note-visibility: "Rango de publicación predeterminado"
-    web-search-engine: "Buscador web"
-    web-search-engine-desc: "Ejemplo: https://www.google.com/?#q={{query}}"
-    keep-cw: "Mantener CW"
-    this-setting-is-this-device-only: "Solo para este dispositivo"
-    use-os-default-emojis: "Usar los emoticonos estándar del sistema operativo"
-    line-width: "Grosor de línea"
-    line-width-thin: "Fino"
-    line-width-normal: "Normal"
-    line-width-thick: "Grosor"
-    font-size: "Tamaño del texto"
-    font-size-x-small: "Muy pequeño"
-    font-size-small: "Pequeño"
-    font-size-medium: "Normal"
-    font-size-large: "Grande"
-    font-size-x-large: "Muy grande"
-    deck-column-align: "Alineamiento de las columnas"
-    deck-column-align-center: "Centrar"
-    deck-column-align-left: "Izquierda"
-    deck-column-align-flexible: "Flexible"
-    deck-column-width: "Ancho de las columnas"
-    deck-column-width-narrow: "Estrecho"
-    deck-column-width-narrower: "Un poco estrecho"
-    deck-column-width-normal: "Normal"
-    deck-column-width-wider: "Un poco ancho"
-    deck-column-width-wide: "Ancho"
-    use-shadow: "Usar sombras en la Interfaz de Usuario"
-    rounded-corners: "Esquinas redondeadas en la Interfaz de Usuario"
-    circle-icons: "Usar avatar circulares"
-    contrasted-acct: "Añadir contraste al nombre de usuario"
-    wallpaper: "Fondo de pantalla"
-    choose-wallpaper: "Escoge un fondo de pantalla"
-    delete-wallpaper: "Quitar fondo de pantalla"
-    post-form-on-timeline: "Mostrar el formulario de las entradas encima de la línea de tiempo"
-    show-clock-on-header: "Muestra el reloj en la parte superior derecha"
-    show-reply-target: "Mostrar destinatario de la mención"
-    timeline: "Timeline"
-    show-my-renotes: "Mostrar mis renotes en la timeline"
-    show-renoted-my-notes: "Mostrar renotes de mis posts en la timeline"
-    sound: "Sonido"
-    enable-sounds: "Habilitar sonido"
-    volume: "Volúmen"
-    test: "Prueba"
-    update: "Actualizar Misskey"
-    version: "Versión"
-    latest-version: "Última versión"
-    update-checking: "Buscando actualizaciones"
-    no-updates: "No hay actualizaciones disponibles"
-    no-updates-desc: "Tu Misskey está actualizado"
-    update-available: "¡Una nueva versión está disponible!"
-    update-available-desc: "Las actualizaciones se aplicarán cuando la página se vuelva a cargar."
-    advanced-settings: "Configuraciones avanzadas"
-    navbar-position-left: "Izquierda"
-    save: "Guardar"
-    saved: "Guardado"
-    preview: "Vista previa"
-  search: "Buscar"
-  delete: "eliminar"
-  loading: "cargando"
-  ok: "Confirmar"
-  cancel: "Cancelar"
-  update-available-title: "Actualización disponible"
-  update-available: "Hay disponible una nueva versión de Misskey ({newer}, la versión actual es {current}). Refresca la página para aplicar las actualizaciones."
-  my-token-regenerated: "Tu token se ha regenerado vas a ser desconectado."
-  hide-password: "Ocultar contraseña"
-  show-password: "Mostrar contraseña"
-  enter-username: "Ingresar nombre de usuario"
-  do-not-use-in-production: "Esto está en desarrollo, no usarlo para producción."
-  user-suspended: "Este usuario ha sido suspendido"
-  is-remote-user: "La información sobre este usuario puede no estar completa"
-  is-remote-post: "Es una publicación remota"
-  view-on-remote: "Consultar el perfil completo"
-  renoted-by: "Renotado por {user}"
-  no-notes: "No hay publicaciones"
-  turn-on-darkmode: "Cambiar a modo oscuro"
-  turn-off-darkmode: "Modo claro"
-  error:
-    title: "Se ha producido un problema :("
-    retry: "Inténtalo otra vez"
-  reversi:
-    drawn: "Empatado"
-    my-turn: "Mi turno"
-    opponent-turn: "Turno del oponente"
-    turn-of: "Turno de {name}"
-    past-turn-of: "Turno de {name}"
-    won: "{name} ha ganado"
-    black: "Negro"
-    white: "Blanco"
-    total: "Total"
-    this-turn: "Turno {count}"
-  widgets:
-    analog-clock: "Reloj analógico"
-    profile: "Perfil"
-    calendar: "Calendario"
-    timemachine: "Calendario (máquina del tiempo)"
-    activity: "Actividad"
-    rss: "Lector RSS"
-    memo: "Notas adhesivas"
-    trends: "Tendencias"
-    photo-stream: "Secuencia de fotos"
-    posts-monitor: "Gráfico de publicaciones"
-    slideshow: "Diapositivas"
-    version: "Versión"
-    broadcast: "Transmisión"
-    notifications: "Notificaciones"
-    users: "Usuarios destacados"
-    polls: "Encuestas"
-    post-form: "Formulario"
-    server: "Información del servidor"
-    nav: "Navegación"
-    tips: "Consejos"
-    hashtags: "Etiquetas"
-    queue: "En cola"
-  dev: "Se ha producido un error creando la aplicación. Intentelo de nuevo."
-  ai-chan-kawaii: "Ai-chan es muy mona!"
-  you: "Tú"
-auth/views/form.vue:
-  share-access: "¿Deseas permitir a <i>{name}</i> acceder a tu cuenta?"
-  permission-ask: "La aplicación requiere los siguientes permisos:"
-  cancel: "Cancelar"
-  accept: "Garantizar acceso."
-auth/views/index.vue:
-  loading: "Cargando"
-  denied: "Acceso de aplicación denegado."
-  denied-paragraph: "Esta aplicación no tendrá acceso a tu cuenta."
-  already-authorized: "Esta aplicación ha sido previamente autorizada."
-  allowed: "Accesos de aplicaciones autorizados."
-  callback-url: "Volviendo a la aplicación."
-  please-go-back: "Por favor, vuelve a la aplicación."
-  error: "Esta sesión no existe."
-  sign-in: "Por favor inicia sesión."
-common/views/pages/explore.vue:
-  popular-users: "Usuarios populares"
-  recently-updated-users: "Usuarios activos recientemente"
-  recently-registered-users: "Usuarios que se han unido recientemente"
-  popular-tags: "Etiquetas populares"
-  federated: "Desde el fediverso"
-  explore: "Explorar {host}"
-  users-info: "Actualmente hay {users} registrados aquí"
-common/views/components/url-preview.vue:
-  enable-player: "Activar reproducción"
-  disable-player: "Cerrar el reproductor"
-common/views/components/user-list.vue:
-  no-users: "No hay usuarios."
-common/views/components/games/reversi/reversi.vue:
-  matching:
-    waiting-for: "Esperando por {}"
-    cancel: "Cancelar"
-common/views/components/games/reversi/reversi.game.vue:
-  surrender: "Rendirse"
-  surrendered: "Por rendirse"
-  is-llotheo: "El último gana (Llotheo)"
-  looped-map: "Mapa en bucle"
-  can-put-everywhere: "Puedes colocar donde quieras"
-common/views/components/games/reversi/reversi.index.vue:
-  title: "Misskey Reversi"
-  sub-title: "¡Juega Reversi con tus amigos!"
-  invite: "Invitar"
-  rule: "Cómo jugar"
-  rule-desc: "Reversi es un juego de estrategia para dos jugadores, el cual se juega en un tablero de 8x8. Hay 64 fichas llamadas discos, las cuales son claras de un lado y oscuras del otro. Los jugadores toman turnos colocando fichas en el tablero con su color asignado mirando hacia arriba. Durante una jugada, cualquier disco del color del oponente que esté en fila entre un disco del oponente y otro del mismo color, será volteado para tener el color del jugador que haya hecho la movida. El objetivo del juego es tener la mayoría de los discos de tu color cuando el último cuadro es llenado."
-  mode-invite: "Invitar"
-  mode-invite-desc: "Invitar un usuario al juego."
-  invitations: "¡Has recibido una invitación!"
-  my-games: "Mis juegos"
-  all-games: "Todos los juegos"
-  enter-username: "Ingresar nombre de usuario"
-  game-state:
-    ended: "Finalizado"
-    playing: "En progreso"
-common/views/components/games/reversi/reversi.room.vue:
-  settings-of-the-game: "Configuración de juego"
-  choose-map: "Elije un mapa"
-  random: "Aleatorio"
-  black-or-white: "Negro/Blanco"
-  black-is: "Negro es {}"
-  rules: "Reglas"
-  is-llotheo: "El que tenga menos gana"
-  looped-map: "Mapa en bucle"
-  can-put-everywhere: "Puedes colocar donde quieras"
-  settings-of-the-bot: "Configuración de bot"
-  this-game-is-started-soon: "El juego comenzará pronto"
-  waiting-for-other: "Esperando a que se prepare el adversario"
-  waiting-for-me: "Esperando por la preparación"
-  waiting-for-both: "Esperando por ti"
-  cancel: "Cancelar"
-  ready: "Listo"
-  cancel-ready: "Cancelar \"Listo\""
-common/views/components/connect-failed.vue:
-  title: "Imposible conectar al servidor"
-  description: "Hay un problema en tu conexió o puede que el servidor esté caido o en mantenimiento. Por favor {try again} más tarde."
-  thanks: "Gracias por usar Misskey."
-  troubleshoot: "Problemas más frecuentes"
-common/views/components/connect-failed.troubleshooter.vue:
-  title: "Resolución de problemas"
-  network: "Conexión de red"
-  checking-network: "Verificar la conexión a la red"
-  internet: "Conexión a Internet"
-  checking-internet: "Comprobando la conexión a Internet"
-  server: "Conexión al servidor"
-  checking-server: "Probando la conexión al servidor"
-  finding: "Buscando cualquier problema"
-  no-network: "Sin conexión"
-  no-network-desc: "Por favor, asegurate que estás conectado a una red"
-  no-internet: "Sin conexión a Internet"
-  no-internet-desc: "Por favor, asegurate de estar conectado a Internet."
-  no-server: "Imposible conectarse al servidor de Misskey"
-  no-server-desc: "La conexión de red de tu PC es correcta, aún así no puedes conectarte al servidor de Misskey. Es posible que el servidor esté caido o en mantenimiento. Por favor vuelve a intentarlo más tarde."
-  success: "Conectado al servidor de Misskey de manera correcta"
-  success-desc: "Parece que la conexión ha sido posible. Por favor refresca la página."
-  flush: "Limpiar la memoria caché"
-  set-version: "Escoge la versión"
-common/views/components/media-banner.vue:
-  sensitive: "Este contenido no es apropiado para ver en el trabajo"
-  click-to-show: "Click para mostrar"
-common/views/components/theme.vue:
-  theme: "Tema"
-  light-theme: "Tema a usar en Light mode"
-  dark-theme: "Tema a usar en dark mode"
-  light-themes: "Tema claro"
-  dark-themes: "Tema oscuro"
-  install-a-theme: "Instalar tema"
-  theme-code: "Código del tema"
-  install: "Instalación"
-  installed: "\"{}\" se ha instalado"
-  create-a-theme: "Crear tema"
-  save-created-theme: "Guardar tema"
-  primary-color: "Color primario"
-  secondary-color: "Color secundario"
-  text-color: "Color del texto"
-  base-theme: "Tema base"
-  base-theme-light: "Claro"
-  base-theme-dark: "Oscuro"
-  find-more-theme: "Obtener más temas"
-  theme-name: "Nombre del tema"
-  preview-created-theme: "Vista previa"
-  invalid-theme: "No es un tema válido"
-  already-installed: "Este tema ya está instalado."
-  saved: "Guardado"
-  manage-themes: "Gestor de temas"
-  builtin-themes: "Temas estandar"
-  my-themes: "Mis temas"
-  installed-themes: "Temas instalados"
-  select-theme: "Elegir tema"
-  uninstall: "Desinstalar"
-  uninstalled: "\"{}\" ha sido desinstalado"
-  author: "Autor"
-  desc: "Descripción"
-  export: "Exportar"
-  import: "Importar"
-  import-by-code: "o pega el código"
-common/views/components/cw-button.vue:
-  show: "Mostrar"
-  chars: "{count} letras"
-  files: "{count} archivos"
-  poll: "Encuesta"
-common/views/components/messaging.vue:
-  search-user: "Encuentra un usuario"
-  you: "Tu"
-  no-history: "Sin historial"
-common/views/components/messaging-room.vue:
-  no-history: "El historial se ha acabado"
-  new-message: "Nuevo mensaje"
-common/views/components/messaging-room.form.vue:
-  input-message-here: "Escribe el mensaje aquí"
-  send: "Enviar"
-  attach-from-local: "Adjunta ficheros desde tu PC"
-  attach-from-drive: "Adjunta ficheros desde tu disco"
-common/views/components/messaging-room.message.vue:
-  is-read: "Leer"
-  deleted: "El mensaje se ha borrado"
-common/views/components/nav.vue:
-  about: "Sobre"
-  stats: "Estadísticas"
-  status: "Estado"
-  wiki: "Wiki"
-  donors: "Donantes"
-  repository: "Repositorio"
-  develop: "Desarrolladores"
-  feedback: "Opiniones"
-common/views/components/note-menu.vue:
-  mention: "Menciones"
-  detail: "Detalles"
-  copy-link: "Copiar enlace"
-  favorite: "Me gusta esta nota"
-  pin: "Fijar en el perfil"
-  delete: "Borrar"
-  delete-confirm: "¿Seguro que quieres borrar la publicación?"
-  remote: "Ver el original"
-common/views/components/user-menu.vue:
-  mention: "Menciones"
-  mute: "Silenciar"
-  block: "Bloquear"
-common/views/components/poll.vue:
-  vote-to: "'{}' para votar"
-  vote-count: "{} votos"
-  vote: "Vota"
-  show-result: "Mostrar resultados"
-  voted: "Votado"
-common/views/components/poll-editor.vue:
-  no-only-one-choice: "Selecciona dos o más opciones."
-  choice-n: "{} opcion(es)"
-  remove: "Borra la opción"
-  add: "+ Añade una opción"
-  destroy: "Cancelar la encuesta"
-  day: "domingo"
-common/views/components/reaction-picker.vue:
-  choose-reaction: "Escoge una reacción"
-common/views/components/emoji-picker.vue:
-  custom-emoji: "Personalizados"
-  people: "Gente"
-  animals-and-nature: "Naturaleza"
-  food-and-drink: "Comida y bebida"
-  activity: "Actividad"
-  travel-and-places: "Viajes y lugares"
-  objects: "Objetos"
-  symbols: "Símbolos"
-  flags: "Países"
-common/views/components/settings/app-type.vue:
-  info: "Necesitas recargar la página para que los cambios tengan efecto."
-common/views/components/signin.vue:
-  username: "Usuario"
-  password: "Contraseña"
-  token: "Identificador"
-  signing-in: "Entrando..."
-  or: "O"
-  signin-with-twitter: "Ingresar con Twitter"
-  signin-with-github: "Ingresar con Github"
-  signin-with-discord: "Ingresar con Discord"
-  login-failed: "Autenticación fallida. Asegúrate de haber usado el nombre de usuario y contraseña correctos."
-common/views/components/signup.vue:
-  invitation-code: "Código de invitación"
-  invitation-info: "Si no tienes un código de invitación, por favor contacta un <a href=\"{}\">administrador</a>."
-  username: "Usuario"
-  checking: "Comprobando..."
-  available: "Disponible"
-  unavailable: "Utilizado"
-  error: "Error de conexión"
-  invalid-format: "utiliza letras, números y/o -."
-  too-short: "¡Mínimo tienes que introducir un caracter!"
-  too-long: "No puedes usar más de 20 caracteres."
-  password: "Contraseña"
-  password-placeholder: "Te recomendamos más de 8 caracteres"
-  weak-password: "Contraseña débil"
-  normal-password: "No está mal"
-  strong-password: "Muy buena contraseña"
-  retype: "Inténtalo otra vez"
-  retype-placeholder: "Confirma la contraseña"
-  password-matched: "OK"
-  password-not-matched: "Las contraseñas no son las mismas"
-  recaptcha: "Verificar"
-  create: "Crea una cuenta"
-  some-error: "Por algún motivo no se ha podido crear la cuenta. Por favor inténtalo de nuevo."
-common/views/components/special-message.vue:
-  new-year: "¡Feliz Año Nuevo!"
-  christmas: "¡Feliz Navidad!"
-common/views/components/stream-indicator.vue:
-  connecting: "Conectando"
-  reconnecting: "Reconectando"
-  connected: "Conectado"
-common/views/components/notification-settings.vue:
-  title: "Notificaciones"
-common/views/components/integration-settings.vue:
-  title: "Integraciones"
-  connect: "Conectar"
-  disconnect: "Desconectarse"
-  connected-to: "Estas conectado a la siguiente cuenta"
-common/views/components/github-setting.vue:
-  description: "Una vez conectada tu cuenta de GitHub a Misskey podrás ver la información sobre tu perfil de GitHub y además podrás registrarte mediante tu cuenta de GitHub."
-  connected-to: "Estas conectado a esta cuenta de GitHub"
-  detail: "Ver detalles..."
-  reconnect: "Reconectar"
-  connect: "Vincular tu cuenta de GitHub"
-  disconnect: "Desconectarse"
-common/views/components/discord-setting.vue:
-  description: "Una vez conectada tu cuenta de Discord a Misskey podrás ver la información sobre tu perfil de Discord y además podrás registrarte mediante tu cuenta de Discord."
-  connected-to: "Estas conectado a esta cuenta de Discord"
-  detail: "Ver detalles..."
-  reconnect: "Reconectar"
-  connect: "Vincular tu cuenta de Discord"
-  disconnect: "Desconectarse"
-common/views/components/uploader.vue:
-  waiting: "Un momento"
-common/views/components/visibility-chooser.vue:
-  public: "Público"
-  home: "Inicio"
-  home-desc: "Publica solo en la página de inicio"
-  followers: "Seguidores"
-  followers-desc: "Piblica solo para tus seguidores"
-  specified: "Directo"
-  specified-desc: "Publica solo para los seguidores que quieras"
-  local-public: "Público (sólo local)"
-  local-public-desc: "No publicar para remoto"
-  local-home: "Inicio (sólo local)"
-  local-followers: "Seguidores (sólo local)"
-common/views/components/trends.vue:
-  count: "{} usuarios mencionados"
-  empty: "Ninguna tendencia popular ahora"
-common/views/components/language-settings.vue:
-  title: "Mostrar idioma"
-  pick-language: "Selecciona un idioma"
-  recommended: "Recomendado"
-  auto: "Automático"
-  specify-language: "Especifica el idioma"
-  info: "Necesitas recargar la página para que los cambios tengan efecto."
-common/views/components/profile-editor.vue:
-  title: "Perfil"
-  name: "Nombre"
-  account: "Cuenta"
-  location: "Localización"
-  description: "Acerca de mí"
-  you-can-include-hashtags: "También puedes incluir hashtags en la descripción de tu perfil."
-  language: "Idioma"
-  birthday: "Fecha de nacimiento"
-  avatar: "Avatar"
-  banner: "Banner"
-  is-cat: "Esta cuenta es un gato"
-  is-bot: "Esta cuenta es un bot"
-  is-locked: "Las peticiones de seguimiento necesitan aprobación"
-  careful-bot: "Las peticiones de seguimiento de bots necesitan aprobación"
-  auto-accept-followed: "Aprobar automaticamente las peticiones de follow de gente a la que sigues"
-  advanced: "Otros"
-  privacy: "Privacidad"
-  save: "Guardar"
-  saved: "Perfil actualizado con exito"
-  uploading: "Subiendo"
-  upload-failed: "Error al subir"
-  unable-to-process: "La operación no se puede llevar a cabo"
-  email: "Preferencias de correo"
-  email-address: "Correo electrónico"
-  email-verified: "Tu cuenta de correo ha sido verificada."
-  email-not-verified: "Tu cuenta de correo no está verificada. Por favor comprueba tu bandeja de entrada."
-  export: "Exportar"
-  import: "Importar"
-  export-and-import: "Exportar/Importar"
-  export-targets:
-    all-notes: "Todas las notas publicadas"
-    following-list: "Seguidores"
-    mute-list: "Silenciar"
-    blocking-list: "Bloquear"
-    user-lists: "Listas"
-  export-requested: "Has solicitado una exportación. Esto puede tardar un rato. Después de que termine la exportación el archivo se añadirá al drive."
-  import-requested: "Has empezado una importación. Esto puede tardar un rato."
-  enter-password: "Escribe una contraseña"
-  danger-zone: "Zona de peligro"
-  delete-account: "Eliminar cuenta"
-  account-deleted: "Esta cuenta ha sido eliminada. Puede tardar un rato hasta que toda la información desaparazca."
-common/views/components/user-list-editor.vue:
-  users: "Usuarios"
-  rename: "Cambiar el nombre de la lista"
-  delete: "Eliminar lista"
-  remove-user: "Eliminar de la lista"
-common/views/components/user-group-editor.vue:
-  invite: "Invitar"
-common/views/components/user-lists.vue:
-  user-lists: "Listas"
-  list-name: "Nombre de lista"
-common/views/components/user-groups.vue:
-  invites: "Invitar"
-common/views/widgets/broadcast.vue:
-  fetching: "Recuperando"
-  no-broadcasts: "Sin emisión"
-  have-a-nice-day: "¡Buenos dias!"
-  next: "Siguiente"
-common/views/widgets/calendar.vue:
-  year: "Año {}"
-  month: "Mes {}"
-  day: "Día {}"
-  today: "Hoy:"
-  this-month: "Este mes:"
-  this-year: "Este año:"
-common/views/widgets/photo-stream.vue:
-  title: "Galería de fotos"
-  no-photos: "No hay fotos."
-common/views/widgets/posts-monitor.vue:
-  title: "Tabla de publicaciones"
-  toggle: "Alternar vistas"
-common/views/widgets/hashtags.vue:
-  title: "Etiquetas"
-common/views/widgets/server.vue:
-  title: "Información del servidor"
-  toggle: "Alternar vistas"
-common/views/widgets/memo.vue:
-  title: "Notas"
-  memo: "¡Escribe aquí!"
-  save: "Guardar"
-common/views/widgets/slideshow.vue:
-  folder-customize-mode: "Para especificar una carpeta, por favor sal de modo de personalización"
-  folder: "Por favor, cliquea y especifica una carpeta"
-  no-image: "No hay imágenes en esta carpeta"
-common/views/widgets/tips.vue:
-  tips-line1: "Puedes enfocarte en las publicaciones con <kbd>t</kbd>"
-  tips-line2: "Abrir formulario de publicación con <kbd>p</kbd> or <kbd>n</kbd>"
-  tips-line3: "Puedes arrastrar y soltar archivos en el formulario de publicación"
-  tips-line4: "Puedes pegar una imagen del portapapeles en el formulario de publicación"
-  tips-line5: "Puedes cargar archivos con sólo arrastrarlos y soltarlos en Drive"
-  tips-line6: "Puedes mover una carpeta arrastrándola hacia el Drive"
-  tips-line7: "Puedes mover una carpeta arrastrándola hacia el Drive"
-  tips-line8: "Inicio se puede personalizar desde la configuración"
-  tips-line9: "Misskey está hecho bajo licencia AGPLv3"
-  tips-line10: "Usando el accesorio de Máquina del Tiempo puedes encontrar publicaciones antiguas"
-  tips-line11: "Puedes resaltar publicaciones en la página de usuario haciendo click en \"...\""
-  tips-line13: "Todos los archivos añadidos a la publicación se han guardado en tu unidad."
-  tips-line14: "Cuando personalizas el inicio puedas dar click derecho a un accesorio y cambiar el diseño."
-  tips-line17: "Al colocar ** delante y luego del texto, lo estarás destacando en negrillas"
-  tips-line19: "Algunas ventanas pueden ser separadas fuera del navegador"
-  tips-line20: "El porcentaje mostrando en el accesorio de calendario indica el porcentaje de tiempo transcurrido."
-  tips-line21: "También puedes usar la API para desarrollar tus propios bots."
-  tips-line24: "Misskey inició en 2014."
-  tips-line25: "Puedes recibir notificaciones incluso si Misskey no está abierto en un navegador compatible."
-common/views/pages/follow.vue:
-  signed-in-as: "Autenticado como {}"
-  following: "Siguiendo"
-  follow: "Seguir"
-  request-pending: "Solicitud pendiente"
-  follow-processing: "Solicitud en proceso"
-  follow-request: "Solicitar suscripción"
-common/views/pages/follow-requests.vue:
-  received-follow-requests: "Solicitudes de seguimiento"
-desktop:
-  banner-crop-title: "Corta la parte que aparece como un banner"
-  banner: "Banner"
-  uploading-banner: "Cargando un nuevo banner"
-  banner-updated: "Banner actualizado"
-  choose-banner: "Escoge un banner"
-  avatar-crop-title: "Corta la parte que aparece como un avatar"
-  avatar: "Avatar"
-  uploading-avatar: "Cargando un nuevo avatar"
-  avatar-updated: "Avatar actualizado"
-  choose-avatar: "Escoge una imagen de avatar"
-  unable-to-process: "La operación no se puede llevar a cabo"
-  invalid-filetype: "Este tipo de archivo no es compatible aquí"
-desktop/views/components/activity.chart.vue:
-  total: "Negro ... Total"
-  notes: "Azul ... Notas"
-  replies: "Rojo ... Respuestas"
-  renotes: "Verde ... Republicaciones"
-desktop/views/components/activity.vue:
-  title: "Actividad"
-  toggle: "Alternar vistas"
-desktop/views/components/calendar.vue:
-  title: "{year} / {month}"
-  prev: "Mes anterior"
-  next: "Próximo mes"
-  go: "Click para navegar"
-desktop/views/components/choose-file-from-drive-window.vue:
-  upload: "Cargar archivos de tu dispositivo"
-  cancel: "Cancelar"
-  ok: "OK"
-  choose-prompt: "Escoger archivos"
-desktop/views/components/choose-folder-from-drive-window.vue:
-  cancel: "Cancelar"
-  ok: "OK"
-  choose-prompt: "Escoge una Carpeta"
-desktop/views/components/crop-window.vue:
-  skip: "Ignorar el cortado"
-  cancel: "Cancelar"
-  ok: "OK"
-desktop/views/components/drive-window.vue:
-  used: "usado"
-desktop/views/components/drive.file.vue:
-  avatar: "Avatar"
-  banner: "Banner"
-  nsfw: "Ver más"
-  contextmenu:
-    rename: "Renombrar"
-    mark-as-sensitive: "Marcar como 'sensible'"
-    unmark-as-sensitive: "Desmarcar como 'sensible'"
-    copy-url: "Copia la URL"
-    download: "Descargar"
-    else-files: "Otros"
-    set-as-avatar: "Utilizar como avatar"
-    set-as-banner: "Utilizar como banner"
-    open-in-app: "Abrir en la aplicación"
-    add-app: "Añadir aplicación"
-    rename-file: "Renombra el fichero"
-    input-new-file-name: "Escribe el nombre nuevo"
-    copied: "Copiado"
-    copied-url-to-clipboard: "URL copiada al porta papeles"
-desktop/views/components/drive.folder.vue:
-  unable-to-process: "La operación no se puede llevar a cabo"
-  circular-reference-detected: "La carpeta de destino es una sub-carpeta de la carpeta que quieres mover."
-  unhandled-error: "Error desconocido"
-  contextmenu:
-    move-to-this-folder: "Mover a esta carpeta"
-    show-in-new-window: "Abrir en una ventana nueva"
-    rename: "Renombrar"
-    rename-folder: "Renombrar carpeta"
-    input-new-folder-name: "Escribe el nombre nuevo"
-    else-folders: "Otros"
-desktop/views/components/drive.vue:
-  search: "Buscar"
-  empty-draghover: "¡Saluda!"
-  empty-drive: "Tu disco está vacio"
-  empty-drive-description: "También puedes subir archivos seleccionándolos y con el botón derecho selecciona \"Subir fichero\" o puedes arrastrarlo hasta la ventana."
-  empty-folder: "La carpeta está vacia"
-  unable-to-process: "La operación no se puede llevar a cabo."
-  circular-reference-detected: "La carpeta de destino es una sub-carpeta de la carpeta que quieres mover."
-  unhandled-error: "Errer desconocido"
-  url-upload: "Subir desde una URL"
-  url-of-file: "URL del fichero que quieres subir"
-  url-upload-requested: "Subida solicitada"
-  may-take-time: "Subir el fichero puede tardar un tiempo."
-  create-folder: "Crear una carpeta"
-  folder-name: "Nombre de la carpeta"
-  contextmenu:
-    create-folder: "Crear una carpeta"
-    upload: "Subir fichero"
-    url-upload: "Subir desde una URL"
-desktop/views/components/media-video.vue:
-  sensitive: "Este contenido no es apropiado para ver en el trabajo"
-  click-to-show: "Click para mostrar"
-desktop/views/components/followers-window.vue:
-  followers: "{} seguidores"
-desktop/views/components/followers.vue:
-  empty: "Parece que no tienes seguidores aún."
-desktop/views/components/following-window.vue:
-  following: "Siguiendo {}"
-desktop/views/components/following.vue:
-  empty: "Parece que aún no sigues a nadie."
-desktop/views/components/game-window.vue:
-  game: "Reversi"
-desktop/views/components/home.vue:
-  done: "Listo"
-  add-widget: "Agregar accesorio:"
-  add: "Agregar"
-desktop/views/input-dialog.vue:
-  cancel: "Cancelar"
-  ok: "OK"
-desktop/views/components/note-detail.vue:
-  private: "Esta publicación es privada"
-  deleted: "Esta publicación ha sido removida"
-  location: "Localización"
-  renote: "Republicar"
-  add-reaction: "Agregar una reacción"
-desktop/views/components/note.vue:
-  reply: "Responder"
-  renote: "Volver a publicar"
-  add-reaction: "Reacción"
-  detail: "Detalles"
-  private: "Esta publicación es privada"
-  deleted: "Esta publicación ha sido removida"
-desktop/views/components/notes.vue:
-  error: "Error al cargar."
-  retry: "Reintentar"
-desktop/views/components/notifications.vue:
-  empty: "No hay notificaciones"
-desktop/views/components/post-form.vue:
-  posted: "¡Publicado!"
-  replied: "¡Respondido!"
-  reposted: "¡Republicado!"
-  note-failed: "Error al publicar nota"
-  reply-failed: "Error al responder"
-  renote-failed: "Error al republicar"
-desktop/views/components/post-form-window.vue:
-  note: "Nota nueva"
-  reply: "Responder"
-  attaches: "{} archivo(s) multimedia adjuntados"
-  uploading-media: "Subiendo {} archivo(s) multimedia"
-desktop/views/components/progress-dialog.vue:
-  waiting: "Un momento"
-desktop/views/components/renote-form.vue:
-  quote: "Cita..."
-  cancel: "Cancelar"
-  renote: "Volver a publicar"
-  reposting: "Publicando de nuevo..."
-  success: "¡Publicado!"
-  failure: "La publicación ha fallado"
-desktop/views/components/renote-form-window.vue:
-  title: "¿Seguro qué quieres volver a publicarlo?"
-desktop/views/pages/user-following-or-followers.vue:
-  following: "{user} sigue a"
-  followers: "Seguidores de {user}"
-desktop/views/components/settings.2fa.vue:
-  detail: "Ver detalles..."
-  url: "https://www.google.com/landing/2step/"
-  caution: "Si pierdes acceso al dispositivo, no podrás conectarte a Misskey."
-  register: "Registrar un dispositivo"
-  already-registered: "Un dispositivo ya fue registrado"
-  unregister: "Inhabilitado"
-  unregistered: "Autenticación de dos pasos fue deshabilitada."
-  enter-password: "Escribe una contraseña"
-  authenticator: "Primero, necesitas instalar Google Authenticator en tu dispositivo:"
-  howtoinstall: "Cómo instalar"
-  token: "Token"
-  scan: "Luego, escanea el código QR:"
-  done: "Por favor ingresa el token mostrado en tu dispositivo:"
-  submit: "Enviar"
-  success: "¡Configuraciones guardadas!"
-  failed: "Error al configurar. Por favor asegúrate de que el token es correcto."
-  info: "Desde ahora, ingresa el token que se muestra en tu dispositivo adicionalmente a tu contraseña cuando inicies sesión en Misskey"
-common/views/components/media-image.vue:
-  sensitive: "Este contenido no es apropiado para ver en el trabajo"
-  click-to-show: "Click para mostrar"
-common/views/components/api-settings.vue:
-  token: "Token:"
-  enter-password: "Escribe una contraseña"
-  console:
-    title: "Consola API"
-    send: "Enviar"
-desktop/views/components/settings.apps.vue:
-  no-apps: "No hay aplicaciones asociadas"
-common/views/components/drive-settings.vue:
-  in-use: "usado"
-  stats: "Estadísticas"
-common/views/components/mute-and-block.vue:
-  mute-and-block: "Silenciar y bloquear"
-  mute: "Silenciar"
-  block: "Bloquear"
-  save: "Guardar"
-common/views/components/post-form-attaches.vue:
-  mark-as-sensitive: "Marcar como 'sensible'"
-  unmark-as-sensitive: "Desmarcar como 'sensible'"
-desktop/views/components/sub-note-content.vue:
-  private: "Esta publicación es privada"
-  deleted: "Esta publicación ha sido removida"
-  poll: "Encuesta"
-desktop/views/components/settings.tags.vue:
-  title: "Etiqueta"
-  add: "Agregar"
-  save: "Guardar"
-desktop/views/components/timeline.vue:
-  home: "Inicio"
-  local: "Local"
-  hybrid: "Social"
-  global: "Global"
-  list: "Listas"
-  hashtag: "Hashtags"
-  list-name: "Nombre de lista"
-desktop/views/components/ui.header.vue:
-  welcome-back: "Bienvenido/a de vuelta,"
-  adjective: "-san"
-desktop/views/components/ui.header.account.vue:
-  profile: "Tu perfil"
-  lists: "Listas"
-  follow-requests: "Solicitudes de seguimiento"
-  admin: "Admin"
-desktop/views/components/ui.header.nav.vue:
-  game: "Juegos"
-desktop/views/components/ui.header.notifications.vue:
-  title: "Notificaciones"
-desktop/views/components/ui.header.post.vue:
-  post: "Crear una publicación"
-desktop/views/components/ui.header.search.vue:
-  placeholder: "Buscar"
-desktop/views/components/user-preview.vue:
-  notes: "Publicaciones"
-  following: "Sigue"
-  followers: "Seguidores"
-desktop/views/components/users-list.vue:
-  all: "Todo"
-desktop/views/components/window.vue:
-  close: "Cerrar"
-admin/views/index.vue:
-  dashboard: "Panel de control"
-  instance: "Instancia"
-  moderators: "Moderadores"
-  users: "Usuarios"
-  federation: "Federado"
-  queue: "Cola de trabajos"
-  logs: "Registros"
-  back-to-misskey: "Volver a Misskey"
-admin/views/dashboard.vue:
-  dashboard: "Panel de Control"
-  accounts: "Cuenta"
-  notes: "Publicaciones"
-  drive: "Drive"
-  instances: "Instancias"
-  this-instance: "Esta instancia"
-  federated: "Federado"
-admin/views/queue.vue:
-  title: "Cola"
-  remove-all-jobs: "Limpiar todos los trabajos pendientes"
-  queue: "Cola"
-admin/views/logs.vue:
-  logs: "Registros"
-admin/views/abuse.vue:
-  title: "Abuso"
-  target: "Destinatario"
-  reporter: "Informador"
-  details: "Detalles"
-  remove-report: "eliminar"
-admin/views/instance.vue:
-  instance: "Instancia"
-  instance-name: "Nombre de la instancia"
-  instance-description: "Descripción de la instancia"
-  host: "Host"
-  banner-url: "URL de la imagen de banner"
-  error-image-url: "Error en la URL de la imagen"
-  languages: "Idioma de esta instancia"
-  languages-desc: "Puedes añadir mas de uno, separado por espacios."
-  maintainer-config: "Información del administrador"
-  maintainer-name: "Nombre del administrador"
-  maintainer-email: "Contactar con el administrador"
-  drive-config: "Ajustes del Drive"
-  cache-remote-files: "Mantener en cache los archivos remotos"
-  recaptcha-preview: "Vista previa"
-  invite: "Invitar"
-  save: "Guardar"
-  saved: "Guardado"
-  email: "Correo electrónico"
-  smtp-host: "Host SMTP"
-  smtp-port: "Puerto SMTP"
-  smtp-user: "Usuario SMTP"
-  smtp-pass: "Contraseña SMTP"
-  test-email: "Prueba"
-admin/views/charts.vue:
-  title: "Gráficos"
-  per-day: "Por día"
-  per-hour: "Por hora"
-  federation: "Federación"
-  users: "Usuarios"
-  drive: "Drive"
-  network: "Red"
-admin/views/drive.vue:
-  sort:
-    title: "Ordenar"
-  origin:
-    combined: "Local+Remoto"
-    local: "Local"
-    remote: "Remoto"
-  delete: "eliminar"
-  mark-as-sensitive: "Marcar como 'sensible'"
-  unmark-as-sensitive: "Desmarcar como 'sensible'"
-admin/views/users.vue:
-  username: "Usuario"
-  host: "Host"
-  users:
-    state:
-      all: "Todo"
-      moderator: "Moderadores"
-    origin:
-      local: "Local"
-admin/views/moderators.vue:
-  logs:
-    title: "Registros"
-    moderator: "Moderadores"
-admin/views/emoji.vue:
-  add-emoji:
-    add: "Agregar"
-  emojis:
-    remove: "eliminar"
-admin/views/announcements.vue:
-  save: "Guardar"
-  remove: "eliminar"
-  add: "Agregar"
-  saved: "Guardado"
-admin/views/federation.vue:
-  instance: "Instancia"
-  host: "Host"
-  following: "Siguiendo"
-  status: "Estado"
-  block: "Bloquear"
-  instances: "Federado"
-  states:
-    all: "Todo"
-    blocked: "Bloquear"
-  charts: "Gráficos"
-  chart-spans:
-    hour: "Por hora"
-    day: "Por día"
-  blocked-hosts: "Bloquear"
-  save: "Guardar"
-desktop/views/pages/welcome.vue:
-  timeline: "Timeline"
-desktop/views/pages/selectdrive.vue:
-  cancel: "Cancelar"
-desktop/views/pages/user-list.users.vue:
-  username: "Usuario"
-desktop/views/pages/user/user.followers-you-know.vue:
-  loading: "cargando"
-desktop/views/pages/user/user.friends.vue:
-  loading: "cargando"
-desktop/views/pages/user/user.photos.vue:
-  loading: "cargando"
-  no-photos: "No hay fotos."
-desktop/views/pages/user/user.header.vue:
-  month: "lunes"
-  day: "domingo"
-desktop/views/pages/user/user.timeline.vue:
-  default: "Posts"
-  with-replies: "Posts y respuestas"
-  with-media: "Multimedia"
-  my-posts: "Mis posts"
-desktop/views/widgets/notifications.vue:
-  title: "Notificaciones"
-desktop/views/widgets/polls.vue:
-  title: "Encuestas"
-  nothing: "No hay notificaciones"
-desktop/views/widgets/trends.vue:
-  nothing: "No hay notificaciones"
-mobile/views/components/drive.vue:
-  used: "usado"
-  folder-name: "Nombre de la carpeta"
-  url-prompt: "URL del fichero que quieres subir"
-mobile/views/components/drive.file.vue:
-  nsfw: "Este contenido no es apropiado para ver en el trabajo"
-mobile/views/components/drive.file-detail.vue:
-  download: "Descargar"
-  rename: "Renombrar"
-  nsfw: "Este contenido no es apropiado para ver en el trabajo"
-  mark-as-sensitive: "Marcar como 'sensible'"
-  unmark-as-sensitive: "Desmarcar como 'sensible'"
-mobile/views/components/media-video.vue:
-  sensitive: "Este contenido no es apropiado para ver en el trabajo"
-  click-to-show: "Click para mostrar"
-common/views/components/follow-button.vue:
-  following: "Siguiendo"
-  request-pending: "Solicitud pendiente"
-  follow-processing: "Solicitud en proceso"
-  follow-request: "Solicitudes de seguimiento"
-mobile/views/components/note.vue:
-  private: "Esta publicación es privada"
-  deleted: "Esta publicación ha sido removida"
-  location: "Localización"
-mobile/views/components/note-detail.vue:
-  reply: "Responder"
-  private: "Esta publicación es privada"
-  deleted: "Esta publicación ha sido removida"
-  location: "Localización"
-mobile/views/components/notifications.vue:
-  empty: "No hay notificaciones"
-mobile/views/components/sub-note-content.vue:
-  private: "Esta publicación es privada"
-  deleted: "Esta publicación ha sido removida"
-  poll: "Encuestas"
-mobile/views/components/ui.header.vue:
-  welcome-back: "Bienvenido/a de vuelta,"
-  adjective: "-san"
-mobile/views/components/ui.nav.vue:
-  timeline: "Timeline"
-  notifications: "Notificaciones"
-  follow-requests: "Solicitudes de seguimiento"
-  search: "Buscar"
-  user-lists: "Listas"
-  game: "Juegos"
-  admin: "Admin"
-  about: "Sobre"
-mobile/views/pages/drive.vue:
-  contextmenu:
-    upload: "Subir fichero"
-    create-folder: "Crear una carpeta"
-mobile/views/pages/home.vue:
-  home: "Inicio"
-  local: "Local"
-  hybrid: "Social"
-  global: "Global"
-mobile/views/pages/widgets.vue:
-  dashboard: "Panel de control"
-  add-widget: "Agregar"
-  customization-tips: "Consejos de personalización"
-mobile/views/pages/widgets/activity.vue:
-  activity: "Actividad"
-mobile/views/pages/games/reversi.vue:
-  reversi: "Reversi"
-mobile/views/pages/search.vue:
-  search: "Buscar"
-mobile/views/pages/notifications.vue:
-  notifications: "Notificaciones"
-mobile/views/pages/user.vue:
-  timeline: "Timeline"
-mobile/views/pages/user/home.vue:
-  activity: "Actividad"
-mobile/views/pages/user/home.photos.vue:
-  no-photos: "No hay fotos."
-deck:
-  home: "Inicio"
-  local: "Local"
-  hybrid: "Social"
-  hashtag: "Etiquetas"
-  global: "Global"
-  notifications: "Notificaciones"
-  list: "Listas"
-  rename: "Renombrar"
-deck/deck.user-column.vue:
-  activity: "Actividad"
-  timeline: "Timeline"
-pages:
-  pin-this-page: "Fijar en el perfil"
-  like: "Me gusta"
-  blocks:
-    post: "Formulario"
-  script:
-    categories:
-      random: "Aleatorio"
-      list: "Listas"
-    blocks:
-      _join:
-        arg1: "Listas"
-      random: "Aleatorio"
-      _randomPick:
-        arg1: "Listas"
-      _dailyRandomPick:
-        arg1: "Listas"
-      _seedRandomPick:
-        arg2: "Listas"
-      _pick:
-        arg1: "Listas"
-      _listLen:
-        arg1: "Listas"
-    types:
-      array: "Listas"
-room:
-  save: "Guardar"
-  saved: "Guardado"
-  furnitures:
-    moon: "Luna"
-    bin: "Papelera"
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
deleted file mode 100644
index 3f698f1c1588d576f578f71c6f1bad4bd5e8179c..0000000000000000000000000000000000000000
--- a/locales/fr-FR.yml
+++ /dev/null
@@ -1,2135 +0,0 @@
----
-meta:
-  lang: "Français"
-common:
-  misskey: "Une ⭐ du fédiverse"
-  about-title: "Une ⭐ du fédiverse."
-  about: "Merci d’avoir choisis Misskey. Misskey est une <b>plateforme de microblogage distribuée</b> née sur Terre et fait partie du Fédiverse (un univers composé de diverses plateformes de réseaux sociaux organisées), elle est connectée mutuellement avec d’autres plateformes de réseaux sociaux. Désirez-vous prendre une pause, un court instant, loin de l’agitation de la ville et plonger dans un Internet d’un nouveau genre ?"
-  intro:
-    title: "C’est quoi Misskey ?"
-    about: "Misskey est un <b>réseau social de Microblogage</b> open source. Il offre une interface utilisateur riche et hautement personnalisable, une variété de réactions aux publications et un lecteur pour la gestion centralisée de fichiers. De plus, comme il est possible de se connecter au reste du Fédiverse, vous pouvez interagir avec d'autres plateformes fédérées. Par exemple, si vous publiez quelque chose, la note sera transmise non seulement aux utilisateurs de Misskey, mais aussi à d'autres plateformes de réseaux sociaux dans le Fédiverse. Imaginez que vous puissiez transmettre des ondes radio d'une planète vers l'autre."
-    features: "Options"
-    rich-contents: "Notes"
-    rich-contents-desc: "Partagez vos idées, les événements et les sujets qui vous tiennent à cœur ainsi que tout autre chose que vous souhaitez partager avec les autres. Si vous le désirez, vous pouvez décorer vos messages en utilisant une syntaxe différente ou en y joignant des sondages et des fichiers, tels que les photos ou les vidéos que vous aimez."
-    reaction: "Réactions"
-    reaction-desc: "Une manière simple d'exprimer vos émotions. Misskey peut attacher diverses réactions aux publications des autres utilisateur·rice·s. Si vous essayez les réactions sur Misskey, vous ne pourrez plus retourner sur une autre plateforme de réseaux sociaux n'offrant que des « J'aime »."
-    ui: "Interface"
-    ui-desc: "Aucune interface graphique ne peut plaire à tout le monde. Par conséquent, Misskey possède une interface utilisateur hautement personnalisable selon vos goûts. Vous pouvez rendre votre page d'accueil originale en modifiant la mise en page de votre fil et en déplaçant les widgets que vous pouvez facilement ajuster pour vous approprier cet espace."
-    drive: "Drive"
-    drive-desc: "Vous voulez poster une photo que vous avez déjà transférée ? Vous souhaitez organiser, nommer et créer un dossier pour vos fichiers téléversés ? Misskey Drive est la meilleure solution pour vous. Très facile de partager vos fichiers en ligne."
-    outro: "Découvrez vous-même les fonctionnalités de Misskey. Étant donné que Misskey est un réseau social fédéré, vous pouvez essayer d’autres instances afin de trouver vos amis si la présente instance ne vous correspond pas. Bonne chance et amusez-vous bien !"
-  application-authorization: "Autorisations de l’application"
-  close: "Fermer"
-  do-not-copy-paste: "Veuillez ne pas entrer ou coller le code ici. Le compte pourrait être compromis."
-  load-more: "Charger plus"
-  enter-password: "Veuillez entrer le mot de passe"
-  2fa: "Authentification à deux facteurs"
-  customize-home: "Personnaliser la disposition de votre accueil"
-  featured-notes: "Les notes mises en avant"
-  dark-mode: "Mode nuit"
-  signin: "Se connecter"
-  signup: "S'enregistrer"
-  signout: "Se déconnecter"
-  reload-to-apply-the-setting: "Le rechargement de la page est nécessaire pour appliquer ces paramètres. Désirez-vous la recharger maintenant ?"
-  fetching-as-ap-object: "Récupération depuis le fédiverse"
-  unfollow-confirm: "Désirez-vous vous désabonner de {name} ?"
-  delete-confirm: "Supprimer cette publication ?"
-  signin-required: "Veuillez vous connecter"
-  notification-type: "Type de notifications"
-  notification-types:
-    all: "Tout"
-    pollVote: "Sondages"
-    follow: "Abonnements"
-    receiveFollowRequest: "Demandes d’abonnements"
-    reply: "Réponses"
-    quote: "Cité par"
-    renote: "Partages"
-    mention: "Mentions"
-    reaction: "Réactions"
-  got-it: "J’ai compris !"
-  customization-tips:
-    title: "Conseils de personnalisation"
-    paragraph: "<p>La personnalisation de la page d'accueil vous permet d'ajouter/supprimer, glisser-déposer et réarranger les widgets.</p><p>Vous pouvez changer l'apparence de certain widget avec le <strong><strong>clic</strong>droit</strong>.</p><p>Pour supprimer un widget, faites glisser le widget sur <strong>la zone \"Corbeille\"</strong> dans l'en-tête.</p><p>Pour terminer la personnalisation, cliquez sur \"Terminé\" en haut à droite.</p>"
-    gotit: "Compris !"
-  notification:
-    file-uploaded: "Le fichier a été téléversé !"
-    message-from: "Message de {} :"
-    reversi-invited: "Invité à jouer"
-    reversi-invited-by: "Invité par {} :"
-    notified-by: "Notifié par {} :"
-    reply-from: "Réponse de {} :"
-    quoted-by: "Cité par {} :"
-  time:
-    unknown: "inconnu"
-    future: "à l’instant"
-    just_now: "à l'instant"
-    seconds_ago: "Il y a {} seconde(s)"
-    minutes_ago: "Il y a {} min"
-    hours_ago: "Il y a {} h"
-    days_ago: "Il y a {} j"
-    weeks_ago: "Il y a {} semaines"
-    months_ago: "Il y a {} mois"
-    years_ago: "Il y a {} an(s)"
-  month-and-day: "{day}-{month}"
-  trash: "Corbeille"
-  drive: "Drive"
-  pages: "Pages"
-  messaging: "Conversations"
-  home: "Principal"
-  deck: "Deck"
-  timeline: "Fil"
-  explore: "Découvrir"
-  following: "Suit"
-  followers: "Abonné·e·s"
-  favorites: "Favorites"
-  permissions:
-    "read:account": "Afficher les informations du compte"
-    "write:account": "Mettre à jour les informations de votre compte"
-    "read:blocks": "Voir les blocs"
-    "write:blocks": "Écrire des blocs"
-    "read:drive": "Parcourir le Drive"
-    "write:drive": "Écrire sur le Drive"
-    "read:favorites": "Afficher les favoris"
-    "write:favorites": "Écrire des favoris"
-    "read:following": "Voir les informations de l'abonné"
-    "write:following": "Suivre/Ne plus suivre"
-    "read:messaging": "Lire les conversations"
-    "write:messaging": "Utiliser la messagerie"
-    "read:mutes": "Voir les comptes masqués"
-    "write:mutes": "Gérer les comptes muets"
-    "write:notes": "Créer ou supprimer des publications"
-    "read:notifications": "Afficher les notifications"
-    "write:notifications": "Gérer vos notifications"
-    "read:reactions": "Lire les réactions"
-    "write:reactions": "Gérer vos réactions"
-    "write:votes": "Vote"
-    "read:pages": "Afficher la page"
-    "write:pages": "Mettre à jour les Pages"
-    "read:page-likes": "Lire les favoris sur les Pages"
-    "write:page-likes": "Mettre à jour les favoris sur les Pages"
-    "read:user-groups": "Voir les groupes d'utilisateur·rice·s"
-    "write:user-groups": "Éditer les groupes des utilisateur·rice·s"
-  empty-timeline-info:
-    follow-users-to-make-your-timeline: "Les utilisateur·rice·s suivant·e·s afficheront leurs publications sur votre fil."
-    explore: "Trouver des utilisateur·rice·s"
-  post-form:
-    attach-location-information: "Joindre des informations de localisation"
-    hide-contents: "Masquer les contenus"
-    reply-placeholder: "Répondre à cette note …"
-    quote-placeholder: "Citer cette note …"
-    option-quote-placeholder: "Citer ce billet ... (Facultatif)"
-    quote-attached: "Cité"
-    quote-question: "Souhaitez-vous ajoutez une citation ?"
-    submit: "Publication"
-    reply: "Répondre"
-    renote: "Republier"
-    posting: "Publication …"
-    attach-media-from-local: "Joindre un média depuis votre appareil"
-    attach-media-from-drive: "Joindre un média depuis votre Drive"
-    insert-a-kao: "v('ω')v"
-    create-poll: "Créer un sondage"
-    text-remain: "{} caractères restants"
-    recent-tags: "Récent"
-    local-only-message: "Ce message sera publié uniquement sur le fil local"
-    click-to-tagging: "Cliquer pour taguer"
-    visibility: "Visibilité"
-    geolocation-alert: "Votre appareil ne prend pas en charge les services de localisation"
-    error: "Erreur"
-    enter-username: "Saisir un nom d'utilisateur"
-    specified-recipient: "Correspondant·e"
-    add-visible-user: "Ajouter un utilisateur"
-    cw-placeholder: "Commenter le contenu (optionnel)"
-    username-prompt: "Saisir un nom d'utilisateur"
-    enter-file-name: "Éditer le nom du fichier"
-  weekday-short:
-    sunday: "D"
-    monday: "L"
-    tuesday: "M"
-    wednesday: "M"
-    thursday: "J"
-    friday: "V"
-    saturday: "S"
-  weekday:
-    sunday: "Dimanche"
-    monday: "Lundi"
-    tuesday: "Mardi"
-    wednesday: "Mercredi"
-    thursday: "Jeudi"
-    friday: "Vendredi"
-    saturday: "Samedi"
-  reactions:
-    like: "Bien"
-    love: "Adore"
-    laugh: "Rire"
-    hmm: "Hmm … ?"
-    surprise: "Wow"
-    congrats: "Félicitations !"
-    angry: "En colère"
-    confused: "Confus"
-    rip: "RIP"
-    pudding: "Pudding"
-  note-visibility:
-    public: "Public"
-    home: "Principal"
-    home-desc: "Publier sur le fil principal uniquement"
-    followers: "Abonné·e·s"
-    followers-desc: "Publier à vos abonné·e·s uniquement"
-    specified: "Direct"
-    specified-desc: "Publier uniquement aux utilisateur·rice·s mentionné·e·s"
-    local-public: "Local (Public)"
-    local-home: "Accueil (local uniquement)"
-    local-followers: "Abonné·e·s (Local uniquement)"
-  note-placeholders:
-    a: "Que faites-vous maintenant ?"
-    b: "Quoi de neuf ?"
-    c: "Qu’avez-vous en tête ?"
-    d: "Désirez-vous publier quelques mots ?"
-    e: "Écrivez ici"
-    f: "En attente de vos écrits"
-  settings: "Paramètres"
-  _settings:
-    profile: "Votre profil"
-    notification: "Notifications"
-    apps: "Applications"
-    tags: "Hashtags"
-    mute-and-block: "Masqués / Bloqués"
-    blocking: "En cours blocage"
-    security: "Sécurité"
-    signin: "Historique des connexions"
-    password: "Mot de passe"
-    other: "Avancé"
-    appearance: "Apparence"
-    behavior: "Comportement"
-    reactions: "Réaction"
-    reactions-description: "Personnaliser les émojis à afficher dans le sélecteur de réactions, délimités par les sauts de ligne."
-    fetch-on-scroll: "Chargement automatique lors du défilement"
-    fetch-on-scroll-desc: "Chargement automatique du contenu lors du défilement de la page."
-    note-visibility: "Visibilité de la publication"
-    default-note-visibility: "Visibilité par défaut"
-    remember-note-visibility: "Se souvenir du mode de visibilité de la publication"
-    web-search-engine: "Moteur de recherche Web"
-    web-search-engine-desc: "Exemple : https://www.google.com/?#q={{query}}"
-    paste: "Coller"
-    pasted-file-name: "Modèle de nom de fichier collé"
-    pasted-file-name-desc: "Exemple : \"yyyy-MM-dd HH-mm-ss [{{number}}]\" → \"2018-03-20 21-30-24 1\""
-    paste-dialog: "Modifier le nom du fichier collé"
-    keep-cw: "Maintenir l'avertissement de contenu"
-    keep-cw-desc: "Lorsque vous répondez à un message, le même avertissement de contenu est reprit par défaut dans la réponse, le même que celui qui a été défini dans le message original."
-    i-like-sushi: "Je préfère les sushis plutôt que le pudding"
-    show-reversi-board-labels: "Afficher les étiquettes des lignes et colonnes dans Reversi"
-    use-avatar-reversi-stones: "Utiliser l’avatar comme pion dans Reversi"
-    disable-animated-mfm: "Désactiver les textes animés dans les publications"
-    disable-showing-animated-images: "Désactiver l'animation des images"
-    enable-quick-notification-view: "Activer l'affichage rapide des notifications"
-    suggest-recent-hashtags: "Afficher les hashtags populaires dans le champs de saisie"
-    always-show-nsfw: "Toujours afficher les contenus sensibles"
-    always-mark-nsfw: "Toujours marquer les notes ayant des médias comme sensibles"
-    show-full-acct: "Afficher l’adresse complète de l’utilisateur"
-    show-via: "Afficher via"
-    reduce-motion: "Réduire les animations dans l’interface utilisateur"
-    this-setting-is-this-device-only: "Uniquement sur cet appareil"
-    use-os-default-emojis: "Utiliser les émojis standards du système"
-    line-width: "Epaisseur du trait"
-    line-width-thin: "Fine"
-    line-width-normal: "Normale"
-    line-width-thick: "Épaisse"
-    font-size: "Taille du texte"
-    font-size-x-small: "Très petit"
-    font-size-small: "Petite"
-    font-size-medium: "Normale"
-    font-size-large: "Grande"
-    font-size-x-large: "Large"
-    deck-column-align: "Alignement des colonnes du Deck"
-    deck-column-align-center: "Centrer"
-    deck-column-align-left: "À gauche"
-    deck-column-align-flexible: "Flexible"
-    deck-column-width: "Largeur des colonnes du Deck"
-    deck-column-width-narrow: "Étroite"
-    deck-column-width-narrower: "Légèrement étroite"
-    deck-column-width-normal: "Normale"
-    deck-column-width-wider: "Légèrement large"
-    deck-column-width-wide: "Large"
-    use-shadow: "Utiliser les ombres dans l'interface utilisateur"
-    rounded-corners: "Coins arrondis de l'interface utilisateur"
-    circle-icons: "Utiliser des avatar circulaires"
-    contrasted-acct: "Ajouter du contraste au nom de l’utilisateur"
-    wallpaper: "Image du fond d'écran"
-    choose-wallpaper: "Sélectionner un fond d'écran"
-    delete-wallpaper: "Supprimer le fond d'écran"
-    post-form-on-timeline: "Afficher le champs de saisie en haut du fil"
-    show-clock-on-header: "Afficher l'horloge sur le coté supérieur droit"
-    show-reply-target: "Afficher les réponses"
-    timeline: "Fil d’actualité"
-    show-my-renotes: "Afficher mes republications dans le fil"
-    show-renoted-my-notes: "Afficher les partages de mes propres notes sur le fil"
-    show-local-renotes: "Afficher les partages locaux sur les fils"
-    remain-deleted-note: "Continuer à afficher les notes supprimées"
-    sound: "Son"
-    enable-sounds: "Activer les sons"
-    enable-sounds-desc: "Jouer un son lorsque vous recevez un message/publication. Ce paramètre est sauvegardé dans le navigateur."
-    volume: "Volume"
-    test: "Test"
-    update: "Mise à jour de Misskey"
-    version: "Version actuelle :"
-    latest-version: "Dernière version :"
-    update-checking: "Recherche de mises à jour"
-    do-update: "Rechercher des mises à jour"
-    update-settings: "Paramètres avancés"
-    no-updates: "Aucune mise à jour disponible"
-    no-updates-desc: "Votre Misskey est à jour."
-    update-available: "Nouvelle version disponible !"
-    update-available-desc: "Les mises à jour seront appliquées une fois la page est rechargée."
-    advanced-settings: "Paramètres avancés"
-    debug-mode: "Activer le mode débogage"
-    debug-mode-desc: "Ce paramètre est stocké dans le navigateur."
-    navbar-position: "Position de la barre de navigation"
-    navbar-position-top: "En haut"
-    navbar-position-left: "À gauche"
-    navbar-position-right: "À droite"
-    i-am-under-limited-internet: "J'ai un accès Internet limité"
-    post-style: "Style d'affichage des notes"
-    post-style-standard: "Standard"
-    post-style-smart: "Intelligent"
-    notification-position: "Afficher les notifications"
-    notification-position-bottom: "en bas"
-    notification-position-top: "En haut"
-    disable-via-mobile: "Enlever la mention publié via 'mobile'"
-    load-raw-images: "Afficher les photos jointes dans leur qualité originale"
-    load-remote-media: "Afficher les médias depuis le serveur distant"
-    sync: "Synchroniser"
-    save: "Enregistrer"
-    saved: "enregistré"
-    preview: "Prévisualisation"
-    home-profile: "Profil principal"
-    deck-profile: "Profil deck"
-    room: "Pièce"
-    _room:
-      graphicsQuality: "Qualité des graphismes"
-      _graphicsQuality:
-        ultra: "Très élevée"
-        high: "Élevée"
-        medium: "Moyenne"
-        low: "Basse"
-        cheep: "Minimale"
-      useOrthographicCamera: "Utiliser une caméra orthographique"
-  search: "Recherche"
-  delete: "Supprimer"
-  loading: "Chargement en cours …"
-  ok: "Confirmer"
-  cancel: "Quitter"
-  update-available-title: "Mise à jour disponible"
-  update-available: "Une nouvelle version de Misskey est disponible ({newer}, version actuelle: {current}). Veuillez recharger la page pour appliquer la mise à jour."
-  my-token-regenerated: "Votre jeton vient d’être généré, vous allez maintenant être déconnecté."
-  hide-password: "Masquer le mot de passe"
-  show-password: "Afficher le mot de passe"
-  enter-username: "Saisir un nom d'utilisateur"
-  do-not-use-in-production: "Il s’agit d’une version de développement. Ne pas utiliser dans un environnement de production."
-  user-suspended: "Cet·te utilisateur·trice a été suspendu·e"
-  is-remote-user: "Les informations à propos de ce compte peuvent être incomplètes."
-  is-remote-post: "Ceci est une publication distante."
-  view-on-remote: " Consulter le profil complet"
-  renoted-by: "Renoté par {user}"
-  no-notes: "Sans aucune note"
-  turn-on-darkmode: "Mode nuit"
-  turn-off-darkmode: "Mode jour"
-  error:
-    title: "Une erreur est survenue"
-    retry: "Réessayer"
-  reversi:
-    drawn: "Partie nulle"
-    my-turn: "C’est votre tour"
-    opponent-turn: "Tour de l’adversaire"
-    turn-of: "Tour de {name}"
-    past-turn-of: "Tour de {name}"
-    won: "{name} a gagné"
-    black: "Noirs"
-    white: "Blancs"
-    total: "Total"
-    this-turn: "Tour {count}"
-  widgets:
-    analog-clock: "Horloge analogique"
-    profile: "Profil"
-    calendar: "Calendrier"
-    timemachine: "Calendrier (Machine temporelle)"
-    activity: "Activité"
-    rss: "Lecteur de flux RSS"
-    memo: "Pense-bête"
-    trends: "Tendances"
-    photo-stream: "Flux de photos"
-    posts-monitor: "Graphe des publications"
-    slideshow: "Diaporama"
-    version: "Version"
-    broadcast: "Diffusion"
-    notifications: "Notifications"
-    users: "Utilisateur·rice·s recommandé·e·s"
-    polls: "Sondages"
-    post-form: "Champs de publication"
-    server: "Infos sur le serveur"
-    nav: "Navigation"
-    tips: "Conseils"
-    hashtags: "Hashtags"
-    queue: "File d'attente"
-  dev: "Échec lors de la création de l’application. Veuillez réessayer."
-  ai-chan-kawaii: "Ai-Chan est mignonne !"
-  you: "Vous"
-auth/views/form.vue:
-  share-access: "Désirez-vous autoriser <i>{name}</i> à avoir accès à votre compte ?"
-  permission-ask: "Cette application nécessite les autorisations suivantes :"
-  cancel: "Annuler"
-  accept: "Autoriser l’accès"
-auth/views/index.vue:
-  loading: "Chargement en cours"
-  denied: "L’autorisation de l’application a été refusée."
-  denied-paragraph: "Cette application ne va pas accéder à votre compte."
-  already-authorized: "Cette application est déjà autorisée."
-  allowed: "Permissions autorisées de l’application."
-  callback-url: "Retour vers l’application."
-  please-go-back: "Veillez retourner à l'application."
-  error: "La session n’existe pas."
-  sign-in: "Veuillez vous connecter"
-common/views/pages/explore.vue:
-  pinned-users: "Utilisateur·rice·s épinglé·e·s"
-  popular-users: "Utilisateur·rice·s populaires"
-  recently-updated-users: "Utilisateur·rice·s actif·ve·s récemment"
-  recently-registered-users: "Les nouveaux inscrits"
-  recently-discovered-users: "Utilisateurs récemment découverts"
-  popular-tags: "Mots-clés populaires"
-  federated: "Du Fédiverse"
-  explore: "Explorer {host}"
-  explore-fediverse: "Explorer le Fédiverse"
-  users-info: "Actuellement, {users} utilisateur·rice·s se sont inscrit ici"
-common/views/components/reactions-viewer.details.vue:
-  few-users: "{users} ont réagit avec {reaction}"
-common/views/components/url-preview.vue:
-  enable-player: "Activer la lecture"
-  disable-player: "Fermer le lecteur"
-common/views/components/user-list.vue:
-  no-users: "Il n'y a aucun utilisateur"
-common/views/components/games/reversi/reversi.vue:
-  matching:
-    waiting-for: "En attente de {}"
-    cancel: "Annuler"
-common/views/components/games/reversi/reversi.game.vue:
-  surrender: "Se rendre"
-  surrendered: "Par abandon"
-  is-llotheo: "Celui ou celle qui a moins de pierres gagne (Roseo)"
-  looped-map: "Carte en boucle"
-  can-put-everywhere: "Peut poser partout"
-common/views/components/games/reversi/reversi.index.vue:
-  title: "Misskey Reversi"
-  sub-title: "Jouer à Reversi avec vos amis !"
-  invite: "Inviter"
-  rule: "Comment jouer ?"
-  rule-desc: "Reversi est un jeu qui se joue sur un tablier et dans lequel les joueurs placent des pions sur ce dernier, à tour de rôle avec l'adversaire. Le but du jeu est d'avoir plus de pions de sa couleur que l'adversaire à la fin de la partie, celle-ci s'achevant lorsque aucun des deux joueurs ne peut plus jouer de coup légal, généralement lorsque les 64 cases sont occupées."
-  mode-invite: "Inviter"
-  mode-invite-desc: "Inviter un joueur."
-  invitations: "Vous avez reçu une invitation !"
-  my-games: "Mes jeux"
-  all-games: "Tous les jeux"
-  enter-username: "Saisir un nom d'utilisateur"
-  game-state:
-    ended: "Terminée"
-    playing: "En cours"
-common/views/components/games/reversi/reversi.room.vue:
-  settings-of-the-game: "Paramètres du jeu"
-  choose-map: "Sélectionnez une carte"
-  random: "Aléatoire"
-  black-or-white: "Noirs/Blancs"
-  black-is: "{} Noirs"
-  rules: "Règles"
-  is-llotheo: "Celui ou celle qui a le moins de pièces gagne (Llotheo)"
-  looped-map: "Carte en boucle"
-  can-put-everywhere: "Peut poser partout"
-  settings-of-the-bot: "Configuration du bot"
-  this-game-is-started-soon: "La partie commencera dans quelques instants"
-  waiting-for-other: "En attente que l'adversaire soit prêt"
-  waiting-for-me: "En attente que vous soyez prêt"
-  waiting-for-both: "En attente que vous soyez prêt"
-  cancel: "Annuler"
-  ready: "Prêt"
-  cancel-ready: "Annuler « Prêt »"
-common/views/components/connect-failed.vue:
-  title: "Échec de connexion au serveur"
-  description: "Il se peut qu’il y est un problème avec votre connexion internet, ou le serveur est hors-ligne ou en maintenance. Veuillez {réessayer} plus tard."
-  thanks: "On vous remercie d’avoir choisi d’utiliser Misskey."
-  troubleshoot: "Dépanner"
-common/views/components/connect-failed.troubleshooter.vue:
-  title: "Dépannage"
-  network: "Connexion au réseau"
-  checking-network: "Vérification de la connexion au réseau"
-  internet: "Connexion Internet"
-  checking-internet: "Vérification de la connexion internet"
-  server: "Connexion au serveur"
-  checking-server: "Vérification de la connexion au serveur"
-  finding: "Recherche d'un problème"
-  no-network: "Aucune connexion au réseau"
-  no-network-desc: "Veuillez vérifier que vous êtes bien connecté au réseau."
-  no-internet: "Aucune connexion internet."
-  no-internet-desc: "Assurez-vous que vous êtes bien connectés à internet."
-  no-server: "Impossible de se connecter au serveur"
-  no-server-desc: "Votre connexion semble correcte, mais il a été impossible de vous connecter au serveur de Misskey. Il se peut que le serveur soit hors-ligne ou en maintenance, veuillez ressayer plus tard."
-  success: "Connexion au serveur de Misskey réussie !"
-  success-desc: "Succès de la connexion au serveur de Misskey. Veuillez recharger la page."
-  flush: "Vider le cache"
-  set-version: "Choisissez une version"
-common/views/components/media-banner.vue:
-  sensitive: "Contenu sensible"
-  click-to-show: "Cliquer pour afficher"
-common/views/components/theme.vue:
-  theme: "Thème"
-  light-theme: "Thème en mode jour"
-  dark-theme: "Thème en mode nuit"
-  light-themes: "Thème clair"
-  dark-themes: "Thème sombre"
-  install-a-theme: "Installer un thème"
-  theme-code: "Code du thème"
-  install: "Installation"
-  installed: "« {} » a été installé"
-  create-a-theme: "Créer un thème"
-  save-created-theme: "Enregistrer le thème"
-  primary-color: "Couleur primaire"
-  secondary-color: "Couleur secondaire"
-  text-color: "Couleur du texte"
-  base-theme: "Thème de base"
-  base-theme-light: "Clair"
-  base-theme-dark: "Sombre"
-  find-more-theme: "Obtenir d’autres thèmes"
-  theme-name: "Nom du Thème"
-  preview-created-theme: "Prévisualisation"
-  invalid-theme: "Thème n’est pas valide."
-  already-installed: "Le thème est déjà installé."
-  saved: "enregistré"
-  manage-themes: "Gestion des thèmes"
-  builtin-themes: "Thèmes standards"
-  my-themes: "Mes thèmes"
-  installed-themes: "Thèmes installés"
-  select-theme: "Veuillez sélectionner un thème"
-  uninstall: "Désinstaller"
-  uninstalled: "« {} » a été désinstallé"
-  author: "Auteur"
-  desc: "Description"
-  export: "Exporter"
-  import: "Importer"
-  import-by-code: "Ou coller du code"
-  theme-name-required: "Nom du thème est obligatoire."
-common/views/components/cw-button.vue:
-  hide: "Masquer"
-  show: "Voir plus"
-  chars: "{count} caractères"
-  files: "{count} fichiers"
-  poll: "Sondage"
-common/views/components/messaging.vue:
-  search-user: "Trouver un utilisateur"
-  you: "Vous"
-  no-history: "Pas d'historique"
-  user: "Utilisateur·rice·s"
-  group: "Groupe"
-  start-with-user: "Initier une discussion avec un·e utilisateur·rice"
-  start-with-group: "Démarrer un groupe et converser"
-  select-group: "Sélectionner un groupe"
-common/views/components/messaging-room.vue:
-  not-talked-user: "Vous n'avez pas encore discuté avec cet·te utilisateur·rice"
-  not-talked-group: "Il n y a aucune conversation dans ce groupe"
-  no-history: "Aucun historique"
-  new-message: "Nouveau message"
-  only-one-file-attached: "Vous ne pouvez joindre qu'un seul fichier au message"
-common/views/components/messaging-room.form.vue:
-  input-message-here: "Tapez ici votre message"
-  send: "Envoyer"
-  attach-from-local: "Joindre un fichier depuis votre ordinateur"
-  attach-from-drive: "Joindre un fichier depuis votre Drive"
-  only-one-file-attached: "Vous ne pouvez joindre qu'un seul fichier au message"
-common/views/components/messaging-room.message.vue:
-  is-read: "Lu"
-  deleted: "Ce message a été supprimé"
-common/views/components/nav.vue:
-  about: "À propos"
-  stats: "Statistiques"
-  status: "Statut"
-  wiki: "Wiki"
-  donors: "Donateur·rice·s"
-  repository: "Dépôt"
-  develop: "Développeurs"
-  feedback: "Suggestions"
-  tos: "Conditions d'utilisation"
-common/views/components/note-menu.vue:
-  mention: "Mention"
-  detail: "Détails"
-  copy-content: "Copier le contenu"
-  copy-link: "Copier le lien"
-  favorite: "Mettre cette note en favoris"
-  unfavorite: "Retirer des favoris"
-  watch: "Surveiller"
-  unwatch: "Ne plus surveiller"
-  pin: "Épingler sur votre profil"
-  unpin: "Désépingler"
-  delete: "Supprimer"
-  delete-confirm: "Supprimer cette publication ?"
-  delete-and-edit: "Supprimer et réécrire"
-  delete-and-edit-confirm: "Êtes-vous sûr de vouloir effacer cette note et la modifier ? Vous perdrez toutes les réactions, renotes et réponses."
-  remote: "Afficher la note originale"
-  pin-limit-exceeded: "Vous ne pouvez plus épingler davantage de publications."
-common/views/components/user-menu.vue:
-  mention: "Mention"
-  mute: "Silencier"
-  unmute: "Enlever la sourdine"
-  mute-confirm: "Rendre muet cet utilisateur ?"
-  unmute-confirm: "Ne plus masquer cet utilisateur ?"
-  block: "Bloquer"
-  unblock: "Débloquer"
-  block-confirm: "Bloquer cet utilisateur ?"
-  unblock-confirm: "Débloquer cet utilisateur ?"
-  push-to-list: "Ajouter à une liste"
-  select-list: "Sélectionnez une liste"
-  report-abuse: "Signaler un abus"
-  report-abuse-detail: "Détail du signalement"
-  report-abuse-reported: "Transmit à l’administrateur. Merci de votre collaboration."
-  silence: "Mettre en sourdine"
-  unsilence: "Enlever la sourdine"
-  silence-confirm: "Êtes-vous surs de vouloir mettre cet·te utilisateur·rice en sourdine ?"
-  suspend: "Suspendre"
-  unsuspend: "Ne plus suspendre"
-  suspend-confirm: "Êtes-vous surs de vouloir suspendre cet·te utilisateur·rice ?"
-  unsuspend-confirm: "Êtes-vous sûr de vouloir débloquer cet utilisateur ?"
-common/views/components/poll.vue:
-  vote-to: "Voter pour '{}'"
-  vote-count: "{} votes"
-  total-votes: "{} Total des votes"
-  vote: "Vote"
-  show-result: "Montrer les résultats"
-  voted: "Voté"
-  closed: "Terminé"
-  remaining-days: "{d} jours, {h} heures restantes"
-  remaining-hours: "{h} heures et {m} minutes restantes"
-  remaining-minutes: "{m} minutes et {s} secondes restantes"
-  remaining-seconds: "{s} secondes restantes"
-common/views/components/poll-editor.vue:
-  no-only-one-choice: "Vous devez saisir au moins deux choix."
-  choice-n: "Choix {}"
-  remove: "Supprimer ce choix"
-  add: "+ Ajouter un choix"
-  destroy: "Annuler ce sondage"
-  multiple: "Autoriser le multi-choix"
-  expiration: "Valide jusqu'à"
-  infinite: "Illimité"
-  at: "Choisir une date et une durée"
-  after: "Choisir la durée"
-  no-more: "Vous ne pouvez pas en ajouter davantage"
-  deadline-date: "Date d’échéance"
-  deadline-time: "Durée"
-  interval: "Durée"
-  unit: "Unité"
-  second: "secondes"
-  minute: "Minutes"
-  hour: "Heures"
-  day: "D"
-common/views/components/reaction-picker.vue:
-  choose-reaction: "Envoyer une réaction"
-  input-reaction-placeholder: "ou insérez un émoji"
-common/views/components/emoji-picker.vue:
-  recent-emoji: "Utilisés récemment"
-  custom-emoji: "Émoji personnalisé"
-  no-category: "Sans catégorie"
-  people: "Personnes"
-  animals-and-nature: "Animaux et nature"
-  food-and-drink: "Nourriture et boisson"
-  activity: "Activités"
-  travel-and-places: "Lieux et voyages"
-  objects: "Objets"
-  symbols: "Symboles"
-  flags: "Drapeaux"
-common/views/components/settings/app-type.vue:
-  title: "Mode"
-  intro: "Vous pouvez choisir, si vous voulez utiliser la disposition de bureau ou mobile."
-  choices:
-    auto: "Choisir la disposition automatiquement"
-    desktop: "Toujours utiliser la disposition de bureau"
-    mobile: "Toujours utiliser la disposition mobile"
-  info: "Le rechargement de la page est requis afin d'appliquer les modifications."
-common/views/components/signin.vue:
-  username: "Nom d'utilisateur·rice"
-  password: "Mot de passe"
-  token: "Jeton"
-  signing-in: "Connexion…"
-  or: "Ou"
-  signin-with-twitter: "Se connecter via Twitter"
-  signin-with-github: "Se connecter avec GitHub"
-  signin-with-discord: "Se connecter avec Discord"
-  login-failed: "Échec d’authentification. Veuillez vérifier que votre nom d’utilisateur et mot de passe sont corrects."
-  tap-key: "Cliquez sur la clé de sécurité pour vous connecter"
-  enter-2fa-code: "Entrez votre code de vérification"
-common/views/components/signup.vue:
-  invitation-code: "Code d’invitation"
-  invitation-info: "Si vous n’avez pas de code d’invitation, contactez un <a href=\"{}\">administrateur</a>."
-  username: "Nom d'utilisateur·rice"
-  checking: "Vérification…"
-  available: "Disponible"
-  unavailable: "Non disponible"
-  error: "Erreur du réseau"
-  invalid-format: "Vous pouvez utiliser des lettres, des nombres et _."
-  too-short: "Veuillez saisir au moins un caractère !"
-  too-long: "Veuillez entrer au maximum 20 caractères."
-  password: "Mot de passe"
-  password-placeholder: "Nous recommandons au moins 8 caractères."
-  weak-password: "Faible"
-  normal-password: "Moyen"
-  strong-password: "Fort"
-  retype: "Retapez"
-  retype-placeholder: "Confirmez votre mot de passe"
-  password-matched: "OK"
-  password-not-matched: "Les mots de passe ne correspondent pas."
-  recaptcha: "Vérifier"
-  agree-to: "Accepter {0}."
-  tos: "Conditions d'utilisation"
-  create: "Créer un compte"
-  some-error: "La création du compte a échoué. Veuillez réessayer."
-common/views/components/special-message.vue:
-  new-year: "Bonne année !"
-  christmas: "Joyeux Noël !"
-common/views/components/stream-indicator.vue:
-  connecting: "Connexion en cours"
-  reconnecting: "Reconnexion en cours"
-  connected: "Connecté"
-common/views/components/notification-settings.vue:
-  title: "Notifications"
-  mark-as-read-all-notifications: "Marquer toutes les notifications comme lues"
-  mark-as-read-all-unread-notes: "Marquer toutes les notes comme lues"
-  mark-as-read-all-talk-messages: "Marquer toutes les conversations comme lues"
-  auto-watch: "Surveiller automatiquement les publications"
-  auto-watch-desc: "Recevoir automatiquement des notifications à propos des publications auxquelles vous avez réagi ou répondu"
-common/views/components/integration-settings.vue:
-  title: "Intégrations"
-  connect: "Connecter"
-  disconnect: "Déconnecter"
-  connected-to: "Vous êtes connectés aux services suivants"
-common/views/components/github-setting.vue:
-  description: "Si vous liez votre compte GitHub à votre compte Misskey, vous verrez votre compte GitHub s’afficher sur votre profil, vous aurez également la possibilité de vous connecter à Misskey en utilisant ce dernier."
-  connected-to: "Vous êtes connecté à votre compte GitHub"
-  detail: "Détails …"
-  reconnect: "Reconnecter"
-  connect: "Se connecter avec GitHub"
-  disconnect: "Déconnecter"
-common/views/components/discord-setting.vue:
-  description: "Si vous liez votre compte Discord à votre compte Misskey, vous serez en mesure de voir votre compte Twitter s'afficher sur votre profil, vous aurez aussi la possibilité de vous connecter à Misskey en utilisant votre compte Discord."
-  connected-to: "Vous êtes connecté à votre compte Discord"
-  detail: "Détails…"
-  reconnect: "Reconnecter"
-  connect: "Lier votre compte Discord"
-  disconnect: "Déconnecter"
-common/views/components/uploader.vue:
-  waiting: "Veuillez patienter"
-common/views/components/visibility-chooser.vue:
-  public: "Public"
-  home: "Accueil"
-  home-desc: "Publier sur le fil d’Accueil uniquement"
-  followers: "Abonné·e·s"
-  followers-desc: "Publier à vos abonné·e·s uniquement"
-  specified: "Direct"
-  specified-desc: "Publier uniquement aux utilisateur·rice·s mentionné·e·s"
-  local-public: "Local (Public)"
-  local-public-desc: "Ne pas publier pour les distants"
-  local-home: "Accueil (local uniquement)"
-  local-followers: "Abonné·e·s (Local uniquement)"
-common/views/components/trends.vue:
-  count: "{} utilisateur·rice·s mentionné·e·s"
-  empty: "Aucune tendance"
-common/views/components/language-settings.vue:
-  title: "Langue "
-  pick-language: "Sélectionner une langue"
-  recommended: "Recommandé"
-  auto: "Automatique"
-  specify-language: "Spécifier la langue"
-  info: "Le rechargement de la page est requis afin d'appliquer les modifications."
-common/views/components/profile-editor.vue:
-  title: "Profil"
-  name: "Nom"
-  account: "Compte"
-  location: "Lieu"
-  description: "À propos de moi"
-  you-can-include-hashtags: "Vous pouvez également inclure un hashtag sur votre description de profile."
-  language: "Langue"
-  birthday: "Date de naissance"
-  avatar: "Avatar"
-  banner: "Bannière"
-  is-cat: "Ce compte est un Chat"
-  is-bot: "Ce compte est un Bot"
-  is-locked: "Demandes d’abonnements requièrent l’approbation"
-  careful-bot: "Les demandes d’abonnements venant de Bots requièrent l’approbation"
-  auto-accept-followed: "Accepter automatiquement les demandes d’abonnement venant des gens que vous suivez"
-  advanced: "Avancé"
-  privacy: "Vie privée"
-  save: "Mettre à jour le profil"
-  saved: "Profil mis à jour avec succès"
-  uploading: "En cours d’envoi …"
-  upload-failed: "Échec de l'envoi"
-  unable-to-process: "L'opération n'a pas pu être complétée"
-  avatar-not-an-image: "Le fichier sélectionné pour votre avatar n'est pas une image"
-  banner-not-an-image: "Le fichier sélectionné pour votre bannière n'est pas une image"
-  email: "Paramètres de messagerie"
-  email-address: "Adresse de courrier électronique"
-  email-verified: "L’adresse du courrier électronique a été vérifiée."
-  email-not-verified: "Adresse de courriel n’est pas confirmée. Veuillez vérifier votre boite de réception."
-  export: "Exporter"
-  import: "Importer"
-  export-and-import: "Exportation et importation"
-  export-targets:
-    all-notes: "Toutes les notes publiées"
-    following-list: "Liste des abonnements"
-    mute-list: "Liste des comptes mis en sourdine"
-    blocking-list: "Liste des comptes bloqués"
-    user-lists: "Listes"
-  export-requested: "Vous avez demandé une exportation. Cela peut prendre un certain temps. Une fois l'exportation terminée, le fichier résultant sera ajouté dans le Drive."
-  import-requested: "Vous avez initié un import. Ceci peut prendre un peu de temps."
-  enter-password: "Veuillez saisir votre mot de passe"
-  danger-zone: "Zone de danger"
-  delete-account: "Supprimer le compte"
-  account-deleted: "Le compte a été supprimé. Cela peut prendre un certain temps avant que toutes les données disparaissent."
-  profile-metadata: "Métadonnées du profil"
-  metadata-label: "Étiquette"
-  metadata-content: "Contenu"
-common/views/components/user-list-editor.vue:
-  users: "Utilisateur·rice"
-  rename: "Renommer la liste"
-  delete: "Supprimer la liste"
-  remove-user: "Retirer de cette liste"
-  delete-are-you-sure: "Voulez-vous vraiment supprimer la liste « $1 » ?"
-  deleted: "Supprimé"
-  add-user: "Ajouter un utilisateur"
-common/views/components/user-group-editor.vue:
-  users: "Membres"
-  rename: "Renommer le groupe"
-  delete: "Supprimer le groupe"
-  transfer: "Transférer de groupe"
-  transfer-are-you-sure: "Êtes vous surs de vouloir ajouter @$2 au groupe $1 ?"
-  transferred: "Groupe transféré"
-  remove-user: "Enlever un utilisateur de ce groupe"
-  delete-are-you-sure: "Désirez-vous vraiment supprimer le groupe $1 ?"
-  deleted: "Supprimé"
-  invite: "Inviter"
-  invited: "Invitation envoyée avec succès"
-common/views/components/user-lists.vue:
-  user-lists: "Listes"
-  create-list: "Créer une liste"
-  list-name: "Nom de la liste"
-common/views/components/user-groups.vue:
-  user-groups: "Groupe"
-  create-group: "Créer un groupe"
-  group-name: "Nom du groupe"
-  owned-groups: "Mes groupes"
-  joined-groups: "Membre dans les groupes"
-  invites: "Inviter"
-  accept-invite: "Participer"
-  reject-invite: "Refuser"
-common/views/widgets/broadcast.vue:
-  fetching: "Récupération"
-  no-broadcasts: "Aucune annonce"
-  have-a-nice-day: "Passez une bonne journée !"
-  next: "Suivant"
-  prev: "Précédent"
-common/views/widgets/calendar.vue:
-  year: "Année {}"
-  month: "{},"
-  day: "{}"
-  today: "Aujourd’hui :"
-  this-month: "Ce mois-ci :"
-  this-year: "Cette année :"
-common/views/widgets/photo-stream.vue:
-  title: "Flux de photos"
-  no-photos: "Pas de photo"
-common/views/widgets/posts-monitor.vue:
-  title: "Graphe des publications"
-  toggle: "Basculer entre les vues"
-common/views/widgets/hashtags.vue:
-  title: "Hashtags"
-common/views/widgets/server.vue:
-  title: "Informations sur le serveur"
-  toggle: "Afficher les vues"
-common/views/widgets/memo.vue:
-  title: "Pense-bête"
-  memo: "Écrivez ici !"
-  save: "Enregistrer"
-common/views/widgets/slideshow.vue:
-  folder-customize-mode: "Pour pouvoir spécifier un dossier, veuillez quitter le mode de personnalisation"
-  folder: "Veuillez cliquer pour spécifier le dossier"
-  no-image: "Il n'y a aucune image dans ce dossier"
-common/views/widgets/tips.vue:
-  tips-line1: "Vous pouvez vous concentrer sur le fil avec <kbd>t</kbd>"
-  tips-line2: "Ouvre la fenêtre de publication en appuyant sur <kbd>p</kbd> ou <kbd>n</kbd>."
-  tips-line3: "Vous pouvez glisser et déposer des fichiers sur la fenêtre de la note"
-  tips-line4: "Vous pouvez coller des images à partir du presse-papier sur la fenêtre de la note"
-  tips-line5: "Vous pouvez téléverser des fichiers sur le Drive en faisant un glisser-déposer"
-  tips-line6: "Vous pouvez déplacer un dossier en le glissant dans le Drive"
-  tips-line7: "Vous pouvez déplacer des dossiers en les glissant dans le Drive"
-  tips-line8: "Vous pouvez personnaliser l'Accueil via les paramètres"
-  tips-line9: "Misskey est sous licence AGPLv3"
-  tips-line10: "L'utilisation du widget Time Machine permet de remonter facilement dans le passé du fil."
-  tips-line11: "Vous pouvez épingler des notes sur votre page en cliquant sur « … »"
-  tips-line13: "Tous les fichiers attachés à cette publication sont sauvegardés dans le Drive"
-  tips-line14: "Lorsque vous personnalisez la disposition de votre page d’accueil, vous pouvez effectuer un clique droit sur un widget pour changer son apparence."
-  tips-line17: "Vous pouvez mettre un texte en surbrillance en le mettant entre ** **"
-  tips-line19: "Plusieurs fenêtres peuvent être détachées en dehors du navigateur."
-  tips-line20: "Pourcentage sur le widget calendrier qui indique le pourcentage de temps passé"
-  tips-line21: "Vous pouvez aussi utiliser l'API pour développer des Bots."
-  tips-line23: "Ai-chan kawaii!"
-  tips-line24: "Misskey est fonctionnel depuis 2014"
-  tips-line25: "Vous pouvez recevoir les notifications de Misskey dans un navigateur web compatible"
-common/views/pages/not-found.vue:
-  page-not-found: "La page demandée est introuvable !"
-common/views/pages/follow.vue:
-  signed-in-as: "Connecté en tant que {}"
-  following: "Suit"
-  follow: "Suivre"
-  request-pending: "Demande d’abonnement en attente"
-  follow-processing: "Demande en attente"
-  follow-request: "Demande d’abonnement"
-common/views/pages/follow-requests.vue:
-  received-follow-requests: "Demandes d’abonnement"
-  accept: "Accepter"
-  reject: "Refuser"
-desktop:
-  banner-crop-title: "Découpez la partie qui apparaîtra comme bannière"
-  banner: "Bannière"
-  uploading-banner: "Téléversement d'une nouvelle bannière"
-  banner-updated: "Mise à jour de la bannière avec succès"
-  choose-banner: "Choisir une bannière"
-  avatar-crop-title: "Découpez la partie qui apparaîtra comme avatar"
-  avatar: "Avatar"
-  uploading-avatar: "Téléversement du nouvel avatar"
-  avatar-updated: "Mise à jour de l’avatar avec succès"
-  choose-avatar: "Choisir un avatar"
-  unable-to-process: "L'opération n'a pas pu être complétée"
-  invalid-filetype: "Ce format de fichier n’est pas pris en charge"
-desktop/views/components/activity.chart.vue:
-  total: "Noirs ... Total"
-  notes: "Bleu ... Notes"
-  replies: "Rouge ... Réponses"
-  renotes: "Vert ... Partages"
-desktop/views/components/activity.vue:
-  title: "Activité"
-  toggle: "Afficher les vues"
-desktop/views/components/calendar.vue:
-  title: "{month} - {year}"
-  prev: "Mois précédent"
-  next: "Mois suivant"
-  go: "Cliquez pour naviguer"
-desktop/views/components/choose-file-from-drive-window.vue:
-  chosen-files: "{count} fichier(s) sélectionné(s)"
-  upload: "Téléverser des fichiers à partir de votre ordinateur"
-  cancel: "Annuler"
-  ok: "OK"
-  choose-prompt: "Choisir un fichier"
-desktop/views/components/choose-folder-from-drive-window.vue:
-  cancel: "Annuler"
-  ok: "OK"
-  choose-prompt: "Choisir un dossier"
-desktop/views/components/crop-window.vue:
-  skip: "Ignorer la découpe"
-  cancel: "Annuler"
-  ok: "OK"
-desktop/views/components/drive-window.vue:
-  used: "utilisé"
-desktop/views/components/drive.file.vue:
-  avatar: "Avatar"
-  banner: "Bannière"
-  nsfw: "CW"
-  contextmenu:
-    rename: "Renommer"
-    mark-as-sensitive: "Marquer comme sensible"
-    unmark-as-sensitive: "Ne pas marquer comme sensible"
-    copy-url: "Copier l’URL"
-    download: "Télécharger"
-    else-files: "Avancé"
-    set-as-avatar: "Utiliser en tant qu'avatar"
-    set-as-banner: "Utiliser en tant que bannière"
-    open-in-app: "Ouvrir dans l'application"
-    add-app: "Ajouter une application"
-    rename-file: "Renommer le ficher"
-    input-new-file-name: "Entrer un nouveau nom"
-    copied: "Copié"
-    copied-url-to-clipboard: "L'URL a été copiée dans le presse-papier"
-desktop/views/components/drive.folder.vue:
-  upload-folder: "Emplacement de téléversement par défaut"
-  unable-to-process: "L'opération n'a pas pu être complétée"
-  circular-reference-detected: "Le dossier de destination est un sous-dossier du dossier que vous souhaitez déplacer."
-  unhandled-error: "Erreur inconnue"
-  unable-to-delete: "Ne peut pas être supprimé"
-  has-child-files-or-folders: "Ce dossier n'est pas vide, il ne peut pas être supprimé"
-  contextmenu:
-    move-to-this-folder: "Déplacer dans ce dossier"
-    show-in-new-window: "Ouvrir dans une nouvelle fenêtre"
-    rename: "Renommer"
-    rename-folder: "Renommer le dossier"
-    input-new-folder-name: "Entrer un nouveau nom"
-    else-folders: "Avancé"
-    set-as-upload-folder: "Spécifier en tant que dossier de téléversement par défaut"
-desktop/views/components/drive.vue:
-  search: "Rechercher"
-  empty-draghover: "Drop Welcome!"
-  empty-drive: "Votre Drive est vide"
-  empty-drive-description: "Vous pouvez également téléverser le fichier en faisant un clic droit et en choisissant « Téléverser » ou tout simplement en faisant glisser votre fichier."
-  empty-folder: "Ce dossier est vide"
-  unable-to-process: "L'opération n'a pas pu être complétée"
-  circular-reference-detected: "Le dossier de destination est un sous-dossier du dossier que vous souhaitez déplacer."
-  unhandled-error: "Erreur inconnue"
-  url-upload: "Téléverser via une URL"
-  url-of-file: "URL de l'image que vous souhaitez téléverser."
-  url-upload-requested: "Téléversement demandé"
-  may-take-time: "Le téléversement de votre fichier peut prendre un certain temps."
-  create-folder: "Créer un dossier"
-  folder-name: "Nom du dossier"
-  contextmenu:
-    create-folder: "Créer un dossier"
-    upload: "Téléverser un fichier"
-    url-upload: "Téléverser à partir d’une URL"
-desktop/views/components/media-video.vue:
-  sensitive: "Le contenu est NSFW"
-  click-to-show: "Cliquer pour afficher"
-desktop/views/components/followers-window.vue:
-  followers: "Abonné·e·s de {}"
-desktop/views/components/followers.vue:
-  empty: "Il semble que vous n’avez pas encore d’abonné·e·s."
-desktop/views/components/following-window.vue:
-  following: "Suit {}"
-desktop/views/components/following.vue:
-  empty: "Vous ne suivez aucun compte."
-desktop/views/components/game-window.vue:
-  game: "Reversi"
-desktop/views/components/home.vue:
-  done: "Envoyer"
-  add-widget: "Ajouter un widget"
-  add: "Ajouter"
-desktop/views/input-dialog.vue:
-  cancel: "Annuler"
-  ok: "OK"
-desktop/views/components/note-detail.vue:
-  private: "cette publication est privée"
-  deleted: "cette publication a été supprimée"
-  location: "Géolocalisation"
-  renote: "Republier"
-  add-reaction: "Ajouter une réaction"
-  undo-reaction: "Annuler la réaction"
-desktop/views/components/note.vue:
-  reply: "Répondre"
-  renote: "Partager"
-  add-reaction: "Ajouter votre réaction"
-  undo-reaction: "Inverser la réaction"
-  detail: "Détails"
-  private: "Cette publication est privée"
-  deleted: "Cette publication a été supprimée"
-desktop/views/components/notes.vue:
-  error: "Échec du chargement."
-  retry: "Réessayer"
-desktop/views/components/notifications.vue:
-  empty: "Aucune de notification !"
-desktop/views/components/post-form.vue:
-  posted: "Publié !"
-  replied: "Répondu !"
-  reposted: "Reposté !"
-  note-failed: "La note à échoué"
-  reply-failed: "La réponse a échoué"
-  renote-failed: "Échec lors de la republication"
-desktop/views/components/post-form-window.vue:
-  note: "Nouvelle note"
-  reply: "Répondre"
-  attaches: "{} media joint(s)"
-  uploading-media: "Téléversement du média {}"
-desktop/views/components/progress-dialog.vue:
-  waiting: "En attente"
-desktop/views/components/renote-form.vue:
-  quote: "Citer..."
-  cancel: "Annuler"
-  renote: "Republier"
-  renote-home: "Renote (accueil)"
-  reposting: "Republication en cours…"
-  success: "Republié !"
-  failure: "La renote a échoué"
-desktop/views/components/renote-form-window.vue:
-  title: "Êtes vous sûr de vouloir renote cette note?"
-desktop/views/pages/user-following-or-followers.vue:
-  following: "{user} suit"
-  followers: "Abonné·e·s de {user}"
-desktop/views/components/settings.2fa.vue:
-  intro: "Si vous configurez la vérication en deux étapes vous aurez non seulement besoin de votre mot de passe mais aussi un appareil déjà pré-enregistré(tel que votre smartphone) ce qui ameliora grandement la sécurité de votre compte."
-  detail: "Voir les détails..."
-  url: "https://www.google.com/landing/2step/"
-  caution: "Activer la vérification en deux étapes vient aussi avec des contraintes, si vous perdez votre appareil ou ne pouvez tout simplement plus y accéder vous ne serez plus en mesure de vous connecter à Misskey."
-  register: "Enregistrer un appareil"
-  already-registered: "Cette étape à déjà été complétée"
-  unregister: "Désactiver"
-  unregistered: "L'authentification à deux facteurs a été désactivée."
-  enter-password: "Entrez un mot de passe"
-  authenticator: "Vous devez au préalable installer Google Authenticator sur votre appareil :"
-  howtoinstall: "Comment installer"
-  token: "Jeton"
-  scan: "Ensuite, scannez le code QR affiché sur votre écran :"
-  done: "Veuillez entrer le token qui s'affiche sur votre appareil :"
-  submit: "Envoyer"
-  success: "Sauvegarde des paramètres avec succès !"
-  failed: "L’opération a échoué. Veuillez vous assurer que le jeton a été saisi correctement."
-  info: "À partir de maintenant, à chaque fois que vous vous connectez entrez votre mot de passe ainsi que le jeton généré sur votre appareil."
-  totp-header: "Application d'authentification"
-  security-key-header: "Clé de sécurité"
-  security-key: "Pour plus de sécurité, vous pouvez vous connecter à votre compte à l'aide d'une clé de sécurité matérielle qui prend en charge FIDO2. Lorsque vous vous connecterez, vous aurez besoin de la clé de sécurité enregistrée ou d'une application d'authentification avec vous."
-  last-used: "Dernière utilisation :"
-  activate-key: "Cliquez pour activer la clé de sécurité"
-  security-key-name: "Nom de la clé"
-  something-went-wrong: "Oula ! Il y a eu un problème lors de l’enregistrement de la clé."
-  key-unregistered: "La clé a été supprimée"
-  use-password-less-login: "Utiliser une connexion sans mot de passe"
-common/views/components/media-image.vue:
-  sensitive: "Contenu sensible"
-  click-to-show: "Cliquer pour afficher"
-common/views/components/api-settings.vue:
-  intro: "Pour accéder à l'API, définissez ce jeton comme la clé de « i » dans les paramètres de requête."
-  caution: "Merci de ne pas introduire ce jeton dans aucune application ou le divulguer à quiconque. Ceci risque de compromettre votre compte."
-  regeneration-of-token: "Si votre jeton est compromis, vous pouvez le régénérer."
-  regenerate-token: "Régénérer le jeton"
-  token: "Jeton :"
-  enter-password: "Entrez le mot de passe"
-  console:
-    title: "Console API"
-    endpoint: "Point de terminaison"
-    parameter: "Paramètres"
-    credential-info: "Le paramètre « i » est requis dans la console."
-    send: "Envoyer"
-    sending: "Envoi en cours"
-    response: "Résultat"
-desktop/views/components/settings.apps.vue:
-  no-apps: "Aucune application autorisée"
-common/views/components/drive-settings.vue:
-  max: "Maximale"
-  in-use: "utilisé"
-  stats: "Statistiques"
-  default-upload-folder: "Emplacement par défaut du dossier de transfert"
-  default-upload-folder-name: "Dossier·s"
-  change-default-upload-folder: "Changer de dossier"
-common/views/components/mute-and-block.vue:
-  mute-and-block: "Masqués / Bloqués"
-  mute: "Mis en sourdine"
-  block: "Vous avez bloqué"
-  no-muted-users: "Aucun utilisateur n’est mis en sourdine"
-  no-blocked-users: "Aucun utilisateur n’est bloqué"
-  word-mute: "Filtre de mots"
-  muted-words: "Mots masqués"
-  muted-words-description: "Description des mots mis en sourdine"
-  unmute-confirm: "Ne plus masquer cet utilisateur ?"
-  unblock-confirm: "Débloquer cet utilisateur ?"
-  save: "Enregistrer"
-common/views/components/password-settings.vue:
-  reset: "Modifier le mot de passe"
-  enter-current-password: "Entrez votre mot de passe actuel"
-  enter-new-password: "Saisissez le nouveau mot de passe"
-  enter-new-password-again: "Entrez à nouveau le nouveau mot de passe"
-  not-match: "Les nouveaux mots de passe ne sont pas identiques"
-  changed: "Mot de passe modifié avec succès"
-  failed: "Échec lors de la modification du mot de passe"
-common/views/components/post-form-attaches.vue:
-  attach-cancel: "Enlever le fichier attaché"
-  mark-as-sensitive: "Marquer comme sensible"
-  unmark-as-sensitive: "Ne pas marquer comme sensible"
-desktop/views/components/sub-note-content.vue:
-  private: "cette publication est privée"
-  deleted: "cette publication a été supprimée"
-  media-count: "{} médias attachés"
-  poll: "Sondage"
-desktop/views/components/settings.tags.vue:
-  title: "Étiquettes"
-  query: "Requête (optionnelle)"
-  add: "Ajouter"
-  save: "Enregistrer"
-desktop/views/components/timeline.vue:
-  home: "Accueil"
-  local: "Local"
-  hybrid: "Social"
-  global: "Global"
-  mentions: "Mentions"
-  messages: "Messages directs"
-  list: "Listes"
-  hashtag: "Hashtag"
-  add-tag-timeline: "Ajouter un fil de hashtags"
-  add-list: "Ajouter une nouvelle liste"
-  list-name: "Nom de la liste"
-desktop/views/components/ui.header.vue:
-  welcome-back: "Content de vous revoir !"
-  adjective: "M."
-desktop/views/components/ui.header.account.vue:
-  profile: "Votre profil"
-  lists: "Listes"
-  groups: "Groupes"
-  follow-requests: "Demandes d’abonnement"
-  admin: "Admin"
-  room: "Pièce"
-desktop/views/components/ui.header.nav.vue:
-  game: "Jeux"
-desktop/views/components/ui.header.notifications.vue:
-  title: "Notifications"
-desktop/views/components/ui.header.post.vue:
-  post: "Rédiger une nouvelle publication"
-desktop/views/components/ui.header.search.vue:
-  placeholder: "Chercher"
-desktop/views/components/user-preview.vue:
-  notes: "Publications"
-  following: "Abonné à"
-  followers: "Abonné·e·s"
-desktop/views/components/users-list.vue:
-  all: "Tout"
-  iknow: "Vous connaissez"
-  fetching: "Chargement..."
-desktop/views/components/users-list-item.vue:
-  followed: "vous suit"
-desktop/views/components/window.vue:
-  popout: "Fenêtre contextuelle"
-  close: "Fermer"
-admin/views/index.vue:
-  dashboard: "Tableau de bord"
-  instance: "Instance"
-  emoji: "Émoji"
-  moderators: "Modérateurs"
-  users: "Utilisateur·rice·s"
-  federation: "Fédération"
-  announcements: "Annonces"
-  abuse: "Abus"
-  queue: "File d’attente"
-  logs: "Journaux"
-  db: "Base de données"
-  back-to-misskey: "Retour vers Misskey"
-admin/views/db.vue:
-  tables: "Tables"
-  vacuum: "Vacuum"
-  vacuum-info: "Range la base de données. Conserve les données intactes et réduit l'utilisation du disque. Cela se fait généralement automatiquement et périodiquement."
-admin/views/dashboard.vue:
-  dashboard: "Tableau de bord"
-  accounts: "Comptes"
-  notes: "Notes"
-  drive: "Lecteur"
-  instances: "Instances"
-  this-instance: "Cette instance"
-  federated: "Fédérées"
-admin/views/queue.vue:
-  title: "File d'attente"
-  remove-all-jobs: "Enlever toutes les tâches en attente"
-  jobs: "Tâches"
-  queue: "File d'attente"
-  domains:
-    deliver: "Délivrées"
-    inbox: "Reçues"
-    db: "Base de données"
-    objectStorage: "Stockage d'objets"
-  state: "État"
-  states:
-    active: "en cours"
-    delayed: "Programmé"
-    waiting: "En file d'attente"
-  result-is-truncated: "Le résultat est tronqué"
-  other-queues: "Autres files d’attente"
-admin/views/logs.vue:
-  logs: "Journaux"
-  domain: "Domaine"
-  level: "Niveau"
-  levels:
-    all: "Tous"
-    info: "Informations"
-    success: "Succès"
-    warning: "Avertissement"
-    error: "Erreur"
-    debug: "Débogage"
-  delete-all: "Effacer tout"
-admin/views/abuse.vue:
-  title: "Abus"
-  target: "Cible"
-  reporter: "Signalé par"
-  details: "Détails"
-  remove-report: "Supprimer"
-admin/views/instance.vue:
-  instance: "Instance"
-  instance-name: "Nom de l’instance"
-  instance-description: "Description de l’instance"
-  host: "Hôte"
-  icon-url: "URL de l'icône"
-  logo-url: "URL do logo"
-  banner-url: "URL de l’image de la bannière"
-  error-image-url: "URL de l’image d’erreur"
-  languages: "Langue de l’instance"
-  languages-desc: "Vous pouvez en définir plus d’une, séparées par des espaces."
-  tos-url: "URL des conditions d'utilisation"
-  repository-url: "URL du dépôt"
-  feedback-url: "URL pour les commentaires"
-  maintainer-config: "Informations de l’administrateur"
-  maintainer-name: "Nom de l’administrateur"
-  maintainer-email: "Contact administratif"
-  advanced-config: "Autres réglages"
-  note-and-tl: "Notes et fils"
-  drive-config: "Paramètres du lecteur"
-  use-object-storage: "Utiliser le stockage d'objets"
-  object-storage-base-url: "URL"
-  object-storage-prefix: "Préfixe"
-  object-storage-endpoint: "Point de terminaison"
-  object-storage-region: "Région"
-  object-storage-port: "Port"
-  object-storage-access-key: "Clé d'accès"
-  object-storage-secret-key: "Clé secrète"
-  object-storage-use-ssl: "Utiliser SSL"
-  object-storage-s3-info-here: "ici"
-  cache-remote-files: "Mettre en cache des fichiers distants"
-  local-drive-capacity-mb: "Volume du lecteur par utilisateur"
-  remote-drive-capacity-mb: "Volume du lecteur par utilisateur distant"
-  mb: "en mégaoctets"
-  recaptcha-config: "Paramètres de reCAPTCHA"
-  recaptcha-info: "Si activé, un jeton reCAPTCHA est requis. Vous pouvez en obtenir un sur https://www.google.com/recaptcha/intro/"
-  recaptcha-info2: "v3 n'est pas supportée. Veuillez utiliser v2."
-  enable-recaptcha: "Activation de reCAPTCHA"
-  recaptcha-site-key: "Clé du site"
-  recaptcha-secret-key: "Clé secrète"
-  recaptcha-preview: "Prévisualisation"
-  hidden-tags: "Tags cachés"
-  external-service-integration-config: "Services connectés"
-  twitter-integration-config: "Paramètres de connexion à Twitter"
-  twitter-integration-info: "L'URL de callback est {url}."
-  enable-twitter-integration: "Activer la connexion à Twitter"
-  twitter-integration-consumer-key: "Clé du consommateur"
-  twitter-integration-consumer-secret: "Secret du consommateur"
-  github-integration-config: "Paramètres d’authentification GitHub"
-  github-integration-info: "L'URL de callback est {url}."
-  enable-github-integration: "Activer l’authentification avec Github"
-  github-integration-client-id: "ID client"
-  github-integration-client-secret: "Secret client"
-  discord-integration-config: "Paramètres d’authentification Discord"
-  discord-integration-info: "L'URL de callback est {url}."
-  enable-discord-integration: "Activer l’authentification avec Discord"
-  discord-integration-client-id: "ID client"
-  discord-integration-client-secret: "Secret client"
-  proxy-account-config: "Compte proxy"
-  proxy-account-info: "Un compte proxy se comporte, dans certaines conditions, comme un·e abonné·e distant pour les utilisateurs d'autres instances.\nExemple : quand un·e utilisateur·rice distant·e est ajouté·e à une liste, ses publications ne serait pas visibles sur l'instance si personne ne le·la suit. Le compte proxy va donc le·la suivre pour que ses publications soient acheminées."
-  proxy-account-username: "Nom d’utilisateur du compte proxy"
-  proxy-account-username-desc: "Spécifiez le nom d’utilisateur du compte utilisé comme proxy."
-  proxy-account-warn: "Avant d’entamer cette action, vous devez au préalable avoir créé un compte avec ce nom d’utilisateur."
-  max-note-text-length: "Nombre maximal de caractères pour les messages"
-  disable-registration: "Désactiver les inscriptions"
-  disable-local-timeline: "Désactiver le fil local"
-  disable-global-timeline: "Désactiver le fil global"
-  disabling-timelines-info: "Même si vous désactivez ces fils, l'administrateur et les modérateurs peuvent continuer à les utiliser."
-  enable-emoji-reaction: "Activer les pictogrammes dans les réactions"
-  use-star-for-reaction-fallback: "Utiliser une étoile si une réaction est inconnue"
-  invite: "Inviter"
-  save: "Sauvegarder"
-  saved: "Enregistré"
-  pinned-users: "Utilisateur·rice épinglé·e"
-  email-config: "Paramètres du serveur de messagerie"
-  email-config-info: "Utilisé pour confirmer votre adresse de courrier électronique et la réinitialisation de votre mot de passe."
-  enable-email: "Activation de la distribution du courrier"
-  email: "Adresse de courrier électronique"
-  smtp-secure: "Utiliser SSL/TLS implicitement dans la connexion SMTP"
-  smtp-secure-info: "Désactiver STARTTLS lorsque celui-ci est utilisé."
-  smtp-host: "Hôte SMTP"
-  smtp-port: "Port SMTP"
-  smtp-auth: "Effectuer une authentification SMTP"
-  smtp-user: "Utilisateur SMTP"
-  smtp-pass: "Mot de passe SMTP"
-  test-email: "Test"
-  serviceworker-config: "ServiceWorker"
-  enable-serviceworker: "Activer ServiceWorker"
-  serviceworker-info: "Devrait être activé pour les notifications push."
-  vapid-publickey: "Clé Publique VAPID"
-  vapid-privatekey: "Clé privée VAPID"
-  vapid-info: "Vous devez activer ServiceWorker pour pouvoir générer les clés VAPID. Vous devez lancer ceci en mode root :"
-admin/views/charts.vue:
-  title: "Graphe"
-  per-day: "par jour"
-  per-hour: "par heure"
-  federation: "Fédération"
-  notes: "Publications"
-  users: "Utilisateur·rice·s"
-  drive: "Lecteur"
-  network: "Réseau"
-  charts:
-    federation-instances: "Nombre d’instances : augmentation/diminution"
-    federation-instances-total: "Nombre total d’instances"
-    notes: "Nombre de publications : augmentation/diminution (combinés)"
-    local-notes: "Nombre des publications : augmentation/diminution (Local)"
-    remote-notes: "Nombre de publications : augmentation/diminution (distants)"
-    notes-total: "Total des notes"
-    users: "Nombre d’utilisateur·rice·s : augmentation/diminution"
-    users-total: "Nombre total des utilisateur·rice·s"
-    active-users: "Utilisateur·rice·s actif·ve·s"
-    drive: "Capacité utilisée comme stockage : augmentation/diminution"
-    drive-total: "Utilisation totale du lecteur"
-    drive-files: "Le nombre de fichiers sur l'espace de stockage : augmentation/diminution"
-    drive-files-total: "Nombre total de fichiers sur le lecteur"
-    network-requests: "Requêtes"
-    network-time: "Temps de réponse"
-    network-usage: "Traffic"
-admin/views/drive.vue:
-  operation: "Actions"
-  fileid-or-url: "ID du fichier ou URL"
-  file-not-found: "Fichier non trouvé"
-  lookup: "Recherche"
-  sort:
-    title: "Tri"
-    createdAtAsc: "Âge - Du plus ancien"
-    createdAtDesc: "Âge - Du plus récent"
-    sizeAsc: "Taille - Ascendant"
-    sizeDesc: "Taille - Volumineux en premier"
-  origin:
-    title: "Origine"
-    combined: "Locaux et distants combinés"
-    local: "Local"
-    remote: "Distant"
-  delete: "Supprimer"
-  deleted: "Supprimé"
-  mark-as-sensitive: "Marquer comme sensible"
-  unmark-as-sensitive: "Ne pas marquer comme sensible"
-  marked-as-sensitive: "Marqué comme sensible"
-  unmarked-as-sensitive: "Marqué comme non sensible"
-  clean-remote-files: "Nettoyer le cache des fichiers distants"
-  clean-remote-files-are-you-sure: "Êtes-vous sûr de vouloir effacer tout les fichiers distants mis en cache ?"
-  clean-up: "Nettoyage"
-admin/views/users.vue:
-  operation: "Actions"
-  username-or-userid: "Nom d’utilisateur·rice ou ID utilisateur"
-  user-not-found: "Utilisateur non trouvé"
-  lookup: "Recherche"
-  reset-password: "Réinitialiser mot de passe"
-  reset-password-confirm: "Souhaitez-vous réinitialiser votre mot de passe ?"
-  password-updated: "Le mot de passe est « {password} »"
-  suspend: "Suspendre"
-  suspend-confirm: "Désirez-vous suspendre ce compte ?"
-  suspended: "Suspendu avec succès."
-  unsuspend: "Suspension levée"
-  unsuspend-confirm: "Souhaiteriez-vous ne plus suspendre ce compte ?"
-  unsuspended: "La suspension de l’utilisateur a été levée avec succès"
-  make-silence: "Mettre en sourdine"
-  silence-confirm: "Mettre l'utilisateur sous silence ?"
-  unmake-silence: "Enlever la sourdine"
-  update-remote-user: "Mettre à jour les informations de l’utilisateur·rice distant·e"
-  remote-user-updated: "Les informations de l’utilisateur·rice distant·e ont étés mis à jour"
-  delete-all-files: "Supprimer tous les fichiers"
-  delete-all-files-confirm: "Êtes vous surs de vouloir supprimer tous les fichiers ?"
-  username: "Nom d'utilisateur·rice"
-  host: "Hôte"
-  users:
-    title: "Utilisateur·rice·s"
-    sort:
-      title: "Trier par"
-      createdAtAsc: "Date d’inscription (Ascendant)"
-      createdAtDesc: "Date d’inscription (Descendant)"
-      updatedAtAsc: "Mis à jour récemment (Ascendant)"
-      updatedAtDesc: "Mis à jour récemment (descendant)"
-    state:
-      title: "État"
-      all: "Tout"
-      available: "Disponible"
-      admin: "Admin"
-      moderator: "Modérateur"
-      adminOrModerator: "Administrateur/Modérateur"
-      silenced: "Déjà mis en sourdine"
-      suspended: "Suspendu"
-    origin:
-      title: "Origine"
-      combined: "Locaux + distants"
-      local: "Locaux"
-      remote: "Distants"
-    createdAt: "Créé le"
-    updatedAt: "Mis à jour le"
-admin/views/moderators.vue:
-  add-moderator:
-    title: "Ajout d’un modérateur"
-    add: "Ajouter"
-    added: "Ajouté en tant que modérateur"
-    remove: "Révoquer"
-    removed: "Le modérateur a été révoqué"
-  logs:
-    title: "Journaux"
-    moderator: "Modérateurs"
-    type: "Actions"
-    at: "Date de modification"
-    info: "Informations"
-admin/views/emoji.vue:
-  add-emoji:
-    title: "Ajouter un émoji"
-    name: "Nom de l’émoji"
-    name-desc: "Vous pouvez utiliser les caractères a~z 0~9 _"
-    category: "Catégories"
-    aliases: "Aliases"
-    aliases-desc: "Vous pouvez définir plus d’un, séparés par des espaces."
-    url: "URL de l’image"
-    add: "Ajouter"
-    info: "Nous recommandons l’usage d’images PNG moins de 50 Ko."
-    added: "Émoji ajouté avec succès"
-  emojis:
-    title: "Émojis"
-    update: "Mise à jour"
-    remove: "Supprimer"
-  updated: "À été mis à jour"
-  remove-emoji:
-    are-you-sure: "Supprimer « %1$s » ?"
-    removed: "Supprimé"
-admin/views/announcements.vue:
-  announcements: "Annonces"
-  save: "Enregistrer"
-  remove: "Supprimer"
-  add: "Ajouter"
-  title: "Titre"
-  text: "Contenu"
-  saved: "Sauvegardé"
-  _remove:
-    are-you-sure: "Supprimer « %1$s » ?"
-    removed: "Supprimé"
-admin/views/hashtags.vue:
-  hided-tags: "Tags cachés"
-admin/views/federation.vue:
-  instance: "Instance"
-  host: "Hôte"
-  notes: "Notes"
-  users: "Utilisateur·rice·s"
-  following: "Abonnements"
-  followers: "Abonné·e·s"
-  caught-at: "Créé le"
-  status: "Statuts"
-  latest-request-sent-at: "Dernière requête envoyée"
-  latest-request-received-at: "Dernière requête reçue"
-  remove-all-following-info: "Se désabonner de tous les comptes de {host}. Exécutez cette commande si l'instance n'existe plus."
-  delete-all-files: "Supprimer tous les fichiers"
-  block: "Bloquer"
-  marked-as-closed: "Marquées comme fermées"
-  lookup: "Recherche"
-  instances: "Fédérées"
-  instance-not-registered: "L’instance n’a pas encore été découverte"
-  sort: "Trier par"
-  sorts:
-    caughtAtAsc: "Date d’inscription (Ascendant)"
-    caughtAtDesc: "Date d’inscription (Descendant)"
-    lastCommunicatedAtAsc: "La date et l'heure des interactions plus anciennes"
-    lastCommunicatedAtDesc: "La date et l'heure des nouvelles interactions"
-    notesDesc: "Description des notes"
-    usersAsc: "Peu d'abonné·e·s"
-    followingAsc: "Les moins suivies"
-    followingDesc: "Ayant le plus d'abonné·e·s"
-    followersAsc: "Ayant le moins d'abonné·e·s"
-    followersDesc: "Ayant le plus d'abonné·e·s"
-    driveUsageAsc: "Moins d'espace de stockage utilisé"
-  state: "État"
-  states:
-    all: "Tout"
-    blocked: "Bloquées"
-    not-responding: "Sans réponse"
-    marked-as-closed: "Marquée comme fermée"
-  charts: "Graphs"
-  chart-srcs:
-    requests: "Requêtes"
-    users: "Nombre d’utilisateur·trice·s : augmentation/diminution"
-    users-total: "Nombre total des utilisateur·rice·s"
-    notes: "Augmentation/diminution du nombre des notes"
-    notes-total: "Nombre total des notes"
-    ff: "Augmentation des abonné·e·s"
-    ff-total: "Nombre total d'abonnements"
-    drive-usage: "Augmentation et diminution de la capacité stockage"
-    drive-usage-total: "Utilisation totale du stockage"
-    drive-files-total: "Nombre total des fichiers sur le Drive"
-  chart-spans:
-    hour: "Par heure"
-    day: "Par jour"
-  blocked-hosts: "En cours blocage"
-  save: "Enregistrer"
-desktop/views/pages/welcome.vue:
-  about: "à propos"
-  timeline: "Fil d’actualité"
-  announcements: "Notices"
-  photos: "Images récentes"
-  powered-by-misskey: "Propulsé par <b>Misskey</b>."
-  info: "Informations"
-desktop/views/pages/drive.vue:
-  title: "Lecteur de Misskey"
-desktop/views/pages/note.vue:
-  prev: "Note précédente"
-  next: "Note suivante"
-desktop/views/pages/selectdrive.vue:
-  title: "Choisir fichier(s)"
-  ok: "OK"
-  cancel: "Annuler"
-  upload: "Téléverser des fichiers à partir de votre ordinateur"
-desktop/views/pages/search.vue:
-  not-available: "La fonction de recherche est désactivée dans les paramètres de l’instance."
-  not-found: "Aucune publication trouvée pour « {q} »."
-desktop/views/pages/tag.vue:
-  no-posts-found: "Aucune publication contenant « {q} » n’a été trouvée."
-desktop/views/pages/user-list.users.vue:
-  users: "Utilisateur·rice·s"
-  add-user: "Ajouter un utilisateur"
-  username: "Nom d'utilisateur"
-desktop/views/pages/user/user.followers-you-know.vue:
-  title: "Abonné·e·s que vous connaissez"
-  loading: "Chargement en cours"
-  no-users: "Aucun·e abonné·e connu·e"
-desktop/views/pages/user/user.friends.vue:
-  title: "Mentions fréquentes"
-  loading: "Chargement en cours"
-  no-users: "Aucune mention fréquente"
-desktop/views/pages/user/user.photos.vue:
-  title: "Photos"
-  loading: "Chargement en cours"
-  no-photos: "Pas de photos"
-desktop/views/pages/user/user.header.vue:
-  posts: "Notes"
-  following: "Suit"
-  followers: "Abonné·e·s"
-  is-bot: "Ce compte est un Bot"
-  no-description: "L'utilisateur n'a pas renseigné d'introduction sur son profile"
-  years-old: "{age} ans"
-  year: "/"
-  month: "/"
-  day: "-"
-  follows-you: "Vous suit"
-desktop/views/pages/user/user.timeline.vue:
-  default: "Publications"
-  with-replies: "Publications et réponses"
-  with-media: "Média"
-  my-posts: "Mes Messages"
-desktop/views/widgets/notifications.vue:
-  title: "Notifications"
-desktop/views/widgets/polls.vue:
-  title: "Sondages"
-  refresh: "Afficher d'autres"
-  nothing: "Rien"
-desktop/views/widgets/post-form.vue:
-  title: "Publication"
-  note: "Publication"
-desktop/views/widgets/profile.vue:
-  update-banner: "Cliquer pour éditer votre bannière"
-  update-avatar: "Cliquer pour éditer votre avatar"
-desktop/views/widgets/trends.vue:
-  title: "Tendances"
-  refresh: "Afficher d'autres"
-  nothing: "Rien"
-desktop/views/widgets/users.vue:
-  title: "Utilisateurs·rices"
-  refresh: "Afficher d'autres"
-  no-one: "Personne"
-mobile/views/components/drive.vue:
-  used: "utilisé"
-  folder-count: "Dossier·s"
-  count-separator: ", "
-  file-count: "Fichier·s"
-  nothing-in-drive: "Rien"
-  folder-is-empty: "Ce dossier est vide"
-  folder-name: "Nom du dossier"
-  here-is-root: "Actuellement, vous êtes dans la racine et non pas dans un dossier."
-  url-prompt: "URL du fichier que vous souhaitez téléverser"
-  uploading: "Envoi demandé. Le téléversement pourrait prendre un certain temps avant de s'achever."
-  folder-name-cannot-empty: "Le nom du dossier ne peut être laissé vide."
-mobile/views/components/drive-file-chooser.vue:
-  select-file: "Choisissez un fichier"
-mobile/views/components/drive-folder-chooser.vue:
-  select-folder: "Choisissez un dossier"
-mobile/views/components/drive.file.vue:
-  nsfw: "CW"
-mobile/views/components/drive.file-detail.vue:
-  download: "Télécharger"
-  rename: "Renommer"
-  move: "Déplacer"
-  hash: "Hash (md5)"
-  exif: "EXIF"
-  nsfw: "CW"
-  mark-as-sensitive: "Marquer comme sensible"
-  unmark-as-sensitive: "Ne pas marquer comme sensible"
-mobile/views/components/media-video.vue:
-  sensitive: "Le contenu est NSFW"
-  click-to-show: "Cliquer pour afficher"
-common/views/components/follow-button.vue:
-  following: "Abonné·e"
-  follow: " Suivre"
-  request-pending: "Demande en attente"
-  follow-processing: "En cours d’abonnement"
-  follow-request: "Demande d’abonnement"
-mobile/views/components/note.vue:
-  private: "cette publication est privée"
-  deleted: "cette publication a été supprimée"
-  location: "Géolocalisation"
-mobile/views/components/note-detail.vue:
-  reply: "Répondre"
-  reaction: "Réaction"
-  private: "cette publication est privée"
-  deleted: "cette publication a été supprimée"
-  location: "Lieu"
-mobile/views/components/note-preview.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "chat"
-mobile/views/components/note-sub.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "chat"
-mobile/views/components/notifications.vue:
-  empty: "Aucune de notification !"
-mobile/views/components/sub-note-content.vue:
-  private: "cette publication est privée"
-  deleted: "cette publication a été supprimée"
-  media-count: "{} médias attachés"
-  poll: "Sondage"
-mobile/views/components/ui.header.vue:
-  welcome-back: "Content de vous revoir ! "
-  adjective: "M."
-mobile/views/components/ui.nav.vue:
-  timeline: "Fil d’actualité"
-  notifications: "Notifications"
-  follow-requests: "Demandes d’abonnement"
-  search: "Rechercher"
-  user-lists: "Listes"
-  user-groups: "Groupe"
-  widgets: "Modules"
-  game: "Jeux"
-  admin: "Admin"
-  about: "À propos de Misskey"
-mobile/views/pages/drive.vue:
-  contextmenu:
-    upload: "Téléverser un fichier"
-    url-upload: "Transférer un fichier depuis une URL"
-    create-folder: "Créer un dossier"
-    rename-folder: "Renommer le dossier"
-    move-folder: "Déplacer ce dossier"
-    delete-folder: "Supprimer ce dossier"
-mobile/views/pages/signup.vue:
-  lets-start: "Votre compte est prêt ! 📦"
-mobile/views/pages/followers.vue:
-  followers-of: "Abonné·e·s de {name}"
-mobile/views/pages/following.vue:
-  following-of: "Abonné·e·s de {name}"
-mobile/views/pages/home.vue:
-  home: "Accueil"
-  local: "Local"
-  hybrid: "Social"
-  global: "Global"
-  mentions: "Mentions"
-  messages: "Messages directs"
-mobile/views/pages/tag.vue:
-  no-posts-found: "Aucune publication ayant pour hashtag « {q} » n’a été trouvée."
-mobile/views/pages/widgets.vue:
-  dashboard: "Tableau de bord"
-  add-widget: "Ajouter"
-  customization-tips: "Conseils de personnalisation"
-mobile/views/pages/widgets/activity.vue:
-  activity: "Activité"
-mobile/views/pages/share.vue:
-  share-with: "Partager avec {name}"
-mobile/views/pages/note.vue:
-  title: "Publication"
-  prev: "Note précédente"
-  next: "Note suivante"
-mobile/views/pages/games/reversi.vue:
-  reversi: "Reversi"
-mobile/views/pages/search.vue:
-  search: "Chercher"
-  not-found: "Aucune publication trouvée pour « {q} »."
-mobile/views/pages/selectdrive.vue:
-  select-file: "Choisissez un fichier"
-mobile/views/pages/notifications.vue:
-  notifications: "Notifications"
-mobile/views/pages/settings.vue:
-  signed-in-as: "Connecté·e en tant que {}"
-mobile/views/pages/user.vue:
-  follows-you: "Vous suit"
-  following: "Abonnements"
-  followers: "Abonné·e·s"
-  notes: "Notes"
-  overview: "Aperçu"
-  timeline: "Fil d’actualité"
-  media: "Média"
-  years-old: "{age} ans"
-mobile/views/pages/user/home.vue:
-  recent-notes: "Notes récentes"
-  images: "Images"
-  activity: "Activité"
-  keywords: "Mot clés"
-  domains: "Domaines"
-  frequently-replied-users: "Mentions fréquentes"
-  followers-you-know: "Abonné·e·s que vous connaissez"
-  last-used-at: "Dernière connexion il y a"
-mobile/views/pages/user/home.photos.vue:
-  no-photos: "Pas de photos"
-deck:
-  widgets: "Widgets"
-  home: "Principal"
-  local: "Local"
-  hybrid: "Social"
-  hashtag: "Hashtags"
-  global: "Global"
-  mentions: "Mentions"
-  direct: "Messages directs"
-  notifications: "Notifications"
-  list: "Listes"
-  select-list: "Sélectionnez une liste"
-  swap-left: "Déplacer à gauche"
-  swap-right: "Déplacer à droite"
-  swap-up: "Déplacer vers le haut"
-  swap-down: "Déplacer vers le bas"
-  remove: "Supprimer la colonne"
-  add-column: "Ajouter une colonne"
-  rename: "Renommer"
-  stack-left: "Empiler à gauche"
-  pop-right: "Vers la droite"
-  disabled-timeline:
-    title: "Le fil été désactivé"
-    description: "Ce fil a été désactivé par l'administrateur du serveur."
-deck/deck.tl-column.vue:
-  is-media-only: "Les publications médias uniquement"
-  edit: "Option"
-deck/deck.user-column.vue:
-  follows-you: "Vous suit"
-  posts: "Notes"
-  following: "Suit"
-  followers: "Abonné·e·s"
-  images: "Images"
-  activity: "Activité"
-  timeline: "Fil d’actualité"
-  pinned-notes: "Notes épinglées"
-  pinned-page: "Page épinglée"
-docs:
-  edit-this-page-on-github: "Vous avez trouvé une erreur ou vous voulez contribuer à la documentation ?"
-  edit-this-page-on-github-link: "Éditez cette page sur GitHub !"
-dev/views/index.vue:
-  manage-apps: "Gestion des applications"
-dev/views/apps.vue:
-  manage-apps: "Gestion des applications"
-  create-app: "Créer une app"
-  app-missing: "Aucune application"
-dev/views/new-app.vue:
-  new-app: "Nouvelle application"
-  new-app-info: "Vous pouvez aussi créer une application avec l'API. (app/create)"
-  create-app: "Création d’une application"
-  app-name: "Nom de l’application"
-  app-name-placeholder: "p. ex. Misskey pour iOS"
-  app-name-desc: "Le nom de votre application"
-  app-overview: "Description courte de l’application"
-  app-overview-placeholder: "p. ex. Misskey pour iOS"
-  app-overview-desc: "Brève description introductive à votre application."
-  callback-url: "L’Url de callback (facultatif)"
-  callback-url-placeholder: "p. ex. https://votre.app.example.com/callback.php"
-  callback-url-desc: "Vous pouvez définir l’URL de redirection lorsque l’utilisateur s’est authentifié via formulaire d’authentification."
-  authority: "Autorisations "
-  authority-desc: "Sont accessibles via l’API, uniquement les fonctionnalités demandées ici."
-  authority-warning: "Vous pouvez le changer même après avoir créé l'application, mais si vous attribuez une nouvelle permission, toutes les clés utilisateur associées seront dès lors invalides."
-pages:
-  new-page: "Créer une page"
-  edit-page: "Modifier une page"
-  read-page: "Voir la source"
-  page-created: "Page a été créée !"
-  page-updated: "A mis à jour la page"
-  name-already-exists: "Une page portant le même nom existe déjà"
-  title-invalid-name: "L’URL de la page spécifiée n’est pas valide"
-  are-you-sure-delete: "Confirmez-vous la suppression de cette page ?"
-  page-deleted: "La page a bien été supprimée."
-  edit-this-page: "Éditer cette page"
-  pin-this-page: "Épingler sur votre profil"
-  unpin-this-page: "Désépingler"
-  view-source: "Afficher la source"
-  view-page: "Afficher la page"
-  like: "Bien"
-  unlike: "Je n’aime pas"
-  liked-pages: "Pages favorites"
-  my-pages: "Mes pages"
-  inspector: "Inspecteur"
-  content: "Bloc de page"
-  variables: "Variables"
-  more-details: "Description"
-  title: "Titre"
-  url: "URL de page"
-  summary: "Résumé de page"
-  align-center: "Centrée"
-  hide-title-when-pinned: "Masquer le titre de la page lorsque celle-ci est épinglée au profil"
-  font: "Police de caractères"
-  fontSerif: "Serif"
-  fontSansSerif: "Sans Serif"
-  set-eye-catching-image: "Définir une image attirante"
-  remove-eye-catching-image: "Supprimer une image attirante"
-  choose-block: "Ajouter un bloc"
-  select-type: "Choisir un type"
-  enter-variable-name: "Veuillez choisir un nom de variable"
-  the-variable-name-is-already-used: "Cette variable est déjà utilisée"
-  content-blocks: "Contenu du cadre"
-  input-blocks: "Entrée"
-  special-blocks: "Spécial"
-  post-from-post-form: "Publier ce contenu"
-  posted-from-post-form: "Publié !"
-  blocks:
-    text: "Texte"
-    textarea: "Zone de texte"
-    section: "Section"
-    image: "Images"
-    button: "Bouton"
-    if: "Si"
-    _if:
-      variable: "Variables"
-    post: "Champs de publication"
-    _post:
-      text: "Contenu"
-    textInput: "Entrée textuelle"
-    _textInput:
-      name: "Nom de la variable"
-      text: "Titre"
-      default: "Valeur par défaut"
-    _textareaInput:
-      name: "Nom de la variable"
-      text: "Titre"
-      default: "Valeur par défaut"
-    numberInput: "Entrée numérique"
-    _numberInput:
-      name: "Nom de la variable"
-      text: "Titre"
-      default: "Valeur par défaut"
-    switch: "Basculer"
-    _switch:
-      name: "Nom de la variable"
-      text: "Titre"
-      default: "Valeur par défaut"
-    counter: "Compteur"
-    _counter:
-      name: "Nom de la variable"
-      text: "Titre"
-      inc: "Augmenter le chiffre"
-    _button:
-      text: "Titre"
-      colored: "Couleur"
-      action: "L'opération lorsque le bouton sera pressé"
-      _action:
-        dialog: "Afficher une fenêtre de dialogue"
-        _dialog:
-          content: "Contenu"
-        resetRandom: "Réinitialiser le nombre aléatoire"
-        pushEvent: "Envoyer un évènement"
-        _pushEvent:
-          event: "Nom de l'évènement"
-          message: "Message à afficher lorsque appuyé"
-          variable: "Variable à envoyer"
-          no-variable: "Aucune"
-    radioButton: "Choix"
-    _radioButton:
-      name: "Nom de la variable"
-      title: "Titre"
-      default: "Valeur par défaut"
-  script:
-    categories:
-      flow: "Contrôle"
-      logical: "Opération logique"
-      operation: "Calculer"
-      comparison: "Comparer"
-      random: "Aléatoire"
-      value: "Valeur"
-      fn: "Fonction"
-      text: "Actions texte"
-      convert: "Convertir"
-      list: "Listes"
-    blocks:
-      text: "Texte"
-      multiLineText: "Texte (Multi-lignes)"
-      textList: "Liste de texte"
-      strLen: "Longueur du texte"
-      _strLen:
-        arg1: "Texte"
-      strPick: "Extraire un caractère"
-      _strPick:
-        arg1: "Texte"
-        arg2: "Position du joueur"
-      strReplace: "Remplacement de texte"
-      _strReplace:
-        arg1: "Texte"
-        arg2: "Avant le remplacement"
-        arg3: "Après le remplacement"
-      strReverse: "Inverser le texte"
-      _strReverse:
-        arg1: "Texte"
-      _join:
-        arg1: "Listes"
-        arg2: "Séparateur"
-      add: "+ Plus"
-      _add:
-        arg1: "A"
-        arg2: "B"
-      subtract: "- Moins"
-      _subtract:
-        arg1: "A"
-        arg2: "B"
-      multiply: "× Multiplier par"
-      _multiply:
-        arg1: "A"
-        arg2: "B"
-      divide: "÷ Diviser par"
-      _divide:
-        arg1: "A"
-        arg2: "B"
-      _mod:
-        arg1: "A"
-        arg2: "B"
-      _round:
-        arg1: "Numérique"
-      eq: "A et B sont équivalents"
-      _eq:
-        arg1: "A"
-        arg2: "B"
-      notEq: "A et B sont différents"
-      _notEq:
-        arg1: "A"
-        arg2: "B"
-      and: "A et B"
-      _and:
-        arg1: "A"
-        arg2: "B"
-      or: "A ou B"
-      _or:
-        arg1: "A"
-        arg2: "B"
-      lt: "A est plus petit que B"
-      _lt:
-        arg1: "A"
-        arg2: "B"
-      gt: "A est supérieur à B"
-      _gt:
-        arg1: "A"
-        arg2: "B"
-      ltEq: "A est plus petit ou égal à B"
-      _ltEq:
-        arg1: "A"
-        arg2: "B"
-      gtEq: "A est supérieur ou égal à B"
-      _gtEq:
-        arg1: "A"
-        arg2: "B"
-      if: "Branche"
-      _if:
-        arg1: "Si"
-        arg2: "donc"
-        arg3: "sinon"
-      not: "négation"
-      _not:
-        arg1: "négation"
-      random: "Aléatoire"
-      _random:
-        arg1: "Probabilité"
-      rannum: "Nombre aléatoire"
-      _rannum:
-        arg1: "Minimum"
-        arg2: "Maximum"
-      randomPick: "Choisir aléatoirement depuis la liste"
-      _randomPick:
-        arg1: "Listes"
-      dailyRandom: "Aléatoire (Quotidien pour chaque utilisateur)"
-      _dailyRandom:
-        arg1: "Probabilité"
-      _dailyRannum:
-        arg1: "Minimum"
-        arg2: "Maximum"
-      _dailyRandomPick:
-        arg1: "Listes"
-      seedRandom: "Aléatoire (graine)"
-      _seedRandom:
-        arg1: "Graine"
-        arg2: "Probabilité"
-      seedRannum: "Nombre aléatoire (Graine)"
-      _seedRannum:
-        arg1: "Graine"
-        arg2: "Min"
-        arg3: "Max"
-      seedRandomPick: "Sélection aléatoire dans une liste (Graine)"
-      _seedRandomPick:
-        arg1: "Graine"
-        arg2: "Listes"
-      DRPWPM: "Sélection aléatoire à partir d'une liste pondérée (mise à jour quotidienne par utilisateur)"
-      _DRPWPM:
-        arg1: "Liste de texte"
-      pick: "Sélectionner dans la liste"
-      _pick:
-        arg1: "Listes"
-        arg2: "Position"
-      listLen: "Longueur de la liste"
-      _listLen:
-        arg1: "Listes"
-      number: "Numérique"
-      stringToNumber: "Chaîne en chiffres"
-      _stringToNumber:
-        arg1: "Texte"
-      numberToString: "Chiffres en chaîne"
-      _numberToString:
-        arg1: "Numérique"
-      splitStrByLine: "Séparer le texte par lignes"
-      _splitStrByLine:
-        arg1: "Texte"
-      ref: "Variables"
-      fn: "Fonction"
-      _fn:
-        slots: "Emplacement"
-        slots-info: "Veuillez délimiter chaque emplacement par un saut de ligne"
-        arg1: "Sortie"
-      for: "Répéter"
-      _for:
-        arg1: "Compter"
-        arg2: "Action"
-    thereIsEmptySlot: "Slot {slot} est vide !"
-    types:
-      string: "Texte"
-      number: "Numérique"
-      boolean: "Marqueur"
-      array: "Listes"
-      stringArray: "Liste de texte"
-    emptySlot: "Slot vide"
-    enviromentVariables: "Variables d'environnement"
-    pageVariables: "Élément de page"
-    argVariables: "Entrée vide"
-room:
-  add-furniture: "Placer des meubles"
-  translate: "Déplacer"
-  rotate: "Tourner"
-  exit: "Retour"
-  remove: "Enlever"
-  save: "Enregistrer"
-  saved: "enregistré"
-  clear: "Tout enlever"
-  clear-confirm: "Désirez-vous enlever tout les meubles de votre chambre ?"
-  leave-confirm: "Vous avez des modifications non-sauvegardées. Voulez-vous vraiment quitter ?"
-  chooseImage: "Sélectionnez une image"
-  room-type: "Type de chambre"
-  carpet-color: "Couleur du tapis"
-  rooms:
-    default: "Par défaut"
-    washitsu: "Style japonnais"
-  furnitures:
-    milk: "Lait en carton"
-    bed: "Lit"
-    low-table: "Table basse"
-    desk: "Bureau"
-    chair: "Chaise"
-    chair2: "Chaise 2"
-    fan: "Ventilateur"
-    pc: "Ordinateur"
-    plant: "Plante d’intérieur"
-    plant2: "Plante d’intérieur 2"
-    eraser: "Gomme"
-    pencil: "Crayon"
-    pudding: "Pudding"
-    cardboard-box: "Boîte en carton"
-    cardboard-box2: "Boîte en carton 2"
-    cardboard-box3: "Boîte en carton 3"
-    book: "Livre"
-    book2: "Livre 2"
-    piano: "Piano"
-    facial-tissue: "Mouchoirs en papier"
-    server: "Serveurs"
-    moon: "Lune"
-    corkboard: "Tableau en liège"
-    mousepad: "Tapis de souris"
-    monitor: "Écran"
-    keyboard: "Clavier"
-    carpet-stripe: "Tapis (zébré)"
-    mat: "Tapis"
-    color-box: "Étagère"
-    wall-clock: "Horloge murale"
-    photoframe: "Cadre photo"
-    cube: "Cube"
-    tv: "Téléviseur"
-    pinguin: "Pingouin"
-    rubik-cube: "Cube de Rubik"
-    poster-h: "Affiche (horizontale)"
-    poster-v: "Affiche (verticale)"
-    sofa: "Canapé"
-    spiral: "Escaliers en spirale"
-    bin: "Corbeille"
-    cup-noodle: "Bol de nouilles"
-    holo-display: "Affichage holographique"
-    energy-drink: "Boisson énergétique"
diff --git a/locales/index.js b/locales/index.js
index b71625dbc69f320bfa665d859bba44fc9b50be85..749a3e0e1333d26e2655ce67708afec26eebe19d 100644
--- a/locales/index.js
+++ b/locales/index.js
@@ -14,19 +14,19 @@ const merge = (...args) => args.reduce((a, c) => ({
 }), {});
 
 const languages = [
-	'cs-CZ',
+	/*'cs-CZ',
 	'da-DK',
 	'de-DE',
 	'en-US',
 	'es-ES',
-	'fr-FR',
+	'fr-FR',*/
 	'ja-JP',
-	'ja-KS',
+	/*'ja-KS',
 	'ko-KR',
 	'nl-NL',
 	'pl-PL',
 	'zh-CN',
-	'zh-TW',
+	'zh-TW',*/
 ];
 
 const primaries = {
diff --git a/locales/it-IT.yml b/locales/it-IT.yml
deleted file mode 100644
index 8b14dc617055a440e380f305cf3d125b457b4f2b..0000000000000000000000000000000000000000
--- a/locales/it-IT.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-meta:
-  lang: "Italiano"
-common:
-  misskey: "A ⭐ of the fediverse"
-  about-title: "A ⭐ of the fediverse."
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 7ced7bdca97b2826776e60525862c556bb6a4d86..5673a2d41692681f4af29fdc0759683e7979e74f 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1,2039 +1,499 @@
-meta:
-  lang: "日本語"
-  divider: ""
-
-common:
-  misskey: "A ⭐ of fediverse"
-  about-title: "A ⭐ of fediverse."
-  about: "Misskeyを見つけていただき、ありがとうございます。Misskeyは、地球で生まれた<b>分散マイクロブログSNS</b>です。Fediverse(様々なSNSで構成される宇宙)の中に存在するため、他のSNSと相互に繋がっています。暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。"
-  intro:
-    title: "Misskeyって?"
-    about: "Misskeyはオープンソースの<b>分散型マイクロブログSNS</b>です。リッチで高度にカスタマイズできるUI、投稿へのリアクション、ファイルを一元管理できるドライブなど、先進的な機能を揃えています。また、Fediverseと呼ばれるネットワークに接続できるため、他のSNSともやり取りできます。例えば、あなたが何か投稿すると、その投稿はMisskeyだけでなく他のSNSにも伝わります。ちょうどある惑星から他の惑星に電波を発信している様子をイメージしてください。"
-    features: "特徴"
-    rich-contents: "投稿"
-    rich-contents-desc: "自分の考え、話題の出来事、皆と共有したいことについて発信してください。必要であれば、様々な構文を使って投稿を装飾したり、好きな画像、動画などのファイルやアンケートを添付することもできます。"
-    reaction: "リアクション"
-    reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。"
-    ui: "インターフェース"
-    ui-desc: "どのようなUIが使いやすいかは人それぞれです。だから、Misskeyは自由度の高いUIを持っています。レイアウトやデザインを調整したり、カスタマイズ可能な様々なウィジェットを配置したりして、自分だけのホームを作ってください。"
-    drive: "ドライブ"
-    drive-desc: "以前投稿したことのある画像をまた投稿したくなったことはありませんか?もしくは、アップロードしたファイルをフォルダ分けして整理したくなったことはありませんか?Misskeyの根幹に組み込まれたドライブ機能によってそれらが解決します。ファイルの共有も簡単です。"
-    outro: "他にもMisskeyにしかない機能はまだまだあるので、ぜひあなた自身の目で確かめてください。Misskeyは分散型SNSなので、このインスタンスが気に入らなければ他のインスタンスを試すこともできます。それでは、GLHF!"
-  application-authorization: "アプリの連携"
-  close: "閉じる"
-  do-not-copy-paste: "ここにコードを入力したり張り付けたりしないでください。アカウントが不正利用される可能性があります。"
-  load-more: "もっと読み込む"
-  enter-password: "パスワードを入力してください"
-  2fa: "二段階認証"
-  customize-home: "ホームをカスタマイズ"
-  featured-notes: "ハイライト"
-  dark-mode: "ダークモード"
-  signin: "ログイン"
-  signup: "新規登録"
-  signout: "ログアウト"
-  reload-to-apply-the-setting: "この設定を反映するにはページをリロードする必要があります。今すぐリロードしますか?"
-  fetching-as-ap-object: "連合に照会中"
-  unfollow-confirm: "{name}さんをフォロー解除しますか?"
-  delete-confirm: "この投稿を削除しますか?"
-  signin-required: "ログインしてください"
-  notification-type: "通知の種類"
-  notification-types:
-    all: "すべて"
-    pollVote: "投票"
-    follow: "フォロー"
-    receiveFollowRequest: "フォローリクエスト"
-    reply: "返信"
-    quote: "引用"
-    renote: "Renote"
-    mention: "言及"
-    reaction: "リアクション"
-
-  got-it: "わかった"
-  customization-tips:
-    title: "カスタマイズのヒント"
-    paragraph: "<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p><p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p><p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p><p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>"
-    gotit: "Got it!"
-  notification:
-    file-uploaded: "ファイルがアップロードされました"
-    message-from: "{}さんからメッセージ:"
-    reversi-invited: "対局への招待があります"
-    reversi-invited-by: "{}さんから"
-    notified-by: "{}さんから"
-    reply-from: "{}さんから返信:"
-    quoted-by: "{}さんが引用:"
-  time:
-    unknown: "なぞのじかん"
-    future: "未来"
-    just_now: "たった今"
-    seconds_ago: "{}秒前"
-    minutes_ago: "{}分前"
-    hours_ago: "{}時間前"
-    days_ago: "{}日前"
-    weeks_ago: "{}週間前"
-    months_ago: "{}ヶ月前"
-    years_ago: "{}年前"
-  month-and-day: "{month}月 {day}日"
+_ago:
+  unknown: "謎"
+  future: "未来"
+  justNow: "たった今"
+  secondsAgo: "{n}秒前"
+  minutesAgo: "{n}分前"
+  hoursAgo: "{n}時間前"
+  daysAgo: "{n}日前"
+  weeksAgo: "{n}週間前"
+  monthsAgo: "{n}ヶ月前"
+  yearsAgo: "{n}年前"
+
+_time:
+  second: "秒"
+  minute: "分"
+  hour: "時間"
+  day: "æ—¥"
 
-  trash: "ゴミ箱"
-  drive: "ドライブ"
-  pages: "ページ"
-  messaging: "トーク"
-  home: "ホーム"
-  deck: "デッキ"
+introMisskey: "ようこそ!Misskeyは、オープンソースの分散型マイクロブログサービスです。\n「ノート」を作成して、いま起こっていることを共有したり、あなたについて皆に発信しよう📡\n「リアクション」機能で、皆のノートに素早く反応を追加することもできます👍\n新しい世界を探検しよう🚀"
+monthAndDay: "{month}月 {day}日"
+search: "検索"
+notifications: "通知"
+username: "ユーザー名"
+password: "パスワード"
+fetchingAsApObject: "連合に照会中"
+ok: "OK"
+gotIt: "わかった"
+cancel: "キャンセル"
+enterUsername: "ユーザー名を入力"
+renotedBy: "{user}がRenote"
+noNotes: "投稿はありません"
+noNotifications: "通知はありません"
+instance: "インスタンス"
+settings: "設定"
+profile: "プロフィール"
+timeline: "タイムライン"
+noAccountDescription: "自己紹介はありません"
+login: "ログイン"
+loggingIn: "ログイン中"
+logout: "ログアウト"
+signup: "新規登録"
+uploading: "アップロード中"
+save: "保存"
+users: "ユーザー"
+addUser: "ユーザーを追加"
+favorite: "お気に入り"
+favorites: "お気に入り"
+unfavorite: "お気に入り解除"
+pin: "ピン留め"
+unpin: "ピン留め解除"
+copyContent: "内容をコピー"
+copyLink: "リンクをコピー"
+delete: "削除"
+addToList: "リストに追加"
+sendMessage: "メッセージを送信"
+copyUsername: "ユーザー名をコピー"
+reply: "返信"
+loadMore: "もっと見る"
+youGotNewFollower: "フォローされました"
+receiveFollowRequest: "フォローリクエストされました"
+followRequestAccepted: "フォローが承認されました"
+mentions: "あなた宛て"
+directNotes: "ダイレクト投稿"
+importAndExport: "インポートとエクスポート"
+import: "インポート"
+export: "エクスポート"
+files: "ファイル"
+download: "ダウンロード"
+driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを添付した投稿も消えます。"
+unfollowConfirm: "{name}のフォローを解除しますか?"
+exportRequested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。"
+importRequested: "インポートをリクエストしました。これには時間がかかる場合があります。"
+lists: "リスト"
+noLists: "リストはありません"
+notes: "投稿"
+following: "フォロー"
+followers: "フォロワー"
+followsYou: "フォローされています"
+createList: "リスト作成"
+manageLists: "リストの管理"
+error: "問題が発生しました"
+retry: "再試行"
+enterListName: "リスト名を入力"
+renameList: "リスト名を変更"
+deleteList: "リストを削除"
+privacy: "プライバシー"
+makeFollowManuallyApprove: "フォローを承認制にする"
+defaultNoteVisibility: "デフォルトの公開範囲"
+followRequests: "フォロー申請"
+enterEmoji: "絵文字を入力"
+renote: "Renote"
+quote: "引用"
+pinnedNote: "ピン留めされた投稿"
+you: "あなた"
+clickToShow: "クリックして表示"
+sensitive: "閲覧注意"
+add: "追加"
+reaction: "リアクション"
+reactionSettingDescription: "リアクションピッカーに表示するリアクションを改行で区切って設定します。"
+rememberNoteVisibility: "公開範囲を記憶する"
+renameFile: "ファイル名を変更"
+attachCancel: "添付取り消し"
+markAsSensitive: "閲覧注意にする"
+unmarkAsSensitive: "閲覧注意を解除する"
+enterFileName: "ファイル名を入力"
+mute: "ミュート"
+unmute: "ミュート解除"
+block: "ブロック"
+unblock: "ブロック解除"
+suspend: "凍結"
+unsuspend: "解凍"
+blockConfirm: "ブロックしますか?"
+unblockConfirm: "ブロック解除しますか?"
+suspendConfirm: "凍結しますか?"
+unsuspendConfirm: "解凍しますか?"
+selectList: "リストを選択"
+customEmojis: "カスタム絵文字"
+emojiName: "絵文字名"
+emojiUrl: "絵文字画像URL"
+addEmoji: "絵文字を追加"
+cacheRemoteFiles: "リモートのファイルをキャッシュする"
+cacheRemoteFilesDescription: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。サーバーのストレージを節約できますが、サムネイルが生成されないので通信量が増加します。"
+flagAsBot: "Botとして設定"
+flagAsCat: "Catとして設定"
+autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認"
+addAcount: "アカウント追加"
+loginFailed: "ログインに失敗しました"
+showOnRemote: "リモートで表示"
+general: "全般"
+wallpaper: "壁紙"
+removeWallpaper: "壁紙を削除"
+searchWith: "検索: {q}"
+youHaveNoLists: "リストがありません"
+followConfirm: "{name}をフォローしますか?"
+proxyAccount: "プロキシアカウント"
+proxyAccountDescription: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがインスタンスに配達されないため、代わりにプロキシアカウントがフォローするようにします。"
+host: "ホスト"
+selectUser: "ユーザーを選択"
+recipient: "宛先"
+annotation: "注釈"
+federation: "連合"
+instances: "インスタンス"
+registeredAt: "初観測"
+latestRequestSentAt: "直近のリクエスト送信"
+latestRequestReceivedAt: "直近のリクエスト受信"
+latestStatus: "直近のステータス"
+storageUsage: "ストレージ使用量"
+charts: "チャート"
+perHour: "1時間ごと"
+perDay: "1日ごと"
+stopActivityDelivery: "アクティビティの配送を停止"
+blockThisInstance: "このインスタンスをブロック"
+operations: "操作"
+software: "ソフトウェア"
+version: "バージョン"
+metadata: "メタデータ"
+withNFiles: "{n}つのファイル"
+monitor: "モニター"
+jobQueue: "ジョブキュー"
+cpuAndMemory: "CPUとメモリ"
+network: "ネットワーク"
+disk: "ディスク"
+instanceInfo: "インスタンス情報"
+statistics: "統計"
+clearQueue: "キューをクリア"
+clearQueueConfirmTitle: "キューをクリアしますか?"
+clearQueueConfirmText: "未配達の投稿は配送されなくなります。通常この操作を行う必要はありません。"
+clearCachedFiles: "キャッシュをクリア"
+clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?"
+blockedInstances: "インスタンスブロック"
+blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。"
+muteAndBlock: "ミュートとブロック"
+mutedUsers: "ミュートしたユーザー"
+blockedUsers: "ブロックしたユーザー"
+noUsers: "ユーザーはいません"
+editProfile: "プロフィールを編集"
+noteDeleteConfirm: "この投稿を削除しますか?"
+pinLimitExceeded: "これ以上ピン留めできません"
+intro: "Misskeyのインストールが完了しました!管理者アカウントを作成しましょう。"
+done: "完了"
+processing: "処理中"
+preview: "プレビュー"
+noCustomEmojis: "絵文字はありません"
+customEmojisOfRemote: "リモートの絵文字"
+noJobs: "ジョブはありません"
+federating: "連合中"
+blocked: "ブロック中"
+suspended: "配信停止"
+all: "全て"
+subscribing: "購読中"
+publishing: "配信中"
+notResponding: "応答なし"
+instanceFollowing: "インスタンスのフォロー"
+instanceFollowers: "インスタンスのフォロワー"
+instanceUsers: "インスタンスのユーザー"
+changePassword: "パスワードを変更"
+security: "セキュリティ"
+retypedNotMatch: "入力が一致しません。"
+currentPassword: "現在のパスワード"
+newPassword: "新しいパスワード"
+newPasswordRetype: "新しいパスワード(再入力)"
+attachFile: "ファイルを添付"
+more: "もっと!"
+featured: "ハイライト"
+usernameOrUserId: "ユーザー名かユーザーID"
+noSuchUser: "ユーザーが見つかりません"
+lookup: "照会"
+announcements: "お知らせ"
+imageUrl: "画像URL"
+remove: "削除"
+removed: "削除しました"
+removeAreYouSure: "「{x}」を削除しますか?"
+saved: "保存しました"
+messaging: "トーク"
+upload: "アップロード"
+fromDrive: "ドライブから"
+fromUrl: "URLから"
+editWidgets: "ウィジェットを編集"
+exitEdit: "編集を終了"
+explore: "みつける"
+games: "Misskey Games"
+messageRead: "既読"
+recentUsedEmojis: "最近使用した絵文字"
+noMoreHistory: "これより過去の履歴はありません"
+startMessaging: "トークを開始"
+nUsersRead: "{n}人が読みました"
+agreeTo: "{0}に同意"
+tos: "利用規約"
+start: "始める"
+home: "ホーム"
+remoteUserCaution: "リモートユーザーのため、情報が不完全です。"
+activity: "アクティビティ"
+images: "画像"
+birthday: "誕生日"
+yearsOld: "{age}æ­³"
+registeredDate: "登録日"
+location: "場所"
+theme: "テーマ"
+lightThemes: "明るいテーマ"
+darkThemes: "暗いテーマ"
+drive: "ドライブ"
+selectFile: "ファイルを選択"
+selectFiles: "ファイルを選択"
+renameFolder: "フォルダー名を変更"
+createFolder: "フォルダーを作成"
+deleteFolder: "フォルダーを削除"
+addFile: "ファイルを追加"
+emptyDrive: "ドライブは空です"
+emptyFolder: "フォルダーは空です"
+copyUrl: "URLをコピー"
+rename: "名前を変更"
+avatar: "アイコン"
+banner: "バナー"
+nsfw: "閲覧注意"
+disconnectedFromServer: "サーバーから切断されました"
+reloadConfirm: "リロードしますか?"
+watch: "ウォッチ"
+unwatch: "ウォッチ解除"
+accept: "許可"
+reject: "拒否"
+instanceName: "インスタンス名"
+instanceDescription: "インスタンスの紹介"
+maintainerName: "管理者の名前"
+maintainerEmail: "管理者のメールアドレス"
+tosUrl: "利用規約URL"
+thisYear: "今年"
+thisMonth: "今月"
+today: "今日"
+dayX: "{day}æ—¥"
+monthX: "{month}月"
+yearX: "{year}å¹´"
+pages: "ページ"
+integration: "連携"
+connectSerice: "接続する"
+disconnectSerice: "切断する"
+enableLocalTimeline: "ローカルタイムラインを有効にする"
+enableGlobalTimeline: "グローバルタイムラインを有効にする"
+disablingTimelinesInfo: "これらのタイムラインを無効化しても、利便性のため管理者およびモデレーターは引き続き利用することができます。"
+registration: "登録"
+enableRegistration: "誰でも新規登録できるようにする"
+invite: "招待"
+proxyRemoteFiles: "リモートのファイルをプロキシする"
+proxyRemoteFilesDescription: "この設定を有効にすると、未保存または保存容量超過で削除されたリモートファイルをローカルでプロキシし、サムネイルも生成するようになります。サーバーのストレージには影響しません、"
+driveCapacityPerLocalAccount: "ローカルユーザーひとりあたりのドライブ容量"
+driveCapacityPerRemoteAccount: "リモートユーザーひとりあたりのドライブ容量"
+inMb: "メガバイト単位"
+iconUrl: "アイコン画像のURL"
+bannerUrl: "バナー画像のURL"
+basicInfo: "基本情報"
+pinnedUsers: "ピン留めユーザー"
+pinnedUsersDescription: "「みつける」ページなどにピン留めしたいユーザーを改行で区切って記述します。"
+recaptcha: "reCAPTCHA"
+enableRecaptcha: "reCAPTCHAを有効にする"
+recaptchaSiteKey: "サイトキー"
+recaptchaSecretKey: "シークレットキー"
+antennas: "アンテナ"
+manageAntennas: "アンテナの管理"
+name: "名前"
+antennaSource: "受信ソース"
+antennaKeywords: "受信キーワード"
+antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
+notifyAntenna: "新しい投稿を通知する"
+withFileAntenna: "ファイルが添付された投稿のみ"
+serviceworker: "ServiceWorker"
+enableServiceworker: "ServiceWorkerを有効にする"
+antennaUsersDescription: "ユーザー名を改行で区切って指定します"
+caseSensitive: "大文字小文字を区別する"
+withReplies: "返信を含む"
+connectedTo: "次のアカウントに接続されています"
+notesAndReplies: "投稿と返信"
+withFiles: "ファイル付き"
+silence: "サイレンス"
+silenceConfirm: "サイレンスしますか?"
+unsilenceConfirm: "サイレンス解除しますか?"
+popularUsers: "人気のユーザー"
+recentlyUpdatedUsers: "最近投稿したユーザー"
+recentlyRegisteredUsers: "最近登録したユーザー"
+recentlyDiscoveredUsers: "最近発見されたユーザー"
+exploreUsersCount: "{count}のユーザーがいます"
+exploreFediverse: "Fediverseを探索"
+popularTags: "人気のタグ"
+userList: "リスト"
+about: "情報"
+aboutMisskey: "Misskeyについて"
+aboutMisskeyText: "Misskeyはsyuiloによって2014年から開発されている、オープンソースのソフトウェアです。"
+misskeyMembers: "現在以下のメンバーによって開発・メンテナンスされています:"
+misskeySource: "ソースコードはここで公開されています:"
+administrator: "管理者"
+token: "トークン"
+twoStepAuthentication: "二段階認証"
+
+_2fa:
+  registerDevice: "デバイスを登録"
+  step1: "まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。"
+  step2: "次に、表示されているQRコードをアプリでスキャンします。"
+  step3: "アプリに表示されているトークンを入力して完了です。"
+  step4: "これからログインするときも、同じようにトークンを入力します。"
+
+_permissions:
+  "read:account": "アカウントの情報を見る"
+  "write:account": "アカウントの情報を変更する"
+  "read:blocks": "ブロックを見る"
+  "write:blocks": "ブロックを操作する"
+  "read:drive": "ドライブを見る"
+  "write:drive": "ドライブを操作する"
+  "read:favorites": "お気に入りを見る"
+  "write:favorites": "お気に入りを操作する"
+  "read:following": "フォローの情報を見る"
+  "write:following": "フォロー・フォロー解除する"
+  "read:messaging": "トークを見る"
+  "write:messaging": "トークを操作する"
+  "read:mutes": "ミュートを見る"
+  "write:mutes": "ミュートを操作する"
+  "write:notes": "投稿を作成・削除する"
+  "read:notifications": "通知を見る"
+  "write:notifications": "通知を操作する"
+  "read:reactions": "リアクションを見る"
+  "write:reactions": "リアクションを操作する"
+  "write:votes": "投票する"
+  "read:pages": "ページを見る"
+  "write:pages": "ページを操作する"
+  "read:page-likes": "ページのいいねを見る"
+  "write:page-likes": "ページのいいねを操作する"
+  "read:user-groups": "ユーザーグループを見る"
+  "write:user-groups": "ユーザーグループを操作する"
+
+_auth:
+  shareAccess: "「{name}」がアカウントにアクセスすることを許可しますか?"
+  permissionAsk: "このアプリは次の権限を要求しています"
+
+_antennaSources:
+  all: "全ての投稿"
+  homeTimeline: "フォローしているユーザーの投稿"
+  users: "指定した一人または複数のユーザーの投稿"
+  userList: "指定したリストのユーザーの投稿"
+
+_weekday:
+  sunday: "日曜日"
+  monday: "月曜日"
+  tuesday: "火曜日"
+  wednesday: "水曜日"
+  thursday: "木曜日"
+  friday: "金曜日"
+  saturday: "土曜日"
+
+_widgets:
+  memo: "付箋"
+  notifications: "通知"
   timeline: "タイムライン"
-  explore: "みつける"
-  following: "フォロー中"
-  followers: "フォロワー"
-  favorites: "お気に入り"
-
-  permissions:
-    "read:account": "アカウントの情報を見る"
-    "write:account": "アカウントの情報を変更する"
-    "read:blocks": "ブロックを見る"
-    "write:blocks": "ブロックを操作する"
-    "read:drive": "ドライブを見る"
-    "write:drive": "ドライブを操作する"
-    "read:favorites": "お気に入りを見る"
-    "write:favorites": "お気に入りを操作する"
-    "read:following": "フォローの情報を見る"
-    "write:following": "フォロー・フォロー解除する"
-    "read:messaging": "トークを見る"
-    "write:messaging": "トークを操作する"
-    "read:mutes": "ミュートを見る"
-    "write:mutes": "ミュートを操作する"
-    "write:notes": "投稿を作成・削除する"
-    "read:notifications": "通知を見る"
-    "write:notifications": "通知を操作する"
-    "read:reactions": "リアクションを見る"
-    "write:reactions": "リアクションを操作する"
-    "write:votes": "投票する"
-    "read:pages": "ページを見る"
-    "write:pages": "ページを操作する"
-    "read:page-likes": "ページのいいねを見る"
-    "write:page-likes": "ページのいいねを操作する"
-    "read:user-groups": "ユーザーグループを見る"
-    "write:user-groups": "ユーザーグループを操作する"
-
-  empty-timeline-info:
-    follow-users-to-make-your-timeline: "ユーザーをフォローすると投稿がタイムラインに表示されます。"
-    explore: "ユーザーを探索する"
-
-  post-form:
-    attach-location-information: "位置情報を添付する"
-    hide-contents: "内容を隠す"
-    reply-placeholder: "この投稿への返信..."
-    quote-placeholder: "この投稿を引用..."
-    option-quote-placeholder: "この投稿を引用... (オプション)"
-    quote-attached: "引用付き"
-    quote-question: "引用として添付しますか?"
-    submit: "投稿"
-    reply: "返信"
-    renote: "Renote"
-    posting: "投稿中"
-    attach-media-from-local: "PCからメディアを添付"
-    attach-media-from-drive: "ドライブからメディアを添付"
-    insert-a-kao: "v('ω')v"
-    create-poll: "アンケートを作成"
-    text-remain: "残り{}文字"
-    recent-tags: "最近"
-    local-only-message: "この投稿はローカルにのみ公開されます"
-    click-to-tagging: "クリックでタグ付け"
-    visibility: "公開範囲"
-    geolocation-alert: "お使いの端末は位置情報に対応していません"
-    error: "エラー"
-    enter-username: "ユーザー名を入力してください"
-    specified-recipient: "宛先"
-    add-visible-user: "ユーザーを追加"
-    cw-placeholder: "内容への注釈 (オプション)"
-    username-prompt: "ユーザー名を入力してください"
-    enter-file-name: "ファイル名を編集"
-
-  weekday-short:
-    sunday: "æ—¥"
-    monday: "月"
-    tuesday: "火"
-    wednesday: "æ°´"
-    thursday: "木"
-    friday: "金"
-    saturday: "土"
-
-  weekday:
-    sunday: "日曜日"
-    monday: "月曜日"
-    tuesday: "火曜日"
-    wednesday: "水曜日"
-    thursday: "木曜日"
-    friday: "金曜日"
-    saturday: "土曜日"
-
-  reactions:
-    like: "いいね"
-    love: "しゅき"
-    laugh: "笑"
-    hmm: "ふぅ~む"
-    surprise: "わお"
-    congrats: "おめでとう"
-    angry: "おこ"
-    confused: "こまこまのこまり"
-    rip: "RIP"
-    pudding: "Pudding"
-
-  note-visibility:
-    public: "公開"
-    home: "ホーム"
-    home-desc: "ホームタイムラインにのみ公開"
-    followers: "フォロワー"
-    followers-desc: "自分のフォロワーにのみ公開"
-    specified: "ダイレクト"
-    specified-desc: "指定したユーザーにのみ公開"
-    local-public: "公開 (ローカルのみ)"
-    local-home: "ホーム (ローカルのみ)"
-    local-followers: "フォロワー (ローカルのみ)"
-
-  note-placeholders:
-    a: "今どうしてる?"
-    b: "何かありましたか?"
-    c: "何をお考えですか?"
-    d: "言いたいことは?"
-    e: "ここに書いてください"
-    f: "あなたが書くのを待っています..."
-
-  settings: "設定"
-  _settings:
-    profile: "プロフィール"
-    notification: "通知"
-    apps: "アプリ"
-    tags: "ハッシュタグ"
-    mute-and-block: "ミュート/ブロック"
-    blocking: "ブロック"
-    security: "セキュリティ"
-    signin: "ログイン履歴"
-    password: "パスワード"
-    other: "その他"
-    appearance: "デザイン"
-    behavior: "動作"
-    reactions: "リアクション"
-    reactions-description: "リアクションピッカーに表示するリアクションを改行で区切って設定します。"
-    fetch-on-scroll: "スクロールで自動読み込み"
-    fetch-on-scroll-desc: "ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。"
-    note-visibility: "投稿の公開範囲"
-    default-note-visibility: "デフォルトの公開範囲"
-    remember-note-visibility: "投稿の公開範囲を記憶する"
-    web-search-engine: "ウェブ検索エンジン"
-    web-search-engine-desc: "例: https://www.google.com/?#q={{query}}"
-    paste: "ペースト"
-    pasted-file-name: "ペーストされたファイル名のテンプレート"
-    pasted-file-name-desc: "例: \"yyyy-MM-dd HH-mm-ss [{{number}}]\" → \"2018-03-20 21-30-24 1\""
-    paste-dialog: "ペースト時にファイル名を編集"
-    paste-dialog-desc: "ペースト時にファイル名を編集するダイアログを表示するようにします。"
-    keep-cw: "CW保持"
-    keep-cw-desc: "投稿にリプライする際、リプライ元の投稿にCWが設定されていたとき、デフォルトで同じCWを設定するようにします。"
-    i-like-sushi: "私は(プリンよりむしろ)寿司が好き"
-    show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示"
-    use-avatar-reversi-stones: "リバーシの石にアバターを使う"
-    disable-animated-mfm: "投稿内の動きのあるテキストを無効にする"
-    disable-showing-animated-images: "アニメーション画像を再生しない"
-    enable-quick-notification-view: "通知のクイックビューを有効にする"
-    suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
-    always-show-nsfw: "常に閲覧注意のメディアを表示する"
-    always-mark-nsfw: "常にメディアを閲覧注意として投稿"
-    show-full-acct: "ユーザー名のホストを省略しない"
-    show-via: "viaを表示する"
-    reduce-motion: "UIの動きを減らす"
-    this-setting-is-this-device-only: "このデバイスのみ"
-    use-os-default-emojis: "OS標準の絵文字を使用"
-    line-width: "線の太さ"
-    line-width-thin: "細い"
-    line-width-normal: "普通"
-    line-width-thick: "太い"
-    font-size: "文字の大きさ"
-    font-size-x-small: "小さい"
-    font-size-small: "少し小さい"
-    font-size-medium: "普通"
-    font-size-large: "少し大きい"
-    font-size-x-large: "大きい"
-    deck-column-align: "デッキのカラムの配置"
-    deck-column-align-center: "中央"
-    deck-column-align-left: "å·¦"
-    deck-column-align-flexible: "フレキシブル"
-    deck-column-width: "デッキのカラムの幅"
-    deck-column-width-narrow: "ç‹­"
-    deck-column-width-narrower: "ã‚„ã‚„ç‹­"
-    deck-column-width-normal: "普通"
-    deck-column-width-wider: "やや広"
-    deck-column-width-wide: "広"
-    use-shadow: "UIに影を使用"
-    rounded-corners: "UIの角を丸める"
-    circle-icons: "円形のアバターを使用"
-    contrasted-acct: "ユーザー名にコントラストを付ける"
-    wallpaper: "壁紙"
-    choose-wallpaper: "壁紙を選択"
-    delete-wallpaper: "壁紙を削除"
-    post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
-    show-clock-on-header: "右上に時計を表示する"
-    show-reply-target: "リプライ先を表示する"
-    timeline: "タイムライン"
-    show-my-renotes: "自分の行ったRenoteをタイムラインに表示する"
-    show-renoted-my-notes: "自分の投稿のRenoteをタイムラインに表示する"
-    show-local-renotes: "ローカルの投稿のRenoteをタイムラインに表示する"
-    remain-deleted-note: "削除された投稿を表示し続ける"
-    sound: "サウンド"
-    enable-sounds: "サウンドを有効にする"
-    enable-sounds-desc: "投稿やメッセージを送受信したときなどにサウンドを再生します。この設定はブラウザに記憶されます。"
-    volume: "ボリューム"
-    test: "テスト"
-    update: "Misskey Update"
-    version: "バージョン:"
-    latest-version: "最新のバージョン:"
-    update-checking: "アップデートを確認中"
-    do-update: "アップデートを確認"
-    update-settings: "詳細設定"
-    no-updates: "利用可能な更新はありません"
-    no-updates-desc: "お使いのMisskeyは最新です。"
-    update-available: "新しいバージョンが利用可能です"
-    update-available-desc: "ページを再度読み込みすると更新が適用されます。"
-    advanced-settings: "高度な設定"
-    debug-mode: "デバッグモードを有効にする"
-    debug-mode-desc: "この設定はブラウザに記憶されます。"
-    navbar-position: "ナビゲーションバーの位置"
-    navbar-position-top: "上"
-    navbar-position-left: "å·¦"
-    navbar-position-right: "右"
-    i-am-under-limited-internet: "私は通信を制限されている"
-    post-style: "投稿の表示スタイル"
-    post-style-standard: "標準"
-    post-style-smart: "スマート"
-    notification-position: "通知の表示"
-    notification-position-bottom: "下"
-    notification-position-top: "上"
-    disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
-    load-raw-images: "添付された画像を高画質で表示する"
-    load-remote-media: "リモートサーバーのメディアを表示する"
-    sync: "同期"
-    save: "保存"
-    saved: "保存しました"
-    preview: "プレビュー"
-    home-profile: "ホームのプロファイル"
-    deck-profile: "デッキのプロファイル"
-    room: "ルーム"
-    _room:
-      graphicsQuality: "グラフィックの品質"
-      _graphicsQuality:
-        ultra: "最高"
-        high: "高"
-        medium: "中"
-        low: "低"
-        cheep: "最低"
-      useOrthographicCamera: "平行投影カメラを使用"
-
-  search: "検索"
-  delete: "削除"
-  loading: "読み込み中"
-  ok: "おk"
-  cancel: "やめる"
-  update-available-title: "更新があります"
-  update-available: "Misskeyの新しいバージョンがあります({newer}。現在{current}を利用中)。ページを再度読み込みすると更新が適用されます。"
-  my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。"
-  hide-password: "パスワードを隠す"
-  show-password: "パスワードを表示する"
-  enter-username: "ユーザー名を入力してください"
-
-  do-not-use-in-production: "これは開発ビルドです。本番環境で使用しないでください。"
-  user-suspended: "このユーザーは凍結されています。"
-  is-remote-user: "このユーザー情報は不正確な可能性があります。"
-  is-remote-post: "この投稿情報はコピーです。"
-  view-on-remote: "正確な情報を見る"
-  renoted-by: "{user}がRenote"
-  no-notes: "投稿がありません"
-  turn-on-darkmode: "闇に飲まれる"
-  turn-off-darkmode: "光あれ"
-
-  error:
-    title: "問題が発生しました"
-    retry: "やり直す"
-
-  reversi:
-    drawn: "引き分け"
-    my-turn: "あなたのターンです"
-    opponent-turn: "相手のターンです"
-    turn-of: "{name}のターンです"
-    past-turn-of: "{name}のターン"
-    won: "{name}の勝ち"
-    black: "é»’"
-    white: "白"
-    total: "合計"
-    this-turn: "{count}ターン目"
-
-  widgets:
-    analog-clock: "アナログ時計"
-    profile: "プロフィール"
-    calendar: "カレンダー"
-    timemachine: "カレンダー(タイムマシン)"
-    activity: "アクティビティ"
-    rss: "RSSリーダー"
-    memo: "付箋"
-    trends: "トレンド"
-    photo-stream: "フォトストリーム"
-    posts-monitor: "投稿チャート"
-    slideshow: "スライドショー"
-    version: "バージョン"
-    broadcast: "ブロードキャスト"
-    notifications: "通知"
-    users: "おすすめユーザー"
-    polls: "アンケート"
-    post-form: "投稿フォーム"
-    server: "サーバー情報"
-    nav: "ナビゲーション"
-    tips: "ヒント"
-    hashtags: "ハッシュタグ"
-    queue: "キュー"
-
-  dev: "アプリの作成に失敗しました。再度お試しください。"
-  ai-chan-kawaii: "藍ちゃかわいい"
-  you: "あなた"
-
-auth/views/form.vue:
-  share-access: "<i>{name}</i>があなたのアカウントにアクセスすることを許可しますか?"
-  permission-ask: "このアプリは次の権限を要求しています:"
-  cancel: "キャンセル"
-  accept: "アクセスを許可"
-
-auth/views/index.vue:
-  loading: "読み込み中"
-  denied: "アプリケーションの連携をキャンセルしました。"
-  denied-paragraph: "このアプリがあなたのアカウントにアクセスすることはありません。"
-  already-authorized: "このアプリは既に連携済みです"
-  allowed: "アプリケーションの連携を許可しました"
-  callback-url: "アプリケーションに戻っています"
-  please-go-back: "アプリケーションに戻って、やっていってください。"
-  error: "セッションが存在しません。"
-  sign-in: "サインインしてください"
-
-common/views/pages/explore.vue:
-  pinned-users: "ピン留めされたユーザー"
-  popular-users: "人気のユーザー"
-  recently-updated-users: "最近投稿したユーザー"
-  recently-registered-users: "新規ユーザー"
-  recently-discovered-users: "最近発見されたユーザー"
-  popular-tags: "人気のタグ"
-  federated: "連合"
-  explore: "{host}を探索"
-  explore-fediverse: "Fediverseを探索"
-  users-info: "現在{users}ユーザーが登録されています"
+  calendar: "カレンダー"
+  trends: "トレンド"
+  clock: "時計"
 
-common/views/components/reactions-viewer.details.vue:
-  few-users: "{users}が{reaction}をリアクション"
-  many-users: "{users}と他{omitted}人が{reaction}をリアクション"
-
-common/views/components/url-preview.vue:
-  enable-player: "プレイヤーを開く"
-  disable-player: "プレイヤーを閉じる"
-
-common/views/components/user-list.vue:
-  no-users: "ユーザーがいません"
-
-common/views/components/games/reversi/reversi.vue:
-  matching:
-    waiting-for: "{}を待っています"
-    cancel: "キャンセル"
-
-common/views/components/games/reversi/reversi.game.vue:
-  surrender: "投了"
-  surrendered: "投了により"
-  is-llotheo: "石の少ない方が勝ち(ロセオ)"
-  looped-map: "ループマップ"
-  can-put-everywhere: "どこでも置けるモード"
-
-common/views/components/games/reversi/reversi.index.vue:
-  title: "Misskey Reversi"
-  sub-title: "他のMisskeyユーザーとリバーシで対戦しよう"
-  invite: "招待"
-  rule: "遊び方"
-  rule-desc: "リバーシは、相手と交互に石をボードに置いて、相手の石を挟んで自分の色に変えてゆき、最終的に残った石が多い方が勝ちというボードゲームです。"
-  mode-invite: "招待"
-  mode-invite-desc: "指定したユーザーと対戦するモードです。"
-  invitations: "対局の招待があります!"
-  my-games: "自分の対局"
-  all-games: "みんなの対局"
-  enter-username: "ユーザー名を入力してください"
-  game-state:
-    ended: "終了"
-    playing: "進行中"
-
-common/views/components/games/reversi/reversi.room.vue:
-  settings-of-the-game: "ゲームの設定"
-  choose-map: "マップを選択"
-  random: "ランダム"
-  black-or-white: "先手/後手"
-  black-is: "{}が黒"
-  rules: "ルール"
-  is-llotheo: "石の少ない方が勝ち(ロセオ)"
-  looped-map: "ループマップ"
-  can-put-everywhere: "どこでも置けるモード"
-  settings-of-the-bot: "Botの設定"
-  this-game-is-started-soon: "ゲームは数秒後に開始されます"
-  waiting-for-other: "相手の準備が完了するのを待っています"
-  waiting-for-me: "あなたの準備が完了するのを待っています"
-  waiting-for-both: "準備中"
-  cancel: "キャンセル"
-  ready: "準備完了"
-  cancel-ready: "準備続行"
-
-common/views/components/connect-failed.vue:
-  title: "サーバーに接続できません"
-  description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。"
-  thanks: "いつもMisskeyをご利用いただきありがとうございます。"
-  troubleshoot: "トラブルシュート"
-
-common/views/components/connect-failed.troubleshooter.vue:
-  title: "トラブルシューティング"
-  network: "ネットワーク接続"
-  checking-network: "ネットワーク接続を確認中"
-  internet: "インターネット接続"
-  checking-internet: "インターネット接続を確認中"
-  server: "サーバー接続"
-  checking-server: "サーバー接続を確認中"
-  finding: "問題を調べています"
-  no-network: "ネットワークに接続されていません"
-  no-network-desc: "お使いのPCのネットワーク接続が正常か確認してください。"
-  no-internet: "インターネットに接続されていません"
-  no-internet-desc: "ネットワークには接続されていますが、インターネットには接続されていないようです。お使いのPCのインターネット接続が正常か確認してください。"
-  no-server: "Misskeyのサーバーに接続できません"
-  no-server-desc: "お使いのPCのインターネット接続は正常ですが、Misskeyのサーバーには接続できませんでした。サーバーがダウンまたはメンテナンスしている可能性があるので、しばらくしてから再度御アクセスください。"
-  success: "Misskeyのサーバーに接続できました"
-  success-desc: "正常に接続できるようです。ページを再度読み込みしてください。"
-  flush: "キャッシュの削除"
-  set-version: "バージョン指定"
-
-common/views/components/media-banner.vue:
-  sensitive: "閲覧注意"
-  click-to-show: "クリックして表示"
-
-common/views/components/theme.vue:
-  theme: "テーマ"
-  light-theme: "非ダークモード時に使用するテーマ"
-  dark-theme: "ダークモード時に使用するテーマ"
-  light-themes: "明るいテーマ"
-  dark-themes: "暗いテーマ"
-  install-a-theme: "テーマのインストール"
-  theme-code: "テーマコード"
-  install: "インストール"
-  installed: "「{}」をインストールしました"
-  create-a-theme: "テーマの作成"
-  save-created-theme: "テーマを保存"
-  primary-color: "プライマリ カラー"
-  secondary-color: "セカンダリ カラー"
-  text-color: "文字色"
-  base-theme: "ベーステーマ"
-  base-theme-light: "Light"
-  base-theme-dark: "Dark"
-  find-more-theme: "その他のテーマを入手"
-  theme-name: "テーマ名"
-  preview-created-theme: "プレビュー"
-  invalid-theme: "テーマが正しくありません。"
-  already-installed: "既にそのテーマはインストールされています。"
-  saved: "保存しました"
-  manage-themes: "テーマの管理"
-  builtin-themes: "標準テーマ"
-  my-themes: "マイテーマ"
-  installed-themes: "インストールされたテーマ"
-  select-theme: "テーマを選択してください"
-  uninstall: "アンインストール"
-  uninstalled: "「{}」をアンインストールしました"
-  author: "作者"
-  desc: "説明"
-  export: "エクスポート"
-  import: "インポート"
-  import-by-code: "またはコードをペースト"
-  theme-name-required: "テーマ名は必須です。"
-
-common/views/components/cw-button.vue:
+_cw:
   hide: "隠す"
   show: "もっと見る"
   chars: "{count}文字"
   files: "{count}ファイル"
   poll: "アンケート"
 
-common/views/components/messaging.vue:
-  search-user: "ユーザーを探す"
-  you: "あなた"
-  no-history: "履歴はありません"
-  user: "ユーザー"
-  group: "グループ"
-  start-with-user: "ユーザーとトークを開始"
-  start-with-group: "グループとトークを開始"
-  select-group: "グループを選択してください"
-
-common/views/components/messaging-room.vue:
-  not-talked-user: "このユーザーとの会話はありません"
-  not-talked-group: "このグループでの会話はありません"
-  no-history: "これより過去の履歴はありません"
-  new-message: "新しいメッセージがあります"
-  only-one-file-attached: "メッセージに添付できるファイルはひとつです"
-
-common/views/components/messaging-room.form.vue:
-  input-message-here: "ここにメッセージを入力"
-  send: "送信"
-  attach-from-local: "PCからファイルを添付する"
-  attach-from-drive: "ドライブからファイルを添付する"
-  only-one-file-attached: "メッセージに添付できるファイルはひとつです"
-
-common/views/components/messaging-room.message.vue:
-  is-read: "既読"
-  deleted: "このメッセージは削除されました"
-
-common/views/components/nav.vue:
-  about: "Misskeyについて"
-  stats: "統計"
-  status: "ステータス"
-  wiki: "Wiki"
-  donors: "ドナー"
-  repository: "リポジトリ"
-  develop: "開発者"
-  feedback: "フィードバック"
-  tos: "利用規約"
-
-common/views/components/note-menu.vue:
-  mention: "メンション"
-  detail: "詳細"
-  copy-content: "内容をコピー"
-  copy-link: "リンクをコピー"
-  favorite: "お気に入り"
-  unfavorite: "お気に入り解除"
-  watch: "ウォッチ"
-  unwatch: "ウォッチ解除"
-  pin: "ピン留め"
-  unpin: "ピン留め解除"
-  delete: "削除"
-  delete-confirm: "この投稿を削除しますか?"
-  delete-and-edit: "削除して編集"
-  delete-and-edit-confirm: "この投稿を削除してもう一度編集しますか?この投稿へのリアクション、Renote、返信も全て削除されます。"
-  remote: "投稿元で見る"
-  pin-limit-exceeded: "これ以上ピン留めできません。"
-
-common/views/components/user-menu.vue:
-  mention: "メンション"
-  mute: "ミュート"
-  unmute: "ミュート解除"
-  mute-confirm: "このユーザーをミュートしますか?"
-  unmute-confirm: "このユーザーをミュート解除しますか?"
-  block: "ブロック"
-  unblock: "ブロック解除"
-  block-confirm: "このユーザーをブロックしますか?"
-  unblock-confirm: "このユーザーをブロック解除しますか?"
-  push-to-list: "リストに追加"
-  select-list: "リストを選択してください"
-  report-abuse: "スパムを報告"
-  report-abuse-detail: "どのような迷惑行為を行っていますか?"
-  report-abuse-reported: "管理者に報告されました。ご協力ありがとうございました。"
-  silence: "サイレンス"
-  unsilence: "サイレンス解除"
-  silence-confirm: "このユーザーをサイレンスしますか?"
-  unsilence-confirm: "このユーザーをサイレンス解除しますか?"
-  suspend: "凍結"
-  unsuspend: "凍結解除"
-  suspend-confirm: "このユーザーを凍結しますか?"
-  unsuspend-confirm: "このユーザーを凍結解除しますか?"
-
-common/views/components/poll.vue:
-  vote-to: "「{}」に投票する"
-  vote-count: "{}票"
-  total-votes: "計{}票"
-  vote: "投票する"
-  show-result: "結果を見る"
-  voted: "投票済み"
-  closed: "終了済み"
-  remaining-days: "終了まであと{d}日{h}時間"
-  remaining-hours: "終了まであと{h}時間{m}分"
-  remaining-minutes: "終了まであと{m}分{s}秒"
-  remaining-seconds: "終了まであと{s}秒"
-
-common/views/components/poll-editor.vue:
-  no-only-one-choice: "アンケートには、選択肢が最低2つ必要です"
-  choice-n: "選択肢{}"
-  remove: "この選択肢を削除"
-  add: "+選択肢を追加"
-  destroy: "アンケートを破棄"
-  multiple: "複数回答可"
+_poll:
+  noOnlyOneChoice: "選択肢は最低2つ必要です"
+  choiceN: "選択肢{n}"
+  noMore: "これ以上追加できません"
+  canMultipleVote: "複数回答可"
   expiration: "期限"
   infinite: "無期限"
   at: "日時指定"
   after: "経過指定"
-  no-more: "これ以上追加できません"
-  deadline-date: "期日"
-  deadline-time: "時間"
-  interval: "期間"
-  unit: "単位"
-  second: "秒"
-  minute: "分"
-  hour: "時間"
-  day: "æ—¥"
-
-common/views/components/reaction-picker.vue:
-  choose-reaction: "リアクションを選択"
-  input-reaction-placeholder: "または絵文字を入力"
-
-common/views/components/emoji-picker.vue:
-  recent-emoji: "最近使った絵文字"
-  custom-emoji: "カスタム絵文字"
-  no-category: "カテゴリなし"
-  people: "人"
-  animals-and-nature: "動物&自然"
-  food-and-drink: "食べ物&飲み物"
-  activity: "アクティビティ"
-  travel-and-places: "場所"
-  objects: "物"
-  symbols: "記号"
-  flags: "æ——"
-
-common/views/components/settings/app-type.vue:
-  title: "モード"
-  intro: "デスクトップ版とモバイル版のどちらを使うかを指定できます。"
-  choices:
-    auto: "自動で選択"
-    desktop: "デスクトップ版に固定"
-    mobile: "モバイル版に固定"
-  info: "変更はページの再度読み込み後に反映されます。"
-
-common/views/components/signin.vue:
-  username: "ユーザー名"
-  password: "パスワード"
-  token: "トークン"
-  signing-in: "やってます..."
-  or: "または"
-  signin-with-twitter: "Twitterでログイン"
-  signin-with-github: "GitHubでログイン"
-  signin-with-discord: "Discordでログイン"
-  login-failed: "ログインできませんでした。ユーザー名とパスワードを確認してください。"
-  tap-key: "セキュリティキーをクリックしてログイン"
-  enter-2fa-code: "認証コードを入力してください"
-
-common/views/components/signup.vue:
-  invitation-code: "招待コード"
-  invitation-info: "招待コードをお持ちでない方は、<a href=\"{}\">管理者</a>までご連絡ください。"
-  username: "ユーザー名"
-  checking: "確認しています..."
-  available: "利用できます"
-  unavailable: "既に利用されています"
-  error: "通信エラー"
-  invalid-format: "a~z、A~Z、0~9、_が使えます"
-  too-short: "1文字以上でお願いします!"
-  too-long: "20文字以内でお願いします"
-  password: "パスワード"
-  password-placeholder: "8文字以上を推奨します"
-  weak-password: "弱いパスワード"
-  normal-password: "まあまあのパスワード"
-  strong-password: "強いパスワード"
-  retype: "再入力"
-  retype-placeholder: "確認のため再入力してください"
-  password-matched: "確認されました"
-  password-not-matched: "一致していません"
-  recaptcha: "認証"
-  agree-to: "{0}に同意します。"
-  tos: "利用規約"
-  create: "アカウント作成"
-  some-error: "何らかの原因によりアカウントの作成に失敗しました。再度お試しください。"
-
-common/views/components/special-message.vue:
-  new-year: "Happy New Year!"
-  christmas: "Merry Christmas!"
-
-common/views/components/stream-indicator.vue:
-  connecting: "接続中"
-  reconnecting: "再接続中"
-  connected: "接続完了"
-
-common/views/components/notification-settings.vue:
-  title: "通知"
-  mark-as-read-all-notifications: "すべての通知を既読にする"
-  mark-as-read-all-unread-notes: "すべての投稿を既読にする"
-  mark-as-read-all-talk-messages: "すべてのトークを既読にする"
-  auto-watch: "投稿の自動ウォッチ"
-  auto-watch-desc: "リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにします。"
-
-common/views/components/instance.vue:
-  start: 始める
-
-common/views/components/integration-settings.vue:
-  title: "サービス連携"
-  connect: "接続する"
-  disconnect: "切断する"
-  connected-to: "次のアカウントに接続されています"
-
-common/views/components/github-setting.vue:
-  description: "お使いのGitHubアカウントをお使いのMisskeyアカウントに接続しておくと、プロフィールでGitHubアカウント情報が表示されるようになったり、GitHubを用いた便利なサインインを利用できるようになります。"
-  connected-to: "次のGitHubアカウントに接続されています"
-  detail: "詳細..."
-  reconnect: "再接続する"
-  connect: "GitHubと接続する"
-  disconnect: "切断する"
-
-common/views/components/discord-setting.vue:
-  description: "お使いのDiscordアカウントをお使いのMisskeyアカウントに接続しておくと、プロフィールでDiscordアカウント情報が表示されるようになったり、Discordを用いた便利なサインインを利用できるようになります。"
-  connected-to: "次のDiscordアカウントに接続されています"
-  detail: "詳細..."
-  reconnect: "再接続する"
-  connect: "Discordと接続する"
-  disconnect: "切断する"
-
-common/views/components/uploader.vue:
-  waiting: "待機中"
-
-common/views/components/visibility-chooser.vue:
-  public: "公開"
+  deadlineDate: "期日"
+  deadlineTime: "時間"
+  duration: "期間"
+  votesCount: "{n}票"
+  totalVotes: "計{n}票"
+  vote: "投票する"
+  showResult: "結果を見る"
+  voted: "投票済み"
+  closed: "終了済み"
+  remainingDays: "終了まであと{d}日{h}時間"
+  remainingHours: "終了まであと{h}時間{m}分"
+  remainingMinutes: "終了まであと{m}分{s}秒"
+  remainingSeconds: "終了まであと{s}秒"
+
+_visibility:
+  public: "パブリック"
+  publicDescription: "全てのユーザーに公開"
   home: "ホーム"
-  home-desc: "ホームタイムラインにのみ公開"
+  homeDescription: "ホームタイムラインのみに公開"
   followers: "フォロワー"
-  followers-desc: "自分のフォロワーにのみ公開"
+  followersDescription: "自分のフォロワーのみに公開"
   specified: "ダイレクト"
-  specified-desc: "指定したユーザーにのみ公開"
-  local-public: "公開 (ローカルのみ)"
-  local-public-desc: "リモートへは公開しない"
-  local-home: "ホーム (ローカルのみ)"
-  local-followers: "フォロワー (ローカルのみ)"
-
-common/views/components/trends.vue:
-  count: "{}人が投稿"
-  empty: "トレンドなし"
-
-common/views/components/language-settings.vue:
-  title: "表示言語"
-  pick-language: "言語を選択"
-  recommended: "推奨"
-  auto: "自動"
-  specify-language: "言語を指定"
-  info: "変更はページの再度読み込み後に反映されます。"
+  specifiedDescription: "指定したユーザーのみに公開"
+
+_postForm:
+  replyPlaceholder: "この投稿に返信..."
+  quotePlaceholder: "この投稿を引用..."
+  post: "投稿"
+  _placeholders:
+    a: "いまどうしてる?"
+    b: "何かありましたか?"
+    c: "何をお考えですか?"
+    d: "言いたいことは?"
+    e: "ここに書いてください"
+    f: "あなたが書くのを待っています..."
 
-common/views/components/profile-editor.vue:
-  title: "プロフィール"
+_profile:
   name: "名前"
-  account: "アカウント"
-  location: "場所"
-  description: "自己紹介"
-  you-can-include-hashtags: "ハッシュタグを含めることができます。"
-  language: "言語"
-  birthday: "誕生日"
-  avatar: "アバター"
-  banner: "バナー"
-  is-cat: "このアカウントはCatです"
-  is-bot: "このアカウントはBotです"
-  is-locked: "フォローを承認制にする"
-  careful-bot: "Botからのフォローだけ承認制にする"
-  auto-accept-followed: "フォローしているユーザーからのフォローを自動承認する"
-  advanced: "その他"
-  privacy: "プライバシー"
-  save: "保存"
-  saved: "プロフィールを保存しました"
-  uploading: "アップロード中"
-  upload-failed: "アップロードに失敗しました"
-  unable-to-process: "操作を完了できません"
-  avatar-not-an-image: "アバターとして指定したファイルは画像ではありません"
-  banner-not-an-image: "バナーとして指定したファイルは画像ではありません"
-  email: "メール設定"
-  email-address: "メールアドレス"
-  email-verified: "メールアドレスが確認されました"
-  email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
-  export: "エクスポート"
-  import: "インポート"
-  export-and-import: "エクスポートとインポート"
-  export-targets:
-    all-notes: "すべての投稿データ"
-    following-list: "フォロー"
-    mute-list: "ミュート"
-    blocking-list: "ブロック"
-    user-lists: "リスト"
-  export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
-  import-requested: "インポートをリクエストしました。これには時間がかかる場合があります。"
-  enter-password: "パスワードを入力してください"
-  danger-zone: "危険な設定"
-  delete-account: "アカウントを削除"
-  account-deleted: "アカウントが削除されました。データが消えるまで時間がかかる場合があります。"
-  profile-metadata: "プロフィール補足情報"
-  metadata-label: "ラベル"
-  metadata-content: "内容"
-
-common/views/components/user-list-editor.vue:
-  users: "ユーザー"
-  rename: "リスト名を変更"
-  delete: "リストを削除"
-  remove-user: "このリストから削除"
-  delete-are-you-sure: "リスト「$1」を削除しますか?"
-  deleted: "削除しました"
-  add-user: "ユーザーを追加"
-
-common/views/components/user-group-editor.vue:
-  users: "メンバー"
-  rename: "グループ名を変更"
-  delete: "グループを削除"
-  transfer: "グループを譲渡"
-  transfer-are-you-sure: "グループ「$1」を「@$2」さんに譲渡しますか?"
-  transferred: "グループを譲渡しました"
-  remove-user: "このグループから削除"
-  delete-are-you-sure: "グループ「$1」を削除しますか?"
-  deleted: "削除しました"
-  invite: "招待"
-  invited: "招待を送信しました"
-
-common/views/components/user-lists.vue:
-  user-lists: "リスト"
-  create-list: "リストを作成"
-  list-name: "リスト名"
-
-common/views/components/user-groups.vue:
-  user-groups: "グループ"
-  create-group: "グループを作成"
-  group-name: "グループ名"
-  owned-groups: "自分のグループ"
-  joined-groups: "参加しているグループ"
-  invites: "招待"
-  accept-invite: "参加"
-  reject-invite: "拒否"
-
-common/views/widgets/broadcast.vue:
-  fetching: "確認中"
-  no-broadcasts: "お知らせはありません"
-  have-a-nice-day: "良い一日を!"
-  next: "次"
-  prev: "前"
-
-common/views/widgets/calendar.vue:
-  year: "{}å¹´"
-  month: "{}月"
-  day: "{}æ—¥"
-  today: "今日:"
-  this-month: "今月:"
-  this-year: "今年:"
-
-common/views/widgets/photo-stream.vue:
-  title: "フォトストリーム"
-  no-photos: "写真はありません"
-
-common/views/widgets/posts-monitor.vue:
-  title: "投稿チャート"
-  toggle: "表示を切り替え"
-
-common/views/widgets/hashtags.vue:
-  title: "ハッシュタグ"
-
-common/views/widgets/server.vue:
-  title: "サーバー情報"
-  toggle: "表示を切り替え"
-
-common/views/widgets/memo.vue:
-  title: "付箋"
-  memo: "ここに書いて!"
-  save: "保存"
-
-common/views/widgets/slideshow.vue:
-  folder-customize-mode: "フォルダを指定するには、カスタマイズモードを終了してください"
-  folder: "クリックしてフォルダを指定してください"
-  no-image: "このフォルダには画像がありません"
-
-common/views/widgets/tips.vue:
-  tips-line1: "<kbd>t</kbd>でタイムラインにフォーカスできます"
-  tips-line2: "<kbd>p</kbd>または<kbd>n</kbd>で投稿フォームを開きます"
-  tips-line3: "投稿フォームにはファイルをドラッグ&ドロップできます"
-  tips-line4: "投稿フォームにクリップボードにある画像データをペーストできます"
-  tips-line5: "ドライブにファイルをドラッグ&ドロップしてアップロードできます"
-  tips-line6: "ドライブでファイルをドラッグしてフォルダ移動できます"
-  tips-line7: "ドライブでフォルダをドラッグしてフォルダ移動できます"
-  tips-line8: "ホームは設定からカスタマイズできます"
-  tips-line9: "MisskeyはAGPLv3です"
-  tips-line10: "タイムマシンウィジェットを利用すると、簡単に過去のタイムラインに遡れます"
-  tips-line11: "投稿の ... をクリックして、投稿をユーザーページにピン留めできます"
-  tips-line13: "投稿に添付したファイルは全てドライブに保存されます"
-  tips-line14: "ホームのカスタマイズ中、ウィジェットを右クリックしてデザインを変更できます"
-  tips-line17: "「**」でテキストを囲むと**強調表示**されます"
-  tips-line19: "いくつかのウィンドウはブラウザの外に切り離すことができます"
-  tips-line20: "カレンダーウィジェットのパーセンテージは、経過の割合を示しています"
-  tips-line21: "APIを利用してbotの開発なども行えます"
-  tips-line23: "藍かわいいよ藍"
-  tips-line24: "Misskeyは2014年にサービスを開始しました"
-  tips-line25: "対応ブラウザではMisskeyを開いていなくても通知を受け取れます"
-
-common/views/pages/not-found.vue:
-  page-not-found: "ページが見つかりませんでした"
-
-common/views/pages/follow.vue:
-  signed-in-as: "{}としてサインイン中"
-  following: "フォロー中"
-  follow: "フォロー"
-  request-pending: "フォロー許可待ち"
-  follow-processing: "フォロー処理中"
-  follow-request: "フォロー申請"
-
-common/views/pages/follow-requests.vue:
-  received-follow-requests: "フォロー申請"
-  accept: "承認"
-  reject: "拒否"
-
-desktop:
-  banner-crop-title: "バナーとして表示する部分を選択"
-  banner: "バナー"
-  uploading-banner: "新しいバナーをアップロードしています"
-  banner-updated: "バナーを更新しました"
-  choose-banner: "バナーにする画像を選択"
-  avatar-crop-title: "アバターとして表示する部分を選択"
-  avatar: "アバター"
-  uploading-avatar: "新しいアバターをアップロードしています"
-  avatar-updated: "アバターを更新しました"
-  choose-avatar: "アバターにする画像を選択"
-  unable-to-process: "操作を完了できません"
-  invalid-filetype: "この形式のファイルはサポートされていません"
-
-desktop/views/components/activity.chart.vue:
-  total: "Black ... Total"
-  notes: "Blue ... Notes"
-  replies: "Red ... Replies"
-  renotes: "Green ... Renotes"
-
-desktop/views/components/activity.vue:
-  title: "アクティビティ"
-  toggle: "表示を切り替え"
-
-desktop/views/components/calendar.vue:
-  title: "{year}年 {month}月"
-  prev: "前の月"
-  next: "次の月"
-  go: "クリックして時間遡行"
-
-desktop/views/components/choose-file-from-drive-window.vue:
-  chosen-files: "{count}ファイル選択中"
-  upload: "PCからドライブにファイルをアップロード"
-  cancel: "キャンセル"
-  ok: "決定"
-  choose-prompt: "ファイルを選択"
-
-desktop/views/components/choose-folder-from-drive-window.vue:
-  cancel: "キャンセル"
-  ok: "決定"
-  choose-prompt: "フォルダを選択"
-
-desktop/views/components/crop-window.vue:
-  skip: "クロップをスキップ"
-  cancel: "キャンセル"
-  ok: "決定"
-
-desktop/views/components/drive-window.vue:
-  used: "使用中"
-
-desktop/views/components/drive.file.vue:
-  avatar: "アバター"
-  banner: "バナー"
-  nsfw: "閲覧注意"
-  contextmenu:
-    rename: "名前を変更"
-    mark-as-sensitive: "閲覧注意に設定"
-    unmark-as-sensitive: "閲覧注意を解除"
-    copy-url: "URLをコピー"
-    download: "ダウンロード"
-    else-files: "その他"
-    set-as-avatar: "アバターに設定"
-    set-as-banner: "バナーに設定"
-    open-in-app: "アプリで開く"
-    add-app: "アプリを追加"
-    rename-file: "ファイル名の変更"
-    input-new-file-name: "新しいファイル名を入力してください"
-    copied: "コピー完了"
-    copied-url-to-clipboard: "URLをクリップボードにコピーしました"
-
-desktop/views/components/drive.folder.vue:
-  upload-folder: "既定アップロード先"
-  unable-to-process: "操作を完了できません"
-  circular-reference-detected: "移動先のフォルダーは、移動するフォルダーのサブフォルダーです。"
-  unhandled-error: "不明なエラー"
-  unable-to-delete: "削除できません"
-  has-child-files-or-folders: "このフォルダは空でないため、削除できません。"
-  contextmenu:
-    move-to-this-folder: "このフォルダへ移動"
-    show-in-new-window: "新しいウィンドウで表示"
-    rename: "名前を変更"
-    rename-folder: "フォルダ名の変更"
-    input-new-folder-name: "新しいフォルダ名を入力してください"
-    else-folders: "その他"
-    set-as-upload-folder: "既定アップロード先に設定"
-
-desktop/views/components/drive.vue:
-  search: "検索"
-  empty-draghover: "ドロップですか?いいですよ、ボクはカワイイですからね"
-  empty-drive: "ドライブには何もありません。"
-  empty-drive-description: "右クリックして「ファイルをアップロード」を選んだり、ファイルをドラッグ&ドロップすることでもアップロードできます。"
-  empty-folder: "このフォルダーは空です"
-  unable-to-process: "操作を完了できません"
-  circular-reference-detected: "移動先のフォルダーは、移動するフォルダーのサブフォルダーです。"
-  unhandled-error: "不明なエラー"
-  url-upload: "URLアップロード"
-  url-of-file: "アップロードしたいファイルのURL"
-  url-upload-requested: "アップロードをリクエストしました"
-  may-take-time: "アップロードが完了するまで時間がかかる場合があります。"
-  create-folder: "フォルダー作成"
-  folder-name: "フォルダー名"
-  contextmenu:
-    create-folder: "フォルダーを作成"
-    upload: "ファイルをアップロード"
-    url-upload: "URLからアップロード"
-
-desktop/views/components/media-video.vue:
-  sensitive: "閲覧注意"
-  click-to-show: "クリックして表示"
-
-desktop/views/components/followers-window.vue:
-  followers: "{} のフォロワー"
-
-desktop/views/components/followers.vue:
-  empty: "フォロワーはいないようです。"
-
-desktop/views/components/following-window.vue:
-  following: "{} のフォロー"
-
-desktop/views/components/following.vue:
-  empty: "フォロー中のユーザーはいないようです。"
-
-desktop/views/components/game-window.vue:
-  game: "リバーシ"
-
-desktop/views/components/home.vue:
-  done: "完了"
-  add-widget: "ウィジェットを追加:"
-  add: "追加"
-
-desktop/views/input-dialog.vue:
-  cancel: "キャンセル"
-  ok: "決定"
-
-desktop/views/components/note-detail.vue:
-  private: "この投稿は非公開です"
-  deleted: "この投稿は削除されました"
-  location: "位置情報"
-  renote: "Renote"
-  add-reaction: "リアクション"
-  undo-reaction: "リアクション解除"
-
-desktop/views/components/note.vue:
-  reply: "返信"
-  renote: "Renote"
-  add-reaction: "リアクション"
-  undo-reaction: "リアクション解除"
-  detail: "詳細"
-  private: "この投稿は非公開です"
-  deleted: "この投稿は削除されました"
-
-desktop/views/components/notes.vue:
-  error: "読み込みに失敗しました。"
-  retry: "リトライ"
-
-desktop/views/components/notifications.vue:
-  empty: "ありません!"
-
-desktop/views/components/post-form.vue:
-  posted: "投稿しました!"
-  replied: "返信しました!"
-  reposted: "Renoteしました!"
-  note-failed: "投稿に失敗しました"
-  reply-failed: "返信に失敗しました"
-  renote-failed: "Renoteに失敗しました"
-
-desktop/views/components/post-form-window.vue:
-  note: "新規投稿"
-  reply: "返信"
-  attaches: "添付: {}メディア"
-  uploading-media: "{}個のメディアをアップロード中"
-
-desktop/views/components/progress-dialog.vue:
-  waiting: "待機中"
-
-desktop/views/components/renote-form.vue:
-  quote: "引用する..."
-  cancel: "キャンセル"
-  renote: "Renote"
-  renote-home: "Renote (Home)"
-  reposting: "しています..."
-  success: "Renoteしました!"
-  failure: "Renoteに失敗しました"
-
-desktop/views/components/renote-form-window.vue:
-  title: "この投稿をRenoteしますか?"
-
-desktop/views/pages/user-following-or-followers.vue:
-  following: "{user}のフォロー"
-  followers: "{user}のフォロワー"
-
-desktop/views/components/settings.2fa.vue:
-  intro: "二段階認証を設定すると、サインイン時にパスワードだけでなく、予め登録しておいた物理的なデバイス(例えばあなたのスマートフォンなど)も必要になり、よりセキュリティが向上します。"
-  detail: "詳細..."
-  url: "https://www.google.co.jp/intl/ja/landing/2step/"
-  caution: "登録したデバイスを紛失するなどした場合、Misskeyにサインインできなくなりますのでご注意ください。"
-  register: "デバイスを登録する"
-  already-registered: "既に設定は完了しています。"
-  unregister: "設定を解除"
-  unregistered: "二段階認証が無効になりました。"
-  enter-password: "パスワードを入力してください"
-  authenticator: "まず、Google Authenticatorをお使いのデバイスにインストールします:"
-  howtoinstall: "インストール方法はこちら"
-  token: "トークン"
-  scan: "次に、表示されているQRコードをスキャンします:"
-  done: "お使いのデバイスに表示されているトークンを入力して完了します:"
-  submit: "完了"
-  success: "設定が完了しました!"
-  failed: "設定に失敗しました。トークンに誤りがないかご確認ください。"
-  info: "次回サインインからは、同様にパスワードに加えてデバイスに表示されているトークンを入力します。"
-  totp-header: "認証アプリ"
-  security-key-header: "セキュリティキー"
-  security-key: "セキュリティを強化するために、FIDO2をサポートするハードウェアセキュリティキーを使用してアカウントにログインできます。 サインインの際は、登録されたセキュリティキーまたは認証アプリが必要になります。"
-  last-used: "最後の使用:"
-  activate-key: "クリックしてセキュリティキーをアクティベートしてください"
-  security-key-name: "キー名"
-  register-security-key: "キーの登録を完了"
-  something-went-wrong: "わー! キーを登録する際に問題が発生しました:"
-  key-unregistered: "キーが削除されました"
-  use-password-less-login: "パスワードなしのログインを使用"
-
-common/views/components/media-image.vue:
-  sensitive: "閲覧注意"
-  click-to-show: "クリックして表示"
-
-common/views/components/api-settings.vue:
-  intro: "APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。"
-  caution: "アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。"
-  regeneration-of-token: "万が一このトークンが漏れたりその可能性がある場合はトークンを再生成できます。"
-  regenerate-token: "トークンを再生成"
-  token: "Token:"
-  enter-password: "パスワードを入力してください"
-  console:
-    title: "APIコンソール"
-    endpoint: "エンドポイント"
-    parameter: "パラメータ"
-    credential-info: "「i」パラメータは自動で付与されます。"
-    send: "送信"
-    sending: "応答待ち"
-    response: "結果"
-
-desktop/views/components/settings.apps.vue:
-  no-apps: "連携しているアプリケーションはありません"
-
-common/views/components/drive-settings.vue:
-  max: "容量"
-  in-use: "使用中"
-  stats: "統計"
-  default-upload-folder: "既定のアップロード先フォルダ"
-  default-upload-folder-name: "フォルダ"
-  change-default-upload-folder: "フォルダを変更"
-
-common/views/components/mute-and-block.vue:
-  mute-and-block: "ミュートとブロック"
-  mute: "ミュート"
-  block: "ブロック"
-  no-muted-users: "ミュートしているユーザーはいません"
-  no-blocked-users: "ブロックしているユーザーはいません"
-  word-mute: "ワードミュート"
-  muted-words: "ミュートされたキーワード"
-  muted-words-description: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
-  unmute-confirm: "このユーザーをミュート解除しますか?"
-  unblock-confirm: "このユーザーをブロック解除しますか?"
-  save: "保存"
-
-common/views/components/password-settings.vue:
-  reset: "パスワードを変更する"
-  enter-current-password: "現在のパスワードを入力してください"
-  enter-new-password: "新しいパスワードを入力してください"
-  enter-new-password-again: "もう一度新しいパスワードを入力してください"
-  not-match: "新しいパスワードが一致しません"
-  changed: "パスワードを変更しました"
-  failed: "パスワード変更に失敗しました"
-
-common/views/components/post-form-attaches.vue:
-  attach-cancel: "添付取り消し"
-  mark-as-sensitive: "閲覧注意に設定"
-  unmark-as-sensitive: "閲覧注意を解除"
-
-desktop/views/components/sub-note-content.vue:
-  private: "この投稿は非公開です"
-  deleted: "この投稿は削除されました"
-  media-count: "{}つのメディア"
-  poll: "アンケート"
-
-desktop/views/components/settings.tags.vue:
-  title: "ã‚¿ã‚°"
-  query: "クエリ (省略可)"
-  add: "追加"
-  save: "保存"
-
-desktop/views/components/timeline.vue:
-  home: "ホーム"
-  local: "ローカル"
-  hybrid: "ソーシャル"
-  global: "グローバル"
-  mentions: "あなた宛て"
-  messages: "ダイレクト投稿"
-  list: "リスト"
-  hashtag: "ハッシュタグ"
-  add-tag-timeline: "ハッシュタグを追加"
-  add-list: "リストを追加"
-  list-name: "リスト名"
-
-desktop/views/components/ui.header.vue:
-  welcome-back: "おかえりなさい、"
-  adjective: "さん"
-
-desktop/views/components/ui.header.account.vue:
-  profile: "プロフィール"
-  lists: "リスト"
-  groups: "グループ"
-  follow-requests: "フォロー申請"
-  admin: "管理"
-  room: "ルーム"
-
-desktop/views/components/ui.header.nav.vue:
-  game: "ゲーム"
-
-desktop/views/components/ui.header.notifications.vue:
-  title: "通知"
-
-desktop/views/components/ui.header.post.vue:
-  post: "新規投稿"
-
-desktop/views/components/ui.header.search.vue:
-  placeholder: "検索"
-
-desktop/views/components/user-preview.vue:
-  notes: "投稿"
-  following: "フォロー"
-  followers: "フォロワー"
-
-desktop/views/components/users-list.vue:
-  all: "すべて"
-  iknow: "知り合い"
-  fetching: "読み込んでいます"
-
-desktop/views/components/users-list-item.vue:
-  followed: "フォローされています"
-
-desktop/views/components/window.vue:
-  popout: "ポップアウト"
-  close: "閉じる"
-
-admin/views/index.vue:
-  dashboard: "ダッシュボード"
-  instance: "インスタンス"
-  emoji: "カスタム絵文字"
-  moderators: "モデレーター"
-  users: "ユーザー"
-  federation: "連合"
-  announcements: "お知らせ"
-  abuse: "スパム報告"
-  queue: "ジョブキュー"
-  logs: "ログ"
-  db: "データベース"
-  back-to-misskey: "Misskeyに戻る"
-
-admin/views/db.vue:
-  tables: "テーブル"
-  vacuum: "バキューム"
-  vacuum-info: "データベースの掃除を行います。データはそのままで、ディスク使用量を減らします。通常この操作は自動で定期的に行われます。"
-  vacuum-exclamation: "バキュームを行うと、しばらくの間データベースの負荷が高くなり、ユーザーの操作を受け付けなくなる場合があります。"
-
-admin/views/dashboard.vue:
-  dashboard: "ダッシュボード"
-  accounts: "アカウント"
-  notes: "投稿"
-  drive: "ドライブ"
-  instances: "インスタンス"
-  this-instance: "このインスタンス"
-  federated: "連合"
-
-admin/views/queue.vue:
-  title: "キュー"
-  remove-all-jobs: "すべてのジョブをクリア"
-  jobs: "ジョブ"
-  queue: "キュー"
-  domains:
-    deliver: "配送"
-    inbox: "受信"
-    db: "データベース"
-    objectStorage: "オブジェクトストレージ"
-  state: "状態"
-  states:
-    active: "処理中"
-    delayed: "予約済み"
-    waiting: "順番待ち"
-  result-is-truncated: "結果は省略されています"
-  other-queues: "その他のキュー"
-
-admin/views/logs.vue:
-  logs: "ログ"
-  domain: "ドメイン"
-  level: "レベル"
-  levels:
-    all: "全て"
-    info: "情報"
-    success: "成功"
-    warning: "警告"
-    error: "エラー"
-    debug: "デバッグ"
-  delete-all: "全て削除"
-
-admin/views/abuse.vue:
-  title: "スパム報告"
-  target: "対象"
-  reporter: "報告者"
-  details: "詳細"
-  remove-report: "削除"
-
-admin/views/instance.vue:
-  instance: "インスタンス"
-  instance-name: "インスタンス名"
-  instance-description: "インスタンスの紹介"
-  host: "ホスト"
-  icon-url: "アイコンURL"
-  logo-url: "ロゴURL"
-  banner-url: "バナー画像URL"
-  error-image-url: "エラー画像URL"
-  languages: "インスタンスの対象言語"
-  languages-desc: "スペースで区切って複数設定できます。"
-  tos-url: "利用規約URL"
-  repository-url: "リポジトリURL"
-  feedback-url: "フィードバックURL"
-  maintainer-config: "管理者情報"
-  maintainer-name: "管理者名"
-  maintainer-email: "管理者の連絡先"
-  advanced-config: "その他の設定"
-  note-and-tl: "投稿とタイムライン"
-  drive-config: "ドライブの設定"
-  use-object-storage: "オブジェクトストレージを使用する"
-  object-storage-base-url: "URL"
-  object-storage-bucket: "バケット名"
-  object-storage-prefix: "プレフィックス"
-  object-storage-endpoint: "エンドポイント"
-  object-storage-region: "リージョン"
-  object-storage-port: "ポート"
-  object-storage-access-key: "アクセスキー"
-  object-storage-secret-key: "シークレットキー"
-  object-storage-use-ssl: "SSLを使用"
-  object-storage-s3-info: "Amazon S3をオブジェクトストレージとして使用する場合の「エンドポイント」と「リージョン」の設定については{0}をご確認ください。"
-  object-storage-s3-info-here: "こちら"
-  object-storage-gcs-info: "Google Cloud Storageをオブジェクトストレージとして使用する場合、「エンドポイント」は storage.googleapis.com に設定し、「リージョン」は空欄にします。"
-  cache-remote-files: "リモートのファイルをキャッシュする"
-  cache-remote-files-desc: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。そのためサーバーのストレージを節約できますが、プライバシー設定で直リンクを無効にしているユーザーにはファイルが見えなくなったり、サムネイルが生成されないので通信量が増加します。通常はこの設定をオンにするか次のリモートファイルのプロキシを有効にすることをおすすめします。"
-  proxy-remote-files: "リモートのファイルをプロキシする"
-  proxy-remote-files-desc: "この設定を有効にすると、未保存または保存容量超過で削除されたリモートファイルをローカルでプロキシし、サムネイルも生成するようになります。"
-  local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量"
-  remote-drive-capacity-mb: "リモートユーザーひとりあたりのドライブ容量"
-  mb: "メガバイト単位"
-  recaptcha-config: "reCAPTCHAの設定"
-  recaptcha-info: "reCAPTCHAを有効にする場合、reCAPTCHAトークンを取得する必要があります。https://www.google.com/recaptcha/intro/ にアクセスしてトークンを取得してください。"
-  recaptcha-info2: "v3は非対応です。v2を使用してください。"
-  enable-recaptcha: "reCAPTCHAを有効にする"
-  recaptcha-site-key: "サイトキー"
-  recaptcha-secret-key: "シークレットキー"
-  recaptcha-preview: "プレビュー"
-  hidden-tags: "非表示ハッシュタグ"
-  hidden-tags-info: "集計から除外するハッシュタグを改行で区切って記述します。"
-  external-service-integration-config: "外部サービス連携"
-  twitter-integration-config: "Twitter連携の設定"
-  twitter-integration-info: "コールバックURLは {url} に設定します。"
-  enable-twitter-integration: "Twitter連携を有効にする"
-  twitter-integration-consumer-key: "Consumer key"
-  twitter-integration-consumer-secret: "Consumer secret"
-  github-integration-config: "GitHub連携の設定"
-  github-integration-info: "コールバックURLは {url} に設定します。"
-  enable-github-integration: "GitHub連携を有効にする"
-  github-integration-client-id: "Client ID"
-  github-integration-client-secret: "Client Secret"
-  discord-integration-config: "Discord連携の設定"
-  discord-integration-info: "コールバックURLは {url} に設定します。"
-  enable-discord-integration: "Discord連携を有効にする"
-  discord-integration-client-id: "Client ID"
-  discord-integration-client-secret: "Client Secret"
-  proxy-account-config: "プロキシアカウントの設定"
-  proxy-account-info: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。"
-  proxy-account-username: "プロキシアカウントのユーザー名"
-  proxy-account-username-desc: "プロキシとして使用するアカウントのユーザー名を指定してください。"
-  proxy-account-warn: "アカウントは自動で作られないため、そのユーザー名のアカウントを予め作成しておく必要があります。"
-  max-note-text-length: "投稿の最大文字数"
-  disable-registration: "ユーザー登録の受付を停止する"
-  disable-local-timeline: "ローカルタイムラインを無効にする"
-  disable-global-timeline: "グローバルタイムラインを無効にする"
-  disabling-timelines-info: "これらのタイムラインを無効にしても、管理者およびモデレーターは引き続き利用できます。"
-  enable-emoji-reaction: "リアクションに絵文字を使えるようにする"
-  use-star-for-reaction-fallback: "不明なリアクションのフォールバックに star を使う"
-  invite: "招待"
-  save: "保存"
-  saved: "保存しました"
-  pinned-users: "ピン留めユーザー"
-  pinned-users-info: "ピン留めしたいユーザーを改行で区切って記述します。"
-  email-config: "メールサーバーの設定"
-  email-config-info: "メールアドレス確認やパスワードリセットの際に使われます。"
-  enable-email: "メール配信を有効にする"
-  email: "メールアドレス"
-  smtp-secure: "SMTP接続に暗黙的なSSL/TLSを使用する"
-  smtp-secure-info: "STARTTLS使用時はオフにします。"
-  smtp-host: "SMTPホスト"
-  smtp-port: "SMTPポート"
-  smtp-auth: "SMTP認証を行う"
-  smtp-user: "SMTPユーザー"
-  smtp-pass: "SMTPパスワード"
-  test-email: "テスト"
-  serviceworker-config: "ServiceWorker"
-  enable-serviceworker: "ServiceWorkerを有効にする"
-  serviceworker-info: "プッシュ通知を行うには有効する必要があります。"
-  vapid-publickey: "VAPID公開鍵"
-  vapid-privatekey: "VAPID秘密鍵"
-  vapid-info: "ServiceWorkerを有効にする場合、VAPIDキーペアを生成する必要があります。シェルで次のようにします:"
-
-admin/views/charts.vue:
-  title: "チャート"
-  per-day: "1日ごと"
-  per-hour: "1時間ごと"
-  federation: "フェデレーション"
-  notes: "投稿"
-  users: "ユーザー"
-  drive: "ドライブ"
-  network: "ネットワーク"
-  charts:
-    federation-instances: "インスタンスの増減"
-    federation-instances-total: "インスタンスの積算"
-    notes: "投稿の増減 (統合)"
-    local-notes: "投稿の増減 (ローカル)"
-    remote-notes: "投稿の増減 (リモート)"
-    notes-total: "投稿の積算"
-    users: "ユーザーの増減"
-    users-total: "ユーザーの積算"
-    active-users: "アクティブユーザー数"
-    drive: "ドライブ使用量の増減"
-    drive-total: "ドライブ使用量の積算"
-    drive-files: "ドライブのファイル数の増減"
-    drive-files-total: "ドライブのファイル数の積算"
-    network-requests: "リクエスト"
-    network-time: "応答時間"
-    network-usage: "通信量"
-
-admin/views/drive.vue:
-  operation: "操作"
-  fileid-or-url: "ファイルIDまたはファイルURL"
-  file-not-found: "ファイルが見つかりません"
-  lookup: "照会"
-  sort:
-    title: "ソート"
-    createdAtAsc: "アップロード日時が古い順"
-    createdAtDesc: "アップロード日時が新しい順"
-    sizeAsc: "サイズが小さい順"
-    sizeDesc: "サイズが大きい順"
-  origin:
-    title: "オリジン"
-    combined: "ローカル+リモート"
-    local: "ローカル"
-    remote: "リモート"
-  delete: "削除"
-  deleted: "削除しました"
-  mark-as-sensitive: "閲覧注意に設定"
-  unmark-as-sensitive: "閲覧注意を解除"
-  marked-as-sensitive: "閲覧注意に設定しました"
-  unmarked-as-sensitive: "閲覧注意を解除しました"
-  clean-remote-files: "リモートファイルのキャッシュを削除"
-  clean-remote-files-are-you-sure: "すべてのリモートファイルのキャッシュを削除してもよろしいですか?"
-  clean-up: "クリーンアップ"
-
-admin/views/users.vue:
-  operation: "操作"
-  username-or-userid: "ユーザー名またはユーザーID"
-  user-not-found: "ユーザーが見つかりません"
-  lookup: "照会"
-  reset-password: "パスワードをリセット"
-  reset-password-confirm: "パスワードをリセットしますか?"
-  password-updated: "パスワードは現在「{password}」です"
-  suspend: "凍結"
-  suspend-confirm: "凍結しますか?"
-  suspended: "凍結しました"
-  unsuspend: "凍結の解除"
-  unsuspend-confirm: "凍結を解除しますか?"
-  unsuspended: "凍結を解除しました"
-  make-silence: "サイレンス"
-  silence-confirm: "サイレンスしますか?"
-  unmake-silence: "サイレンスの解除"
-  unsilence-confirm: "サイレンスを解除しますか?"
-  update-remote-user: "リモートユーザー情報の更新"
-  remote-user-updated: "リモートユーザー情報を更新しました"
-  delete-all-files: "すべてのファイルを削除"
-  delete-all-files-confirm: "すべてのファイルを削除しますか?"
   username: "ユーザー名"
-  host: "ホスト"
-  users:
-    title: "ユーザー"
-    sort:
-      title: "ソート"
-      createdAtAsc: "登録日時が古い順"
-      createdAtDesc: "登録日時が新しい順"
-      updatedAtAsc: "更新日時が古い順"
-      updatedAtDesc: "更新日時が新しい順"
-    state:
-      title: "状態"
-      all: "すべて"
-      available: "利用可能"
-      admin: "管理者"
-      moderator: "モデレーター"
-      adminOrModerator: "管理者+モデレーター"
-      silenced: "サイレンス済み"
-      suspended: "凍結済み"
-    origin:
-      title: "オリジン"
-      combined: "ローカル+リモート"
-      local: "ローカル"
-      remote: "リモート"
-    createdAt: "登録日時"
-    updatedAt: "更新日時"
-
-admin/views/moderators.vue:
-  add-moderator:
-    title: "モデレーターの登録"
-    add: "登録"
-    added: "モデレーターを登録しました"
-    remove: "解除"
-    removed: "モデレーター登録を解除しました"
-  logs:
-    title: "ログ"
-    moderator: "モデレーター"
-    type: "操作"
-    at: "日時"
-    info: "情報"
-
-admin/views/emoji.vue:
-  add-emoji:
-    title: "絵文字の登録"
-    name: "絵文字名"
-    name-desc: "a~z 0~9 _ の文字が使えます。"
-    category: "カテゴリ"
-    aliases: "エイリアス"
-    aliases-desc: "スペースで区切って複数設定できます。"
-    url: "絵文字画像URL"
-    add: "追加"
-    info: "50KB以下のPNG画像をおすすめします。"
-    added: "絵文字を登録しました"
-  emojis:
-    title: "絵文字一覧"
-    update: "æ›´æ–°"
-    remove: "削除"
-  updated: "更新しました"
-  remove-emoji:
-    are-you-sure: "「$1」を削除しますか?"
-    removed: "削除しました"
-
-admin/views/announcements.vue:
-  announcements: "お知らせ"
-  save: "保存"
-  remove: "削除"
-  add: "追加"
-  title: "タイトル"
-  text: "内容"
-  saved: "保存しました"
-  _remove:
-    are-you-sure: "「$1」を削除しますか?"
-    removed: "削除しました"
-
-admin/views/hashtags.vue:
-  hided-tags: "Hidden Tags"
-
-admin/views/federation.vue:
-  instance: "インスタンス"
-  host: "ホスト"
-  notes: "投稿"
-  users: "ユーザー"
-  following: "フォロー中"
-  followers: "フォロワー"
-  caught-at: "登録日時"
-  status: "ステータス"
-  latest-request-sent-at: "直近のリクエスト送信"
-  latest-request-received-at: "直近のリクエスト受信"
-  remove-all-following: "フォローを全解除"
-  remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
-  delete-all-files: "ファイルをすべて削除"
-  block: "ブロック"
-  marked-as-closed: "閉鎖されているとマーク"
-  lookup: "照会"
-  instances: "連合"
-  instance-not-registered: "そのインスタンスは登録されていません"
-  sort: "ソート"
-  sorts:
-    caughtAtAsc: "登録日時が古い順"
-    caughtAtDesc: "登録日時が新しい順"
-    lastCommunicatedAtAsc: "最後にやり取りした日時が古い順"
-    lastCommunicatedAtDesc: "最後にやり取りした日時が新しい順"
-    notesAsc: "投稿が少ない順"
-    notesDesc: "投稿が多い順"
-    usersAsc: "ユーザーが少ない順"
-    usersDesc: "ユーザーが多い順"
-    followingAsc: "フォローが少ない順"
-    followingDesc: "フォローが多い順"
-    followersAsc: "フォロワーが少ない順"
-    followersDesc: "フォロワーが多い順"
-    driveUsageAsc: "ドライブ使用量が少ない順"
-    driveUsageDesc: "ドライブ使用量が多い順"
-    driveFilesAsc: "ドライブのファイル数が少ない順"
-    driveFilesDesc: "ドライブのファイル数が多い順"
-  state: "状態"
-  states:
-    all: "すべて"
-    blocked: "ブロック"
-    not-responding: "応答なし"
-    marked-as-closed: "閉鎖とマーク済み"
-  result-is-truncated: "上位{n}件を表示しています。"
-  charts: "チャート"
-  chart-srcs:
-    requests: "リクエスト"
-    users: "ユーザーの増減"
-    users-total: "ユーザーの積算"
-    notes: "投稿の増減"
-    notes-total: "投稿の積算"
-    ff: "フォロー/フォロワーの増減"
-    ff-total: "フォロー/フォロワーの積算"
-    drive-usage: "ドライブ使用量の増減"
-    drive-usage-total: "ドライブ使用量の積算"
-    drive-files: "ドライブファイル数の増減"
-    drive-files-total: "ドライブファイル数の積算"
-  chart-spans:
-    hour: "1時間ごと"
-    day: "1日ごと"
-  blocked-hosts: "ブロック"
-  blocked-hosts-info: "ブロックしたいホストを改行で区切って記述します。"
-  save: "保存"
-
-desktop/views/pages/welcome.vue:
-  about: "詳しく..."
-  timeline: "タイムライン"
-  announcements: "お知らせ"
-  photos: "最近の画像"
-  powered-by-misskey: "Powered by <b>Misskey</b>."
-  info: "情報"
-
-desktop/views/pages/drive.vue:
-  title: "Misskey Drive"
-
-desktop/views/pages/note.vue:
-  prev: "前の投稿"
-  next: "次の投稿"
-
-desktop/views/pages/selectdrive.vue:
-  title: "ファイルを選択してください"
-  ok: "決定"
-  cancel: "キャンセル"
-  upload: "PCからドライブにファイルをアップロード"
-
-desktop/views/pages/search.vue:
-  not-available: "検索機能はインスタンスの設定で無効になっています。"
-  not-found: "「{q}」に関する投稿は見つかりませんでした。"
-
-desktop/views/pages/tag.vue:
-  no-posts-found: "ハッシュタグ「{q}」が付けられた投稿は見つかりませんでした。"
-
-desktop/views/pages/user-list.users.vue:
-  users: "ユーザー"
-  add-user: "ユーザーを追加"
-  username: "ユーザー名"
-
-desktop/views/pages/user/user.followers-you-know.vue:
-  title: "知り合いのフォロワー"
-  loading: "読み込み中"
-  no-users: "知り合いのフォロワーはいません"
-
-desktop/views/pages/user/user.friends.vue:
-  title: "よく話すユーザー"
-  loading: "読み込み中"
-  no-users: "よく話すユーザーはいません"
-
-desktop/views/pages/user/user.photos.vue:
-  title: "フォト"
-  loading: "読み込み中"
-  no-photos: "写真はありません"
-
-desktop/views/pages/user/user.header.vue:
-  posts: "投稿"
-  following: "フォロー"
-  followers: "フォロワー"
-  is-bot: "このアカウントはBotです"
-  no-description: "自己紹介はありません"
-  years-old: "{age}æ­³"
-  year: "å¹´"
-  month: "月"
-  day: "æ—¥"
-  follows-you: "フォローされています"
-
-desktop/views/pages/user/user.timeline.vue:
-  default: "投稿"
-  with-replies: "投稿と返信"
-  with-media: "メディア"
-  my-posts: "私の投稿"
-
-desktop/views/widgets/notifications.vue:
-  title: "通知"
-
-desktop/views/widgets/polls.vue:
-  title: "アンケート"
-  refresh: "他を見る"
-  nothing: "ありません!"
-
-desktop/views/widgets/post-form.vue:
-  title: "投稿"
-  note: "投稿"
-  something-happened: "何らかの事情で投稿できませんでした。"
-
-desktop/views/widgets/profile.vue:
-  update-banner: "クリックでバナー編集"
-  update-avatar: "クリックでアバター編集"
-
-desktop/views/widgets/trends.vue:
-  title: "トレンド"
-  refresh: "他を見る"
-  nothing: "ありません!"
-
-desktop/views/widgets/users.vue:
-  title: "おすすめユーザー"
-  refresh: "他を見る"
-  no-one: "いません!"
-
-mobile/views/components/drive.vue:
-  used: "使用中"
-  folder-count: "フォルダ"
-  count-separator: "、"
-  file-count: "ファイル"
-  nothing-in-drive: "ドライブには何もありません"
-  folder-is-empty: "このフォルダは空です"
-  folder-name: "フォルダー名"
-  here-is-root: "現在いる場所はルートで、フォルダではありません。"
-  url-prompt: "アップロードしたいファイルのURL"
-  uploading: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。"
-  folder-name-cannot-empty: "フォルダ名を空白にすることはできません。"
-
-mobile/views/components/drive-file-chooser.vue:
-  select-file: "ファイルを選択"
-
-mobile/views/components/drive-folder-chooser.vue:
-  select-folder: "フォルダーを選択"
-
-mobile/views/components/drive.file.vue:
-  nsfw: "閲覧注意"
-
-mobile/views/components/drive.file-detail.vue:
-  download: "ダウンロード"
-  rename: "名前を変更"
-  move: "移動"
-  hash: "ハッシュ (md5)"
-  exif: "EXIF"
-  nsfw: "閲覧注意"
-  mark-as-sensitive: "閲覧注意に設定"
-  unmark-as-sensitive: "閲覧注意を解除"
-
-mobile/views/components/media-video.vue:
-  sensitive: "閲覧注意"
-  click-to-show: "クリックして表示"
-
-common/views/components/follow-button.vue:
-  following: "フォロー中"
-  follow: "フォロー"
-  request-pending: "フォロー許可待ち"
-  follow-processing: "フォロー処理中"
-  follow-request: "フォロー申請"
-
-mobile/views/components/note.vue:
-  private: "この投稿は非公開です"
-  deleted: "この投稿は削除されました"
-  location: "位置情報"
-
-mobile/views/components/note-detail.vue:
-  reply: "返信"
-  reaction: "リアクション"
-  private: "この投稿は非公開です"
-  deleted: "この投稿は削除されました"
-  location: "位置情報"
-
-mobile/views/components/note-preview.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "cat"
-
-mobile/views/components/note-sub.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "cat"
-
-mobile/views/components/notifications.vue:
-  empty: "ありません!"
-
-mobile/views/components/sub-note-content.vue:
-  private: "この投稿は非公開です"
-  deleted: "この投稿は削除されました"
-  media-count: "{}つのメディア"
-  poll: "アンケート"
-
-mobile/views/components/ui.header.vue:
-  welcome-back: "おかえりなさい、"
-  adjective: "さん"
-
-mobile/views/components/ui.nav.vue:
-  timeline: "タイムライン"
-  notifications: "通知"
-  follow-requests: "フォロー申請"
-  search: "検索"
-  user-lists: "リスト"
-  user-groups: "グループ"
-  widgets: "ウィジェット"
-  game: "ゲーム"
-  admin: "管理"
-  about: "Misskeyについて"
-
-mobile/views/pages/drive.vue:
-  contextmenu:
-    upload: "ファイルをアップロード"
-    url-upload: "ファイルをURLでアップロード"
-    create-folder: "フォルダーを作成"
-    rename-folder: "フォルダー名を変更"
-    move-folder: "このフォルダを移動"
-    delete-folder: "このフォルダを削除"
-
-mobile/views/pages/signup.vue:
-  lets-start: "📦 始めましょう"
-
-mobile/views/pages/followers.vue:
-  followers-of: "{name}のフォロワー"
-
-mobile/views/pages/following.vue:
-  following-of: "{name}のフォロー"
-
-mobile/views/pages/home.vue:
-  home: "ホーム"
-  local: "ローカル"
-  hybrid: "ソーシャル"
-  global: "グローバル"
-  mentions: "あなた宛て"
-  messages: "ダイレクト投稿"
-
-mobile/views/pages/tag.vue:
-  no-posts-found: "ハッシュタグ「{q}」が付けられた投稿は見つかりませんでした。"
-
-mobile/views/pages/widgets.vue:
-  dashboard: "ダッシュボード"
-  widgets-hints: "ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。"
-  add-widget: "追加"
-  customization-tips: "カスタマイズのヒント"
-
-mobile/views/pages/widgets/activity.vue:
-  activity: "アクティビティ"
-
-mobile/views/pages/share.vue:
-  share-with: "{name}で共有"
-
-mobile/views/pages/note.vue:
-  title: "投稿"
-  prev: "前の投稿"
-  next: "次の投稿"
-
-mobile/views/pages/games/reversi.vue:
-  reversi: "リバーシ"
-
-mobile/views/pages/search.vue:
-  search: "検索"
-  not-found: "「{q}」に関する投稿は見つかりませんでした。"
-
-mobile/views/pages/selectdrive.vue:
-  select-file: "ファイルを選択"
-
-mobile/views/pages/notifications.vue:
-  notifications: "通知"
-
-mobile/views/pages/settings.vue:
-  signed-in-as: "{}としてサインイン中"
-
-mobile/views/pages/user.vue:
-  follows-you: "フォローされています"
-  following: "フォロー"
-  followers: "フォロワー"
-  notes: "投稿"
-  overview: "概要"
-  timeline: "タイムライン"
-  media: "メディア"
-  years-old: "{age}æ­³"
-
-mobile/views/pages/user/home.vue:
-  recent-notes: "最近の投稿"
-  images: "画像"
-  activity: "アクティビティ"
-  keywords: "キーワード"
-  domains: "頻出ドメイン"
-  frequently-replied-users: "よく話すユーザー"
-  followers-you-know: "知り合いのフォロワー"
-  last-used-at: "最終ログイン"
-
-mobile/views/pages/user/home.photos.vue:
-  no-photos: "写真はありません"
-
-deck:
-  widgets: "ウィジェット"
+  description: "自己紹介"
+  youCanIncludeHashtags: "ハッシュタグを含めることができます。"
+  metadata: "補足情報"
+  metadataLabel: "ラベル"
+  metadataContent: "内容"
+
+_exportOrImport:
+  allNotes: "全ての投稿"
+  followingList: "フォロー"
+  muteList: "ミュート"
+  blockingList: "ブロック"
+  userLists: "リスト"
+
+_charts:
+  federationInstancesIncDec: "連合の増減"
+  federationInstancesTotal: "連合の合計"
+  usersIncDec: "ユーザーの増減"
+  usersTotal: "ユーザーの合計"
+  activeUsers: "アクティブユーザー数"
+  notesIncDec: "投稿の増減"
+  localNotesIncDec: "ローカルの投稿の増減"
+  remoteNotesIncDec: "リモートの投稿の増減"
+  notesTotal: "投稿の合計"
+  filesIncDec: "ファイルの増減"
+  filesTotal: "ファイルの合計"
+  storageUsageIncDec: "ストレージ使用量の増減"
+  storageUsageTotal: "ストレージ使用量の合計"
+
+_instanceCharts:
+  requests: "リクエスト"
+  users: "ユーザーの増減"
+  usersTotal: "ユーザーの積算"
+  notes: "投稿の増減"
+  notesTotal: "投稿の積算"
+  ff: "フォロー/フォロワーの増減"
+  ffTotal: "フォロー/フォロワーの積算"
+  cacheSize: "キャッシュサイズの増減"
+  cacheSizeTotal: "キャッシュサイズの積算"
+  files: "ファイル数の増減"
+  filesTotal: "ファイル数の積算"
+
+_timelines:
   home: "ホーム"
   local: "ローカル"
-  hybrid: "ソーシャル"
-  hashtag: "ハッシュタグ"
+  social: "ソーシャル"
   global: "グローバル"
-  mentions: "あなた宛て"
-  direct: "ダイレクト投稿"
-  notifications: "通知"
-  list: "リスト"
-  select-list: "リストを選択してください"
-  swap-left: "左に移動"
-  swap-right: "右に移動"
-  swap-up: "上に移動"
-  swap-down: "下に移動"
-  remove: "カラムを削除"
-  add-column: "カラムを追加"
-  rename: "名前を変更"
-  stack-left: "左に重ねる"
-  pop-right: "右に出す"
-  disabled-timeline:
-    title: "無効化されたタイムライン"
-    description: "サーバーの運営者により、このタイムラインは使用できない状態に設定されています。"
 
-deck/deck.tl-column.vue:
-  is-media-only: "メディア投稿のみ"
-  edit: "オプション"
-
-deck/deck.user-column.vue:
-  follows-you: "フォローされています"
-  posts: "投稿"
-  following: "フォロー"
-  followers: "フォロワー"
-  images: "画像"
-  activity: "アクティビティ"
-  timeline: "タイムライン"
-  pinned-notes: "ピン留めされた投稿"
-  pinned-page: "ピン留めされたページ"
-
-docs:
-  edit-this-page-on-github: "間違いや改善点を見つけましたか?"
-  edit-this-page-on-github-link: "このページをGitHubで編集"
-
-dev/views/index.vue:
-  manage-apps: "アプリの管理"
-
-dev/views/apps.vue:
-  manage-apps: "アプリを管理"
-  create-app: "アプリ作成"
-  app-missing: "アプリなし"
-
-dev/views/new-app.vue:
-  new-app: "新しいアプリケーション"
-  new-app-info: "アプリケーションはAPIからでも作成できます。 (app/create)"
-  create-app: "アプリケーションの作成"
-  app-name: "アプリケーション名"
-  app-name-placeholder: "ex) Misskey for iOS"
-  app-name-desc: "あなたのアプリの名称。"
-  app-overview: "アプリの概要"
-  app-overview-placeholder: " ex) Misskey iOSクライアント。"
-  app-overview-desc: "あなたのアプリの簡単な説明や紹介。"
-  callback-url: "コールバックURL (オプション)"
-  callback-url-placeholder: "ex) https://your.app.example.com/callback.php"
-  callback-url-desc: "ユーザーが認証フォームで認証した際にリダイレクトするURLを設定できます。"
-  authority: "権限"
-  authority-desc: "ここで要求した機能だけがAPIからアクセスできます。"
-  authority-warning: "アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。"
-
-pages:
+_pages:
   new-page: "ページの作成"
   edit-page: "ページの編集"
   read-page: "ソースを表示中"
@@ -2185,23 +645,23 @@ pages:
       _join:
         arg1: "リスト"
         arg2: "区切り"
-      add: "+ 足す"
+      add: "足す"
       _add:
         arg1: "A"
         arg2: "B"
-      subtract: "- 引く"
+      subtract: "引く"
       _subtract:
         arg1: "A"
         arg2: "B"
-      multiply: "× 掛ける"
+      multiply: "掛ける"
       _multiply:
         arg1: "A"
         arg2: "B"
-      divide: "÷ 割る"
+      divide: "割る"
       _divide:
         arg1: "A"
         arg2: "B"
-      mod: "÷ 割った余り"
+      mod: "割った余り"
       _mod:
         arg1: "A"
         arg2: "B"
@@ -2323,65 +783,3 @@ pages:
     enviromentVariables: "環境変数"
     pageVariables: "ページ要素"
     argVariables: "入力スロット"
-
-room:
-  add-furniture: "家具を置く"
-  translate: "移動"
-  rotate: "回転"
-  exit: "戻る"
-  remove: "しまう"
-  save: "保存"
-  saved: "保存しました"
-  clear: "片付け"
-  clear-confirm: "全ての家具をしまいますか?"
-  leave-confirm: "未保存の変更があります、移動しますか?"
-  chooseImage: "画像を選択"
-  room-type: "部屋のタイプ"
-  carpet-color: "床の色"
-  rooms:
-    default: "デフォルト"
-    washitsu: "和室"
-  furnitures:
-    milk: "牛乳パック"
-    bed: "ベッド"
-    low-table: "ローテーブル"
-    desk: "デスク"
-    chair: "チェア"
-    chair2: "チェア2"
-    fan: "換気扇"
-    pc: "パソコン"
-    plant: "観葉植物"
-    plant2: "観葉植物2"
-    eraser: "消しゴム"
-    pencil: "鉛筆"
-    pudding: "プリン"
-    cardboard-box: "段ボール箱"
-    cardboard-box2: "段ボール箱2"
-    cardboard-box3: "段ボール箱3"
-    book: "本"
-    book2: "本2"
-    piano: "ピアノ"
-    facial-tissue: "ティッシュボックス"
-    server: "サーバー"
-    moon: "月"
-    corkboard: "コルクボード"
-    mousepad: "マウスパッド"
-    monitor: "モニター"
-    keyboard: "キーボード"
-    carpet-stripe: "カーペット(縞)"
-    mat: "マット"
-    color-box: "カラーボックス"
-    wall-clock: "壁掛け時計"
-    photoframe: "額縁"
-    cube: "キューブ"
-    tv: "テレビ"
-    pinguin: "ピンギン"
-    rubik-cube: "ルービックキューブ"
-    poster-h: "ポスター(横長)"
-    poster-v: "ポスター(縦長)"
-    sofa: "ソファ"
-    spiral: "螺旋階段"
-    bin: "ゴミ箱"
-    cup-noodle: "カップ麺"
-    holo-display: "ホログラフィックディスプレイ"
-    energy-drink: "エナジードリンク"
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
deleted file mode 100644
index c2a430e6303df750586cbc9f6801dfe8ea7f8df6..0000000000000000000000000000000000000000
--- a/locales/ja-KS.yml
+++ /dev/null
@@ -1,1291 +0,0 @@
----
-meta:
-  lang: "日本語 (関西弁)"
-common:
-  misskey: "A ⭐ of fediverse"
-  about-title: "A ⭐ of fediverse."
-  about: "ようMisskeyを見つけてくれて、おおきにやで。Misskeyは、地球で生まれた<b>分散マイクロブログSNS</b>やねん。Fediverse(ぎょうさんのSNSで構成されとる宇宙)っちゅうもんの中におるから、お隣さんのSNSとも仲良うさせてもろてんねん。ちょいとやかましい心斎橋から離れて、新しいインターネットにダイブしてみぃひん?"
-  intro:
-    title: "Misskeyってなんやねん"
-    about: "Misskeyってのはな、オープンソースの<b>分散型マイクロブログSNS</b>のことや。ごっついええ感じにできるUIやったり、投稿へのリアクションやったり、ファイルをまとめとけるドライブやったり、いろんな機能が目白押しや。Fediverseに対応しとるから、よそのSNSともノリツッコミできるんやで。タイガースが東京ドームに野球しに行くようなもんや。"
-    features: "ええとこ"
-    rich-contents: "投稿"
-    rich-contents-desc: "思っとること、タイガースの実況、他に言いたいことがあればなんでも言ってええで。いろんな構文あるから、好きにつこうてくれや。画像や動画、アンケートも添付できるで。"
-    reaction: "リアクション"
-    reaction-desc: "「何思っとるか言うてみ?」言われても、わからんわ!リアクション使うて、エモーションをダイレクトに伝えるんや!Misskeyはな、他のユーザーの投稿にいろんなリアクション付けられるんや。もう「いいね」とかいうもんだけのSNSには戻れへんわな。551の豚まん食うてみ?もう他の豚まん食えへんで?"
-    ui: "インターフェイス"
-    ui-desc: "このUIええ言うてたで、知らんけど。あんたの好みのUIなんて知ったこっちゃない。Misskeyは好きにいじれるからな、レイアウトやデザイン変えたり、色んなウィジェットひっつけたりして、あんただけのMisskey作って楽しんでな!"
-    drive: "ドライブ"
-    drive-desc: "「こないだの画像、どこやったかな……また投稿したいんやけど……」「さっきのファイルあのフォルダに直しといて」そんなこと言わんとって。Misskeyはもとからドライブ機能持っとるさかい、心配あらへん。ファイルの「わけわけ」したってな。"
-    outro: "Misskeyの機能は無限大や!知らんけど。知らん言うとるやんけ、あんたが見に行けや!Misskeyは分散型SNSやから、ここがあかんくても他がある。阪神でもオリックスでもワイは応援するで!"
-  application-authorization: "アプリの連携"
-  close: "さいなら"
-  do-not-copy-paste: "ここにコードを入力したり張り付けたりせんといてください。アカウントが不正利用されるかも分からん。知らんけど。"
-  load-more: "もっとあらへんのか!"
-  enter-password: "パスワードを入れてや"
-  2fa: "二段階認証"
-  delete-confirm: "この投稿を削除してもええか?"
-  notification-types:
-    all: "すべて"
-    follow: "フォロー"
-    reply: "返す"
-    renote: "Renote"
-    reaction: "リアクション"
-  got-it: "ほい"
-  customization-tips:
-    title: "カスタマイズのヒント"
-    paragraph: "<p>ホームのカスタマイズは、ウィジェットを増やしたりほかしたり、ドラッグ&ドロップして並び替えたりしていじれるで。</p><p>一部ウィジェットは<strong><strong>右</strong>クリック</strong>で表示もいじれるんや。</p><p>ほかしたいときはヘッダーの<strong>「ゴミ箱」</strong>にほうりこんでら。</p><p>「完了」押したらお終いやで。</p>"
-    gotit: "Got it!"
-  notification:
-    file-uploaded: "ファイルがアップロードされたで"
-    message-from: "{}はんからメッセージ:"
-    reversi-invited: "対局への招待がきとるで"
-    reversi-invited-by: "{}はんから"
-    notified-by: "{}はんから"
-    reply-from: "{}はんから返信:"
-    quoted-by: "{}はんが引用:"
-  time:
-    unknown: "なぞのじかん"
-    future: "未来"
-    just_now: "たった今"
-    seconds_ago: "{}秒前"
-    minutes_ago: "{}分前"
-    hours_ago: "{}時間前"
-    days_ago: "{}日前"
-    weeks_ago: "{}週間前"
-    months_ago: "{}ヶ月前"
-    years_ago: "{}年前"
-  month-and-day: "{month}月 {day}日"
-  trash: "ゴミ箱"
-  drive: "ドライブ"
-  messaging: "トーク"
-  home: "ホーム"
-  timeline: "タイムライン"
-  following: "フォローしとる"
-  followers: "フォロワー"
-  favorites: "お気に入り"
-  permissions:
-    "write:votes": "投票するで"
-  post-form:
-    submit: "投稿"
-    reply: "返す"
-    renote: "Renote"
-    error: "エラー"
-    enter-username: "ユーザー名を入力してや"
-    add-visible-user: "ユーザー増やす"
-    username-prompt: "ユーザー名を入力してや"
-  weekday-short:
-    sunday: "æ—¥"
-    monday: "月"
-    tuesday: "火"
-    wednesday: "æ°´"
-    thursday: "木"
-    friday: "金"
-    saturday: "土"
-  weekday:
-    sunday: "日曜日"
-    monday: "月曜日"
-    tuesday: "火曜日"
-    wednesday: "水曜日"
-    thursday: "木曜日"
-    friday: "金曜日"
-    saturday: "土曜日"
-  reactions:
-    like: "ええやん"
-    love: "好きやねん"
-    laugh: "わろた"
-    hmm: "ふぅ~む"
-    surprise: "わお"
-    congrats: "おめでとうさん"
-    angry: "何言うてまんねん"
-    confused: "こまこまのこまりやわぁ"
-    rip: "RIP"
-    pudding: "アメちゃんちゃうんちゃう?"
-  note-visibility:
-    public: "公開"
-    home: "ホーム"
-    home-desc: "ホームタイムライン以外に見せんとって"
-    followers: "フォロワー"
-    followers-desc: "自分のフォロワー以外に見せんとって"
-    specified: "ダイレクト"
-    specified-desc: "今から言うユーザー以外に見せんとってや"
-    local-public: "公開 (ローカルだけ)"
-    local-home: "ホーム (ローカルだけ)"
-    local-followers: "フォロワー (ローカルだけ)"
-  note-placeholders:
-    a: "今なにしてん?"
-    b: "何かあったんか?"
-    c: "何考えとりますん?"
-    d: "言うときたいことは?"
-    e: "ここに書いてや"
-    f: "あんさんが書くんを待っちょります..."
-  _settings:
-    profile: "プロフィール"
-    notification: "通知"
-    tags: "ハッシュタグ"
-    blocking: "ブロック"
-    password: "パスワード"
-    other: "その他"
-    reactions: "リアクション"
-    timeline: "タイムライン"
-    save: "保存"
-    saved: "保存したで!"
-    preview: "試してみる"
-  search: "検索"
-  delete: "削除"
-  loading: "読み込み中"
-  update-available-title: "更新があんで"
-  update-available: "Misskeyの新しいバージョンがあんで({newer}。現在{current}をつこてるわ)。ページを再度読み込みしたると更新が適用されるわ。"
-  my-token-regenerated: "あんさんのトークンが更新されたらしいわ。すまんがとりあえずサインアウトすんで。"
-  enter-username: "ユーザー名を入力してや"
-  do-not-use-in-production: "開発ビルドや。本番環境で使わんといて!知らんで!"
-  is-remote-post: "この投稿情報はコピーです。"
-  view-on-remote: "ちゃんとした情報見せてや!"
-  renoted-by: "{user}がRenote"
-  error:
-    title: "問題が起こったわ"
-    retry: "もっぺん"
-  reversi:
-    drawn: "おあいこ"
-    my-turn: "あんさんのターンや"
-    opponent-turn: "相手のターンや"
-    turn-of: "{name}のターンや"
-    past-turn-of: "{name}のターン"
-    won: "{name}の勝ちやで!"
-    black: "é»’"
-    white: "白"
-    total: "合計"
-    this-turn: "{count}ターン目"
-  widgets:
-    analog-clock: "アナログ時計"
-    profile: "プロフィール"
-    calendar: "カレンダー"
-    timemachine: "カレンダー(タイムマシン)"
-    activity: "アクティビティ"
-    rss: "RSSリーダー"
-    memo: "付箋"
-    trends: "トレンド"
-    photo-stream: "フォトストリーム"
-    posts-monitor: "投稿チャート"
-    slideshow: "スライドショー"
-    version: "バージョン"
-    broadcast: "ブロードキャスト"
-    notifications: "通知"
-    users: "おすすめユーザー"
-    polls: "アンケート"
-    post-form: "投稿フォーム"
-    server: "サーバー情報"
-    nav: "ナビゲーション"
-    tips: "ヒント"
-    hashtags: "ハッシュタグ"
-  dev: "アプリの作成あかんかったわ。もっぺんやってみて。"
-  ai-chan-kawaii: "藍ちゃめっさべっぴんさんや"
-  you: "あんさん"
-auth/views/form.vue:
-  share-access: "あんたのアカウントに<i>{name}</i>がアクセスしようとしてるで?ええか?"
-  permission-ask: "このアプリは次の権限を要求してんで:"
-  cancel: "やめとくわ"
-  accept: "アクセスを許可や!"
-auth/views/index.vue:
-  loading: "読み込み中"
-  denied: "アプリケーションの連携をやめといたわ。"
-  denied-paragraph: "このアプリがあんさんのアカウントにアクセスすることは多分あらへん。知らんけど。"
-  already-authorized: "このアプリはもう連携済みやったわ"
-  allowed: "アプリケーションの連携を許可したで"
-  callback-url: "アプリケーションに戻っとります"
-  please-go-back: "アプリケーションに戻って、気張ってってな。"
-  error: "セッションが存在してへん。"
-  sign-in: "サインインしてや"
-common/views/pages/explore.vue:
-  federated: "連合"
-common/views/components/games/reversi/reversi.vue:
-  matching:
-    waiting-for: "{}を待っとります"
-    cancel: "やめとくわ"
-common/views/components/games/reversi/reversi.game.vue:
-  surrender: "投了や..."
-  surrendered: "投了により"
-  is-llotheo: "石の少ない方が勝ち(ロセオ)"
-  looped-map: "ループマップ"
-  can-put-everywhere: "どこに置いてもええモード"
-common/views/components/games/reversi/reversi.index.vue:
-  title: "Misskey Reversi"
-  sub-title: "お隣のミスキストはんらとリバーシで対戦や!"
-  invite: "招待"
-  rule: "遊び方"
-  rule-desc: "リバーシは、相手と交互に石をボードに置いて、相手の石を挟んで自分の色に変えてって、最終的に残った石が多い方が勝ちっちゅうボードゲームや。"
-  mode-invite: "招待"
-  mode-invite-desc: "指定したユーザーと対戦するモードや。"
-  invitations: "対局の招待がきてんで!"
-  my-games: "自分の対局"
-  all-games: "みんなの対局"
-  enter-username: "ユーザー名を入力してや"
-  game-state:
-    ended: "終了"
-    playing: "進行中"
-common/views/components/games/reversi/reversi.room.vue:
-  settings-of-the-game: "ゲームの設定"
-  choose-map: "マップを選択"
-  random: "いんじゃんほい"
-  black-or-white: "先手/後手"
-  black-is: "{}が黒や"
-  rules: "ルール"
-  is-llotheo: "石の少ない方が勝ちや!(ロセオ)"
-  looped-map: "ループマップ"
-  can-put-everywhere: "どこに置いてもええモード"
-  settings-of-the-bot: "Botの設定"
-  this-game-is-started-soon: "ゲームは数秒後に開始されんで"
-  waiting-for-other: "相手の準備が完了すんのを待ってんで"
-  waiting-for-me: "あんさんの準備が完了すんのを待ってんで"
-  waiting-for-both: "準備中"
-  cancel: "やめとくわ"
-  ready: "準備完了"
-  cancel-ready: "準備続行"
-common/views/components/connect-failed.vue:
-  title: "サーバーに接続でけへんわ"
-  description: "インターネット回線に問題が起きとるか、サーバーがダウンまたはメンテナンスしとるっぽいわ。知らんけど。とりあえずあとで{再試行}してや。"
-  thanks: "いつもMisskeyをつこてくれてほんまありがとな。"
-  troubleshoot: "トラブルシュート"
-common/views/components/connect-failed.troubleshooter.vue:
-  title: "トラブルシューティング"
-  network: "ネットワーク接続"
-  checking-network: "ネットワーク接続を確認中"
-  internet: "インターネット接続"
-  checking-internet: "インターネット接続を確認中"
-  server: "サーバー接続"
-  checking-server: "サーバー接続を確認中"
-  finding: "問題を調べとるで"
-  no-network: "ネットワークに接続されとらんで"
-  no-network-desc: "つこてるPCのネットワーク接続が正常か確認してや。"
-  no-internet: "インターネットに接続されとらんで"
-  no-internet-desc: "ネットワークには接続されとるけど、インターネットには接続されとらんようやわ。つこてるPCのインターネット接続が正常か確認してや。"
-  no-server: "Misskeyのサーバーに接続でけへんわ"
-  no-server-desc: "つこてるPCのインターネット接続は正常やけど、Misskeyのサーバーにはつながらんわ。多分サーバーがダウンまたはメンテナンスしとるわ、知らんけど。すまんけどしばらくしてから再度アクセスしてみてや。"
-  success: "Misskeyのサーバーに接続できたわ"
-  success-desc: "正常に接続できるようやわ。ページを再度読み込みしてな。"
-  flush: "キャッシュの削除"
-  set-version: "バージョン指定"
-common/views/components/media-banner.vue:
-  sensitive: "見せたらあかん"
-  click-to-show: "押してみ、見せたるわ"
-common/views/components/theme.vue:
-  light-theme: "ナイトゲームちゃう時のテーマどないする?"
-  dark-theme: "ナイトゲームの時のテーマどないする?"
-  light-themes: "デイゲーム"
-  dark-themes: "ナイトゲーム"
-  install-a-theme: "テーマ入れるで"
-  theme-code: "テーマコード"
-  install: "インストール"
-  installed: "「{}」を入れたで!"
-  create-a-theme: "テーマ作る"
-  save-created-theme: "テーマ保存"
-  primary-color: "この色一番重要や"
-  secondary-color: "次はこの色出したって"
-  text-color: "文字はこの色や!"
-  base-theme: "この色が背景や!"
-  base-theme-light: "Light"
-  base-theme-dark: "Dark"
-  theme-name: "テーマ名"
-  preview-created-theme: "試してみる"
-  invalid-theme: "このテーマあかんわ、なんか間違うとる"
-  already-installed: "このテーマもうあるで"
-  saved: "保存したで!"
-  manage-themes: "テーマの管理"
-  builtin-themes: "いつものテーマ"
-  my-themes: "ワイのテーマ"
-  installed-themes: "入れたテーマ"
-  select-theme: "テーマ選んでや!"
-  uninstall: "ほかす"
-  uninstalled: "「{}」をほかしてもうたわ"
-  author: "作った人"
-  desc: "説明"
-  export: "エクスポート"
-  import: "インポート"
-  import-by-code: "それかコードを貼っつける"
-  theme-name-required: "テーマ名は絶対要るで"
-common/views/components/cw-button.vue:
-  hide: "もうええわ"
-  show: "見たいやろ?"
-  poll: "アンケート"
-common/views/components/messaging.vue:
-  search-user: "ユーザーを探す"
-  you: "あんさん"
-  no-history: "履歴はあらへんで"
-  user: "ユーザー"
-common/views/components/messaging-room.vue:
-  no-history: "これより過去の履歴はあらへんで"
-  new-message: "新しいメッセージがあるで"
-common/views/components/messaging-room.form.vue:
-  input-message-here: "ここにメッセージ書いてや"
-  send: "送信"
-  attach-from-local: "PCからファイルを添付する"
-  attach-from-drive: "ドライブからファイルを添付する"
-common/views/components/messaging-room.message.vue:
-  is-read: "既読"
-  deleted: "このメッセージは削除されたわ"
-common/views/components/nav.vue:
-  about: "Misskeyについて"
-  stats: "統計"
-  status: "ステータス"
-  wiki: "Wiki"
-  donors: "支援者"
-  repository: "リポジトリ"
-  develop: "開発者"
-  feedback: "フィードバック"
-common/views/components/note-menu.vue:
-  detail: "もっと"
-  copy-link: "リンクをコピー"
-  favorite: "お気に入り"
-  unfavorite: "お気に入りやめる"
-  pin: "ピン留め"
-  unpin: "ピン留めやめる"
-  delete: "ほかす"
-  delete-confirm: "この投稿を削除してもええか?"
-  remote: "投稿元に行ってみよか"
-common/views/components/user-menu.vue:
-  mute: "ミュート"
-  block: "ブロック"
-  suspend: "凍結"
-common/views/components/poll.vue:
-  vote-to: "「{}」に投票や!"
-  vote-count: "{}票"
-  vote: "投票するで"
-  show-result: "結果を見よか"
-  voted: "投票済みや"
-common/views/components/poll-editor.vue:
-  no-only-one-choice: "選択肢が最低2つ必要やで"
-  choice-n: "選択肢{}"
-  remove: "この選択肢を消すで"
-  add: "+選択肢を追加"
-  destroy: "アンケートをほかそ"
-  day: "æ—¥"
-common/views/components/reaction-picker.vue:
-  choose-reaction: "リアクション、どれにするんや?"
-common/views/components/emoji-picker.vue:
-  custom-emoji: "カスタム絵文字"
-  people: "人"
-  animals-and-nature: "動物&自然"
-  food-and-drink: "食いもん&飲みもん"
-  activity: "アクティビティ"
-  travel-and-places: "場所"
-  objects: "物"
-  symbols: "記号"
-  flags: "æ——"
-common/views/components/settings/app-type.vue:
-  info: "ページもっぺん読み込んだら反映したるで。"
-common/views/components/signin.vue:
-  username: "ユーザー名"
-  password: "パスワード"
-  token: "トークン"
-  signing-in: "サインイン中や..."
-  or: "それか"
-  signin-with-twitter: "Twitterでサインイン"
-  signin-with-github: "GitHubでログイン"
-  signin-with-discord: "Discordでログイン"
-  login-failed: "なんかログインできんかったわ。ユーザー名とパスワードとかを確認してや。"
-common/views/components/signup.vue:
-  invitation-code: "招待コード"
-  invitation-info: "招待コードをもっとらんのやったら、<a href=\"{}\">管理者</a>まで連絡してや。"
-  username: "ユーザー名"
-  checking: "確認中や……"
-  available: "使えるで"
-  unavailable: "もう使われとるで"
-  error: "通信あかんわ"
-  invalid-format: "a~z、A~Z、0~9、_が使えるで"
-  too-short: "1文字以上やで!"
-  too-long: "20文字以内やで"
-  password: "パスワード"
-  password-placeholder: "8文字以上にしときや"
-  weak-password: "へぼいパスワード"
-  normal-password: "ぼちぼちなパスワード"
-  strong-password: "良さげなパスワード"
-  retype: "もっかい入力頼むで"
-  retype-placeholder: "確認のためもっぺん入力してや"
-  password-matched: "一致しとるで"
-  password-not-matched: "一致しとらんで"
-  recaptcha: "認証"
-  create: "アカウント作成"
-  some-error: "何かよう分からんけど、アカウントの作成に失敗してしもたわ。すまんがもっぺん試してくれへんか?"
-common/views/components/special-message.vue:
-  new-year: "おおきに。今年もよろしゅう。"
-  christmas: "メリークリスマス!"
-common/views/components/stream-indicator.vue:
-  connecting: "つないどるで"
-  reconnecting: "つなぎ直すで"
-  connected: "つないだわ"
-common/views/components/notification-settings.vue:
-  title: "通知"
-common/views/components/integration-settings.vue:
-  title: "サービス連携"
-  connect: "つなげる"
-  disconnect: "接続をほかす"
-  connected-to: "このアカウントと繋がっとるで"
-common/views/components/github-setting.vue:
-  description: "あんたがつことるTwitterアカウントをMisskeyアカウントに接続しとくと、あんさんのプロフィールにTwitterアカウント情報が表示されるようになったり、Twitterを使うた便利なサインインが使えるようになったりすんで。"
-  connected-to: "次のGitHubアカウントに接続されとるで"
-  detail: "くわしく..."
-  reconnect: "つなぎ直す"
-  connect: "GitHubと接続する"
-  disconnect: "接続をほかす"
-common/views/components/discord-setting.vue:
-  description: "あんたがつことるDiscordアカウントをMisskeyアカウントに接続しとくと、あんさんのプロフィールにDiscordアカウント情報が表示されるようになったり、Discordを使うた便利なサインインが使えるようになったりすんで。"
-  connected-to: "次のDiscordアカウントに接続されとるで"
-  detail: "くわしく..."
-  reconnect: "つなぎ直す"
-  connect: "Discordと接続する"
-  disconnect: "接続をほかす"
-common/views/components/uploader.vue:
-  waiting: "待っとる"
-common/views/components/visibility-chooser.vue:
-  public: "公開"
-  home: "ホーム"
-  home-desc: "ホームタイムライン以外に見せんとって"
-  followers: "フォロワー"
-  followers-desc: "自分のフォロワー以外に見せんとって"
-  specified: "ダイレクト"
-  specified-desc: "今から言うユーザー以外に見せんとってや"
-  local-public: "公開 (ローカルだけ)"
-  local-public-desc: "リモートには見せへん"
-  local-home: "ホーム (ローカルだけ)"
-  local-followers: "フォロワー (ローカルだけ)"
-common/views/components/trends.vue:
-  count: "{}人が投稿"
-  empty: "流行は自分で作るんや"
-common/views/components/language-settings.vue:
-  title: "表示言語"
-  pick-language: "言語選んでや"
-  recommended: "これええで"
-  auto: "勝手にやる"
-  specify-language: "言語選びや"
-  info: "ページもっぺん読み込んだら反映したるで。"
-common/views/components/profile-editor.vue:
-  title: "プロフィール"
-  name: "名前"
-  account: "アカウント"
-  location: "場所"
-  description: "自己紹介"
-  language: "言語"
-  birthday: "誕生日"
-  avatar: "アバター"
-  banner: "バナー"
-  is-cat: "このアカウントはCatやで"
-  is-bot: "このアカウントはBotやで"
-  is-locked: "他人のフォローは許可してからや!"
-  careful-bot: "Botからのフォローだけは許可制や"
-  advanced: "その他"
-  privacy: "プライバシーってなんや?オカンの年齢か?"
-  save: "保存"
-  saved: "プロフィールを保存したで"
-  uploading: "アップロードしとります"
-  upload-failed: "これアップロードでけへんわ"
-  unable-to-process: "あかん、無理やわ"
-  email: "メール設定"
-  email-address: "メールアドレス"
-  email-verified: "このメールアドレスOKや!"
-  email-not-verified: "メールアドレスが確認されとらん。メールボックスもっぺん見てくれへん?"
-  export: "エクスポート"
-  import: "インポート"
-  export-targets:
-    following-list: "フォロー"
-    mute-list: "ミュート"
-    blocking-list: "ブロック"
-    user-lists: "リスト"
-  enter-password: "パスワードを入れてや"
-common/views/components/user-list-editor.vue:
-  users: "ユーザー"
-  add-user: "ユーザー増やす"
-common/views/components/user-group-editor.vue:
-  invite: "招待"
-common/views/components/user-lists.vue:
-  user-lists: "リスト"
-  list-name: "リスト名"
-common/views/components/user-groups.vue:
-  invites: "招待"
-common/views/widgets/broadcast.vue:
-  fetching: "見てみるわ…"
-  no-broadcasts: "お知らせはあらへんで"
-  have-a-nice-day: "おおきに!"
-  next: "次"
-common/views/widgets/calendar.vue:
-  year: "{}å¹´"
-  month: "{}月"
-  day: "{}æ—¥"
-  today: "今日:"
-  this-month: "今月:"
-  this-year: "今年:"
-common/views/widgets/photo-stream.vue:
-  title: "フォトストリーム"
-  no-photos: "写真はあらへんで"
-common/views/widgets/posts-monitor.vue:
-  title: "投稿チャート"
-  toggle: "表示を切り替え"
-common/views/widgets/hashtags.vue:
-  title: "ハッシュタグ"
-common/views/widgets/server.vue:
-  title: "サーバー情報"
-  toggle: "表示を切り替え"
-common/views/widgets/memo.vue:
-  title: "付箋"
-  memo: "書くんや!"
-  save: "保存"
-common/views/widgets/slideshow.vue:
-  folder-customize-mode: "フォルダを指定するんやったら、一旦カスタマイズモードを終了してや"
-  folder: "クリックしてフォルダ決めてや"
-  no-image: "このフォルダには画像無いわ"
-common/views/widgets/tips.vue:
-  tips-line1: "<kbd>t</kbd>でタイムラインにフォーカスできんで"
-  tips-line2: "<kbd>p</kbd>または<kbd>n</kbd>で投稿フォームを開くで"
-  tips-line3: "投稿フォームにはファイルをドラッグ&ドロップできんで"
-  tips-line4: "投稿フォームにクリップボードにおる画像データをペーストできんで"
-  tips-line5: "ドライブにファイルをドラッグ&ドロップしてアップロードできんで"
-  tips-line6: "ドライブやと、ファイルをドラッグしてフォルダ移動できんで"
-  tips-line7: "ドライブやと、フォルダをドラッグしてフォルダ移動できんで"
-  tips-line8: "ホームは設定からカスタマイズできんで"
-  tips-line9: "MisskeyはAGPLv3やで"
-  tips-line10: "タイムマシンウィジェットを利用すると、簡単に過去のタイムラインに遡れんで"
-  tips-line11: "投稿の ... をクリックして、ピン留めから投稿をユーザーページにピン留めできんで"
-  tips-line13: "投稿に添付したファイルは全てドライブに保存されんで"
-  tips-line14: "ホームのカスタマイズ中、ウィジェットを右クリックしてデザインを変更できんで"
-  tips-line17: "「**」でテキストを囲ったると**強調表示**されんで"
-  tips-line19: "いくつかのウィンドウはブラウザの外に切り離すことができんで"
-  tips-line20: "カレンダーウィジェットのパーセンテージは、経過の割合を示してんねん"
-  tips-line21: "APIをつこてbotの開発なども行えるで"
-  tips-line24: "Misskeyは2014年にサービスを開始したんよ"
-  tips-line25: "対応ブラウザやったらMisskeyを開いとらんでも通知を受け取れんで"
-common/views/pages/follow.vue:
-  signed-in-as: "{}としてサインイン中"
-  following: "フォローしとる"
-  follow: "フォロー"
-  request-pending: "フォローの許し待っとる"
-  follow-processing: "今フォロー処理やっとる‥"
-  follow-request: "フォロー許してくれや!言うてみる"
-common/views/pages/follow-requests.vue:
-  received-follow-requests: "フォロー許してくれや!言うてみる"
-desktop:
-  banner-crop-title: "どこバナーとして出す?"
-  banner: "バナー"
-  uploading-banner: "新しいバナーをアップロードしとるで"
-  banner-updated: "バナーを更新したで"
-  choose-banner: "バナーにする画像選んでや"
-  avatar-crop-title: "どこアバターとして出しとく?"
-  avatar: "アバター"
-  uploading-avatar: "新しいアバターをアップロードしとるで"
-  avatar-updated: "アバターを更新したで"
-  choose-avatar: "アバターにする画像選んでや"
-  unable-to-process: "あかん、無理やわ"
-  invalid-filetype: "この形式のファイル無理やねん"
-desktop/views/components/activity.chart.vue:
-  total: "黒いの ... 全部"
-  notes: "青いの ... 投稿"
-  replies: "赤いの ... 返信"
-  renotes: "碧いの ... Renotes"
-desktop/views/components/activity.vue:
-  title: "アクティビティ"
-  toggle: "表示変える"
-desktop/views/components/calendar.vue:
-  title: "{year}年 {month} 月"
-  prev: "前の月"
-  next: "次の月"
-  go: "クリックしてタイムリープ"
-desktop/views/components/choose-file-from-drive-window.vue:
-  chosen-files: "{count}ファイル選択中"
-  upload: "PCからドライブにファイル上げる"
-  cancel: "やめとくわ"
-  ok: "そうする"
-  choose-prompt: "ファイル選んでや"
-desktop/views/components/choose-folder-from-drive-window.vue:
-  cancel: "やめとくわ"
-  ok: "そうする"
-  choose-prompt: "フォルダ選んでや"
-desktop/views/components/crop-window.vue:
-  skip: "クロップせーへんわ"
-  cancel: "やめとくわ"
-  ok: "そうする"
-desktop/views/components/drive-window.vue:
-  used: "使うとる"
-desktop/views/components/drive.file.vue:
-  avatar: "アバター"
-  banner: "バナー"
-  nsfw: "見たらあかんで"
-  contextmenu:
-    rename: "名前を変えるで"
-    mark-as-sensitive: "見たらあかん感じにしとく"
-    unmark-as-sensitive: "やっぱ見せたるわ"
-    copy-url: "URLをコピー"
-    download: "ダウンロード"
-    else-files: "その他"
-    set-as-avatar: "アバターにする"
-    set-as-banner: "バナーにする"
-    open-in-app: "アプリで開く"
-    add-app: "アプリ増やす"
-    rename-file: "ファイル名をいらう(変える)"
-    input-new-file-name: "新しいファイル名を入力してや"
-    copied: "コピー完了や"
-    copied-url-to-clipboard: "URLをクリップボードに写したわ"
-desktop/views/components/drive.folder.vue:
-  unable-to-process: "あかん、無理やわ"
-  circular-reference-detected: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。"
-  unhandled-error: "ようわからん"
-  contextmenu:
-    move-to-this-folder: "ここに持ってくるわ"
-    show-in-new-window: "新しいウィンドウで出す"
-    rename: "名前を変えるで"
-    rename-folder: "フォルダ名を変えるで"
-    input-new-folder-name: "新しいフォルダ名を入力してや"
-    else-folders: "その他"
-desktop/views/components/drive.vue:
-  search: "検索"
-  empty-draghover: "ドロップするにゃ!お魚以外なら何でもいいにゃ!"
-  empty-drive: "ドライブには何もあらへんで。"
-  empty-drive-description: "右クリックして「ファイルをアップロード」を選んだり、ファイルをドラッグ&ドロップすることでもアップロードできんねん。"
-  empty-folder: "このフォルダーは空や"
-  unable-to-process: "あかん、無理やわ"
-  circular-reference-detected: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。"
-  unhandled-error: "ようわからん"
-  url-upload: "URLアップロード"
-  url-of-file: "このURLのファイルをアップロードしたいねん"
-  url-upload-requested: "アップロードしたい言うといたで"
-  may-take-time: "アップロード終わるんにちょい時間かかるかもしれへんわ。"
-  create-folder: "フォルダー作成"
-  folder-name: "フォルダー名"
-  contextmenu:
-    create-folder: "フォルダー作る"
-    upload: "ファイル上げる"
-    url-upload: "URLつこうて上げる"
-desktop/views/components/media-video.vue:
-  sensitive: "ちょっと見せられへんわ"
-  click-to-show: "クリックして見せるで"
-desktop/views/components/followers-window.vue:
-  followers: "{} のフォロワー"
-desktop/views/components/followers.vue:
-  empty: "フォロワーはおらんっぽいで、知らんけど。"
-desktop/views/components/following-window.vue:
-  following: "{} のフォロー"
-desktop/views/components/following.vue:
-  empty: "フォロー中のユーザーはおらんっぽいで、知らんけど。"
-desktop/views/components/game-window.vue:
-  game: "ゲーム"
-desktop/views/components/home.vue:
-  done: "完了"
-  add-widget: "ウィジェット増やす"
-  add: "増やす"
-desktop/views/input-dialog.vue:
-  cancel: "やめとくわ"
-  ok: "これや!"
-desktop/views/components/note-detail.vue:
-  private: "この投稿は見せられへんわ"
-  deleted: "この投稿なんか無くなってもうたわ"
-  location: "ここおるで:"
-  renote: "Renote"
-  add-reaction: "リアクション"
-desktop/views/components/note.vue:
-  reply: "返す"
-  renote: "Renote"
-  add-reaction: "リアクション"
-  detail: "もっと"
-  private: "この投稿は見せられへんわ"
-  deleted: "この投稿なんか無くなってもうたわ"
-desktop/views/components/notes.vue:
-  error: "あかん、読み込めへんわ"
-  retry: "もっぺん"
-desktop/views/components/notifications.vue:
-  empty: "あらへん!"
-desktop/views/components/post-form.vue:
-  posted: "投稿したで!"
-  replied: "返信したで!"
-  reposted: "Renoteしたで!"
-  note-failed: "投稿に失敗したで"
-  reply-failed: "返信に失敗したで"
-  renote-failed: "Renoteでけへん"
-desktop/views/components/post-form-window.vue:
-  note: "新規投稿"
-  reply: "返す"
-  attaches: "添付: {}メディア"
-  uploading-media: "{}個のメディアを上げとんねん……"
-desktop/views/components/progress-dialog.vue:
-  waiting: "待っとる"
-desktop/views/components/renote-form.vue:
-  quote: "取ってくる……"
-  cancel: "やめとくわ"
-  renote: "Renote"
-  renote-home: "Renote (Home)"
-  reposting: "やっとります..."
-  success: "Renoteしたで!"
-  failure: "Renoteでけへん"
-desktop/views/components/renote-form-window.vue:
-  title: "この投稿をRenoteしてもええか?"
-desktop/views/pages/user-following-or-followers.vue:
-  following: "{user}のフォロー"
-  followers: "{user}のフォロワー"
-desktop/views/components/settings.2fa.vue:
-  intro: "二段階認証を設定すると、サインイン時にパスワードだけとちゃうくて、予め登録しておいた物理的なデバイス(例えばあんさんのスマートフォンなど)も必要になり、よりセキュリティが向上すんで。"
-  detail: "詳細..."
-  url: "https://www.google.co.jp/intl/ja/landing/2step/"
-  caution: "登録したデバイスを紛失してもうたら、もうMisskeyにサインインできんくなるで。"
-  register: "デバイス登録する"
-  already-registered: "もう設定終わっとるわ"
-  unregister: "設定をほかす"
-  unregistered: "二段階認証もうせーへんで"
-  enter-password: "パスワードを入れてや"
-  authenticator: "まず、Google Authenticatorとかのをつこてるデバイスにインストールしてや:"
-  howtoinstall: "インストール方法はここやで"
-  token: "トークン"
-  scan: "んで、ここに出とるQRコードをスキャンしてな:"
-  done: "最後にデバイスに表示されとるトークンを入力してな:"
-  submit: "送信"
-  success: "設定が完了したで!"
-  failed: "なんか設定に失敗したで。トークンを間違えとらんか確認してや。"
-  info: "次のサインインからは、パスワードに加えてデバイスに出とるトークンを入力してな。"
-common/views/components/media-image.vue:
-  sensitive: "ちょっと見せられへんわ"
-  click-to-show: "クリックして見せるで"
-common/views/components/api-settings.vue:
-  intro: "API使うんやったらこのトークンを「i」っちゅうパラメータにくっつけてリクエストできるで。"
-  caution: "アカウント勝手にいじられるかも知れんから、このトークンは教えたらあかんし、アプリにも書いたらあかんで(これはフリちゃうで)"
-  regeneration-of-token: "トークン漏れてもうたんやったらもっかい生成できるで。"
-  regenerate-token: "トークンもっかい生成"
-  token: "Token:"
-  enter-password: "パスワードを入れてや"
-  console:
-    title: "APIコンソール"
-    endpoint: "エンドポイント"
-    parameter: "パラメータ"
-    credential-info: "「i」パラメータは勝手に付くで。"
-    send: "送る"
-    sending: "応答待っとる"
-    response: "こんなん返ってきたわ"
-desktop/views/components/settings.apps.vue:
-  no-apps: "連携しているアプリケーションはあらへんで"
-common/views/components/drive-settings.vue:
-  max: "容量"
-  in-use: "使うとる"
-  stats: "統計"
-  default-upload-folder-name: "フォルダ"
-common/views/components/mute-and-block.vue:
-  mute-and-block: "ミュートとブロック"
-  mute: "ミュート"
-  block: "ブロック"
-  no-muted-users: "ミュートしとるユーザーはおらんで"
-  no-blocked-users: "ブロックしとるユーザーはおらんで"
-  word-mute: "ワードミュート"
-  muted-words: "ミュートしとるキーワード"
-  muted-words-description: "スペースで区切るとAND指定で、改行で区切るとOR指定や"
-  save: "保存"
-common/views/components/password-settings.vue:
-  reset: "パスワード変える"
-  enter-current-password: "今のパスワードを入れてや"
-  enter-new-password: "こんどのパスワード入れてや"
-  enter-new-password-again: "もっぺん入れてや"
-  not-match: "パスワードがおうとらん"
-  changed: "パスワード変えたわ"
-common/views/components/post-form-attaches.vue:
-  mark-as-sensitive: "見たらあかん感じにしとく"
-  unmark-as-sensitive: "やっぱ見せたるわ"
-desktop/views/components/sub-note-content.vue:
-  private: "この投稿は見せられへんわ"
-  deleted: "この投稿なんか無くなってもうたわ"
-  media-count: "{}つのメディア"
-  poll: "アンケート"
-desktop/views/components/settings.tags.vue:
-  add: "増やす"
-  save: "保存"
-desktop/views/components/timeline.vue:
-  home: "ホーム"
-  local: "ローカル"
-  global: "グローバル"
-  mentions: "あんた宛て"
-  messages: "ダイレクト投稿"
-  list: "リスト"
-  hashtag: "ハッシュタグ"
-  add-tag-timeline: "ハッシュタグ増やす"
-  add-list: "リストに入れる"
-  list-name: "リスト名"
-desktop/views/components/ui.header.vue:
-  welcome-back: "おかえり、"
-  adjective: "はん"
-desktop/views/components/ui.header.account.vue:
-  profile: "プロフィール"
-  lists: "リスト"
-  follow-requests: "フォロー許してくれや!言うてみる"
-  admin: "管理"
-desktop/views/components/ui.header.nav.vue:
-  game: "ゲーム"
-desktop/views/components/ui.header.notifications.vue:
-  title: "通知"
-desktop/views/components/ui.header.post.vue:
-  post: "新規投稿"
-desktop/views/components/ui.header.search.vue:
-  placeholder: "検索"
-desktop/views/components/user-preview.vue:
-  notes: "投稿"
-  following: "フォロー"
-  followers: "フォロワー"
-desktop/views/components/users-list.vue:
-  all: "すべて"
-  iknow: "知っとる"
-  fetching: "読み込んどります"
-desktop/views/components/users-list-item.vue:
-  followed: "フォローされとるで"
-desktop/views/components/window.vue:
-  popout: "ポップアウト"
-  close: "さいなら"
-admin/views/index.vue:
-  dashboard: "ダッシュボード"
-  instance: "インスタンス"
-  emoji: "カスタム絵文字"
-  moderators: "モデレーター"
-  users: "ユーザー"
-  federation: "連合"
-  announcements: "知っといてや"
-  back-to-misskey: "Misskeyに戻る"
-admin/views/dashboard.vue:
-  dashboard: "ダッシュボード"
-  accounts: "アカウント"
-  notes: "投稿"
-  drive: "ドライブ"
-  instances: "インスタンス"
-  this-instance: "ワイのインスタンス"
-  federated: "連合"
-admin/views/logs.vue:
-  levels:
-    info: "情報"
-    error: "エラー"
-admin/views/abuse.vue:
-  details: "もっと"
-  remove-report: "削除"
-admin/views/instance.vue:
-  instance: "インスタンス"
-  instance-name: "インスタンス名"
-  instance-description: "インスタンスの紹介"
-  host: "ホスト"
-  banner-url: "バナー画像URL"
-  languages: "インスタンスの対象言語"
-  languages-desc: "スペースで区切って複数設定できるで。"
-  maintainer-config: "管理者情報"
-  maintainer-name: "管理者名"
-  maintainer-email: "管理者の連絡先"
-  drive-config: "ドライブの設定"
-  object-storage-endpoint: "エンドポイント"
-  cache-remote-files: "リモートのファイルをキャッシュする"
-  local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量"
-  remote-drive-capacity-mb: "リモートユーザーひとりあたりのドライブ容量"
-  mb: "メガバイト単位"
-  recaptcha-config: "reCAPTCHAの設定"
-  recaptcha-info: "reCAPTCHAを有効にするにはreCAPTCHAトークンが要るで。https://www.google.com/recaptcha/intro/ にアクセスしてトークンを取得してな。"
-  enable-recaptcha: "reCAPTCHAを有効にする"
-  recaptcha-preview: "試してみる"
-  twitter-integration-config: "Twitter連携の設定"
-  twitter-integration-info: "コールバックURLは {url} に設定してや。"
-  enable-twitter-integration: "Twitter連携を有効にする"
-  twitter-integration-consumer-key: "Consumer key"
-  twitter-integration-consumer-secret: "Consumer secret"
-  github-integration-config: "GitHub連携の設定"
-  github-integration-info: "コールバックURLは {url} に設定してや。"
-  enable-github-integration: "GitHub連携を使えるようにする"
-  github-integration-client-id: "Client ID"
-  github-integration-client-secret: "Client Secret"
-  discord-integration-config: "Discord連携の設定"
-  discord-integration-info: "コールバックURLは {url} に設定してや。"
-  enable-discord-integration: "Discord連携を有効にする"
-  discord-integration-client-id: "Client ID"
-  discord-integration-client-secret: "Client Secret"
-  proxy-account-config: "プロキシアカウントの設定"
-  proxy-account-info: "プロキシアカウントは、代わりにフォローしてくれるアカウントや。例えば、551に豚まんが無いときやったり、ユーザーがリモートユーザーをアカウントに入れたとき、リストに入れられたユーザーが誰からもフォローされてないと寂しいやん。寂しいし、アクティビティも配達されへんから、プロキシアカウントがフォローしてくれるで。ええやつやん…"
-  proxy-account-username: "プロキシアカウントのユーザー名"
-  proxy-account-username-desc: "プロキシとして使用するアカウントのユーザー名を指定してや"
-  proxy-account-warn: "アカウント作るんはあんたがやってや。あんたのおかんもMisskeyもやってくれへんで。"
-  max-note-text-length: "投稿の最大文字数"
-  disable-registration: "ユーザー登録の受付を止める"
-  disable-local-timeline: "ローカルタイムラインを使えんようにする"
-  invite: "来てや"
-  save: "保存"
-  saved: "保存したで!"
-  email-config: "メールサーバーの設定"
-  email-config-info: "メールアドレス確認やパスワードリセットの際に使うで。"
-  enable-email: "メール配信を有効にする"
-  email: "メールアドレス"
-  smtp-secure: "SMTP接続に暗黙的なSSL/TLSを使用する"
-  smtp-secure-info: "STARTTLS使用時はオフにします。"
-  smtp-host: "SMTPホスト"
-  smtp-port: "SMTPポート"
-  smtp-user: "SMTPユーザー"
-  smtp-pass: "SMTPパスワード"
-admin/views/charts.vue:
-  title: "チャート"
-  per-day: "1日ごと"
-  per-hour: "1時間ごと"
-  federation: "フェデレーション"
-  notes: "投稿"
-  users: "ユーザー"
-  drive: "ドライブ"
-  network: "ネットワーク"
-  charts:
-    federation-instances: "インスタンスの増減"
-    federation-instances-total: "インスタンスの積算"
-    notes: "投稿の増減(統合)"
-    local-notes: "投稿の増減 (ローカル)"
-    remote-notes: "投稿の増減 (リモート)"
-    notes-total: "投稿の積算"
-    users: "ユーザーの増減"
-    users-total: "ユーザーの積算"
-    drive: "ドライブ使用量の増減"
-    drive-total: "ドライブ使用量の積算"
-    drive-files: "ドライブのファイル数の増減"
-    drive-files-total: "ドライブのファイル数の積算"
-    network-requests: "リクエスト"
-    network-time: "応答時間"
-    network-usage: "通信量"
-admin/views/drive.vue:
-  operation: "操作"
-  lookup: "照会"
-  origin:
-    local: "ローカル"
-  delete: "削除"
-  mark-as-sensitive: "見たらあかん感じにしとく"
-  unmark-as-sensitive: "やっぱ見せたるわ"
-admin/views/users.vue:
-  operation: "操作"
-  username-or-userid: "ユーザー名またはユーザーID"
-  user-not-found: "ユーザーが見つからへん!"
-  lookup: "照会"
-  reset-password: "パスワードをリセット"
-  password-updated: "パスワードは現在「{password} 」やで"
-  suspend: "凍結"
-  username: "ユーザー名"
-  host: "ホスト"
-  users:
-    title: "ユーザー"
-    state:
-      all: "すべて"
-      moderator: "モデレーター"
-    origin:
-      local: "ローカル"
-admin/views/moderators.vue:
-  logs:
-    moderator: "モデレーター"
-    type: "操作"
-    info: "情報"
-admin/views/emoji.vue:
-  add-emoji:
-    add: "増やす"
-  emojis:
-    remove: "削除"
-admin/views/announcements.vue:
-  announcements: "知っときや"
-  save: "保存"
-  remove: "削除"
-  add: "増やす"
-  saved: "保存したで!"
-admin/views/federation.vue:
-  instance: "インスタンス"
-  host: "ホスト"
-  notes: "投稿"
-  users: "ユーザー"
-  following: "フォローしとる"
-  followers: "フォロワー"
-  status: "ステータス"
-  block: "ブロック"
-  lookup: "照会"
-  instances: "連合"
-  states:
-    all: "すべて"
-    blocked: "ブロック"
-  charts: "チャート"
-  chart-srcs:
-    requests: "リクエスト"
-    users: "ユーザーの増減"
-    users-total: "ユーザーの積算"
-    notes-total: "投稿の積算"
-    drive-usage: "ドライブ使用量の増減"
-    drive-usage-total: "ドライブ使用量の積算"
-  chart-spans:
-    hour: "1時間ごと"
-    day: "1日ごと"
-  blocked-hosts: "ブロック"
-  save: "保存"
-desktop/views/pages/welcome.vue:
-  about: "もうちょい……"
-  timeline: "タイムライン"
-  announcements: "知っときや"
-  photos: "最近の画像"
-  powered-by-misskey: "<b>Misskey</b>のおかげや"
-  info: "情報"
-desktop/views/pages/drive.vue:
-  title: "ドライブ"
-desktop/views/pages/note.vue:
-  prev: "前のやつ"
-  next: "次のやつ"
-desktop/views/pages/selectdrive.vue:
-  title: "ファイルを選択してや"
-  ok: "決定"
-  cancel: "やめとくわ"
-  upload: "PCからドライブにファイル上げる"
-desktop/views/pages/search.vue:
-  not-available: "検索機能は使えへんわ。管理者がそう言うとる。"
-desktop/views/pages/user-list.users.vue:
-  users: "ユーザー"
-  add-user: "ユーザー増やす"
-  username: "ユーザー名"
-desktop/views/pages/user/user.followers-you-know.vue:
-  title: "知っとるフォロワー"
-  loading: "読み込んどります"
-  no-users: "フォロワー全員知らんわ"
-desktop/views/pages/user/user.friends.vue:
-  title: "よう話すツレ"
-  loading: "読み込んどります"
-  no-users: "よう話すツレは居らん"
-desktop/views/pages/user/user.photos.vue:
-  title: "写真"
-  loading: "読み込んどります"
-  no-photos: "写真はあらへんで"
-desktop/views/pages/user/user.header.vue:
-  posts: "投稿"
-  following: "フォロー"
-  followers: "フォロワー"
-  is-bot: "このアカウントはBotや"
-  years-old: "{age}æ­³"
-  year: "å¹´"
-  month: "月"
-  day: "æ—¥"
-  follows-you: "フォローされとるで"
-desktop/views/pages/user/user.timeline.vue:
-  default: "投稿"
-  with-replies: "投稿と返信"
-  with-media: "メディア"
-desktop/views/widgets/notifications.vue:
-  title: "通知"
-desktop/views/widgets/polls.vue:
-  title: "アンケート"
-  refresh: "他を見る"
-  nothing: "あらへん!"
-desktop/views/widgets/post-form.vue:
-  title: "投稿"
-  note: "投稿"
-desktop/views/widgets/profile.vue:
-  update-banner: "クリックしてバナー編集"
-  update-avatar: "クリックしてアバター編集"
-desktop/views/widgets/trends.vue:
-  title: "流行"
-  refresh: "他を見る"
-  nothing: "あらへん!"
-desktop/views/widgets/users.vue:
-  title: "おすすめユーザー"
-  refresh: "他を見る"
-  no-one: "おらん!"
-mobile/views/components/drive.vue:
-  used: "使うとる"
-  folder-count: "フォルダ"
-  count-separator: "、"
-  file-count: "ファイル"
-  nothing-in-drive: "ドライブには何もあらへんで。"
-  folder-is-empty: "このフォルダ何もないわ"
-  folder-name: "フォルダー名"
-  url-prompt: "このURLのファイルをアップロードしたいねん"
-  uploading: "アップロードをリクエストしたで。アップロードが完了するまで時間がかかるかも分からん、知らんけど。"
-mobile/views/components/drive-file-chooser.vue:
-  select-file: "ファイル選んでや"
-mobile/views/components/drive-folder-chooser.vue:
-  select-folder: "フォルダ選んでや"
-mobile/views/components/drive.file.vue:
-  nsfw: "ちょっと見せられへんわ"
-mobile/views/components/drive.file-detail.vue:
-  download: "ダウンロード"
-  rename: "名前を変えるで"
-  move: "移動"
-  hash: "ハッシュ(md5)"
-  exif: "EXIF"
-  nsfw: "ちょっと見せられへんわ"
-  mark-as-sensitive: "見たらあかん感じにしとく"
-  unmark-as-sensitive: "やっぱ見せたるわ"
-mobile/views/components/media-video.vue:
-  sensitive: "ちょっと見せられへんわ"
-  click-to-show: "押してみ、見せたるわ"
-common/views/components/follow-button.vue:
-  following: "フォローしとる"
-  follow: "フォロー"
-  request-pending: "フォロー許してくれるん待っとる"
-  follow-processing: "今フォロー処理やっとる‥"
-  follow-request: "フォローさせてや!言うてみる"
-mobile/views/components/note.vue:
-  private: "この投稿は見せられへんわ"
-  deleted: "この投稿なんか無くなってもうたわ"
-  location: "ここおるで:"
-mobile/views/components/note-detail.vue:
-  reply: "返す"
-  reaction: "リアクション"
-  private: "この投稿は見せられへんわ"
-  deleted: "この投稿なんか無くなってもうたわ"
-  location: "ここおるで:"
-mobile/views/components/note-preview.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "cat"
-mobile/views/components/note-sub.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "cat"
-mobile/views/components/notifications.vue:
-  empty: "あらへん!"
-mobile/views/components/sub-note-content.vue:
-  private: "この投稿は見せられへんわ"
-  deleted: "この投稿なんか無くなってもうたわ"
-  media-count: "{}つのメディア"
-  poll: "アンケート"
-mobile/views/components/ui.header.vue:
-  welcome-back: "おかえり、"
-  adjective: "はん"
-mobile/views/components/ui.nav.vue:
-  timeline: "タイムライン"
-  notifications: "通知"
-  follow-requests: "フォロー許してくれや!言うてみる"
-  search: "検索"
-  user-lists: "リスト"
-  widgets: "ウィジェット"
-  game: "ゲーム"
-  admin: "管理"
-  about: "Misskeyってなんや?"
-mobile/views/pages/drive.vue:
-  contextmenu:
-    upload: "ファイル上げる"
-    create-folder: "フォルダー作る"
-mobile/views/pages/signup.vue:
-  lets-start: "📦 始めようや"
-mobile/views/pages/followers.vue:
-  followers-of: "{name}のフォロワー"
-mobile/views/pages/following.vue:
-  following-of: "{name}のフォロー"
-mobile/views/pages/home.vue:
-  home: "ホーム"
-  local: "ローカル"
-  global: "グローバル"
-  mentions: "あんた宛て"
-  messages: "ダイレクト投稿"
-mobile/views/pages/tag.vue:
-  no-posts-found: "ハッシュタグ「{q}」が付けられた投稿はあらへんかった。"
-mobile/views/pages/widgets.vue:
-  dashboard: "ダッシュボード"
-  add-widget: "増やす"
-  customization-tips: "カスタマイズのヒント"
-mobile/views/pages/widgets/activity.vue:
-  activity: "やっとること"
-mobile/views/pages/share.vue:
-  share-with: "{name}で共有"
-mobile/views/pages/note.vue:
-  title: "投稿"
-  prev: "前のやつ"
-  next: "次のやつ"
-mobile/views/pages/games/reversi.vue:
-  reversi: "リバーシ"
-mobile/views/pages/search.vue:
-  search: "探す"
-  not-found: "ワイは「{q}」なんて投稿知らんわ、無いんちゃう?知らんけど。"
-mobile/views/pages/selectdrive.vue:
-  select-file: "ファイル選んでや"
-mobile/views/pages/notifications.vue:
-  notifications: "通知"
-mobile/views/pages/settings.vue:
-  signed-in-as: "あんたは橋の下で拾った{}や!"
-mobile/views/pages/user.vue:
-  follows-you: "フォローされとるで"
-  following: "フォロー"
-  followers: "フォロワー"
-  notes: "投稿"
-  overview: "こんなやつ"
-  timeline: "タイムライン"
-  media: "メディア"
-  years-old: "{age}æ­³"
-mobile/views/pages/user/home.vue:
-  recent-notes: "最近儲かりまっか?"
-  images: "画像"
-  activity: "やっとること"
-  keywords: "キーワード"
-  domains: "よく出るドメイン"
-  frequently-replied-users: "よう話すツレ"
-  followers-you-know: "知っとるフォロワー"
-  last-used-at: "最後いつ来た?"
-mobile/views/pages/user/home.photos.vue:
-  no-photos: "写真はあらへんで"
-deck:
-  widgets: "ウィジェット"
-  home: "ホーム"
-  local: "ローカル"
-  hashtag: "ハッシュタグ"
-  global: "グローバル"
-  mentions: "あんた宛て"
-  direct: "ダイレクト投稿"
-  notifications: "通知"
-  list: "リスト"
-  swap-left: "左に移動や!"
-  swap-right: "右に移動や!"
-  swap-up: "上に移動や!"
-  swap-down: "下に移動や!"
-  remove: "カラムにさいなら"
-  add-column: "カラム増やす"
-  rename: "名前を変えるで"
-  stack-left: "左に重ねんで!"
-  pop-right: "右に出すで!"
-deck/deck.tl-column.vue:
-  is-media-only: "メディア投稿だけや"
-  edit: "オプション"
-deck/deck.user-column.vue:
-  follows-you: "フォローされとるで"
-  posts: "投稿"
-  following: "フォロー"
-  followers: "フォロワー"
-  images: "画像"
-  activity: "アクティビティ"
-  timeline: "タイムライン"
-  pinned-notes: "ピン留めしはった投稿"
-docs:
-  edit-this-page-on-github: "間違いや改善点を見つけましたか?"
-  edit-this-page-on-github-link: "このページをGitHubで編集"
-dev/views/index.vue:
-  manage-apps: "アプリの管理"
-dev/views/apps.vue:
-  manage-apps: "アプリを管理"
-  create-app: "アプリ作る"
-  app-missing: "アプリあらへん"
-dev/views/new-app.vue:
-  create-app: "アプリケーション作る"
-  app-name: "アプリケーションの名前"
-  app-name-desc: "あんたのアプリの名前。"
-  app-overview: "このアプリどんなん?"
-  callback-url: "コールバックURL (無くてもええで)"
-  callback-url-desc: "ユーザーが認証フォームで認証した後どこに連れてくかを設定できるで"
-  authority: "権限"
-  authority-desc: "ここにチェックした機能しかAPIからアクセスできひんから気ぃつけてな"
-  authority-warning: "アプリ作った後でも変えれるけど、新しいやつ追加したらそん時関連付いてるユーザーキーは全部ほかされるで。"
-pages:
-  pin-this-page: "ピン留め"
-  unpin-this-page: "ピン留めやめる"
-  like: "ええやん"
-  blocks:
-    image: "画像"
-    post: "投稿フォーム"
-  script:
-    categories:
-      random: "いんじゃんほい"
-      list: "リスト"
-    blocks:
-      _join:
-        arg1: "リスト"
-      random: "いんじゃんほい"
-      _randomPick:
-        arg1: "リスト"
-      _dailyRandomPick:
-        arg1: "リスト"
-      _seedRandomPick:
-        arg2: "リスト"
-      _pick:
-        arg1: "リスト"
-      _listLen:
-        arg1: "リスト"
-    types:
-      array: "リスト"
-room:
-  translate: "移動"
-  save: "保存"
-  saved: "保存したで!"
-  furnitures:
-    moon: "月"
-    bin: "ゴミ箱"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
deleted file mode 100644
index bfbe224075bdfad7c097826ee8027c1f7cb90314..0000000000000000000000000000000000000000
--- a/locales/ko-KR.yml
+++ /dev/null
@@ -1,2175 +0,0 @@
----
-meta:
-  lang: "한국어"
-common:
-  misskey: "연합우주의 ⭐"
-  about-title: "연합우주의 ⭐."
-  about: "Misskey를 발견해주셔서 감사합니다! Misskey는 지구에서 태어난 <b>분산 마이크로 블로그 SNS</b> 입니다. Fediverse (다양한 SNS가 함께하는 우주) 속에 존재하고 있어서, 다른 SNS와 서로 연결되어 있습니다. 번잡한 도시에서 벗어나 새로운 인터넷에 빠져보지 않으시겠어요?"
-  intro:
-    title: "Misskey란?"
-    about: "Misskey는 오픈소스 <b>분산형 마이크로블로그 SNS</b>입니다. 다양하고 폭넓게 커스터마이징할 수 있는 UI, 글에 대한 리액션, 파일을 관리할 수 있는 드라이브 등의 선진적인 기능을 갖추고 있습니다. 더하여 Fediverse라고 부르는 네트워크에 연결할 수 있어 다른 SNS와도 주고받을 수 있습니다. 예를 들자면, 당신이 무언가를 게시하면, 해당 게시물은 Misskey 뿐만 아니라 다른 SNS에도 전해집니다. 살짝 어떤 행성에서 다른 행성으로 전파를 발신하고 있는 모습을 상상해주세요."
-    features: "특징"
-    rich-contents: "글"
-    rich-contents-desc: "자신의 생각, 화제의 사건, 모두와 공유하고 싶은 것을 올려주세요. 필요한 경우 다양한 스타일을 사용하여 글을 장식하거나 마음에 드는 이미지, 영상 등의 파일이나 투표를 올리는 것도 가능합니다."
-    reaction: "리액션"
-    reaction-desc: "당신의 감정을 전하는 가장 쉬운 방법입니다. Misskey는 다른 사용자의 글에 다양한 리액션을 붙이는 것이 가능합니다. 한 번 Misskey의 리액션 기능을 경험해버리면, 더는 \"좋아요\" 밖에 감정표현이 없는 SNS로는 돌아갈 수 없게 되어버립니다."
-    ui: "인터페이스"
-    ui-desc: "어떤 형태의 UI가 편한가는 사람마다 다 다릅니다. 그래서 Misskey는 자유도가 높은 UI를 만들고 있습니다. 레이아웃이나 디자인을 변경하거나, 커스터마이징 가능한 다양한 위젯을 배치하거나 하여 자신만의 홈을 만들어주세요."
-    drive: "드라이브"
-    drive-desc: "이전에 올렸던 적 있는 이미지를 다시 올리고 싶을 때가 있지 않으신가요? 아니면 업로드했던 파일을 폴더로 정리하고 싶지 않으신가요? Misskey에 기본적으로 내장된 드라이브 기능으로 해결됩니다. 파일 공유도 쉽습니다."
-    outro: "이외에도 Misskey에만 있는 기능이 아직도 더 있으니 부디 여러분 자신의 눈으로 확인해보시기 바랍니다. Misskey는 분산형 SNS라서 이 인스턴스가 마음에 들지 않으신다면 다른 인스턴스를 시도해보실 수도 있습니다. 그럼, GLHF!"
-  application-authorization: "앱 연계"
-  close: "닫기"
-  do-not-copy-paste: "여기에 코드를 입력하거나 붙여넣지 마십시오. 계정이 무단으로 사용될 수 있습니다."
-  load-more: "더보기"
-  enter-password: "비밀번호를 입력하여 주십시오"
-  2fa: "2단계 인증"
-  customize-home: "홈 커스터마이징"
-  featured-notes: "하이라이트"
-  dark-mode: "다크 모드"
-  signin: "로그인"
-  signup: "신규 등록"
-  signout: "로그아웃"
-  reload-to-apply-the-setting: "이 설정을 적용하려면 페이지를 새로고침해야 합니다. 바로 새로고침하시겠습니까?"
-  fetching-as-ap-object: "연합에서 조회 중"
-  unfollow-confirm: "{name} 님을 팔로우 해제하시겠습니까?"
-  delete-confirm: "이 글을 삭제하시겠습니까?"
-  signin-required: "로그인 해주세요"
-  notification-type: "알림의 종류"
-  notification-types:
-    all: "모두"
-    pollVote: "투표"
-    follow: "팔로잉"
-    receiveFollowRequest: "팔로우 요청"
-    reply: "답글 달기"
-    quote: "인용"
-    renote: "리노트"
-    mention: "멘션"
-    reaction: "리액션"
-  got-it: "알겠습니다"
-  customization-tips:
-    title: "커스터마이징 도움말"
-    paragraph: "<p>홈 화면 커스터마이징에서는 위젯을 추가/삭제하거나 드래그 & 드롭하여 정렬하거나 할 수 있습니다.</p><p>일부 위젯은<strong><strong>오른쪽</strong>클릭</strong>을 하여 보기를 변경할 수 있습니다.</p><p>위젯을 삭제하려면 헤더의 <strong>\"휴지통\"</strong>이라고 적혀있는 영역에 창을 끌어넣습니다.</p><p>커스터마이징을 종료하려면 오른쪽 상단의 \"완료\" 버튼을 클릭해주세요.</p>"
-    gotit: "Got it!"
-  notification:
-    file-uploaded: "파일이 업로드되었습니다"
-    message-from: "{}님의 메시지:"
-    reversi-invited: "게임 초대가 있습니다"
-    reversi-invited-by: "{}님으로부터"
-    notified-by: "{}님으로부터"
-    reply-from: "{}님으로부터 답글:"
-    quoted-by: "{}님이 인용:"
-  time:
-    unknown: "알 수 없는 시간"
-    future: "미래"
-    just_now: "방금 전"
-    seconds_ago: "{}ì´ˆ ì „"
-    minutes_ago: "{}분 전"
-    hours_ago: "{}시간 전"
-    days_ago: "{}일 전"
-    weeks_ago: "{}주 전"
-    months_ago: "{}개월 전"
-    years_ago: "{}ë…„ ì „"
-  month-and-day: "{month}월 {day}일"
-  trash: "휴지통"
-  drive: "드라이브"
-  pages: "페이지"
-  messaging: "대화"
-  home: "홈"
-  deck: "덱"
-  timeline: "타임라인"
-  explore: "발견하기"
-  following: "팔로우 중"
-  followers: "팔로워"
-  favorites: "즐겨찾기"
-  permissions:
-    "read:account": "계정 정보 보기"
-    "write:account": "계정 정보 변경"
-    "read:blocks": "차단 보기"
-    "write:blocks": "차단 수정"
-    "read:drive": "드라이브 보기"
-    "write:drive": "드라이브 수정"
-    "read:favorites": "즐겨찾기 보기"
-    "write:favorites": "즐겨찾기 수정"
-    "read:following": "팔로우 정보 보기"
-    "write:following": "팔로잉, 팔로우 수정"
-    "read:messaging": "대화 보기"
-    "write:messaging": "대화 수정"
-    "read:mutes": "뮤트 보기"
-    "write:mutes": "뮤트 수정"
-    "write:notes": "글 작성, 삭제"
-    "read:notifications": "알림 보기"
-    "write:notifications": "알림 수정"
-    "read:reactions": "리액션 보기"
-    "write:reactions": "리액션 수정"
-    "write:votes": "투표하기"
-    "read:pages": "페이지 보기"
-    "write:pages": "페이지 변경"
-    "read:page-likes": "페이지의 좋아요 보기"
-    "write:page-likes": "페이지의 좋아요 변경"
-    "read:user-groups": "유저 그룹 보기"
-    "write:user-groups": "유저 그룹을 변경"
-  empty-timeline-info:
-    follow-users-to-make-your-timeline: "사용자를 팔로우하면 글이 타임라인에 표시됩니다."
-    explore: "사용자 탐색"
-  post-form:
-    attach-location-information: "위치 정보를 첨부합니다"
-    hide-contents: "내용 숨기기"
-    reply-placeholder: "이 글에 답글..."
-    quote-placeholder: "이 글을 인용..."
-    option-quote-placeholder: "이 글을 인용... (옵션)"
-    quote-attached: "인용함"
-    quote-question: "인용해서 작성하시겠습니까?"
-    submit: "글쓰기"
-    reply: "답글 달기"
-    renote: "리노트"
-    posting: "게시중"
-    attach-media-from-local: "PC에서 미디어 첨부"
-    attach-media-from-drive: "드라이브에서 미디어 첨부"
-    insert-a-kao: "v('ω')v"
-    create-poll: "투표 만들기"
-    text-remain: "{}문자 남음"
-    recent-tags: "최근"
-    local-only-message: "이 글은 로컬에만 공개되어 있습니다"
-    click-to-tagging: "클릭하여 태그 넣기"
-    visibility: "공개 범위"
-    geolocation-alert: "사용 중이신 장치에서는 위치 정보를 사용할 수 없습니다"
-    error: "오류"
-    enter-username: "사용자명을 입력해주세요"
-    specified-recipient: "수신인"
-    add-visible-user: "사용자 추가"
-    cw-placeholder: "내용에 대한 주석 (옵션)"
-    username-prompt: "사용자명을 입력해주세요"
-    enter-file-name: "파일 이름 수정"
-  weekday-short:
-    sunday: "일"
-    monday: "ì›”"
-    tuesday: "í™”"
-    wednesday: "수"
-    thursday: "목"
-    friday: "금"
-    saturday: "토"
-  weekday:
-    sunday: "일요일"
-    monday: "월요일"
-    tuesday: "화요일"
-    wednesday: "수요일"
-    thursday: "목요일"
-    friday: "금요일"
-    saturday: "토요일"
-  reactions:
-    like: "좋아요"
-    love: "죠아"
-    laugh: "ã…‹ã…‹ã…‹ã…‹"
-    hmm: "으음...?"
-    surprise: "와우"
-    congrats: "축하합니다"
-    angry: "화남"
-    confused: "곤-란"
-    rip: "RIP"
-    pudding: "Pudding"
-  note-visibility:
-    public: "공개"
-    home: "홈"
-    home-desc: "홈 타임라인에만 공개"
-    followers: "팔로워"
-    followers-desc: "자신의 팔로워에게만 공개"
-    specified: "다이렉트"
-    specified-desc: "지정한 사용자에게만 공개"
-    local-public: "공개 (로컬 한정)"
-    local-home: "홈 (로컬 한정)"
-    local-followers: "팔로워 (로컬 한정)"
-  note-placeholders:
-    a: "지금 무엇을 하고 있나요?"
-    b: "무슨 일이 일어나고 있나요?"
-    c: "무엇을 생각하고 있나요?"
-    d: "말하고 싶은 게 있나요?"
-    e: "여기에 적어주세요"
-    f: "작성해주시길 기다리고 있어요..."
-  settings: "설정"
-  _settings:
-    profile: "프로필"
-    notification: "알림"
-    apps: "앱"
-    tags: "해시태그"
-    mute-and-block: "뮤트/차단"
-    blocking: "차단"
-    security: "보안"
-    signin: "로그인 기록"
-    password: "비밀번호"
-    other: "기타"
-    appearance: "디자인"
-    behavior: "동작"
-    reactions: "리액션"
-    reactions-description: "리액션 선택창에 표시할 리액션을 줄바꿈으로 구분해 설정합니다."
-    fetch-on-scroll: "스크롤하여 자동으로 불러오기"
-    fetch-on-scroll-desc: "페이지를 아래로 스크롤하였을 때 자동으로 추가 콘텐츠를 불러옵니다."
-    note-visibility: "게시물의 공개 범위"
-    default-note-visibility: "기본 공개 범위"
-    remember-note-visibility: "글의 공개 범위를 기억하기"
-    web-search-engine: "웹 검색엔진"
-    web-search-engine-desc: "예: https://www.google.com/?#q={{query}}"
-    paste: "붙여넣기"
-    pasted-file-name: "붙여넣은 파일의 이름 템플릿"
-    pasted-file-name-desc: "예시: \"yyyy-MM-dd HH-mm-ss [{{number}}]\" → \"2018-03-20 21-30-24 1\""
-    paste-dialog: "붙여넣기 시 파일 이름을 편집"
-    paste-dialog-desc: "붙여넣기 시 파일 이름을 편집할 수 있도록 대화 상자를 표시합니다."
-    keep-cw: "CW 유지"
-    keep-cw-desc: "글에 답글을 달 때, 답글할 글에 CW가 설정되어 있는 경우 기본값으로 동일한 CW를 설정하도록 합니다."
-    i-like-sushi: "저는 (푸딩보다 차라리) 초밥이 좋아요"
-    show-reversi-board-labels: "리버시 보드의 행과 열 레이블을 표시"
-    use-avatar-reversi-stones: "리버시의 돌로 아바타를 사용"
-    disable-animated-mfm: "글의 문자 애니메이션을 비활성화"
-    disable-showing-animated-images: "움직이는 이미지를 자동으로 재생하지 않음"
-    enable-quick-notification-view: "알림의 빠른 보기를 사용합니다"
-    suggest-recent-hashtags: "최근 해시태그를 글 작성란에 표시"
-    always-show-nsfw: "항상 열람주의 미디어를 표시"
-    always-mark-nsfw: "항상 미디어를 열람주의로 설정하여 게시"
-    show-full-acct: "사용자명의 호스트를 표시하지 않기"
-    show-via: "via 표시하기"
-    reduce-motion: "UI의 애니메이션 줄이기"
-    this-setting-is-this-device-only: "이 장치만"
-    use-os-default-emojis: "운영체제의 기본 이모지 사용"
-    line-width: "선 두께"
-    line-width-thin: "얇음"
-    line-width-normal: "보통"
-    line-width-thick: "두꺼움"
-    font-size: "글씨 크기"
-    font-size-x-small: "작음"
-    font-size-small: "조금 작음"
-    font-size-medium: "보통"
-    font-size-large: "조금 큼"
-    font-size-x-large: "큼"
-    deck-column-align: "덱의 칼럼 위치"
-    deck-column-align-center: "가운데"
-    deck-column-align-left: "왼쪽"
-    deck-column-align-flexible: "플렉서블"
-    deck-column-width: "덱의 칼럼 폭"
-    deck-column-width-narrow: "좁음"
-    deck-column-width-narrower: "조금 좁음"
-    deck-column-width-normal: "보통"
-    deck-column-width-wider: "조금 넓음"
-    deck-column-width-wide: "넓음"
-    use-shadow: "UI에 그림자 효과 적용"
-    rounded-corners: "UI의 모서리를 둥글게 설정"
-    circle-icons: "원형 아바타를 사용"
-    contrasted-acct: "사용자명에 대비 추가"
-    wallpaper: "ë°°ê²½"
-    choose-wallpaper: "배경 설정"
-    delete-wallpaper: "배경 제거"
-    post-form-on-timeline: "타임라인 상단에 글 작성란을 표시"
-    show-clock-on-header: "오른쪽 상단에 시계 표시"
-    show-reply-target: "답글 대상 표시"
-    timeline: "타임라인"
-    show-my-renotes: "내 리노트를 타임라인에 보이기"
-    show-renoted-my-notes: "내 글이 리노트될 경우 타임라인에 보이기"
-    show-local-renotes: "로컬 글의 리노트를 타임라인에 보이기"
-    remain-deleted-note: "삭제된 글을 계속 표시"
-    sound: "소리"
-    enable-sounds: "소리 사용"
-    enable-sounds-desc: "글이나 메시지를 송수신하였을 때 소리를 재생합니다. 이 설정은 브라우저에 저장됩니다."
-    volume: "음량"
-    test: "테스트"
-    update: "Misskey Update"
-    version: "버전:"
-    latest-version: "최신 버전:"
-    update-checking: "업데이트 확인 중"
-    do-update: "업데이트 확인"
-    update-settings: "고급 설정"
-    no-updates: "사용 가능한 업데이트가 없습니다"
-    no-updates-desc: "사용중인 Misskey는 최신 버전입니다."
-    update-available: "새 버전을 사용할 수 있습니다"
-    update-available-desc: "페이지를 다시 로드하면 업데이트가 적용됩니다."
-    advanced-settings: "고급 설정"
-    debug-mode: "디버그 모드를 사용하도록 설정"
-    debug-mode-desc: "이 설정은 브라우저에 저장됩니다."
-    navbar-position: "내비게이션 막대 위치"
-    navbar-position-top: "위"
-    navbar-position-left: "왼쪽"
-    navbar-position-right: "오른쪽"
-    i-am-under-limited-internet: "저는 통신 대역폭이 제한되어 있습니다"
-    post-style: "글 표시 스타일"
-    post-style-standard: "표준"
-    post-style-smart: "스마트"
-    notification-position: "알림 표시"
-    notification-position-bottom: "아래"
-    notification-position-top: "위"
-    disable-via-mobile: "작성하는 글에 \"모바일에서 작성함\" 을 붙이지 않음"
-    load-raw-images: "첨부 이미지를 고품질로 표시"
-    load-remote-media: "원격 서버의 미디어를 표시"
-    sync: "동기화"
-    save: "저장"
-    saved: "저장하였습니다"
-    preview: "미리보기"
-    home-profile: "홈 프로필"
-    deck-profile: "덱 프로필"
-    room: "룸"
-    _room:
-      graphicsQuality: "그래픽 품질"
-      _graphicsQuality:
-        ultra: "최고"
-        high: "높음"
-        medium: "보통"
-        low: "낮음"
-        cheep: "최저"
-      useOrthographicCamera: "평행 투시 카메라를 사용"
-  search: "검색"
-  delete: "삭제"
-  loading: "로드 중"
-  ok: "ㅇㅇ"
-  cancel: "그만두기"
-  update-available-title: "업데이트가 있습니다"
-  update-available: "Misskey의 새로운 버전이 있습니다 ({newer}. 현재 {current}을 사용 중). 페이지를 다시 로드하면 업데이트가 적용됩니다."
-  my-token-regenerated: "당신의 토큰이 업데이트되었으므로 로그아웃합니다."
-  hide-password: "비밀번호 숨기기"
-  show-password: "비밀번호 표시"
-  enter-username: "사용자명을 입력해주세요"
-  do-not-use-in-production: "이것은 개발 빌드입니다. 프로덕션 환경에서 사용하지 마십시오."
-  user-suspended: "이 사용자는 정지된 상태입니다."
-  is-remote-user: "이 사용자 정보는 정확하지 않을 수 있습니다."
-  is-remote-post: "이 글 정보는 복사본입니다."
-  view-on-remote: "정확한 정보 보기"
-  renoted-by: "{user} 님이 리노트"
-  no-notes: "글이 없습니다"
-  turn-on-darkmode: "어둠에 삼켜져라"
-  turn-off-darkmode: "빛이 있으라"
-  error:
-    title: "오류가 발생했습니다"
-    retry: "다시 시도"
-  reversi:
-    drawn: "무승부"
-    my-turn: "당신의 차례입니다"
-    opponent-turn: "상대의 차례입니다"
-    turn-of: "{name}의 차례입니다"
-    past-turn-of: "{name}의 차례"
-    won: "{name}의 승리"
-    black: "흑"
-    white: "ë°±"
-    total: "합계"
-    this-turn: "{count}턴 째"
-  widgets:
-    analog-clock: "아날로그 시계"
-    profile: "프로필"
-    calendar: "달력"
-    timemachine: "달력(타임머신)"
-    activity: "활동"
-    rss: "RSS 리더"
-    memo: "스티커 메모"
-    trends: "트렌드"
-    photo-stream: "포토 스트림"
-    posts-monitor: "글 차트"
-    slideshow: "슬라이드 쇼"
-    version: "버전"
-    broadcast: "브로드캐스트"
-    notifications: "알림"
-    users: "추천 사용자"
-    polls: "투표"
-    post-form: "글 입력란"
-    server: "서버 정보"
-    nav: "내비게이션"
-    tips: "팁"
-    hashtags: "해시태그"
-    queue: "큐"
-  dev: "앱을 만드는 데 실패했습니다. 다시 시도하시기 바랍니다."
-  ai-chan-kawaii: "아이쨩 귀여워"
-  you: "당신"
-auth/views/form.vue:
-  share-access: "<i>{name}</i>가 당신의 계정에 엑세스하도록 허용하시겠습니까?"
-  permission-ask: "이 앱은 다음의 권한을 요청합니다:"
-  cancel: "취소"
-  accept: "접근 권한 허용"
-auth/views/index.vue:
-  loading: "로드 중"
-  denied: "애플리케이션의 연계를 취소하였습니다."
-  denied-paragraph: "이 앱이 당신의 계정에 액세스할 수 없습니다."
-  already-authorized: "이 앱은 이미 연결되어 있습니다."
-  allowed: "애플리케이션의 연동을 허용하였습니다."
-  callback-url: "애플리케이션으로 돌아갑니다."
-  please-go-back: "애플리케이션으로 돌아가여 시도하여 주십시오."
-  error: "세션이 존재하지 않습니다."
-  sign-in: "로그인 해주시기 바랍니다"
-common/views/pages/explore.vue:
-  pinned-users: "고정된 사용자"
-  popular-users: "인기 사용자"
-  recently-updated-users: "최근 게시한 사용자"
-  recently-registered-users: "신규 사용자"
-  recently-discovered-users: "최근 발견된 유저"
-  popular-tags: "인기 태그"
-  federated: "ì—°í•©"
-  explore: "{host}을(를) 탐색"
-  explore-fediverse: "연합 우주를 탐색"
-  users-info: "현재 {users} 사용자가 등록되어 있습니다"
-common/views/components/reactions-viewer.details.vue:
-  few-users: "{users}님이 {reaction} 리액션"
-  many-users: "{users}님 외 {omitted}명이 {reaction} 리액션"
-common/views/components/url-preview.vue:
-  enable-player: "플레이어 열기"
-  disable-player: "플레이어 닫기"
-common/views/components/user-list.vue:
-  no-users: "사용자가 없습니다"
-common/views/components/games/reversi/reversi.vue:
-  matching:
-    waiting-for: "{}을(를) 기다리고 있습니다"
-    cancel: "취소"
-common/views/components/games/reversi/reversi.game.vue:
-  surrender: "기권"
-  surrendered: "기권에 의해"
-  is-llotheo: "돌이 적은 쪽이 승리 (llotheo)"
-  looped-map: "루프 지도"
-  can-put-everywhere: "어디에도 둘 수 있는 모드"
-common/views/components/games/reversi/reversi.index.vue:
-  title: "Misskey Reversi"
-  sub-title: "다른 Misskey 사용자와 리버시로 대결하자"
-  invite: "초대"
-  rule: "게임 방법"
-  rule-desc: "리버시는 상대와 번갈아가며 돌을 판에 두고, 상대의 돌을 자신의 돌 사이에 두어 자신의 색으로 바꿔나가며, 최종적으로 남아있는 돌이 많은 쪽이 승리하는 보드게임입니다."
-  mode-invite: "초대"
-  mode-invite-desc: "지정한 사용자와 대결하는 모드입니다."
-  invitations: "게임 초대가 있습니다!"
-  my-games: "내 대국"
-  all-games: "모두의 대국"
-  enter-username: "사용자명을 입력해주세요"
-  game-state:
-    ended: "종료"
-    playing: "진행중"
-common/views/components/games/reversi/reversi.room.vue:
-  settings-of-the-game: "게임 설정"
-  choose-map: "맵 선택"
-  random: "랜덤"
-  black-or-white: "선수/후수"
-  black-is: "{}가 흑"
-  rules: "규칙"
-  is-llotheo: "돌이 적은 사람이 승리 (llotheo)"
-  looped-map: "루프 지도"
-  can-put-everywhere: "어디나 놓을 수 있음"
-  settings-of-the-bot: "Bot 설정"
-  this-game-is-started-soon: "게임이 몇 초 후에 시작됩니다"
-  waiting-for-other: "상대가 준비가 완료될 때까지 기다리고 있습니다"
-  waiting-for-me: "당신의 준비 완료를 기다리고 있습니다"
-  waiting-for-both: "준비중"
-  cancel: "취소"
-  ready: "준비 완료"
-  cancel-ready: "준비 취소"
-common/views/components/connect-failed.vue:
-  title: "서버에 연결할 수 없습니다"
-  description: "인터넷 회선에 문제가 있거나, 서버가 다운되었거나 점검중일 가능성이 있습니다. 잠시후에 {다시 시도}해 주십시오."
-  thanks: "항상 Misskey를 이용해주셔서 감사합니다."
-  troubleshoot: "트러블슈팅"
-common/views/components/connect-failed.troubleshooter.vue:
-  title: "트러블슈팅"
-  network: "네트워크 연결"
-  checking-network: "네트워크 연결 확인중"
-  internet: "인터넷 연결"
-  checking-internet: "인터넷 연결을 확인중"
-  server: "서버 연결"
-  checking-server: "서버 연결을 확인중"
-  finding: "문제를 확인하고 있습니다"
-  no-network: "네트워크에 연결되어 있지 않습니다"
-  no-network-desc: "사용중인 PC의 네트워크 연결이 정상적인지 확인해주십시오."
-  no-internet: "인터넷에 접속되어 있지 않습니다"
-  no-internet-desc: "네트워크에 연결되어 있으나 인터넷에 연결되어 있지 않은 것 같습니다. 사용중이신 PC의 인터넷 연결이 정상적인지 확인하여 주십시오."
-  no-server: "Misskey 서버에 연결할 수 없습니다"
-  no-server-desc: "사용중이신 PC의 인터넷 연결이 정상적이나 Misskey의 서버에 연결할 수 없었습니다. 서버가 다운되었거나 점검중일 가능성이 있으므로, 잠시 후에 다시 시도해주십시오."
-  success: "Misskey 서버에 연결하였습니다"
-  success-desc: "정상적으로 연결 가능한 것 같습니다. 페이지를 새로고침하여 주십시오."
-  flush: "캐시 삭제"
-  set-version: "버전 지정"
-common/views/components/media-banner.vue:
-  sensitive: "열람주의"
-  click-to-show: "클릭하여 표시"
-common/views/components/theme.vue:
-  theme: "테마"
-  light-theme: "다크 모드가 아닐 때 사용하는 테마"
-  dark-theme: "다크 모드일 때 사용하는 테마"
-  light-themes: "밝은 테마"
-  dark-themes: "어두운 테마"
-  install-a-theme: "테마 설치"
-  theme-code: "테마 코드"
-  install: "설치"
-  installed: "\"{}\"를 설치했습니다"
-  create-a-theme: "테마 만들기"
-  save-created-theme: "테마 저장"
-  primary-color: "기본 색"
-  secondary-color: "보조 색"
-  text-color: "글자 색상"
-  base-theme: "기본 테마"
-  base-theme-light: "밝음"
-  base-theme-dark: "어두움"
-  find-more-theme: "그 외 테마 찾아보기"
-  theme-name: "테마명"
-  preview-created-theme: "미리보기"
-  invalid-theme: "테마가 올바르지 않습니다."
-  already-installed: "이미 해당 테마가 설치되어 있습니다."
-  saved: "저장하였습니다"
-  manage-themes: "테마 관리"
-  builtin-themes: "표준 테마"
-  my-themes: "내 테마"
-  installed-themes: "설치된 테마"
-  select-theme: "테마를 선택하여 주십시오"
-  uninstall: "제거"
-  uninstalled: "\"{}\"을 제거하였습니다"
-  author: "작성자"
-  desc: "설명"
-  export: "내보내기"
-  import: "가져오기"
-  import-by-code: "또는 코드 붙여넣기"
-  theme-name-required: "테마명은 필수 항목입니다."
-common/views/components/cw-button.vue:
-  hide: "숨기기"
-  show: "더 보기"
-  chars: "{count}문자"
-  files: "{count}파일"
-  poll: "투표"
-common/views/components/messaging.vue:
-  search-user: "사용자 찾기"
-  you: "당신"
-  no-history: "기록이 없습니다"
-  user: "사용자"
-  group: "그룹"
-  start-with-user: "사용자와 대화 시작"
-  start-with-group: "그룹과 대화 시작"
-  select-group: "그룹을 선택하여 주십시오"
-common/views/components/messaging-room.vue:
-  not-talked-user: "이 사용자와의 대화가 없습니다"
-  not-talked-group: "이 그룹과의 대화가 없습니다"
-  no-history: "이것보다 과거의 기록이 없습니다"
-  new-message: "새 메시지가 있습니다"
-  only-one-file-attached: "메시지에 첨부할 수 있는 파일은 하나까지입니다"
-common/views/components/messaging-room.form.vue:
-  input-message-here: "여기에 메시지를 입력하세요"
-  send: "전송"
-  attach-from-local: "PC에서 파일 첨부"
-  attach-from-drive: "드라이브에서 파일 첨부"
-  only-one-file-attached: "메시지에 첨부할 수 있는 파일은 하나까지입니다"
-common/views/components/messaging-room.message.vue:
-  is-read: "읽음"
-  deleted: "이 메시지는 삭제되었습니다"
-common/views/components/nav.vue:
-  about: "Misskey에 대하여"
-  stats: "통계"
-  status: "상태"
-  wiki: "위키"
-  donors: "기증자"
-  repository: "저장소"
-  develop: "개발자"
-  feedback: "피드백"
-  tos: "이용 약관"
-common/views/components/note-menu.vue:
-  mention: "멘션"
-  detail: "상세"
-  copy-content: "내용 복사"
-  copy-link: "링크 복사"
-  favorite: "이 노트 즐겨찾기"
-  unfavorite: "즐겨찾기에서 제거"
-  watch: "지켜보기"
-  unwatch: "지켜보기 해제"
-  pin: "프로필에 고정"
-  unpin: "프로필에서 고정 해제"
-  delete: "삭제"
-  delete-confirm: "이 글을 삭제하시겠습니까?"
-  delete-and-edit: "삭제 후 편집"
-  delete-and-edit-confirm: "이 글을 삭제한 뒤 다시 편집하시겠습니까? 이 글에 대한 리액션, 리노트, 답글 또한 모두 삭제됩니다."
-  remote: "글 원본 보기"
-  pin-limit-exceeded: "더 이상 고정할 수 없습니다."
-common/views/components/user-menu.vue:
-  mention: "멘션"
-  mute: "뮤트"
-  unmute: "뮤트 해제"
-  mute-confirm: "이 사용자를 뮤트하시겠습니까?"
-  unmute-confirm: "이 사용자를 뮤트 해제하시겠습니까?"
-  block: "차단"
-  unblock: "차단 해제"
-  block-confirm: "이 사용자를 차단하시겠습니까?"
-  unblock-confirm: "이 사용자를 차단 해제하시겠습니까?"
-  push-to-list: "리스트에 추가"
-  select-list: "리스트를 선택하여 주십시오"
-  report-abuse: "스팸 신고"
-  report-abuse-detail: "어떤 스팸 행위를 하고 있습니까?"
-  report-abuse-reported: "관리자에게 보고되었습니다. 협조해주셔서 감사합니다."
-  silence: "침묵"
-  unsilence: "침묵 해제"
-  silence-confirm: "이 사용자를 침묵하시겠습니까?"
-  unsilence-confirm: "이 사용자를 침묵 해제하시겠습니까?"
-  suspend: "정지"
-  unsuspend: "정지 해제"
-  suspend-confirm: "이 사용자를 정지하시겠습니까?"
-  unsuspend-confirm: "이 사용자를 정지 해제하시겠습니까?"
-common/views/components/poll.vue:
-  vote-to: "\"{}\"에 투표하기"
-  vote-count: "{}표"
-  total-votes: "총 {}표"
-  vote: "투표하기"
-  show-result: "결과 보기"
-  voted: "투표함"
-  closed: "종료됨"
-  remaining-days: "종료까지 앞으로 {d}일 {h}시간"
-  remaining-hours: "종료까지 앞으로 {h}시간 {m}분"
-  remaining-minutes: "종료까지 앞으로 {m}분 {s}초"
-  remaining-seconds: "종료까지 앞으로 {s}초"
-common/views/components/poll-editor.vue:
-  no-only-one-choice: "투표에는 선택지가 최소한 두 개 필요합니다"
-  choice-n: "선택지 {}"
-  remove: "이 선택지를 제거"
-  add: "+선택지 추가"
-  destroy: "투표 제거"
-  multiple: "복수 응답 가능"
-  expiration: "기한"
-  infinite: "무기한"
-  at: "일시 지정"
-  after: "기간 지정"
-  no-more: "더 이상 추가할 수 없습니다"
-  deadline-date: "기한"
-  deadline-time: "시간"
-  interval: "기간"
-  unit: "단위"
-  second: "ì´ˆ"
-  minute: "분"
-  hour: "시간"
-  day: "일"
-common/views/components/reaction-picker.vue:
-  choose-reaction: "리액션 선택"
-  input-reaction-placeholder: "또는 이모지 입력"
-common/views/components/emoji-picker.vue:
-  recent-emoji: "최근 사용한 이모지"
-  custom-emoji: "커스텀 이모지"
-  no-category: "카테고리 없음"
-  people: "사람들"
-  animals-and-nature: "동물 & 자연"
-  food-and-drink: "음식 & 음료"
-  activity: "활동"
-  travel-and-places: "장소"
-  objects: "사물"
-  symbols: "기호"
-  flags: "깃발"
-common/views/components/settings/app-type.vue:
-  title: "모드"
-  intro: "데스크톱과 모바일 중 어떤 레이아웃을 사용할 지 지정할 수 있습니다."
-  choices:
-    auto: "자동으로 선택"
-    desktop: "데스크톱 레이아웃으로 고정"
-    mobile: "모바일 레이아웃으로 고정"
-  info: "변경사항은 페이지를 새로고침한 뒤에 반영됩니다."
-common/views/components/signin.vue:
-  username: "사용자명"
-  password: "비밀번호"
-  token: "토큰"
-  signing-in: "로그인 중입니다..."
-  or: "또는"
-  signin-with-twitter: "Twitter로 로그인"
-  signin-with-github: "GitHub으로 로그인"
-  signin-with-discord: "Discord로 로그인"
-  login-failed: "로그인할 수 없습니다. 사용자명과 비밀번호를 확인하여 주십시오."
-  tap-key: "보안 키를 클릭하여 로그인"
-  enter-2fa-code: "인증 코드를 입력하여 주십시오"
-common/views/components/signup.vue:
-  invitation-code: "초대 코드"
-  invitation-info: "초대 코드가 없으신 분은 <a href=\"{}\">관리자</a>에게 연락하시기 바랍니다."
-  username: "사용자명"
-  checking: "확인하는 중입니다..."
-  available: "사용 가능"
-  unavailable: "이미 사용중입니다"
-  error: "통신 오류"
-  invalid-format: "a~z, A~Z, 0-9, _를 사용할 수 있습니다"
-  too-short: "1자 이상 작성해야 합니다!"
-  too-long: "20글자 이하로 작성하여 주십시오"
-  password: "비밀번호"
-  password-placeholder: "8글자 이상을 권장합니다"
-  weak-password: "약한 비밀번호"
-  normal-password: "적당한 비밀번호"
-  strong-password: "강한 비밀번호"
-  retype: "다시 입력"
-  retype-placeholder: "확인을 위해 다시 입력하여 주십시오"
-  password-matched: "확인되었습니다"
-  password-not-matched: "일치하지 않습니다"
-  recaptcha: "자동 가입 방지"
-  agree-to: "{0}에 동의합니다."
-  tos: "이용 약관"
-  create: "계정 만들기"
-  some-error: "알 수 없는 이유로 계정 만들기에 실패했습니다. 다시 한번 시도해 주세요."
-common/views/components/special-message.vue:
-  new-year: "새해 복 많이 받으세요!"
-  christmas: "메리 크리스마스!"
-common/views/components/stream-indicator.vue:
-  connecting: "연결중"
-  reconnecting: "다시 연결 중"
-  connected: "연결 완료"
-common/views/components/notification-settings.vue:
-  title: "알림"
-  mark-as-read-all-notifications: "모든 알림을 읽은 상태로 표시"
-  mark-as-read-all-unread-notes: "모든 글을 읽은 상태로 표시"
-  mark-as-read-all-talk-messages: "모든 대화를 읽은 상태로 표시"
-  auto-watch: "글 자동 감시"
-  auto-watch-desc: "리액션, 답글, 게시물에 대한 알림을 자동으로 받을 수 있도록 합니다."
-common/views/components/integration-settings.vue:
-  title: "서비스 연계"
-  connect: "접속"
-  disconnect: "연결 끊기"
-  connected-to: "다음 계정에 연결되어 있습니다"
-common/views/components/github-setting.vue:
-  description: "사용중이신 Github 계정을 Misskey 계정에 연결하면 프로필에 Github 정보가 표시되고, Github를 사용하여 편리하게 로그인할 수 있습니다."
-  connected-to: "다음 Github 계정에 연결되어 있습니다"
-  detail: "자세히..."
-  reconnect: "다시 연결"
-  connect: "GitHub와 연결 하기"
-  disconnect: "연결 끊기"
-common/views/components/discord-setting.vue:
-  description: "사용중이신 Discord 계정을 Misskey 계정에 연결하면 프로필에 Discord 정보가 표시되고, Discord를 사용하여 편리하게 로그인할 수 있습니다."
-  connected-to: "다음 Discord 계정에 연결되어 있습니다"
-  detail: "자세히..."
-  reconnect: "다시 연결"
-  connect: "Discord와 연결하기"
-  disconnect: "연결 끊기"
-common/views/components/uploader.vue:
-  waiting: "기다리는 중"
-common/views/components/visibility-chooser.vue:
-  public: "공개"
-  home: "홈"
-  home-desc: "홈 타임라인에만 공개"
-  followers: "팔로워"
-  followers-desc: "자신의 팔로워에게만 공개"
-  specified: "다이렉트"
-  specified-desc: "지정한 사용자에게만 공개"
-  local-public: "공개 (로컬 한정)"
-  local-public-desc: "원격에는 공개하지 않음"
-  local-home: "홈 (로컬 한정)"
-  local-followers: "팔로워 (로컬 한정)"
-common/views/components/trends.vue:
-  count: "{}명이 언급함"
-  empty: "트렌드 없음"
-common/views/components/language-settings.vue:
-  title: "표시 언어"
-  pick-language: "언어 설정"
-  recommended: "추천"
-  auto: "자동"
-  specify-language: "언어 지정"
-  info: "변경사항은 페이지를 새로고침한 뒤에 반영됩니다."
-common/views/components/profile-editor.vue:
-  title: "프로필"
-  name: "이름"
-  account: "계정"
-  location: "장소"
-  description: "자기소개"
-  you-can-include-hashtags: "해시 태그를 포함할 수 있습니다."
-  language: "언어"
-  birthday: "생일"
-  avatar: "아바타"
-  banner: "배너"
-  is-cat: "이 계정은 Cat입니다"
-  is-bot: "이 계정은 Bot입니다"
-  is-locked: "팔로우를 수동으로 승인"
-  careful-bot: "Bot의 팔로우만 수동으로 승인"
-  auto-accept-followed: "팔로우중인 사용자로부터의 팔로우를 자동으로 승인"
-  advanced: "기타"
-  privacy: "프라이버시"
-  save: "저장"
-  saved: "프로필을 저장하였습니다"
-  uploading: "업로드 중"
-  upload-failed: "업로드에 실패하였습니다"
-  unable-to-process: "작업을 완료할 수 없습니다"
-  avatar-not-an-image: "아바타로 지정한 파일이 이미지 형식이 아닙니다"
-  banner-not-an-image: "배너로 지정한 파일이 이미지 형식이 아닙니다"
-  email: "메일 설정"
-  email-address: "메일 주소"
-  email-verified: "매일 주소가 확인되었습니다"
-  email-not-verified: "메일 주소가 확인되지 않았습니다. 받은 편지함을 확인하여 주시기 바랍니다."
-  export: "내보내기"
-  import: "가져오기"
-  export-and-import: "내보내기와 가져오기"
-  export-targets:
-    all-notes: "모든 글 데이터"
-    following-list: "팔로잉"
-    mute-list: "뮤트"
-    blocking-list: "차단"
-    user-lists: "리스트"
-  export-requested: "내보내기를 요청하였습니다. 이 작업은 시간이 걸릴 수 있습니다. 내보내기가 완료되면 드라이브에 파일이 추가됩니다."
-  import-requested: "가져오기를 요청하였습니다. 이 작업에는 시간이 걸릴 수 있습니다."
-  enter-password: "비밀번호를 입력하여 주십시오"
-  danger-zone: "위험한 설정"
-  delete-account: "계정 삭제"
-  account-deleted: "계정이 삭제되었습니다. 데이터가 사라질 때까지 시간이 걸릴 수 있습니다."
-  profile-metadata: "프로필 추가 정보"
-  metadata-label: "라벨"
-  metadata-content: "ë‚´ìš©"
-common/views/components/user-list-editor.vue:
-  users: "사용자"
-  rename: "리스트 이름 바꾸기"
-  delete: "리스트 삭제"
-  remove-user: "이 리스트에서 제거"
-  delete-are-you-sure: "리스트 \"$1\"을 삭제하시겠습니까?"
-  deleted: "삭제하였습니다"
-  add-user: "사용자 추가"
-common/views/components/user-group-editor.vue:
-  users: "멤버"
-  rename: "그룹명을 변경"
-  delete: "그룹을 삭제"
-  transfer: "그룹을 양도"
-  transfer-are-you-sure: "그룹 「$1」을 「@$2」 님에게 양도하시겠습니까?"
-  transferred: "그룹을 양도하였습니다"
-  remove-user: "이 그룹에서 삭제"
-  delete-are-you-sure: "그룹 「$1」을 삭제하시겠습니까?"
-  deleted: "삭제하였습니다"
-  invite: "초대"
-  invited: "초대를 보냈습니다"
-common/views/components/user-lists.vue:
-  user-lists: "리스트"
-  create-list: "리스트 만들기"
-  list-name: "리스트 이름"
-common/views/components/user-groups.vue:
-  user-groups: "그룹"
-  create-group: "그룹 만들기"
-  group-name: "그룹명"
-  owned-groups: "자신의 그룹"
-  joined-groups: "참여중인 그룹"
-  invites: "초대"
-  accept-invite: "참여"
-  reject-invite: "거부"
-common/views/widgets/broadcast.vue:
-  fetching: "확인중"
-  no-broadcasts: "공지사항이 없습니다"
-  have-a-nice-day: "좋은 하루 되세요!"
-  next: "다음"
-  prev: "이전"
-common/views/widgets/calendar.vue:
-  year: "{}ë…„"
-  month: "{}ì›”"
-  day: "{}일"
-  today: "오늘:"
-  this-month: "이번 달:"
-  this-year: "올해:"
-common/views/widgets/photo-stream.vue:
-  title: "포토 스트림"
-  no-photos: "사진이 없습니다"
-common/views/widgets/posts-monitor.vue:
-  title: "글 차트"
-  toggle: "보기 전환"
-common/views/widgets/hashtags.vue:
-  title: "해시태그"
-common/views/widgets/server.vue:
-  title: "서버 정보"
-  toggle: "보기 전환"
-common/views/widgets/memo.vue:
-  title: "스티커 메모"
-  memo: "여기에 써주세요!"
-  save: "저장"
-common/views/widgets/slideshow.vue:
-  folder-customize-mode: "폴더를 지정하려면 커스터마이징 모드를 종료하여 주십시오"
-  folder: "클릭하여 폴더를 지정하여 주십시오"
-  no-image: "이 폴더에 사진이 없습니다"
-common/views/widgets/tips.vue:
-  tips-line1: "<kbd>1</kbd>로 타임라인을 활성화(focus)할 수 있습니다"
-  tips-line2: "<kbd>p</kbd> 혹은<kbd>n</kbd> 키로 글 작성 폼을 열 수 있습니다"
-  tips-line3: "글에 파일을 끌어넣을 수 있습니다"
-  tips-line4: "글쓰기 영역에 클립보드에 있는 이미지를 붙여넣을 수 있습니다"
-  tips-line5: "드라이브에 파일을 끌어넣어 업로드 하실 수 있습니다"
-  tips-line6: "드라이브에서 파일을 끌어 폴더를 이동할 수 있습니다"
-  tips-line7: "드라이브에서 폴더를 끌어 폴더를 이동할 수 있습니다"
-  tips-line8: "홈을 설정에서 커스터마이징 할 수 있습니다"
-  tips-line9: "Misskey는 AGPLv3입니다"
-  tips-line10: "타임머신 위젯을 사용하면 간단하게 과거의 타임라인으로 거슬러올라갈 수 있습니다"
-  tips-line11: "글의 \"...\" 을 클릭하여 글을 사용자의 페이지에 고정할 수 있습니다"
-  tips-line13: "글에 첨부된 파일은 드라이브에 저장됩니다"
-  tips-line14: "홈의 커스터마이징에서 위젯을 마우스 오른쪽 클릭 하여 디자인을 변경할 수 있습니다"
-  tips-line17: "\"**\" 으로 텍스트를 감싸면 **강조표시**됩니다."
-  tips-line19: "몇몇 창은 브라우저 밖으로 분리할 수 있습니다"
-  tips-line20: "달력 위젯의 퍼센트는 경과된 비율을 나타냅니다"
-  tips-line21: "API를 사용하여 bot의 개발 등을 할 수 있습니다"
-  tips-line23: "아이 귀여워요 아이"
-  tips-line24: "Misskey는 2014년에 서비스를 시작했습니다"
-  tips-line25: "대응하는 브라우저인 경우 Misskey를 열어놓지 않아도 알림을 받을 수 있습니다"
-common/views/pages/not-found.vue:
-  page-not-found: "페이지를 찾을 수 없습니다"
-common/views/pages/follow.vue:
-  signed-in-as: "{}으로 로그인"
-  following: "팔로우 중"
-  follow: "팔로우"
-  request-pending: "팔로우 허가 대기중"
-  follow-processing: "팔로우 처리중"
-  follow-request: "팔로우 요청"
-common/views/pages/follow-requests.vue:
-  received-follow-requests: "팔로우 요청"
-  accept: "승인"
-  reject: "거부"
-desktop:
-  banner-crop-title: "배너로 표시할 부분을 선택"
-  banner: "배너"
-  uploading-banner: "새로운 배너를 업로드하고 있습니다"
-  banner-updated: "배너를 업데이트 했습니다"
-  choose-banner: "배너 이미지를 선택"
-  avatar-crop-title: "아바타로 표시할 부분을 선택"
-  avatar: "아바타"
-  uploading-avatar: "새로운 아바타를 업로드하고 있습니다"
-  avatar-updated: "아바타가 변경되었습니다"
-  choose-avatar: "아바타 이미지를 선택"
-  unable-to-process: "작업을 완료할 수 없습니다"
-  invalid-filetype: "이 형식의 파일은 지원되지 않습니다"
-desktop/views/components/activity.chart.vue:
-  total: "검은색 ... 전체"
-  notes: "파랑색 ... 노트"
-  replies: "빨강색 ... 답글"
-  renotes: "초록색 ... 리노트"
-desktop/views/components/activity.vue:
-  title: "활동"
-  toggle: "보기 전환"
-desktop/views/components/calendar.vue:
-  title: "{year}ë…„ {month}ì›”"
-  prev: "이전 달"
-  next: "다음 달"
-  go: "클릭하여 시간역행"
-desktop/views/components/choose-file-from-drive-window.vue:
-  chosen-files: "{count} 파일 선택중"
-  upload: "PC에서 드라이브에 파일을 업로드"
-  cancel: "취소"
-  ok: "확인"
-  choose-prompt: "파일 선택"
-desktop/views/components/choose-folder-from-drive-window.vue:
-  cancel: "취소"
-  ok: "확인"
-  choose-prompt: "폴더 선택"
-desktop/views/components/crop-window.vue:
-  skip: "자르기 건너뛰기"
-  cancel: "취소"
-  ok: "확인"
-desktop/views/components/drive-window.vue:
-  used: "사용중"
-desktop/views/components/drive.file.vue:
-  avatar: "아바타"
-  banner: "배너"
-  nsfw: "열람주의"
-  contextmenu:
-    rename: "이름 변경"
-    mark-as-sensitive: "열람주의로 설정"
-    unmark-as-sensitive: "열람주의 해제"
-    copy-url: "URL 복사"
-    download: "다운로드"
-    else-files: "기타"
-    set-as-avatar: "아바타로 설정"
-    set-as-banner: "배너로 설정"
-    open-in-app: "앱에서 열기"
-    add-app: "앱 추가"
-    rename-file: "파일 이름 변경"
-    input-new-file-name: "새 파일 이름을 입력해 주십시오"
-    copied: "복사 완료"
-    copied-url-to-clipboard: "URL을 클립보드에 복사하였습니다"
-desktop/views/components/drive.folder.vue:
-  upload-folder: "기본 업로드 위치"
-  unable-to-process: "작업을 완료할 수 없습니다"
-  circular-reference-detected: "대상 폴더가 이동할 폴더의 하위 폴더입니다."
-  unhandled-error: "알 수 없는 오류"
-  unable-to-delete: "삭제할 수 없습니다"
-  has-child-files-or-folders: "이 폴더는 비어있지 않기 때문에 삭제할 수 없습니다."
-  contextmenu:
-    move-to-this-folder: "이 폴더로 이동"
-    show-in-new-window: "새 창으로 보기"
-    rename: "이름 변경"
-    rename-folder: "폴더 이름 변경"
-    input-new-folder-name: "새 폴더 이름을 입력하여 주십시오"
-    else-folders: "기타"
-    set-as-upload-folder: "기본 업로드 위치로 설정"
-desktop/views/components/drive.vue:
-  search: "검색"
-  empty-draghover: "끌어놓으신 거 맞나요? 괜찮아요, 저는 귀여우니까요"
-  empty-drive: "드라이브에 아무것도 없습니다"
-  empty-drive-description: "오른쪽 클릭하여 \"파일 업로드\"를 선택하거나, 파일을 끌어넣어 업로드할 수 있습니다."
-  empty-folder: "이 폴더는 비어있습니다"
-  unable-to-process: "작업을 완료할 수 없습니다"
-  circular-reference-detected: "대상 폴더가 이동할 폴더의 하위 폴더입니다."
-  unhandled-error: "알 수 없는 오류"
-  url-upload: "URL 업로드"
-  url-of-file: "업로드 하려는 파일의 URL"
-  url-upload-requested: "업로드를 요청했습니다"
-  may-take-time: "업로드가 완료될 때까지 시간이 소요될 수 있습니다."
-  create-folder: "폴더 만들기"
-  folder-name: "폴더 이름"
-  contextmenu:
-    create-folder: "폴더 만들기"
-    upload: "파일 업로드"
-    url-upload: "URL에서 업로드"
-desktop/views/components/media-video.vue:
-  sensitive: "열람주의"
-  click-to-show: "클릭하여 표시"
-desktop/views/components/followers-window.vue:
-  followers: "{} 의 팔로워"
-desktop/views/components/followers.vue:
-  empty: "팔로워가 없는 것 같습니다."
-desktop/views/components/following-window.vue:
-  following: "{} 의 팔로우"
-desktop/views/components/following.vue:
-  empty: "팔로우중인 사용자가 없는 것 같습니다."
-desktop/views/components/game-window.vue:
-  game: "리버시"
-desktop/views/components/home.vue:
-  done: "완료"
-  add-widget: "위젯 추가:"
-  add: "추가"
-desktop/views/input-dialog.vue:
-  cancel: "취소"
-  ok: "확인"
-desktop/views/components/note-detail.vue:
-  private: "이 글은 비공개입니다"
-  deleted: "이 글은 삭제되었습니다"
-  location: "위치 정보"
-  renote: "리노트"
-  add-reaction: "리액션 추가"
-  undo-reaction: "리액션 취소"
-desktop/views/components/note.vue:
-  reply: "답글 달기"
-  renote: "리노트"
-  add-reaction: "리액션 추가"
-  undo-reaction: "리액션 취소"
-  detail: "상세"
-  private: "이 글은 비공개입니다"
-  deleted: "이 글은 삭제되었습니다"
-desktop/views/components/notes.vue:
-  error: "불러오지 못했습니다"
-  retry: "재시도"
-desktop/views/components/notifications.vue:
-  empty: "비었습니다!"
-desktop/views/components/post-form.vue:
-  posted: "게시하였습니다!"
-  replied: "답글을 달았습니다!"
-  reposted: "리노트 하였습니다!"
-  note-failed: "게시에 실패하였습니다"
-  reply-failed: "답글을 달지 못했습니다"
-  renote-failed: "리노트에 실패하였습니다"
-desktop/views/components/post-form-window.vue:
-  note: "새 글"
-  reply: "답글 달기"
-  attaches: "첨부: {} 미디어"
-  uploading-media: "{}개의 미디어를 업로드 중"
-desktop/views/components/progress-dialog.vue:
-  waiting: "대기중"
-desktop/views/components/renote-form.vue:
-  quote: "인용하기..."
-  cancel: "취소"
-  renote: "리노트"
-  renote-home: "리노트 (홈)"
-  reposting: "작업중입니다..."
-  success: "리노트 하였습니다!"
-  failure: "리노트에 실패하였습니다"
-desktop/views/components/renote-form-window.vue:
-  title: "이 글을 리노트하시겠습니까?"
-desktop/views/pages/user-following-or-followers.vue:
-  following: "{user}의 팔로잉"
-  followers: "{user}의 팔로워"
-desktop/views/components/settings.2fa.vue:
-  intro: "2단계 인증을 설정하면 로그인 하려면 비밀번호 외에도 미리 등록 해놓은 물리적 장치 (예를 들면 당신의 스마트 폰 등) 도 필요하게 되어 보안 수준을 보다 향상시킵니다."
-  detail: "자세히..."
-  url: "https://www.google.com/intl/ko/landing/2step/"
-  caution: "등록한 장치를 분실한 경우 Misskey에 로그인할 수 없게 되므로 주의하여 주십시오."
-  register: "장치 등록"
-  already-registered: "이미 설정이 완료되었습니다."
-  unregister: "설정 해제"
-  unregistered: "2단계 인증이 비활성화되었습니다."
-  enter-password: "비밀번호를 입력하여 주십시오"
-  authenticator: "먼저, 가지고 계신 장치에 Google Authenticator를 설치해야 합니다:"
-  howtoinstall: "설치 방법은 여기에 있습니다"
-  token: "토큰"
-  scan: "다음으로 표시되어 있는 QR 코드를 스캔합니다:"
-  done: "사용중이신 장치에 표시된 토큰을 입력해주시면 마무리됩니다:"
-  submit: "완료"
-  success: "설정이 완료되었습니다!"
-  failed: "설정에 실패했습니다. 토큰이 잘못되었는지 확인해주십시오."
-  info: "다음 로그인부터는 이와 동일하게 비밀번호에 더해 장치에 표시된 토큰을 입력합니다."
-  totp-header: "인증 앱"
-  security-key-header: "보안 키"
-  security-key: "보안을 강화하려면 FIDO2를 지원하는 하드웨어 보안 키를 사용하여 계정에 로그인할 수 있습니다. 로그인 시 등록하였던 보안 키 또는 인증 앱이 필요하게 됩니다."
-  last-used: "마지막 사용:"
-  activate-key: "클릭하여 보안 키를 활성화하여 주십시오"
-  security-key-name: "키 이름"
-  register-security-key: "키 등록 완료"
-  something-went-wrong: "으악! 키를 등록하는 도중 문제가 발생하였습니다:"
-  key-unregistered: "키가 등록되어 있지 않습니다"
-  use-password-less-login: "비밀번호 없는 로그인 사용"
-common/views/components/media-image.vue:
-  sensitive: "열람주의"
-  click-to-show: "클릭하여 보기"
-common/views/components/api-settings.vue:
-  intro: "API를 사용하려면 위의 토큰을 \"i\" 라는 키의 값으로 매개변수를 추가하여 요청합니다."
-  caution: "계정을 부정 사용할 가능성이 있으므로, 이 토큰은 제 3자에게 알려주지 마십시오 (앱 등에 붙여넣지 마십시오)."
-  regeneration-of-token: "만일 이 토큰이 유출되었거나 그럴 가능성이 있는 경우 토큰을 재생성할 수 있습니다."
-  regenerate-token: "토큰 재생성"
-  token: "Token:"
-  enter-password: "비밀번호를 입력하여 주십시오"
-  console:
-    title: "API 콘솔"
-    endpoint: "엔드포인트"
-    parameter: "매개변수"
-    credential-info: "\"i\" 패러미터는 자동으로 추가됩니다."
-    send: "전송"
-    sending: "응답을 기다리는 중"
-    response: "ê²°ê³¼"
-desktop/views/components/settings.apps.vue:
-  no-apps: "연결된 애플리케이션이 없습니다"
-common/views/components/drive-settings.vue:
-  max: "최대 용량"
-  in-use: "사용중"
-  stats: "통계"
-  default-upload-folder: "기본 업로드 폴더 위치"
-  default-upload-folder-name: "폴더"
-  change-default-upload-folder: "폴더 변경"
-common/views/components/mute-and-block.vue:
-  mute-and-block: "뮤트 및 차단"
-  mute: "뮤트"
-  block: "차단"
-  no-muted-users: "뮤트한 사용자가 없습니다"
-  no-blocked-users: "차단한 사용자가 없습니다"
-  word-mute: "단어 뮤트"
-  muted-words: "뮤트된 키워드"
-  muted-words-description: "공백으로 구분하는 경우 AND로 지정되며, 줄바꿈으로 구분하는 경우 OR로 지정됩니다"
-  unmute-confirm: "이 사용자를 뮤트 해제하시겠습니까?"
-  unblock-confirm: "이 사용자를 차단 해제하시겠습니까?"
-  save: "저장"
-common/views/components/password-settings.vue:
-  reset: "비밀번호 변경"
-  enter-current-password: "현재 비밀번호를 입력하여 주십시오"
-  enter-new-password: "새 비밀번호를 입력하여 주십시오"
-  enter-new-password-again: "다시 한 번 새 비밀번호를 입력하여 주십시오"
-  not-match: "새 비밀번호가 일치하지 않습니다"
-  changed: "비밀번호를 변경하였습니다"
-  failed: "비밀번호 변경을 실패하였습니다."
-common/views/components/post-form-attaches.vue:
-  attach-cancel: "첨부 취소"
-  mark-as-sensitive: "열람주의로 설정"
-  unmark-as-sensitive: "열람주의 해제"
-desktop/views/components/sub-note-content.vue:
-  private: "이 글은 비공개입니다"
-  deleted: "이 글은 삭제되었습니다"
-  media-count: "{}개의 미디어"
-  poll: "투표"
-desktop/views/components/settings.tags.vue:
-  title: "태그"
-  query: "쿼리 (생략 가능)"
-  add: "추가"
-  save: "저장"
-desktop/views/components/timeline.vue:
-  home: "홈"
-  local: "로컬"
-  hybrid: "소셜"
-  global: "글로벌"
-  mentions: "받은 멘션"
-  messages: "다이렉트 게시글"
-  list: "리스트"
-  hashtag: "해시태그"
-  add-tag-timeline: "해시태그 추가"
-  add-list: "리스트 추가"
-  list-name: "리스트 이름"
-desktop/views/components/ui.header.vue:
-  welcome-back: "돌아오신 걸 환영합니다."
-  adjective: "님"
-desktop/views/components/ui.header.account.vue:
-  profile: "프로필"
-  lists: "리스트"
-  groups: "그룹"
-  follow-requests: "팔로우 요청"
-  admin: "관리"
-  room: "룸"
-desktop/views/components/ui.header.nav.vue:
-  game: "게임"
-desktop/views/components/ui.header.notifications.vue:
-  title: "알림"
-desktop/views/components/ui.header.post.vue:
-  post: "새 글"
-desktop/views/components/ui.header.search.vue:
-  placeholder: "검색"
-desktop/views/components/user-preview.vue:
-  notes: "글"
-  following: "팔로잉"
-  followers: "팔로워"
-desktop/views/components/users-list.vue:
-  all: "모두"
-  iknow: "아는 사람"
-  fetching: "불러오는 중입니다"
-desktop/views/components/users-list-item.vue:
-  followed: "당신을 팔로우합니다"
-desktop/views/components/window.vue:
-  popout: "팝아웃"
-  close: "닫기"
-admin/views/index.vue:
-  dashboard: "대시보드"
-  instance: "인스턴스"
-  emoji: "커스텀 이모지"
-  moderators: "모더레이터"
-  users: "사용자"
-  federation: "ì—°í•©"
-  announcements: "공지사항"
-  abuse: "스팸 신고"
-  queue: "작업 대기열"
-  logs: "로그"
-  db: "데이터베이스"
-  back-to-misskey: "Misskey로 돌아가기"
-admin/views/db.vue:
-  tables: "테이블"
-  vacuum: "청소"
-  vacuum-info: "데이터베이스를 청소합니다. 데이터는 그대로인 채로 데이터의 사용량을 줄입니다. 일반적으로 이 작업은 자동으로 정기적으로 수행됩니다."
-  vacuum-exclamation: "청소를 수행하면 한동안 데이터베이스의 부하가 높아져 사용자 조작을 처리하지 못 할 수 있습니다."
-admin/views/dashboard.vue:
-  dashboard: "대시보드"
-  accounts: "계정"
-  notes: "글"
-  drive: "드라이브"
-  instances: "인스턴스"
-  this-instance: "이 인스턴스"
-  federated: "ì—°í•©"
-admin/views/queue.vue:
-  title: "큐"
-  remove-all-jobs: "모든 작업 제거"
-  jobs: "ìž‘ì—…"
-  queue: "큐"
-  domains:
-    deliver: "전송"
-    inbox: "수신"
-    db: "데이터베이스"
-    objectStorage: "오브젝트 스토리지"
-  state: "상태"
-  states:
-    active: "처리중"
-    delayed: "지연됨"
-    waiting: "대기열에 있음"
-  result-is-truncated: "결과는 생략되었습니다"
-  other-queues: "기타 큐"
-admin/views/logs.vue:
-  logs: "로그"
-  domain: "도메인"
-  level: "수준"
-  levels:
-    all: "ì „ì²´"
-    info: "ì •ë³´"
-    success: "성공"
-    warning: "경고"
-    error: "오류"
-    debug: "디버그"
-  delete-all: "모두 삭제"
-admin/views/abuse.vue:
-  title: "스팸 신고"
-  target: "대상"
-  reporter: "신고자"
-  details: "상세"
-  remove-report: "삭제"
-admin/views/instance.vue:
-  instance: "인스턴스"
-  instance-name: "인스턴스 이름"
-  instance-description: "인스턴스의 소개"
-  host: "관리자"
-  icon-url: "아이콘 URL"
-  logo-url: "로고 URL"
-  banner-url: "배너 이미지 URL"
-  error-image-url: "오류 이미지 URL"
-  languages: "인스턴스의 대상 언어"
-  languages-desc: "공백으로 구분하여 여러 개 설정할 수 있습니다."
-  tos-url: "이용약관 URL"
-  repository-url: "저장소 URL"
-  feedback-url: "피드백 URL"
-  maintainer-config: "관리자 정보"
-  maintainer-name: "관리자 이름"
-  maintainer-email: "관리자 연락처"
-  advanced-config: "그 외 설정"
-  note-and-tl: "글과 타임라인"
-  drive-config: "드라이브 설정"
-  use-object-storage: "오브젝트 스토리지를 사용"
-  object-storage-base-url: "URL"
-  object-storage-bucket: "버킷 이름"
-  object-storage-prefix: "프리픽스"
-  object-storage-endpoint: "엔드포인트"
-  object-storage-region: "리전"
-  object-storage-port: "포트"
-  object-storage-access-key: "액세스 키"
-  object-storage-secret-key: "시크릿 키"
-  object-storage-use-ssl: "SSL 사용"
-  object-storage-s3-info: "Amazon S3를 오브젝트 스토리지로 사용하는 경우의 「엔드포인트」와 「리전」의 설정값에 대해서는 {0}을 확인하여 주십시오."
-  object-storage-s3-info-here: "이곳"
-  object-storage-gcs-info: "Google Cloud Storage를 오브젝트 스토리지로 사용하는 경우, 「엔드포인트」는 storage.googleapis.com 으로 설정하고, 「리전」 란은 비웁니다."
-  cache-remote-files: "원격 파일을 캐시"
-  cache-remote-files-desc: "이 설정을 해지하면 원격 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 프라이버시 설정에서 직접 링크를 무효로 설정한 사용자에게는 파일이 보이지 않거나, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다. 보통은 이 설정을 사용하거나 아래의 원격 파일 프록시를 설정하는 것을 추천합니다."
-  proxy-remote-files: "원격 파일 프록시"
-  proxy-remote-files-desc: "이 설정을 사용하면, 저장되지 않았거나 용량 초과로 삭제된 원격 파일을 로컬에서 프록시하여 썸네일을 생성하게 됩니다."
-  local-drive-capacity-mb: "로컬 사용자 한 명당 드라이브 용량"
-  remote-drive-capacity-mb: "원격 사용자 한 명당 드라이브 용량"
-  mb: "메가바이트 단위"
-  recaptcha-config: "reCAPCHA 설정"
-  recaptcha-info: "reCAPCHA를 사용하도록 설정하는 경우 reCAPCHA 토큰을 확보해야 합니다. https://www.google.com/recaptcha/intro/ 에 접속하여 토큰을 가져와주십시오."
-  recaptcha-info2: "v3는지원하지 않습니다. v2를 사용하여 주십시오."
-  enable-recaptcha: "reCAPCHA 활성화"
-  recaptcha-site-key: "사이트 키"
-  recaptcha-secret-key: "시크릿 키"
-  recaptcha-preview: "미리보기"
-  hidden-tags: "숨긴 해시태그"
-  hidden-tags-info: "집계에서 제외할 해시태그를 줄 바꿈으로 구분하여 기술합니다."
-  external-service-integration-config: "외부 서비스 연계"
-  twitter-integration-config: "Twitter 연동 설정"
-  twitter-integration-info: "콜백 URL은 {url} 로 설정됩니다."
-  enable-twitter-integration: "트위터 연동 활성화"
-  twitter-integration-consumer-key: "Consumer key"
-  twitter-integration-consumer-secret: "Consumer secret"
-  github-integration-config: "Github 연동 설정"
-  github-integration-info: "콜백 URL은 {url} 로 설정됩니다."
-  enable-github-integration: "Github 연동 활성화"
-  github-integration-client-id: "Client ID"
-  github-integration-client-secret: "Client Secret"
-  discord-integration-config: "Discord 연동 설정"
-  discord-integration-info: "콜백 URL은 {url} 로 설정됩니다."
-  enable-discord-integration: "Discord 연동 활성화"
-  discord-integration-client-id: "Client ID"
-  discord-integration-client-secret: "Client Secret"
-  proxy-account-config: "프록시 계정 설정"
-  proxy-account-info: "프록시 계정은 특정 조건에서 사용자의 원격 팔로잉을 대행하는 계정입니다. 예를 들면, 사용자가 원격 사용자를 리스트에 넣었을 때 리스트에 삽입된 사용자를 아무도 팔로우하지 않으면 해당 사용자의 액티비티가 이 서버로 전송되지 않습니다. 그런 경우 대신 프록시 계정이 해당 사용자를 팔로우하도록 합니다."
-  proxy-account-username: "프록시 계정 사용자명"
-  proxy-account-username-desc: "프록시로 사용할 사용자의 사용자명을 지정하여 주십시오."
-  proxy-account-warn: "프록시 계정은 자동으로 생성되지 않으므로 해당 사용자명의 계정을 미리 생성해둬야 합니다."
-  max-note-text-length: "글의 최대 문자수"
-  disable-registration: "사용자 등록 비활성화"
-  disable-local-timeline: "로컬 타임라인 비활성화"
-  disable-global-timeline: "글로벌 타임라인 비활성화"
-  disabling-timelines-info: "이 타임라인들을 비활성화해도 관리자 및 모더레이터는 계속 사용할 수 있습니다."
-  enable-emoji-reaction: "리액션에 이모지를 사용할 수 있게 함"
-  use-star-for-reaction-fallback: "알 수 없는 리액션을 star로 대체하여 사용"
-  invite: "초대"
-  save: "저장"
-  saved: "저장하였습니다"
-  pinned-users: "고정된 사용자"
-  pinned-users-info: "고정해두고 싶은 사용자를 줄바꿈으로 구분하여 기술합니다."
-  email-config: "메일 서버 설정"
-  email-config-info: "메일 주소 확인 혹은 비밀번호 재설정에 사용 됩니다."
-  enable-email: "메일 발신 활성화"
-  email: "메일 주소"
-  smtp-secure: "SMTP 연결에 암시적으로 SSL/TLS를 사용"
-  smtp-secure-info: "STARTTLS를 사용 시 ON으로 합니다."
-  smtp-host: "SMTP 호스트"
-  smtp-port: "SMTP 포트"
-  smtp-auth: "SMTP 인증 수행"
-  smtp-user: "SMTP 사용자"
-  smtp-pass: "SMTP 비밀번호"
-  test-email: "테스트"
-  serviceworker-config: "ServiceWorker"
-  enable-serviceworker: "ServiceWorker 사용"
-  serviceworker-info: "푸시알림을 수행하려면 사용해야 합니다."
-  vapid-publickey: "VAPID 공개키"
-  vapid-privatekey: "VAPID 개인키"
-  vapid-info: "ServiceWorker를 사용하는 경우 VAPID 키 쌍을 생성해야 합니다. 셸에서 다음과 같이 합니다:"
-admin/views/charts.vue:
-  title: "차트"
-  per-day: "1일마다"
-  per-hour: "1시간마다"
-  federation: "ì—°í•©"
-  notes: "글"
-  users: "사용자"
-  drive: "드라이브"
-  network: "네트워크"
-  charts:
-    federation-instances: "인스턴스 수 증감"
-    federation-instances-total: "인스턴스 수 누계"
-    notes: "글 증감 (통합)"
-    local-notes: "글 증감 (로컬)"
-    remote-notes: "글 증감 (원격)"
-    notes-total: "글 누적"
-    users: "사용자 증감"
-    users-total: "사용자 누적"
-    active-users: "활성 사용자 수"
-    drive: "드라이브 사용량 증감"
-    drive-total: "드라이브 사용량 누적"
-    drive-files: "드라이브 파일 수 증감"
-    drive-files-total: "드라이브 파일 수 누적"
-    network-requests: "요청"
-    network-time: "응답시간"
-    network-usage: "통신량"
-admin/views/drive.vue:
-  operation: "ìž‘ì—…"
-  fileid-or-url: "파일 ID 또는 파일 URL"
-  file-not-found: "파일을 찾을 수 없습니다"
-  lookup: "조회"
-  sort:
-    title: "ì •ë ¬"
-    createdAtAsc: "업로드 날짜 오랜 순"
-    createdAtDesc: "업로드 날짜 최신순"
-    sizeAsc: "크기가 작은 순"
-    sizeDesc: "크기가 큰 순"
-  origin:
-    title: "출처"
-    combined: "로컬 + 리모트"
-    local: "로컬"
-    remote: "리모트"
-  delete: "삭제"
-  deleted: "삭제하였습니다"
-  mark-as-sensitive: "열람주의로 설정"
-  unmark-as-sensitive: "열람주의 해제"
-  marked-as-sensitive: "열람주의로 설정하였습니다"
-  unmarked-as-sensitive: "열람주의를 제거하였습니다"
-  clean-remote-files: "리모트 파일 캐시를 삭제"
-  clean-remote-files-are-you-sure: "정말 모든 리모트 파일의 캐시를 삭제하시겠습니까?"
-  clean-up: "청소"
-admin/views/users.vue:
-  operation: "ìž‘ì—…"
-  username-or-userid: "사용자명 혹은 사용자 ID"
-  user-not-found: "사용자를 찾을 수 없습니다"
-  lookup: "조회"
-  reset-password: "비밀번호 재설정"
-  reset-password-confirm: "비밀번호를 재설정하시겠습니까?"
-  password-updated: "비밀번호는 현재 \"{password}\" 입니다"
-  suspend: "정지"
-  suspend-confirm: "정지하시겠습니까?"
-  suspended: "정지하였습니다"
-  unsuspend: "정지 해제"
-  unsuspend-confirm: "정지를 해제하시겠습니까?"
-  unsuspended: "정지를 해제하였습니다"
-  make-silence: "침묵"
-  silence-confirm: "침묵으로 설정합니까?"
-  unmake-silence: "침묵 해제"
-  unsilence-confirm: "침묵 해제하시겠습니까?"
-  update-remote-user: "원격 사용자 정보 갱신"
-  remote-user-updated: "원격 사용자 정보를 갱신하였습니다"
-  delete-all-files: "모든 파일 삭제"
-  delete-all-files-confirm: "모든 파일을 삭제하시겠습니까?"
-  username: "사용자명"
-  host: "호스트"
-  users:
-    title: "사용자"
-    sort:
-      title: "ì •ë ¬"
-      createdAtAsc: "등록일이 오래된 순"
-      createdAtDesc: "등록일이 최신인 순"
-      updatedAtAsc: "수정일이 오래된 순"
-      updatedAtDesc: "수정일이 최신인 순"
-    state:
-      title: "상태"
-      all: "모두"
-      available: "이용 가능"
-      admin: "관리자"
-      moderator: "모더레이터"
-      adminOrModerator: "관리자+모더레이터"
-      silenced: "침묵됨"
-      suspended: "정지됨"
-    origin:
-      title: "위치 (오리진)"
-      combined: "로컬 + 원격"
-      local: "로컬"
-      remote: "원격"
-    createdAt: "등록 날짜"
-    updatedAt: "수정한 날짜"
-admin/views/moderators.vue:
-  add-moderator:
-    title: "모더레이터 등록"
-    add: "등록"
-    added: "모더레이터를 등록하였습니다"
-    remove: "해제"
-    removed: "모더레이터 등록을 해제했습니다"
-  logs:
-    title: "로그"
-    moderator: "모더레이터"
-    type: "ìž‘ì—…"
-    at: "일시"
-    info: "ì •ë³´"
-admin/views/emoji.vue:
-  add-emoji:
-    title: "이모지 등록"
-    name: "이모지 이름"
-    name-desc: "a~z 0~9 _ 의 문자를 사용할 수 있습니다."
-    category: "카테고리"
-    aliases: "별칭"
-    aliases-desc: "공백으로 구분하여 여러 개 설정할 수 있습니다."
-    url: "이모지 이미지 URL"
-    add: "추가"
-    info: "50KB 미만의 PNG 이미지를 추천합니다."
-    added: "이모지를 등록하였습니다"
-  emojis:
-    title: "이모지 목록"
-    update: "업데이트"
-    remove: "삭제"
-  updated: "업데이트 하였습니다"
-  remove-emoji:
-    are-you-sure: "\"$1\" 을 삭제하시겠습니까?"
-    removed: "삭제하였습니다"
-admin/views/announcements.vue:
-  announcements: "공지사항"
-  save: "저장"
-  remove: "삭제"
-  add: "추가"
-  title: "제목"
-  text: "ë‚´ìš©"
-  saved: "저장하였습니다"
-  _remove:
-    are-you-sure: "\"$1\" 을 삭제하시겠습니까?"
-    removed: "삭제하였습니다"
-admin/views/hashtags.vue:
-  hided-tags: "Hidden Tags"
-admin/views/federation.vue:
-  instance: "인스턴스"
-  host: "호스트"
-  notes: "글"
-  users: "사용자"
-  following: "팔로우 중"
-  followers: "팔로워"
-  caught-at: "등록 날짜"
-  status: "상태"
-  latest-request-sent-at: "마지막으로 요청을 전송한 시간"
-  latest-request-received-at: "마지막으로 요청을 받은 시간"
-  remove-all-following: "모든 팔로잉 해제"
-  remove-all-following-info: "{host}(으)로부터 모든 팔로잉을 해제합니다. 해당 인스턴스가 더 이상 존재하지 않게 된 경우 등에 실행하십시오."
-  delete-all-files: "파일을 모두 삭제"
-  block: "차단"
-  marked-as-closed: "폐쇄된 것으로 표시"
-  lookup: "조회"
-  instances: "ì—°í•©"
-  instance-not-registered: "해당 인스턴스가 등록되어 있지 않습니다"
-  sort: "ì •ë ¬"
-  sorts:
-    caughtAtAsc: "등록일이 오래된 순"
-    caughtAtDesc: "등록일이 최신인 순"
-    lastCommunicatedAtAsc: "마지막으로 요청을 주고받은 일시가 오래된 순"
-    lastCommunicatedAtDesc: "마지막으로 요청을 주고받은 일시가 빠른 순"
-    notesAsc: "글이 적은 순"
-    notesDesc: "글이 많은 순"
-    usersAsc: "사용자가 적은 순"
-    usersDesc: "사용자가 많은 순"
-    followingAsc: "팔로잉이 적은 순"
-    followingDesc: "팔로잉이 많은 순"
-    followersAsc: "팔로워가 적은 순"
-    followersDesc: "팔로워가 많은 순"
-    driveUsageAsc: "드라이브 사용량이 적은 순"
-    driveUsageDesc: "드라이브 사용량이 많은 순"
-    driveFilesAsc: "드라이브 파일 수가 적은 순"
-    driveFilesDesc: "드라이브 파일 수가 많은 순"
-  state: "상태"
-  states:
-    all: "모두"
-    blocked: "차단됨"
-    not-responding: "응답 없음"
-    marked-as-closed: "폐쇄된 것으로 표시됨"
-  result-is-truncated: "상위 {n}개를 표시하고 있습니다."
-  charts: "차트"
-  chart-srcs:
-    requests: "요청"
-    users: "사용자 증감"
-    users-total: "사용자 누적"
-    notes: "글 증감"
-    notes-total: "글 누적"
-    ff: "팔로잉/팔로워 증감"
-    ff-total: "팔로잉/팔로워 누적"
-    drive-usage: "드라이브 사용량 증감"
-    drive-usage-total: "드라이브 사용량 누적"
-    drive-files: "드라이브 파일 수 증감"
-    drive-files-total: "드라이브 파일 수 누적"
-  chart-spans:
-    hour: "1시간마다"
-    day: "1일마다"
-  blocked-hosts: "차단"
-  blocked-hosts-info: "차단할 호스트를 줄바꿈으로 구분하여 기술합니다."
-  save: "저장"
-desktop/views/pages/welcome.vue:
-  about: "자세히..."
-  timeline: "타임라인"
-  announcements: "공지사항"
-  photos: "최근 이미지"
-  powered-by-misskey: "Powered by <b>Misskey</b>."
-  info: "ì •ë³´"
-desktop/views/pages/drive.vue:
-  title: "Misskey Drive"
-desktop/views/pages/note.vue:
-  prev: "이전 글"
-  next: "다음 글"
-desktop/views/pages/selectdrive.vue:
-  title: "파일을 선택하여 주십시오"
-  ok: "확인"
-  cancel: "취소"
-  upload: "PC에서 드라이브에 파일을 업로드"
-desktop/views/pages/search.vue:
-  not-available: "검색 기능은 인스턴스 설정에서 비활성화되어 있습니다."
-  not-found: "\"{q}\" 와 일치하는 글을 찾을 수 없습니다."
-desktop/views/pages/tag.vue:
-  no-posts-found: "해시태그 \"{q}\"가 붙은 글을 찾을 수 없습니다."
-desktop/views/pages/user-list.users.vue:
-  users: "사용자"
-  add-user: "사용자 추가"
-  username: "사용자명"
-desktop/views/pages/user/user.followers-you-know.vue:
-  title: "아는 사람의 팔로워"
-  loading: "로드 중"
-  no-users: "아는 사람의 팔로워가 없습니다."
-desktop/views/pages/user/user.friends.vue:
-  title: "자주 언급되는 사용자"
-  loading: "로드 중"
-  no-users: "자주 언급되는 사용자가 없습니다"
-desktop/views/pages/user/user.photos.vue:
-  title: "사진"
-  loading: "로드 중"
-  no-photos: "사진이 없습니다"
-desktop/views/pages/user/user.header.vue:
-  posts: "글"
-  following: "팔로잉"
-  followers: "팔로워"
-  is-bot: "이 계정은 Bot입니다"
-  no-description: "자기소개가 없습니다"
-  years-old: "{age}세"
-  year: "ë…„"
-  month: "ì›”"
-  day: "일"
-  follows-you: "당신을 팔로우합니다"
-desktop/views/pages/user/user.timeline.vue:
-  default: "글"
-  with-replies: "글과 답글"
-  with-media: "미디어"
-  my-posts: "내 글"
-desktop/views/widgets/notifications.vue:
-  title: "알림"
-desktop/views/widgets/polls.vue:
-  title: "투표"
-  refresh: "새로고침"
-  nothing: "없습니다!"
-desktop/views/widgets/post-form.vue:
-  title: "글쓰기"
-  note: "글쓰기"
-  something-happened: "알 수 없는 문제로 글을 게시할 수 없습니다."
-desktop/views/widgets/profile.vue:
-  update-banner: "클릭하여 배너 변경"
-  update-avatar: "클릭하여 아바타 변경"
-desktop/views/widgets/trends.vue:
-  title: "트렌드"
-  refresh: "새로고침"
-  nothing: "없습니다!"
-desktop/views/widgets/users.vue:
-  title: "추천 사용자"
-  refresh: "새로고침"
-  no-one: "없습니다!"
-mobile/views/components/drive.vue:
-  used: "사용중"
-  folder-count: "폴더"
-  count-separator: ", "
-  file-count: "파일"
-  nothing-in-drive: "드라이브에 아무것도 없습니다"
-  folder-is-empty: "폴더가 비어있습니다"
-  folder-name: "폴더 이름"
-  here-is-root: "현재 경로는 루트 경로로 폴더가 아닙니다."
-  url-prompt: "업로드 하려는 파일의 URL"
-  uploading: "업로드를 요청하였습니다. 업로드가 완료될 때까지 시간이 소요될 수 있습니다."
-  folder-name-cannot-empty: "폴더 이름은 비워둘 수 없습니다."
-mobile/views/components/drive-file-chooser.vue:
-  select-file: "파일 선택"
-mobile/views/components/drive-folder-chooser.vue:
-  select-folder: "폴더 선택"
-mobile/views/components/drive.file.vue:
-  nsfw: "열람주의"
-mobile/views/components/drive.file-detail.vue:
-  download: "다운로드"
-  rename: "이름 변경"
-  move: "이동"
-  hash: "해시 (md5)"
-  exif: "EXIF"
-  nsfw: "열람주의"
-  mark-as-sensitive: "열람주의로 설정"
-  unmark-as-sensitive: "열람주의 해제"
-mobile/views/components/media-video.vue:
-  sensitive: "열람주의"
-  click-to-show: "클릭하여 표시"
-common/views/components/follow-button.vue:
-  following: "팔로우 중"
-  follow: "팔로우"
-  request-pending: "팔로우 허가 대기중"
-  follow-processing: "팔로우 처리중"
-  follow-request: "팔로우 요청"
-mobile/views/components/note.vue:
-  private: "이 글은 비공개입니다"
-  deleted: "이 글은 삭제되었습니다"
-  location: "위치 정보"
-mobile/views/components/note-detail.vue:
-  reply: "답글 달기"
-  reaction: "리액션"
-  private: "이 글은 비공개입니다"
-  deleted: "이 글은 삭제되었습니다"
-  location: "위치 정보"
-mobile/views/components/note-preview.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "cat"
-mobile/views/components/note-sub.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "cat"
-mobile/views/components/notifications.vue:
-  empty: "없습니다!"
-mobile/views/components/sub-note-content.vue:
-  private: "이 글은 비공개입니다"
-  deleted: "이 글은 삭제되었습니다"
-  media-count: "{}개의 미디어"
-  poll: "투표"
-mobile/views/components/ui.header.vue:
-  welcome-back: "돌아오신 걸 환영합니다."
-  adjective: "님"
-mobile/views/components/ui.nav.vue:
-  timeline: "타임라인"
-  notifications: "알림"
-  follow-requests: "팔로우 요청"
-  search: "검색"
-  user-lists: "리스트"
-  user-groups: "그룹"
-  widgets: "위젯"
-  game: "게임"
-  admin: "관리"
-  about: "Misskey에 대하여"
-mobile/views/pages/drive.vue:
-  contextmenu:
-    upload: "파일 업로드"
-    url-upload: "파일을 URL로부터 업로드"
-    create-folder: "폴더 만들기"
-    rename-folder: "폴더 이름 바꾸기"
-    move-folder: "이 폴더를 이동"
-    delete-folder: "이 폴더를 삭제"
-mobile/views/pages/signup.vue:
-  lets-start: "📦 이제 시작해도 됩니다"
-mobile/views/pages/followers.vue:
-  followers-of: "{name}의 팔로워"
-mobile/views/pages/following.vue:
-  following-of: "{name}의 팔로잉"
-mobile/views/pages/home.vue:
-  home: "홈"
-  local: "로컬"
-  hybrid: "소셜"
-  global: "글로벌"
-  mentions: "받은 멘션"
-  messages: "다이렉트 게시글"
-mobile/views/pages/tag.vue:
-  no-posts-found: "해시태그 \"{q}\"가 붙은 글을 찾을 수 없습니다."
-mobile/views/pages/widgets.vue:
-  dashboard: "대시보드"
-  widgets-hints: "위젯을 추가 / 제거하거나 정렬할 수 있습니다. 위젯을 이동하려면 창틀의 \"☰\" 아이콘을 드래그합니다. 위젯을 삭제하려면 \"X\" 아이콘을 탭 합니다. 몇몇 위젯은 탭하면 표시형식을 바꿀 수 있습니다."
-  add-widget: "추가"
-  customization-tips: "커스터마이징 도움말"
-mobile/views/pages/widgets/activity.vue:
-  activity: "활동"
-mobile/views/pages/share.vue:
-  share-with: "{name}(으)로 공유"
-mobile/views/pages/note.vue:
-  title: "글"
-  prev: "이전 글"
-  next: "다음 글"
-mobile/views/pages/games/reversi.vue:
-  reversi: "리버시"
-mobile/views/pages/search.vue:
-  search: "검색"
-  not-found: "\"{q}\" 와 일치하는 글을 찾을 수 없습니다."
-mobile/views/pages/selectdrive.vue:
-  select-file: "파일 선택"
-mobile/views/pages/notifications.vue:
-  notifications: "알림"
-mobile/views/pages/settings.vue:
-  signed-in-as: "{}(으)로 로그인"
-mobile/views/pages/user.vue:
-  follows-you: "당신을 팔로우합니다"
-  following: "팔로잉"
-  followers: "팔로워"
-  notes: "글"
-  overview: "요약"
-  timeline: "타임라인"
-  media: "미디어"
-  years-old: "{age}세"
-mobile/views/pages/user/home.vue:
-  recent-notes: "최근 글"
-  images: "이미지"
-  activity: "활동"
-  keywords: "키워드"
-  domains: "자주 보이는 도메인"
-  frequently-replied-users: "자주 언급되는 사용자"
-  followers-you-know: "아는 사람의 팔로워"
-  last-used-at: "마지막 로그인"
-mobile/views/pages/user/home.photos.vue:
-  no-photos: "사진이 없습니다"
-deck:
-  widgets: "위젯"
-  home: "홈"
-  local: "로컬"
-  hybrid: "소셜"
-  hashtag: "해시태그"
-  global: "글로벌"
-  mentions: "받은 멘션"
-  direct: "다이렉트 게시글"
-  notifications: "알림"
-  list: "리스트"
-  select-list: "리스트를 선택하여 주십시오"
-  swap-left: "왼쪽으로 이동"
-  swap-right: "오른쪽으로 이동"
-  swap-up: "위로 이동"
-  swap-down: "아래로 이동"
-  remove: "칼럼 제거"
-  add-column: "칼럼 추가"
-  rename: "이름 변경"
-  stack-left: "왼쪽에 쌓기"
-  pop-right: "오른쪽으로 빼기"
-  disabled-timeline:
-    title: "비활성화된 타임라인"
-    description: "서버 운영자에 의해 이 타임라인이 사용할 수 없도록 설정되어 있습니다."
-deck/deck.tl-column.vue:
-  is-media-only: "미디어가 달린 글만"
-  edit: "옵션"
-deck/deck.user-column.vue:
-  follows-you: "당신을 팔로우합니다"
-  posts: "글"
-  following: "팔로잉"
-  followers: "팔로워"
-  images: "이미지"
-  activity: "활동"
-  timeline: "타임라인"
-  pinned-notes: "고정해놓은 글"
-  pinned-page: "고정해놓은 페이지"
-docs:
-  edit-this-page-on-github: "틀린 점이나 개선할 점을 찾으셨나요?"
-  edit-this-page-on-github-link: "이 페이지를 GitHub에서 편집"
-dev/views/index.vue:
-  manage-apps: "앱 관리"
-dev/views/apps.vue:
-  manage-apps: "앱 관리"
-  create-app: "앱 생성"
-  app-missing: "앱 없음"
-dev/views/new-app.vue:
-  new-app: "새 애플리케이션"
-  new-app-info: "애플리케이션은 API에서도 생성할 수 있습니다. (app/create)"
-  create-app: "애플리케이션 생성"
-  app-name: "애플리케이션 이름"
-  app-name-placeholder: "ex) Misskey for iOS"
-  app-name-desc: "앱의 이름."
-  app-overview: "앱 개요"
-  app-overview-placeholder: "ex) Misskey iOS 클라이언트."
-  app-overview-desc: "애플리케이션에 대한 간단한 설명이나 소개"
-  callback-url: "콜백 URL (옵션)"
-  callback-url-placeholder: "ex) https://your.app.example.com/callback.php"
-  callback-url-desc: "사용자가 인증 폼에서 인증한 뒤 리다이렉트할 URL을 설정합니다."
-  authority: "권한"
-  authority-desc: "이곳에서 요청한 권한에 한정하여 API로 액세스할 수 있습니다."
-  authority-warning: "앱을 생성한 뒤에도 변경할 수 있지만, 새로운 권한을 설정하는 경우 그 시점부터 예전에 발급받았던 유저 키는 모두 무효화됩니다."
-pages:
-  new-page: "페이지 만들기"
-  edit-page: "페이지 수정"
-  read-page: "소스 표시중"
-  page-created: "페이지를 만들었습니다"
-  page-updated: "페이지를 수정했습니다"
-  name-already-exists: "지정한 페이지 URL은 이미 존재합니다"
-  title-invalid-name: "유효하지 않은 페이지 URL입니다"
-  text-invalid-name: "비어있지 않은지 확인해주세요"
-  are-you-sure-delete: "이 페이지를 삭제하시겠습니까?"
-  page-deleted: "페이지가 삭제되었습니다"
-  edit-this-page: "이 페이지를 편집"
-  pin-this-page: "프로필에 고정"
-  unpin-this-page: "프로필에서 고정 해제"
-  view-source: "소스 보기"
-  view-page: "페이지 보기"
-  like: "좋아요"
-  unlike: "좋아요 해제"
-  liked-pages: "좋아요한 페이지"
-  my-pages: "내 페이지"
-  inspector: "인스펙터"
-  content: "페이지 블록"
-  variables: "변수"
-  variables-info: "변수를 사용하면 동적인 페이지를 만들 수 있습니다. 텍스트에 <b>{ 변수명 }</b>을 적으면 그 위치에 변수의 값을 집어넣습니다. 예를 들자면 <b>Hello { thing } world!</b> 라는 텍스트가 있을 때, 변수(thing)의 값이 <b>ai</b>인 경우 텍스트는 <b>Hello ai world!</b>가 됩니다."
-  variables-info2: "변수의 평가(값을 산출해내는 것)는 위에서부터 아래로 진행되므로 어떤 변수의 내부에서 자신보다 아래에 있는 변수를 참조할 수는 없습니다. 예를 들자면 위에서부터 <b>A, B, C</b>의 3개의 변수가 정의되어 있을 때, <b>C</b>의 내부에 <b>A</b>나 <b>B</b>를 참조할 수는 있지만, <b>A</b>의 내부에서 <b>B</b>나 <b>C</b>를 참조할 수는 없습니다."
-  variables-info3: "사용자로부터 입력을 받으려면, 페이지에 「사용자 입력」 블록을 삽입하고 「변수명」에 입력받은 값을 저장하고 싶은 변수명을 설정합니다 (변수는 자동으로 생성됩니다). 그 변수를 사용하여 사용자 입력에 따라 동작할 수 있습니다."
-  variables-info4: "함수를 사용하면 반복되는 작업을 손쉽게 처리할 수 있습니다. 함수를 만드시려면 「함수」 타입의 변수를 만듭니다. 함수에서 슬롯(인수)를 받도록 설정하면, 함수를 사용할 때 슬롯에 입력된 값을 함수 안에서 변수로써 이용할 수 있게 됩니다. 또한, AiScript 표준에는 함수를 인수로 받는 함수(고차함수)도 존재합니다. 함수를 미리 정의하는 것 외에, 이와 같은 고차함수를 즉석으로 설정할 수 있습니다."
-  more-details: "자세한 설명"
-  title: "제목"
-  url: "페이지 URL"
-  summary: "페이지 요약"
-  align-center: "가운데 정렬"
-  hide-title-when-pinned: "프로필에 고정해놓은 경우 타이틀을 표시하지 않음"
-  font: "글꼴"
-  fontSerif: "세리프"
-  fontSansSerif: "산 세리프"
-  set-eye-catching-image: "아이캐치 이미지를 설정"
-  remove-eye-catching-image: "아이캐치 이미지를 삭제"
-  choose-block: "블록 추가"
-  select-type: "종류 선택"
-  enter-variable-name: "변수명을 설정해주십시오"
-  the-variable-name-is-already-used: "그 변수명은 이미 사용중입니다"
-  content-blocks: "콘텐츠"
-  input-blocks: "ìž…ë ¥"
-  special-blocks: "특수"
-  post-from-post-form: "이 내용을 올리기"
-  posted-from-post-form: "게시하였습니다"
-  blocks:
-    text: "텍스트"
-    textarea: "텍스트 영역"
-    section: "섹션"
-    image: "이미지"
-    button: "버튼"
-    if: "만약"
-    _if:
-      variable: "변수"
-    post: "글 입력란"
-    _post:
-      text: "ë‚´ìš©"
-    textInput: "텍스트 입력"
-    _textInput:
-      name: "변수명"
-      text: "제목"
-      default: "기본값"
-    textareaInput: "여러 줄 텍스트 입력"
-    _textareaInput:
-      name: "변수명"
-      text: "제목"
-      default: "기본값"
-    numberInput: "수치 입력"
-    _numberInput:
-      name: "변수명"
-      text: "제목"
-      default: "기본값"
-    switch: "스위치"
-    _switch:
-      name: "변수명"
-      text: "제목"
-      default: "기본값"
-    counter: "ì¹´ìš´í„°"
-    _counter:
-      name: "변수명"
-      text: "제목"
-      inc: "증가치"
-    _button:
-      text: "제목"
-      colored: "색상"
-      action: "버튼을 눌렀을 때의 동작"
-      _action:
-        dialog: "대화상자를 표시"
-        _dialog:
-          content: "ë‚´ìš©"
-        resetRandom: "난수를 초기화"
-        pushEvent: "이벤트 보내기"
-        _pushEvent:
-          event: "이벤트 이름"
-          message: "눌렀을 때 표시할 메시지"
-          variable: "보낼 변수"
-          no-variable: "없음"
-    radioButton: "선택지"
-    _radioButton:
-      name: "변수명"
-      title: "제목"
-      values: "줄바꿈으로 구분된 선택지"
-      default: "기본값"
-  script:
-    categories:
-      flow: "흐름 제어"
-      logical: "논리 연산"
-      operation: "계산"
-      comparison: "비교"
-      random: "랜덤"
-      value: "ê°’"
-      fn: "함수"
-      text: "텍스트 조작"
-      convert: "변환"
-      list: "리스트"
-    blocks:
-      text: "텍스트"
-      multiLineText: "텍스트 (여러줄)"
-      textList: "텍스트 목록"
-      _textList:
-        info: "각각을 줄 바꿈으로 구분해주십시오"
-      strLen: "텍스트의 길이"
-      _strLen:
-        arg1: "텍스트"
-      strPick: "문자 추출"
-      _strPick:
-        arg1: "텍스트"
-        arg2: "문자 위치"
-      strReplace: "텍스트 치환"
-      _strReplace:
-        arg1: "텍스트"
-        arg2: "치환 전"
-        arg3: "치환 후"
-      strReverse: "텍스트 뒤집기"
-      _strReverse:
-        arg1: "텍스트"
-      join: "텍스트 접합"
-      _join:
-        arg1: "리스트"
-        arg2: "구분자"
-      add: "+ 더하기"
-      _add:
-        arg1: "A"
-        arg2: "B"
-      subtract: "- 빼기"
-      _subtract:
-        arg1: "A"
-        arg2: "B"
-      multiply: "× 곱하기"
-      _multiply:
-        arg1: "A"
-        arg2: "B"
-      divide: "÷ 나누기"
-      _divide:
-        arg1: "A"
-        arg2: "B"
-      mod: "÷ 나눈 나머지"
-      _mod:
-        arg1: "A"
-        arg2: "B"
-      round: "소수점을 반올림"
-      _round:
-        arg1: "수치"
-      eq: "A와 B가 동일"
-      _eq:
-        arg1: "A"
-        arg2: "B"
-      notEq: "A와 B가 다름"
-      _notEq:
-        arg1: "A"
-        arg2: "B"
-      and: "A 그리고 B"
-      _and:
-        arg1: "A"
-        arg2: "B"
-      or: "A 혹은 B"
-      _or:
-        arg1: "A"
-        arg2: "B"
-      lt: "< A가 B보다 작음"
-      _lt:
-        arg1: "A"
-        arg2: "B"
-      gt: "> A가 B보다 큼"
-      _gt:
-        arg1: "A"
-        arg2: "B"
-      ltEq: "<= A가 B보다 작거나 같음"
-      _ltEq:
-        arg1: "A"
-        arg2: "B"
-      gtEq: ">= A가 B보다 크거나 같음"
-      _gtEq:
-        arg1: "A"
-        arg2: "B"
-      if: "분기"
-      _if:
-        arg1: "만약"
-        arg2: "그러면"
-        arg3: "그렇지 않으면"
-      not: "부정"
-      _not:
-        arg1: "부정"
-      random: "랜덤"
-      _random:
-        arg1: "확률"
-      rannum: "난수"
-      _rannum:
-        arg1: "최소"
-        arg2: "최대"
-      randomPick: "목록에서 임의로 선택"
-      _randomPick:
-        arg1: "리스트"
-      dailyRandom: "랜덤 (하루동안 결과 유지)"
-      _dailyRandom:
-        arg1: "확률"
-      dailyRannum: "난수 (하루동안 결과 유지)"
-      _dailyRannum:
-        arg1: "최소"
-        arg2: "최대"
-      dailyRandomPick: "목록에서 임의로 선택 (하루동안 결과 유지)"
-      _dailyRandomPick:
-        arg1: "리스트"
-      seedRandom: "무작위 (시드)"
-      _seedRandom:
-        arg1: "시드"
-        arg2: "확률"
-      seedRannum: "난수 (시드)"
-      _seedRannum:
-        arg1: "시드"
-        arg2: "최소"
-        arg3: "최대"
-      seedRandomPick: "목록에서 무작위로 선택 (시드)"
-      _seedRandomPick:
-        arg1: "시드"
-        arg2: "리스트"
-      DRPWPM: "확률형 목록에서 임의로 선택 (하루동안 결과 유지)"
-      _DRPWPM:
-        arg1: "텍스트 목록"
-      pick: "목록에서 선택"
-      _pick:
-        arg1: "리스트"
-        arg2: "위치"
-      listLen: "리스트의 길이 가져오기"
-      _listLen:
-        arg1: "리스트"
-      number: "수치"
-      stringToNumber: "텍스트를 수치로"
-      _stringToNumber:
-        arg1: "텍스트"
-      numberToString: "수치를 텍스트로"
-      _numberToString:
-        arg1: "수치"
-      splitStrByLine: "텍스트를 행 단위로 분할"
-      _splitStrByLine:
-        arg1: "텍스트"
-      ref: "변수"
-      fn: "함수"
-      _fn:
-        slots: "슬롯"
-        slots-info: "각 슬롯을 줄 바꿈으로 구분하여 주십시오"
-        arg1: "출력"
-      for: "반복"
-      _for:
-        arg1: "횟수"
-        arg2: "처리"
-    typeError: "슬롯 {slot}은 \"{expect}\"를 사용할 수 있지만 \"{actual}이 들어있습니다!"
-    thereIsEmptySlot: "슬롯 {slot}이(가) 비었습니다!"
-    types:
-      string: "텍스트"
-      number: "수치"
-      boolean: "플래그"
-      array: "리스트"
-      stringArray: "텍스트 목록"
-    emptySlot: "빈 슬롯"
-    enviromentVariables: "환경 변수"
-    pageVariables: "페이지 요소"
-    argVariables: "입력 슬롯"
-room:
-  add-furniture: "가구를 배치"
-  translate: "이동"
-  rotate: "회전"
-  exit: "선택 해제"
-  remove: "치우기"
-  save: "저장"
-  saved: "저장하였습니다"
-  clear: "모두 치우기"
-  clear-confirm: "정말 방 안의 모든 가구를 치우시겠습니까?"
-  leave-confirm: "저장되지 않은 변경 사항이 있습니다. 정말 나가시겠습니까?"
-  chooseImage: "이미지 선택"
-  room-type: "룸 종류"
-  carpet-color: "바닥 색상"
-  rooms:
-    default: "기본"
-    washitsu: "일본식"
-  furnitures:
-    milk: "우유 팩"
-    bed: "침대"
-    low-table: "낮은 테이블"
-    desk: "책상"
-    chair: "의자"
-    chair2: "의자 2"
-    fan: "환기구"
-    pc: "컴퓨터"
-    plant: "관엽식물"
-    plant2: "관엽식물 2"
-    eraser: "지우개"
-    pencil: "ì—°í•„"
-    pudding: "푸딩"
-    cardboard-box: "골판지 상자"
-    cardboard-box2: "골판지 상자 2"
-    cardboard-box3: "골판지 상자 3"
-    book: "ì±…"
-    book2: "ì±… 2"
-    piano: "피아노"
-    facial-tissue: "휴지 상자"
-    server: "서버"
-    moon: "달"
-    corkboard: "게시판"
-    mousepad: "마우스 패드"
-    monitor: "모니터"
-    keyboard: "키보드"
-    carpet-stripe: "카페트 (줄무늬)"
-    mat: "매트"
-    color-box: "책장"
-    wall-clock: "벽걸이 시계"
-    photoframe: "액자"
-    cube: "큐브"
-    tv: "TV"
-    pinguin: "펭귄"
-    rubik-cube: "루빅스 큐브"
-    poster-h: "포스터 (가로)"
-    poster-v: "포스터 (세로)"
-    sofa: "소파"
-    spiral: "나선형 계단"
-    bin: "휴지통"
-    cup-noodle: "컵라면"
-    holo-display: "홀로그램"
-    energy-drink: "에너지 드링크"
diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml
deleted file mode 100644
index c76e98229dd1a7578e96d517fea390de2ac09f25..0000000000000000000000000000000000000000
--- a/locales/nl-NL.yml
+++ /dev/null
@@ -1,701 +0,0 @@
----
-meta:
-  lang: "Nederlands"
-common:
-  misskey: "Deel alles met anderen die ook Misskey gebruiken."
-  about-title: "Een ster van het fediverse"
-  about: "Bedankt voor het ontdekken van Misskey. Misskey is een <b>gedecentraliseerd microblogging platform</b> geboren op aarde. Omdat het bestaat binnen het Fediverse (een georganiseerd universum van verschillende sociale mediaplatformen), staat het verbonden met andere sociale medieplatformen. Neem een pauze van de stedelijke drukte, en duik in het nieuwe intenet?"
-  intro:
-    title: "Wat is Misskey?"
-    about: "Misskey is een open source <b>gedecentraliseerd blogplatform</b>. Het heeft een gesofisticeerde, volledig aanpasbare gebruikersinterface, uitgebreide reactiemogelijkheden voor posts, gratis geïntegreerd bestandsoplagbeheer en andere geavanceerde mogelijkheden. Daarnaast staat Misskey verbonden aan een netwerksysteem genaam het \"Fediverse\", hiermee kunnen we communiceren met andere gebruikers op andere SNSs. Dit betekent dat wanneer je iets post het niet enkel verstuurd wordt naar andere Misskey gebruikers, maar ook naar gebruikers op Mastodon en Pleroma. Stel je voor dat een planeet een radiosignaal verzendt naar een andere planeet als manier van communiceren."
-    features: "Kenmerken"
-    rich-contents: "Bericht"
-    rich-contents-desc: "Post jouw idee, hot topic, wat je ook maar wil delen. Maak jouw teksten aantrekkelijk met je favoriete foto's, verzend bestanden, zelfs video's, of maak een poll. Dit zijn enkele van de mogelijkheden die Misskey aanbiedt!"
-    reaction: "Reactie"
-    reaction-desc: "Dé makkelijkste manier om jouw gevoelens uit te drukken. Met Misskey kan je verschillende reacties toevoegen aan jouw posts. Andere SNSs hebben enkel maar een \"vind ik leuk\" reactie."
-    ui: "Interface"
-    ui-desc: "Niet één UI past nij iedereen. Daarom heeft Misskey een uitgebreide keuze om de UI naar jouw hand te zetten. Je kan jouw nieuwe thuis zo origineel maken als je zelf wil door jouw tijdslijn aan te passen door widgets te verplaatsen en hun look te veranderen. Zo maak je van Misskey jouw eigen stek."
-    drive: "Drive"
-    drive-desc: "Wil je een foto posten die je reeds het geüpload? Wens je georganiseerde map met zelfgekozen naam maken voor al jouw bestanden? De beste oplossing voor jou  is Misskey Drive. Dit maakt het supermakkelijk om jouw bestanden online te delen."
-  application-authorization: "Geauthoriseerde applicaties"
-  close: "Sluiten"
-  do-not-copy-paste: "Gelieve de code hier niet in te geven of te plakken. De account kan gecompromiseerd zijn."
-  load-more: "Laad meer resultaten"
-  enter-password: "Voer het wachtwoord in"
-  2fa: "Tweestapsverificatie"
-  customize-home: "Layout aanpassen"
-  featured-notes: "Uitgelicht"
-  dark-mode: "Donker thema"
-  signin: "Aanmelden"
-  signup: "Registreren"
-  signout: "Afmelden"
-  reload-to-apply-the-setting: "Herlaad de pagina om je aanpassingen te bekijken. Wil je de pagina nu herladen?"
-  fetching-as-ap-object: "Verzenden naar Fediverse"
-  unfollow-confirm: "Wil stoppen met {name} te volgen?"
-  delete-confirm: "Ben je zeker dat je dit bericht wil verwijderen?"
-  signin-required: "Gelieve in te loggen"
-  notification-type: "Notificatietype"
-  notification-types:
-    all: "Alle"
-    pollVote: "Stemmen"
-    follow: "Volgend"
-    receiveFollowRequest: "Volgverzoeken"
-    reply: "Beantwoorden"
-    quote: "Bron"
-    mention: "Vermeldingen"
-    reaction: "Reactie"
-  got-it: "Ik snap het!"
-  customization-tips:
-    title: "Aanpassingstips"
-    gotit: "Ik snap het!"
-  notification:
-    file-uploaded: "Je bestand is geüpload"
-    message-from: "Bericht van {}:"
-    reversi-invited: "Uitgenodigd voor spel"
-    notified-by: "Bemerkt door: {}"
-    reply-from: "Antwoord van: {}"
-    quoted-by: "Geciteerd door: {}"
-  time:
-    unknown: "onbekend"
-    future: "toekomstig"
-    just_now: "zojuist"
-    seconds_ago: "{}s geleden"
-    minutes_ago: "{}m geleden"
-    hours_ago: "{}u geleden"
-    days_ago: "{}d geleden"
-    weeks_ago: "{}week/weken geleden"
-    months_ago: "{}maand(en) geleden"
-    years_ago: "{}jaar geleden"
-  month-and-day: "{day} {month}"
-  trash: "Prullenbak"
-  drive: "Drive"
-  pages: "Pagina's"
-  messaging: "Gesprekken"
-  home: "Startpagina"
-  deck: "Deck"
-  timeline: "Tijdlijn"
-  followers: "Volgers"
-  favorites: "Deze notitie toevoegen aan favorieten"
-  permissions:
-    "write:votes": "Stemmen"
-  post-form:
-    submit: "Bericht"
-    reply: "Beantwoorden"
-    add-visible-user: "Gebruiker toevoegen"
-  weekday-short:
-    sunday: "Z"
-    monday: "M"
-    tuesday: "D"
-    wednesday: "W"
-    thursday: "D"
-    friday: "V"
-    saturday: "Z"
-  reactions:
-    like: "Leuk"
-    love: "Geweldig"
-    laugh: "Grappig"
-    hmm: "Eh...?"
-    surprise: "Wauw"
-    congrats: "Gefeliciteerd!"
-    angry: "Boos"
-    confused: "Verward"
-    pudding: "Pudding"
-  note-visibility:
-    home: "Startpagina"
-    followers: "Volgers"
-  _settings:
-    profile: "Je profiel"
-    notification: "Meldingen"
-    password: "Wachtwoord"
-    reactions: "Reactie"
-    deck-column-align-center: "Centreren"
-    deck-column-align-left: "Links"
-    timeline: "Tijdlijn"
-    navbar-position-left: "Links"
-  search: "Zoeken"
-  delete: "Verwijderen"
-  loading: "Bezig met laden"
-  update-available: "Er is een nieuwe versie van Misskey beschikbaar: {newer} (de huidige versie is {current}). Herlaad de pagina om de update toe te passen."
-  my-token-regenerated: "Je sleutel is gegenereerd; je wordt nu uitgelogd."
-  widgets:
-    profile: "Je profiel"
-    activity: "Activiteit"
-    trends: "Populair"
-    photo-stream: "Fotostream"
-    notifications: "Meldingen"
-    users: "Aanbevolen gebruikers"
-    server: "Serverinformatie"
-  you: "Jij"
-auth/views/form.vue:
-  cancel: "Annuleren"
-auth/views/index.vue:
-  loading: "Bezig met laden"
-common/views/components/games/reversi/reversi.vue:
-  matching:
-    cancel: "Annuleren"
-common/views/components/games/reversi/reversi.room.vue:
-  cancel: "Annuleren"
-common/views/components/connect-failed.vue:
-  title: "Verbinden met server mislukt"
-  description: "Er is een probleem met je internetverbinding, de server ligt plat of er wordt aan gewerkt. {Probeer} het later opnieuw."
-  thanks: "Bedankt voor het gebruiken van Misskey."
-  troubleshoot: "Probleemoplossing"
-common/views/components/connect-failed.troubleshooter.vue:
-  title: "Probleemoplossing"
-  network: "Netwerkverbinding"
-  checking-network: "Bezig met controleren van netwerkverbinding"
-  internet: "Internetverbinding"
-  checking-internet: "Bezig met controleren van internetverbinding"
-  server: "Serververbinding"
-  checking-server: "Bezig met controleren van serververbinding"
-  finding: "Bezig met vaststellen van probleem"
-  no-network: "Er is geen internetverbinding"
-  no-network-desc: "Zorg ervoor dat je verbonden bent met een netwerk."
-  no-internet: "Er is geen internetverbinding"
-  no-internet-desc: "Zorg ervoor dat je verbonden bent met het internet."
-  no-server: "Verbinden met Misskey-server mislukt"
-  no-server-desc: "De netwerkverbinding van je computer is goed, maar er kan geen verbinding worden gemaakt met de Misskey-server. Het kan dat de server plat ligt of dat eraan wordt gewerkt. Probeer het later opnieuw."
-  success: "Verbonden met de Misskey-server"
-  success-desc: "Het verbinden lijkt te lukken. Herlaad de pagina."
-  flush: "Cache leegmaken"
-  set-version: "Versie opgeven"
-common/views/components/theme.vue:
-  desc: "Omschrijving"
-common/views/components/messaging.vue:
-  search-user: "Gebruiker zoeken"
-  you: "Jij"
-  no-history: "Geen geschiedenis"
-  user: "Gebruiker"
-common/views/components/messaging-room.vue:
-  no-history: "Er is geen verdere geschiedenis"
-  new-message: "Nieuw bericht"
-common/views/components/messaging-room.form.vue:
-  input-message-here: "Voer hier je bericht in"
-  send: "Versturen"
-  attach-from-local: "Bestanden bijvoegen van je computer"
-  attach-from-drive: "Bestanden bijvoegen van je Drive"
-common/views/components/messaging-room.message.vue:
-  is-read: "Gelezen"
-  deleted: "Dit bericht is verwijderd"
-common/views/components/nav.vue:
-  about: "Over"
-  stats: "Statistieken"
-  status: "Status"
-  donors: "Donateurs"
-  repository: "Broncode"
-  develop: "Ontwikkelaars"
-  feedback: "Feedback"
-common/views/components/note-menu.vue:
-  favorite: "Deze notitie toevoegen aan favorieten"
-  pin: "Vastmaken aan profielpagina"
-  delete: "Verwijderen"
-  remote: "Origineel tonen"
-common/views/components/poll.vue:
-  vote-to: "Stemmen op '{}'"
-  vote-count: "{} stemmen"
-  vote: "Stemmen"
-  show-result: "Resultaten tonen"
-  voted: "Gestemd"
-common/views/components/poll-editor.vue:
-  no-only-one-choice: "Je moet twee of meer keuzes invoeren."
-  choice-n: "Keuze {}"
-  remove: "Deze keuze verwijderen"
-  add: "+ Keuze toevoegen"
-  destroy: "Deze peiling vernietigen"
-  day: "Z"
-common/views/components/reaction-picker.vue:
-  choose-reaction: "Kies een reactie"
-common/views/components/emoji-picker.vue:
-  activity: "Activiteit"
-common/views/components/signin.vue:
-  username: "Gebruikersnaam"
-  password: "Wachtwoord"
-  token: "Sleutel"
-  signing-in: "Bezig met inloggen..."
-common/views/components/signup.vue:
-  username: "Gebruikersnaam"
-  checking: "Bezig met controleren..."
-  available: "Beschikbaar"
-  unavailable: "Niet beschikbaar"
-  error: "Netwerkfout"
-  invalid-format: "Gebruik alleen letters, cijfers en -."
-  too-short: "Voer minimaal 1 teken in!"
-  too-long: "Voer maximaal 20 tekens in."
-  password: "Wachtwoord"
-  password-placeholder: "Wij raden aan meer dan 8 tekens te gebruiken."
-  weak-password: "Zwak"
-  normal-password: "'t Ken net"
-  strong-password: "Sterk"
-  retype: "Opnieuw invoeren"
-  retype-placeholder: "Wachtwoord bevestigen"
-  password-matched: "Oké"
-  password-not-matched: "Komt niet overeen"
-  recaptcha: "Verifiëren"
-  create: "Account creëren"
-  some-error: "Het creëren van een account is mislukt. Probeer het opnieuw."
-common/views/components/special-message.vue:
-  new-year: "Gelukkig nieuwjaar!"
-  christmas: "Fijne kerstdagen!"
-common/views/components/stream-indicator.vue:
-  connecting: "Bezig met verbinden"
-  reconnecting: "Bezig met herverbinden"
-  connected: "Verbonden"
-common/views/components/notification-settings.vue:
-  title: "Meldingen"
-common/views/components/github-setting.vue:
-  detail: "Details bekijken..."
-common/views/components/discord-setting.vue:
-  detail: "Details bekijken..."
-common/views/components/uploader.vue:
-  waiting: "Bezig met wachten"
-common/views/components/visibility-chooser.vue:
-  home: "Startpagina"
-  followers: "Volgers"
-common/views/components/profile-editor.vue:
-  title: "Je profiel"
-  name: "Naam"
-  avatar: "Gebruikersafbeelding"
-  banner: "Omslagfoto"
-  unable-to-process: "De operatie kan niet worden voltooid."
-  export-targets:
-    following-list: "Volgend"
-    user-lists: "Lijsten"
-  enter-password: "Voer het wachtwoord in"
-common/views/components/user-list-editor.vue:
-  users: "Gebruiker"
-  add-user: "Gebruiker toevoegen"
-common/views/components/user-lists.vue:
-  user-lists: "Lijsten"
-common/views/widgets/broadcast.vue:
-  fetching: "Bezig met ophalen"
-  no-broadcasts: "Geen uitzendingen"
-  have-a-nice-day: "Fijne dag!"
-  next: "Volgende"
-common/views/widgets/photo-stream.vue:
-  title: "Fotostream"
-  no-photos: "Geen foto's"
-common/views/widgets/posts-monitor.vue:
-  toggle: "Schakelen tussen weergaven"
-common/views/widgets/server.vue:
-  title: "Serverinformatie"
-  toggle: "Schakelen tussen weergaven"
-common/views/pages/follow.vue:
-  signed-in-as: "Ingelogd als {}"
-  follow: "Volgend"
-desktop:
-  banner: "Omslagfoto"
-  avatar: "Gebruikersafbeelding"
-  unable-to-process: "De operatie kan niet worden voltooid."
-desktop/views/components/activity.chart.vue:
-  total: "Zwart ... totaal"
-  notes: "Blauw ... notities"
-  replies: "Rood ... antwoorden"
-  renotes: "Groen ... gedeelde notities"
-desktop/views/components/activity.vue:
-  title: "Activiteit"
-  toggle: "Schakelen tussen weergaven"
-desktop/views/components/calendar.vue:
-  prev: "Vorige maand"
-  next: "Volgende maand"
-  go: "Klik om te navigeren"
-desktop/views/components/choose-file-from-drive-window.vue:
-  upload: "Bestanden uploaden van je computer"
-  cancel: "Annuleren"
-  ok: "Oké"
-  choose-prompt: "Kies een bestand"
-desktop/views/components/choose-folder-from-drive-window.vue:
-  cancel: "Annuleren"
-  ok: "Oké"
-  choose-prompt: "Kies een map"
-desktop/views/components/crop-window.vue:
-  skip: "Bijsnijden overslaan"
-  cancel: "Annuleren"
-  ok: "Oké"
-desktop/views/components/drive-window.vue:
-  used: "gebruikt"
-desktop/views/components/drive.file.vue:
-  avatar: "Gebruikersafbeelding"
-  banner: "Omslagfoto"
-  contextmenu:
-    rename: "Naam wijzigen"
-    copy-url: "URL kopiëren"
-    download: "Downloaden"
-    set-as-avatar: "Instellen als gebruikersafbeelding"
-    set-as-banner: "Instellen als omslagfoto"
-    open-in-app: "Openen in app"
-    add-app: "App toevoegen"
-    rename-file: "Bestandsnaam wijzigen"
-    input-new-file-name: "Voer een nieuwe naam in"
-    copied: "Gekopieerd"
-    copied-url-to-clipboard: "URL gekopieerd naar klembord"
-desktop/views/components/drive.folder.vue:
-  unable-to-process: "De operatie kan niet worden voltooid."
-  circular-reference-detected: "De bestemmingsmap is een submap van de map die je wilt verplaatsen."
-  unhandled-error: "Onbekende fout"
-  contextmenu:
-    move-to-this-folder: "Verplaatsen naar deze map"
-    show-in-new-window: "Openen in nieuw venster"
-    rename: "Naam wijzigen"
-    rename-folder: "Mapnaam wijzigen"
-    input-new-folder-name: "Voer een nieuwe naam in"
-desktop/views/components/drive.vue:
-  search: "Zoeken"
-  empty-draghover: "Welkom!"
-  empty-drive: "Je schijf is leeg"
-  empty-drive-description: "Je kunt ook uploaden door te klikken met de rechtermuisknop en te kiezen voor \"Bestand uploaden\" of door een bestand naar dit venster te slepen."
-  empty-folder: "Deze map is leeg"
-  unable-to-process: "De operatie kan niet worden voltooid."
-  circular-reference-detected: "De bestemmingsmap is een submap van de te verplaatsen map."
-  unhandled-error: "Onbekende fout"
-  url-upload: "Uploaden via URL"
-  url-of-file: "URL van het te uploaden bestand"
-  url-upload-requested: "Uploadverzoek"
-  may-take-time: "Het kan even duren voordat het uploaden voltooid is."
-  create-folder: "Map creëren"
-  folder-name: "Mapnaam"
-  contextmenu:
-    create-folder: "Map creëren"
-    upload: "Bestand uploaden"
-    url-upload: "Uploaden via URL"
-desktop/views/components/followers-window.vue:
-  followers: "Volgers van {}"
-desktop/views/components/followers.vue:
-  empty: "Het lijkt erop dat je geen volgers hebt."
-desktop/views/components/following-window.vue:
-  following: "Volgend {}"
-desktop/views/components/following.vue:
-  empty: "Je volgt niemand."
-desktop/views/components/game-window.vue:
-  game: "Othello"
-desktop/views/components/home.vue:
-  done: "Versturen"
-  add-widget: "Widget toevoegen:"
-  add: "Toevoegen"
-desktop/views/input-dialog.vue:
-  cancel: "Annuleren"
-  ok: "Oké"
-desktop/views/components/note-detail.vue:
-  private: "(dit bericht is privé)"
-  location: "Locatie"
-  add-reaction: "Reactie"
-desktop/views/components/note.vue:
-  reply: "Beantwoorden"
-  add-reaction: "Reactie"
-  private: "(dit bericht is privé)"
-desktop/views/components/notes.vue:
-  error: "Laden mislukt."
-  retry: "Opnieuw proberen"
-desktop/views/components/notifications.vue:
-  empty: "Geen meldingen"
-desktop/views/components/post-form.vue:
-  posted: "Geplaatst!"
-  replied: "Beantwoord!"
-  reposted: "Hergeplaatst!"
-  note-failed: "Noteren mislukt"
-  reply-failed: "Beantwoorden mislukt"
-  renote-failed: "Renote mislukt"
-desktop/views/components/post-form-window.vue:
-  note: "Nieuwe notitie"
-  reply: "Beantwoorden"
-  attaches: "{} media bijgevoegd"
-  uploading-media: "Bezig met uploaden van media {}"
-desktop/views/components/progress-dialog.vue:
-  waiting: "Bezig met wachten"
-desktop/views/components/renote-form.vue:
-  quote: "Citeren..."
-  cancel: "Annuleren"
-  reposting: "Bezig met herplaatsen..."
-  success: "Hergeplaatst!"
-  failure: "Renote mislukt"
-desktop/views/components/renote-form-window.vue:
-  title: "Weet je zeker dat je deze notitie wilt renoten?"
-desktop/views/components/settings.2fa.vue:
-  intro: "Als je verificatie in twee stappen instelt, dan heb je niet alleen een wachtwoord nodig bij het inloggen, maar ook een geregistreerd fysiek apparaat (zoals je smartphone). Dit verhoogt de veiligheid. "
-  detail: "Details bekijken..."
-  url: "https://www.google.com/landing/2step/"
-  caution: "Als je geen toegang meer hebt tot je apparaat, dan kun je niet meer verbinden met Misskey!"
-  register: "Apparaat registreren"
-  already-registered: "Er is al een apparaat geregistreerd"
-  unregister: "Uitschakelen"
-  unregistered: "Authenticatie in twee stappen is uitgeschakeld."
-  enter-password: "Voer het wachtwoord in"
-  authenticator: "Installeer eerst Google Authenticator op je apparaat:"
-  howtoinstall: "Hoe installeer ik dit?"
-  token: "Sleutel"
-  scan: "Scan daarna de QR-code:"
-  done: "Voer de op je apparaat getoonde sleutel in:"
-  submit: "Versturen"
-  success: "Instellen voltooid!"
-  failed: "Instellen mislukt. Zorg ervoor dat de sleutel juist is."
-  info: "Vanaf nu moet je ook de op je apparaat getoonde sleutel tonen bij het inloggen op Misskey."
-common/views/components/api-settings.vue:
-  enter-password: "Voer het wachtwoord in"
-  console:
-    parameter: "Parameters"
-    send: "Versturen"
-common/views/components/drive-settings.vue:
-  in-use: "gebruikt"
-  stats: "Statistieken"
-  default-upload-folder-name: "Map(pen)"
-desktop/views/components/sub-note-content.vue:
-  private: "(dit bericht is privé)"
-  poll: "Peilingen"
-desktop/views/components/settings.tags.vue:
-  add: "Toevoegen"
-desktop/views/components/timeline.vue:
-  home: "Startpagina"
-  local: "Lokaal"
-  global: "Algemeen"
-  list: "Lijsten"
-desktop/views/components/ui.header.account.vue:
-  profile: "Je profiel"
-  lists: "Lijsten"
-desktop/views/components/ui.header.nav.vue:
-  game: "Othello spelen"
-desktop/views/components/ui.header.notifications.vue:
-  title: "Meldingen"
-desktop/views/components/ui.header.post.vue:
-  post: "Nieuw bericht opstellen"
-desktop/views/components/ui.header.search.vue:
-  placeholder: "Zoeken"
-desktop/views/components/user-preview.vue:
-  notes: "Berichten"
-  following: "Volgend"
-  followers: "Volgers"
-desktop/views/components/users-list.vue:
-  all: "Alle"
-  iknow: "die ik ken"
-  fetching: "Bezig met laden…"
-desktop/views/components/users-list-item.vue:
-  followed: "Volgt jou"
-desktop/views/components/window.vue:
-  popout: "Uitvouwen"
-  close: "Sluiten"
-admin/views/index.vue:
-  users: "Gebruiker"
-admin/views/dashboard.vue:
-  notes: "Bericht"
-  drive: "Drive"
-admin/views/abuse.vue:
-  remove-report: "Verwijderen"
-admin/views/charts.vue:
-  notes: "Bericht"
-  users: "Gebruiker"
-  drive: "Drive"
-admin/views/drive.vue:
-  origin:
-    local: "Lokaal"
-  delete: "Verwijderen"
-admin/views/users.vue:
-  username: "Gebruikersnaam"
-  users:
-    title: "Gebruiker"
-    state:
-      all: "Alle"
-    origin:
-      local: "Lokaal"
-admin/views/emoji.vue:
-  add-emoji:
-    add: "Toevoegen"
-  emojis:
-    remove: "Verwijderen"
-admin/views/announcements.vue:
-  remove: "Verwijderen"
-  add: "Toevoegen"
-admin/views/federation.vue:
-  notes: "Bericht"
-  users: "Gebruiker"
-  followers: "Volgers"
-  status: "Status"
-  states:
-    all: "Alle"
-desktop/views/pages/welcome.vue:
-  timeline: "Tijdlijn"
-desktop/views/pages/note.vue:
-  prev: "Vorige notitie"
-  next: "Volgende notitie"
-desktop/views/pages/selectdrive.vue:
-  title: "Bestand(en) kiezen"
-  ok: "Oké"
-  cancel: "Annuleren"
-  upload: "Bestanden uploaden van je PC"
-desktop/views/pages/user-list.users.vue:
-  users: "Gebruiker"
-  add-user: "Gebruiker toevoegen"
-  username: "Gebruikersnaam"
-desktop/views/pages/user/user.followers-you-know.vue:
-  title: "Volgers die je kent"
-  loading: "Bezig met laden"
-  no-users: "Geen gebruikers"
-desktop/views/pages/user/user.friends.vue:
-  title: "Frequent beantwoord"
-  loading: "Bezig met laden"
-  no-users: "Geen gebruikers"
-desktop/views/pages/user/user.photos.vue:
-  title: "Foto's"
-  loading: "Bezig met laden"
-  no-photos: "Geen foto's"
-desktop/views/pages/user/user.header.vue:
-  posts: "Bericht"
-  following: "Volgend"
-  followers: "Volgers"
-  month: "M"
-  day: "Z"
-  follows-you: "Volgt jou"
-desktop/views/pages/user/user.timeline.vue:
-  default: "Berichten"
-  with-replies: "Berichten en antwoorden"
-  with-media: "Media"
-desktop/views/widgets/notifications.vue:
-  title: "Meldingen"
-desktop/views/widgets/polls.vue:
-  title: "Peilingen"
-  refresh: "Anderen tonen"
-  nothing: "Niks"
-desktop/views/widgets/post-form.vue:
-  title: "Bericht"
-  note: "Bericht"
-desktop/views/widgets/profile.vue:
-  update-banner: "Klik om je omslagfoto te wijzigen"
-  update-avatar: "Klik om je gebruikersafbeelding te wijzigen"
-desktop/views/widgets/trends.vue:
-  title: "Populair"
-  refresh: "Anderen tonen"
-  nothing: "Niks"
-desktop/views/widgets/users.vue:
-  title: "Aanbevolen gebruikers"
-  refresh: "Anderen tonen"
-  no-one: "Niemand"
-mobile/views/components/drive.vue:
-  used: "gebruikt"
-  folder-count: "Map(pen)"
-  count-separator: ", "
-  file-count: "Bestand(en)"
-  nothing-in-drive: "Niks"
-  folder-is-empty: "Deze map is leeg"
-  folder-name: "Mapnaam"
-  url-prompt: "URL van het te uploaden bestand"
-mobile/views/components/drive-file-chooser.vue:
-  select-file: "Kies een bestand"
-mobile/views/components/drive-folder-chooser.vue:
-  select-folder: "Kies een map"
-mobile/views/components/drive.file-detail.vue:
-  download: "Downloaden"
-  rename: "Naam wijzigen"
-  move: "Verplaatsen"
-  hash: "Hash (md5)"
-common/views/components/follow-button.vue:
-  follow: "Volgend"
-mobile/views/components/note.vue:
-  private: "(dit bericht is privé)"
-  location: "Locatie"
-mobile/views/components/note-detail.vue:
-  reply: "Beantwoorden"
-  reaction: "Reactie"
-  private: "(dit bericht is privé)"
-  location: "Locatie"
-mobile/views/components/notifications.vue:
-  empty: "Geen meldingen"
-mobile/views/components/sub-note-content.vue:
-  private: "(dit bericht is privé)"
-  media-count: "{} media"
-  poll: "Peiling"
-mobile/views/components/ui.nav.vue:
-  timeline: "Tijdlijn"
-  notifications: "Meldingen"
-  search: "Zoeken"
-  user-lists: "Lijsten"
-  game: "Othello spelen"
-  about: "Over Misskey"
-mobile/views/pages/drive.vue:
-  contextmenu:
-    upload: "Bestand uploaden"
-    create-folder: "Map creëren"
-mobile/views/pages/home.vue:
-  home: "Startpagina"
-  local: "Lokaal"
-  global: "Algemeen"
-mobile/views/pages/widgets.vue:
-  add-widget: "Toevoegen"
-  customization-tips: "Aanpassingstips"
-mobile/views/pages/widgets/activity.vue:
-  activity: "Activiteit"
-mobile/views/pages/note.vue:
-  title: "Bericht"
-  prev: "Vorige notitie"
-  next: "Volgende notitie"
-mobile/views/pages/games/reversi.vue:
-  reversi: "Othello"
-mobile/views/pages/search.vue:
-  search: "Zoeken"
-mobile/views/pages/selectdrive.vue:
-  select-file: "Kies een bestand"
-mobile/views/pages/notifications.vue:
-  notifications: "Meldingen"
-mobile/views/pages/settings.vue:
-  signed-in-as: "Ingelogd als {}"
-mobile/views/pages/user.vue:
-  follows-you: "Volgt jou"
-  following: "Volgend"
-  followers: "Volgers"
-  notes: "Berichten"
-  overview: "Overzicht"
-  timeline: "Tijdlijn"
-  media: "Media"
-mobile/views/pages/user/home.vue:
-  recent-notes: "Recente notities"
-  images: "Afbeeldingen"
-  activity: "Activiteit"
-  keywords: "Sleutelwoorden"
-  domains: "Domeinnamen"
-  frequently-replied-users: "Frequent beantwoord"
-  followers-you-know: "Volgers die je kent"
-  last-used-at: "Laatst actief"
-mobile/views/pages/user/home.photos.vue:
-  no-photos: "Geen foto's"
-deck:
-  home: "Startpagina"
-  local: "Lokaal"
-  global: "Algemeen"
-  notifications: "Meldingen"
-  list: "Lijsten"
-  rename: "Naam wijzigen"
-deck/deck.user-column.vue:
-  follows-you: "Volgt jou"
-  posts: "Bericht"
-  following: "Volgend"
-  followers: "Volgers"
-  images: "Afbeeldingen"
-  activity: "Activiteit"
-  timeline: "Tijdlijn"
-docs:
-  edit-this-page-on-github: "Heb je een fout ontdekt of wil je bijdragen aan de documentatie? "
-  edit-this-page-on-github-link: "Bewerk deze pagina op GitHub!"
-pages:
-  pin-this-page: "Vastmaken aan profielpagina"
-  like: "Leuk"
-  blocks:
-    image: "Afbeeldingen"
-  script:
-    categories:
-      list: "Lijsten"
-    blocks:
-      _join:
-        arg1: "Lijsten"
-      _randomPick:
-        arg1: "Lijsten"
-      _dailyRandomPick:
-        arg1: "Lijsten"
-      _seedRandomPick:
-        arg2: "Lijsten"
-      _pick:
-        arg1: "Lijsten"
-      _listLen:
-        arg1: "Lijsten"
-    types:
-      array: "Lijsten"
-room:
-  translate: "Verplaatsen"
-  furnitures:
-    moon: "Maan"
-    bin: "Prullenbak"
diff --git a/locales/no-NO.yml b/locales/no-NO.yml
deleted file mode 100644
index 2af76bf7d94b276c08a7438af6755db983dded2f..0000000000000000000000000000000000000000
--- a/locales/no-NO.yml
+++ /dev/null
@@ -1,528 +0,0 @@
----
-meta:
-  lang: "Norsk Bokmål"
-common:
-  misskey: "En ⭐ av fediverse"
-  about-title: "En ⭐ av fediverse"
-  about: "Takk for at du fant Misskey. Misskey er en <b>desentralisert mikroblogging platform</b> født på jorden. Siden den eksisterer sammen med Fediverset (Et univers hvor forskjellige sosiale media-plattformer blir organisert), så blir den gjensidig tilknyttet med andre sosiale media-plattformer. Hvorfor ikke ta en pause fra kjas og mas fra storbyen og hoppe inn i en ny type internett?"
-  intro:
-    title: "Hva er Misskey?"
-    features: "Funksjoner"
-    rich-contents: "Innlegg"
-    drive: "Disk"
-  close: "Lukk"
-  notification-types:
-    all: "Alle"
-    follow: "Følger"
-    reply: "Svar"
-  got-it: "Skjønner!"
-  notification:
-    file-uploaded: "Filen ble lastet opp!"
-    message-from: "Melding fra {}:"
-    reversi-invited: "Invitert til et spill"
-    reversi-invited-by: "Invitert av {}:"
-    notified-by: "Invitert av {}:"
-    reply-from: "Svar fra {}:"
-    quoted-by: "Sitert av {}:"
-  time:
-    unknown: "ukjent"
-    future: "fremtidig"
-    just_now: "akkurat nå"
-    seconds_ago: "{} sekunder siden"
-    minutes_ago: "{} minutter siden"
-    hours_ago: "{} t siden"
-    days_ago: "{} d siden"
-    weeks_ago: "{} uke(r) siden"
-    months_ago: "{} måned(er) siden"
-    years_ago: "{} år siden"
-  month-and-day: "{day}/{month}"
-  trash: "Papirkurv"
-  drive: "Disk"
-  home: "Hjem"
-  followers: "Følgere"
-  favorites: "Merket som favoritt"
-  permissions:
-    "write:votes": "Stem"
-  post-form:
-    submit: "Innlegg"
-    reply: "Svar"
-    error: "Feil"
-  weekday-short:
-    sunday: "S"
-    monday: "M"
-    tuesday: "T"
-    wednesday: "O"
-    thursday: "T"
-    friday: "F"
-    saturday: "L"
-  weekday:
-    sunday: "Søndag"
-    monday: "Mandag"
-    tuesday: "Tirsdag"
-    wednesday: "Onsdag"
-    thursday: "Torsdag"
-    friday: "Fredag"
-    saturday: "Lørdag"
-  reactions:
-    like: "Lik"
-    love: "Elsk"
-    laugh: "Le"
-    hmm: "Hmm…?"
-    surprise: "Wow"
-    congrats: "Gratulerer!"
-    angry: "Sint"
-    confused: "Forvirret"
-    rip: "RIP"
-    pudding: "Pudding"
-  note-visibility:
-    public: "Offentlig"
-    home: "Hjem"
-    followers: "Følgere"
-    specified: "Direkte"
-  _settings:
-    notification: "Notifikasjon"
-    password: "Passord"
-    save: "Lagre"
-  search: "Søk"
-  delete: "Slett"
-  loading: "Laster inn..."
-  update-available: "En ny versjon av Misskey er nå tilgjengelig ({newer}, nåværende versjon er {current}). Last inn siden igjen for at oppdateringen skal tre i kraft."
-  my-token-regenerated: "Ditt synbol har blitt generert. Du vil nå bli utlogget."
-  reversi:
-    black: "Sort"
-    white: "Hvit"
-    total: "Totalt"
-  widgets:
-    calendar: "Kalender"
-    memo: "Notis"
-    trends: "Populært nå"
-    version: "Versjon"
-    notifications: "Notifikasjon"
-    tips: "Tips"
-  you: "Du"
-auth/views/form.vue:
-  cancel: "Avbryt"
-auth/views/index.vue:
-  loading: "Laster inn..."
-common/views/components/games/reversi/reversi.vue:
-  matching:
-    cancel: "Avbryt"
-common/views/components/games/reversi/reversi.game.vue:
-  surrender: "Gi opp"
-common/views/components/games/reversi/reversi.index.vue:
-  invite: "Inviter"
-  rule: "Slik spiller du"
-  mode-invite: "Inviter"
-  game-state:
-    ended: "Ferdig"
-    playing: "Pågår"
-common/views/components/games/reversi/reversi.room.vue:
-  random: "Tilfeldig"
-  black-is: "Sort er {}"
-  rules: "Regler"
-  waiting-for-both: "Venter på deg"
-  cancel: "Avbryt"
-  ready: "Klar"
-  cancel-ready: "Avbryt \"Klar\""
-common/views/components/connect-failed.vue:
-  title: "Kunne ikke koble til tjeneren."
-  description: "Det er enten et problem med internettilknytningen din, eller så har tjeneren blitt tatt ned for vedlikehold. {Prøv igjen} senere."
-common/views/components/media-banner.vue:
-  sensitive: "Sensitivt innhold"
-common/views/components/theme.vue:
-  text-color: "Tekstfarge"
-  base-theme-dark: "Mørk"
-  theme-name: "Tema navn"
-  author: "Forfatter"
-  desc: "Beskrivelse"
-common/views/components/cw-button.vue:
-  hide: "Skjul"
-common/views/components/messaging.vue:
-  you: "Du"
-  user: "Bruker"
-common/views/components/messaging-room.form.vue:
-  send: "Send"
-common/views/components/messaging-room.message.vue:
-  is-read: "Lest"
-common/views/components/nav.vue:
-  stats: "Statistikk"
-  status: "Status"
-  wiki: "Wiki"
-  donors: "Donatorer"
-  repository: "Kodelager"
-  develop: "Utviklere"
-common/views/components/note-menu.vue:
-  detail: "Detaljer"
-  favorite: "Merket som favoritt"
-  pin: "Fest til profilen din"
-  delete: "Slett"
-common/views/components/poll.vue:
-  vote-count: "{} stemmer"
-  vote: "Stem"
-  show-result: "Vis resultater"
-  voted: "Stemt"
-common/views/components/poll-editor.vue:
-  choice-n: "Valg {}"
-  day: "S"
-common/views/components/signin.vue:
-  username: "Brukernavn"
-  password: "Passord"
-  token: "Token"
-  or: "Eller"
-common/views/components/signup.vue:
-  username: "Brukernavn"
-  error: "Nettverksfeil"
-  password: "Passord"
-  retype: "Gjenta"
-  recaptcha: "Captcha"
-common/views/components/stream-indicator.vue:
-  connecting: "Tilkobler"
-  reconnecting: "Kobler til på nytt"
-  connected: "Tilkoblet"
-common/views/components/notification-settings.vue:
-  title: "Notifikasjon"
-common/views/components/github-setting.vue:
-  detail: "Detaljer..."
-common/views/components/discord-setting.vue:
-  detail: "Detaljer..."
-common/views/components/uploader.vue:
-  waiting: "Venter"
-common/views/components/visibility-chooser.vue:
-  public: "Offentlig"
-  home: "Hjem"
-  followers: "Følgere"
-  specified: "Direkte"
-common/views/components/profile-editor.vue:
-  name: "Navn"
-  avatar: "Avatar"
-  banner: "Banner"
-  save: "Lagre"
-  export-targets:
-    following-list: "Følger"
-    user-lists: "Lister"
-common/views/components/user-list-editor.vue:
-  users: "Bruker"
-common/views/components/user-group-editor.vue:
-  invite: "Inviter"
-common/views/components/user-lists.vue:
-  user-lists: "Lister"
-  list-name: "Liste navn"
-common/views/components/user-groups.vue:
-  invites: "Inviter"
-common/views/widgets/broadcast.vue:
-  fetching: "Henter"
-  next: "Neste"
-common/views/widgets/calendar.vue:
-  year: "Ã…r {}"
-  month: "MÃ¥ned {}"
-  day: "Dag {}"
-  today: "I dag:"
-  this-month: "Denne måneden:"
-  this-year: "Dette året:"
-common/views/widgets/memo.vue:
-  title: "Notis"
-  save: "Lagre"
-common/views/pages/follow.vue:
-  follow: "Følg"
-desktop:
-  banner: "Banner"
-  avatar: "Avatar"
-desktop/views/components/calendar.vue:
-  prev: "Forrige måned"
-  next: "Neste måned"
-desktop/views/components/choose-file-from-drive-window.vue:
-  cancel: "Avbryt"
-  ok: "Ok"
-desktop/views/components/choose-folder-from-drive-window.vue:
-  cancel: "Avbryt"
-  ok: "Ok"
-desktop/views/components/crop-window.vue:
-  cancel: "Avbryt"
-  ok: "Ok"
-desktop/views/components/drive-window.vue:
-  used: "brukt"
-desktop/views/components/drive.file.vue:
-  avatar: "Avatar"
-  banner: "Banner"
-  nsfw: "NSFW"
-  contextmenu:
-    rename: "Endre navn"
-    copied: "Kopiert"
-desktop/views/components/drive.folder.vue:
-  contextmenu:
-    rename: "Endre navn"
-desktop/views/components/drive.vue:
-  search: "Søk"
-desktop/views/components/media-video.vue:
-  sensitive: "Innholdet er NSFW"
-desktop/views/components/game-window.vue:
-  game: "Reversi"
-desktop/views/components/home.vue:
-  done: "Fullført"
-  add: "Legg til"
-desktop/views/input-dialog.vue:
-  cancel: "Avbryt"
-  ok: "Ok"
-desktop/views/components/note-detail.vue:
-  location: "Lokasjon"
-desktop/views/components/note.vue:
-  reply: "Svar"
-  detail: "Detaljer"
-desktop/views/components/notes.vue:
-  retry: "Prøv på nytt"
-desktop/views/components/post-form-window.vue:
-  note: "Nytt innlegg"
-  reply: "Svar"
-desktop/views/components/progress-dialog.vue:
-  waiting: "Venter"
-desktop/views/components/renote-form.vue:
-  cancel: "Avbryt"
-desktop/views/components/settings.2fa.vue:
-  detail: "Detaljer..."
-  unregister: "Avregistrer"
-  token: "Token"
-  submit: "Send"
-common/views/components/media-image.vue:
-  sensitive: "Innholdet er NSFW"
-common/views/components/api-settings.vue:
-  console:
-    parameter: "Parametere"
-    send: "Send"
-common/views/components/drive-settings.vue:
-  in-use: "brukt"
-  stats: "Statistikk"
-  default-upload-folder-name: "Mappe(r)"
-common/views/components/mute-and-block.vue:
-  save: "Lagre"
-desktop/views/components/settings.tags.vue:
-  add: "Legg til"
-  save: "Lagre"
-desktop/views/components/timeline.vue:
-  home: "Hjem"
-  local: "Lokalt"
-  global: "Globalt"
-  list: "Lister"
-  list-name: "Liste navn"
-desktop/views/components/ui.header.vue:
-  adjective: "-san"
-desktop/views/components/ui.header.account.vue:
-  lists: "Lister"
-  admin: "Admin"
-desktop/views/components/ui.header.nav.vue:
-  game: "Spill"
-desktop/views/components/ui.header.notifications.vue:
-  title: "Notifikasjon"
-desktop/views/components/ui.header.post.vue:
-  post: "Skriv nytt innlegg"
-desktop/views/components/ui.header.search.vue:
-  placeholder: "Søk"
-desktop/views/components/user-preview.vue:
-  notes: "Innlegg"
-  following: "Følger"
-  followers: "Følgere"
-desktop/views/components/users-list.vue:
-  all: "Alle"
-  iknow: "Du kjenner"
-desktop/views/components/window.vue:
-  close: "Lukk"
-admin/views/index.vue:
-  users: "Bruker"
-  announcements: "Kunngjøringer"
-admin/views/dashboard.vue:
-  notes: "Innlegg"
-  drive: "Disk"
-admin/views/logs.vue:
-  levels:
-    info: "Informasjon"
-    error: "Feil"
-admin/views/abuse.vue:
-  details: "Detaljer"
-  remove-report: "Slett"
-admin/views/instance.vue:
-  invite: "Inviter"
-  save: "Lagre"
-admin/views/charts.vue:
-  notes: "Innlegg"
-  users: "Bruker"
-  drive: "Disk"
-admin/views/drive.vue:
-  origin:
-    local: "Lokalt"
-  delete: "Slett"
-admin/views/users.vue:
-  username: "Brukernavn"
-  users:
-    title: "Bruker"
-    state:
-      all: "Alle"
-    origin:
-      local: "Lokalt"
-admin/views/moderators.vue:
-  logs:
-    info: "Informasjon"
-admin/views/emoji.vue:
-  add-emoji:
-    add: "Legg til"
-  emojis:
-    remove: "Slett"
-admin/views/announcements.vue:
-  announcements: "Kunngjøringer"
-  save: "Lagre"
-  remove: "Slett"
-  add: "Legg til"
-admin/views/federation.vue:
-  notes: "Innlegg"
-  users: "Bruker"
-  followers: "Følgere"
-  status: "Status"
-  states:
-    all: "Alle"
-  save: "Lagre"
-desktop/views/pages/welcome.vue:
-  announcements: "Kunngjøringer"
-  info: "Informasjon"
-desktop/views/pages/note.vue:
-  prev: "Forrige innlegg"
-  next: "Neste innlegg"
-desktop/views/pages/selectdrive.vue:
-  ok: "Ok"
-  cancel: "Avbryt"
-desktop/views/pages/user-list.users.vue:
-  users: "Bruker"
-  username: "Brukernavn"
-desktop/views/pages/user/user.followers-you-know.vue:
-  loading: "Laster inn"
-desktop/views/pages/user/user.friends.vue:
-  loading: "Laster inn"
-desktop/views/pages/user/user.photos.vue:
-  title: "Bilder"
-  loading: "Laster inn"
-desktop/views/pages/user/user.header.vue:
-  posts: "Innlegg"
-  following: "Følger"
-  followers: "Følgere"
-  month: "M"
-  day: "S"
-desktop/views/pages/user/user.timeline.vue:
-  default: "Innlegg"
-  with-replies: "Innlegg og svar"
-  with-media: "Media"
-desktop/views/widgets/notifications.vue:
-  title: "Notifikasjon"
-desktop/views/widgets/polls.vue:
-  refresh: "Oppdater"
-desktop/views/widgets/post-form.vue:
-  title: "Innlegg"
-  note: "Innlegg"
-desktop/views/widgets/trends.vue:
-  title: "Populært nå"
-  refresh: "Oppdater"
-desktop/views/widgets/users.vue:
-  refresh: "Oppdater"
-  no-one: "Ingen"
-mobile/views/components/drive.vue:
-  used: "brukt"
-  folder-count: "Mappe(r)"
-  count-separator: ","
-  file-count: "Fil(er)"
-mobile/views/components/drive.file.vue:
-  nsfw: "NSFW"
-mobile/views/components/drive.file-detail.vue:
-  rename: "Endre navn"
-  move: "Flytt"
-  exif: "EXIF"
-  nsfw: "NSFW"
-mobile/views/components/media-video.vue:
-  sensitive: "Innholdet er NSFW"
-common/views/components/follow-button.vue:
-  follow: "Følger"
-mobile/views/components/note.vue:
-  location: "Lokasjon"
-mobile/views/components/note-detail.vue:
-  reply: "Svar"
-  location: "Lokasjon"
-mobile/views/components/note-preview.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "katt"
-mobile/views/components/note-sub.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "katt"
-mobile/views/components/ui.header.vue:
-  adjective: "Mr."
-mobile/views/components/ui.nav.vue:
-  notifications: "Notifikasjon"
-  search: "Søk"
-  user-lists: "Lister"
-  game: "Spill"
-  admin: "Admin"
-mobile/views/pages/home.vue:
-  home: "Hjem"
-  local: "Lokalt"
-  global: "Globalt"
-mobile/views/pages/widgets.vue:
-  add-widget: "Legg til"
-mobile/views/pages/note.vue:
-  title: "Innlegg"
-  prev: "Forrige innlegg"
-  next: "Neste innlegg"
-mobile/views/pages/games/reversi.vue:
-  reversi: "Reversi"
-mobile/views/pages/search.vue:
-  search: "Søk"
-mobile/views/pages/notifications.vue:
-  notifications: "Notifikasjon"
-mobile/views/pages/user.vue:
-  following: "Følger"
-  followers: "Følgere"
-  notes: "Innlegg"
-  overview: "Oversikt"
-  media: "Media"
-mobile/views/pages/user/home.vue:
-  recent-notes: "Nylige innlegg"
-  images: "Bilder"
-  keywords: "Nøkkelord"
-deck:
-  home: "Hjem"
-  local: "Lokalt"
-  global: "Globalt"
-  notifications: "Notifikasjon"
-  list: "Lister"
-  rename: "Endre navn"
-deck/deck.user-column.vue:
-  posts: "Innlegg"
-  following: "Følger"
-  followers: "Følgere"
-  images: "Bilder"
-pages:
-  pin-this-page: "Fest til profilen din"
-  like: "Lik"
-  blocks:
-    image: "Bilder"
-  script:
-    categories:
-      random: "Tilfeldig"
-      list: "Lister"
-    blocks:
-      _join:
-        arg1: "Lister"
-      random: "Tilfeldig"
-      _randomPick:
-        arg1: "Lister"
-      _dailyRandomPick:
-        arg1: "Lister"
-      _seedRandomPick:
-        arg2: "Lister"
-      _pick:
-        arg1: "Lister"
-      _listLen:
-        arg1: "Lister"
-    types:
-      array: "Lister"
-room:
-  translate: "Flytt"
-  save: "Lagre"
-  furnitures:
-    moon: "MÃ¥ne"
-    bin: "Papirkurv"
diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml
deleted file mode 100644
index 3600fedd264c94e6f58459921b2dd082303cc1e3..0000000000000000000000000000000000000000
--- a/locales/pl-PL.yml
+++ /dev/null
@@ -1,1268 +0,0 @@
----
-meta:
-  lang: "język polski"
-common:
-  misskey: "⭐ Fediwersum"
-  about-title: "⭐ Fediwersum"
-  about: "Dziękujemy za znalezienie Misskey. Misskey jest <b>zdecentralizowaną platformą mikroblogową</b> powstałą na Ziemi. Ponieważ działa ona w Fediwersum (uniwersum, w którego skład wchodzi wiele sieci społecznościowych), jest ona połączona z innymi platformami społecznościowymi. Spróbujesz odpocząć od zatłoczoneo miasta i zanurzyć się w nowym Internecie?"
-  intro:
-    title: "Czym jest Misskey?"
-    features: "Funkcje"
-    rich-contents: "Wpis"
-    rich-contents-desc: "Po prostu opublikuj swój pomysł, gorące tematy i wszystko, co chcesz udostępnić. Możesz ozdobić swoje słowa, dołączyć swoje ulubione zdjęcia, wysłać pliki, w tym filmy i utworzyć ankietę - to są rzeczy, które możesz zrobić w Misskey!"
-    reaction: "Reakcja"
-    ui: "Interfejs"
-    drive: "Dysk"
-    drive-desc: "Chcesz opublikować zdjęcie, które już przesłałeś? Chcesz uporządkować, nazwać i utworzyć folder dla przesłanych plików? Dysk Misskey to najlepsze rozwiązanie dla Ciebie. Bardzo łatwo udostępniać swoje pliki online."
-  application-authorization: "Współpraca aplikacji"
-  close: "Zamknij"
-  load-more: "Załaduj więcej"
-  enter-password: "Wprowadź Hasło"
-  2fa: "Uwierzytelnienie dwuetapowe"
-  customize-home: "Dostosuj stronę główną"
-  featured-notes: "Wyróżnienia"
-  dark-mode: "Tryb ciemny"
-  signin: "Zaloguj siÄ™"
-  signup: "Rejestracja"
-  signout: "Wyloguj siÄ™"
-  delete-confirm: "Czy na pewno chcesz usunąć ten wpis?"
-  notification-type: "Typy powiadomień"
-  notification-types:
-    all: "Wszyscy"
-    pollVote: "GÅ‚osy"
-    follow: "Åšledzeni"
-    receiveFollowRequest: "Prośby o śledzenie"
-    reply: "Odpowiedzi"
-    quote: "Cytat"
-    renote: "Udostępnij"
-    mention: "Wzmianki"
-    reaction: "Reakcje"
-  got-it: "Rozumiem!"
-  customization-tips:
-    title: "Wskazówki o dostosowywaniu"
-    gotit: "Rozumiem!"
-  notification:
-    file-uploaded: "Wysłano plik!"
-    message-from: "Wiadomość od {}:"
-    reversi-invited: "Zaproszono do gry"
-    reversi-invited-by: "Zaproszono przez {}:"
-    notified-by: "Powiadomiono przez {}:"
-    reply-from: "Odpowiedź od {}:"
-    quoted-by: "Zacytowano przez {}:"
-  time:
-    unknown: "nieznany"
-    future: "w przyszłości"
-    just_now: "teraz"
-    seconds_ago: "{} sek. temu"
-    minutes_ago: "{} min. temu"
-    hours_ago: "{} godz. temu"
-    days_ago: "{} dni temu"
-    weeks_ago: "{} tyg. temu"
-    months_ago: "{} mies. temu"
-    years_ago: "{} lat temu"
-  month-and-day: "{month}-{day}"
-  trash: "Kosz"
-  drive: "Dysk"
-  pages: "Strony"
-  messaging: "Rozmowy"
-  home: "Strona główna"
-  deck: "Tablice"
-  timeline: "OÅ› czasu"
-  explore: "Znajdź"
-  following: "Åšledzisz"
-  followers: "ÅšledzÄ…cy"
-  favorites: "Moje ulubione"
-  permissions:
-    "read:drive": "Wyświetl dysk"
-    "read:messaging": "Zobacz konwersacjÄ™"
-    "write:votes": "Zagłosuj"
-  empty-timeline-info:
-    explore: "Poznaj"
-  post-form:
-    hide-contents: "Ukryj zawartość"
-    reply-placeholder: "Odpowiedź na ten wpis…"
-    quote-placeholder: "Zacytuj ten wpis…"
-    quote-attached: "Z cytatem"
-    submit: "Wpis"
-    reply: "Odpowiedz"
-    renote: "Udostępnij"
-    posting: "Wysyłanie"
-    attach-media-from-local: "Załącz zawartość multimedialną z komputera"
-    attach-media-from-drive: "Załącz zawartość multimedialną z dysku"
-    create-poll: "Utwórz ankietę"
-    text-remain: "pozostałe znaki: {}"
-    recent-tags: "Ostatnie"
-    visibility: "Widoczność"
-    error: "BÅ‚Ä…d"
-    enter-username: "Wprowadź nazwę użytkownika"
-    specified-recipient: "Adresat"
-    add-visible-user: "Dodaj użytkownika"
-    username-prompt: "Wprowadź nazwę użytkownika"
-    enter-file-name: "Wprowadź nazwę pliku"
-  weekday-short:
-    sunday: "N"
-    monday: "Pn"
-    tuesday: "W"
-    wednesday: "Åš"
-    thursday: "C"
-    friday: "P"
-    saturday: "S"
-  weekday:
-    sunday: "Niedziela"
-    monday: "Poniedziałek"
-    tuesday: "Wtorek"
-    wednesday: "Åšroda"
-    thursday: "Czwartek"
-    friday: "PiÄ…tek"
-    saturday: "Sobota"
-  reactions:
-    like: "LubiÄ™"
-    love: "Kocham"
-    laugh: "Åšmieszne"
-    hmm: "Hmm…?"
-    surprise: "Wow"
-    congrats: "GratulujÄ™!"
-    angry: "Wściekły"
-    confused: "Zmieszany"
-    rip: "RIP"
-    pudding: "Pudding"
-  note-visibility:
-    public: "Publiczny"
-    home: "Lokalny"
-    home-desc: "Widoczny tylko na tej instancji"
-    followers: "Dla śledzących"
-    followers-desc: "Widoczny tylko dla osób, które Cię śledzą"
-    specified: "Bezpośredni"
-    specified-desc: "Tylko dla określonych użytkowników"
-    local-public: "Publiczny (tylko lokalnie)"
-    local-followers: "Dla śledzących (tylko lokalnie)"
-  note-placeholders:
-    a: "Co robisz?"
-    b: "Co się wydarzyło?"
-    c: "Co Ci chodzi po głowie?"
-    d: "Czy masz coÅ› do powiedzenia?"
-    e: "Napisz coÅ› tutaj!"
-    f: "Czekamy, aż coś napiszesz."
-  settings: "Ustawienia"
-  _settings:
-    profile: "Profil"
-    notification: "Powiadomienia"
-    apps: "Aplikacje"
-    tags: "Hashtagi"
-    mute-and-block: "Wycisz / Zablokuj"
-    blocking: "Blokowanie"
-    security: "Zabezpieczenia"
-    signin: "Historia logowania"
-    password: "Hasło"
-    other: "Inne"
-    appearance: "WyglÄ…d"
-    behavior: "Zachowanie"
-    reactions: "Reakcja"
-    fetch-on-scroll: "Automatycznie ładuj po przeciągnięciu w dół"
-    note-visibility: "Widoczność wpisów"
-    remember-note-visibility: "Zapamiętaj widoczność wpisów"
-    web-search-engine: "Wyszukiwarka internetowa"
-    web-search-engine-desc: "Wzór: https://www.google.com/?#q={{query}}"
-    paste: "Wklej"
-    line-width: "Szerokości linii"
-    line-width-thin: "Cienka"
-    line-width-normal: "Normalna"
-    line-width-thick: "Gruba"
-    font-size: "Rozmiar tekstu"
-    font-size-x-small: "Małe"
-    font-size-medium: "Normalna"
-    font-size-large: "Trochę duży"
-    font-size-x-large: "Duży"
-    deck-column-align-center: "Po środku"
-    deck-column-align-left: "Z lewej"
-    deck-column-align-flexible: "Elastyczne"
-    deck-column-width: "Szerokość kolumn w talii"
-    deck-column-width-narrow: "WÄ…ska"
-    deck-column-width-narrower: "TrochÄ™ wÄ…ska"
-    deck-column-width-normal: "Normalna"
-    deck-column-width-wider: "TrochÄ™ szerokie"
-    deck-column-width-wide: "Szeroka"
-    wallpaper: "Tapeta"
-    choose-wallpaper: "Wybierz tapetÄ™"
-    timeline: "OÅ› czasu"
-    sound: "Dźwięk"
-    volume: "Głośność"
-    test: "Test"
-    update: "Aktualizacja Misskey"
-    version: "Wersja:"
-    do-update: "Sprawdź dostępność nowych aktualizacji"
-    navbar-position-left: "Z lewej"
-    save: "Zapisz"
-    saved: "Zapisano"
-    preview: "Pokaż podgląd"
-  search: "Szukaj"
-  delete: "Usuń"
-  loading: "Ładowanie"
-  ok: "Możesz OK"
-  cancel: "Anuluj"
-  update-available-title: "Aktualizacja jest dostępna"
-  update-available: "Nowa wersja Misskey jest dostępna ({newer}, obecna to {current}). Odśwież stronę, aby zastosować aktualizację."
-  my-token-regenerated: "Twój token został wygenerowany. Zostaniesz wylogowany."
-  hide-password: "Ukryj hasło"
-  show-password: "Pokaż hasło"
-  enter-username: "Wprowadź nazwę użytkownika"
-  view-on-remote: "Dla dopełnienia, zobacz to zdalnie."
-  renoted-by: "{user} udostępnił(a)"
-  error:
-    title: "Coś poszło nie tak"
-    retry: "Ponów próbę"
-  reversi:
-    drawn: "Remis"
-    my-turn: "Twoja kolej"
-    opponent-turn: "Kolej na przeciwnika"
-    won: "{name} wygrał(a)"
-    black: "Czarny"
-    white: "Biały"
-    total: "Łącznie"
-  widgets:
-    analog-clock: "Zegar analogowy"
-    profile: "Profil"
-    calendar: "Kalendarz"
-    timemachine: "Kalendarz (wehikuł czasu)"
-    activity: "Aktywność"
-    rss: "Czytnik RSS"
-    memo: "Notatka"
-    trends: "Na czasie"
-    photo-stream: "Photostream"
-    posts-monitor: "Wykres wpisów"
-    slideshow: "Pokaz slajdów"
-    version: "Wersja"
-    broadcast: "Transmisja"
-    notifications: "Powiadomienia"
-    users: "Polecani użytkownicy"
-    polls: "Ankiety"
-    post-form: "Formularz tworzenia"
-    server: "Informacje o serwerze"
-    nav: "Nawigacja"
-    tips: "Wskazówki"
-    hashtags: "Hashtagi"
-  you: "Ty"
-auth/views/form.vue:
-  permission-ask: "Ta aplikacja wymaga następujących uprawnień:"
-  cancel: "Anuluj"
-  accept: "Przyznaj dostęp."
-auth/views/index.vue:
-  loading: "Ładowanie"
-  denied: "Odrzucono uwierzytelnianie aplikacji."
-  denied-paragraph: "Ta aplikacja nie uzyska dostępu do Twojego konta."
-  already-authorized: "Ta aplikacja została już uwierzytelniona."
-  callback-url: "Powracam do aplikacji."
-  please-go-back: "Wróć do aplikacji."
-  error: "Sesja nie istnieje."
-  sign-in: "Proszę zalogować się."
-common/views/pages/explore.vue:
-  popular-users: "Popularni użytkownicy"
-  popular-tags: "Popularne tagi"
-common/views/components/games/reversi/reversi.vue:
-  matching:
-    waiting-for: "Oczekiwanie na {}"
-    cancel: "Anuluj"
-common/views/components/games/reversi/reversi.game.vue:
-  surrender: "Poddaj siÄ™"
-  surrendered: "Przez poddanie siÄ™"
-common/views/components/games/reversi/reversi.index.vue:
-  title: "Misskey Reversi"
-  sub-title: "Zagraj w Reversi ze znajomymi!"
-  invite: "ZaproÅ›"
-  rule: "Jak grać"
-  mode-invite: "ZaproÅ›"
-  mode-invite-desc: "Zaproś użytkownika do gry."
-  invitations: "Otrzymałeś(-aś) zaproszenie!"
-  my-games: "Moje gry"
-  all-games: "Wszystkie gry"
-  enter-username: "Wprowadź nazwę użytkownika"
-  game-state:
-    ended: "Zakończono"
-    playing: "W trakcie"
-common/views/components/games/reversi/reversi.room.vue:
-  settings-of-the-game: "Ustawienia gry"
-  choose-map: "Wybierz mapÄ™"
-  random: "Losowy"
-  black-or-white: "Czarny/biały"
-  rules: "Zasady"
-  settings-of-the-bot: "Ustawienia bota"
-  this-game-is-started-soon: "Gra rozpocznie się wkrótce"
-  waiting-for-both: "Oczekiwanie na Ciebie"
-  cancel: "Anuluj"
-  ready: "Gotowy"
-  cancel-ready: "Cofnij „gotowy”"
-common/views/components/connect-failed.vue:
-  title: "Nie udało się połączyć z serwerem"
-  description: "Wystąpił problem z Twoim połączeniem z Internetem, lub z serwerem. {Spróbuj ponownie} wkrótce."
-  thanks: "Dziękujemy za korzystanie z Misskey."
-  troubleshoot: "Rozwiązywanie problemów"
-common/views/components/connect-failed.troubleshooter.vue:
-  title: "Rozwiązywanie problemów"
-  network: "Połączenie z siecią"
-  checking-network: "Sprawdzanie połączenia sieciowego"
-  internet: "Połączenie z Internetem"
-  checking-internet: "Sprawdzanie połączenia z Internetem"
-  server: "Połączenie z serwerem"
-  checking-server: "Sprawdzanie połączenia z serwerem"
-  finding: "Wyszukiwanie problemu"
-  no-network: "Brak połączenia z siecią"
-  no-network-desc: "Upewnij się, że jesteś połączony z siecią."
-  no-internet: "Brak połączenia z Internetem"
-  no-internet-desc: "Upewnij się, że jesteś połączony z Internetem."
-  no-server: "Nie udało się połączyć z serwerem"
-  no-server-desc: "Połączenie sieciowe działa, ale nie udało się połączyć z serwerem Misskey. Możliwe że serwer nie działa lub trwają prace konserwacyjne, spróbuj ponownie później."
-  success: "Pomyślnie połączono z serwerem Misskey"
-  success-desc: "Wygląda na to, że udało się połączyć. Odśwież stronę."
-  flush: "Wyczyść pamięć podręczną"
-  set-version: "Określ wersję"
-common/views/components/media-banner.vue:
-  sensitive: "NSFW"
-  click-to-show: "Naciśnij aby wyświetlić"
-common/views/components/theme.vue:
-  theme: "Motyw"
-  light-theme: "Motyw"
-  light-themes: "Jasny Motyw"
-  dark-themes: "Ciemny motyw"
-  install-a-theme: "Zainstaluj motyw"
-  theme-code: "Kod motywu"
-  install: "Zainstaluj"
-  installed: "\"{}\" został zainstalowany"
-  create-a-theme: "Stwórz motyw"
-  save-created-theme: "Zapisz motyw"
-  primary-color: "Kolor podstawowy"
-  secondary-color: "Kolor dodatkowy"
-  text-color: "Kolor tekstu"
-  base-theme: "Podstawowy motyw"
-  base-theme-light: "Jasny"
-  base-theme-dark: "Ciemny"
-  find-more-theme: "Odkryj więcej motywów"
-  theme-name: "Nazwa motywu"
-  preview-created-theme: "Pokaż podgląd"
-  invalid-theme: "Nieprawidłowy motyw"
-  already-installed: "Ten motyw jest już zainstalowany"
-  saved: "Zapisano"
-  manage-themes: "ZarzÄ…dzanie motywami"
-  builtin-themes: "Standardowe motywy"
-  my-themes: "Moje motywy"
-  installed-themes: "Zainstalowane motywy"
-  select-theme: "Wybierz motyw"
-  uninstall: "Odinstaluj"
-  uninstalled: "\"{}\" został odinstalowany"
-  author: "Author"
-  desc: "Opis"
-  export: "Eksportuj"
-  import: "Importuj"
-  import-by-code: "lub wklej kod"
-  theme-name-required: "Nazwa motywu jest obowiÄ…zkowa."
-common/views/components/cw-button.vue:
-  hide: "Ukryj"
-  show: "Pokaż więcej"
-  chars: "{count} znaków"
-  files: "{count} plików"
-  poll: "Ankieta"
-common/views/components/messaging.vue:
-  search-user: "Znajdź użytkownika"
-  you: "Ty"
-  no-history: "Brak historii"
-  user: "Użytkownicy"
-common/views/components/messaging-room.vue:
-  no-history: "Brak dalszej historii"
-  new-message: "Nowa wiadomość"
-common/views/components/messaging-room.form.vue:
-  input-message-here: "Wprowadź wiadomość tutaj"
-  send: "Wyślij"
-  attach-from-local: "Załącz pliki z komputera"
-  attach-from-drive: "Załącz pliki z dysku"
-common/views/components/messaging-room.message.vue:
-  is-read: "Przeczytano"
-  deleted: "Wiadomość została usunięta"
-common/views/components/nav.vue:
-  about: "O stronie"
-  stats: "Statystyki"
-  status: "Stan"
-  wiki: "Wiki"
-  donors: "Sponsorzy"
-  repository: "Repozytorium"
-  develop: "Autorzy"
-  feedback: "Podziel siÄ™ opiniÄ…"
-common/views/components/note-menu.vue:
-  mention: "Wspomnij"
-  detail: "Szczegóły"
-  copy-content: "Skopiuj zawartość"
-  copy-link: "Skopiuj adres"
-  favorite: "Dodaj do ulubionych"
-  unfavorite: "Usuń z ulubionych"
-  pin: "Przypnij do profilu"
-  unpin: "Odepnij"
-  delete: "Usuń"
-  delete-confirm: "Czy na pewno chcesz usunąć ten wpis?"
-  remote: "Pokaż oryginał"
-common/views/components/user-menu.vue:
-  mention: "Wspomnij"
-  mute: "Wycisz"
-  unmute: "Cofnij wyciszenie"
-  block: "Zablokuj"
-  unblock: "Odblokuj"
-  push-to-list: "Dodaj do listy"
-  select-list: "Wybierz listÄ™"
-  report-abuse: "Zgłoś nadużycie"
-common/views/components/poll.vue:
-  vote-to: "Zagłosuj na '{}'"
-  vote-count: "{} głosów"
-  vote: "Zagłosuj"
-  show-result: "Pokaż wyniki"
-  voted: "Zagłosowano"
-common/views/components/poll-editor.vue:
-  no-only-one-choice: "Musisz wprowadzić przynajmniej dwie opcje."
-  choice-n: "Opcja {}"
-  remove: "Usuń tą opcję"
-  add: "+ Dodaj opcjÄ™"
-  destroy: "Usuń tę ankietę"
-  day: "N"
-common/views/components/reaction-picker.vue:
-  choose-reaction: "Wybierz reakcjÄ™"
-common/views/components/emoji-picker.vue:
-  custom-emoji: "Niestandardowe Emoji"
-  people: "Ludzie"
-  animals-and-nature: "Zwierzęta i Natura"
-  food-and-drink: "Żywność i napoje"
-  activity: "Aktywność"
-  travel-and-places: "Podróże i Miejsca"
-  objects: "Rzeczy"
-  symbols: "Symbole"
-  flags: "Flagi"
-common/views/components/settings/app-type.vue:
-  info: "Musisz odświeżyć stronę, aby zmiany zostały uwzględnione."
-common/views/components/signin.vue:
-  username: "Nazwa użytkownika"
-  password: "Hasło"
-  token: "Token"
-  signing-in: "Logowanie…"
-  or: "lub"
-  signin-with-twitter: "Zaloguj siÄ™ za pomocÄ… Twittera"
-  signin-with-github: "Zaloguj się za pomocą GitHuba"
-  signin-with-discord: "Zaloguj się za pomocą Discorda"
-  login-failed: "Logowanie nie powiodło się. Upewnij się, że podałeś prawidłową nazwę użytkownika i hasło."
-common/views/components/signup.vue:
-  invitation-code: "Kod zaproszenia"
-  username: "Nazwa użytkownika"
-  checking: "Sprawdzanie…"
-  available: "Dostępna"
-  unavailable: "Niedostępna"
-  error: "BÅ‚Ä…d sieci"
-  invalid-format: "Może zawierać litery, cyfry i myślniki."
-  too-short: "Wprowadź przynajmniej jeden znak"
-  too-long: "Nazwa nie może zawierać więcej niż 20 znaków"
-  password: "Hasło"
-  password-placeholder: "Zalecamy korzystanie z hasła zawierającego przynajmniej 8 znaków."
-  weak-password: "SÅ‚abe"
-  normal-password: "Åšrednie"
-  strong-password: "Silne"
-  retype: "Powtórz hasło"
-  retype-placeholder: "Potwierdź hasło"
-  password-matched: "OK"
-  password-not-matched: "Hasła nie zgadzają się"
-  recaptcha: "Weryfikacja"
-  create: "Utwórz konto"
-  some-error: "Nie udało się utworzyć konta. Spróbuj ponownie."
-common/views/components/special-message.vue:
-  new-year: "Szczęśliwego nowego roku!"
-  christmas: "Wesołych świąt!"
-common/views/components/stream-indicator.vue:
-  connecting: "Łączenie"
-  reconnecting: "Ponowne Å‚Ä…czenie"
-  connected: "Połączono"
-common/views/components/notification-settings.vue:
-  title: "Powiadomienia"
-  mark-as-read-all-notifications: "Oznacz wszystkie powiadomienia jako przeczytane"
-  mark-as-read-all-unread-notes: "Oznacz wszystkie wpisy jako przeczytane"
-  mark-as-read-all-talk-messages: "Oznacz wszystkie rozmowy jako przeczytane"
-  auto-watch: "Automatycznie nasłuchuj wpisów"
-  auto-watch-desc: "Automatycznie otrzymuj powiadomienia o wpisach, w których zareagowałeś(-aś) lub odpowiedziałeś(-aś)."
-common/views/components/integration-settings.vue:
-  connect: "Połącz"
-  disconnect: "Rozłącz"
-  connected-to: "Jesteś połączony(-a) z następującym kontem"
-common/views/components/github-setting.vue:
-  detail: "Więcej..."
-  reconnect: "Połącz ponownie"
-  disconnect: "Rozłącz"
-common/views/components/discord-setting.vue:
-  detail: "Szczegóły…"
-  reconnect: "Połącz ponownie"
-  disconnect: "Rozłącz"
-common/views/components/uploader.vue:
-  waiting: "Oczekiwanie"
-common/views/components/visibility-chooser.vue:
-  public: "Publiczny"
-  home: "Lokalny"
-  home-desc: "Widoczny tylko na tej instancji"
-  followers: "Dla śledzących"
-  followers-desc: "Widoczny tylko dla osób, które Cię śledzą"
-  specified: "Bezpośredni"
-  specified-desc: "Tylko dla określonych użytkowników"
-  local-public: "Publiczny (tylko lokalnie)"
-  local-followers: "Dla śledzących (tylko lokalnie)"
-common/views/components/trends.vue:
-  empty: "Brak popularnych hashtagów"
-common/views/components/language-settings.vue:
-  title: "Język"
-  pick-language: "Wybierz język"
-  recommended: "Zalecane"
-  auto: "Automatyczny"
-  specify-language: "Wybierz język"
-  info: "Musisz odświeżyć stronę, aby zmiany zostały uwzględnione."
-common/views/components/profile-editor.vue:
-  title: "Twój profil"
-  name: "Nazwa"
-  account: "Konto"
-  location: "Lokalizacja"
-  description: "O mnie"
-  language: "Język"
-  birthday: "Data urodzenia"
-  avatar: "Awatar"
-  banner: "Baner"
-  is-cat: "To konto jest prowadzone przez kota"
-  is-bot: "To konto jest prowadzone przez bota"
-  is-locked: "Prośby śledzenia wymagają zatwierdzenia"
-  careful-bot: "Prośby śledzenia od botów wymagają zatwierdzenia"
-  auto-accept-followed: "Automatyczne zatwierdzaj śledzenia od osób, które śledzisz."
-  advanced: "Inne"
-  privacy: "Prywatność"
-  save: "Zapisz"
-  saved: "Pomyślnie zaktualizowano profil"
-  uploading: "Wysyłanie"
-  upload-failed: "Wysyłanie nie powiodło się"
-  unable-to-process: "Nie udało się ukończyć działania."
-  email: "Ustawienia e-mail"
-  email-address: "Adres e-mail"
-  email-verified: "Twój adres e-mail został zweryfikowany."
-  export: "Eksportuj"
-  import: "Importuj"
-  export-targets:
-    following-list: "Åšledzeni"
-    mute-list: "Wycisz"
-    blocking-list: "Zablokuj"
-    user-lists: "Listy"
-  enter-password: "Wprowadź hasło"
-common/views/components/user-list-editor.vue:
-  users: "Użytkownicy"
-  rename: "Zmień nazwę listy"
-  delete: "Usuń listę"
-  remove-user: "Usuń z tej listy"
-  delete-are-you-sure: "Usunąć listę \"$1\"?"
-  deleted: "Usunięto"
-  add-user: "Dodaj użytkownika"
-common/views/components/user-group-editor.vue:
-  deleted: "Usunięto"
-  invite: "ZaproÅ›"
-common/views/components/user-lists.vue:
-  user-lists: "Listy"
-  list-name: "Nazwa listy"
-common/views/components/user-groups.vue:
-  invites: "ZaproÅ›"
-common/views/widgets/broadcast.vue:
-  fetching: "Sprawdzanie"
-  no-broadcasts: "Brak transmisji"
-  have-a-nice-day: "Miłego dnia!"
-  next: "Dalej"
-common/views/widgets/calendar.vue:
-  year: "Rok {}"
-  month: "MiesiÄ…c {}"
-  day: "Dzień {}"
-  today: "Dzisiaj:"
-  this-month: "Ten miesiÄ…c:"
-  this-year: "Ten rok:"
-common/views/widgets/photo-stream.vue:
-  title: "Photostream"
-  no-photos: "Brak zdjęć"
-common/views/widgets/posts-monitor.vue:
-  title: "Wykres wpisów"
-  toggle: "Przełącz widok"
-common/views/widgets/hashtags.vue:
-  title: "Hashtagi"
-common/views/widgets/server.vue:
-  title: "Informacje o serwerze"
-  toggle: "Przełącz widok"
-common/views/widgets/memo.vue:
-  title: "Notatka"
-  memo: "Napisz tutaj!"
-  save: "Zapisz"
-common/views/widgets/slideshow.vue:
-  folder-customize-mode: "Aby określić katalog, opuść tryb dostosowywania"
-  folder: "Naciśnij i wybierz folder"
-  no-image: "Brak obrazu w tym folderze"
-common/views/widgets/tips.vue:
-  tips-line1: "Możesz przejść do osi czasu używając <kbd>t</kbd>."
-  tips-line2: "Otwórz formularz nowego wpisu używając <kbd>p</kbd> lub <kbd>n</kbd>."
-  tips-line3: "Możesz przeciągnąć i upuścić pliki w formularzu wpisu."
-  tips-line5: "Możesz wysłać pliki przeciągając i upuszczając je w Dysku."
-  tips-line6: "Możesz przenieść katalog przeciągając go w Dysku."
-  tips-line7: "Możesz przenieść katalog przeciągając go w Dysku."
-  tips-line8: "Strona główna może zostać dostosowana w ustawieniach."
-  tips-line9: "Misskey jest dostępny na licencji AGPLv3."
-  tips-line11: "Możesz przypiąć wpis na stronie użytkownika klikając na „…”"
-  tips-line17: "Oznaczenie tekstu **w ten sposób** wyróżni go."
-  tips-line19: "Część okien może zostać odłączona z przeglądarki."
-  tips-line21: "Możesz też używać API, aby tworzyć boty."
-  tips-line24: "Misskey zaczął działać w 2014."
-  tips-line25: "Możesz otrzymywać powiadomienia nawet jeżeli Misskey nie jest otwarty w obsługiwanej przeglądarce."
-common/views/pages/not-found.vue:
-  page-not-found: "Strona nie została znaleziona"
-common/views/pages/follow.vue:
-  signed-in-as: "Zalogowany jako {}"
-  following: "Åšledzisz"
-  follow: "Śledź"
-  request-pending: "Oczekiwanie na pozwolenie"
-  follow-processing: "Przetwarzanie śledzenia"
-  follow-request: "Poproś o śledzenie"
-common/views/pages/follow-requests.vue:
-  received-follow-requests: "Prośby o śledzenie"
-desktop:
-  banner: "Baner"
-  uploading-banner: "Wysyłanie baneru"
-  banner-updated: "Zmieniono baner"
-  choose-banner: "Wybierz baner"
-  avatar-crop-title: "Wybierz część obrazu, która zostanie użyta jako awatar"
-  avatar: "Awatar"
-  uploading-avatar: "Wysyłanie awatara"
-  avatar-updated: "Wysłano awatar"
-  choose-avatar: "Wybierz awatar"
-  unable-to-process: "Nie udało się ukończyć działania."
-desktop/views/components/activity.chart.vue:
-  total: "Czarny … Łącznie"
-  notes: "Niebieski … Wpisy"
-  replies: "Czerwony … Odpowiedzi"
-  renotes: "Czerwony … Udostępnienia"
-desktop/views/components/activity.vue:
-  title: "Aktywność"
-  toggle: "Przełącz widok"
-desktop/views/components/calendar.vue:
-  title: "{year} / {month}"
-  prev: "Poprzedni miesiÄ…c"
-  next: "Następny miesiąc"
-  go: "Naciśnij, aby przejść"
-desktop/views/components/choose-file-from-drive-window.vue:
-  chosen-files: "Wybrano {count} Plik(ów)"
-  upload: "Wyślij pliki z Twojego komputera"
-  cancel: "Anuluj"
-  ok: "OK"
-  choose-prompt: "Wybierz plik"
-desktop/views/components/choose-folder-from-drive-window.vue:
-  cancel: "Anuluj"
-  ok: "OK"
-  choose-prompt: "Wybierz katalog"
-desktop/views/components/crop-window.vue:
-  skip: "Pomiń przycinanie"
-  cancel: "Anuluj"
-  ok: "OK"
-desktop/views/components/drive-window.vue:
-  used: "wykorzystane"
-desktop/views/components/drive.file.vue:
-  avatar: "Awatar"
-  banner: "Baner"
-  nsfw: "NSFW"
-  contextmenu:
-    rename: "Zmień nazwę"
-    mark-as-sensitive: "Oznacz jako zawartość wrażliwą"
-    unmark-as-sensitive: "Cofnij oznaczenie jako zawartość wrażliwą"
-    copy-url: "Skopiuj adres"
-    download: "Pobierz"
-    else-files: "Inne"
-    set-as-avatar: "Ustaw jako awatar"
-    set-as-banner: "Ustaw jako baner"
-    open-in-app: "Otwórz w aplikacji"
-    add-app: "Dodaj aplikacjÄ™"
-    rename-file: "Zmień nazwę pliku"
-    input-new-file-name: "Wprowadź nową nazwę"
-    copied: "Skopiowano"
-    copied-url-to-clipboard: "Skopiowano adres do schowka"
-desktop/views/components/drive.folder.vue:
-  unable-to-process: "Nie udało się ukończyć działania."
-  circular-reference-detected: "Docelowy katalog znajduje się w katalogu, który chcesz przenieść."
-  unhandled-error: "Nieznany błąd"
-  contextmenu:
-    move-to-this-folder: "PrzenieÅ› do tego katalogu"
-    show-in-new-window: "Otwórz w nowym oknie"
-    rename: "Zmień nazwę"
-    rename-folder: "Zmień nazwę katalogu"
-    input-new-folder-name: "Wprowadź nową nazwę"
-    else-folders: "Inne"
-desktop/views/components/drive.vue:
-  search: "Szukaj"
-  empty-draghover: "PrzeciÄ…gnij tutaj!"
-  empty-drive: "Twój dysk jest pusty"
-  empty-drive-description: "Możesz wysłać plik klikając prawym przyciskiem myszy i wybierając \"Wyślij plik\" lub przeciągnąć plik i upuścić w tym oknie."
-  empty-folder: "Ten katalog jest posty"
-  unable-to-process: "Nie udało się dokończyć działania."
-  circular-reference-detected: "Ten katalog znajduje się w katalogu, który chcesz przenieść."
-  unhandled-error: "Nieznany błąd"
-  url-upload: "Wyślij z adresu"
-  url-of-file: "Adres URL pliku, który chcesz wysłać"
-  url-upload-requested: "Zaplanowano wysyłanie"
-  may-take-time: "Może trochę potrwać, zanim wysyłanie zostanie ukończone."
-  create-folder: "Utwórz katalog"
-  folder-name: "Nazwa katalogu"
-  contextmenu:
-    create-folder: "Utwórz katalog"
-    upload: "Wyślij plik"
-    url-upload: "Wyślij z adresu URL"
-desktop/views/components/media-video.vue:
-  sensitive: "To jest zawartość NSFW"
-  click-to-show: "Naciśnij aby wyświetlić"
-desktop/views/components/followers-window.vue:
-  followers: "ÅšledzÄ…cy"
-desktop/views/components/followers.vue:
-  empty: "Wygląda na to, że nikt Cię nie śledzi…"
-desktop/views/components/following-window.vue:
-  following: "Åšledzeni przez {}"
-desktop/views/components/following.vue:
-  empty: "Nikt Cię nie śledzi."
-desktop/views/components/game-window.vue:
-  game: "Reversi"
-desktop/views/components/home.vue:
-  done: "Zakończ"
-  add-widget: "Dodaj widżet:"
-  add: "Dodaj"
-desktop/views/input-dialog.vue:
-  cancel: "Anuluj"
-  ok: "OK"
-desktop/views/components/note-detail.vue:
-  private: "ten wpis jest prywatny"
-  deleted: "ten wpis został usunięty"
-  location: "Informacje o lokalizacji"
-  renote: "Udostępnij"
-  add-reaction: "Dodaj reakcjÄ™"
-desktop/views/components/note.vue:
-  reply: "Odpowiedz"
-  renote: "Udostępnij"
-  add-reaction: "Dodaj reakcjÄ™"
-  detail: "Szczegóły"
-  private: "Ten wpis jest prywatny"
-  deleted: "ten wpis został usunięty"
-desktop/views/components/notes.vue:
-  error: "Ładowanie nie powiodło się."
-  retry: "Spróbuj ponownie"
-desktop/views/components/notifications.vue:
-  empty: "Brak powiadomień"
-desktop/views/components/post-form.vue:
-  posted: "Opublikowano!"
-  replied: "Odpowiedziano!"
-  reposted: "Udostępniono!"
-  note-failed: "Nie udało się wysłać"
-  reply-failed: "Nie udało się odpowiedzieć"
-  renote-failed: "Nie udało się udostępnić"
-desktop/views/components/post-form-window.vue:
-  note: "Nowy wpis"
-  reply: "Odpowiedz"
-  attaches: "{} załączników multimedialnych"
-  uploading-media: "Wysyłanie {} treści multimedialnych"
-desktop/views/components/progress-dialog.vue:
-  waiting: "Oczekiwanie"
-desktop/views/components/renote-form.vue:
-  quote: "Cytuj…"
-  cancel: "Anuluj"
-  renote: "Udostępnij"
-  reposting: "Udostępnianie…"
-  success: "Udostępniono!"
-  failure: "Nie udało się udostępnić"
-desktop/views/components/renote-form-window.vue:
-  title: "Czy na pewno chcesz udostępnić ten wpis?"
-desktop/views/components/settings.2fa.vue:
-  intro: "Jeżeli skonfigurujesz uwierzytelnianie dwuetapowe, aby zablokować się będziesz potrzebować (oprócz hasła) kodu ze skonfigurowanego urządzenia (np. smartfonu), co zwiększy bezpieczeństwo."
-  detail: "Zobacz szczegóły…"
-  url: "https://www.google.com/landing/2step/"
-  caution: "Jeżeli stracisz dostęp do urządzenia, nie będziesz mógł logować się do Misskey!"
-  register: "Zarejestruj urzÄ…dzenie"
-  already-registered: "Urządzenie jest już zarejestrowane"
-  unregister: "Wyłącz"
-  unregistered: "Wyłączono uwierzytelnianie dwuetapowe."
-  enter-password: "Wprowadź hasło"
-  authenticator: "Na początek musisz zainstalować Google Authenticator na swoim urządzeniu:"
-  howtoinstall: "Jak zainstalować"
-  token: "Token"
-  scan: "Później, zeskanuje ten kod QR:"
-  done: "Wprowadź token wyświetlony na Twoim urządzeniu:"
-  submit: "Wyślij"
-  success: "Pomyślnie ukończono konfigurację!"
-  failed: "Nie udało się skonfigurować uwierzytelniania dwuetapowego, upewnij się że wprowadziłeś prawidłowy token."
-  info: "Od teraz, wprowadzaj token wyświetlany na urządzeniu przy każdym logowaniu do Misskey."
-common/views/components/media-image.vue:
-  sensitive: "To jest zawartość NSFW"
-  click-to-show: "Naciśnij aby wyświetlić"
-common/views/components/api-settings.vue:
-  intro: "Aby uzyskać dostęp do API, ustaw ten token jako klucz 'i' parametrów żądań."
-  caution: "Nie pokazuj tego tokenu osobom trzecim (nie wprowadzaj go nigdzie indziej), aby konto nie trafiło w niepowołane ręce."
-  regeneration-of-token: "W przypadku wycieku tokenu, możesz wygenerować nowy."
-  regenerate-token: "Wygeneruj nowy token"
-  token: "Token:"
-  enter-password: "Wprowadź hasło"
-  console:
-    title: "Konsola API"
-    parameter: "Parametry"
-    send: "Wyślij"
-desktop/views/components/settings.apps.vue:
-  no-apps: "Brak zautoryzowanych aplikacji"
-common/views/components/drive-settings.vue:
-  max: "Max"
-  in-use: "użyto"
-  stats: "Statystyki"
-  default-upload-folder-name: "Katalog(i)"
-common/views/components/mute-and-block.vue:
-  mute-and-block: "Wycisz / Zablokuj"
-  mute: "Wycisz"
-  block: "Zablokuj"
-  no-muted-users: "Brak wyciszonych użytkowników"
-  no-blocked-users: "Brak zablokowanych użytkowników"
-  word-mute: "Wyciszenie słowa"
-  muted-words: "Wyciszone słowa kluczowe"
-  save: "Zapisz"
-common/views/components/password-settings.vue:
-  reset: "Zmień hasło"
-  enter-current-password: "Wprowadź obecne hasło"
-  enter-new-password: "Wprowadź nowe hasło"
-  enter-new-password-again: "Wprowadź ponownie nowe hasło"
-common/views/components/post-form-attaches.vue:
-  mark-as-sensitive: "Oznacz jako zawartość wrażliwą"
-  unmark-as-sensitive: "Cofnij oznaczenie jako zawartość wrażliwą"
-desktop/views/components/sub-note-content.vue:
-  private: "ten wpis jest prywatny"
-  deleted: "ten wpis został usunięty"
-  media-count: "{}zawartości multimedialnej"
-  poll: "Ankieta"
-desktop/views/components/settings.tags.vue:
-  title: "Tagi"
-  query: "Zapytanie (opcjonalne)"
-  add: "Dodaj"
-  save: "Zapisz"
-desktop/views/components/timeline.vue:
-  home: "Strona główna"
-  local: "Lokalne"
-  global: "Globalne"
-  mentions: "Wspomnienia"
-  messages: "Bezpośrednie wpisy"
-  list: "Listy"
-  hashtag: "Hashtag"
-  add-tag-timeline: "Dodaj hashtag"
-  add-list: "Dodaj listÄ™"
-  list-name: "Nazwa listy"
-desktop/views/components/ui.header.vue:
-  welcome-back: "Witaj ponownie,"
-desktop/views/components/ui.header.account.vue:
-  profile: "Twój profil"
-  lists: "Listy"
-  follow-requests: "Prośby o śledzenie"
-  admin: "Admin"
-desktop/views/components/ui.header.nav.vue:
-  game: "Gra"
-desktop/views/components/ui.header.notifications.vue:
-  title: "Powiadomienia"
-desktop/views/components/ui.header.post.vue:
-  post: "Utwórz nowy wpis"
-desktop/views/components/ui.header.search.vue:
-  placeholder: "Szukaj"
-desktop/views/components/user-preview.vue:
-  notes: "Wpisy"
-  following: "Åšledzeni"
-  followers: "ÅšledzÄ…cy"
-desktop/views/components/users-list.vue:
-  all: "Wszyscy"
-  iknow: "Znasz"
-  fetching: "Ładowanie…"
-desktop/views/components/users-list-item.vue:
-  followed: "Obserwuje CiÄ™"
-desktop/views/components/window.vue:
-  popout: "Pop-out"
-  close: "Zamknij"
-admin/views/index.vue:
-  dashboard: "Kokpit"
-  instance: "Instancja"
-  emoji: "Emoji"
-  moderators: "Moderatorzy"
-  users: "Użytkownicy"
-  announcements: "Ogłoszenia"
-admin/views/dashboard.vue:
-  dashboard: "Kokpit"
-  accounts: "Konta"
-  notes: "Wpisy"
-  drive: "Dysk"
-  instances: "Instancja"
-admin/views/logs.vue:
-  levels:
-    info: "Informacje"
-    error: "BÅ‚Ä…d"
-admin/views/abuse.vue:
-  details: "Szczegóły"
-  remove-report: "Usuń"
-admin/views/instance.vue:
-  instance: "Instancja"
-  recaptcha-preview: "Pokaż podgląd"
-  github-integration-client-id: "Client ID"
-  github-integration-client-secret: "Client Secret"
-  discord-integration-client-id: "Client ID"
-  discord-integration-client-secret: "Client Secret"
-  invite: "ZaproÅ›"
-  save: "Zapisz"
-  saved: "Zapisano"
-  email: "Adres e-mail"
-  test-email: "Test"
-admin/views/charts.vue:
-  notes: "Wpisy"
-  users: "Użytkownicy"
-  drive: "Dysk"
-  network: "Sieć"
-  charts:
-    network-requests: "Żądania"
-    network-time: "Czas reakcji"
-admin/views/drive.vue:
-  sort:
-    title: "Sortuj"
-  origin:
-    title: "Źródło"
-    local: "Lokalne"
-    remote: "Zdalny"
-  delete: "Usuń"
-  deleted: "Usunięto"
-  mark-as-sensitive: "Oznacz jako zawartość wrażliwą"
-  unmark-as-sensitive: "Cofnij oznaczenie jako zawartość wrażliwą"
-admin/views/users.vue:
-  user-not-found: "Nie znaleziono użytkownika"
-  username: "Nazwa użytkownika"
-  users:
-    title: "Użytkownicy"
-    sort:
-      title: "Sortuj"
-    state:
-      all: "Wszyscy"
-      moderator: "Moderatorzy"
-    origin:
-      title: "Źródło"
-      local: "Lokalny"
-      remote: "Zdalny"
-    createdAt: "Utworzono"
-admin/views/moderators.vue:
-  add-moderator:
-    add: "Zarejestruj siÄ™"
-  logs:
-    moderator: "Moderatorzy"
-    info: "Informacje"
-admin/views/emoji.vue:
-  add-emoji:
-    name: "Nazwa Emoji"
-    aliases: "Aliasy"
-    add: "Dodaj"
-  emojis:
-    update: "Aktualizuj"
-    remove: "Usuń"
-  updated: "Zaktualizowano"
-  remove-emoji:
-    are-you-sure: "Usunąć \"$1\"?"
-    removed: "Usunięto"
-admin/views/announcements.vue:
-  announcements: "Ogłoszenia"
-  save: "Zapisz"
-  remove: "Usuń"
-  add: "Dodaj"
-  title: "Tytuł"
-  saved: "Zapisano"
-  _remove:
-    are-you-sure: "Usunąć \"$1\"?"
-    removed: "Usunięto"
-admin/views/federation.vue:
-  instance: "Instancja"
-  notes: "Wpis"
-  users: "Użytkownicy"
-  following: "Åšledzisz"
-  followers: "ÅšledzÄ…cy"
-  caught-at: "Utworzono"
-  status: "Stan"
-  block: "Zablokuj"
-  sort: "Sortuj"
-  states:
-    all: "Wszyscy"
-    blocked: "Zablokuj"
-  chart-srcs:
-    requests: "Żądania"
-  blocked-hosts: "Zablokuj"
-  save: "Zapisz"
-desktop/views/pages/welcome.vue:
-  about: "O Misskey"
-  timeline: "OÅ› czasu"
-  announcements: "Ogłoszenia"
-  photos: "Ostatnie obrazy"
-  powered-by-misskey: "Oparto o <b>Misskey</b>."
-  info: "Informacje"
-desktop/views/pages/drive.vue:
-  title: "Dysk Misskey"
-desktop/views/pages/note.vue:
-  prev: "Poprzedni wpis"
-  next: "Następny wpis"
-desktop/views/pages/selectdrive.vue:
-  title: "Wybierz plik(i)"
-  ok: "OK"
-  cancel: "Anuluj"
-  upload: "Wyślij pliki z Twojego komputera"
-desktop/views/pages/user-list.users.vue:
-  users: "Użytkownicy"
-  add-user: "Dodaj użytkownika"
-  username: "Nazwa użytkownika"
-desktop/views/pages/user/user.followers-you-know.vue:
-  title: "Śledzący których znasz"
-  loading: "Ładowanie"
-  no-users: "Brak użytkowników"
-desktop/views/pages/user/user.friends.vue:
-  title: "Najbardziej aktywni"
-  loading: "Ładowanie"
-  no-users: "Brak użytkowników"
-desktop/views/pages/user/user.photos.vue:
-  title: "Zdjęcia"
-  loading: "Ładowanie"
-  no-photos: "Brak zdjęć"
-desktop/views/pages/user/user.header.vue:
-  posts: "Wpisy"
-  following: "Åšledzeni"
-  followers: "ÅšledzÄ…cy"
-  is-bot: "To konto jest botem"
-  years-old: "{age} lat"
-  year: "/"
-  month: "/"
-  day: "-"
-  follows-you: "Åšledzi CiÄ™"
-desktop/views/pages/user/user.timeline.vue:
-  default: "Wpisy"
-  with-replies: "Wpisy i odpowiedzi"
-  with-media: "Multimedia"
-  my-posts: "Moje wpisy"
-desktop/views/widgets/notifications.vue:
-  title: "Powiadomienia"
-desktop/views/widgets/polls.vue:
-  title: "Ankiety"
-  refresh: "Pokaż inne"
-  nothing: "Pusto"
-desktop/views/widgets/post-form.vue:
-  title: "Wpis"
-  note: "Wpis"
-desktop/views/widgets/profile.vue:
-  update-banner: "Naciśnij, aby zmienić baner"
-  update-avatar: "Naciśnij, aby zmienić awatar"
-desktop/views/widgets/trends.vue:
-  title: "Na czasie"
-  refresh: "Pokaż inne"
-  nothing: "Pusto"
-desktop/views/widgets/users.vue:
-  title: "Polecani użytkownicy"
-  refresh: "Pokaż innych"
-  no-one: "Pusto"
-mobile/views/components/drive.vue:
-  used: "użyto"
-  folder-count: "Katalog(i)"
-  count-separator: ", "
-  file-count: "Plik(i)"
-  nothing-in-drive: "Pusto"
-  folder-is-empty: "Ten katalog jest pusty"
-  folder-name: "Nazwa katalogu"
-  url-prompt: "Adres URL pliku, który chcesz wysłać"
-  uploading: "Rozpoczęto wysyłanie. Może to trochę potrwać."
-mobile/views/components/drive-file-chooser.vue:
-  select-file: "Wybierz plik"
-mobile/views/components/drive-folder-chooser.vue:
-  select-folder: "Wybierz katalog"
-mobile/views/components/drive.file.vue:
-  nsfw: "NSFW"
-mobile/views/components/drive.file-detail.vue:
-  download: "Pobierz"
-  rename: "Zmień nazwę"
-  move: "PrzenieÅ›"
-  hash: "Hash (md5)"
-  exif: "EXIF"
-  nsfw: "NSFW"
-  mark-as-sensitive: "Oznacz jako zawartość wrażliwą"
-  unmark-as-sensitive: "Cofnij oznaczenie jako zawartość wrażliwą"
-mobile/views/components/media-video.vue:
-  sensitive: "To jest zawartość NSFW"
-  click-to-show: "Naciśnij aby wyświetlić"
-common/views/components/follow-button.vue:
-  following: "Åšledzisz"
-  follow: "Śledź"
-  request-pending: "Oczekiwanie na pozwolenie"
-  follow-processing: "Przetwarzanie"
-  follow-request: "Poproś o śledzenie"
-mobile/views/components/note.vue:
-  private: "ten wpis jest prywatny"
-  deleted: "ten wpis został usunięty"
-  location: "Informacje o lokalizacji"
-mobile/views/components/note-detail.vue:
-  reply: "Odpowiedz"
-  reaction: "Reakcja"
-  private: "ten wpis jest prywatny"
-  deleted: "ten wpis został usunięty"
-  location: "Informacje o lokalizacji"
-mobile/views/components/note-preview.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "kot"
-mobile/views/components/note-sub.vue:
-  admin: "admin"
-  bot: "bot"
-  cat: "kot"
-mobile/views/components/notifications.vue:
-  empty: "Brak powiadomień"
-mobile/views/components/sub-note-content.vue:
-  private: "ten wpis jest prywatny"
-  deleted: "ten wpis został usunięty"
-  media-count: "{}zawartości multimedialnej"
-  poll: "Ankieta"
-mobile/views/components/ui.header.vue:
-  welcome-back: "Witaj ponownie, "
-mobile/views/components/ui.nav.vue:
-  timeline: "OÅ› czasu"
-  notifications: "Powiadomienia"
-  follow-requests: "Prośby o śledzenie"
-  search: "Szukaj"
-  user-lists: "Listy"
-  widgets: "Widżety"
-  game: "Gry"
-  admin: "Admin"
-  about: "O Misskey"
-mobile/views/pages/drive.vue:
-  contextmenu:
-    upload: "Wyślij plik"
-    create-folder: "Utwórz katalog"
-mobile/views/pages/signup.vue:
-  lets-start: "Rozpocznijmy! 📦"
-mobile/views/pages/home.vue:
-  home: "Strona główna"
-  local: "Lokalne"
-  global: "Globalne"
-  mentions: "Wspomnienia"
-  messages: "Bezpośrednie wpisy"
-mobile/views/pages/widgets.vue:
-  dashboard: "Kokpit"
-  add-widget: "Dodaj"
-  customization-tips: "Wskazówki o dostosowywaniu"
-mobile/views/pages/widgets/activity.vue:
-  activity: "Aktywność"
-mobile/views/pages/note.vue:
-  title: "Wpis"
-  prev: "Poprzedni wpis"
-  next: "Następny wpis"
-mobile/views/pages/games/reversi.vue:
-  reversi: "Reversi"
-mobile/views/pages/search.vue:
-  search: "Szukaj"
-mobile/views/pages/selectdrive.vue:
-  select-file: "Wybierz plik"
-mobile/views/pages/notifications.vue:
-  notifications: "Powiadomienia"
-mobile/views/pages/settings.vue:
-  signed-in-as: "Zalogowany jako {}"
-mobile/views/pages/user.vue:
-  follows-you: "Åšledzi CiÄ™"
-  following: "Åšledzeni"
-  followers: "ÅšledzÄ…cy"
-  notes: "Wpisy"
-  overview: "PrzeglÄ…d"
-  timeline: "OÅ› czasu"
-  media: "Multimedia"
-  years-old: "{age} lat"
-mobile/views/pages/user/home.vue:
-  recent-notes: "Ostatnie wpisy"
-  images: "Zdjęcia"
-  activity: "Aktywność"
-  keywords: "SÅ‚owa kluczowe"
-  domains: "Domeny"
-  frequently-replied-users: "Najbardziej aktywni"
-  followers-you-know: "Śledzący których znasz"
-  last-used-at: "Ostatnio aktywny"
-mobile/views/pages/user/home.photos.vue:
-  no-photos: "Brak zdjęć"
-deck:
-  widgets: "Widżety"
-  home: "Strona główna"
-  local: "Lokalne"
-  hashtag: "Hashtag"
-  global: "Globalne"
-  mentions: "Wspomnienia"
-  direct: "Bezpośrednie wpisy"
-  notifications: "Powiadomienia"
-  list: "Listy"
-  select-list: "Wybierz listÄ™"
-  swap-left: "Przesuń w lewo"
-  swap-right: "Przesuń w prawo"
-  swap-up: "Przenieś w górę"
-  remove: "Usuń"
-  add-column: "Dodaj kolumnÄ™"
-  rename: "Zmień nazwę"
-  stack-left: "Przypnij do lewej"
-deck/deck.tl-column.vue:
-  is-media-only: "Tylko wpisy z zawartością multimedialną"
-  edit: "Opcje"
-deck/deck.user-column.vue:
-  follows-you: "Åšledzi CiÄ™"
-  posts: "Wpisy"
-  following: "Åšledzeni"
-  followers: "ÅšledzÄ…cy"
-  images: "Zdjęcia"
-  activity: "Aktywność"
-  timeline: "OÅ› czasu"
-  pinned-notes: "Przypięte posty"
-docs:
-  edit-this-page-on-github: "Znalazłeś błąd lub chcesz pomóc w tworzeniu dokumentacji?"
-  edit-this-page-on-github-link: "Edytuj stronÄ™ na GitHubie!"
-dev/views/index.vue:
-  manage-apps: "ZarzÄ…dzaj aplikacjami"
-dev/views/apps.vue:
-  manage-apps: "ZarzÄ…dzaj aplikacjami"
-  app-missing: "Brak aplikacji"
-dev/views/new-app.vue:
-  app-name: "Nazwa Aplikacji"
-  authority: "Uprawnienia"
-pages:
-  pin-this-page: "Przypnij do profilu"
-  unpin-this-page: "Odepnij"
-  like: "LubiÄ™"
-  title: "Tytuł"
-  blocks:
-    image: "Zdjęcia"
-    post: "Formularz tworzenia"
-    _textInput:
-      text: "Tytuł"
-    _textareaInput:
-      text: "Tytuł"
-    _numberInput:
-      text: "Tytuł"
-    _switch:
-      text: "Tytuł"
-    _counter:
-      text: "Tytuł"
-    _button:
-      text: "Tytuł"
-    _radioButton:
-      title: "Tytuł"
-  script:
-    categories:
-      random: "Losowy"
-      list: "Listy"
-    blocks:
-      _join:
-        arg1: "Listy"
-      random: "Losowy"
-      _randomPick:
-        arg1: "Listy"
-      _dailyRandomPick:
-        arg1: "Listy"
-      _seedRandomPick:
-        arg2: "Listy"
-      _pick:
-        arg1: "Listy"
-      _listLen:
-        arg1: "Listy"
-    types:
-      array: "Listy"
-room:
-  translate: "PrzenieÅ›"
-  save: "Zapisz"
-  saved: "Zapisano"
-  furnitures:
-    moon: "Księżyc"
-    bin: "Kosz"
diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml
deleted file mode 100644
index 772e1c198ca03a203c1180582055a30ea5d080c4..0000000000000000000000000000000000000000
--- a/locales/pt-PT.yml
+++ /dev/null
@@ -1,288 +0,0 @@
----
-meta:
-  lang: "Português"
-common:
-  misskey: "Uma ⭐ do fediverso"
-  about-title: "Uma ⭐ do fediverso."
-  about: "Obrigado por encontrar Misskey. Uma <b>plataforma descentralizada de microblog</b> nascida na Terra. Já que ela existe no Fediverso (um universo onde várias plataformas de mídia social são organizadas), ela é ligada com outras plataformas.Por que você não tira uma folga do agito e confusão da cidade, e mergulha em uma nova internet?"
-  intro:
-    title: "O que é Misskey?"
-    about: "Misskey é um <b>serviço de microblog descentralizado</b>. Personalização sofisticada da interface, variedade de reações a posts, armazenamento de arquivos grátis com gerenciamento integrado e outras funções avançadas estão disponíveis. Um sistema em rede chamado \"Fediverso\" permite que nos comuniquemos com usuários em outras redes sociais. Se você postar algo, por exemplo, seu post não será mandado apenas para o Misskey, mas também para o Mastodon. Apenas imagine que o planeta está enviando ondas de rádio para outros planetas para se comunicar."
-    features: "Recursos"
-    rich-contents: "Post"
-    rich-contents-desc: "Apenas poste suas ideias, temas do momento e qualquer coisa que você queira compartilhar. Você pode querer decorar suas palavras, anexar suas imagens favoritas, enviar arquivos, inclusive vídeos ou criar uma enquete. Essas são as coisas que você pode fazer em Misskey."
-    reaction: "Reações"
-    reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません"
-  application-authorization: "Aplicativos autorizados"
-  close: "Fechar"
-  do-not-copy-paste: "Por favor, não digite ou copie o código aqui. A conta pode ser comprometida."
-  notification-types:
-    follow: "Seguindo"
-  got-it: "Entendi!"
-  customization-tips:
-    title: "Dicas de personalização"
-    gotit: "Entendi!"
-  notification:
-    file-uploaded: "Arquivo enviado!"
-    message-from: "Mensagem de {}:"
-    reversi-invited: "Convidado a jogar"
-    reversi-invited-by: "Convidado por {}:"
-    notified-by: "Notificado por {}:"
-    reply-from: "Resposta de {}:"
-    quoted-by: "Citado por {}:"
-  time:
-    unknown: "Desconhecido"
-    future: "futuro"
-    just_now: "agora"
-    seconds_ago: "{} sec atrás"
-    minutes_ago: "{} min atrás"
-    hours_ago: "{} h atrás"
-    days_ago: "{} d atrás"
-    weeks_ago: "{} sem atrás"
-    months_ago: "{} m atrás"
-    years_ago: "{} ano(s) atrás"
-  month-and-day: "{day}/{month}"
-  trash: "Lixo"
-  timeline: "Linha do tempo"
-  followers: "Seguidores"
-  post-form:
-    enter-username: "Digite o nome de usuário."
-    username-prompt: "Digite o nome de usuário."
-  weekday-short:
-    sunday: "Dom"
-    monday: "Seg"
-    tuesday: "Ter"
-    wednesday: "Qua"
-    thursday: "Qui"
-    friday: "Sex"
-    saturday: "Seb"
-  weekday:
-    sunday: "domingo"
-    monday: "segunda"
-    tuesday: "terça"
-    wednesday: "quarta"
-    thursday: "quinta"
-    friday: "sexta"
-    saturday: "sábado"
-  reactions:
-    like: "Curtir"
-    love: "Amei"
-    laugh: "Riso"
-    hmm: "Hmm...?"
-    surprise: "Uau"
-    congrats: "Parabéns!"
-    angry: "Raiva"
-    confused: "Confuso"
-    rip: "RIP"
-    pudding: "Pudim"
-  note-visibility:
-    followers: "Seguidores"
-  note-placeholders:
-    a: "O que está fazendo?"
-    b: "O que está acontecendo?"
-    c: "No que está pensando?"
-    d: "Quer postar algo?"
-    e: "Escreva aqui"
-    f: "Esperando você escrever."
-  _settings:
-    timeline: "Linha do tempo"
-  search: "Buscar"
-  delete: "Apagar"
-  loading: "Carregando"
-  update-available-title: "Atualização disponível"
-  update-available: "Uma nova versão de Misskey está disponível ({newer}). A versão atual é {current}. Recarregue a página para atualizar."
-  my-token-regenerated: "Seu token foi recriado, portanto você foi deslogado."
-  enter-username: "Digite o nome de usuário."
-  reversi:
-    drawn: "Empatado"
-    my-turn: "Seu turno"
-    opponent-turn: "Turno do oponente"
-    black: "Pretas"
-    white: "Brancas"
-    total: "Total"
-  widgets:
-    analog-clock: "Relógio analógico"
-    profile: "Perfil"
-    calendar: "Calendário"
-    timemachine: "Calendário (máquina do tempo)"
-    activity: "Atividade"
-    rss: "Leitor de RSS"
-    memo: "Nota adesiva"
-    trends: "Tendências"
-    posts-monitor: "Gráfico de publicações"
-    version: "Versão"
-    notifications: "Notificações"
-    users: "Usuário sugeridos"
-    polls: "Enquetes"
-    post-form: "Formulário de publicação"
-    server: "Informações do servidor"
-    nav: "Navegação"
-    tips: "Dicas"
-    hashtags: "Hashtags"
-  you: "Você"
-auth/views/form.vue:
-  permission-ask: "Este aplicativo precisa das seguintes permissões:"
-  cancel: "Cancelar"
-  accept: "Permitir acesso"
-auth/views/index.vue:
-  loading: "Carregando"
-  already-authorized: "Este aplicativo já foi autorizado"
-  allowed: "Aplicativos com acesso autorizado"
-  callback-url: "Voltando ao aplicativo"
-  please-go-back: "Por favor, volte ao aplicativo."
-  error: "A sessão não existe."
-  sign-in: "Por favor, entre."
-common/views/components/games/reversi/reversi.index.vue:
-  invite: "Convidar"
-  rule: "Como jogar"
-  mode-invite: "Convidar"
-  mode-invite-desc: "Convidar um usuário para jogar"
-  invitations: "Você foi convidado!"
-  my-games: "Meu jogo"
-  all-games: "Todos os jogos"
-  enter-username: "Digite o nome de usuário."
-  game-state:
-    ended: "Terminado"
-common/views/components/games/reversi/reversi.room.vue:
-  rules: "Regras"
-  cancel: "Cancelar"
-common/views/components/connect-failed.troubleshooter.vue:
-  flush: "Limpar o cache"
-common/views/components/theme.vue:
-  desc: "Descrição"
-common/views/components/cw-button.vue:
-  poll: "Enquetes"
-common/views/components/messaging.vue:
-  you: "Você"
-common/views/components/note-menu.vue:
-  delete: "Apagar"
-common/views/components/poll-editor.vue:
-  day: "Dom"
-common/views/components/visibility-chooser.vue:
-  followers: "Seguidores"
-common/views/components/profile-editor.vue:
-  name: "Nome"
-  export-targets:
-    following-list: "Seguindo"
-common/views/components/user-group-editor.vue:
-  invite: "Convidar"
-common/views/components/user-groups.vue:
-  invites: "Convidar"
-common/views/widgets/posts-monitor.vue:
-  title: "Gráfico de publicações"
-common/views/widgets/memo.vue:
-  title: "Nota adesiva"
-common/views/pages/follow.vue:
-  follow: "Seguindo"
-desktop/views/components/choose-file-from-drive-window.vue:
-  upload: "Envie arquivos do seu dispositivo"
-  ok: "OK"
-desktop/views/components/choose-folder-from-drive-window.vue:
-  ok: "OK"
-desktop/views/components/crop-window.vue:
-  ok: "OK"
-desktop/views/input-dialog.vue:
-  ok: "OK"
-common/views/components/api-settings.vue:
-  console:
-    parameter: "Parâmetros"
-desktop/views/components/sub-note-content.vue:
-  poll: "Enquetes"
-desktop/views/components/user-preview.vue:
-  following: "Seguindo"
-  followers: "Seguidores"
-desktop/views/components/users-list-item.vue:
-  followed: "Te segue"
-admin/views/abuse.vue:
-  remove-report: "Apagar"
-admin/views/instance.vue:
-  invite: "Convidar"
-admin/views/drive.vue:
-  delete: "Apagar"
-admin/views/emoji.vue:
-  emojis:
-    remove: "Apagar"
-admin/views/announcements.vue:
-  remove: "Apagar"
-admin/views/federation.vue:
-  followers: "Seguidores"
-desktop/views/pages/welcome.vue:
-  timeline: "Timeline"
-  powered-by-misskey: "Desenvolvido por <b>Misskey</b>."
-desktop/views/pages/drive.vue:
-  title: "Drive Misskey"
-desktop/views/pages/note.vue:
-  prev: "Nota anterior"
-  next: "Próxima nota"
-desktop/views/pages/selectdrive.vue:
-  title: "Selecione um arquivo"
-  ok: "OK"
-  cancel: "Cancelar"
-  upload: "Envie arquivos do seu dispositivo"
-desktop/views/pages/search.vue:
-  not-available: "A pesquisa está desligada nas configurações desta instância."
-desktop/views/pages/user/user.followers-you-know.vue:
-  loading: "Carregando"
-desktop/views/pages/user/user.friends.vue:
-  loading: "Carregando"
-desktop/views/pages/user/user.photos.vue:
-  loading: "Carregando"
-desktop/views/pages/user/user.header.vue:
-  following: "Seguindo"
-  followers: "Seguidores"
-  month: "Seg"
-  day: "Dom"
-  follows-you: "Te segue"
-desktop/views/pages/user/user.timeline.vue:
-  with-media: "Mídia"
-desktop/views/widgets/polls.vue:
-  title: "Enquetes"
-common/views/components/follow-button.vue:
-  follow: "Seguindo"
-mobile/views/components/sub-note-content.vue:
-  poll: "Enquetes"
-mobile/views/components/ui.nav.vue:
-  timeline: "Linha do tempo"
-mobile/views/pages/widgets.vue:
-  customization-tips: "Dicas de personalização"
-mobile/views/pages/note.vue:
-  prev: "Nota anterior"
-  next: "Próxima nota"
-mobile/views/pages/search.vue:
-  search: "Pesquisar"
-mobile/views/pages/user.vue:
-  follows-you: "Te segue"
-  following: "Seguindo"
-  followers: "Seguidores"
-  notes: "Posts"
-  timeline: "Linha do tempo"
-  media: "Mídia"
-mobile/views/pages/user/home.vue:
-  recent-notes: "Notas recentes"
-  images: "Imagens"
-  activity: "Atividade"
-  keywords: "Palavras chave"
-  domains: "Domínios"
-  followers-you-know: "Seguidores que você conhece"
-  last-used-at: "Ativo pela última vez"
-mobile/views/pages/user/home.photos.vue:
-  no-photos: "Sem fotos"
-deck/deck.user-column.vue:
-  follows-you: "Te segue"
-  following: "Seguindo"
-  followers: "Seguidores"
-  images: "Imagens"
-  timeline: "Linha do tempo"
-docs:
-  edit-this-page-on-github-link: "Edite esta página no GitHub!"
-dev/views/index.vue:
-  manage-apps: "Gerenciar aplicativos"
-pages:
-  like: "Curtir"
-  blocks:
-    image: "Imagens"
-    post: "Formulário de publicação"
-room:
-  furnitures:
-    moon: "Lua"
-    bin: "Lixo"
diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
deleted file mode 100644
index 3b665766a2412fc1668309c18e9f62918f2403ab..0000000000000000000000000000000000000000
--- a/locales/ru-RU.yml
+++ /dev/null
@@ -1,171 +0,0 @@
----
-meta:
-  lang: "Русский язык"
-common:
-  misskey: "Мы — ⭐ fediverse"
-  about-title: "Мы — ⭐ fediverse"
-  about: "Спасибо, что нашли Misskey. Misskey — это <b>децентрализованная платформа для микроблоггинга</b> родом с планеты Земля. Поскольку она существует внутри Fediverse (вселенной различных социальных платформ), она связана с другими платформами. Отдохните от шума большого города — и познакомьтесь с новым интернетом."
-  intro:
-    title: "Что такое Misskey?"
-    about: "Misskey - это <b>децентрализованный сервис микроблогинга</b> с открытым исходным кодом. Он имеет такие функции, как: навороченный, полностью настраиваемый пользовательский интерфейс, множество реакций на посты, бесплатное хранилище файлов с интегрированной системой управления и ещё куча передовых фишек. А ещё сетевая система под названием “Fediverse” позволяет нам общаться с пользователями других социальных сетей. Например, если ты что-нибудь запостишь, то твой пост будет отослан не только в Misskey, но ещё и mastodon. Просто представь, что планета посылает микроволны на другую планету для коммуникации."
-    features: "Особенности"
-    rich-contents: "Посты"
-    rich-contents-desc: "Просто выложи свою идею, актуальные темы и всё, что тебе хочется показать миру. Ты можешь декорировать свои слова, прикреплять свои любимые картинки, отправлять файлы с фильмами и создать голосование - это те вещи, которые ты можешь сделать с помощью Misskey!"
-    reaction: "Реакции"
-    reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。"
-    ui: "Интерфейс"
-    ui-desc: "Нет такого интерфейса, понравившегося всем. Поэтому у Misskey имеется пользовательский интерфейс, широко настраиваемый под ваши вкусы. Создай себе уникальную домашнюю страницу редактируя, подстраивая оформление ленты и размещая виджеты, которые тоже можно кастомизировать."
-    drive: "Хранилище файлов"
-    drive-desc: "Хотите запостить картинку, которую уже отправляли ранее? Хочется сортировать, переименовать и создать папку для ваших выложенных файлов? Тогда Misskey Drive - это лучшее решение для вас. Очень лёгкий способ делиться своими файлами онлайн."
-    outro: "Попробуйте будущие, уникальные для Misskey функции своими глазами! Если чувствуете, что это не в вашем вкусе, то попробуйте другие инстанции, ведь Misskey - это децентрализованная социальная сеть, так что ты можешь с лёгкостью найти себе товарищей. И наконец, GLHF!"
-  application-authorization: "Авторизация приложений"
-  close: "Закрыть"
-  do-not-copy-paste: "Пожалуйста, не вводите и не вставляйте сюда код. Аккаунту может угрожать опасность."
-  load-more: "Загрузить больше"
-  enter-password: "Пожалуйста, введите ваш пароль"
-  2fa: "Двухфакторная аутентификация"
-  customize-home: "Настройка домашней страницы"
-  featured-notes: "Рекомендуемые"
-  dark-mode: "Тёмная тема"
-  signin: "Войти"
-  signup: "Регистрация"
-  signout: "Выйти"
-  reload-to-apply-the-setting: "Вам необходимо перезагрузить страницу, чтобы применить настройки. Вы хотите перезагрузить сейчас?"
-  customization-tips:
-    title: "Советы по настройке"
-    gotit: "Понятно!"
-  notification:
-    file-uploaded: "Файл отправлен!"
-    message-from: "Сообщение от {}:"
-    reversi-invited: "Приглашён в игру"
-    reversi-invited-by: "Был приглашён {}:"
-    notified-by: "Был приглашён {}:"
-    reply-from: "Ответ от {}:"
-    quoted-by: "Цитировано {}:"
-  time:
-    unknown: "неизвестно"
-    future: "сейчас"
-    just_now: "сейчас"
-    seconds_ago: "{} секунд назад"
-    minutes_ago: "{} минут назад"
-    hours_ago: "{} часов назад"
-    days_ago: "{} дней назад"
-    weeks_ago: "{} недель назад"
-    months_ago: "{} месяцев назад"
-    years_ago: "{} лет назад"
-  month-and-day: "{day}.{month}"
-  trash: "Мусорное ведро"
-  drive: "Drive"
-  pages: "Страницы"
-  messaging: "Чат"
-  timeline: "Лента"
-  followers: "Подписчики"
-  favorites: "Избранное"
-  post-form:
-    reply: "Ответить"
-    create-poll: "Создать опрос"
-  weekday-short:
-    sunday: "Вс"
-    monday: "Пн"
-    tuesday: "Ð’Ñ‚"
-    wednesday: "Ср"
-    thursday: "Чт"
-    friday: "Пт"
-    saturday: "Сб"
-  weekday:
-    sunday: "Воскресенье"
-    monday: "Понедельник"
-    tuesday: "Вторник"
-    wednesday: "Среда"
-    thursday: "Четверг"
-    friday: "Пятница"
-    saturday: "Суббота"
-  reactions:
-    like: "Нравится"
-    laugh: "Ха-Ха"
-    rip: "RIP"
-  do-not-use-in-production: "Эта сборка для разработчиков. Не используйте в продакшне."
-  error:
-    title: "Что-то пошло не так :("
-    retry: "Повторить"
-  reversi:
-    drawn: "Ничья"
-    my-turn: "Ваш ход"
-    opponent-turn: "Ход оппонента"
-    turn-of: "Ход {name}"
-    past-turn-of: "Ход {name}"
-    won: "{name} победил"
-    black: "Чёрный"
-    white: "Белый"
-    total: "Всего"
-    this-turn: "Ход {count}"
-  widgets:
-    analog-clock: "Аналоговые часы"
-    profile: "Профиль"
-    calendar: "Календарь"
-    timemachine: "Календарь (машина времени)"
-    activity: "Активность"
-    rss: "Ридер RSS"
-    memo: "Заметка"
-    trends: "Популярное"
-    photo-stream: "Фотопоток"
-    slideshow: "Слайдшоу"
-    version: "Версия"
-    notifications: "Уведомления"
-    users: "Рекомендованные пользователи"
-    polls: "Голосования"
-    server: "Информация о сервере"
-    hashtags: "Хэштеги"
-  dev: "Не удалось создать приложение. Пожалуйста, попробуйте ещё раз."
-  ai-chan-kawaii: "Ai-chan kawaii!"
-auth/views/form.vue:
-  share-access: "Вы разрешаете <i>{name}</i> получить доступ к вашему аккаунту?"
-common/views/components/games/reversi/reversi.index.vue:
-  game-state:
-    ended: "Завершено"
-    playing: "В процессе"
-common/views/components/games/reversi/reversi.room.vue:
-  settings-of-the-game: "Настройки игры"
-  random: "Случайно"
-  black-or-white: "Чёрные/Белые"
-  black-is: "{} ходит чёрными"
-  rules: "Правила"
-  settings-of-the-bot: "Настройки бота"
-  this-game-is-started-soon: "Игра вот-вот начнётся"
-  waiting-for-other: "Ожидание оппонента"
-  cancel: "Отмена"
-  ready: "Готов"
-common/views/components/connect-failed.vue:
-  title: "Невозможно подключиться к серверу"
-common/views/components/cw-button.vue:
-  poll: "Голосования"
-common/views/components/poll-editor.vue:
-  day: "Вс"
-common/views/widgets/memo.vue:
-  title: "Заметка"
-desktop/views/components/sub-note-content.vue:
-  poll: "Голосования"
-admin/views/dashboard.vue:
-  drive: "Хранилище файлов"
-admin/views/charts.vue:
-  drive: "Хранилище файлов"
-desktop/views/pages/user/user.header.vue:
-  month: "Пн"
-  day: "Вс"
-desktop/views/widgets/polls.vue:
-  title: "Голосования"
-mobile/views/components/sub-note-content.vue:
-  poll: "Голосования"
-mobile/views/pages/widgets.vue:
-  customization-tips: "Советы по настройке"
-pages:
-  like: "Нравится"
-  script:
-    categories:
-      random: "Случайно"
-    blocks:
-      random: "Случайно"
-room:
-  furnitures:
-    moon: "Луна"
-    bin: "Мусорное ведро"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
deleted file mode 100644
index 9e379c5c29f828495deafd877e46418d319244ca..0000000000000000000000000000000000000000
--- a/locales/zh-CN.yml
+++ /dev/null
@@ -1,2173 +0,0 @@
----
-meta:
-  lang: "中文(简体)"
-common:
-  misskey: "联邦宇宙中的一颗⭐"
-  about-title: "联邦宇宙中的一颗⭐"
-  about: "非常感谢您找到了Misskey。 Misskey是诞生于地球的<b>分布式微博SNS</b>。因为她处于联邦宇宙(由各种SNS组成的宇宙)中,所以她与其他SNS相互连接。为什么不试试远离喧嚣的城市,潜入这片新的网络海洋之中呢?"
-  intro:
-    title: "什么是 Misskey 呢?"
-    about: "Misskey是开源的<b>分散式微博SNS</b>。丰富且可以高度定制的Ui,对别人的帖子进行各种回应,集成管理系统的网盘等先进功能。此外,称为“联邦世界”的网络系统使我们能够与其他SNS的用户进行通信。比如,如果你发布一个帖子,那么你的帖子不仅会发送给Misskey,还会发送到其他SNS平台。想象一下,正如一颗行星和另一颗行星通过电磁波来进行通信一样。"
-    features: "功能"
-    rich-contents: "发布"
-    rich-contents-desc: "请分享您的想法,热门话题,以及任何您想与大家分享的内容。如果有需要的话,您可以使用各种语法来修饰文章,发布问卷调查,或者添加各种您喜欢的图像和视频等文件。"
-    reaction: "回应"
-    reaction-desc: "这是表达情绪的最简单方法。 Misskey允许您向其他帖子添加各种类型的回应。 一旦体验过Misskey的回应功能,就再也不会想回到那些只有点赞功能的其他SNS上了。"
-    ui: "交互界面"
-    ui-desc: "世界上没有一个UI可以适合每一个人. 所以, Misskey 提供一个可以高度定制的UI交互界面. 您可以通过编辑, 调整布局, 放置可选择的小部件来轻松定制您的专属UI界面。"
-    drive: "网盘"
-    drive-desc: "想要发布一张您已经上传过的照片吗?想要管理文件或为上传的文件创建文件夹吗?Misskey的内置网盘将为您完美解决这些问题。简单地分享您的文件。"
-    outro: "Misskey还有其他更多功能,请亲身体验一下吧。因为 Misskey 是一个分布式的 SNS,如果您感觉某个功能不适合自己,试试其他的吧。祝您玩得开心!"
-  application-authorization: "应用程序授权"
-  close: "关闭"
-  do-not-copy-paste: "请不要在这里输入或粘贴代码。您帐户可能会受到损害。"
-  load-more: "加载更多"
-  enter-password: "请输入您的密码"
-  2fa: "双重身份验证"
-  customize-home: "自定义主页"
-  featured-notes: "高亮"
-  dark-mode: "黑暗模式"
-  signin: "登录"
-  signup: "注册"
-  signout: "退出"
-  reload-to-apply-the-setting: "必须重新加载页面以应用此设置。 确实要立即重新加载吗?"
-  fetching-as-ap-object: "联合查询"
-  unfollow-confirm: "取消对{name}的关注?"
-  delete-confirm: "确定删除这个投稿吗?"
-  signin-required: "请先登录"
-  notification-type: "通知类型"
-  notification-types:
-    all: "所有"
-    pollVote: "投票"
-    follow: "关注中"
-    receiveFollowRequest: "关注请求"
-    reply: "回复"
-    quote: "引用"
-    renote: "转推"
-    mention: "提及"
-    reaction: "回应"
-  got-it: "知道了"
-  customization-tips:
-    title: "自定义提示"
-    paragraph: "<p>主页定制允许您添加或删除, 拖放和重新排列小组件.</p></p>您可以通过<strong><strong>右键</strong>点击</strong>某些小部件来更改显示</p><p>若要删除小部件, 请将其拖到标头为<strong>「垃圾箱」</strong>的区域</p><p>如果您完成了定制过程,单击右上角的「完成」</p>"
-    gotit: "明白了!"
-  notification:
-    file-uploaded: "文件已上传"
-    message-from: "来自{}的消息:"
-    reversi-invited: "您已被邀请加入一场游戏"
-    reversi-invited-by: "来自{}的邀请"
-    notified-by: "来自{}的通知"
-    reply-from: "来自{}的回复:"
-    quoted-by: "来自{}的引用:"
-  time:
-    unknown: "未知"
-    future: "未来"
-    just_now: "刚刚"
-    seconds_ago: "{}秒前"
-    minutes_ago: "{}分前"
-    hours_ago: "{}小时前"
-    days_ago: "{}天前"
-    weeks_ago: "{}周前"
-    months_ago: "{}月前"
-    years_ago: "{}年前"
-  month-and-day: "{month}月 {day}日"
-  trash: "垃圾箱"
-  drive: "网盘"
-  pages: "页面"
-  messaging: "聊天"
-  home: "首页"
-  deck: "Deck"
-  timeline: "时间线"
-  explore: "发现"
-  following: "正在关注"
-  followers: "关注者"
-  favorites: "最爱"
-  permissions:
-    "read:account": "查看账户信息"
-    "write:account": "更改我的帐户信息"
-    "read:blocks": "查看黑名单"
-    "write:blocks": "编辑黑名单"
-    "read:drive": "查看网盘"
-    "write:drive": "管理网盘文件"
-    "read:favorites": "查看收藏夹"
-    "write:favorites": "编辑收藏夹"
-    "read:following": "查看关注信息"
-    "write:following": "关注/取消关注"
-    "read:messaging": "查看对话"
-    "write:messaging": "对话操作"
-    "read:mutes": "查看屏蔽列表"
-    "write:mutes": "编辑屏蔽列表"
-    "write:notes": "创建或删除帖子"
-    "read:notifications": "查看通知"
-    "write:notifications": "管理通知"
-    "read:reactions": "查看回应"
-    "write:reactions": "回应操作"
-    "write:votes": "投票"
-    "read:pages": "查看页面"
-    "write:pages": "操作页面"
-    "read:page-likes": "查看喜欢的页面"
-    "write:page-likes": "操作喜欢的页面"
-    "read:user-groups": "查看用户组"
-    "write:user-groups": "操作用户组"
-  empty-timeline-info:
-    follow-users-to-make-your-timeline: "关注其他用户时,帖子将显示在时间线中。"
-    explore: "查找用户"
-  post-form:
-    attach-location-information: "添加位置信息"
-    hide-contents: "隐藏内容"
-    reply-placeholder: "回复此贴..."
-    quote-placeholder: "引用此帖…"
-    option-quote-placeholder: "引用此帖…(可选)"
-    quote-attached: "已引用"
-    quote-question: "是否将其作为引用附上?"
-    submit: "帖子"
-    reply: "回复"
-    renote: "转推"
-    posting: "发送中"
-    attach-media-from-local: "从PC中添加媒体文件"
-    attach-media-from-drive: "从网盘中添加媒体文件"
-    insert-a-kao: "v('ω')v"
-    create-poll: "创建一个投票"
-    text-remain: "还剩{}个字符"
-    recent-tags: "最近"
-    local-only-message: "这篇文章只会在本地发布"
-    click-to-tagging: "点击添加标签"
-    visibility: "可见性"
-    geolocation-alert: "您的设备不支持定位服务"
-    error: "错误"
-    enter-username: "输入用户名"
-    specified-recipient: "收件人"
-    add-visible-user: "添加用户"
-    cw-placeholder: "评论帖子(可选)"
-    username-prompt: "输入用户名"
-    enter-file-name: "编辑文件名"
-  weekday-short:
-    sunday: "æ—¥"
-    monday: "一"
-    tuesday: "二"
-    wednesday: "三"
-    thursday: "å››"
-    friday: "五"
-    saturday: "å…­"
-  weekday:
-    sunday: "星期日"
-    monday: "星期一"
-    tuesday: "星期二 "
-    wednesday: "星期三"
-    thursday: "星期四"
-    friday: "星期五"
-    saturday: "星期六"
-  reactions:
-    like: "赞"
-    love: "喜爱"
-    laugh: "笑"
-    hmm: "emmm...?"
-    surprise: "哇! "
-    congrats: "恭喜"
-    angry: "生气"
-    confused: "困惑"
-    rip: "RIP"
-    pudding: "布丁"
-  note-visibility:
-    public: "公开"
-    home: "首页"
-    home-desc: "仅发送至首页的时间线"
-    followers: "关注者"
-    followers-desc: "仅发送至粉丝"
-    specified: "指定用户"
-    specified-desc: "仅发送至指定用户"
-    local-public: "公开(仅限本地)"
-    local-home: "首页(仅限本地)"
-    local-followers: "关注者(仅限本地)"
-  note-placeholders:
-    a: "现在在做什么?"
-    b: "发生了什么?"
-    c: "你有什么想法?"
-    d: "你想要发布些什么吗?"
-    e: "请写下来吧"
-    f: "等待您的发布..."
-  settings: "设置"
-  _settings:
-    profile: "个人资料"
-    notification: "通知"
-    apps: "应用程序"
-    tags: "话题标签"
-    mute-and-block: "屏蔽/拉黑"
-    blocking: "拉黑"
-    security: "安全性"
-    signin: "登录历史"
-    password: "密码"
-    other: "其他"
-    appearance: "设计"
-    behavior: "行为"
-    reactions: "回应"
-    reactions-description: "快速选择回应中的自定义表情符号,以换行符分隔。"
-    fetch-on-scroll: "向下滚动时自动加载"
-    fetch-on-scroll-desc: "向下滚动页面时,它会自动提取其他内容。"
-    note-visibility: "帖子可见性"
-    default-note-visibility: "默认可见性"
-    remember-note-visibility: "记住帖子可见性"
-    web-search-engine: "搜索引擎"
-    web-search-engine-desc: "例如: https://www.google.com/?#q={{query}}"
-    paste: "粘贴"
-    pasted-file-name: "粘贴的文件名模板"
-    pasted-file-name-desc: "例: \"yyyy-MM-dd HH-mm-ss [{{number}}]\" → \"2018-03-20 21-30-24 1\""
-    paste-dialog: "粘贴时编辑文件名"
-    paste-dialog-desc: "粘贴时显示编辑文件名的对话框"
-    keep-cw: "保留内容警告"
-    keep-cw-desc: "在回复帖子时,如果原帖设置了内容警告,默认情况下回帖也会设置相同的内容警告。"
-    i-like-sushi: "相比于布丁来说, 我更喜欢寿司。"
-    show-reversi-board-labels: "在黑白棋中显示行和列表签"
-    use-avatar-reversi-stones: "用头像作为黑白棋的棋子"
-    disable-animated-mfm: "在帖子中禁用动画文本"
-    disable-showing-animated-images: "不播放动画"
-    enable-quick-notification-view: "启用通知快速查看"
-    suggest-recent-hashtags: "在帖子表单上显示最近流行的哈希标签"
-    always-show-nsfw: "总是显示 NSFW 的内容"
-    always-mark-nsfw: "总是用 NSFW 来标记附件"
-    show-full-acct: "不要从用户名中忽略主机名"
-    show-via: "显示 via"
-    reduce-motion: "减弱UI中的动画效果"
-    this-setting-is-this-device-only: "设置仅在本设备中生效"
-    use-os-default-emojis: "使用设备系统默认的表情符号"
-    line-width: "线条宽度"
-    line-width-thin: "细"
-    line-width-normal: "正常"
-    line-width-thick: "ç²—"
-    font-size: "文字大小"
-    font-size-x-small: "小"
-    font-size-small: "较小"
-    font-size-medium: "普通"
-    font-size-large: "较大"
-    font-size-x-large: "大"
-    deck-column-align: "列对齐设置"
-    deck-column-align-center: "中央"
-    deck-column-align-left: "å·¦"
-    deck-column-align-flexible: "可变"
-    deck-column-width: "Deck列宽"
-    deck-column-width-narrow: "窄"
-    deck-column-width-narrower: "很窄"
-    deck-column-width-normal: "普通"
-    deck-column-width-wider: "很宽"
-    deck-column-width-wide: "宽"
-    use-shadow: "在UI中使用阴影效果"
-    rounded-corners: "UI界面圆角效果"
-    circle-icons: "使用圆形头像"
-    contrasted-acct: "增加用户名的对比度"
-    wallpaper: "壁纸"
-    choose-wallpaper: "选择壁纸"
-    delete-wallpaper: "移除壁纸"
-    post-form-on-timeline: "在时间线顶部显示帖子表单"
-    show-clock-on-header: "在右上角显示时钟"
-    show-reply-target: "显示回复目标"
-    timeline: "时间线"
-    show-my-renotes: "在时间线上显示我的转推"
-    show-renoted-my-notes: "在时间线上显示我的帖子的转推"
-    show-local-renotes: "在时间线上显示本地帖子的转推"
-    remain-deleted-note: "继续显示已删除的帖子"
-    sound: "声音"
-    enable-sounds: "开启声音"
-    enable-sounds-desc: "收到帖子/留言时播放声音。 此设置将被存储在浏览器中。"
-    volume: "音量"
-    test: "测试"
-    update: "Misskeyæ›´æ–°"
-    version: "版本:"
-    latest-version: "最新版本:"
-    update-checking: "正在检查更新"
-    do-update: "检查更新"
-    update-settings: "详细设置"
-    no-updates: "无可用更新"
-    no-updates-desc: "您所使用的 Misskey 已经是最新版本。"
-    update-available: "有可用的新版本"
-    update-available-desc: "重新加载页面以应用更新。"
-    advanced-settings: "高级设置"
-    debug-mode: "启用调试模式"
-    debug-mode-desc: "此设置存储在浏览器中。"
-    navbar-position: "导航栏位置"
-    navbar-position-top: "顶部"
-    navbar-position-left: "左边"
-    navbar-position-right: "右边"
-    i-am-under-limited-internet: "我的带宽有限"
-    post-style: "发帖的展示风格"
-    post-style-standard: "标准"
-    post-style-smart: "Smart"
-    notification-position: "通知形式"
-    notification-position-bottom: "底部"
-    notification-position-top: "顶部"
-    disable-via-mobile: "不要将帖子标记为“来自手机”"
-    load-raw-images: "以原始质量显示附加图像"
-    load-remote-media: "显示来自远程服务器的媒体"
-    sync: "同步"
-    save: "保存"
-    saved: "已保存"
-    preview: "预览"
-    home-profile: "定制首页数据"
-    deck-profile: "定制Deck数据"
-    room: "房间"
-    _room:
-      graphicsQuality: "图形质量"
-      _graphicsQuality:
-        ultra: "最高"
-        high: "高"
-        medium: "中"
-        low: "低"
-        cheep: "最低"
-      useOrthographicCamera: "使用正交相机"
-  search: "搜索"
-  delete: "删除"
-  loading: "正在加载中"
-  ok: "确定"
-  cancel: "取消"
-  update-available-title: "有可用更新"
-  update-available: "新的 Misskey 版本现已发布({newer}。目前版本{current}). 刷新页面以应用更新。"
-  my-token-regenerated: "您的 Token 已被重置, 您将自动登出。"
-  hide-password: "隐藏密码"
-  show-password: "显示密码"
-  enter-username: "输入用户名"
-  do-not-use-in-production: "这是一个开发者测试版. 请勿在生产环境中使用."
-  user-suspended: "该用户已被冻结。"
-  is-remote-user: "此用户信息可能不准确。"
-  is-remote-post: "该投稿已被复制."
-  view-on-remote: "查看准确的信息"
-  renoted-by: "由 {user} 转推"
-  no-notes: "没有帖子"
-  turn-on-darkmode: "切换暗色主题"
-  turn-off-darkmode: "切换亮色主题"
-  error:
-    title: "出现问题"
-    retry: "重试"
-  reversi:
-    drawn: "平局"
-    my-turn: "轮到你了"
-    opponent-turn: "轮到对手了"
-    turn-of: "{name}的回合"
-    past-turn-of: "轮到{name}的回合了"
-    won: "{name}获胜"
-    black: "黑"
-    white: "白"
-    total: "总计"
-    this-turn: "{count}回合"
-  widgets:
-    analog-clock: "模拟时钟"
-    profile: "个人资料"
-    calendar: "日历"
-    timemachine: "日历 (时间机器)"
-    activity: "动态"
-    rss: "RSS阅读器"
-    memo: "便签"
-    trends: "趋势"
-    photo-stream: "照片流"
-    posts-monitor: "投稿图表"
-    slideshow: "幻灯片"
-    version: "版本"
-    broadcast: "广播"
-    notifications: "通知"
-    users: "推荐用户"
-    polls: "调查问卷"
-    post-form: "投稿窗口"
-    server: "服务器信息"
-    nav: "导航"
-    tips: "提示"
-    hashtags: "哈希标签"
-    queue: "队列"
-  dev: "构建应用程序失败,请再试一次。"
-  ai-chan-kawaii: "小蓝真可爱"
-  you: "您"
-auth/views/form.vue:
-  share-access: "您要允许<i>{name}</i>来访问您的账户吗?"
-  permission-ask: "这个应用程序需要以下权限:"
-  cancel: "取消"
-  accept: "允许访问。"
-auth/views/index.vue:
-  loading: "正在加载中"
-  denied: "已拒绝应用程序授权。"
-  denied-paragraph: "这个应用程序将不会访问您的账户"
-  already-authorized: "这个应用程序已授权。"
-  allowed: "允许应用程序授权。"
-  callback-url: "回到应用程序。"
-  please-go-back: "请返回到应用程序"
-  error: "会话不存在。"
-  sign-in: "请登录。"
-common/views/pages/explore.vue:
-  pinned-users: "已置顶用户"
-  popular-users: "热门用户"
-  recently-updated-users: "活跃用户"
-  recently-registered-users: "新用户"
-  recently-discovered-users: "最近发现的用户"
-  popular-tags: "热门标签"
-  federated: "联邦"
-  explore: "查找{host}"
-  explore-fediverse: "探索Fediverse"
-  users-info: "当前有{users}个注册用户"
-common/views/components/reactions-viewer.details.vue:
-  few-users: "{users}作出了{reaction}的回应"
-  many-users: "{users}和其他{omitted}人做出了{reaction}的回应"
-common/views/components/url-preview.vue:
-  enable-player: "打开播放器"
-  disable-player: "关闭播放器"
-common/views/components/user-list.vue:
-  no-users: "无用户"
-common/views/components/games/reversi/reversi.vue:
-  matching:
-    waiting-for: "等待 {}"
-    cancel: "取消"
-common/views/components/games/reversi/reversi.game.vue:
-  surrender: "认输"
-  surrendered: "已认输"
-  is-llotheo: "棋子较少一方获胜(LLoTheO规则)"
-  looped-map: "循环棋盘"
-  can-put-everywhere: "可以下在任意位置"
-common/views/components/games/reversi/reversi.index.vue:
-  title: "Misskey 黑白棋"
-  sub-title: "和其他人一起来玩Misskey黑白棋"
-  invite: "邀请"
-  rule: "游戏说明"
-  rule-desc: "黑白棋是一种棋盘游戏。两人交替在棋盘上落子,并将该棋子和另一个己方棋子之间的对方棋子转换成自己的颜色。最终保留最多棋子的人获胜。"
-  mode-invite: "邀请"
-  mode-invite-desc: "邀请指定用户参加游戏"
-  invitations: "您收到了一则邀请!"
-  my-games: "我的游戏"
-  all-games: "所有游戏"
-  enter-username: "输入用户名"
-  game-state:
-    ended: "结束"
-    playing: "游戏进行中"
-common/views/components/games/reversi/reversi.room.vue:
-  settings-of-the-game: "游戏设置"
-  choose-map: "棋盘选择"
-  random: "随机"
-  black-or-white: "黑/白"
-  black-is: "{}是黑"
-  rules: "规则"
-  is-llotheo: "棋子较少一方获胜(LLoTheO规则)"
-  looped-map: "循环棋盘"
-  can-put-everywhere: "可以下在任意位置"
-  settings-of-the-bot: "机器人设定"
-  this-game-is-started-soon: "游戏即将在数秒后开始"
-  waiting-for-other: "等待对手准备"
-  waiting-for-me: "等待您的准备"
-  waiting-for-both: "准备中"
-  cancel: "取消"
-  ready: "准备完成"
-  cancel-ready: "取消准备"
-common/views/components/connect-failed.vue:
-  title: "无法连接到服务器"
-  description: "您的网络连接可能出现了问题, 或是远程服务器暂时不可用. 请稍后{重试}."
-  thanks: "感谢您使用 Misskey"
-  troubleshoot: "故障排除"
-common/views/components/connect-failed.troubleshooter.vue:
-  title: "正在排除故障"
-  network: "网络已连接"
-  checking-network: "正在检查网络连接"
-  internet: "网络连接"
-  checking-internet: "正在检查网络连接"
-  server: "已连接至服务器"
-  checking-server: "正在检查与服务器的连接"
-  finding: "搜索问题"
-  no-network: "无网络连接"
-  no-network-desc: "请确保您已连接至互联网"
-  no-internet: "无网络连接"
-  no-internet-desc: "网络已连接,但无法连接到Internet。 请确保您的PC的Internet连接正常。"
-  no-server: "无法连接到 Misskey 服务器"
-  no-server-desc: "您设备与互联网的网络连接正常,但是无法连接至 Misskey 服务器。这可能是服务器暂时不可用或正在维护,请稍后再试。"
-  success: "成功连接至 Misskey 服务器"
-  success-desc: "看起来我们连接正常. 请刷新网页."
-  flush: "清除缓存"
-  set-version: "指定版本"
-common/views/components/media-banner.vue:
-  sensitive: "阅读注意"
-  click-to-show: "点击以显示"
-common/views/components/theme.vue:
-  theme: "主题"
-  light-theme: "亮色模式使用的主题"
-  dark-theme: "暗色模式使用的主题"
-  light-themes: "亮色主题"
-  dark-themes: "暗色主题"
-  install-a-theme: "安装一个主题"
-  theme-code: "主题代码"
-  install: "安装"
-  installed: "\"{}\" 已安装"
-  create-a-theme: "创建一个主题"
-  save-created-theme: "保存主题"
-  primary-color: "主要颜色"
-  secondary-color: "次要颜色"
-  text-color: "文本颜色"
-  base-theme: "基础主题"
-  base-theme-light: "亮"
-  base-theme-dark: "æš—"
-  find-more-theme: "获取更多主题"
-  theme-name: "主题名称"
-  preview-created-theme: "预览"
-  invalid-theme: "无效主题"
-  already-installed: "这个主题已经被安装。"
-  saved: "已保存"
-  manage-themes: "主题管理"
-  builtin-themes: "标准主题"
-  my-themes: "我的主题"
-  installed-themes: "已安装的主题"
-  select-theme: "选择您的主题"
-  uninstall: "卸载"
-  uninstalled: "\"{}\" 已被卸载"
-  author: "作者"
-  desc: "描述"
-  export: "导出"
-  import: "导入"
-  import-by-code: "或者粘贴代码"
-  theme-name-required: "必须填写主题名称"
-common/views/components/cw-button.vue:
-  hide: "隐藏"
-  show: "查看更多"
-  chars: "{count}个字符"
-  files: "{count} 个文件"
-  poll: "调查问卷"
-common/views/components/messaging.vue:
-  search-user: "查找用户"
-  you: "您"
-  no-history: "没有历史记录"
-  user: "用户"
-  group: "群组"
-  start-with-user: "开始用户聊天"
-  start-with-group: "开始群组聊天"
-  select-group: "请选择群组"
-common/views/components/messaging-room.vue:
-  not-talked-user: "没有用户的会话记录"
-  not-talked-group: "没有群组的会话记录"
-  no-history: "没有更多的历史记录"
-  new-message: "新信息"
-  only-one-file-attached: "只能添加一个附件"
-common/views/components/messaging-room.form.vue:
-  input-message-here: "在此键入信息"
-  send: "发送"
-  attach-from-local: "从电脑中添加文件"
-  attach-from-drive: "从网盘中添加文件"
-  only-one-file-attached: "只能添加一个附件"
-common/views/components/messaging-room.message.vue:
-  is-read: "已读"
-  deleted: "此消息已被删除"
-common/views/components/nav.vue:
-  about: "关于 Misskey"
-  stats: "统计"
-  status: "状态"
-  wiki: "维基百科"
-  donors: "捐赠者"
-  repository: "源码库"
-  develop: "开发人员"
-  feedback: "反馈"
-  tos: "服务条款"
-common/views/components/note-menu.vue:
-  mention: "提到"
-  detail: "详细信息"
-  copy-content: "复制内容"
-  copy-link: "复制链接"
-  favorite: "收藏这个投稿"
-  unfavorite: "取消收藏"
-  watch: "关注"
-  unwatch: "取消关注"
-  pin: "置顶"
-  unpin: "取消置顶"
-  delete: "删除"
-  delete-confirm: "确定删除这个投稿吗?"
-  delete-and-edit: "删除和编辑"
-  delete-and-edit-confirm: "要删除此帖并再次编辑吗?对此帖的所有回应,转推和回复也将被删除。"
-  remote: "显示原始投稿"
-  pin-limit-exceeded: "无法置顶更多了。"
-common/views/components/user-menu.vue:
-  mention: "提到"
-  mute: "屏蔽"
-  unmute: "解除屏蔽"
-  mute-confirm: "屏蔽此用户?"
-  unmute-confirm: "取消屏蔽用户?"
-  block: "拉黑"
-  unblock: "取消拉黑"
-  block-confirm: "确定拉黑此用户?"
-  unblock-confirm: "取消拉黑此用户?"
-  push-to-list: "添加至列表"
-  select-list: "请选择一个列表"
-  report-abuse: "举报骚扰"
-  report-abuse-detail: "做了什么骚扰的行为?"
-  report-abuse-reported: "已报告给管理员。 非常感谢你的合作。"
-  silence: "禁言"
-  unsilence: "解除禁言"
-  silence-confirm: "确认屏蔽此用户?"
-  unsilence-confirm: "取消屏蔽此用户?"
-  suspend: "冻结"
-  unsuspend: "解除冻结"
-  suspend-confirm: "确认冻结此用户?"
-  unsuspend-confirm: "确认解冻此用户?"
-common/views/components/poll.vue:
-  vote-to: "为\"{}\"投票"
-  vote-count: "{}票"
-  total-votes: "总票数{}"
-  vote: "投票"
-  show-result: "显示结果"
-  voted: "已投票"
-  closed: "已截止"
-  remaining-days: "{d}天{h}小时后截止"
-  remaining-hours: "{h}小时{m}分后截止"
-  remaining-minutes: "{m}分{s}秒后截止"
-  remaining-seconds: "{s}秒后截止"
-common/views/components/poll-editor.vue:
-  no-only-one-choice: "至少选择两个选项"
-  choice-n: "选择{}"
-  remove: "删除选项"
-  add: "+添加一个选项"
-  destroy: "放弃投票"
-  multiple: "允许多个投票"
-  expiration: "截止时间"
-  infinite: "永久"
-  at: "指定日期"
-  after: "指定时间"
-  no-more: "最多只能添加十个回答"
-  deadline-date: "日期"
-  deadline-time: "时间"
-  interval: "时长"
-  unit: "单位"
-  second: "秒"
-  minute: "分"
-  hour: "小时"
-  day: "æ—¥"
-common/views/components/reaction-picker.vue:
-  choose-reaction: "选择回应"
-  input-reaction-placeholder: "表情符号输入"
-common/views/components/emoji-picker.vue:
-  recent-emoji: "最近使用的表情符号"
-  custom-emoji: "自定义表情符号"
-  no-category: "未分类"
-  people: "人"
-  animals-and-nature: "动物与自然"
-  food-and-drink: "食物与饮品"
-  activity: "活动"
-  travel-and-places: "位置"
-  objects: "物品"
-  symbols: "符号"
-  flags: "旗帜"
-common/views/components/settings/app-type.vue:
-  title: "模式"
-  intro: "您可以指定使用桌面版或移动版。"
-  choices:
-    auto: "自动选择"
-    desktop: "固定为桌面版"
-    mobile: "固定为移动版"
-  info: "更改将在刷新页面后生效。"
-common/views/components/signin.vue:
-  username: "用户名"
-  password: "密码"
-  token: "Token (令牌)"
-  signing-in: "在弄了在弄了..."
-  or: "或者"
-  signin-with-twitter: "用 Twitter 登录"
-  signin-with-github: "用 GitHub 登录"
-  signin-with-discord: "用 Discord 登录"
-  login-failed: "登录失败。请检查用户名和密码。"
-  tap-key: "点击安全密钥登录"
-  enter-2fa-code: "输入验证码"
-common/views/components/signup.vue:
-  invitation-code: "邀请码"
-  invitation-info: "如果您没有邀请码,请联系<a href=\"{}\">管理员</a>。"
-  username: "用户名"
-  checking: "正在确认..."
-  available: "可用"
-  unavailable: "不可用"
-  error: "网络错误"
-  invalid-format: "可使用大小写英文字母、数字和下划线。"
-  too-short: "请至少输入1个字符!"
-  too-long: "请不要超过20个字符"
-  password: "密码"
-  password-placeholder: "推荐使用8个字符以上的密码。"
-  weak-password: "密码强度:弱"
-  normal-password: "密码强度:中等"
-  strong-password: "密码强度:强"
-  retype: "重新输入"
-  retype-placeholder: "重新输入您的密码"
-  password-matched: "确认"
-  password-not-matched: "密码不一致"
-  recaptcha: "验证"
-  agree-to: "同意{0}"
-  tos: "服务条款"
-  create: "创建一个账户"
-  some-error: "由于某种原因,创建帐户失败。请再试一次。"
-common/views/components/special-message.vue:
-  new-year: "新年快乐哦~"
-  christmas: "圣诞快乐!"
-common/views/components/stream-indicator.vue:
-  connecting: "连接中"
-  reconnecting: "重新连接中"
-  connected: "已连接"
-common/views/components/notification-settings.vue:
-  title: "通知"
-  mark-as-read-all-notifications: "将所有通知标为已读"
-  mark-as-read-all-unread-notes: "将所有帖子标为已读"
-  mark-as-read-all-talk-messages: "将所有对话标为已读"
-  auto-watch: "自动查看帖子"
-  auto-watch-desc: "自动接收有关您做出回应或回复的帖子的通知。"
-common/views/components/integration-settings.vue:
-  title: "服务合作"
-  connect: "连接"
-  disconnect: "断开连接"
-  connected-to: "您的账号已连接以下社交账号"
-common/views/components/github-setting.vue:
-  description: "当您用GitHub连接Misskey账户后,您将能够看到有关您自己的信息,并且您将能够使用GitHub登录。"
-  connected-to: "此账户已连接GitHub"
-  detail: "详细信息..."
-  reconnect: "重新连接"
-  connect: "连接您的GitHub账户"
-  disconnect: "未连接"
-common/views/components/discord-setting.vue:
-  description: "当您用Discord连接Misskey账户后,您将能够看到有关您自己的信息,并且您将能够使用Discord登录。"
-  connected-to: "此账户已连接Discord"
-  detail: "详细信息..."
-  reconnect: "重新连接"
-  connect: "连接您的Discord账户"
-  disconnect: "断开连接"
-common/views/components/uploader.vue:
-  waiting: "等待中"
-common/views/components/visibility-chooser.vue:
-  public: "公开"
-  home: "首页"
-  home-desc: "仅发送至首页"
-  followers: "关注者"
-  followers-desc: "仅发送至关注者"
-  specified: "直接"
-  specified-desc: "仅发送至指定用户"
-  local-public: "公开(仅限本地)"
-  local-public-desc: "不要公开发布"
-  local-home: "首页(仅限本地)"
-  local-followers: "关注者(仅限本地)"
-common/views/components/trends.vue:
-  count: "{} 被提到"
-  empty: "没有趋势"
-common/views/components/language-settings.vue:
-  title: "显示语言"
-  pick-language: "选择语言"
-  recommended: "推荐"
-  auto: "自动"
-  specify-language: "指定语言"
-  info: "更改将在刷新页面后生效。"
-common/views/components/profile-editor.vue:
-  title: "个人资料"
-  name: "名称"
-  account: "账户"
-  location: "位置"
-  description: "个人简介"
-  you-can-include-hashtags: "您可以包含一个哈希标签。"
-  language: "语言"
-  birthday: "生日"
-  avatar: "头像"
-  banner: "横幅背景"
-  is-cat: "这个账户是CAT"
-  is-bot: "这个账户是BOT"
-  is-locked: "关注者请求需要批准"
-  careful-bot: "BOT的关注者请求需要批准"
-  auto-accept-followed: "自动同意来自您关注的人的关注申请"
-  advanced: "其他"
-  privacy: "隐私"
-  save: "保存"
-  saved: "您的个人资料已保存"
-  uploading: "正在上传"
-  upload-failed: "上传失败"
-  unable-to-process: "无法完成操作"
-  avatar-not-an-image: "选择的头像文件不是图片格式"
-  banner-not-an-image: "选择的横幅背景不是图片格式"
-  email: "邮件设置"
-  email-address: "电子邮件地址"
-  email-verified: "电子邮件地址已验证"
-  email-not-verified: "邮件地址尚未验证。 请检查您的邮箱。"
-  export: "导出"
-  import: "导入"
-  export-and-import: "导出/导入"
-  export-targets:
-    all-notes: "所有发帖"
-    following-list: "关注列表"
-    mute-list: "屏蔽列表"
-    blocking-list: "黑名单"
-    user-lists: "列表"
-  export-requested: "导出请求已提交。可能需要花一些时间。导出的文件将保存到网盘中。"
-  import-requested: "导入请求已提交。这可能需要花一点时间。"
-  enter-password: "请输入您的密码"
-  danger-zone: "危险选项"
-  delete-account: "删除帐户"
-  account-deleted: "帐户已被删除。 数据会在一段时间之后清除。"
-  profile-metadata: "个人资料补充信息"
-  metadata-label: "标签"
-  metadata-content: "内容"
-common/views/components/user-list-editor.vue:
-  users: "用户"
-  rename: "重命名列表"
-  delete: "删除列表"
-  remove-user: "从此列表中删除"
-  delete-are-you-sure: "删除列表“$1”?"
-  deleted: "已删除"
-  add-user: "添加用户"
-common/views/components/user-group-editor.vue:
-  users: "成员"
-  rename: "更改群组名"
-  delete: "删除群组"
-  transfer: "群组转让"
-  transfer-are-you-sure: "将群组「$1」转让给「@$2」吗?"
-  transferred: "群组已转让"
-  remove-user: "从本群组中删除"
-  delete-are-you-sure: "确定要删除「$1」组?"
-  deleted: "已删除"
-  invite: "邀请"
-  invited: "邀请已发送"
-common/views/components/user-lists.vue:
-  user-lists: "列表"
-  create-list: "创建列表"
-  list-name: "列表名称"
-common/views/components/user-groups.vue:
-  user-groups: "群组"
-  create-group: "创建群组"
-  group-name: "群组名"
-  owned-groups: "我的群组"
-  joined-groups: "加入群组"
-  invites: "邀请"
-  accept-invite: "加入"
-  reject-invite: "拒绝"
-common/views/widgets/broadcast.vue:
-  fetching: "确认中"
-  no-broadcasts: "没有公告"
-  have-a-nice-day: "祝你有愉快的一天!"
-  next: "下一个"
-  prev: "上一首"
-common/views/widgets/calendar.vue:
-  year: "{}å¹´"
-  month: "{}月"
-  day: "{}æ—¥"
-  today: "今天:"
-  this-month: "本月:"
-  this-year: "今年:"
-common/views/widgets/photo-stream.vue:
-  title: "图片轮播"
-  no-photos: "没有图片"
-common/views/widgets/posts-monitor.vue:
-  title: "投稿表格"
-  toggle: "切换视图"
-common/views/widgets/hashtags.vue:
-  title: "哈希标签"
-common/views/widgets/server.vue:
-  title: "服务器信息"
-  toggle: "切换显示"
-common/views/widgets/memo.vue:
-  title: "便签"
-  memo: "在这儿输入!"
-  save: "保存"
-common/views/widgets/slideshow.vue:
-  folder-customize-mode: "要指定文件夹,请退出自定义模式"
-  folder: "请单击并指定文件夹"
-  no-image: "这个文件夹里没有图片"
-common/views/widgets/tips.vue:
-  tips-line1: "您可以用<kbd>t</kbd>专注于时间轴"
-  tips-line2: "从 <kbd>p</kbd> 或者 <kbd>n</kbd>打开投稿表单"
-  tips-line3: "您可以在投稿表单上拖放文件。"
-  tips-line4: "您可以将剪贴板中的图像粘贴到提交表单中。"
-  tips-line5: "您可以通过将文件拖放到网盘来上传文件。"
-  tips-line6: "您可以通过在网盘中通过拖动操作来移动文件夹"
-  tips-line7: "您可以通过在网盘中通过拖动操作来移动文件夹。"
-  tips-line8: "可以从设置中定制主页。"
-  tips-line9: "Misskey 根据 AGPLv3 获得许可。"
-  tips-line10: "使用Time Machine(时光机)小部件可以轻松追溯到过去的时间轴。"
-  tips-line11: "您可以点击“...”将帖子置顶到用户页面"
-  tips-line13: "附在帖子上的所有文件都会保存到网盘中。"
-  tips-line14: "在自定义首页布局时,您可以右键单击窗口小部件以更改其设计。"
-  tips-line17: "用“**”围绕文本将突出显示它。"
-  tips-line19: "可以在浏览器外部分离多个窗口。"
-  tips-line20: "日历小部件的百分比显示经过的时间百分比。"
-  tips-line21: "您也可以使用API开发机器人。"
-  tips-line23: "小蓝很可爱"
-  tips-line24: "Misskey自2014年开始运营。"
-  tips-line25: "在与通知功能兼容的浏览器中,您可以在Misskey未打开的情况下接收通知"
-common/views/pages/not-found.vue:
-  page-not-found: "您要找的网页不存在。"
-common/views/pages/follow.vue:
-  signed-in-as: "用 {}登录"
-  following: "正在关注"
-  follow: "关注"
-  request-pending: "发送关注申请"
-  follow-processing: "申请处理中"
-  follow-request: "关注请求"
-common/views/pages/follow-requests.vue:
-  received-follow-requests: "关注申请"
-  accept: "接受"
-  reject: "拒绝"
-desktop:
-  banner-crop-title: "裁剪显示为背景的部分"
-  banner: "背景"
-  uploading-banner: "上传一个新的背景"
-  banner-updated: "成功上传背景"
-  choose-banner: "选择一个背景"
-  avatar-crop-title: "裁剪显示为头像的部分"
-  avatar: "头像"
-  uploading-avatar: "上传一个新的头像"
-  avatar-updated: "成功上传头像"
-  choose-avatar: "选择作为头像的图片"
-  unable-to-process: "无法完成操作"
-  invalid-filetype: "不接受此文件类型"
-desktop/views/components/activity.chart.vue:
-  total: "黑 ... 总计"
-  notes: "蓝 ... 投稿"
-  replies: "红 ... 回复"
-  renotes: "绿 ... 转推"
-desktop/views/components/activity.vue:
-  title: "活动"
-  toggle: "切换显示"
-desktop/views/components/calendar.vue:
-  title: "{year}年{month}月"
-  prev: "上个月"
-  next: "下个月"
-  go: "点击按时间浏览"
-desktop/views/components/choose-file-from-drive-window.vue:
-  chosen-files: "{count}文件已被选择"
-  upload: "从设备中上传文件"
-  cancel: "取消"
-  ok: "确定"
-  choose-prompt: "选择文件"
-desktop/views/components/choose-folder-from-drive-window.vue:
-  cancel: "取消"
-  ok: "确定"
-  choose-prompt: "选择一个文件夹"
-desktop/views/components/crop-window.vue:
-  skip: "跳过裁剪"
-  cancel: "取消"
-  ok: "确定"
-desktop/views/components/drive-window.vue:
-  used: "已使用"
-desktop/views/components/drive.file.vue:
-  avatar: "头像"
-  banner: "背景"
-  nsfw: "阅读注意"
-  contextmenu:
-    rename: "重命名"
-    mark-as-sensitive: "标记为“敏感”"
-    unmark-as-sensitive: "取消标记为“敏感”"
-    copy-url: "复制链接"
-    download: "下载"
-    else-files: "其他"
-    set-as-avatar: "设为头像"
-    set-as-banner: "设置为背景"
-    open-in-app: "在应用程序中打开"
-    add-app: "添加应用"
-    rename-file: "重命名文件"
-    input-new-file-name: "请输入新文件名"
-    copied: "已复制"
-    copied-url-to-clipboard: "已复制链接到剪贴板"
-desktop/views/components/drive.folder.vue:
-  upload-folder: "默认上传文件夹"
-  unable-to-process: "无法完成操作"
-  circular-reference-detected: "目标文件夹是您要移动的文件夹的子文件夹。"
-  unhandled-error: "未知错误"
-  unable-to-delete: "无法删除"
-  has-child-files-or-folders: "此文件夹不为空,无法删除。"
-  contextmenu:
-    move-to-this-folder: "移动到此文件夹"
-    show-in-new-window: "在新窗口打开"
-    rename: "重命名"
-    rename-folder: "重命名文件夹"
-    input-new-folder-name: "请输入新文件名"
-    else-folders: "其他"
-    set-as-upload-folder: "设置为默认上传文件夹"
-desktop/views/components/drive.vue:
-  search: "搜索"
-  empty-draghover: "放在这里!因为你知道我很可爱,对吗?"
-  empty-drive: "您的媒体存储是空的"
-  empty-drive-description: "右键单击以打开菜单,或将文件拖放到此处以进行上传。"
-  empty-folder: "这个文件夹是空的"
-  unable-to-process: "操作无法完成"
-  circular-reference-detected: "目标文件夹是您要移动的文件夹的子文件夹。"
-  unhandled-error: "未知错误"
-  url-upload: "从网址上传"
-  url-of-file: "要上载的文件的URL"
-  url-upload-requested: "请求上传"
-  may-take-time: "上传完成可能需要一些时间。"
-  create-folder: "创建一个文件夹"
-  folder-name: "文件夹名称"
-  contextmenu:
-    create-folder: "创建文件夹"
-    upload: "上传文件"
-    url-upload: "从URL上传"
-desktop/views/components/media-video.vue:
-  sensitive: "阅读注意"
-  click-to-show: "点击以显示"
-desktop/views/components/followers-window.vue:
-  followers: "{} 的关注者"
-desktop/views/components/followers.vue:
-  empty: "看起来您没有关注者。"
-desktop/views/components/following-window.vue:
-  following: "正在关注 {}"
-desktop/views/components/following.vue:
-  empty: "看起来您没有正在关注的用户..."
-desktop/views/components/game-window.vue:
-  game: "游戏"
-desktop/views/components/home.vue:
-  done: "完成"
-  add-widget: "添加小部件:"
-  add: "添加"
-desktop/views/input-dialog.vue:
-  cancel: "取消"
-  ok: "确定"
-desktop/views/components/note-detail.vue:
-  private: "私密投稿"
-  deleted: "投稿已删除"
-  location: "位置信息"
-  renote: "转推"
-  add-reaction: "回应"
-  undo-reaction: "取消回应"
-desktop/views/components/note.vue:
-  reply: "回复"
-  renote: "转推"
-  add-reaction: "回应"
-  undo-reaction: "取消回应"
-  detail: "详细信息"
-  private: "这个投稿是私密的"
-  deleted: "投稿已删除"
-desktop/views/components/notes.vue:
-  error: "加载失败。"
-  retry: "重试"
-desktop/views/components/notifications.vue:
-  empty: "没有通知哦!"
-desktop/views/components/post-form.vue:
-  posted: "已发送投稿!"
-  replied: "已回复!"
-  reposted: "已转推!"
-  note-failed: "发帖失败"
-  reply-failed: "回复失败"
-  renote-failed: "转推失败"
-desktop/views/components/post-form-window.vue:
-  note: "新建帖子"
-  reply: "回复"
-  attaches: "已添加{}媒体文件"
-  uploading-media: "正在上传 {} 媒体文件"
-desktop/views/components/progress-dialog.vue:
-  waiting: "等待中"
-desktop/views/components/renote-form.vue:
-  quote: "引用..."
-  cancel: "取消"
-  renote: "转推"
-  renote-home: "转推(首页)"
-  reposting: "重新发送中..."
-  success: "已转推!"
-  failure: "转推失败"
-desktop/views/components/renote-form-window.vue:
-  title: "您是否要转推?"
-desktop/views/pages/user-following-or-followers.vue:
-  following: "{user}的正在关注"
-  followers: "{user}的关注者"
-desktop/views/components/settings.2fa.vue:
-  intro: "如果设置了两步验证,则不仅需要在登录时使用密码,还需要验证设备(如智能手机),这将提高安全性。"
-  detail: "详细信息..."
-  url: "https://www.google.com/landing/2step/"
-  caution: "如果您无法访问已注册的设备,您将无法再连接到 Misskey!"
-  register: "注册设备"
-  already-registered: "此设备已被注册"
-  unregister: "解除注册"
-  unregistered: "两步验证已被停用。"
-  enter-password: "请输入您的密码"
-  authenticator: "首先,您需要在设备上安装 Google Authenticator:"
-  howtoinstall: "怎样安装"
-  token: "令牌"
-  scan: "然后,扫描二维码:"
-  done: "请输入显示在您设备上的密钥:"
-  submit: "提交"
-  success: "设置完成"
-  failed: "设置失败, 请确保您的密钥是正确的。"
-  info: "从下次登录Misskey时,您的设备上显示的令牌以及密码也是必需的。"
-  totp-header: "身份验证 App"
-  security-key-header: "安全密钥"
-  security-key: "为了增强安全性,您可以使用支持FIDO2的硬件安全密钥登录您的帐户。 登录时,您将需要注册安全密钥或身份验证应用。"
-  last-used: "最后使用:"
-  activate-key: "单击以激活您的安全密钥"
-  security-key-name: "密钥名称"
-  register-security-key: "安全密钥注册完成"
-  something-went-wrong: "糟糕!安全密钥注册出现问题:"
-  key-unregistered: "安全密钥已被删除。"
-  use-password-less-login: "使用免密码登录"
-common/views/components/media-image.vue:
-  sensitive: "阅读注意"
-  click-to-show: "点击查看"
-common/views/components/api-settings.vue:
-  intro: "要访问API,请将此标记设置为请求参数的关键字“i”。"
-  caution: "请勿将此令牌输入任何应用,也不要将此令牌告诉其他人,否则您的账户可能会受到损害。"
-  regeneration-of-token: "如果您的令牌泄露,您可以重新生成。"
-  regenerate-token: "重新生成令牌"
-  token: "令牌:"
-  enter-password: "请输入您的密码"
-  console:
-    title: "API 控制台"
-    endpoint: "端点"
-    parameter: "参数"
-    credential-info: "此控制台不需要参数“i”。"
-    send: "发送"
-    sending: "等待回应"
-    response: "结果"
-desktop/views/components/settings.apps.vue:
-  no-apps: "没有已连接的应用程序"
-common/views/components/drive-settings.vue:
-  max: "容量"
-  in-use: "已使用"
-  stats: "统计"
-  default-upload-folder: "默认上传文件夹"
-  default-upload-folder-name: "文件夹"
-  change-default-upload-folder: "更改文件夹"
-common/views/components/mute-and-block.vue:
-  mute-and-block: "屏蔽/拉黑"
-  mute: "屏蔽"
-  block: "拉黑"
-  no-muted-users: "无屏蔽用户"
-  no-blocked-users: "无拉黑的用户"
-  word-mute: "文字屏蔽"
-  muted-words: "屏蔽关键字"
-  muted-words-description: "使用空格分隔会产生AND规范,并且使用换行符分隔会产生OR规范"
-  unmute-confirm: "取消屏蔽用户?"
-  unblock-confirm: "取消拉黑此用户?"
-  save: "保存"
-common/views/components/password-settings.vue:
-  reset: "更改密码"
-  enter-current-password: "输入当前的密码"
-  enter-new-password: "输入新密码"
-  enter-new-password-again: "请再次输入新密码"
-  not-match: "新密码不匹配"
-  changed: "密码已更改"
-  failed: "更改密码失败"
-common/views/components/post-form-attaches.vue:
-  attach-cancel: "删除附件"
-  mark-as-sensitive: "标记为“敏感”"
-  unmark-as-sensitive: "取消标记为“敏感”"
-desktop/views/components/sub-note-content.vue:
-  private: "这个帖子是私密的"
-  deleted: "帖子已删除"
-  media-count: "附加{}媒体"
-  poll: "投票"
-desktop/views/components/settings.tags.vue:
-  title: "标签"
-  query: "查询 (可选)"
-  add: "添加"
-  save: "保存"
-desktop/views/components/timeline.vue:
-  home: "首页"
-  local: "本地"
-  hybrid: "社交"
-  global: "全球"
-  mentions: "提到的"
-  messages: "直接发布"
-  list: "列表"
-  hashtag: "哈希标签"
-  add-tag-timeline: "添加哈希标签"
-  add-list: "添加列表"
-  list-name: "列表名称"
-desktop/views/components/ui.header.vue:
-  welcome-back: "欢迎回来!"
-  adjective: "先生"
-desktop/views/components/ui.header.account.vue:
-  profile: "个人资料"
-  lists: "列表"
-  groups: "群组"
-  follow-requests: "关注申请"
-  admin: "管理"
-  room: "房间"
-desktop/views/components/ui.header.nav.vue:
-  game: "游戏"
-desktop/views/components/ui.header.notifications.vue:
-  title: "通知"
-desktop/views/components/ui.header.post.vue:
-  post: "撰写新帖子"
-desktop/views/components/ui.header.search.vue:
-  placeholder: "搜索"
-desktop/views/components/user-preview.vue:
-  notes: "帖子"
-  following: "关注中"
-  followers: "关注者"
-desktop/views/components/users-list.vue:
-  all: "所有"
-  iknow: "你懂的"
-  fetching: "正在加载..."
-desktop/views/components/users-list-item.vue:
-  followed: "关注您"
-desktop/views/components/window.vue:
-  popout: "弹出"
-  close: "关闭"
-admin/views/index.vue:
-  dashboard: "仪表盘"
-  instance: "实例"
-  emoji: "自定义Emoji"
-  moderators: "版主"
-  users: "用户"
-  federation: "联邦"
-  announcements: "公告"
-  abuse: "举报垃圾信息"
-  queue: "作业队列"
-  logs: "日志"
-  db: "数据库"
-  back-to-misskey: "返回 Misskey"
-admin/views/db.vue:
-  tables: "表格"
-  vacuum: "VACUUM"
-  vacuum-info: "清理数据库。 保持数据完整并减少磁盘使用量。 此操作通常会自动定期执行。"
-  vacuum-exclamation: "运行VACUUM之后,数据库上的负载可能会持续一段时间,并且可能不响应用户操作。"
-admin/views/dashboard.vue:
-  dashboard: "Dashboard"
-  accounts: "账户"
-  notes: "帖子"
-  drive: "网盘"
-  instances: "实例"
-  this-instance: "此实例"
-  federated: "联合"
-admin/views/queue.vue:
-  title: "队列"
-  remove-all-jobs: "清除所有作业"
-  jobs: "任务"
-  queue: "队列"
-  domains:
-    deliver: "交付"
-    inbox: "收件箱"
-    db: "数据库"
-    objectStorage: "对象存储"
-  state: "状态"
-  states:
-    active: "处理中"
-    delayed: "已预订"
-    waiting: "队列等待中"
-  result-is-truncated: "结果已省略"
-  other-queues: "其他队列"
-admin/views/logs.vue:
-  logs: "日志"
-  domain: "域"
-  level: "级别"
-  levels:
-    all: "所有"
-    info: "信息"
-    success: "成功"
-    warning: "警告"
-    error: "错误"
-    debug: "调试"
-  delete-all: "全部删除"
-admin/views/abuse.vue:
-  title: "举报垃圾信息"
-  target: "目标"
-  reporter: "报告者"
-  details: "详情"
-  remove-report: "删除"
-admin/views/instance.vue:
-  instance: "实例"
-  instance-name: "实例名称"
-  instance-description: "实例介绍"
-  host: "主机名"
-  icon-url: "图标URL"
-  logo-url: "Logo URL"
-  banner-url: "背景图片地址"
-  error-image-url: "无效的图像URL"
-  languages: "实例语言"
-  languages-desc: "您可以添加多个,以空格分隔。"
-  tos-url: "服务条款URL"
-  repository-url: "源码库URL"
-  feedback-url: "反馈URL"
-  maintainer-config: "管理员信息"
-  maintainer-name: "管理员名称"
-  maintainer-email: "联系管理员"
-  advanced-config: "其他设置"
-  note-and-tl: "帖子和时间线"
-  drive-config: "网盘设置"
-  use-object-storage: "使用对象存储"
-  object-storage-base-url: "URL"
-  object-storage-bucket: "存储空间名"
-  object-storage-prefix: "前缀"
-  object-storage-endpoint: "端点"
-  object-storage-region: "区域"
-  object-storage-port: "端口"
-  object-storage-access-key: "访问密钥"
-  object-storage-secret-key: "密钥"
-  object-storage-use-ssl: "使用 SSL"
-  object-storage-s3-info: "使用Amazon S3作为对象存储时,请确认{0}相关“终端”和“区域”的设置。"
-  object-storage-s3-info-here: "这里"
-  object-storage-gcs-info: "将Google Cloud Storage用作对象存储时,请将“终端”设置为storage.googleapis.com,并将“区域”留空。"
-  cache-remote-files: "远程文件缓存"
-  proxy-remote-files: "代理远程文件"
-  local-drive-capacity-mb: "每个用户的网盘空间"
-  remote-drive-capacity-mb: "每个远程用户的网盘容量"
-  mb: "以兆字节(Mbps)为单位"
-  recaptcha-config: "reCAPTCHA设置"
-  recaptcha-info: "reCAPTCHA token是必要的. 请从 https://www.google.com/recaptcha/intro/ 获取。\n请注意, 该功能在中国大陆不可用。"
-  recaptcha-info2: "不支持v3。请使用v2。"
-  enable-recaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
-  recaptcha-site-key: "网站密钥"
-  recaptcha-secret-key: "密钥"
-  recaptcha-preview: "预览"
-  hidden-tags: "隐藏哈希标签"
-  hidden-tags-info: "使用换行符分隔要从集合中排除的哈希标签。"
-  external-service-integration-config: "连接外部服务"
-  twitter-integration-config: "连接到Twitter的设置"
-  twitter-integration-info: "设置返回的URL{url}。"
-  enable-twitter-integration: "启用连接到Twitter"
-  twitter-integration-consumer-key: "Consumer key"
-  twitter-integration-consumer-secret: "Consumer Secret"
-  github-integration-config: "连接到GitHub设置"
-  github-integration-info: "设置返回的URL{url}。"
-  enable-github-integration: "启用连接到GitHub"
-  github-integration-client-id: "Client ID"
-  github-integration-client-secret: "Client Secret"
-  discord-integration-config: "设置 Discord Integration"
-  discord-integration-info: "设置返回的URL{url}。"
-  enable-discord-integration: "启用 Discord 连接"
-  discord-integration-client-id: "Client ID"
-  discord-integration-client-secret: "Client Secret"
-  proxy-account-config: "代理帐户设置"
-  proxy-account-info: "如果此实例中没有人跟随他或她,则代理帐户可以跟随远程用户进行活动。 当您将此实例中没有人的远程用户添加到列表中时,为了获取他或她的数据,代理账户会跟随他或她,而不是您的跟随者。"
-  proxy-account-username: "代理账户用户名"
-  proxy-account-username-desc: "指定用作代理的账户的用户名。"
-  proxy-account-warn: "在进行此操作之前,您必须创建一个拥有此用户名的账户。"
-  max-note-text-length: "最大帖子字符数"
-  disable-registration: "停用新用户注册功能"
-  disable-local-timeline: "停用本地时间线功能"
-  disable-global-timeline: "禁用全局时间线"
-  disabling-timelines-info: "即使禁用时间线,管理员和版主仍然可用。"
-  enable-emoji-reaction: "在回应上使用表情符号"
-  use-star-for-reaction-fallback: "使用默认的star来表示未知的回应"
-  invite: "邀请"
-  save: "保存"
-  saved: "保存完毕"
-  pinned-users: "置顶用户"
-  pinned-users-info: "描述您要置顶的用户,以换行符分隔。"
-  email-config: "电子邮件服务器设置"
-  email-config-info: "用于确认电子邮件和密码重置等。"
-  enable-email: "启用电子邮件送递"
-  email: "电子邮件地址"
-  smtp-secure: "在 SMTP 连接中使用隐式 SSL / TLS"
-  smtp-secure-info: "使用时关闭 STARTTLS。"
-  smtp-host: "SMTP 服务器地址 (主机名)"
-  smtp-port: "SMTP 端口"
-  smtp-auth: "SMTP身份验证"
-  smtp-user: "SMTP 用户名"
-  smtp-pass: "SMTP 密码"
-  test-email: "测试"
-  serviceworker-config: "ServiceWorker"
-  enable-serviceworker: "启用ServiceWorker"
-  serviceworker-info: "您需要启用推送通知"
-  vapid-publickey: "VAPID公钥"
-  vapid-privatekey: "VAPID私钥"
-  vapid-info: "如果您想要启用ServiceWorker,那么您需要生成VAPID秘钥。除非您已经在其他地方设置了全局node_modules位置,否则您需要将其作为root用户运行:"
-admin/views/charts.vue:
-  title: "历史记录"
-  per-day: "每天"
-  per-hour: "每小时"
-  federation: "联合"
-  notes: "投稿"
-  users: "用户"
-  drive: "网盘"
-  network: "网络"
-  charts:
-    federation-instances: "实例数:增加/减少"
-    federation-instances-total: "实例总数"
-    notes: "帖子数量:增加/减少(总和)"
-    local-notes: "帖子数量:增加/减少(Local)"
-    remote-notes: "帖子数量:增加/减少(远程)"
-    notes-total: "帖子总数"
-    users: "用户数量:增加/减少"
-    users-total: "用户总数"
-    active-users: "活跃用户数"
-    drive: "存储容量:增加/减少"
-    drive-total: "网盘总使用量"
-    drive-files: "网盘文件数量变化"
-    drive-files-total: "网盘文件总数"
-    network-requests: "请求"
-    network-time: "响应时间"
-    network-usage: "网络流量"
-admin/views/drive.vue:
-  operation: "操作"
-  fileid-or-url: "文件ID或文件URL"
-  file-not-found: "找不到文件"
-  lookup: "查询"
-  sort:
-    title: "排序"
-    createdAtAsc: "按上传时间(升序)"
-    createdAtDesc: "按上传时间(降序)"
-    sizeAsc: "按大小(升序)"
-    sizeDesc: "按大小(降序)"
-  origin:
-    title: "源自"
-    combined: "本地+远程"
-    local: "本地"
-    remote: "远程"
-  delete: "删除"
-  deleted: "已删除"
-  mark-as-sensitive: "标记为“敏感”"
-  unmark-as-sensitive: "取消标记为“敏感”"
-  marked-as-sensitive: "标记为“敏感”"
-  unmarked-as-sensitive: "取消标记为“敏感”"
-  clean-remote-files: "删除远程文件缓存"
-  clean-remote-files-are-you-sure: "确定要删除所有远程文件缓存吗?"
-  clean-up: "清除缓存"
-admin/views/users.vue:
-  operation: "操作"
-  username-or-userid: "用户名或用户ID"
-  user-not-found: "用户不存在"
-  lookup: "订阅"
-  reset-password: "密码重置"
-  reset-password-confirm: "是否重置密码?"
-  password-updated: "密码为「{password}」"
-  suspend: "被冻结"
-  suspend-confirm: "是否冻结?"
-  suspended: "成功冻结用户"
-  unsuspend: "已解除冻结"
-  unsuspend-confirm: "是否解除冻结?"
-  unsuspended: "已成功解除用户冻结"
-  make-silence: "禁言"
-  silence-confirm: "确认屏蔽?"
-  unmake-silence: "解除禁言"
-  unsilence-confirm: "解除屏蔽?"
-  update-remote-user: "更新远程用户信息"
-  remote-user-updated: "远程用户信息已更新"
-  delete-all-files: "删除所有文件"
-  delete-all-files-confirm: "删除所有文件吗?"
-  username: "用户名"
-  host: "主机名"
-  users:
-    title: "用户"
-    sort:
-      title: "排序"
-      createdAtAsc: "注册时间从旧到新"
-      createdAtDesc: "注册时间从新到旧"
-      updatedAtAsc: "更新时间从旧到新"
-      updatedAtDesc: "更新时间从新到旧"
-    state:
-      title: "状态"
-      all: "全部"
-      available: "可用"
-      admin: "管理员"
-      moderator: "版主"
-      adminOrModerator: "管理员+版主"
-      silenced: "已禁言"
-      suspended: "已冻结"
-    origin:
-      title: "源自"
-      combined: "本地+远程"
-      local: "本地"
-      remote: "远程"
-    createdAt: "注册日期"
-    updatedAt: "最后更新"
-admin/views/moderators.vue:
-  add-moderator:
-    title: "注册版主"
-    add: "注册"
-    added: "已注册版主。"
-    remove: "取消"
-    removed: "取消注册版主"
-  logs:
-    title: "日志"
-    moderator: "版主"
-    type: "操作"
-    at: "日期和时间"
-    info: "信息"
-admin/views/emoji.vue:
-  add-emoji:
-    title: "添加emoji"
-    name: "Emoji 名称"
-    name-desc: "你可以使用字符a~z 0~9 _"
-    category: "类别"
-    aliases: "别名"
-    aliases-desc: "您可以添加多个,以空格分隔。"
-    url: "emoji 地址"
-    add: "添加"
-    info: "我们建议使用50KB以下的PNG图像。"
-    added: "Emoji 已添加"
-  emojis:
-    title: "表情符号列表"
-    update: "æ›´æ–°"
-    remove: "移除"
-  updated: "已更新"
-  remove-emoji:
-    are-you-sure: "删除「$1」?"
-    removed: "已删除"
-admin/views/announcements.vue:
-  announcements: "公告"
-  save: "保存"
-  remove: "移除"
-  add: "添加"
-  title: "标题"
-  text: "内容"
-  saved: "已保存"
-  _remove:
-    are-you-sure: "删除「$1」?"
-    removed: "已删除"
-admin/views/hashtags.vue:
-  hided-tags: "隐藏标签"
-admin/views/federation.vue:
-  instance: "例"
-  host: "主机名"
-  notes: "帖子"
-  users: "用户"
-  following: "正在关注"
-  followers: "关注者"
-  caught-at: "注册日期"
-  status: "状态"
-  latest-request-sent-at: "上次发送的请求"
-  latest-request-received-at: "上次收到的请求"
-  remove-all-following: "取消所有关注"
-  remove-all-following-info: "取消{host}的所有关注者。当实例不存在时执行。"
-  delete-all-files: "删除所有文件"
-  block: "拉黑"
-  marked-as-closed: "标记为已关闭"
-  lookup: "查询"
-  instances: "联邦"
-  instance-not-registered: "实例未注册"
-  sort: "排序"
-  sorts:
-    caughtAtAsc: "注册时间从旧到新"
-    caughtAtDesc: "注册时间从新到旧"
-    lastCommunicatedAtAsc: "上次互动时间从旧到新"
-    lastCommunicatedAtDesc: "上次互动时间从新到旧"
-    notesAsc: "发帖数量从少到多"
-    notesDesc: "发帖数量从多到少"
-    usersAsc: "用户数从少到多"
-    usersDesc: "用户数从多到少"
-    followingAsc: "关注数从少到多"
-    followingDesc: "关注数从多到少"
-    followersAsc: "粉丝数从少到多"
-    followersDesc: "粉丝数从多到少"
-    driveUsageAsc: "网盘使用量从少到多"
-    driveUsageDesc: "网盘使用量从多到少"
-    driveFilesAsc: "网盘文件数从少到多"
-    driveFilesDesc: "网盘文件数从多到少"
-  state: "状态"
-  states:
-    all: "所有"
-    blocked: "已拉黑"
-    not-responding: "没有响应"
-    marked-as-closed: "已标记为已关闭"
-  result-is-truncated: "显示最前面的{n}项。"
-  charts: "图表"
-  chart-srcs:
-    requests: "请求"
-    users: "用户数量变化"
-    users-total: "用户总数"
-    notes: "发帖数变化"
-    notes-total: "帖子总数"
-    ff: "关注/被关注数量变化"
-    ff-total: "关注/被关注总数"
-    drive-usage: "网盘使用量变化"
-    drive-usage-total: "网盘总使用量"
-    drive-files: "网盘文件数量变化"
-    drive-files-total: "网盘文件总数"
-  chart-spans:
-    hour: "每小时"
-    day: "每天"
-  blocked-hosts: "拉黑"
-  blocked-hosts-info: "描述您要阻止的主机,以换行符分隔。"
-  save: "保存"
-desktop/views/pages/welcome.vue:
-  about: "更多信息..."
-  timeline: "时间线"
-  announcements: "公告"
-  photos: "最近图片"
-  powered-by-misskey: "Powered by <b>Misskey</b>."
-  info: "信息"
-desktop/views/pages/drive.vue:
-  title: "Misskey 网盘"
-desktop/views/pages/note.vue:
-  prev: "上一个帖子"
-  next: "下一个帖子"
-desktop/views/pages/selectdrive.vue:
-  title: "选择文件"
-  ok: "确定"
-  cancel: "取消"
-  upload: "从设备上传文件"
-desktop/views/pages/search.vue:
-  not-available: "在此实例的设置中关闭搜索功能。"
-  not-found: "没有找到“{q}”的帖子"
-desktop/views/pages/tag.vue:
-  no-posts-found: "没有找到带有哈希标签“{q}”的帖子"
-desktop/views/pages/user-list.users.vue:
-  users: "用户"
-  add-user: "添加用户"
-  username: "用户名"
-desktop/views/pages/user/user.followers-you-know.vue:
-  title: "您可能认识的关注者"
-  loading: "正在加载中"
-  no-users: "没有你知道的关注者"
-desktop/views/pages/user/user.friends.vue:
-  title: "活跃用户"
-  loading: "正在加载中"
-  no-users: "没有活跃用户"
-desktop/views/pages/user/user.photos.vue:
-  title: "照片"
-  loading: "正在加载中"
-  no-photos: "没有图片"
-desktop/views/pages/user/user.header.vue:
-  posts: "帖子"
-  following: "关注中"
-  followers: "关注者"
-  is-bot: "这个账户是Bot"
-  no-description: "没有自我介绍"
-  years-old: "{age}岁"
-  year: "å¹´"
-  month: "月"
-  day: "æ—¥"
-  follows-you: "关注您"
-desktop/views/pages/user/user.timeline.vue:
-  default: "帖子"
-  with-replies: "帖子与回复"
-  with-media: "媒体"
-  my-posts: "我的帖子"
-desktop/views/widgets/notifications.vue:
-  title: "通知"
-desktop/views/widgets/polls.vue:
-  title: "投票"
-  refresh: "更多"
-  nothing: "没有投票哦!"
-desktop/views/widgets/post-form.vue:
-  title: "帖子"
-  note: "帖子"
-  something-happened: "由于某种原因无法发帖。"
-desktop/views/widgets/profile.vue:
-  update-banner: "点击来剪辑背景"
-  update-avatar: "点击来剪辑头像"
-desktop/views/widgets/trends.vue:
-  title: "趋势"
-  refresh: "更多"
-  nothing: "没有趋势图哦!"
-desktop/views/widgets/users.vue:
-  title: "推荐用户"
-  refresh: "更多"
-  no-one: "没有任何推荐用户!"
-mobile/views/components/drive.vue:
-  used: "已使用"
-  folder-count: "文件夹"
-  count-separator: ","
-  file-count: "文件"
-  nothing-in-drive: "网盘为空"
-  folder-is-empty: "这文件夹是空的"
-  folder-name: "文件夹名称"
-  here-is-root: "当前位置为根目录。"
-  url-prompt: "要上传的文件的URL"
-  uploading: "已请求上传。 上传完成可能需要一段时间。"
-  folder-name-cannot-empty: "文件夹名不能为空。"
-mobile/views/components/drive-file-chooser.vue:
-  select-file: "选择文件"
-mobile/views/components/drive-folder-chooser.vue:
-  select-folder: "选择一个文件夹"
-mobile/views/components/drive.file.vue:
-  nsfw: "阅读注意"
-mobile/views/components/drive.file-detail.vue:
-  download: "下载"
-  rename: "重命名"
-  move: "移动"
-  hash: "哈希(md5)"
-  exif: "EXIF"
-  nsfw: "阅读注意"
-  mark-as-sensitive: "标记为“敏感”"
-  unmark-as-sensitive: "取消标记为“敏感”"
-mobile/views/components/media-video.vue:
-  sensitive: "阅读注意"
-  click-to-show: "点击以显示"
-common/views/components/follow-button.vue:
-  following: "正在关注"
-  follow: "关注"
-  request-pending: "发送关注申请"
-  follow-processing: "申请处理中"
-  follow-request: "关注申请"
-mobile/views/components/note.vue:
-  private: "私密帖子"
-  deleted: "帖子已删除"
-  location: "位置信息"
-mobile/views/components/note-detail.vue:
-  reply: "回复"
-  reaction: "回应"
-  private: "这个帖子是私密的"
-  deleted: "帖子已删除"
-  location: "位置信息"
-mobile/views/components/note-preview.vue:
-  admin: "管理员"
-  bot: "bot"
-  cat: "cat"
-mobile/views/components/note-sub.vue:
-  admin: "管理员"
-  bot: "bot"
-  cat: "cat"
-mobile/views/components/notifications.vue:
-  empty: "没有通知哦!"
-mobile/views/components/sub-note-content.vue:
-  private: "私密帖子"
-  deleted: "帖子已删除"
-  media-count: "附加{}媒体"
-  poll: "投票"
-mobile/views/components/ui.header.vue:
-  welcome-back: "欢迎回来!"
-  adjective: "先生"
-mobile/views/components/ui.nav.vue:
-  timeline: "时间线"
-  notifications: "通知"
-  follow-requests: "关注申请"
-  search: "搜索"
-  user-lists: "列表"
-  user-groups: "群组"
-  widgets: "小部件"
-  game: "游戏"
-  admin: "管理"
-  about: "关于 Misskey"
-mobile/views/pages/drive.vue:
-  contextmenu:
-    upload: "上传文件"
-    url-upload: "从URL上传文件"
-    create-folder: "创建文件夹"
-    rename-folder: "重命名文件夹"
-    move-folder: "移动此文件夹"
-    delete-folder: "删除此文件夹"
-mobile/views/pages/signup.vue:
-  lets-start: "您的账户现已准备就绪! 📦"
-mobile/views/pages/followers.vue:
-  followers-of: "{name}的关注者"
-mobile/views/pages/following.vue:
-  following-of: "{name}的正在关注"
-mobile/views/pages/home.vue:
-  home: "首页"
-  local: "Local"
-  hybrid: "社交"
-  global: "Global"
-  mentions: "Mentions"
-  messages: "直接发布"
-mobile/views/pages/tag.vue:
-  no-posts-found: "没有找到带有哈希标签“{q}”的帖子"
-mobile/views/pages/widgets.vue:
-  dashboard: "仪表盘"
-  widgets-hints: "您可以添加/删除/重新排列小部件。 要移动小部件,请拖动“三”。 点击“×”删除小部件。 某些小部件可以通过点击来更改显示。"
-  add-widget: "添加"
-  customization-tips: "定制提示"
-mobile/views/pages/widgets/activity.vue:
-  activity: "活动"
-mobile/views/pages/share.vue:
-  share-with: "共享{name}"
-mobile/views/pages/note.vue:
-  title: "帖文"
-  prev: "上一个帖子"
-  next: "下一个帖子"
-mobile/views/pages/games/reversi.vue:
-  reversi: "游戏"
-mobile/views/pages/search.vue:
-  search: "搜索"
-  not-found: "没有找到有关于“{q}”的帖子"
-mobile/views/pages/selectdrive.vue:
-  select-file: "选择文件"
-mobile/views/pages/notifications.vue:
-  notifications: "通知"
-mobile/views/pages/settings.vue:
-  signed-in-as: "以{}登录"
-mobile/views/pages/user.vue:
-  follows-you: "关注您"
-  following: "关注中"
-  followers: "关注者"
-  notes: "帖子"
-  overview: "概观"
-  timeline: "时间线"
-  media: "媒体"
-  years-old: "{age}岁"
-mobile/views/pages/user/home.vue:
-  recent-notes: "最近的帖子"
-  images: "图片"
-  activity: "活动"
-  keywords: "关键字"
-  domains: "域名"
-  frequently-replied-users: "活跃用户"
-  followers-you-know: "您可能认识的关注者"
-  last-used-at: "上次登录:"
-mobile/views/pages/user/home.photos.vue:
-  no-photos: "没有图片"
-deck:
-  widgets: "小部件"
-  home: "首页"
-  local: "Local"
-  hybrid: "社交"
-  hashtag: "哈希标签"
-  global: "Global"
-  mentions: "Mentions"
-  direct: "直接发布"
-  notifications: "通知"
-  list: "列表"
-  select-list: "请选择一个列表"
-  swap-left: "向左移动"
-  swap-right: "向右移动"
-  swap-up: "向上移动"
-  swap-down: "向下移动"
-  remove: "移除"
-  add-column: "添加一列"
-  rename: "重命名"
-  stack-left: "向左折叠"
-  pop-right: "带到右边"
-  disabled-timeline:
-    title: "禁用时间线"
-    description: "服务器管理员已禁用时间线。"
-deck/deck.tl-column.vue:
-  is-media-only: "只有媒体的帖子"
-  edit: "选项"
-deck/deck.user-column.vue:
-  follows-you: "关注您"
-  posts: "帖子"
-  following: "关注中"
-  followers: "关注者"
-  images: "图片"
-  activity: "活动"
-  timeline: "时间线"
-  pinned-notes: "置顶帖"
-  pinned-page: "已置顶的页面"
-docs:
-  edit-this-page-on-github: "发现错误或想要为文档做出贡献?"
-  edit-this-page-on-github-link: "在GitHub上编辑这个页面。"
-dev/views/index.vue:
-  manage-apps: "管理应用"
-dev/views/apps.vue:
-  manage-apps: "管理应用"
-  create-app: "创建应用"
-  app-missing: "没有应用"
-dev/views/new-app.vue:
-  new-app: "新应用"
-  new-app-info: "可以从 API 中创建应用。 (app/create)"
-  create-app: "正在创建应用"
-  app-name: "应用名称"
-  app-name-placeholder: "ex) iOS版Misskey"
-  app-name-desc: "您应用的名称"
-  app-overview: "应用摘要"
-  app-overview-placeholder: " ex) iOS版Misskey客户端."
-  app-overview-desc: "您的应用的简要说明或介绍。"
-  callback-url: "回应URL (optional)"
-  callback-url-placeholder: "ex) https://your.app.example.com/callback.php"
-  callback-url-desc: "通过身份验证表单对用户进行身份验证后重定向到的URL。"
-  authority: "权限"
-  authority-desc: "只能通过API访问此处请求的功能。"
-  authority-warning: "您可以在创建应用程序后对其进行更改,但如果您授予不同的权限,则当时关联的所有用户密钥都将失效。"
-pages:
-  new-page: "创建页面"
-  edit-page: "编辑页面"
-  read-page: "查看源"
-  page-created: "页面已创建"
-  page-updated: "页面已更新"
-  name-already-exists: "该页面URL已存在"
-  title-invalid-name: "无效的页面URL"
-  text-invalid-name: "请确认该项不为空"
-  are-you-sure-delete: "是否删除此页面?"
-  page-deleted: "该页面已被删除。"
-  edit-this-page: "编辑此页面"
-  pin-this-page: "置顶"
-  unpin-this-page: "取消置顶"
-  view-source: "查看源代码"
-  view-page: "查看页面"
-  like: "赞"
-  unlike: "取消赞"
-  liked-pages: "喜欢的页面"
-  my-pages: "个人页面"
-  inspector: "检查器"
-  content: "页面内容"
-  variables: "变量"
-  variables-info: "您可以使用变量创建动态页面。在文本中通过<b>{变量名}</b>的写法来嵌入变量值。例如在文本<b>Hello { thing } world!</b>中,如果变量(thing)的值为<b>ai</b>,那么该文本会成为<b>Hello ai world!</b>。"
-  variables-info2: "因为变量的计算(计算变量值)是从上到下执行的,所以不能在变量中引用下面的变量。例如从上到下依次定义了<b>A,B,C</b>3个变量,那么<b>C</b>中可以引用<b>A</b>或<b>B</b>,但是<b>A</b>无法引用<b>B</b>或<b>C</b>。"
-  variables-info3: "为了接收来自用户的输入,页面上设有“用户输入”块,在“变量名称”中设置要在其中保存输入值的变量名(变量会自动创建)。您可以使用该变量执行操作以响应用户输入。"
-  variables-info4: "通过使用函数,您可以将数值计算过程组合成可重用的形式。要创建函数,需要创建一个“函数”类型的变量。你可以将函数设定为槽函数(参数)的格式,槽函数的值可作为函数中的变量使用。另外,AiScript标准中还有一些函数会将函数作为参数(称为高阶函数)。\n除了已经预先定义的函数外,您也可以将它们设置为这些高阶函数的槽函数。"
-  more-details: "详细说明"
-  title: "标题"
-  url: "页面URL"
-  summary: "页面摘要"
-  align-center: "居中"
-  hide-title-when-pinned: "置顶时隐藏标题"
-  font: "字体"
-  fontSerif: "衬线字体"
-  fontSansSerif: "无衬线字体"
-  set-eye-catching-image: "设置封面图片"
-  remove-eye-catching-image: "删除封面图片"
-  choose-block: "添加块"
-  select-type: "类型选择"
-  enter-variable-name: "请确定变量名"
-  the-variable-name-is-already-used: "变量名已使用"
-  content-blocks: "内容"
-  input-blocks: "输入"
-  special-blocks: "特殊"
-  post-from-post-form: "发布此内容"
-  posted-from-post-form: "已发布"
-  blocks:
-    text: "文本"
-    textarea: "文本区域"
-    section: "章节"
-    image: "图片"
-    button: "按钮"
-    if: "判断"
-    _if:
-      variable: "变量"
-    post: "投稿窗口"
-    _post:
-      text: "内容"
-    textInput: "文本输入"
-    _textInput:
-      name: "变量名"
-      text: "标题"
-      default: "默认值"
-    textareaInput: "多行文本输入"
-    _textareaInput:
-      name: "变量名"
-      text: "标题"
-      default: "默认值"
-    numberInput: "数值输入"
-    _numberInput:
-      name: "变量名"
-      text: "标题"
-      default: "默认值"
-    switch: "开关"
-    _switch:
-      name: "变量名"
-      text: "标题"
-      default: "默认值"
-    counter: "计数器"
-    _counter:
-      name: "变量名"
-      text: "标题"
-      inc: "增加值"
-    _button:
-      text: "标题"
-      colored: "彩色"
-      action: "按下按钮时的行为"
-      _action:
-        dialog: "显示对话框"
-        _dialog:
-          content: "内容"
-        resetRandom: "随机值重置"
-        pushEvent: "发送事件"
-        _pushEvent:
-          event: "事件名称"
-          message: "按下时显示的消息"
-          variable: "发送的变量"
-          no-variable: "空"
-    radioButton: "选择项"
-    _radioButton:
-      name: "变量名"
-      title: "标题"
-      values: "使用换行区分的选择项"
-      default: "默认值"
-  script:
-    categories:
-      flow: "控制"
-      logical: "逻辑运算"
-      operation: "计算"
-      comparison: "比较"
-      random: "随机"
-      value: "值"
-      fn: "函数"
-      text: "文本操作"
-      convert: "转换"
-      list: "列表"
-    blocks:
-      text: "文本"
-      multiLineText: "文本 (多行)"
-      textList: "文本列表"
-      _textList:
-        info: "情使用换行符分隔每行"
-      strLen: "文本长度"
-      _strLen:
-        arg1: "文本"
-      strPick: "字符提取"
-      _strPick:
-        arg1: "文本"
-        arg2: "字符位置"
-      strReplace: "文本替换"
-      _strReplace:
-        arg1: "文本"
-        arg2: "替换之前"
-        arg3: "替换之后"
-      strReverse: "文本反向"
-      _strReverse:
-        arg1: "文本"
-      join: "合并文本"
-      _join:
-        arg1: "列表"
-        arg2: "分隔符"
-      add: "+ 加"
-      _add:
-        arg1: "A"
-        arg2: "B"
-      subtract: "- 减"
-      _subtract:
-        arg1: "A"
-        arg2: "B"
-      multiply: "× 乘"
-      _multiply:
-        arg1: "A"
-        arg2: "B"
-      divide: "÷ 除"
-      _divide:
-        arg1: "A"
-        arg2: "B"
-      mod: "÷ 取模"
-      _mod:
-        arg1: "A"
-        arg2: "B"
-      round: "四舍五入"
-      _round:
-        arg1: "数值"
-      eq: "A和B相等"
-      _eq:
-        arg1: "A"
-        arg2: "B"
-      notEq: "A和B不等"
-      _notEq:
-        arg1: "A"
-        arg2: "B"
-      and: "Aå’ŒB"
-      _and:
-        arg1: "A"
-        arg2: "B"
-      or: "A或B"
-      _or:
-        arg1: "A"
-        arg2: "B"
-      lt: "< A小于B"
-      _lt:
-        arg1: "A"
-        arg2: "B"
-      gt: "> A大于B"
-      _gt:
-        arg1: "A"
-        arg2: "B"
-      ltEq: "<= A小于等于B"
-      _ltEq:
-        arg1: "A"
-        arg2: "B"
-      gtEq: ">= A大于等于B"
-      _gtEq:
-        arg1: "A"
-        arg2: "B"
-      if: "分支"
-      _if:
-        arg1: "如果"
-        arg2: "的话"
-        arg3: "否则"
-      not: "否定"
-      _not:
-        arg1: "否定"
-      random: "随机"
-      _random:
-        arg1: "概率"
-      rannum: "随机"
-      _rannum:
-        arg1: "最小"
-        arg2: "最大"
-      randomPick: "从列表中随机选择"
-      _randomPick:
-        arg1: "列表"
-      dailyRandom: "随机(每个用户每日)"
-      _dailyRandom:
-        arg1: "概率"
-      dailyRannum: "随机数(每个用户每日)"
-      _dailyRannum:
-        arg1: "最小"
-        arg2: "最大"
-      dailyRandomPick: "从列表中随机选择(每个用户每日)"
-      _dailyRandomPick:
-        arg1: "列表"
-      seedRandom: "随机 (种子)"
-      _seedRandom:
-        arg1: "种子"
-        arg2: "概率"
-      seedRannum: "随机数(种子)"
-      _seedRannum:
-        arg1: "种子"
-        arg2: "最小"
-        arg3: "最大"
-      seedRandomPick: "从列表中随机选择 (种子)"
-      _seedRandomPick:
-        arg1: "种子"
-        arg2: "列表"
-      DRPWPM: "从概率列表中随机选择(每用户每天)"
-      _DRPWPM:
-        arg1: "文本列表"
-      pick: "从列表中选择"
-      _pick:
-        arg1: "列表"
-        arg2: "位置"
-      listLen: "获取列表长度"
-      _listLen:
-        arg1: "列表"
-      number: "数值"
-      stringToNumber: "文本到数字"
-      _stringToNumber:
-        arg1: "文本"
-      numberToString: "数字到文本"
-      _numberToString:
-        arg1: "数值"
-      splitStrByLine: "将文本按行拆分"
-      _splitStrByLine:
-        arg1: "文本"
-      ref: "变量"
-      fn: "函数"
-      _fn:
-        slots: "槽函数"
-        slots-info: "请使用换行符分隔每个槽函数"
-        arg1: "输出"
-      for: "重复"
-      _for:
-        arg1: "次数"
-        arg2: "处理"
-    typeError: "槽函数{slot}需要传入“{expect}”,但是实际传入为“{actual}”!"
-    thereIsEmptySlot: "槽函数{slot}为空!"
-    types:
-      string: "文本"
-      number: "数值"
-      boolean: "布尔值"
-      array: "列表"
-      stringArray: "文本列表"
-    emptySlot: "空白槽函数"
-    enviromentVariables: "环境变量"
-    pageVariables: "页面元素"
-    argVariables: "输入槽函数"
-room:
-  add-furniture: "放置家具"
-  translate: "移动"
-  rotate: "旋转"
-  exit: "返回"
-  remove: "移除"
-  save: "保存"
-  saved: "已保存"
-  clear: "清理"
-  clear-confirm: "是否清除所有家具?"
-  leave-confirm: "有尚未保存的修改。是否离开?"
-  chooseImage: "选择图片"
-  room-type: "房间类型"
-  carpet-color: "地板颜色"
-  rooms:
-    default: "默认"
-    washitsu: "和式房间"
-  furnitures:
-    milk: "牛奶纸箱"
-    bed: "床"
-    low-table: "矮桌"
-    desk: "书桌"
-    chair: "椅子"
-    chair2: "椅子2"
-    fan: "换气扇"
-    pc: "电脑"
-    plant: "观叶植物"
-    plant2: "观叶植物2"
-    eraser: "橡皮擦"
-    pencil: "铅笔"
-    pudding: "布丁"
-    cardboard-box: "纸板箱"
-    cardboard-box2: "纸板箱2"
-    cardboard-box3: "纸板箱3"
-    book: "书"
-    book2: "书2"
-    piano: "钢琴"
-    facial-tissue: "纸巾盒"
-    server: "服务器"
-    moon: "月球"
-    corkboard: "软木板"
-    mousepad: "鼠标垫"
-    monitor: "显示器"
-    keyboard: "键盘"
-    carpet-stripe: "地毯(条纹)"
-    mat: "垫子"
-    color-box: "收纳柜"
-    wall-clock: "挂钟"
-    photoframe: "相框"
-    cube: "立方体"
-    tv: "电视"
-    pinguin: "企鹅君"
-    rubik-cube: "魔方"
-    poster-h: "海报(横向)"
-    poster-v: "海报(纵向)"
-    sofa: "沙发"
-    spiral: "螺旋楼梯"
-    bin: "垃圾箱"
-    cup-noodle: "杯面"
-    holo-display: "全息显示器"
-    energy-drink: "能量饮料"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
deleted file mode 100644
index a0138b465fd314a60c786a0e0c83c2eb812b2b9e..0000000000000000000000000000000000000000
--- a/locales/zh-TW.yml
+++ /dev/null
@@ -1,91 +0,0 @@
----
-meta:
-  lang: "中文(繁体)"
-common:
-  intro:
-    title: "什麽是 Misskey 呢?"
-    rich-contents: "發佈"
-    reaction: "回應"
-    drive: "雲端硬碟"
-  close: "關閉"
-  enter-password: "請輸入密碼"
-  2fa: "雙重身份驗證"
-  dark-mode: "夜間模式"
-  signup: "註冊"
-  signout: "登出"
-  notification:
-    reversi-invited: "您已被邀請加入壹場遊戲"
-    reversi-invited-by: "來自{}的邀請"
-    notified-by: "來自{}的邀請"
-  time:
-    future: "未來"
-    just_now: "剛剛"
-  drive: "雲端硬碟"
-  weekday:
-    sunday: "週日"
-    monday: "週一"
-    tuesday: "週二"
-    wednesday: "週三"
-    thursday: "週四"
-    friday: "週五"
-    saturday: "週六"
-  reactions:
-    like: "è´Š"
-    love: "喜歡"
-    congrats: "恭喜"
-  _settings:
-    password: "密碼"
-    font-size: "字體大小"
-    font-size-x-small: "小"
-    font-size-small: "較小"
-    deck-column-width-wide: "寬"
-    timeline: "時間軸"
-common/views/components/connect-failed.troubleshooter.vue:
-  flush: "清除快取"
-common/views/components/theme.vue:
-  light-themes: "淺色主題"
-  dark-themes: "深色主題"
-  install-a-theme: "安裝主題"
-  save-created-theme: "保存主題"
-common/views/components/signin.vue:
-  signin-with-twitter: "用 Twitter 帳號登入"
-  signin-with-github: "用 GitHub 帳號登入"
-  signin-with-discord: "用 Discord 帳號登入"
-  login-failed: "登錄失敗。 請檢查用戶名和密碼。"
-common/views/components/signup.vue:
-  invitation-code: "邀請碼"
-  username: "用戶名"
-  available: "可用"
-  too-long: "請不要超過20個字元"
-  password: "密碼"
-  password-placeholder: "建議至少8個字元"
-common/views/components/stream-indicator.vue:
-  connecting: "正在連線"
-  reconnecting: "正在重新連線"
-  connected: "已建立連線"
-common/views/components/integration-settings.vue:
-  disconnect: "中斷連線"
-common/views/components/github-setting.vue:
-  reconnect: "重新連線"
-  disconnect: "中斷連線"
-common/views/components/discord-setting.vue:
-  reconnect: "重新連線"
-  disconnect: "中斷連線"
-common/views/components/language-settings.vue:
-  recommended: "推薦"
-  auto: "自動"
-  specify-language: "指定語言"
-common/views/components/profile-editor.vue:
-  title: "個人資料"
-  name: "名稱"
-  birthday: "生日:"
-  privacy: "隱私"
-admin/views/dashboard.vue:
-  drive: "雲端硬碟"
-admin/views/charts.vue:
-  drive: "雲端硬碟"
-pages:
-  like: "è´Š"
-room:
-  furnitures:
-    moon: "月"
diff --git a/migration/1579267006611-v12.ts b/migration/1579267006611-v12.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2c15283fa4d547730924e7dd02a2906747cd714b
--- /dev/null
+++ b/migration/1579267006611-v12.ts
@@ -0,0 +1,34 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class v121579267006611 implements MigrationInterface {
+    name = 'v121579267006611'
+
+    public async up(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`CREATE TABLE "announcement" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "text" character varying(8192) NOT NULL, "title" character varying(256) NOT NULL, "imageUrl" character varying(1024), CONSTRAINT "PK_e0ef0550174fd1099a308fd18a0" PRIMARY KEY ("id"))`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_118ec703e596086fc4515acb39" ON "announcement" ("createdAt") `, undefined);
+        await queryRunner.query(`CREATE TABLE "announcement_read" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "announcementId" character varying(32) NOT NULL, CONSTRAINT "PK_4b90ad1f42681d97b2683890c5e" PRIMARY KEY ("id"))`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_8288151386172b8109f7239ab2" ON "announcement_read" ("userId") `, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_603a7b1e7aa0533c6c88e9bfaf" ON "announcement_read" ("announcementId") `, undefined);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_924fa71815cfa3941d003702a0" ON "announcement_read" ("userId", "announcementId") `, undefined);
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isVerified"`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "announcements"`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableEmojiReaction"`, undefined);
+        await queryRunner.query(`ALTER TABLE "announcement_read" ADD CONSTRAINT "FK_8288151386172b8109f7239ab28" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "announcement_read" ADD CONSTRAINT "FK_603a7b1e7aa0533c6c88e9bfafe" FOREIGN KEY ("announcementId") REFERENCES "announcement"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "announcement_read" DROP CONSTRAINT "FK_603a7b1e7aa0533c6c88e9bfafe"`, undefined);
+        await queryRunner.query(`ALTER TABLE "announcement_read" DROP CONSTRAINT "FK_8288151386172b8109f7239ab28"`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "enableEmojiReaction" boolean NOT NULL DEFAULT true`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "announcements" jsonb NOT NULL DEFAULT '[]'`, undefined);
+        await queryRunner.query(`ALTER TABLE "user" ADD "isVerified" boolean NOT NULL DEFAULT false`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_924fa71815cfa3941d003702a0"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_603a7b1e7aa0533c6c88e9bfaf"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_8288151386172b8109f7239ab2"`, undefined);
+        await queryRunner.query(`DROP TABLE "announcement_read"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_118ec703e596086fc4515acb39"`, undefined);
+        await queryRunner.query(`DROP TABLE "announcement"`, undefined);
+    }
+
+}
diff --git a/migration/1579270193251-v12-2.ts b/migration/1579270193251-v12-2.ts
new file mode 100644
index 0000000000000000000000000000000000000000..efad0cd56075fb54907a4ea121dbd79315424adb
--- /dev/null
+++ b/migration/1579270193251-v12-2.ts
@@ -0,0 +1,14 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class v1221579270193251 implements MigrationInterface {
+    name = 'v1221579270193251'
+
+    public async up(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "announcement_read" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`, undefined);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "announcement_read" DROP COLUMN "createdAt"`, undefined);
+    }
+
+}
diff --git a/migration/1579282808087-v12-3.ts b/migration/1579282808087-v12-3.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a330caa978f01ed83ee2965fa05a69a03e60c4f8
--- /dev/null
+++ b/migration/1579282808087-v12-3.ts
@@ -0,0 +1,14 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class v1231579282808087 implements MigrationInterface {
+    name = 'v1231579282808087'
+
+    public async up(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "announcement" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`, undefined);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "updatedAt"`, undefined);
+    }
+
+}
diff --git a/migration/1579544426412-v12-4.ts b/migration/1579544426412-v12-4.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d35b25d045e1dd393f5ee5dc7026517d52df339b
--- /dev/null
+++ b/migration/1579544426412-v12-4.ts
@@ -0,0 +1,16 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class v1241579544426412 implements MigrationInterface {
+    name = 'v1241579544426412'
+
+    public async up(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "notification" ADD "followRequestId" character varying(32)`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_bd7fab507621e635b32cd31892c" FOREIGN KEY ("followRequestId") REFERENCES "follow_request"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_bd7fab507621e635b32cd31892c"`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "followRequestId"`, undefined);
+    }
+
+}
diff --git a/migration/1579977526288-v12-5.ts b/migration/1579977526288-v12-5.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5f824a676e1f6e2733ba99b65fa6d7f0f525375a
--- /dev/null
+++ b/migration/1579977526288-v12-5.ts
@@ -0,0 +1,54 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class v1251579977526288 implements MigrationInterface {
+    name = 'v1251579977526288'
+
+    public async up(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`CREATE TABLE "clip" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "isPublic" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_f0685dac8d4dd056d7255670b75" PRIMARY KEY ("id"))`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_2b5ec6c574d6802c94c80313fb" ON "clip" ("userId") `, undefined);
+        await queryRunner.query(`CREATE TABLE "clip_note" ("id" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "clipId" character varying(32) NOT NULL, CONSTRAINT "PK_e94cda2f40a99b57e032a1a738b" PRIMARY KEY ("id"))`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_a012eaf5c87c65da1deb5fdbfa" ON "clip_note" ("noteId") `, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_ebe99317bbbe9968a0c6f579ad" ON "clip_note" ("clipId") `, undefined);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_6fc0ec357d55a18646262fdfff" ON "clip_note" ("noteId", "clipId") `, undefined);
+        await queryRunner.query(`CREATE TYPE "antenna_src_enum" AS ENUM('home', 'all', 'list')`, undefined);
+        await queryRunner.query(`CREATE TABLE "antenna" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "src" "antenna_src_enum" NOT NULL, "userListId" character varying(32), "keywords" jsonb NOT NULL DEFAULT '[]', "withFile" boolean NOT NULL, "expression" character varying(2048), "notify" boolean NOT NULL, "hasNewNote" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_c170b99775e1dccca947c9f2d5f" PRIMARY KEY ("id"))`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_6446c571a0e8d0f05f01c78909" ON "antenna" ("userId") `, undefined);
+        await queryRunner.query(`CREATE TABLE "antenna_note" ("id" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "antennaId" character varying(32) NOT NULL, CONSTRAINT "PK_fb28d94d0989a3872df19fd6ef8" PRIMARY KEY ("id"))`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_bd0397be22147e17210940e125" ON "antenna_note" ("noteId") `, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_0d775946662d2575dfd2068a5f" ON "antenna_note" ("antennaId") `, undefined);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_335a0bf3f904406f9ef3dd51c2" ON "antenna_note" ("noteId", "antennaId") `, undefined);
+        await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "geo"`, undefined);
+        await queryRunner.query(`ALTER TABLE "clip" ADD CONSTRAINT "FK_2b5ec6c574d6802c94c80313fb2" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "clip_note" ADD CONSTRAINT "FK_a012eaf5c87c65da1deb5fdbfa3" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "clip_note" ADD CONSTRAINT "FK_ebe99317bbbe9968a0c6f579adf" FOREIGN KEY ("clipId") REFERENCES "clip"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" ADD CONSTRAINT "FK_6446c571a0e8d0f05f01c789096" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" ADD CONSTRAINT "FK_709d7d32053d0dd7620f678eeb9" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna_note" ADD CONSTRAINT "FK_bd0397be22147e17210940e125b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna_note" ADD CONSTRAINT "FK_0d775946662d2575dfd2068a5f5" FOREIGN KEY ("antennaId") REFERENCES "antenna"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "antenna_note" DROP CONSTRAINT "FK_0d775946662d2575dfd2068a5f5"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna_note" DROP CONSTRAINT "FK_bd0397be22147e17210940e125b"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" DROP CONSTRAINT "FK_709d7d32053d0dd7620f678eeb9"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" DROP CONSTRAINT "FK_6446c571a0e8d0f05f01c789096"`, undefined);
+        await queryRunner.query(`ALTER TABLE "clip_note" DROP CONSTRAINT "FK_ebe99317bbbe9968a0c6f579adf"`, undefined);
+        await queryRunner.query(`ALTER TABLE "clip_note" DROP CONSTRAINT "FK_a012eaf5c87c65da1deb5fdbfa3"`, undefined);
+        await queryRunner.query(`ALTER TABLE "clip" DROP CONSTRAINT "FK_2b5ec6c574d6802c94c80313fb2"`, undefined);
+        await queryRunner.query(`ALTER TABLE "note" ADD "geo" jsonb`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_335a0bf3f904406f9ef3dd51c2"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_0d775946662d2575dfd2068a5f"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_bd0397be22147e17210940e125"`, undefined);
+        await queryRunner.query(`DROP TABLE "antenna_note"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_6446c571a0e8d0f05f01c78909"`, undefined);
+        await queryRunner.query(`DROP TABLE "antenna"`, undefined);
+        await queryRunner.query(`DROP TYPE "antenna_src_enum"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_6fc0ec357d55a18646262fdfff"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_ebe99317bbbe9968a0c6f579ad"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_a012eaf5c87c65da1deb5fdbfa"`, undefined);
+        await queryRunner.query(`DROP TABLE "clip_note"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_2b5ec6c574d6802c94c80313fb"`, undefined);
+        await queryRunner.query(`DROP TABLE "clip"`, undefined);
+    }
+
+}
diff --git a/migration/1579993013959-v12-6.ts b/migration/1579993013959-v12-6.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4fa4623c3e6f307a17a20dd472cb854b1d860c44
--- /dev/null
+++ b/migration/1579993013959-v12-6.ts
@@ -0,0 +1,18 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class v1261579993013959 implements MigrationInterface {
+    name = 'v1261579993013959'
+
+    public async up(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "hasNewNote"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna_note" ADD "read" boolean NOT NULL DEFAULT false`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_9937ea48d7ae97ffb4f3f063a4" ON "antenna_note" ("read") `, undefined);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`DROP INDEX "IDX_9937ea48d7ae97ffb4f3f063a4"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna_note" DROP COLUMN "read"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" ADD "hasNewNote" boolean NOT NULL DEFAULT false`, undefined);
+    }
+
+}
diff --git a/migration/1580069531114-v12-7.ts b/migration/1580069531114-v12-7.ts
new file mode 100644
index 0000000000000000000000000000000000000000..227e7cceb6d99d54613dba77d334adfebbec76f7
--- /dev/null
+++ b/migration/1580069531114-v12-7.ts
@@ -0,0 +1,24 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class v1271580069531114 implements MigrationInterface {
+    name = 'v1271580069531114'
+
+    public async up(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "antenna" ADD "users" character varying(1024) array NOT NULL DEFAULT '{}'::varchar[]`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" ADD "caseSensitive" boolean NOT NULL DEFAULT false`, undefined);
+        await queryRunner.query(`ALTER TYPE "public"."antenna_src_enum" RENAME TO "antenna_src_enum_old"`, undefined);
+        await queryRunner.query(`CREATE TYPE "antenna_src_enum" AS ENUM('home', 'all', 'users', 'list')`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum" USING "src"::"text"::"antenna_src_enum"`, undefined);
+        await queryRunner.query(`DROP TYPE "antenna_src_enum_old"`, undefined);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`CREATE TYPE "antenna_src_enum_old" AS ENUM('home', 'all', 'list')`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum_old" USING "src"::"text"::"antenna_src_enum_old"`, undefined);
+        await queryRunner.query(`DROP TYPE "antenna_src_enum"`, undefined);
+        await queryRunner.query(`ALTER TYPE "antenna_src_enum_old" RENAME TO  "antenna_src_enum"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "caseSensitive"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "users"`, undefined);
+    }
+
+}
diff --git a/migration/1580148575182-v12-8.ts b/migration/1580148575182-v12-8.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c63bdb4eb4f36a6507df8b6837ac95953b8ef224
--- /dev/null
+++ b/migration/1580148575182-v12-8.ts
@@ -0,0 +1,16 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class v1281580148575182 implements MigrationInterface {
+    name = 'v1281580148575182'
+
+    public async up(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_ec5c201576192ba8904c345c5cc"`, undefined);
+        await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "appId"`, undefined);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "note" ADD "appId" character varying(32)`, undefined);
+        await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_ec5c201576192ba8904c345c5cc" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, undefined);
+    }
+
+}
diff --git a/migration/1580154400017-v12-9.ts b/migration/1580154400017-v12-9.ts
new file mode 100644
index 0000000000000000000000000000000000000000..de06d26e49e9a7fd7e71f6f143f25f9ea4c980f3
--- /dev/null
+++ b/migration/1580154400017-v12-9.ts
@@ -0,0 +1,14 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class v1291580154400017 implements MigrationInterface {
+    name = 'v1291580154400017'
+
+    public async up(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "antenna" ADD "withReplies" boolean NOT NULL DEFAULT false`, undefined);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "withReplies"`, undefined);
+    }
+
+}
diff --git a/migration/1580276619901-v12-10.ts b/migration/1580276619901-v12-10.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f48f42b4ac258d8c3c29847d3c42b1909c5ad074
--- /dev/null
+++ b/migration/1580276619901-v12-10.ts
@@ -0,0 +1,19 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class v12101580276619901 implements MigrationInterface {
+    name = 'v12101580276619901'
+
+    public async up(queryRunner: QueryRunner): Promise<any> {
+				await queryRunner.query(`TRUNCATE TABLE "notification"`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "type"`, undefined);
+        await queryRunner.query(`CREATE TYPE "notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted')`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ADD "type" "notification_type_enum" NOT NULL`, undefined);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "type"`, undefined);
+        await queryRunner.query(`DROP TYPE "notification_type_enum"`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ADD "type" character varying(32) NOT NULL`, undefined);
+    }
+
+}
diff --git a/package.json b/package.json
index d5b85fc8ca7e0bed9970e3a4c71eb68022a539a2..3a6d1c81de4703ff97af69ea5f51c71e69e8eaeb 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
 	"name": "misskey",
-	"author": "syuilo <i@syuilo.com>",
-	"version": "11.37.1",
-	"codename": "daybreak",
+	"author": "syuilo <syuilotan@yahoo.co.jp>",
+	"version": "12.0.0-alpha.10",
+	"codename": "indigo",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/syuilo/misskey.git"
@@ -112,6 +112,7 @@
 		"cbor": "5.0.1",
 		"chai": "4.2.0",
 		"chalk": "3.0.0",
+		"chart.js": "2.9.3",
 		"cli-highlight": "2.1.4",
 		"commander": "4.1.0",
 		"content-disposition": "0.5.3",
@@ -125,15 +126,16 @@
 		"eslint-plugin-vue": "6.1.2",
 		"eventemitter3": "4.0.0",
 		"feed": "4.1.0",
+		"fibers": "4.0.2",
 		"file-type": "13.0.1",
 		"fluent-ffmpeg": "2.1.2",
 		"gulp": "4.0.2",
 		"gulp-clean-css": "4.2.0",
+		"gulp-dart-sass": "0.9.1",
 		"gulp-mocha": "7.0.2",
 		"gulp-rename": "2.0.0",
 		"gulp-replace": "1.0.0",
 		"gulp-sourcemaps": "2.6.5",
-		"gulp-stylus": "2.7.0",
 		"gulp-terser": "1.2.0",
 		"gulp-tslint": "8.1.4",
 		"gulp-typescript": "5.0.1",
@@ -177,6 +179,7 @@
 		"parse5": "5.1.1",
 		"parsimmon": "1.13.0",
 		"pg": "7.17.0",
+		"portal-vue": "2.1.7",
 		"portscanner": "2.2.0",
 		"postcss-loader": "3.0.0",
 		"prismjs": "1.18.0",
@@ -204,6 +207,8 @@
 		"rimraf": "3.0.0",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.2",
+		"sass": "1.25.0",
+		"sass-loader": "8.0.1",
 		"seedrandom": "3.0.5",
 		"sharp": "0.23.4",
 		"showdown": "1.9.1",
@@ -211,8 +216,6 @@
 		"speakeasy": "2.0.0",
 		"stringz": "2.0.0",
 		"style-loader": "1.1.2",
-		"stylus": "0.54.7",
-		"stylus-loader": "3.0.2",
 		"summaly": "2.3.1",
 		"syslog-pro": "1.0.0",
 		"systeminformation": "4.17.3",
@@ -238,10 +241,10 @@
 		"vue-content-loading": "1.6.0",
 		"vue-cropperjs": "4.0.1",
 		"vue-i18n": "8.15.3",
-		"vue-js-modal": "1.3.31",
 		"vue-json-pretty": "1.6.3",
 		"vue-loader": "15.8.3",
 		"vue-marquee-text-component": "1.1.1",
+		"vue-meta": "2.3.1",
 		"vue-prism-component": "1.1.1",
 		"vue-router": "3.1.3",
 		"vue-sequential-entrance": "1.1.3",
@@ -249,7 +252,6 @@
 		"vue-svg-inline-loader": "1.4.4",
 		"vue-template-compiler": "2.6.11",
 		"vuedraggable": "2.23.2",
-		"vuewordcloud": "18.7.11",
 		"vuex": "3.1.2",
 		"vuex-persistedstate": "2.7.0",
 		"web-push": "3.4.3",
diff --git a/src/@types/const.json.d.ts b/src/@types/const.json.d.ts
deleted file mode 100644
index 40a96f2a2af7008c2b5a4b3d33a4d963bce4ce0f..0000000000000000000000000000000000000000
--- a/src/@types/const.json.d.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-declare module '*/const.json' {
-	const copyright: string;
-}
diff --git a/src/boot/master.ts b/src/boot/master.ts
index db063ef4b08931955b011c0028a32942c9c67726..d6d5ed438f2a81de2e090fdfd13b130779dc8097 100644
--- a/src/boot/master.ts
+++ b/src/boot/master.ts
@@ -77,7 +77,6 @@ export async function masterMain() {
 
 	if (!program.noDaemons) {
 		require('../daemons/server-stats').default();
-		require('../daemons/notes-stats').default();
 		require('../daemons/queue-stats').default();
 		require('../daemons/janitor').default();
 	}
diff --git a/src/client/app.vue b/src/client/app.vue
new file mode 100644
index 0000000000000000000000000000000000000000..3e65880b0a37b30154f24a4509b54a821962f750
--- /dev/null
+++ b/src/client/app.vue
@@ -0,0 +1,1105 @@
+<template>
+<div class="mk-app" v-hotkey.global="keymap">
+	<header class="header">
+		<div class="title" ref="title">
+			<transition name="header" mode="out-in" appear>
+				<button class="_button back" v-if="canBack" @click="back()"><fa :icon="faChevronLeft"/></button>
+			</transition>
+			<transition name="header" mode="out-in" appear>
+				<div class="body" :key="pageKey">
+					<div class="default">
+						<portal-target name="avatar" slim/>
+						<h1 class="title"><portal-target name="icon" slim/><portal-target name="title" slim/></h1>
+					</div>
+					<div class="custom">
+						<portal-target name="header" slim/>
+					</div>
+				</div>
+			</transition>
+		</div>
+		<div class="sub">
+			<fa :icon="faSearch"/>
+			<input type="search" class="search" :placeholder="$t('search')" v-model="searchQuery" v-autocomplete="{ model: 'searchQuery' }" :disabled="searchWait" @keypress="searchKeypress"/>
+			<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
+		</div>
+	</header>
+
+	<nav class="nav" ref="nav">
+		<div>
+			<button class="item _button account" @click="openAccountMenu" v-if="$store.getters.isSignedIn">
+				<mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/>
+			</button>
+			<router-link class="item" active-class="active" to="/" exact v-if="$store.getters.isSignedIn">
+				<fa :icon="faHome" fixed-width/><span class="text">{{ $t('timeline') }}</span>
+			</router-link>
+			<router-link class="item" active-class="active" to="/" exact v-else>
+				<fa :icon="faHome" fixed-width/><span class="text">{{ $t('home') }}</span>
+			</router-link>
+			<router-link class="item" active-class="active" to="/featured">
+				<fa :icon="faFireAlt" fixed-width/><span class="text">{{ $t('featured') }}</span>
+			</router-link>
+			<router-link class="item" active-class="active" to="/explore">
+				<fa :icon="faHashtag" fixed-width/><span class="text">{{ $t('explore') }}</span>
+			</router-link>
+			<button class="item _button" @click="notificationsOpen = !notificationsOpen" ref="notificationButton" v-if="$store.getters.isSignedIn">
+				<fa :icon="faBell" fixed-width/><span class="text">{{ $t('notifications') }}</span>
+				<i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i>
+			</button>
+			<router-link class="item" active-class="active" to="/my/messaging" v-if="$store.getters.isSignedIn">
+				<fa :icon="faComments" fixed-width/><span class="text">{{ $t('messaging') }}</span>
+				<i v-if="$store.state.i.hasUnreadMessagingMessage"><fa :icon="faCircle"/></i>
+			</router-link>
+			<router-link class="item" active-class="active" to="/my/follow-requests" v-if="$store.getters.isSignedIn && $store.state.i.isLocked">
+				<fa :icon="faUserClock" fixed-width/><span class="text">{{ $t('followRequests') }}</span>
+				<i v-if="$store.state.i.pendingReceivedFollowRequestsCount"><fa :icon="faCircle"/></i>
+			</router-link>
+			<router-link class="item" active-class="active" to="/my/drive" v-if="$store.getters.isSignedIn">
+				<fa :icon="faCloud" fixed-width/><span class="text">{{ $t('drive') }}</span>
+			</router-link>
+			<router-link class="item" active-class="active" to="/announcements">
+				<fa :icon="faBroadcastTower" fixed-width/><span class="text">{{ $t('announcements') }}</span>
+				<i v-if="$store.getters.isSignedIn && $store.state.i.hasUnreadAnnouncement"><fa :icon="faCircle"/></i>
+			</router-link>
+			<button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)" @click="oepnInstanceMenu">
+				<fa :icon="faServer" fixed-width/><span class="text">{{ $t('instance') }}</span>
+			</button>
+			<button class="item _button" @click="search()">
+				<fa :icon="faSearch" fixed-width/><span class="text">{{ $t('search') }}</span>
+			</button>
+			<button class="item _button" @click="more">
+				<fa :icon="faEllipsisH" fixed-width/><span class="text">{{ $t('more') }}</span>
+				<i v-if="$store.getters.isSignedIn && ($store.state.i.hasUnreadMentions || $store.state.i.hasUnreadSpecifiedNotes)"><fa :icon="faCircle"/></i>
+			</button>
+		</div>
+	</nav>
+
+	<div class="contents">
+		<main ref="main">
+			<div class="content">
+				<transition name="page" mode="out-in">
+					<router-view></router-view>
+				</transition>
+			</div>
+			<div class="powerd-by" :class="{ visible: !$store.getters.isSignedIn }">
+				<b><router-link to="/">{{ host }}</router-link></b>
+				<small>Powered by <a href="https://github.com/syuilo/misskey" target="_blank">Misskey</a></small>
+			</div>
+		</main>
+
+		<div class="widgets">
+			<div ref="widgets" :class="{ edit: widgetsEditMode }">
+				<template v-if="enableWidgets && $store.getters.isSignedIn">
+					<template v-if="widgetsEditMode">
+						<mk-button primary @click="addWidget" class="add"><fa :icon="faPlus"/></mk-button>
+						<x-draggable
+							:list="widgets"
+							handle=".handle"
+							animation="150"
+							class="sortable"
+							@sort="onWidgetSort"
+						>
+							<div v-for="widget in widgets" class="customize-container" :key="widget.id">
+								<header>
+									<span class="handle"><fa :icon="faBars"/></span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)"><fa :icon="faTimes"/></button>
+								</header>
+								<div @click="widgetFunc(widget.id)">
+									<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true"/>
+								</div>
+							</div>
+						</x-draggable>
+					</template>
+					<template v-else>
+						<component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget"/>
+					</template>
+					<button ref="widgetsEditButton" v-if="widgetsEditMode" class="_button edit" @click="widgetsEditMode = false">{{ $t('exitEdit') }}</button>
+					<button ref="widgetsEditButton" v-else class="_button edit" @click="widgetsEditMode = true">{{ $t('editWidgets') }}</button>
+				</template>
+			</div>
+		</div>
+	</div>
+
+	<div class="buttons">
+		<button v-if="$store.getters.isSignedIn" class="button nav _button" @click="showNav" ref="navButton"><fa :icon="faBars"/><i v-if="$store.state.i.hasUnreadSpecifiedNotes || $store.state.i.pendingReceivedFollowRequestsCount || $store.state.i.hasUnreadMessagingMessage || $store.state.i.hasUnreadAnnouncement"><fa :icon="faCircle"/></i></button>
+		<button v-if="$store.getters.isSignedIn" class="button home _button" :disabled="$route.path === '/'" @click="$router.push('/')"><fa :icon="faHome"/></button>
+		<button v-if="$store.getters.isSignedIn" class="button notifications _button" @click="notificationsOpen = !notificationsOpen" ref="notificationButton2"><fa :icon="notificationsOpen ? faTimes : faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button>
+		<button v-if="$store.getters.isSignedIn" class="button post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
+	</div>
+
+	<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
+
+	<transition name="zoom-in-top">
+		<x-notifications v-if="notificationsOpen" class="notifications" ref="notifications"/>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faGamepad, faServer, faFileAlt, faSatellite, faInfoCircle } from '@fortawesome/free-solid-svg-icons';
+import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons';
+import { v4 as uuid } from 'uuid';
+import i18n from './i18n';
+import { host } from './config';
+import { search } from './scripts/search';
+import contains from './scripts/contains';
+import MkToast from './components/toast.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XNotifications: () => import('./components/notifications.vue').then(m => m.default),
+		MkButton: () => import('./components/ui/button.vue').then(m => m.default),
+		XDraggable: () => import('vuedraggable'),
+	},
+
+	data() {
+		return {
+			host: host,
+			pageKey: 0,
+			searching: false,
+			notificationsOpen: false,
+			accounts: [],
+			lists: [],
+			connection: null,
+			searchQuery: '',
+			searchWait: false,
+			widgetsEditMode: false,
+			enableWidgets: window.innerWidth >= 1100,
+			canBack: false,
+			faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer
+		};
+	},
+
+	computed: {
+		keymap(): any {
+			return {
+				'p': this.post,
+				'n': this.post,
+			};
+		},
+
+		widgets(): any[] {
+			return this.$store.state.settings.widgets;
+		}
+	},
+
+	watch:{
+		$route(to, from) {
+			this.pageKey++;
+			this.notificationsOpen = false;
+			this.canBack = (window.history.length > 0 && !['index'].includes(to.name));
+		},
+
+		notificationsOpen(open) {
+			if (open) {
+				for (const el of Array.from(document.querySelectorAll('*'))) {
+					el.addEventListener('mousedown', this.onMousedown);
+				}
+			} else {
+				for (const el of Array.from(document.querySelectorAll('*'))) {
+					el.removeEventListener('mousedown', this.onMousedown);
+				}
+			}
+		}
+	},
+
+	created() {
+		if (this.$store.getters.isSignedIn) {
+			this.connection = this.$root.stream.useSharedConnection('main');
+			this.connection.on('notification', this.onNotification);
+
+			if (this.widgets.length === 0) {
+				this.$store.dispatch('settings/setWidgets', [{
+					name: 'notifications',
+					id: 'a', data: {}
+				}]);
+			}
+		}
+
+		this.$root.stream.on('_disconnected_', async () => {
+			const confirm = await this.$root.dialog({
+				type: 'warning',
+				showCancelButton: true,
+				title: this.$t('disconnectedFromServer'),
+				text: this.$t('reloadConfirm'),
+			});
+			if (!confirm.canceled) {
+				location.reload();
+			}
+		});
+
+		setInterval(() => {
+			this.$refs.title.style.left = (this.$refs.main.getBoundingClientRect().left - this.$refs.nav.offsetWidth) + 'px';
+		}, 1000);
+
+		// https://stackoverflow.com/questions/33891709/when-flexbox-items-wrap-in-column-mode-container-does-not-grow-its-width
+		if (this.enableWidgets) {
+			setInterval(() => {
+				const width = this.$refs.widgetsEditButton.offsetLeft + 300;
+				this.$refs.widgets.style.width = width + 'px';
+			}, 1000);
+		}
+	},
+
+	methods: {
+		back() {
+			window.history.back();
+		},
+
+		post() {
+			this.$root.post();
+		},
+
+		search() {
+			if (this.searching) return;
+
+			this.$root.dialog({
+				title: this.$t('search'),
+				input: true
+			}).then(async ({ canceled, result: query }) => {
+				if (canceled || query == null || query == '') return;
+
+				this.searching = true;
+				search(this, query).finally(() => {
+					this.searching = false;
+				});
+			});
+		},
+
+		searchKeypress(e) {
+			if (e.keyCode == 13) {
+				this.searchWait = true;
+				search(this, this.searchQuery).finally(() => {
+					this.searchWait = false;
+					this.searchQuery = '';
+				});
+			}
+		},
+
+		showNav(ev) {
+			this.$root.menu({
+				items: [{
+					text: this.$t('search'),
+					icon: faSearch,
+					action: this.search,
+				}, null, this.$store.state.i.isAdmin || this.$store.state.i.isModerator ? {
+					text: this.$t('instance'),
+					icon: faServer,
+					action: () => this.oepnInstanceMenu(ev),
+				} : undefined, {
+					type: 'link',
+					text: this.$t('announcements'),
+					to: '/announcements',
+					icon: faBroadcastTower,
+					indicate: this.$store.state.i.hasUnreadAnnouncement,
+				}, {
+					type: 'link',
+					text: this.$t('featured'),
+					to: '/featured',
+					icon: faFireAlt,
+				}, {
+					type: 'link',
+					text: this.$t('explore'),
+					to: '/explore',
+					icon: faHashtag,
+				}, {
+					type: 'link',
+					text: this.$t('messaging'),
+					to: '/my/messaging',
+					icon: faComments,
+					indicate: this.$store.state.i.hasUnreadMessagingMessage,
+				}, this.$store.state.i.isLocked ? {
+					type: 'link',
+					text: this.$t('followRequests'),
+					to: '/my/follow-requests',
+					icon: faUserClock,
+					indicate: this.$store.state.i.pendingReceivedFollowRequestsCount > 0,
+				} : undefined, {
+					type: 'link',
+					text: this.$t('drive'),
+					to: '/my/drive',
+					icon: faCloud,
+				}, {
+					text: this.$t('more'),
+					icon: faEllipsisH,
+					action: () => this.more(ev),
+					indicate: this.$store.state.i.hasUnreadMentions || this.$store.state.i.hasUnreadSpecifiedNotes
+				}, null, {
+					type: 'user',
+					user: this.$store.state.i,
+					action: () => this.openAccountMenu(ev),
+				}],
+				direction: 'up',
+				align: 'left',
+				fixed: true,
+				width: 200,
+				source: ev.currentTarget || ev.target,
+			});
+		},
+
+		async openAccountMenu(ev) {
+			const accounts = (await this.$root.api('users/show', { userIds: this.$store.state.device.accounts.map(x => x.id) })).filter(x => x.id !== this.$store.state.i.id);
+
+			const accountItems = accounts.map(account => ({
+				type: 'user',
+				user: account,
+				action: () => { this.switchAccount(account) }
+			}));
+
+			this.$root.menu({
+				items: [...[{
+					type: 'link',
+					text: this.$t('profile'),
+					to: `/@${ this.$store.state.i.username }`,
+					avatar: this.$store.state.i,
+				}, {
+					type: 'link',
+					text: this.$t('settings'),
+					to: '/my/settings',
+					icon: faCog,
+				}, null, {
+					type: 'item',
+					text: this.$t('addAcount'),
+					icon: faPlus,
+					action: () => { this.addAcount() },
+				}], ...accountItems],
+				align: 'left',
+				fixed: true,
+				width: 240,
+				source: ev.currentTarget || ev.target,
+			});
+		},
+
+		oepnInstanceMenu(ev) {
+			this.$root.menu({
+				items: [{
+					type: 'link',
+					text: this.$t('statistics'),
+					to: '/instance/stats',
+					icon: faChartBar,
+				}, {
+					type: 'link',
+					text: this.$t('customEmojis'),
+					to: '/instance/emojis',
+					icon: faLaugh,
+				}, {
+					type: 'link',
+					text: this.$t('users'),
+					to: '/instance/users',
+					icon: faUsers,
+				}, {
+					type: 'link',
+					text: this.$t('files'),
+					to: '/instance/files',
+					icon: faCloud,
+				}, {
+					type: 'link',
+					text: this.$t('monitor'),
+					to: '/instance/monitor',
+					icon: faTachometerAlt,
+				}, {
+					type: 'link',
+					text: this.$t('jobQueue'),
+					to: '/instance/queue',
+					icon: faExchangeAlt,
+				}, {
+					type: 'link',
+					text: this.$t('federation'),
+					to: '/instance/federation',
+					icon: faGlobe,
+				}, {
+					type: 'link',
+					text: this.$t('announcements'),
+					to: '/instance/announcements',
+					icon: faBroadcastTower,
+				}, null, {
+					type: 'link',
+					text: this.$t('general'),
+					to: '/instance',
+					icon: faCog,
+				}],
+				align: 'left',
+				fixed: true,
+				width: 200,
+				source: ev.currentTarget || ev.target,
+			});
+		},
+
+		more(ev) {
+			this.$root.menu({
+				items: [...(this.$store.getters.isSignedIn ? [{
+					type: 'link',
+					text: this.$t('lists'),
+					to: '/my/lists',
+					icon: faListUl,
+				}, {
+					type: 'link',
+					text: this.$t('antennas'),
+					to: '/my/antennas',
+					icon: faSatellite,
+				}, {
+					type: 'link',
+					text: this.$t('mentions'),
+					to: '/my/mentions',
+					icon: faAt,
+					indicate: this.$store.state.i.hasUnreadMentions
+				}, {
+					type: 'link',
+					text: this.$t('directNotes'),
+					to: '/my/messages',
+					icon: faEnvelope,
+					indicate: this.$store.state.i.hasUnreadSpecifiedNotes
+				}, {
+					type: 'link',
+					text: this.$t('favorites'),
+					to: '/my/favorites',
+					icon: faStar,
+				}, {
+					type: 'link',
+					text: this.$t('pages'),
+					to: '/my/pages',
+					icon: faFileAlt,
+				}, {
+					type: 'link',
+					text: this.$t('games'),
+					to: '/games',
+					icon: faGamepad,
+				}, null] : []), {
+					type: 'link',
+					text: this.$t('about'),
+					to: '/about',
+					icon: faInfoCircle,
+				}],
+				align: 'left',
+				fixed: true,
+				width: 200,
+				source: ev.currentTarget || ev.target,
+			});
+		},
+
+		async addAcount() {
+			this.$root.new(await import('./components/signin-dialog.vue').then(m => m.default)).$once('login', res => {
+				this.$store.dispatch('addAcount', res);
+				this.$root.dialog({
+					type: 'success',
+					iconOnly: true, autoClose: true
+				});
+			});
+		},
+
+		async switchAccount(account) {
+			const token = this.$store.state.device.accounts.find(x => x.id === account.id).token;
+			this.$root.api('i', {}, token).then(i => {
+				this.$store.dispatch('switchAccount', {
+					...i,
+					token: token
+				});
+				location.reload();
+			});
+		},
+
+		onNotification(notification) {
+			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
+			this.$root.stream.send('readNotification', {
+				id: notification.id
+			});
+
+			this.$root.new(MkToast, {
+				notification
+			});
+		},
+
+		onMousedown(e) {
+			e.preventDefault();
+			if (!contains(this.$refs.notifications.$el, e.target) &&
+				!contains(this.$refs.notificationButton, e.target) &&
+				!contains(this.$refs.notificationButton2, e.target)
+				) this.notificationsOpen = false;
+			return false;
+		},
+
+		widgetFunc(id) {
+			const w = this.$refs[id][0];
+			if (w.func) w.func();
+		},
+
+		onWidgetSort() {
+			this.saveHome();
+		},
+
+		addWidget(ev) {
+			const widgets = [
+				'memo',
+				'notifications',
+				'timeline',
+				'calendar',
+				'rss',
+				'trends',
+			];
+
+			this.$root.menu({
+				items: widgets.map(widget => ({
+					text: this.$t('_widgets.' + widget),
+					action: () => {
+						this.$store.dispatch('settings/addWidget', {
+							name: widget,
+							id: uuid(),
+							data: {}
+						});
+					}
+				})),
+				source: ev.currentTarget || ev.target,
+			});
+		},
+
+		removeWidget(widget) {
+			this.$store.dispatch('settings/removeWidget', widget);
+		},
+
+		saveHome() {
+			this.$store.dispatch('settings/setWidgets', this.widgets);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+@keyframes blink {
+	0% { opacity: 1; }
+	30% { opacity: 1; }
+	90% { opacity: 0; }
+}
+
+.header-enter-active, .header-leave-active {
+	transition: opacity 0.5s, transform 0.5s !important;
+}
+.header-enter {
+	opacity: 0;
+	transform: scale(0.9);
+}
+.header-leave-to {
+	opacity: 0;
+	transform: scale(0.9);
+}
+
+.page-enter-active, .page-leave-active {
+	transition: opacity 0.5s, transform 0.5s !important;
+}
+.page-enter {
+	opacity: 0;
+	transform: translateY(-32px);
+}
+.page-leave-to {
+	opacity: 0;
+	transform: translateY(32px);
+}
+
+.mk-app {
+	$header-height: 60px;
+	$nav-width: 250px;
+	$nav-icon-only-width: 74px;
+	$main-width: 700px;
+	$ui-font-size: 1em;
+	$nav-icon-only-threshold: 1300px;
+	$nav-hide-threshold: 700px;
+	$side-hide-threshold: 1100px;
+
+	min-height: 100vh;
+	box-sizing: border-box;
+	padding-top: $header-height;
+
+	&, > .header > .body {
+		display: flex;
+		margin: 0 auto;
+	}
+
+	> .header {
+		position: fixed;
+		z-index: 1000;
+		top: 0;
+		right: 0;
+		height: $header-height;
+		width: calc(100% - #{$nav-width});
+		//background-color: var(--panel);
+		-webkit-backdrop-filter: blur(32px);
+		backdrop-filter: blur(32px);
+		background-color: var(--header);
+		border-bottom: solid 1px var(--divider);
+
+		@media (max-width: $nav-icon-only-threshold) {
+			width: calc(100% - #{$nav-icon-only-width});
+		}
+
+		@media (max-width: $nav-hide-threshold) {
+			width: 100%;
+		}
+
+		> .title {
+			position: relative;
+			line-height: $header-height;
+			height: $header-height;
+			max-width: $main-width;
+			text-align: center;
+
+			> .back {
+				position: absolute;
+				z-index: 1;
+				top: 0;
+				left: 0;
+				height: $header-height;
+				width: $header-height;
+			}
+
+			> .body {
+				white-space: nowrap;
+				overflow: hidden;
+				text-overflow: ellipsis;
+				height: $header-height;
+
+				> .default {
+					padding: 0 $header-height;
+
+					> .avatar {
+						$size: 32px;
+						display: inline-block;
+						width: $size;
+						height: $size;
+						vertical-align: bottom;
+						margin: (($header-height - $size) / 2) 8px (($header-height - $size) / 2) 0;
+					}
+
+					> .title {
+						display: inline-block;
+						font-size: $ui-font-size;
+						margin: 0;
+						line-height: $header-height;
+
+						> [data-icon] {
+							margin-right: 8px;
+						}
+					}
+				}
+
+				> .custom {
+					position: absolute;
+					top: 0;
+					left: 0;
+					height: 100%;
+					width: 100%;
+				}
+			}
+		}
+
+		> .sub {
+			$post-button-size: 42px;
+			$post-button-margin: (($header-height - $post-button-size) / 2);
+			position: absolute;
+			top: 0;
+			right: 16px;
+			height: $header-height;
+
+			@media (max-width: $side-hide-threshold) {
+				display: none;
+			}
+
+			> [data-icon] {
+				position: absolute;
+				top: 0;
+				left: 16px;
+				height: $header-height;
+				pointer-events: none;
+				font-size: 16px;
+			}
+
+			> .search {
+				$margin: 8px;
+				width: calc(100% - #{$post-button-size + $post-button-margin + $margin});
+				box-sizing: border-box;
+				margin-right: $margin;
+				padding: 0 12px 0 42px;
+				font-size: 1rem;
+				line-height: 38px;
+				border: none;
+				border-radius: 38px;
+				color: var(--fg);
+				background: var(--bg);
+
+				&:focus {
+					outline: none;
+				}
+			}
+
+			> .post {
+				width: $post-button-size;
+				height: $post-button-size;
+				margin: $post-button-margin 0 $post-button-margin $post-button-margin;
+				border-radius: 100%;
+				font-size: 16px;
+			}
+		}
+	}
+
+	> .nav {
+		$avatar-size: 32px;
+		$avatar-margin: ($header-height - $avatar-size) / 2;
+
+		flex: 0 0 $nav-width;
+		width: $nav-width;
+		box-sizing: border-box;
+
+		@media (max-width: $nav-icon-only-threshold) {
+			flex: 0 0 $nav-icon-only-width;
+			width: $nav-icon-only-width;
+		}
+
+		@media (max-width: $nav-hide-threshold) {
+			display: none;
+		}
+
+		> div {
+			position: fixed;
+			top: 0;
+			left: 0;
+			z-index: 1001;
+			width: $nav-width;
+			height: 100vh;
+			padding-top: 16px;
+			box-sizing: border-box;
+			background: var(--navBg);
+			border-right: solid 1px var(--divider);
+
+			@media (max-width: $nav-icon-only-threshold) {
+				width: $nav-icon-only-width;
+			}
+
+			> .item {
+				position: relative;
+				display: block;
+				padding-left: 32px;
+				font-size: $ui-font-size;
+				font-weight: bold;
+				line-height: 3.2rem;
+				text-overflow: ellipsis;
+				overflow: hidden;
+				white-space: nowrap;
+				width: 100%;
+				text-align: left;
+				box-sizing: border-box;
+				color: var(--navFg);
+
+				&:not(.active) {
+					opacity: 0.85;
+
+					&:hover {
+						opacity: 1;
+
+						> [data-icon] {
+							opacity: 1;
+						}
+					}
+
+					> [data-icon] {
+						opacity: 0.85;
+					}
+				}
+
+				> [data-icon] {
+					width: ($header-height - ($avatar-margin * 2));
+				}
+		
+				> [data-icon],
+				> .avatar {
+					margin-right: $avatar-margin;
+				}
+
+				> .avatar {
+					width: $avatar-size;
+					height: $avatar-size;
+					vertical-align: middle;
+				}
+
+				> i {
+					position: absolute;
+					top: 0;
+					left: 20px;
+					color: var(--navIndicator);
+					font-size: 8px;
+					animation: blink 1s infinite;
+				}
+
+				&:hover {
+					text-decoration: none;
+				}
+
+				&.active {
+					color: var(--navActive);
+				}
+
+				@media (max-width: $nav-icon-only-threshold) {
+					padding-left: 0;
+					width: 100%;
+					text-align: center;
+					font-size: $ui-font-size * 1.2;
+					line-height: 3.5rem;
+
+					> [data-icon],
+					> .avatar {
+						margin-right: 0;
+					}
+
+					> i {
+						left: 10px;
+					}
+
+					> .text {
+						display: none;
+					}
+				}
+			}
+		}
+	}
+
+	> .contents {
+		display: flex;
+		margin: 0 auto;
+		min-width: 0;
+
+		> main {
+			width: $main-width;
+			min-width: $main-width;
+
+			@media (max-width: $side-hide-threshold) {
+				min-width: 0;
+			}
+
+			> .content {
+				padding: 16px;
+				box-sizing: border-box;
+
+				@media (max-width: 500px) {
+					padding: 8px;
+				}
+			}
+
+			> .powerd-by {
+				font-size: 14px;
+				text-align: center;
+				margin: 32px 0;
+				visibility: hidden;
+
+				&.visible {
+					visibility: visible;
+				}
+
+				&:not(.visible) {
+					@media (min-width: 850px) {
+						display: none;
+					}
+				}
+
+				@media (max-width: 500px) {
+					margin-top: 16px;
+				}
+
+				> small {
+					display: block;
+					margin-top: 8px;
+					opacity: 0.5;
+
+					@media (max-width: 500px) {
+						margin-top: 4px;
+					}
+				}
+			}
+		}
+
+		> .widgets {
+			box-sizing: border-box;
+
+			@media (max-width: $side-hide-threshold) {
+				display: none;
+			}
+
+			> div {
+				position: sticky;
+				top: calc(#{$header-height} + var(--margin));
+				height: calc(100vh - #{$header-height} - var(--margin));
+
+				&.edit {
+					overflow: auto;
+					width: auto !important;
+				}
+
+				&:not(.edit) {
+					display: inline-flex;
+					flex-wrap: wrap;
+					flex-direction: column;
+					place-content: flex-start;
+				}
+
+				> * {
+					margin: 0 var(--margin) var(--margin) 0;
+					width: 300px;
+				}
+
+				> .add {
+					margin: 0 auto;
+				}
+
+				> .edit {
+					display: block;
+					font-size: 0.9em;
+					margin: 0 auto;
+				}
+
+				.customize-container {
+					margin: 8px 0;
+					background: #fff;
+
+					> header {
+						position: relative;
+						line-height: 32px;
+						background: #eee;
+
+						> .handle {
+							padding: 0 8px;
+						}
+
+						> .remove {
+							position: absolute;
+							top: 0;
+							right: 0;
+							padding: 0 8px;
+							line-height: 32px;
+						}
+					}
+
+					> div {
+						padding: 8px;
+
+						> * {
+							pointer-events: none;
+						}
+					}
+				}
+			}
+		}
+	}
+
+	> .post {
+		display: none;
+		position: fixed;
+		z-index: 1000;
+		bottom: 32px;
+		right: 32px;
+		width: 64px;
+		height: 64px;
+		border-radius: 100%;
+		box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
+		font-size: 22px;
+
+		@media (min-width: ($nav-hide-threshold + 1px)) {
+			display: block;
+		}
+
+		@media (min-width: ($side-hide-threshold + 1px)) {
+			display: none;
+		}
+	}
+
+	> .buttons {
+		position: fixed;
+		z-index: 1000;
+		bottom: 0;
+		padding: 0 32px 32px 32px;
+		display: flex;
+		width: 100%;
+		box-sizing: border-box;
+		background: linear-gradient(0deg, var(--bg), var(--bonzsgfz));
+
+		@media (max-width: 500px) {
+			padding: 0 16px 16px 16px;
+		}
+
+		@media (min-width: ($nav-hide-threshold + 1px)) {
+			display: none;
+		}
+
+		> .button {
+			position: relative;
+			padding: 0;
+			margin: auto;
+			width: 64px;
+			height: 64px;
+			border-radius: 100%;
+			box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
+
+			&:first-child {
+				margin-left: 0;
+			}
+
+			&:last-child {
+				margin-right: 0;
+			}
+
+			> * {
+				font-size: 22px;
+			}
+
+			&:disabled {
+				cursor: default;
+
+				> * {
+					opacity: 0.5;
+				}
+			}
+
+			&:not(.post) {
+				background: var(--panel);
+				color: var(--fg);
+
+				&:hover {
+					background: var(--pcncwizz);
+				}
+
+				> i {
+					position: absolute;
+					top: 0;
+					left: 0;
+					color: var(--accent);
+					font-size: 16px;
+					animation: blink 1s infinite;
+				}
+			}
+		}
+	}
+
+	> .notifications {
+		position: fixed;
+		top: 32px;
+		left: 0;
+		right: 0;
+		margin: 0 auto;
+		z-index: 10001;
+		width: 350px;
+		height: 400px;
+		background: var(--vocsgcxy);
+		-webkit-backdrop-filter: blur(12px);
+		backdrop-filter: blur(12px);
+		border-radius: 6px;
+		box-shadow: 0 3px 12px rgba(27, 31, 35, 0.15);
+		overflow: hidden;
+
+		@media (max-width: 800px) {
+			width: 320px;
+			height: 350px;
+		}
+
+		@media (max-width: 500px) {
+			width: 290px;
+			height: 310px;
+		}
+	}
+}
+</style>
diff --git a/src/client/app/admin/assets/header-icon.svg b/src/client/app/admin/assets/header-icon.svg
deleted file mode 100644
index d677d2d16304709f93cc3d9be631cf686d724697..0000000000000000000000000000000000000000
Binary files a/src/client/app/admin/assets/header-icon.svg and /dev/null differ
diff --git a/src/client/app/admin/script.ts b/src/client/app/admin/script.ts
deleted file mode 100644
index 3f2d6466ace03b499e6d11d64b6607fac256e0d2..0000000000000000000000000000000000000000
--- a/src/client/app/admin/script.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Admin
- */
-
-import VueRouter from 'vue-router';
-
-// Style
-import './style.styl';
-
-import init from '../init';
-import Index from './views/index.vue';
-import NotFound from '../common/views/pages/not-found.vue';
-
-init(launch => {
-	document.title = 'Admin';
-
-	// Init router
-	const router = new VueRouter({
-		mode: 'history',
-		base: '/admin/',
-		routes: [
-			{ path: '/:page', component: Index },
-			{ path: '/', redirect: '/dashboard' },
-			{ path: '*', component: NotFound }
-		]
-	});
-
-	// Launch the app
-	launch(router);
-});
diff --git a/src/client/app/admin/style.styl b/src/client/app/admin/style.styl
deleted file mode 100644
index ae1a28226a53ad93b6e6a1f68e4a0f1a3250d09c..0000000000000000000000000000000000000000
--- a/src/client/app/admin/style.styl
+++ /dev/null
@@ -1,6 +0,0 @@
-@import "../app"
-@import "../reset"
-
-html
-	height 100%
-	background var(--bg)
diff --git a/src/client/app/admin/views/abuse.vue b/src/client/app/admin/views/abuse.vue
deleted file mode 100644
index afa285debcd09cd2b3133cb0a243b86be8666b6f..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/abuse.vue
+++ /dev/null
@@ -1,83 +0,0 @@
-<template>
-<div>
-	<ui-card>
-		<template #title><fa :icon="faExclamationCircle"/> {{ $t('title') }}</template>
-		<section class="fit-top">
-			<sequential-entrance animation="entranceFromTop" delay="25">
-				<div v-for="report in userReports" :key="report.id" class="haexwsjc">
-					<ui-horizon-group inputs>
-						<ui-input :value="report.user | acct" type="text" readonly>
-							<span>{{ $t('target') }}</span>
-						</ui-input>
-						<ui-input :value="report.reporter | acct" type="text" readonly>
-							<span>{{ $t('reporter') }}</span>
-						</ui-input>
-					</ui-horizon-group>
-					<ui-textarea :value="report.comment" readonly>
-						<span>{{ $t('details') }}</span>
-					</ui-textarea>
-					<ui-button @click="removeReport(report)">{{ $t('remove-report') }}</ui-button>
-				</div>
-			</sequential-entrance>
-			<ui-button v-if="existMore" @click="fetchUserReports">{{ $t('@.load-more') }}</ui-button>
-		</section>
-	</ui-card>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('admin/views/abuse.vue'),
-
-	data() {
-		return {
-			limit: 10,
-			untilId: undefined,
-			userReports: [],
-			existMore: false,
-			faExclamationCircle
-		};
-	},
-
-	mounted() {
-		this.fetchUserReports();
-	},
-
-	methods: {
-		fetchUserReports() {
-			this.$root.api('admin/abuse-user-reports', {
-				untilId: this.untilId,
-				limit: this.limit + 1
-			}).then(reports => {
-				if (reports.length == this.limit + 1) {
-					reports.pop();
-					this.existMore = true;
-				} else {
-					this.existMore = false;
-				}
-				this.userReports = this.userReports.concat(reports);
-				this.untilId = this.userReports[this.userReports.length - 1].id;
-			});
-		},
-
-		removeReport(report) {
-			this.$root.api('admin/remove-abuse-user-report', {
-				reportId: report.id
-			}).then(() => {
-				this.userReports = this.userReports.filter(r => r.id != report.id);
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.haexwsjc
-	padding-bottom 16px
-	border-bottom solid 1px var(--faceDivider)
-
-</style>
diff --git a/src/client/app/admin/views/announcements.vue b/src/client/app/admin/views/announcements.vue
deleted file mode 100644
index f6c0540b3756e72fa1d891643a93ed76ef4d6292..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/announcements.vue
+++ /dev/null
@@ -1,91 +0,0 @@
-<template>
-<div>
-	<ui-card>
-		<template #title><fa :icon="faBroadcastTower"/> {{ $t('announcements') }}</template>
-		<section v-for="(announcement, i) in announcements" class="fit-top">
-			<ui-input v-model="announcement.title" @change="save">
-				<span>{{ $t('title') }}</span>
-			</ui-input>
-			<ui-textarea v-model="announcement.text">
-				<span>{{ $t('text') }}</span>
-			</ui-textarea>
-			<ui-input v-model="announcement.image">
-				<span>{{ $t('image-url') }}</span>
-			</ui-input>
-			<ui-horizon-group class="fit-bottom">
-				<ui-button @click="save()"><fa :icon="['far', 'save']"/> {{ $t('save') }}</ui-button>
-				<ui-button @click="remove(i)"><fa :icon="['far', 'trash-alt']"/> {{ $t('remove') }}</ui-button>
-			</ui-horizon-group>
-		</section>
-		<section>
-			<ui-button @click="add"><fa :icon="faPlus"/> {{ $t('add') }}</ui-button>
-		</section>
-	</ui-card>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-import { faBroadcastTower, faPlus } from '@fortawesome/free-solid-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('admin/views/announcements.vue'),
-	data() {
-		return {
-			announcements: [],
-			faBroadcastTower, faPlus
-		};
-	},
-
-	created() {
-		this.$root.getMeta().then(meta => {
-			this.announcements = meta.announcements;
-		});
-	},
-
-	methods: {
-		add() {
-			this.announcements.unshift({
-				title: '',
-				text: '',
-				image: null
-			});
-		},
-
-		remove(i) {
-			this.$root.dialog({
-				type: 'warning',
-				text: this.$t('_remove.are-you-sure').replace('$1', this.announcements.find((_, j) => j == i).title),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-				this.announcements = this.announcements.filter((_, j) => j !== i);
-				this.save(true);
-				this.$root.dialog({
-					type: 'success',
-					text: this.$t('_remove.removed')
-				});
-			});
-		},
-
-		save(silent) {
-			this.$root.api('admin/update-meta', {
-				announcements: this.announcements
-			}).then(() => {
-				if (!silent) {
-					this.$root.dialog({
-						type: 'success',
-						text: this.$t('saved')
-					});
-				}
-			}).catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e
-				});
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/app/admin/views/dashboard.ap-log.vue b/src/client/app/admin/views/dashboard.ap-log.vue
deleted file mode 100644
index ee48ef15ea7c00bfdabcbdce794bf4fa83f6f5a2..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/dashboard.ap-log.vue
+++ /dev/null
@@ -1,109 +0,0 @@
-<template>
-<div class="hyhctythnmwihguaaapnbrbszsjqxpio">
-	<table>
-		<thead>
-			<tr>
-				<th><fa :icon="faExchangeAlt"/> In/Out</th>
-				<th><fa :icon="faBolt"/> Activity</th>
-				<th><fa icon="server"/> Host</th>
-				<th><fa icon="user"/> Actor</th>
-			</tr>
-		</thead>
-		<tbody>
-			<tr v-for="log in logs" :key="log.id">
-				<td :class="log.direction">{{ log.direction == 'in' ? '<' : '>' }} {{ log.direction }}</td>
-				<td>{{ log.activity }}</td>
-				<td>{{ log.host }}</td>
-				<td>@{{ log.actor }}</td>
-			</tr>
-		</tbody>
-	</table>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { faBolt, faExchangeAlt } from '@fortawesome/free-solid-svg-icons';
-
-export default Vue.extend({
-	data() {
-		return {
-			logs: [],
-			connection: null,
-			faBolt, faExchangeAlt
-		};
-	},
-
-	mounted() {
-		this.connection = this.$root.stream.useSharedConnection('apLog');
-		this.connection.on('log', this.onLog);
-		this.connection.on('logs', this.onLogs);
-		this.connection.send('requestLog', {
-			id: Math.random().toString().substr(2, 8),
-			length: 50
-		});
-	},
-
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		onLog(log) {
-			log.id = Math.random();
-			this.logs.unshift(log);
-			if (this.logs.length > 50) this.logs.pop();
-		},
-
-		onLogs(logs) {
-			for (const log of logs.reverse()) {
-				this.onLog(log)
-			}
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.hyhctythnmwihguaaapnbrbszsjqxpio
-	display block
-	padding 12px 16px 16px 16px
-	height 250px
-	overflow auto
-	box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
-	background var(--adminDashboardCardBg)
-	border-radius 8px
-
-	> table
-		width 100%
-		max-width 100%
-		overflow auto
-		border-spacing 0
-		border-collapse collapse
-		color var(--adminDashboardCardFg)
-		font-size 14px
-
-		thead
-			border-bottom solid 1px var(--adminDashboardCardDivider)
-
-			tr
-				th
-					font-weight normal
-					text-align left
-
-		tbody
-			tr
-				&:nth-child(odd)
-					background rgba(0, 0, 0, 0.025)
-
-		th, td
-			padding 8px 16px
-			min-width 128px
-
-		td.in
-			color #d26755
-
-		td.out
-			color #55bb83
-
-</style>
diff --git a/src/client/app/admin/views/dashboard.cpu-memory.vue b/src/client/app/admin/views/dashboard.cpu-memory.vue
deleted file mode 100644
index a3951e76182d6974f7eeb6173aa38856030fb11f..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/dashboard.cpu-memory.vue
+++ /dev/null
@@ -1,185 +0,0 @@
-<template>
-<div class="zyknedwtlthezamcjlolyusmipqmjgxz">
-	<div>
-		<header>
-			<span><fa icon="microchip"/> CPU <span>{{ cpuP }}%</span></span>
-			<span v-if="meta">{{ meta.cpu.model }}</span>
-		</header>
-		<div ref="cpu"></div>
-	</div>
-	<div>
-		<header>
-			<span><fa icon="memory"/> MEM <span>{{ memP }}%</span></span>
-			<span v-if="meta"></span>
-		</header>
-		<div ref="mem"></div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import ApexCharts from 'apexcharts';
-
-export default Vue.extend({
-	props: ['connection'],
-
-	data() {
-		return {
-			stats: [],
-			cpuChart: null,
-			memChart: null,
-			cpuP: '',
-			memP: '',
-			meta: null
-		};
-	},
-
-	watch: {
-		stats(stats) {
-			this.cpuChart.updateSeries([{
-				data: stats.map((x, i) => ({ x: i, y: x.cpu_usage }))
-			}]);
-			this.memChart.updateSeries([{
-				data: stats.map((x, i) => ({ x: i, y: (x.mem.used / x.mem.total) }))
-			}]);
-		}
-	},
-
-	mounted() {
-		this.$root.getMeta().then(meta => {
-			this.meta = meta;
-		});
-
-		this.connection.on('stats', this.onStats);
-		this.connection.on('statsLog', this.onStatsLog);
-		this.connection.send('requestLog', {
-			id: Math.random().toString().substr(2, 8),
-			length: 200
-		});
-
-		const chartOpts = {
-			chart: {
-				type: 'area',
-				height: 200,
-				animations: {
-					dynamicAnimation: {
-						enabled: false
-					}
-				},
-				toolbar: {
-					show: false
-				},
-				zoom: {
-					enabled: false
-				}
-			},
-			dataLabels: {
-				enabled: false
-			},
-			grid: {
-				clipMarkers: false,
-				borderColor: 'rgba(0, 0, 0, 0.1)'
-			},
-			stroke: {
-				curve: 'straight',
-				width: 2
-			},
-			tooltip: {
-				enabled: false
-			},
-			series: [{
-				data: []
-			}],
-			xaxis: {
-				type: 'numeric',
-				labels: {
-					show: false
-				},
-				tooltip: {
-					enabled: false
-				}
-			},
-			yaxis: {
-				show: false,
-				min: 0,
-				max: 1
-			}
-		};
-
-		this.cpuChart = new ApexCharts(this.$refs.cpu, chartOpts);
-		this.memChart = new ApexCharts(this.$refs.mem, chartOpts);
-
-		this.cpuChart.render();
-		this.memChart.render();
-	},
-
-	beforeDestroy() {
-		this.connection.off('stats', this.onStats);
-		this.connection.off('statsLog', this.onStatsLog);
-
-		this.cpuChart.destroy();
-		this.memChart.destroy();
-	},
-
-	methods: {
-		onStats(stats) {
-			this.stats.push(stats);
-			if (this.stats.length > 200) this.stats.shift();
-
-			this.cpuP = (stats.cpu_usage * 100).toFixed(0);
-			this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0);
-		},
-
-		onStatsLog(statsLog) {
-			for (const stats of statsLog.reverse()) {
-				this.onStats(stats);
-			}
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.zyknedwtlthezamcjlolyusmipqmjgxz
-	display flex
-
-	> div
-		display block
-		flex 1
-		padding 20px 12px 0 12px
-		box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
-		background var(--face)
-		border-radius 8px
-
-		&:first-child
-			margin-right 16px
-
-		> header
-			display flex
-			padding 0 8px
-			margin-bottom -16px
-			color var(--adminDashboardCardFg)
-			font-size 14px
-
-			> span
-				&:last-child
-					margin-left auto
-					opacity 0.7
-
-				> span
-					opacity 0.7
-
-		> div
-			margin-bottom -10px
-
-	@media (max-width 1000px)
-		display block
-		margin-bottom 26px
-
-		> div
-			&:first-child
-				margin-right 0
-				margin-bottom 26px
-
-</style>
diff --git a/src/client/app/admin/views/dashboard.queue-charts.vue b/src/client/app/admin/views/dashboard.queue-charts.vue
deleted file mode 100644
index d2d7811bffcd12adf18d44078ebbb49456b046c9..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/dashboard.queue-charts.vue
+++ /dev/null
@@ -1,196 +0,0 @@
-<template>
-<div class="mzxlfysy">
-	<div>
-		<header>
-			<span><fa :icon="faInbox"/> In</span>
-			<span v-if="latestStats">{{ latestStats.inbox.activeSincePrevTick | number }} / {{ latestStats.inbox.delayed | number }}</span>
-		</header>
-		<div ref="in"></div>
-	</div>
-	<div>
-		<header>
-			<span><fa :icon="faPaperPlane"/> Out</span>
-			<span v-if="latestStats">{{ latestStats.deliver.activeSincePrevTick | number }} / {{ latestStats.deliver.delayed | number }}</span>
-		</header>
-		<div ref="out"></div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { faInbox } from '@fortawesome/free-solid-svg-icons';
-import { faPaperPlane } from '@fortawesome/free-regular-svg-icons';
-import ApexCharts from 'apexcharts';
-
-const limit = 150;
-
-export default Vue.extend({
-	data() {
-		return {
-			stats: [],
-			inChart: null,
-			outChart: null,
-			faInbox, faPaperPlane
-		};
-	},
-
-	computed: {
-		latestStats(): any {
-			return this.stats[this.stats.length - 1];
-		}
-	},
-
-	watch: {
-		stats(stats) {
-			this.inChart.updateSeries([{
-				data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick }))
-			}, {
-				data: stats.map((x, i) => ({ x: i, y: x.inbox.active }))
-			}, {
-				data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting }))
-			}, {
-				data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
-			}]);
-			this.outChart.updateSeries([{
-				data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick }))
-			}, {
-				data: stats.map((x, i) => ({ x: i, y: x.deliver.active }))
-			}, {
-				data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting }))
-			}, {
-				data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
-			}]);
-		}
-	},
-
-	mounted() {
-		const chartOpts = {
-			chart: {
-				type: 'area',
-				height: 200,
-				animations: {
-					dynamicAnimation: {
-						enabled: false
-					}
-				},
-				toolbar: {
-					show: false
-				},
-				zoom: {
-					enabled: false
-				}
-			},
-			dataLabels: {
-				enabled: false
-			},
-			grid: {
-				clipMarkers: false,
-				borderColor: 'rgba(0, 0, 0, 0.1)'
-			},
-			stroke: {
-				curve: 'straight',
-				width: 2
-			},
-			tooltip: {
-				enabled: false
-			},
-			legend: {
-				show: false
-			},
-			colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'],
-			series: [{ data: [] }, { data: [] }, { data: [] }, { data: [] }] as any,
-			xaxis: {
-				type: 'numeric',
-				labels: {
-					show: false
-				},
-				tooltip: {
-					enabled: false
-				}
-			},
-			yaxis: {
-				show: false,
-				min: 0,
-			}
-		};
-
-		this.inChart = new ApexCharts(this.$refs.in, chartOpts);
-		this.outChart = new ApexCharts(this.$refs.out, chartOpts);
-
-		this.inChart.render();
-		this.outChart.render();
-
-		const connection = this.$root.stream.useSharedConnection('queueStats');
-		connection.on('stats', this.onStats);
-		connection.on('statsLog', this.onStatsLog);
-		connection.send('requestLog', {
-			id: Math.random().toString().substr(2, 8),
-			length: limit
-		});
-
-		this.$once('hook:beforeDestroy', () => {
-			connection.dispose();
-			this.inChart.destroy();
-			this.outChart.destroy();
-		});
-	},
-
-	methods: {
-		onStats(stats) {
-			this.stats.push(stats);
-			if (this.stats.length > limit) this.stats.shift();
-		},
-
-		onStatsLog(statsLog) {
-			for (const stats of statsLog.reverse()) {
-				this.onStats(stats);
-			}
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mzxlfysy
-	display flex
-
-	> div
-		display block
-		flex 1
-		padding 20px 12px 0 12px
-		box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
-		background var(--face)
-		border-radius 8px
-
-		&:first-child
-			margin-right 16px
-
-		> header
-			display flex
-			padding 0 8px
-			margin-bottom -16px
-			color var(--adminDashboardCardFg)
-			font-size 14px
-
-			> span
-				&:last-child
-					margin-left auto
-					opacity 0.7
-
-				> span
-					opacity 0.7
-
-		> div
-			margin-bottom -10px
-
-	@media (max-width 1000px)
-		display block
-		margin-bottom 26px
-
-		> div
-			&:first-child
-				margin-right 0
-				margin-bottom 26px
-
-</style>
diff --git a/src/client/app/admin/views/dashboard.vue b/src/client/app/admin/views/dashboard.vue
deleted file mode 100644
index 5ccfaa06ca593765149b80c71938487d79be6909..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/dashboard.vue
+++ /dev/null
@@ -1,286 +0,0 @@
-<template>
-<div class="obdskegsannmntldydackcpzezagxqfy">
-	<header v-if="meta">
-		<p><b>Misskey</b><span>{{ meta.version }}</span></p>
-		<p><b>Machine</b><span>{{ meta.machine }}</span></p>
-		<p><b>OS</b><span>{{ meta.os }}</span></p>
-		<p><b>Node</b><span>{{ meta.node }}</span></p>
-		<p>{{ $t('@.ai-chan-kawaii') }}</p>
-	</header>
-
-	<marquee-text v-if="instances.length > 0" class="instances" :repeat="10" :duration="60">
-		<span v-for="instance in instances" class="instance">
-			<b :style="{ background: instance.bg }">{{ instance.host }}</b>{{ instance.notesCount | number }} / {{ instance.usersCount | number }}
-		</span>
-	</marquee-text>
-
-	<div v-if="stats" class="stats">
-		<div>
-			<div>
-				<div><fa icon="user"/></div>
-				<div>
-					<span>{{ $t('accounts') }}</span>
-					<b>{{ stats.originalUsersCount | number }}</b>
-				</div>
-			</div>
-			<div>
-				<span><fa icon="home"/> {{ $t('this-instance') }}</span>
-				<span @click="setChartSrc('users')"><fa :icon="['far', 'chart-bar']"/></span>
-			</div>
-		</div>
-		<div>
-			<div>
-				<div><fa icon="pencil-alt"/></div>
-				<div>
-					<span>{{ $t('notes') }}</span>
-					<b>{{ stats.originalNotesCount | number }}</b>
-				</div>
-			</div>
-			<div>
-				<span><fa icon="home"/> {{ $t('this-instance') }}</span>
-				<span @click="setChartSrc('notes')"><fa :icon="['far', 'chart-bar']"/></span>
-			</div>
-		</div>
-		<div>
-			<div>
-				<div><fa :icon="faDatabase"/></div>
-				<div>
-					<span>{{ $t('drive') }}</span>
-					<b>{{ stats.driveUsageLocal | bytes }}</b>
-				</div>
-			</div>
-			<div>
-				<span><fa icon="home"/> {{ $t('this-instance') }}</span>
-				<span @click="setChartSrc('drive')"><fa :icon="['far', 'chart-bar']"/></span>
-			</div>
-		</div>
-		<div>
-			<div>
-				<div><fa :icon="['far', 'hdd']"/></div>
-				<div>
-					<span>{{ $t('instances') }}</span>
-					<b>{{ stats.instances | number }}</b>
-				</div>
-			</div>
-			<div>
-				<span><fa icon="globe"/> {{ $t('federated') }}</span>
-				<span @click="setChartSrc('federation-instances-total')"><fa :icon="['far', 'chart-bar']"/></span>
-			</div>
-		</div>
-	</div>
-
-	<div class="charts">
-		<x-charts ref="charts"/>
-	</div>
-
-	<div class="queue">
-		<x-queue/>
-	</div>
-
-	<div class="cpu-memory">
-		<x-cpu-memory :connection="connection"/>
-	</div>
-
-	<div class="ap">
-		<x-ap-log/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-import XCpuMemory from "./dashboard.cpu-memory.vue";
-import XQueue from "./dashboard.queue-charts.vue";
-import XCharts from "./dashboard.charts.vue";
-import XApLog from "./dashboard.ap-log.vue";
-import { faDatabase } from '@fortawesome/free-solid-svg-icons';
-import MarqueeText from 'vue-marquee-text-component';
-import randomColor from 'randomcolor';
-
-export default Vue.extend({
-	i18n: i18n('admin/views/dashboard.vue'),
-
-	components: {
-		XCpuMemory,
-		XQueue,
-		XCharts,
-		XApLog,
-		MarqueeText
-	},
-
-	data() {
-		return {
-			stats: null,
-			connection: null,
-			meta: null,
-			instances: [],
-			faDatabase
-		};
-	},
-
-	created() {
-		this.connection = this.$root.stream.useSharedConnection('serverStats');
-
-		this.updateStats();
-
-		this.$root.getMeta().then(meta => {
-			this.meta = meta;
-		});
-
-		this.$root.api('federation/instances', {
-			sort: '+notes'
-		}).then(instances => {
-			for (const i of instances) {
-				i.bg = randomColor({
-					seed: i.host,
-					luminosity: 'dark'
-				});
-			}
-			this.instances = instances;
-		});
-	},
-
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		setChartSrc(src) {
-			this.$refs.charts.setSrc(src);
-		},
-
-		updateStats() {
-			this.$root.api('stats', {}, true).then(stats => {
-				this.stats = stats;
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.obdskegsannmntldydackcpzezagxqfy
-	padding 16px
-
-	@media (min-width 500px)
-		padding 16px
-
-	> header
-		display flex
-		padding-bottom 16px
-		border-bottom solid 1px var(--adminDashboardHeaderBorder)
-		color var(--adminDashboardHeaderFg)
-		font-size 14px
-		white-space nowrap
-
-		@media (max-width 1000px)
-			display none
-
-		> p
-			display block
-			margin 0 32px 0 0
-			overflow hidden
-			text-overflow ellipsis
-
-			> b
-				&:after
-					content ':'
-					margin-right 8px
-
-			&:last-child
-				margin-left auto
-				margin-right 0
-
-	> .instances
-		padding 16px
-		color var(--adminDashboardHeaderFg)
-		font-size 13px
-
-		>>> .instance
-			margin 0 10px
-
-			> b
-				padding 2px 6px
-				margin-right 4px
-				border-radius 4px
-				color #fff
-
-	> .stats
-		display flex
-		justify-content space-between
-		margin-bottom 16px
-
-		> div
-			flex 1
-			margin-right 16px
-			color var(--adminDashboardCardFg)
-			box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
-			background var(--adminDashboardCardBg)
-			border-radius 8px
-
-			&:last-child
-				margin-right 0
-
-			> div:first-child
-				display flex
-				align-items center
-				text-align center
-
-				&:last-child
-					margin-right 0
-
-				> div:first-child
-					padding 16px 24px
-					font-size 28px
-
-				> div:last-child
-					flex 1
-					padding 16px 32px 16px 0
-					text-align right
-
-					> span
-						font-size 70%
-						opacity 0.7
-
-					> b
-						display block
-
-			> div:last-child
-				display flex
-				padding 6px 16px
-				border-top solid 1px var(--adminDashboardCardDivider)
-
-				> span
-					font-size 70%
-					opacity 0.7
-
-					&:last-child
-						margin-left auto
-						cursor pointer
-
-		@media (max-width 900px)
-			display grid
-			grid-template-columns 1fr 1fr
-			grid-template-rows 1fr 1fr
-			gap 16px
-
-			> div
-				margin-right 0
-
-		@media (max-width 500px)
-			display block
-
-			> div:not(:last-child)
-				margin-bottom 16px
-
-	> .charts
-		margin-bottom 16px
-
-	> .queue
-		margin-bottom 16px
-
-	> .cpu-memory
-		margin-bottom 16px
-
-</style>
diff --git a/src/client/app/admin/views/db.vue b/src/client/app/admin/views/db.vue
deleted file mode 100644
index 9f87a749b6d22811619dbc01aea5caef430254ed..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/db.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<template>
-<div>
-	<ui-card>
-		<template #title><fa :icon="faDatabase"/> {{ $t('tables') }}</template>
-		<section v-if="tables">
-			<div v-for="table in Object.keys(tables)"><b>{{ table }}</b> {{ tables[table].count | number }} {{ tables[table].size | bytes }}</div>
-		</section>
-		<section>
-			<header><fa :icon="faBroom"/> {{ $t('vacuum') }}</header>
-			<ui-info>{{ $t('vacuum-info') }}</ui-info>
-			<ui-switch v-model="fullVacuum">FULL</ui-switch>
-			<ui-switch v-model="analyzeVacuum">ANALYZE</ui-switch>
-			<ui-button @click="vacuum()"><fa :icon="faBroom"/> {{ $t('vacuum') }}</ui-button>
-			<ui-info warn>{{ $t('vacuum-exclamation') }}</ui-info>
-		</section>
-	</ui-card>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-import { faDatabase, faBroom } from '@fortawesome/free-solid-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('admin/views/db.vue'),
-
-	data() {
-		return {
-			tables: null,
-			fullVacuum: true,
-			analyzeVacuum: true,
-			faDatabase, faBroom
-		};
-	},
-
-	mounted() {
-		this.fetch();
-	},
-
-	methods: {
-		fetch() {
-			this.$root.api('admin/get-table-stats').then(tables => {
-				this.tables = tables;
-			});
-		},
-
-		vacuum() {
-			this.$root.api('admin/vacuum', {
-				full: this.fullVacuum,
-				analyze: this.analyzeVacuum,
-			}).then(() => {
-				this.$root.dialog({
-					type: 'success',
-					splash: true
-				});
-			});
-		},
-	}
-});
-</script>
diff --git a/src/client/app/admin/views/drive.vue b/src/client/app/admin/views/drive.vue
deleted file mode 100644
index 1152db2b919288ea9c1b2af40790b2b143b5258d..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/drive.vue
+++ /dev/null
@@ -1,292 +0,0 @@
-<template>
-<div>
-	<ui-card>
-		<template #title><fa :icon="faTerminal"/> {{ $t('operation') }}</template>
-		<section class="fit-top">
-			<ui-input v-model="target" type="text">
-				<span>{{ $t('fileid-or-url') }}</span>
-			</ui-input>
-			<ui-horizon-group>
-				<ui-button @click="findAndToggleSensitive(true)"><fa :icon="faEyeSlash"/> {{ $t('mark-as-sensitive') }}</ui-button>
-				<ui-button @click="findAndToggleSensitive(false)"><fa :icon="faEye"/> {{ $t('unmark-as-sensitive') }}</ui-button>
-			</ui-horizon-group>
-			<ui-button @click="findAndDel()"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button>
-			<ui-button @click="show()"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
-			<ui-textarea v-if="file" :value="file | json5" readonly tall style="margin-top:16px;"></ui-textarea>
-		</section>
-		<section>
-			<ui-button @click="cleanUp()"><fa :icon="faTrashAlt"/> {{ $t('clean-up') }}</ui-button>
-			<ui-button @click="cleanRemoteFiles()"><fa :icon="faTrashAlt"/> {{ $t('clean-remote-files') }}</ui-button>
-		</section>
-	</ui-card>
-
-	<ui-card>
-		<template #title><fa :icon="faCloud"/> {{ $t('@.drive') }}</template>
-		<section class="fit-top">
-			<ui-horizon-group inputs>
-				<ui-select v-model="sort">
-					<template #label>{{ $t('sort.title') }}</template>
-					<option value="-createdAt">{{ $t('sort.createdAtAsc') }}</option>
-					<option value="+createdAt">{{ $t('sort.createdAtDesc') }}</option>
-					<option value="-size">{{ $t('sort.sizeAsc') }}</option>
-					<option value="+size">{{ $t('sort.sizeDesc') }}</option>
-				</ui-select>
-				<ui-select v-model="origin">
-					<template #label>{{ $t('origin.title') }}</template>
-					<option value="combined">{{ $t('origin.combined') }}</option>
-					<option value="local">{{ $t('origin.local') }}</option>
-					<option value="remote">{{ $t('origin.remote') }}</option>
-				</ui-select>
-			</ui-horizon-group>
-			<sequential-entrance animation="entranceFromTop" delay="25">
-				<div class="kidvdlkg" v-for="file in files">
-					<div @click="file._open = !file._open">
-						<div>
-							<x-file-thumbnail class="thumbnail" :file="file" fit="contain" @click="showFileMenu(file)"/>
-						</div>
-						<div>
-							<header>
-								<b>{{ file.name }}</b>
-								<span class="username">@{{ file.user | acct }}</span>
-							</header>
-							<div>
-								<div>
-									<span style="margin-right:16px;">{{ file.type }}</span>
-									<span>{{ file.size | bytes }}</span>
-								</div>
-								<div><mk-time :time="file.createdAt" mode="detail"/></div>
-							</div>
-						</div>
-					</div>
-					<div v-show="file._open">
-						<ui-input readonly :value="file.url"></ui-input>
-						<ui-horizon-group>
-							<ui-button @click="toggleSensitive(file)" v-if="file.isSensitive"><fa :icon="faEye"/> {{ $t('unmark-as-sensitive') }}</ui-button>
-							<ui-button @click="toggleSensitive(file)" v-else><fa :icon="faEyeSlash"/> {{ $t('mark-as-sensitive') }}</ui-button>
-							<ui-button @click="del(file)"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button>
-						</ui-horizon-group>
-					</div>
-				</div>
-			</sequential-entrance>
-			<ui-button v-if="existMore" @click="fetch">{{ $t('@.load-more') }}</ui-button>
-		</section>
-	</ui-card>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-import { faCloud, faTerminal, faSearch } from '@fortawesome/free-solid-svg-icons';
-import { faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
-import XFileThumbnail from '../../common/views/components/drive-file-thumbnail.vue';
-
-export default Vue.extend({
-	i18n: i18n('admin/views/drive.vue'),
-
-	components: {
-		XFileThumbnail
-	},
-
-	data() {
-		return {
-			file: null,
-			target: null,
-			sort: '+createdAt',
-			origin: 'combined',
-			limit: 10,
-			offset: 0,
-			files: [],
-			existMore: false,
-			faCloud, faTrashAlt, faEye, faEyeSlash, faTerminal, faSearch
-		};
-	},
-
-	watch: {
-		sort() {
-			this.files = [];
-			this.offset = 0;
-			this.fetch();
-		},
-
-		origin() {
-			this.files = [];
-			this.offset = 0;
-			this.fetch();
-		}
-	},
-
-	mounted() {
-		this.fetch();
-	},
-
-	methods: {
-		async fetchFile() {
-			try {
-				return await this.$root.api('drive/files/show', this.target.startsWith('http') ? { url: this.target } : { fileId: this.target });
-			} catch (e) {
-				if (e == 'file-not-found') {
-					this.$root.dialog({
-						type: 'error',
-						text: this.$t('file-not-found')
-					});
-				} else {
-					this.$root.dialog({
-						type: 'error',
-						text: e.toString()
-					});
-				}
-			}
-		},
-
-		fetch() {
-			this.$root.api('admin/drive/files', {
-				origin: this.origin,
-				sort: this.sort,
-				offset: this.offset,
-				limit: this.limit + 1
-			}).then(files => {
-				if (files.length == this.limit + 1) {
-					files.pop();
-					this.existMore = true;
-				} else {
-					this.existMore = false;
-				}
-				for (const x of files) {
-					x._open = false;
-				}
-				this.files = this.files.concat(files);
-				this.offset += this.limit;
-			});
-		},
-
-		async del(file: any) {
-			const process = async () => {
-				await this.$root.api('drive/files/delete', { fileId: file.id });
-				this.$root.dialog({
-					type: 'success',
-					text: this.$t('deleted')
-				});
-			};
-
-			await process().catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e.toString()
-				});
-			});
-		},
-
-		toggleSensitive(file: any) {
-			this.$root.api('drive/files/update', {
-				fileId: file.id,
-				isSensitive: !file.isSensitive
-			}).then(() => {
-				file.isSensitive = !file.isSensitive;
-			});
-		},
-
-		async show() {
-			const file = await this.fetchFile();
-			this.$root.api('admin/drive/show-file', { fileId: file.id }).then(info => {
-				this.file = info;
-			});
-		},
-
-		async findAndToggleSensitive(sensitive) {
-			const process = async () => {
-				const file = await this.fetchFile();
-				await this.$root.api('drive/files/update', {
-					fileId: file.id,
-					isSensitive: sensitive
-				});
-				this.$root.dialog({
-					type: 'success',
-					text: sensitive ? this.$t('marked-as-sensitive') : this.$t('unmarked-as-sensitive')
-				});
-			};
-
-			await process().catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e.toString()
-				});
-			});
-		},
-
-		async findAndDel() {
-			const process = async () => {
-				const file = await this.fetchFile();
-				await this.$root.api('drive/files/delete', { fileId: file.id });
-				this.$root.dialog({
-					type: 'success',
-					text: this.$t('deleted')
-				});
-			};
-
-			await process().catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e.toString()
-				});
-			});
-		},
-
-		cleanRemoteFiles() {
-			this.$root.dialog({
-				type: 'warning',
-				text: this.$t('clean-remote-files-are-you-sure'),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-				this.$root.api('admin/drive/clean-remote-files');
-				this.$root.dialog({
-					type: 'success',
-					splash: true
-				});
-			});
-		},
-
-		cleanUp() {
-			this.$root.api('admin/drive/cleanup');
-			this.$root.dialog({
-				type: 'success',
-				splash: true
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.kidvdlkg
-	padding 16px 0
-	border-top solid 1px var(--faceDivider)
-
-	> div:first-child
-		display flex
-		cursor pointer
-
-		> div:nth-child(1)
-			> .thumbnail
-				display flex
-				width 64px
-				height 64px
-				background-size cover
-				background-position center center
-
-		> div:nth-child(2)
-			flex 1
-			padding-left 16px
-
-			@media (max-width 500px)
-				font-size 14px
-
-			> header
-				word-break break-word
-
-				> .username
-					margin-left 8px
-					opacity 0.7
-
-</style>
diff --git a/src/client/app/admin/views/emoji.vue b/src/client/app/admin/views/emoji.vue
deleted file mode 100644
index 2925fcab57b17c35ab42fc6c6127bc49fc95240f..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/emoji.vue
+++ /dev/null
@@ -1,185 +0,0 @@
-<template>
-<div>
-	<ui-card>
-		<template #title><fa icon="plus"/> {{ $t('add-emoji.title') }}</template>
-		<section class="fit-top">
-			<ui-horizon-group inputs>
-				<ui-input v-model="name">
-					<span>{{ $t('add-emoji.name') }}</span>
-					<template #desc>{{ $t('add-emoji.name-desc') }}</template>
-				</ui-input>
-				<ui-input v-model="category" :datalist="categoryList">
-					<span>{{ $t('add-emoji.category') }}</span>
-				</ui-input>
-				<ui-input v-model="aliases">
-					<span>{{ $t('add-emoji.aliases') }}</span>
-					<template #desc>{{ $t('add-emoji.aliases-desc') }}</template>
-				</ui-input>
-			</ui-horizon-group>
-			<ui-input v-model="url">
-				<template #icon><fa icon="link"/></template>
-				<span>{{ $t('add-emoji.url') }}</span>
-			</ui-input>
-			<ui-info>{{ $t('add-emoji.info') }}</ui-info>
-			<ui-button @click="add">{{ $t('add-emoji.add') }}</ui-button>
-		</section>
-	</ui-card>
-
-	<ui-card>
-		<template #title><fa :icon="faGrin"/> {{ $t('emojis.title') }}</template>
-		<section v-for="emoji in emojis" :key="emoji.name" class="oryfrbft">
-			<div>
-				<img :src="emoji.url" :alt="emoji.name" style="width: 64px;"/>
-			</div>
-			<div>
-				<ui-horizon-group>
-					<ui-input v-model="emoji.name">
-						<span>{{ $t('add-emoji.name') }}</span>
-					</ui-input>
-					<ui-input v-model="emoji.category" :datalist="categoryList">
-						<span>{{ $t('add-emoji.category') }}</span>
-					</ui-input>
-					<ui-input v-model="emoji.aliases">
-						<span>{{ $t('add-emoji.aliases') }}</span>
-					</ui-input>
-				</ui-horizon-group>
-				<ui-input v-model="emoji.url">
-					<template #icon><fa icon="link"/></template>
-					<span>{{ $t('add-emoji.url') }}</span>
-				</ui-input>
-				<ui-horizon-group class="fit-bottom">
-					<ui-button @click="updateEmoji(emoji)"><fa :icon="['far', 'save']"/> {{ $t('emojis.update') }}</ui-button>
-					<ui-button @click="removeEmoji(emoji)"><fa :icon="['far', 'trash-alt']"/> {{ $t('emojis.remove') }}</ui-button>
-				</ui-horizon-group>
-			</div>
-		</section>
-	</ui-card>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-import { faGrin } from '@fortawesome/free-regular-svg-icons';
-import { unique } from '../../../../prelude/array';
-
-export default Vue.extend({
-	i18n: i18n('admin/views/emoji.vue'),
-	data() {
-		return {
-			name: '',
-			category: '',
-			url: '',
-			aliases: '',
-			emojis: [],
-			faGrin
-		};
-	},
-
-	mounted() {
-		this.fetchEmojis();
-	},
-
-	computed: {
-		categoryList() {
-			return unique(this.emojis.map((x: any) => x.category || '').filter((x: string) => x !== ''));
-		}
-	},
-
-	methods: {
-		add() {
-			this.$root.api('admin/emoji/add', {
-				name: this.name,
-				category: this.category,
-				url: this.url,
-				aliases: this.aliases.split(' ').filter(x => x.length > 0)
-			}).then(() => {
-				this.$root.dialog({
-					type: 'success',
-					text: this.$t('add-emoji.added')
-				});
-				this.fetchEmojis();
-			}).catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e
-				});
-			});
-		},
-
-		fetchEmojis() {
-			this.$root.api('admin/emoji/list').then(emojis => {
-				for (const e of emojis) {
-					e.aliases = (e.aliases || []).join(' ');
-				}
-				this.emojis = emojis;
-			});
-		},
-
-		updateEmoji(emoji) {
-			this.$root.api('admin/emoji/update', {
-				id: emoji.id,
-				name: emoji.name,
-				category: emoji.category,
-				url: emoji.url,
-				aliases: emoji.aliases.split(' ').filter(x => x.length > 0)
-			}).then(() => {
-				this.$root.dialog({
-					type: 'success',
-					text: this.$t('updated')
-				});
-			}).catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e
-				});
-			});
-		},
-
-		removeEmoji(emoji) {
-			this.$root.dialog({
-				type: 'warning',
-				text: this.$t('remove-emoji.are-you-sure').replace('$1', emoji.name),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				this.$root.api('admin/emoji/remove', {
-					id: emoji.id
-				}).then(() => {
-					this.$root.dialog({
-						type: 'success',
-						text: this.$t('remove-emoji.removed')
-					});
-					this.fetchEmojis();
-				}).catch(e => {
-					this.$root.dialog({
-						type: 'error',
-						text: e
-					});
-				});
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.oryfrbft
-	@media (min-width 500px)
-		display flex
-
-	> div:first-child
-		@media (max-width 500px)
-			padding-bottom 16px
-
-		> img
-			vertical-align bottom
-
-	> div:last-child
-		flex 1
-
-		@media (min-width 500px)
-			padding-left 16px
-
-</style>
diff --git a/src/client/app/admin/views/federation.vue b/src/client/app/admin/views/federation.vue
deleted file mode 100644
index b419cca1d7c149a7517873dead0c7893778ca803..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/federation.vue
+++ /dev/null
@@ -1,553 +0,0 @@
-<template>
-<div>
-	<ui-card>
-		<template #title><fa :icon="faTerminal"/> {{ $t('instance') }}</template>
-		<section class="fit-top">
-			<ui-input class="target" v-model="target" type="text" @enter="showInstance()">
-				<span>{{ $t('host') }}</span>
-				<template #prefix><fa :icon="faServer"/></template>
-			</ui-input>
-			<ui-button @click="showInstance()"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
-
-			<div class="instance" v-if="instance">
-				<ui-horizon-group inputs>
-					<ui-input :value="instance.host" type="text" readonly>
-						<span>{{ $t('host') }}</span>
-						<template #prefix><fa :icon="faServer"/></template>
-					</ui-input>
-					<ui-input :value="instance.caughtAt | date" type="text" readonly>
-						<span>{{ $t('caught-at') }}</span>
-						<template #prefix><fa :icon="faCrosshairs"/></template>
-					</ui-input>
-				</ui-horizon-group>
-				<ui-horizon-group inputs>
-					<ui-input :value="instance.notesCount | number" type="text" readonly>
-						<span>{{ $t('notes') }}</span>
-						<template #prefix><fa :icon="faEnvelopeOpenText"/></template>
-					</ui-input>
-					<ui-input :value="instance.usersCount | number" type="text" readonly>
-						<span>{{ $t('users') }}</span>
-						<template #prefix><fa :icon="faUsers"/></template>
-					</ui-input>
-				</ui-horizon-group>
-				<ui-horizon-group inputs>
-					<ui-input :value="instance.followingCount | number" type="text" readonly>
-						<span>{{ $t('following') }}</span>
-						<template #prefix><fa :icon="faCaretDown"/></template>
-					</ui-input>
-					<ui-input :value="instance.followersCount | number" type="text" readonly>
-						<span>{{ $t('followers') }}</span>
-						<template #prefix><fa :icon="faCaretUp"/></template>
-					</ui-input>
-				</ui-horizon-group>
-				<ui-horizon-group inputs>
-					<ui-input :value="instance.latestRequestSentAt | date" type="text" readonly>
-						<span>{{ $t('latest-request-sent-at') }}</span>
-						<template #prefix><fa :icon="faPaperPlane"/></template>
-					</ui-input>
-					<ui-input :value="instance.latestStatus" type="text" readonly>
-						<span>{{ $t('status') }}</span>
-						<template #prefix><fa :icon="faTrafficLight"/></template>
-					</ui-input>
-				</ui-horizon-group>
-				<ui-input :value="instance.latestRequestReceivedAt | date" type="text" readonly>
-					<span>{{ $t('latest-request-received-at') }}</span>
-					<template #prefix><fa :icon="faInbox"/></template>
-				</ui-input>
-				<ui-switch v-model="instance.isMarkedAsClosed" @change="updateInstance()">{{ $t('marked-as-closed') }}</ui-switch>
-				<details>
-					<summary>{{ $t('charts') }}</summary>
-					<ui-horizon-group inputs>
-						<ui-select v-model="chartSrc">
-							<option value="requests">{{ $t('chart-srcs.requests') }}</option>
-							<option value="users">{{ $t('chart-srcs.users') }}</option>
-							<option value="users-total">{{ $t('chart-srcs.users-total') }}</option>
-							<option value="notes">{{ $t('chart-srcs.notes') }}</option>
-							<option value="notes-total">{{ $t('chart-srcs.notes-total') }}</option>
-							<option value="ff">{{ $t('chart-srcs.ff') }}</option>
-							<option value="ff-total">{{ $t('chart-srcs.ff-total') }}</option>
-							<option value="drive-usage">{{ $t('chart-srcs.drive-usage') }}</option>
-							<option value="drive-usage-total">{{ $t('chart-srcs.drive-usage-total') }}</option>
-							<option value="drive-files">{{ $t('chart-srcs.drive-files') }}</option>
-							<option value="drive-files-total">{{ $t('chart-srcs.drive-files-total') }}</option>
-						</ui-select>
-						<ui-select v-model="chartSpan">
-							<option value="hour">{{ $t('chart-spans.hour') }}</option>
-							<option value="day">{{ $t('chart-spans.day') }}</option>
-						</ui-select>
-					</ui-horizon-group>
-					<div ref="chart"></div>
-				</details>
-				<details>
-					<summary>{{ $t('delete-all-files') }}</summary>
-					<ui-button @click="deleteAllFiles()" style="margin-top: 16px;"><fa :icon="faTrashAlt"/> {{ $t('delete-all-files') }}</ui-button>
-				</details>
-				<details>
-					<summary>{{ $t('remove-all-following') }}</summary>
-					<ui-button @click="removeAllFollowing()" style="margin-top: 16px;"><fa :icon="faMinusCircle"/> {{ $t('remove-all-following') }}</ui-button>
-					<ui-info warn>{{ $t('remove-all-following-info', { host: instance.host }) }}</ui-info>
-				</details>
-			</div>
-		</section>
-	</ui-card>
-
-	<ui-card>
-		<template #title><fa :icon="faServer"/> {{ $t('instances') }}</template>
-		<section class="fit-top">
-			<ui-horizon-group inputs>
-				<ui-select v-model="sort">
-					<template #label>{{ $t('sort') }}</template>
-					<option value="-caughtAt">{{ $t('sorts.caughtAtAsc') }}</option>
-					<option value="+caughtAt">{{ $t('sorts.caughtAtDesc') }}</option>
-					<option value="-lastCommunicatedAt">{{ $t('sorts.lastCommunicatedAtAsc') }}</option>
-					<option value="+lastCommunicatedAt">{{ $t('sorts.lastCommunicatedAtDesc') }}</option>
-					<option value="-notes">{{ $t('sorts.notesAsc') }}</option>
-					<option value="+notes">{{ $t('sorts.notesDesc') }}</option>
-					<option value="-users">{{ $t('sorts.usersAsc') }}</option>
-					<option value="+users">{{ $t('sorts.usersDesc') }}</option>
-					<option value="-following">{{ $t('sorts.followingAsc') }}</option>
-					<option value="+following">{{ $t('sorts.followingDesc') }}</option>
-					<option value="-followers">{{ $t('sorts.followersAsc') }}</option>
-					<option value="+followers">{{ $t('sorts.followersDesc') }}</option>
-					<option value="-driveUsage">{{ $t('sorts.driveUsageAsc') }}</option>
-					<option value="+driveUsage">{{ $t('sorts.driveUsageDesc') }}</option>
-					<option value="-driveFiles">{{ $t('sorts.driveFilesAsc') }}</option>
-					<option value="+driveFiles">{{ $t('sorts.driveFilesDesc') }}</option>
-				</ui-select>
-				<ui-select v-model="state">
-					<template #label>{{ $t('state') }}</template>
-					<option value="all">{{ $t('states.all') }}</option>
-					<option value="blocked">{{ $t('states.blocked') }}</option>
-					<option value="notResponding">{{ $t('states.not-responding') }}</option>
-					<option value="markedAsClosed">{{ $t('states.marked-as-closed') }}</option>
-				</ui-select>
-			</ui-horizon-group>
-
-			<div class="instances">
-				<header>
-					<span>{{ $t('host') }}</span>
-					<span>{{ $t('notes') }}</span>
-					<span>{{ $t('users') }}</span>
-					<span>{{ $t('following') }}</span>
-					<span>{{ $t('followers') }}</span>
-					<span>{{ $t('status') }}</span>
-				</header>
-				<div v-for="instance in instances" :style="{ opacity: instance.isNotResponding ? 0.5 : 1 }">
-					<a @click.prevent="showInstance(instance.host)" rel="nofollow noopener" target="_blank" :href="`https://${instance.host}`" :style="{ textDecoration: instance.isMarkedAsClosed ? 'line-through' : 'none' }">{{ instance.host }}</a>
-					<span>{{ instance.notesCount | number }}</span>
-					<span>{{ instance.usersCount | number }}</span>
-					<span>{{ instance.followingCount | number }}</span>
-					<span>{{ instance.followersCount | number }}</span>
-					<span>{{ instance.latestStatus }}</span>
-				</div>
-			</div>
-
-			<ui-info v-if="instances.length == limit">{{ $t('result-is-truncated', { n: limit }) }}</ui-info>
-		</section>
-	</ui-card>
-
-	<ui-card>
-		<template #title><fa :icon="faBan"/> {{ $t('blocked-hosts') }}</template>
-		<section class="fit-top">
-			<ui-textarea v-model="blockedHosts">
-				<template #desc>{{ $t('blocked-hosts-info') }}</template>
-			</ui-textarea>
-			<ui-button @click="saveBlockedHosts">{{ $t('save') }}</ui-button>
-		</section>
-	</ui-card>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-import { faPaperPlane } from '@fortawesome/free-regular-svg-icons';
-import { faTrashAlt, faBan, faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faTrafficLight, faInbox } from '@fortawesome/free-solid-svg-icons';
-import ApexCharts from 'apexcharts';
-import * as tinycolor from 'tinycolor2';
-
-const chartLimit = 90;
-const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
-const negate = arr => arr.map(x => -x);
-
-export default Vue.extend({
-	i18n: i18n('admin/views/federation.vue'),
-
-	filters: {
-		date: v => v ? new Date(v).toLocaleString() : 'N/A'
-	},
-
-	data() {
-		return {
-			instance: null,
-			target: null,
-			sort: '+lastCommunicatedAt',
-			state: 'all',
-			limit: 100,
-			instances: [],
-			chart: null,
-			chartSrc: 'requests',
-			chartSpan: 'hour',
-			chartInstance: null,
-			blockedHosts: '',
-			faTrashAlt, faBan, faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faPaperPlane, faTrafficLight, faInbox
-		};
-	},
-
-	computed: {
-		data(): any {
-			if (this.chart == null) return null;
-			switch (this.chartSrc) {
-				case 'requests': return this.requestsChart();
-				case 'users': return this.usersChart(false);
-				case 'users-total': return this.usersChart(true);
-				case 'notes': return this.notesChart(false);
-				case 'notes-total': return this.notesChart(true);
-				case 'ff': return this.ffChart(false);
-				case 'ff-total': return this.ffChart(true);
-				case 'drive-usage': return this.driveUsageChart(false);
-				case 'drive-usage-total': return this.driveUsageChart(true);
-				case 'drive-files': return this.driveFilesChart(false);
-				case 'drive-files-total': return this.driveFilesChart(true);
-			}
-		},
-
-		stats(): any[] {
-			const stats =
-				this.chartSpan == 'day' ? this.chart.perDay :
-				this.chartSpan == 'hour' ? this.chart.perHour :
-				null;
-
-			return stats;
-		}
-	},
-
-	watch: {
-		sort() {
-			this.fetchInstances();
-		},
-
-		state() {
-			this.fetchInstances();
-		},
-
-		async instance() {
-			this.now = new Date();
-
-			const [perHour, perDay] = await Promise.all([
-				this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }),
-				this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }),
-			]);
-
-			const chart = {
-				perHour: perHour,
-				perDay: perDay
-			};
-
-			this.chart = chart;
-
-			this.renderChart();
-		},
-
-		chartSrc() {
-			this.renderChart();
-		},
-
-		chartSpan() {
-			this.renderChart();
-		}
-	},
-
-	mounted() {
-		this.fetchInstances();
-
-		this.$root.getMeta().then(meta => {
-			this.blockedHosts = meta.blockedHosts.join('\n');
-		});
-	},
-
-	beforeDestroy() {
-		this.chartInstance.destroy();
-	},
-
-	methods: {
-		showInstance(target?: string) {
-			this.$root.api('federation/show-instance', {
-				host: target || this.target
-			}).then(instance => {
-				if (instance == null) {
-					this.$root.dialog({
-						type: 'error',
-						text: this.$t('instance-not-registered')
-					});
-				} else {
-					this.instance = instance;
-					this.target = '';
-				}
-			});
-		},
-
-		fetchInstances() {
-			this.instances = [];
-			this.$root.api('federation/instances', {
-				blocked: this.state === 'blocked' ? true : null,
-				notResponding: this.state === 'notResponding' ? true : null,
-				markedAsClosed: this.state === 'markedAsClosed' ? true : null,
-				sort: this.sort,
-				limit: this.limit
-			}).then(instances => {
-				this.instances = instances;
-			});
-		},
-
-		removeAllFollowing() {
-			this.$root.api('admin/federation/remove-all-following', {
-				host: this.instance.host
-			}).then(() => {
-				this.$root.dialog({
-					type: 'success',
-					splash: true
-				});
-			});
-		},
-
-		deleteAllFiles() {
-			this.$root.api('admin/federation/delete-all-files', {
-				host: this.instance.host
-			}).then(() => {
-				this.$root.dialog({
-					type: 'success',
-					splash: true
-				});
-			});
-		},
-
-		updateInstance() {
-			this.$root.api('admin/federation/update-instance', {
-				host: this.instance.host,
-				isBlocked: this.instance.isBlocked || false,
-				isClosed: this.instance.isMarkedAsClosed || false
-			});
-		},
-
-		setSrc(src) {
-			this.chartSrc = src;
-		},
-
-		renderChart() {
-			if (this.chartInstance) {
-				this.chartInstance.destroy();
-			}
-
-			this.chartInstance = new ApexCharts(this.$refs.chart, {
-				chart: {
-					type: 'area',
-					height: 300,
-					animations: {
-						dynamicAnimation: {
-							enabled: false
-						}
-					},
-					toolbar: {
-						show: false
-					},
-					zoom: {
-						enabled: false
-					}
-				},
-				dataLabels: {
-					enabled: false
-				},
-				grid: {
-					clipMarkers: false,
-					borderColor: 'rgba(0, 0, 0, 0.1)'
-				},
-				stroke: {
-					curve: 'straight',
-					width: 2
-				},
-				tooltip: {
-					theme: this.$store.state.device.darkmode ? 'dark' : 'light'
-				},
-				legend: {
-					labels: {
-						colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
-					},
-				},
-				xaxis: {
-					type: 'datetime',
-					labels: {
-						style: {
-							colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
-						}
-					},
-					axisBorder: {
-						color: 'rgba(0, 0, 0, 0.1)'
-					},
-					axisTicks: {
-						color: 'rgba(0, 0, 0, 0.1)'
-					},
-				},
-				yaxis: {
-					labels: {
-						formatter: this.data.bytes ? v => Vue.filter('bytes')(v, 0) : v => Vue.filter('number')(v),
-						style: {
-							color: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
-						}
-					}
-				},
-				series: this.data.series
-			});
-
-			this.chartInstance.render();
-		},
-
-		getDate(i: number) {
-			const y = this.now.getFullYear();
-			const m = this.now.getMonth();
-			const d = this.now.getDate();
-			const h = this.now.getHours();
-
-			return (
-				this.chartSpan == 'day' ? new Date(y, m, d - i) :
-				this.chartSpan == 'hour' ? new Date(y, m, d, h - i) :
-				null
-			);
-		},
-
-		format(arr) {
-			return arr.map((v, i) => ({ x: this.getDate(i).getTime(), y: v }));
-		},
-
-		requestsChart(): any {
-			return {
-				series: [{
-					name: 'Incoming',
-					data: this.format(this.stats.requests.received)
-				}, {
-					name: 'Outgoing (succeeded)',
-					data: this.format(this.stats.requests.succeeded)
-				}, {
-					name: 'Outgoing (failed)',
-					data: this.format(this.stats.requests.failed)
-				}]
-			};
-		},
-
-		usersChart(total: boolean): any {
-			return {
-				series: [{
-					name: 'Users',
-					type: 'area',
-					data: this.format(total
-						? this.stats.users.total
-						: sum(this.stats.users.inc, negate(this.stats.users.dec))
-					)
-				}]
-			};
-		},
-
-		notesChart(total: boolean): any {
-			return {
-				series: [{
-					name: 'Notes',
-					type: 'area',
-					data: this.format(total
-						? this.stats.notes.total
-						: sum(this.stats.notes.inc, negate(this.stats.notes.dec))
-					)
-				}]
-			};
-		},
-
-		ffChart(total: boolean): any {
-			return {
-				series: [{
-					name: 'Following',
-					type: 'area',
-					data: this.format(total
-						? this.stats.following.total
-						: sum(this.stats.following.inc, negate(this.stats.following.dec))
-					)
-				}, {
-					name: 'Followers',
-					type: 'area',
-					data: this.format(total
-						? this.stats.followers.total
-						: sum(this.stats.followers.inc, negate(this.stats.followers.dec))
-					)
-				}]
-			};
-		},
-
-		driveUsageChart(total: boolean): any {
-			return {
-				bytes: true,
-				series: [{
-					name: 'Drive usage',
-					type: 'area',
-					data: this.format(total
-						? this.stats.drive.totalUsage
-						: sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage))
-					)
-				}]
-			};
-		},
-
-		driveFilesChart(total: boolean): any {
-			return {
-				series: [{
-					name: 'Drive files',
-					type: 'area',
-					data: this.format(total
-						? this.stats.drive.totalFiles
-						: sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles))
-					)
-				}]
-			};
-		},
-
-		saveBlockedHosts() {
-			this.$root.api('admin/update-meta', {
-				blockedHosts: this.blockedHosts ? this.blockedHosts.split('\n') : []
-			}).then(() => {
-				this.$root.dialog({
-					type: 'success',
-					text: this.$t('saved')
-				});
-			}).catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e
-				});
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.target
-	margin-bottom 16px !important
-
-.instances
-	width 100%
-
-	> header
-		display flex
-
-		> *
-			color var(--text)
-			font-weight bold
-
-	> div
-		display flex
-
-	> * > *
-		flex 1
-		overflow auto
-
-		&:first-child
-			min-width 200px
-
-</style>
diff --git a/src/client/app/admin/views/index.vue b/src/client/app/admin/views/index.vue
deleted file mode 100644
index 1b81185749657e507d5d0b44863dd96296a3ba2e..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/index.vue
+++ /dev/null
@@ -1,297 +0,0 @@
-<template>
-<div class="mk-admin" :class="{ isMobile }">
-	<header v-show="isMobile">
-		<button class="nav" @click="navOpend = true"><fa icon="bars"/></button>
-		<span>MisskeyMyAdmin</span>
-	</header>
-	<div class="nav-backdrop"
-		v-if="navOpend && isMobile"
-		@click="navOpend = false"
-		@touchstart="navOpend = false"
-	></div>
-	<nav v-show="navOpend">
-		<div class="mi">
-			<img svg-inline src="../assets/header-icon.svg"/>
-		</div>
-		<div class="me">
-			<img class="avatar" :src="$store.state.i.avatarUrl" alt="avatar"/>
-			<p class="name"><mk-user-name :user="$store.state.i"/></p>
-		</div>
-		<ul>
-			<li><router-link to="/dashboard" active-class="active"><fa icon="home" fixed-width/>{{ $t('dashboard') }}</router-link></li>
-			<li><router-link to="/instance" active-class="active"><fa icon="cog" fixed-width/>{{ $t('instance') }}</router-link></li>
-			<li><router-link to="/queue" active-class="active"><fa :icon="faTasks" fixed-width/>{{ $t('queue') }}</router-link></li>
-			<li><router-link to="/logs" active-class="active"><fa :icon="faStream" fixed-width/>{{ $t('logs') }}</router-link></li>
-			<li><router-link to="/db" active-class="active"><fa :icon="faDatabase" fixed-width/>{{ $t('db') }}</router-link></li>
-			<li><router-link to="/moderators" active-class="active"><fa :icon="faHeadset" fixed-width/>{{ $t('moderators') }}</router-link></li>
-			<li><router-link to="/users" active-class="active"><fa icon="users" fixed-width/>{{ $t('users') }}</router-link></li>
-			<li><router-link to="/drive" active-class="active"><fa icon="cloud" fixed-width/>{{ $t('@.drive') }}</router-link></li>
-			<li><router-link to="/federation" active-class="active"><fa :icon="faGlobe" fixed-width/>{{ $t('federation') }}</router-link></li>
-			<li><router-link to="/emoji" active-class="active"><fa :icon="faGrin" fixed-width/>{{ $t('emoji') }}</router-link></li>
-			<li><router-link to="/announcements" active-class="active"><fa icon="broadcast-tower" fixed-width/>{{ $t('announcements') }}</router-link></li>
-			<li><router-link to="/abuse" active-class="active"><fa :icon="faExclamationCircle" fixed-width/>{{ $t('abuse') }}</router-link></li>
-		</ul>
-		<div class="back-to-misskey">
-			<a href="/"><fa :icon="faArrowLeft"/> {{ $t('back-to-misskey') }}</a>
-		</div>
-		<div class="version">
-			<small>Misskey {{ version }}</small>
-		</div>
-	</nav>
-	<main>
-		<div class="page">
-			<div v-if="page == 'dashboard'"><x-dashboard/></div>
-			<div v-if="page == 'instance'"><x-instance/></div>
-			<div v-if="page == 'queue'"><x-queue/></div>
-			<div v-if="page == 'logs'"><x-logs/></div>
-			<div v-if="page == 'db'"><x-db/></div>
-			<div v-if="page == 'moderators'"><x-moderators/></div>
-			<div v-if="page == 'users'"><x-users/></div>
-			<div v-if="page == 'emoji'"><x-emoji/></div>
-			<div v-if="page == 'announcements'"><x-announcements/></div>
-			<div v-if="page == 'drive'"><x-drive/></div>
-			<div v-if="page == 'federation'"><x-federation/></div>
-			<div v-if="page == 'abuse'"><x-abuse/></div>
-		</div>
-	</main>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-import { version } from '../../config';
-import XDashboard from './dashboard.vue';
-import XInstance from './instance.vue';
-import XQueue from './queue.vue';
-import XLogs from './logs.vue';
-import XDb from './db.vue';
-import XModerators from './moderators.vue';
-import XEmoji from './emoji.vue';
-import XAnnouncements from './announcements.vue';
-import XUsers from './users.vue';
-import XDrive from './drive.vue';
-import XAbuse from './abuse.vue';
-import XFederation from './federation.vue';
-
-import { faHeadset, faArrowLeft, faGlobe, faExclamationCircle, faTasks, faStream, faDatabase } from '@fortawesome/free-solid-svg-icons';
-import { faGrin } from '@fortawesome/free-regular-svg-icons';
-
-// Detect the user agent
-const ua = navigator.userAgent.toLowerCase();
-const isMobile = /mobile|iphone|ipad|android/.test(ua);
-
-export default Vue.extend({
-	i18n: i18n('admin/views/index.vue'),
-	components: {
-		XDashboard,
-		XInstance,
-		XQueue,
-		XLogs,
-		XDb,
-		XModerators,
-		XEmoji,
-		XAnnouncements,
-		XUsers,
-		XDrive,
-		XAbuse,
-		XFederation,
-	},
-	provide: {
-		isMobile
-	},
-	data() {
-		return {
-			version,
-			isMobile,
-			navOpend: !isMobile,
-			faGrin,
-			faArrowLeft,
-			faHeadset,
-			faGlobe,
-			faExclamationCircle,
-			faTasks,
-			faStream,
-			faDatabase,
-		};
-	},
-	computed: {
-		page() {
-			return this.$route.params.page;
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-admin
-	$headerHeight = 48px
-
-	display flex
-	height 100%
-
-	> header
-		position fixed
-		top 0
-		z-index 10000
-		width 100%
-		color var(--mobileHeaderFg)
-		background-color var(--mobileHeaderBg)
-		box-shadow 0 1px 0 rgba(#000, 0.075)
-
-		&, *
-			user-select none
-
-		> span
-			display block
-			line-height $headerHeight
-			text-align center
-
-		> .nav
-			display block
-			position absolute
-			top 0
-			left 0
-			z-index 10001
-			padding 0
-			width $headerHeight
-			font-size 1.4em
-			line-height $headerHeight
-			border-right solid 1px rgba(#000, 0.1)
-
-			> [data-icon]
-				transition all 0.2s ease
-
-	> nav
-		position fixed
-		z-index 20001
-		top 0
-		left 0
-		width 250px
-		height 100vh
-		overflow auto
-		background #333
-		color #fff
-
-		> .mi
-			text-align center
-
-			> svg
-				width 24px
-				height 82px
-				vertical-align top
-				fill #fff
-				opacity 0.7
-
-		> .me
-			display flex
-			margin 0 16px 16px 16px
-			padding 16px 0
-			align-items center
-			border-top solid 1px #555
-			border-bottom solid 1px #555
-
-			> .avatar
-				height 48px
-				border-radius 100%
-				vertical-align middle
-
-			> .name
-				margin 0 16px
-				padding 0
-				color #fff
-				overflow hidden
-				text-overflow ellipsis
-				white-space nowrap
-				font-size 15px
-
-		> .back-to-misskey
-			margin 16px 16px 0 16px
-			padding 0
-			border-top solid 1px #555
-
-			> a
-				display block
-				padding 16px 4px
-				color inherit
-				text-decoration none
-				color #eee
-				font-size 15px
-
-				&:hover
-					color #fff
-
-				> [data-icon]
-					margin-right 6px
-
-		> .version
-			margin 0 16px 16px 16px
-			padding-top 16px
-			border-top solid 1px #555
-			text-align center
-
-			> small
-				opacity 0.7
-
-		> ul
-			margin 0
-			padding 0
-			list-style none
-			font-size 15px
-
-			> li > a
-				display block
-				padding 10px 16px
-				margin 0
-				user-select none
-				color #eee
-				transition margin-left 0.2s ease
-
-				&:hover
-					color #fff
-
-				> [data-icon]
-					margin-right 6px
-
-				&.active
-					margin-left 8px
-					color var(--primary) !important
-
-					&:after
-						content ""
-						display block
-						position absolute
-						top 0
-						right 0
-						bottom 0
-						margin auto 0
-						height 0
-						border-top solid 16px transparent
-						border-right solid 16px var(--bg)
-						border-bottom solid 16px transparent
-						border-left solid 16px transparent
-
-	> .nav-backdrop
-		position fixed
-		top 0
-		left 0
-		z-index 20000
-		width 100%
-		height 100%
-		background var(--mobileNavBackdrop)
-
-	> main
-		width 100%
-		padding 0 0 0 250px
-
-		> .page
-			max-width 1150px
-
-			@media (min-width 500px)
-				padding 16px
-
-	&.isMobile
-		> main
-			padding $headerHeight 0 0 0
-
-</style>
diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue
deleted file mode 100644
index ebc554f955c99b44f8f31997255c3e50cf989437..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/instance.vue
+++ /dev/null
@@ -1,523 +0,0 @@
-<template>
-<div>
-	<ui-card>
-		<template #title><fa icon="cog"/> {{ $t('instance') }}</template>
-		<section class="fit-top">
-			<ui-input :value="host" readonly>{{ $t('host') }}</ui-input>
-			<ui-input v-model="name">{{ $t('instance-name') }}</ui-input>
-			<ui-textarea v-model="description">{{ $t('instance-description') }}</ui-textarea>
-			<ui-input v-model="iconUrl"><template #icon><fa icon="link"/></template>{{ $t('icon-url') }}</ui-input>
-			<ui-input v-model="mascotImageUrl"><template #icon><fa icon="link"/></template>{{ $t('logo-url') }}</ui-input>
-			<ui-input v-model="bannerUrl"><template #icon><fa icon="link"/></template>{{ $t('banner-url') }}</ui-input>
-			<ui-input v-model="ToSUrl"><template #icon><fa icon="link"/></template>{{ $t('tos-url') }}</ui-input>
-			<details>
-				<summary>{{ $t('advanced-config') }}</summary>
-				<ui-input v-model="errorImageUrl"><template #icon><fa icon="link"/></template>{{ $t('error-image-url') }}</ui-input>
-				<ui-input v-model="languages"><template #icon><fa icon="language"/></template>{{ $t('languages') }}<template #desc>{{ $t('languages-desc') }}</template></ui-input>
-				<ui-input v-model="repositoryUrl"><template #icon><fa icon="link"/></template>{{ $t('repository-url') }}</ui-input>
-				<ui-input v-model="feedbackUrl"><template #icon><fa icon="link"/></template>{{ $t('feedback-url') }}</ui-input>
-			</details>
-		</section>
-		<section class="fit-bottom">
-			<header><fa :icon="faHeadset"/> {{ $t('maintainer-config') }}</header>
-			<ui-input v-model="maintainerName">{{ $t('maintainer-name') }}</ui-input>
-			<ui-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="farEnvelope"/></template>{{ $t('maintainer-email') }}</ui-input>
-		</section>
-		<section>
-			<ui-switch v-model="disableRegistration">{{ $t('disable-registration') }}</ui-switch>
-			<ui-button v-if="disableRegistration" @click="invite">{{ $t('invite') }}</ui-button>
-		</section>
-		<section>
-			<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
-		</section>
-	</ui-card>
-
-	<ui-card>
-		<template #title><fa :icon="faPencilAlt"/> {{ $t('note-and-tl') }}</template>
-		<section class="fit-top fit-bottom">
-			<ui-input v-model="maxNoteTextLength">{{ $t('max-note-text-length') }}</ui-input>
-		</section>
-		<section>
-			<ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch>
-			<ui-switch v-model="disableGlobalTimeline">{{ $t('disable-global-timeline') }}</ui-switch>
-			<ui-info>{{ $t('disabling-timelines-info') }}</ui-info>
-		</section>
-		<section>
-			<ui-switch v-model="enableEmojiReaction">{{ $t('enable-emoji-reaction') }}</ui-switch>
-			<ui-switch v-model="useStarForReactionFallback">{{ $t('use-star-for-reaction-fallback') }}</ui-switch>
-		</section>
-		<section>
-			<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
-		</section>
-	</ui-card>
-
-	<ui-card>
-		<template #title><fa icon="cloud"/> {{ $t('drive-config') }}</template>
-		<section>
-			<ui-switch v-model="useObjectStorage">{{ $t('use-object-storage') }}</ui-switch>
-			<template v-if="useObjectStorage">
-				<ui-info>
-					<i18n path="object-storage-s3-info">
-						<a href="https://docs.aws.amazon.com/general/latest/gr/rande.html" target="_blank">{{ $t('object-storage-s3-info-here') }}</a>
-					</i18n>
-				</ui-info>
-				<ui-info>{{ $t('object-storage-gcs-info') }}</ui-info>
-				<ui-input v-model="objectStorageBaseUrl" :disabled="!useObjectStorage">{{ $t('object-storage-base-url') }}</ui-input>
-				<ui-horizon-group inputs>
-					<ui-input v-model="objectStorageBucket" :disabled="!useObjectStorage">{{ $t('object-storage-bucket') }}</ui-input>
-					<ui-input v-model="objectStoragePrefix" :disabled="!useObjectStorage">{{ $t('object-storage-prefix') }}</ui-input>
-				</ui-horizon-group>
-				<ui-input v-model="objectStorageEndpoint" :disabled="!useObjectStorage">{{ $t('object-storage-endpoint') }}</ui-input>
-				<ui-horizon-group inputs>
-					<ui-input v-model="objectStorageRegion" :disabled="!useObjectStorage">{{ $t('object-storage-region') }}</ui-input>
-					<ui-input v-model="objectStoragePort" type="number" :disabled="!useObjectStorage">{{ $t('object-storage-port') }}</ui-input>
-				</ui-horizon-group>
-				<ui-horizon-group inputs>
-					<ui-input v-model="objectStorageAccessKey" :disabled="!useObjectStorage"><template #icon><fa icon="key"/></template>{{ $t('object-storage-access-key') }}</ui-input>
-					<ui-input v-model="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><fa icon="key"/></template>{{ $t('object-storage-secret-key') }}</ui-input>
-				</ui-horizon-group>
-				<ui-switch v-model="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $t('object-storage-use-ssl') }}</ui-switch>
-			</template>
-		</section>
-		<section>
-			<ui-switch v-model="cacheRemoteFiles">{{ $t('cache-remote-files') }}<template #desc>{{ $t('cache-remote-files-desc') }}</template></ui-switch>
-			<ui-switch v-model="proxyRemoteFiles">{{ $t('proxy-remote-files') }}<template #desc>{{ $t('proxy-remote-files-desc') }}</template></ui-switch>
-		</section>
-		<section class="fit-top fit-bottom">
-			<ui-input v-model="localDriveCapacityMb" type="number">{{ $t('local-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input>
-			<ui-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">{{ $t('remote-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input>
-		</section>
-		<section>
-			<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
-		</section>
-	</ui-card>
-
-	<ui-card>
-		<template #title><fa :icon="faThumbtack"/> {{ $t('pinned-users') }}</template>
-		<section class="fit-top">
-			<ui-textarea v-model="pinnedUsers">
-				<template #desc>{{ $t('pinned-users-info') }}</template>
-			</ui-textarea>
-			<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
-		</section>
-	</ui-card>
-
-	<ui-card>
-		<template #title><fa :icon="faGhost"/> {{ $t('proxy-account-config') }}</template>
-		<section>
-			<ui-info>{{ $t('proxy-account-info') }}</ui-info>
-			<ui-input v-model="proxyAccount"><template #prefix>@</template>{{ $t('proxy-account-username') }}<template #desc>{{ $t('proxy-account-username-desc') }}</template></ui-input>
-			<ui-info warn>{{ $t('proxy-account-warn') }}</ui-info>
-		</section>
-		<section>
-			<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
-		</section>
-	</ui-card>
-
-	<ui-card>
-		<template #title><fa :icon="farEnvelope"/> {{ $t('email-config') }}</template>
-		<section>
-			<ui-switch v-model="enableEmail">{{ $t('enable-email') }}<template #desc>{{ $t('email-config-info') }}</template></ui-switch>
-			<template v-if="enableEmail">
-				<ui-input v-model="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</ui-input>
-				<ui-horizon-group inputs>
-					<ui-input v-model="smtpHost" :disabled="!enableEmail">{{ $t('smtp-host') }}</ui-input>
-					<ui-input v-model="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtp-port') }}</ui-input>
-				</ui-horizon-group>
-				<ui-switch v-model="smtpAuth">{{ $t('smtp-auth') }}</ui-switch>
-				<ui-horizon-group inputs>
-					<ui-input v-model="smtpUser" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-user') }}</ui-input>
-					<ui-input v-model="smtpPass" type="password" :with-password-toggle="true" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-pass') }}</ui-input>
-				</ui-horizon-group>
-				<ui-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtp-secure') }}<template #desc>{{ $t('smtp-secure-info') }}</template></ui-switch>
-				<ui-button @click="testEmail()">{{ $t('test-email') }}</ui-button>
-			</template>
-		</section>
-		<section>
-			<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
-		</section>
-	</ui-card>
-
-	<ui-card>
-		<template #title><fa :icon="faBolt"/> {{ $t('serviceworker-config') }}</template>
-		<section>
-			<ui-switch v-model="enableServiceWorker">{{ $t('enable-serviceworker') }}<template #desc>{{ $t('serviceworker-info') }}</template></ui-switch>
-			<template v-if="enableServiceWorker">
-				<ui-info>{{ $t('vapid-info') }}<br><code>npm i web-push -g<br>web-push generate-vapid-keys</code></ui-info>
-				<ui-horizon-group inputs class="fit-bottom">
-					<ui-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-publickey') }}</ui-input>
-					<ui-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-privatekey') }}</ui-input>
-				</ui-horizon-group>
-			</template>
-		</section>
-		<section>
-			<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
-		</section>
-	</ui-card>
-
-	<ui-card>
-		<template #title><fa :icon="faShieldAlt"/> {{ $t('recaptcha-config') }}</template>
-		<section :class="enableRecaptcha ? 'fit-bottom' : ''">
-			<ui-switch v-model="enableRecaptcha">{{ $t('enable-recaptcha') }}</ui-switch>
-			<template v-if="enableRecaptcha">
-				<ui-info>{{ $t('recaptcha-info') }}</ui-info>
-				<ui-info warn>{{ $t('recaptcha-info2') }}</ui-info>
-				<ui-horizon-group inputs>
-					<ui-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-site-key') }}</ui-input>
-					<ui-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-secret-key') }}</ui-input>
-				</ui-horizon-group>
-			</template>
-		</section>
-		<section v-if="enableRecaptcha && recaptchaSiteKey">
-			<header>{{ $t('recaptcha-preview') }}</header>
-			<div ref="recaptcha" style="margin: 16px 0 0 0;" :key="recaptchaSiteKey"></div>
-		</section>
-		<section>
-			<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
-		</section>
-	</ui-card>
-
-	<ui-card>
-		<template #title><fa :icon="faShieldAlt"/> {{ $t('external-service-integration-config') }}</template>
-		<section>
-			<header><fa :icon="['fab', 'twitter']"/> {{ $t('twitter-integration-config') }}</header>
-			<ui-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</ui-switch>
-			<template v-if="enableTwitterIntegration">
-				<ui-horizon-group>
-					<ui-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-key') }}</ui-input>
-					<ui-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-secret') }}</ui-input>
-				</ui-horizon-group>
-				<ui-info>{{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }}</ui-info>
-			</template>
-		</section>
-		<section>
-			<header><fa :icon="['fab', 'github']"/> {{ $t('github-integration-config') }}</header>
-			<ui-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</ui-switch>
-			<template v-if="enableGithubIntegration">
-				<ui-horizon-group>
-					<ui-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-id') }}</ui-input>
-					<ui-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-secret') }}</ui-input>
-				</ui-horizon-group>
-				<ui-info>{{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }}</ui-info>
-			</template>
-		</section>
-		<section>
-			<header><fa :icon="['fab', 'discord']"/> {{ $t('discord-integration-config') }}</header>
-			<ui-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</ui-switch>
-			<template v-if="enableDiscordIntegration">
-				<ui-horizon-group>
-					<ui-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-id') }}</ui-input>
-					<ui-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-secret') }}</ui-input>
-				</ui-horizon-group>
-				<ui-info>{{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }}</ui-info>
-			</template>
-		</section>
-		<section>
-			<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
-		</section>
-	</ui-card>
-
-	<details>
-		<summary style="color:var(--text);">{{ $t('advanced-config') }}</summary>
-
-		<ui-card>
-			<template #title><fa :icon="faHashtag"/> {{ $t('hidden-tags') }}</template>
-			<section class="fit-top">
-				<ui-textarea v-model="hiddenTags">
-					<template #desc>{{ $t('hidden-tags-info') }}</template>
-				</ui-textarea>
-				<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
-			</section>
-		</ui-card>
-
-		<ui-card>
-			<template #title>summaly Proxy</template>
-			<section class="fit-top fit-bottom">
-				<ui-input v-model="summalyProxy">URL</ui-input>
-			</section>
-			<section>
-				<ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
-			</section>
-		</ui-card>
-	</details>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-import { url, host } from '../../config';
-import { toUnicode } from 'punycode';
-import { faHeadset, faShieldAlt, faGhost, faUserPlus, faBolt, faThumbtack, faPencilAlt, faHashtag } from '@fortawesome/free-solid-svg-icons';
-import { faEnvelope as farEnvelope, faSave } from '@fortawesome/free-regular-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('admin/views/instance.vue'),
-
-	data() {
-		return {
-			url,
-			host: toUnicode(host),
-			maintainerName: null,
-			maintainerEmail: null,
-			ToSUrl: null,
-			repositoryUrl: "https://github.com/syuilo/misskey",
-			feedbackUrl: null,
-			disableRegistration: false,
-			disableLocalTimeline: false,
-			disableGlobalTimeline: false,
-			enableEmojiReaction: true,
-			useStarForReactionFallback: false,
-			mascotImageUrl: null,
-			bannerUrl: null,
-			errorImageUrl: null,
-			iconUrl: null,
-			name: null,
-			description: null,
-			languages: null,
-			cacheRemoteFiles: false,
-			proxyRemoteFiles: false,
-			localDriveCapacityMb: null,
-			remoteDriveCapacityMb: null,
-			maxNoteTextLength: null,
-			enableRecaptcha: false,
-			recaptchaSiteKey: null,
-			recaptchaSecretKey: null,
-			enableTwitterIntegration: false,
-			twitterConsumerKey: null,
-			twitterConsumerSecret: null,
-			enableGithubIntegration: false,
-			githubClientId: null,
-			githubClientSecret: null,
-			enableDiscordIntegration: false,
-			discordClientId: null,
-			discordClientSecret: null,
-			proxyAccount: null,
-			summalyProxy: null,
-			enableEmail: false,
-			email: null,
-			smtpSecure: false,
-			smtpHost: null,
-			smtpPort: null,
-			smtpUser: null,
-			smtpPass: null,
-			smtpAuth: false,
-			enableServiceWorker: false,
-			swPublicKey: null,
-			swPrivateKey: null,
-			pinnedUsers: '',
-			hiddenTags: '',
-			useObjectStorage: false,
-			objectStorageBaseUrl: null,
-			objectStorageBucket: null,
-			objectStoragePrefix: null,
-			objectStorageEndpoint: null,
-			objectStorageRegion: null,
-			objectStoragePort: null,
-			objectStorageAccessKey: null,
-			objectStorageSecretKey: null,
-			objectStorageUseSSL: false,
-			faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope, faBolt, faThumbtack, faPencilAlt, faSave, faHashtag
-		};
-	},
-
-	created() {
-		this.$root.getMeta(true).then(meta => {
-			this.maintainerName = meta.maintainerName;
-			this.maintainerEmail = meta.maintainerEmail;
-			this.ToSUrl = meta.ToSUrl;
-			this.repositoryUrl = meta.repositoryUrl;
-			this.feedbackUrl = meta.feedbackUrl;
-			this.disableRegistration = meta.disableRegistration;
-			this.disableLocalTimeline = meta.disableLocalTimeline;
-			this.disableGlobalTimeline = meta.disableGlobalTimeline;
-			this.enableEmojiReaction = meta.enableEmojiReaction;
-			this.useStarForReactionFallback = meta.useStarForReactionFallback;
-			this.mascotImageUrl = meta.mascotImageUrl;
-			this.bannerUrl = meta.bannerUrl;
-			this.errorImageUrl = meta.errorImageUrl;
-			this.iconUrl = meta.iconUrl;
-			this.name = meta.name;
-			this.description = meta.description;
-			this.languages = meta.langs.join(' ');
-			this.cacheRemoteFiles = meta.cacheRemoteFiles;
-			this.proxyRemoteFiles = meta.proxyRemoteFiles;
-			this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
-			this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
-			this.maxNoteTextLength = meta.maxNoteTextLength;
-			this.enableRecaptcha = meta.enableRecaptcha;
-			this.recaptchaSiteKey = meta.recaptchaSiteKey;
-			this.recaptchaSecretKey = meta.recaptchaSecretKey;
-			this.proxyAccount = meta.proxyAccount;
-			this.enableTwitterIntegration = meta.enableTwitterIntegration;
-			this.twitterConsumerKey = meta.twitterConsumerKey;
-			this.twitterConsumerSecret = meta.twitterConsumerSecret;
-			this.enableGithubIntegration = meta.enableGithubIntegration;
-			this.githubClientId = meta.githubClientId;
-			this.githubClientSecret = meta.githubClientSecret;
-			this.enableDiscordIntegration = meta.enableDiscordIntegration;
-			this.discordClientId = meta.discordClientId;
-			this.discordClientSecret = meta.discordClientSecret;
-			this.summalyProxy = meta.summalyProxy;
-			this.enableEmail = meta.enableEmail;
-			this.email = meta.email;
-			this.smtpSecure = meta.smtpSecure;
-			this.smtpHost = meta.smtpHost;
-			this.smtpPort = meta.smtpPort;
-			this.smtpUser = meta.smtpUser;
-			this.smtpPass = meta.smtpPass;
-			this.smtpAuth = meta.smtpUser != null && meta.smtpUser !== '';
-			this.enableServiceWorker = meta.enableServiceWorker;
-			this.swPublicKey = meta.swPublickey;
-			this.swPrivateKey = meta.swPrivateKey;
-			this.pinnedUsers = meta.pinnedUsers.join('\n');
-			this.hiddenTags = meta.hiddenTags.join('\n');
-			this.useObjectStorage = meta.useObjectStorage;
-			this.objectStorageBaseUrl = meta.objectStorageBaseUrl;
-			this.objectStorageBucket = meta.objectStorageBucket;
-			this.objectStoragePrefix = meta.objectStoragePrefix;
-			this.objectStorageEndpoint = meta.objectStorageEndpoint;
-			this.objectStorageRegion = meta.objectStorageRegion;
-			this.objectStoragePort = meta.objectStoragePort;
-			this.objectStorageAccessKey = meta.objectStorageAccessKey;
-			this.objectStorageSecretKey = meta.objectStorageSecretKey;
-			this.objectStorageUseSSL = meta.objectStorageUseSSL;
-		});
-	},
-
-	mounted() {
-		const renderRecaptchaPreview = () => {
-			if (!(window as any).grecaptcha) return;
-			if (!this.$refs.recaptcha) return;
-			if (!this.recaptchaSiteKey) return;
-			(window as any).grecaptcha.render(this.$refs.recaptcha, {
-				sitekey: this.recaptchaSiteKey
-			});
-		};
-
-		window.onRecaotchaLoad = () => {
-			renderRecaptchaPreview();
-		};
-
-		const head = document.getElementsByTagName('head')[0];
-		const script = document.createElement('script');
-		script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaotchaLoad');
-		head.appendChild(script);
-
-		this.$watch('enableRecaptcha', () => {
-			renderRecaptchaPreview();
-		});
-
-		this.$watch('recaptchaSiteKey', () => {
-			renderRecaptchaPreview();
-		});
-	},
-
-	methods: {
-		invite() {
-			this.$root.api('admin/invite').then(x => {
-				this.$root.dialog({
-					type: 'info',
-					text: x.code
-				});
-			}).catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e
-				});
-			});
-		},
-
-		async testEmail() {
-			this.$root.api('admin/send-email', {
-				to: this.maintainerEmail,
-				subject: 'Test email',
-				text: 'Yo'
-			}).then(x => {
-				this.$root.dialog({
-					type: 'success',
-					splash: true
-				});
-			}).catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e
-				});
-			});
-		},
-
-		updateMeta() {
-			this.$root.api('admin/update-meta', {
-				maintainerName: this.maintainerName,
-				maintainerEmail: this.maintainerEmail,
-				ToSUrl: this.ToSUrl,
-				repositoryUrl: this.repositoryUrl,
-				feedbackUrl: this.feedbackUrl,
-				disableRegistration: this.disableRegistration,
-				disableLocalTimeline: this.disableLocalTimeline,
-				disableGlobalTimeline: this.disableGlobalTimeline,
-				enableEmojiReaction: this.enableEmojiReaction,
-				useStarForReactionFallback: this.useStarForReactionFallback,
-				mascotImageUrl: this.mascotImageUrl,
-				bannerUrl: this.bannerUrl,
-				errorImageUrl: this.errorImageUrl,
-				iconUrl: this.iconUrl,
-				name: this.name,
-				description: this.description,
-				langs: this.languages ? this.languages.split(' ') : [],
-				cacheRemoteFiles: this.cacheRemoteFiles,
-				proxyRemoteFiles: this.proxyRemoteFiles,
-				localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
-				remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
-				maxNoteTextLength: parseInt(this.maxNoteTextLength, 10),
-				enableRecaptcha: this.enableRecaptcha,
-				recaptchaSiteKey: this.recaptchaSiteKey,
-				recaptchaSecretKey: this.recaptchaSecretKey,
-				proxyAccount: this.proxyAccount,
-				enableTwitterIntegration: this.enableTwitterIntegration,
-				twitterConsumerKey: this.twitterConsumerKey,
-				twitterConsumerSecret: this.twitterConsumerSecret,
-				enableGithubIntegration: this.enableGithubIntegration,
-				githubClientId: this.githubClientId,
-				githubClientSecret: this.githubClientSecret,
-				enableDiscordIntegration: this.enableDiscordIntegration,
-				discordClientId: this.discordClientId,
-				discordClientSecret: this.discordClientSecret,
-				summalyProxy: this.summalyProxy,
-				enableEmail: this.enableEmail,
-				email: this.email,
-				smtpSecure: this.smtpSecure,
-				smtpHost: this.smtpHost,
-				smtpPort: parseInt(this.smtpPort, 10),
-				smtpUser: this.smtpAuth ? this.smtpUser : '',
-				smtpPass: this.smtpAuth ? this.smtpPass : '',
-				enableServiceWorker: this.enableServiceWorker,
-				swPublicKey: this.swPublicKey,
-				swPrivateKey: this.swPrivateKey,
-				pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [],
-				hiddenTags: this.hiddenTags ? this.hiddenTags.split('\n') : [],
-				useObjectStorage: this.useObjectStorage,
-				objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null,
-				objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null,
-				objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null,
-				objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null,
-				objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null,
-				objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null,
-				objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null,
-				objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
-				objectStorageUseSSL: this.objectStorageUseSSL,
-			}).then(() => {
-				this.$root.dialog({
-					type: 'success',
-					text: this.$t('saved')
-				});
-			}).catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e
-				});
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/app/admin/views/logs.vue b/src/client/app/admin/views/logs.vue
deleted file mode 100644
index cb54318187e0a53529b7d034345c040e6f3342e4..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/logs.vue
+++ /dev/null
@@ -1,119 +0,0 @@
-<template>
-<div>
-	<ui-card>
-		<template #title><fa :icon="faStream"/> {{ $t('logs') }}</template>
-		<section class="fit-top">
-			<ui-horizon-group inputs>
-				<ui-input v-model="domain" :debounce="true">
-					<span>{{ $t('domain') }}</span>
-				</ui-input>
-				<ui-select v-model="level">
-					<template #label>{{ $t('level') }}</template>
-					<option value="all">{{ $t('levels.all') }}</option>
-					<option value="info">{{ $t('levels.info') }}</option>
-					<option value="success">{{ $t('levels.success') }}</option>
-					<option value="warning">{{ $t('levels.warning') }}</option>
-					<option value="error">{{ $t('levels.error') }}</option>
-					<option value="debug">{{ $t('levels.debug') }}</option>
-				</ui-select>
-			</ui-horizon-group>
-
-			<div class="nqjzuvev">
-				<code v-for="log in logs" :key="log.id" :class="log.level">
-					<details>
-						<summary><mk-time :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary>
-						<vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty>
-					</details>
-				</code>
-			</div>
-
-			<ui-button @click="deleteAll()">{{ $t('delete-all') }}</ui-button>
-		</section>
-	</ui-card>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-import { faStream } from '@fortawesome/free-solid-svg-icons';
-import VueJsonPretty from 'vue-json-pretty';
-
-export default Vue.extend({
-	i18n: i18n('admin/views/logs.vue'),
-
-	components: {
-		VueJsonPretty
-	},
-
-	data() {
-		return {
-			logs: [],
-			level: 'all',
-			domain: '',
-			faStream
-		};
-	},
-
-	watch: {
-		level() {
-			this.logs = [];
-			this.fetch();
-		},
-
-		domain() {
-			this.logs = [];
-			this.fetch();
-		}
-	},
-
-	mounted() {
-		this.fetch();
-	},
-
-	methods: {
-		fetch() {
-			this.$root.api('admin/logs', {
-				level: this.level === 'all' ? null : this.level,
-				domain: this.domain === '' ? null : this.domain,
-				limit: 100
-			}).then(logs => {
-				this.logs = logs.reverse();
-			});
-		},
-
-		deleteAll() {
-			this.$root.api('admin/delete-logs').then(() => {
-				this.$root.dialog({
-					type: 'success',
-					splash: true
-				});
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.nqjzuvev
-	padding 8px
-	background #000
-	color #fff
-	font-size 14px
-
-	> code
-		display block
-
-		&.error
-			color #f00
-
-		&.warning
-			color #ff0
-
-		&.success
-			color #0f0
-
-		&.debug
-			opacity 0.7
-
-</style>
diff --git a/src/client/app/admin/views/moderators.vue b/src/client/app/admin/views/moderators.vue
deleted file mode 100644
index 8ceab02d97af51b120d93f202244f2e0fa19b50c..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/moderators.vue
+++ /dev/null
@@ -1,127 +0,0 @@
-<template>
-<div>
-	<ui-card>
-		<template #title><fa icon="plus"/> {{ $t('add-moderator.title') }}</template>
-		<section class="fit-top">
-			<ui-input v-model="username" type="text">
-				<template #prefix>@</template>
-			</ui-input>
-			<ui-horizon-group>
-				<ui-button @click="add" :disabled="changing">{{ $t('add-moderator.add') }}</ui-button>
-				<ui-button @click="remove" :disabled="changing">{{ $t('add-moderator.remove') }}</ui-button>
-			</ui-horizon-group>
-		</section>
-	</ui-card>
-
-	<ui-card>
-		<template #title>{{ $t('logs.title') }}</template>
-		<section class="fit-top">
-			<sequential-entrance animation="entranceFromTop" delay="25">
-				<div v-for="log in logs" :key="log.id" class="">
-					<ui-horizon-group inputs>
-						<ui-input :value="log.user | acct" type="text" readonly>
-							<span>{{ $t('logs.moderator') }}</span>
-						</ui-input>
-						<ui-input :value="log.type" type="text" readonly>
-							<span>{{ $t('logs.type') }}</span>
-						</ui-input>
-						<ui-input :value="log.createdAt | date" type="text" readonly>
-							<span>{{ $t('logs.at') }}</span>
-						</ui-input>
-					</ui-horizon-group>
-					<ui-textarea :value="JSON.stringify(log.info, null, 4)" readonly>
-						<span>{{ $t('logs.info') }}</span>
-					</ui-textarea>
-				</div>
-			</sequential-entrance>
-			<ui-button v-if="existMoreLogs" @click="fetchLogs">{{ $t('@.load-more') }}</ui-button>
-		</section>
-	</ui-card>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-import parseAcct from "../../../../misc/acct/parse";
-
-export default Vue.extend({
-	i18n: i18n('admin/views/moderators.vue'),
-
-	data() {
-		return {
-			username: '',
-			changing: false,
-			logs: [],
-			untilLogId: null,
-			existMoreLogs: false
-		};
-	},
-
-	created() {
-		this.fetchLogs();
-	},
-
-	methods: {
-		async add() {
-			this.changing = true;
-
-			const process = async () => {
-				const user = await this.$root.api('users/show', parseAcct(this.username));
-				await this.$root.api('admin/moderators/add', { userId: user.id });
-				this.$root.dialog({
-					type: 'success',
-					text: this.$t('add-moderator.added')
-				});
-			};
-
-			await process().catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e.toString()
-				});
-			});
-
-			this.changing = false;
-		},
-
-		async remove() {
-			this.changing = true;
-
-			const process = async () => {
-				const user = await this.$root.api('users/show', parseAcct(this.username));
-				await this.$root.api('admin/moderators/remove', { userId: user.id });
-				this.$root.dialog({
-					type: 'success',
-					text: this.$t('add-moderator.removed')
-				});
-			};
-
-			await process().catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e.toString()
-				});
-			});
-
-			this.changing = false;
-		},
-
-		fetchLogs() {
-			this.$root.api('admin/show-moderation-logs', {
-				untilId: this.untilId,
-				limit: 10 + 1
-			}).then(logs => {
-				if (logs.length == 10 + 1) {
-					logs.pop();
-					this.existMoreLogs = true;
-				} else {
-					this.existMoreLogs = false;
-				}
-				this.logs = this.logs.concat(logs);
-				this.untilLogId = this.logs[this.logs.length - 1].id;
-			});
-		},
-	}
-});
-</script>
diff --git a/src/client/app/admin/views/queue.chart.vue b/src/client/app/admin/views/queue.chart.vue
deleted file mode 100644
index ff29aa8392c90e341dbd0b357802589cf689f396..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/queue.chart.vue
+++ /dev/null
@@ -1,181 +0,0 @@
-<template>
-<div>
-	<ui-info warn v-if="latestStats && latestStats.waiting > 0">The queue is jammed.</ui-info>
-	<ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
-		<ui-input :value="latestStats.activeSincePrevTick | number" type="text" readonly>
-			<span>Process</span>
-			<template #prefix><fa :icon="fasPlayCircle"/></template>
-			<template #suffix>jobs/tick</template>
-		</ui-input>
-		<ui-input :value="latestStats.active | number" type="text" readonly>
-			<span>Active</span>
-			<template #prefix><fa :icon="farPlayCircle"/></template>
-			<template #suffix>jobs</template>
-		</ui-input>
-		<ui-input :value="latestStats.waiting | number" type="text" readonly>
-			<span>Waiting</span>
-			<template #prefix><fa :icon="faStopCircle"/></template>
-			<template #suffix>jobs</template>
-		</ui-input>
-		<ui-input :value="latestStats.delayed | number" type="text" readonly>
-			<span>Delayed</span>
-			<template #prefix><fa :icon="faStopwatch"/></template>
-			<template #suffix>jobs</template>
-		</ui-input>
-	</ui-horizon-group>
-	<div ref="chart" class="wptihjuy"></div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-import ApexCharts from 'apexcharts';
-import * as tinycolor from 'tinycolor2';
-import { faStopwatch, faPlayCircle as fasPlayCircle } from '@fortawesome/free-solid-svg-icons';
-import { faStopCircle, faPlayCircle as farPlayCircle } from '@fortawesome/free-regular-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('admin/views/queue.vue'),
-
-	props: {
-		type: {
-			type: String,
-			required: true
-		},
-		connection: {
-			required: true
-		},
-		limit: {
-			type: Number,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			stats: [],
-			chart: null,
-			faStopwatch, faStopCircle, farPlayCircle, fasPlayCircle
-		};
-	},
-
-	computed: {
-		latestStats(): any {
-			return this.stats.length > 0 ? this.stats[this.stats.length - 1][this.type] : null;
-		}
-	},
-
-	watch: {
-		stats(stats) {
-			this.chart.updateSeries([{
-				name: 'Process',
-				type: 'area',
-				data: stats.map((x, i) => ({ x: i, y: x[this.type].activeSincePrevTick }))
-			}, {
-				name: 'Active',
-				type: 'area',
-				data: stats.map((x, i) => ({ x: i, y: x[this.type].active }))
-			}, {
-				name: 'Waiting',
-				type: 'line',
-				data: stats.map((x, i) => ({ x: i, y: x[this.type].waiting }))
-			}, {
-				name: 'Delayed',
-				type: 'line',
-				data: stats.map((x, i) => ({ x: i, y: x[this.type].delayed }))
-			}]);
-		},
-	},
-
-	mounted() {
-		this.chart = new ApexCharts(this.$refs.chart, {
-			chart: {
-				id: this.type,
-				group: 'queue',
-				type: 'area',
-				height: 200,
-				animations: {
-					dynamicAnimation: {
-						enabled: false
-					}
-				},
-				toolbar: {
-					show: false
-				},
-				zoom: {
-					enabled: false
-				}
-			},
-			dataLabels: {
-				enabled: false
-			},
-			grid: {
-				clipMarkers: false,
-				borderColor: 'rgba(0, 0, 0, 0.1)',
-				xaxis: {
-					lines: {
-						show: true,
-					}
-				},
-			},
-			stroke: {
-				curve: 'straight',
-				width: 2
-			},
-			tooltip: {
-				enabled: false
-			},
-			legend: {
-				labels: {
-					colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
-				},
-			},
-			series: [] as any,
-			colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'],
-			xaxis: {
-				type: 'numeric',
-				labels: {
-					show: false
-				},
-				tooltip: {
-					enabled: false
-				}
-			},
-			yaxis: {
-				show: false,
-				min: 0,
-			}
-		});
-
-		this.chart.render();
-
-		this.connection.on('stats', this.onStats);
-		this.connection.on('statsLog', this.onStatsLog);
-
-		this.$once('hook:beforeDestroy', () => {
-			if (this.chart) this.chart.destroy();
-		});
-	},
-
-	methods: {
-		onStats(stats) {
-			this.stats.push(stats);
-			if (this.stats.length > this.limit) this.stats.shift();
-		},
-
-		onStatsLog(statsLog) {
-			for (const stats of statsLog.reverse()) {
-				this.onStats(stats);
-			}
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.wptihjuy
-	min-height 200px !important
-	margin -8px
-
-</style>
diff --git a/src/client/app/admin/views/queue.vue b/src/client/app/admin/views/queue.vue
deleted file mode 100644
index 9aa740c68c2b26449b8e546367dc745220d77291..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/queue.vue
+++ /dev/null
@@ -1,159 +0,0 @@
-<template>
-<div>
-	<ui-card>
-		<template #title><fa :icon="faChartBar"/> {{ $t('title') }}</template>
-		<section>
-			<header><fa :icon="faPaperPlane"/> {{ $t('domains.deliver') }}</header>
-			<x-chart v-if="connection" :connection="connection" :limit="chartLimit" type="deliver"/>
-		</section>
-		<section>
-			<header><fa :icon="faInbox"/> {{ $t('domains.inbox') }}</header>
-			<x-chart v-if="connection" :connection="connection" :limit="chartLimit" type="inbox"/>
-		</section>
-		<section>
-			<details>
-				<summary>{{ $t('other-queues') }}</summary>
-				<section>
-					<header><fa :icon="faDatabase"/> {{ $t('domains.db') }}</header>
-					<x-chart v-if="connection" :connection="connection" :limit="chartLimit" type="db"/>
-				</section>
-				<ui-hr/>
-				<section>
-					<header><fa :icon="faCloud"/> {{ $t('domains.objectStorage') }}</header>
-					<x-chart v-if="connection" :connection="connection" :limit="chartLimit" type="objectStorage"/>
-				</section>
-			</details>
-		</section>
-		<section>
-			<ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button>
-		</section>
-	</ui-card>
-
-	<ui-card>
-		<template #title><fa :icon="faTasks"/> {{ $t('jobs') }}</template>
-		<section class="fit-top">
-			<ui-horizon-group inputs>
-				<ui-select v-model="domain">
-					<template #label>{{ $t('queue') }}</template>
-					<option value="deliver">{{ $t('domains.deliver') }}</option>
-					<option value="inbox">{{ $t('domains.inbox') }}</option>
-					<option value="db">{{ $t('domains.db') }}</option>
-					<option value="objectStorage">{{ $t('domains.objectStorage') }}</option>
-				</ui-select>
-				<ui-select v-model="state">
-					<template #label>{{ $t('state') }}</template>
-					<option value="active">{{ $t('states.active') }}</option>
-					<option value="waiting">{{ $t('states.waiting') }}</option>
-					<option value="delayed">{{ $t('states.delayed') }}</option>
-				</ui-select>
-			</ui-horizon-group>
-			<sequential-entrance animation="entranceFromTop" delay="25">
-				<div class="xvvuvgsv" v-for="job in jobs" :key="job.id">
-					<b>{{ job.id }}</b>
-					<template v-if="domain === 'deliver'">
-						<span>{{ job.data.to }}</span>
-					</template>
-					<template v-if="domain === 'inbox'">
-						<span>{{ job.data.activity.id }}</span>
-					</template>
-					<span>{{ `(${job.attempts}/${job.maxAttempts}, ${Math.floor((jobsFetched - job.timestamp) / 1000 / 60)}min)` }}</span>
-				</div>
-			</sequential-entrance>
-			<ui-info v-if="jobs.length == jobsLimit">{{ $t('result-is-truncated', { n: jobsLimit }) }}</ui-info>
-		</section>
-	</ui-card>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { faTasks, faInbox, faDatabase, faCloud } from '@fortawesome/free-solid-svg-icons';
-import { faPaperPlane, faChartBar } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../../i18n';
-import XChart from './queue.chart.vue';
-
-export default Vue.extend({
-	i18n: i18n('admin/views/queue.vue'),
-
-	components: {
-		XChart
-	},
-
-	data() {
-		return {
-			connection: null,
-			chartLimit: 200,
-			jobs: [],
-			jobsLimit: 50,
-			jobsFetched: Date.now(),
-			domain: 'deliver',
-			state: 'delayed',
-			faTasks, faPaperPlane, faInbox, faChartBar, faDatabase, faCloud
-		};
-	},
-
-	watch: {
-		domain() {
-			this.jobs = [];
-			this.fetchJobs();
-		},
-
-		state() {
-			this.jobs = [];
-			this.fetchJobs();
-		},
-	},
-
-	mounted() {
-		this.fetchJobs();
-
-		this.connection = this.$root.stream.useSharedConnection('queueStats');
-		this.connection.send('requestLog', {
-			id: Math.random().toString().substr(2, 8),
-			length: this.chartLimit
-		});
-
-		this.$once('hook:beforeDestroy', () => {
-			this.connection.dispose();
-		});
-	},
-
-	methods: {
-		async removeAllJobs() {
-			const process = async () => {
-				await this.$root.api('admin/queue/clear');
-				this.$root.dialog({
-					type: 'success',
-					splash: true
-				});
-			};
-
-			await process().catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e.toString()
-				});
-			});
-		},
-
-		fetchJobs() {
-			this.$root.api('admin/queue/jobs', {
-				domain: this.domain,
-				state: this.state,
-				limit: this.jobsLimit
-			}).then(jobs => {
-				this.jobsFetched = Date.now(),
-				this.jobs = jobs;
-			});
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.xvvuvgsv
-	margin-left -6px
-	> b, span
-		margin 0 6px
-
-</style>
diff --git a/src/client/app/admin/views/users.user.vue b/src/client/app/admin/views/users.user.vue
deleted file mode 100644
index 9c3db2d6c2a8838b0423ad6b21bb278bcce312f9..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/users.user.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<template>
-<div class="kofvwchc">
-	<div>
-		<a :href="user | userPage(null, true)">
-			<mk-avatar class="avatar" :user="user" :disable-link="true"/>
-		</a>
-	</div>
-	<div @click="click(user.id)">
-		<header>
-			<b><mk-user-name :user="user"/></b>
-			<span class="username">@{{ user | acct }}</span>
-			<span class="is-admin" v-if="user.isAdmin">admin</span>
-			<span class="is-moderator" v-if="user.isModerator">moderator</span>
-			<span class="is-silenced" v-if="user.isSilenced" :title="$t('@.silenced-user')"><fa :icon="faMicrophoneSlash"/></span>
-			<span class="is-suspended" v-if="user.isSuspended" :title="$t('@.suspended-user')"><fa :icon="faSnowflake"/></span>
-		</header>
-		<div>
-			<span>{{ $t('users.updatedAt') }}: <mk-time :time="user.updatedAt" mode="detail"/></span>
-		</div>
-		<div>
-			<span>{{ $t('users.createdAt') }}: <mk-time :time="user.createdAt" mode="detail"/></span>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-import { faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons';
-import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('admin/views/users.vue'),
-	props: ['user', 'click'],
-	data() {
-		return {
-			faSnowflake, faMicrophoneSlash
-		};
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-.kofvwchc
-	display flex
-	padding 16px
-	border-top solid 1px var(--faceDivider)
-
-	> div:first-child
-		> a
-			> .avatar
-				width 64px
-				height 64px
-
-	> div:last-child
-		flex 1
-		cursor pointer
-		padding-left 16px
-
-		@media (max-width 500px)
-			font-size 14px
-
-		> header
-			> .username
-				margin-left 8px
-				opacity 0.7
-
-			> .is-admin
-			> .is-moderator
-				flex-shrink 0
-				align-self center
-				margin 0 0 0 .5em
-				padding 1px 6px
-				font-size 80%
-				border-radius 3px
-				background var(--noteHeaderAdminBg)
-				color var(--noteHeaderAdminFg)
-
-			> .is-silenced
-			> .is-suspended
-				margin 0 0 0 .5em
-				color #4dabf7
-
-	&:hover
-		color var(--primaryForeground)
-		background var(--primary)
-		text-decoration none
-		border-radius 3px
-
-	&:active
-		color var(--primaryForeground)
-		background var(--primaryDarken10)
-		border-radius 3px
-</style>
diff --git a/src/client/app/admin/views/users.vue b/src/client/app/admin/views/users.vue
deleted file mode 100644
index 920bfc381e9bc7d6621b07fa3fdf4fd70f97fd6a..0000000000000000000000000000000000000000
--- a/src/client/app/admin/views/users.vue
+++ /dev/null
@@ -1,366 +0,0 @@
-<template>
-<div>
-	<ui-card>
-		<template #title><fa :icon="faTerminal"/> {{ $t('operation') }}</template>
-		<section class="fit-top">
-			<ui-input class="target" v-model="target" type="text" @enter="showUser">
-				<span>{{ $t('username-or-userid') }}</span>
-			</ui-input>
-			<ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
-
-			<div ref="user" class="user" v-if="user" :key="user.id">
-				<x-user :user="user"/>
-				<div class="actions">
-					<ui-button v-if="user.host != null" @click="updateRemoteUser"><fa :icon="faSync"/> {{ $t('update-remote-user') }}</ui-button>
-					<ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button>
-					<ui-horizon-group>
-						<ui-button @click="silenceUser"><fa :icon="faMicrophoneSlash"/> {{ $t('make-silence') }}</ui-button>
-						<ui-button @click="unsilenceUser">{{ $t('unmake-silence') }}</ui-button>
-					</ui-horizon-group>
-					<ui-horizon-group>
-						<ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button>
-						<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
-					</ui-horizon-group>
-					<ui-button @click="deleteAllFiles"><fa :icon="faTrashAlt"/> {{ $t('delete-all-files') }}</ui-button>
-					<ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea>
-				</div>
-			</div>
-		</section>
-	</ui-card>
-
-	<ui-card>
-		<template #title><fa :icon="faUsers"/> {{ $t('users.title') }}</template>
-		<section class="fit-top">
-			<ui-horizon-group inputs>
-				<ui-select v-model="sort">
-					<template #label>{{ $t('users.sort.title') }}</template>
-					<option value="-createdAt">{{ $t('users.sort.createdAtAsc') }}</option>
-					<option value="+createdAt">{{ $t('users.sort.createdAtDesc') }}</option>
-					<option value="-updatedAt">{{ $t('users.sort.updatedAtAsc') }}</option>
-					<option value="+updatedAt">{{ $t('users.sort.updatedAtDesc') }}</option>
-				</ui-select>
-				<ui-select v-model="state">
-					<template #label>{{ $t('users.state.title') }}</template>
-					<option value="all">{{ $t('users.state.all') }}</option>
-					<option value="available">{{ $t('users.state.available') }}</option>
-					<option value="admin">{{ $t('users.state.admin') }}</option>
-					<option value="moderator">{{ $t('users.state.moderator') }}</option>
-					<option value="silenced">{{ $t('users.state.silenced') }}</option>
-					<option value="suspended">{{ $t('users.state.suspended') }}</option>
-				</ui-select>
-				<ui-select v-model="origin">
-					<template #label>{{ $t('users.origin.title') }}</template>
-					<option value="combined">{{ $t('users.origin.combined') }}</option>
-					<option value="local">{{ $t('users.origin.local') }}</option>
-					<option value="remote">{{ $t('users.origin.remote') }}</option>
-				</ui-select>
-			</ui-horizon-group>
-			<ui-horizon-group searchboxes>
-				<ui-input v-model="searchUsername" type="text" spellcheck="false" @input="fetchUsers(true)">
-					<span>{{ $t('username') }}</span>
-				</ui-input>
-				<ui-input v-model="searchHost" type="text" spellcheck="false" @input="fetchUsers(true)" :disabled="origin === 'local'">
-					<span>{{ $t('host') }}</span>
-				</ui-input>
-			</ui-horizon-group>
-			<sequential-entrance animation="entranceFromTop" delay="25">
-				<x-user v-for="user in users" :key="user.id" :user='user' :click="showUserOnClick"/>
-			</sequential-entrance>
-			<ui-button v-if="existMore" @click="fetchUsers">{{ $t('@.load-more') }}</ui-button>
-		</section>
-	</ui-card>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-import parseAcct from "../../../../misc/acct/parse";
-import { faUsers, faTerminal, faSearch, faKey, faSync, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons';
-import { faSnowflake, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
-import XUser from './users.user.vue';
-
-export default Vue.extend({
-	i18n: i18n('admin/views/users.vue'),
-	components: {
-		XUser
-	},
-	data() {
-		return {
-			user: null,
-			target: null,
-			suspending: false,
-			unsuspending: false,
-			sort: '+createdAt',
-			state: 'all',
-			origin: 'local',
-			searchUsername: '',
-			searchHost: '',
-			limit: 10,
-			offset: 0,
-			users: [],
-			existMore: false,
-			faTerminal, faUsers, faSnowflake, faSearch, faKey, faSync, faMicrophoneSlash, faTrashAlt
-		};
-	},
-
-	watch: {
-		sort() {
-			this.users = [];
-			this.offset = 0;
-			this.fetchUsers();
-		},
-
-		state() {
-			this.users = [];
-			this.offset = 0;
-			this.fetchUsers();
-		},
-
-		origin() {
-			if (this.origin === 'local') this.searchHost = '';
-			this.users = [];
-			this.offset = 0;
-			this.fetchUsers();
-		}
-	},
-
-	mounted() {
-		this.fetchUsers();
-	},
-
-	methods: {
-		/** テキストエリアのユーザーを解決する */
-		fetchUser() {
-			return new Promise((res) => {
-				const usernamePromise = this.$root.api('users/show', parseAcct(this.target));
-				const idPromise = this.$root.api('users/show', { userId: this.target });
-
-				let _notFound = false;
-				const notFound = () => {
-					if (_notFound) {
-						this.$root.dialog({
-							type: 'error',
-							text: this.$t('user-not-found')
-						});
-					} else {
-						_notFound = true;
-					}
-				};
-
-				usernamePromise.then(res).catch(e => {
-					if (e == 'user not found') {
-						notFound();
-					}
-				});
-				idPromise.then(res).catch(e => {
-					notFound();
-				});
-			});
-		},
-
-		/** テキストエリアから処理対象ユーザーを設定する */
-		async showUser() {
-			this.user = null;
-			const user = await this.fetchUser();
-			this.$root.api('admin/show-user', { userId: user.id }).then(info => {
-				this.user = info;
-			});
-			this.target = '';
-		},
-
-		async showUserOnClick(userId: string) {
-			this.$root.api('admin/show-user', { userId: userId }).then(info => {
-				this.user = info;
-				this.$nextTick(() => {
-					this.$refs.user.scrollIntoView();
-				});
-			});
-		},
-
-		/** 処理対象ユーザーの情報を更新する */
-		async refreshUser() {
-			this.$root.api('admin/show-user', { userId: this.user.id }).then(info => {
-				this.user = info;
-			});
-		},
-
-		async resetPassword() {
-			if (!await this.getConfirmed(this.$t('reset-password-confirm'))) return;
-
-			this.$root.api('admin/reset-password', { userId: this.user.id }).then(res => {
-				this.$root.dialog({
-					type: 'success',
-					text: this.$t('password-updated', { password: res.password })
-				});
-			});
-		},
-
-		async silenceUser() {
-			if (!await this.getConfirmed(this.$t('silence-confirm'))) return;
-
-			const process = async () => {
-				await this.$root.api('admin/silence-user', { userId: this.user.id });
-				this.$root.dialog({
-					type: 'success',
-					splash: true
-				});
-			};
-
-			await process().catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e.toString()
-				});
-			});
-
-			this.refreshUser();
-		},
-
-		async unsilenceUser() {
-			if (!await this.getConfirmed(this.$t('unsilence-confirm'))) return;
-
-			const process = async () => {
-				await this.$root.api('admin/unsilence-user', { userId: this.user.id });
-				this.$root.dialog({
-					type: 'success',
-					splash: true
-				});
-			};
-
-			await process().catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e.toString()
-				});
-			});
-
-			this.refreshUser();
-		},
-
-		async suspendUser() {
-			if (!await this.getConfirmed(this.$t('suspend-confirm'))) return;
-
-			this.suspending = true;
-
-			const process = async () => {
-				await this.$root.api('admin/suspend-user', { userId: this.user.id });
-				this.$root.dialog({
-					type: 'success',
-					text: this.$t('suspended')
-				});
-			};
-
-			await process().catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e.toString()
-				});
-			});
-
-			this.suspending = false;
-
-			this.refreshUser();
-		},
-
-		async unsuspendUser() {
-			if (!await this.getConfirmed(this.$t('unsuspend-confirm'))) return;
-
-			this.unsuspending = true;
-
-			const process = async () => {
-				await this.$root.api('admin/unsuspend-user', { userId: this.user.id });
-				this.$root.dialog({
-					type: 'success',
-					text: this.$t('unsuspended')
-				});
-			};
-
-			await process().catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e.toString()
-				});
-			});
-
-			this.unsuspending = false;
-
-			this.refreshUser();
-		},
-
-		async updateRemoteUser() {
-			this.$root.api('admin/update-remote-user', { userId: this.user.id }).then(res => {
-				this.$root.dialog({
-					type: 'success',
-					text: this.$t('remote-user-updated')
-				});
-			});
-
-			this.refreshUser();
-		},
-
-		async deleteAllFiles() {
-			if (!await this.getConfirmed(this.$t('delete-all-files-confirm'))) return;
-
-			const process = async () => {
-				await this.$root.api('admin/delete-all-files-of-a-user', { userId: this.user.id });
-				this.$root.dialog({
-					type: 'success',
-					splash: true
-				});
-			};
-
-			await process().catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e.toString()
-				});
-			});
-		},
-
-		async getConfirmed(text: string): Promise<Boolean> {
-			const confirm = await this.$root.dialog({
-				type: 'warning',
-				showCancelButton: true,
-				title: 'confirm',
-				text,
-			});
-
-			return !confirm.canceled;
-		},
-
-		fetchUsers(truncate?: boolean) {
-			if (truncate) this.offset = 0;
-			this.$root.api('admin/show-users', {
-				state: this.state,
-				origin: this.origin,
-				sort: this.sort,
-				offset: this.offset,
-				limit: this.limit + 1,
-				username: this.searchUsername,
-				hostname: this.searchHost
-			}).then(users => {
-				if (users.length == this.limit + 1) {
-					users.pop();
-					this.existMore = true;
-				} else {
-					this.existMore = false;
-				}
-				this.users = truncate ? users : this.users.concat(users);
-				this.offset += this.limit;
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.target
-	margin-bottom 16px !important
-
-.user
-	margin-top 32px
-
-	> .actions
-		margin-left 80px
-</style>
diff --git a/src/client/app/animation.styl b/src/client/app/animation.styl
deleted file mode 100644
index 6c4d5b8b6fa93873fbdff9e624f5a42399aed1b2..0000000000000000000000000000000000000000
--- a/src/client/app/animation.styl
+++ /dev/null
@@ -1,47 +0,0 @@
-.zoom-in-top-enter-active,
-.zoom-in-top-leave-active {
-	opacity: 1;
-	transform: scaleY(1);
-	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
-	transform-origin: center top;
-}
-.zoom-in-top-enter,
-.zoom-in-top-leave-active {
-	opacity: 0;
-	transform: scaleY(0);
-}
-
-.entranceFromTop {
-	animation-duration: 0.5s;
-	animation-name: entranceFromTop;
-}
-
-@keyframes entranceFromTop {
-	from {
-		opacity: 0;
-		transform: translateY(-64px);
-	}
-	to {
-		opacity: 1;
-		transform: translateY(0);
-	}
-}
-
-@keyframes spin {
-	0% { transform: rotate(0deg); }
-	100% { transform: rotate(360deg); }
-}
-
-@keyframes jump {
-	0% { transform: translateY(0); }
-	25% { transform: translateY(-16px); }
-	50% { transform: translateY(0); }
-	75% { transform: translateY(-8px); }
-	100% { transform: translateY(0); }
-}
-
-@keyframes blink {
-	0% { opacity: 1; }
-	30% { opacity: 1; }
-	90% { opacity: 0; }
-}
diff --git a/src/client/app/app.styl b/src/client/app/app.styl
deleted file mode 100644
index 6389aa0a875f6c8158cb0191fef555ca6ff22b5f..0000000000000000000000000000000000000000
--- a/src/client/app/app.styl
+++ /dev/null
@@ -1,84 +0,0 @@
-@import "../style"
-@import "../animation"
-
-html
-	&.progress
-		&, *
-			cursor progress !important
-
-html
-	// iOSのため
-	overflow auto
-
-body
-	overflow-wrap break-word
-
-#nprogress
-	pointer-events none
-
-	position absolute
-	z-index 65536
-
-	.bar
-		background var(--primary)
-
-		position fixed
-		z-index 65537
-		top 0
-		left 0
-
-		width 100%
-		height 2px
-
-	/* Fancy blur effect */
-	.peg
-		display block
-		position absolute
-		right 0
-		width 100px
-		height 100%
-		box-shadow 0 0 10px var(--primary), 0 0 5px var(--primary)
-		opacity 1
-
-		transform rotate(3deg) translate(0px, -4px)
-
-#wait
-	display block
-	position fixed
-	z-index 65537
-	top 15px
-	right 15px
-
-	&:before
-		content ""
-		display block
-		width 18px
-		height 18px
-		box-sizing border-box
-
-		border solid 2px transparent
-		border-top-color var(--primary)
-		border-left-color var(--primary)
-		border-radius 50%
-
-		animation progress-spinner 400ms linear infinite
-
-	@keyframes progress-spinner
-		0%
-			transform rotate(0deg)
-		100%
-			transform rotate(360deg)
-
-code
-	font-family Consolas, 'Courier New', Courier, Monaco, monospace
-
-pre
-	display block
-
-	> code
-		display block
-		overflow auto
-		tab-size 2
-
-[data-icon]
-	display inline-block
diff --git a/src/client/app/app.vue b/src/client/app/app.vue
deleted file mode 100644
index e639c9f9ac6b400baefd9ab10ab8b177ae9b8930..0000000000000000000000000000000000000000
--- a/src/client/app/app.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<template>
-<router-view id="app" v-hotkey.global="keymap"></router-view>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { url, lang } from './config';
-
-export default Vue.extend({
-	computed: {
-		keymap(): any {
-			return {
-				'h|slash': this.help,
-				'd': this.dark
-			};
-		}
-	},
-
-	methods: {
-		help() {
-			window.open(`${url}/docs/${lang}/keyboard-shortcut`, '_blank');
-		},
-
-		dark() {
-			this.$store.commit('device/set', {
-				key: 'darkmode',
-				value: !this.$store.state.device.darkmode
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/app/auth/assets/icon.svg b/src/client/app/auth/assets/icon.svg
deleted file mode 100644
index 36f5d3e40478767d5e9d111a10411b28ccdd8baa..0000000000000000000000000000000000000000
Binary files a/src/client/app/auth/assets/icon.svg and /dev/null differ
diff --git a/src/client/app/auth/script.ts b/src/client/app/auth/script.ts
deleted file mode 100644
index 91bb24b1085e4fcc9f1f75dd541d60e8d70b1727..0000000000000000000000000000000000000000
--- a/src/client/app/auth/script.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Authorize Form
- */
-
-import VueRouter from 'vue-router';
-
-// Style
-import './style.styl';
-
-import init from '../init';
-import Index from './views/index.vue';
-import NotFound from '../common/views/pages/not-found.vue';
-
-/**
- * init
- */
-init(launch => {
-	// Init router
-	const router = new VueRouter({
-		mode: 'history',
-		base: '/auth/',
-		routes: [
-			{ path: '/:token', component: Index },
-			{ path: '*', component: NotFound }
-		]
-	});
-
-	// Launch the app
-	launch(router);
-});
diff --git a/src/client/app/auth/style.styl b/src/client/app/auth/style.styl
deleted file mode 100644
index bd25e1b572fcf36cb2208394c3c277b7a4f4fba0..0000000000000000000000000000000000000000
--- a/src/client/app/auth/style.styl
+++ /dev/null
@@ -1,15 +0,0 @@
-@import "../app"
-@import "../reset"
-
-html
-	background #eee
-
-	@media (max-width 600px)
-		background #fff
-
-body
-	margin 0
-	padding 32px 0
-
-	@media (max-width 600px)
-		padding 0
diff --git a/src/client/app/auth/views/form.vue b/src/client/app/auth/views/form.vue
deleted file mode 100644
index 064dbf388705af251e42a69612fe166a082d2011..0000000000000000000000000000000000000000
--- a/src/client/app/auth/views/form.vue
+++ /dev/null
@@ -1,141 +0,0 @@
-<template>
-<div class="form">
-	<header>
-		<h1 v-html="$t('share-access', { name })"></h1>
-		<img :src="app.iconUrl"/>
-	</header>
-	<div class="app">
-		<section>
-			<h2>{{ app.name }}</h2>
-			<p class="id">{{ app.id }}</p>
-			<p class="description">{{ app.description }}</p>
-		</section>
-		<section>
-			<h2>{{ $t('permission-ask') }}</h2>
-			<ul>
-				<template v-for="p in app.permission">
-					<li :key="p">{{ $t(`@.permissions.${p}`) }}</li>
-				</template>
-			</ul>
-		</section>
-	</div>
-	<div class="action">
-		<button @click="cancel">{{ $t('cancel') }}</button>
-		<button @click="accept">{{ $t('accept') }}</button>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('auth/views/form.vue'),
-	props: ['session'],
-	computed: {
-		name(): string {
-			const el = document.createElement('div');
-			el.textContent = this.app.name
-			return el.innerHTML;
-		},
-		app(): any {
-			return this.session.app;
-		}
-	},
-	methods: {
-		cancel() {
-			this.$root.api('auth/deny', {
-				token: this.session.token
-			}).then(() => {
-				this.$emit('denied');
-			});
-		},
-
-		accept() {
-			this.$root.api('auth/accept', {
-				token: this.session.token
-			}).then(() => {
-				this.$emit('accepted');
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.form
-
-	> header
-		> h1
-			margin 0
-			padding 32px 32px 20px 32px
-			font-size 24px
-			font-weight normal
-			color #777
-
-			i
-				color #77aeca
-
-				&:before
-					content '「'
-
-				&:after
-					content '」'
-
-			b
-				color #666
-
-		> img
-			display block
-			z-index 1
-			width 84px
-			height 84px
-			margin 0 auto -38px auto
-			border solid 5px #fff
-			border-radius 100%
-			box-shadow 0 2px 2px rgba(#000, 0.1)
-
-	> .app
-		padding 44px 16px 0 16px
-		color #555
-		background #eee
-		box-shadow 0 2px 2px rgba(#000, 0.1) inset
-
-		&:after
-			content ''
-			display block
-			clear both
-
-		> section
-			float left
-			width 50%
-			padding 8px
-			text-align left
-
-			> h2
-				margin 0
-				font-size 16px
-				color #777
-
-	> .action
-		padding 16px
-
-		> button
-			margin 0 8px
-			padding 0
-
-	@media (max-width 600px)
-		> header
-			> img
-				box-shadow none
-
-		> .app
-			box-shadow none
-
-	@media (max-width 500px)
-		> header
-			> h1
-				font-size 16px
-
-</style>
diff --git a/src/client/app/auth/views/index.vue b/src/client/app/auth/views/index.vue
deleted file mode 100644
index ad9b1e4e35b1da9154f9aaebc65e50945e4c0961..0000000000000000000000000000000000000000
--- a/src/client/app/auth/views/index.vue
+++ /dev/null
@@ -1,153 +0,0 @@
-<template>
-<div class="index">
-	<main v-if="$store.getters.isSignedIn">
-		<p class="fetching" v-if="fetching">{{ $t('loading') }}<mk-ellipsis/></p>
-		<x-form
-			class="form"
-			ref="form"
-			v-if="state == 'waiting'"
-			:session="session"
-			@denied="state = 'denied'"
-			@accepted="accepted"
-		/>
-		<div class="denied" v-if="state == 'denied'">
-			<h1>{{ $t('denied') }}</h1>
-			<p>{{ $t('denied-paragraph') }}</p>
-		</div>
-		<div class="accepted" v-if="state == 'accepted'">
-			<h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$t('allowed') }}</h1>
-			<p v-if="session.app.callbackUrl">{{ $t('callback-url') }}<mk-ellipsis/></p>
-			<p v-if="!session.app.callbackUrl">{{ $t('please-go-back') }}</p>
-		</div>
-		<div class="error" v-if="state == 'fetch-session-error'">
-			<p>{{ $t('error') }}</p>
-		</div>
-	</main>
-	<main class="signin" v-if="!$store.getters.isSignedIn">
-		<h1>{{ $t('sign-in') }}</h1>
-		<mk-signin/>
-	</main>
-	<footer><img src="/assets/auth/icon.svg" alt="Misskey"/></footer>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-import XForm from './form.vue';
-
-export default Vue.extend({
-	i18n: i18n('auth/views/index.vue'),
-	components: {
-		XForm
-	},
-	data() {
-		return {
-			state: null,
-			session: null,
-			fetching: true
-		};
-	},
-	computed: {
-		token(): string {
-			return this.$route.params.token;
-		}
-	},
-	mounted() {
-		if (!this.$store.getters.isSignedIn) return;
-
-		// Fetch session
-		this.$root.api('auth/session/show', {
-			token: this.token
-		}).then(session => {
-			this.session = session;
-			this.fetching = false;
-
-			// 既に連携していた場合
-			if (this.session.app.isAuthorized) {
-				this.$root.api('auth/accept', {
-					token: this.session.token
-				}).then(() => {
-					this.accepted();
-				});
-			} else {
-				this.state = 'waiting';
-			}
-		}).catch(error => {
-			this.state = 'fetch-session-error';
-			this.fetching = false;
-		});
-	},
-	methods: {
-		accepted() {
-			this.state = 'accepted';
-			if (this.session.app.callbackUrl) {
-				location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`;
-			}
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.index
-
-	> main
-		width 100%
-		max-width 500px
-		margin 0 auto
-		text-align center
-		background #fff
-		box-shadow 0 4px 16px rgba(#000, 0.2)
-
-		> .fetching
-			margin 0
-			padding 32px
-			color #555
-
-		> div:not(.form)
-			padding 64px
-
-			> h1
-				margin 0 0 8px 0
-				padding 0
-				font-size 20px
-				font-weight normal
-
-			> p
-				margin 0
-				color #555
-
-			&.denied > h1
-				color #e65050
-
-			&.accepted > h1
-				color #54af7c
-
-		&.signin
-			padding 32px 32px 16px 32px
-
-			> h1
-				margin 0 0 22px 0
-				padding 0
-				font-size 20px
-				font-weight normal
-				color #555
-
-		@media (max-width 600px)
-			max-width none
-			box-shadow none
-
-		@media (max-width 500px)
-			> div
-				> h1
-					font-size 16px
-
-	> footer
-		> img
-			display block
-			width 32px
-			height 32px
-			margin 16px auto
-
-</style>
diff --git a/src/client/app/boot.js b/src/client/app/boot.js
deleted file mode 100644
index 64d46298830b1be30373d280fda2f871a3702564..0000000000000000000000000000000000000000
--- a/src/client/app/boot.js
+++ /dev/null
@@ -1,171 +0,0 @@
-/**
- * MISSKEY BOOT LOADER
- * (ENTRY POINT)
- */
-
-'use strict';
-
-(async function() {
-	// キャッシュ削除要求があれば従う
-	if (localStorage.getItem('shouldFlush') == 'true') {
-		refresh();
-		return;
-	}
-
-	const langs = LANGS;
-
-	//#region Apply theme
-	const theme = localStorage.getItem('theme');
-	if (theme) {
-		for (const [k, v] of Object.entries(JSON.parse(theme))) {
-			document.documentElement.style.setProperty(`--${k}`, v.toString());
-		}
-	}
-	//#endregion
-
-	//#region Load settings
-	let settings = null;
-	const vuex = localStorage.getItem('vuex');
-	if (vuex) {
-		settings = JSON.parse(vuex);
-	}
-	//#endregion
-
-	// Get the current url information
-	const url = new URL(location.href);
-
-	//#region Detect app name
-	let app = null;
-
-	if (`${url.pathname}/`.startsWith('/docs/')) app = 'docs';
-	if (`${url.pathname}/`.startsWith('/dev/')) app = 'dev';
-	if (`${url.pathname}/`.startsWith('/auth/')) app = 'auth';
-	if (`${url.pathname}/`.startsWith('/admin/')) app = 'admin';
-	//#endregion
-
-	// Script version
-	const ver = localStorage.getItem('v') || VERSION;
-
-	//#region Detect the user language
-	let lang = null;
-
-	if (langs.includes(navigator.language)) {
-		lang = navigator.language;
-	} else {
-		lang = langs.find(x => x.split('-')[0] == navigator.language);
-
-		if (lang == null) {
-			// Fallback
-			lang = 'en-US';
-		}
-	}
-
-	if (settings && settings.device.lang &&
-		langs.includes(settings.device.lang))
-	{
-		lang = settings.device.lang;
-	}
-
-	localStorage.setItem('lang', lang);
-	//#endregion
-
-	//#region Fetch locale data
-	const cachedLocale = localStorage.getItem('locale');
-	const localeKey = localStorage.getItem('localeKey');
-	let localeData = null;
-
-	if (cachedLocale == null || localeKey != `${ver}.${lang}`) {
-		const locale = await fetch(`/assets/locales/${lang}.json?ver=${ver}`)
-			.then(response => response.json());
-		localeData = locale;
-
-		localStorage.setItem('locale', JSON.stringify(locale));
-		localStorage.setItem('localeKey', `${ver}.${lang}`);
-	} else {
-		localeData = JSON.parse(cachedLocale);
-	}
-	//#endregion
-
-	// Detect the user agent
-	const ua = navigator.userAgent.toLowerCase();
-	let isMobile = /mobile|iphone|ipad|android/.test(ua) || window.innerWidth < 576;
-	if (settings && settings.device.appTypeForce) {
-		if (settings.device.appTypeForce === 'mobile') {
-			isMobile = true;
-		} else if (settings.device.appTypeForce === 'desktop') {
-			isMobile = false;
-		}
-	}
-
-	// Get the <head> element
-	const head = document.getElementsByTagName('head')[0];
-
-	// If mobile, insert the viewport meta tag
-	if (isMobile) {
-		const viewport = document.getElementsByName("viewport").item(0);
-		viewport.content = `${viewport.content},minimum-scale=1,maximum-scale=1,user-scalable=no`;
-		head.appendChild(viewport);
-	}
-
-	// Switch desktop or mobile version
-	if (app == null) {
-		app = isMobile ? 'mobile' : 'desktop';
-	}
-
-	// Load an app script
-	// Note: 'async' make it possible to load the script asyncly.
-	//       'defer' make it possible to run the script when the dom loaded.
-	const script = document.createElement('script');
-	script.src = `/assets/${app}.${ver}.js`;
-	script.async = true;
-	script.defer = true;
-	head.appendChild(script);
-
-	// 3秒経ってもスクリプトがロードされない場合はバージョンが古くて
-	// 404になっているせいかもしれないので、バージョンを確認して古ければ更新する
-	//
-	// 読み込まれたスクリプトからこのタイマーを解除できるように、
-	// グローバルにタイマーIDを代入しておく
-	window.mkBootTimer = window.setTimeout(async () => {
-		// Fetch meta
-		const res = await fetch('/api/meta', {
-			method: 'POST',
-			cache: 'no-cache'
-		});
-
-		// Parse
-		const meta = await res.json();
-
-		// Compare versions
-		if (meta.version != ver) {
-			localStorage.setItem('v', meta.version);
-
-			alert(
-				localeData.common._settings["update-available"] +
-				'\n' +
-				localeData.common._settings["update-available-desc"]
-			);
-			refresh();
-		}
-	}, 3000);
-
-	function refresh() {
-		localStorage.setItem('shouldFlush', 'false');
-
-		localStorage.removeItem('locale');
-
-		// Clear cache (service worker)
-		try {
-			navigator.serviceWorker.controller.postMessage('clear');
-
-			navigator.serviceWorker.getRegistrations().then(registrations => {
-				for (const registration of registrations) registration.unregister();
-			});
-		} catch (e) {
-			console.error(e);
-		}
-
-		// Force reload
-		location.reload(true);
-	}
-})();
diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts
deleted file mode 100644
index d487915766ee07c5f823a1bc9cdfa29e3c16e896..0000000000000000000000000000000000000000
--- a/src/client/app/common/scripts/check-for-update.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { version as current } from '../../config';
-
-export default async function($root: any, force = false, silent = false) {
-	const meta = await $root.getMeta(force);
-	const newer = meta.version;
-
-	if (newer != current) {
-		localStorage.setItem('should-refresh', 'true');
-		localStorage.setItem('v', newer);
-
-		// Clear cache (service worker)
-		try {
-			if (navigator.serviceWorker.controller) {
-				navigator.serviceWorker.controller.postMessage('clear');
-			}
-
-			const registrations = await navigator.serviceWorker.getRegistrations();
-			for (const registration of registrations) {
-				registration.unregister();
-			}
-		} catch (e) {
-			console.error(e);
-		}
-
-		/*if (!silent) {
-			$root.dialog({
-				title: $root.$t('@.update-available-title'),
-				text: $root.$t('@.update-available', { newer, current })
-			});
-		}*/
-
-		return newer;
-	} else {
-		return null;
-	}
-}
diff --git a/src/client/app/common/scripts/format-uptime.ts b/src/client/app/common/scripts/format-uptime.ts
deleted file mode 100644
index 6550e4cc398bd4c6705024dd1dc310e5cdc9f7ed..0000000000000000000000000000000000000000
--- a/src/client/app/common/scripts/format-uptime.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-
-/**
- * Format like the uptime command
- */
-export default function(sec) {
-	if (!sec) return sec;
-
-	const day = Math.floor(sec / 86400);
-	const tod = sec % 86400;
-
-	// Days part in string: 2 days, 1 day, null
-	const d = day >= 2 ? `${day} days` : day >= 1 ? `${day} day` : null;
-
-	// Time part in string: 1 sec, 1 min, 1:01
-	const t
-		= tod < 60 ? `${Math.floor(tod)} sec`
-		: tod < 3600 ? `${Math.floor(tod / 60)} min`
-		: `${Math.floor(tod / 60 / 60)}:${Math.floor((tod / 60) % 60).toString().padStart(2, '0')}`;
-
-	let str = '';
-	if (d) str += `${d}, `;
-	str += t;
-
-	return str;
-}
diff --git a/src/client/app/common/scripts/get-face.ts b/src/client/app/common/scripts/get-face.ts
deleted file mode 100644
index 19f2bdb064f0f97253c0a4a0832be6ac3935eaa5..0000000000000000000000000000000000000000
--- a/src/client/app/common/scripts/get-face.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-const faces = [
-	'(=^・・^=)',
-	'v(\'ω\')v',
-	'🐡( \'-\' 🐡 )フグパンチ!!!!',
-	'✌️(´・_・`)✌️',
-	'(。>﹏<。)',
-	'(Δ・x・Δ)',
-	'(コ`・ヘ・´ケ)'
-];
-
-export default () => faces[Math.floor(Math.random() * faces.length)];
diff --git a/src/client/app/common/scripts/note-mixin.ts b/src/client/app/common/scripts/note-mixin.ts
deleted file mode 100644
index 84e134cc320bc75e1bf4637af33ef0f883852cc9..0000000000000000000000000000000000000000
--- a/src/client/app/common/scripts/note-mixin.ts
+++ /dev/null
@@ -1,239 +0,0 @@
-import { parse } from '../../../../mfm/parse';
-import { sum, unique } from '../../../../prelude/array';
-import shouldMuteNote from './should-mute-note';
-import MkNoteMenu from '../views/components/note-menu.vue';
-import MkReactionPicker from '../views/components/reaction-picker.vue';
-import pleaseLogin from './please-login';
-import i18n from '../../i18n';
-
-function focus(el, fn) {
-	const target = fn(el);
-	if (target) {
-		if (target.hasAttribute('tabindex')) {
-			target.focus();
-		} else {
-			focus(target, fn);
-		}
-	}
-}
-
-type Opts = {
-	mobile?: boolean;
-};
-
-export default (opts: Opts = {}) => ({
-	i18n: i18n(),
-
-	data() {
-		return {
-			showContent: false,
-			hideThisNote: false,
-			openingMenu: false
-		};
-	},
-
-	computed: {
-		keymap(): any {
-			return {
-				'r': () => this.reply(true),
-				'e|a|plus': () => this.react(true),
-				'q': () => this.renote(true),
-				'f|b': this.favorite,
-				'delete|ctrl+d': this.del,
-				'ctrl+q': this.renoteDirectly,
-				'up|k|shift+tab': this.focusBefore,
-				'down|j|tab': this.focusAfter,
-				//'esc': this.blur,
-				'm|o': () => this.menu(true),
-				's': this.toggleShowContent,
-				'1': () => this.reactDirectly('like'),
-				'2': () => this.reactDirectly('love'),
-				'3': () => this.reactDirectly('laugh'),
-				'4': () => this.reactDirectly('hmm'),
-				'5': () => this.reactDirectly('surprise'),
-				'6': () => this.reactDirectly('congrats'),
-				'7': () => this.reactDirectly('angry'),
-				'8': () => this.reactDirectly('confused'),
-				'9': () => this.reactDirectly('rip'),
-				'0': () => this.reactDirectly('pudding'),
-			};
-		},
-
-		isRenote(): boolean {
-			return (this.note.renote &&
-				this.note.text == null &&
-				this.note.fileIds.length == 0 &&
-				this.note.poll == null);
-		},
-
-		appearNote(): any {
-			return this.isRenote ? this.note.renote : this.note;
-		},
-
-		isMyNote(): boolean {
-			return this.$store.getters.isSignedIn && (this.$store.state.i.id === this.appearNote.userId);
-		},
-
-		reactionsCount(): number {
-			return this.appearNote.reactions
-				? sum(Object.values(this.appearNote.reactions))
-				: 0;
-		},
-
-		title(): string {
-			return '';
-		},
-
-		urls(): string[] {
-			if (this.appearNote.text) {
-				const ast = parse(this.appearNote.text);
-				// TODO: 再帰的にURL要素がないか調べる
-				const urls = unique(ast
-					.filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent))
-					.map(t => t.node.props.url));
-
-				// unique without hash
-				// [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ]
-				const removeHash = x => x.replace(/#[^#]*$/, '');
-
-				return urls.reduce((array, url) => {
-					const removed = removeHash(url);
-					if (!array.map(x => removeHash(x)).includes(removed)) array.push(url);
-					return array;
-				}, []);
-			} else {
-				return null;
-			}
-		}
-	},
-
-	created() {
-		this.hideThisNote = shouldMuteNote(this.$store.state.i, this.$store.state.settings, this.appearNote);
-	},
-
-	methods: {
-		reply(viaKeyboard = false) {
-			pleaseLogin(this.$root);
-			this.$root.$post({
-				reply: this.appearNote,
-				animation: !viaKeyboard,
-				cb: () => {
-					this.focus();
-				}
-			});
-		},
-
-		renote(viaKeyboard = false) {
-			pleaseLogin(this.$root);
-			this.$root.$post({
-				renote: this.appearNote,
-				animation: !viaKeyboard,
-				cb: () => {
-					this.focus();
-				}
-			});
-		},
-
-		renoteDirectly() {
-			(this as any).api('notes/create', {
-				renoteId: this.appearNote.id
-			});
-		},
-
-		react(viaKeyboard = false) {
-			pleaseLogin(this.$root);
-			this.blur();
-			const w = this.$root.new(MkReactionPicker, {
-				source: this.$refs.reactButton,
-				showFocus: viaKeyboard,
-				animation: !viaKeyboard
-			});
-			w.$once('chosen', reaction => {
-				this.$root.api('notes/reactions/create', {
-					noteId: this.appearNote.id,
-					reaction: reaction
-				}).then(() => {
-					w.close();
-				});
-			});
-			w.$once('closed', this.focus);
-		},
-
-		reactDirectly(reaction) {
-			this.$root.api('notes/reactions/create', {
-				noteId: this.appearNote.id,
-				reaction: reaction
-			});
-		},
-
-		undoReact(note) {
-			const oldReaction = note.myReaction;
-			if (!oldReaction) return;
-			this.$root.api('notes/reactions/delete', {
-				noteId: note.id
-			});
-		},
-
-		favorite() {
-			pleaseLogin(this.$root);
-			this.$root.api('notes/favorites/create', {
-				noteId: this.appearNote.id
-			}).then(() => {
-				this.$root.dialog({
-					type: 'success',
-					splash: true
-				});
-			});
-		},
-
-		del() {
-			this.$root.dialog({
-				type: 'warning',
-				text: this.$t('@.delete-confirm'),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				this.$root.api('notes/delete', {
-					noteId: this.appearNote.id
-				});
-			});
-		},
-
-		menu(viaKeyboard = false) {
-			if (this.openingMenu) return;
-			this.openingMenu = true;
-			const w = this.$root.new(MkNoteMenu, {
-				source: this.$refs.menuButton,
-				note: this.appearNote,
-				animation: !viaKeyboard
-			}).$once('closed', () => {
-				this.openingMenu = false;
-				this.focus();
-			});
-			this.$once('hook:beforeDestroy', () => {
-				w.destroyDom();
-			});
-		},
-
-		toggleShowContent() {
-			this.showContent = !this.showContent;
-		},
-
-		focus() {
-			this.$el.focus();
-		},
-
-		blur() {
-			this.$el.blur();
-		},
-
-		focusBefore() {
-			focus(this.$el, e => e.previousElementSibling);
-		},
-
-		focusAfter() {
-			focus(this.$el, e => e.nextElementSibling);
-		}
-	}
-});
diff --git a/src/client/app/common/scripts/note-subscriber.ts b/src/client/app/common/scripts/note-subscriber.ts
deleted file mode 100644
index 5b31a9f9d087a37d3c65bd78fa662e9ed9cbdb49..0000000000000000000000000000000000000000
--- a/src/client/app/common/scripts/note-subscriber.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import Vue from 'vue';
-
-export default prop => ({
-	data() {
-		return {
-			connection: null
-		};
-	},
-
-	computed: {
-		$_ns_note_(): any {
-			return this[prop];
-		},
-
-		$_ns_isRenote(): boolean {
-			return (this.$_ns_note_.renote != null &&
-				this.$_ns_note_.text == null &&
-				this.$_ns_note_.fileIds.length == 0 &&
-				this.$_ns_note_.poll == null);
-		},
-
-		$_ns_target(): any {
-			return this.$_ns_isRenote ? this.$_ns_note_.renote : this.$_ns_note_;
-		},
-	},
-
-	created() {
-		if (this.$store.getters.isSignedIn) {
-			this.connection = this.$root.stream;
-		}
-	},
-
-	mounted() {
-		this.capture(true);
-
-		if (this.$store.getters.isSignedIn) {
-			this.connection.on('_connected_', this.onStreamConnected);
-		}
-	},
-
-	beforeDestroy() {
-		this.decapture(true);
-
-		if (this.$store.getters.isSignedIn) {
-			this.connection.off('_connected_', this.onStreamConnected);
-		}
-	},
-
-	methods: {
-		capture(withHandler = false) {
-			if (this.$store.getters.isSignedIn) {
-				const data = {
-					id: this.$_ns_target.id
-				} as any;
-
-				if (
-					(this.$_ns_target.visibleUserIds || []).includes(this.$store.state.i.id) ||
-					(this.$_ns_target.mentions || []).includes(this.$store.state.i.id)
-				) {
-					data.read = true;
-				}
-
-				this.connection.send('sn', data);
-				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
-			}
-		},
-
-		decapture(withHandler = false) {
-			if (this.$store.getters.isSignedIn) {
-				this.connection.send('un', {
-					id: this.$_ns_target.id
-				});
-				if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated);
-			}
-		},
-
-		onStreamConnected() {
-			this.capture();
-		},
-
-		onStreamNoteUpdated(data) {
-			const { type, id, body } = data;
-
-			if (id !== this.$_ns_target.id) return;
-
-			switch (type) {
-				case 'reacted': {
-					const reaction = body.reaction;
-
-					if (this.$_ns_target.reactions == null) {
-						Vue.set(this.$_ns_target, 'reactions', {});
-					}
-
-					if (this.$_ns_target.reactions[reaction] == null) {
-						Vue.set(this.$_ns_target.reactions, reaction, 0);
-					}
-
-					// Increment the count
-					this.$_ns_target.reactions[reaction]++;
-
-					if (body.userId == this.$store.state.i.id) {
-						Vue.set(this.$_ns_target, 'myReaction', reaction);
-					}
-					break;
-				}
-
-				case 'unreacted': {
-					const reaction = body.reaction;
-
-					if (this.$_ns_target.reactions == null) {
-						return;
-					}
-
-					if (this.$_ns_target.reactions[reaction] == null) {
-						return;
-					}
-
-					// Decrement the count
-					if (this.$_ns_target.reactions[reaction] > 0) this.$_ns_target.reactions[reaction]--;
-
-					if (body.userId == this.$store.state.i.id) {
-						Vue.set(this.$_ns_target, 'myReaction', null);
-					}
-					break;
-				}
-
-				case 'pollVoted': {
-					const choice = body.choice;
-					this.$_ns_target.poll.choices[choice].votes++;
-					if (body.userId == this.$store.state.i.id) {
-						Vue.set(this.$_ns_target.poll.choices[choice], 'isVoted', true);
-					}
-					break;
-				}
-
-				case 'deleted': {
-					Vue.set(this.$_ns_target, 'deletedAt', body.deletedAt);
-					Vue.set(this.$_ns_target, 'renote', null);
-					this.$_ns_target.text = null;
-					this.$_ns_target.fileIds = [];
-					this.$_ns_target.poll = null;
-					this.$_ns_target.geo = null;
-					this.$_ns_target.cw = null;
-					break;
-				}
-			}
-		},
-	}
-});
diff --git a/src/client/app/common/scripts/room/furniture.ts b/src/client/app/common/scripts/room/furniture.ts
deleted file mode 100644
index 7734e32668699547a00291fef0d828190f1ed209..0000000000000000000000000000000000000000
--- a/src/client/app/common/scripts/room/furniture.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-export type RoomInfo = {
-	roomType: string;
-	carpetColor: string;
-	furnitures: Furniture[];
-};
-
-export type Furniture = {
-	id: string; // 同じ家具が複数ある場合にそれぞれを識別するためのIDであり、家具IDではない
-	type: string; // こっちが家具ID(chairとか)
-	position: {
-		x: number;
-		y: number;
-		z: number;
-	};
-	rotation: {
-		x: number;
-		y: number;
-		z: number;
-	};
-	props?: Record<string, any>;
-};
diff --git a/src/client/app/common/scripts/room/furnitures.json5 b/src/client/app/common/scripts/room/furnitures.json5
deleted file mode 100644
index 7c1a90a3f93b38f277389663e445e4123766e298..0000000000000000000000000000000000000000
--- a/src/client/app/common/scripts/room/furnitures.json5
+++ /dev/null
@@ -1,397 +0,0 @@
-// 家具メタデータ
-
-// 家具にはユーザーが設定できるプロパティを設定可能です:
-//
-// props: {
-//   <propname>: <proptype>
-// }
-//
-// proptype一覧:
-// * image ... 画像選択ダイアログを出し、その画像のURLが格納されます
-// * color ... 色選択コントロールを出し、選択された色が格納されます
-
-// 家具にカスタムテクスチャを適用できるようにするには、textureプロパティに以下の追加の情報を含めます:
-// 便宜上そのUVのどの部分にカスタムテクスチャを貼り合わせるかのエリアをテクスチャエリアと呼びます。
-// UVは1024*1024だと仮定します。
-//
-// <key>: {
-//   prop: <プロパティ名>,
-//   uv: {
-//     x: <テクスチャエリアX座標>,
-//     y: <テクスチャエリアY座標>,
-//     width: <テクスチャエリアの幅>,
-//     height: <テクスチャエリアの高さ>,
-//   },
-// }
-//
-// <key>には、カスタムテクスチャを適用したいメッシュ名を指定します
-// <プロパティ名>には、カスタムテクスチャとして使用する画像を格納するプロパティ(前述)名を指定します
-
-// 家具にカスタムカラーを適用できるようにするには、colorプロパティに以下の追加の情報を含めます:
-//
-// <key>: <プロパティ名>
-//
-// <key>には、カスタムカラーを適用したいマテリアル名を指定します
-// <プロパティ名>には、カスタムカラーとして使用する色を格納するプロパティ(前述)名を指定します
-
-[
-	{
-		id: "milk",
-		place: "floor"
-	},
-	{
-		id: "bed",
-		place: "floor"
-	},
-	{
-		id: "low-table",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Table: 'color'
-		}
-	},
-	{
-		id: "desk",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Board: 'color'
-		}
-	},
-	{
-		id: "chair",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Chair: 'color'
-		}
-	},
-	{
-		id: "chair2",
-		place: "floor",
-		props: {
-			color1: 'color',
-			color2: 'color'
-		},
-		color: {
-			Cushion: 'color1',
-			Leg: 'color2'
-		}
-	},
-	{
-		id: "fan",
-		place: "wall"
-	},
-	{
-		id: "pc",
-		place: "floor"
-	},
-	{
-		id: "plant",
-		place: "floor"
-	},
-	{
-		id: "plant2",
-		place: "floor"
-	},
-	{
-		id: "eraser",
-		place: "floor"
-	},
-	{
-		id: "pencil",
-		place: "floor"
-	},
-	{
-		id: "pudding",
-		place: "floor"
-	},
-	{
-		id: "cardboard-box",
-		place: "floor"
-	},
-	{
-		id: "cardboard-box2",
-		place: "floor"
-	},
-	{
-		id: "cardboard-box3",
-		place: "floor"
-	},
-	{
-		id: "book",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Cover: 'color'
-		}
-	},
-	{
-		id: "book2",
-		place: "floor"
-	},
-	{
-		id: "piano",
-		place: "floor"
-	},
-	{
-		id: "facial-tissue",
-		place: "floor"
-	},
-	{
-		id: "server",
-		place: "floor"
-	},
-	{
-		id: "moon",
-		place: "floor"
-	},
-	{
-		id: "corkboard",
-		place: "wall"
-	},
-	{
-		id: "mousepad",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Pad: 'color'
-		}
-	},
-	{
-		id: "monitor",
-		place: "floor",
-		props: {
-			screen: 'image'
-		},
-		texture: {
-			Screen: {
-				prop: 'screen',
-				uv: {
-					x: 0,
-					y: 434,
-					width: 1024,
-					height: 588,
-				},
-			},
-		},
-	},
-	{
-		id: "tv",
-		place: "floor",
-		props: {
-			screen: 'image'
-		},
-		texture: {
-			Screen: {
-				prop: 'screen',
-				uv: {
-					x: 0,
-					y: 434,
-					width: 1024,
-					height: 588,
-				},
-			},
-		},
-	},
-	{
-		id: "keyboard",
-		place: "floor"
-	},
-	{
-		id: "carpet-stripe",
-		place: "floor",
-		props: {
-			color1: 'color',
-			color2: 'color'
-		},
-		color: {
-			CarpetAreaA: 'color1',
-			CarpetAreaB: 'color2'
-		},
-	},
-	{
-		id: "mat",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Mat: 'color'
-		}
-	},
-	{
-		id: "color-box",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			main: 'color'
-		}
-	},
-	{
-		id: "wall-clock",
-		place: "wall"
-	},
-	{
-		id: "cube",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Cube: 'color'
-		}
-	},
-	{
-		id: "photoframe",
-		place: "wall",
-		props: {
-			photo: 'image',
-			color: 'color'
-		},
-		texture: {
-			Photo: {
-				prop: 'photo',
-				uv: {
-					x: 0,
-					y: 342,
-					width: 1024,
-					height: 683,
-				},
-			},
-		},
-		color: {
-			Frame: 'color'
-		}
-	},
-	{
-		id: "pinguin",
-		place: "floor",
-		props: {
-			body: 'color',
-			belly: 'color'
-		},
-		color: {
-			Body: 'body',
-			Belly: 'belly',
-		}
-	},
-	{
-		id: "rubik-cube",
-		place: "floor",
-	},
-	{
-		id: "poster-h",
-		place: "wall",
-		props: {
-			picture: 'image'
-		},
-		texture: {
-			Poster: {
-				prop: 'picture',
-				uv: {
-					x: 0,
-					y: 277,
-					width: 1024,
-					height: 745,
-				},
-			},
-		},
-	},
-	{
-		id: "poster-v",
-		place: "wall",
-		props: {
-			picture: 'image'
-		},
-		texture: {
-			Poster: {
-				prop: 'picture',
-				uv: {
-					x: 0,
-					y: 0,
-					width: 745,
-					height: 1024,
-				},
-			},
-		},
-	},
-	{
-		id: "sofa",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Sofa: 'color'
-		}
-	},
-	{
-		id: "spiral",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Step: 'color'
-		}
-	},
-	{
-		id: "bin",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Bin: 'color'
-		}
-	},
-	{
-		id: "cup-noodle",
-		place: "floor"
-	},
-	{
-		id: "holo-display",
-		place: "floor",
-		props: {
-			image: 'image'
-		},
-		texture: {
-			Image_Front: {
-				prop: 'image',
-				uv: {
-					x: 0,
-					y: 0,
-					width: 1024,
-					height: 1024,
-				},
-			},
-			Image_Back: {
-				prop: 'image',
-				uv: {
-					x: 0,
-					y: 0,
-					width: 1024,
-					height: 1024,
-				},
-			},
-		},
-	},
-	{
-		id: 'energy-drink',
-		place: "floor",
-	}
-]
diff --git a/src/client/app/common/scripts/room/room.ts b/src/client/app/common/scripts/room/room.ts
deleted file mode 100644
index c2a989c784af85043dcc564d4b3083e02131380f..0000000000000000000000000000000000000000
--- a/src/client/app/common/scripts/room/room.ts
+++ /dev/null
@@ -1,776 +0,0 @@
-import autobind from 'autobind-decorator';
-import { v4 as uuid } from 'uuid';
-import * as THREE from 'three';
-import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
-import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
-import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
-import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
-import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
-import { BloomPass } from 'three/examples/jsm/postprocessing/BloomPass.js';
-import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader.js';
-import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
-import { Furniture, RoomInfo } from './furniture';
-import { query as urlQuery } from '../../../../../prelude/url';
-const furnitureDefs = require('./furnitures.json5');
-
-THREE.ImageUtils.crossOrigin = '';
-
-type Options = {
-	graphicsQuality: Room['graphicsQuality'];
-	onChangeSelect: Room['onChangeSelect'];
-	useOrthographicCamera: boolean;
-};
-
-/**
- * MisskeyRoom Core Engine
- */
-export class Room {
-	private clock: THREE.Clock;
-	private scene: THREE.Scene;
-	private renderer: THREE.WebGLRenderer;
-	private camera: THREE.PerspectiveCamera | THREE.OrthographicCamera;
-	private controls: OrbitControls;
-	private composer: EffectComposer;
-	private mixers: THREE.AnimationMixer[] = [];
-	private furnitureControl: TransformControls;
-	private roomInfo: RoomInfo;
-	private graphicsQuality: 'cheep' | 'low' | 'medium' | 'high' | 'ultra';
-	private roomObj: THREE.Object3D;
-	private objects: THREE.Object3D[] = [];
-	private selectedObject: THREE.Object3D = null;
-	private onChangeSelect: Function;
-	private isTransformMode = false;
-	private renderFrameRequestId: number;
-
-	private get canvas(): HTMLCanvasElement {
-		return this.renderer.domElement;
-	}
-
-	private get furnitures(): Furniture[] {
-		return this.roomInfo.furnitures;
-	}
-
-	private set furnitures(furnitures: Furniture[]) {
-		this.roomInfo.furnitures = furnitures;
-	}
-
-	private get enableShadow() {
-		return this.graphicsQuality != 'cheep';
-	}
-
-	private get usePostFXs() {
-		return this.graphicsQuality !== 'cheep' && this.graphicsQuality !== 'low';
-	}
-
-	private get shadowQuality() {
-		return (
-			this.graphicsQuality === 'ultra' ? 16384 :
-			this.graphicsQuality === 'high' ? 8192 :
-			this.graphicsQuality === 'medium' ? 4096 :
-			this.graphicsQuality === 'low' ? 1024 :
-			0); // cheep
-	}
-
-	constructor(user, isMyRoom, roomInfo: RoomInfo, container, options: Options) {
-		this.roomInfo = roomInfo;
-		this.graphicsQuality = options.graphicsQuality;
-		this.onChangeSelect = options.onChangeSelect;
-
-		this.clock = new THREE.Clock(true);
-
-		//#region Init a scene
-		this.scene = new THREE.Scene();
-
-		const width = window.innerWidth;
-		const height = window.innerHeight;
-
-		//#region Init a renderer
-		this.renderer = new THREE.WebGLRenderer({
-			antialias: false,
-			stencil: false,
-			alpha: false,
-			powerPreference:
-				this.graphicsQuality === 'ultra' ? 'high-performance' :
-				this.graphicsQuality === 'high' ? 'high-performance' :
-				this.graphicsQuality === 'medium' ? 'default' :
-				this.graphicsQuality === 'low' ? 'low-power' :
-				'low-power' // cheep
-		});
-
-		this.renderer.setPixelRatio(window.devicePixelRatio);
-		this.renderer.setSize(width, height);
-		this.renderer.autoClear = false;
-		this.renderer.setClearColor(new THREE.Color(0x051f2d));
-		this.renderer.shadowMap.enabled = this.enableShadow;
-		this.renderer.shadowMap.type =
-			this.graphicsQuality === 'ultra' ? THREE.PCFSoftShadowMap :
-			this.graphicsQuality === 'high' ? THREE.PCFSoftShadowMap :
-			this.graphicsQuality === 'medium' ? THREE.PCFShadowMap :
-			this.graphicsQuality === 'low' ? THREE.BasicShadowMap :
-			THREE.BasicShadowMap; // cheep
-
-		container.appendChild(this.canvas);
-		//#endregion
-
-		//#region Init a camera
-		this.camera = options.useOrthographicCamera
-			? new THREE.OrthographicCamera(
-				width / - 2, width / 2, height / 2, height / - 2, -10, 10)
-			: new THREE.PerspectiveCamera(45, width / height);
-
-		if (options.useOrthographicCamera) {
-			this.camera.position.x = 2;
-			this.camera.position.y = 2;
-			this.camera.position.z = 2;
-			this.camera.zoom = 100;
-			this.camera.updateProjectionMatrix();
-		} else {
-			this.camera.position.x = 5;
-			this.camera.position.y = 2;
-			this.camera.position.z = 5;
-		}
-
-		this.scene.add(this.camera);
-		//#endregion
-
-		//#region AmbientLight
-		const ambientLight = new THREE.AmbientLight(0xffffff, 1);
-		this.scene.add(ambientLight);
-		//#endregion
-
-		if (this.graphicsQuality !== 'cheep') {
-			//#region Room light
-			const roomLight = new THREE.SpotLight(0xffffff, 0.1);
-
-			roomLight.position.set(0, 8, 0);
-			roomLight.castShadow = this.enableShadow;
-			roomLight.shadow.bias = -0.0001;
-			roomLight.shadow.mapSize.width = this.shadowQuality;
-			roomLight.shadow.mapSize.height = this.shadowQuality;
-			roomLight.shadow.camera.near = 0.1;
-			roomLight.shadow.camera.far = 9;
-			roomLight.shadow.camera.fov = 45;
-
-			this.scene.add(roomLight);
-			//#endregion
-		}
-
-		//#region Out light
-		const outLight1 = new THREE.SpotLight(0xffffff, 0.4);
-		outLight1.position.set(9, 3, -2);
-		outLight1.castShadow = this.enableShadow;
-		outLight1.shadow.bias = -0.001; // アクネ、アーチファクト対策 その代わりピーターパンが発生する可能性がある
-		outLight1.shadow.mapSize.width = this.shadowQuality;
-		outLight1.shadow.mapSize.height = this.shadowQuality;
-		outLight1.shadow.camera.near = 6;
-		outLight1.shadow.camera.far = 15;
-		outLight1.shadow.camera.fov = 45;
-		this.scene.add(outLight1);
-
-		const outLight2 = new THREE.SpotLight(0xffffff, 0.2);
-		outLight2.position.set(-2, 3, 9);
-		outLight2.castShadow = false;
-		outLight2.shadow.bias = -0.001; // アクネ、アーチファクト対策 その代わりピーターパンが発生する可能性がある
-		outLight2.shadow.camera.near = 6;
-		outLight2.shadow.camera.far = 15;
-		outLight2.shadow.camera.fov = 45;
-		this.scene.add(outLight2);
-		//#endregion
-
-		//#region Init a controller
-		this.controls = new OrbitControls(this.camera, this.canvas);
-
-		this.controls.target.set(0, 1, 0);
-		this.controls.enableZoom = true;
-		this.controls.enablePan = isMyRoom;
-		this.controls.minPolarAngle = 0;
-		this.controls.maxPolarAngle = Math.PI / 2;
-		this.controls.minAzimuthAngle = 0;
-		this.controls.maxAzimuthAngle = Math.PI / 2;
-		this.controls.enableDamping = true;
-		this.controls.dampingFactor = 0.2;
-		this.controls.mouseButtons.LEFT = 1;
-		this.controls.mouseButtons.MIDDLE = 2;
-		this.controls.mouseButtons.RIGHT = 0;
-		//#endregion
-
-		//#region POST FXs
-		if (!this.usePostFXs) {
-			this.composer = null;
-		} else {
-			const renderTarget = new THREE.WebGLRenderTarget(width, height, {
-				minFilter: THREE.LinearFilter,
-				magFilter: THREE.LinearFilter,
-				format: THREE.RGBFormat,
-				stencilBuffer: false,
-			});
-
-			const fxaa = new ShaderPass(FXAAShader);
-			fxaa.uniforms['resolution'].value = new THREE.Vector2(1 / width, 1 / height);
-			fxaa.renderToScreen = true;
-
-			this.composer = new EffectComposer(this.renderer, renderTarget);
-			this.composer.addPass(new RenderPass(this.scene, this.camera));
-			if (this.graphicsQuality === 'ultra') {
-				this.composer.addPass(new BloomPass(0.25, 30, 128.0, 512));
-			}
-			this.composer.addPass(fxaa);
-		}
-		//#endregion
-		//#endregion
-
-		//#region Label
-		//#region Avatar
-		const avatarUrl = `/proxy/?${urlQuery({ url: user.avatarUrl })}`;
-
-		const textureLoader = new THREE.TextureLoader();
-		textureLoader.crossOrigin = 'anonymous';
-
-		const iconTexture = textureLoader.load(avatarUrl);
-		iconTexture.wrapS = THREE.RepeatWrapping;
-		iconTexture.wrapT = THREE.RepeatWrapping;
-		iconTexture.anisotropy = 16;
-
-		const avatarMaterial = new THREE.MeshBasicMaterial({
-			map: iconTexture,
-			side: THREE.DoubleSide,
-			alphaTest: 0.5
-		});
-
-		const iconGeometry = new THREE.PlaneGeometry(1, 1);
-
-		const avatarObject = new THREE.Mesh(iconGeometry, avatarMaterial);
-		avatarObject.position.set(-3, 2.5, 2);
-		avatarObject.rotation.y = Math.PI / 2;
-		avatarObject.castShadow = false;
-
-		this.scene.add(avatarObject);
-		//#endregion
-
-		//#region Username
-		const name = user.username;
-
-		new THREE.FontLoader().load('/assets/fonts/helvetiker_regular.typeface.json', font => {
-			const nameGeometry = new THREE.TextGeometry(name, {
-				size: 0.5,
-				height: 0,
-				curveSegments: 8,
-				font: font,
-				bevelThickness: 0,
-				bevelSize: 0,
-				bevelEnabled: false
-			});
-
-			const nameMaterial = new THREE.MeshLambertMaterial({
-				color: 0xffffff
-			});
-
-			const nameObject = new THREE.Mesh(nameGeometry, nameMaterial);
-			nameObject.position.set(-3, 2.25, 1.25);
-			nameObject.rotation.y = Math.PI / 2;
-			nameObject.castShadow = false;
-
-			this.scene.add(nameObject);
-		});
-		//#endregion
-		//#endregion
-
-		//#region Interaction
-		if (isMyRoom) {
-			this.furnitureControl = new TransformControls(this.camera, this.canvas);
-			this.scene.add(this.furnitureControl);
-
-			// Hover highlight
-			this.canvas.onmousemove = this.onmousemove;
-
-			// Click
-			this.canvas.onmousedown = this.onmousedown;
-		}
-		//#endregion
-
-		//#region Init room
-		this.loadRoom();
-		//#endregion
-
-		//#region Load furnitures
-		for (const furniture of this.furnitures) {
-			this.loadFurniture(furniture).then(obj => {
-				this.scene.add(obj.scene);
-				this.objects.push(obj.scene);
-			});
-		}
-		//#endregion
-
-		// Start render
-		if (this.usePostFXs) {
-			this.renderWithPostFXs();
-		} else {
-			this.renderWithoutPostFXs();
-		}
-	}
-
-	@autobind
-	private renderWithoutPostFXs() {
-		this.renderFrameRequestId =
-			window.requestAnimationFrame(this.renderWithoutPostFXs);
-
-		// Update animations
-		const clock = this.clock.getDelta();
-		for (const mixer of this.mixers) {
-			mixer.update(clock);
-		}
-
-		this.controls.update();
-		this.renderer.render(this.scene, this.camera);
-	}
-
-	@autobind
-	private renderWithPostFXs() {
-		this.renderFrameRequestId =
-			window.requestAnimationFrame(this.renderWithPostFXs);
-
-		// Update animations
-		const clock = this.clock.getDelta();
-		for (const mixer of this.mixers) {
-			mixer.update(clock);
-		}
-
-		this.controls.update();
-		this.renderer.clear();
-		this.composer.render();
-	}
-
-	@autobind
-	private loadRoom() {
-		const type = this.roomInfo.roomType;
-		new GLTFLoader().load(`/assets/room/rooms/${type}/${type}.glb`, gltf => {
-			gltf.scene.traverse(child => {
-				if (!(child instanceof THREE.Mesh)) return;
-
-				child.receiveShadow = this.enableShadow;
-
-				child.material = new THREE.MeshLambertMaterial({
-					color: (child.material as THREE.MeshStandardMaterial).color,
-					map: (child.material as THREE.MeshStandardMaterial).map,
-					name: (child.material as THREE.MeshStandardMaterial).name,
-				});
-
-				// 異方性フィルタリング
-				if ((child.material as THREE.MeshLambertMaterial).map && this.graphicsQuality !== 'cheep') {
-					(child.material as THREE.MeshLambertMaterial).map.minFilter = THREE.LinearMipMapLinearFilter;
-					(child.material as THREE.MeshLambertMaterial).map.magFilter = THREE.LinearMipMapLinearFilter;
-					(child.material as THREE.MeshLambertMaterial).map.anisotropy = 8;
-				}
-			});
-
-			gltf.scene.position.set(0, 0, 0);
-
-			this.scene.add(gltf.scene);
-			this.roomObj = gltf.scene;
-			if (this.roomInfo.roomType === 'default') {
-				this.applyCarpetColor();
-			}
-		});
-	}
-
-	@autobind
-	private loadFurniture(furniture: Furniture) {
-		const def = furnitureDefs.find(d => d.id === furniture.type);
-		return new Promise<GLTF>((res, rej) => {
-			const loader = new GLTFLoader();
-			loader.load(`/assets/room/furnitures/${furniture.type}/${furniture.type}.glb`, gltf => {
-				const model = gltf.scene;
-
-				// Load animation
-				if (gltf.animations.length > 0) {
-					const mixer = new THREE.AnimationMixer(model);
-					this.mixers.push(mixer);
-					for (const clip of gltf.animations) {
-						mixer.clipAction(clip).play();
-					}
-				}
-
-				model.name = furniture.id;
-				model.position.x = furniture.position.x;
-				model.position.y = furniture.position.y;
-				model.position.z = furniture.position.z;
-				model.rotation.x = furniture.rotation.x;
-				model.rotation.y = furniture.rotation.y;
-				model.rotation.z = furniture.rotation.z;
-
-				model.traverse(child => {
-					if (!(child instanceof THREE.Mesh)) return;
-					child.castShadow = this.enableShadow;
-					child.receiveShadow = this.enableShadow;
-					(child.material as THREE.MeshStandardMaterial).metalness = 0;
-
-					// 異方性フィルタリング
-					if ((child.material as THREE.MeshStandardMaterial).map && this.graphicsQuality !== 'cheep') {
-						(child.material as THREE.MeshStandardMaterial).map.minFilter = THREE.LinearMipMapLinearFilter;
-						(child.material as THREE.MeshStandardMaterial).map.magFilter = THREE.LinearMipMapLinearFilter;
-						(child.material as THREE.MeshStandardMaterial).map.anisotropy = 8;
-					}
-				});
-
-				if (def.color) { // カスタムカラー
-					this.applyCustomColor(model);
-				}
-
-				if (def.texture) { // カスタムテクスチャ
-					this.applyCustomTexture(model);
-				}
-
-				res(gltf);
-			}, null, rej);
-		});
-	}
-
-	@autobind
-	private applyCarpetColor() {
-		this.roomObj.traverse(child => {
-			if (!(child instanceof THREE.Mesh)) return;
-			if (child.material &&
-				(child.material as THREE.MeshStandardMaterial).name &&
-				(child.material as THREE.MeshStandardMaterial).name === 'Carpet'
-			) {
-				const colorHex = parseInt(this.roomInfo.carpetColor.substr(1), 16);
-				(child.material as THREE.MeshStandardMaterial).color.setHex(colorHex);
-			}
-		});
-	}
-
-	@autobind
-	private applyCustomColor(model: THREE.Object3D) {
-		const furniture = this.furnitures.find(furniture => furniture.id === model.name);
-		const def = furnitureDefs.find(d => d.id === furniture.type);
-		if (def.color == null) return;
-		model.traverse(child => {
-			if (!(child instanceof THREE.Mesh)) return;
-			for (const t of Object.keys(def.color)) {
-				if (!child.material ||
-					!(child.material as THREE.MeshStandardMaterial).name ||
-					(child.material as THREE.MeshStandardMaterial).name !== t
-				) continue;
-
-				const prop = def.color[t];
-				const val = furniture.props ? furniture.props[prop] : undefined;
-
-				if (val == null) continue;
-
-				const colorHex = parseInt(val.substr(1), 16);
-				(child.material as THREE.MeshStandardMaterial).color.setHex(colorHex);
-			}
-		});
-	}
-
-	@autobind
-	private applyCustomTexture(model: THREE.Object3D) {
-		const furniture = this.furnitures.find(furniture => furniture.id === model.name);
-		const def = furnitureDefs.find(d => d.id === furniture.type);
-		if (def.texture == null) return;
-
-		model.traverse(child => {
-			if (!(child instanceof THREE.Mesh)) return;
-			for (const t of Object.keys(def.texture)) {
-				if (child.name !== t) continue;
-
-				const prop = def.texture[t].prop;
-				const val = furniture.props ? furniture.props[prop] : undefined;
-
-				if (val == null) continue;
-
-				const canvas = document.createElement('canvas');
-				canvas.height = 1024;
-				canvas.width = 1024;
-
-				child.material = new THREE.MeshLambertMaterial({
-					emissive: 0x111111,
-					side: THREE.DoubleSide,
-					alphaTest: 0.5,
-				});
-
-				const img = new Image();
-				img.crossOrigin = 'anonymous';
-				img.onload = () => {
-					const uvInfo = def.texture[t].uv;
-
-					const ctx = canvas.getContext('2d');
-					ctx.drawImage(img,
-						0, 0, img.width, img.height,
-						uvInfo.x, uvInfo.y, uvInfo.width, uvInfo.height);
-
-					const texture = new THREE.Texture(canvas);
-					texture.wrapS = THREE.RepeatWrapping;
-					texture.wrapT = THREE.RepeatWrapping;
-					texture.anisotropy = 16;
-					texture.flipY = false;
-
-					(child.material as THREE.MeshLambertMaterial).map = texture;
-					(child.material as THREE.MeshLambertMaterial).needsUpdate = true;
-					(child.material as THREE.MeshLambertMaterial).map.needsUpdate = true;
-				};
-				img.src = val;
-			}
-		});
-	}
-
-	@autobind
-	private onmousemove(ev: MouseEvent) {
-		if (this.isTransformMode) return;
-
-		const rect = (ev.target as HTMLElement).getBoundingClientRect();
-		const x = (((ev.clientX * window.devicePixelRatio) - rect.left) / this.canvas.width) * 2 - 1;
-		const y = -(((ev.clientY * window.devicePixelRatio) - rect.top) / this.canvas.height) * 2 + 1;
-		const pos = new THREE.Vector2(x, y);
-
-		this.camera.updateMatrixWorld();
-
-		const raycaster = new THREE.Raycaster();
-		raycaster.setFromCamera(pos, this.camera);
-
-		const intersects = raycaster.intersectObjects(this.objects, true);
-
-		for (const object of this.objects) {
-			if (this.isSelectedObject(object)) continue;
-			object.traverse(child => {
-				if (child instanceof THREE.Mesh) {
-					(child.material as THREE.MeshStandardMaterial).emissive.setHex(0x000000);
-				}
-			});
-		}
-
-		if (intersects.length > 0) {
-			const intersected = this.getRoot(intersects[0].object);
-			if (this.isSelectedObject(intersected)) return;
-			intersected.traverse(child => {
-				if (child instanceof THREE.Mesh) {
-					(child.material as THREE.MeshStandardMaterial).emissive.setHex(0x191919);
-				}
-			});
-		}
-	}
-
-	@autobind
-	private onmousedown(ev: MouseEvent) {
-		if (this.isTransformMode) return;
-		if (ev.target !== this.canvas || ev.button !== 0) return;
-
-		const rect = (ev.target as HTMLElement).getBoundingClientRect();
-		const x = (((ev.clientX * window.devicePixelRatio) - rect.left) / this.canvas.width) * 2 - 1;
-		const y = -(((ev.clientY * window.devicePixelRatio) - rect.top) / this.canvas.height) * 2 + 1;
-		const pos = new THREE.Vector2(x, y);
-
-		this.camera.updateMatrixWorld();
-
-		const raycaster = new THREE.Raycaster();
-		raycaster.setFromCamera(pos, this.camera);
-
-		const intersects = raycaster.intersectObjects(this.objects, true);
-
-		for (const object of this.objects) {
-			object.traverse(child => {
-				if (child instanceof THREE.Mesh) {
-					(child.material as THREE.MeshStandardMaterial).emissive.setHex(0x000000);
-				}
-			});
-		}
-
-		if (intersects.length > 0) {
-			const selectedObj = this.getRoot(intersects[0].object);
-			this.selectFurniture(selectedObj);
-		} else {
-			this.selectedObject = null;
-			this.onChangeSelect(null);
-		}
-	}
-
-	@autobind
-	private getRoot(obj: THREE.Object3D): THREE.Object3D {
-		let found = false;
-		let x = obj.parent;
-		while (!found) {
-			if (x.parent.parent == null) {
-				found = true;
-			} else {
-				x = x.parent;
-			}
-		}
-		return x;
-	}
-
-	@autobind
-	private isSelectedObject(obj: THREE.Object3D): boolean {
-		if (this.selectedObject == null) {
-			return false;
-		} else {
-			return obj.name === this.selectedObject.name;
-		}
-	}
-
-	@autobind
-	private selectFurniture(obj: THREE.Object3D) {
-		this.selectedObject = obj;
-		this.onChangeSelect(obj);
-		obj.traverse(child => {
-			if (child instanceof THREE.Mesh) {
-				(child.material as THREE.MeshStandardMaterial).emissive.setHex(0xff0000);
-			}
-		});
-	}
-
-	/**
-	 * 家具の移動/回転モードにします
-	 * @param type 移動か回転か
-	 */
-	@autobind
-	public enterTransformMode(type: 'translate' | 'rotate') {
-		this.isTransformMode = true;
-		this.furnitureControl.setMode(type);
-		this.furnitureControl.attach(this.selectedObject);
-	}
-
-	/**
-	 * 家具の移動/回転モードを終了します
-	 */
-	@autobind
-	public exitTransformMode() {
-		this.isTransformMode = false;
-		this.furnitureControl.detach();
-	}
-
-	/**
-	 * 家具プロパティを更新します
-	 * @param key プロパティ名
-	 * @param value 値
-	 */
-	@autobind
-	public updateProp(key: string, value: any) {
-		const furniture = this.furnitures.find(furniture => furniture.id === this.selectedObject.name);
-		if (furniture.props == null) furniture.props = {};
-		furniture.props[key] = value;
-		this.applyCustomColor(this.selectedObject);
-		this.applyCustomTexture(this.selectedObject);
-	}
-
-	/**
-	 * 部屋に家具を追加します
-	 * @param type 家具の種類
-	 */
-	@autobind
-	public addFurniture(type: string) {
-		const furniture = {
-			id: uuid(),
-			type: type,
-			position: {
-				x: 0,
-				y: 0,
-				z: 0,
-			},
-			rotation: {
-				x: 0,
-				y: 0,
-				z: 0,
-			},
-		};
-
-		this.furnitures.push(furniture);
-
-		this.loadFurniture(furniture).then(obj => {
-			this.scene.add(obj.scene);
-			this.objects.push(obj.scene);
-		});
-	}
-
-	/**
-	 * 現在選択されている家具を部屋から削除します
-	 */
-	@autobind
-	public removeFurniture() {
-		this.exitTransformMode();
-		const obj = this.selectedObject;
-		this.scene.remove(obj);
-		this.objects = this.objects.filter(object => object.name !== obj.name);
-		this.furnitures = this.furnitures.filter(furniture => furniture.id !== obj.name);
-		this.selectedObject = null;
-		this.onChangeSelect(null);
-	}
-
-	/**
-	 * 全ての家具を部屋から削除します
-	 */
-	@autobind
-	public removeAllFurnitures() {
-		this.exitTransformMode();
-		for (const obj of this.objects) {
-			this.scene.remove(obj);
-		}
-		this.objects = [];
-		this.furnitures = [];
-		this.selectedObject = null;
-		this.onChangeSelect(null);
-	}
-
-	/**
-	 * 部屋の床の色を変更します
-	 * @param color 色
-	 */
-	@autobind
-	public updateCarpetColor(color: string) {
-		this.roomInfo.carpetColor = color;
-		this.applyCarpetColor();
-	}
-
-	/**
-	 * 部屋の種類を変更します
-	 * @param type 種類
-	 */
-	@autobind
-	public changeRoomType(type: string) {
-		this.roomInfo.roomType = type;
-		this.scene.remove(this.roomObj);
-		this.loadRoom();
-	}
-
-	/**
-	 * 部屋データを取得します
-	 */
-	@autobind
-	public getRoomInfo() {
-		for (const obj of this.objects) {
-			const furniture = this.furnitures.find(f => f.id === obj.name);
-			furniture.position.x = obj.position.x;
-			furniture.position.y = obj.position.y;
-			furniture.position.z = obj.position.z;
-			furniture.rotation.x = obj.rotation.x;
-			furniture.rotation.y = obj.rotation.y;
-			furniture.rotation.z = obj.rotation.z;
-		}
-
-		return this.roomInfo;
-	}
-
-	/**
-	 * 選択されている家具を取得します
-	 */
-	@autobind
-	public getSelectedObject() {
-		return this.selectedObject;
-	}
-
-	@autobind
-	public findFurnitureById(id: string) {
-		return this.furnitures.find(furniture => furniture.id === id);
-	}
-
-	/**
-	 * レンダリングを終了します
-	 */
-	@autobind
-	public destroy() {
-		// Stop render loop
-		window.cancelAnimationFrame(this.renderFrameRequestId);
-
-		this.controls.dispose();
-		this.scene.dispose();
-	}
-}
diff --git a/src/client/app/common/scripts/should-mute-note.ts b/src/client/app/common/scripts/should-mute-note.ts
deleted file mode 100644
index 8fd78886287d9fe3f7b94c17872ce310f0273494..0000000000000000000000000000000000000000
--- a/src/client/app/common/scripts/should-mute-note.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-export default function(me, settings, note) {
-	const isMyNote = me && (note.userId == me.id);
-	const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null;
-
-	const includesMutedWords = (text: string) =>
-		text
-			? settings.mutedWords.some(q => q.length > 0 && !q.some(word =>
-				word.startsWith('/') && word.endsWith('/') ? !(new RegExp(word.substr(1, word.length - 2)).test(text)) : !text.includes(word)))
-			: false;
-
-	return (
-		(!isMyNote && note.reply && includesMutedWords(note.reply.text)) ||
-		(!isMyNote && note.renote && includesMutedWords(note.renote.text)) ||
-		(!settings.showMyRenotes && isMyNote && isPureRenote) ||
-		(!settings.showRenotedMyNotes && isPureRenote && note.renote.userId == me.id) ||
-		(!settings.showLocalRenotes && isPureRenote && note.renote.user.host == null) ||
-		(!isMyNote && includesMutedWords(note.text))
-	);
-}
diff --git a/src/client/app/common/size.ts b/src/client/app/common/size.ts
deleted file mode 100644
index 6abb3057470e68f6604110ef008fbe5905cfcce9..0000000000000000000000000000000000000000
--- a/src/client/app/common/size.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-export default {
-	install(Vue) {
-		Vue.directive('size', {
-			inserted(el, binding) {
-				const query = binding.value;
-				const width = el.clientWidth;
-				for (const q of query) {
-					if (q.lt && (width <= q.lt)) {
-						el.classList.add(q.class);
-					}
-					if (q.gt && (width >= q.gt)) {
-						el.classList.add(q.class);
-					}
-				}
-			}
-		});
-	}
-};
diff --git a/src/client/app/common/views/components/acct.vue b/src/client/app/common/views/components/acct.vue
deleted file mode 100644
index e802000833f80e75e63fafd9ccab334905a8bc88..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/acct.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<template>
-<span class="mk-acct" v-once>
-	<span class="name">@{{ user.username }}</span>
-	<span class="host" :class="{ fade: $store.state.settings.contrastedAcct }" v-if="user.host || detail || $store.state.settings.showFullAcct">@{{ user.host || host }}</span>
-	<fa v-if="user.isLocked == true" class="locked" icon="lock" fixed-width/>
-</span>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { host } from '../../../config';
-import { toUnicode } from 'punycode';
-export default Vue.extend({
-	props: ['user', 'detail'],
-	data() {
-		return {
-			host: toUnicode(host)
-		};
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-acct
-	> .host.fade
-		opacity 0.5
-
-	> .locked
-		opacity 0.8
-		margin-left 0.5em
-</style>
diff --git a/src/client/app/common/views/components/analog-clock.vue b/src/client/app/common/views/components/analog-clock.vue
deleted file mode 100644
index 5eb7ffd153c4f0fbe20b6123e7601e87a3ae7f89..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/analog-clock.vue
+++ /dev/null
@@ -1,140 +0,0 @@
-<template>
-<svg class="mk-analog-clock" viewBox="0 0 10 10" preserveAspectRatio="none">
-	<circle v-for="angle, i in graduations"
-		:cx="5 + (Math.sin(angle) * (5 - graduationsPadding))"
-		:cy="5 - (Math.cos(angle) * (5 - graduationsPadding))"
-		:r="i % 5 == 0 ? 0.125 : 0.05"
-		:fill="i % 5 == 0 ? majorGraduationColor : minorGraduationColor"/>
-
-	<line
-		:x1="5 - (Math.sin(sAngle) * (sHandLengthRatio * handsTailLength))"
-		:y1="5 + (Math.cos(sAngle) * (sHandLengthRatio * handsTailLength))"
-		:x2="5 + (Math.sin(sAngle) * ((sHandLengthRatio * 5) - handsPadding))"
-		:y2="5 - (Math.cos(sAngle) * ((sHandLengthRatio * 5) - handsPadding))"
-		:stroke="sHandColor"
-		stroke-width="0.05"/>
-	<line
-		:x1="5 - (Math.sin(mAngle) * (mHandLengthRatio * handsTailLength))"
-		:y1="5 + (Math.cos(mAngle) * (mHandLengthRatio * handsTailLength))"
-		:x2="5 + (Math.sin(mAngle) * ((mHandLengthRatio * 5) - handsPadding))"
-		:y2="5 - (Math.cos(mAngle) * ((mHandLengthRatio * 5) - handsPadding))"
-		:stroke="mHandColor"
-		stroke-width="0.1"/>
-	<line
-		:x1="5 - (Math.sin(hAngle) * (hHandLengthRatio * handsTailLength))"
-		:y1="5 + (Math.cos(hAngle) * (hHandLengthRatio * handsTailLength))"
-		:x2="5 + (Math.sin(hAngle) * ((hHandLengthRatio * 5) - handsPadding))"
-		:y2="5 - (Math.cos(hAngle) * ((hHandLengthRatio * 5) - handsPadding))"
-		:stroke="hHandColor"
-		stroke-width="0.1"/>
-</svg>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import * as tinycolor from 'tinycolor2';
-
-export default Vue.extend({
-	props: {
-		dark: {
-			type: Boolean,
-			default: false
-		},
-		smooth: {
-			type: Boolean,
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			now: new Date(),
-			enabled: true,
-
-			graduationsPadding: 0.5,
-			handsPadding: 1,
-			handsTailLength: 0.7,
-			hHandLengthRatio: 0.75,
-			mHandLengthRatio: 1,
-			sHandLengthRatio: 1
-		};
-	},
-
-	computed: {
-		majorGraduationColor(): string {
-			return this.dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
-		},
-		minorGraduationColor(): string {
-			return this.dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
-		},
-
-		sHandColor(): string {
-			return this.dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
-		},
-		mHandColor(): string {
-			return this.dark ? '#fff' : '#777';
-		},
-		hHandColor(): string {
-			return tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--primary')).toHexString();
-		},
-
-		ms(): number {
-			return this.now.getMilliseconds() * this.smooth;
-		},
-		s(): number {
-			return this.now.getSeconds();
-		},
-		m(): number {
-			return this.now.getMinutes();
-		},
-		h(): number {
-			return this.now.getHours();
-		},
-
-		hAngle(): number {
-			return Math.PI * (this.h % 12 + (this.m + (this.s + this.ms / 1000) / 60) / 60) / 6;
-		},
-		mAngle(): number {
-			return Math.PI * (this.m + (this.s + this.ms / 1000) / 60) / 30;
-		},
-		sAngle(): number {
-			return Math.PI * (this.s + this.ms / 1000) / 30;
-		},
-
-		graduations(): any {
-			const angles = [];
-			for (let i = 0; i < 60; i++) {
-				const angle = Math.PI * i / 30;
-				angles.push(angle);
-			}
-
-			return angles;
-		}
-	},
-
-	mounted() {
-		const update = () => {
-			if (this.enabled) {
-				this.tick();
-				requestAnimationFrame(update);
-			}
-		};
-		update();
-	},
-
-	beforeDestroy() {
-		this.enabled = false;
-	},
-
-	methods: {
-		tick() {
-			this.now = new Date();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-analog-clock
-	display block
-</style>
diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue
deleted file mode 100644
index cd02c6957d58e71c59b67002c189a903766c258f..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/avatar.vue
+++ /dev/null
@@ -1,116 +0,0 @@
-<template>
-	<span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick" v-once>
-		<span class="inner" :style="icon"></span>
-	</span>
-	<span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick" v-once>
-		<span class="inner" :style="icon"></span>
-	</span>
-	<router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id" v-once>
-		<span class="inner" :style="icon"></span>
-	</router-link>
-	<router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview" v-once>
-		<span class="inner" :style="icon"></span>
-	</router-link>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url';
-
-export default Vue.extend({
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-		target: {
-			required: false,
-			default: null
-		},
-		disableLink: {
-			required: false,
-			default: false
-		},
-		disablePreview: {
-			required: false,
-			default: false
-		}
-	},
-	computed: {
-		lightmode(): boolean {
-			return this.$store.state.device.lightmode;
-		},
-		cat(): boolean {
-			return this.user.isCat && this.$store.state.settings.circleIcons;
-		},
-		style(): any {
-			return {
-				borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
-			};
-		},
-		url(): string {
-			return this.$store.state.device.disableShowingAnimatedImages
-				? getStaticImageUrl(this.user.avatarUrl)
-				: this.user.avatarUrl;
-		},
-		icon(): any {
-			return {
-				backgroundColor: this.user.avatarColor,
-				backgroundImage: this.lightmode ? null : `url(${this.url})`,
-				borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
-			};
-		}
-	},
-	mounted() {
-		if (this.user.avatarColor) {
-			this.$el.style.color = this.user.avatarColor;
-		}
-	},
-	methods: {
-		onClick(e) {
-			this.$emit('click', e);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-avatar
-	display inline-block
-	vertical-align bottom
-	flex-shrink 0
-
-	&:not(.cat)
-		overflow hidden
-		border-radius 8px
-
-	&.cat::before,
-	&.cat::after
-		background #df548f
-		border solid 4px currentColor
-		box-sizing border-box
-		content ''
-		display inline-block
-		height 50%
-		width 50%
-
-	&.cat::before
-		border-radius 0 75% 75%
-		transform rotate(37.5deg) skew(30deg)
-
-	&.cat::after
-		border-radius 75% 0 75% 75%
-		transform rotate(-37.5deg) skew(-30deg)
-
-	.inner
-		background-position center center
-		background-size cover
-		bottom 0
-		left 0
-		position absolute
-		right 0
-		top 0
-		transition border-radius 1s ease
-		z-index 1
-
-</style>
diff --git a/src/client/app/common/views/components/connect-failed.troubleshooter.vue b/src/client/app/common/views/components/connect-failed.troubleshooter.vue
deleted file mode 100644
index 19b8c3e974f8d73d84a2d4a08d86b6f2ddb4bde7..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/connect-failed.troubleshooter.vue
+++ /dev/null
@@ -1,148 +0,0 @@
-<template>
-<div class="troubleshooter">
-	<div class="body">
-		<h1><fa icon="wrench"/>{{ $t('title') }}</h1>
-		<div>
-			<p :data-wip="network == null">
-				<template v-if="network != null">
-					<template v-if="network"><fa icon="check"/></template>
-					<template v-if="!network"><fa icon="times"/></template>
-				</template>
-				{{ network == null ? this.$t('checking-network') : this.$t('network') }}<mk-ellipsis v-if="network == null"/>
-			</p>
-			<p v-if="network == true" :data-wip="internet == null">
-				<template v-if="internet != null">
-					<template v-if="internet"><fa icon="check"/></template>
-					<template v-if="!internet"><fa icon="times"/></template>
-				</template>
-				{{ internet == null ? this.$t('checking-internet') : this.$t('internet') }}<mk-ellipsis v-if="internet == null"/>
-			</p>
-			<p v-if="internet == true" :data-wip="server == null">
-				<template v-if="server != null">
-					<template v-if="server"><fa icon="check"/></template>
-					<template v-if="!server"><fa icon="times"/></template>
-				</template>
-				{{ server == null ? this.$t('checking-server') : this.$t('server') }}<mk-ellipsis v-if="server == null"/>
-			</p>
-		</div>
-		<p v-if="!end">{{ $t('finding') }}<mk-ellipsis/></p>
-		<p v-if="network === false"><b><fa icon="exclamation-triangle"/>{{ $t('no-network') }}</b><br>{{ $t('no-network-desc') }}</p>
-		<p v-if="internet === false"><b><fa icon="exclamation-triangle"/>{{ $t('no-internet') }}</b><br>{{ $t('no-internet-desc') }}</p>
-		<p v-if="server === false"><b><fa icon="exclamation-triangle"/>{{ $t('no-server') }}</b><br>{{ $t('no-server-desc') }}</p>
-		<p v-if="server === true" class="success"><b><fa icon="info-circle"/>{{ $t('success') }}</b><br>{{ $t('success-desc') }}</p>
-	</div>
-	<footer>
-		<a href="/assets/flush.html">{{ $t('flush') }}</a> | <a href="/assets/version.html">{{ $t('set-version') }}</a>
-	</footer>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { apiUrl } from '../../../config';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/connect-failed.troubleshooter.vue'),
-	data() {
-		return {
-			network: navigator.onLine,
-			end: false,
-			internet: null,
-			server: null
-		};
-	},
-	mounted() {
-		if (!this.network) {
-			this.end = true;
-			return;
-		}
-
-		// Check internet connection
-		fetch(`https://google.com?rand=${Math.random()}`, {
-			mode: 'no-cors'
-		}).then(() => {
-			this.internet = true;
-
-			// Check misskey server is available
-			fetch(`${apiUrl}/meta`).then(() => {
-				this.end = true;
-				this.server = true;
-			})
-			.catch(() => {
-				this.end = true;
-				this.server = false;
-			});
-		})
-		.catch(() => {
-			this.end = true;
-			this.internet = false;
-		});
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.troubleshooter
-	margin-top 1em
-
-	> .body
-		width 100%
-		max-width 500px
-		margin 0 auto
-		text-align left
-		background #fff
-		border-radius 8px
-		border solid 1px #ddd
-
-		> h1
-			margin 0
-			padding 0.6em 1.2em
-			font-size 1em
-			color #444
-			border-bottom solid 1px #eee
-
-			> [data-icon]
-				margin-right 0.25em
-
-		> div
-			overflow hidden
-			padding 0.6em 1.2em
-
-			> p
-				margin 0.5em 0
-				font-size 0.9em
-				color #444
-
-				&[data-wip]
-					color #888
-
-				> [data-icon]
-					margin-right 0.25em
-
-					&.times
-						color #e03524
-
-					&.check
-						color #84c32f
-
-		> p
-			margin 0
-			padding 0.7em 1.2em
-			font-size 1em
-			color #444
-			border-top solid 1px #eee
-
-			> b
-				> [data-icon]
-					margin-right 0.25em
-
-			&.success
-				> b
-					color #39adad
-
-			&:not(.success)
-				> b
-					color #ad4339
-
-</style>
diff --git a/src/client/app/common/views/components/connect-failed.vue b/src/client/app/common/views/components/connect-failed.vue
deleted file mode 100644
index a364304a63c3b91bcb9a379131ec5cd72c58a0ad..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/connect-failed.vue
+++ /dev/null
@@ -1,105 +0,0 @@
-<template>
-<div class="mk-connect-failed">
-	<img src="/assets/error.jpg" onerror="this.src='https://raw.githubusercontent.com/syuilo/misskey/develop/src/client/assets/error.jpg';" alt=""/>
-	<h1>{{ $t('title') }}</h1>
-	<p class="text">
-		<span>{{ this.$t('description').substr(0, this.$t('description').indexOf('{')) }}</span>
-		<a @click="reload">{{ this.$t('description').match(/\{(.+?)\}/)[1] }}</a>
-		<span>{{ this.$t('description').substr(this.$t('description').indexOf('}') + 1) }}</span>
-	</p>
-	<button v-if="!troubleshooting" @click="troubleshooting = true">{{ $t('troubleshoot') }}</button>
-	<x-troubleshooter v-if="troubleshooting"/>
-	<p class="thanks">{{ $t('thanks') }}</p>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XTroubleshooter from './connect-failed.troubleshooter.vue';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/connect-failed.vue'),
-	components: {
-		XTroubleshooter
-	},
-	data() {
-		return {
-			troubleshooting: false
-		};
-	},
-	mounted() {
-		document.title = 'Oops!';
-		document.documentElement.style.setProperty('background', '#f8f8f8', 'important');
-	},
-	methods: {
-		reload() {
-			location.reload(true);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-
-
-.mk-connect-failed
-	width 100%
-	padding 32px 18px
-	text-align center
-
-	> img
-		display block
-		height 200px
-		margin 0 auto
-		pointer-events none
-		user-select none
-
-	> h1
-		display block
-		margin 1.25em auto 0.65em auto
-		font-size 1.5em
-		color #555
-
-	> .text
-		display block
-		margin 0 auto
-		max-width 600px
-		font-size 1em
-		color #666
-
-	> button
-		display block
-		margin 1em auto 0 auto
-		padding 8px 10px
-		color var(--primaryForeground)
-		background var(--primary)
-
-		&:focus
-			outline solid 3px var(--primaryAlpha03)
-
-		&:hover
-			background var(--primaryLighten10)
-
-		&:active
-			background var(--primaryDarken10)
-
-	> .thanks
-		display block
-		margin 2em auto 0 auto
-		padding 2em 0 0 0
-		max-width 600px
-		font-size 0.9em
-		font-style oblique
-		color #aaa
-		border-top solid 1px #eee
-
-	@media (max-width 500px)
-		padding 24px 18px
-		font-size 80%
-
-		> img
-			height 150px
-
-</style>
-
diff --git a/src/client/app/common/views/components/cw-button.vue b/src/client/app/common/views/components/cw-button.vue
deleted file mode 100644
index 098aa021d113677c7c372d883ae3773e2b66f03b..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/cw-button.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-<template>
-<button class="nrvgflfuaxwgkxoynpnumyookecqrrvh" @click="toggle">
-	<b>{{ value ? this.$t('hide') : this.$t('show') }}</b>
-	<span v-if="!value">{{ this.label }}</span>
-</button>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { length } from 'stringz';
-import { concat } from '../../../../../prelude/array';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/cw-button.vue'),
-
-	props: {
-		value: {
-			type: Boolean,
-			required: true
-		},
-		note: {
-			type: Object,
-			required: true
-		}
-	},
-
-	computed: {
-		label(): string {
-			return concat([
-				this.note.text ? [this.$t('chars', { count: length(this.note.text) })] : [],
-				this.note.files && this.note.files.length !== 0 ? [this.$t('files', { count: this.note.files.length }) ] : [],
-				this.note.poll != null ? [this.$t('poll')] : []
-			] as string[][]).join(' / ');
-		}
-	},
-
-	methods: {
-		length,
-
-		toggle() {
-			this.$emit('input', !this.value);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.nrvgflfuaxwgkxoynpnumyookecqrrvh
-	display inline-block
-	padding 4px 8px
-	font-size 0.7em
-	color var(--cwButtonFg)
-	background var(--cwButtonBg)
-	border-radius 2px
-	cursor pointer
-	user-select none
-
-	&:hover
-		background var(--cwButtonHoverBg)
-
-	> span
-		margin-left 4px
-
-		&:before
-			content '('
-		&:after
-			content ')'
-
-</style>
diff --git a/src/client/app/common/views/components/dialog.vue b/src/client/app/common/views/components/dialog.vue
deleted file mode 100644
index 27449030075fa01f4d5508066a4d70fb07b859ce..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/dialog.vue
+++ /dev/null
@@ -1,263 +0,0 @@
-<template>
-<ui-modal
-	ref="modal"
-	class="modal"
-	:class="{ splash }"
-	:close-anime-duration="300"
-	:close-on-bg-click="false"
-	@bg-click="onBgClick"
-	@before-close="onBeforeClose">
-	<div class="main" ref="main" :class="{ round: $store.state.device.roundedCorners }">
-		<template v-if="type == 'signin'">
-			<mk-signin/>
-		</template>
-		<template v-else>
-			<div class="icon" v-if="icon">
-				<fa :icon="icon"/>
-			</div>
-			<div class="icon" v-else-if="!input && !select && !user" :class="type">
-				<fa icon="check" v-if="type === 'success'"/>
-				<fa :icon="faTimesCircle" v-if="type === 'error'"/>
-				<fa icon="exclamation-triangle" v-if="type === 'warning'"/>
-				<fa icon="info-circle" v-if="type === 'info'"/>
-				<fa :icon="faQuestionCircle" v-if="type === 'question'"/>
-				<fa icon="spinner" pulse v-if="type === 'waiting'"/>
-			</div>
-			<header v-if="title" v-html="title"></header>
-			<header v-if="title == null && user">{{ $t('@.enter-username') }}</header>
-			<div class="body" v-if="text" v-html="text"></div>
-			<ui-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></ui-input>
-			<ui-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></ui-input>
-			<ui-select v-if="select" v-model="selectedValue" autofocus>
-				<template v-if="select.items">
-					<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
-				</template>
-				<template v-else>
-					<optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label">
-						<option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option>
-					</optgroup>
-				</template>
-			</ui-select>
-			<ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash && (showOkButton || showCancelButton)">
-				<ui-button @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user" :disabled="!canOk">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button>
-				<ui-button @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('@.cancel') }}</ui-button>
-			</ui-horizon-group>
-		</template>
-	</div>
-</ui-modal>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import anime from 'animejs';
-import { faTimesCircle, faQuestionCircle } from '@fortawesome/free-regular-svg-icons';
-import parseAcct from "../../../../../misc/acct/parse";
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n(),
-	props: {
-		type: {
-			type: String,
-			required: false,
-			default: 'info'
-		},
-		title: {
-			type: String,
-			required: false
-		},
-		text: {
-			type: String,
-			required: false
-		},
-		input: {
-			required: false
-		},
-		select: {
-			required: false
-		},
-		user: {
-			required: false
-		},
-		icon: {
-			required: false
-		},
-		showOkButton: {
-			type: Boolean,
-			default: true
-		},
-		showCancelButton: {
-			type: Boolean,
-			default: false
-		},
-		cancelableByBgClick: {
-			type: Boolean,
-			default: true
-		},
-		splash: {
-			type: Boolean,
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			inputValue: this.input && this.input.default ? this.input.default : null,
-			userInputValue: null,
-			selectedValue: this.select ? this.select.default ? this.select.default : this.select.items ? this.select.items[0].value : this.select.groupedItems[0].items[0].value : null,
-			canOk: true,
-			faTimesCircle, faQuestionCircle
-		};
-	},
-
-	watch: {
-		userInputValue() {
-			if (this.user) {
-				this.$root.api('users/show', parseAcct(this.userInputValue)).then(u => {
-					this.canOk = u != null;
-				}).catch(() => {
-					this.canOk = false;
-				});
-			}
-		}
-	},
-
-	mounted() {
-		if (this.user) this.canOk = false;
-
-		this.$nextTick(() => {
-			anime({
-				targets: this.$refs.main,
-				opacity: 1,
-				scale: [1.2, 1],
-				duration: 300,
-				easing: 'cubicBezier(0, 0.5, 0.5, 1)'
-			});
-
-			if (this.splash) {
-				setTimeout(() => {
-					this.close();
-				}, 1000);
-			}
-		});
-	},
-
-	methods: {
-		async ok() {
-			if (!this.canOk) return;
-			if (!this.showOkButton) return;
-
-			if (this.user) {
-				const user = await this.$root.api('users/show', parseAcct(this.userInputValue));
-				if (user) {
-					this.$emit('ok', user);
-					this.close();
-				}
-			} else {
-				const result =
-					this.input ? this.inputValue :
-					this.select ? this.selectedValue :
-					true;
-				this.$emit('ok', result);
-				this.close();
-			}
-		},
-
-		cancel() {
-			this.$emit('cancel');
-			this.close();
-		},
-
-		onBgClick() {
-			if (this.cancelableByBgClick) this.cancel();
-		}
-
-		close() {
-			this.$refs.modal.close();
-		},
-
-		onBeforeClose() {
-			this.$el.style.pointerEvents = 'none';
-			(this.$refs.main as any).style.pointerEvents = 'none';
-
-			anime({
-				targets: this.$refs.main,
-				opacity: 0,
-				scale: 0.8,
-				duration: 300,
-				easing: 'cubicBezier(0, 0.5, 0.5, 1)',
-			});
-		},
-
-		onInputKeydown(e) {
-			if (e.which == 13) { // Enter
-				e.preventDefault();
-				e.stopPropagation();
-				this.ok();
-			}
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.modal
-	display flex
-	align-items center
-	justify-content center
-
-	&.splash
-		> .main
-			min-width 0
-			width initial
-
-.main
-	display block
-	position fixed
-	margin auto
-	padding 32px
-	min-width 320px
-	max-width 480px
-	width calc(100% - 32px)
-	text-align center
-	background var(--face)
-	color var(--faceText)
-	opacity 0
-
-	&.round
-		border-radius 8px
-
-	> .icon
-		font-size 32px
-
-		&.success
-			color #85da5a
-
-		&.error
-			color #ec4137
-
-		&.warning
-			color #ecb637
-
-		> *
-			display block
-			margin 0 auto
-
-		& + header
-			margin-top 16px
-
-	> header
-		margin 0 0 8px 0
-		font-weight bold
-		font-size 20px
-
-		& + .body
-			margin-top 8px
-
-	> .body
-		margin 16px 0 0 0
-
-	> .buttons
-		margin-top 16px
-
-</style>
diff --git a/src/client/app/common/views/components/dummy.vue b/src/client/app/common/views/components/dummy.vue
deleted file mode 100644
index 5634efc509b15d7c286b950d81291460407d17ae..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/dummy.vue
+++ /dev/null
@@ -1,11 +0,0 @@
-<template>
-<div>
-	<slot></slot>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-});
-</script>
diff --git a/src/client/app/common/views/components/ellipsis.vue b/src/client/app/common/views/components/ellipsis.vue
deleted file mode 100644
index 07349902dee4de47418032a114eec0487a6d6717..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/ellipsis.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<template>
-	<span class="mk-ellipsis">
-		<span>.</span><span>.</span><span>.</span>
-	</span>
-</template>
-
-<style lang="stylus" scoped>
-.mk-ellipsis
-	> span
-		animation ellipsis 1.4s infinite ease-in-out both
-
-		&:nth-child(1)
-			animation-delay 0s
-
-		&:nth-child(2)
-			animation-delay 0.16s
-
-		&:nth-child(3)
-			animation-delay 0.32s
-
-	@keyframes ellipsis
-		0%, 80%, 100%
-			opacity 1
-		40%
-			opacity 0
-</style>
diff --git a/src/client/app/common/views/components/emoji-picker.vue b/src/client/app/common/views/components/emoji-picker.vue
deleted file mode 100644
index abae69e28ab2f2f86ee5bc93c27d2de7812c9afe..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/emoji-picker.vue
+++ /dev/null
@@ -1,243 +0,0 @@
-<template>
-<div class="prlncendiewqqkrevzeruhndoakghvtx">
-	<header>
-		<button v-for="category in categories"
-			:title="category.text"
-			@click="go(category)"
-			:class="{ active: category.isActive }"
-			:key="category.text"
-		>
-			<fa :icon="category.icon" fixed-width/>
-		</button>
-	</header>
-	<div class="emojis">
-		<template v-if="categories[0].isActive">
-			<header class="category"><fa :icon="faHistory" fixed-width/> {{ $t('recent-emoji') }}</header>
-			<div class="list">
-				<button v-for="(emoji, i) in ($store.state.device.recentEmojis || [])"
-					:title="emoji.name"
-					@click="chosen(emoji)"
-					:key="i"
-				>
-					<mk-emoji v-if="emoji.char != null" :emoji="emoji.char"/>
-					<img v-else :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
-				</button>
-			</div>
-		</template>
-
-		<header class="category"><fa :icon="categories.find(x => x.isActive).icon" fixed-width/> {{ categories.find(x => x.isActive).text }}</header>
-		<template v-if="categories.find(x => x.isActive).name">
-			<div class="list">
-				<button v-for="emoji in emojilist.filter(e => e.category === categories.find(x => x.isActive).name)"
-					:title="emoji.name"
-					@click="chosen(emoji)"
-					:key="emoji.name"
-				>
-					<mk-emoji :emoji="emoji.char"/>
-				</button>
-			</div>
-		</template>
-		<template v-else>
-			<div v-for="(key, i) in Object.keys(customEmojis)" :key="i">
-				<header class="sub">{{ key || $t('no-category') }}</header>
-				<div class="list">
-					<button v-for="emoji in customEmojis[key]"
-						:title="emoji.name"
-						@click="chosen(emoji)"
-						:key="emoji.name"
-					>
-						<img :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
-					</button>
-				</div>
-			</div>
-		</template>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { emojilist } from '../../../../../misc/emojilist';
-import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url';
-import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice, faGlobe, faHistory } from '@fortawesome/free-solid-svg-icons';
-import { faHeart, faFlag } from '@fortawesome/free-regular-svg-icons';
-import { groupByX } from '../../../../../prelude/array';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/emoji-picker.vue'),
-
-	data() {
-		return {
-			emojilist,
-			getStaticImageUrl,
-			customEmojis: {},
-			faGlobe, faHistory,
-			categories: [{
-				text: this.$t('custom-emoji'),
-				icon: faAsterisk,
-				isActive: true
-			}, {
-				name: 'people',
-				text: this.$t('people'),
-				icon: ['far', 'laugh'],
-				isActive: false
-			}, {
-				name: 'animals_and_nature',
-				text: this.$t('animals-and-nature'),
-				icon: faLeaf,
-				isActive: false
-			}, {
-				name: 'food_and_drink',
-				text: this.$t('food-and-drink'),
-				icon: faUtensils,
-				isActive: false
-			}, {
-				name: 'activity',
-				text: this.$t('activity'),
-				icon: faFutbol,
-				isActive: false
-			}, {
-				name: 'travel_and_places',
-				text: this.$t('travel-and-places'),
-				icon: faCity,
-				isActive: false
-			}, {
-				name: 'objects',
-				text: this.$t('objects'),
-				icon: faDice,
-				isActive: false
-			}, {
-				name: 'symbols',
-				text: this.$t('symbols'),
-				icon: faHeart,
-				isActive: false
-			}, {
-				name: 'flags',
-				text: this.$t('flags'),
-				icon: faFlag,
-				isActive: false
-			}]
-		}
-	},
-
-	created() {
-		let local = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
-		local = groupByX(local, (x: any) => x.category || '');
-		this.customEmojis = local;
-
-		if (this.$store.state.device.activeEmojiCategoryName) {
-			this.goCategory(this.$store.state.device.activeEmojiCategoryName);
-		}
-	},
-
-	methods: {
-		go(category: any) {
-			this.goCategory(category.name);
-		},
-
-		goCategory(name: string) {
-			let matched = false;
-			for (const c of this.categories) {
-				c.isActive = c.name === name;
-				if (c.isActive) {
-					matched = true;
-					this.$store.commit('device/set', { key: 'activeEmojiCategoryName', value: c.name });
-				}
-			}
-			if (!matched) {
-				this.categories[0].isActive = true;
-			}
-		},
-
-		chosen(emoji: any) {
-			const getKey = (emoji: any) => emoji.char || `:${emoji.name}:`;
-
-			let recents = this.$store.state.device.recentEmojis || [];
-			recents = recents.filter((e: any) => getKey(e) !== getKey(emoji));
-			recents.unshift(emoji)
-			this.$store.commit('device/set', { key: 'recentEmojis', value: recents.splice(0, 16) });
-
-			this.$emit('chosen', getKey(emoji));
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.prlncendiewqqkrevzeruhndoakghvtx
-	width 350px
-	background var(--face)
-
-	> header
-		display flex
-
-		> button
-			flex 1
-			padding 10px 0
-			font-size 16px
-			color var(--text)
-			transition color 0.2s ease
-
-			&:hover
-				color var(--textHighlighted)
-				transition color 0s
-
-			&.active
-				color var(--primary)
-				transition color 0s
-
-	> .emojis
-		height 300px
-		overflow-y auto
-		overflow-x hidden
-
-		> header.category
-			position sticky
-			top 0
-			left 0
-			z-index 1
-			padding 8px
-			background var(--faceHeader)
-			color var(--text)
-			font-size 12px
-
-		>>> header.sub
-			padding 4px 8px
-			color var(--text)
-			font-size 12px
-
-		>>> div.list
-			display grid
-			grid-template-columns 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr
-			gap 4px
-			padding 8px
-
-			> button
-				padding 0
-				width 100%
-
-				&:before
-					content ''
-					display block
-					width 1px
-					height 0
-					padding-bottom 100%
-
-				&:hover
-					> *
-						transform scale(1.2)
-						transition transform 0s
-
-				> *
-					position absolute
-					top 0
-					left 0
-					width 100%
-					height 100%
-					object-fit contain
-					font-size 28px
-					transition transform 0.2s ease
-					pointer-events none
-
-</style>
diff --git a/src/client/app/common/views/components/error.vue b/src/client/app/common/views/components/error.vue
deleted file mode 100644
index 0462a6efda4cb4d169d30772fdcfe1dfb9701bad..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/error.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<template>
-<div class="wjqjnyhzogztorhrdgcpqlkxhkmuetgj">
-	<p><fa icon="exclamation-triangle"/> {{ $t('@.error.title') }}</p>
-	<ui-button @click="() => $emit('retry')">{{ $t('@.error.retry') }}</ui-button>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n()
-});
-</script>
-
-<style lang="stylus" scoped>
-.wjqjnyhzogztorhrdgcpqlkxhkmuetgj
-	max-width 350px
-	margin 0 auto
-	padding 32px
-	text-align center
-	color var(--text)
-
-	> p
-		margin 0 0 8px 0
-
-</style>
diff --git a/src/client/app/common/views/components/file-type-icon.vue b/src/client/app/common/views/components/file-type-icon.vue
deleted file mode 100644
index 3a9fe768d1769239512136bf95116bb74a2854c0..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/file-type-icon.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-<template>
-<span class="mk-file-type-icon">
-	<template v-if="kind == 'image'"><fa icon="file-image"/></template>
-</span>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	props: ['type'],
-	computed: {
-		kind(): string {
-			return this.type.split('/')[0];
-		}
-	}
-});
-</script>
diff --git a/src/client/app/common/views/components/follow-button.vue b/src/client/app/common/views/components/follow-button.vue
deleted file mode 100644
index 074a0c05b6ae04889e368e972bc774dee40730e4..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/follow-button.vue
+++ /dev/null
@@ -1,209 +0,0 @@
-<template>
-<button class="wfliddvnhxvyusikowhxozkyxyenqxqr"
-	:class="{ wait, block, inline, mini, transparent, active: isFollowing || hasPendingFollowRequestFromYou }"
-	@click="onClick"
-	:disabled="wait"
-	:inline="inline"
->
-	<template v-if="!wait">
-		<fa :icon="iconAndText[0]"/> <template v-if="!mini">{{ iconAndText[1] }}</template>
-	</template>
-	<template v-else><fa icon="spinner" pulse fixed-width/></template>
-</button>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/follow-button.vue'),
-
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-		block: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		inline: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		mini: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		transparent: {
-			type: Boolean,
-			required: false,
-			default: true
-		},
-	},
-
-	data() {
-		return {
-			isFollowing: this.user.isFollowing,
-			hasPendingFollowRequestFromYou: this.user.hasPendingFollowRequestFromYou,
-			wait: false,
-			connection: null
-		};
-	},
-
-	computed: {
-		iconAndText(): any[] {
-			return (
-				(this.hasPendingFollowRequestFromYou && this.user.isLocked) ? ['hourglass-half', this.$t('request-pending')] :
-				(this.hasPendingFollowRequestFromYou && !this.user.isLocked) ? ['spinner', this.$t('follow-processing')] :
-				(this.isFollowing) ? ['minus', this.$t('following')] :
-				(!this.isFollowing && this.user.isLocked) ? ['plus', this.$t('follow-request')] :
-				(!this.isFollowing && !this.user.isLocked) ? ['plus', this.$t('follow')] :
-				[]
-			);
-		}
-	},
-
-	mounted() {
-		this.connection = this.$root.stream.useSharedConnection('main');
-
-		this.connection.on('follow', this.onFollowChange);
-		this.connection.on('unfollow', this.onFollowChange);
-	},
-
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		onFollowChange(user) {
-			if (user.id == this.user.id) {
-				this.isFollowing = user.isFollowing;
-				this.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
-			}
-		},
-
-		async onClick() {
-			this.wait = true;
-
-			try {
-				if (this.isFollowing) {
-					const { canceled } = await this.$root.dialog({
-						type: 'warning',
-						text: this.$t('@.unfollow-confirm', { name: this.user.name || this.user.username }),
-						showCancelButton: true
-					});
-
-					if (canceled) return;
-
-					await this.$root.api('following/delete', {
-						userId: this.user.id
-					});
-				} else {
-					if (this.hasPendingFollowRequestFromYou) {
-						await this.$root.api('following/requests/cancel', {
-							userId: this.user.id
-						});
-					} else if (this.user.isLocked) {
-						await this.$root.api('following/create', {
-							userId: this.user.id
-						});
-						this.hasPendingFollowRequestFromYou = true;
-					} else {
-						await this.$root.api('following/create', {
-							userId: this.user.id
-						});
-						this.hasPendingFollowRequestFromYou = true;
-					}
-				}
-			} catch (e) {
-				console.error(e);
-			} finally {
-				this.wait = false;
-			}
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.wfliddvnhxvyusikowhxozkyxyenqxqr
-	display block
-	user-select none
-	cursor pointer
-	padding 0 16px
-	margin 0
-	min-width 100px
-	line-height 36px
-	font-size 14px
-	font-weight bold
-	color var(--primary)
-	background transparent
-	outline none
-	border solid 1px var(--primary)
-	border-radius 36px
-
-	&:not(.transparent)
-		background #fff
-
-	&.inline
-		display inline-block
-
-	&.mini
-		padding 0
-		min-width 0
-		width 32px
-		height 32px
-		font-size 16px
-		border-radius 4px
-		line-height 32px
-
-		&:focus
-			&:after
-				border-radius 8px
-
-	&.block
-		width 100%
-
-	&:focus
-		&:after
-			content ""
-			pointer-events none
-			position absolute
-			top -5px
-			right -5px
-			bottom -5px
-			left -5px
-			border 2px solid var(--primaryAlpha03)
-			border-radius 36px
-
-	&:hover
-		background var(--primaryAlpha01)
-
-	&:active
-		background var(--primaryAlpha02)
-
-	&.active
-		color var(--primaryForeground)
-		background var(--primary)
-
-		&:hover
-			background var(--primaryLighten10)
-			border-color var(--primaryLighten10)
-
-		&:active
-			background var(--primaryDarken10)
-			border-color var(--primaryDarken10)
-
-	&.wait
-		cursor wait !important
-		opacity 0.7
-
-	*
-		pointer-events none
-
-</style>
diff --git a/src/client/app/common/views/components/forkit.vue b/src/client/app/common/views/components/forkit.vue
deleted file mode 100644
index 328e3ca7b09e125ac26030165bc4abd258e84596..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/forkit.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<template>
-<a class="a" :href="repositoryUrl" rel="noopener" target="_blank" title="View source on GitHub">
-	<svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="aria-hidden">
-		<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
-		<path class="octo-arm" d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor"></path>
-		<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor"></path>
-	</svg>
-</a>
-</template>
-
-<script lang="ts">
-import Vue from 'vue'
-export default Vue.extend({
-	data() {
-		return {
-			repositoryUrl: 'https://github.com/syuilo/misskey'
-		};
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.a
-	display block
-
-	> svg
-		display block
-		//fill #151513
-		//color #fff
-		fill var(--primary)
-		color var(--primaryForeground)
-
-		.octo-arm
-			transform-origin 130px 106px
-
-	&:hover
-		.octo-arm
-			animation octocat-wave 560ms ease-in-out
-
-	@keyframes octocat-wave
-		0%, 100%
-			transform rotate(0)
-		20%, 60%
-			transform rotate(-25deg)
-		40%, 80%
-			transform rotate(10deg)
-
-</style>
diff --git a/src/client/app/common/views/components/frac.vue b/src/client/app/common/views/components/frac.vue
deleted file mode 100644
index 1840bd28fe7af736f0adca5005f391a87e4e1a29..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/frac.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<template>
-<span class="mk-frac"><span>{{ pad }}</span><span>{{ value }} / {{ total }}</span></span>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n(),
-	props: {
-		value: {
-			type: Number,
-			required: true,
-		},
-		total: {
-			type: Number,
-			required: true,
-		},
-	},
-	computed: {
-		pad(this: {
-			value: number;
-			total: number;
-			length(value: number): number;
-		}) {
-			return '0'.repeat(this.length(this.total) - this.length(this.value));
-		},
-	},
-	methods: {
-		length(value: number) {
-			const string = value.toString();
-
-			return string.includes('e') ? -~string.substr(string.indexOf('e')) : string.length;
-		},
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-frac
-	-webkit-font-feature-settings 'tnum'
-	-moz-font-feature-settings 'tnum'
-	font-feature-settings 'tnum'
-	font-variant-numeric tabular-nums
-
-	> :first-child
-		visibility hidden
-</style>
diff --git a/src/client/app/common/views/components/games/reversi/reversi.game.vue b/src/client/app/common/views/components/games/reversi/reversi.game.vue
deleted file mode 100644
index a7c918aa7188c7894ece12700cb561e46fe14352..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/games/reversi/reversi.game.vue
+++ /dev/null
@@ -1,473 +0,0 @@
-<template>
-<div class="xqnhankfuuilcwvhgsopeqncafzsquya">
-	<button class="go-index" v-if="selfNav" @click="goIndex"><fa icon="arrow-left"/></button>
-	<header><b><router-link :to="blackUser | userPage"><mk-user-name :user="blackUser"/></router-link></b>({{ $t('@.reversi.black') }}) vs <b><router-link :to="whiteUser | userPage"><mk-user-name :user="whiteUser"/></router-link></b>({{ $t('@.reversi.white') }})</header>
-
-	<div style="overflow: hidden; line-height: 28px;">
-		<p class="turn" v-if="!iAmPlayer && !game.isEnded">
-			<mfm :key="'turn:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.turn-of', { name: $options.filters.userName(turnUser) })" :plain="true" :custom-emojis="turnUser.emojis"/>
-			<mk-ellipsis/>
-		</p>
-		<p class="turn" v-if="logPos != logs.length">
-			<mfm :key="'past-turn-of:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.past-turn-of', { name: $options.filters.userName(turnUser) })" :plain="true" :custom-emojis="turnUser.emojis"/>
-		</p>
-		<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">{{ $t('@.reversi.opponent-turn') }}<mk-ellipsis/></p>
-		<p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">{{ $t('@.reversi.my-turn') }}</p>
-		<p class="result" v-if="game.isEnded && logPos == logs.length">
-			<template v-if="game.winner">
-				<mfm :key="'won'" :text="$t('@.reversi.won', { name: $options.filters.userName(game.winner) })" :plain="true" :custom-emojis="game.winner.emojis"/>
-				<span v-if="game.surrendered != null"> ({{ $t('surrendered') }})</span>
-			</template>
-			<template v-else>{{ $t('@.reversi.drawn') }}</template>
-		</p>
-	</div>
-
-	<div class="board">
-		<div class="labels-x" v-if="$store.state.settings.gamesReversiShowBoardLabels">
-			<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
-		</div>
-		<div class="flex">
-			<div class="labels-y" v-if="$store.state.settings.gamesReversiShowBoardLabels">
-				<div v-for="i in game.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}`">
-					<template v-if="$store.state.settings.gamesReversiUseAvatarStones">
-						<img v-if="stone === true" :src="blackUser.avatarUrl" alt="black">
-						<img v-if="stone === false" :src="whiteUser.avatarUrl" alt="white">
-					</template>
-					<template v-else>
-						<fa v-if="stone === true" :icon="fasCircle"/>
-						<fa v-if="stone === false" :icon="farCircle"/>
-					</template>
-				</div>
-			</div>
-			<div class="labels-y" v-if="this.$store.state.settings.gamesReversiShowBoardLabels">
-				<div v-for="i in game.map.length">{{ i }}</div>
-			</div>
-		</div>
-		<div class="labels-x" v-if="this.$store.state.settings.gamesReversiShowBoardLabels">
-			<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
-		</div>
-	</div>
-
-	<p class="status"><b>{{ $t('@.reversi.this-turn', { count: logPos }) }}</b> {{ $t('@.reversi.black') }}:{{ o.blackCount }} {{ $t('@.reversi.white') }}:{{ o.whiteCount }} {{ $t('@.reversi.total') }}:{{ o.blackCount + o.whiteCount }}</p>
-
-	<div class="actions" v-if="!game.isEnded && iAmPlayer">
-		<form-button @click="surrender">{{ $t('surrender') }}</form-button>
-	</div>
-
-	<div class="player" v-if="game.isEnded">
-		<span>{{ logPos }} / {{ logs.length }}</span>
-		<ui-horizon-group>
-			<ui-button @click="logPos = 0" :disabled="logPos == 0"><fa :icon="faAngleDoubleLeft"/></ui-button>
-			<ui-button @click="logPos--" :disabled="logPos == 0"><fa :icon="faAngleLeft"/></ui-button>
-			<ui-button @click="logPos++" :disabled="logPos == logs.length"><fa :icon="faAngleRight"/></ui-button>
-			<ui-button @click="logPos = logs.length" :disabled="logPos == logs.length"><fa :icon="faAngleDoubleRight"/></ui-button>
-		</ui-horizon-group>
-	</div>
-
-	<div class="info">
-		<p v-if="game.isLlotheo">{{ $t('is-llotheo') }}</p>
-		<p v-if="game.loopedBoard">{{ $t('looped-map') }}</p>
-		<p v-if="game.canPutEverywhere">{{ $t('can-put-everywhere') }}</p>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../../i18n';
-import * as CRC32 from 'crc-32';
-import Reversi, { Color } from '../../../../../../../games/reversi/core';
-import { url } from '../../../../../config';
-import { faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons';
-import { faCircle as fasCircle } from '@fortawesome/free-solid-svg-icons';
-import { faCircle as farCircle } from '@fortawesome/free-regular-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/games/reversi/reversi.game.vue'),
-	props: {
-		initGame: {
-			type: Object,
-			require: true
-		},
-		connection: {
-			type: Object,
-			require: true
-		},
-		selfNav: {
-			type: Boolean,
-			require: true
-		}
-	},
-
-	data() {
-		return {
-			game: null,
-			o: null as Reversi,
-			logs: [],
-			logPos: 0,
-			pollingClock: null,
-			faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight, fasCircle, farCircle
-		};
-	},
-
-	computed: {
-		iAmPlayer(): boolean {
-			if (!this.$store.getters.isSignedIn) return false;
-			return this.game.user1Id == this.$store.state.i.id || this.game.user2Id == this.$store.state.i.id;
-		},
-
-		myColor(): Color {
-			if (!this.iAmPlayer) return null;
-			if (this.game.user1Id == this.$store.state.i.id && this.game.black == 1) return true;
-			if (this.game.user2Id == this.$store.state.i.id && this.game.black == 2) return true;
-			return false;
-		},
-
-		opColor(): Color {
-			if (!this.iAmPlayer) return null;
-			return this.myColor === true ? false : true;
-		},
-
-		blackUser(): any {
-			return this.game.black == 1 ? this.game.user1 : this.game.user2;
-		},
-
-		whiteUser(): any {
-			return this.game.black == 1 ? this.game.user2 : this.game.user1;
-		},
-
-		turnUser(): any {
-			if (this.o.turn === true) {
-				return this.game.black == 1 ? this.game.user1 : this.game.user2;
-			} else if (this.o.turn === false) {
-				return this.game.black == 1 ? this.game.user2 : this.game.user1;
-			} else {
-				return null;
-			}
-		},
-
-		isMyTurn(): boolean {
-			if (!this.iAmPlayer) return false;
-			if (this.turnUser == null) return false;
-			return this.turnUser.id == this.$store.state.i.id;
-		},
-
-		cellsStyle(): any {
-			return {
-				'grid-template-rows': `repeat(${this.game.map.length}, 1fr)`,
-				'grid-template-columns': `repeat(${this.game.map[0].length}, 1fr)`
-			};
-		}
-	},
-
-	watch: {
-		logPos(v) {
-			if (!this.game.isEnded) return;
-			this.o = new Reversi(this.game.map, {
-				isLlotheo: this.game.isLlotheo,
-				canPutEverywhere: this.game.canPutEverywhere,
-				loopedBoard: this.game.loopedBoard
-			});
-			for (const log of this.logs.slice(0, v)) {
-				this.o.put(log.color, log.pos);
-			}
-			this.$forceUpdate();
-		}
-	},
-
-	created() {
-		this.game = this.initGame;
-
-		this.o = new Reversi(this.game.map, {
-			isLlotheo: this.game.isLlotheo,
-			canPutEverywhere: this.game.canPutEverywhere,
-			loopedBoard: this.game.loopedBoard
-		});
-
-		for (const log of this.game.logs) {
-			this.o.put(log.color, log.pos);
-		}
-
-		this.logs = this.game.logs;
-		this.logPos = this.logs.length;
-
-		// 通信を取りこぼしてもいいように定期的にポーリングさせる
-		if (this.game.isStarted && !this.game.isEnded) {
-			this.pollingClock = setInterval(() => {
-				if (this.game.isEnded) return;
-				const crc32 = CRC32.str(this.logs.map(x => x.pos.toString()).join(''));
-				this.connection.send('check', {
-					crc32: crc32
-				});
-			}, 3000);
-		}
-	},
-
-	mounted() {
-		this.connection.on('set', this.onSet);
-		this.connection.on('rescue', this.onRescue);
-		this.connection.on('ended', this.onEnded);
-	},
-
-	beforeDestroy() {
-		this.connection.off('set', this.onSet);
-		this.connection.off('rescue', this.onRescue);
-		this.connection.off('ended', this.onEnded);
-
-		clearInterval(this.pollingClock);
-	},
-
-	methods: {
-		set(pos) {
-			if (this.game.isEnded) return;
-			if (!this.iAmPlayer) return;
-			if (!this.isMyTurn) return;
-			if (!this.o.canPut(this.myColor, pos)) return;
-
-			this.o.put(this.myColor, pos);
-
-			// サウンドを再生する
-			if (this.$store.state.device.enableSounds) {
-				const sound = new Audio(`${url}/assets/reversi-put-me.mp3`);
-				sound.volume = this.$store.state.device.soundVolume;
-				sound.play();
-			}
-
-			this.connection.send('set', {
-				pos: pos
-			});
-
-			this.checkEnd();
-
-			this.$forceUpdate();
-		},
-
-		onSet(x) {
-			this.logs.push(x);
-			this.logPos++;
-			this.o.put(x.color, x.pos);
-			this.checkEnd();
-			this.$forceUpdate();
-
-			// サウンドを再生する
-			if (this.$store.state.device.enableSounds && x.color != this.myColor) {
-				const sound = new Audio(`${url}/assets/reversi-put-you.mp3`);
-				sound.volume = this.$store.state.device.soundVolume;
-				sound.play();
-			}
-		},
-
-		onEnded(x) {
-			this.game = x.game;
-		},
-
-		checkEnd() {
-			this.game.isEnded = this.o.isEnded;
-			if (this.game.isEnded) {
-				if (this.o.winner === true) {
-					this.game.winnerId = this.game.black == 1 ? this.game.user1Id : this.game.user2Id;
-					this.game.winner = this.game.black == 1 ? this.game.user1 : this.game.user2;
-				} else if (this.o.winner === false) {
-					this.game.winnerId = this.game.black == 1 ? this.game.user2Id : this.game.user1Id;
-					this.game.winner = this.game.black == 1 ? this.game.user2 : this.game.user1;
-				} else {
-					this.game.winnerId = null;
-					this.game.winner = null;
-				}
-			}
-		},
-
-		// 正しいゲーム情報が送られてきたとき
-		onRescue(game) {
-			this.game = game;
-
-			this.o = new Reversi(this.game.map, {
-				isLlotheo: this.game.isLlotheo,
-				canPutEverywhere: this.game.canPutEverywhere,
-				loopedBoard: this.game.loopedBoard
-			});
-
-			for (const log of this.game.logs) {
-				this.o.put(log.color, log.pos, true);
-			}
-
-			this.logs = this.game.logs;
-			this.logPos = this.logs.length;
-
-			this.checkEnd();
-			this.$forceUpdate();
-		},
-
-		surrender() {
-			this.$root.api('games/reversi/games/surrender', {
-				gameId: this.game.id
-			});
-		},
-
-		goIndex() {
-			this.$emit('go-index');
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.xqnhankfuuilcwvhgsopeqncafzsquya
-	text-align center
-
-	> .go-index
-		position absolute
-		top 0
-		left 0
-		z-index 1
-		width 42px
-		height 42px
-
-	> header
-		padding 8px
-		border-bottom dashed 1px var(--reversiGameHeaderLine)
-
-		a
-			color inherit
-
-	> .board
-		width calc(100% - 16px)
-		max-width 500px
-		margin 0 auto
-
-		$label-size = 16px
-		$gap = 4px
-
-		> .labels-x
-			height $label-size
-			padding 0 $label-size
-			display flex
-
-			> *
-				flex 1
-				display flex
-				align-items center
-				justify-content center
-				font-size 12px
-
-				&:first-child
-					margin-left -($gap / 2)
-
-				&:last-child
-					margin-right -($gap / 2)
-
-		> .flex
-			display flex
-
-			> .labels-y
-				width $label-size
-				display flex
-				flex-direction column
-
-				> *
-					flex 1
-					display flex
-					align-items center
-					justify-content center
-					font-size 12px
-
-					&:first-child
-						margin-top -($gap / 2)
-
-					&:last-child
-						margin-bottom -($gap / 2)
-
-			> .cells
-				flex 1
-				display grid
-				grid-gap $gap
-
-				> div
-					background transparent
-					border-radius 6px
-					overflow hidden
-
-					*
-						pointer-events none
-						user-select none
-
-					&.empty
-						border solid 2px var(--reversiGameEmptyCell)
-
-					&.empty.can
-						background var(--reversiGameEmptyCell)
-
-					&.empty.myTurn
-						border-color var(--reversiGameEmptyCellMyTurn)
-
-						&.can
-							background var(--reversiGameEmptyCellCanPut)
-							cursor pointer
-
-							&:hover
-								border-color var(--primaryDarken10)
-								background var(--primary)
-
-							&:active
-								background var(--primaryDarken10)
-
-					&.prev
-						box-shadow 0 0 0 4px var(--primaryAlpha07)
-
-					&.isEnded
-						border-color var(--reversiGameEmptyCellMyTurn)
-
-					&.none
-						border-color transparent !important
-
-					> svg
-						display block
-						width 100%
-						height 100%
-
-					> img
-						display block
-						width 100%
-						height 100%
-
-	> .graph
-		display grid
-		grid-template-columns repeat(61, 1fr)
-		width 300px
-		height 38px
-		margin 0 auto 16px auto
-
-		> div
-			&:not(:empty)
-				background #ccc
-
-			> div:first-child
-				background #333
-
-			> div:last-child
-				background #ccc
-
-	> .status
-		margin 0
-		padding 16px 0
-
-	> .actions
-		padding-bottom 16px
-
-	> .player
-		padding 0 16px 32px 16px
-		margin 0 auto
-		max-width 500px
-
-		> span
-			display inline-block
-			margin 0 8px
-			min-width 70px
-
-</style>
diff --git a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue b/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue
deleted file mode 100644
index 40993895024a39b061a0e50a6da03aa029a72cf4..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-<template>
-<div>
-	<x-room v-if="!g.isStarted" :game="g" :connection="connection"/>
-	<x-game v-else :init-game="g" :connection="connection" :self-nav="selfNav" @go-index="goIndex"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../../i18n';
-import XGame from './reversi.game.vue';
-import XRoom from './reversi.room.vue';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/games/reversi/reversi.gameroom.vue'),
-	components: {
-		XGame,
-		XRoom
-	},
-	props: {
-		game: {
-			type: Object,
-			required: true
-		},
-		selfNav: {
-			type: Boolean,
-			require: true
-		}
-	},
-	data() {
-		return {
-			connection: null,
-			g: null
-		};
-	},
-	created() {
-		this.g = this.game;
-		this.connection = this.$root.stream.connectToChannel('gamesReversiGame', {
-			gameId: this.game.id
-		});
-		this.connection.on('started', this.onStarted);
-	},
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-	methods: {
-		onStarted(game) {
-			Object.assign(this.g, game);
-			this.$forceUpdate();
-		},
-		goIndex() {
-			this.$emit('go-index');
-		}
-	}
-});
-</script>
diff --git a/src/client/app/common/views/components/games/reversi/reversi.index.vue b/src/client/app/common/views/components/games/reversi/reversi.index.vue
deleted file mode 100644
index 94e1d9a7e3970cace4915d6694e5528a00b308a6..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/games/reversi/reversi.index.vue
+++ /dev/null
@@ -1,245 +0,0 @@
-<template>
-<div class="phgnkghfpyvkrvwiajkiuoxyrdaqpzcx">
-	<h1>{{ $t('title') }}</h1>
-	<p>{{ $t('sub-title') }}</p>
-	<div class="play">
-		<form-button primary round @click="match">{{ $t('invite') }}</form-button>
-		<details>
-			<summary>{{ $t('rule') }}</summary>
-			<div>
-				<p>{{ $t('rule-desc') }}</p>
-				<dl>
-					<dt><b>{{ $t('mode-invite') }}</b></dt>
-					<dd>{{ $t('mode-invite-desc') }}</dd>
-				</dl>
-			</div>
-		</details>
-	</div>
-	<section v-if="invitations.length > 0">
-		<h2>{{ $t('invitations') }}</h2>
-		<div class="invitation" v-for="i in invitations" tabindex="-1" @click="accept(i)">
-			<mk-avatar class="avatar" :user="i.parent"/>
-			<span class="name"><b><mk-user-name :user="i.parent"/></b></span>
-			<span class="username">@{{ i.parent.username }}</span>
-			<mk-time :time="i.createdAt"/>
-		</div>
-	</section>
-	<section v-if="myGames.length > 0">
-		<h2>{{ $t('my-games') }}</h2>
-		<a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/games/reversi/${g.id}`">
-			<mk-avatar class="avatar" :user="g.user1"/>
-			<mk-avatar class="avatar" :user="g.user2"/>
-			<span><b><mk-user-name :user="g.user1"/></b> vs <b><mk-user-name :user="g.user2"/></b></span>
-			<span class="state">{{ g.isEnded ? $t('game-state.ended') : $t('game-state.playing') }}</span>
-			<mk-time :time="g.createdAt" />
-		</a>
-	</section>
-	<section v-if="games.length > 0">
-		<h2>{{ $t('all-games') }}</h2>
-		<a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/games/reversi/${g.id}`">
-			<mk-avatar class="avatar" :user="g.user1"/>
-			<mk-avatar class="avatar" :user="g.user2"/>
-			<span><b><mk-user-name :user="g.user1"/></b> vs <b><mk-user-name :user="g.user2"/></b></span>
-			<span class="state">{{ g.isEnded ? $t('game-state.ended') : $t('game-state.playing') }}</span>
-			<mk-time :time="g.createdAt" />
-		</a>
-	</section>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/games/reversi/reversi.index.vue'),
-	data() {
-		return {
-			games: [],
-			gamesFetching: true,
-			gamesMoreFetching: false,
-			myGames: [],
-			matching: null,
-			invitations: [],
-			connection: null
-		};
-	},
-
-	mounted() {
-		if (this.$store.getters.isSignedIn) {
-			this.connection = this.$root.stream.useSharedConnection('gamesReversi');
-
-			this.connection.on('invited', this.onInvited);
-
-			this.$root.api('games/reversi/games', {
-				my: true
-			}).then(games => {
-				this.myGames = games;
-			});
-
-			this.$root.api('games/reversi/invitations').then(invitations => {
-				this.invitations = this.invitations.concat(invitations);
-			});
-		}
-
-		this.$root.api('games/reversi/games').then(games => {
-			this.games = games;
-			this.gamesFetching = false;
-		});
-	},
-
-	beforeDestroy() {
-		if (this.connection) {
-			this.connection.dispose();
-		}
-	},
-
-	methods: {
-		go(game) {
-			this.$emit('go', game);
-		},
-
-		async match() {
-			const { result: user } = await this.$root.dialog({
-				title: this.$t('enter-username'),
-				user: {
-					local: true
-				}
-			});
-			if (user == null) return;
-			this.$root.api('games/reversi/match', {
-				userId: user.id
-			}).then(res => {
-				if (res == null) {
-					this.$emit('matching', user);
-				} else {
-					this.$emit('go', res);
-				}
-			});
-		},
-
-		accept(invitation) {
-			this.$root.api('games/reversi/match', {
-				userId: invitation.parent.id
-			}).then(game => {
-				if (game) {
-					this.$emit('go', game);
-				}
-			});
-		},
-
-		onInvited(invite) {
-			this.invitations.unshift(invite);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.phgnkghfpyvkrvwiajkiuoxyrdaqpzcx
-	> h1
-		margin 0
-		padding 24px
-		font-size 24px
-		text-align center
-		font-weight normal
-		color #fff
-		background linear-gradient(to bottom, var(--reversiBannerGradientStart), var(--reversiBannerGradientEnd))
-
-		& + p
-			margin 0
-			padding 12px
-			margin-bottom 12px
-			text-align center
-			font-size 14px
-			border-bottom solid 1px var(--faceDivider)
-
-	> .play
-		margin 0 auto
-		padding 0 16px
-		max-width 500px
-		text-align center
-
-		> details
-			margin 8px 0
-
-			> div
-				padding 16px
-				font-size 14px
-				text-align left
-				background var(--reversiDescBg)
-				border-radius 8px
-
-	> section
-		margin 0 auto
-		padding 0 16px 16px 16px
-		max-width 500px
-		border-top solid 1px var(--faceDivider)
-
-		> h2
-			margin 0
-			padding 16px 0 8px 0
-			font-size 16px
-			font-weight bold
-
-	.invitation
-		margin 8px 0
-		padding 8px
-		color var(--text)
-		background var(--face)
-		box-shadow 0 2px 16px var(--reversiListItemShadow)
-		border-radius 6px
-		cursor pointer
-
-		*
-			pointer-events none
-			user-select none
-
-		&:focus
-			border-color var(--primary)
-
-		&:hover
-			box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
-
-		&:active
-			box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
-
-		> .avatar
-			width 32px
-			height 32px
-			border-radius 100%
-
-		> span
-			margin 0 8px
-			line-height 32px
-
-	.game
-		display block
-		margin 8px 0
-		padding 8px
-		color var(--text)
-		background var(--face)
-		box-shadow 0 2px 16px var(--reversiListItemShadow)
-		border-radius 6px
-		cursor pointer
-
-		*
-			pointer-events none
-			user-select none
-
-		&:hover
-			box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
-
-		&:active
-			box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
-
-		> .avatar
-			width 32px
-			height 32px
-			border-radius 100%
-
-		> span
-			margin 0 8px
-			line-height 32px
-
-</style>
diff --git a/src/client/app/common/views/components/games/reversi/reversi.room.vue b/src/client/app/common/views/components/games/reversi/reversi.room.vue
deleted file mode 100644
index c1657f49e5a905c29ff103fa36a73aaa706004bb..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/games/reversi/reversi.room.vue
+++ /dev/null
@@ -1,355 +0,0 @@
-<template>
-<div class="urbixznjwwuukfsckrwzwsqzsxornqij">
-	<header><b><mk-user-name :user="game.user1"/></b> vs <b><mk-user-name :user="game.user2"/></b></header>
-
-	<div>
-		<p>{{ $t('settings-of-the-game') }}</p>
-
-		<div class="card map">
-			<header>
-				<select v-model="mapName" :placeholder="$t('choose-map')" @change="onMapChange">
-					<option label="-Custom-" :value="mapName" v-if="mapName == '-Custom-'"/>
-					<option :label="$t('random')" :value="null"/>
-					<optgroup v-for="c in mapCategories" :key="c" :label="c">
-						<option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name">{{ m.name }}</option>
-					</optgroup>
-				</select>
-			</header>
-
-			<div>
-				<div class="random" v-if="game.map == null"><fa icon="dice"/></div>
-				<div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }">
-					<div v-for="(x, i) in game.map.join('')"
-							:data-none="x == ' '"
-							@click="onPixelClick(i, x)">
-						<fa v-if="x == 'b'" :icon="fasCircle"/>
-						<fa v-if="x == 'w'" :icon="farCircle"/>
-					</div>
-				</div>
-			</div>
-		</div>
-
-		<div class="card">
-			<header>
-				<span>{{ $t('black-or-white') }}</span>
-			</header>
-
-			<div>
-				<form-radio v-model="game.bw" value="random" @change="updateSettings('bw')">{{ $t('random') }}</form-radio>
-				<form-radio v-model="game.bw" :value="1" @change="updateSettings('bw')">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user1"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio>
-				<form-radio v-model="game.bw" :value="2" @change="updateSettings('bw')">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user2"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio>
-			</div>
-		</div>
-
-		<div class="card">
-			<header>
-				<span>{{ $t('rules') }}</span>
-			</header>
-
-			<div>
-				<ui-switch v-model="game.isLlotheo" @change="updateSettings('isLlotheo')">{{ $t('is-llotheo') }}</ui-switch>
-				<ui-switch v-model="game.loopedBoard" @change="updateSettings('loopedBoard')">{{ $t('looped-map') }}</ui-switch>
-				<ui-switch v-model="game.canPutEverywhere" @change="updateSettings('canPutEverywhere')">{{ $t('can-put-everywhere') }}</ui-switch>
-			</div>
-		</div>
-
-		<div class="card form" v-if="form">
-			<header>
-				<span>{{ $t('settings-of-the-bot') }}</span>
-			</header>
-
-			<div>
-				<template v-for="item in form">
-					<ui-switch v-if="item.type == 'switch'" v-model="item.value" :key="item.id" @change="onChangeForm(item)">{{ item.label || item.desc || '' }}</ui-switch>
-
-					<div class="card" v-if="item.type == 'radio'" :key="item.id">
-						<header>
-							<span>{{ item.label }}</span>
-						</header>
-
-						<div>
-							<form-radio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :value="r.value" @change="onChangeForm(item)">{{ r.label }}</form-radio>
-						</div>
-					</div>
-
-					<div class="card" v-if="item.type == 'slider'" :key="item.id">
-						<header>
-							<span>{{ item.label }}</span>
-						</header>
-
-						<div>
-							<input type="range" :min="item.min" :max="item.max" :step="item.step || 1" v-model="item.value" @change="onChangeForm(item)"/>
-						</div>
-					</div>
-
-					<div class="card" v-if="item.type == 'textbox'" :key="item.id">
-						<header>
-							<span>{{ item.label }}</span>
-						</header>
-
-						<div>
-							<input v-model="item.value" @change="onChangeForm(item)"/>
-						</div>
-					</div>
-				</template>
-			</div>
-		</div>
-	</div>
-
-	<footer>
-		<p class="status">
-			<template v-if="isAccepted && isOpAccepted">{{ $t('this-game-is-started-soon') }}<mk-ellipsis/></template>
-			<template v-if="isAccepted && !isOpAccepted">{{ $t('waiting-for-other') }}<mk-ellipsis/></template>
-			<template v-if="!isAccepted && isOpAccepted">{{ $t('waiting-for-me') }}</template>
-			<template v-if="!isAccepted && !isOpAccepted">{{ $t('waiting-for-both') }}<mk-ellipsis/></template>
-		</p>
-
-		<div class="actions">
-			<form-button @click="exit">{{ $t('cancel') }}</form-button>
-			<form-button primary @click="accept" v-if="!isAccepted">{{ $t('ready') }}</form-button>
-			<form-button primary @click="cancel" v-if="isAccepted">{{ $t('cancel-ready') }}</form-button>
-		</div>
-	</footer>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../../i18n';
-import * as maps from '../../../../../../../games/reversi/maps';
-import { faCircle as fasCircle } from '@fortawesome/free-solid-svg-icons';
-import { faCircle as farCircle } from '@fortawesome/free-regular-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/games/reversi/reversi.room.vue'),
-	props: ['game', 'connection'],
-
-	data() {
-		return {
-			o: null,
-			isLlotheo: false,
-			mapName: maps.eighteight.name,
-			maps: maps,
-			form: null,
-			messages: [],
-			fasCircle, farCircle
-		};
-	},
-
-	computed: {
-		mapCategories(): string[] {
-			const categories = Object.values(maps).map(x => x.category);
-			return categories.filter((item, pos) => categories.indexOf(item) == pos);
-		},
-		isAccepted(): boolean {
-			if (this.game.user1Id == this.$store.state.i.id && this.game.user1Accepted) return true;
-			if (this.game.user2Id == this.$store.state.i.id && this.game.user2Accepted) return true;
-			return false;
-		},
-		isOpAccepted(): boolean {
-			if (this.game.user1Id != this.$store.state.i.id && this.game.user1Accepted) return true;
-			if (this.game.user2Id != this.$store.state.i.id && this.game.user2Accepted) return true;
-			return false;
-		}
-	},
-
-	created() {
-		this.connection.on('changeAccepts', this.onChangeAccepts);
-		this.connection.on('updateSettings', this.onUpdateSettings);
-		this.connection.on('initForm', this.onInitForm);
-		this.connection.on('message', this.onMessage);
-
-		if (this.game.user1Id != this.$store.state.i.id && this.game.form1) this.form = this.game.form1;
-		if (this.game.user2Id != this.$store.state.i.id && this.game.form2) this.form = this.game.form2;
-	},
-
-	beforeDestroy() {
-		this.connection.off('changeAccepts', this.onChangeAccepts);
-		this.connection.off('updateSettings', this.onUpdateSettings);
-		this.connection.off('initForm', this.onInitForm);
-		this.connection.off('message', this.onMessage);
-	},
-
-	methods: {
-		exit() {
-
-		},
-
-		accept() {
-			this.connection.send('accept', {});
-		},
-
-		cancel() {
-			this.connection.send('cancelAccept', {});
-		},
-
-		onChangeAccepts(accepts) {
-			this.game.user1Accepted = accepts.user1;
-			this.game.user2Accepted = accepts.user2;
-			this.$forceUpdate();
-		},
-
-		updateSettings(key: string) {
-			this.connection.send('updateSettings', {
-				key: key,
-				value: this.game[key]
-			});
-		},
-
-		onUpdateSettings({ key, value }) {
-			this.game[key] = value;
-			if (this.game.map == null) {
-				this.mapName = null;
-			} else {
-				const found = Object.values(maps).find(x => x.data.join('') == this.game.map.join(''));
-				this.mapName = found ? found.name : '-Custom-';
-			}
-		},
-
-		onInitForm(x) {
-			if (x.userId == this.$store.state.i.id) return;
-			this.form = x.form;
-		},
-
-		onMessage(x) {
-			if (x.userId == this.$store.state.i.id) return;
-			this.messages.unshift(x.message);
-		},
-
-		onChangeForm(item) {
-			this.connection.send('updateForm', {
-				id: item.id,
-				value: item.value
-			});
-		},
-
-		onMapChange() {
-			if (this.mapName == null) {
-				this.game.map = null;
-			} else {
-				this.game.map = Object.values(maps).find(x => x.name == this.mapName).data;
-			}
-			this.$forceUpdate();
-			this.updateSettings('map');
-		},
-
-		onPixelClick(pos, pixel) {
-			const x = pos % this.game.map[0].length;
-			const y = Math.floor(pos / this.game.map[0].length);
-			const newPixel =
-				pixel == ' ' ? '-' :
-				pixel == '-' ? 'b' :
-				pixel == 'b' ? 'w' :
-				' ';
-			const line = this.game.map[y].split('');
-			line[x] = newPixel;
-			this.$set(this.game.map, y, line.join(''));
-			this.$forceUpdate();
-			this.updateSettings('map');
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.urbixznjwwuukfsckrwzwsqzsxornqij
-	text-align center
-	background var(--bg)
-
-	> header
-		padding 8px
-		border-bottom dashed 1px #c4cdd4
-
-	> div
-		padding 0 16px
-
-		> .card
-			margin 0 auto 16px auto
-
-			&.map
-				> header
-					> select
-						width 100%
-						padding 12px 14px
-						background var(--face)
-						border 1px solid var(--reversiMapSelectBorder)
-						border-radius 4px
-						color var(--text)
-						cursor pointer
-						transition border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1)
-						-webkit-appearance none
-						-moz-appearance none
-						appearance none
-
-						&:hover
-							border-color var(--reversiMapSelectHoverBorder)
-
-						&:focus
-						&:active
-							border-color var(--primary)
-
-				> div
-					> .random
-						padding 32px 0
-						font-size 64px
-						color var(--text)
-						opacity 0.7
-
-					> .board
-						display grid
-						grid-gap 4px
-						width 300px
-						height 300px
-						margin 0 auto
-						color var(--text)
-
-						> div
-							background transparent
-							border solid 2px var(--faceDivider)
-							border-radius 6px
-							overflow hidden
-							cursor pointer
-
-							*
-								pointer-events none
-								user-select none
-								width 100%
-								height 100%
-
-							&[data-none]
-								border-color transparent
-
-			&.form
-				> div
-					> .card + .card
-						margin-top 16px
-
-					input[type='range']
-						width 100%
-
-		.card
-			max-width 400px
-			border-radius 4px
-			background var(--face)
-			color var(--text)
-			box-shadow 0 2px 12px 0 var(--reversiRoomFormShadow)
-
-			> header
-				padding 18px 20px
-				border-bottom 1px solid var(--faceDivider)
-
-			> div
-				padding 20px
-				color var(--text)
-
-	> footer
-		position sticky
-		bottom 0
-		padding 16px
-		background var(--reversiRoomFooterBg)
-		border-top solid 1px var(--faceDivider)
-
-		> .status
-			margin 0 0 16px 0
-
-</style>
diff --git a/src/client/app/common/views/components/games/reversi/reversi.vue b/src/client/app/common/views/components/games/reversi/reversi.vue
deleted file mode 100644
index d33471a04981bbeb1a3fd6178247cc323c468125..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/games/reversi/reversi.vue
+++ /dev/null
@@ -1,175 +0,0 @@
-<template>
-<div class="vchtoekanapleubgzioubdtmlkribzfd">
-	<div v-if="game">
-		<x-gameroom :game="game" :self-nav="selfNav" @go-index="goIndex"/>
-	</div>
-	<div class="matching" v-else-if="matching">
-		<h1>{{ this.$t('matching.waiting-for').split('{}')[0] }}<b><mk-user-name :user="matching"/></b>{{ this.$t('matching.waiting-for').split('{}')[1] }}<mk-ellipsis/></h1>
-		<div class="cancel">
-			<form-button round @click="cancel">{{ $t('matching.cancel') }}</form-button>
-		</div>
-	</div>
-	<div v-else-if="gameId">
-		...
-	</div>
-	<div class="index" v-else>
-		<x-index @go="nav" @matching="onMatching"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../../i18n';
-import XGameroom from './reversi.gameroom.vue';
-import XIndex from './reversi.index.vue';
-import Progress from '../../../../scripts/loading';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/games/reversi/reversi.vue'),
-	components: {
-		XGameroom,
-		XIndex
-	},
-
-	props: {
-		gameId: {
-			type: String,
-			required: false
-		},
-		selfNav: {
-			type: Boolean,
-			require: false,
-			default: true
-		}
-	},
-
-	data() {
-		return {
-			game: null,
-			matching: null,
-			connection: null,
-			pingClock: null
-		};
-	},
-
-	watch: {
-		game() {
-			this.$emit('gamed', this.game);
-		},
-
-		gameId() {
-			this.fetch();
-		}
-	},
-
-	mounted() {
-		this.fetch();
-
-		if (this.$store.getters.isSignedIn) {
-			this.connection = this.$root.stream.useSharedConnection('gamesReversi');
-
-			this.connection.on('matched', this.onMatched);
-
-			this.pingClock = setInterval(() => {
-				if (this.matching) {
-					this.connection.send('ping', {
-						id: this.matching.id
-					});
-				}
-			}, 3000);
-		}
-	},
-
-	beforeDestroy() {
-		if (this.connection) {
-			this.connection.dispose();
-			clearInterval(this.pingClock);
-		}
-	},
-
-	methods: {
-		fetch() {
-			if (this.gameId == null) {
-				this.game = null;
-			} else {
-				Progress.start();
-				this.$root.api('games/reversi/games/show', {
-					gameId: this.gameId
-				}).then(game => {
-					this.game = game;
-					Progress.done();
-				});
-			}
-		},
-
-		async nav(game, actualNav = true) {
-			if (this.selfNav) {
-				// 受け取ったゲーム情報が省略されたものなら完全な情報を取得する
-				if (game != null && game.map == null) {
-					game = await this.$root.api('games/reversi/games/show', {
-						gameId: game.id
-					});
-				}
-
-				this.game = game;
-			} else {
-				this.$emit('nav', game, actualNav);
-			}
-		},
-
-		onMatching(user) {
-			this.matching = user;
-		},
-
-		cancel() {
-			this.matching = null;
-			this.$root.api('games/reversi/match/cancel');
-		},
-
-		accept(invitation) {
-			this.$root.api('games/reversi/match', {
-				userId: invitation.parent.id
-			}).then(game => {
-				if (game) {
-					this.matching = null;
-
-					this.nav(game);
-				}
-			});
-		},
-
-		onMatched(game) {
-			this.matching = null;
-			this.game = game;
-			this.nav(game, false);
-		},
-
-		goIndex() {
-			this.nav(null);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.vchtoekanapleubgzioubdtmlkribzfd
-	color var(--text)
-	background var(--bg)
-
-	> .matching
-		> h1
-			margin 0
-			padding 24px
-			font-size 20px
-			text-align center
-			font-weight normal
-
-		> .cancel
-			margin 0 auto
-			padding 24px 0 0 0
-			max-width 200px
-			text-align center
-			border-top dashed 1px #c4cdd4
-
-</style>
diff --git a/src/client/app/common/views/components/google.vue b/src/client/app/common/views/components/google.vue
deleted file mode 100644
index 1e881473995c238598a9a4adc37797d3af0b5181..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/google.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<template>
-<div class="mk-google">
-	<input type="search" v-model="query" :placeholder="q">
-	<button @click="search"><fa icon="search"/> {{ $t('@.search') }}</button>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n(),
-	props: ['q'],
-	data() {
-		return {
-			query: null
-		};
-	},
-	mounted() {
-		this.query = this.q;
-	},
-	methods: {
-		search() {
-			const engine = this.$store.state.settings.webSearchEngine ||
-				'https://www.google.com/?#q={{query}}';
-			const url = engine.replace('{{query}}', this.query)
-			window.open(url, '_blank');
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-google
-	display flex
-	margin 8px 0
-
-	> input
-		flex-shrink 1
-		padding 10px
-		width 100%
-		height 40px
-		font-size 16px
-		color var(--googleSearchFg)
-		background var(--googleSearchBg)
-		border solid 1px var(--googleSearchBorder)
-		border-radius 4px 0 0 4px
-
-		&:hover
-			border-color var(--googleSearchHoverBorder)
-
-	> button
-		flex-shrink 0
-		padding 0 16px
-		border solid 1px var(--googleSearchBorder)
-		border-left none
-		border-radius 0 4px 4px 0
-
-		&:hover
-			background-color var(--googleSearchHoverButton)
-
-		&:active
-			box-shadow 0 2px 4px rgba(#000, 0.15) inset
-
-</style>
diff --git a/src/client/app/common/views/components/image-viewer.vue b/src/client/app/common/views/components/image-viewer.vue
deleted file mode 100644
index 63b5e28d000822a3e95b3b7f7595003b9ebbb8cd..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/image-viewer.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<template>
-<ui-modal ref="modal" v-hotkey.global="keymap">
-	<img :src="image.url" :alt="image.name" :title="image.name" @click="close" />
-</ui-modal>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: ['image'],
-	computed: {
-		keymap(): any {
-			return {
-				'esc': this.close,
-			};
-		}
-	},
-	methods: {
-		close() {
-			(this.$refs.modal as any).close();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-img
-	position fixed
-	z-index 2
-	top 0
-	right 0
-	bottom 0
-	left 0
-	max-width 100%
-	max-height 100%
-	margin auto
-	cursor zoom-out
-	image-orientation from-image
-
-</style>
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
deleted file mode 100644
index 88cd4931d42359a42341d2252c07cd6bbbf23de2..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/index.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import Vue from 'vue';
-
-import dummy from './dummy.vue';
-import userName from './user-name.vue';
-import followButton from './follow-button.vue';
-import error from './error.vue';
-import noteSkeleton from './note-skeleton.vue';
-import instance from './instance.vue';
-import cwButton from './cw-button.vue';
-import tagCloud from './tag-cloud.vue';
-import trends from './trends.vue';
-import analogClock from './analog-clock.vue';
-import menu from './menu.vue';
-import noteHeader from './note-header.vue';
-import renote from './renote.vue';
-import signin from './signin.vue';
-import signup from './signup.vue';
-import forkit from './forkit.vue';
-import acct from './acct.vue';
-import avatar from './avatar.vue';
-import nav from './nav.vue';
-import misskeyFlavoredMarkdown from './misskey-flavored-markdown.vue';
-import poll from './poll.vue';
-import reactionIcon from './reaction-icon.vue';
-import reactionsViewer from './reactions-viewer.vue';
-import time from './time.vue';
-import mediaList from './media-list.vue';
-import uploader from './uploader.vue';
-import streamIndicator from './stream-indicator.vue';
-import ellipsis from './ellipsis.vue';
-import urlPreview from './url-preview.vue';
-import fileTypeIcon from './file-type-icon.vue';
-import emoji from './emoji.vue';
-import welcomeTimeline from './welcome-timeline.vue';
-import userList from './user-list.vue';
-import frac from './frac.vue';
-import uiInput from './ui/input.vue';
-import uiButton from './ui/button.vue';
-import uiHorizonGroup from './ui/horizon-group.vue';
-import uiCard from './ui/card.vue';
-import uiForm from './ui/form.vue';
-import uiTextarea from './ui/textarea.vue';
-import uiSwitch from './ui/switch.vue';
-import uiRadio from './ui/radio.vue';
-import uiSelect from './ui/select.vue';
-import uiInfo from './ui/info.vue';
-import uiMargin from './ui/margin.vue';
-import uiHr from './ui/hr.vue';
-import uiPagination from './ui/pagination.vue';
-import uiModal from './ui/modal.vue';
-import formButton from './ui/form/button.vue';
-import formRadio from './ui/form/radio.vue';
-
-Vue.component('mfm', misskeyFlavoredMarkdown);
-Vue.component('mk-dummy', dummy);
-Vue.component('mk-user-name', userName);
-Vue.component('mk-follow-button', followButton);
-Vue.component('mk-error', error);
-Vue.component('mk-note-skeleton', noteSkeleton);
-Vue.component('mk-instance', instance);
-Vue.component('mk-cw-button', cwButton);
-Vue.component('mk-tag-cloud', tagCloud);
-Vue.component('mk-trends', trends);
-Vue.component('mk-analog-clock', analogClock);
-Vue.component('mk-menu', menu);
-Vue.component('mk-note-header', noteHeader);
-Vue.component('mk-renote', renote);
-Vue.component('mk-signin', signin);
-Vue.component('mk-signup', signup);
-Vue.component('mk-forkit', forkit);
-Vue.component('mk-acct', acct);
-Vue.component('mk-avatar', avatar);
-Vue.component('mk-nav', nav);
-Vue.component('mk-poll', poll);
-Vue.component('mk-reaction-icon', reactionIcon);
-Vue.component('mk-reactions-viewer', reactionsViewer);
-Vue.component('mk-time', time);
-Vue.component('mk-media-list', mediaList);
-Vue.component('mk-uploader', uploader);
-Vue.component('mk-stream-indicator', streamIndicator);
-Vue.component('mk-ellipsis', ellipsis);
-Vue.component('mk-url-preview', urlPreview);
-Vue.component('mk-file-type-icon', fileTypeIcon);
-Vue.component('mk-emoji', emoji);
-Vue.component('mk-welcome-timeline', welcomeTimeline);
-Vue.component('mk-user-list', userList);
-Vue.component('mk-frac', frac);
-Vue.component('ui-input', uiInput);
-Vue.component('ui-button', uiButton);
-Vue.component('ui-horizon-group', uiHorizonGroup);
-Vue.component('ui-card', uiCard);
-Vue.component('ui-form', uiForm);
-Vue.component('ui-textarea', uiTextarea);
-Vue.component('ui-switch', uiSwitch);
-Vue.component('ui-radio', uiRadio);
-Vue.component('ui-select', uiSelect);
-Vue.component('ui-info', uiInfo);
-Vue.component('ui-margin', uiMargin);
-Vue.component('ui-hr', uiHr);
-Vue.component('ui-pagination', uiPagination);
-Vue.component('ui-modal', uiModal);
-Vue.component('form-button', formButton);
-Vue.component('form-radio', formRadio);
diff --git a/src/client/app/common/views/components/instance.vue b/src/client/app/common/views/components/instance.vue
deleted file mode 100644
index 497e4976f59b80f48aa25487f1530fd57ad13cd1..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/instance.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<template>
-<div class="nhasjydimbopojusarffqjyktglcuxjy" v-if="meta">
-	<div class="banner" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"></div>
-
-	<h1>{{ meta.name || 'Misskey' }}</h1>
-	<p v-html="meta.description || this.$t('@.about')"></p>
-	<router-link to="/">{{ $t('start') }}</router-link>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/instance.vue'),
-	data() {
-		return {
-			meta: null
-		}
-	},
-	created() {
-		this.$root.getMeta().then(meta => {
-			this.meta = meta;
-		});
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.nhasjydimbopojusarffqjyktglcuxjy
-	color var(--text)
-	background var(--face)
-	text-align center
-
-	> .banner
-		height 100px
-		background-position center
-		background-size cover
-
-	> h1
-		margin 16px
-		font-size 16px
-
-	> p
-		margin 16px
-		font-size 14px
-
-	> a
-		display block
-		padding-bottom 16px
-
-</style>
diff --git a/src/client/app/common/views/components/integrations.integration.vue b/src/client/app/common/views/components/integrations.integration.vue
deleted file mode 100644
index 51995843b1f8003b88a91553e3a37599defb0510..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/integrations.integration.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<template>
-<a class="zxrjzpcj" :href="url" :class="service" rel="noopener" target="_blank">
-	<fa :icon="icon" size="lg" fixed-width /><span>{{ text }}</span>
-</a>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: ['url', 'text', 'icon', 'service']
-});
-</script>
-
-<style lang="stylus" scoped>
-.zxrjzpcj
-	display inline-block
-	padding 6px 8px 6px 6px
-	margin-top 4px
-	margin-bottom 4px
-	border-radius 32px
-	white-space nowrap
-
-	&:hover
-		text-decoration none
-
-	&.twitter
-		color #fff
-		background #1da1f3
-
-		&:hover
-			background #0c87cf
-
-	&.github
-		color #fff
-		background #171515
-
-		&:hover
-			background #000
-
-	&.discord
-		color #fff
-		background #7289da
-
-		&:hover
-			background #4968ce
-
-</style>
diff --git a/src/client/app/common/views/components/integrations.vue b/src/client/app/common/views/components/integrations.vue
deleted file mode 100644
index 7a341a14fdada878737a13a7f44592c4913eaf03..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/integrations.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<template>
-<div class="nbogcrmo" :v-if="user.twitter || user.github || user.discord">
-	<x-integration v-if="user.twitter" service="twitter" :url="`https://twitter.com/${user.twitter.screenName}`" :text="user.twitter.screenName" :icon="['fab', 'twitter']"/>
-	<x-integration v-if="user.github" service="github" :url="`https://github.com/${user.github.login}`" :text="user.github.login" :icon="['fab', 'github']"/>
-	<x-integration v-if="user.discord" service="discord" :url="`https://discordapp.com/users/${user.discord.id}`" :text="`${user.discord.username}#${user.discord.discriminator}`" :icon="['fab', 'discord']"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import XIntegration from './integrations.integration.vue';
-
-export default Vue.extend({
-	components: {
-		XIntegration
-	},
-	props: ['user']
-});
-</script>
-
-<style lang="stylus" scoped>
-.nbogcrmo
-	> *
-		margin-right 10px
-
-</style>
diff --git a/src/client/app/common/views/components/media-image.vue b/src/client/app/common/views/components/media-image.vue
deleted file mode 100644
index b8b164aed0eff3fc309bb0f76fa1199e16497ee3..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/media-image.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-<template>
-<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false">
-	<div>
-		<b><fa icon="exclamation-triangle"/> {{ $t('sensitive') }}</b>
-		<span>{{ $t('click-to-show') }}</span>
-	</div>
-</div>
-<a class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else
-	:href="image.url"
-	:style="style"
-	:title="image.name"
-	@click.prevent="onClick"
->
-	<div v-if="image.type === 'image/gif'">GIF</div>
-</a>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import ImageViewer from './image-viewer.vue';
-import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/media-image.vue'),
-	props: {
-		image: {
-			type: Object,
-			required: true
-		},
-		raw: {
-			default: false
-		}
-	},
-	data() {
-		return {
-			hide: true
-		};
-	},
-	computed: {
-		style(): any {
-			let url = `url(${
-				this.$store.state.device.disableShowingAnimatedImages
-					? getStaticImageUrl(this.image.thumbnailUrl)
-					: this.image.thumbnailUrl
-			})`;
-
-			if (this.$store.state.device.loadRemoteMedia || this.$store.state.device.lightmode) {
-				url = null;
-			} else if (this.raw || this.$store.state.device.loadRawImages) {
-				url = `url(${this.image.url})`;
-			}
-
-			return {
-				'background-color': this.image.properties.avgColor || 'transparent',
-				'background-image': url
-			};
-		}
-	},
-	methods: {
-		onClick() {
-			const viewer = this.$root.new(ImageViewer, {
-				image: this.image
-			});
-			this.$once('hook:beforeDestroy', () => {
-				viewer.close();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.gqnyydlzavusgskkfvwvjiattxdzsqlf
-	display block
-	cursor zoom-in
-	overflow hidden
-	width 100%
-	height 100%
-	background-position center
-	background-size contain
-	background-repeat no-repeat
-
-	> div
-		background-color var(--text)
-		border-radius 6px
-		color var(--secondary)
-		display inline-block
-		font-size 14px
-		font-weight bold
-		left 12px
-		opacity .5
-		padding 0 6px
-		text-align center
-		top 12px
-		pointer-events none
-
-.qjewsnkgzzxlxtzncydssfbgjibiehcy
-	display flex
-	justify-content center
-	align-items center
-	background #111
-	color #fff
-
-	> div
-		display table-cell
-		text-align center
-		font-size 12px
-
-		> *
-			display block
-
-</style>
diff --git a/src/client/app/common/views/components/media-list.vue b/src/client/app/common/views/components/media-list.vue
deleted file mode 100644
index bfbc9366d3475c87a71427c6b2761fca2cfc661a..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/media-list.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-<template>
-<div class="mk-media-list">
-	<template v-for="media in mediaList.filter(media => !previewable(media))">
-		<x-banner :media="media" :key="media.id"/>
-	</template>
-	<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container">
-		<div :data-count="mediaList.filter(media => previewable(media)).length" ref="grid">
-			<template v-for="media in mediaList">
-				<mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/>
-				<x-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/>
-			</template>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import XBanner from './media-banner.vue';
-import XImage from './media-image.vue';
-
-export default Vue.extend({
-	components: {
-		XBanner,
-		XImage
-	},
-	props: {
-		mediaList: {
-			required: true
-		},
-		raw: {
-			default: false
-		}
-	},
-	mounted() {
-		//#region for Safari bug
-		if (this.$refs.grid) {
-			this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px`
-				: this.$store.state.device.inDeckMode ? '128px' : this.$root.isMobile ? '173px' : '287px';
-		}
-		//#endregion
-	},
-	methods: {
-		previewable(file) {
-			return (file.type.startsWith('video') || file.type.startsWith('image')) && file.thumbnailUrl;
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-media-list
-	> .gird-container
-		width 100%
-		margin-top 4px
-
-		&:before
-			content ''
-			display block
-			padding-top 56.25% // 16:9
-
-		> div
-			position absolute
-			top 0
-			right 0
-			bottom 0
-			left 0
-			display grid
-			grid-gap 4px
-
-			> *
-				overflow hidden
-				border-radius 4px
-
-			&[data-count="1"]
-				grid-template-rows 1fr
-
-			&[data-count="2"]
-				grid-template-columns 1fr 1fr
-				grid-template-rows 1fr
-
-			&[data-count="3"]
-				grid-template-columns 1fr 0.5fr
-				grid-template-rows 1fr 1fr
-
-				> *:nth-child(1)
-					grid-row 1 / 3
-
-				> *:nth-child(3)
-					grid-column 2 / 3
-					grid-row 2 / 3
-
-			&[data-count="4"]
-				grid-template-columns 1fr 1fr
-				grid-template-rows 1fr 1fr
-
-			> *:nth-child(1)
-				grid-column 1 / 2
-				grid-row 1 / 2
-
-			> *:nth-child(2)
-				grid-column 2 / 3
-				grid-row 1 / 2
-
-			> *:nth-child(3)
-				grid-column 1 / 2
-				grid-row 2 / 3
-
-			> *:nth-child(4)
-				grid-column 2 / 3
-				grid-row 2 / 3
-
-</style>
diff --git a/src/client/app/common/views/components/menu.vue b/src/client/app/common/views/components/menu.vue
deleted file mode 100644
index 68fa0f5e62cff8bdef6bce2b92162a3a0730b063..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/menu.vue
+++ /dev/null
@@ -1,196 +0,0 @@
-<template>
-<div class="onchrpzrvnoruiaenfcqvccjfuupzzwv" :class="{ isMobile: $root.isMobile }">
-	<div class="backdrop" ref="backdrop" @click="close"></div>
-	<div class="popover" :class="{ bubble }" ref="popover">
-		<template v-for="item, i in items">
-			<div v-if="item === null"></div>
-			<button v-if="item" @click="clicked(item.action)" :tabindex="i">
-				<fa v-if="item.icon" :icon="item.icon"/>{{ item.text }}
-			</button>
-		</template>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import anime from 'animejs';
-
-export default Vue.extend({
-	props: {
-		source: {
-			required: true
-		},
-		items: {
-			type: Array,
-			required: true
-		}
-	},
-	data() {
-		return {
-			bubble: !this.$root.isMobile
-		};
-	},
-	mounted() {
-		this.$nextTick(() => {
-			const popover = this.$refs.popover as any;
-
-			const rect = this.source.getBoundingClientRect();
-			const width = popover.offsetWidth;
-			const height = popover.offsetHeight;
-
-			let left;
-			let top;
-
-			if (this.$root.isMobile) {
-				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-				const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
-				left = (x - (width / 2));
-				top = (y - (height / 2));
-			} else {
-				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-				const y = rect.top + window.pageYOffset + this.source.offsetHeight;
-				left = (x - (width / 2));
-				top = y;
-			}
-
-			if (left + width - window.pageXOffset > window.innerWidth) {
-				left = window.innerWidth - width + window.pageXOffset;
-				this.bubble = false;
-			}
-
-			if (top + height - window.pageYOffset > window.innerHeight) {
-				top = window.innerHeight - height + window.pageYOffset;
-				this.bubble = false;
-			}
-
-			if (top < 0) {
-				top = 0;
-			}
-
-			popover.style.left = left + 'px';
-			popover.style.top = top + 'px';
-
-			anime({
-				targets: this.$refs.backdrop,
-				opacity: 1,
-				duration: 100,
-				easing: 'linear'
-			});
-
-			anime({
-				targets: this.$refs.popover,
-				opacity: 1,
-				scale: [0.5, 1],
-				duration: 500
-			});
-		});
-	},
-	methods: {
-		clicked(fn) {
-			fn();
-			this.close();
-		},
-		close() {
-			(this.$refs.backdrop as any).style.pointerEvents = 'none';
-			anime({
-				targets: this.$refs.backdrop,
-				opacity: 0,
-				duration: 200,
-				easing: 'linear'
-			});
-
-			(this.$refs.popover as any).style.pointerEvents = 'none';
-			anime({
-				targets: this.$refs.popover,
-				opacity: 0,
-				scale: 0.5,
-				duration: 200,
-				easing: 'easeInBack',
-				complete: () => {
-					this.$emit('closed');
-					this.destroyDom();
-				}
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.onchrpzrvnoruiaenfcqvccjfuupzzwv
-	$bg-color = var(--popupBg)
-
-	position initial
-
-	&.isMobile
-		> .popover
-			> button
-				font-size 15px
-
-	> .backdrop
-		position fixed
-		top 0
-		left 0
-		z-index 10000
-		width 100%
-		height 100%
-		background var(--modalBackdrop)
-		opacity 0
-
-	> .popover
-		position absolute
-		z-index 10001
-		padding 8px 0
-		background $bg-color
-		border-radius 4px
-		box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
-		transform scale(0.5)
-		opacity 0
-
-		$balloon-size = 16px
-
-		&.bubble
-			margin-top $balloon-size
-			transform-origin center -($balloon-size)
-
-			&:before
-			&:after
-				content ""
-				display block
-				position absolute
-				pointer-events none
-
-			&:before
-				top -($balloon-size * 2)
-				left s('calc(50% - %s)', $balloon-size)
-				border-top solid $balloon-size transparent
-				border-left solid $balloon-size transparent
-				border-right solid $balloon-size transparent
-				border-bottom solid $balloon-size $bg-color
-
-		> button
-			display block
-			padding 8px 16px
-			width 100%
-			color var(--popupFg)
-			white-space nowrap
-
-			&:hover
-				color var(--primaryForeground)
-				background var(--primary)
-				text-decoration none
-
-			&:active
-				color var(--primaryForeground)
-				background var(--primaryDarken10)
-
-			> [data-icon]
-				margin-right 4px
-
-		> div
-			margin 8px 0
-			height var(--lineWidth)
-			background var(--faceDivider)
-
-</style>
diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
deleted file mode 100644
index 1ab6359415e19ea3bef1056c8a42f8e72702a928..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ /dev/null
@@ -1,279 +0,0 @@
-<template>
-<div class="message" :data-is-me="isMe">
-	<mk-avatar class="avatar" :user="message.user" target="_blank"/>
-	<div class="content">
-		<div class="balloon" :data-no-text="message.text == null">
-			<button class="delete-button" v-if="isMe" :title="$t('@.delete')" @click="del">
-				<img src="/assets/desktop/remove.png" alt="Delete"/>
-			</button>
-			<div class="content" v-if="!message.isDeleted">
-				<mfm 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" rel="noopener" target="_blank" :title="message.file.name">
-						<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"
-							:style="{ backgroundColor: message.file.properties.avgColor || 'transparent' }"/>
-						<p v-else>{{ message.file.name }}</p>
-					</a>
-				</div>
-			</div>
-			<div class="content" v-else>
-				<p class="is-deleted">{{ $t('deleted') }}</p>
-			</div>
-		</div>
-		<div></div>
-		<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
-		<footer>
-			<template v-if="isGroup">
-				<span class="read" v-if="message.reads.length > 0">{{ $t('is-read') }} {{ message.reads.length }}</span>
-			</template>
-			<template v-else>
-				<span class="read" v-if="isMe && message.isRead">{{ $t('is-read') }}</span>
-			</template>
-			<mk-time :time="message.createdAt"/>
-			<template v-if="message.is_edited"><fa icon="pencil-alt"/></template>
-		</footer>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { parse } from '../../../../../mfm/parse';
-import { unique } from '../../../../../prelude/array';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/messaging-room.message.vue'),
-	props: {
-		message: {
-			required: true
-		},
-		isGroup: {
-			required: false
-		}
-	},
-	computed: {
-		isMe(): boolean {
-			return this.message.userId == this.$store.state.i.id;
-		},
-		urls(): string[] {
-			if (this.message.text) {
-				const ast = parse(this.message.text);
-				return unique(ast
-					.filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent))
-					.map(t => t.node.props.url));
-			} else {
-				return null;
-			}
-		}
-	},
-	methods: {
-		del() {
-			this.$root.api('messaging/messages/delete', {
-				messageId: this.message.id
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.message
-	$me-balloon-color = var(--primary)
-
-	padding 10px 12px 10px 12px
-	background-color transparent
-
-	> .avatar
-		display block
-		position absolute
-		top 10px
-		width 54px
-		height 54px
-		border-radius 8px
-		transition all 0.1s ease
-
-	> .content
-
-		> .balloon
-			display flex
-			align-items center
-			padding 0
-			max-width calc(100% - 16px)
-			min-height 38px
-			border-radius 16px
-
-			&:before
-				content ""
-				pointer-events none
-				display block
-				position absolute
-				top 12px
-
-			& + *
-				clear both
-
-			&:hover
-				> .delete-button
-					display block
-
-			> .delete-button
-				display none
-				position absolute
-				z-index 1
-				top -4px
-				right -4px
-				margin 0
-				padding 0
-				cursor pointer
-				outline none
-				border none
-				border-radius 0
-				box-shadow none
-				background transparent
-
-				> img
-					vertical-align bottom
-					width 16px
-					height 16px
-					cursor pointer
-
-			> .content
-				max-width 100%
-
-				> .is-deleted
-					display block
-					margin 0
-					padding 0
-					overflow hidden
-					overflow-wrap break-word
-					font-size 1em
-					color rgba(#000, 0.5)
-
-				> .text
-					display block
-					margin 0
-					padding 8px 16px
-					overflow hidden
-					overflow-wrap break-word
-					word-break break-word
-					font-size 1em
-					color rgba(#000, 0.8)
-
-					& + .file
-						> a
-							border-radius 0 0 16px 16px
-
-				> .file
-					> a
-						display block
-						max-width 100%
-						border-radius 16px
-						overflow hidden
-						text-decoration none
-
-						&:hover
-							text-decoration none
-
-							> p
-								background #ccc
-
-						> *
-							display block
-							margin 0
-							width 100%
-							max-height 512px
-							object-fit contain
-
-						> p
-							padding 30px
-							text-align center
-							color #555
-							background #ddd
-
-		> .mk-url-preview
-			margin 8px 0
-
-		> footer
-			display block
-			margin 2px 0 0 0
-			font-size 10px
-			color var(--messagingRoomMessageInfo)
-
-			> .read
-				margin 0 8px
-
-			> [data-icon]
-				margin-left 4px
-
-	&:not([data-is-me])
-		> .avatar
-			left 12px
-
-		> .content
-			padding-left 66px
-
-			> .balloon
-				$color = var(--messagingRoomMessageBg)
-				float left
-				background $color
-
-				&[data-no-text]
-					background transparent
-
-				&:not([data-no-text]):before
-					left -14px
-					border-top solid 8px transparent
-					border-right solid 8px $color
-					border-bottom solid 8px transparent
-					border-left solid 8px transparent
-
-				> .content
-					> .text
-							color var(--messagingRoomMessageFg)
-
-			> footer
-				text-align left
-
-	&[data-is-me]
-		> .avatar
-			right 12px
-
-		> .content
-			padding-right 66px
-
-			> .balloon
-				float right
-				background $me-balloon-color
-
-				&[data-no-text]
-					background transparent
-
-				&:not([data-no-text]):before
-					right -14px
-					left auto
-					border-top solid 8px transparent
-					border-right solid 8px transparent
-					border-bottom solid 8px transparent
-					border-left solid 8px $me-balloon-color
-
-				> .content
-
-					> p.is-deleted
-						color rgba(#fff, 0.5)
-
-					> .text >>>
-						&, *
-							color #fff !important
-
-			> footer
-				text-align right
-
-				> .read
-					user-select none
-
-	&[data-is-deleted]
-		> .balloon
-			opacity 0.5
-
-</style>
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
deleted file mode 100644
index 52f55e4333f9a9c3c943118f7cc23437fc42137e..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/messaging.vue
+++ /dev/null
@@ -1,500 +0,0 @@
-<template>
-<div class="mk-messaging" :data-compact="compact">
-	<div class="search" v-if="!compact" :style="{ top: headerTop + 'px' }">
-		<div class="form">
-			<label for="search-input"><i><fa icon="search"/></i></label>
-			<input v-model="q" type="search" @input="search" @keydown="onSearchKeydown" :placeholder="$t('search-user')"/>
-		</div>
-		<div class="result">
-			<ol class="users" v-if="result.length > 0" ref="searchResult">
-				<li v-for="(user, i) in result"
-					@keydown.enter="navigate(user)"
-					@keydown="onSearchResultKeydown(i)"
-					@click="navigate(user)"
-					tabindex="-1"
-				>
-					<mk-avatar class="avatar" :user="user" :key="user.id"/>
-					<span class="name"><mk-user-name :user="user" :key="user.id"/></span>
-					<span class="username">@{{ user | acct }}</span>
-				</li>
-			</ol>
-		</div>
-	</div>
-	<div class="history" v-if="messages.length > 0">
-		<a v-for="message in messages"
-			class="user"
-			:href="message.groupId ? `/i/messaging/group/${message.groupId}` : `/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
-			:data-is-me="isMe(message)"
-			:data-is-read="message.groupId ? message.reads.includes($store.state.i.id) : message.isRead"
-			@click.prevent="message.groupId ? navigateGroup(message.group) : navigate(isMe(message) ? message.recipient : message.user)"
-			:key="message.id"
-		>
-			<div>
-				<mk-avatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/>
-				<header v-if="message.groupId">
-					<span class="name">{{ message.group.name }}</span>
-					<mk-time :time="message.createdAt"/>
-				</header>
-				<header v-else>
-					<span class="name"><mk-user-name :user="isMe(message) ? message.recipient : message.user"/></span>
-					<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span>
-					<mk-time :time="message.createdAt"/>
-				</header>
-				<div class="body">
-					<p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p>
-				</div>
-			</div>
-		</a>
-	</div>
-	<p class="no-history" v-if="!fetching && messages.length == 0">{{ $t('no-history') }}</p>
-	<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
-	<ui-margin>
-		<ui-button @click="startUser()"><fa :icon="faUser"/> {{ $t('start-with-user') }}</ui-button>
-		<ui-button @click="startGroup()"><fa :icon="faUsers"/> {{ $t('start-with-group') }}</ui-button>
-	</ui-margin>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { faUser, faUsers } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../i18n';
-import getAcct from '../../../../../misc/acct/render';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/messaging.vue'),
-	props: {
-		compact: {
-			type: Boolean,
-			default: false
-		},
-		headerTop: {
-			type: Number,
-			default: 0
-		}
-	},
-	data() {
-		return {
-			fetching: true,
-			moreFetching: false,
-			messages: [],
-			q: null,
-			result: [],
-			connection: null,
-			faUser, faUsers
-		};
-	},
-	mounted() {
-		this.connection = this.$root.stream.useSharedConnection('messagingIndex');
-
-		this.connection.on('message', this.onMessage);
-		this.connection.on('read', this.onRead);
-
-		this.$root.api('messaging/history', { group: false }).then(userMessages => {
-			this.$root.api('messaging/history', { group: true }).then(groupMessages => {
-				const messages = userMessages.concat(groupMessages);
-				messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
-				this.messages = messages;
-				this.fetching = false;
-			});
-		});
-	},
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-	methods: {
-		getAcct,
-		isMe(message) {
-			return message.userId == this.$store.state.i.id;
-		},
-		onMessage(message) {
-			if (message.recipientId) {
-				this.messages = this.messages.filter(m => !(
-					(m.recipientId == message.recipientId && m.userId == message.userId) ||
-					(m.recipientId == message.userId && m.userId == message.recipientId)));
-
-				this.messages.unshift(message);
-			} else if (message.groupId) {
-				this.messages = this.messages.filter(m => m.groupId !== message.groupId);
-				this.messages.unshift(message);
-			}
-		},
-		onRead(ids) {
-			for (const id of ids) {
-				const found = this.messages.find(m => m.id == id);
-				if (found) {
-					if (found.recipientId) {
-						found.isRead = true;
-					} else if (found.groupId) {
-						found.reads.push(this.$store.state.i.id);
-					}
-				}
-			}
-		},
-		search() {
-			if (this.q == '') {
-				this.result = [];
-				return;
-			}
-			this.$root.api('users/search', {
-				query: this.q,
-				localOnly: false,
-				limit: 10,
-				detail: false
-			}).then(users => {
-				this.result = users.filter(user => user.id != this.$store.state.i.id);
-			});
-		},
-		navigate(user) {
-			this.$emit('navigate', user);
-		},
-		navigateGroup(group) {
-			this.$emit('navigateGroup', group);
-		},
-		onSearchKeydown(e) {
-			switch (e.which) {
-				case 9: // [TAB]
-				case 40: // [↓]
-					e.preventDefault();
-					e.stopPropagation();
-					(this.$refs.searchResult as any).childNodes[0].focus();
-					break;
-			}
-		},
-		onSearchResultKeydown(i, e) {
-			const list = this.$refs.searchResult as any;
-
-			const cancel = () => {
-				e.preventDefault();
-				e.stopPropagation();
-			};
-
-			switch (true) {
-				case e.which == 27: // [ESC]
-					cancel();
-					(this.$refs.search as any).focus();
-					break;
-
-				case e.which == 9 && e.shiftKey: // [TAB] + [Shift]
-				case e.which == 38: // [↑]
-					cancel();
-					(list.childNodes[i].previousElementSibling || list.childNodes[this.result.length - 1]).focus();
-					break;
-
-				case e.which == 9: // [TAB]
-				case e.which == 40: // [↓]
-					cancel();
-					(list.childNodes[i].nextElementSibling || list.childNodes[0]).focus();
-					break;
-			}
-		},
-		async startUser() {
-			const { result: user } = await this.$root.dialog({
-				user: {
-					local: true
-				}
-			});
-			if (user == null) return;
-			this.navigate(user);
-		},
-		async startGroup() {
-			const groups1 = await this.$root.api('users/groups/owned');
-			const groups2 = await this.$root.api('users/groups/joined');
-			const { canceled, result: group } = await this.$root.dialog({
-				type: null,
-				title: this.$t('select-group'),
-				select: {
-					items: groups1.concat(groups2).map(group => ({
-						value: group, text: group.name
-					}))
-				},
-				showCancelButton: true
-			});
-			if (canceled) return;
-			this.navigateGroup(group);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-messaging
-
-	&[data-compact]
-		font-size 0.8em
-
-		> .history
-			> a
-				&:last-child
-					border-bottom none
-
-				&:not([data-is-me]):not([data-is-read])
-					> div
-						background-image none
-						border-left solid 4px #3aa2dc
-
-				> div
-					padding 16px
-
-					> header
-						> .mk-time
-							font-size 1em
-
-					> .avatar
-						width 42px
-						height 42px
-						margin 0 12px 0 0
-
-	> .search
-		display block
-		position -webkit-sticky
-		position sticky
-		top 0
-		left 0
-		z-index 1
-		width 100%
-		box-shadow 0 0 2px rgba(#000, 0.2)
-
-		> .form
-			background rgba(0, 0, 0, 0.02)
-
-			> label
-				display block
-				position absolute
-				top 0
-				left 8px
-				z-index 1
-				height 100%
-				width 38px
-				pointer-events none
-
-				> i
-					display block
-					position absolute
-					top 0
-					right 0
-					bottom 0
-					left 0
-					width 1em
-					line-height 48px
-					margin auto
-					color #555
-
-			> input
-				margin 0
-				padding 0 0 0 42px
-				width 100%
-				font-size 1em
-				line-height 48px
-				color var(--faceText)
-				outline none
-				background transparent
-				border none
-				border-radius 5px
-				box-shadow none
-
-		> .result
-			display block
-			top 0
-			left 0
-			z-index 2
-			width 100%
-			margin 0
-			padding 0
-			background #fff
-
-			> .users
-				margin 0
-				padding 0
-				list-style none
-
-				> li
-					display inline-block
-					z-index 1
-					width 100%
-					padding 8px 32px
-					vertical-align top
-					white-space nowrap
-					overflow hidden
-					color rgba(#000, 0.8)
-					text-decoration none
-					transition none
-					cursor pointer
-
-					&:hover
-					&:focus
-						color #fff
-						background var(--primary)
-
-						.name
-							color #fff
-
-						.username
-							color #fff
-
-					&:active
-						color #fff
-						background var(--primaryDarken10)
-
-						.name
-							color #fff
-
-						.username
-							color #fff
-
-					.avatar
-						vertical-align middle
-						min-width 32px
-						min-height 32px
-						max-width 32px
-						max-height 32px
-						margin 0 8px 0 0
-						border-radius 6px
-
-					.name
-						margin 0 8px 0 0
-						/*font-weight bold*/
-						font-weight normal
-						color rgba(#000, 0.8)
-
-					.username
-						font-weight normal
-						color rgba(#000, 0.3)
-
-	> .history
-		> a
-			display block
-			text-decoration none
-			background var(--face)
-			border-bottom solid 1px var(--faceDivider)
-
-			*
-				pointer-events none
-				user-select none
-
-			&:hover
-				box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
-
-				.avatar
-					filter saturate(200%)
-
-			&:active
-				box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
-
-			&[data-is-read]
-			&[data-is-me]
-				opacity 0.8
-
-			&:not([data-is-me]):not([data-is-read])
-				> div
-					background-image url("/assets/unread.svg")
-					background-repeat no-repeat
-					background-position 0 center
-
-			&:after
-				content ""
-				display block
-				clear both
-
-			> div
-				max-width 500px
-				margin 0 auto
-				padding 20px 30px
-
-				&:after
-					content ""
-					display block
-					clear both
-
-				> header
-					display flex
-					align-items center
-					margin-bottom 2px
-					white-space nowrap
-					overflow hidden
-
-					> .name
-						margin 0
-						padding 0
-						overflow hidden
-						text-overflow ellipsis
-						font-size 1em
-						color var(--noteHeaderName)
-						font-weight bold
-						transition all 0.1s ease
-
-					> .username
-						margin 0 8px
-						color var(--noteHeaderAcct)
-
-					> .mk-time
-						margin 0 0 0 auto
-						color var(--noteHeaderInfo)
-						font-size 80%
-
-				> .avatar
-					float left
-					width 54px
-					height 54px
-					margin 0 16px 0 0
-					border-radius 8px
-					transition all 0.1s ease
-
-				> .body
-
-					> .text
-						display block
-						margin 0 0 0 0
-						padding 0
-						overflow hidden
-						overflow-wrap break-word
-						font-size 1.1em
-						color var(--faceText)
-
-						.me
-							opacity 0.7
-
-					> .image
-						display block
-						max-width 100%
-						max-height 512px
-
-	> .no-history
-		margin 0
-		padding 2em 1em
-		text-align center
-		color #999
-		font-weight 500
-
-	> .fetching
-		margin 0
-		padding 16px
-		text-align center
-		color var(--text)
-
-		> [data-icon]
-			margin-right 4px
-
-	// TODO: element base media query
-	@media (max-width 400px)
-		> .search
-			> .result
-				> .users
-					> li
-						padding 8px 16px
-
-		> .history
-			> a
-				&:not([data-is-me]):not([data-is-read])
-					> div
-						background-image none
-						border-left solid 4px #3aa2dc
-
-				> div
-					padding 16px
-					font-size 14px
-
-					> .avatar
-						margin 0 12px 0 0
-
-</style>
diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.vue b/src/client/app/common/views/components/misskey-flavored-markdown.vue
deleted file mode 100644
index 40c444242c49890031d4a3b4e715763aa4535ec8..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/misskey-flavored-markdown.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<template>
-<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }" v-once/>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import MfmCore from './mfm';
-
-export default Vue.extend({
-	components: {
-		MfmCore
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.havbbuyv
-	white-space pre-wrap
-
-	&.nowrap
-		white-space pre
-		word-wrap normal // https://codeday.me/jp/qa/20190424/690106.html
-
-	>>> .title
-		display block
-		margin-bottom 4px
-		padding 4px
-		font-size 90%
-		text-align center
-		background var(--mfmTitleBg)
-		border-radius 4px
-
-	>>> .quote
-		display block
-		margin 8px
-		padding 6px 0 6px 12px
-		color var(--mfmQuote)
-		border-left solid 3px var(--mfmQuoteLine)
-
-	>>> pre code
-		font-size 80%
-
-</style>
diff --git a/src/client/app/common/views/components/nav.vue b/src/client/app/common/views/components/nav.vue
deleted file mode 100644
index 41b65604dec21ca52044d5ca5a6c7ae2cddcf483..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/nav.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<template>
-<span class="mk-nav">
-	<a :href="aboutUrl">{{ $t('about') }}</a>
-	<template v-if="ToSUrl !== null">
-		<i>・</i>
-		<a :href="ToSUrl" target="_blank">{{ $t('tos') }}</a>
-	</template>
-	<i>・</i>
-	<a :href="repositoryUrl" rel="noopener" target="_blank">{{ $t('repository') }}</a>
-	<i>・</i>
-	<a :href="feedbackUrl" rel="noopener" target="_blank">{{ $t('feedback') }}</a>
-	<i>・</i>
-	<a href="/dev" target="_blank">{{ $t('develop') }}</a>
-</span>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { lang } from '../../../config';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/nav.vue'),
-	data() {
-		return {
-			aboutUrl: `/docs/${lang}/about`,
-			repositoryUrl: 'https://github.com/syuilo/misskey',
-			feedbackUrl: 'https://github.com/syuilo/misskey/issues/new',
-			ToSUrl: null
-		}
-	},
-
-	mounted() {
-		this.$root.getMeta(true).then(meta => {
-			this.repositoryUrl = meta.repositoryUrl;
-			this.feedbackUrl = meta.feedbackUrl;
-			this.ToSUrl = meta.ToSUrl;
-		})
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-nav
-	a
-		color inherit
-</style>
diff --git a/src/client/app/common/views/components/note-header.vue b/src/client/app/common/views/components/note-header.vue
deleted file mode 100644
index a72863e1dda16ed5b6a7ee5ffc9fd296e5043a00..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/note-header.vue
+++ /dev/null
@@ -1,118 +0,0 @@
-<template>
-<header class="bvonvjxbwzaiskogyhbwgyxvcgserpmu">
-	<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/>
-	<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">
-		<mk-user-name :user="note.user"/>
-	</router-link>
-	<span class="is-admin" v-if="note.user.isAdmin">admin</span>
-	<span class="is-bot" v-if="note.user.isBot">bot</span>
-	<span class="is-cat" v-if="note.user.isCat">cat</span>
-	<span class="username"><mk-acct :user="note.user"/></span>
-	<div class="info">
-		<span class="app" v-if="note.app && !mini && $store.state.settings.showVia">via <b>{{ note.app.name }}</b></span>
-		<span class="mobile" v-if="note.viaMobile"><fa icon="mobile-alt"/></span>
-		<router-link class="created-at" :to="note | notePage">
-			<mk-time :time="note.createdAt"/>
-		</router-link>
-		<span class="visibility" v-if="note.visibility != 'public'">
-			<fa v-if="note.visibility == 'home'" icon="home"/>
-			<fa v-if="note.visibility == 'followers'" icon="unlock"/>
-			<fa v-if="note.visibility == 'specified'" icon="envelope"/>
-		</span>
-		<span class="localOnly" v-if="note.localOnly == true"><fa icon="heart"/></span>
-	</div>
-</header>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n(),
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		mini: {
-			type: Boolean,
-			required: false,
-			default: false
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.bvonvjxbwzaiskogyhbwgyxvcgserpmu
-	display flex
-	align-items baseline
-	white-space nowrap
-
-	> .avatar
-		flex-shrink 0
-		margin-right 8px
-		width 20px
-		height 20px
-		border-radius 100%
-
-	> .name
-		display block
-		margin 0 .5em 0 0
-		padding 0
-		overflow hidden
-		color var(--noteHeaderName)
-		font-size 1em
-		font-weight bold
-		text-decoration none
-		text-overflow ellipsis
-
-		&:hover
-			text-decoration underline
-
-	> .is-admin
-	> .is-bot
-	> .is-cat
-		flex-shrink 0
-		align-self center
-		margin 0 .5em 0 0
-		padding 1px 6px
-		font-size 80%
-		color var(--noteHeaderBadgeFg)
-		background var(--noteHeaderBadgeBg)
-		border-radius 3px
-
-		&.is-admin
-			background var(--noteHeaderAdminBg)
-			color var(--noteHeaderAdminFg)
-
-	> .username
-		margin 0 .5em 0 0
-		overflow hidden
-		text-overflow ellipsis
-		color var(--noteHeaderAcct)
-		flex-shrink 2147483647
-
-	> .info
-		margin-left auto
-		font-size 0.9em
-
-		> *
-			color var(--noteHeaderInfo)
-
-		> .mobile
-			margin-right 8px
-
-		> .app
-			margin-right 8px
-			padding-right 8px
-			border-right solid 1px var(--faceDivider)
-
-		> .visibility
-			margin-left 8px
-
-		> .localOnly
-			margin-left 4px
-
-</style>
diff --git a/src/client/app/common/views/components/note-skeleton.vue b/src/client/app/common/views/components/note-skeleton.vue
deleted file mode 100644
index a2e09e3222740901b36b8e5e9d612242e487ce89..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/note-skeleton.vue
+++ /dev/null
@@ -1,52 +0,0 @@
-<template>
-<div>
-	<vue-content-loading v-if="width" :width="width" :height="100" :primary="primary" :secondary="secondary">
-		<circle cx="30" cy="30" r="30" />
-		<rect x="75" y="13" rx="4" ry="4" :width="150 + r1" height="15" />
-		<rect x="75" y="39" rx="4" ry="4" :width="260 + r2" height="10" />
-		<rect x="75" y="59" rx="4" ry="4" :width="230 + r3" height="10" />
-	</vue-content-loading>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import VueContentLoading from 'vue-content-loading';
-import * as tinycolor from 'tinycolor2';
-
-export default Vue.extend({
-	components: {
-		VueContentLoading,
-	},
-
-	data() {
-		return {
-			width: 0,
-			r1: (Math.random() * 100) - 50,
-			r2: (Math.random() * 100) - 50,
-			r3: (Math.random() * 100) - 50
-		};
-	},
-
-	computed: {
-		text(): tinycolor.Instance {
-			const text = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text'));
-			return text;
-		},
-
-		primary(): string {
-			return '#' + this.text.clone().toHex();
-		},
-
-		secondary(): string {
-			return '#' + this.text.clone().darken(20).toHex();
-		}
-	},
-
-	mounted() {
-		let width = this.$el.clientWidth;
-		if (width < 400) width = 400;
-		this.width = width;
-	}
-});
-</script>
diff --git a/src/client/app/common/views/components/page-preview.vue b/src/client/app/common/views/components/page-preview.vue
deleted file mode 100644
index e3e73bd08f7e16a9deb3a15cf489809bc88e5c82..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/page-preview.vue
+++ /dev/null
@@ -1,138 +0,0 @@
-<template>
-<router-link :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1">
-	<div class="thumbnail" v-if="page.eyeCatchingImage" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
-	<article>
-		<header>
-			<h1 :title="page.title">{{ page.title }}</h1>
-		</header>
-		<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p>
-		<footer>
-			<img class="icon" :src="page.user.avatarUrl"/>
-			<p>{{ page.user | userName }}</p>
-		</footer>
-	</article>
-</router-link>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: {
-		page: {
-			type: Object,
-			required: true
-		},
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-.vhpxefrj
-	display block
-	overflow hidden
-	width 100%
-	border solid var(--lineWidth) var(--urlPreviewBorder)
-	border-radius 4px
-	overflow hidden
-
-	&:hover
-		text-decoration none
-		border-color var(--urlPreviewBorderHover)
-
-	> .thumbnail
-		position absolute
-		width 100px
-		height 100%
-		background-position center
-		background-size cover
-		display flex
-		justify-content center
-		align-items center
-
-		> button
-			font-size 3.5em
-			opacity: 0.7
-
-			&:hover
-				font-size 4em
-				opacity 0.9
-
-		& + article
-			left 100px
-			width calc(100% - 100px)
-
-	> article
-		padding 16px
-
-		> header
-			margin-bottom 8px
-
-			> h1
-				margin 0
-				font-size 1em
-				color var(--urlPreviewTitle)
-
-		> p
-			margin 0
-			color var(--urlPreviewText)
-			font-size 0.8em
-
-		> footer
-			margin-top 8px
-			height 16px
-
-			> img
-				display inline-block
-				width 16px
-				height 16px
-				margin-right 4px
-				vertical-align top
-
-			> p
-				display inline-block
-				margin 0
-				color var(--urlPreviewInfo)
-				font-size 0.8em
-				line-height 16px
-				vertical-align top
-
-	@media (max-width 700px)
-		> .thumbnail
-			position relative
-			width 100%
-			height 100px
-
-			& + article
-				left 0
-				width 100%
-
-	@media (max-width 550px)
-		font-size 12px
-
-		> .thumbnail
-			height 80px
-
-		> article
-			padding 12px
-
-	@media (max-width 500px)
-		font-size 10px
-
-		> .thumbnail
-			height 70px
-
-		> article
-			padding 8px
-
-			> header
-				margin-bottom 4px
-
-			> footer
-				margin-top 4px
-
-				> img
-					width 12px
-					height 12px
-
-</style>
diff --git a/src/client/app/common/views/components/page/page.post.vue b/src/client/app/common/views/components/page/page.post.vue
deleted file mode 100644
index cb695e21e983f1d3ae1f0d8a6dfedd67c5e3d2ff..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/page/page.post.vue
+++ /dev/null
@@ -1,68 +0,0 @@
-<template>
-<div class="ngbfujlo">
-	<ui-textarea class="textarea" :value="text" readonly></ui-textarea>
-	<ui-button primary @click="post()" :disabled="posting || posted">{{ posted ? $t('posted-from-post-form') : $t('post-from-post-form') }}</ui-button>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('pages'),
-
-	props: {
-		value: {
-			required: true
-		},
-		script: {
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			text: this.script.interpolate(this.value.text),
-			posted: false,
-			posting: false,
-		};
-	},
-
-	created() {
-		this.$watch('script.vars', () => {
-			this.text = this.script.interpolate(this.value.text);
-		}, { deep: true });
-	},
-
-	methods: {
-		post() {
-			this.posting = true;
-			this.$root.api('notes/create', {
-				text: this.text,
-			}).then(() => {
-				this.posted = true;
-				this.$root.dialog({
-					type: 'success',
-					splash: true
-				});
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.ngbfujlo
-	padding 0 32px 32px 32px
-	border solid 2px var(--pageBlockBorder)
-	border-radius 6px
-
-	@media (max-width 600px)
-		padding 0 16px 16px 16px
-
-		> .textarea
-			margin-top 16px
-			margin-bottom 16px
-
-</style>
diff --git a/src/client/app/common/views/components/particle.vue b/src/client/app/common/views/components/particle.vue
deleted file mode 100644
index 33c118f00085e9810387850f2a9bd3375d14d1fd..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/particle.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<template>
-<div class="vswabwbm" :style="{ top: `${y - 50}px`, left: `${x - 50}px` }" :class="{ active }"></div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: {
-		x: {
-			type: Number,
-			required: true
-		},
-		y: {
-			type: Number,
-			required: true
-		}
-	},
-	data() {
-		return {
-			active: false
-		}
-	},
-	mounted() {
-		setTimeout(() => {
-			this.active = true;
-		}, 1);
-
-		setTimeout(() => {
-			this.destroyDom();
-		}, 1000);
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.vswabwbm
-	pointer-events none
-	position fixed
-	z-index 1000000
-	width 100px
-	height 100px
-	background url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABqUAAABkCAYAAAAPKjqIAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA25pVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo1ZmMyNTFlNy02ZmI3LTg3NDMtYWFkNy1kZWQ2ZWY1NzIzYWUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDIyMEQ0QjBFNTE2MTFFNkFGREZCRkYzMDQ2QkI0RDciIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDIyMEQ0QUZFNTE2MTFFNkFGREZCRkYzMDQ2QkI0RDciIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MUU1NkMyNUZFNTAzMTFFNkI1RjJFOTE0NTRGREQ2MDgiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MUU1NkMyNjBFNTAzMTFFNkI1RjJFOTE0NTRGREQ2MDgiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz5nGnsGAABRHklEQVR42uydB3xUVfbH731TMiUNkkACCQRQOrYkoq4gYEVFZUXsfQUbCuta1nV33f/adQVFRVwrdnFlEV2wgQYVIYmutABSQnqAEFJmMsnMvPt/Z5LJDggkwMy8Mr/v5zO8kmHeuee8W94975zLhRAMAAAAAAAAAAAAAAAAAAAAgEgiQQUAAAAAAAAAAAAAAAAAAAAg0sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4pj1KjjnHNYDAAAAAAAAAAAAAAAAAEBM8sela9NO2tnSeOGlOR61ZRFCdOl7vKtf1BpwSgEAAAAAAAAAAAAcOkXP1mYJbjuT+fg3uTMcW6ARdfnwnWVnZfOEycX9j/rX1SOTF0Mj6vL61vwJybVsakU6y78ta/QT0Ii6vFix/Bqv5L/VLKSNmd85bpxwSa4PWlHXHk0m9kac8K/sk58wRguOkFhvrxpM1o9MptbKzO+cg9S2R1d9TWaYDgAAAAAAAAAAAJGi4LnaVMlrf0kwNohzsaiyd/EDmFRU0R4zm4bZHWwNYzRxJFjBLNcpedOdP0Az6jB7wdahSQnyZ3XKfnrjrhvfXLnnXDim1OO5dfOT4j22f/ucjPVsZONf35pffF3/0YugGfXwC/H3FKs7S9k9sTaHfahsYQ8VIQchYybWwk0jS0/claecWg6tqIewyqNazV4zY7zPD2lxCcopXTgJ4ZQCAAAAAAAAAGAoCme6znP3trxI+/Yq74t5dzofhlbUQ2q1/8OeKF/Ufjgko3rIJmX7KjSjlkH4RW0OqfZDxsg2cEqpRG9L5WmMpXccm7auGaNs4JRSiYqaIdb0dHfHsSeOd4NW1EU2yVXKhpxSrFVybYJG1MUim15oMbGRgUipVakF0Ii6uOp3PpySlEZOwrV/GDd8p17khlMKAAAAAAAAAI6Ql1e0XttrpzyG9tNKXXfn3Z6yC1pRh4XvF9laZMsik7/tuLWH5e+rZjcuOXFaQhG0oxKc9drr0M8GQikqmkOIbXudkFgFtKIeWSVD5jcNrvuzj7EMOnb2dX0ArajHo+OG73y+LP+PvUtNdzOrf2XGGifsoTIZovziGnefK2Vf84pp/c7eCI2oy829R81TNvOgCW1w+7BL6pXNPXqTG04pAAAAAAAAADgC3ly5Z3xCLXul1SwFjhvSEwYom9HQjDp0K+1lZpl7n5P8pj7KBk4plRBm8a6yOT147DfJ86EV9ajqveGD9Moh/SSZj2FcFFX2Lp4DragHvcRQ8FztMRvyTHlNdTvW3XLKOaXQirq0ryOFtaQ0wqReV1XAHgAYC97Vxac0JzjnsB4AAAAAAIhJyAniEY7naL/F7v+i76Z1t2N9luiQ/2RVPG1H353RFDz3n489r7WapatDv9d9XW1y6HdAZCic6R7ATc2tOXeklIWe/+rD1nybl51K+34Tq7DvaDwe0WuRZ9Xsxhza7i8qrWimaywzseGyXyzNmxG/DtqKTl9BW6xPpA1erFh+DW3bowwA7AFgD83yUsniQbSdkj0ekWkagLIA0PbCS3M0v15UV31NcEoBAAAAAIAOKAWZ4OwvbQNF9n+/O9n6BrSirj1S69grtL+rG7uR7EFOkS2npjSEfs/P2PWwVeQpnO26oSXV8jLtx+3y/i53mvPVfe1EdG9iVaMnW7Ogscjbw2EXAXu4m9hVudPj3wl9eO+9Y/AVSjvmZMzz732dViAC9pjpOs+RIAKLz7sb+YTcGc5PoRX1mL1g69DBCXLA+behURo2bWL/9dCKuny4flUgqeikoSeaoA31mVOZHxg33dJr9LXQhvrACQKAMeiqrwnp+wAAAAAAQIDn19TlmJrYayFrn782e8HWgmzf7k2lRw25V0hSFpflsj6bix9HVE7kIefT7hBHBzk9Fr5f9P6aobZBjn3ibzwO/8nKBk6pCOPuaflbcJ0i2lc2AadUz/LVb5viho+haKkEN/t+zRDXDGgr8nAfP5+1N1hcSJOUTYdTqv1N0lehpShiZj33uw9U4aSKpB1Ng+uqgvvQiPo02TwXQQvaAc4obQFnFACxNmwEAAAAAABAIa7JOXzfc/b0zLztjow7/Nw0NXDCZGLbBw6jCJCbobHIsm5EY/cMOWWvc712Zsez9VvX7eg7YptZsH7B8za3aQU0Fnm8Ettm8rPewf3g+XYn7fXtHxAlBGdvKpuLQvaBilT02PBOr+rBGbRf3asY6Zf2w9dXrUyrn2BujEb6HUpXuWh+Ia2lxsbefhReJNkHevGj5Js9J2SflvxjtFKtXtd/9CJofv8svuyn/jubbJlJV7tX6SE9ldF5La90Im2vL+izANpQF4r89r5nu8ntlX6+5pMh+dCI+v34Llf8ZbDHkYP0fQAAAABQlTlLNvWRe/YOrMUi1VS8ecs5A7G4s0pQqp/49My1oeeaqsuHywMyf7X2x53HWDEYiwKhaxVZffKb515gCzg9KKqNSdKT1kZ739Y476u3He98GNqKPLR+0Y4B0gO037PE9RekhFOfgudqU2mL9aJ+zS8PLR3OnNbHA/2ro+m1AVPP+RBaUQdygKSWb/7a7LSdQMf+2urTh8ydsAyaUa9u1GxI/Yn2vbK3xCNLZ49/7/it0Iw6kAMkNcH3UfvhGnbL7hMQka8erxxb9lfOxV9pXwj+txt/zvobtKIe8/K2vefzmibTvjnTNQaOEJXbq6kF33HGT6T9lDOYHW3Vr0H6PgAAAABoHpqk+alX95WM8fTAiV59b39u3fzBtw+7pB7aiT603sSbK/ec1+A0PUbHiS7/fXTumdUt1R02ahtqVkNb0YGcUKtmNz5XMMbHbhvRrSh4vn1/HDQUXXJnOLYwREMdkMA6TrJ4zm9puT7Olbhqs2XXXyddMfbzSF4Tzqj9s2h+oVnsEB9aRWtgjQ7mTTj3l4eWjjj6gXFroZ3ok1L+y1Sz035CRy9qS36SmhRoRh08laYbg/sWyZLdI20PRYX8A5pRBxPzXxpyOML8LxNF+MFJqBIWq2+Iz9u27JnExRXKBk4pjeCvcaZAC+rCGaP1GU8UTKyacEkeHFJHAJxSAAAAAFCN7aPto/Z2dvD0pKYzT1F2FkM70YMiDYSw9jX5pET2PfMIxu5vG3RLnqKZrrE/+H3P+UyWh4Lfd7r4fdBa9DhxWkIRtHBgyBHS2s39JufsRHOyWMO97r9eeMo50JkK+J3N1/s9POC0a3E2nJjFrIsXzS+Mw1uk0adXVb8Mq7RhUOg5Ka11sLKBUwrEPJKFl+/VdnkZXrZREUem+ztWbw86ptb4LvaXsvegF9X6cmZ6X9kEInOYhWHMrzJpA/bcv3NLMvO2movT7qlZxC6BTtSk2zhpWv2bjjf7JDcUQxtHBtL3AQAAiAnojeGM8qF9ORN9lJ7PofSASYE/CF6v9ChuwXhpVeb67Zg4iywFM5uGcTMfyf1soKL7gYotBn13unlo6Hd+85VvvWKTjYqNNgkT2yR8YmXejPh10F746kLP6kHHSj4p/VDufUqzaEnKHnvcylYv1RvZLFfXpG/8OZbrzHPr5ieZk9MvtLWIum4Fzi+wBoI6fFGw8DF3c+q9wWNvi1wat8c+AO159Cl8t/BlipIKPSdVD+6JaCZ12vrBVeYKi+Tq0VE39rQgUkolaA2KHimti4Pp+zx7mq8+5oXRb0Mz6kCR+vGbN79U3+jIS01u+qRkrPNu9BnqQin8EjIbM1OdTe+NeWvkTmhE/TpC22ittwYAMA5d9TXBKQUAAGGGIg641zaSm1h/4WdDeFsUSILS3ia3t197lE2jYKJa+U6x8p2twuJZiQmb8FL0bG2WkONGcVk6W3CWq/SM9LawqZP/5lcMtJELVigk+TMutSzHeiFHRiCVU9WQ8YpOr1bqwGjl1K9SDpQPtvi3Z7bZpm8583er9Jj+e6KNBY8zNwTyR9QqdSefFrKvyChejMn/Q4fWwlGGiIOVtqckXE6+gJOR8WylZdvQntYsZviw8q3eNXKf761NPJOOPUn+gj75CWNwb0affxfkf8KbTeeFnvMetS1zUq+rKqCdKNeLd5adlWW1dkS6Ugq/464dcTI0ow5YU0pbBByFGxsG70k+thrjfgAAAAAYETilAIghaNJ34NLKk2Vb0gmyV2R6/VJPi8nf02QxBSJB/F5/vddvqrGY5BpKVSB56n/cNK7XCkychQ+amJU4v1rI7HR2+PnhC7nEvpKFeBNRIYcHvdHlsCVO5j5+udK/nRGm/uZLYRbvuj0NH+BNsa4TcAr6bX9kIpD6IdQRFXAuBaKgmFjNBa/ym+UGyWciZy2Tzf7kFadZPwlN6RfimOr4DcbZB9zkeTRWnYazt302SjLbT5Z9zSum9Tt7+cG+u2p2Yw5FRVVlFn8WqbeA2yIRh5xN0VOxkmrutfKvn/A1me4KPedPYNff3HvUPLQA0WXfSClhYrtMlXEZeOtdHbbMXTKpLjH1nIAtavvdh8l3AAAAAAAAYgM4pQAwOJQCIi2l5RrZK5/LmfiNciruEH+iRTD+nWSR/rOzNm4eQuQPnUAEyI7BVzAvu4WFf6HiQmZhcyp6bHgHzsPOoeg05rdN5X42g+0nEidM1AoTm8lMnrmYYDsw5IxiPvtDgokrWTAyjfP1QhLvMFn8uzOH6x+Xrk1LTx24I/ScSfjnjlzqn80kfhGX+RXKKCeY7s/PGX+bmZsfiCXn1PNl+fc0W/jjwWO7V9x7W9boJ37ViLRHRkXSGbUvQedULEROwSmlHSiNYs+K9JctTmmMzy27LU7Xb7GmFAAAAAAAAABEFzilADAoxVMXjfWZEh7gTIwLa6PB+FKzv/GhIXMnLIOWD05g0rV6yDXMyx5UDrMifLkyZmEPVqUXz8Mb3/u3Ra+qIbcIH39MuYsdUeqB3Nws7qvMKJ4Dm+xti/TKIfdyP7+/3RZ+pa/+WObiibzpzh8O5beeX1e/1Oe3jw0ed292n3v1yOSOdFAFs1wnSYLfo4xhLmABxxd3C5N4pLpX8eNGtwk5w385zd2873lbbXXy7cMuqe9oo8qHnK1mWtBgGtNoOsSibovvl+Ts6O5cFXquW+L2PkgZBwAAAAAAAAAgFoFTCgCDQc4oYUt+Uvb6ciJ5HcliLuKePXfDObV/AmmwvNKLLPyRUZ1RKFvkm2MlLVaXbeEzzQuJmol2R7ReNvuvgU3aI3KEeC9YL5Q+eoHM5D8fbhpKipbK7NZvipCkrCRPy8JQh1QogbSZTPq7MpaZGBRFufhlRo7QId2kDK3bse/52vXdejw6bvjOQKSa33ZUzgynJtrwopmusczk2WzUSDZKo2iOs//OJss1Xl/zK1Oyx29E6wwAAAAAAAAAIBaBUwqoQvlxl1hom/nf+V5oIzxQmr5EZ+szVpPv8mhet9VvfrfBZb0Taf3a+F8UCPsbC6Ykiz5+YWJ/jYVokM4oetY1TfjYTBVt0WETbmYzcu5wzo5VWxTOarqCydI/26Ojyhhnt+TOcH4aVRlmus5jgs1hgchF7maSfFPu9Ph3jKrz2VVf/9DCTSODx3HCv3JaxpiT2h21e7TmlCOnJa0Vtq8Dd81LRffbXM2/8Tjt321NYk8jVam67PrPM7lse+pdbs5LrAM2zUo/88EaaEU9di9+IIFJBbf56rOPd2dn/CX7xAfh8FTbJp+dfV9rNznX3tQwM2ncyu+gkUObeAj3c3zJqgcH2dh3D0ueYW/3GD1rASyh/rPSbxL/9Afa7372Z49BI+qzI3/6xG1DMweNTP0D7KEBPqx8q3eWtfrq6q/GzsKYVxus3PXUfQU1fecEs00A9Z9Fvj4upQpZJ7QDZWk5nPYKTikQUUqHTEr1ZeaeLjjLVSyRq9xF/VnbOi7O9q+4GC1mz9hW5W+FXLBCc3nhV32KP8Q6LIcARUfJ5oS3lXqaoVI9q5J8jVfGetQUpaGSvPZ3FTucoZH270vZ0nx5LK5rRJ1iZsWQuYKJazTVJzE+r7x38dRYecCgh6qdrM8jsszGDNzQq0+31RltqfoszVPUTBen1NOXglFTwsQeyLvT+bCR9P5SyeJBVtk5sFVybfJbnff7uDzILKSN/rqaO07+/NwTZItnjVbbhTb72EYEI7jIIcW81oeCf2/p3vJq7uW5v9NVe/T9kkDkslSR+rPeXxSo/uLBnp6KPh0PgLaklm/SJ95yul7kp8lQufeuYzML83YYJSpvxwdT31c2wShQ1mPyXKue7DF4Y8Pg9UcnbTZKv1i/dORvTL6W/ODxsvqX4/RS7/OfrIrPaCnOPvqBcWvD+btHOo9wpM/zjcuO+4l72TG03+o/P7n7+Ica9/c9emnypz/dK7RiL3rhUDq2T/PouzOaInmdQ7FPOOZWyAHiSdr8Ee1bd+SmH+jFBnK4m+K+OKbZP36zFl5+IHs0Xmiqi9T9oZbTNjD22vHntXb3zmGpv6Qf0B7UXo9OuS3wopMWnO1kD6O+FPvNjlmnOpgvf7svPetgk+7kcN+enZzW8M2pP6jdbtH9YeSXYckp1fenptcO1hZRm7UmJ/XYEUW7fj5QPwNbhG+uR5KEr7NyUl3a6UvdBueVNqA2i7ahL9DBKQXCDg3oW3rmXMcFu0K5a8Yclt2UcYbg7J24mqLXEU11cNbdmX+X8Pqf0kR9s5j+MOyZ0f+IRTtQKizhty9RLUXcgRvB9dzUfI5RU2IdaFLFaU5aEB7nIHcrXWVt+35KONajImehy1c/MdKTDFpg30idbvVpX9w4aPBZWpCt4BnXn7ifBZwdXGKPy2bPU8xvmxo4lvl7ek3t93xZ/j1WF3+0Y6BnaxoaTBVHKfJcvoYCrd97bXU4MY8cU/99Y80KUxPruIdsfv/Oo+84rqde7PFFwcLH3M2p9wZsYfd/elHe6PP1XKfpzcSmHQl7rf2Wfd0NZr3IX/hu4ct+S8v1tJ9a23jpgKnnfKj3drYzpxRNTpzs/O44n6WuTEtRbSTXoB1irVW0Dmrl1o3FqdJxRnBM7euU2nPP0bZ9n2XoodxRUvV/gYN0+3taiN6hdjelZOMmS5Iz3etqvjsc4/lwzx8c7nN99aozP4zfs3OisLDV9TOOPnF/z5YU3WblNYEXVFpFzz+pHcFDE+5mZq+k/Uaff9D4947fGs7fD4dtDtseXzzYkyV993xgLLb90cv2N6lI33Ga/70k6Ex028ZMUrOeLL7sp/6+HUlb6hr5+9cU9LtMS3Y4Elvs1SZ59gw9mI6pHrXGxV1M+/HV3j+qWUfmnV882lfu/Nqc6RpzzSdD8sP9+0dil2jNP5Jzd0cfT8C52+xIW5fX4+/D1bLHa3mlE03Mf2k464beIIfUlhy+grGkocpIYH3lsoty1RrTUP1Ii/eUh7vf0BvkSJRk9gjtd+bgjSSHG0FkNILO9kCfzsyjT+sx/dtDaW8lBkAnkDNqy1mP39XSI2czE+ylw3VIBW5M+r/Kb9Bv0W8G0/2BvVlz6zczteKQCthNkYVkijU7UMop4bN9pzmHVFsrP5RkC6zlEwNQp38EDqlaimRiFnajbJFz3f6GhNzfO5y5v3f2afs4nHSO/kbfCXxX+T+H8aBzBslIshpN/zTJSGvn0Oe5dfOTQh1SgQFIYnWiVmQNREdJ4kpG6S5ldi9vtW0gJ1XAUSXESorY0aMNQh1SBEVKBSZ+ZrlO0oNDiiAZSVaS2Wdt2Wu9MVeSaaue2qOgQyowUdFsOo+i2PRcx8mxEXoc3xw3P7j/y0NLh1NkG320WH9IpqBDiqhLTD3HEA1vuv294K45qeQvoX+iid0RrtUeciRShFvpi+88qhWxe1X1yyCHVKDdUrZDf6k/KvxDIHHInyOFIgjIqdGUnLbAb44bva8DhCauHCVVa1ibI3Eiq25+P5ASU2UoQoocUgF7ZEvjwqH3SNnzkNutKcmXky1cvovO2Z9DiupJ0CEVKL+yT85FNe3R7LMmROaxIHy2OdzfIud4+olfTKLPgd5yt8orrg86pAi7/+u/aOHR25Hp/k5rdgjH79Fb6wd1SCl1JOiQIprSLar2JdmnJf+Y2K/xzqSr3asi0V9o4Xc6Q7atu7Kjfrh3Dgs4e1Xi+oI+CyLtkIp2X37IjUNO6rFtDqnASGBo8tjlqvXr5KiNdYcUIcn1VwX341xNp6glBxxSbTTU2zqyejk2Jww7ZHtCheBgUPq4gDOKCXKQ9AnjT/eh36TfpmtA0yEd363fzORMnq41uUimWHJMBSbehKA0U1kaFjOLZNTrJHtXIYdIZuWQdw7VIUWRS8qNe35VVnF6zu8d1+ZOc75Ka9rsb/KeztHf6Dv0Xfo/9H8Dv3FoA+szSFaS2Uj6Lzm16dsWuyOfPjyp52pri2WvyQZKIaclmQPrSUn8EmXXz9pSywZJ4b64s4xiG3JKS5KnQk/ReSQrySxqhrzsj2crAw8UzLOptSH7dlorruhp9xu01XIdorQSwsT2SpPo3zCgWc/3Ek0mesuzj5M8ticDn+ziKXSeHFKebmmrA6kWlU8S3/otRV5oSfbqntv2hB7HNXJDPLDTRKLcLSOLPvu+ud66ZeBe40TZ5rlbzYmrUCiC2+sV/wnIya0bKYVfuH77SCalwjGhRXagCff9pbiSRdqvHNOS6T+qp32mlH0UIcXTWhYzV+u9R6L7SHOo1yBHFNniQJGCniSWvO+5FvPJPdS0B00o9hy863j6hGtyMVK20WtGnUO1x4Sl/Y+ZtGDY7CPVVST1FanfL9hzPr2EVxpyqlTtMSLZIhyTvZG0SSR/e3v/swuD+xQp1W4jQ3EkTqZoO6j2LBtVSBFSbUf16ymlIgPqjvvre/+NbFHSal8g/5i3yMhl1bLDtoP/5i6QJXY/fSrLjnvjUP870veB/UKTQUP/+SW9KfOHKF3yqfU3nfFHo+dI7Qwtpew7YN2LgVR+9BZ874rBy5XdXJ2IXFjRe8Moo76tUTTL9RhFvByKPoTEpuVNd4Zl0EhRHVxmsw/lfqC0cTnTnfcZQf8UHUXOqNBzx67NYMVH7/K1xnnNccK/Mq6l+dpgKjktEZrKr2NwJ7GTw3VvRJPXyr9+wtdkuit4bLGKi4Z+enwlOVP1eF+tmt2Y4/E0bazPrvBQ30/OqNC14ihikRzEWpX/9a35E5KrTK/Svlwn/vLb80+dE66HDy2NhSktXtzuuBtCz7V4+10VcPxqqZ94tjbLaiu5+pdezXUml/21cPWHh/OcFA277Jrz9rtN9pZL9jo5tHRYaC53LdikMmNbVTjG9pF4Xg23ncgpKNVV7Z1SOd1+qRZS+GlR/9GyTei6U5Tmz+s5f7Saa4Lo0TbhtAdFFFpsn+QHbUKRblpYxyhW6wdB0YPN8YkzaN/e1DAT9lDfJpTCb9vQzEGdrXUE20Rn3EVzoyP6fjJgzfbzt8T6fCXQRz3Rgr8Ea0qBw6Z84MXxrdl5i44kTd9h2ZSxr60lBRMyN/2rKRb1vvrW/Csl5n9LD7LKzHTVMS+Mftuotiic6XqBCXaLvhpFNid3hvNWA9riPMUWn3RRCW5mEdMo2ikissx23cC8fHaX15/i7HzFJp/q3Qb7c0oNXZX3+rkX2K6nVH63D7ukXsvyF810fxGMstO7s5BsYbE6+kneppUnLBh9lN7vL6rfVAaK9uSttl8tak1pNWNhjbYjfQCJ5Jh4f04ph7lishHWbIrkw2CkbbNl7pJJprjKjvR+tqSWb9In3nI6bKGujWgikdL20T6lXFR7/SK92CCSdiEnCJMKbqP9VulkTPBqpC0jJy5FgOh9glfv9QP9BexiFPtA99G3E3Su/Tqipo3glAKHhVoOqQ67xqhjihZZzeq+h8KC43QickvZ7uShRsxpWzjLPZHJ4iNdCi/x3+ZOdywwii3aJ6o3sL3Trx2IMsHF+LwZ8esiKtPMpmFc8MWsa2kda4XVMzjv9pRderfFnMr8N1wSD0SxJLm61fQobMnWS2ReIPKxckhR+9pwtcLEZgbWndJz3ZjlOqm6d3Gh3idz6M3D9Iohuczs2bwfp5Tf7W9INrpTSusOkI70fe1QqsVqd2auEe0SyWeiSNiGHFNJcu3Fbs5L4vuuf8RI0R+RtgeeK7VnA9hF+7aBPVA/YAvYReu2gf7VsRH0HlvPIOEsE5xSoIPy4y6xtPbI+Vwth1SHbckxtaPorP0tWGtU1t+5vFD2+nL0JLNkMRcNfWZUrpHsQOtkOEyJJaxrThAtUuv2N2QbZbLwECLWCoXVMz5azp92Zxk5pjq//w0SwVY00zW2KcG/lPYdjdLwSDv/wm8v+/a9ItwkcaXW0o91FXKyZewYOEyvafv2hdL4VfXYtC6zYsjc0PR9Ro3+1OPDB61dFucsvpT26+p7PQuHFJ5djGAP2EebdoBNtG0X2EQbdQP20J4tYBft2Ab6V88+0D2eQQ61XBJuARCkpUfOP9V2SAVuXkUGkiVW9E5p+/TmkCJIZpLdSLZwmBOfYPp1SBEp7WXQPTRR3SWHFOfro+mQIuhadE26dhcatFsCZdH7YInx55wNJhbfZHpcTw6pwEDHaxvxq5SLgp+qV1v0rhx8ulEcUgSVhcpUmbX+RmZhN5IzirZVmcV34OFDG7+fO8OxZcSUnEfoYzSHVDQXB1Z9IWLUD01eF3aATfSkE9hEWzqIdXtotfyoJ+rqAPpXT0fQvTb1pWW7wCkFAmw98zF6A1ZLC4pf2y6ToaHIHBOXn9Sr/CQ7lcEItqA3wZlgU/Tfs7EpgbLoHJPP1JV1GGq5qfkcNdLj0TXp2iRDmMqi3bpBKS3bUt+VubwND+nvZvJsVv71h57igjv1aAuKkhJMlERywNrZJzLNliiRZW6m9eAoOoq2Rl1IGA8fsWcL2EY/uoFttKcH2ASgbqCO6LHcsdx24aWG2LYPdK9NPWnVLnBKAVY6ZFKq4OwVzVVSRSaSzci6Tyn/ZarSOGTouCHNoDIYpFu4S/nHZICCmNrLolsoski5t87o9IucXZtzR0qZWnIGrs07d+ZTWXQdLSWL+wPlMLG5eoySCNjJQg5n7u6wCQs42XQHRRRFIlLtUBxOkXBOUZmobHj4wENhrOgGttGuTmAbgDqCOgJQF1BX9F9mtFWRf4bTwrVQT/RdJyS9V6hwfGIdb1buU8pGi2+NO9tlMyT0xjvzyXfpviBKGQJl0TG05owhoqQ6Gkg2JVAmnWLymjpN28UZn5c7w/mp2rKSDCRLOMqkyboxy3USa1s7q7a5teEZvd5TgQic3zucwupJY23RbbntZdMVnDG3Vgan4R4/RaJsAGj5gQzPIAD1A3VFrzrAZDtkAwAAoL/2Wmv9BSKlYpztgy7sx7SVtm9frm2X0XD0+8Z1sZ6jpEIatQwqi57LIMm2y5kxoqSCmNrLpDsoHaTSTU7q5Gu1srVZMw7ddlkOmsaPyqTHVJdcsGvadtgHRlhLJpDqUSnLXmXTCZSWszKzeLmWBqXhHNRS2YyQelRrDwCYqAKoH6g3QN+UDrrAAS0AoM/2OZb6E7zUANtA/+BQgVMqxvH1PfmvkFEdOJNvQFk00kn79DU5beQyOSwJFyjSH/Thm0vsZTXWkToQgfWlFJk6sYijrWz6YdH8QjMTbHJAes7mGaZuBMuilC1QRv1IPjic6yyF6yEhXL/TVjYxGA+CeCCMJV3ANgCALjQUNigBAP32mejrAZ5DgJZ0oSWbwCkVw5QPvJje2p+kA1EntctqGL6+amUaZ2KcUcpDZaEy6VH2omdrs1hbejKjkdteNn3dS0LqpE3ibtns0VxazzaZuPvIyqYt0iuGUL1IUT5ledOdPxilYrSXhdYiS2kvo07aWeOnt0MKPxCLD8Wx/rAOu6DMsMfBydr48W7YBGWFPQDuP9QNgDpiNHnglIphWvvmnce0uZbUvjjbZTUM3RNbzjLa/aTXMgk5bpRhOz4dlk3pHEcf7O+cs8+0FCUVJBAtpch2JGXTHJyd3rbhyyI9IIr2mo8dZWovo9ah1I+yxbNGq4PQcP0elVGPaS4BAACASFA28MLu0AIAAAAtPPPpXQ6gPeCUiuUGirPxkFUdOBOGi8zRa5m4LJ1t1Dqut7IVzGwaxtoicw7SFsgfared6lS2lPYy6mOAIPMxgXJJ8meRGpx2NkCNlHMqWKZgGbWO3ZI4XIvO2HBDZaSy4mEQD4MgRp5FNH5fot4A9ZFN0AEA+m+PjdyfIJIQAHC4wCkV24yFrKp1i8cYcDiiyzIJJoYadoCos7Jxzo7ttEzmls81q+8uyNaVMmpo8D6obct+VvvBIPyRPW1lCpZR83VDHNxZq4WHsnD9bjjLCkCk7/tYkQ8AoBoeqAAAAIDex5axPNZFquoDA6dUjFI6ZFKqsumjI5H7tMtsCCTOhhjtntJvmfhQ49Z0fZVNcN7v4MXh67UcLRKQTZHxiMqoEdpTqNGaZP7qzA0btTD4Ceegqb1Mfioj0sUBAAAAQJMjeQGnFAAAAACMCZxSMYpr9LUjILM6fH3VyjQhRIbR7ikqE5VNTzIXPFebqkjuMG5NF462MuqkQ/LzgQd9MGdso9bL0JmMnZVRK9jNCX3bdysnXJLrM1rNaC9T5T5lBQAAAADQDL03feyFFgAAAABgROCUilHitq7rAZnVodlnTTDqfaW3snGvPcnodV1XZeTsoPePYKJa60XoVMZOyqgdW/CgnDVh1c8RRjuFOcS8Zp+yarhqMHesjE9iqawAAADAwYdjHEoAAAAAgCGBUypGERJLhMzqcNTwOsNG5uitbLLZn2z0uq6zMiYc/MGcNWi9AF2QURdOqeC6PpzzPUatG8Gy6WENI5mz5lgZn8RSWQEAAAAAAAAAgFgETqlYRfBEyKwOvjJXmlFvK72VzeSTEo1e1Y1URpmzRsgYJTi3uhL9bE+WOW7h+0U2Q1cSpaxaEuePS9emPb+mLsfwetc4sxdsHUq2gCbU5cPKt3o/t25+EjShLovmF5phBwAAAAAAAEC4MEMFMQoXDUzoUGYjVLos505/pUEbFKVsepLXb5YbJK+xffNURqOURRLajzIiGYUBdP3DWH62nwd8IqMYG75t9oKtp0+b2H+9MbtDoZlosJdXtF7rcrLXfcpNVDJkePXza+rOv21EtyIMWqIHOaIye/T9kpvdx2QxJ3v+J9cDtx3vfBiaiS7kALGbR8xv9DWf4VRa/hcrll93c+9R86CZ6PP61vwJjUk7FnZX6sO7lfN+rK2zn377sEvqoZno882OWafWxTveUXZ7eHd7P7WtGHmpEdd91APkqDUPLp67J0Uca5Ybt9fU97gB9UI98p+sii/P3PxnX2NrliNeen3SFWM/h1bUtceO73dfT/uWyzz/vPDSHA+0oq49tnzgPTN9QO3P4987fis0oi704qF1gdQLtgAAkVIxC5dZA2RWh81ruxl2vQy9lU3ymfYYva7rrIwHjTISQvspPLsgo+YjqWhS3s9NU0Na33TeP+MOo9UNIUQgtaVW0sWR3skhFap3xs2vBuqxYPaYGZiqXNbeKQP+Tg6pDivYmx6iqCmMHKOLMyntT5K9+YzgcZw//nWKmoJmogtNvFuTmhZ2tJtxqSeQbaAZdWh3SGVRlbB0t/y2ecS6W6AVdSDdt0jSDfY6U46lPvm3A2TP09CKepBDinsa77ZYWi7ztjQvmbNkU5+wzkPoYG0vLcm4ZXP5Z009mp+mT91SeTbuUJXt8ba3SHjZv6o2pGxefNlP/aER9SAH4e5H034mW8zL2/YeNKIuVB+WXVH0C31QN1R69ocKYhNL2cqNkFkdnDnpO4x6X+mtbMLSbPg3GvVURsFE9UEfthg/WvMPhJ3I2FkZtUCvesevUpZxc+tAA1aPnm1GEZpwFGam9f7VBIpPjjum7b5hhl2L8Nd1RN2y7u9eT+7VvS9GjtHFI0k9fzUOLElNh2aiy/ah25xdsQ2IPOQgZG0OqQ76pu50QjPqkOCVjgs9burWkAytqIc5UR4Xepy2u2IwtKLq89CJIeM6vNijIl9ftTJNsUHH82n1lpRjoRX1KPlmzwlBe/i9puOhEXXp3b3xZItkyaaPq8V6HjQSfeCUitWBgixthszqMPrujCbOeZXh7imlTFQ2Pcmcd3vKLmVTa+CqXtteRl0gJFZx0L8LMUjzZehExs7KqAUoTZ9Zalkdei6uwf6GkSoGvaXmSvRnVeT+lxVe+uM9s7d9Nkr9+/9LpY/b22lpNjUvi0BbHak+IKy/R+nbVImMaTV/FXooS76G9a7yQq3pW69ydFlewfeyg89r2cHK0tZhBB9dKB0Zb9n1Y+g5u4d9CM1EH0rT593t/SjkVMvPTcMWxEo7oTXZHD1rX9urD2+K/zrW2m8tyWUWplc7unGvVNqjYvD3aDVUHdW+HtxL2OF4z8j1QOvyj3lr5E6zxf9BQAbGfuk3qOpb3J/q0e1a1/dBeyT0a3wOGlGX3Q1xnzuc9fOTEnfnpzqbELmmyjOfEFCCzju4w2XLWY9tVzZ9dCJu6YDP7zPMm8prbv36K87EOEMNPRlfOuKFMafrTe6ime4vlHbwDIO2bV/mzHCcqRd5C2e5JzJZfHTQMpk9fXLuSCnT5L30bG2W8NlKD/olif82d7pjgdZtQenKsuIyA46pigzvX8O1pk44xhzh6LMLZrlOWjFxzfLWOG/H2pq167v1eHTccFXXxXtz5Z7xu+32Vyl1HzkG5V92Xk5OwsKZrvNyZzg/DVt7HYGxX7jGUlTWwsk/pphamwMTGh6bdRXfvfPsaK3VQdEIpUcNuZdZ5MmMy7VMlu8+0nW9tDDWPhL7kE5s2RUP7epdf0ZyLf+5df3Q26KxPsSLFcuvsTSlXdWasKPK1Op6ZEr2+I16t0O47FP9xYM9y5xZFwX2K459JZLrCpGDmFL2UYQUOaSu6z96kZbbIrVsQzYxe7tlpZ57Z2GkZKF1KFqOXXsTRUhVbxr+xYWnnBP2NQe1ahMtPq8v/H5JTkv3mlN6dq/76bQe0yM60atFu2jNJmQPb0lcSvWxu1ZGasyA+tF1gqmwwrlujp7nLtW2EUVMkYMqluqFHupJpNGSbWJ13t1ozyDhLrOZgViG3sC+VkeyGgi+mhnMKdVWJh12EEz8omwM6ZRqL5ueBO70HhLcRk62VzUpfptsR1xGtSmc1XQF28bnMeY10XHvMN5FNOg5kkFZuAZNv/T8cWJr3N5joEHZu09SNovU1P3VI5MXK5sMmnjsXTVkPDP1PKngudodspDDmvbxSO0QycGs4KzW1NrcYQebp/XE1sSetMbZE9GwQfvk/sPtnw4ouo62eosIDge0dorfb77XXmdiLRLLcfYvp0mN+yJ93Zt7j1LaITYPw/VfU2bv8amfSScE6kh2Rb9I2qN9cveeSD0IG+FBfdd/nsltktgqFsdY1ZczX8w4Y8atkZCn3RmM9Vm6wNZlT99T7fU/lG4xPdB/7O8j0n+0OwWLoO3OIec2byy9Py05tXzy4GtnR9AeoItQNPqz2xvS80++NSJ6C6czKlag8X+kXvqJlENKD315uJ9VAADhA+n7Yhgu2GLIqg5mf8PHRrufdFsmzr8wbiXXV9lyZzi2KJuDRkFxH79cs+ruXLay9jJqG5k/pvxr6iiXzK8wWtXos+P47H3PtUquTVqRL6V60BvlXHxULrNXKvy21ZLPtIci8Yw+LqEy/nRRft2+5x1cTlVTrifm7nh8hTO+nj5Pzqm7UW8Pwkd6/W6NyXulUfTZGtLULM8Vr1XcfdM/d64+//WdM9vX2Yk5/JL9hOB+ckuFqi/WnPp67cVkkz8uXZvGYpQGu39cSIU7R01ZyA5HYgstTtwdjkzkkArd6n9Iz3UtT19r7Vmyh/2+pnrX0+Sggh3Ul2lmWc2cXj7vipdKFg+C/tWXe/SKF3L+2O/rJtoyoAnG/vD0I3q3h5HSiFPfoVd7GDUNLznSj1QGOKViGOvOIkqTVaoDUUvbZTUMm8b1WqFsWgxUpJb2MukOYW5ermz8Bqzi/vay6avDZvygUZFCiLGFM90DtCY3yUSyHUnZNIRjb6Wz7HBO/B7u4Cccg7nny/Lveb48f/uPF6397bFrM5jdJ0rjhH9lvJ9de6SpwcIFOWZ2MT455FSGkMRlzG87SouD47AOspUykh0oZV/o6SZvi2oRbJTKUvjtfwgey8L8kp4eRsJxXbc3bn7osc/mWqKWPa56Y9tYqzfxMa+wDUtttd3xuKvfhbH4QFiXYP7fejZS3D/Vkp0cUs1Cmr+ROR7/vKz367H6oC45GxbGMVFC+82S/KKa9vi8PrGKPnOWbNJLivaIQBFSoVu12qtLPl9WeMHyV29Uq63QSpu1vTXl8/SjbB9LNvZ0tNLx/qo/3/bZKBoHHumYVktt1pHIktLabWGNxf9URkGaKi/MUaTW4awdqrs1Mrso7+TEtM09Zf4EbfXY5hpxTUK/sP1Lr/YwIn3X93P9pnl0KTShHcIR2Yn0fTFM5n/ne7eclUOL7P1B46J+QLIarfL+OHX5R1aT73IjlKfVb/4oGutLRIK821N2Fc10LzPaulLK4GtZrlI2vcktuPiACXbNQb5iUr51l7K9VWOS38VCoosOWDY92MDEZnI/C75Z7FfOODLKh5yt7H8axvvzkNI8hONB5/Wt+RN2WfjjbaMfL/t5eBVL9LjOmpKpDWdUp3bZ11mogh0i/QAaLCOtIUUp+yhCihxS0/qdrTsHu6HGLKecU7Tw+yW5tHZK3O6e30845UrVUiSViMTuod7ZPsLTP5r3fCTq4OEwfsTVNyo2eeFY8w9N2Sc+qFobRvrf2N40CZkNidU6QjZY+H7R0JHdFyWln/5gjVpyJDP51Ob2/berU/LYYb58qKV6crh1pD1l3xNqyt6Uve0Kp8uSw5r73aIcvhLL/Ui7I+piNWUwx9l/V9q4+xo5t5lepFx+pPel2nXkSMdgH4+6IXBPqvXGnJfJC81y43a17wuN1ZH71bLHu5XzCi1MunBSr6sqYI02KLVlvkrXpufWcK3hqXZ7Fa7nxfYU61Ff/5migSRJ+CK5fmssg0ipGMdSVkgTdC4Ni+hql9Fw2Nmef6Is2kBw+TWj3V96LVNVZvFnyqb24IVjU7QULRWQRZGpk6/VtpdN8+Td6XxYSOxkJokrGWeBqBAu9orcCdsAtbNBale+0/XO5NcTprIlfqTW9J9zR0pZpszmhFYLZvLM5SbP6uC6Rlp4UAi3Q4rKRmUMrDuR2HOqZGLDWrhprdoOqWkT+6/fZq7viAySuG9KtHWtheuRY4rWAVF7zY677SWf2libPSzcs+7mXdKcWB3Dky3UdEgRffvWva48TS4WjJU4uHy3XupGJOShl7PSz1TPIUWM6FPzCGtI+Cyj3j77Xue2hVppP4xyXxwqZzdn/t3l9L5ck1A81Sg60bNNTK2uR/rGp1ybXZiyQu+6MML6OI2taVdyKekOI5dfT3YKh0PKSOkt1SalyLHYCHowQltF47twOKSMMNaNiBxaX5AOFSXybDnrsSeZdqOlnhrw+X13G1X36277ulKpgxk6rz9Vw54f00vPZaDJUIcpsYT6f4PcWrVuf0P26LszmvQofOFM1wtMsFs6ue++zJnhOFML8hbNdH/RaaQdZ3NyZzhv1Z8tyOEmNisFcAtrc988HUbfBQlESjn4XmvfxTW7R2s1CqdgZtMwifEeLl9DQbAuF810jc2Z4YzYS5RdGRNGasxEZavMLF5ecmrTty3c1OEstHvFvbdljX5CbXtQKqxSa2vzo+OG74yGnrU4tqV1agZl7z6J1l/TSrpLLdsAzx/6swlsoy2bwA7asg3soQ1bwA7a7DdgJ23aBPUF/QbsoI5dulpOREoBFldS8DemzbWlSttlMyx+Id2NMqgPTfhSyjLDdHRKWfTqkAp0kibPo6yTdb7ICVT0rGua2rKSDF1I/ehvL5PuyJ3h2EIOQErhJ/lsf9BzvaAUCN3q076wtlh89KF1pLScFi5vRvw6ckBFsy4HI9MO9okkJbm1J4c6pNqEYrdpwR63nDOwNBwOqWg8CETi98mp279fZY2XexZyYSqmdTn03B4YKdqA1uSgtVIoyhA2MY4csa4L2EFb+oE9tKEb2EEf+ohlOxllzTWj2iZaOonmtYC+gVMKsMxN/2pq7TfsOq3JRTKRbEbWfffvZ39AkUY67tiqqAyGMIbJM5eiQQww3HC3lUW/UPoyZRjzdmffEz7+WMEs10lqyUnXJhk6tYhSFiqTXu0hc/HngL5lPq3gudpUvZaDZD/20z6/OeVfI8wnLxgx6ubeo+bprm7McC5T856PZF2KZARYrDwYRvIBsNEu/yn02OrzPAYniPrX/WDDG9PMdbw8y70rP0uWShd+vyQHNlH/+iWrHhxU8P0rUxf98uIli+YXmmETfd0HsA3soaX+Nxq/j3qA+mI0HcAO6ukGuo+NsW64kPRsyHB9AGND5k5YpmhVQ2/B8z+0yWRsMv873yv5Gq/Uq/wkO5XBCLagtGTCJB7RezmoDHpOsdZRDs7+r3MnoXBwmX2ixvpSdE26NsnQSVvmbiuLjuvGdOcPHdFSrfZ/6La9Csiu3DNKWahMui2HYHaj9YXBMtE6D3HCv3Lvas6eN/qDyZGORaMxnpX80q9SDad3a4mH/tW7Fjk74rzxD3VUFcmTKCyOvxnBJnq+LjkGq319i/1MmpO2M+598+DiubCJuteiRcqrvpz5QtVXs7ZWL5jzVfUXD/Y0Uh+ix98nm5Djduuyp+8hJy76dPX7KHrRRK9Rt4i0hU1gB23oCLqPjbFuWJ8xcTuAIAM+v5cmG9/QgChvtMsSE5DzrdVvfldvcpPMRnMcVvcqflxpqdfruHdbHyiDAaC0cV10EqYwIVaumt0YtbfDA9dSrsm6sAYZlYHKond7CMZuZpStk4lraO0fvclPMpPsbWUIlEW3UESRHm1wMNsEo6RoEVm+e+fZtI6UUxbzUt3iAi2sJxWth4RDfVCI5mSVg8nvhx57bNZVR7ogdiw9HEbi92WZm8kRFXquuXdJOuyh7vWsCTv3Wj+yW6Pver1HS0XLJpG6xoge39zRwqWbWxjP9iS2jmVNPd8xWv+hN5ufmJL/tLdp9+xqr/+hyj3ONUZyFIb2z4ejQzVenn6pZPEgsbN0F9u+dilt6TgWxlFavvYFy1+98bLlz7TSZ/SKF3L0Wg+Mck1y1v5m5T8+po9e7RHJ+1aN+kd2GPvD04/QmreoH9q4HtmEXjo5lP8DpxTYi/U3nfE7pq5j6o12GWKKBpf1Tj2l8SNZSWaj2YEmRAUXN7JO1jPSKH6SncpgFHtUpm/4RxedhCmSV/qscJZ7YqRlomvQtVgXHFIke6AMBiCwtpTEnqJ9IdgbekrjR7KSzAGTKGUwgpNQZmJH/pNVuo9SoTJQWULP3T7sknpyRN3Sa/S1tA5YrI0HtBrpn7oi/v5Ws+0+ckaZLdanKotTJxhV91r/zSAXXprjiZPlV0PPmRrEG0ayhx6vY/Yk7mQGJVL3c6TbNLufZ4cei8SWfrCNevYgtsm+vZ73y5xZFxm53pQOviipfNAFeWWDLhimfFKVj4U+yrlE5W9WtbP4eKvX33CwY7RT0b0WTeo6WOOcjod8qfmvsIe61/lXY9m9dZydSx8926OzZ49o/L9wQI6oXabmlTWSuCff+dkrGOuqfx1yEPbyeVe81a3h20P5f3BKgb2gCe0Bn993nbL7lAqXf4qubaRJ9a4y5q2RO9vT+LXoQNwWkpVkNqItKK2XMDHdDTRIZj2nJNsfNOEmmDy5i2t9pTBZfFQ40/VCJCbr6Tfpt+karCsOKUrbp8hOZTCKPcozNjyobAqVT5bktb+rhze/SUaSlWQm2dvLoP92akb8Ooc58TS9l4PKQGXB6Esf40NyFk7LGHPS9Zlj7nl03HDDT7wf7gNcNB/SW9cPva3V5LuDnFMma+PkyYOvnW1EW+jpt+t7N842yc0/Bo9NTL7FaM824dJdtOqJybr9i70HzWIJ2i/12i3C4nCuDj3ut6epyMh9aN+NC+uzNi0qkASjZ5T7lM+jQogRyrkG5W+tasvXJ8VZGHqclpxajnZKvbZLksRefYZV8LVGsIcefzvW0OMyOOQoRN1Qv360cjE8MOayrz7mUNLAcqUzhOHAftl65mOXCs7I6+yM8KVcXLAb+39x3/uxrvPVt+ZfKTH/W1qWUWamq455YfTbRrdF0Uz3R0r7OFEPsirt4YKcGY7fGtUWhbNdNzAvO5Q3YMqYhT1YlV4870gngsipkVE95Brl+g+yNsdGF5+22Y2505yvGs4WtH5XMHUhZ3NyZzhv1ba8rheYYLcou7VKRRlphCip0HuzV/mQUcHUd/prY11jKzOLl8fiiygAgMMjHM+t0XiGDKQuydo5bKTrh/L0Mx+sgV3Uf47fkT99or+175nNJlGybtfop2Op79k+6EJat9HXd+NCzawDTOtIxTUk3VluizuRSXH/zDvlxrkMqMrzP/zj8R6ZtqN2lHs29yk77U9GrCNH0oeokZqMInLIIXVxQtbjlEkAdlDPJjTJ/m5T6Zu0b5Ltf8s/+dYitBrqQikua611FxrFHpHwzUSzjlCb1b118M2yVPLDx6NueKWr5YFTChyU0kEXZHj7nvKosntthC7xhmX793/ss/HjKmi7jXV35t8lvP6nNFnvLKY/DHtmdEys90UTCpmVQxYpbeQZGm8LvyzvVTzBSBE5+6NolusxIbN7D/G/lQkTmytxz7ycO1LKDul6z9ZmycJ2DfezqexQnFEskCLu8ZzpzvsMawtan0kweuvYpOj3gbw7nQ9rUc6CZ1x/Uuz3kLLrV4YMZ+rVeXPQMlIaRW9cT71FGxXMbBrGLC01eben7EKvDwCIxoM7nh0BAAAAfffn6MsB6oQ+6gmcUiCsFE9dNDZu27q/KHfLmLDYj7GvW/oN+78hcycsg3Z/TXvEFEWGxGlEpBaZmW6MhQipUChlm8OUSPdorkZFLHT7G8aOvjujKRbsERL1cjidxnrOxSJhYpuEn63ngu8UlubAG2fca08SXKRxExvK/WygEHyC0osOPczGTfPRQ2GxxaymK5jMA+2BFp1wezkxJXFl7vT4d4xbL9wDaKuXKDC9yQsAAAAAAAAAAESTQ/HXaM1HAqcUiAhlgy4a3tr3JIqamqx8+hzify9VPh9Yt//wRtbGf6+FNg8OOQJlc8LbSh3NULmuVdEaUrHqQCTHlNOctEBrEVMUIeXy1U+MFYdUkMOMmIqOTQweIbUv7WkVX1J2TZzxeT+MFZuY1Xe68Fk3marKHrnlnIGl4brWLw8tHS6ltQ6Wd1o3HP3AuLUHra+mpOcFE9coh35mYVOMmEZxX1bNbswxidYdhxoRGPX6+2xtlp9be5w4LQEpLwAAAAAAAAAAAIMBpxSIOGUDLzy6td/JY5hgA3l7JIlyN/UP2Iexre3HhcrBJuu2FV9nbVr4C7R2aCy+7Kf+fXs2fSB7fTlqXF+ymIu218RPHv/e8Vtj2Q6Uyq935eCnDztKJ+wNIJtT0WvD742esu9AtKdl+xuj9cO1gV+Y2F+1msYukhTOdJ3HBP+gfLA5bntmqD1Edf+Na7LCkY9+y9wlk3q66uZ39D0b48ftz0lOKeE4kz5oi3LjbmWEMzl3hvPTmKkXbeX3aDUCiSKkBJNteks1CAAAAAAAAAAAgK4BpxQABoEWs+//rftO4fXThHe00vm1cIvpT1tPdTyDRej/R+Es90Qmi38quykqiVDLJH5T7nTHgli3Rfu6Rm+wQ1zvKQKUKd3RtUZcr6irUJTOuty4ggYn26tjNnNX7m0juh1xRMyuOW+/a2uWLgsee+zye6m3XHl58Jicxr2qB9/F/fx+ZfjjIJvIFnliLEbjBNZCk22986Y7f9CSXAWzXCdJkqdC65FcAAAAAAAAAAAAOHy66muSoCoAtA05hYY9M/ofuxocWYJJs5RTLRG8XAtdg65F14RDam/IGSSsnsGUqiza16Zr0rXhkGqDnECKPk5Qwxb72OSEWHZIEeT8cTn8L+17fuTX1mPD8ftuzksOdLzmpaL7h9aYtrdFzgmHxyQ+cPsbhsZqejhy+lRmbPgvOdDphQa15SEZSBaSCQ4pAAAAAAAAAAAAEIiUAkBnfH3VyrSURM/9EheXhmu9KVo3Shb8/doG2yNj3hq5E1runMCb/4L/PdJrTdHaUTIXf9Za5IOWCERNMf5cW9q2qHRA6zkTt8e6MyqUOUs29ZEzMxb55Lhj6LhvOfPXdWt78WX4Ku+fm1sbnjnc9c92L34gQS4Z8pKt2Xyqx+77dpv/nGlc2C5IEr88muEr7hH83rb4gX8aMSXnkVi3g6/ZHJ/t272pV/mQUTITO9RKl0fpBCXGe6CeAAAAAAAAAAAAsQHS9wFgcOgN9KO+rBrlMyVeYLJIow513SlaL8rvlZeb/Q0fbz4jYzmiog4PSl1m8pruUFrSSe2pw8LRwrmVFu5Dv8X/bKxGfBxOfcioGDqByeJ+1r7GXQQoZBJ/pKr3+kWoL/sn4JzqnZbmE87C0PO/+dIXuKeFJH/GpZblhxo1Q2nphBw3isvS2cG6lmr9L0ttLe34zr5p/WKN535uftHPTVNp3yy1rJbKqyYMW5Ow22FJOlOYm5fn3Z6yK5zX++WhpcPjUquv9PLuReu7p/47WCcKnqtN5T77KLe3/ovDdUQCAAAAAAAAAABAf8ApBUCMQRFUPZ07hntNSb04E4FJea9f6klbi0muCTQMjBda/PWVNa4eaxERFV7yn6yKd1gSLmibNBdj2aGvdVTGGV9Gk/Zub+PHmMw9fMhRKPmkG5UbfjI78vW/ahlnH8hm+RU4CLvGyytar3U52euh545b5WHOBlNoJ75e6cU3KqOQTUxiFYIzF/PzusDfTKIbF8zJZNabCT5QGaUM2k8UXOFg9kk6Y3Jm8EQsO6XeXLln/G674z+h50zCP/f2Y+03037QUcSl5sJwpNEjh1SGo2ZNqO63e8+5R8j2XNnkL023LJpE53e2nv1CLKbtIyd5eUJir8zGhkotOLBp3TXaXnhpjgctFAAAAAAAAACASAGnFAAAqAhFdjC/7ShmYsNpcl1pahO54M5AA82FS2nCGmgynvnZWmbybMZ6K5EhkELMxMc1C3Gqzc/7K73H0ANHtHG38rf1HpPYauf8W9kvlqqV+kzPPL+mLmfvSClRffI3redzWTpHkvkYpS6ccuhRhdyt1JnvZUl8zWX+Xu4Mx5Zd/3km17YhfUGbY0oqr3KnjT/6gXFrY1LnP7n+5DNZHgo9R9FSmes2jSwbPHQWN/lOtlkqatzrrdNPKekZeFlBtnjWHG70VOmL7zza3c3vCz1XbD0rzfXDZt79hJ1Voef3+HOSY8nJTve/iHMvlWRzoiz5GniLY9xtI7p1OLQpmtA+sPrYeFvpj5N6XVURzmvTyykJJx5lC+1PZr249aJ4ufpftO/o5n/qistH3YtWSl1Ofb324j7C0//k5JZPp03svx4aUR9yJCMCGgAAAAAAgCMHTikAAABgPwSiRrz2JMHkQPQAZ5JHWJrrw53eLJahaKmWhJbfMy7XMlm+O3RSnqI2MnYMHCb5+SDO+DFM5hmKERKUPyW0f6WRCeUjiSrBxGrZJDZW9di0bn9RHuXHXWJpnnTLIPuHczZm/ne+N1b1/WtHYFukFJflMm5v6nBWCb9t2W3DksbRPkV3Os2Jecoo0KGMqNy0/lSzr3H7gRxI9H27OaEvrRPV1/bvKbZm6bL//VUqj//9pVlb5i6Z5HL53gv9f06n+bIBU8/5MGZssbbxZ252H9Ohc59j9W3DE46l/dnbPhsVb+rxTfBv3ctr8i485ZywRGCuuzP/Ln/fhsdp386TP9uQabuQJtlffuF7f+j3mjPSR8SKI4TamqpT3ZP9ski3+F0Lp2SP37jv33f/pvH/bFLpuHrR79mbe4+aF2mZrnit4u4Ef9NjweNzE5qciGD7Hx9WvtU7zbyr32k9pn8bjeuRM+pB94CFTGbjM7jvmU+uS5sBK6jL7AVbh35Wn3BTMm/6+K1r+2FNQgAAAAAAnQGnFAAAAABAjNCWNlE8poyQ0s2m5mWCfT6Rs7MWcJNnbOj3bh3c84ADqEAaUnNSz4DDlvOE9hFlIzlu3b76mqDDavfiBxLkkiEvtTmmpPIaZ9IMcjwVT1001juIfxH6m3a347hYiWCjCe6yEVm/co5mrSmzkINoTmX+GzZfwtUdf5Dqn7o+c8w94bhu37LqvRwb5Ayktb5qdra2hJ6PFacU6WTHMfH/kezNZwTP7esEfL4s/55ujv85iJoa+ZB9HVdHAkXFtZSYT4jL9v14yzkDAwvgnf/6zpm9fbvvCH5neIolZpyEL5UsHtQipB59VqUW7M8R982OWafWxTu+VHbjvLu9H12SedvFkZbpqje2jS2Wk78KHp/Vt6Lno+OGI701a3PaWoeuf572t0i2398+7JL6aFz3hNfqtiqdVLZgrOTH67v1hyX2blNKra3N0bxHqY6YW1K/tLH6JS9OyTwPVlCfJ+fU3Timxboub7rzB2gDAACAFumqr0mCqgAAAAAA9M3vTra+cecxcRnZxWvsFA0VmEBsNX+11+DQbzvoW+fkdKLUiJS2kiY7Ah9ln86FRlB1H/9QI63f1TRiY/qyrAH9gpFQQ+ZOWJbsaHgy+D3aj6WUiuR42lfHdBxMC2aT29Z3DOIW0q5wXfdA57v3kW8PHlvNze/HigOkKm/ngFCHFFGZ7bgj9NjB5dTQ456VPD5c1//wnWVnWbbu2kapE2lLx4E6wZs+Dn6Hy84lsWKP18q/fiI+QRSnJPq/Kf+NayNFRO37HZvkO1XZxNG+pbslLxpyZWW51pLzI1BXle0ptZ66WLAH6f+DDW9MW/j9kpwDThKcUDChRZJuoE8Pk/uaaMnGJVaMHv3XUARZctXmkmO2l9Y8t25+UrSuK8vWwDrFfh6XBSv8D3Lavj614J+v5ZVOjOZ1yTEpC/NLKyy+F2GFvaEUxmpct3Cm64Vvn2l6HxbQBrSEQsEs10nQBAD6AE4pAAAAAACDEBqBkLlpwz/klsS5lEIujtd9IZXVXBfOa6Wf+WDNvg6RPjdf8UdaR4o+tB9zA2tFx+SIovWkaBuqc1f9zoflZvuXPq9lh8fc+KZoqJkbruvy+Iy/BPcpfd/a5LRPaP+35586Z8vgxHRv/9R+10w5/YpYrhv7OgUTpPJneMuuH2m/viX+zXClUiTcTfJ1+zumdGTd+3vTXRbrGed1q74wFvROk7dxdvcfgsfd7E1ZjXLmnft+7+emYQuUTWA9tAx3wwvhloOi1Ka+vNF/0z93rqYJfjpHESdn960YaefyJbSNhXWlKCLNnOzeYu3T8qw4rrRw0S8vXrK/71VZ+q4O7vfsXvdTtOQ7K6viOrLHTRk7x8RC/SAH4eI1b74yv/z5fx3IFmpyqaN6dqul4b5ma9OdsWAPirKllwjoQ/sH+p51gdRL6XmvYyfU/CGa8lHUrcR9U2RT7YMY8f4PckhtXZtR8+HEddOifnEuvrVJ/DNYYW9eObbsrwerQ5HCz609JInlwQK/btvUujald4cFtDUu15I8SN8HAAAAAACAzvnloaXDabthUOKGWJhc74zQdInkCBSmstPCmZ7vYLzz7vLH3XWmjslKRzf/U1dcPureWLQDRXV0T3fuFYHU0uzYb+pKmjTZPnSbM9yp4igFmdPb+mXHc6TsjNlUZOT8sHS3/DZ43LzHVnR5r2tyD2S79G4t8ZN6XVURbjlofbWNzPE4Rag5uHz3t9el/CsW7fFu5bxCe52pI2Jth7P74AO1U+RMNdt9TcF0oOHm1NdrAykz73VuWxirfci8l756x2JpCayZ6fXGvXewlznmnV88Oi3eUz7+veO3YgQSGcjJ1NSj+WnBxKr0evPVB9M1Ra31G1T17Zi3RiIFa4RYfNlP/SuLU6+WrGL19QV9FnRmu7grmudgPBp5m3SlDVp9a/6Vx7ww+m1oTBuQY8joa8piTSkAAAAAAABAzPL61vwJnjjebfvGboujuQ4Lrc9WkrT+JcG8p3Bm+T67fuiU0BSYsQY5CJPimjrWUwv3+l2dAafU/zgUp1SkoMmY/3P3dwePY3n9qH+ve3uvyRiTtXHyhKNvnh9tOSiSsEqY26KhJLa46NpuMVc/yCne1LSjNfRc713HJ6rRdpMDcvWuhPe8wjZsmN835e5bur0Sa/agdqJuqWj83xnx+nVz825SSx5ay0tI3oEJffc8HynHsNbrx66He6xTGqyj6dic6RpzzSdD8tWSh8ZZdnNCX0ozHqtjq3l5297zeU2TzRb/B9cU9LsMo351IUds/x67T66pj38ALyt03Sllxq0DAAAAAAAAMBrX9R+9SI3rtk9iXgELtJH5nePG2hz2oaN1W7aUaPpoSnb4I28OxqWO2uWf1qUvEZLrHDpuivM+Fau24FLSHYy5KbURrRFU1su8e3q0ZZAkgTfn2/Em7fnIUp/c4SRsccZ/r4YclcJ8YccrvzIbH4u2oIiON15aUmq1yH3ouNUrlar1MsFn9Qk3pQrbMNpfZzK/pGxizimlpXZi1otbL2qRFTv4zcy7Je5s5dRxsWaPpJLetp3Me3Tw2F/jTFFLFnJIOUyJ65lgvYpmuZ7Kme68L9bs0Zay0jSZ9skxpejkd2q+/FT0rGuaKWnrFf7d/e/PmeFcFov2MDP/025XEuuZFAj2V/UZoOC52tTuloIxFQ3HLtH6S3FYUwoAAAAAAAAAQESgyV5yEE4efO3sSKSC68r1KTKK1vM6N6HJSWt7xaotSP+mT0/o79vjyOSLcgae1mP6t2rYg9aNogipYPq+WLVHTX2PG1pNvjuc7vjHeX1Krhr1g+jFfQs7DiS2OFbt0ZA28FxK20cf2ldLjkTRWhnct3BPTEaCUDshVThPp9R99Ml2yferJYtXdEthMQ5NbAvB/8YZ+4UicwZMtnyhliw2W/wgarZoX8js9Fi0B6WpJDvQPm3VdjzYHeIZq7ffSFP3rY/Eoj0aLzTVeWVvCe1v3dF9hdryWBIqF+1KSXjfkVkxS+u6Q/o+AAAAAAAAAAAAgBiEUnM97up3Ie3fbS/51OhrXWgdSl33vjt9WgO39jovvW5mLKaL01r9KN7d52GbL+7sk2TpxhOnJRRBK+pSONP1AhMsj3H2YO4M56fQiPr2iLNuvblF7v+73GnOV2O1nUhY6O+mhXXtCt8tfNlvabnesdv25xFTclRxFHbZ10Rf1OMHAAAAAAAAAAAAAAAAAAAAtKXwU/P6XfXtcDh4AAAAAAAAAAAAAAAAAAAAQKTBmlIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACIOnFIAAAAAAAAAAAAAAAAAAAAg4sApBQAAAAAAAAAAAAAAAAAAACLO/wswAA1Niv+YaMCdAAAAAElFTkSuQmCC") no-repeat;
-	background-size initial
-	background-position 0 0
-	transition background-position 1s steps(25)
-	transition-duration 0s
-
-	&.active
-		transition-duration 1s
-		background-position -2500px 0
-
-</style>
diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue
deleted file mode 100644
index 51c73003d1b544ccd9a7b7d6de85606d83a24864..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/poll-editor.vue
+++ /dev/null
@@ -1,235 +0,0 @@
-<template>
-<div class="zmdxowus">
-	<p class="caution" v-if="choices.length < 2">
-		<fa icon="exclamation-triangle"/>{{ $t('no-only-one-choice') }}
-	</p>
-	<ul ref="choices">
-		<li v-for="(choice, i) in choices">
-			<input :value="choice" @input="onInput(i, $event)" :placeholder="$t('choice-n').replace('{}', i + 1)">
-			<button @click="remove(i)" :title="$t('remove')">
-				<fa icon="times"/>
-			</button>
-		</li>
-	</ul>
-	<button class="add" v-if="choices.length < 10" @click="add">{{ $t('add') }}</button>
-	<button class="add" v-else disabled>{{ $t('no-more') }}</button>
-	<button class="destroy" @click="destroy" :title="$t('destroy')">
-		<fa icon="times"/>
-	</button>
-	<section>
-		<ui-switch v-model="multiple">{{ $t('multiple') }}</ui-switch>
-		<div>
-			<ui-select v-model="expiration">
-				<template #label>{{ $t('expiration') }}</template>
-				<option value="infinite">{{ $t('infinite') }}</option>
-				<option value="at">{{ $t('at') }}</option>
-				<option value="after">{{ $t('after') }}</option>
-			</ui-select>
-			<section v-if="expiration === 'at'">
-				<ui-input v-model="atDate" type="date">
-					<template #title>{{ $t('deadline-date') }}</template>
-				</ui-input>
-				<ui-input v-model="atTime" type="time">
-					<template #title>{{ $t('deadline-time') }}</template>
-				</ui-input>
-			</section>
-			<section v-if="expiration === 'after'">
-				<ui-input v-model="after" type="number">
-					<template #title>{{ $t('interval') }}</template>
-				</ui-input>
-				<ui-select v-model="unit">
-					<template #title>{{ $t('unit') }}</template>
-					<option value="second">{{ $t('second') }}</option>
-					<option value="minute">{{ $t('minute') }}</option>
-					<option value="hour">{{ $t('hour') }}</option>
-					<option value="day">{{ $t('day') }}</option>
-				</ui-select>
-			</section>
-		</div>
-	</section>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { erase } from '../../../../../prelude/array';
-import { addTimespan } from '../../../../../prelude/time';
-import { formatDateTimeString } from '../../../../../misc/format-time-string';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/poll-editor.vue'),
-	data() {
-		return {
-			choices: ['', ''],
-			multiple: false,
-			expiration: 'infinite',
-			atDate: formatDateTimeString(addTimespan(new Date(), 1, 'days'), 'yyyy-MM-dd'),
-			atTime: '00:00',
-			after: 0,
-			unit: 'second'
-		};
-	},
-	watch: {
-		choices() {
-			this.$emit('updated');
-		}
-	},
-	methods: {
-		onInput(i, e) {
-			Vue.set(this.choices, i, e.target.value);
-		},
-
-		add() {
-			this.choices.push('');
-			this.$nextTick(() => {
-				(this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
-			});
-		},
-
-		remove(i) {
-			this.choices = this.choices.filter((_, _i) => _i != i);
-		},
-
-		destroy() {
-			this.$emit('destroyed');
-		},
-
-		get() {
-			const at = () => {
-				return new Date(`${this.atDate} ${this.atTime}`).getTime();
-			};
-
-			const after = () => {
-				let base = parseInt(this.after);
-				switch (this.unit) {
-					case 'day': base *= 24;
-					case 'hour': base *= 60;
-					case 'minute': base *= 60;
-					case 'second': return base *= 1000;
-					default: return null;
-				}
-			};
-
-			return {
-				choices: erase('', this.choices),
-				multiple: this.multiple,
-				...(
-					this.expiration === 'at' ? { expiresAt: at() } :
-					this.expiration === 'after' ? { expiredAfter: after() } : {})
-			};
-		},
-
-		set(data) {
-			if (data.choices.length == 0) return;
-			this.choices = data.choices;
-			if (data.choices.length == 1) this.choices = this.choices.concat('');
-			this.multiple = data.multiple;
-			if (data.expiresAt) {
-				this.expiration = 'at';
-				this.atDate = this.atTime = data.expiresAt;
-			} else if (typeof data.expiredAfter === 'number') {
-				this.expiration = 'after';
-				this.after = data.expiredAfter;
-			} else {
-				this.expiration = 'infinite';
-			}
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.zmdxowus
-	padding 8px
-
-	> .caution
-		margin 0 0 8px 0
-		font-size 0.8em
-		color #f00
-
-		> [data-icon]
-			margin-right 4px
-
-	> ul
-		display block
-		margin 0
-		padding 0
-		list-style none
-
-		> li
-			display block
-			margin 8px 0
-			padding 0
-			width 100%
-
-			&:first-child
-				margin-top 0
-
-			&:last-child
-				margin-bottom 0
-
-			> input
-				padding 6px 8px
-				width 300px
-				font-size 14px
-				color var(--inputText)
-				background var(--pollEditorInputBg)
-				border solid 1px var(--primaryAlpha01)
-				border-radius 4px
-
-				&:hover
-					border-color var(--primaryAlpha02)
-
-				&:focus
-					border-color var(--primaryAlpha05)
-
-			> button
-				padding 4px 8px
-				color var(--primaryAlpha04)
-
-				&:hover
-					color var(--primaryAlpha06)
-
-				&:active
-					color var(--primaryDarken30)
-
-	> .add
-		margin 8px 0 0 0
-		vertical-align top
-		color var(--primary)
-		z-index 1
-
-	> .destroy
-		position absolute
-		top 0
-		right 0
-		padding 4px 8px
-		color var(--primaryAlpha04)
-
-		&:hover
-			color var(--primaryAlpha06)
-
-		&:active
-			color var(--primaryDarken30)
-
-	> section
-		margin 16px 0 -16px 0
-
-		> div
-			margin 0 8px
-
-			&:last-child
-				flex 1 0 auto
-
-				> section
-					align-items center
-					display flex
-					margin -32px 0 0
-
-					> :first-child
-						margin-right 16px
-
-					> .ui-input
-						flex 1 0 auto
-</style>
diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue
deleted file mode 100644
index bd5eeaf832d3340d8404cd938e7f527f1f717e44..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/poll.vue
+++ /dev/null
@@ -1,148 +0,0 @@
-<template>
-<div class="mk-poll" :data-done="closed || isVoted">
-	<ul>
-		<li v-for="(choice, i) in poll.choices" :key="i" @click="vote(i)" :class="{ voted: choice.voted }" :title="!closed && !isVoted ? $t('vote-to').replace('{}', choice.text) : ''">
-			<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
-			<span>
-				<template v-if="choice.isVoted"><fa icon="check"/></template>
-				<mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/>
-				<span class="votes" v-if="showResult">({{ $t('vote-count').replace('{}', choice.votes) }})</span>
-			</span>
-		</li>
-	</ul>
-	<p>
-		<span>{{ $t('total-votes').replace('{}', total) }}</span>
-		<span> · </span>
-		<a v-if="!closed && !isVoted" @click="toggleShowResult">{{ showResult ? $t('vote') : $t('show-result') }}</a>
-		<span v-if="isVoted">{{ $t('voted') }}</span>
-		<span v-else-if="closed">{{ $t('closed') }}</span>
-		<span v-if="remaining > 0"> · {{ timer }}</span>
-	</p>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { sum } from '../../../../../prelude/array';
-export default Vue.extend({
-	i18n: i18n('common/views/components/poll.vue'),
-	props: ['note'],
-	data() {
-		return {
-			remaining: -1,
-			showResult: false
-		};
-	},
-	computed: {
-		poll(): any {
-			return this.note.poll;
-		},
-		total(): number {
-			return sum(this.poll.choices.map(x => x.votes));
-		},
-		closed(): boolean {
-			return !this.remaining;
-		},
-		timer(): string {
-			return this.$t(
-				this.remaining > 86400 ? 'remaining-days' :
-				this.remaining > 3600 ? 'remaining-hours' :
-				this.remaining > 60 ? 'remaining-minutes' : 'remaining-seconds')
-				.replace('{s}', Math.floor(this.remaining % 60))
-				.replace('{m}', Math.floor(this.remaining / 60) % 60)
-				.replace('{h}', Math.floor(this.remaining / 3600) % 24)
-				.replace('{d}', Math.floor(this.remaining / 86400));
-		},
-		isVoted(): boolean {
-			return !this.poll.multiple && this.poll.choices.some(c => c.isVoted);
-		}
-	},
-	created() {
-		this.showResult = this.isVoted;
-
-		if (this.note.poll.expiresAt) {
-			const update = () => {
-				if (this.remaining = Math.floor(Math.max(new Date(this.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000))
-					requestAnimationFrame(update);
-				else
-					this.showResult = true;
-			};
-
-			update();
-		}
-	},
-	methods: {
-		toggleShowResult() {
-			this.showResult = !this.showResult;
-		},
-		vote(id) {
-			if (this.closed || !this.poll.multiple && this.poll.choices.some(c => c.isVoted)) return;
-			this.$root.api('notes/polls/vote', {
-				noteId: this.note.id,
-				choice: id
-			}).then(() => {
-				if (!this.showResult) this.showResult = !this.poll.multiple;
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-poll
-	> ul
-		display block
-		margin 0
-		padding 0
-		list-style none
-
-		> li
-			display block
-			margin 4px 0
-			padding 4px 8px
-			width 100%
-			color var(--pollChoiceText)
-			border solid 1px var(--pollChoiceBorder)
-			border-radius 4px
-			overflow hidden
-			cursor pointer
-
-			&:hover
-				background rgba(#000, 0.05)
-
-			&:active
-				background rgba(#000, 0.1)
-
-			> .backdrop
-				position absolute
-				top 0
-				left 0
-				height 100%
-				background var(--primary)
-				transition width 1s ease
-
-			> span
-				> [data-icon]
-					margin-right 4px
-
-				> .votes
-					margin-left 4px
-
-	> p
-		color var(--text)
-
-		a
-			color inherit
-
-	&[data-done]
-		> ul > li
-			cursor default
-
-			&:hover
-				background transparent
-
-			&:active
-				background transparent
-
-</style>
diff --git a/src/client/app/common/views/components/post-form-attaches.vue b/src/client/app/common/views/components/post-form-attaches.vue
deleted file mode 100644
index e051b6a808a578696670d716d816bd4fbbad42ab..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/post-form-attaches.vue
+++ /dev/null
@@ -1,139 +0,0 @@
-<template>
-<div class="skeikyzd" v-show="files.length != 0">
-	<x-draggable class="files" :list="files" animation="150">
-		<div v-for="file in files" :key="file.id" @click="showFileMenu(file, $event)" @contextmenu.prevent="showFileMenu(file, $event)">
-			<x-file-thumbnail :data-id="file.id" class="thumbnail" :file="file" fit="cover"/>
-			<img class="remove" @click.stop="detachMedia(file.id)" src="/assets/desktop/remove.png" :title="$t('attach-cancel')" alt=""/>
-			<div class="sensitive" v-if="file.isSensitive">
-				<fa class="icon" :icon="faExclamationTriangle"/>
-			</div>
-		</div>
-	</x-draggable>
-	<p class="remain">{{ 4 - files.length }}/4</p>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import * as XDraggable from 'vuedraggable';
-import XMenu from '../../../common/views/components/menu.vue';
-import { faTimesCircle, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
-import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
-import XFileThumbnail from './drive-file-thumbnail.vue'
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/post-form-attaches.vue'),
-
-	components: {
-		XDraggable,
-		XFileThumbnail
-	},
-
-	props: {
-		files: {
-			type: Array,
-			required: true
-		},
-		detachMediaFn: {
-			type: Function,
-			required: false
-		}
-	},
-
-	data() {
-		return {
-			faExclamationTriangle
-		};
-	},
-
-	methods: {
-		detachMedia(id) {
-			if (this.detachMediaFn) this.detachMediaFn(id)
-			else if (this.$parent.detachMedia) this.$parent.detachMedia(id)
-		},
-		toggleSensitive(file) {
-			this.$root.api('drive/files/update', {
-				fileId: file.id,
-				isSensitive: !file.isSensitive
-			}).then(() => {
-				file.isSensitive = !file.isSensitive;
-			});
-		},
-		showFileMenu(file, ev: MouseEvent) {
-			this.$root.new(XMenu, {
-				items: [{
-					type: 'item',
-					text: file.isSensitive ? this.$t('unmark-as-sensitive') : this.$t('mark-as-sensitive'),
-					icon: file.isSensitive ? faEyeSlash : faEye,
-					action: () => { this.toggleSensitive(file) }
-				}, {
-					type: 'item',
-					text: this.$t('attach-cancel'),
-					icon: faTimesCircle,
-					action: () => { this.detachMedia(file.id) }
-				}],
-				source: ev.currentTarget || ev.target
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.skeikyzd
-	padding 4px
-
-	> .files
-		display flex
-		flex-wrap wrap
-
-		> div
-			width 64px
-			height 64px
-			margin 4px
-			cursor move
-
-			&:hover > .remove
-				display block
-
-			> .thumbnail
-				width 100%
-				height 100%
-				z-index 1
-				color var(--text)
-
-			> .remove
-				display none
-				position absolute
-				top -6px
-				right -6px
-				width 16px
-				height 16px
-				cursor pointer
-				z-index 1000
-
-			> .sensitive
-				display flex
-				position absolute
-				width 64px
-				height 64px
-				top 0
-				left 0
-				z-index 2
-				background rgba(17, 17, 17, .7)
-				color #fff
-
-				> .icon
-					margin auto
-
-	> .remain
-		display block
-		position absolute
-		top 8px
-		right 8px
-		margin 0
-		padding 0
-		color var(--primaryAlpha04)
-
-</style>
diff --git a/src/client/app/common/views/components/reaction-icon.vue b/src/client/app/common/views/components/reaction-icon.vue
deleted file mode 100644
index afe51d783359944a3a37f5d2c1d2f70f379a2f19..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/reaction-icon.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<template>
-<mk-emoji :emoji="str.startsWith(':') ? null : str" :name="str.startsWith(':') ? str.substr(1, str.length - 2) : null" :is-reaction="true" :custom-emojis="customEmojis" :normal="true"/>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-export default Vue.extend({
-	i18n: i18n(),
-	props: {
-		reaction: {
-			type: String,
-			required: true
-		},
-	},
-	data() {
-		return {
-			customEmojis: []
-		};
-	},
-	created() {
-		this.$root.getMeta().then(meta => {
-			if (meta && meta.emojis) this.customEmojis = meta.emojis;
-		});
-	},
-	computed: {
-		str(): any {
-			switch (this.reaction) {
-				case 'like': return '👍';
-				case 'love': return '❤';
-				case 'laugh': return '😆';
-				case 'hmm': return '🤔';
-				case 'surprise': return '😮';
-				case 'congrats': return '🎉';
-				case 'angry': return '💢';
-				case 'confused': return '😥';
-				case 'rip': return '😇';
-				case 'pudding': return (this.$store.getters.isSignedIn && this.$store.state.settings.iLikeSushi) ? '🍣' : '🍮';
-				case 'star': return '⭐';
-				default: return this.reaction;
-			}
-		},
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-reaction-icon
-	img
-		vertical-align middle
-		width 1em
-		height 1em
-</style>
diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue
deleted file mode 100644
index f363fe9779b8d10bea499a183f54560f39b4f7e9..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/reaction-picker.vue
+++ /dev/null
@@ -1,323 +0,0 @@
-<template>
-<div class="rdfaahpb" v-hotkey.global="keymap">
-	<div class="backdrop" ref="backdrop" @click="close"></div>
-	<div class="popover" :class="{ isMobile: $root.isMobile }" ref="popover">
-		<p v-if="!$root.isMobile">{{ title }}</p>
-		<div class="buttons" ref="buttons" :class="{ showFocus }">
-			<button v-for="(reaction, i) in rs" :key="reaction" @click="react(reaction)" @mouseover="onMouseover" @mouseout="onMouseout" :tabindex="i + 1" :title="/^[a-z]+$/.test(reaction) ? $t('@.reactions.' + reaction) : reaction" v-particle><mk-reaction-icon :reaction="reaction"/></button>
-		</div>
-		<div v-if="enableEmojiReaction" class="text">
-			<input v-model="text" :placeholder="$t('input-reaction-placeholder')" @keyup.enter="reactText" @input="tryReactText" v-autocomplete="{ model: 'text' }">
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import anime from 'animejs';
-import { emojiRegex } from '../../../../../misc/emoji-regex';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/reaction-picker.vue'),
-	props: {
-		source: {
-			required: true
-		},
-
-		reactions: {
-			required: false
-		},
-
-		showFocus: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-
-		animation: {
-			type: Boolean,
-			required: false,
-			default: true
-		}
-	},
-
-	data() {
-		return {
-			rs: this.reactions || this.$store.state.settings.reactions,
-			title: this.$t('choose-reaction'),
-			text: null,
-			enableEmojiReaction: false,
-			focus: null
-		};
-	},
-
-	computed: {
-		keymap(): any {
-			return {
-				'esc': this.close,
-				'enter|space|plus': this.choose,
-				'up|k': this.focusUp,
-				'left|h|shift+tab': this.focusLeft,
-				'right|l|tab': this.focusRight,
-				'down|j': this.focusDown,
-				'1': () => this.react('like'),
-				'2': () => this.react('love'),
-				'3': () => this.react('laugh'),
-				'4': () => this.react('hmm'),
-				'5': () => this.react('surprise'),
-				'6': () => this.react('congrats'),
-				'7': () => this.react('angry'),
-				'8': () => this.react('confused'),
-				'9': () => this.react('rip'),
-				'0': () => this.react('pudding'),
-			};
-		}
-	},
-
-	watch: {
-		focus(i) {
-			this.$refs.buttons.children[i].focus();
-
-			if (this.showFocus) {
-				this.title = this.$refs.buttons.children[i].title;
-			}
-		}
-	},
-
-	mounted() {
-		this.$root.getMeta().then(meta => {
-			this.enableEmojiReaction = meta.enableEmojiReaction;
-		});
-
-		this.$nextTick(() => {
-			this.focus = 0;
-
-			const popover = this.$refs.popover as any;
-
-			const rect = this.source.getBoundingClientRect();
-			const width = popover.offsetWidth;
-			const height = popover.offsetHeight;
-
-			if (this.$root.isMobile) {
-				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-				const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
-				popover.style.left = (x - (width / 2)) + 'px';
-				popover.style.top = (y - (height / 2)) + 'px';
-			} else {
-				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-				const y = rect.top + window.pageYOffset + this.source.offsetHeight;
-				popover.style.left = (x - (width / 2)) + 'px';
-				popover.style.top = y + 'px';
-			}
-
-			anime({
-				targets: this.$refs.backdrop,
-				opacity: 1,
-				duration: this.animation ? 100 : 0,
-				easing: 'linear'
-			});
-
-			anime({
-				targets: this.$refs.popover,
-				opacity: 1,
-				scale: [0.5, 1],
-				duration: this.animation ? 500 : 0
-			});
-		});
-	},
-
-	methods: {
-		react(reaction) {
-			this.$emit('chosen', reaction);
-		},
-
-		reactText() {
-			if (!this.text) return;
-			this.react(this.text);
-		},
-
-		tryReactText() {
-			if (!this.text) return;
-			if (!this.text.match(emojiRegex)) return;
-			this.reactText();
-		},
-
-		onMouseover(e) {
-			this.title = e.target.title;
-		},
-
-		onMouseout(e) {
-			this.title = this.$t('choose-reaction');
-		},
-
-		close() {
-			(this.$refs.backdrop as any).style.pointerEvents = 'none';
-			anime({
-				targets: this.$refs.backdrop,
-				opacity: 0,
-				duration: this.animation ? 200 : 0,
-				easing: 'linear'
-			});
-
-			(this.$refs.popover as any).style.pointerEvents = 'none';
-			anime({
-				targets: this.$refs.popover,
-				opacity: 0,
-				scale: 0.5,
-				duration: this.animation ? 200 : 0,
-				easing: 'easeInBack',
-				complete: () => {
-					this.$emit('closed');
-					this.destroyDom();
-				}
-			});
-		},
-
-		focusUp() {
-			this.focus = this.focus == 0 ? 9 : this.focus < 5 ? (this.focus + 4) : (this.focus - 5);
-		},
-
-		focusDown() {
-			this.focus = this.focus == 9 ? 0 : this.focus >= 5 ? (this.focus - 4) : (this.focus + 5);
-		},
-
-		focusRight() {
-			this.focus = this.focus == 9 ? 0 : (this.focus + 1);
-		},
-
-		focusLeft() {
-			this.focus = this.focus == 0 ? 9 : (this.focus - 1);
-		},
-
-		choose() {
-			this.$refs.buttons.childNodes[this.focus].click();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.rdfaahpb
-	position initial
-
-	> .backdrop
-		position fixed
-		top 0
-		left 0
-		z-index 10000
-		width 100%
-		height 100%
-		background var(--modalBackdrop)
-		opacity 0
-
-	> .popover
-		$bgcolor = var(--popupBg)
-		position absolute
-		z-index 10001
-		background $bgcolor
-		border-radius 4px
-		box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
-		transform scale(0.5)
-		opacity 0
-
-		&.isMobile
-			> div
-				width 280px
-
-				> button
-					width 50px
-					height 50px
-					font-size 28px
-					border-radius 4px
-
-		&:not(.isMobile)
-			$arrow-size = 16px
-
-			margin-top $arrow-size
-			transform-origin center -($arrow-size)
-
-			&:before
-				content ""
-				display block
-				position absolute
-				top -($arrow-size * 2)
-				left s('calc(50% - %s)', $arrow-size)
-				border-top solid $arrow-size transparent
-				border-left solid $arrow-size transparent
-				border-right solid $arrow-size transparent
-				border-bottom solid $arrow-size $bgcolor
-
-		> p
-			display block
-			margin 0
-			padding 8px 10px
-			font-size 14px
-			color var(--popupFg)
-			border-bottom solid var(--lineWidth) var(--faceDivider)
-			line-height 20px
-
-		> .buttons
-			padding 4px 4px 8px 4px
-			width 216px
-			text-align center
-
-			&.showFocus
-				> button:focus
-					z-index 1
-
-					&:after
-						content ""
-						pointer-events none
-						position absolute
-						top 0
-						right 0
-						bottom 0
-						left 0
-						border 2px solid var(--primaryAlpha03)
-						border-radius 4px
-
-			> button
-				padding 0
-				width 40px
-				height 40px
-				font-size 24px
-				border-radius 2px
-
-				> *
-					height 1em
-
-				&:hover
-					background var(--reactionPickerButtonHoverBg)
-
-				&:active
-					background var(--primary)
-					box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15)
-
-		> .text
-			width 216px
-			padding 0 8px 8px 8px
-
-			> input
-				width 100%
-				padding 10px
-				margin 0
-				text-align center
-				font-size 16px
-				color var(--desktopPostFormTextareaFg)
-				background var(--desktopPostFormTextareaBg)
-				outline none
-				border solid 1px var(--primaryAlpha01)
-				border-radius 4px
-				transition border-color .2s ease
-
-				&:hover
-					border-color var(--primaryAlpha02)
-					transition border-color .1s ease
-
-				&:focus
-					border-color var(--primaryAlpha05)
-					transition border-color 0s ease
-
-</style>
diff --git a/src/client/app/common/views/components/reactions-viewer.details.vue b/src/client/app/common/views/components/reactions-viewer.details.vue
deleted file mode 100644
index 778b9368967dc031df1b348a091514eab3ad73d9..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/reactions-viewer.details.vue
+++ /dev/null
@@ -1,122 +0,0 @@
-<template>
-<transition name="zoom-in-top">
-	<div class="buebdbiu" ref="popover" v-if="show">
-		<i18n path="few-users" v-if="users.length <= 10">
-			<span slot="users">
-				<b v-for="u in users" :key="u.id" style="margin-right: 12px;">
-					<mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/>
-					<mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/>
-				</b>
-			</span>
-			<mk-reaction-icon slot="reaction" :reaction="reaction" ref="icon" />
-		</i18n>
-		<i18n path="many-users" v-if="10 < users.length">
-			<span slot="users">
-				<b v-for="u in users" :key="u.id" style="margin-right: 12px;">
-					<mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/>
-					<mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/>
-				</b>
-			</span>
-			<span slot="omitted">{{ count - 10 }}</span>
-			<mk-reaction-icon slot="reaction" :reaction="reaction" ref="icon" />
-		</i18n>
-	</div>
-</transition>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/reactions-viewer.details.vue'),
-	props: {
-		reaction: {
-			type: String,
-			required: true,
-		},
-		users: {
-			type: Array,
-			required: true,
-		},
-		count: {
-			type: Number,
-			required: true,
-		},
-		source: {
-			required: true,
-		}
-	},
-	data() {
-		return {
-			show: false
-		};
-	},
-	mounted() {
-		this.show = true;
-
-		this.$nextTick(() => {
-			const popover = this.$refs.popover as any;
-
-			if (this.source == null) {
-				this.destroyDom();
-				return;
-			}
-			const rect = this.source.getBoundingClientRect();
-
-			const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-			const y = rect.top + window.pageYOffset + this.source.offsetHeight;
-			popover.style.left = (x - 28) + 'px';
-			popover.style.top = (y + 16) + 'px';
-		});
-	}
-	methods: {
-		close() {
-			this.show = false;
-			setTimeout(this.destroyDom, 300);
-		}
-	}
-})
-</script>
-
-<style lang="stylus" scoped>
-.buebdbiu
-	$bgcolor = var(--popupBg)
-	z-index 10000
-	display block
-	position absolute
-	max-width 240px
-	font-size 0.8em
-	padding 6px 8px
-	background $bgcolor
-	text-align center
-	color var(--text)
-	border-radius 4px
-	box-shadow 0 var(--lineWidth) 4px rgba(#000, 0.25)
-	pointer-events none
-	transform-origin center -16px
-
-	&:before
-		content ""
-		pointer-events none
-		display block
-		position absolute
-		top -28px
-		left 12px
-		border-top solid 14px transparent
-		border-right solid 14px transparent
-		border-bottom solid 14px rgba(#000, 0.1)
-		border-left solid 14px transparent
-
-	&:after
-		content ""
-		pointer-events none
-		display block
-		position absolute
-		top -27px
-		left 12px
-		border-top solid 14px transparent
-		border-right solid 14px transparent
-		border-bottom solid 14px $bgcolor
-		border-left solid 14px transparent
-</style>
diff --git a/src/client/app/common/views/components/renote.vue b/src/client/app/common/views/components/renote.vue
deleted file mode 100644
index 58a0a26593e649067d6fda7836db7ac364d5e672..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/renote.vue
+++ /dev/null
@@ -1,104 +0,0 @@
-<template>
-<div class="puqkfets" :class="{ mini: narrow }">
-	<mk-avatar class="avatar" :user="note.user"/>
-	<fa icon="retweet"/>
-	<i18n path="@.renoted-by" tag="span">
-		<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId" place="user">
-			<mk-user-name :user="note.user"/>
-		</router-link>
-	</i18n>
-	<div class="info">
-		<span class="mobile" v-if="note.viaMobile"><fa icon="mobile-alt"/></span>
-		<mk-time :time="note.createdAt"/>
-		<span class="visibility" v-if="note.visibility != 'public'">
-			<fa v-if="note.visibility == 'home'" icon="home"/>
-			<fa v-if="note.visibility == 'followers'" icon="unlock"/>
-			<fa v-if="note.visibility == 'specified'" icon="envelope"/>
-		</span>
-		<span class="localOnly" v-if="note.localOnly == true"><fa icon="heart"/></span>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n(),
-	props: {
-		note: {
-			type: Object,
-			required: true
-		}
-	},
-	inject: {
-		narrow: {
-			default: false
-		}
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-.puqkfets
-	display flex
-	align-items center
-	padding 8px 16px
-	line-height 28px
-	white-space pre
-	color var(--renoteText)
-	background linear-gradient(to bottom, var(--renoteGradient) 0%, var(--face) 100%)
-
-	&:not(.mini)
-		padding 8px 16px
-
-		@media (min-width 500px)
-			padding 8px 16px
-
-		@media (min-width 600px)
-			padding 16px 32px 8px 32px
-
-	> .avatar
-		flex-shrink 0
-		display inline-block
-		width 28px
-		height 28px
-		margin 0 8px 0 0
-		border-radius 6px
-
-	> [data-icon]
-		margin-right 4px
-
-	> span
-		overflow hidden
-		flex-shrink 1
-		text-overflow ellipsis
-		white-space nowrap
-
-		> .name
-			font-weight bold
-
-	> .info
-		margin-left auto
-		font-size 0.9em
-
-		> .mobile
-			margin-right 8px
-
-		> .mk-time
-			flex-shrink 0
-
-		> .visibility
-			margin-left 8px
-
-			[data-icon]
-				margin-right 0
-
-		> .localOnly
-			margin-left 4px
-
-			[data-icon]
-				margin-right 0
-
-</style>
diff --git a/src/client/app/common/views/components/settings/2fa.vue b/src/client/app/common/views/components/settings/2fa.vue
deleted file mode 100644
index 813a91b5c02890a1b9470729e5bf76f45d6ac5e4..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/settings/2fa.vue
+++ /dev/null
@@ -1,259 +0,0 @@
-<template>
-<div class="2fa totp-section">
-	<p style="margin-top:0;">{{ $t('intro') }}<a :href="$t('url')" target="_blank">{{ $t('detail') }}</a></p>
-	<ui-info warn>{{ $t('caution') }}</ui-info>
-	<p v-if="!data && !$store.state.i.twoFactorEnabled"><ui-button @click="register">{{ $t('register') }}</ui-button></p>
-	<template v-if="$store.state.i.twoFactorEnabled">
-		<h2 class="heading">{{ $t('totp-header') }}</h2>
-		<p>{{ $t('already-registered') }}</p>
-		<ui-button @click="unregister">{{ $t('unregister') }}</ui-button>
-
-		<template v-if="supportsCredentials">
-			<hr class="totp-method-sep">
-
-			<h2 class="heading">{{ $t('security-key-header') }}</h2>
-			<p>{{ $t('security-key') }}</p>
-			<div class="key-list">
-				<div class="key" v-for="key in $store.state.i.securityKeysList">
-					<h3>
-						{{ key.name }}
-					</h3>
-					<div class="last-used">
-						{{ $t('last-used') }}
-						<mk-time :time="key.lastUsed"/>
-					</div>
-					<ui-button @click="unregisterKey(key)">
-						{{ $t('unregister') }}
-					</ui-button>
-				</div>
-			</div>
-
-			<ui-switch v-model="usePasswordLessLogin" @change="updatePasswordLessLogin" v-if="$store.state.i.securityKeysList.length > 0">
-				{{ $t('use-password-less-login') }}
-			</ui-switch>
-
-			<ui-info warn v-if="registration && registration.error">{{ $t('something-went-wrong') }} {{ registration.error }}</ui-info>
-			<ui-button v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('register') }}</ui-button>
-
-			<ol v-if="registration && !registration.error">
-				<li v-if="registration.stage >= 0">
-					{{ $t('activate-key') }}
-					<fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 0" />
-				</li>
-				<li v-if="registration.stage >= 1">
-					<ui-form :disabled="registration.stage != 1 || registration.saving">
-						<ui-input v-model="keyName" :max="30">
-							<span>{{ $t('security-key-name') }}</span>
-						</ui-input>
-						<ui-button @click="registerKey" :disabled="this.keyName.length == 0">
-							{{ $t('register-security-key') }}
-						</ui-button>
-						<fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 1" />
-					</ui-form>
-				</li>
-			</ol>
-		</template>
-	</template>
-	<div v-if="data && !$store.state.i.twoFactorEnabled">
-		<ol>
-			<li>{{ $t('authenticator') }}<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank">{{ $t('howtoinstall') }}</a></li>
-			<li>{{ $t('scan') }}<br><img :src="data.qr"></li>
-			<li>{{ $t('done') }}<br>
-				<ui-input v-model="token">{{ $t('token') }}</ui-input>
-				<ui-button primary @click="submit">{{ $t('submit') }}</ui-button>
-			</li>
-		</ol>
-		<ui-info>{{ $t('info') }}</ui-info>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import { hostname } from '../../../../config';
-import { hexifyAB } from '../../../scripts/2fa';
-
-function stringifyAB(buffer) {
-	return String.fromCharCode.apply(null, new Uint8Array(buffer));
-}
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/settings.2fa.vue'),
-	data() {
-		return {
-			data: null,
-			supportsCredentials: !!navigator.credentials,
-			usePasswordLessLogin: this.$store.state.i.usePasswordLessLogin,
-			registration: null,
-			keyName: '',
-			token: null
-		};
-	},
-	methods: {
-		register() {
-			this.$root.dialog({
-				title: this.$t('enter-password'),
-				input: {
-					type: 'password'
-				}
-			}).then(({ canceled, result: password }) => {
-				if (canceled) return;
-				this.$root.api('i/2fa/register', {
-					password: password
-				}).then(data => {
-					this.data = data;
-				});
-			});
-		},
-
-		unregister() {
-			this.$root.dialog({
-				title: this.$t('enter-password'),
-				input: {
-					type: 'password'
-				}
-			}).then(({ canceled, result: password }) => {
-				if (canceled) return;
-				this.$root.api('i/2fa/unregister', {
-					password: password
-				}).then(() => {
-					this.usePasswordLessLogin = false;
-					this.updatePasswordLessLogin();
-				}).then(() => {
-					this.$notify(this.$t('unregistered'));
-					this.$store.state.i.twoFactorEnabled = false;
-				});
-			});
-		},
-
-		submit() {
-			this.$root.api('i/2fa/done', {
-				token: this.token
-			}).then(() => {
-				this.$notify(this.$t('success'));
-				this.$store.state.i.twoFactorEnabled = true;
-			}).catch(() => {
-				this.$notify(this.$t('failed'));
-			});
-		},
-
-		registerKey() {
-			this.registration.saving = true;
-			this.$root.api('i/2fa/key-done', {
-				password: this.registration.password,
-				name: this.keyName,
-				challengeId: this.registration.challengeId,
-				// we convert each 16 bits to a string to serialise
-				clientDataJSON: stringifyAB(this.registration.credential.response.clientDataJSON),
-				attestationObject: hexifyAB(this.registration.credential.response.attestationObject)
-			}).then(key => {
-				this.registration = null;
-				key.lastUsed = new Date();
-				this.$notify(this.$t('success'));
-			})
-		},
-
-		unregisterKey(key) {
-			this.$root.dialog({
-				title: this.$t('enter-password'),
-				input: {
-					type: 'password'
-				}
-			}).then(({ canceled, result: password }) => {
-				if (canceled) return;
-				return this.$root.api('i/2fa/remove-key', {
-					password,
-					credentialId: key.id
-				}).then(() => {
-					this.usePasswordLessLogin = false;
-					this.updatePasswordLessLogin();
-				}).then(() => {
-					this.$notify(this.$t('key-unregistered'));
-				});
-			});
-		},
-
-		addSecurityKey() {
-			this.$root.dialog({
-				title: this.$t('enter-password'),
-				input: {
-					type: 'password'
-				}
-			}).then(({ canceled, result: password }) => {
-				if (canceled) return;
-				this.$root.api('i/2fa/register-key', {
-					password
-				}).then(registration => {
-					this.registration = {
-						password,
-						challengeId: registration.challengeId,
-						stage: 0,
-						publicKeyOptions: {
-							challenge: Buffer.from(
-								registration.challenge
-									.replace(/\-/g, "+")
-									.replace(/_/g, "/"),
-								'base64'
-							),
-							rp: {
-								id: hostname,
-								name: 'Misskey'
-							},
-							user: {
-								id: Uint8Array.from(this.$store.state.i.id, c => c.charCodeAt(0)),
-								name: this.$store.state.i.username,
-								displayName: this.$store.state.i.name,
-							},
-							pubKeyCredParams: [{alg: -7, type: 'public-key'}],
-							timeout: 60000,
-							attestation: 'direct'
-						},
-						saving: true
-					};
-					return navigator.credentials.create({
-						publicKey: this.registration.publicKeyOptions
-					});
-				}).then(credential => {
-					this.registration.credential = credential;
-					this.registration.saving = false;
-					this.registration.stage = 1;
-				}).catch(err => {
-					console.warn('Error while registering?', err);
-					this.registration.error = err.message;
-					this.registration.stage = -1;
-				});
-			});
-		},
-		updatePasswordLessLogin() {
-			this.$root.api('i/2fa/password-less', {
-				value: !!this.usePasswordLessLogin
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.totp-section
-	.totp-method-sep
-		margin 1.5em 0 1em
-		border none
-		border-top solid var(--lineWidth) var(--faceDivider)
-
-	h2.heading
-		margin 0
-
-	.key
-		padding 1em
-		margin 0.5em 0
-		background #161616
-		border-radius 6px
-
-		h3
-			margin-top 0
-			margin-bottom .3em
-
-		.last-used
-			margin-bottom .5em
-</style>
diff --git a/src/client/app/common/views/components/settings/api.vue b/src/client/app/common/views/components/settings/api.vue
deleted file mode 100644
index 184fa069fb3c68989307ebef0ec252163b810e8a..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/settings/api.vue
+++ /dev/null
@@ -1,102 +0,0 @@
-<template>
-<ui-card>
-	<template #title><fa icon="key"/> API</template>
-
-	<section class="fit-top">
-		<ui-input :value="$store.state.i.token" readonly>
-			<span>{{ $t('token') }}</span>
-		</ui-input>
-		<p>{{ $t('intro') }}</p>
-		<ui-info warn>{{ $t('caution') }}</ui-info>
-		<p>{{ $t('regeneration-of-token') }}</p>
-		<ui-button @click="regenerateToken"><fa icon="sync-alt"/> {{ $t('regenerate-token') }}</ui-button>
-	</section>
-
-	<section>
-		<header><fa icon="terminal"/> {{ $t('console.title') }}</header>
-		<ui-input v-model="endpoint" :datalist="endpoints" @change="onEndpointChange()">
-			<span>{{ $t('console.endpoint') }}</span>
-		</ui-input>
-		<ui-textarea v-model="body">
-			<span>{{ $t('console.parameter') }} (JSON or JSON5)</span>
-			<template #desc>{{ $t('console.credential-info') }}</template>
-		</ui-textarea>
-		<ui-button @click="send" :disabled="sending">
-			<template v-if="sending">{{ $t('console.sending') }}</template>
-			<template v-else><fa icon="paper-plane"/> {{ $t('console.send') }}</template>
-		</ui-button>
-		<ui-textarea v-if="res" v-model="res" readonly tall>
-			<span>{{ $t('console.response') }}</span>
-		</ui-textarea>
-	</section>
-</ui-card>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import * as JSON5 from 'json5';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/api-settings.vue'),
-
-	data() {
-		return {
-			endpoint: '',
-			body: '{}',
-			res: null,
-			sending: false,
-			endpoints: []
-		};
-	},
-
-	created() {
-		this.$root.api('endpoints').then(endpoints => {
-			this.endpoints = endpoints;
-		});
-	},
-
-	methods: {
-		regenerateToken() {
-			this.$root.dialog({
-				title: this.$t('enter-password'),
-				input: {
-					type: 'password'
-				}
-			}).then(({ canceled, result: password }) => {
-				if (canceled) return;
-				this.$root.api('i/regenerate_token', {
-					password: password
-				});
-			});
-		},
-
-		send() {
-			this.sending = true;
-			this.$root.api(this.endpoint, JSON5.parse(this.body)).then(res => {
-				this.sending = false;
-				this.res = JSON5.stringify(res, null, 2);
-			}, err => {
-				this.sending = false;
-				this.res = JSON5.stringify(err, null, 2);
-			});
-		},
-
-		onEndpointChange() {
-			this.$root.api('endpoint', { endpoint: this.endpoint }).then(endpoint => {
-				const body = {};
-				for (const p of endpoint.params) {
-					body[p.name] =
-						p.type === 'String' ? '' :
-						p.type === 'Number' ? 0 :
-						p.type === 'Boolean' ? false :
-						p.type === 'Array' ? [] :
-						p.type === 'Object' ? {} :
-						null;
-				}
-				this.body = JSON5.stringify(body, null, 2);
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/app/common/views/components/settings/app-type.vue b/src/client/app/common/views/components/settings/app-type.vue
deleted file mode 100644
index d163f1e746d7d6cf0fa53873a5c7ebcbc5ed72c4..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/settings/app-type.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<template>
-<ui-card>
-	<template #title><fa :icon="faMobileAlt"/> {{ $t('title') }}</template>
-
-	<section class="fit-top">
-		<p>{{ $t('intro') }}</p>
-		<ui-select v-model="appTypeForce" :placeholder="$t('intro')">
-			<option v-for="x in ['auto', 'desktop', 'mobile']" :value="x" :key="x">{{ $t(`choices.${x}`) }}</option>
-		</ui-select>
-		<ui-info warn>{{ $t('info') }}</ui-info>
-	</section>
-</ui-card>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import { faMobileAlt } from '@fortawesome/free-solid-svg-icons'
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/settings/app-type.vue'),
-
-	data() {
-		return {
-			faMobileAlt
-		};
-	},
-
-	computed: {
-		appTypeForce: {
-			get() { return this.$store.state.device.appTypeForce; },
-			set(value) {
-				this.$store.commit('device/set', { key: 'appTypeForce', value });
-				this.reload();
-			}
-		},
-	},
-
-	methods: {
-		reload() {
-			this.$root.dialog({
-				type: 'warning',
-				text: this.$t('@.reload-to-apply-the-setting'),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (!canceled) {
-					location.reload();
-				}
-			});
-		},
-	}
-});
-</script>
diff --git a/src/client/app/common/views/components/settings/apps.vue b/src/client/app/common/views/components/settings/apps.vue
deleted file mode 100644
index c5beaa1fe217ad60823dff780ad273e9e5eac76b..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/settings/apps.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-<template>
-<div class="root">
-	<ui-info v-if="!fetching && apps.length == 0">{{ $t('no-apps') }}</ui-info>
-	<div class="apps" v-if="apps.length != 0">
-		<div v-for="app in apps">
-			<p><b>{{ app.name }}</b></p>
-			<p>{{ app.description }}</p>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/settings.apps.vue'),
-	data() {
-		return {
-			fetching: true,
-			apps: []
-		};
-	},
-	mounted() {
-		this.$root.api('i/authorized_apps').then(apps => {
-			this.apps = apps;
-			this.fetching = false;
-		});
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.root
-	> .apps
-		> div
-			padding 16px 0 0 0
-			border-bottom solid 1px #eee
-</style>
diff --git a/src/client/app/common/views/components/settings/drive.vue b/src/client/app/common/views/components/settings/drive.vue
deleted file mode 100644
index da028e85efe12838b0832f6964c1d2a8d63c62a1..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/settings/drive.vue
+++ /dev/null
@@ -1,209 +0,0 @@
-<template>
-<ui-card>
-	<template #title><fa icon="cloud"/> {{ $t('@.drive') }}</template>
-
-	<section v-if="!fetching" class="juakhbxthdewydyreaphkepoxgxvfogn">
-		<div class="meter"><div :style="meterStyle"></div></div>
-		<p>{{ $t('max') }}: <b>{{ capacity | bytes }}</b> {{ $t('in-use') }}: <b>{{ usage | bytes }}</b></p>
-	</section>
-
-	<section>
-		<header>{{ $t('stats') }}</header>
-		<div ref="chart" style="margin-bottom: -16px; margin-left: -8px; color: #000;"></div>
-	</section>
-
-	<section>
-		<header>{{ $t('default-upload-folder') }}</header>
-		<ui-input v-model="uploadFolderName" readonly>{{ $t('default-upload-folder-name') }}</ui-input>
-		<ui-button @click="chooseUploadFolder()">{{ $t('change-default-upload-folder') }}</ui-button>
-	</section>
-</ui-card>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import * as tinycolor from 'tinycolor2';
-import ApexCharts from 'apexcharts';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/drive-settings.vue'),
-	data() {
-		return {
-			fetching: true,
-			usage: null,
-			capacity: null,
-			uploadFolderName: null
-		};
-	},
-
-	computed: {
-		meterStyle(): any {
-			return {
-				width: `${this.usage / this.capacity * 100}%`,
-				background: tinycolor({
-					h: 180 - (this.usage / this.capacity * 180),
-					s: 0.7,
-					l: 0.5
-				})
-			};
-		},
-
-		uploadFolder: {
-			get() { return this.$store.state.settings.uploadFolder; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'uploadFolder', value }); }
-		},
-	},
-
-	mounted() {
-		if (this.uploadFolder == null) {
-			this.uploadFolderName = this.$t('@._settings.root');
-		} else {
-			this.$root.api('drive/folders/show', {
-				folderId: this.uploadFolder
-			}).then(folder => {
-				this.uploadFolderName = folder.name;
-			});
-		}
-	
-		this.$root.api('drive').then(info => {
-			this.capacity = info.capacity;
-			this.usage = info.usage;
-			this.fetching = false;
-
-			this.$nextTick(() => {
-				this.renderChart();
-			});
-		});
-	},
-
-	methods: {
-		renderChart() {
-			this.$root.api('charts/user/drive', {
-				userId: this.$store.state.i.id,
-				span: 'day',
-				limit: 21
-			}).then(stats => {
-				const addition = [];
-				const deletion = [];
-
-				const now = new Date();
-				const y = now.getFullYear();
-				const m = now.getMonth();
-				const d = now.getDate();
-
-				for (let i = 0; i < 21; i++) {
-					const x = new Date(y, m, d - i);
-					addition.push([
-						x,
-						stats.incSize[i]
-					]);
-					deletion.push([
-						x,
-						-stats.decSize[i]
-					]);
-				}
-
-				const chart = new ApexCharts(this.$refs.chart, {
-					chart: {
-						type: 'bar',
-						stacked: true,
-						height: 150,
-						zoom: {
-							enabled: false
-						},
-						toolbar: {
-							show: false
-						}
-					},
-					plotOptions: {
-						bar: {
-							columnWidth: '80%'
-						}
-					},
-					grid: {
-						clipMarkers: false,
-						borderColor: 'rgba(0, 0, 0, 0.1)',
-						xaxis: {
-							lines: {
-								show: true,
-							}
-						},
-					},
-					tooltip: {
-						shared: true,
-						intersect: false
-					},
-					dataLabels: {
-						enabled: false
-					},
-					legend: {
-						show: false
-					},
-					series: [{
-						name: 'Additions',
-						data: addition
-					}, {
-						name: 'Deletions',
-						data: deletion
-					}],
-					xaxis: {
-						type: 'datetime',
-						labels: {
-							style: {
-								colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
-							}
-						},
-						axisBorder: {
-							color: 'rgba(0, 0, 0, 0.1)'
-						},
-						axisTicks: {
-							color: 'rgba(0, 0, 0, 0.1)'
-						},
-						crosshairs: {
-							width: 1,
-							opacity: 1
-						}
-					},
-					yaxis: {
-						labels: {
-							formatter: v => Vue.filter('bytes')(v, 0),
-							style: {
-								color: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
-							}
-						}
-					}
-				});
-
-				chart.render();
-			});
-		},
-
-		chooseUploadFolder() {
-			this.$chooseDriveFolder().then(folder => {
-				this.uploadFolder = folder ? folder.id : null;
-				this.uploadFolderName = folder ? folder.name : this.$t('@._settings.root');
-			})
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.juakhbxthdewydyreaphkepoxgxvfogn
-	> .meter
-		$size = 12px
-
-		margin-bottom 16px
-		background rgba(0, 0, 0, 0.1)
-		border-radius ($size / 2)
-		overflow hidden
-
-		> div
-			height $size
-			border-radius ($size / 2)
-
-	> p
-		margin 0
-
-</style>
diff --git a/src/client/app/common/views/components/settings/integration.vue b/src/client/app/common/views/components/settings/integration.vue
deleted file mode 100644
index 71ad8b4509e5b7e00040ea6ce305db7e98e5ee35..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/settings/integration.vue
+++ /dev/null
@@ -1,118 +0,0 @@
-<template>
-<ui-card v-if="enableTwitterIntegration || enableDiscordIntegration || enableGithubIntegration">
-	<template #title><fa icon="share-alt"/> {{ $t('title') }}</template>
-
-	<section v-if="enableTwitterIntegration">
-		<header><fa :icon="['fab', 'twitter']"/> Twitter</header>
-		<p v-if="$store.state.i.twitter">{{ $t('connected-to') }}: <a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
-		<ui-button v-if="$store.state.i.twitter" @click="disconnectTwitter">{{ $t('disconnect') }}</ui-button>
-		<ui-button v-else @click="connectTwitter">{{ $t('connect') }}</ui-button>
-	</section>
-
-	<section v-if="enableDiscordIntegration">
-		<header><fa :icon="['fab', 'discord']"/> Discord</header>
-		<p v-if="$store.state.i.discord">{{ $t('connected-to') }}: <a :href="`https://discordapp.com/users/${$store.state.i.discord.id}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.discord.username }}#{{ $store.state.i.discord.discriminator }}</a></p>
-		<ui-button v-if="$store.state.i.discord" @click="disconnectDiscord">{{ $t('disconnect') }}</ui-button>
-		<ui-button v-else @click="connectDiscord">{{ $t('connect') }}</ui-button>
-	</section>
-
-	<section v-if="enableGithubIntegration">
-		<header><fa :icon="['fab', 'github']"/> GitHub</header>
-		<p v-if="$store.state.i.github">{{ $t('connected-to') }}: <a :href="`https://github.com/${$store.state.i.github.login}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.github.login }}</a></p>
-		<ui-button v-if="$store.state.i.github" @click="disconnectGithub">{{ $t('disconnect') }}</ui-button>
-		<ui-button v-else @click="connectGithub">{{ $t('connect') }}</ui-button>
-	</section>
-</ui-card>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import { apiUrl } from '../../../../config';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/integration-settings.vue'),
-
-	data() {
-		return {
-			apiUrl,
-			twitterForm: null,
-			discordForm: null,
-			githubForm: null,
-			enableTwitterIntegration: false,
-			enableDiscordIntegration: false,
-			enableGithubIntegration: false,
-		};
-	},
-
-	created() {
-		this.$root.getMeta().then(meta => {
-			this.enableTwitterIntegration = meta.enableTwitterIntegration;
-			this.enableDiscordIntegration = meta.enableDiscordIntegration;
-			this.enableGithubIntegration = meta.enableGithubIntegration;
-		});
-	},
-
-	mounted() {
-		if (!document.cookie.match(/i=(\w+)/)) {
-			document.cookie = `i=${this.$store.state.i.token}; path=/;` +
-			` domain=${document.location.hostname}; max-age=31536000;` +
-			(document.location.protocol.startsWith('https') ? ' secure' : '');
-		}
-		this.$watch('$store.state.i', () => {
-			if (this.$store.state.i.twitter) {
-				if (this.twitterForm) this.twitterForm.close();
-			}
-			if (this.$store.state.i.discord) {
-				if (this.discordForm) this.discordForm.close();
-			}
-			if (this.$store.state.i.github) {
-				if (this.githubForm) this.githubForm.close();
-			}
-		}, {
-			deep: true
-		});
-	},
-
-	methods: {
-		connectTwitter() {
-			this.twitterForm = window.open(apiUrl + '/connect/twitter',
-				'twitter_connect_window',
-				'height=570, width=520');
-		},
-
-		disconnectTwitter() {
-			window.open(apiUrl + '/disconnect/twitter',
-				'twitter_disconnect_window',
-				'height=570, width=520');
-		},
-
-		connectDiscord() {
-			this.discordForm = window.open(apiUrl + '/connect/discord',
-				'discord_connect_window',
-				'height=570, width=520');
-		},
-
-		disconnectDiscord() {
-			window.open(apiUrl + '/disconnect/discord',
-				'discord_disconnect_window',
-				'height=570, width=520');
-		},
-
-		connectGithub() {
-			this.githubForm = window.open(apiUrl + '/connect/github',
-				'github_connect_window',
-				'height=570, width=520');
-		},
-
-		disconnectGithub() {
-			window.open(apiUrl + '/disconnect/github',
-				'github_disconnect_window',
-				'height=570, width=520');
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-</style>
diff --git a/src/client/app/common/views/components/settings/language.vue b/src/client/app/common/views/components/settings/language.vue
deleted file mode 100644
index f81775f09bcb9c16480b778e54ae197100f56344..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/settings/language.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<template>
-<ui-card>
-	<template #title><fa icon="language"/> {{ $t('title') }}</template>
-
-	<section class="fit-top">
-		<ui-select v-model="lang" :placeholder="$t('pick-language')">
-			<optgroup :label="$t('recommended')">
-				<option value="">{{ $t('auto') }}</option>
-			</optgroup>
-
-			<optgroup :label="$t('specify-language')">
-				<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
-			</optgroup>
-		</ui-select>
-		<ui-info>Current: <i>{{ currentLanguage }}</i></ui-info>
-		<ui-info warn>{{ $t('info') }}</ui-info>
-	</section>
-</ui-card>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import { langs } from '../../../../config';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/language-settings.vue'),
-
-	data() {
-		return {
-			langs,
-			currentLanguage: 'Unknown',
-		};
-	},
-
-	computed: {
-		lang: {
-			get() { return this.$store.state.device.lang; },
-			set(value) { this.$store.commit('device/set', { key: 'lang', value }); }
-		},
-	},
-
-	created() {
-		try {
-			const locale = JSON.parse(localStorage.getItem('locale') || "{}");
-			const localeKey = localStorage.getItem('localeKey');
-			this.currentLanguage = `${locale.meta.lang} (${localeKey})`;
-		} catch { }
-	},
-
-	methods: {
-	}
-});
-</script>
diff --git a/src/client/app/common/views/components/settings/mute-and-block.user.vue b/src/client/app/common/views/components/settings/mute-and-block.user.vue
deleted file mode 100644
index 29ef1f7a6753fd3af9ee8616ff797746c456d9b3..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/settings/mute-and-block.user.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-<template>
-<div class="muteblockuser">
-	<div class="avatar-link">
-		<a :href="user | userPage(null, true)">
-			<mk-avatar class="avatar" :user="user" :disable-link="true"/>
-		</a>
-	</div>
-	<div class="text">
-		<div><mk-user-name :user="user"/></div>
-		<div class="username">@{{ user | acct }}</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/mute-and-block.user.vue'),
-	props: ['user'],
-});
-</script>
-
-<style lang="stylus" scoped>
-.muteblockuser
-	display flex
-	padding 16px
-
-	> .avatar-link
-		> a
-			> .avatar
-				width 40px
-				height 40px
-
-	> .text
-		color var(--text)
-		margin-left 16px
-</style>
diff --git a/src/client/app/common/views/components/settings/mute-and-block.vue b/src/client/app/common/views/components/settings/mute-and-block.vue
deleted file mode 100644
index 8ff58041688dfe94e27edb0a3c2d0c3a42997732..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/settings/mute-and-block.vue
+++ /dev/null
@@ -1,181 +0,0 @@
-<template>
-<ui-card>
-	<template #title><fa icon="ban"/> {{ $t('mute-and-block') }}</template>
-
-	<section>
-		<header>{{ $t('mute') }}</header>
-		<ui-info v-if="!muteFetching && mute.length == 0">{{ $t('no-muted-users') }}</ui-info>
-		<div class="users" v-if="mute.length != 0">
-			<div class="user" v-for="user in mute" :key="user.id">
-				<x-user :user="user"/>
-				<span @click="unmute(user)">
-					<fa icon="times"/>
-				</span>
-			</div>
-			<ui-button v-if="this.muteCursor != null" @click="updateMute()">{{ $t('@.load-more') }}</ui-button>
-		</div>
-	</section>
-
-	<section>
-		<header>{{ $t('block') }}</header>
-		<ui-info v-if="!blockFetching && block.length == 0">{{ $t('no-blocked-users') }}</ui-info>
-		<div class="users" v-if="block.length != 0">
-			<div class="user" v-for="user in block" :key="user.id">
-				<x-user :user="user"/>
-				<span @click="unblock(user)">
-					<fa icon="times"/>
-				</span>
-			</div>
-			<ui-button v-if="this.blockCursor != null" @click="updateBlock()">{{ $t('@.load-more') }}</ui-button>
-		</div>
-	</section>
-
-	<section>
-		<header>{{ $t('word-mute') }}</header>
-		<ui-textarea v-model="mutedWords">
-			{{ $t('muted-words') }}<template #desc>{{ $t('muted-words-description') }}</template>
-		</ui-textarea>
-		<ui-button @click="save">{{ $t('save') }}</ui-button>
-	</section>
-</ui-card>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import XUser from './mute-and-block.user.vue';
-
-const fetchLimit = 30;
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/mute-and-block.vue'),
-
-	components: {
-		XUser
-	},
-
-	data() {
-		return {
-			muteFetching: true,
-			blockFetching: true,
-			mute: [],
-			block: [],
-			muteCursor: undefined,
-			blockCursor: undefined,
-			mutedWords: ''
-		};
-	},
-
-	computed: {
-		_mutedWords: {
-			get() { return this.$store.state.settings.mutedWords; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'mutedWords', value }); }
-		},
-	},
-
-	mounted() {
-		this.mutedWords = this._mutedWords.map(words => words.join(' ')).join('\n');
-
-		this.updateMute();
-		this.updateBlock();
-	},
-
-	methods: {
-		save() {
-			this._mutedWords = this.mutedWords.split('\n').map(line => line.split(' ').filter(x => x != ''));
-		},
-
-		unmute(user) {
-			this.$root.dialog({
-				type: 'warning',
-				text: this.$t('unmute-confirm'),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-				this.$root.api('mute/delete', {
-					userId: user.id
-				}).then(() => {
-					this.muteCursor = undefined;
-					this.updateMute();
-				});
-			});
-		},
-
-		unblock(user) {
-			this.$root.dialog({
-				type: 'warning',
-				text: this.$t('unblock-confirm'),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-				this.$root.api('blocking/delete', {
-					userId: user.id
-				}).then(() => {
-					this.updateBlock();
-				});
-			});
-		},
-
-		updateMute() {
-			this.muteFetching = true;
-			this.$root.api('mute/list', {
-				limit: fetchLimit + 1,
-				untilId: this.muteCursor,
-			}).then((items: Object[]) => {
-				const past = this.muteCursor ? this.mute : [];
-
-				if (items.length === fetchLimit + 1) {
-					items.pop()
-					this.muteCursor = items[items.length - 1].id;
-				} else {
-					this.muteCursor = undefined;
-				}
-
-				this.mute = past.concat(items.map(x => x.mutee));
-				this.muteFetching = false;
-			});
-		},
-
-		updateBlock() {
-			this.blockFetching = true;
-			this.$root.api('blocking/list', {
-				limit: fetchLimit + 1,
-				untilId: this.blockCursor,
-			}).then((items: Object[]) => {
-				const past = this.blockCursor ? this.block : [];
-
-				if (items.length === fetchLimit + 1) {
-					items.pop()
-					this.blockCursor = items[items.length - 1].id;
-				} else {
-					this.blockCursor = undefined;
-				}
-
-				this.block = past.concat(items.map(x => x.blockee));
-				this.blockFetching = false;
-			});
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-	.users
-		> .user
-			display flex
-			align-items center
-			justify-content flex-end
-			border-radius 6px
-
-			&:hover
-				background-color var(--primary)
-
-			> span
-				margin-left auto
-				cursor pointer
-				padding 16px
-		
-		> button
-			margin-top 16px
-</style>
-
diff --git a/src/client/app/common/views/components/settings/notification.vue b/src/client/app/common/views/components/settings/notification.vue
deleted file mode 100644
index 2554fe633137dcd589f264cdbf08059f049424ea..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/settings/notification.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<template>
-<ui-card>
-	<template #title><fa :icon="['far', 'bell']"/> {{ $t('title') }}</template>
-	<section>
-		<ui-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch">
-			{{ $t('auto-watch') }}<template #desc>{{ $t('auto-watch-desc') }}</template>
-		</ui-switch>
-		<section>
-			<ui-button @click="readAllNotifications">{{ $t('mark-as-read-all-notifications') }}</ui-button>
-			<ui-button @click="readAllUnreadNotes">{{ $t('mark-as-read-all-unread-notes') }}</ui-button>
-			<ui-button @click="readAllMessagingMessages">{{ $t('mark-as-read-all-talk-messages') }}</ui-button>
-		</section>
-	</section>
-</ui-card>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/notification-settings.vue'),
-
-	methods: {
-		onChangeAutoWatch(v) {
-			this.$root.api('i/update', {
-				autoWatch: v
-			});
-		},
-
-		readAllUnreadNotes() {
-			this.$root.api('i/read_all_unread_notes');
-		},
-
-		readAllMessagingMessages() {
-			this.$root.api('i/read_all_messaging_messages');
-		},
-
-		readAllNotifications() {
-			this.$root.api('notifications/mark_all_as_read');
-		}
-	}
-});
-</script>
diff --git a/src/client/app/common/views/components/settings/password.vue b/src/client/app/common/views/components/settings/password.vue
deleted file mode 100644
index c867561518db6867c191c5fb7b380a712dbac262..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/settings/password.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<template>
-<div>
-	<ui-button @click="reset">{{ $t('reset') }}</ui-button>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/password-settings.vue'),
-	methods: {
-		async reset() {
-			const { canceled: canceled1, result: currentPassword } = await this.$root.dialog({
-				title: this.$t('enter-current-password'),
-				input: {
-					type: 'password'
-				}
-			});
-			if (canceled1) return;
-
-			const { canceled: canceled2, result: newPassword } = await this.$root.dialog({
-				title: this.$t('enter-new-password'),
-				input: {
-					type: 'password'
-				}
-			});
-			if (canceled2) return;
-
-			const { canceled: canceled3, result: newPassword2 } = await this.$root.dialog({
-				title: this.$t('enter-new-password-again'),
-				input: {
-					type: 'password'
-				}
-			});
-			if (canceled3) return;
-
-			if (newPassword !== newPassword2) {
-				this.$root.dialog({
-					title: null,
-					text: this.$t('not-match')
-				});
-				return;
-			}
-			this.$root.api('i/change_password', {
-				currentPassword,
-				newPassword
-			}).then(() => {
-				this.$root.dialog({
-					type: 'success',
-					text: this.$t('changed')
-				});
-			}).catch(() => {
-				this.$root.dialog({
-					type: 'error',
-					text: this.$t('failed')
-				});
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/app/common/views/components/settings/profile.vue b/src/client/app/common/views/components/settings/profile.vue
deleted file mode 100644
index 0c291f90296d7dea76974dc8d476160286528a59..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/settings/profile.vue
+++ /dev/null
@@ -1,442 +0,0 @@
-<template>
-<ui-card>
-	<template #title><fa icon="user"/> {{ $t('title') }}</template>
-
-	<section class="esokaraujimuwfttfzgocmutcihewscl">
-		<div class="header" :style="bannerStyle">
-			<mk-avatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true"/>
-		</div>
-
-		<ui-form :disabled="saving">
-			<ui-input v-model="name" :max="30">
-				<span>{{ $t('name') }}</span>
-			</ui-input>
-
-			<ui-input v-model="username" readonly>
-				<span>{{ $t('account') }}</span>
-				<template #prefix>@</template>
-				<template #suffix>@{{ host }}</template>
-			</ui-input>
-
-			<ui-input v-model="location">
-				<span>{{ $t('location') }}</span>
-				<template #prefix><fa icon="map-marker-alt"/></template>
-			</ui-input>
-
-			<ui-input v-model="birthday" type="date">
-				<template #title>{{ $t('birthday') }}</template>
-				<template #prefix><fa icon="birthday-cake"/></template>
-			</ui-input>
-
-			<ui-textarea v-model="description" :max="500">
-				<span>{{ $t('description') }}</span>
-				<template #desc>{{ $t('you-can-include-hashtags') }}</template>
-			</ui-textarea>
-
-			<ui-select v-model="lang">
-				<template #label>{{ $t('language') }}</template>
-				<template #icon><fa icon="language"/></template>
-				<option v-for="lang in unique(Object.values(langmap).map(x => x.nativeName)).map(name => Object.keys(langmap).find(k => langmap[k].nativeName == name))" :value="lang" :key="lang">{{ langmap[lang].nativeName }}</option>
-			</ui-select>
-
-			<ui-input type="file" @change="onAvatarChange">
-				<span>{{ $t('avatar') }}</span>
-				<template #icon><fa icon="image"/></template>
-				<template #desc v-if="avatarUploading">{{ $t('uploading') }}<mk-ellipsis/></template>
-			</ui-input>
-
-			<ui-input type="file" @change="onBannerChange">
-				<span>{{ $t('banner') }}</span>
-				<template #icon><fa icon="image"/></template>
-				<template #desc v-if="bannerUploading">{{ $t('uploading') }}<mk-ellipsis/></template>
-			</ui-input>
-
-			<div class="fields">
-				<header>{{ $t('profile-metadata') }}</header>
-				<ui-horizon-group>
-					<ui-input v-model="fieldName0">{{ $t('metadata-label') }}</ui-input>
-					<ui-input v-model="fieldValue0">{{ $t('metadata-content') }}</ui-input>
-				</ui-horizon-group>
-				<ui-horizon-group>
-					<ui-input v-model="fieldName1">{{ $t('metadata-label') }}</ui-input>
-					<ui-input v-model="fieldValue1">{{ $t('metadata-content') }}</ui-input>
-				</ui-horizon-group>
-				<ui-horizon-group>
-					<ui-input v-model="fieldName2">{{ $t('metadata-label') }}</ui-input>
-					<ui-input v-model="fieldValue2">{{ $t('metadata-content') }}</ui-input>
-				</ui-horizon-group>
-				<ui-horizon-group>
-					<ui-input v-model="fieldName3">{{ $t('metadata-label') }}</ui-input>
-					<ui-input v-model="fieldValue3">{{ $t('metadata-content') }}</ui-input>
-				</ui-horizon-group>
-			</div>
-
-			<ui-button @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
-		</ui-form>
-	</section>
-
-	<section>
-		<header><fa :icon="faCogs"/> {{ $t('advanced') }}</header>
-
-		<div>
-			<ui-switch v-model="isCat" @change="save(false)">{{ $t('is-cat') }}</ui-switch>
-			<ui-switch v-model="isBot" @change="save(false)">{{ $t('is-bot') }}</ui-switch>
-			<ui-switch v-model="alwaysMarkNsfw">{{ $t('@._settings.always-mark-nsfw') }}</ui-switch>
-		</div>
-	</section>
-
-	<section>
-		<header><fa :icon="faUnlockAlt"/> {{ $t('privacy') }}</header>
-
-		<div>
-			<ui-switch v-model="isLocked" @change="save(false)">{{ $t('is-locked') }}</ui-switch>
-			<ui-switch v-model="carefulBot" :disabled="isLocked" @change="save(false)">{{ $t('careful-bot') }}</ui-switch>
-			<ui-switch v-model="autoAcceptFollowed" :disabled="!isLocked && !carefulBot" @change="save(false)">{{ $t('auto-accept-followed') }}</ui-switch>
-		</div>
-	</section>
-
-	<section v-if="enableEmail">
-		<header><fa :icon="faEnvelope"/> {{ $t('email') }}</header>
-
-		<div>
-			<template v-if="$store.state.i.email != null">
-				<ui-info v-if="$store.state.i.emailVerified">{{ $t('email-verified') }}</ui-info>
-				<ui-info v-else warn>{{ $t('email-not-verified') }}</ui-info>
-			</template>
-			<ui-input v-model="email" type="email"><span>{{ $t('email-address') }}</span></ui-input>
-			<ui-button @click="updateEmail()" :disabled="email === $store.state.i.email"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
-		</div>
-	</section>
-
-	<section>
-		<header><fa :icon="faBoxes"/> {{ $t('export-and-import') }}</header>
-
-		<div>
-			<ui-select v-model="exportTarget">
-				<option value="notes">{{ $t('export-targets.all-notes') }}</option>
-				<option value="following">{{ $t('export-targets.following-list') }}</option>
-				<option value="mute">{{ $t('export-targets.mute-list') }}</option>
-				<option value="blocking">{{ $t('export-targets.blocking-list') }}</option>
-				<option value="user-lists">{{ $t('export-targets.user-lists') }}</option>
-			</ui-select>
-			<ui-horizon-group class="fit-bottom">
-				<ui-button @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</ui-button>
-				<ui-button @click="doImport()" :disabled="!['following', 'user-lists'].includes(exportTarget)"><fa :icon="faUpload"/> {{ $t('import') }}</ui-button>
-			</ui-horizon-group>
-		</div>
-	</section>
-
-	<section>
-		<details>
-			<summary>{{ $t('danger-zone') }}</summary>
-			<ui-button @click="deleteAccount()">{{ $t('delete-account') }}</ui-button>
-		</details>
-	</section>
-</ui-card>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import { apiUrl, host } from '../../../../config';
-import { toUnicode } from 'punycode';
-import langmap from 'langmap';
-import { unique } from '../../../../../../prelude/array';
-import { faDownload, faUpload, faUnlockAlt, faBoxes, faCogs } from '@fortawesome/free-solid-svg-icons';
-import { faSave, faEnvelope } from '@fortawesome/free-regular-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/profile-editor.vue'),
-
-	data() {
-		return {
-			unique,
-			langmap,
-			host: toUnicode(host),
-			enableEmail: false,
-			email: null,
-			name: null,
-			username: null,
-			location: null,
-			description: null,
-			fieldName0: null,
-			fieldValue0: null,
-			fieldName1: null,
-			fieldValue1: null,
-			fieldName2: null,
-			fieldValue2: null,
-			fieldName3: null,
-			fieldValue3: null,
-			lang: null,
-			birthday: null,
-			avatarId: null,
-			bannerId: null,
-			isCat: false,
-			isBot: false,
-			isLocked: false,
-			carefulBot: false,
-			autoAcceptFollowed: false,
-			saving: false,
-			avatarUploading: false,
-			bannerUploading: false,
-			exportTarget: 'notes',
-			faDownload, faUpload, faSave, faEnvelope, faUnlockAlt, faBoxes, faCogs
-		};
-	},
-
-	computed: {
-		alwaysMarkNsfw: {
-			get() { return this.$store.state.i.alwaysMarkNsfw; },
-			set(value) { this.$root.api('i/update', { alwaysMarkNsfw: value }); }
-		},
-
-		bannerStyle(): any {
-			if (this.$store.state.i.bannerUrl == null) return {};
-			return {
-				backgroundColor: this.$store.state.i.bannerColor,
-				backgroundImage: `url(${ this.$store.state.i.bannerUrl })`
-			};
-		},
-	},
-
-	created() {
-		this.$root.getMeta().then(meta => {
-			this.enableEmail = meta.enableEmail;
-		});
-		this.email = this.$store.state.i.email;
-		this.name = this.$store.state.i.name;
-		this.username = this.$store.state.i.username;
-		this.location = this.$store.state.i.location;
-		this.description = this.$store.state.i.description;
-		this.lang = this.$store.state.i.lang;
-		this.birthday = this.$store.state.i.birthday;
-		this.avatarId = this.$store.state.i.avatarId;
-		this.bannerId = this.$store.state.i.bannerId;
-		this.isCat = this.$store.state.i.isCat;
-		this.isBot = this.$store.state.i.isBot;
-		this.isLocked = this.$store.state.i.isLocked;
-		this.carefulBot = this.$store.state.i.carefulBot;
-		this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed;
-
-		this.fieldName0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].name : null;
-		this.fieldValue0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].value : null;
-		this.fieldName1 = this.$store.state.i.fields[1] ? this.$store.state.i.fields[1].name : null;
-		this.fieldValue1 = this.$store.state.i.fields[1] ? this.$store.state.i.fields[1].value : null;
-		this.fieldName2 = this.$store.state.i.fields[2] ? this.$store.state.i.fields[2].name : null;
-		this.fieldValue2 = this.$store.state.i.fields[2] ? this.$store.state.i.fields[2].value : null;
-		this.fieldName3 = this.$store.state.i.fields[3] ? this.$store.state.i.fields[3].name : null;
-		this.fieldValue3 = this.$store.state.i.fields[3] ? this.$store.state.i.fields[3].value : null;
-	},
-
-	methods: {
-		onAvatarChange([file]) {
-			this.avatarUploading = true;
-
-			const data = new FormData();
-			data.append('file', file);
-			data.append('i', this.$store.state.i.token);
-
-			fetch(apiUrl + '/drive/files/create', {
-				method: 'POST',
-				body: data
-			})
-				.then(response => response.json())
-				.then(f => {
-					this.avatarId = f.id;
-					this.avatarUploading = false;
-				})
-				.catch(e => {
-					this.avatarUploading = false;
-					alert('%18n:@upload-failed%');
-				});
-		},
-
-		onBannerChange([file]) {
-			this.bannerUploading = true;
-
-			const data = new FormData();
-			data.append('file', file);
-			data.append('i', this.$store.state.i.token);
-
-			fetch(apiUrl + '/drive/files/create', {
-				method: 'POST',
-				body: data
-			})
-				.then(response => response.json())
-				.then(f => {
-					this.bannerId = f.id;
-					this.bannerUploading = false;
-				})
-				.catch(e => {
-					this.bannerUploading = false;
-					alert('%18n:@upload-failed%');
-				});
-		},
-
-		save(notify) {
-			const fields = [
-				{ name: this.fieldName0, value: this.fieldValue0 },
-				{ name: this.fieldName1, value: this.fieldValue1 },
-				{ name: this.fieldName2, value: this.fieldValue2 },
-				{ name: this.fieldName3, value: this.fieldValue3 },
-			];
-
-			this.saving = true;
-
-			this.$root.api('i/update', {
-				name: this.name || null,
-				location: this.location || null,
-				description: this.description || null,
-				lang: this.lang,
-				birthday: this.birthday || null,
-				avatarId: this.avatarId || undefined,
-				bannerId: this.bannerId || undefined,
-				fields,
-				isCat: !!this.isCat,
-				isBot: !!this.isBot,
-				isLocked: !!this.isLocked,
-				carefulBot: !!this.carefulBot,
-				autoAcceptFollowed: !!this.autoAcceptFollowed
-			}).then(i => {
-				this.saving = false;
-				this.$store.state.i.avatarId = i.avatarId;
-				this.$store.state.i.avatarUrl = i.avatarUrl;
-				this.$store.state.i.bannerId = i.bannerId;
-				this.$store.state.i.bannerUrl = i.bannerUrl;
-
-				if (notify) {
-					this.$root.dialog({
-						type: 'success',
-						text: this.$t('saved')
-					});
-				}
-			}).catch(err => {
-				this.saving = false;
-				switch(err.id) {
-					case 'f419f9f8-2f4d-46b1-9fb4-49d3a2fd7191':
-						this.$root.dialog({
-							type: 'error',
-							title: this.$t('unable-to-process'),
-							text: this.$t('avatar-not-an-image')
-						});
-						break;
-					case '75aedb19-2afd-4e6d-87fc-67941256fa60':
-						this.$root.dialog({
-							type: 'error',
-							title: this.$t('unable-to-process'),
-							text: this.$t('banner-not-an-image')
-						});
-						break;
-					default:
-						this.$root.dialog({
-							type: 'error',
-							text: this.$t('unable-to-process')
-						});
-				}
-			});
-		},
-
-		updateEmail() {
-			this.$root.dialog({
-				title: this.$t('@.enter-password'),
-				input: {
-					type: 'password'
-				}
-			}).then(({ canceled, result: password }) => {
-				if (canceled) return;
-				this.$root.api('i/update_email', {
-					password: password,
-					email: this.email == '' ? null : this.email
-				});
-			});
-		},
-
-		doExport() {
-			this.$root.api(
-				this.exportTarget == 'notes' ? 'i/export-notes' :
-				this.exportTarget == 'following' ? 'i/export-following' :
-				this.exportTarget == 'mute' ? 'i/export-mute' :
-				this.exportTarget == 'blocking' ? 'i/export-blocking' :
-				this.exportTarget == 'user-lists' ? 'i/export-user-lists' :
-				null, {}).then(() => {
-					this.$root.dialog({
-						type: 'info',
-						text: this.$t('export-requested')
-					});
-				}).catch((e: any) => {
-					this.$root.dialog({
-						type: 'error',
-						text: e.message
-					});
-				});
-		},
-
-		doImport() {
-			this.$chooseDriveFile().then(file => {
-				this.$root.api(
-					this.exportTarget == 'following' ? 'i/import-following' :
-					this.exportTarget == 'user-lists' ? 'i/import-user-lists' :
-					null, {
-						fileId: file.id
-				}).then(() => {
-					this.$root.dialog({
-						type: 'info',
-						text: this.$t('import-requested')
-					});
-				}).catch((e: any) => {
-					this.$root.dialog({
-						type: 'error',
-						text: e.message
-					});
-				});
-			});
-		},
-
-		async deleteAccount() {
-			const { canceled: canceled, result: password } = await this.$root.dialog({
-				title: this.$t('enter-password'),
-				input: {
-					type: 'password'
-				}
-			});
-			if (canceled) return;
-
-			this.$root.api('i/delete-account', {
-				password
-			}).then(() => {
-				this.$root.dialog({
-					type: 'success',
-					text: this.$t('account-deleted')
-				});
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.esokaraujimuwfttfzgocmutcihewscl
-	> .header
-		height 150px
-		overflow hidden
-		background-size cover
-		background-position center
-		border-radius 4px
-
-		> .avatar
-			position absolute
-			top 0
-			bottom 0
-			left 0
-			right 0
-			display block
-			width 72px
-			height 72px
-			margin auto
-
-.fields
-	> header
-		padding 8px 0px
-		font-weight bold
-
-</style>
diff --git a/src/client/app/common/views/components/settings/settings.vue b/src/client/app/common/views/components/settings/settings.vue
deleted file mode 100644
index 3a0ba561afc85badf93a50e2417cf950744e94be..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/settings/settings.vue
+++ /dev/null
@@ -1,671 +0,0 @@
-<template>
-<div class="nqfhvmnl">
-	<template v-if="page == null || page == 'profile'">
-		<x-profile/>
-		<x-integration/>
-	</template>
-
-	<template v-if="page == null || page == 'appearance'">
-		<x-theme/>
-
-		<ui-card>
-			<template #title><fa icon="desktop"/> {{ $t('@._settings.appearance') }}</template>
-
-			<section v-if="!$root.isMobile">
-				<ui-switch v-model="showPostFormOnTopOfTl">{{ $t('@._settings.post-form-on-timeline') }}</ui-switch>
-				<ui-button @click="customizeHome">{{ $t('@.customize-home') }}</ui-button>
-			</section>
-			<section v-if="!$root.isMobile">
-				<header>{{ $t('@._settings.wallpaper') }}</header>
-				<ui-horizon-group class="fit-bottom">
-					<ui-button @click="updateWallpaper">{{ $t('@._settings.choose-wallpaper') }}</ui-button>
-					<ui-button @click="deleteWallpaper">{{ $t('@._settings.delete-wallpaper') }}</ui-button>
-				</ui-horizon-group>
-			</section>
-			<section v-if="!$root.isMobile">
-				<header>{{ $t('@._settings.navbar-position') }}</header>
-				<ui-radio v-model="navbar" value="top">{{ $t('@._settings.navbar-position-top') }}</ui-radio>
-				<ui-radio v-model="navbar" value="left">{{ $t('@._settings.navbar-position-left') }}</ui-radio>
-				<ui-radio v-model="navbar" value="right">{{ $t('@._settings.navbar-position-right') }}</ui-radio>
-			</section>
-			<section>
-				<ui-switch v-model="useShadow">{{ $t('@._settings.use-shadow') }}</ui-switch>
-				<ui-switch v-model="roundedCorners">{{ $t('@._settings.rounded-corners') }}</ui-switch>
-				<ui-switch v-model="circleIcons">{{ $t('@._settings.circle-icons') }}</ui-switch>
-				<ui-switch v-model="reduceMotion">{{ $t('@._settings.reduce-motion') }}</ui-switch>
-				<ui-switch v-model="contrastedAcct">{{ $t('@._settings.contrasted-acct') }}</ui-switch>
-				<ui-switch v-model="showFullAcct">{{ $t('@._settings.show-full-acct') }}</ui-switch>
-				<ui-switch v-model="showVia">{{ $t('@._settings.show-via') }}</ui-switch>
-				<ui-switch v-model="useOsDefaultEmojis">{{ $t('@._settings.use-os-default-emojis') }}</ui-switch>
-				<ui-switch v-model="iLikeSushi">{{ $t('@._settings.i-like-sushi') }}</ui-switch>
-			</section>
-			<section>
-				<ui-switch v-model="suggestRecentHashtags">{{ $t('@._settings.suggest-recent-hashtags') }}</ui-switch>
-				<ui-switch v-model="showClockOnHeader" v-if="!$root.isMobile">{{ $t('@._settings.show-clock-on-header') }}</ui-switch>
-				<ui-switch v-model="alwaysShowNsfw">{{ $t('@._settings.always-show-nsfw') }}</ui-switch>
-				<ui-switch v-model="showReplyTarget">{{ $t('@._settings.show-reply-target') }}</ui-switch>
-				<ui-switch v-model="disableAnimatedMfm">{{ $t('@._settings.disable-animated-mfm') }}</ui-switch>
-				<ui-switch v-model="disableShowingAnimatedImages">{{ $t('@._settings.disable-showing-animated-images') }}</ui-switch>
-				<ui-switch v-model="remainDeletedNote">{{ $t('@._settings.remain-deleted-note') }}</ui-switch>
-				<ui-switch v-model="enableMobileQuickNotificationView">{{ $t('@._settings.enable-quick-notification-view') }}</ui-switch>
-			</section>
-			<section>
-				<header>{{ $t('@._settings.line-width') }}</header>
-				<ui-radio v-model="lineWidth" :value="0.5">{{ $t('@._settings.line-width-thin') }}</ui-radio>
-				<ui-radio v-model="lineWidth" :value="1">{{ $t('@._settings.line-width-normal') }}</ui-radio>
-				<ui-radio v-model="lineWidth" :value="2">{{ $t('@._settings.line-width-thick') }}</ui-radio>
-			</section>
-			<section>
-				<header>{{ $t('@._settings.font-size') }}</header>
-				<ui-radio v-model="fontSize" :value="-2">{{ $t('@._settings.font-size-x-small') }}</ui-radio>
-				<ui-radio v-model="fontSize" :value="-1">{{ $t('@._settings.font-size-small') }}</ui-radio>
-				<ui-radio v-model="fontSize" :value="0">{{ $t('@._settings.font-size-medium') }}</ui-radio>
-				<ui-radio v-model="fontSize" :value="1">{{ $t('@._settings.font-size-large') }}</ui-radio>
-				<ui-radio v-model="fontSize" :value="2">{{ $t('@._settings.font-size-x-large') }}</ui-radio>
-			</section>
-			<section v-if="$root.isMobile">
-				<header>{{ $t('@._settings.post-style') }}</header>
-				<ui-radio v-model="postStyle" value="standard">{{ $t('@._settings.post-style-standard') }}</ui-radio>
-				<ui-radio v-model="postStyle" value="smart">{{ $t('@._settings.post-style-smart') }}</ui-radio>
-			</section>
-			<section v-if="$root.isMobile">
-				<header>{{ $t('@._settings.notification-position') }}</header>
-				<ui-radio v-model="mobileNotificationPosition" value="bottom">{{ $t('@._settings.notification-position-bottom') }}</ui-radio>
-				<ui-radio v-model="mobileNotificationPosition" value="top">{{ $t('@._settings.notification-position-top') }}</ui-radio>
-			</section>
-			<section>
-				<header>{{ $t('@._settings.deck-column-align') }}</header>
-				<ui-radio v-model="deckColumnAlign" value="center">{{ $t('@._settings.deck-column-align-center') }}</ui-radio>
-				<ui-radio v-model="deckColumnAlign" value="left">{{ $t('@._settings.deck-column-align-left') }}</ui-radio>
-				<ui-radio v-model="deckColumnAlign" value="flexible">{{ $t('@._settings.deck-column-align-flexible') }}</ui-radio>
-			</section>
-			<section>
-				<header>{{ $t('@._settings.deck-column-width') }}</header>
-				<ui-radio v-model="deckColumnWidth" value="narrow">{{ $t('@._settings.deck-column-width-narrow') }}</ui-radio>
-				<ui-radio v-model="deckColumnWidth" value="narrower">{{ $t('@._settings.deck-column-width-narrower') }}</ui-radio>
-				<ui-radio v-model="deckColumnWidth" value="normal">{{ $t('@._settings.deck-column-width-normal') }}</ui-radio>
-				<ui-radio v-model="deckColumnWidth" value="wider">{{ $t('@._settings.deck-column-width-wider') }}</ui-radio>
-				<ui-radio v-model="deckColumnWidth" value="wide">{{ $t('@._settings.deck-column-width-wide') }}</ui-radio>
-			</section>
-			<section>
-				<ui-switch v-model="games_reversi_showBoardLabels">{{ $t('@._settings.show-reversi-board-labels') }}</ui-switch>
-				<ui-switch v-model="games_reversi_useAvatarStones">{{ $t('@._settings.use-avatar-reversi-stones') }}</ui-switch>
-			</section>
-		</ui-card>
-	</template>
-
-	<template v-if="page == null || page == 'behavior'">
-		<ui-card>
-			<template #title><fa icon="sliders-h"/> {{ $t('@._settings.behavior') }}</template>
-
-			<section>
-				<ui-switch v-model="fetchOnScroll">{{ $t('@._settings.fetch-on-scroll') }}
-					<template #desc>{{ $t('@._settings.fetch-on-scroll-desc') }}</template>
-				</ui-switch>
-				<ui-switch v-model="keepCw">{{ $t('@._settings.keep-cw') }}
-					<template #desc>{{ $t('@._settings.keep-cw-desc') }}</template>
-				</ui-switch>
-				<ui-switch v-if="$root.isMobile" v-model="disableViaMobile">{{ $t('@._settings.disable-via-mobile') }}</ui-switch>
-			</section>
-
-			<section>
-				<header>{{ $t('@._settings.reactions') }}</header>
-				<ui-textarea v-model="reactions">
-					{{ $t('@._settings.reactions') }}<template #desc>{{ $t('@._settings.reactions-description') }}</template>
-				</ui-textarea>
-				<ui-horizon-group>
-					<ui-button @click="save('reactions', reactions.trim().split('\n'))" primary><fa :icon="faSave"/> {{ $t('@._settings.save') }}</ui-button>
-					<ui-button @click="previewReaction()" ref="reactionsPreviewButton"><fa :icon="faEye"/> {{ $t('@._settings.preview') }}</ui-button>
-				</ui-horizon-group>
-			</section>
-
-			<section>
-				<header>{{ $t('@._settings.timeline') }}</header>
-				<ui-switch v-model="showMyRenotes">{{ $t('@._settings.show-my-renotes') }}</ui-switch>
-				<ui-switch v-model="showRenotedMyNotes">{{ $t('@._settings.show-renoted-my-notes') }}</ui-switch>
-				<ui-switch v-model="showLocalRenotes">{{ $t('@._settings.show-local-renotes') }}</ui-switch>
-			</section>
-
-			<section>
-				<header>{{ $t('@._settings.note-visibility') }}</header>
-				<ui-switch v-model="rememberNoteVisibility">{{ $t('@._settings.remember-note-visibility') }}</ui-switch>
-				<section>
-					<header>{{ $t('@._settings.default-note-visibility') }}</header>
-					<ui-select v-model="defaultNoteVisibility">
-						<option value="public">{{ $t('@.note-visibility.public') }}</option>
-						<option value="home">{{ $t('@.note-visibility.home') }}</option>
-						<option value="followers">{{ $t('@.note-visibility.followers') }}</option>
-						<option value="specified">{{ $t('@.note-visibility.specified') }}</option>
-						<option value="local-public">{{ $t('@.note-visibility.local-public') }}</option>
-						<option value="local-home">{{ $t('@.note-visibility.local-home') }}</option>
-						<option value="local-followers">{{ $t('@.note-visibility.local-followers') }}</option>
-					</ui-select>
-				</section>
-			</section>
-
-			<section>
-				<header>{{ $t('@._settings.sync') }}</header>
-				<ui-input v-if="$root.isMobile" v-model="mobileHomeProfile" :datalist="Object.keys($store.state.settings.mobileHomeProfiles)">{{ $t('@._settings.home-profile') }}</ui-input>
-				<ui-input v-else v-model="homeProfile" :datalist="Object.keys($store.state.settings.homeProfiles)">{{ $t('@._settings.home-profile') }}</ui-input>
-				<ui-input v-model="deckProfile" :datalist="Object.keys($store.state.settings.deckProfiles)">{{ $t('@._settings.deck-profile') }}</ui-input>
-			</section>
-
-			<section>
-				<header>{{ $t('@._settings.web-search-engine') }}</header>
-				<ui-input v-model="webSearchEngine">{{ $t('@._settings.web-search-engine') }}
-					<template #desc>{{ $t('@._settings.web-search-engine-desc') }}</template>
-				</ui-input>
-				<ui-button @click="save('webSearchEngine', webSearchEngine)"><fa :icon="faSave"/> {{ $t('@._settings.save') }}</ui-button>
-			</section>
-
-			<section v-if="!$root.isMobile">
-				<header>{{ $t('@._settings.paste') }}</header>
-				<ui-input v-model="pastedFileName">{{ $t('@._settings.pasted-file-name') }}
-					<template v-if="pastedFileName === this.$store.state.settings.pastedFileName" #desc>{{ $t('@._settings.pasted-file-name-desc') }}</template>
-					<template v-else #desc>{{ pastedFileNamePreview() }}</template>
-				</ui-input>
-				<ui-button @click="save('pastedFileName', pastedFileName)"><fa :icon="faSave"/> {{ $t('@._settings.save') }}</ui-button>
-
-				<ui-switch v-model="pasteDialog">{{ $t('@._settings.paste-dialog') }}
-					<template #desc>{{ $t('@._settings.paste-dialog-desc') }}</template>
-				</ui-switch>
-			</section>
-
-			<section>
-				<header>{{ $t('@._settings.room') }}</header>
-				<ui-select v-model="roomGraphicsQuality">
-					<template #label>{{ $t('@._settings._room.graphicsQuality') }}</template>
-					<option value="ultra">{{ $t('@._settings._room._graphicsQuality.ultra') }}</option>
-					<option value="high">{{ $t('@._settings._room._graphicsQuality.high') }}</option>
-					<option value="medium">{{ $t('@._settings._room._graphicsQuality.medium') }}</option>
-					<option value="low">{{ $t('@._settings._room._graphicsQuality.low') }}</option>
-					<option value="cheep">{{ $t('@._settings._room._graphicsQuality.cheep') }}</option>
-				</ui-select>
-				<ui-switch v-model="roomUseOrthographicCamera">{{ $t('@._settings._room.useOrthographicCamera') }}</ui-switch>
-			</section>
-		</ui-card>
-
-		<ui-card>
-			<template #title><fa icon="volume-up"/> {{ $t('@._settings.sound') }}</template>
-
-			<section>
-				<ui-switch v-model="enableSounds">{{ $t('@._settings.enable-sounds') }}
-					<template #desc>{{ $t('@._settings.enable-sounds-desc') }}</template>
-				</ui-switch>
-				<label>{{ $t('@._settings.volume') }}</label>
-				<input type="range"
-					v-model="soundVolume"
-					:disabled="!enableSounds"
-					max="1"
-					step="0.1"
-				/>
-				<ui-button @click="soundTest"><fa icon="volume-up"/> {{ $t('@._settings.test') }}</ui-button>
-			</section>
-		</ui-card>
-
-		<x-language/>
-		<x-app-type/>
-	</template>
-
-	<template v-if="page == null || page == 'notification'">
-		<x-notification/>
-	</template>
-
-	<template v-if="page == null || page == 'drive'">
-		<x-drive/>
-	</template>
-
-	<template v-if="page == null || page == 'hashtags'">
-		<ui-card>
-			<template #title><fa icon="hashtag"/> {{ $t('@._settings.tags') }}</template>
-			<section>
-				<x-tags/>
-			</section>
-		</ui-card>
-	</template>
-
-	<template v-if="page == null || page == 'muteAndBlock'">
-		<x-mute-and-block/>
-	</template>
-
-	<!--
-	<template v-if="page == null || page == 'apps'">
-		<ui-card>
-			<template #title><fa icon="puzzle-piece"/> {{ $t('@._settings.apps') }}</template>
-			<section>
-				<x-apps/>
-			</section>
-		</ui-card>
-	</template>
-	-->
-
-	<template v-if="page == null || page == 'security'">
-		<ui-card>
-			<template #title><fa icon="unlock-alt"/> {{ $t('@._settings.password') }}</template>
-			<section>
-				<x-password/>
-			</section>
-		</ui-card>
-
-		<ui-card v-if="!$root.isMobile">
-			<template #title><fa icon="mobile-alt"/> {{ $t('@.2fa') }}</template>
-			<section>
-				<x-2fa/>
-			</section>
-		</ui-card>
-
-		<!--
-		<ui-card>
-			<template #title><fa icon="sign-in-alt"/> {{ $t('@._settings.signin') }}</template>
-			<section>
-				<x-signins/>
-			</section>
-		</ui-card>
-		-->
-	</template>
-
-	<template v-if="page == null || page == 'api'">
-		<x-api/>
-	</template>
-
-	<template v-if="page == null || page == 'other'">
-		<ui-card>
-			<template #title><fa icon="sync-alt"/> {{ $t('@._settings.update') }}</template>
-			<section>
-				<p>
-					<span>{{ $t('@._settings.version') }} <i>{{ version }}</i></span>
-					<template v-if="latestVersion !== undefined">
-						<br>
-						<span>{{ $t('@._settings.latest-version') }} <i>{{ latestVersion ? latestVersion : version }}</i></span>
-					</template>
-				</p>
-				<ui-button @click="checkForUpdate" :disabled="checkingForUpdate">
-					<template v-if="checkingForUpdate">{{ $t('@._settings.update-checking') }}<mk-ellipsis/></template>
-					<template v-else>{{ $t('@._settings.do-update') }}</template>
-				</ui-button>
-			</section>
-		</ui-card>
-
-		<ui-card>
-			<template #title><fa icon="cogs"/> {{ $t('@._settings.advanced-settings') }}</template>
-			<section>
-				<ui-switch v-model="debug">
-					{{ $t('@._settings.debug-mode') }}<template #desc>{{ $t('@._settings.debug-mode-desc') }}</template>
-				</ui-switch>
-			</section>
-		</ui-card>
-	</template>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import X2fa from './2fa.vue';
-import XApps from './apps.vue';
-import XSignins from './signins.vue';
-import XTags from './tags.vue';
-import XIntegration from './integration.vue';
-import XTheme from './theme.vue';
-import XDrive from './drive.vue';
-import XMuteAndBlock from './mute-and-block.vue';
-import XPassword from './password.vue';
-import XProfile from './profile.vue';
-import XApi from './api.vue';
-import XLanguage from './language.vue';
-import XAppType from './app-type.vue';
-import XNotification from './notification.vue';
-import MkReactionPicker from '../reaction-picker.vue';
-
-import { url, version } from '../../../../config';
-import checkForUpdate from '../../../scripts/check-for-update';
-import { formatTimeString } from '../../../../../../misc/format-time-string';
-import { faSave, faEye } from '@fortawesome/free-regular-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n(),
-	components: {
-		X2fa,
-		XApps,
-		XSignins,
-		XTags,
-		XIntegration,
-		XTheme,
-		XDrive,
-		XMuteAndBlock,
-		XPassword,
-		XProfile,
-		XApi,
-		XLanguage,
-		XAppType,
-		XNotification,
-	},
-	props: {
-		page: {
-			type: String,
-			required: false,
-			default: null
-		}
-	},
-	data() {
-		return {
-			meta: null,
-			version,
-			reactions: this.$store.state.settings.reactions.join('\n'),
-			webSearchEngine: this.$store.state.settings.webSearchEngine,
-			pastedFileName : this.$store.state.settings.pastedFileName,
-			latestVersion: undefined,
-			checkingForUpdate: false,
-			faSave, faEye
-		};
-	},
-	computed: {
-		useOsDefaultEmojis: {
-			get() { return this.$store.state.device.useOsDefaultEmojis; },
-			set(value) { this.$store.commit('device/set', { key: 'useOsDefaultEmojis', value }); }
-		},
-
-		reduceMotion: {
-			get() { return this.$store.state.device.reduceMotion; },
-			set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }
-		},
-
-		keepCw: {
-			get() { return this.$store.state.settings.keepCw; },
-			set(value) { this.$store.commit('settings/set', { key: 'keepCw', value }); }
-		},
-
-		navbar: {
-			get() { return this.$store.state.device.navbar; },
-			set(value) { this.$store.commit('device/set', { key: 'navbar', value }); }
-		},
-
-		deckColumnAlign: {
-			get() { return this.$store.state.device.deckColumnAlign; },
-			set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); }
-		},
-
-		deckColumnWidth: {
-			get() { return this.$store.state.device.deckColumnWidth; },
-			set(value) { this.$store.commit('device/set', { key: 'deckColumnWidth', value }); }
-		},
-
-		enableSounds: {
-			get() { return this.$store.state.device.enableSounds; },
-			set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); }
-		},
-
-		soundVolume: {
-			get() { return this.$store.state.device.soundVolume; },
-			set(value) { this.$store.commit('device/set', { key: 'soundVolume', value }); }
-		},
-
-		debug: {
-			get() { return this.$store.state.device.debug; },
-			set(value) { this.$store.commit('device/set', { key: 'debug', value }); }
-		},
-
-		alwaysShowNsfw: {
-			get() { return this.$store.state.device.alwaysShowNsfw; },
-			set(value) { this.$store.commit('device/set', { key: 'alwaysShowNsfw', value }); }
-		},
-
-		postStyle: {
-			get() { return this.$store.state.device.postStyle; },
-			set(value) { this.$store.commit('device/set', { key: 'postStyle', value }); }
-		},
-
-		disableViaMobile: {
-			get() { return this.$store.state.settings.disableViaMobile; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'disableViaMobile', value }); }
-		},
-
-		useShadow: {
-			get() { return this.$store.state.device.useShadow; },
-			set(value) { this.$store.commit('device/set', { key: 'useShadow', value }); }
-		},
-
-		roundedCorners: {
-			get() { return this.$store.state.device.roundedCorners; },
-			set(value) { this.$store.commit('device/set', { key: 'roundedCorners', value }); }
-		},
-
-		lineWidth: {
-			get() { return this.$store.state.device.lineWidth; },
-			set(value) { this.$store.commit('device/set', { key: 'lineWidth', value }); }
-		},
-
-		fontSize: {
-			get() { return this.$store.state.device.fontSize; },
-			set(value) { this.$store.commit('device/set', { key: 'fontSize', value }); }
-		},
-
-		fetchOnScroll: {
-			get() { return this.$store.state.settings.fetchOnScroll; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'fetchOnScroll', value }); }
-		},
-
-		rememberNoteVisibility: {
-			get() { return this.$store.state.settings.rememberNoteVisibility; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); }
-		},
-
-		defaultNoteVisibility: {
-			get() { return this.$store.state.settings.defaultNoteVisibility; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); }
-		},
-
-		pasteDialog: {
-			get() { return this.$store.state.settings.pasteDialog; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'pasteDialog', value }); }
-		},
-
-		showReplyTarget: {
-			get() { return this.$store.state.settings.showReplyTarget; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', value }); }
-		},
-
-		showMyRenotes: {
-			get() { return this.$store.state.settings.showMyRenotes; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'showMyRenotes', value }); }
-		},
-
-		showRenotedMyNotes: {
-			get() { return this.$store.state.settings.showRenotedMyNotes; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'showRenotedMyNotes', value }); }
-		},
-
-		showLocalRenotes: {
-			get() { return this.$store.state.settings.showLocalRenotes; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'showLocalRenotes', value }); }
-		},
-
-		showPostFormOnTopOfTl: {
-			get() { return this.$store.state.settings.showPostFormOnTopOfTl; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'showPostFormOnTopOfTl', value }); }
-		},
-
-		suggestRecentHashtags: {
-			get() { return this.$store.state.settings.suggestRecentHashtags; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'suggestRecentHashtags', value }); }
-		},
-
-		showClockOnHeader: {
-			get() { return this.$store.state.settings.showClockOnHeader; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'showClockOnHeader', value }); }
-		},
-
-		circleIcons: {
-			get() { return this.$store.state.settings.circleIcons; },
-			set(value) {
-				this.$store.dispatch('settings/set', { key: 'circleIcons', value });
-				this.reload();
-			}
-		},
-
-		contrastedAcct: {
-			get() { return this.$store.state.settings.contrastedAcct; },
-			set(value) {
-				this.$store.dispatch('settings/set', { key: 'contrastedAcct', value });
-				this.reload();
-			}
-		},
-
-		showFullAcct: {
-			get() { return this.$store.state.settings.showFullAcct; },
-			set(value) {
-				this.$store.dispatch('settings/set', { key: 'showFullAcct', value });
-				this.reload();
-			}
-		},
-
-		showVia: {
-			get() { return this.$store.state.settings.showVia; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'showVia', value }); }
-		},
-
-		iLikeSushi: {
-			get() { return this.$store.state.settings.iLikeSushi; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'iLikeSushi', value }); }
-		},
-
-		roomUseOrthographicCamera: {
-			get() { return this.$store.state.device.roomUseOrthographicCamera; },
-			set(value) { this.$store.commit('device/set', { key: 'roomUseOrthographicCamera', value }); }
-		},
-
-		roomGraphicsQuality: {
-			get() { return this.$store.state.device.roomGraphicsQuality; },
-			set(value) { this.$store.commit('device/set', { key: 'roomGraphicsQuality', value }); }
-		},
-
-		games_reversi_showBoardLabels: {
-			get() { return this.$store.state.settings.gamesReversiShowBoardLabels; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'gamesReversiShowBoardLabels', value }); }
-		},
-
-		games_reversi_useAvatarStones: {
-			get() { return this.$store.state.settings.gamesReversiUseAvatarStones; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'gamesReversiUseAvatarStones', value }); }
-		},
-
-		disableAnimatedMfm: {
-			get() { return this.$store.state.settings.disableAnimatedMfm; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'disableAnimatedMfm', value }); }
-		},
-
-		disableShowingAnimatedImages: {
-			get() { return this.$store.state.device.disableShowingAnimatedImages; },
-			set(value) { this.$store.commit('device/set', { key: 'disableShowingAnimatedImages', value }); }
-		},
-
-		remainDeletedNote: {
-			get() { return this.$store.state.settings.remainDeletedNote; },
-			set(value) { this.$store.dispatch('settings/set', { key: 'remainDeletedNote', value }); }
-		},
-
-		mobileNotificationPosition: {
-			get() { return this.$store.state.device.mobileNotificationPosition; },
-			set(value) { this.$store.commit('device/set', { key: 'mobileNotificationPosition', value }); }
-		},
-
-		enableMobileQuickNotificationView: {
-			get() { return this.$store.state.device.enableMobileQuickNotificationView; },
-			set(value) { this.$store.commit('device/set', { key: 'enableMobileQuickNotificationView', value }); }
-		},
-
-		homeProfile: {
-			get() { return this.$store.state.device.homeProfile; },
-			set(value) { this.$store.commit('device/set', { key: 'homeProfile', value }); }
-		},
-
-		mobileHomeProfile: {
-			get() { return this.$store.state.device.mobileHomeProfile; },
-			set(value) { this.$store.commit('device/set', { key: 'mobileHomeProfile', value }); }
-		},
-
-		deckProfile: {
-			get() { return this.$store.state.device.deckProfile; },
-			set(value) { this.$store.commit('device/set', { key: 'deckProfile', value }); }
-		},
-	},
-	created() {
-		this.$root.getMeta().then(meta => {
-			this.meta = meta;
-		});
-	},
-	methods: {
-		reload() {
-			this.$root.dialog({
-				type: 'warning',
-				text: this.$t('@.reload-to-apply-the-setting'),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (!canceled) {
-					location.reload();
-				}
-			});
-		},
-		save(key, value) {
-			this.$store.dispatch('settings/set', {
-				key,
-				value
-			}).then(() => {
-				this.$root.dialog({
-					type: 'success',
-					text: this.$t('@._settings.saved')
-				})
-			});
-		},
-		customizeHome() {
-			location.href = '/?customize';
-		},
-		updateWallpaper() {
-			this.$chooseDriveFile({
-				multiple: false
-			}).then(file => {
-				this.$store.dispatch('settings/set', { key: 'wallpaper', value: file.url });
-			});
-		},
-		deleteWallpaper() {
-			this.$store.dispatch('settings/set', { key: 'wallpaper', value: null });
-		},
-		checkForUpdate() {
-			this.checkingForUpdate = true;
-			checkForUpdate(this.$root, true, true).then(newer => {
-				this.checkingForUpdate = false;
-				this.latestVersion = newer;
-				if (newer == null) {
-					this.$root.dialog({
-						title: this.$t('@._settings.no-updates'),
-						text: this.$t('@._settings.no-updates-desc')
-					});
-				} else {
-					this.$root.dialog({
-						title: this.$t('@._settings.update-available'),
-						text: this.$t('@._settings.update-available-desc')
-					});
-				}
-			});
-		},
-		soundTest() {
-			const sound = new Audio(`${url}/assets/message.mp3`);
-			sound.volume = this.$store.state.device.soundVolume;
-			sound.play();
-		},
-		pastedFileNamePreview() {
-			return `${formatTimeString(new Date(), this.pastedFileName).replace(/{{number}}/g, `1`)}.png`
-		},
-		previewReaction() {
-			const picker = this.$root.new(MkReactionPicker, {
-				source: this.$refs.reactionsPreviewButton.$el,
-				reactions: this.reactions.trim().split('\n'),
-				showFocus: false,
-			});
-			picker.$once('chosen', reaction => {
-				picker.close();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/app/common/views/components/settings/signins.vue b/src/client/app/common/views/components/settings/signins.vue
deleted file mode 100644
index 048fa2fc5bbad7c6326a808bcd4987ff29083554..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/settings/signins.vue
+++ /dev/null
@@ -1,98 +0,0 @@
-<template>
-<div class="root">
-<div class="signins" v-if="signins.length != 0">
-	<div v-for="signin in signins">
-		<header @click="signin._show = !signin._show">
-			<template v-if="signin.success"><fa icon="check"/></template>
-			<template v-else><fa icon="times"/></template>
-			<span class="ip">{{ signin.ip }}</span>
-			<mk-time :time="signin.createdAt"/>
-		</header>
-		<div class="headers" v-show="signin._show">
-			<!-- TODO -->
-		</div>
-	</div>
-</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	data() {
-		return {
-			fetching: true,
-			signins: [],
-			connection: null
-		};
-	},
-
-	mounted() {
-		this.$root.api('i/signin_history').then(signins => {
-			this.signins = signins;
-			this.fetching = false;
-		});
-
-		this.connection = this.$root.stream.useSharedConnection('main');
-
-		this.connection.on('signin', this.onSignin);
-	},
-
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		onSignin(signin) {
-			this.signins.unshift(signin);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.root
-	> .signins
-		> div
-			border-bottom solid 1px #eee
-
-			> header
-				display flex
-				padding 8px 0
-				line-height 32px
-				cursor pointer
-
-				> [data-icon]
-					margin-right 8px
-					text-align left
-
-					&.check
-						color #0fda82
-
-					&.times
-						color #ff3100
-
-				> .ip
-					display inline-block
-					text-align left
-					padding 8px
-					line-height 16px
-					font-family monospace
-					font-size 14px
-					color #444
-					background #f8f8f8
-					border-radius 4px
-
-				> .mk-time
-					margin-left auto
-					text-align right
-					color #777
-
-			> .headers
-				overflow auto
-				margin 0 0 16px 0
-				max-height 100px
-				white-space pre-wrap
-				word-break break-all
-
-</style>
diff --git a/src/client/app/common/views/components/settings/tags.vue b/src/client/app/common/views/components/settings/tags.vue
deleted file mode 100644
index 2e17f35e3ef9ea74b9f079dcf983dc3af9604d06..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/settings/tags.vue
+++ /dev/null
@@ -1,67 +0,0 @@
-<template>
-<div class="vfcitkilproprqtbnpoertpsziierwzi">
-	<div v-for="timeline in timelines" class="timeline" :key="timeline.id">
-		<ui-input v-model="timeline.title" @change="save">
-			<span>{{ $t('title') }}</span>
-		</ui-input>
-		<ui-textarea :value="timeline.query ? timeline.query.map(tags => tags.join(' ')).join('\n') : ''" :pre="true" @input="onQueryChange(timeline, $event)">
-			<span>{{ $t('query') }}</span>
-		</ui-textarea>
-	</div>
-	<ui-button class="add" @click="add">{{ $t('add') }}</ui-button>
-	<ui-button class="save" @click="save">{{ $t('save') }}</ui-button>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import { v4 as uuid } from 'uuid';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/settings.tags.vue'),
-	data() {
-		return {
-			timelines: this.$store.state.settings.tagTimelines
-		};
-	},
-
-	methods: {
-		add() {
-			this.timelines.push({
-				id: uuid(),
-				title: '',
-				query: ''
-			});
-		},
-
-		save() {
-			const timelines = this.timelines
-				.filter(timeline => timeline.title)
-				.map(timeline => {
-					if (!(timeline.query && timeline.query[0] && timeline.query[0][0])) {
-						timeline.query = timeline.title.split('\n').map(tags => tags.split(' '));
-					}
-					return timeline;
-				});
-
-			this.$store.dispatch('settings/set', { key: 'tagTimelines', value: timelines });
-		},
-
-		onQueryChange(timeline, value) {
-			timeline.query = value.split('\n').map(tags => tags.split(' '));
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.vfcitkilproprqtbnpoertpsziierwzi
-	> .timeline
-		padding-bottom 16px
-		border-bottom solid 1px rgba(#000, 0.1)
-
-	> .add
-		margin-top 16px
-
-</style>
diff --git a/src/client/app/common/views/components/settings/theme.vue b/src/client/app/common/views/components/settings/theme.vue
deleted file mode 100644
index d916a5750808ef775457bbe2a58f1b95ac6cb8ef..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/settings/theme.vue
+++ /dev/null
@@ -1,558 +0,0 @@
-<template>
-<ui-card>
-	<template #title><fa icon="palette"/> {{ $t('theme') }}</template>
-	<section class="nicnklzforebnpfgasiypmpdaaglujqm fit-top">
-		<div class="dark">
-			<div class="toggleWrapper">
-				<input type="checkbox" class="dn" id="dn" v-model="darkmode"/>
-				<label for="dn" class="toggle">
-					<span class="toggle__handler">
-						<span class="crater crater--1"></span>
-						<span class="crater crater--2"></span>
-						<span class="crater crater--3"></span>
-					</span>
-					<span class="star star--1"></span>
-					<span class="star star--2"></span>
-					<span class="star star--3"></span>
-					<span class="star star--4"></span>
-					<span class="star star--5"></span>
-					<span class="star star--6"></span>
-				</label>
-			</div>
-		</div>
-
-		<label>
-			<ui-select v-model="light" :placeholder="$t('light-theme')">
-				<template #label><fa :icon="faSun"/> {{ $t('light-theme') }}</template>
-				<optgroup :label="$t('light-themes')">
-					<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
-				</optgroup>
-				<optgroup :label="$t('dark-themes')">
-					<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
-				</optgroup>
-			</ui-select>
-		</label>
-
-		<label>
-			<ui-select v-model="dark" :placeholder="$t('dark-theme')">
-				<template #label><fa :icon="faMoon"/> {{ $t('dark-theme') }}</template>
-				<optgroup :label="$t('dark-themes')">
-					<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
-				</optgroup>
-				<optgroup :label="$t('light-themes')">
-					<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
-				</optgroup>
-			</ui-select>
-		</label>
-
-		<a href="https://assets.msky.cafe/theme/list" rel="noopener" target="_blank">{{ $t('find-more-theme') }}</a>
-
-		<details class="creator">
-			<summary><fa icon="palette"/> {{ $t('create-a-theme') }}</summary>
-			<div>
-				<span>{{ $t('base-theme') }}:</span>
-				<ui-radio v-model="myThemeBase" value="light">{{ $t('base-theme-light') }}</ui-radio>
-				<ui-radio v-model="myThemeBase" value="dark">{{ $t('base-theme-dark') }}</ui-radio>
-			</div>
-			<div>
-				<ui-input v-model="myThemeName">
-					<span>{{ $t('theme-name') }}</span>
-				</ui-input>
-				<ui-textarea v-model="myThemeDesc">
-					<span>{{ $t('desc') }}</span>
-				</ui-textarea>
-			</div>
-			<div>
-				<div style="padding-bottom:8px;">{{ $t('primary-color') }}:</div>
-				<color-picker v-model="myThemePrimary"/>
-			</div>
-			<div>
-				<div style="padding-bottom:8px;">{{ $t('secondary-color') }}:</div>
-				<color-picker v-model="myThemeSecondary"/>
-			</div>
-			<div>
-				<div style="padding-bottom:8px;">{{ $t('text-color') }}:</div>
-				<color-picker v-model="myThemeText"/>
-			</div>
-			<ui-button @click="preview()"><fa icon="eye"/> {{ $t('preview-created-theme') }}</ui-button>
-			<ui-button primary @click="gen()"><fa :icon="['far', 'save']"/> {{ $t('save-created-theme') }}</ui-button>
-		</details>
-
-		<details>
-			<summary><fa icon="download"/> {{ $t('install-a-theme') }}</summary>
-			<ui-button @click="import_()"><fa icon="file-import"/> {{ $t('import') }}</ui-button>
-			<input ref="file" type="file" accept=".misskeytheme" style="display:none;" @change="onUpdateImportFile"/>
-			<p>{{ $t('import-by-code') }}:</p>
-			<ui-textarea v-model="installThemeCode">
-				<span>{{ $t('theme-code') }}</span>
-			</ui-textarea>
-			<ui-button @click="() => install(this.installThemeCode)"><fa icon="check"/> {{ $t('install') }}</ui-button>
-		</details>
-
-		<details>
-			<summary><fa icon="folder-open"/> {{ $t('manage-themes') }}</summary>
-			<ui-select v-model="selectedThemeId" :placeholder="$t('select-theme')">
-				<optgroup :label="$t('builtin-themes')">
-					<option v-for="x in builtinThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
-				</optgroup>
-				<optgroup :label="$t('my-themes')">
-					<option v-for="x in installedThemes.filter(t => t.author == this.$store.state.i.username)" :value="x.id" :key="x.id">{{ x.name }}</option>
-				</optgroup>
-				<optgroup :label="$t('installed-themes')">
-					<option v-for="x in installedThemes.filter(t => t.author != this.$store.state.i.username)" :value="x.id" :key="x.id">{{ x.name }}</option>
-				</optgroup>
-			</ui-select>
-			<template v-if="selectedTheme">
-				<ui-input readonly :value="selectedTheme.author">
-					<span>{{ $t('author') }}</span>
-				</ui-input>
-				<ui-textarea v-if="selectedTheme.desc" readonly :value="selectedTheme.desc">
-					<span>{{ $t('desc') }}</span>
-				</ui-textarea>
-				<ui-textarea readonly tall :value="selectedThemeCode">
-					<span>{{ $t('theme-code') }}</span>
-				</ui-textarea>
-				<ui-button @click="export_()" link :download="`${selectedTheme.name}.misskeytheme`" ref="export"><fa icon="box"/> {{ $t('export') }}</ui-button>
-				<ui-button @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><fa :icon="['far', 'trash-alt']"/> {{ $t('uninstall') }}</ui-button>
-			</template>
-		</details>
-	</section>
-</ui-card>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import { lightTheme, darkTheme, builtinThemes, applyTheme, Theme } from '../../../../theme';
-import { Chrome } from 'vue-color';
-import { v4 as uuid } from 'uuid';
-import * as tinycolor from 'tinycolor2';
-import * as JSON5 from 'json5';
-import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/theme.vue'),
-	components: {
-		ColorPicker: Chrome
-	},
-
-	data() {
-		return {
-			builtinThemes: builtinThemes,
-			installThemeCode: null,
-			selectedThemeId: null,
-			myThemeBase: 'light',
-			myThemeName: '',
-			myThemeDesc: '',
-			myThemePrimary: lightTheme.vars.primary,
-			myThemeSecondary: lightTheme.vars.secondary,
-			myThemeText: lightTheme.vars.text,
-			faMoon, faSun
-		};
-	},
-
-	computed: {
-		themes(): Theme[] {
-			return builtinThemes.concat(this.$store.state.device.themes);
-		},
-
-		darkThemes(): Theme[] {
-			return this.themes.filter(t => t.base == 'dark' || t.kind == 'dark');
-		},
-
-		lightThemes(): Theme[] {
-			return this.themes.filter(t => t.base == 'light' || t.kind == 'light');
-		},
-
-		installedThemes(): Theme[] {
-			return this.$store.state.device.themes;
-		},
-
-		light: {
-			get() { return this.$store.state.device.lightTheme; },
-			set(value) { this.$store.commit('device/set', { key: 'lightTheme', value }); }
-		},
-
-		dark: {
-			get() { return this.$store.state.device.darkTheme; },
-			set(value) { this.$store.commit('device/set', { key: 'darkTheme', value }); }
-		},
-
-		selectedTheme() {
-			if (this.selectedThemeId == null) return null;
-			return this.themes.find(x => x.id == this.selectedThemeId);
-		},
-
-		selectedThemeCode() {
-			if (this.selectedTheme == null) return null;
-			return JSON5.stringify(this.selectedTheme, null, '\t');
-		},
-
-		myTheme(): any {
-			return {
-				name: this.myThemeName,
-				author: this.$store.state.i.username,
-				desc: this.myThemeDesc,
-				base: this.myThemeBase,
-				vars: {
-					primary: tinycolor(typeof this.myThemePrimary == 'string' ? this.myThemePrimary : this.myThemePrimary.rgba).toRgbString(),
-					secondary: tinycolor(typeof this.myThemeSecondary == 'string' ? this.myThemeSecondary : this.myThemeSecondary.rgba).toRgbString(),
-					text: tinycolor(typeof this.myThemeText == 'string' ? this.myThemeText : this.myThemeText.rgba).toRgbString()
-				}
-			};
-		},
-
-		darkmode: {
-			get() { return this.$store.state.device.darkmode; },
-			set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
-		},
-	},
-
-	watch: {
-		myThemeBase(v) {
-			const theme = v == 'light' ? lightTheme : darkTheme;
-			this.myThemePrimary = theme.vars.primary;
-			this.myThemeSecondary = theme.vars.secondary;
-			this.myThemeText = theme.vars.text;
-		}
-	},
-
-	methods: {
-		install(code) {
-			let theme;
-
-			try {
-				theme = JSON5.parse(code);
-			} catch (e) {
-				this.$root.dialog({
-					type: 'error',
-					text: this.$t('invalid-theme')
-				});
-				return;
-			}
-
-			if (theme.id == null) {
-				this.$root.dialog({
-					type: 'error',
-					text: this.$t('invalid-theme')
-				});
-				return;
-			}
-
-			if (this.$store.state.device.themes.some(t => t.id == theme.id)) {
-				this.$root.dialog({
-					type: 'info',
-					text: this.$t('already-installed')
-				});
-				return;
-			}
-
-			const themes = this.$store.state.device.themes.concat(theme);
-			this.$store.commit('device/set', {
-				key: 'themes', value: themes
-			});
-
-			this.$root.dialog({
-				type: 'success',
-				text: this.$t('installed').replace('{}', theme.name)
-			});
-		},
-
-		uninstall() {
-			const theme = this.selectedTheme;
-			const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
-			this.$store.commit('device/set', {
-				key: 'themes', value: themes
-			});
-
-			this.$root.dialog({
-				type: 'info',
-				text: this.$t('uninstalled').replace('{}', theme.name)
-			});
-		},
-
-		import_() {
-			(this.$refs.file as any).click();
-		},
-
-		export_() {
-			const blob = new Blob([this.selectedThemeCode], {
-				type: 'application/json5'
-			});
-			this.$refs.export.$el.href = window.URL.createObjectURL(blob);
-		},
-
-		onUpdateImportFile() {
-			const f = (this.$refs.file as any).files[0];
-
-			const reader = new FileReader();
-
-			reader.onload = e => {
-				this.install(e.target.result);
-			};
-
-			reader.readAsText(f);
-		},
-
-		preview() {
-			applyTheme(this.myTheme, false);
-		},
-
-		gen() {
-			const theme = this.myTheme;
-
-			if (theme.name == null || theme.name.trim() == '') {
-				this.$root.dialog({
-					type: 'warning',
-					text: this.$t('theme-name-required')
-				});
-				return;
-			}
-
-			theme.id = uuid();
-
-			const themes = this.$store.state.device.themes.concat(theme);
-			this.$store.commit('device/set', {
-				key: 'themes', value: themes
-			});
-
-			this.$root.dialog({
-				type: 'success',
-				text: this.$t('saved')
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.nicnklzforebnpfgasiypmpdaaglujqm
-	> .dark
-		margin-top 48px
-		margin-bottom 110px
-
-		.toggleWrapper {
-			position: absolute;
-			top: 50%;
-			left: 50%;
-			overflow: hidden;
-			padding: 0 200px;
-			transform: translate3d(-50%, -50%, 0);
-
-			input {
-				position: absolute;
-				left: -99em;
-			}
-		}
-
-		.toggle {
-			cursor: pointer;
-			display: inline-block;
-			position: relative;
-			width: 90px;
-			height: 50px;
-			background-color: #83D8FF;
-			border-radius: 90px - 6;
-			transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
-
-			&:before {
-				content: 'Light';
-				position: absolute;
-				left: -60px;
-				top: 15px;
-				font-size: 18px;
-				color: var(--primary);
-			}
-
-			&:after {
-				content: 'Dark';
-				position: absolute;
-				right: -58px;
-				top: 15px;
-				font-size: 18px;
-				color: var(--text);
-			}
-		}
-
-		.toggle__handler {
-			display: inline-block;
-			position: relative;
-			z-index: 1;
-			top: 3px;
-			left: 3px;
-			width: 50px - 6;
-			height: 50px - 6;
-			background-color: #FFCF96;
-			border-radius: 50px;
-			box-shadow: 0 2px 6px rgba(0,0,0,.3);
-			transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important;
-			transform:  rotate(-45deg);
-
-			.crater {
-				position: absolute;
-				background-color: #E8CDA5;
-				opacity: 0;
-				transition: opacity 200ms ease-in-out !important;
-				border-radius: 100%;
-			}
-
-			.crater--1 {
-				top: 18px;
-				left: 10px;
-				width: 4px;
-				height: 4px;
-			}
-
-			.crater--2 {
-				top: 28px;
-				left: 22px;
-				width: 6px;
-				height: 6px;
-			}
-
-			.crater--3 {
-				top: 10px;
-				left: 25px;
-				width: 8px;
-				height: 8px;
-			}
-		}
-
-		.star {
-			position: absolute;
-			background-color: #ffffff;
-			transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
-			border-radius: 50%;
-		}
-
-		.star--1 {
-			top: 10px;
-			left: 35px;
-			z-index: 0;
-			width: 30px;
-			height: 3px;
-		}
-
-		.star--2 {
-			top: 18px;
-			left: 28px;
-			z-index: 1;
-			width: 30px;
-			height: 3px;
-		}
-
-		.star--3 {
-			top: 27px;
-			left: 40px;
-			z-index: 0;
-			width: 30px;
-			height: 3px;
-		}
-
-		.star--4,
-		.star--5,
-		.star--6 {
-			opacity: 0;
-			transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
-		}
-
-		.star--4 {
-			top: 16px;
-			left: 11px;
-			z-index: 0;
-			width: 2px;
-			height: 2px;
-			transform: translate3d(3px,0,0);
-		}
-
-		.star--5 {
-			top: 32px;
-			left: 17px;
-			z-index: 0;
-			width: 3px;
-			height: 3px;
-			transform: translate3d(3px,0,0);
-		}
-
-		.star--6 {
-			top: 36px;
-			left: 28px;
-			z-index: 0;
-			width: 2px;
-			height: 2px;
-			transform: translate3d(3px,0,0);
-		}
-
-		input:checked {
-			+ .toggle {
-				background-color: #749DD6;
-
-				&:before {
-					color: var(--text);
-				}
-
-				&:after {
-					color: var(--primary);
-				}
-
-				.toggle__handler {
-					background-color: #FFE5B5;
-					transform: translate3d(40px, 0, 0) rotate(0);
-
-					.crater { opacity: 1; }
-				}
-
-				.star--1 {
-					width: 2px;
-					height: 2px;
-				}
-
-				.star--2 {
-					width: 4px;
-					height: 4px;
-					transform: translate3d(-5px, 0, 0);
-				}
-
-				.star--3 {
-					width: 2px;
-					height: 2px;
-					transform: translate3d(-7px, 0, 0);
-				}
-
-				.star--4,
-				.star--5,
-				.star--6 {
-					opacity: 1;
-					transform: translate3d(0,0,0);
-				}
-				.star--4 {
-					transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
-				}
-				.star--5 {
-					transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
-				}
-				.star--6 {
-					transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
-				}
-			}
-		}
-
-	> a
-		display block
-		margin-top -16px
-		margin-bottom 16px
-
-	> details
-		border-top solid var(--lineWidth) var(--faceDivider)
-
-		> summary
-			padding 16px 0
-
-		> *:last-child
-			margin-bottom 16px
-
-	> .creator
-		> div
-			padding 16px 0
-			border-bottom solid var(--lineWidth) var(--faceDivider)
-</style>
diff --git a/src/client/app/common/views/components/stream-indicator.vue b/src/client/app/common/views/components/stream-indicator.vue
deleted file mode 100644
index 8ab1cfcfeb30ca00c5d2ca9779f761b7fc6a2688..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/stream-indicator.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-<template>
-<div class="mk-stream-indicator">
-	<p v-if="stream.state == 'initializing'">
-		<fa icon="spinner" pulse/>
-		<span>{{ $t('connecting') }}<mk-ellipsis/></span>
-	</p>
-	<p v-if="stream.state == 'reconnecting'">
-		<fa icon="spinner" pulse/>
-		<span>{{ $t('reconnecting') }}<mk-ellipsis/></span>
-	</p>
-	<p v-if="stream.state == 'connected'">
-		<fa icon="check"/>
-		<span>{{ $t('connected') }}</span>
-	</p>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import anime from 'animejs';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/stream-indicator.vue'),
-	computed: {
-		stream() {
-			return this.$root.stream;
-		}
-	},
-	created() {
-		this.$root.stream.on('_connected_', this.onConnected);
-		this.$root.stream.on('_disconnected_', this.onDisconnected);
-
-		this.$nextTick(() => {
-			if (this.stream.state == 'connected') {
-				this.$el.style.opacity = '0';
-			}
-		});
-	},
-	beforeDestroy() {
-		this.$root.stream.off('_connected_', this.onConnected);
-		this.$root.stream.off('_disconnected_', this.onDisconnected);
-	},
-	methods: {
-		onConnected() {
-			setTimeout(() => {
-				anime({
-					targets: this.$el,
-					opacity: 0,
-					easing: 'linear',
-					duration: 200
-				});
-			}, 1000);
-		},
-		onDisconnected() {
-			anime({
-				targets: this.$el,
-				opacity: 1,
-				easing: 'linear',
-				duration: 100
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-stream-indicator
-	pointer-events none
-	position fixed
-	z-index 16384
-	bottom 8px
-	right 8px
-	margin 0
-	padding 6px 12px
-	font-size 0.9em
-	color #fff
-	background rgba(#000, 0.8)
-	border-radius 4px
-
-	> p
-		display block
-		margin 0
-
-		> [data-icon]
-			margin-right 0.25em
-
-</style>
diff --git a/src/client/app/common/views/components/tag-cloud.vue b/src/client/app/common/views/components/tag-cloud.vue
deleted file mode 100644
index 3fa5e3b9d4ebf6e6767dcf5d2f0352081665d2dd..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/tag-cloud.vue
+++ /dev/null
@@ -1,86 +0,0 @@
-<template>
-<div class="jtivnzhfwquxpsfidertopbmwmchmnmo">
-	<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
-	<p class="empty" v-else-if="tags.length == 0"><fa icon="exclamation-circle"/>{{ $t('empty') }}</p>
-	<div v-else>
-		<vue-word-cloud
-				:words="tags.slice(0, 20).map(x => [x.tag, x.count])"
-				:color="color"
-				:spacing="1">
-			<template slot-scope="{word, text, weight}">
-				<div style="cursor: pointer;" :title="weight">
-					{{ text }}
-				</div>
-			</template>
-		</vue-word-cloud>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import * as VueWordCloud from 'vuewordcloud';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/tag-cloud.vue'),
-	components: {
-		[VueWordCloud.name]: VueWordCloud
-	},
-	data() {
-		return {
-			tags: [],
-			fetching: true,
-			clock: null
-		};
-	},
-	mounted() {
-		this.fetch();
-		this.clock = setInterval(this.fetch, 1000 * 60);
-	},
-	beforeDestroy() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		fetch() {
-			this.$root.api('hashtags/trend').then(tags => {
-				this.tags = tags;
-				this.fetching = false;
-			});
-		},
-		color([, weight]) {
-			const peak = Math.max.apply(null, this.tags.map(x => x.count));
-			const w = weight / peak;
-
-			if (w > 0.9) {
-				return this.$store.state.device.darkmode ? '#ff4e69' : '#ff4e69';
-			} else if (w > 0.5) {
-				return this.$store.state.device.darkmode ? '#3bc4c7' : '#3bc4c7';
-			} else {
-				return this.$store.state.device.darkmode ? '#fff' : '#555';
-			}
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.jtivnzhfwquxpsfidertopbmwmchmnmo
-	height 100%
-	width 100%
-
-	> .fetching
-	> .empty
-		margin 0
-		padding 16px
-		text-align center
-		color var(--text)
-
-		> [data-icon]
-			margin-right 4px
-
-	> div
-		height 100%
-		width 100%
-
-</style>
diff --git a/src/client/app/common/views/components/trends.vue b/src/client/app/common/views/components/trends.vue
deleted file mode 100644
index 536d55247cdd6721ff7cb815f678ca8308d9dfb4..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/trends.vue
+++ /dev/null
@@ -1,100 +0,0 @@
-<template>
-<div class="csqvmxybqbycalfhkxvyfrgbrdalkaoc">
-	<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
-	<p class="empty" v-else-if="stats.length == 0"><fa icon="exclamation-circle"/>{{ $t('empty') }}</p>
-	<!-- トランジションを有効にするとなぜかメモリリークする -->
-	<transition-group v-else tag="div" name="chart">
-		<div v-for="stat in stats" :key="stat.tag">
-			<div class="tag">
-				<router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
-				<p>{{ $t('count').replace('{}', stat.usersCount) }}</p>
-			</div>
-			<x-chart class="chart" :src="stat.chart"/>
-		</div>
-	</transition-group>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XChart from './trends.chart.vue';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/trends.vue'),
-	components: {
-		XChart
-	},
-	data() {
-		return {
-			stats: [],
-			fetching: true,
-			clock: null
-		};
-	},
-	mounted() {
-		this.fetch();
-		this.clock = setInterval(this.fetch, 1000 * 60);
-	},
-	beforeDestroy() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		fetch() {
-			this.$root.api('hashtags/trend').then(stats => {
-				this.stats = stats;
-				this.fetching = false;
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.csqvmxybqbycalfhkxvyfrgbrdalkaoc
-	> .fetching
-	> .empty
-		margin 0
-		padding 16px
-		text-align center
-		color var(--text)
-		opacity 0.7
-
-		> [data-icon]
-			margin-right 4px
-
-	> div
-		.chart-move
-			transition transform 1s ease
-
-		> div
-			display flex
-			align-items center
-			padding 14px 16px
-
-			&:not(:last-child)
-				border-bottom solid 1px var(--faceDivider)
-
-			> .tag
-				flex 1
-				overflow hidden
-				font-size 14px
-				color var(--text)
-
-				> a
-					display block
-					width 100%
-					white-space nowrap
-					overflow hidden
-					text-overflow ellipsis
-					color inherit
-
-				> p
-					margin 0
-					font-size 75%
-					opacity 0.7
-
-			> .chart
-				height 30px
-
-</style>
diff --git a/src/client/app/common/views/components/ui/button.vue b/src/client/app/common/views/components/ui/button.vue
deleted file mode 100644
index 59a5c858a724edeb35c4392f65d4da7e7b38bef1..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/ui/button.vue
+++ /dev/null
@@ -1,224 +0,0 @@
-<template>
-<component class="dmtdnykelhudezerjlfpbhgovrgnqqgr"
-	:is="link ? 'a' : 'button'"
-	:class="{ inline, primary, wait, round: $store.state.device.roundedCorners }"
-	:type="type"
-	@click="$emit('click')"
-	@mousedown="onMousedown"
->
-	<div ref="ripples" class="ripples"></div>
-	<div class="content">
-		<slot></slot>
-	</div>
-</component>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	inject: {
-		horizonGrouped: {
-			default: false
-		}
-	},
-	props: {
-		type: {
-			type: String,
-			required: false
-		},
-		primary: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		inline: {
-			type: Boolean,
-			required: false,
-			default(): boolean {
-				return this.horizonGrouped;
-			}
-		},
-		link: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		autofocus: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		wait: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-	mounted() {
-		if (this.autofocus) {
-			this.$nextTick(() => {
-				this.$el.focus();
-			});
-		}
-	},
-	methods: {
-		onMousedown(e: MouseEvent) {
-			function distance(p, q) {
-				return Math.hypot(p.x - q.x, p.y - q.y);
-			}
-
-			function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY) {
-				const origin = {x: circleCenterX, y: circleCenterY};
-				const dist1 = distance({x: 0, y: 0}, origin);
-				const dist2 = distance({x: boxW, y: 0}, origin);
-				const dist3 = distance({x: 0, y: boxH}, origin);
-				const dist4 = distance({x: boxW, y: boxH }, origin);
-				return Math.max(dist1, dist2, dist3, dist4) * 2;
-			}
-
-			const rect = e.target.getBoundingClientRect();
-
-			const ripple = document.createElement('div');
-			ripple.style.top = (e.clientY - rect.top - 1).toString() + 'px';
-			ripple.style.left = (e.clientX - rect.left - 1).toString() + 'px';
-
-			this.$refs.ripples.appendChild(ripple);
-
-			const circleCenterX = e.clientX - rect.left;
-			const circleCenterY = e.clientY - rect.top;
-
-			const scale = calcCircleScale(e.target.clientWidth, e.target.clientHeight, circleCenterX, circleCenterY);
-
-			setTimeout(() => {
-				ripple.style.transform = 'scale(' + (scale / 2) + ')';
-			}, 1);
-			setTimeout(() => {
-				ripple.style.transition = 'all 1s ease';
-				ripple.style.opacity = '0';
-			}, 1000);
-			setTimeout(() => {
-				if (this.$refs.ripples) this.$refs.ripples.removeChild(ripple);
-			}, 2000);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.dmtdnykelhudezerjlfpbhgovrgnqqgr
-	display block
-	width 100%
-	margin 0
-	padding 8px 10px
-	text-align center
-	font-weight normal
-	font-size 14px
-	line-height 24px
-	border none
-	outline none
-	box-shadow none
-	text-decoration none
-	user-select none
-	color var(--text)
-	background var(--buttonBg)
-
-	&.round
-		border-radius 6px
-
-	&:not(:disabled):hover
-		background var(--buttonHoverBg)
-
-	&:not(:disabled):active
-		background var(--buttonActiveBg)
-
-	&.primary
-		color var(--primaryForeground)
-		background var(--primary)
-
-		&:not(:disabled):hover
-			background var(--primaryLighten5)
-
-		&:not(:disabled):active
-			background var(--primaryDarken5)
-
-	*
-		pointer-events none
-		user-select none
-
-	&:disabled
-		opacity 0.7
-
-	&:focus
-		&:after
-			content ""
-			pointer-events none
-			position absolute
-			top -5px
-			right -5px
-			bottom -5px
-			left -5px
-			border 2px solid var(--primaryAlpha03)
-
-	&.round:focus:after
-		border-radius 10px
-
-	&:not(.inline) + .dmtdnykelhudezerjlfpbhgovrgnqqgr
-		margin-top 16px
-
-	&.inline
-		display inline-block
-		width auto
-		min-width 100px
-
-	&.primary
-		font-weight bold
-
-	&.wait
-		background linear-gradient(
-			45deg,
-			var(--primaryDarken10) 25%,
-			var(--primary)              25%,
-			var(--primary)              50%,
-			var(--primaryDarken10) 50%,
-			var(--primaryDarken10) 75%,
-			var(--primary)              75%,
-			var(--primary)
-		)
-		background-size 32px 32px
-		animation stripe-bg 1.5s linear infinite
-		opacity 0.7
-		cursor wait
-
-		@keyframes stripe-bg
-			from {background-position: 0 0;}
-			to   {background-position: -64px 32px;}
-
-	> .ripples
-		position absolute
-		z-index 0
-		top 0
-		left 0
-		width 100%
-		height 100%
-		overflow hidden
-
-		>>> div
-			position absolute
-			width 2px
-			height 2px
-			border-radius 100%
-			background rgba(0, 0, 0, 0.1)
-			opacity 1
-			transform scale(1)
-			transition all 0.5s cubic-bezier(0, .5, .5, 1)
-
-	&.round > .ripples
-		border-radius 6px
-
-	&.primary > .ripples >>> div
-		background rgba(0, 0, 0, 0.15)
-
-	> .content
-		z-index 1
-
-</style>
diff --git a/src/client/app/common/views/components/ui/card.vue b/src/client/app/common/views/components/ui/card.vue
deleted file mode 100644
index a83013f5d0105678af9e1e6dcce2eb3e7ef3103f..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/ui/card.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-<template>
-<div class="ui-card" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
-	<header>
-		<slot name="title"></slot>
-	</header>
-
-	<slot></slot>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	provide() {
-		return {
-			isCardChild: true
-		};
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.ui-card
-	margin 16px
-	max-width 850px
-	color var(--faceText)
-	background var(--face)
-
-	&.round
-		border-radius 6px
-
-	&.shadow
-		box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12)
-
-	> header
-		padding 16px
-		font-weight bold
-		font-size 20px
-		color var(--faceText)
-
-		@media (min-width 500px)
-			padding 24px 32px
-
-	> section
-		padding 20px 16px
-		border-top solid var(--lineWidth) var(--faceDivider)
-
-		@media (min-width 500px)
-			padding 32px
-
-		&.fit-top
-			padding-top 0
-
-		&.fit-bottom
-			padding-bottom 0
-
-		> header
-			margin-bottom 16px
-			font-weight bold
-			color var(--faceText)
-
-		> section
-			margin 16px 0
-
-			> header
-				font-weight bold
-				color var(--text)
-
-</style>
diff --git a/src/client/app/common/views/components/ui/form.vue b/src/client/app/common/views/components/ui/form.vue
deleted file mode 100644
index 5c5bbd7256feba52b75d5c276c4cf3c4e206e472..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/ui/form.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-<template>
-<div class="ui-form">
-	<fieldset :disabled="disabled">
-		<slot></slot>
-	</fieldset>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	props: {
-		disabled: {
-			type: Boolean,
-			required: false
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-
-
-.ui-form
-	> fieldset
-		margin 0
-		padding 0
-		border none
-
-</style>
diff --git a/src/client/app/common/views/components/ui/form/button.vue b/src/client/app/common/views/components/ui/form/button.vue
deleted file mode 100644
index 3fd7b476298a6c4a50e4e163e444f0290064a294..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/ui/form/button.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-<template>
-<div class="nvemkhtwcnnpkdrwfcbzuwhfulejhmzg" :class="{ round, primary }">
-	<button @click="$emit('click')">
-		<slot></slot>
-	</button>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	props: {
-		round: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		primary: {
-			type: Boolean,
-			required: false,
-			default: false
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.nvemkhtwcnnpkdrwfcbzuwhfulejhmzg
-	display inline-block
-
-	& + .nvemkhtwcnnpkdrwfcbzuwhfulejhmzg
-		margin-left 12px
-
-	> button
-		display inline-block
-		margin 0
-		padding 12px 20px
-		font-size 14px
-		border 1px solid var(--formButtonBorder)
-		border-radius 4px
-		outline none
-		box-shadow none
-		color var(--text)
-		transition 0.1s
-
-		*
-			pointer-events none
-
-		&:hover
-		&:focus
-			color var(--primary)
-			background var(--formButtonHoverBg)
-			border-color var(--formButtonHoverBorder)
-
-		&:active
-			color var(--primaryDarken20)
-			background var(--formButtonActiveBg)
-			border-color var(--primary)
-			transition all 0s
-
-	&.primary
-		> button
-			border 1px solid var(--primary)
-			background var(--primary)
-			color var(--primaryForeground)
-
-			&:hover
-			&:focus
-				background var(--primaryLighten20)
-				border-color var(--primaryLighten20)
-
-			&:active
-				background var(--primaryDarken20)
-				border-color var(--primaryDarken20)
-				transition all 0s
-
-	&.round
-		> button
-			border-radius 64px
-
-</style>
diff --git a/src/client/app/common/views/components/ui/form/radio.vue b/src/client/app/common/views/components/ui/form/radio.vue
deleted file mode 100644
index 396b2997e51ca8e19d2194588d33a58df2ce9d1e..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/ui/form/radio.vue
+++ /dev/null
@@ -1,118 +0,0 @@
-<template>
-<div
-	class="uywduthvrdnlpsvsjkqigicixgyfctto"
-	:class="{ disabled, checked }"
-	:aria-checked="checked"
-	:aria-disabled="disabled"
-	@click="toggle"
->
-	<input type="radio"
-		:disabled="disabled"
-	>
-	<span class="button">
-		<span></span>
-	</span>
-	<span class="label"><slot></slot></span>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	model: {
-		prop: 'model',
-		event: 'change'
-	},
-	props: {
-		model: {
-			required: false
-		},
-		value: {
-			required: false
-		},
-		disabled: {
-			type: Boolean,
-			default: false
-		}
-	},
-	computed: {
-		checked(): boolean {
-			return this.model === this.value;
-		}
-	},
-	methods: {
-		toggle() {
-			this.$emit('change', this.value);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.uywduthvrdnlpsvsjkqigicixgyfctto
-	display inline-flex
-	margin 0 16px 0 0
-	cursor pointer
-	transition all 0.3s
-
-	> *
-		user-select none
-
-	&:hover
-		> .button
-			border solid 2px var(--inputLabel)
-
-	&.disabled
-		opacity 0.6
-		cursor not-allowed
-
-	&.checked
-		> .button
-			border-color var(--primary)
-
-			&:after
-				background-color var(--primary)
-				transform scale(1)
-				opacity 1
-
-		> .label
-			color var(--primary)
-
-	> input
-		position absolute
-		width 0
-		height 0
-		opacity 0
-		margin 0
-
-	> .button
-		display inline-block
-		flex-shrink 0
-		width 20px
-		height 20px
-		background none
-		border solid 2px var(--radioBorder)
-		border-radius 100%
-		transition inherit
-
-		&:after
-			content ''
-			display block
-			position absolute
-			top 3px
-			right 3px
-			bottom 3px
-			left 3px
-			border-radius 100%
-			opacity 0
-			transform scale(0)
-			transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
-
-	> .label
-		margin-left 8px
-		display block
-		font-size 14px
-		line-height 20px
-		cursor pointer
-
-</style>
diff --git a/src/client/app/common/views/components/ui/horizon-group.vue b/src/client/app/common/views/components/ui/horizon-group.vue
deleted file mode 100644
index 33d0300101d8566ed82a5ebd5a16a4a95f830bee..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/ui/horizon-group.vue
+++ /dev/null
@@ -1,76 +0,0 @@
-<template>
-<div class="vnxwkwuf" :class="{ inputs, noGrow }" :data-children-count="children">
-	<slot></slot>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	provide: {
-		horizonGrouped: true
-	},
-	props: {
-		inputs: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		noGrow: {
-			type: Boolean,
-			required: false,
-			default: false
-		}
-	},
-	data() {
-		return {
-			children: 0
-		};
-	},
-	mounted() {
-		this.$nextTick(() => {
-			this.children = this.$slots.default.length;
-		});
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.vnxwkwuf
-	margin 16px 0
-
-	&.inputs
-		margin 32px 0
-
-	&.fit-top
-		margin-top 0
-
-	&.fit-bottom
-		margin-bottom 0
-
-	&:not(.noGrow)
-		display flex
-
-		> *
-			flex 1
-			min-width 0 !important
-
-	> *:not(:last-child)
-		margin-right 16px !important
-
-	&[data-children-count="3"]
-		@media (max-width 600px)
-			display block
-
-			> *
-				display block
-				width 100% !important
-				margin 16px 0 !important
-
-				&:first-child
-					margin-top 0 !important
-
-				&:last-child
-					margin-bottom 0 !important
-
-</style>
diff --git a/src/client/app/common/views/components/ui/info.vue b/src/client/app/common/views/components/ui/info.vue
deleted file mode 100644
index 30fd8cb344e24b4c8ba147058b711848a3f065fd..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/ui/info.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<template>
-<div class="ymxyweixqwsxauxldgpvecjepnwxbylu" :class="{ warn }">
-	<i v-if="warn"><fa icon="exclamation-triangle"/></i>
-	<i v-else><fa icon="info-circle"/></i>
-	<slot></slot>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	props: {
-		warn: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-.ymxyweixqwsxauxldgpvecjepnwxbylu
-	margin 16px 0
-	padding 16px
-	font-size 90%
-	background var(--infoBg)
-	color var(--infoFg)
-
-	&.warn
-		background var(--infoWarnBg)
-		color var(--infoWarnFg)
-
-	&:first-child
-		margin-top 0
-
-	&:last-child
-		margin-bottom 0
-
-	> i
-		margin-right 4px
-
-</style>
diff --git a/src/client/app/common/views/components/ui/input.vue b/src/client/app/common/views/components/ui/input.vue
deleted file mode 100644
index 1b339a9ae0052de4ed396668af67b4d7e037258f..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/ui/input.vue
+++ /dev/null
@@ -1,503 +0,0 @@
-<template>
-<div class="ui-input" :class="[{ focused, filled, inline, disabled }, styl]">
-	<div class="icon" ref="icon"><slot name="icon"></slot></div>
-	<div class="input">
-		<div class="password-meter" v-if="withPasswordMeter" v-show="passwordStrength != ''" :data-strength="passwordStrength">
-			<div class="value" ref="passwordMetar"></div>
-		</div>
-		<span class="label" ref="label"><slot></slot></span>
-		<span class="title" ref="title">
-			<slot name="title"></slot>
-			<span class="warning" v-if="invalid"><fa :icon="['fa', 'exclamation-circle']"/>{{ $refs.input.validationMessage }}</span>
-		</span>
-		<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
-		<template v-if="type != 'file'">
-			<input v-if="debounce" ref="input"
-				v-debounce="500"
-				:type="type"
-				v-model.lazy="v"
-				:disabled="disabled"
-				:required="required"
-				:readonly="readonly"
-				:placeholder="placeholder"
-				:pattern="pattern"
-				:autocomplete="autocomplete"
-				:spellcheck="spellcheck"
-				@focus="focused = true"
-				@blur="focused = false"
-				@keydown="$emit('keydown', $event)"
-				@change="$emit('change', $event)"
-				:list="id"
-			>
-			<input v-else ref="input"
-				:type="type"
-				v-model="v"
-				:disabled="disabled"
-				:required="required"
-				:readonly="readonly"
-				:placeholder="placeholder"
-				:pattern="pattern"
-				:autocomplete="autocomplete"
-				:spellcheck="spellcheck"
-				@focus="focused = true"
-				@blur="focused = false"
-				@keydown="$emit('keydown', $event)"
-				@change="$emit('change', $event)"
-				:list="id"
-			>
-			<datalist :id="id" v-if="datalist">
-				<option v-for="data in datalist" :value="data"/>
-			</datalist>
-		</template>
-		<template v-else>
-			<input ref="input"
-				type="text"
-				:value="filePlaceholder"
-				readonly
-				@click="chooseFile"
-			>
-			<input ref="file"
-				type="file"
-				:value="value"
-				@change="onChangeFile"
-			>
-		</template>
-		<div class="suffix" ref="suffix"><slot name="suffix"></slot></div>
-	</div>
-	<div class="toggle" v-if="withPasswordToggle">
-		<a @click="togglePassword">
-			<span v-if="type == 'password'"><fa :icon="['fa', 'eye']"/> {{ $t('@.show-password') }}</span>
-			<span v-if="type != 'password'"><fa :icon="['far', 'eye-slash']"/> {{ $t('@.hide-password') }}</span>
-		</a>
-	</div>
-	<div class="desc"><slot name="desc"></slot></div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import debounce from 'v-debounce';
-const getPasswordStrength = require('syuilo-password-strength');
-
-export default Vue.extend({
-	directives: {
-		debounce
-	},
-	inject: {
-		horizonGrouped: {
-			default: false
-		}
-	},
-	props: {
-		value: {
-			required: false
-		},
-		type: {
-			type: String,
-			required: false
-		},
-		required: {
-			type: Boolean,
-			required: false
-		},
-		readonly: {
-			type: Boolean,
-			required: false
-		},
-		disabled: {
-			type: Boolean,
-			required: false
-		},
-		pattern: {
-			type: String,
-			required: false
-		},
-		placeholder: {
-			type: String,
-			required: false
-		},
-		autofocus: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		autocomplete: {
-			required: false
-		},
-		spellcheck: {
-			required: false
-		},
-		debounce: {
-			required: false
-		},
-		withPasswordMeter: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		withPasswordToggle: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		datalist: {
-			type: Array,
-			required: false,
-		},
-		inline: {
-			type: Boolean,
-			required: false,
-			default(): boolean {
-				return this.horizonGrouped;
-			}
-		},
-		styl: {
-			type: String,
-			required: false,
-			default: 'line'
-		}
-	},
-	data() {
-		return {
-			v: this.value,
-			focused: false,
-			invalid: false,
-			passwordStrength: '',
-			id: Math.random().toString()
-		};
-	},
-	computed: {
-		filled(): boolean {
-			return this.v != '' && this.v != null;
-		},
-		filePlaceholder(): string {
-			if (this.type != 'file') return null;
-			if (this.v == null) return null;
-
-			if (typeof this.v == 'string') return this.v;
-
-			if (Array.isArray(this.v)) {
-				return this.v.map(file => file.name).join(', ');
-			} else {
-				return this.v.name;
-			}
-		}
-	},
-	watch: {
-		value(v) {
-			this.v = v;
-		},
-		v(v) {
-			if (this.type === 'number') {
-				this.$emit('input', parseInt(v, 10));
-			} else {
-				this.$emit('input', v);
-			}
-
-			if (this.withPasswordMeter) {
-				if (v == '') {
-					this.passwordStrength = '';
-					return;
-				}
-
-				const strength = getPasswordStrength(v);
-				this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
-				(this.$refs.passwordMetar as any).style.width = `${strength * 100}%`;
-			}
-
-			this.invalid = this.$refs.input.validity.badInput;
-		}
-	},
-	mounted() {
-		if (this.autofocus) {
-			this.$nextTick(() => {
-				this.$refs.input.focus();
-			});
-		}
-
-		this.$nextTick(() => {
-			// このコンポーネントが作成された時、非表示状態である場合がある
-			// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
-			const clock = setInterval(() => {
-				if (this.$refs.prefix) {
-					this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
-					if (this.$refs.prefix.offsetWidth) {
-						this.$refs.input.style.paddingLeft = this.$refs.prefix.offsetWidth + 'px';
-					}
-				}
-				if (this.$refs.suffix) {
-					if (this.$refs.suffix.offsetWidth) {
-						this.$refs.input.style.paddingRight = this.$refs.suffix.offsetWidth + 'px';
-					}
-				}
-			}, 100);
-
-			this.$once('hook:beforeDestroy', () => {
-				clearInterval(clock);
-			});
-		});
-
-		this.$on('keydown', (e: KeyboardEvent) => {
-			if (e.code == 'Enter') {
-				this.$emit('enter');
-			}
-		});
-	},
-	methods: {
-		focus() {
-			this.$refs.input.focus();
-		},
-		togglePassword() {
-			if (this.type == 'password') {
-				this.type = 'text'
-			} else {
-				this.type = 'password'
-			}
-		},
-		chooseFile() {
-			this.$refs.file.click();
-		},
-		onChangeFile() {
-			this.v = Array.from((this.$refs.file as any).files);
-			this.$emit('input', this.v);
-			this.$emit('change', this.v);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-root(fill)
-	margin 32px 0
-
-	> .icon
-		position absolute
-		top 0
-		left 0
-		width 24px
-		text-align center
-		line-height 32px
-		color var(--inputLabel)
-
-		&:not(:empty) + .input
-			margin-left 28px
-
-	> .input
-
-		if !fill
-			&:before
-				content ''
-				display block
-				position absolute
-				bottom 0
-				left 0
-				right 0
-				height 1px
-				background var(--inputBorder)
-
-			&:after
-				content ''
-				display block
-				position absolute
-				bottom 0
-				left 0
-				right 0
-				height 2px
-				background var(--primary)
-				opacity 0
-				transform scaleX(0.12)
-				transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)
-				will-change border opacity transform
-
-		> .password-meter
-			position absolute
-			top 0
-			left 0
-			width 100%
-			height 100%
-			border-radius 6px
-			overflow hidden
-			opacity 0.3
-
-			&[data-strength='']
-				display none
-
-			&[data-strength='low']
-				> .value
-					background #d73612
-
-			&[data-strength='medium']
-				> .value
-					background #d7ca12
-
-			&[data-strength='high']
-				> .value
-					background #61bb22
-
-			> .value
-				display block
-				width 0
-				height 100%
-				background transparent
-				border-radius 6px
-				transition all 0.1s ease
-
-		> .label
-			position absolute
-			z-index 1
-			top fill ? 6px : 0
-			left 0
-			pointer-events none
-			transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
-			transition-duration 0.3s
-			font-size 16px
-			line-height 32px
-			color var(--inputLabel)
-			pointer-events none
-			//will-change transform
-			transform-origin top left
-			transform scale(1)
-
-		> .title
-			position absolute
-			z-index 1
-			top fill ? -24px : -17px
-			left 0 !important
-			pointer-events none
-			font-size 16px
-			line-height 32px
-			color var(--inputLabel)
-			pointer-events none
-			//will-change transform
-			transform-origin top left
-			transform scale(.75)
-			white-space nowrap
-			width 133%
-			overflow hidden
-			text-overflow ellipsis
-
-			> .warning
-				margin-left 0.5em
-				color var(--infoWarnFg)
-
-				> svg
-					margin-right 0.1em
-
-		> input
-			display block
-			width 100%
-			margin 0
-			padding 0
-			font inherit
-			font-weight fill ? bold : normal
-			font-size 16px
-			line-height 32px
-			color var(--inputText)
-			background transparent
-			border none
-			border-radius 0
-			outline none
-			box-shadow none
-
-			if fill
-				padding 6px 12px
-				background rgba(#000, 0.035)
-				border-radius 6px
-
-			&[type='file']
-				display none
-
-		> .prefix
-		> .suffix
-			display block
-			position absolute
-			z-index 1
-			top 0
-			font-size 16px
-			line-height fill ? 44px : 32px
-			color var(--inputLabel)
-			pointer-events none
-
-			&:empty
-				display none
-
-			> *
-				display inline-block
-				min-width 16px
-				max-width 150px
-				overflow hidden
-				white-space nowrap
-				text-overflow ellipsis
-
-		> .prefix
-			left 0
-			padding-right 4px
-
-			if fill
-				padding-left 12px
-
-		> .suffix
-			right 0
-			padding-left 4px
-
-			if fill
-				padding-right 12px
-
-	> .toggle
-		cursor pointer
-		padding-left 0.5em
-		font-size 0.7em
-		opacity 0.7
-		text-align left
-
-		> a
-			color var(--inputLabel)
-			text-decoration none
-
-	> .desc
-		margin 6px 0
-		font-size 13px
-
-		&:empty
-			display none
-
-		*
-			margin 0
-
-	&.focused
-		> .input
-			if fill
-				background rgba(#000, 0.05)
-			else
-				&:after
-					opacity 1
-					transform scaleX(1)
-
-			> .label
-				color var(--primary)
-
-	&.focused
-	&.filled
-		> .input
-			> .label
-				top fill ? -24px : -17px
-				left 0 !important
-				transform scale(0.75)
-
-.ui-input
-	&.fill
-		root(true)
-	&:not(.fill)
-		root(false)
-
-	&.inline
-		display inline-block
-		margin 0
-
-	&.disabled
-		opacity 0.7
-
-		&, *
-			cursor not-allowed !important
-
-</style>
diff --git a/src/client/app/common/views/components/ui/margin.vue b/src/client/app/common/views/components/ui/margin.vue
deleted file mode 100644
index 508116f07005125c692e29a3786cb1a25c449716..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/ui/margin.vue
+++ /dev/null
@@ -1,16 +0,0 @@
-<template>
-<div class="zdcrxcne">
-	<slot></slot>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({});
-</script>
-
-<style lang="stylus" scoped>
-.zdcrxcne
-	margin 16px
-
-</style>
diff --git a/src/client/app/common/views/components/ui/modal.vue b/src/client/app/common/views/components/ui/modal.vue
deleted file mode 100644
index 413dc39fa5de78a9582993970b3bb7e3741d1399..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/ui/modal.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-<template>
-<div class="modal">
-	<div class="bg" ref="bg" @click="onBgClick" />
-	<slot class="main" />
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import anime from 'animejs';
-
-export default Vue.extend({
-	props: {
-		closeOnBgClick: {
-			type: Boolean,
-			required: false,
-			default: true
-		},
-		openAnimeDuration: {
-			type: Number,
-			required: false,
-			default: 100
-		},
-		closeAnimeDuration: {
-			type: Number,
-			required: false,
-			default: 100
-		}
-	},
-	mounted() {
-		anime({
-			targets: this.$refs.bg,
-			opacity: 1,
-			duration: this.openAnimeDuration,
-			easing: 'linear'
-		});
-	},
-	methods: {
-		onBgClick() {
-			this.$emit('bg-click');
-			if (this.closeOnBgClick) this.close();
-		},
-		close() {
-			this.$emit('before-close');
-
-			anime({
-				targets: this.$refs.bg,
-				opacity: 0,
-				duration: this.closeAnimeDuration,
-				easing: 'linear',
-				complete: () => (this as any).destroyDom()
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.modal
-	position fixed
-	z-index 2048
-	top 0
-	left 0
-	width 100%
-	height 100%
-
-.bg
-	display block
-	position fixed
-	z-index 1
-	top 0
-	left 0
-	width 100%
-	height 100%
-	background rgba(#000, 0.7)
-	opacity 0
-
-.main
-	z-index 1
-</style>
diff --git a/src/client/app/common/views/components/ui/pagination.vue b/src/client/app/common/views/components/ui/pagination.vue
deleted file mode 100644
index 67aa89d369027de2638a4d56c3fa577b4852fa3c..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/ui/pagination.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<template>
-<div class="mwermpua" v-if="!fetching">
-	<sequential-entrance animation="entranceFromTop" delay="25">
-		<slot :items="items"></slot>
-	</sequential-entrance>
-	<div class="more" v-if="more">
-		<ui-button @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import paging from '../../../scripts/paging';
-
-export default Vue.extend({
-	mixins: [
-		paging({
-			captureWindowScroll: false,
-		}),
-	],
-
-	props: {
-		pagination: {
-			required: true
-		},
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-.mwermpua
-	> .more
-		margin-top 16px
-
-</style>
diff --git a/src/client/app/common/views/components/ui/radio.vue b/src/client/app/common/views/components/ui/radio.vue
deleted file mode 100644
index 468318b58ea08a18ab30d0811b7c30b90246f15b..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/ui/radio.vue
+++ /dev/null
@@ -1,110 +0,0 @@
-<template>
-<div
-	class="ui-radio"
-	:class="{ disabled, checked }"
-	:aria-checked="checked"
-	:aria-disabled="disabled"
-	@click="toggle"
->
-	<input type="radio"
-		:disabled="disabled"
-	>
-	<span class="button">
-		<span></span>
-	</span>
-	<span class="label"><slot></slot></span>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	model: {
-		prop: 'model',
-		event: 'change'
-	},
-	props: {
-		model: {
-			required: false
-		},
-		value: {
-			required: false
-		},
-		disabled: {
-			type: Boolean,
-			default: false
-		}
-	},
-	computed: {
-		checked(): boolean {
-			return this.model === this.value;
-		}
-	},
-	methods: {
-		toggle() {
-			this.$emit('change', this.value);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.ui-radio
-	display inline-block
-	margin 0 32px 0 0
-	cursor pointer
-	transition all 0.3s
-
-	> *
-		user-select none
-
-	&.disabled
-		opacity 0.6
-		cursor not-allowed
-
-	&.checked
-		> .button
-			border-color var(--radioActive)
-
-			&:after
-				background-color var(--radioActive)
-				transform scale(1)
-				opacity 1
-
-	> input
-		position absolute
-		width 0
-		height 0
-		opacity 0
-		margin 0
-
-	> .button
-		position absolute
-		width 20px
-		height 20px
-		background none
-		border solid 2px var(--inputLabel)
-		border-radius 100%
-		transition inherit
-
-		&:after
-			content ''
-			display block
-			position absolute
-			top 3px
-			right 3px
-			bottom 3px
-			left 3px
-			border-radius 100%
-			opacity 0
-			transform scale(0)
-			transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
-
-	> .label
-		margin-left 28px
-		display block
-		font-size 16px
-		line-height 20px
-		cursor pointer
-
-</style>
diff --git a/src/client/app/common/views/components/ui/select.vue b/src/client/app/common/views/components/ui/select.vue
deleted file mode 100644
index 1057d60d07ffdc977efaa059acacc6df51d8bfa2..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/ui/select.vue
+++ /dev/null
@@ -1,238 +0,0 @@
-<template>
-<div class="ui-select" :class="[{ focused, disabled, filled, inline }, styl]">
-	<div class="icon" ref="icon"><slot name="icon"></slot></div>
-	<div class="input" @click="focus">
-		<span class="label" ref="label"><slot name="label"></slot></span>
-		<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
-		<select ref="input"
-			v-model="v"
-			:required="required"
-			:disabled="disabled"
-			@focus="focused = true"
-			@blur="focused = false"
-		>
-			<slot></slot>
-		</select>
-		<div class="suffix"><slot name="suffix"></slot></div>
-	</div>
-	<div class="text"><slot name="text"></slot></div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	inject: {
-		horizonGrouped: {
-			default: false
-		}
-	},
-	props: {
-		value: {
-			required: false
-		},
-		required: {
-			type: Boolean,
-			required: false
-		},
-		disabled: {
-			type: Boolean,
-			required: false
-		},
-		styl: {
-			type: String,
-			required: false,
-			default: 'line'
-		},
-		inline: {
-			type: Boolean,
-			required: false,
-			default(): boolean {
-				return this.horizonGrouped;
-			}
-		},
-	},
-	data() {
-		return {
-			focused: false
-		};
-	},
-	computed: {
-		v: {
-			get() {
-				return this.value;
-			},
-			set(v) {
-				this.$emit('input', v);
-			}
-		},
-		filled(): boolean {
-			return this.v != '' && this.v != null;
-		}
-	},
-	mounted() {
-		if (this.$refs.prefix) {
-			this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
-		}
-	},
-	methods: {
-		focus() {
-			this.$refs.input.focus();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-root(fill)
-	margin 32px 0
-
-	> .icon
-		position absolute
-		top 0
-		left 0
-		width 24px
-		text-align center
-		line-height 32px
-		color var(--inputLabel)
-
-		&:not(:empty) + .input
-			margin-left 28px
-
-	> .input
-		display flex
-
-		if fill
-			padding 6px 12px
-			background rgba(#000, 0.035)
-			border-radius 6px
-		else
-			&:before
-				content ''
-				display block
-				position absolute
-				bottom 0
-				left 0
-				right 0
-				height 1px
-				background var(--inputBorder)
-
-			&:after
-				content ''
-				display block
-				position absolute
-				bottom 0
-				left 0
-				right 0
-				height 2px
-				background var(--primary)
-				opacity 0
-				transform scaleX(0.12)
-				transition border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)
-				will-change border opacity transform
-
-		> .label
-			position absolute
-			top fill ? 6px : 0
-			left 0
-			pointer-events none
-			transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
-			transition-duration 0.3s
-			font-size 16px
-			line-height 32px
-			color var(--inputLabel)
-			pointer-events none
-			//will-change transform
-			transform-origin top left
-			transform scale(1)
-
-		> select
-			display block
-			flex 1
-			width 100%
-			padding 0
-			font inherit
-			font-weight fill ? bold : normal
-			font-size 16px
-			height 32px
-			color var(--inputText)
-			background transparent
-			border none
-			border-radius 0
-			outline none
-			box-shadow none
-
-			*
-				color #000
-
-		> .prefix
-		> .suffix
-			display block
-			align-self center
-			justify-self center
-			font-size 16px
-			line-height 32px
-			color rgba(#000, 0.54)
-			pointer-events none
-
-			&:empty
-				display none
-
-			> *
-				display block
-				min-width 16px
-
-		> .prefix
-			padding-right 4px
-
-		> .suffix
-			padding-left 4px
-
-	> .text
-		margin 6px 0
-		font-size 13px
-
-		&:empty
-			display none
-
-		*
-			margin 0
-
-	&.focused
-		> .input
-			if fill
-				background rgba(#000, 0.05)
-			else
-				&:after
-					opacity 1
-					transform scaleX(1)
-
-			> .label
-				color var(--primary)
-
-	&.focused
-	&.filled
-		> .input
-			> .label
-				top fill ? -24px : -17px
-				left 0 !important
-				transform scale(0.75)
-
-.ui-select
-	&.fill
-		root(true)
-	&:not(.fill)
-		root(false)
-
-	&.inline
-		display inline-block
-		margin 0
-
-	&.disabled
-		opacity 0.7
-
-		&, *
-			cursor not-allowed !important
-
-</style>
diff --git a/src/client/app/common/views/components/ui/switch.vue b/src/client/app/common/views/components/ui/switch.vue
deleted file mode 100644
index 8e3997ae78a5c20989388d1b6a64a2c43632f31a..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/ui/switch.vue
+++ /dev/null
@@ -1,135 +0,0 @@
-<template>
-<div
-	class="ui-switch"
-	:class="{ disabled, checked }"
-	role="switch"
-	:aria-checked="checked"
-	:aria-disabled="disabled"
-	@click="toggle"
->
-	<input
-		type="checkbox"
-		ref="input"
-		:disabled="disabled"
-		@keydown.enter="toggle"
-	>
-	<span class="button">
-		<span></span>
-	</span>
-	<span class="label">
-		<span :aria-hidden="!checked"><slot></slot></span>
-		<p :aria-hidden="!checked">
-			<slot name="desc"></slot>
-		</p>
-	</span>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	model: {
-		prop: 'value',
-		event: 'change'
-	},
-	props: {
-		value: {
-			type: Boolean,
-			default: false
-		},
-		disabled: {
-			type: Boolean,
-			default: false
-		}
-	},
-	computed: {
-		checked(): boolean {
-			return this.value;
-		}
-	},
-	methods: {
-		toggle() {
-			if (this.disabled) return;
-			this.$emit('change', !this.checked);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.ui-switch
-	display flex
-	margin 32px 0
-	cursor pointer
-	transition all 0.3s
-
-	&:first-child
-		margin-top 0
-
-	&:last-child
-		margin-bottom 0
-
-	> *
-		user-select none
-
-	&.disabled
-		opacity 0.6
-		cursor not-allowed
-
-	&.checked
-		> .button
-			background-color var(--switchActiveTrack)
-			border-color var(--switchActiveTrack)
-
-			> *
-				background-color var(--switchActive)
-				transform translateX(14px)
-
-	> input
-		position absolute
-		width 0
-		height 0
-		opacity 0
-		margin 0
-
-	> .button
-		display inline-block
-		flex-shrink 0
-		margin 3px 0 0 0
-		width 34px
-		height 14px
-		background var(--switchTrack)
-		outline none
-		border-radius 14px
-		transition inherit
-
-		> *
-			position absolute
-			top -3px
-			left 0
-			border-radius 100%
-			transition background-color 0.3s, transform 0.3s
-			width 20px
-			height 20px
-			background-color #fff
-			box-shadow 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12)
-
-	> .label
-		margin-left 8px
-		display block
-		font-size 16px
-		cursor pointer
-		transition inherit
-		color var(--text)
-
-		> span
-			display block
-			line-height 20px
-			transition inherit
-
-		> p
-			margin 0
-			opacity 0.7
-			font-size 90%
-
-</style>
diff --git a/src/client/app/common/views/components/ui/textarea.vue b/src/client/app/common/views/components/ui/textarea.vue
deleted file mode 100644
index d265c7ac6d0893c9d6738321232cc02f230e240a..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/ui/textarea.vue
+++ /dev/null
@@ -1,194 +0,0 @@
-<template>
-<div class="ui-textarea" :class="{ focused, filled, tall, pre }">
-	<div class="input">
-		<span class="label" ref="label"><slot></slot></span>
-		<textarea ref="input"
-			:value="value"
-			:required="required"
-			:readonly="readonly"
-			:pattern="pattern"
-			:autocomplete="autocomplete"
-			@input="$emit('input', $event.target.value)"
-			@focus="focused = true"
-			@blur="focused = false"
-		></textarea>
-	</div>
-	<div class="desc"><slot name="desc"></slot></div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: {
-		value: {
-			required: false
-		},
-		required: {
-			type: Boolean,
-			required: false
-		},
-		readonly: {
-			type: Boolean,
-			required: false
-		},
-		pattern: {
-			type: String,
-			required: false
-		},
-		autocomplete: {
-			type: String,
-			required: false
-		},
-		tall: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		pre: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-	data() {
-		return {
-			focused: false,
-			passwordStrength: ''
-		}
-	},
-	computed: {
-		filled(): boolean {
-			return this.value != '' && this.value != null;
-		}
-	},
-	methods: {
-		focus() {
-			this.$refs.input.focus();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-root(fill)
-	margin 42px 0 32px 0
-
-	&:last-child
-		margin-bottom 0
-
-	> .input
-		padding 12px
-
-		if fill
-			background rgba(#000, 0.035)
-			border-radius 6px
-		else
-			&:before
-				content ''
-				display block
-				position absolute
-				top 0
-				bottom 0
-				left 0
-				right 0
-				background none
-				border solid 1px var(--inputBorder)
-				border-radius 3px
-				pointer-events none
-
-			&:after
-				content ''
-				display block
-				position absolute
-				top 0
-				bottom 0
-				left 0
-				right 0
-				background none
-				border solid 2px var(--primary)
-				border-radius 3px
-				opacity 0
-				transition opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1)
-				pointer-events none
-
-		> .label
-			position absolute
-			top 6px
-			left 12px
-			pointer-events none
-			transition 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
-			transition-duration 0.3s
-			font-size 16px
-			line-height 32px
-			color var(--inputLabel)
-			pointer-events none
-			//will-change transform
-			transform-origin top left
-			transform scale(1)
-
-		> textarea
-			display block
-			width 100%
-			min-width 100%
-			max-width 100%
-			min-height 100px
-			padding 0
-			font inherit
-			font-weight fill ? bold : normal
-			font-size 16px
-			color var(--inputText)
-			background transparent
-			border none
-			border-radius 0
-			outline none
-			box-shadow none
-
-	> .desc
-		margin 6px 0
-		font-size 13px
-		opacity 0.7
-
-		&:empty
-			display none
-
-		*
-			margin 0
-
-	&.focused
-		> .input
-			if fill
-				background rgba(#000, 0.05)
-			else
-				&:after
-					opacity 1
-
-			> .label
-				color var(--primary)
-
-	&.focused
-	&.filled
-		> .input
-			> .label
-				top -24px
-				left 0 !important
-				transform scale(0.75)
-
-	&.tall
-		> .input
-			> textarea
-				min-height 200px
-
-	&.pre
-		> .input
-			> textarea
-				white-space pre
-
-.ui-textarea.fill
-	root(true)
-
-.ui-textarea:not(.fill)
-	root(false)
-
-</style>
diff --git a/src/client/app/common/views/components/uploader.vue b/src/client/app/common/views/components/uploader.vue
deleted file mode 100644
index 9f02da6c1e5a48ca5df0dab5880eed4b66586094..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/uploader.vue
+++ /dev/null
@@ -1,231 +0,0 @@
-<template>
-<div class="mk-uploader">
-	<ol v-if="uploads.length > 0">
-		<li v-for="ctx in uploads" :key="ctx.id">
-			<div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div>
-			<div class="top">
-				<p class="name"><fa icon="spinner" pulse/>{{ ctx.name }}</p>
-				<p class="status">
-					<span class="initing" v-if="ctx.progress == undefined">{{ $t('waiting') }}<mk-ellipsis/></span>
-					<span class="kb" v-if="ctx.progress != undefined">{{ String(Math.floor(ctx.progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span>
-					<span class="percentage" v-if="ctx.progress != undefined">{{ Math.floor((ctx.progress.value / ctx.progress.max) * 100) }}</span>
-				</p>
-			</div>
-			<progress v-if="ctx.progress != undefined && ctx.progress.value != ctx.progress.max" :value="ctx.progress.value" :max="ctx.progress.max"></progress>
-			<div class="progress initing" v-if="ctx.progress == undefined"></div>
-			<div class="progress waiting" v-if="ctx.progress != undefined && ctx.progress.value == ctx.progress.max"></div>
-		</li>
-	</ol>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { apiUrl } from '../../../config';
-import getMD5 from '../../scripts/get-md5';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/uploader.vue'),
-	data() {
-		return {
-			uploads: []
-		};
-	},
-	methods: {
-		checkExistence(fileData: ArrayBuffer): Promise<any> {
-			return new Promise((resolve, reject) => {
-				const data = new FormData();
-				data.append('md5', getMD5(fileData));
-
-				this.$root.api('drive/files/find-by-hash', {
-					md5: getMD5(fileData)
-				}).then(resp => {
-					resolve(resp.length > 0 ? resp[0] : null);
-				});
-			});
-		},
-
-		upload(file: File, folder: any, name?: string) {
-			if (folder && typeof folder == 'object') folder = folder.id;
-
-			const id = Math.random();
-
-			const reader = new FileReader();
-			reader.onload = (e: any) => {
-				this.checkExistence(e.target.result).then(result => {
-					if (result !== null) {
-						this.$emit('uploaded', result);
-						return;
-					}
-
-					const ctx = {
-						id: id,
-						name: name || file.name || 'untitled',
-						progress: undefined,
-						img: window.URL.createObjectURL(file)
-					};
-
-					this.uploads.push(ctx);
-					this.$emit('change', this.uploads);
-
-					const data = new FormData();
-					data.append('i', this.$store.state.i.token);
-					data.append('force', 'true');
-					data.append('file', file);
-
-					if (folder) data.append('folderId', folder);
-					if (name) data.append('name', name);
-
-					const xhr = new XMLHttpRequest();
-					xhr.open('POST', apiUrl + '/drive/files/create', true);
-					xhr.onload = (e: any) => {
-						const driveFile = JSON.parse(e.target.response);
-
-						this.$emit('uploaded', driveFile);
-
-						this.uploads = this.uploads.filter(x => x.id != id);
-						this.$emit('change', this.uploads);
-					};
-
-					xhr.upload.onprogress = e => {
-						if (e.lengthComputable) {
-							if (ctx.progress == undefined) ctx.progress = {};
-							ctx.progress.max = e.total;
-							ctx.progress.value = e.loaded;
-						}
-					};
-
-					xhr.send(data);
-				})
-			}
-			reader.readAsArrayBuffer(file);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-uploader
-	overflow auto
-
-	&:empty
-		display none
-
-	> ol
-		display block
-		margin 0
-		padding 0
-		list-style none
-
-		> li
-			display grid
-			margin 8px 0 0 0
-			padding 0
-			height 36px
-			width: 100%
-			box-shadow 0 -1px 0 var(--primaryAlpha01)
-			border-top solid 8px transparent
-			grid-template-columns 36px calc(100% - 44px)
-			grid-template-rows 1fr 8px
-			column-gap 8px
-			box-sizing content-box
-
-			&:first-child
-				margin 0
-				box-shadow none
-				border-top none
-
-			> .img
-				display block
-				background-size cover
-				background-position center center
-				grid-column 1 / 2
-				grid-row 1 / 3
-
-			> .top
-				display flex
-				grid-column 2 / 3
-				grid-row 1 / 2
-
-				> .name
-					display block
-					padding 0 8px 0 0
-					margin 0
-					font-size 0.8em
-					color var(--primaryAlpha07)
-					white-space nowrap
-					text-overflow ellipsis
-					overflow hidden
-					flex-shrink 1
-
-					> [data-icon]
-						margin-right 4px
-
-				> .status
-					display block
-					margin 0 0 0 auto
-					padding 0
-					font-size 0.8em
-					flex-shrink 0
-
-					> .initing
-						color var(--primaryAlpha05)
-
-					> .kb
-						color var(--primaryAlpha05)
-
-					> .percentage
-						display inline-block
-						width 48px
-						text-align right
-
-						color var(--primaryAlpha07)
-
-						&:after
-							content '%'
-
-			> progress
-				display block
-				background transparent
-				border none
-				border-radius 4px
-				overflow hidden
-				grid-column 2 / 3
-				grid-row 2 / 3
-				z-index 2
-
-				&::-webkit-progress-value
-					background var(--primary)
-
-				&::-webkit-progress-bar
-					background var(--primaryAlpha01)
-
-			> .progress
-				display block
-				border none
-				border-radius 4px
-				background linear-gradient(
-					45deg,
-					var(--primaryLighten30) 25%,
-					var(--primary)               25%,
-					var(--primary)               50%,
-					var(--primaryLighten30) 50%,
-					var(--primaryLighten30) 75%,
-					var(--primary)               75%,
-					var(--primary)
-				)
-				background-size 32px 32px
-				animation bg 1.5s linear infinite
-				grid-column 2 / 3
-				grid-row 2 / 3
-				z-index 1
-
-				&.initing
-					opacity 0.3
-
-				@keyframes bg
-					from {background-position: 0 0;}
-					to   {background-position: -64px 32px;}
-
-</style>
diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue
deleted file mode 100644
index 80aae5999d70cb4c51175241d7896ccf3229a819..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/url-preview.vue
+++ /dev/null
@@ -1,343 +0,0 @@
-<template>
-<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
-	<button class="disablePlayer" @click="playerEnabled = false" :title="$t('disable-player')"><fa icon="times"/></button>
-	<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen />
-</div>
-<div v-else-if="tweetUrl && detail" class="twitter">
-	<blockquote ref="tweet" class="twitter-tweet" :data-theme="$store.state.device.darkmode ? 'dark' : null">
-		<a :href="url"></a>
-	</blockquote>
-</div>
-<div v-else class="mk-url-preview">
-	<component :is="hasRoute ? 'router-link' : 'a'" :class="{ mini: narrow, compact }" :[attr]="hasRoute ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url" v-if="!fetching">
-		<div class="thumbnail" v-if="thumbnail" :style="`background-image: url('${thumbnail}')`">
-			<button v-if="!playerEnabled && player.url" @click.prevent="playerEnabled = true" :title="$t('enable-player')"><fa :icon="['far', 'play-circle']"/></button>
-		</div>
-		<article>
-			<header>
-				<h1 :title="title">{{ title }}</h1>
-			</header>
-			<p v-if="description" :title="description">{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p>
-			<footer>
-				<img class="icon" v-if="icon" :src="icon"/>
-				<p :title="sitename">{{ sitename }}</p>
-			</footer>
-		</article>
-	</component>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { url as local, lang } from '../../../config';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/url-preview.vue'),
-
-	props: {
-		url: {
-			type: String,
-			require: true
-		},
-
-		detail: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-
-		compact: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	inject: {
-		narrow: {
-			default: false
-		}
-	},
-
-	data() {
-		const isSelf = this.url.startsWith(local);
-		const hasRoute =
-			(this.url.substr(local.length) === '/') ||
-			this.url.substr(local.length).startsWith('/@') ||
-			this.url.substr(local.length).startsWith('/notes/') ||
-			this.url.substr(local.length).startsWith('/tags/') ||
-			this.url.substr(local.length).startsWith('/pages/');
-		return {
-			local,
-			fetching: true,
-			title: null,
-			description: null,
-			thumbnail: null,
-			icon: null,
-			sitename: null,
-			player: {
-				url: null,
-				width: null,
-				height: null
-			},
-			tweetUrl: null,
-			playerEnabled: false,
-			self: isSelf,
-			hasRoute: hasRoute,
-			attr: hasRoute ? 'to' : 'href',
-			target: hasRoute ? null : '_blank'
-		};
-	},
-
-	created() {
-		const requestUrl = new URL(this.url);
-
-		if (this.detail && requestUrl.hostname == 'twitter.com' && /^\/.+\/status(es)?\/\d+/.test(requestUrl.pathname)) {
-			this.tweetUrl = requestUrl;
-			const twttr = (window as any).twttr || {};
-			const loadTweet = () => twttr.widgets.load(this.$refs.tweet);
-
-			if (twttr.widgets) {
-				Vue.nextTick(loadTweet);
-			} else {
-				const wjsId = 'twitter-wjs';
-				if (!document.getElementById(wjsId)) {
-					const head = document.getElementsByTagName('head')[0];
-					const script = document.createElement('script');
-					script.setAttribute('id', wjsId);
-					script.setAttribute('src', 'https://platform.twitter.com/widgets.js');
-					head.appendChild(script);
-				}
-				twttr.ready = loadTweet;
-				(window as any).twttr = twttr;
-			}
-			return;
-		}
-
-		if (requestUrl.hostname === 'music.youtube.com') {
-			requestUrl.hostname = 'youtube.com';
-		}
-
-		const requestLang = (lang || 'ja-JP').replace('ja-KS', 'ja-JP');
-
-		requestUrl.hash = '';
-
-		fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => {
-			res.json().then(info => {
-				if (info.url == null) return;
-				this.title = info.title;
-				this.description = info.description;
-				this.thumbnail = info.thumbnail;
-				this.icon = info.icon;
-				this.sitename = info.sitename;
-				this.fetching = false;
-				this.player = info.player;
-			})
-		});
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.player
-	position relative
-	width 100%
-
-	> button
-		position absolute
-		top -1.5em
-		right 0
-		font-size 1em
-		width 1.5em
-		height 1.5em
-		padding 0
-		margin 0
-		color var(--text)
-		background rgba(128, 128, 128, 0.2)
-		opacity 0.7
-
-		&:hover
-			opacity 0.9
-
-	> iframe
-		height 100%
-		left 0
-		position absolute
-		top 0
-		width 100%
-
-.mk-url-preview
-	> a
-		display block
-		font-size 14px
-		border solid var(--lineWidth) var(--urlPreviewBorder)
-		border-radius 4px
-		overflow hidden
-
-		&:hover
-			text-decoration none
-			border-color var(--urlPreviewBorderHover)
-
-			> article > header > h1
-				text-decoration underline
-
-		> .thumbnail
-			position absolute
-			width 100px
-			height 100%
-			background-position center
-			background-size cover
-			display flex
-			justify-content center
-			align-items center
-
-			> button
-				font-size 3.5em
-				opacity: 0.7
-
-				&:hover
-					font-size 4em
-					opacity 0.9
-
-			& + article
-				left 100px
-				width calc(100% - 100px)
-
-		> article
-			padding 16px
-
-			> header
-				margin-bottom 8px
-
-				> h1
-					margin 0
-					font-size 1em
-					color var(--urlPreviewTitle)
-
-			> p
-				margin 0
-				color var(--urlPreviewText)
-				font-size 0.8em
-
-			> footer
-				margin-top 8px
-				height 16px
-
-				> img
-					display inline-block
-					width 16px
-					height 16px
-					margin-right 4px
-					vertical-align top
-
-				> p
-					display inline-block
-					margin 0
-					color var(--urlPreviewInfo)
-					font-size 0.8em
-					line-height 16px
-					vertical-align top
-
-		@media (max-width 700px)
-			> .thumbnail
-				position relative
-				width 100%
-				height 100px
-
-				& + article
-					left 0
-					width 100%
-
-		@media (max-width 550px)
-			font-size 12px
-
-			> .thumbnail
-				height 80px
-
-			> article
-				padding 12px
-
-		@media (max-width 500px)
-			font-size 10px
-
-			> .thumbnail
-				height 70px
-
-			> article
-				padding 8px
-
-				> header
-					margin-bottom 4px
-
-				> footer
-					margin-top 4px
-
-					> img
-						width 12px
-						height 12px
-
-			&.compact
-				> .thumbnail
-					position: absolute
-					width 56px
-					height 100%
-
-				> article
-					left 56px
-					width calc(100% - 56px)
-					padding 4px
-
-					> header
-						margin-bottom 2px
-
-					> footer
-						margin-top 2px
-
-		&.mini
-			font-size 10px
-
-			> .thumbnail
-				position relative
-				width 100%
-				height 60px
-
-			> article
-				left 0
-				width 100%
-				padding 8px
-
-				> header
-					margin-bottom 4px
-
-				> footer
-					margin-top 4px
-
-					> img
-						width 12px
-						height 12px
-
-			&.compact
-				> .thumbnail
-					position absolute
-					width 56px
-					height 100%
-
-				> article
-					left 56px
-					width calc(100% - 56px)
-					padding 4px
-
-					> header
-						margin-bottom 2px
-
-					> footer
-						margin-top 2px
-
-		&.compact
-			> article
-				> header h1, p, footer
-					overflow hidden
-					white-space nowrap
-					text-overflow ellipsis
-</style>
diff --git a/src/client/app/common/views/components/user-list.vue b/src/client/app/common/views/components/user-list.vue
deleted file mode 100644
index 4ba4e67e549e03e89d50ee2a1b4f2de496b4e3d6..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/user-list.vue
+++ /dev/null
@@ -1,165 +0,0 @@
-<template>
-<ui-container :body-togglable="true" :expanded="expanded">
-	<template #header><slot></slot></template>
-
-	<mk-error v-if="error" @retry="init()"/>
-
-	<div class="efvhhmdq" :class="{ iconOnly }" v-size="[{ lt: 500, class: 'narrow' }]">
-		<div class="no-users" v-if="empty">
-			<p>{{ $t('no-users') }}</p>
-		</div>
-		<div class="user" v-for="user in users" :key="user.id">
-			<mk-avatar class="avatar" :user="user"/>
-			<div class="body" v-if="!iconOnly">
-				<div class="name">
-					<router-link class="name" :to="user | userPage" v-user-preview="user.id"><mk-user-name :user="user"/></router-link>
-					<p class="username">@{{ user | acct }}</p>
-				</div>
-				<div class="description" v-if="user.description" :title="user.description">
-					<mfm :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :plain="true" :nowrap="true"/>
-				</div>
-				<mk-follow-button class="koudoku-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/>
-			</div>
-		</div>
-		<button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMore()" :disabled="moreFetching">
-			<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? $t('@.loading') : $t('@.load-more') }}
-		</button>
-	</div>
-</ui-container>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import paging from '../../../common/scripts/paging';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/user-list.vue'),
-
-	mixins: [
-		paging({}),
-	],
-
-	props: {
-		pagination: {
-			required: true
-		},
-		extract: {
-			required: false
-		},
-		iconOnly: {
-			type: Boolean,
-			default: false
-		},
-		expanded: {
-			type: Boolean,
-			default: true
-		},
-	},
-
-	computed: {
-		users() {
-			return this.extract ? this.extract(this.items) : this.items;
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.efvhhmdq
-	&.narrow
-		> .user > .body > .name
-			width 100%
-
-		> .user > .body > .description
-			display none
-
-	&.iconOnly
-		padding 12px
-
-		> .user
-			display inline-block
-			padding 0
-			border-bottom none
-
-			> .avatar
-				display inline-block
-				margin 4px
-
-	> .no-users
-		text-align center
-		color var(--text)
-
-	> .user
-		display flex
-		padding 16px
-		border-bottom solid 1px var(--faceDivider)
-
-		&:last-child
-			border-bottom none
-
-		> .avatar
-			display block
-			flex-shrink 0
-			margin 0 12px 0 0
-			width 42px
-			height 42px
-			border-radius 8px
-
-		> .body
-			display flex
-			width calc(100% - 54px)
-
-			> .name
-				width 45%
-
-				> .name
-					margin 0
-					font-size 16px
-					line-height 24px
-					color var(--text)
-
-				> .username
-					display block
-					margin 0
-					font-size 15px
-					line-height 16px
-					color var(--text)
-					opacity 0.7
-
-			> .description
-				width 55%
-				color var(--text)
-				line-height 42px
-				white-space nowrap
-				overflow hidden
-				text-overflow ellipsis
-				opacity 0.7
-				font-size 14px
-				padding-right 40px
-
-			> .koudoku-button
-				position absolute
-				top 8px
-				right 0
-
-	> .more
-		display block
-		width 100%
-		padding 16px
-		color var(--text)
-		border-top solid var(--lineWidth) rgba(#000, 0.05)
-
-		&:hover
-			background rgba(#000, 0.025)
-
-		&:active
-			background rgba(#000, 0.05)
-
-		&.fetching
-			cursor wait
-
-		> [data-icon]
-			margin-right 4px
-
-</style>
diff --git a/src/client/app/common/views/components/user-menu.vue b/src/client/app/common/views/components/user-menu.vue
deleted file mode 100644
index 532dcf35c2d122c07967a705843797e52db645de..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/user-menu.vue
+++ /dev/null
@@ -1,228 +0,0 @@
-<template>
-<div style="position:initial">
-	<mk-menu :source="source" :items="items" @closed="closed"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { faExclamationCircle, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons';
-import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/user-menu.vue'),
-
-	props: ['user', 'source'],
-
-	data() {
-		let menu = [{
-			icon: ['fas', 'at'],
-			text: this.$t('mention'),
-			action: () => {
-				this.$post({ mention: this.user });
-			}
-		}, null, {
-			icon: ['fas', 'list'],
-			text: this.$t('push-to-list'),
-			action: this.pushList
-		}] as any;
-
-		if (this.$store.getters.isSignedIn && this.$store.state.i.id != this.user.id) {
-			menu = menu.concat([null, {
-				icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'],
-				text: this.user.isMuted ? this.$t('unmute') : this.$t('mute'),
-				action: this.toggleMute
-			}, {
-				icon: 'ban',
-				text: this.user.isBlocking ? this.$t('unblock') : this.$t('block'),
-				action: this.toggleBlock
-			}, null, {
-				icon: faExclamationCircle,
-				text: this.$t('report-abuse'),
-				action: this.reportAbuse
-			}]);
-		}
-
-		if (this.$store.getters.isSignedIn && (this.$store.state.i.isAdmin || this.$store.state.i.isModerator)) {
-			menu = menu.concat([null, {
-				icon: faMicrophoneSlash,
-				text: this.user.isSilenced ? this.$t('unsilence') : this.$t('silence'),
-				action: this.toggleSilence
-			}, {
-				icon: faSnowflake,
-				text: this.user.isSuspended ? this.$t('unsuspend') : this.$t('suspend'),
-				action: this.toggleSuspend
-			}]);
-		}
-
-		return {
-			items: menu
-		};
-	},
-
-	methods: {
-		closed() {
-			this.$nextTick(() => {
-				this.destroyDom();
-			});
-		},
-
-		async pushList() {
-			const t = this.$t('select-list'); // なぜか後で参照すると null になるので最初にメモリに確保しておく
-			const lists = await this.$root.api('users/lists/list');
-			const { canceled, result: listId } = await this.$root.dialog({
-				type: null,
-				title: t,
-				select: {
-					items: lists.map(list => ({
-						value: list.id, text: list.name
-					}))
-				},
-				showCancelButton: true
-			});
-			if (canceled) return;
-			await this.$root.api('users/lists/push', {
-				listId: listId,
-				userId: this.user.id
-			});
-			this.$root.dialog({
-				type: 'success',
-				splash: true
-			});
-		},
-
-		async toggleMute() {
-			if (this.user.isMuted) {
-				if (!await this.getConfirmed(this.$t('unmute-confirm'))) return;
-
-				this.$root.api('mute/delete', {
-					userId: this.user.id
-				}).then(() => {
-					this.user.isMuted = false;
-				}, () => {
-					this.$root.dialog({
-						type: 'error',
-						text: e
-					});
-				});
-			} else {
-				if (!await this.getConfirmed(this.$t('mute-confirm'))) return;
-
-				this.$root.api('mute/create', {
-					userId: this.user.id
-				}).then(() => {
-					this.user.isMuted = true;
-				}, () => {
-					this.$root.dialog({
-						type: 'error',
-						text: e
-					});
-				});
-			}
-		},
-
-		async toggleBlock() {
-			if (this.user.isBlocking) {
-				if (!await this.getConfirmed(this.$t('unblock-confirm'))) return;
-
-				this.$root.api('blocking/delete', {
-					userId: this.user.id
-				}).then(() => {
-					this.user.isBlocking = false;
-				}, () => {
-					this.$root.dialog({
-						type: 'error',
-						text: e
-					});
-				});
-			} else {
-				if (!await this.getConfirmed(this.$t('block-confirm'))) return;
-
-				this.$root.api('blocking/create', {
-					userId: this.user.id
-				}).then(() => {
-					this.user.isBlocking = true;
-				}, () => {
-					this.$root.dialog({
-						type: 'error',
-						text: e
-					});
-				});
-			}
-		},
-
-		async reportAbuse() {
-			const reported = this.$t('report-abuse-reported'); // なぜか後で参照すると null になるので最初にメモリに確保しておく
-			const { canceled, result: comment } = await this.$root.dialog({
-				title: this.$t('report-abuse-detail'),
-				input: true
-			});
-			if (canceled) return;
-			this.$root.api('users/report-abuse', {
-				userId: this.user.id,
-				comment: comment
-			}).then(() => {
-				this.$root.dialog({
-					type: 'success',
-					text: reported
-				});
-			}, e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e
-				});
-			});
-		},
-
-		async toggleSilence() {
-			if (!await this.getConfirmed(this.$t(this.user.isSilenced ? 'unsilence-confirm' : 'silence-confirm'))) return;
-
-			this.$root.api(this.user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', {
-				userId: this.user.id
-			}).then(() => {
-				this.user.isSilenced = !this.user.isSilenced;
-				this.$root.dialog({
-					type: 'success',
-					splash: true
-				});
-			}, e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e
-				});
-			});
-		},
-
-		async toggleSuspend() {
-			if (!await this.getConfirmed(this.$t(this.user.isSuspended ? 'unsuspend-confirm' : 'suspend-confirm'))) return;
-
-			this.$root.api(this.user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', {
-				userId: this.user.id
-			}).then(() => {
-				this.user.isSuspended = !this.user.isSuspended;
-				this.$root.dialog({
-					type: 'success',
-					splash: true
-				});
-			}, e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e
-				});
-			});
-		},
-
-		async getConfirmed(text: string): Promise<Boolean> {
-			const confirm = await this.$root.dialog({
-				type: 'warning',
-				showCancelButton: true,
-				title: 'confirm',
-				text,
-			});
-
-			return !confirm.canceled;
-		},
-	}
-});
-</script>
diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue
deleted file mode 100644
index 5aa481ed9a17c32d881b899c7a07b618f4cfc007..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/visibility-chooser.vue
+++ /dev/null
@@ -1,233 +0,0 @@
-<template>
-<div class="gqyayizv">
-	<div class="backdrop" ref="backdrop" @click="close"></div>
-	<div class="popover" :class="{ isMobile: $root.isMobile }" ref="popover">
-		<div @click="choose('public')" :class="{ active: v == 'public' }">
-			<div><fa icon="globe"/></div>
-			<div>
-				<span>{{ $t('public') }}</span>
-			</div>
-		</div>
-		<div @click="choose('home')" :class="{ active: v == 'home' }">
-			<div><fa icon="home"/></div>
-			<div>
-				<span>{{ $t('home') }}</span>
-				<span>{{ $t('home-desc') }}</span>
-			</div>
-		</div>
-		<div @click="choose('followers')" :class="{ active: v == 'followers' }">
-			<div><fa icon="unlock"/></div>
-			<div>
-				<span>{{ $t('followers') }}</span>
-				<span>{{ $t('followers-desc') }}</span>
-			</div>
-		</div>
-		<div @click="choose('specified')" :class="{ active: v == 'specified' }">
-			<div><fa icon="envelope"/></div>
-			<div>
-				<span>{{ $t('specified') }}</span>
-				<span>{{ $t('specified-desc') }}</span>
-			</div>
-		</div>
-		<div @click="choose('local-public')" :class="{ active: v == 'local-public' }">
-			<div><fa icon="globe"/></div>
-			<div>
-				<span>{{ $t('local-public') }}</span>
-				<span>{{ $t('local-public-desc') }}</span>
-			</div>
-		</div>
-		<div @click="choose('local-home')" :class="{ active: v == 'local-home' }">
-			<div><fa icon="home"/></div>
-			<div>
-				<span>{{ $t('local-home') }}</span>
-			</div>
-		</div>
-		<div @click="choose('local-followers')" :class="{ active: v == 'local-followers' }">
-			<div><fa icon="unlock"/></div>
-			<div>
-				<span>{{ $t('local-followers') }}</span>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import anime from 'animejs';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/visibility-chooser.vue'),
-	props: {
-		source: {
-			required: true
-		},
-		currentVisibility: {
-			type: String,
-			required: false
-		}
-	},
-	data() {
-		return {
-			v: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : (this.currentVisibility || this.$store.state.settings.defaultNoteVisibility)
-		}
-	},
-	mounted() {
-		this.$nextTick(() => {
-			const popover = this.$refs.popover as any;
-
-			const rect = this.source.getBoundingClientRect();
-			const width = popover.offsetWidth;
-			const height = popover.offsetHeight;
-
-			let left;
-			let top;
-
-			if (this.$root.isMobile) {
-				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-				const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
-				left = (x - (width / 2));
-				top = (y - (height / 2));
-			} else {
-				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-				const y = rect.top + window.pageYOffset + this.source.offsetHeight;
-				left = (x - (width / 2));
-				top = y;
-			}
-
-			if (left + width > window.innerWidth) {
-				left = window.innerWidth - width;
-			}
-
-			popover.style.left = left + 'px';
-			popover.style.top = top + 'px';
-
-			anime({
-				targets: this.$refs.backdrop,
-				opacity: 1,
-				duration: 100,
-				easing: 'linear'
-			});
-
-			anime({
-				targets: this.$refs.popover,
-				opacity: 1,
-				scale: [0.5, 1],
-				duration: 500
-			});
-		});
-	},
-	methods: {
-		choose(visibility) {
-			if (this.$store.state.settings.rememberNoteVisibility) {
-				this.$store.commit('device/setVisibility', visibility);
-			}
-			this.$emit('chosen', visibility);
-			this.destroyDom();
-		},
-		close() {
-			(this.$refs.backdrop as any).style.pointerEvents = 'none';
-			anime({
-				targets: this.$refs.backdrop,
-				opacity: 0,
-				duration: 200,
-				easing: 'linear'
-			});
-
-			(this.$refs.popover as any).style.pointerEvents = 'none';
-			anime({
-				targets: this.$refs.popover,
-				opacity: 0,
-				scale: 0.5,
-				duration: 200,
-				easing: 'easeInBack',
-				complete: () => this.destroyDom()
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.gqyayizv
-	position initial
-
-	> .backdrop
-		position fixed
-		top 0
-		left 0
-		z-index 10000
-		width 100%
-		height 100%
-		background var(--modalBackdrop)
-		opacity 0
-
-	> .popover
-		$bgcolor = var(--popupBg)
-		position absolute
-		z-index 10001
-		width 240px
-		padding 8px 0
-		background $bgcolor
-		border-radius 4px
-		box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
-		transform scale(0.5)
-		opacity 0
-
-		&:not(.isMobile)
-			$arrow-size = 10px
-
-			margin-top $arrow-size
-			transform-origin center -($arrow-size)
-
-			&:before
-				content ""
-				display block
-				position absolute
-				top -($arrow-size * 2)
-				left s('calc(50% - %s)', $arrow-size)
-				border-top solid $arrow-size transparent
-				border-left solid $arrow-size transparent
-				border-right solid $arrow-size transparent
-				border-bottom solid $arrow-size $bgcolor
-
-		> div
-			display flex
-			padding 8px 14px
-			font-size 12px
-			color var(--popupFg)
-			cursor pointer
-
-			&:hover
-				background var(--faceClearButtonHover)
-
-			&:active
-				background var(--faceClearButtonActive)
-
-			&.active
-				color var(--primaryForeground)
-				background var(--primary)
-
-			> *
-				user-select none
-				pointer-events none
-
-			> *:first-child
-				display flex
-				justify-content center
-				align-items center
-				margin-right 10px
-				width 16px
-
-			> *:last-child
-				flex 1 1 auto
-
-				> span:first-child
-					display block
-					font-weight bold
-
-				> span:last-child:not(:first-child)
-					opacity 0.6
-
-</style>
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
deleted file mode 100644
index d812549b1e76fe085405608e8a8cc970a169ea8c..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ /dev/null
@@ -1,156 +0,0 @@
-<template>
-<div class="mk-welcome-timeline">
-	<transition-group name="ldzpakcixzickvggyixyrhqwjaefknon" tag="div">
-		<div v-for="note in notes" :key="note.id">
-			<mk-avatar class="avatar" :user="note.user" target="_blank"/>
-			<div class="body">
-				<header>
-					<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">
-						<mk-user-name :user="note.user"/>
-					</router-link>
-					<span class="username">@{{ note.user | acct }}</span>
-					<div class="info">
-						<router-link class="created-at" :to="note | notePage">
-							<mk-time :time="note.createdAt"/>
-						</router-link>
-					</div>
-				</header>
-				<div class="text">
-					<mfm v-if="note.text" :text="note.cw != null ? note.cw : note.text" :author="note.user" :custom-emojis="note.emojis"/>
-				</div>
-			</div>
-		</div>
-	</transition-group>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: {
-		max: {
-			type: Number,
-			required: false,
-			default: undefined
-		}
-	},
-
-	data() {
-		return {
-			fetching: true,
-			notes: [],
-			connection: null
-		};
-	},
-
-	mounted() {
-		this.fetch();
-
-		this.connection = this.$root.stream.useSharedConnection('localTimeline');
-
-		this.connection.on('note', this.onNote);
-	},
-
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		fetch(cb?) {
-			this.fetching = true;
-			this.$root.api('notes', {
-				limit: this.max,
-				local: true,
-				reply: false,
-				renote: false,
-				file: false,
-				poll: false
-			}).then(notes => {
-				this.notes = notes;
-				this.fetching = false;
-			});
-		},
-
-		onNote(note) {
-			if (note.replyId != null) return;
-			if (note.renoteId != null) return;
-			if (note.poll != null) return;
-			if (note.localOnly) return;
-
-			this.notes.unshift(note);
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.ldzpakcixzickvggyixyrhqwjaefknon-enter
-.ldzpakcixzickvggyixyrhqwjaefknon-leave-to
-	opacity 0
-	transform translateY(-30px)
-
-.mk-welcome-timeline
-	background var(--face)
-
-	> div
-		> *
-			transition transform .3s ease, opacity .3s ease
-
-		> div
-			padding 16px
-			overflow-wrap break-word
-			font-size .9em
-			color var(--noteText)
-			border-bottom 1px solid var(--faceDivider)
-
-			&:after
-				content ""
-				display block
-				clear both
-
-			> .avatar
-				display block
-				float left
-				position -webkit-sticky
-				position sticky
-				top 16px
-				width 42px
-				height 42px
-				border-radius 6px
-
-			> .body
-				float right
-				width calc(100% - 42px)
-				padding-left 12px
-
-				> header
-					display flex
-					align-items center
-					margin-bottom 4px
-					white-space nowrap
-
-					> .name
-						display block
-						margin 0 .5em 0 0
-						padding 0
-						overflow hidden
-						font-weight bold
-						text-overflow ellipsis
-						color var(--noteHeaderName)
-
-					> .username
-						margin 0 .5em 0 0
-						color var(--noteHeaderAcct)
-
-					> .info
-						margin-left auto
-						font-size 0.9em
-
-						> .created-at
-							color var(--noteHeaderInfo)
-
-				> .text
-					text-align left
-
-</style>
diff --git a/src/client/app/common/views/deck/deck.column-core.vue b/src/client/app/common/views/deck/deck.column-core.vue
deleted file mode 100644
index 974c58235df9e4957df8d6d751514f1466cdcf7b..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.column-core.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<template>
-<x-widgets-column v-if="column.type == 'widgets'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
-<x-notifications-column v-else-if="column.type == 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
-<x-tl-column v-else-if="column.type == 'home'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
-<x-tl-column v-else-if="column.type == 'local'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
-<x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
-<x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
-<x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
-<x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
-<x-mentions-column v-else-if="column.type == 'mentions'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
-<x-direct-column v-else-if="column.type == 'direct'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import XTlColumn from './deck.tl-column.vue';
-import XNotificationsColumn from './deck.notifications-column.vue';
-import XWidgetsColumn from './deck.widgets-column.vue';
-import XMentionsColumn from './deck.mentions-column.vue';
-import XDirectColumn from './deck.direct-column.vue';
-
-export default Vue.extend({
-	components: {
-		XTlColumn,
-		XNotificationsColumn,
-		XWidgetsColumn,
-		XMentionsColumn,
-		XDirectColumn
-	},
-
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: false,
-			default: false
-		}
-	},
-
-	methods: {
-		focus() {
-			this.$children[0].focus();
-		}
-	}
-});
-</script>
diff --git a/src/client/app/common/views/deck/deck.column-template.vue b/src/client/app/common/views/deck/deck.column-template.vue
deleted file mode 100644
index 59232851624720ce30966224de6a9f0e9cd9fa35..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.column-template.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-<template>
-<x-column>
-	<template #header>
-		<fa v-if="icon" :icon="icon"/>{{ title }}
-	</template>
-
-	<div>
-		<component :is="component" @init="init" v-bind="$attrs"/>
-	</div>
-</x-column>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import XColumn from './deck.column.vue';
-
-export default Vue.extend({
-	components: {
-		XColumn,
-	},
-
-	props: {
-		component: {
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			title: null,
-			icon: null,
-		};
-	},
-
-	mounted() {
-	},
-
-	methods: {
-		init(v) {
-			this.title = v.title;
-			this.icon = v.icon;
-		}
-	}
-});
-</script>
diff --git a/src/client/app/common/views/deck/deck.column.vue b/src/client/app/common/views/deck/deck.column.vue
deleted file mode 100644
index ac69a97df50d2c8ad7bce10e538c32c79950e618..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.column.vue
+++ /dev/null
@@ -1,444 +0,0 @@
-<template>
-<div class="dnpfarvgbnfmyzbdquhhzyxcmstpdqzs" :class="{ naked, narrow, active, isStacked, draghover, dragging, dropready, isMobile: $root.isMobile, shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"
-		@dragover.prevent.stop="onDragover"
-		@dragleave="onDragleave"
-		@drop.prevent.stop="onDrop"
-		v-hotkey="keymap">
-	<header :class="{ indicate: count > 0 }"
-			draggable="true"
-			@click="goTop"
-			@dragstart="onDragstart"
-			@dragend="onDragend"
-			@contextmenu.prevent.stop="onContextmenu">
-		<button class="toggleActive" @click="toggleActive" v-if="isStacked">
-			<template v-if="active"><fa icon="angle-up"/></template>
-			<template v-else><fa icon="angle-down"/></template>
-		</button>
-		<span class="header"><slot name="header"></slot></span>
-		<span class="count" v-if="count > 0">({{ count }})</span>
-		<button v-if="!isTemporaryColumn" class="menu" ref="menu" @click.stop="showMenu"><fa icon="caret-down"/></button>
-		<button v-else class="close" @click.stop="close"><fa icon="times"/></button>
-	</header>
-	<div ref="body" v-show="active">
-		<slot></slot>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import Menu from '../../../common/views/components/menu.vue';
-import { countIf } from '../../../../../prelude/array';
-import { faArrowUp, faArrowDown } from '@fortawesome/free-solid-svg-icons';
-import { faWindowMaximize } from '@fortawesome/free-regular-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('deck'),
-	props: {
-		column: {
-			type: Object,
-			required: false,
-			default: null
-		},
-		isStacked: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		name: {
-			type: String,
-			required: false
-		},
-		menu: {
-			type: Array,
-			required: false,
-			default: null
-		},
-		naked: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		narrow: {
-			type: Boolean,
-			required: false,
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			count: 0,
-			active: true,
-			dragging: false,
-			draghover: false,
-			dropready: false,
-			faArrowUp, faArrowDown
-		};
-	},
-
-	computed: {
-		isTemporaryColumn(): boolean {
-			return this.column == null;
-		},
-
-		keymap(): any {
-			return {
-				'shift+up': () => this.$parent.$emit('parentFocus', 'up'),
-				'shift+down': () => this.$parent.$emit('parentFocus', 'down'),
-				'shift+left': () => this.$parent.$emit('parentFocus', 'left'),
-				'shift+right': () => this.$parent.$emit('parentFocus', 'right'),
-			};
-		}
-	},
-
-	inject: {
-		getColumnVm: { from: 'getColumnVm' }
-	},
-
-	watch: {
-		active(v) {
-			if (v && this.isScrollTop()) {
-				this.$emit('top');
-			}
-		},
-		dragging(v) {
-			this.$root.$emit(v ? 'deck.column.dragStart' : 'deck.column.dragEnd');
-		}
-	},
-
-	provide() {
-		return {
-			column: this,
-			isScrollTop: this.isScrollTop,
-			count: v => this.count = v,
-			inNakedDeckColumn: !this.naked
-		};
-	},
-
-	mounted() {
-		this.$refs.body.addEventListener('scroll', this.onScroll, { passive: true });
-
-		if (!this.isTemporaryColumn) {
-			this.$root.$on('deck.column.dragStart', this.onOtherDragStart);
-			this.$root.$on('deck.column.dragEnd', this.onOtherDragEnd);
-		}
-	},
-
-	beforeDestroy() {
-		this.$refs.body.removeEventListener('scroll', this.onScroll);
-
-		if (!this.isTemporaryColumn) {
-			this.$root.$off('deck.column.dragStart', this.onOtherDragStart);
-			this.$root.$off('deck.column.dragEnd', this.onOtherDragEnd);
-		}
-	},
-
-	methods: {
-		onOtherDragStart() {
-			this.dropready = true;
-		},
-
-		onOtherDragEnd() {
-			this.dropready = false;
-		},
-
-		toggleActive() {
-			if (!this.isStacked) return;
-			const deck = this.$store.state.device.deckProfile ? this.$store.state.settings.deckProfiles[this.$store.state.device.deckProfile] : this.$store.state.device.deck;
-			const vms = deck.layout.find(ids => ids.indexOf(this.column.id) != -1).map(id => this.getColumnVm(id));
-			if (this.active && countIf(vm => vm.$el.classList.contains('active'), vms) == 1) return;
-			this.active = !this.active;
-		},
-
-		isScrollTop() {
-			return this.active && this.$refs.body.scrollTop == 0;
-		},
-
-		onScroll() {
-			if (this.isScrollTop()) {
-				this.$emit('top');
-			}
-
-			if (this.$store.state.settings.fetchOnScroll) {
-				const current = this.$refs.body.scrollTop + this.$refs.body.clientHeight;
-				if (current > this.$refs.body.scrollHeight - 1) this.$emit('bottom');
-			}
-		},
-
-		getMenu() {
-			const items = [{
-				icon: 'pencil-alt',
-				text: this.$t('rename'),
-				action: () => {
-					this.$root.dialog({
-						title: this.$t('rename'),
-						input: {
-							default: this.name,
-							allowEmpty: false
-						}
-					}).then(({ canceled, result: name }) => {
-						if (canceled) return;
-						this.$store.commit('renameDeckColumn', { id: this.column.id, name });
-					});
-				}
-			}, null, {
-				icon: 'arrow-left',
-				text: this.$t('swap-left'),
-				action: () => {
-					this.$store.commit('swapLeftDeckColumn', this.column.id);
-				}
-			}, {
-				icon: 'arrow-right',
-				text: this.$t('swap-right'),
-				action: () => {
-					this.$store.commit('swapRightDeckColumn', this.column.id);
-				}
-			}, this.isStacked ? {
-				icon: faArrowUp,
-				text: this.$t('swap-up'),
-				action: () => {
-					this.$store.commit('swapUpDeckColumn', this.column.id);
-				}
-			} : undefined, this.isStacked ? {
-				icon: faArrowDown,
-				text: this.$t('swap-down'),
-				action: () => {
-					this.$store.commit('swapDownDeckColumn', this.column.id);
-				}
-			} : undefined, null, {
-				icon: ['far', 'window-restore'],
-				text: this.$t('stack-left'),
-				action: () => {
-					this.$store.commit('stackLeftDeckColumn', this.column.id);
-				}
-			}, this.isStacked ? {
-				icon: faWindowMaximize,
-				text: this.$t('pop-right'),
-				action: () => {
-					this.$store.commit('popRightDeckColumn', this.column.id);
-				}
-			} : undefined, null, {
-				icon: ['far', 'trash-alt'],
-				text: this.$t('remove'),
-				action: () => {
-					this.$store.commit('removeDeckColumn', this.column.id);
-				}
-			}];
-
-			if (this.menu) {
-				items.unshift(null);
-				for (const i of this.menu.reverse()) {
-					items.unshift(i);
-				}
-			}
-
-			return items;
-		},
-
-		onContextmenu(e) {
-			if (this.isTemporaryColumn) return;
-			this.$contextmenu(e, this.getMenu());
-		},
-
-		showMenu() {
-			this.$root.new(Menu, {
-				source: this.$refs.menu,
-				items: this.getMenu()
-			});
-		},
-
-		close() {
-			this.$router.push('/');
-		},
-
-		goTop() {
-			this.$refs.body.scrollTo({
-				top: 0,
-				behavior: 'smooth'
-			});
-		},
-
-		onDragstart(e) {
-			// テンポラリカラムはドラッグさせない
-			if (this.isTemporaryColumn) {
-				e.preventDefault();
-				return;
-			}
-
-			e.dataTransfer.effectAllowed = 'move';
-			e.dataTransfer.setData('mk-deck-column', this.column.id);
-			this.dragging = true;
-		},
-
-		onDragend(e) {
-			this.dragging = false;
-		},
-
-		onDragover(e) {
-			// テンポラリカラムにはドロップさせない
-			if (this.isTemporaryColumn) {
-				e.dataTransfer.dropEffect = 'none';
-				return;
-			}
-
-			// 自分自身がドラッグされている場合
-			if (this.dragging) {
-				// 自分自身にはドロップさせない
-				e.dataTransfer.dropEffect = 'none';
-				return;
-			}
-
-			const isDeckColumn = e.dataTransfer.types[0] == 'mk-deck-column';
-
-			e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
-
-			if (!this.dragging && isDeckColumn) this.draghover = true;
-		},
-
-		onDragleave() {
-			this.draghover = false;
-		},
-
-		onDrop(e) {
-			this.draghover = false;
-			this.$root.$emit('deck.column.dragEnd');
-
-			const id = e.dataTransfer.getData('mk-deck-column');
-			if (id != null && id != '') {
-				this.$store.commit('swapDeckColumn', {
-					a: this.column.id,
-					b: id
-				});
-			}
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.dnpfarvgbnfmyzbdquhhzyxcmstpdqzs
-	$header-height = 42px
-
-	height 100%
-	background var(--face)
-	overflow hidden
-
-	&.round
-		border-radius 6px
-
-	&.shadow
-		box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
-
-	&.draghover
-		box-shadow 0 0 0 2px var(--primaryAlpha08)
-
-		&:after
-			content ""
-			display block
-			position absolute
-			z-index 1000
-			top 0
-			left 0
-			width 100%
-			height 100%
-			background var(--primaryAlpha02)
-
-	&.dragging
-		box-shadow 0 0 0 2px var(--primaryAlpha04)
-
-	&.dropready
-		*
-			pointer-events none
-
-	&:not(.active)
-		flex-basis $header-height
-		min-height $header-height
-
-	&:not(.isStacked).narrow
-		width 285px
-		min-width 285px
-		flex-grow 0 !important
-
-	&.naked
-		background var(--deckAcrylicColumnBg)
-
-		> header
-			background transparent
-			box-shadow none
-
-			> button
-				color var(--text)
-
-	&.isMobile
-		> header
-			box-shadow none
-
-	> header
-		display flex
-		z-index 2
-		line-height $header-height
-		padding 0 16px
-		font-size 14px
-		color var(--faceHeaderText)
-		background var(--faceHeader)
-		box-shadow 0 var(--lineWidth) rgba(#000, 0.15)
-		cursor pointer
-
-		&, *
-			user-select none
-
-		*:not(button)
-			pointer-events none
-
-		&.indicate
-			box-shadow 0 3px 0 0 var(--primary)
-
-		> .header
-			display inline-block
-			align-items center
-			overflow hidden
-			text-overflow ellipsis
-			white-space nowrap
-
-			[data-icon]
-				margin-right 8px
-
-		> .count
-			margin-left 4px
-			opacity 0.5
-		
-		> span:only-of-type
-			width 100%
-
-		> .toggleActive
-		> .menu
-		> .close
-			padding 0
-			width $header-height
-			line-height $header-height
-			font-size 16px
-			color var(--faceTextButton)
-
-			&:hover
-				color var(--faceTextButtonHover)
-
-			&:active
-				color var(--faceTextButtonActive)
-
-		> .toggleActive
-			margin-left -16px
-
-		> .menu
-		> .close
-			margin-left auto
-			margin-right -16px
-
-	> div
-		height "calc(100% - %s)" % $header-height
-		overflow auto
-		overflow-x hidden
-		-webkit-overflow-scrolling touch
-
-</style>
diff --git a/src/client/app/common/views/deck/deck.direct-column.vue b/src/client/app/common/views/deck/deck.direct-column.vue
deleted file mode 100644
index 66d34520afbe4a4c770c4af7d72aa1295a8a5aee..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.direct-column.vue
+++ /dev/null
@@ -1,79 +0,0 @@
-<template>
-<x-column :name="name" :column="column" :is-stacked="isStacked">
-	<template #header><fa :icon="['far', 'envelope']"/>{{ name }}</template>
-
-	<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
-</x-column>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XColumn from './deck.column.vue';
-import XNotes from './deck.notes.vue';
-
-export default Vue.extend({
-	i18n: i18n(),
-
-	components: {
-		XColumn,
-		XNotes
-	},
-
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			connection: null,
-			pagination: {
-				endpoint: 'notes/mentions',
-				limit: 10,
-				params: {
-					includeMyRenotes: this.$store.state.settings.showMyRenotes,
-					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
-					includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
-					visibility: 'specified'
-				}
-			}
-		};
-	},
-
-	computed: {
-		name(): string {
-			if (this.column.name) return this.column.name;
-			return this.$t('@deck.direct');
-		}
-	},
-
-	mounted() {
-		this.connection = this.$root.stream.useSharedConnection('main');
-		this.connection.on('mention', this.onNote);
-	},
-
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		onNote(note) {
-			// Prepend a note
-			if (note.visibility == 'specified') {
-				(this.$refs.timeline as any).prepend(note);
-			}
-		},
-
-		focus() {
-			this.$refs.timeline.focus();
-		}
-	}
-});
-</script>
diff --git a/src/client/app/common/views/deck/deck.hashtag-column.vue b/src/client/app/common/views/deck/deck.hashtag-column.vue
deleted file mode 100644
index 0d719c219987dee2cf7e2461305e18cc899de9df..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.hashtag-column.vue
+++ /dev/null
@@ -1,122 +0,0 @@
-<template>
-<x-column>
-	<template #header>
-		<fa icon="hashtag"/><span>{{ tag }}</span>
-	</template>
-
-	<div class="xroyrflcmhhtmlwmyiwpfqiirqokfueb">
-		<div ref="chart" class="chart"></div>
-		<x-hashtag-tl :tag-tl="tagTl" class="tl" :key="JSON.stringify(tagTl)"/>
-	</div>
-</x-column>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import XColumn from './deck.column.vue';
-import XHashtagTl from './deck.hashtag-tl.vue';
-import ApexCharts from 'apexcharts';
-
-export default Vue.extend({
-	components: {
-		XColumn,
-		XHashtagTl
-	},
-
-	computed: {
-		tag(): string {
-			return this.$route.params.tag;
-		},
-
-		tagTl(): any {
-			return {
-				query: [[this.tag]]
-			};
-		}
-	},
-
-	watch: {
-		$route: 'fetch'
-	},
-
-	created() {
-		this.fetch();
-	},
-
-	methods: {
-		fetch() {
-			this.$root.api('charts/hashtag', {
-				tag: this.tag,
-				span: 'hour',
-				limit: 24
-			}).then(stats => {
-				const local = [];
-				const remote = [];
-
-				const now = new Date();
-				const y = now.getFullYear();
-				const m = now.getMonth();
-				const d = now.getDate();
-				const h = now.getHours();
-
-				for (let i = 0; i < 24; i++) {
-					const x = new Date(y, m, d, h - i);
-					local.push([x, stats.local.count[i]]);
-					remote.push([x, stats.remote.count[i]]);
-				}
-
-				const chart = new ApexCharts(this.$refs.chart, {
-					chart: {
-						type: 'area',
-						height: 70,
-						sparkline: {
-							enabled: true
-						},
-					},
-					dataLabels: {
-						enabled: false
-					},
-					grid: {
-						clipMarkers: false,
-						padding: {
-							top: 16,
-							right: 16,
-							bottom: 16,
-							left: 16
-						}
-					},
-					stroke: {
-						curve: 'straight',
-						width: 2
-					},
-					series: [{
-						name: 'Local',
-						data: local
-					}, {
-						name: 'Remote',
-						data: remote
-					}],
-					xaxis: {
-						type: 'datetime',
-					}
-				});
-
-				chart.render();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.xroyrflcmhhtmlwmyiwpfqiirqokfueb
-	background var(--deckColumnBg)
-
-	> .chart
-		margin-bottom 16px
-		background var(--face)
-
-	> .tl
-		background var(--face)
-
-</style>
diff --git a/src/client/app/common/views/deck/deck.hashtag-tl.vue b/src/client/app/common/views/deck/deck.hashtag-tl.vue
deleted file mode 100644
index 94d2efc430003c2020eb8a32e4946b75e77a8315..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.hashtag-tl.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-<template>
-<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import XNotes from './deck.notes.vue';
-
-export default Vue.extend({
-	components: {
-		XNotes
-	},
-
-	props: {
-		tagTl: {
-			type: Object,
-			required: true
-		},
-		mediaOnly: {
-			type: Boolean,
-			required: false,
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			connection: null,
-			pagination: {
-				endpoint: 'notes/search-by-tag',
-				limit: 10,
-				params: init => ({
-					untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
-					withFiles: this.mediaOnly,
-					includeMyRenotes: this.$store.state.settings.showMyRenotes,
-					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
-					includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
-					query: this.tagTl.query
-				})
-			}
-		};
-	},
-
-	watch: {
-		mediaOnly() {
-			this.$refs.timeline.reload();
-		}
-	},
-
-	mounted() {
-		if (this.connection) this.connection.close();
-		this.connection = this.$root.stream.connectToChannel('hashtag', {
-			q: this.tagTl.query
-		});
-		this.connection.on('note', this.onNote);
-	},
-
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		onNote(note) {
-			if (this.mediaOnly && note.files.length == 0) return;
-			(this.$refs.timeline as any).prepend(note);
-		},
-
-		focus() {
-			this.$refs.timeline.focus();
-		}
-	}
-});
-</script>
diff --git a/src/client/app/common/views/deck/deck.list-tl.vue b/src/client/app/common/views/deck/deck.list-tl.vue
deleted file mode 100644
index 26d6ea9d5834b18c52de5d7d598bd8913910f321..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.list-tl.vue
+++ /dev/null
@@ -1,83 +0,0 @@
-<template>
-<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import XNotes from './deck.notes.vue';
-
-export default Vue.extend({
-	components: {
-		XNotes
-	},
-
-	props: {
-		list: {
-			type: Object,
-			required: true
-		},
-		mediaOnly: {
-			type: Boolean,
-			required: false,
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			connection: null,
-			pagination: {
-				endpoint: 'notes/user-list-timeline',
-				limit: 10,
-				params: init => ({
-					listId: this.list.id,
-					untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
-					withFiles: this.mediaOnly,
-					includeMyRenotes: this.$store.state.settings.showMyRenotes,
-					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
-					includeLocalRenotes: this.$store.state.settings.showLocalRenotes
-				})
-			}
-		};
-	},
-
-	watch: {
-		mediaOnly() {
-			this.$refs.timeline.reload();
-		}
-	},
-
-	mounted() {
-		if (this.connection) this.connection.dispose();
-		this.connection = this.$root.stream.connectToChannel('userList', {
-			listId: this.list.id
-		});
-		this.connection.on('note', this.onNote);
-		this.connection.on('userAdded', this.onUserAdded);
-		this.connection.on('userRemoved', this.onUserRemoved);
-	},
-
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		onNote(note) {
-			if (this.mediaOnly && note.files.length == 0) return;
-			(this.$refs.timeline as any).prepend(note);
-		},
-
-		onUserAdded() {
-			this.$refs.timeline.reload();
-		},
-
-		onUserRemoved() {
-			this.$refs.timeline.reload();
-		},
-
-		focus() {
-			this.$refs.timeline.focus();
-		}
-	}
-});
-</script>
diff --git a/src/client/app/common/views/deck/deck.mentions-column.vue b/src/client/app/common/views/deck/deck.mentions-column.vue
deleted file mode 100644
index 12d7b2a16b18107607a71adcc4d57f2276e94a79..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.mentions-column.vue
+++ /dev/null
@@ -1,76 +0,0 @@
-<template>
-<x-column :name="name" :column="column" :is-stacked="isStacked">
-	<template #header><fa icon="at"/>{{ name }}</template>
-
-	<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
-</x-column>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XColumn from './deck.column.vue';
-import XNotes from './deck.notes.vue';
-
-export default Vue.extend({
-	i18n: i18n(),
-
-	components: {
-		XColumn,
-		XNotes
-	},
-
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			connection: null,
-			pagination: {
-				endpoint: 'notes/mentions',
-				limit: 10,
-				params: init => ({
-					untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
-					includeMyRenotes: this.$store.state.settings.showMyRenotes,
-					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
-					includeLocalRenotes: this.$store.state.settings.showLocalRenotes
-				})
-			}
-		};
-	},
-
-	computed: {
-		name(): string {
-			if (this.column.name) return this.column.name;
-			return this.$t('@deck.mentions');
-		}
-	},
-
-	mounted() {
-		this.connection = this.$root.stream.useSharedConnection('main');
-		this.connection.on('mention', this.onNote);
-	},
-
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		onNote(note) {
-			(this.$refs.timeline as any).prepend(note);
-		},
-
-		focus() {
-			this.$refs.timeline.focus();
-		}
-	}
-});
-</script>
diff --git a/src/client/app/common/views/deck/deck.note-column.vue b/src/client/app/common/views/deck/deck.note-column.vue
deleted file mode 100644
index bcc887e2fd93d3b1a3978e7c3b93dce95838b4e4..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.note-column.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-<template>
-<x-column>
-	<template #header>
-		<fa :icon="['far', 'comment-alt']"/><mk-user-name :user="note.user" v-if="note"/>
-	</template>
-
-	<div class="rvtscbadixhhbsczoorqoaygovdeecsx" v-if="note">
-		<div class="is-remote" v-if="note.user.host != null">
-			<details>
-				<summary><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-post') }}</summary>
-				<a :href="note.url || note.uri" rel="nofollow noopener" target="_blank">{{ $t('@.view-on-remote') }}</a>
-			</details>
-		</div>
-		<mk-note :note="note" :detail="true" :key="note.id"/>
-	</div>
-</x-column>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XColumn from './deck.column.vue';
-
-export default Vue.extend({
-	i18n: i18n(),
-	components: {
-		XColumn,
-	},
-
-	data() {
-		return {
-			note: null,
-			fetching: true
-		};
-	},
-
-	watch: {
-		$route: 'fetch'
-	},
-
-	created() {
-		this.fetch();
-	},
-
-	methods: {
-		fetch() {
-			this.fetching = true;
-
-			this.$root.api('notes/show', {
-				noteId: this.$route.params.note
-			}).then(note => {
-				this.note = note;
-				this.fetching = false;
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.rvtscbadixhhbsczoorqoaygovdeecsx
-	> .is-remote
-		padding 8px 16px
-		font-size 12px
-
-		&.is-remote
-			color var(--remoteInfoFg)
-			background var(--remoteInfoBg)
-
-		> a
-			font-weight bold
-
-</style>
diff --git a/src/client/app/common/views/deck/deck.notes.vue b/src/client/app/common/views/deck/deck.notes.vue
deleted file mode 100644
index 5081d1f99873da2fb94297817652455b6cd852b1..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.notes.vue
+++ /dev/null
@@ -1,168 +0,0 @@
-<template>
-<div class="eamppglmnmimdhrlzhplwpvyeaqmmhxu">
-	<div class="empty" v-if="empty">{{ $t('@.no-notes') }}</div>
-
-	<mk-error v-if="error" @retry="init()"/>
-
-	<div class="placeholder" v-if="fetching">
-		<template v-for="i in 10">
-			<mk-note-skeleton :key="i"/>
-		</template>
-	</div>
-
-	<!-- トランジションを有効にするとなぜかメモリリークする -->
-	<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition notes" ref="notes" tag="div">
-		<template v-for="(note, i) in _notes">
-			<mk-note
-				:note="note"
-				:key="note.id"
-				:compact="true"
-			/>
-			<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
-				<span><fa icon="angle-up"/>{{ note._datetext }}</span>
-				<span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span>
-			</p>
-		</template>
-	</component>
-
-	<footer v-if="more">
-		<button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
-			<template v-if="!moreFetching">{{ $t('@.load-more') }}</template>
-			<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>
-		</button>
-	</footer>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import shouldMuteNote from '../../../common/scripts/should-mute-note';
-import paging from '../../../common/scripts/paging';
-
-export default Vue.extend({
-	i18n: i18n(),
-
-	inject: ['column', 'isScrollTop', 'count'],
-
-	mixins: [
-		paging({
-			limit: 20,
-
-			onQueueChanged: (self, q) => {
-				self.count(q.length);
-			},
-
-			onPrepend: (self, note, silent) => {
-				// 弾く
-				if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false;
-
-				// タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
-				if (document.hidden || !self.isScrollTop()) {
-					self.$store.commit('pushBehindNote', note);
-				}
-			}
-		}),
-	],
-
-	props: {
-		pagination: {
-			required: true
-		},
-		extract: {
-			required: false
-		}
-	},
-
-	computed: {
-		notes() {
-			return this.extract ? this.extract(this.items) : this.items;
-		},
-
-		_notes(): any[] {
-			return (this.notes as any).map(note => {
-				const date = new Date(note.createdAt).getDate();
-				const month = new Date(note.createdAt).getMonth() + 1;
-				note._date = date;
-				note._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
-				return note;
-			});
-		}
-	},
-
-	created() {
-		this.column.$on('top', this.onTop);
-		this.column.$on('bottom', this.onBottom);
-		this.init();
-	},
-
-	beforeDestroy() {
-		this.column.$off('top', this.onTop);
-		this.column.$off('bottom', this.onBottom);
-	},
-
-	methods: {
-		focus() {
-			(this.$refs.notes as any).children[0].focus ? (this.$refs.notes as any).children[0].focus() : (this.$refs.notes as any).$el.children[0].focus();
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.eamppglmnmimdhrlzhplwpvyeaqmmhxu
-	.transition
-		.mk-notes-enter
-		.mk-notes-leave-to
-			opacity 0
-			transform translateY(-30px)
-
-		> *
-			transition transform .3s ease, opacity .3s ease
-
-	> .empty
-		padding 16px
-		text-align center
-		color var(--text)
-
-	> .placeholder
-		padding 16px
-		opacity 0.3
-
-	> .notes
-		> .date
-			display block
-			margin 0
-			line-height 28px
-			font-size 12px
-			text-align center
-			color var(--dateDividerFg)
-			background var(--dateDividerBg)
-			border-bottom solid var(--lineWidth) var(--faceDivider)
-
-			span
-				margin 0 16px
-
-			[data-icon]
-				margin-right 8px
-
-	> footer
-		> button
-			display block
-			margin 0
-			padding 16px
-			width 100%
-			text-align center
-			color #ccc
-			background var(--face)
-			border-top solid var(--lineWidth) var(--faceDivider)
-			border-bottom-left-radius 6px
-			border-bottom-right-radius 6px
-
-			&:hover
-				box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
-
-			&:active
-				box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
-
-</style>
diff --git a/src/client/app/common/views/deck/deck.notification.vue b/src/client/app/common/views/deck/deck.notification.vue
deleted file mode 100644
index da122ff4db39d4351eb071918fa0f7580c077e11..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.notification.vue
+++ /dev/null
@@ -1,186 +0,0 @@
-<template>
-<div class="dsfykdcjpuwfvpefwufddclpjhzktmpw">
-	<div class="notification reaction" v-if="notification.type == 'reaction'">
-		<mk-avatar class="avatar" :user="notification.user"/>
-		<div>
-			<header>
-				<mk-reaction-icon :reaction="notification.reaction" class="icon"/>
-				<router-link :to="notification.user | userPage" class="name">
-					<mk-user-name :user="notification.user"/>
-				</router-link>
-				<mk-time :time="notification.createdAt"/>
-			</header>
-			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
-				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
-				<fa icon="quote-right"/>
-			</router-link>
-		</div>
-	</div>
-
-	<div class="notification renote" v-if="notification.type == 'renote'">
-		<mk-avatar class="avatar" :user="notification.user"/>
-		<div>
-			<header>
-				<fa icon="retweet" class="icon"/>
-				<router-link :to="notification.user | userPage" class="name">
-					<mk-user-name :user="notification.user"/>
-				</router-link>
-				<mk-time :time="notification.createdAt"/>
-			</header>
-			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)">
-				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :custom-emojis="notification.note.renote.emojis"/>
-				<fa icon="quote-right"/>
-			</router-link>
-		</div>
-	</div>
-
-	<div class="notification follow" v-if="notification.type == 'follow'">
-		<mk-avatar class="avatar" :user="notification.user"/>
-		<div>
-			<header>
-				<fa icon="user-plus" class="icon"/>
-				<router-link :to="notification.user | userPage" class="name">
-					<mk-user-name :user="notification.user"/>
-				</router-link>
-				<mk-time :time="notification.createdAt"/>
-			</header>
-		</div>
-	</div>
-
-	<div class="notification followRequest" v-if="notification.type == 'receiveFollowRequest'">
-		<mk-avatar class="avatar" :user="notification.user"/>
-		<div>
-			<header>
-				<fa icon="user-clock" class="icon"/>
-				<router-link :to="notification.user | userPage" class="name">
-					<mk-user-name :user="notification.user"/>
-				</router-link>
-				<mk-time :time="notification.createdAt"/>
-			</header>
-		</div>
-	</div>
-
-	<div class="notification pollVote" v-if="notification.type == 'pollVote'">
-		<mk-avatar class="avatar" :user="notification.user"/>
-		<div>
-			<header>
-				<fa icon="chart-pie" class="icon"/>
-				<router-link :to="notification.user | userPage" class="name">
-					<mk-user-name :user="notification.user"/>
-				</router-link>
-				<mk-time :time="notification.createdAt"/>
-			</header>
-			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
-				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
-				<fa icon="quote-right"/>
-			</router-link>
-		</div>
-	</div>
-
-	<template v-if="notification.type == 'quote'">
-		<mk-note :note="notification.note"/>
-	</template>
-
-	<template v-if="notification.type == 'reply'">
-		<mk-note :note="notification.note"/>
-	</template>
-
-	<template v-if="notification.type == 'mention'">
-		<mk-note :note="notification.note"/>
-	</template>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import getNoteSummary from '../../../../../misc/get-note-summary';
-
-export default Vue.extend({
-	props: ['notification'],
-	data() {
-		return {
-			getNoteSummary
-		};
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-.dsfykdcjpuwfvpefwufddclpjhzktmpw
-	> .notification
-		padding 16px
-		font-size 12px
-		overflow-wrap break-word
-
-		&:after
-			content ""
-			display block
-			clear both
-
-		> .avatar
-			display block
-			float left
-			width 36px
-			height 36px
-			border-radius 6px
-
-		> div
-			float right
-			width calc(100% - 36px)
-			padding-left 8px
-
-			> header
-				display flex
-				align-items baseline
-				white-space nowrap
-
-				> .icon
-					margin-right 4px
-
-				> .name
-					overflow hidden
-					text-overflow ellipsis
-
-				> .mk-time
-					margin-left auto
-					color var(--noteHeaderInfo)
-					font-size 0.9em
-
-			> .note-preview
-				color var(--noteText)
-
-			> .note-ref
-				color var(--noteText)
-				display inline-block
-				width: 100%
-				overflow hidden
-				white-space nowrap
-				text-overflow ellipsis
-
-				[data-icon]
-					font-size 1em
-					font-weight normal
-					font-style normal
-					display inline-block
-					margin-right 3px
-
-		&.reaction
-			> div > header
-				align-items normal
-
-		&.renote
-			> div > header [data-icon]
-				color #77B255
-
-		&.follow
-			> div > header [data-icon]
-				color #53c7ce
-
-		&.receiveFollowRequest
-			> div > header [data-icon]
-				color #888
-
-</style>
diff --git a/src/client/app/common/views/deck/deck.notifications-column.vue b/src/client/app/common/views/deck/deck.notifications-column.vue
deleted file mode 100644
index b4361b054ae33ce7d4519dea46533fcbedd4fdcc..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.notifications-column.vue
+++ /dev/null
@@ -1,75 +0,0 @@
-<template>
-<x-column :name="name" :column="column" :is-stacked="isStacked" :menu="menu">
-	<template #header><fa :icon="['far', 'bell']"/>{{ name }}</template>
-
-	<x-notifications :type="column.notificationType === 'all' ? null : column.notificationType"/>
-</x-column>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XColumn from './deck.column.vue';
-import XNotifications from './deck.notifications.vue';
-
-export default Vue.extend({
-	i18n: i18n(),
-	components: {
-		XColumn,
-		XNotifications
-	},
-
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			menu: null,
-		}
-	},
-
-	computed: {
-		name(): string {
-			if (this.column.name) return this.column.name;
-			return this.$t('@deck.notifications');
-		}
-	},
-
-	created() {
-		if (this.column.notificationType == null) {
-			this.column.notificationType = 'all';
-			this.$store.commit('updateDeckColumn', this.column);
-		}
-
-		this.menu = [{
-			icon: 'cog',
-			text: this.$t('@.notification-type'),
-			action: () => {
-				this.$root.dialog({
-					title: this.$t('@.notification-type'),
-					type: null,
-					select: {
-						items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({
-							value: x, text: this.$t('@.notification-types.' + x)
-						}))
-						default: this.column.notificationType,
-					},
-					showCancelButton: true
-				}).then(({ canceled, result: type }) => {
-					if (canceled) return;
-					this.column.notificationType = type;
-					this.$store.commit('updateDeckColumn', this.column);
-				});
-			}
-		}];
-	},
-});
-</script>
diff --git a/src/client/app/common/views/deck/deck.notifications.vue b/src/client/app/common/views/deck/deck.notifications.vue
deleted file mode 100644
index aed2af64e987b6e19c8cdccd9c07c4314009dc08..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.notifications.vue
+++ /dev/null
@@ -1,177 +0,0 @@
-<template>
-<div class="oxynyeqmfvracxnglgulyqfgqxnxmehl">
-	<div class="placeholder" v-if="fetching">
-		<template v-for="i in 10">
-			<mk-note-skeleton :key="i"/>
-		</template>
-	</div>
-
-	<!-- トランジションを有効にするとなぜかメモリリークする -->
-	<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications" tag="div">
-		<template v-for="(notification, i) in _notifications">
-			<x-notification class="notification" :notification="notification" :key="notification.id"/>
-			<p class="date" v-if="i != items.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
-				<span><fa icon="angle-up"/>{{ notification._datetext }}</span>
-				<span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span>
-			</p>
-		</template>
-	</component>
-	<button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMore" :disabled="moreFetching">
-		<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? this.$t('@.loading') : this.$t('@.load-more') }}
-	</button>
-	<p class="empty" v-if="empty">{{ $t('empty') }}</p>
-	<mk-error v-if="error" @retry="init()"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XNotification from './deck.notification.vue';
-import paging from '../../../common/scripts/paging';
-
-export default Vue.extend({
-	i18n: i18n(),
-
-	components: {
-		XNotification
-	},
-
-	inject: ['column', 'isScrollTop', 'count'],
-
-	mixins: [
-		paging({
-			onQueueChanged: (self, q) => {
-				self.count(q.length);
-			},
-		}),
-	],
-
-	props: {
-		type: {
-			type: String,
-			required: false
-		}
-	},
-
-	data() {
-		return {
-			connection: null,
-			pagination: {
-				endpoint: 'i/notifications',
-				limit: 20,
-				params: () => ({
-					includeTypes: this.type ? [this.type] : undefined
-				})
-			}
-		};
-	},
-
-	computed: {
-		_notifications(): any[] {
-			return (this.items as any).map(notification => {
-				const date = new Date(notification.createdAt).getDate();
-				const month = new Date(notification.createdAt).getMonth() + 1;
-				notification._date = date;
-				notification._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
-				return notification;
-			});
-		}
-	},
-
-	watch: {
-		type() {
-			this.reload();
-		}
-	},
-
-	mounted() {
-		this.connection = this.$root.stream.useSharedConnection('main');
-		this.connection.on('notification', this.onNotification);
-
-		this.column.$on('top', this.onTop);
-		this.column.$on('bottom', this.onBottom);
-	},
-
-	beforeDestroy() {
-		this.connection.dispose();
-
-		this.column.$off('top', this.onTop);
-		this.column.$off('bottom', this.onBottom);
-	},
-
-	methods: {
-		onNotification(notification) {
-			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
-			this.$root.stream.send('readNotification', {
-				id: notification.id
-			});
-
-			this.prepend(notification);
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.oxynyeqmfvracxnglgulyqfgqxnxmehl
-	.transition
-		.mk-notifications-enter
-		.mk-notifications-leave-to
-			opacity 0
-			transform translateY(-30px)
-
-		> *
-			transition transform .3s ease, opacity .3s ease
-
-	> .placeholder
-		padding 16px
-		opacity 0.3
-
-	> .notifications
-
-		> .notification:not(:last-child)
-			border-bottom solid var(--lineWidth) var(--faceDivider)
-
-		> .date
-			display block
-			margin 0
-			line-height 28px
-			text-align center
-			font-size 12px
-			color var(--dateDividerFg)
-			background var(--dateDividerBg)
-			border-bottom solid var(--lineWidth) var(--faceDivider)
-
-			span
-				margin 0 16px
-
-			[data-icon]
-				margin-right 8px
-
-	> .more
-		display block
-		width 100%
-		padding 16px
-		color var(--text)
-		border-top solid var(--lineWidth) rgba(#000, 0.05)
-
-		&:hover
-			background rgba(#000, 0.025)
-
-		&:active
-			background rgba(#000, 0.05)
-
-		&.fetching
-			cursor wait
-
-		> [data-icon]
-			margin-right 4px
-
-	> .empty
-		margin 0
-		padding 16px
-		text-align center
-		color var(--text)
-
-</style>
diff --git a/src/client/app/common/views/deck/deck.page-column.vue b/src/client/app/common/views/deck/deck.page-column.vue
deleted file mode 100644
index 0ef391a51db9927f25756ef64d6eda38754825fb..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.page-column.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-<template>
-<x-column>
-	<template #header>
-		<fa :icon="faStickyNote"/>{{ page ? page.name : '' }}
-	</template>
-
-	<div v-if="page">
-		<x-page :page="page" :key="page.id"/>
-	</div>
-</x-column>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../../../i18n';
-import XColumn from './deck.column.vue';
-import XPage from '../../../common/views/components/page/page.vue';
-
-export default Vue.extend({
-	i18n: i18n(),
-
-	components: {
-		XColumn,
-		XPage
-	},
-
-	props: {
-		pageName: {
-			type: String,
-			required: true
-		},
-		username: {
-			type: String,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			page: null,
-			faStickyNote
-		};
-	},
-
-	watch: {
-		$route: 'fetch'
-	},
-
-	created() {
-		this.fetch();
-	},
-
-	methods: {
-		fetch() {
-			this.$root.api('pages/show', {
-				name: this.pageName,
-				username: this.username,
-			}).then(page => {
-				this.page = page;
-				this.$emit('init', {
-					title: this.page.title,
-					icon: faStickyNote
-				});
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/app/common/views/deck/deck.search-column.vue b/src/client/app/common/views/deck/deck.search-column.vue
deleted file mode 100644
index a2d1142fbe1c9bc470246d4bc19204088f33e504..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.search-column.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<template>
-<x-column>
-	<template #header>
-		<fa icon="search"/><span>{{ q }}</span>
-	</template>
-
-	<div>
-		<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
-	</div>
-</x-column>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import XColumn from './deck.column.vue';
-import XNotes from './deck.notes.vue';
-import { genSearchQuery } from '../../../common/scripts/gen-search-query';
-
-export default Vue.extend({
-	components: {
-		XColumn,
-		XNotes
-	},
-
-	data() {
-		return {
-			pagination: {
-				endpoint: 'notes/search',
-				limit: 20,
-				params: () => genSearchQuery(this, this.q)
-			}
-		};
-	},
-
-	computed: {
-		q(): string {
-			return this.$route.query.q;
-		}
-	},
-
-	watch: {
-		$route() {
-			this.$refs.timeline.reload();
-		}
-	},
-});
-</script>
diff --git a/src/client/app/common/views/deck/deck.tl-column.vue b/src/client/app/common/views/deck/deck.tl-column.vue
deleted file mode 100644
index cad140ed5f5f90e8d561b392ca9c03f9419bd84b..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.tl-column.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<template>
-<x-column :menu="menu" :name="name" :column="column" :is-stacked="isStacked">
-	<template #header>
-		<fa v-if="column.type == 'home'" icon="home"/>
-		<fa v-if="column.type == 'local'" :icon="['far', 'comments']"/>
-		<fa v-if="column.type == 'hybrid'" icon="share-alt"/>
-		<fa v-if="column.type == 'global'" icon="globe"/>
-		<fa v-if="column.type == 'list'" icon="list"/>
-		<fa v-if="column.type == 'hashtag'" icon="hashtag"/>
-		<span>{{ name }}</span>
-	</template>
-
-	<div class="editor" style="padding:12px" v-if="edit">
-		<ui-switch v-model="column.isMediaOnly" @change="onChangeSettings">{{ $t('is-media-only') }}</ui-switch>
-	</div>
-
-	<x-list-tl v-if="column.type == 'list'"
-		:list="column.list"
-		:media-only="column.isMediaOnly"
-		ref="tl"
-	/>
-	<x-hashtag-tl v-else-if="column.type == 'hashtag'"
-		:tag-tl="$store.state.settings.tagTimelines.find(x => x.id == column.tagTlId)"
-		:media-only="column.isMediaOnly"
-		ref="tl"
-	/>
-	<x-tl v-else
-		:src="column.type"
-		:media-only="column.isMediaOnly"
-		ref="tl"
-	/>
-</x-column>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XColumn from './deck.column.vue';
-import XTl from './deck.tl.vue';
-import XListTl from './deck.list-tl.vue';
-import XHashtagTl from './deck.hashtag-tl.vue';
-
-export default Vue.extend({
-	i18n: i18n('deck/deck.tl-column.vue'),
-	components: {
-		XColumn,
-		XTl,
-		XListTl,
-		XHashtagTl
-	},
-
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			edit: false,
-			menu: [{
-				icon: 'cog',
-				text: this.$t('edit'),
-				action: () => {
-					this.edit = !this.edit;
-				}
-			}]
-		}
-	},
-
-	computed: {
-		name(): string {
-			if (this.column.name) return this.column.name;
-
-			switch (this.column.type) {
-				case 'home': return this.$t('@deck.home');
-				case 'local': return this.$t('@deck.local');
-				case 'hybrid': return this.$t('@deck.hybrid');
-				case 'global': return this.$t('@deck.global');
-				case 'list': return this.column.list.name;
-				case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title;
-			}
-		}
-	},
-
-	methods: {
-		onChangeSettings(v) {
-			this.$store.commit('updateDeckColumn', this.column);
-		},
-
-		focus() {
-			this.$refs.tl.focus();
-		}
-	}
-});
-</script>
diff --git a/src/client/app/common/views/deck/deck.tl.vue b/src/client/app/common/views/deck/deck.tl.vue
deleted file mode 100644
index e6c716070ad144cda500ad01ed6cf6bf19d987eb..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.tl.vue
+++ /dev/null
@@ -1,135 +0,0 @@
-<template>
-<div class="iwaalbte" v-if="disabled">
-	<p>
-		<fa :icon="faMinusCircle"/>
-		{{ $t('disabled-timeline.title') }}
-	</p>
-	<p class="desc">{{ $t('disabled-timeline.description') }}</p>
-</div>
-<x-notes v-else ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import XNotes from './deck.notes.vue';
-import { faMinusCircle } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('deck'),
-
-	components: {
-		XNotes
-	},
-
-	props: {
-		src: {
-			type: String,
-			required: false,
-			default: 'home'
-		},
-		mediaOnly: {
-			type: Boolean,
-			required: false,
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			connection: null,
-			disabled: false,
-			faMinusCircle,
-			pagination: null
-		};
-	},
-
-	computed: {
-		stream(): any {
-			switch (this.src) {
-				case 'home': return this.$root.stream.useSharedConnection('homeTimeline');
-				case 'local': return this.$root.stream.useSharedConnection('localTimeline');
-				case 'hybrid': return this.$root.stream.useSharedConnection('hybridTimeline');
-				case 'global': return this.$root.stream.useSharedConnection('globalTimeline');
-			}
-		},
-
-		endpoint(): string {
-			switch (this.src) {
-				case 'home': return 'notes/timeline';
-				case 'local': return 'notes/local-timeline';
-				case 'hybrid': return 'notes/hybrid-timeline';
-				case 'global': return 'notes/global-timeline';
-			}
-		},
-	},
-
-	watch: {
-		mediaOnly() {
-			(this.$refs.timeline as any).reload();
-		}
-	},
-
-	created() {
-		this.pagination = {
-			endpoint: this.endpoint,
-			limit: 10,
-			params: init => ({
-				untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
-				withFiles: this.mediaOnly,
-				includeMyRenotes: this.$store.state.settings.showMyRenotes,
-				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
-				includeLocalRenotes: this.$store.state.settings.showLocalRenotes
-			})
-		};
-	},
-
-	mounted() {
-		this.connection = this.stream;
-
-		this.connection.on('note', this.onNote);
-		if (this.src == 'home') {
-			this.connection.on('follow', this.onChangeFollowing);
-			this.connection.on('unfollow', this.onChangeFollowing);
-		}
-
-		this.$root.getMeta().then(meta => {
-			this.disabled = !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin && (
-				meta.disableLocalTimeline && ['local', 'hybrid'].includes(this.src) ||
-				meta.disableGlobalTimeline && ['global'].includes(this.src));
-		});
-	},
-
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		onNote(note) {
-			if (this.mediaOnly && note.files.length == 0) return;
-			(this.$refs.timeline as any).prepend(note);
-		},
-
-		onChangeFollowing() {
-			(this.$refs.timeline as any).reload();
-		},
-
-		focus() {
-			(this.$refs.timeline as any).focus();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.iwaalbte
-	color var(--text)
-	text-align center
-
-	> p
-		margin 16px
-
-		&.desc
-			font-size 14px
-
-</style>
diff --git a/src/client/app/common/views/deck/deck.user-column.home.vue b/src/client/app/common/views/deck/deck.user-column.home.vue
deleted file mode 100644
index 9fb50a6672ddd9c2c016f775fa35690740c06af5..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.user-column.home.vue
+++ /dev/null
@@ -1,229 +0,0 @@
-<template>
-<div>
-	<ui-container v-if="user.pinnedPage" :body-togglable="true">
-		<template #header><fa icon="thumbtack"/> {{ $t('pinned-page') }}</template>
-		<div>
-			<x-page :page="user.pinnedPage" :key="user.pinnedPage.id" :show-title="!user.pinnedPage.hideTitleWhenPinned"/>
-		</div>
-	</ui-container>
-	<ui-container v-if="user.pinnedNotes && user.pinnedNotes.length > 0" :body-togglable="true">
-		<template #header><fa icon="thumbtack"/> {{ $t('pinned-notes') }}</template>
-		<div>
-			<mk-note v-for="n in user.pinnedNotes" :key="n.id" :note="n"/>
-		</div>
-	</ui-container>
-	<ui-container v-if="images.length > 0" :body-togglable="true"
-		:expanded="$store.state.device.expandUsersPhotos"
-		@toggle="expanded => $store.commit('device/set', { key: 'expandUsersPhotos', value: expanded })">
-		<template #header><fa :icon="['far', 'images']"/> {{ $t('images') }}</template>
-		<div class="sainvnaq">
-			<router-link v-for="image in images"
-				:style="`background-image: url(${image.thumbnailUrl})`"
-				:key="`${image.id}:${image._note.id}`"
-				:to="image._note | notePage"
-				:title="`${image.name}\n${(new Date(image.createdAt)).toLocaleString()}`"
-			></router-link>
-		</div>
-	</ui-container>
-	<ui-container :body-togglable="true"
-		:expanded="$store.state.device.expandUsersActivity"
-		@toggle="expanded => $store.commit('device/set', { key: 'expandUsersActivity', value: expanded })">
-		<template #header><fa :icon="['far', 'chart-bar']"/> {{ $t('activity') }}</template>
-		<div>
-			<div ref="chart"></div>
-		</div>
-	</ui-container>
-	<ui-container>
-		<template #header><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</template>
-		<div>
-			<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')" :key="user.id"/>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XNotes from './deck.notes.vue';
-import { concat } from '../../../../../prelude/array';
-import ApexCharts from 'apexcharts';
-
-export default Vue.extend({
-	i18n: i18n('deck/deck.user-column.vue'),
-
-	components: {
-		XNotes,
-		XPage: () => import('../../../common/views/components/page/page.vue').then(m => m.default),
-	},
-
-	props: {
-		user: {
-			type: Object,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			withFiles: false,
-			images: [],
-			chart: null as ApexCharts
-		};
-	},
-
-	computed: {
-		pagination() {
-			return {
-				endpoint: 'users/notes',
-				limit: 10,
-				params: init => ({
-					userId: this.user.id,
-					untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
-					withFiles: this.withFiles,
-					includeMyRenotes: this.$store.state.settings.showMyRenotes,
-					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
-					includeLocalRenotes: this.$store.state.settings.showLocalRenotes
-				})
-			}
-		}
-	},
-
-	watch: {
-		user() {
-			this.fetch();
-		}
-	},
-
-	created() {
-		this.fetch();
-	},
-
-	methods: {
-		fetch() {
-			const image = [
-				'image/jpeg',
-				'image/png',
-				'image/gif',
-				'image/apng',
-				'image/vnd.mozilla.apng',
-			];
-
-			this.$root.api('users/notes', {
-				userId: this.user.id,
-				fileType: image,
-				excludeNsfw: !this.$store.state.device.alwaysShowNsfw,
-				limit: 9,
-			}).then(notes => {
-				for (const note of notes) {
-					for (const file of note.files) {
-						file._note = note;
-					}
-				}
-				const files = concat(notes.map((n: any): any[] => n.files));
-				this.images = files.filter(f => image.includes(f.type)).slice(0, 9);
-			});
-
-			this.$root.api('charts/user/notes', {
-				userId: this.user.id,
-				span: 'day',
-				limit: 21
-			}).then(stats => {
-				const normal = [];
-				const reply = [];
-				const renote = [];
-
-				const now = new Date();
-				const y = now.getFullYear();
-				const m = now.getMonth();
-				const d = now.getDate();
-
-				for (let i = 0; i < 21; i++) {
-					const x = new Date(y, m, d - i);
-					normal.push([
-						x,
-						stats.diffs.normal[i]
-					]);
-					reply.push([
-						x,
-						stats.diffs.reply[i]
-					]);
-					renote.push([
-						x,
-						stats.diffs.renote[i]
-					]);
-				}
-
-				if (this.chart) this.chart.destroy();
-
-				this.chart = new ApexCharts(this.$refs.chart, {
-					chart: {
-						type: 'bar',
-						stacked: true,
-						height: 100,
-						sparkline: {
-							enabled: true
-						},
-					},
-					plotOptions: {
-						bar: {
-							columnWidth: '80%'
-						}
-					},
-					dataLabels: {
-						enabled: false
-					},
-					grid: {
-						clipMarkers: false,
-						padding: {
-							top: 16,
-							right: 16,
-							bottom: 16,
-							left: 16
-						}
-					},
-					tooltip: {
-						shared: true,
-						intersect: false
-					},
-					series: [{
-						name: 'Normal',
-						data: normal
-					}, {
-						name: 'Reply',
-						data: reply
-					}, {
-						name: 'Renote',
-						data: renote
-					}],
-					xaxis: {
-						type: 'datetime',
-						crosshairs: {
-							width: 1,
-							opacity: 1
-						}
-					}
-				});
-
-				this.chart.render();
-			});
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.sainvnaq
-	display grid
-	grid-template-columns 1fr 1fr 1fr
-	gap 8px
-	padding 16px
-
-	> *
-		height 70px
-		background-position center center
-		background-size cover
-		background-clip content-box
-		border-radius 4px
-
-</style>
diff --git a/src/client/app/common/views/deck/deck.user-column.vue b/src/client/app/common/views/deck/deck.user-column.vue
deleted file mode 100644
index bc8cbc3154b4e4dc5e3e86e665ef143bb840e484..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.user-column.vue
+++ /dev/null
@@ -1,266 +0,0 @@
-<template>
-<x-column>
-	<template #header>
-		<fa icon="user"/><mk-user-name :user="user" v-if="user" :key="user.id"/>
-	</template>
-
-	<div class="zubukjlciycdsyynicqrnlsmdwmymzqu" v-if="user">
-		<div class="is-remote" v-if="user.host != null">
-			<details>
-				<summary><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}</summary>
-				<a :href="user.url" rel="nofollow noopener" target="_blank">{{ $t('@.view-on-remote') }}</a>
-			</details>
-		</div>
-		<header :style="bannerStyle">
-			<div>
-				<button class="menu" @click="menu" ref="menu"><fa icon="ellipsis-h"/></button>
-				<mk-follow-button v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" class="follow" mini/>
-				<mk-avatar class="avatar" :user="user" :disable-preview="true" :key="user.id"/>
-				<router-link class="name" :to="user | userPage()">
-					<mk-user-name :user="user" :key="user.id" :nowrap="false"/>
-				</router-link>
-				<span class="acct">@{{ user | acct }} <fa v-if="user.isLocked == true" class="locked" icon="lock" fixed-width/></span>
-				<span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span>
-			</div>
-		</header>
-		<div class="info">
-			<div class="description">
-				<mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :key="user.id"/>
-			</div>
-			<div class="fields" v-if="user.fields" :key="user.id">
-				<dl class="field" v-for="(field, i) in user.fields" :key="i">
-					<dt class="name">
-						<mfm :text="field.name" :plain="true" :custom-emojis="user.emojis"/>
-					</dt>
-					<dd class="value">
-						<mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
-					</dd>
-				</dl>
-			</div>
-			<div class="counts">
-				<div>
-					<router-link :to="user | userPage()">
-						<b>{{ user.notesCount | number }}</b>
-						<span>{{ $t('posts') }}</span>
-					</router-link>
-				</div>
-				<div>
-					<router-link :to="user | userPage('following')">
-						<b>{{ user.followingCount | number }}</b>
-						<span>{{ $t('following') }}</span>
-					</router-link>
-				</div>
-				<div>
-					<router-link :to="user | userPage('followers')">
-						<b>{{ user.followersCount | number }}</b>
-						<span>{{ $t('followers') }}</span>
-					</router-link>
-				</div>
-			</div>
-		</div>
-		<router-view :user="user"></router-view>
-	</div>
-</x-column>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import parseAcct from '../../../../../misc/acct/parse';
-import XColumn from './deck.column.vue';
-import XUserMenu from '../../../common/views/components/user-menu.vue';
-
-export default Vue.extend({
-	i18n: i18n('deck/deck.user-column.vue'),
-	components: {
-		XColumn,
-	},
-
-	data() {
-		return {
-			user: null,
-			fetching: true,
-		};
-	},
-
-	computed: {
-		bannerStyle(): any {
-			if (this.user == null) return {};
-			if (this.user.bannerUrl == null) return {};
-			return {
-				backgroundColor: this.user.bannerColor,
-				backgroundImage: `url(${ this.user.bannerUrl })`
-			};
-		},
-	},
-
-	watch: {
-		$route: 'fetch'
-	},
-
-	created() {
-		this.fetch();
-	},
-
-	methods: {
-		fetch() {
-			this.fetching = true;
-			this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
-				this.user = user;
-				this.fetching = false;
-			});
-		},
-
-		menu() {
-			const w = this.$root.new(XUserMenu, {
-				source: this.$refs.menu,
-				user: this.user
-			});
-			this.$once('hook:beforeDestroy', () => {
-				w.destroyDom();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.zubukjlciycdsyynicqrnlsmdwmymzqu
-	background var(--deckColumnBg)
-
-	> .is-remote
-		padding 8px 16px
-		font-size 12px
-
-		&.is-remote
-			color var(--remoteInfoFg)
-			background var(--remoteInfoBg)
-
-		> a
-			font-weight bold
-
-	> header
-		overflow hidden
-		background-size cover
-		background-position center
-
-		> div
-			padding 32px
-			background rgba(#000, 0.5)
-			color #fff
-			text-align center
-
-			> .menu
-				position absolute
-				top 8px
-				left 8px
-				padding 8px
-				font-size 16px
-				text-shadow 0 0 8px #000
-
-			> .follow
-				position absolute
-				top 16px
-				right 16px
-
-			> .avatar
-				display block
-				width 64px
-				height 64px
-				margin 0 auto
-
-			> .name
-				display block
-				margin-top 8px
-				font-weight bold
-				text-shadow 0 0 8px #000
-				color #fff
-
-			> .acct
-				display block
-				font-size 14px
-				opacity 0.7
-				text-shadow 0 0 8px #000
-
-				> .locked
-					opacity 0.8
-
-			> .followed
-				display inline-block
-				font-size 12px
-				background rgba(0, 0, 0, 0.5)
-				opacity 0.7
-				margin-top: 2px
-				padding 4px
-				border-radius 4px
-
-	> .info
-		padding 16px
-		font-size 12px
-		color var(--text)
-		text-align center
-		background var(--face)
-
-		&:before
-			content ""
-			display blcok
-			position absolute
-			top -32px
-			left 0
-			right 0
-			width 0
-			margin 0 auto
-			border-top solid 16px transparent
-			border-left solid 16px transparent
-			border-right solid 16px transparent
-			border-bottom solid 16px var(--face)
-
-		> .fields
-			margin-top 8px
-
-			> .field
-				display flex
-				padding 0
-				margin 0
-				align-items center
-
-				> .name
-					padding 4px
-					margin 4px
-					width 30%
-					overflow hidden
-					white-space nowrap
-					text-overflow ellipsis
-					font-weight bold
-
-				> .value
-					padding 4px
-					margin 4px
-					width 70%
-					overflow hidden
-					white-space nowrap
-					text-overflow ellipsis
-
-		> .counts
-			display grid
-			grid-template-columns 2fr 2fr 2fr
-			margin-top 8px
-			border-top solid var(--lineWidth) var(--faceDivider)
-
-			> div
-				padding 8px 8px 0 8px
-				text-align center
-
-				> a
-					color var(--text)
-
-					> b
-						display block
-						font-size 110%
-
-					> span
-						display block
-						font-size 80%
-						opacity 0.7
-
-</style>
diff --git a/src/client/app/common/views/deck/deck.vue b/src/client/app/common/views/deck/deck.vue
deleted file mode 100644
index a3a26302e9486b8975b23d1370959890136af3b8..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.vue
+++ /dev/null
@@ -1,394 +0,0 @@
-<template>
-<mk-ui :class="$style.root">
-	<div class="qlvquzbjribqcaozciifydkngcwtyzje" ref="body" :style="style" :class="`${$store.state.device.deckColumnAlign} ${$store.state.device.deckColumnWidth}`" v-hotkey.global="keymap">
-		<template v-for="ids in layout">
-			<div v-if="ids.length > 1" class="folder">
-				<template v-for="id, i in ids">
-					<x-column-core :ref="id" :key="id" :column="columns.find(c => c.id == id)" :is-stacked="true" @parentFocus="moveFocus(id, $event)"/>
-				</template>
-			</div>
-			<x-column-core v-else :ref="ids[0]" :key="ids[0]" :column="columns.find(c => c.id == ids[0])" @parentFocus="moveFocus(ids[0], $event)"/>
-		</template>
-		<router-view></router-view>
-		<button ref="add" @click="add" :title="$t('@deck.add-column')"><fa icon="plus"/></button>
-	</div>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XColumnCore from './deck.column-core.vue';
-import Menu from '../../../common/views/components/menu.vue';
-
-import { v4 as uuid } from 'uuid';
-
-export default Vue.extend({
-	i18n: i18n('deck'),
-
-	components: {
-		XColumnCore
-	},
-
-	computed: {
-		deck() {
-			return this.$store.getters.deck;
-		},
-
-		columns(): any[] {
-			if (this.deck == null) return [];
-			return this.deck.columns;
-		},
-
-		layout(): any[] {
-			if (this.deck == null) return [];
-			if (this.deck.layout == null) return this.deck.columns.map(c => [c.id]);
-			return this.deck.layout;
-		},
-
-		style(): any {
-			return {
-				height: `calc(100vh - ${this.$store.state.uiHeaderHeight}px)`
-			};
-		},
-
-		keymap(): any {
-			return {
-				't': this.focus
-			};
-		}
-	},
-
-	watch: {
-		$route() {
-			if (this.$route.name == 'index') return;
-			this.$nextTick(() => {
-				this.$refs.body.scrollTo({
-					left: this.$refs.body.scrollWidth - this.$refs.body.clientWidth,
-					behavior: 'smooth'
-				});
-			});
-		}
-	},
-
-	provide() {
-		return {
-			inDeck: true,
-			getColumnVm: this.getColumnVm,
-			narrow: true
-		};
-	},
-
-	created() {
-		if (this.deck == null) {
-			const deck = {
-				columns: [/*{
-					type: 'widgets',
-					widgets: []
-				}, */{
-					id: uuid(),
-					type: 'home',
-					name: null,
-				}, {
-					id: uuid(),
-					type: 'notifications',
-					name: null,
-				}, {
-					id: uuid(),
-					type: 'local',
-					name: null,
-				}, {
-					id: uuid(),
-					type: 'global',
-					name: null,
-				}]
-			};
-
-			deck.layout = deck.columns.map(c => [c.id]);
-
-			this.$store.commit('setDeck', deck);
-		}
-	},
-
-	mounted() {
-		document.title = this.$root.instanceName;
-		document.documentElement.style.overflow = 'hidden';
-	},
-
-	beforeDestroy() {
-		document.documentElement.style.overflow = 'auto';
-	},
-
-	methods: {
-		getColumnVm(id) {
-			return this.$refs[id][0];
-		},
-
-		add() {
-			this.$root.new(Menu, {
-				source: this.$refs.add,
-				items: [{
-					icon: 'home',
-					text: this.$t('@deck.home'),
-					action: () => {
-						this.$store.commit('addDeckColumn', {
-							id: uuid(),
-							type: 'home'
-						});
-					}
-				}, {
-					icon: ['far', 'comments'],
-					text: this.$t('@deck.local'),
-					action: () => {
-						this.$store.commit('addDeckColumn', {
-							id: uuid(),
-							type: 'local'
-						});
-					}
-				}, {
-					icon: 'share-alt',
-					text: this.$t('@deck.hybrid'),
-					action: () => {
-						this.$store.commit('addDeckColumn', {
-							id: uuid(),
-							type: 'hybrid'
-						});
-					}
-				}, {
-					icon: 'globe',
-					text: this.$t('@deck.global'),
-					action: () => {
-						this.$store.commit('addDeckColumn', {
-							id: uuid(),
-							type: 'global'
-						});
-					}
-				}, {
-					icon: 'at',
-					text: this.$t('@deck.mentions'),
-					action: () => {
-						this.$store.commit('addDeckColumn', {
-							id: uuid(),
-							type: 'mentions'
-						});
-					}
-				}, {
-					icon: ['far', 'envelope'],
-					text: this.$t('@deck.direct'),
-					action: () => {
-						this.$store.commit('addDeckColumn', {
-							id: uuid(),
-							type: 'direct'
-						});
-					}
-				}, {
-					icon: 'list',
-					text: this.$t('@deck.list'),
-					action: async () => {
-						const lists = await this.$root.api('users/lists/list');
-						const { canceled, result: listId } = await this.$root.dialog({
-							type: null,
-							title: this.$t('@deck.select-list'),
-							select: {
-								items: lists.map(list => ({
-									value: list.id, text: list.name
-								}))
-							},
-							showCancelButton: true
-						});
-						if (canceled) return;
-						this.$store.commit('addDeckColumn', {
-							id: uuid(),
-							type: 'list',
-							list: lists.find(l => l.id === listId)
-						});
-					}
-				}, {
-					icon: 'hashtag',
-					text: this.$t('@deck.hashtag'),
-					action: () => {
-						this.$root.dialog({
-							title: this.$t('enter-hashtag-tl-title'),
-							input: true
-						}).then(({ canceled, result: title }) => {
-							if (canceled) return;
-							this.$store.commit('addDeckColumn', {
-								id: uuid(),
-								type: 'hashtag',
-								tagTlId: this.$store.state.settings.tagTimelines.find(x => x.title == title).id
-							});
-						});
-					}
-				}, {
-					icon: ['far', 'bell'],
-					text: this.$t('@deck.notifications'),
-					action: () => {
-						this.$store.commit('addDeckColumn', {
-							id: uuid(),
-							type: 'notifications'
-						});
-					}
-				}, {
-					icon: 'calculator',
-					text: this.$t('@deck.widgets'),
-					action: () => {
-						this.$store.commit('addDeckColumn', {
-							id: uuid(),
-							type: 'widgets',
-							widgets: []
-						});
-					}
-				}]
-			});
-		},
-
-		focus() {
-			// Flatten array of arrays
-			const ids = [].concat.apply([], this.layout);
-			const firstTl = ids.find(id => this.isTlColumn(id));
-
-			if (firstTl) {
-				this.$refs[firstTl][0].focus();
-			}
-		},
-
-		moveFocus(id, direction) {
-			let targetColumn;
-
-			if (direction == 'right') {
-				const currentColumnIndex = this.layout.findIndex(ids => ids.includes(id));
-				this.layout.some((ids, i) => {
-					if (i <= currentColumnIndex) return false;
-					const tl = ids.find(id => this.isTlColumn(id));
-					if (tl) {
-						targetColumn = tl;
-						return true;
-					}
-				});
-			} else if (direction == 'left') {
-				const currentColumnIndex = [...this.layout].reverse().findIndex(ids => ids.includes(id));
-				[...this.layout].reverse().some((ids, i) => {
-					if (i <= currentColumnIndex) return false;
-					const tl = ids.find(id => this.isTlColumn(id));
-					if (tl) {
-						targetColumn = tl;
-						return true;
-					}
-				});
-			} else if (direction == 'down') {
-				const currentColumn = this.layout.find(ids => ids.includes(id));
-				const currentIndex = currentColumn.indexOf(id);
-				currentColumn.some((_id, i) => {
-					if (i <= currentIndex) return false;
-					if (this.isTlColumn(_id)) {
-						targetColumn = _id;
-						return true;
-					}
-				});
-			} else if (direction == 'up') {
-				const currentColumn = [...this.layout.find(ids => ids.includes(id))].reverse();
-				const currentIndex = currentColumn.indexOf(id);
-				currentColumn.some((_id, i) => {
-					if (i <= currentIndex) return false;
-					if (this.isTlColumn(_id)) {
-						targetColumn = _id;
-						return true;
-					}
-				});
-			}
-
-			if (targetColumn) {
-				this.$refs[targetColumn][0].focus();
-			}
-		},
-
-		isTlColumn(id) {
-			const column = this.columns.find(c => c.id === id);
-			return ['home', 'local', 'hybrid', 'global', 'list', 'hashtag', 'mentions', 'direct'].includes(column.type);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" module>
-.root
-	height 100vh
-</style>
-
-<style lang="stylus" scoped>
-.qlvquzbjribqcaozciifydkngcwtyzje
-	display flex
-	flex 1
-	padding 16px 0 16px 16px
-	overflow auto
-	overflow-y hidden
-	-webkit-overflow-scrolling touch
-
-	@media (max-width 500px)
-		padding 8px 0 8px 8px
-
-	> div
-		margin-right 8px
-		width 330px
-		min-width 330px
-
-		&:last-of-type
-			margin-right 0
-
-		&.folder
-			display flex
-			flex-direction column
-
-			> *:not(:last-child)
-				margin-bottom 8px
-
-	&.narrow
-		> div
-			width 303px
-			min-width 303px
-
-	&.narrower
-		> div
-			width 316.5px
-			min-width 316.5px
-
-	&.wider
-		> div
-			width 343.5px
-			min-width 343.5px
-
-	&.wide
-		> div
-			width 357px
-			min-width 357px
-
-	&.center
-		> *
-			&:first-child
-				margin-left auto
-
-			&:last-child
-				margin-right auto
-
-	&.:not(.flexible)
-		> *
-			flex-grow 0
-			flex-shrink 0
-
-	&.flexible
-		> *
-			flex-grow 1
-			flex-shrink 0
-
-	> button
-		padding 0 16px
-		color var(--faceTextButton)
-		flex-grow 0 !important
-
-		&:hover
-			color var(--faceTextButtonHover)
-
-		&:active
-			color var(--faceTextButtonActive)
-
-</style>
diff --git a/src/client/app/common/views/deck/deck.widgets-column.vue b/src/client/app/common/views/deck/deck.widgets-column.vue
deleted file mode 100644
index d9a77479096c6174fbbaf8c964e12ee91e000b71..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/deck/deck.widgets-column.vue
+++ /dev/null
@@ -1,173 +0,0 @@
-<template>
-<x-column :menu="menu" :naked="true" :narrow="true" :name="name" :column="column" :is-stacked="isStacked" class="wtdtxvecapixsepjtcupubtsmometobz">
-	<template #header><fa icon="calculator"/>{{ name }}</template>
-
-	<div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq">
-		<template v-if="edit">
-			<header>
-				<select v-model="widgetAdderSelected" @change="addWidget">
-					<option value="profile">{{ $t('@.widgets.profile') }}</option>
-					<option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option>
-					<option value="calendar">{{ $t('@.widgets.calendar') }}</option>
-					<option value="timemachine">{{ $t('@.widgets.timemachine') }}</option>
-					<option value="activity">{{ $t('@.widgets.activity') }}</option>
-					<option value="rss">{{ $t('@.widgets.rss') }}</option>
-					<option value="trends">{{ $t('@.widgets.trends') }}</option>
-					<option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option>
-					<option value="slideshow">{{ $t('@.widgets.slideshow') }}</option>
-					<option value="version">{{ $t('@.widgets.version') }}</option>
-					<option value="broadcast">{{ $t('@.widgets.broadcast') }}</option>
-					<option value="notifications">{{ $t('@.widgets.notifications') }}</option>
-					<option value="users">{{ $t('@.widgets.users') }}</option>
-					<option value="polls">{{ $t('@.widgets.polls') }}</option>
-					<option value="post-form">{{ $t('@.widgets.post-form') }}</option>
-					<option value="messaging">{{ $t('@.messaging') }}</option>
-					<option value="memo">{{ $t('@.widgets.memo') }}</option>
-					<option value="hashtags">{{ $t('@.widgets.hashtags') }}</option>
-					<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option>
-					<option value="server">{{ $t('@.widgets.server') }}</option>
-					<option value="queue">{{ $t('@.widgets.queue') }}</option>
-					<option value="nav">{{ $t('@.widgets.nav') }}</option>
-					<option value="tips">{{ $t('@.widgets.tips') }}</option>
-				</select>
-			</header>
-			<x-draggable
-				:list="column.widgets"
-				animation="150"
-				@sort="onWidgetSort"
-			>
-				<div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="widgetFunc(widget.id)">
-					<button class="remove" @click="removeWidget(widget)"><fa icon="times"/></button>
-					<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck" :column="column"/>
-				</div>
-			</x-draggable>
-		</template>
-		<template v-else>
-			<component class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="deck" :column="column"/>
-		</template>
-	</div>
-</x-column>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XColumn from './deck.column.vue';
-import * as XDraggable from 'vuedraggable';
-import { v4 as uuid } from 'uuid';
-
-export default Vue.extend({
-	i18n: i18n(),
-	components: {
-		XColumn,
-		XDraggable
-	},
-
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			edit: false,
-			menu: null,
-			widgetAdderSelected: null
-		}
-	},
-
-	computed: {
-		name(): string {
-			if (this.column.name) return this.column.name;
-			return this.$t('@deck.widgets');
-		}
-	},
-
-	created() {
-		this.menu = [{
-			icon: 'cog',
-			text: this.$t('edit'),
-			action: () => {
-				this.edit = !this.edit;
-			}
-		}];
-	},
-
-	methods: {
-		widgetFunc(id) {
-			const w = this.$refs[id][0];
-			if (w.func) w.func();
-		},
-
-		onWidgetSort() {
-			this.saveWidgets();
-		},
-
-		addWidget() {
-			this.$store.commit('addDeckWidget', {
-				id: this.column.id,
-				widget: {
-					name: this.widgetAdderSelected,
-					id: uuid(),
-					data: {}
-				}
-			});
-
-			this.widgetAdderSelected = null;
-		},
-
-		removeWidget(widget) {
-			this.$store.commit('removeDeckWidget', {
-				id: this.column.id,
-				widget
-			});
-		},
-
-		saveWidgets() {
-			this.$store.commit('updateDeckColumn', this.column);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.wtdtxvecapixsepjtcupubtsmometobz
-	.gqpwvtwtprsbmnssnbicggtwqhmylhnq
-		> header
-			padding 16px
-
-			> *
-				width 100%
-				padding 4px
-
-		.widget, .customize-container
-			margin 8px
-
-			&:first-of-type
-				margin-top 0
-
-		.customize-container
-			cursor move
-
-			> *:not(.remove)
-				pointer-events none
-
-			> .remove
-				position absolute
-				z-index 1
-				top 8px
-				right 8px
-				width 32px
-				height 32px
-				color #fff
-				background rgba(#000, 0.7)
-				border-radius 4px
-
-</style>
-
diff --git a/src/client/app/common/views/directives/index.ts b/src/client/app/common/views/directives/index.ts
deleted file mode 100644
index 1bb4fd6d4dfb2799ab11854322d1cd0c4f728385..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/directives/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import Vue from 'vue';
-
-import autocomplete from './autocomplete';
-import particle from './particle';
-
-Vue.directive('autocomplete', autocomplete);
-Vue.directive('particle', particle);
diff --git a/src/client/app/common/views/directives/particle.ts b/src/client/app/common/views/directives/particle.ts
deleted file mode 100644
index 5f8413117f4b94d2d4a7f3ffc10b0b5964697eeb..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/directives/particle.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import Particle from '../components/particle.vue';
-
-export default {
-	bind(el, binding, vn) {
-		if (vn.context.$store.state.device.reduceMotion) return;
-
-		el.addEventListener('click', () => {
-			if (binding.value === false) return;
-
-			const rect = el.getBoundingClientRect();
-
-			const x = rect.left + (el.clientWidth / 2);
-			const y = rect.top + (el.clientHeight / 2);
-
-			const particle = new Particle({
-				parent: vn.context,
-				propsData: {
-					x,
-					y
-				}
-			}).$mount();
-
-			document.body.appendChild(particle.$el);
-		});
-	}
-};
diff --git a/src/client/app/common/views/filters/index.ts b/src/client/app/common/views/filters/index.ts
deleted file mode 100644
index 3dccbfc923be321b9c72ecb5e501f1840e1866fd..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/filters/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import Vue from 'vue';
-import * as JSON5 from 'json5';
-
-Vue.filter('json5', x => {
-	return JSON5.stringify(x, null, 2);
-});
-
-require('./bytes');
-require('./number');
-require('./user');
-require('./note');
diff --git a/src/client/app/common/views/pages/explore.vue b/src/client/app/common/views/pages/explore.vue
deleted file mode 100644
index b4a4e1d5026ea14ca295b453ae7171d255ba70ae..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/explore.vue
+++ /dev/null
@@ -1,198 +0,0 @@
-<template>
-<div>
-	<div class="localfedi7" v-if="meta && stats && tag == null" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }">
-		<header>{{ $t('explore', { host: meta.name || 'Misskey' }) }}</header>
-		<div>{{ $t('users-info', { users: num(stats.originalUsersCount) }) }}</div>
-	</div>
-
-	<template v-if="tag == null">
-		<mk-user-list :pagination="pinnedUsers" :expanded="false">
-			<fa :icon="faBookmark" fixed-width/>{{ $t('pinned-users') }}
-		</mk-user-list>
-		<mk-user-list :pagination="popularUsers" :expanded="false">
-			<fa :icon="faChartLine" fixed-width/>{{ $t('popular-users') }}
-		</mk-user-list>
-		<mk-user-list :pagination="recentlyUpdatedUsers" :expanded="false">
-			<fa :icon="faCommentAlt" fixed-width/>{{ $t('recently-updated-users') }}
-		</mk-user-list>
-		<mk-user-list :pagination="recentlyRegisteredUsers" :expanded="false">
-			<fa :icon="faPlus" fixed-width/>{{ $t('recently-registered-users') }}
-		</mk-user-list>
-	</template>
-
-	<div class="localfedi7" v-if="tag == null" :style="{ backgroundImage: `url(/assets/fedi.jpg)` }">
-		<header>{{ $t('explore-fediverse') }}</header>
-	</div>
-
-	<ui-container :body-togglable="true" :expanded="false" ref="tags">
-		<template #header><fa :icon="faHashtag" fixed-width/>{{ $t('popular-tags') }}</template>
-
-		<div class="vxjfqztj">
-			<router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</router-link>
-			<router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</router-link>
-		</div>
-	</ui-container>
-
-	<mk-user-list v-if="tag != null" :pagination="tagUsers" :key="`${tag}`">
-		<fa :icon="faHashtag" fixed-width/>{{ tag }}
-	</mk-user-list>
-	<template v-if="tag == null">
-		<mk-user-list :pagination="popularUsersF" :expanded="false">
-			<fa :icon="faChartLine" fixed-width/>{{ $t('popular-users') }}
-		</mk-user-list>
-		<mk-user-list :pagination="recentlyUpdatedUsersF" :expanded="false">
-			<fa :icon="faCommentAlt" fixed-width/>{{ $t('recently-updated-users') }}
-		</mk-user-list>
-		<mk-user-list :pagination="recentlyRegisteredUsersF" :expanded="false">
-			<fa :icon="faRocket" fixed-width/>{{ $t('recently-discovered-users') }}
-		</mk-user-list>
-	</template>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { faChartLine, faPlus, faHashtag, faRocket } from '@fortawesome/free-solid-svg-icons';
-import { faBookmark, faCommentAlt } from '@fortawesome/free-regular-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('common/views/pages/explore.vue'),
-
-	props: {
-		tag: {
-			type: String,
-			required: false
-		}
-	},
-
-	inject: {
-		inNakedDeckColumn: {
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			pinnedUsers: { endpoint: 'pinned-users' },
-			popularUsers: { endpoint: 'users', limit: 10, params: {
-				state: 'alive',
-				origin: 'local',
-				sort: '+follower',
-			} },
-			recentlyUpdatedUsers: { endpoint: 'users', limit: 10, params: {
-				origin: 'local',
-				sort: '+updatedAt',
-			} },
-			recentlyRegisteredUsers: { endpoint: 'users', limit: 10, params: {
-				origin: 'local',
-				state: 'alive',
-				sort: '+createdAt',
-			} },
-			popularUsersF: { endpoint: 'users', limit: 10, params: {
-				state: 'alive',
-				origin: 'remote',
-				sort: '+follower',
-			} },
-			recentlyUpdatedUsersF: { endpoint: 'users', limit: 10, params: {
-				origin: 'combined',
-				sort: '+updatedAt',
-			} },
-			recentlyRegisteredUsersF: { endpoint: 'users', limit: 10, params: {
-				origin: 'combined',
-				sort: '+createdAt',
-			} },
-			tagsLocal: [],
-			tagsRemote: [],
-			stats: null,
-			meta: null,
-			num: Vue.filter('number'),
-			faBookmark, faChartLine, faCommentAlt, faPlus, faHashtag, faRocket
-		};
-	},
-
-	computed: {
-		tagUsers(): any {
-			return {
-				endpoint: 'hashtags/users',
-				limit: 30,
-				params: {
-					tag: this.tag,
-					origin: 'combined',
-					sort: '+follower',
-				}
-			};
-		},
-	},
-
-	watch: {
-		tag() {
-			if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null);
-		}
-	},
-
-	created() {
-		this.$emit('init', {
-			title: this.$t('@.explore'),
-			icon: faHashtag
-		});
-		this.$root.api('hashtags/list', {
-			sort: '+attachedLocalUsers',
-			attachedToLocalUserOnly: true,
-			limit: 30
-		}).then(tags => {
-			this.tagsLocal = tags;
-		});
-		this.$root.api('hashtags/list', {
-			sort: '+attachedRemoteUsers',
-			attachedToRemoteUserOnly: true,
-			limit: 30
-		}).then(tags => {
-			this.tagsRemote = tags;
-		});
-		this.$root.api('stats').then(stats => {
-			this.stats = stats;
-		});
-		this.$root.getMeta().then(meta => {
-			this.meta = meta;
-		});
-	},
-
-	mounted() {
-		document.title = this.$root.instanceName;
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-.localfedi7
-	overflow hidden
-	background var(--face)
-	color #fff
-	text-shadow 0 0 8px #000
-	border-radius 6px
-	padding 16px
-	margin-top 16px
-	margin-bottom 16px
-	height 80px
-	background-position 50%
-	background-size cover
-	> header
-		font-size 20px
-		font-weight bold
-	> div
-		font-size 14px
-		opacity 0.8
-
-.localfedi7:first-child
-	margin-top 0
-
-.vxjfqztj
-	padding 16px
-
-	> *
-		margin-right 16px
-
-		&.local
-			font-weight bold
-</style>
diff --git a/src/client/app/common/views/pages/favorites.vue b/src/client/app/common/views/pages/favorites.vue
deleted file mode 100644
index e396615a93feee094927dac672f609a356228006..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/favorites.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<template>
-<div>
-	<component :is="notesComponent" :pagination="pagination" :extract="items => items.map(item => item.note)"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { faStar } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../i18n';
-//import Progress from '../../../common/scripts/loading';
-
-export default Vue.extend({
-	i18n: i18n(),
-
-	props: {
-		platform: {
-			type: String,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			pagination: {
-				endpoint: 'i/favorites',
-				limit: 10,
-			},
-
-			notesComponent:
-				this.platform === 'desktop' ? () => import('../../../desktop/views/components/detail-notes.vue').then(m => m.default) :
-				this.platform === 'mobile' ? () => import('../../../mobile/views/components/detail-notes.vue').then(m => m.default) :
-				this.platform === 'deck' ? () => import('../deck/deck.notes.vue').then(m => m.default) : null
-		};
-	},
-
-	created() {
-		this.$emit('init', {
-			title: this.$t('@.favorites'),
-			icon: faStar
-		});
-	},
-
-	mounted() {
-		document.title = this.$root.instanceName;
-	},
-});
-</script>
diff --git a/src/client/app/common/views/pages/featured.vue b/src/client/app/common/views/pages/featured.vue
deleted file mode 100644
index c00361aa854159df7a0e8681e9ab2980022318a9..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/featured.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<template>
-<div>
-	<component :is="notesComponent" :pagination="pagination"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { faNewspaper } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../i18n';
-//import Progress from '../../../common/scripts/loading';
-
-export default Vue.extend({
-	i18n: i18n(),
-
-	props: {
-		platform: {
-			type: String,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			pagination: {
-				endpoint: 'notes/featured',
-				limit: 29,
-			},
-
-			notesComponent:
-				this.platform === 'desktop' ? () => import('../../../desktop/views/components/detail-notes.vue').then(m => m.default) :
-				this.platform === 'mobile' ? () => import('../../../mobile/views/components/detail-notes.vue').then(m => m.default) :
-				this.platform === 'deck' ? () => import('../deck/deck.notes.vue').then(m => m.default) : null
-		};
-	},
-
-	created() {
-		this.$emit('init', {
-			title: this.$t('@.featured-notes'),
-			icon: faNewspaper
-		});
-	},
-
-	mounted() {
-		document.title = this.$root.instanceName;
-	},
-});
-</script>
diff --git a/src/client/app/common/views/pages/follow-requests.vue b/src/client/app/common/views/pages/follow-requests.vue
deleted file mode 100644
index 07ff7b7d549c6c3d403a0b3c14575c12164c4fa2..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/follow-requests.vue
+++ /dev/null
@@ -1,75 +0,0 @@
-<template>
-<div>
-	<ui-container :body-togglable="true">
-		<template #header>{{ $t('received-follow-requests') }}</template>
-		<div v-if="!fetching">
-			<sequential-entrance animation="entranceFromTop" delay="25" tag="div">
-				<div v-for="req in requests" class="mcbzkkaw">
-					<router-link :key="req.id" :to="req.follower | userPage">
-						<mk-user-name :user="req.follower"/>
-					</router-link>
-					<span>
-						<a @click="accept(req.follower)">{{ $t('accept') }}</a> | <a @click="reject(req.follower)">{{ $t('reject') }}</a>
-					</span>
-				</div>
-			</sequential-entrance>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import Progress from '../../scripts/loading';
-import { faUserClock } from '@fortawesome/free-solid-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('common/views/pages/follow-requests.vue'),
-	data() {
-		return {
-			fetching: true,
-			requests: []
-		};
-	},
-	created() {
-		this.$emit('init', {
-			title: this.$t('received-follow-requests'),
-			icon: faUserClock
-		});
-	},
-	mounted() {
-		Progress.start();
-		this.$root.api('following/requests/list').then(requests => {
-			this.fetching = false;
-			this.requests = requests;
-			Progress.done();
-		});
-	},
-	methods: {
-		accept(user) {
-			this.$root.api('following/requests/accept', { userId: user.id }).then(() => {
-				this.requests = this.requests.filter(r => r.follower.id != user.id);
-			});
-		},
-		reject(user) {
-			this.$root.api('following/requests/reject', { userId: user.id }).then(() => {
-				this.requests = this.requests.filter(r => r.follower.id != user.id);
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mcbzkkaw
-	display flex
-	padding 16px
-	border solid 1px var(--faceDivider)
-	border-radius 4px
-
-	> span
-		margin 0 0 0 auto
-		color var(--text)
-
-</style>
diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue
deleted file mode 100644
index c7b07a5be2312b840aa8133c4cd850faa906ff87..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/follow.vue
+++ /dev/null
@@ -1,242 +0,0 @@
-<template>
-<div class="syxhndwprovvuqhmyvveewmbqayniwkv" v-if="!fetching">
-	<div class="signed-in-as">
-		<mfm :text="$t('signed-in-as').replace('{}', myName)" :plain="true" :custom-emojis="$store.state.i.emojis"/>
-	</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">
-				<mk-user-name :user="user"/>
-			</router-link>
-			<span class="username">@{{ user | acct }}</span>
-			<div class="description">
-				<mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
-			</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 && user.isLocked"><fa icon="hourglass-half"/> {{ $t('request-pending') }}</template>
-			<template v-else-if="user.hasPendingFollowRequestFromYou && !user.isLocked"><fa icon="spinner"/> {{ $t('follow-processing') }}</template>
-			<template v-else-if="user.isFollowing"><fa icon="minus"/> {{ $t('following') }}</template>
-			<template v-else-if="!user.isFollowing && user.isLocked"><fa icon="plus"/> {{ $t('follow-request') }}</template>
-			<template v-else-if="!user.isFollowing && !user.isLocked"><fa icon="plus"/> {{ $t('follow') }}</template>
-		</template>
-		<template v-else><fa icon="spinner" pulse fixed-width/></template>
-	</button>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import parseAcct from '../../../../../misc/acct/parse';
-import Progress from '../../../common/scripts/loading';
-
-export default Vue.extend({
-	i18n: i18n('common/views/pages/follow.vue'),
-	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,
-				backgroundImage: `url(${ this.user.bannerUrl })`
-			};
-		}
-	},
-
-	created() {
-		this.fetch();
-	},
-
-	methods: {
-		fetch() {
-			const acct = new URL(location.href).searchParams.get('acct');
-			this.fetching = true;
-			Progress.start();
-			if (acct.match(/^https?:/)) {
-				this.$root.api('ap/show', {
-					uri: acct
-				}).then((res: { type: string, object: any })  => {
-					if (res.type === 'User') {
-						this.user = res.object;
-					} else if (res.type === 'Note') {
-						this.$router.replace(`/notes/${res.object.id}`);
-					} else {
-						this.$root.dialog({
-							type: 'error',
-							text: 'Not supported'
-						});
-					}
-				}).catch((e: any) => {
-					this.$root.dialog({
-						type: 'error',
-						text: e.message
-					});
-				}).finally(() => {
-					this.fetching = false;
-					Progress.done();
-				});
-			} else {
-				this.$root.api('users/show', parseAcct(acct)).then((user: any) => {
-					this.user = user;
-				}).catch((e: any) => {
-					this.$root.dialog({
-						type: 'error',
-						text: e.message
-					});
-				}).finally(() => {
-					this.fetching = false;
-					Progress.done();
-				});
-			}
-		},
-
-		async onClick() {
-			this.followWait = true;
-
-			try {
-				if (this.user.isFollowing) {
-					this.user = await this.$root.api('following/delete', {
-						userId: this.user.id
-					});
-				} else {
-					if (this.user.hasPendingFollowRequestFromYou) {
-						this.user = await this.$root.api('following/requests/cancel', {
-							userId: this.user.id
-						});
-					} else if (this.user.isLocked) {
-						this.user = await this.$root.api('following/create', {
-							userId: this.user.id
-						});
-					} else {
-						this.user = await this.$root.api('following/create', {
-							userId: this.user.id
-						});
-					}
-				}
-			} catch (e) {
-				console.error(e);
-			} finally {
-				this.followWait = false;
-			}
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.syxhndwprovvuqhmyvveewmbqayniwkv
-	padding 32px
-	max-width 500px
-	margin 0 auto
-	text-align center
-	color var(--text)
-
-	$bg = var(--face)
-
-	@media (max-width 400px)
-		padding 16px
-
-	> .signed-in-as
-		margin-bottom 16px
-		font-size 14px
-		font-weight bold
-
-	> 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 var(--primary)
-		background transparent
-		outline none
-		border solid 1px var(--primary)
-		border-radius 36px
-
-		&:hover
-			background var(--primaryAlpha01)
-
-		&:active
-			background var(--primaryAlpha02)
-
-		&.active
-			color var(--primaryForeground)
-			background var(--primary)
-
-			&:hover
-				background var(--primaryLighten10)
-				border-color var(--primaryLighten10)
-
-			&:active
-				background var(--primaryDarken10)
-				border-color var(--primaryDarken10)
-
-		&.wait
-			cursor wait !important
-			opacity 0.7
-
-		*
-			pointer-events none
-
-</style>
diff --git a/src/client/app/common/views/pages/followers.vue b/src/client/app/common/views/pages/followers.vue
deleted file mode 100644
index b546e69ae38be206dbfc64ef42fff175b3c10bc7..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/followers.vue
+++ /dev/null
@@ -1,25 +0,0 @@
-<template>
-<mk-user-list :pagination="pagination" :extract="items => items.map(item => item.follower)">{{ $t('@.followers') }}</mk-user-list>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import parseAcct from '../../../../../misc/acct/parse';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n(),
-
-	data() {
-		return {
-			pagination: {
-				endpoint: 'users/followers',
-				limit: 30,
-				params: {
-					...parseAcct(this.$route.params.user),
-				}
-			},
-		};
-	},
-});
-</script>
diff --git a/src/client/app/common/views/pages/following.vue b/src/client/app/common/views/pages/following.vue
deleted file mode 100644
index 4e584c19d9a0899898f778003b5b15ce9be2e317..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/following.vue
+++ /dev/null
@@ -1,25 +0,0 @@
-<template>
-<mk-user-list :pagination="pagination" :extract="items => items.map(item => item.followee)">{{ $t('@.following') }}</mk-user-list>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import parseAcct from '../../../../../misc/acct/parse';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n(),
-
-	data() {
-		return {
-			pagination: {
-				endpoint: 'users/following',
-				limit: 30,
-				params: {
-					...parseAcct(this.$route.params.user),
-				}
-			},
-		};
-	},
-});
-</script>
diff --git a/src/client/app/common/views/pages/not-found.vue b/src/client/app/common/views/pages/not-found.vue
deleted file mode 100644
index cb1b19687c4048f70252205dff8b54490017a0df..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/not-found.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-<figure class="megtcxgu">
-	<img :src="src" alt="">
-	<figcaption>
-		<h1><span>Not found</span></h1>
-		<p><span>{{ $t('page-not-found') }}</span></p>
-	</figcaption>
-</figure>
-</template>
-
-<script lang="ts">
-import Vue from 'vue'
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('common/views/pages/not-found.vue'),
-	data() {
-		return {
-			src: ''
-		}
-	},
-	created() {
-		this.$root.getMeta().then(meta => {
-			if (meta.errorImageUrl)
-				this.src = meta.errorImageUrl;
-		});
-	}
-})
-</script>
-
-<style lang="stylus" scoped>
-.megtcxgu
-	align-items center
-	bottom 0
-	display flex
-	justify-content center
-	left 0
-	margin auto
-	position fixed
-	right 0
-	top 0
-
-	> img
-		width 500px
-
-	> figcaption
-		margin 8px
-
-		h1,
-		p
-			color var(--text)
-			display flex
-			flex-flow column
-
-			*
-				position relative
-				width 100%
-
-	@media (max-width: 767px)
-		flex-flow column
-
-		> figcaption
-			text-align center
-
-</style>
diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue
deleted file mode 100644
index 6a82b0eec9072d83a3840e74701dffb3015cc545..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-<template>
-<x-container @remove="() => $emit('remove')" :draggable="true">
-	<template #header><fa :icon="faBolt"/> {{ $t('blocks.button') }}</template>
-
-	<section class="xfhsjczc">
-		<ui-input v-model="value.text"><span>{{ $t('blocks._button.text') }}</span></ui-input>
-		<ui-switch v-model="value.primary"><span>{{ $t('blocks._button.colored') }}</span></ui-switch>
-		<ui-select v-model="value.action">
-			<template #label>{{ $t('blocks._button.action') }}</template>
-			<option value="dialog">{{ $t('blocks._button._action.dialog') }}</option>
-			<option value="resetRandom">{{ $t('blocks._button._action.resetRandom') }}</option>
-			<option value="pushEvent">{{ $t('blocks._button._action.pushEvent') }}</option>
-		</ui-select>
-		<template v-if="value.action === 'dialog'">
-			<ui-input v-model="value.content"><span>{{ $t('blocks._button._action._dialog.content') }}</span></ui-input>
-		</template>
-		<template v-else-if="value.action === 'pushEvent'">
-			<ui-input v-model="value.event"><span>{{ $t('blocks._button._action._pushEvent.event') }}</span></ui-input>
-			<ui-input v-model="value.message"><span>{{ $t('blocks._button._action._pushEvent.message') }}</span></ui-input>
-			<ui-select v-model="value.var">
-				<template #label>{{ $t('blocks._button._action._pushEvent.variable') }}</template>
-				<option :value="null">{{ $t('blocks._button._action._pushEvent.no-variable') }}</option>
-				<option v-for="v in aiScript.getVarsByType()" :value="v.name">{{ v.name }}</option>
-				<optgroup :label="$t('script.pageVariables')">
-					<option v-for="v in aiScript.getPageVarsByType()" :value="v">{{ v }}</option>
-				</optgroup>
-				<optgroup :label="$t('script.enviromentVariables')">
-					<option v-for="v in aiScript.getEnvVarsByType()" :value="v">{{ v }}</option>
-				</optgroup>
-			</ui-select>
-		</template>
-	</section>
-</x-container>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { faBolt } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../../../i18n';
-import XContainer from '../page-editor.container.vue';
-
-export default Vue.extend({
-	i18n: i18n('pages'),
-
-	components: {
-		XContainer
-	},
-
-	props: {
-		value: {
-			required: true
-		},
-		aiScript: {
-			required: true,
-		},
-	},
-
-	data() {
-		return {
-			faBolt
-		};
-	},
-
-	created() {
-		if (this.value.text == null) Vue.set(this.value, 'text', '');
-		if (this.value.action == null) Vue.set(this.value, 'action', 'dialog');
-		if (this.value.content == null) Vue.set(this.value, 'content', null);
-		if (this.value.event == null) Vue.set(this.value, 'event', null);
-		if (this.value.message == null) Vue.set(this.value, 'message', null);
-		if (this.value.primary == null) Vue.set(this.value, 'primary', false);
-		if (this.value.var == null) Vue.set(this.value, 'var', null);
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-.xfhsjczc
-	padding 0 16px 0 16px
-
-</style>
diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.number-input.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.number-input.vue
deleted file mode 100644
index 30c3938111dcfd31d90001186c2e7a4dbc747926..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.number-input.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<template>
-<x-container @remove="() => $emit('remove')" :draggable="true">
-	<template #header><fa :icon="faBolt"/> {{ $t('blocks.numberInput') }}</template>
-
-	<section style="padding: 0 16px 0 16px;">
-		<ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._numberInput.name') }}</span></ui-input>
-		<ui-input v-model="value.text"><span>{{ $t('blocks._numberInput.text') }}</span></ui-input>
-		<ui-input v-model="value.default" type="number"><span>{{ $t('blocks._numberInput.default') }}</span></ui-input>
-	</section>
-</x-container>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../../../i18n';
-import XContainer from '../page-editor.container.vue';
-
-export default Vue.extend({
-	i18n: i18n('pages'),
-
-	components: {
-		XContainer
-	},
-
-	props: {
-		value: {
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			faBolt, faMagic
-		};
-	},
-
-	created() {
-		if (this.value.name == null) Vue.set(this.value, 'name', '');
-	},
-});
-</script>
diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.switch.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.switch.vue
deleted file mode 100644
index 174a344640c71aa05fd62923edab24ed0d8d8487..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.switch.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<template>
-<x-container @remove="() => $emit('remove')" :draggable="true">
-	<template #header><fa :icon="faBolt"/> {{ $t('blocks.switch') }}</template>
-
-	<section class="kjuadyyj">
-		<ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._switch.name') }}</span></ui-input>
-		<ui-input v-model="value.text"><span>{{ $t('blocks._switch.text') }}</span></ui-input>
-		<ui-switch v-model="value.default"><span>{{ $t('blocks._switch.default') }}</span></ui-switch>
-	</section>
-</x-container>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../../../i18n';
-import XContainer from '../page-editor.container.vue';
-
-export default Vue.extend({
-	i18n: i18n('pages'),
-
-	components: {
-		XContainer
-	},
-
-	props: {
-		value: {
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			faBolt, faMagic
-		};
-	},
-
-	created() {
-		if (this.value.name == null) Vue.set(this.value, 'name', '');
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-.kjuadyyj
-	padding 0 16px 16px 16px
-
-</style>
diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.text-input.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.text-input.vue
deleted file mode 100644
index 50f95fd205d1c07c06293568652e23d581a5dbe8..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.text-input.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<template>
-<x-container @remove="() => $emit('remove')" :draggable="true">
-	<template #header><fa :icon="faBolt"/> {{ $t('blocks.textInput') }}</template>
-
-	<section style="padding: 0 16px 0 16px;">
-		<ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._textInput.name') }}</span></ui-input>
-		<ui-input v-model="value.text"><span>{{ $t('blocks._textInput.text') }}</span></ui-input>
-		<ui-input v-model="value.default" type="text"><span>{{ $t('blocks._textInput.default') }}</span></ui-input>
-	</section>
-</x-container>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../../../i18n';
-import XContainer from '../page-editor.container.vue';
-
-export default Vue.extend({
-	i18n: i18n('pages'),
-
-	components: {
-		XContainer
-	},
-
-	props: {
-		value: {
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			faBolt, faMagic
-		};
-	},
-
-	created() {
-		if (this.value.name == null) Vue.set(this.value, 'name', '');
-	},
-});
-</script>
diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea-input.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea-input.vue
deleted file mode 100644
index da3eead0803540e925129e9ce05cd0d3c09f71fa..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea-input.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<template>
-<x-container @remove="() => $emit('remove')" :draggable="true">
-	<template #header><fa :icon="faBolt"/> {{ $t('blocks.textareaInput') }}</template>
-
-	<section style="padding: 0 16px 16px 16px;">
-		<ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._textareaInput.name') }}</span></ui-input>
-		<ui-input v-model="value.text"><span>{{ $t('blocks._textareaInput.text') }}</span></ui-input>
-		<ui-textarea v-model="value.default"><span>{{ $t('blocks._textareaInput.default') }}</span></ui-textarea>
-	</section>
-</x-container>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../../../i18n';
-import XContainer from '../page-editor.container.vue';
-
-export default Vue.extend({
-	i18n: i18n('pages'),
-
-	components: {
-		XContainer
-	},
-
-	props: {
-		value: {
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			faBolt, faMagic
-		};
-	},
-
-	created() {
-		if (this.value.name == null) Vue.set(this.value, 'name', '');
-	},
-});
-</script>
diff --git a/src/client/app/common/views/pages/page-editor/page-editor.container.vue b/src/client/app/common/views/pages/page-editor/page-editor.container.vue
deleted file mode 100644
index a3a501afb4089e0592fe2b32d8630da99c324320..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/page-editor/page-editor.container.vue
+++ /dev/null
@@ -1,146 +0,0 @@
-<template>
-<div class="cpjygsrt" :class="{ error: error != null, warn: warn != null }">
-	<header>
-		<div class="title"><slot name="header"></slot></div>
-		<div class="buttons">
-			<slot name="func"></slot>
-			<button v-if="removable" @click="remove()">
-				<fa :icon="faTrashAlt"/>
-			</button>
-			<button v-if="draggable" class="drag-handle">
-				<fa :icon="faBars"/>
-			</button>
-			<button @click="toggleContent(!showBody)">
-				<template v-if="showBody"><fa icon="angle-up"/></template>
-				<template v-else><fa icon="angle-down"/></template>
-			</button>
-		</div>
-	</header>
-	<p v-show="showBody" class="error" v-if="error != null">{{ $t('script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p>
-	<p v-show="showBody" class="warn" v-if="warn != null">{{ $t('script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p>
-	<div v-show="showBody">
-		<slot></slot>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { faBars } from '@fortawesome/free-solid-svg-icons';
-import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('pages'),
-
-	props: {
-		expanded: {
-			type: Boolean,
-			default: true
-		},
-		removable: {
-			type: Boolean,
-			default: true
-		},
-		draggable: {
-			type: Boolean,
-			default: false
-		},
-		error: {
-			required: false,
-			default: null
-		},
-		warn: {
-			required: false,
-			default: null
-		}
-	},
-	data() {
-		return {
-			showBody: this.expanded,
-			faTrashAlt, faBars
-		};
-	},
-	methods: {
-		toggleContent(show: boolean) {
-			this.showBody = show;
-			this.$emit('toggle', show);
-		},
-		remove() {
-			this.$emit('remove');
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.cpjygsrt
-	overflow hidden
-	background var(--face)
-	border solid 2px var(--pageBlockBorder)
-	border-radius 6px
-
-	&:hover
-		border solid 2px var(--pageBlockBorderHover)
-
-	&.warn
-		border solid 2px #dec44c
-
-	&.error
-		border solid 2px #f00
-
-	& + .cpjygsrt
-		margin-top 16px
-
-	> header
-		> .title
-			z-index 1
-			margin 0
-			padding 0 16px
-			line-height 42px
-			font-size 0.9em
-			font-weight bold
-			color var(--faceHeaderText)
-			box-shadow 0 1px rgba(#000, 0.07)
-
-			> [data-icon]
-				margin-right 6px
-
-			&:empty
-				display none
-
-		> .buttons
-			position absolute
-			z-index 2
-			top 0
-			right 0
-
-			> button
-				padding 0
-				width 42px
-				font-size 0.9em
-				line-height 42px
-				color var(--faceTextButton)
-
-				&:hover
-					color var(--faceTextButtonHover)
-
-				&:active
-					color var(--faceTextButtonActive)
-
-			.drag-handle
-				cursor move
-
-	> .warn
-		color #b19e49
-		margin 0
-		padding 16px 16px 0 16px
-		font-size 14px
-
-	> .error
-		color #f00
-		margin 0
-		padding 16px 16px 0 16px
-		font-size 14px
-
-</style>
diff --git a/src/client/app/common/views/pages/pages.vue b/src/client/app/common/views/pages/pages.vue
deleted file mode 100644
index 236330db464f307302412620461ed7409228baee..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/pages.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-<template>
-<div>
-	<ui-container :body-togglable="true">
-		<template #header><fa :icon="faEdit" fixed-width/>{{ $t('my-pages') }}</template>
-		<div class="rknalgpo my">
-			<ui-button class="new" @click="create()"><fa :icon="faPlus"/></ui-button>
-			<ui-pagination :pagination="myPagesPagination" #default="{items}">
-				<x-page-preview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
-			</ui-pagination>
-		</div>
-	</ui-container>
-
-	<ui-container :body-togglable="true">
-		<template #header><fa :icon="faHeart" fixed-width/>{{ $t('liked-pages') }}</template>
-		<div class="rknalgpo">
-			<ui-pagination :pagination="likedPagesPagination" #default="{items}">
-				<x-page-preview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/>
-			</ui-pagination>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { faPlus, faEdit } from '@fortawesome/free-solid-svg-icons';
-import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../../../i18n';
-import XPagePreview from '../../views/components/page-preview.vue';
-
-export default Vue.extend({
-	i18n: i18n('pages'),
-	components: {
-		XPagePreview
-	},
-	data() {
-		return {
-			myPagesPagination: {
-				endpoint: 'i/pages',
-				limit: 5,
-			},
-			likedPagesPagination: {
-				endpoint: 'i/page-likes',
-				limit: 5,
-			},
-			faStickyNote, faPlus, faEdit, faHeart
-		};
-	},
-	created() {
-		this.$emit('init', {
-			title: this.$t('@.pages'),
-			icon: faStickyNote
-		});
-	},
-	mounted() {
-		document.title = this.$root.instanceName;
-	},
-	methods: {
-		create() {
-			this.$router.push(`/i/pages/new`);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.rknalgpo
-	padding 16px
-
-	&.my .ckltabjg:first-child
-		margin-top 16px
-
-	.ckltabjg:not(:last-child)
-		margin-bottom 8px
-
-	@media (min-width 500px)
-		.ckltabjg:not(:last-child)
-			margin-bottom 16px
-
-</style>
diff --git a/src/client/app/common/views/pages/room/preview.vue b/src/client/app/common/views/pages/room/preview.vue
deleted file mode 100644
index 94c13cee9f1c034c78ebe237c918530b6bdb28db..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/room/preview.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-<template>
-<canvas width=224 height=128></canvas>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import * as THREE from 'three';
-
-export default Vue.extend({
-	data() {
-		return {
-			selected: null,
-			objectHeight: 0,
-			orbitRadius: 5
-		};
-	},
-
-	mounted() {
-		const canvas = this.$el;
-
-		const width = canvas.width;
-		const height = canvas.height;
-
-		const scene = new THREE.Scene();
-
-		const renderer = new THREE.WebGLRenderer({
-			canvas: canvas,
-			antialias: true,
-			alpha: false
-		});
-		renderer.setPixelRatio(window.devicePixelRatio);
-		renderer.setSize(width, height);
-		renderer.setClearColor(0x000000);
-		renderer.autoClear = false;
-		renderer.shadowMap.enabled = true;
-		renderer.shadowMap.cullFace = THREE.CullFaceBack;
-
-		const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 100);
-		camera.zoom = 10;
-		camera.position.x = 0;
-		camera.position.y = 2;
-		camera.position.z = 0;
-		camera.updateProjectionMatrix();
-		scene.add(camera);
-
-		const ambientLight = new THREE.AmbientLight(0xffffff, 1);
-		ambientLight.castShadow = false;
-		scene.add(ambientLight);
-
-		const light = new THREE.PointLight(0xffffff, 1, 100);
-		light.position.set(3, 3, 3);
-		scene.add(light);
-
-		const grid = new THREE.GridHelper(5, 16, 0x444444, 0x222222);
-		scene.add(grid);
-
-		const render = () => {
-			const timer = Date.now() * 0.0004;
-			requestAnimationFrame(render);
-			
-			camera.position.y = Math.sin(Math.PI / 6) * this.orbitRadius;	// Math.PI / 6 => 30deg
-			camera.position.z = Math.cos(timer) * this.orbitRadius;
-			camera.position.x = Math.sin(timer) * this.orbitRadius;
-			camera.lookAt(new THREE.Vector3(0, this.objectHeight / 2, 0));
-			renderer.render(scene, camera);
-		};
-
-		this.selected = selected => {
-			const obj = selected.clone();
-
-			// Remove current object
-			const current = scene.getObjectByName('obj');
-			if (current != null) {
-				scene.remove(current);
-			}
-
-			// Add new object
-			obj.name = 'obj';
-			obj.position.x = 0;
-			obj.position.y = 0;
-			obj.position.z = 0;
-			obj.rotation.x = 0;
-			obj.rotation.y = 0;
-			obj.rotation.z = 0;
-			obj.traverse(child => {
-				if (child instanceof THREE.Mesh) {
-					child.material = child.material.clone();
-					return child.material.emissive.setHex(0x000000);
-				}
-			});
-			const objectBoundingBox = new THREE.Box3().setFromObject(obj);
-			this.objectHeight = objectBoundingBox.max.y - objectBoundingBox.min.y;
-
-			const objectWidth = objectBoundingBox.max.x - objectBoundingBox.min.x;
-			const objectDepth = objectBoundingBox.max.z - objectBoundingBox.min.z;
-
-			const horizontal = Math.hypot(objectWidth, objectDepth) / camera.aspect;
-			this.orbitRadius = Math.max(horizontal, this.objectHeight) * camera.zoom * 0.625 / Math.tan(camera.fov * 0.5 * (Math.PI / 180));
-		
-			scene.add(obj);
-		};
-
-		render();
-	},
-});
-</script>
diff --git a/src/client/app/common/views/pages/room/room.vue b/src/client/app/common/views/pages/room/room.vue
deleted file mode 100644
index 1e81920c22110e707f4ac000f6958c77dd6a0ccf..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/room/room.vue
+++ /dev/null
@@ -1,310 +0,0 @@
-<template>
-<div class="hveuntkp">
-	<div class="controller" v-if="objectSelected">
-		<section>
-			<p class="name">{{ selectedFurnitureName }}</p>
-			<x-preview ref="preview"/>
-			<template v-if="selectedFurnitureInfo.props">
-				<div v-for="k in Object.keys(selectedFurnitureInfo.props)" :key="k">
-					<p>{{ k }}</p>
-					<template v-if="selectedFurnitureInfo.props[k] === 'image'">
-						<ui-button @click="chooseImage(k)">{{ $t('chooseImage') }}</ui-button>
-					</template>
-					<template v-else-if="selectedFurnitureInfo.props[k] === 'color'">
-						<input type="color" :value="selectedFurnitureProps ? selectedFurnitureProps[k] : null" @change="updateColor(k, $event)"/>
-					</template>
-				</div>
-			</template>
-		</section>
-		<section>
-			<ui-button @click="translate()" :primary="isTranslateMode"><fa :icon="faArrowsAlt"/> {{ $t('translate') }}</ui-button>
-			<ui-button @click="rotate()" :primary="isRotateMode"><fa :icon="faUndo"/> {{ $t('rotate') }}</ui-button>
-			<ui-button v-if="isTranslateMode || isRotateMode" @click="exit()"><fa :icon="faBan"/> {{ $t('exit') }}</ui-button>
-		</section>
-		<section>
-			<ui-button @click="remove()"><fa :icon="faTrashAlt"/> {{ $t('remove') }}</ui-button>
-		</section>
-	</div>
-
-	<div class="menu" v-if="isMyRoom">
-		<section>
-			<ui-button @click="add()"><fa :icon="faBoxOpen"/> {{ $t('add-furniture') }}</ui-button>
-		</section>
-		<section>
-			<ui-select :value="roomType" @input="updateRoomType($event)">
-				<template #label>{{ $t('room-type') }}</template>
-				<option value="default">{{ $t('rooms.default') }}</option>
-				<option value="washitsu">{{ $t('rooms.washitsu') }}</option>
-			</ui-select>
-			<label v-if="roomType === 'default'">
-				<span>{{ $t('carpet-color') }}</span>
-				<input type="color" :value="carpetColor" @change="updateCarpetColor($event)"/>
-			</label>
-		</section>
-		<section>
-			<ui-button :primary="changed" @click="save()"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
-			<ui-button @click="clear()"><fa :icon="faBroom"/> {{ $t('clear') }}</ui-button>
-		</section>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import { Room } from '../../../scripts/room/room';
-import parseAcct from '../../../../../../misc/acct/parse';
-import XPreview from './preview.vue';
-const storeItems = require('../../../scripts/room/furnitures.json5');
-import { faBoxOpen, faUndo, faArrowsAlt, faBan, faBroom } from '@fortawesome/free-solid-svg-icons';
-import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
-import { query as urlQuery } from '../../../../../../prelude/url';
-
-let room: Room;
-
-export default Vue.extend({
-	i18n: i18n('room'),
-
-	components: {
-		XPreview
-	},
-
-	props: {
-		acct: {
-			type: String,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			objectSelected: false,
-			selectedFurnitureName: null,
-			selectedFurnitureInfo: null,
-			selectedFurnitureProps: null,
-			roomType: null,
-			carpetColor: null,
-			isTranslateMode: false,
-			isRotateMode: false,
-			isMyRoom: false,
-			changed: false,
-			faBoxOpen, faSave, faTrashAlt, faUndo, faArrowsAlt, faBan, faBroom,
-		};
-	},
-
-	async mounted() {
-		window.addEventListener('beforeunload', this.beforeunload);
-
-		const user = await this.$root.api('users/show', {
-			...parseAcct(this.acct)
-		});
-
-		this.isMyRoom = this.$store.getters.isSignedIn && this.$store.state.i.id === user.id;
-
-		const roomInfo = await this.$root.api('room/show', {
-			userId: user.id
-		});
-
-		this.roomType = roomInfo.roomType;
-		this.carpetColor = roomInfo.carpetColor;
-
-		room = new Room(user, this.isMyRoom, roomInfo, this.$el, {
-			graphicsQuality: this.$store.state.device.roomGraphicsQuality,
-			onChangeSelect: obj => {
-				this.objectSelected = obj != null;
-				if (obj) {
-					const f = room.findFurnitureById(obj.name);
-					this.selectedFurnitureName = this.$t('furnitures.' + f.type);
-					this.selectedFurnitureInfo = storeItems.find(x => x.id === f.type);
-					this.selectedFurnitureProps = f.props
-						? JSON.parse(JSON.stringify(f.props)) // Disable reactivity
-						: null;
-					this.$nextTick(() => {
-						this.$refs.preview.selected(obj);
-					});
-				}
-			},
-			useOrthographicCamera: this.$store.state.device.roomUseOrthographicCamera
-		});
-	},
-
-	beforeRouteLeave(to, from, next) {
-		if (this.changed) {
-			this.$root.dialog({
-				type: 'warning',
-				text: this.$t('leave-confirm'),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) {
-					next(false);
-				} else {
-					next();
-				}
-			});
-		} else {
-			next();
-		}
-	},
-
-	beforeDestroy() {
-		room.destroy();
-		window.removeEventListener('beforeunload', this.beforeunload);
-	},
-
-	methods: {
-		beforeunload(e: BeforeUnloadEvent) {
-			if (this.changed) {
-				e.preventDefault();
-				e.returnValue = '';
-			}
-		},
-
-		async add() {
-			const { canceled, result: id } = await this.$root.dialog({
-				type: null,
-				title: this.$t('add-furniture'),
-				select: {
-					items: storeItems.map(item => ({
-						value: item.id, text: this.$t('furnitures.' + item.id)
-					}))
-				},
-				showCancelButton: true
-			});
-			if (canceled) return;
-			room.addFurniture(id);
-			this.changed = true;
-		},
-
-		remove() {
-			this.isTranslateMode = false;
-			this.isRotateMode = false;
-			room.removeFurniture();
-			this.changed = true;
-		},
-
-		save() {
-			this.$root.api('room/update', {
-				room: room.getRoomInfo()
-			}).then(() => {
-				this.changed = false;
-				this.$root.dialog({
-					type: 'success',
-					text: this.$t('saved')
-				});
-			}).catch((e: any) => {
-				this.$root.dialog({
-					type: 'error',
-					text: e.message
-				});
-			});
-		},
-
-		clear() {
-			this.$root.dialog({
-				type: 'warning',
-				text: this.$t('clear-confirm'),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-				room.removeAllFurnitures();
-				this.changed = true;
-			});
-		},
-
-		chooseImage(key) {
-			this.$chooseDriveFile({
-				multiple: false
-			}).then(file => {
-				room.updateProp(key, `/proxy/?${urlQuery({ url: file.thumbnailUrl })}`);
-				this.$refs.preview.selected(room.getSelectedObject());
-				this.changed = true;
-			});
-		},
-
-		updateColor(key, ev) {
-			room.updateProp(key, ev.target.value);
-			this.$refs.preview.selected(room.getSelectedObject());
-			this.changed = true;
-		},
-
-		updateCarpetColor(ev) {
-			room.updateCarpetColor(ev.target.value);
-			this.carpetColor = ev.target.value;
-			this.changed = true;
-		},
-
-		updateRoomType(type) {
-			room.changeRoomType(type);
-			this.roomType = type;
-			this.changed = true;
-		},
-
-		translate() {
-			if (this.isTranslateMode) {
-				this.exit();
-			} else {
-				this.isRotateMode = false;
-				this.isTranslateMode = true;
-				room.enterTransformMode('translate');
-			}
-			this.changed = true;
-		},
-
-		rotate() {
-			if (this.isRotateMode) {
-				this.exit();
-			} else {
-				this.isTranslateMode = false;
-				this.isRotateMode = true;
-				room.enterTransformMode('rotate');
-			}
-			this.changed = true;
-		},
-
-		exit() {
-			this.isTranslateMode = false;
-			this.isRotateMode = false;
-			room.exitTransformMode();
-			this.changed = true;
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.hveuntkp
-	> .controller
-	> .menu
-		position fixed
-		z-index 1
-		padding 16px
-		background var(--face)
-		color var(--text)
-
-		> section
-			padding 16px 0
-
-			&:first-child
-				padding-top 0
-
-			&:last-child
-				padding-bottom 0
-
-			&:not(:last-child)
-				border-bottom solid 1px var(--faceDivider)
-
-	> .controller
-		top 16px
-		left 16px
-		width 256px
-
-		> section
-			> .name
-				margin 0
-
-	> .menu
-		top 16px
-		right 16px
-		width 256px
-	
-</style>
diff --git a/src/client/app/common/views/pages/share.vue b/src/client/app/common/views/pages/share.vue
deleted file mode 100644
index 293a9bcfb581bc51aec8125453fbba3699c1abec..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/share.vue
+++ /dev/null
@@ -1,79 +0,0 @@
-<template>
-<div class="azibmfpleajagva420swmu4c3r7ni7iw">
-	<h1>{{ $t('share-with', { name }) }}</h1>
-	<div>
-		<mk-signin v-if="!$store.getters.isSignedIn"/>
-		<x-post-form v-else-if="!posted" :initial-text="template" :instant="true" @posted="posted = true"/>
-		<p v-if="posted" class="posted"><fa icon="check"/></p>
-	</div>
-	<ui-button class="close" v-if="posted" @click="close">{{ $t('@.close') }}</ui-button>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/pages/share.vue'),
-	components: {
-		XPostForm: () => import('../../../desktop/views/components/post-form.vue').then(m => m.default)
-	},
-	data() {
-		return {
-			name: null,
-			posted: false,
-			text: new URLSearchParams(location.search).get('text'),
-			url: new URLSearchParams(location.search).get('url'),
-			title: new URLSearchParams(location.search).get('title'),
-		};
-	},
-	computed: {
-		template(): string {
-			let t = '';
-			if (this.title && this.url) t += `【[${this.title}](${this.url})】\n`;
-			if (this.title && !this.url) t += `【${this.title}】\n`;
-			if (this.text) t += `${this.text}\n`;
-			if (!this.title && this.url) t += `${this.url}`;
-			return t.trim();
-		}
-	},
-	mounted() {
-		this.$root.getMeta().then(meta => {
-			this.name = meta.name || 'Misskey';
-		});
-	},
-	methods: {
-		close() {
-			window.close();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.azibmfpleajagva420swmu4c3r7ni7iw
-	> h1
-		margin 8px 0
-		color #555
-		font-size 20px
-		text-align center
-
-	> div
-		max-width 500px
-		margin 0 auto
-
-		> .posted
-			display block
-			margin 0 auto
-			padding 64px
-			text-align center
-			background #fff
-			border-radius 6px
-			width calc(100% - 32px)
-
-	> .close
-		display block
-		margin 16px auto
-		width calc(100% - 32px)
-</style>
diff --git a/src/client/app/common/views/pages/user-group-editor.vue b/src/client/app/common/views/pages/user-group-editor.vue
deleted file mode 100644
index 9cc012af7ae2252c61a97d67ad6eed3a1f85cb41..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/user-group-editor.vue
+++ /dev/null
@@ -1,256 +0,0 @@
-<template>
-<div class="ivrbakop">
-	<ui-container v-if="group">
-		<template #header><fa :icon="faUsers"/> {{ group.name }}</template>
-
-		<section>
-			<ui-margin>
-				<ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button>
-				<ui-button @click="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button>
-				<ui-button @click="transfer"><fa :icon="faCrown"/> {{ $t('transfer') }}</ui-button>
-			</ui-margin>
-		</section>
-	</ui-container>
-
-	<ui-container>
-		<template #header><fa :icon="faUsers"/> {{ $t('users') }}</template>
-
-		<section>
-			<ui-margin>
-				<ui-button @click="invite()"><fa :icon="faPlus"/> {{ $t('invite') }}</ui-button>
-			</ui-margin>
-			<sequential-entrance animation="entranceFromTop" delay="25">
-				<div class="kjlrfbes" v-for="user in users">
-					<div>
-						<a :href="user | userPage">
-							<mk-avatar class="avatar" :user="user" :disable-link="true"/>
-						</a>
-					</div>
-					<div>
-						<header>
-							<b><mk-user-name :user="user"/></b>
-							<span class="is-owner" v-if="group.ownerId === user.id">owner</span>
-							<span class="username">@{{ user | acct }}</span>
-						</header>
-						<div v-if="group.ownerId !== user.id">
-							<a @click="remove(user)">{{ $t('remove-user') }}</a>
-						</div>
-					</div>
-				</div>
-			</sequential-entrance>
-		</section>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { faCrown, faICursor, faUsers, faPlus } from '@fortawesome/free-solid-svg-icons';
-import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/user-group-editor.vue'),
-
-	props: {
-		groupId: {
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			group: null,
-			users: [],
-			faCrown, faICursor, faTrashAlt, faUsers, faPlus
-		};
-	},
-
-	created() {
-		this.$root.api('users/groups/show', {
-			groupId: this.groupId
-		}).then(group => {
-			this.group = group;
-			this.fetchUsers();
-			this.$emit('init', {
-				title: this.group.name,
-				icon: faUsers
-			});
-		});
-	},
-
-	methods: {
-		fetchGroup() {
-			this.$root.api('users/groups/show', {
-				groupId: this.group.id
-			}).then(group => {
-				this.group = group;
-			})
-		},
-
-		fetchUsers() {
-			this.$root.api('users/show', {
-				userIds: this.group.userIds
-			}).then(users => {
-				this.users = users;
-			});
-		},
-
-		rename() {
-			this.$root.dialog({
-				title: this.$t('rename'),
-				input: {
-					default: this.group.name
-				}
-			}).then(({ canceled, result: name }) => {
-				if (canceled) return;
-				this.$root.api('users/groups/update', {
-					groupId: this.group.id,
-					name: name
-				}).then(() => {
-					this.fetchGroup();
-				}).catch(e => {
-					this.$root.dialog({
-						type: 'error',
-						text: e
-					});
-				});
-			})
-		},
-
-		del() {
-			this.$root.dialog({
-				type: 'warning',
-				text: this.$t('delete-are-you-sure').replace('$1', this.group.name),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				this.$root.api('users/groups/delete', {
-					groupId: this.group.id
-				}).then(() => {
-					this.$root.dialog({
-						type: 'success',
-						text: this.$t('deleted')
-					});
-				}).catch(e => {
-					this.$root.dialog({
-						type: 'error',
-						text: e
-					});
-				});
-			});
-		},
-
-		remove(user: any) {
-			this.$root.api('users/groups/pull', {
-				groupId: this.group.id,
-				userId: user.id
-			}).then(() => {
-				this.fetchGroup();
-				this.fetchUsers();
-			}).catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e
-				});
-			});
-		},
-
-		async invite() {
-			const t = this.$t('invited');
-			const { result: user } = await this.$root.dialog({
-				user: {
-					local: true
-				}
-			});
-			if (user == null) return;
-			this.$root.api('users/groups/invite', {
-				groupId: this.group.id,
-				userId: user.id
-			}).then(() => {
-				this.$root.dialog({
-					type: 'success',
-					text: t
-				});
-			}).catch(e => {
-				this.$root.dialog({
-					type: 'error',
-					text: e
-				});
-			});
-		},
-
-		async transfer() {
-			const { result: user } = await this.$root.dialog({
-				user: {
-					local: true
-				}
-			});
-			if (user == null) return;
-
-			this.$root.dialog({
-				type: 'warning',
-				text: this.$t('transfer-are-you-sure').replace('$1', this.group.name).replace('$2', user.username),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				this.$root.api('users/groups/transfer', {
-					groupId: this.group.id,
-					userId: user.id
-				}).then(() => {
-					this.$root.dialog({
-						type: 'success',
-						text: this.$t('transferred')
-					});
-				}).catch(e => {
-					this.$root.dialog({
-						type: 'error',
-						text: e
-					});
-				});
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.ivrbakop
-	.kjlrfbes
-		display flex
-		padding 16px
-		border-top solid 1px var(--faceDivider)
-
-		> div:first-child
-			> a
-				> .avatar
-					width 64px
-					height 64px
-
-		> div:last-child
-			flex 1
-			padding-left 16px
-
-			@media (max-width 500px)
-				font-size 14px
-
-			> header
-				color var(--text)
-
-				> .is-owner
-					flex-shrink 0
-					align-self center
-					margin-left 8px
-					padding 1px 6px
-					font-size 80%
-					background var(--groupUserListOwnerBg)
-					color var(--groupUserListOwnerFg)
-					border-radius 3px
-
-				> .username
-					margin-left 8px
-					opacity 0.7
-
-</style>
diff --git a/src/client/app/common/views/pages/user-groups.vue b/src/client/app/common/views/pages/user-groups.vue
deleted file mode 100644
index 6501a26061e1670bd910166d64c90f8604a2929b..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/user-groups.vue
+++ /dev/null
@@ -1,132 +0,0 @@
-<template>
-<div>
-	<ui-container :body-togglable="true">
-		<template #header><fa :icon="faUsers"/> {{ $t('owned-groups') }}</template>
-		<ui-margin>
-			<ui-button @click="add"><fa :icon="faPlus"/> {{ $t('create-group') }}</ui-button>
-		</ui-margin>
-		<div class="hwgkdrbl" v-for="group in ownedGroups" :key="group.id">
-			<ui-hr/>
-			<ui-margin>
-				<router-link :to="`/i/groups/${group.id}`">{{ group.name }}</router-link>
-				<x-avatars :user-ids="group.userIds" style="margin-top:8px;"/>
-			</ui-margin>
-		</div>
-	</ui-container>
-
-	<ui-container :body-togglable="true">
-		<template #header><fa :icon="faUsers"/> {{ $t('joined-groups') }}</template>
-		<div class="hwgkdrbl" v-for="(group, i) in joinedGroups" :key="group.id">
-			<ui-hr v-if="i != 0"/>
-			<ui-margin>
-				<div style="color:var(--text);">{{ group.name }}</div>
-				<x-avatars :user-ids="group.userIds" style="margin-top:8px;"/>
-			</ui-margin>
-		</div>
-	</ui-container>
-
-	<ui-container :body-togglable="true">
-		<template #header><fa :icon="faEnvelopeOpenText"/> {{ $t('invites') }}</template>
-		<div class="fvlojuur" v-for="(invite, i) in invites" :key="invite.id">
-			<ui-hr v-if="i != 0"/>
-			<ui-margin>
-				<div class="name" style="color:var(--text);">{{ invite.group.name }}</div>
-				<x-avatars :user-ids="invite.group.userIds" style="margin-top:8px;"/>
-				<ui-horizon-group>
-					<ui-button @click="acceptInvite(invite)"><fa :icon="faCheck"/> {{ $t('accept-invite') }}</ui-button>
-					<ui-button @click="rejectInvite(invite)"><fa :icon="faBan"/> {{ $t('reject-invite') }}</ui-button>
-				</ui-horizon-group>
-			</ui-margin>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { faUsers, faPlus, faCheck, faBan, faEnvelopeOpenText } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../i18n';
-import XAvatars from '../../views/components/avatars.vue';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/user-groups.vue'),
-	components: {
-		XAvatars
-	},
-	data() {
-		return {
-			ownedGroups: [],
-			joinedGroups: [],
-			invites: [],
-			faUsers, faPlus, faCheck, faBan, faEnvelopeOpenText
-		};
-	},
-	mounted() {
-		document.title = this.$root.instanceName;
-
-		this.$root.api('users/groups/owned').then(groups => {
-			this.ownedGroups = groups;
-		});
-
-		this.$root.api('users/groups/joined').then(groups => {
-			this.joinedGroups = groups;
-		});
-
-		this.$root.api('i/user-group-invites').then(invites => {
-			this.invites = invites;
-		});
-
-		this.$emit('init', {
-			title: this.$t('user-groups'),
-			icon: faUsers
-		});
-	},
-	methods: {
-		add() {
-			this.$root.dialog({
-				title: this.$t('group-name'),
-				input: true
-			}).then(async ({ canceled, result: name }) => {
-				if (canceled) return;
-				const group = await this.$root.api('users/groups/create', {
-					name
-				});
-
-				this.ownedGroups.push(group)
-			});
-		},
-		acceptInvite(invite) {
-			this.$root.api('users/groups/invitations/accept', {
-				inviteId: invite.id
-			}).then(() => {
-				this.$root.dialog({
-					type: 'success',
-					splash: true
-				});
-				this.$root.api('i/user-group-invites').then(invites => {
-					this.invites = invites;
-				}).then(() => {
-					this.$root.api('users/groups/joined').then(groups => {
-						this.joinedGroups = groups;
-					});
-				});
-			});
-		},
-		rejectInvite(invite) {
-			this.$root.api('users/groups/invitations/reject', {
-				inviteId: invite.id
-			}).then(() => {
-				this.$root.api('i/user-group-invites').then(invites => {
-					this.invites = invites;
-				});
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.hwgkdrbl
-	display block
-
-</style>
diff --git a/src/client/app/common/views/pages/user-list-editor.vue b/src/client/app/common/views/pages/user-list-editor.vue
deleted file mode 100644
index 3bc5cca7781a82124a9036cd4865778b0760d537..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/user-list-editor.vue
+++ /dev/null
@@ -1,182 +0,0 @@
-<template>
-<div class="cudqjmnl">
-	<ui-container v-if="list">
-		<template #header><fa :icon="faListUl"/> {{ list.name }}</template>
-
-		<section class="fwvevrks">
-			<ui-margin>
-				<ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button>
-				<ui-button @click="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button>
-			</ui-margin>
-		</section>
-	</ui-container>
-
-	<ui-container>
-		<template #header><fa :icon="faUsers"/> {{ $t('users') }}</template>
-
-		<section>
-			<ui-margin>
-				<ui-button @click="add"><fa :icon="faPlus"/> {{ $t('add-user') }}</ui-button>
-			</ui-margin>
-			<sequential-entrance animation="entranceFromTop" delay="25">
-				<div class="phcqulfl" v-for="user in users">
-					<div>
-						<a :href="user | userPage">
-							<mk-avatar class="avatar" :user="user" :disable-link="true"/>
-						</a>
-					</div>
-					<div>
-						<header>
-							<b><mk-user-name :user="user"/></b>
-							<span class="username">@{{ user | acct }}</span>
-						</header>
-						<div>
-							<a @click="remove(user)">{{ $t('remove-user') }}</a>
-						</div>
-					</div>
-				</div>
-			</sequential-entrance>
-		</section>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { faListUl, faICursor, faUsers, faPlus } from '@fortawesome/free-solid-svg-icons';
-import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/user-list-editor.vue'),
-
-	props: {
-		listId: {
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			list: null,
-			users: [],
-			faListUl, faICursor, faTrashAlt, faUsers, faPlus
-		};
-	},
-
-	created() {
-		this.$root.api('users/lists/show', {
-			listId: this.listId
-		}).then(list => {
-			this.list = list;
-			this.fetchUsers();
-			this.$emit('init', {
-				title: this.list.name,
-				icon: faListUl
-			});
-		});
-	},
-
-	methods: {
-		fetchUsers() {
-			this.$root.api('users/show', {
-				userIds: this.list.userIds
-			}).then(users => {
-				this.users = users;
-			});
-		},
-
-		rename() {
-			this.$root.dialog({
-				title: this.$t('rename'),
-				input: {
-					default: this.list.name
-				}
-			}).then(({ canceled, result: name }) => {
-				if (canceled) return;
-				this.$root.api('users/lists/update', {
-					listId: this.list.id,
-					name: name
-				});
-			});
-		},
-
-		del() {
-			this.$root.dialog({
-				type: 'warning',
-				text: this.$t('delete-are-you-sure').replace('$1', this.list.name),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				this.$root.api('users/lists/delete', {
-					listId: this.list.id
-				}).then(() => {
-					this.$root.dialog({
-						type: 'success',
-						text: this.$t('deleted')
-					});
-				}).catch(e => {
-					this.$root.dialog({
-						type: 'error',
-						text: e
-					});
-				});
-			});
-		},
-
-		remove(user: any) {
-			this.$root.api('users/lists/pull', {
-				listId: this.list.id,
-				userId: user.id
-			}).then(() => {
-				this.fetchUsers();
-			});
-		},
-
-		async add() {
-			const { result: user } = await this.$root.dialog({
-				user: {
-					local: true
-				}
-			});
-			if (user == null) return;
-			this.$root.api('users/lists/push', {
-				listId: this.list.id,
-				userId: user.id
-			}).then(() => {
-				this.fetchUsers();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.cudqjmnl
-	.phcqulfl
-		display flex
-		padding 16px
-		border-top solid 1px var(--faceDivider)
-
-		> div:first-child
-			> a
-				> .avatar
-					width 64px
-					height 64px
-
-		> div:last-child
-			flex 1
-			padding-left 16px
-
-			@media (max-width 500px)
-				font-size 14px
-
-			> header
-				color var(--text)
-
-				> .username
-					margin-left 8px
-					opacity 0.7
-
-</style>
diff --git a/src/client/app/common/views/pages/user-lists.vue b/src/client/app/common/views/pages/user-lists.vue
deleted file mode 100644
index 955eef993a3c263547909acc5bc5540c8304cf0a..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/pages/user-lists.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-<template>
-<ui-container>
-	<template #header><fa :icon="faListUl"/> {{ $t('user-lists') }}</template>
-	<ui-margin>
-		<ui-button @click="add"><fa :icon="faPlus"/> {{ $t('create-list') }}</ui-button>
-	</ui-margin>
-	<div class="cpqqyrst" v-for="list in lists" :key="list.id">
-		<ui-hr/>
-		<ui-margin>
-			<router-link :to="`/i/lists/${list.id}`">{{ list.name }}</router-link>
-			<x-avatars :user-ids="list.userIds" style="margin-top:8px;"/>
-		</ui-margin>
-	</div>
-</ui-container>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { faListUl, faPlus } from '@fortawesome/free-solid-svg-icons';
-import XAvatars from '../../views/components/avatars.vue';
-
-export default Vue.extend({
-	i18n: i18n('common/views/components/user-lists.vue'),
-	components: {
-		XAvatars
-	},
-	data() {
-		return {
-			fetching: true,
-			lists: [],
-			faListUl, faPlus
-		};
-	},
-	mounted() {
-		document.title = this.$root.instanceName;
-		
-		this.$root.api('users/lists/list').then(lists => {
-			this.fetching = false;
-			this.lists = lists;
-		});
-
-		this.$emit('init', {
-			title: this.$t('user-lists'),
-			icon: faListUl
-		});
-	},
-	methods: {
-		add() {
-			this.$root.dialog({
-				title: this.$t('list-name'),
-				input: true
-			}).then(async ({ canceled, result: name }) => {
-				if (canceled) return;
-				const list = await this.$root.api('users/lists/create', {
-					name
-				});
-
-				this.lists.push(list)
-			});
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.cpqqyrst
-	display block
-
-</style>
diff --git a/src/client/app/common/views/widgets/analog-clock.vue b/src/client/app/common/views/widgets/analog-clock.vue
deleted file mode 100644
index bff01f89b54fedc6c152b0f5b97c762cb6e464e9..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/analog-clock.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-<template>
-<div class="mkw-analog-clock">
-	<ui-container :naked="props.style % 2 === 0" :show-header="false">
-		<div class="mkw-analog-clock--body">
-			<mk-analog-clock :dark="$store.state.device.darkmode" :smooth="props.style < 2"/>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-export default define({
-	name: 'analog-clock',
-	props: () => ({
-		style: 0
-	})
-}).extend({
-	methods: {
-		func() {
-			this.props.style = (this.props.style + 1) % 4;
-			this.save();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mkw-analog-clock
-	.mkw-analog-clock--body
-		padding 8px
-
-</style>
diff --git a/src/client/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue
deleted file mode 100644
index 9423d25da89c54b458c312155f22103927cacb9b..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/broadcast.vue
+++ /dev/null
@@ -1,205 +0,0 @@
-<template>
-<div class="anltbovirfeutcigvwgmgxipejaeozxi">
-	<ui-container :show-header="false" :naked="props.design === 1">
-		<div class="anltbovirfeutcigvwgmgxipejaeozxi-body"
-			:data-found="announcements && announcements.length !== 0"
-			:data-melt="props.design == 1"
-			:data-mobile="platform == 'mobile'"
-		>
-			<div class="broadcast-left" v-show="announcements && announcements.length !== 0">
-				<div class="icon">
-					<svg height="32" version="1.1" viewBox="0 0 32 32" width="32">
-						<path class="tower" d="M16.04,11.24c1.79,0,3.239-1.45,3.239-3.24S17.83,4.76,16.04,4.76c-1.79,0-3.24,1.45-3.24,3.24 C12.78,9.78,14.24,11.24,16.04,11.24z M16.04,13.84c-0.82,0-1.66-0.2-2.4-0.6L7.34,29.98h2.98l1.72-2h8l1.681,2H24.7L18.42,13.24 C17.66,13.64,16.859,13.84,16.04,13.84z M16.02,14.8l2.02,7.2h-4L16.02,14.8z M12.04,25.98l2-2h4l2,2H12.04z"></path>
-						<path class="wave a" d="M4.66,1.04c-0.508-0.508-1.332-0.508-1.84,0c-1.86,1.92-2.8,4.44-2.8,6.94c0,2.52,0.94,5.04,2.8,6.96 c0.5,0.52,1.32,0.52,1.82,0s0.5-1.36,0-1.88C3.28,11.66,2.6,9.82,2.6,7.98S3.28,4.3,4.64,2.9C5.157,2.391,5.166,1.56,4.66,1.04z"></path>
-						<path class="wave b" d="M9.58,12.22c0.5-0.5,0.5-1.34,0-1.84C8.94,9.72,8.62,8.86,8.62,8s0.32-1.72,0.96-2.38c0.5-0.52,0.5-1.34,0-1.84 C9.346,3.534,9.02,3.396,8.68,3.4c-0.32,0-0.66,0.12-0.9,0.38C6.64,4.94,6.08,6.48,6.08,8s0.58,3.06,1.7,4.22 C8.28,12.72,9.1,12.72,9.58,12.22z"></path>
-						<path class="wave c" d="M22.42,3.78c-0.5,0.5-0.5,1.34,0,1.84c0.641,0.66,0.96,1.52,0.96,2.38s-0.319,1.72-0.96,2.38c-0.5,0.52-0.5,1.34,0,1.84 c0.487,0.497,1.285,0.505,1.781,0.018c0.007-0.006,0.013-0.012,0.02-0.018c1.139-1.16,1.699-2.7,1.699-4.22s-0.561-3.06-1.699-4.22 c-0.494-0.497-1.297-0.5-1.794-0.007C22.424,3.775,22.422,3.778,22.42,3.78z"></path>
-						<path class="wave d" d="M29.18,1.06c-0.479-0.502-1.273-0.522-1.775-0.044c-0.016,0.015-0.029,0.029-0.045,0.044c-0.5,0.52-0.5,1.36,0,1.88 c1.361,1.4,2.041,3.24,2.041,5.08s-0.68,3.66-2.041,5.08c-0.5,0.52-0.5,1.36,0,1.88c0.509,0.508,1.332,0.508,1.841,0 c1.86-1.92,2.8-4.44,2.8-6.96C31.99,5.424,30.98,2.931,29.18,1.06z"></path>
-					</svg>
-				</div>
-				<div class="broadcast-nav" v-show="announcements && announcements.length > 1">
-					<mk-frac class="broadcast-page" :value="i + 1" :total="announcements.length"/>
-					<ui-button class="broadcast-prev" @click="prev" :title="$t('next')"><fa :icon="faAngleLeft"/></ui-button>
-					<ui-button class="broadcast-next" @click="next" :title="$t('prev')"><fa :icon="faAngleRight"/></ui-button>
-				</div>
-			</div>
-			<div class="broadcast-right">
-				<p class="fetching" v-if="fetching">{{ $t('fetching') }}<mk-ellipsis/></p>
-				<h1 v-if="!fetching">{{ announcements.length == 0 ? $t('no-broadcasts') : announcements[i].title }}</h1>
-				<p v-if="!fetching">
-					<mfm v-if="announcements.length != 0" :text="announcements[i].text" :key="i"/>
-					<img v-if="announcements.length != 0 && announcements[i].image" :src="announcements[i].image" alt="" style="display: block; max-height: 130px; max-width: 100%;"/>
-					<template v-if="announcements.length == 0">{{ $t('have-a-nice-day') }}</template>
-				</p>
-			</div>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import { faAngleLeft, faAngleRight } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../i18n';
-
-export default define({
-	name: 'broadcast',
-	props: () => ({
-		design: 0
-	})
-}).extend({
-	i18n: i18n('common/views/widgets/broadcast.vue'),
-	data() {
-		return {
-			i: 0,
-			fetching: true,
-			announcements: [],
-			faAngleLeft, faAngleRight
-		};
-	},
-	mounted() {
-		this.$root.getMeta().then(meta => {
-			this.announcements = meta.announcements;
-			this.fetching = false;
-		});
-	},
-	methods: {
-		next() {
-			if (this.i === this.announcements.length - 1) {
-				this.i = 0;
-			} else {
-				this.i++;
-			}
-		},
-		prev() {
-			if (this.i === 0) {
-				this.i = this.announcements.length - 1;
-			} else {
-				this.i--;
-			}
-		},
-		func() {
-			if (this.props.design === 1) {
-				this.props.design = 0;
-			} else {
-				this.props.design++;
-			}
-			this.save();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.anltbovirfeutcigvwgmgxipejaeozxi-body
-	display flex
-	padding 10px
-	background var(--announcementsBg)
-
-	&[data-melt]
-		background transparent
-
-	> .broadcast-left
-		width 32px
-		margin-right 8px
-
-		> .icon
-			> svg
-				fill currentColor
-				color var(--announcementsTitle)
-
-				> .wave
-					opacity 1
-
-					&.a
-						animation wave 20s ease-in-out 2.1s infinite
-					&.b
-						animation wave 20s ease-in-out 2s infinite
-					&.c
-						animation wave 20s ease-in-out 2s infinite
-					&.d
-						animation wave 20s ease-in-out 2.1s infinite
-
-					@keyframes wave
-						0%
-							opacity 1
-						1.5%
-							opacity 0
-						3.5%
-							opacity 0
-						5%
-							opacity 1
-						6.5%
-							opacity 0
-						8.5%
-							opacity 0
-						10%
-							opacity 1
-
-		> .broadcast-nav
-			display flex
-			flex-wrap wrap
-			padding 1px 0 2px
-
-			> .broadcast-page
-				width 100%
-				color var(--announcementsTitle)
-				text-align center
-				font-size .6rem
-
-			> .broadcast-prev,
-			> .broadcast-next
-				flex 1
-				width 50%
-				display block
-				margin 0
-				padding 0
-				font-size .9rem
-				line-height 1.3em
-				color var(--link)
-				background transparent
-				cursor pointer
-
-				&:focus
-					&:after
-						top -1px
-						right -1px
-						bottom -1px
-						left -1px
-
-				&.round:focus:after
-						border-radius 5px
-
-			> .broadcast-prev
-				padding-right 3px
-
-			> .broadcast-next
-				padding-left 3px
-
-	> .broadcast-right
-		flex 1
-		word-break break-word
-
-		> h1
-			margin 0
-			font-size .975em
-			font-weight normal
-			line-height 1.3em
-			color var(--announcementsTitle)
-			padding-bottom 2px
-
-		> p
-			display block
-			z-index 1
-			margin 0
-			font-size .8em
-			color var(--announcementsText)
-			width 100%
-
-			&.fetching
-				text-align center
-
-		&[data-mobile]
-			> p
-				color #fff
-
-</style>
diff --git a/src/client/app/common/views/widgets/calendar.vue b/src/client/app/common/views/widgets/calendar.vue
deleted file mode 100644
index 32ce1efeb7ef500740b23be6f1bb49b6961f4355..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/calendar.vue
+++ /dev/null
@@ -1,202 +0,0 @@
-<template>
-<div class="mkw-calendar" :data-special="special" :data-mobile="platform == 'mobile'">
-	<ui-container :naked="props.design == 1" :show-header="false">
-		<div class="mkw-calendar--body">
-			<div class="calendar" :data-is-holiday="isHoliday">
-				<p class="month-and-year">
-					<span class="year">{{ this.$t('year').split('{}')[0] }}{{ year }}{{ this.$t('year').split('{}')[1] }}</span>
-					<span class="month">{{ this.$t('month').split('{}')[0] }}{{ month }}{{ this.$t('month').split('{}')[1] }}</span>
-				</p>
-				<p class="day">{{ this.$t('day').split('{}')[0] }}{{ day }}{{ this.$t('day').split('{}')[1] }}</p>
-				<p class="week-day">{{ weekDay }}</p>
-			</div>
-			<div class="info">
-				<div>
-					<p>{{ $t('today') }}<b>{{ dayP.toFixed(1) }}%</b></p>
-					<div class="meter">
-						<div class="val" :style="{ width: `${dayP}%` }"></div>
-					</div>
-				</div>
-				<div>
-					<p>{{ $t('this-month') }}<b>{{ monthP.toFixed(1) }}%</b></p>
-					<div class="meter">
-						<div class="val" :style="{ width: `${monthP}%` }"></div>
-					</div>
-				</div>
-				<div>
-					<p>{{ $t('this-year') }}<b>{{ yearP.toFixed(1) }}%</b></p>
-					<div class="meter">
-						<div class="val" :style="{ width: `${yearP}%` }"></div>
-					</div>
-				</div>
-			</div>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-
-export default define({
-	name: 'calendar',
-	props: () => ({
-		design: 0
-	})
-}).extend({
-	i18n: i18n('common/views/widgets/calendar.vue'),
-	data() {
-		return {
-			now: new Date(),
-			year: null,
-			month: null,
-			day: null,
-			weekDay: null,
-			yearP: null,
-			dayP: null,
-			monthP: null,
-			isHoliday: null,
-			special: null,
-			clock: null
-		};
-	},
-	created() {
-		this.tick();
-		this.clock = setInterval(this.tick, 1000);
-	},
-	beforeDestroy() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		func() {
-			if (this.platform == 'mobile') return;
-			if (this.props.design == 2) {
-				this.props.design = 0;
-			} else {
-				this.props.design++;
-			}
-			this.save();
-		},
-		tick() {
-			const now = new Date();
-			const nd = now.getDate();
-			const nm = now.getMonth();
-			const ny = now.getFullYear();
-
-			this.year = ny;
-			this.month = nm + 1;
-			this.day = nd;
-			this.weekDay = [
-				this.$t('@.weekday.sunday'),
-				this.$t('@.weekday.monday'),
-				this.$t('@.weekday.tuesday'),
-				this.$t('@.weekday.wednesday'),
-				this.$t('@.weekday.thursday'),
-				this.$t('@.weekday.friday'),
-				this.$t('@.weekday.saturday')
-			][now.getDay()];
-
-			const dayNumer   = now.getTime() - new Date(ny, nm, nd).getTime();
-			const dayDenom   = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/;
-			const monthNumer = now.getTime() - new Date(ny, nm, 1).getTime();
-			const monthDenom = new Date(ny, nm + 1, 1).getTime() - new Date(ny, nm, 1).getTime();
-			const yearNumer  = now.getTime() - new Date(ny, 0, 1).getTime();
-			const yearDenom  = new Date(ny + 1, 0, 1).getTime() - new Date(ny, 0, 1).getTime();
-
-			this.dayP   = dayNumer   / dayDenom   * 100;
-			this.monthP = monthNumer / monthDenom * 100;
-			this.yearP  = yearNumer  / yearDenom  * 100;
-
-			this.isHoliday = now.getDay() == 0 || now.getDay() == 6;
-
-			this.special =
-				nm == 0 && nd == 1 ? 'on-new-years-day' :
-				false;
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mkw-calendar
-	&[data-special='on-new-years-day']
-		border-color #ef95a0
-
-	.mkw-calendar--body
-		padding 16px 0
-		color var(--calendarDay)
-
-		&:after
-			content ""
-			display block
-			clear both
-
-		> .calendar
-			float left
-			width 60%
-			text-align center
-
-			&[data-is-holiday]
-				> .day
-					color #ef95a0
-
-			> p
-				margin 0
-				line-height 18px
-				font-size 14px
-
-				> span
-					margin 0 4px
-
-			> .day
-				margin 10px 0
-				line-height 32px
-				font-size 28px
-
-		> .info
-			display block
-			float left
-			width 40%
-			padding 0 16px 0 0
-
-			> div
-				margin-bottom 8px
-
-				&:last-child
-					margin-bottom 4px
-
-				> p
-					margin 0 0 2px 0
-					font-size 12px
-					line-height 18px
-					color var(--text)
-					opacity 0.8
-
-					> b
-						margin-left 2px
-
-				> .meter
-					width 100%
-					overflow hidden
-					background var(--materBg)
-					border-radius 8px
-
-					> .val
-						height 4px
-						background var(--primary)
-						transition width .3s cubic-bezier(0.23, 1, 0.32, 1)
-
-				&:nth-child(1)
-					> .meter > .val
-						background #f7796c
-
-				&:nth-child(2)
-					> .meter > .val
-						background #a1de41
-
-				&:nth-child(3)
-					> .meter > .val
-						background #41ddde
-
-</style>
diff --git a/src/client/app/common/views/widgets/hashtags.vue b/src/client/app/common/views/widgets/hashtags.vue
deleted file mode 100644
index b266d5f6e6a97dbaa517fcf3f4e7ed14fd17d7d7..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/hashtags.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<template>
-<div class="mkw-hashtags">
-	<ui-container :show-header="!props.compact">
-		<template #header><fa icon="hashtag"/>{{ $t('title') }}</template>
-
-		<div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'">
-			<mk-trends/>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-
-export default define({
-	name: 'hashtags',
-	props: () => ({
-		compact: false
-	})
-}).extend({
-	i18n: i18n('common/views/widgets/hashtags.vue'),
-	methods: {
-		func() {
-			this.props.compact = !this.props.compact;
-			this.save();
-		}
-	}
-});
-</script>
diff --git a/src/client/app/common/views/widgets/index.ts b/src/client/app/common/views/widgets/index.ts
deleted file mode 100644
index d923a01941bc6d16b0aa119c60d82098771ef9fd..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/index.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import Vue from 'vue';
-
-import wAnalogClock from './analog-clock.vue';
-import wVersion from './version.vue';
-import wRss from './rss.vue';
-import wServer from './server.vue';
-import wPostsMonitor from './posts-monitor.vue';
-import wMemo from './memo.vue';
-import wBroadcast from './broadcast.vue';
-import wCalendar from './calendar.vue';
-import wPhotoStream from './photo-stream.vue';
-import wSlideshow from './slideshow.vue';
-import wTips from './tips.vue';
-import wNav from './nav.vue';
-import wHashtags from './hashtags.vue';
-import wInstance from './instance.vue';
-import wPostForm from './post-form.vue';
-
-Vue.component('mkw-analog-clock', wAnalogClock);
-Vue.component('mkw-nav', wNav);
-Vue.component('mkw-calendar', wCalendar);
-Vue.component('mkw-photo-stream', wPhotoStream);
-Vue.component('mkw-slideshow', wSlideshow);
-Vue.component('mkw-tips', wTips);
-Vue.component('mkw-broadcast', wBroadcast);
-Vue.component('mkw-server', wServer);
-Vue.component('mkw-posts-monitor', wPostsMonitor);
-Vue.component('mkw-memo', wMemo);
-Vue.component('mkw-rss', wRss);
-Vue.component('mkw-version', wVersion);
-Vue.component('mkw-hashtags', wHashtags);
-Vue.component('mkw-instance', wInstance);
-Vue.component('mkw-post-form', wPostForm);
-Vue.component('mkw-queue', () => import('./queue.vue').then(m => m.default));
diff --git a/src/client/app/common/views/widgets/instance.vue b/src/client/app/common/views/widgets/instance.vue
deleted file mode 100644
index 96d6184d1e7796b7f34a52877f90ea83f1e84936..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/instance.vue
+++ /dev/null
@@ -1,14 +0,0 @@
-<template>
-<div class="mkw-instance">
-	<ui-container>
-		<mk-instance/>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-export default define({
-	name: 'instance'
-});
-</script>
diff --git a/src/client/app/common/views/widgets/memo.vue b/src/client/app/common/views/widgets/memo.vue
deleted file mode 100644
index b3b668a9ad9105b19197ba06bdbeea925a58c1ad..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/memo.vue
+++ /dev/null
@@ -1,108 +0,0 @@
-<template>
-<div class="mkw-memo">
-	<ui-container :show-header="!props.compact">
-		<template #header><fa :icon="['far', 'sticky-note']"/>{{ $t('title') }}</template>
-
-		<div class="mkw-memo--body">
-			<textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea>
-			<button @click="saveMemo" :disabled="!changed">{{ $t('save') }}</button>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../define-widget';
-import i18n from '../../../i18n';
-
-export default define({
-	name: 'memo',
-	props: () => ({
-		compact: false
-	})
-}).extend({
-	i18n: i18n('common/views/widgets/memo.vue'),
-	data() {
-		return {
-			text: null,
-			changed: false,
-			timeoutId: null
-		};
-	},
-
-	created() {
-		this.text = this.$store.state.settings.memo;
-
-		this.$watch('$store.state.settings.memo', text => {
-			this.text = text;
-		});
-	},
-
-	methods: {
-		func() {
-			this.props.compact = !this.props.compact;
-			this.save();
-		},
-
-		onChange() {
-			this.changed = true;
-			clearTimeout(this.timeoutId);
-			this.timeoutId = setTimeout(this.saveMemo, 1000);
-		},
-
-		saveMemo() {
-			this.$store.dispatch('settings/set', {
-				key: 'memo',
-				value: this.text
-			});
-			this.changed = false;
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mkw-memo
-	.mkw-memo--body
-		padding-bottom 28px + 16px
-
-		> textarea
-			display block
-			width 100%
-			max-width 100%
-			min-width 100%
-			padding 16px
-			color var(--inputText)
-			background var(--face)
-			border none
-			border-bottom solid var(--lineWidth) var(--faceDivider)
-			border-radius 0
-
-		> button
-			display block
-			position absolute
-			bottom 8px
-			right 8px
-			margin 0
-			padding 0 10px
-			height 28px
-			color var(--primaryForeground)
-			background var(--primary) !important
-			outline none
-			border none
-			border-radius 4px
-			transition background 0.1s ease
-			cursor pointer
-
-			&:hover
-				background var(--primaryLighten10) !important
-
-			&:active
-				background var(--primaryDarken10) !important
-				transition background 0s ease
-
-			&:disabled
-				opacity 0.7
-				cursor default
-
-</style>
diff --git a/src/client/app/common/views/widgets/nav.vue b/src/client/app/common/views/widgets/nav.vue
deleted file mode 100644
index 2b8caa7be897968975a511e94f464e755cbc9f3d..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/nav.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<template>
-<div class="mkw-nav">
-	<ui-container>
-		<div class="mkw-nav--body">
-			<mk-nav/>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-export default define({
-	name: 'nav'
-});
-</script>
-
-<style lang="stylus" scoped>
-.mkw-nav
-	.mkw-nav--body
-		padding 16px
-		font-size 12px
-		color var(--text)
-		background var(--face)
-
-		a
-			color var(--text)
-
-		i
-			color var(--text)
-
-</style>
diff --git a/src/client/app/common/views/widgets/photo-stream.vue b/src/client/app/common/views/widgets/photo-stream.vue
deleted file mode 100644
index eae6d0a1904f57953c582479411a8333d65c6967..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/photo-stream.vue
+++ /dev/null
@@ -1,125 +0,0 @@
-<template>
-<div class="mkw-photo-stream" :class="$style.root" :data-melt="props.design == 2">
-	<ui-container :show-header="props.design == 0" :naked="props.design == 2">
-		<template #header><fa icon="camera"/>{{ $t('title') }}</template>
-
-		<p :class="$style.fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
-		<div :class="$style.stream" v-if="!fetching && images.length > 0">
-			<div v-for="(image, i) in images" :key="i"
-				:class="$style.img"
-				:style="`background-image: url(${thumbnail(image)})`"
-				draggable="true"
-				@dragstart="onDragstart(image, $event)"
-			></div>
-		</div>
-		<p :class="$style.empty" v-if="!fetching && images.length == 0">{{ $t('no-photos') }}</p>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-import { getStaticImageUrl } from '../../scripts/get-static-image-url';
-
-export default define({
-	name: 'photo-stream',
-	props: () => ({
-		design: 0
-	})
-}).extend({
-	i18n: i18n('common/views/widgets/photo-stream.vue'),
-
-	data() {
-		return {
-			images: [],
-			fetching: true,
-			connection: null
-		};
-	},
-
-	mounted() {
-		this.connection = this.$root.stream.useSharedConnection('main');
-
-		this.connection.on('driveFileCreated', this.onDriveFileCreated);
-
-		this.$root.api('drive/stream', {
-			type: 'image/*',
-			limit: 9
-		}).then(images => {
-			this.images = images;
-			this.fetching = false;
-		});
-	},
-
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		onDriveFileCreated(file) {
-			if (/^image\/.+$/.test(file.type)) {
-				this.images.unshift(file);
-				if (this.images.length > 9) this.images.pop();
-			}
-		},
-
-		func() {
-			if (this.props.design == 2) {
-				this.props.design = 0;
-			} else {
-				this.props.design++;
-			}
-
-			this.save();
-		},
-
-		onDragstart(file, e) {
-			e.dataTransfer.effectAllowed = 'move';
-			e.dataTransfer.setData('mk_drive_file', JSON.stringify(file));
-		},
-
-		thumbnail(image: any): string {
-			return this.$store.state.device.disableShowingAnimatedImages
-				? getStaticImageUrl(image.thumbnailUrl)
-				: image.thumbnailUrl;
-		},
-	}
-});
-</script>
-
-<style lang="stylus" module>
-.root[data-melt]
-	.stream
-		padding 0
-
-	.img
-		border solid 4px transparent
-		border-radius 8px
-
-.stream
-	display flex
-	justify-content center
-	flex-wrap wrap
-	padding 8px
-
-	.img
-		flex 1 1 33%
-		width 33%
-		height 80px
-		background-position center center
-		background-size cover
-		border solid 2px transparent
-		border-radius 4px
-
-.fetching
-.empty
-	margin 0
-	padding 16px
-	text-align center
-	color var(--text)
-
-	> [data-icon]
-		margin-right 4px
-
-</style>
diff --git a/src/client/app/common/views/widgets/post-form.vue b/src/client/app/common/views/widgets/post-form.vue
deleted file mode 100644
index 6680a114356598b38c7687c4fadce19203969393..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/post-form.vue
+++ /dev/null
@@ -1,294 +0,0 @@
-<template>
-<div>
-	<ui-container :show-header="props.design == 0">
-		<template #header><fa icon="pencil-alt"/>{{ $t('title') }}</template>
-
-		<div class="lhcuptdmcdkfwmipgazeawoiuxpzaclc-body"
-			@dragover.stop="onDragover"
-			@drop.stop="onDrop"
-		>
-			<div class="textarea">
-				<textarea
-					:disabled="posting"
-					v-model="text"
-					@keydown="onKeydown"
-					@paste="onPaste"
-					:placeholder="placeholder"
-					ref="text"
-					v-autocomplete="{ model: 'text' }"
-				></textarea>
-				<button class="emoji" @click="emoji" ref="emoji" v-if="!$root.isMobile">
-					<fa :icon="['far', 'laugh']"/>
-				</button>
-			</div>
-			<x-post-form-attaches class="files" :files="files" :detach-media-fn="detachMedia"/>
-			<input ref="file" type="file" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
-			<mk-uploader ref="uploader" @uploaded="attachMedia"/>
-			<footer>
-				<button @click="chooseFile"><fa icon="upload"/></button>
-				<button @click="chooseFileFromDrive"><fa icon="cloud"/></button>
-				<button @click="post" :disabled="posting" class="post">{{ $t('note') }}</button>
-			</footer>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-import insertTextAtCursor from 'insert-text-at-cursor';
-import { formatTimeString } from '../../../../../misc/format-time-string';
-
-export default define({
-	name: 'post-form',
-	props: () => ({
-		design: 0
-	})
-}).extend({
-	i18n: i18n('desktop/views/widgets/post-form.vue'),
-
-	components: {
-		XPostFormAttaches: () => import('../components/post-form-attaches.vue').then(m => m.default)
-	},
-
-	data() {
-		return {
-			posting: false,
-			text: '',
-			files: [],
-		};
-	},
-
-	computed: {
-		placeholder(): string {
-			const xs = [
-				this.$t('@.note-placeholders.a'),
-				this.$t('@.note-placeholders.b'),
-				this.$t('@.note-placeholders.c'),
-				this.$t('@.note-placeholders.d'),
-				this.$t('@.note-placeholders.e'),
-				this.$t('@.note-placeholders.f')
-			];
-			return xs[Math.floor(Math.random() * xs.length)];
-		}
-	},
-
-	methods: {
-		func() {
-			if (this.props.design == 1) {
-				this.props.design = 0;
-			} else {
-				this.props.design++;
-			}
-			this.save();
-		},
-
-		chooseFile() {
-			(this.$refs.file as any).click();
-		},
-
-		chooseFileFromDrive() {
-			this.$chooseDriveFile({
-				multiple: true
-			}).then(files => {
-				for (const x of files) this.attachMedia(x);
-			});
-		},
-
-		attachMedia(driveFile) {
-			this.files.push(driveFile);
-			this.$emit('change-attached-files', this.files);
-		},
-
-		detachMedia(id) {
-			this.files = this.files.filter(x => x.id != id);
-			this.$emit('change-attached-files', this.files);
-		},
-
-		onKeydown(e) {
-			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && !this.posting && this.text) this.post();
-		},
-
-		async onPaste(e: ClipboardEvent) {
-			for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
-				if (item.kind == 'file') {
-					const file = item.getAsFile();
-					const lio = file.name.lastIndexOf('.');
-					const ext = lio >= 0 ? file.name.slice(lio) : '';
-					const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
-					const name = this.$store.state.settings.pasteDialog
-						? await this.$root.dialog({
-								title: this.$t('@.post-form.enter-file-name'),
-								input: {
-									default: formatted
-								},
-								allowEmpty: false
-							}).then(({ canceled, result }) => canceled ? false : result)
-						: formatted;
-					if (name) this.upload(file, name);
-				}
-			}
-		},
-
-		onChangeFile() {
-			for (const x of Array.from((this.$refs.file as any).files)) this.upload(x);
-		},
-
-		upload(file: File, name?: string) {
-			(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name);
-		},
-
-		onDragover(e) {
-			const isFile = e.dataTransfer.items[0].kind == 'file';
-			const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
-			if (isFile || isDriveFile) {
-				e.preventDefault();
-				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
-			}
-		},
-
-		onDrop(e): void {
-			// ファイルだったら
-			if (e.dataTransfer.files.length > 0) {
-				e.preventDefault();
-				for (const x of Array.from(e.dataTransfer.files)) this.upload(x);
-				return;
-			}
-
-			//#region ドライブのファイル
-			const driveFile = e.dataTransfer.getData('mk_drive_file');
-			if (driveFile != null && driveFile != '') {
-				const file = JSON.parse(driveFile);
-				this.files.push(file);
-				e.preventDefault();
-			}
-			//#endregion
-		},
-
-		async emoji() {
-			const Picker = await import('../../../desktop/views/components/emoji-picker-dialog.vue').then(m => m.default);
-			const button = this.$refs.emoji;
-			const rect = button.getBoundingClientRect();
-			const vm = this.$root.new(Picker, {
-				x: button.offsetWidth + rect.left + window.pageXOffset,
-				y: rect.top + window.pageYOffset
-			});
-			vm.$once('chosen', emoji => {
-				insertTextAtCursor(this.$refs.text, emoji);
-			});
-		},
-
-		post() {
-			this.posting = true;
-
-			let visibility = 'public';
-			let localOnly = false;
-
-			const m = this.$store.state.settings.defaultNoteVisibility.match(/^local-(.+)/);
-			if (m) {
-				visibility = m[1];
-				localOnly = true;
-			} else {
-				visibility = this.$store.state.settings.defaultNoteVisibility;
-			}
-
-			this.$root.api('notes/create', {
-				text: this.text == '' ? undefined : this.text,
-				fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
-				visibility,
-				localOnly,
-			}).then(data => {
-				this.clear();
-			}).catch(err => {
-				this.$root.dialog({
-					type: 'error',
-					text: this.$t('something-happened')
-				});
-			}).then(() => {
-				this.posting = false;
-				this.$nextTick(() => {
-					this.$refs.text.focus();
-				});
-			});
-		},
-
-		clear() {
-			this.text = '';
-			this.files = [];
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.lhcuptdmcdkfwmipgazeawoiuxpzaclc-body
-	> .textarea
-		> .emoji
-			position absolute
-			top 0
-			right 0
-			padding 10px
-			font-size 18px
-			color var(--text)
-			opacity 0.5
-
-			&:hover
-				color var(--textHighlighted)
-				opacity 1
-
-			&:active
-				color var(--primary)
-				opacity 1
-
-		> textarea
-			display block
-			width 100%
-			max-width 100%
-			min-width 100%
-			padding 16px
-			color var(--desktopPostFormTextareaFg)
-			outline none
-			background var(--desktopPostFormTextareaBg)
-			border none
-			border-bottom solid 1px var(--faceDivider)
-			padding-right 30px
-
-			&:focus
-				& + .emoji
-					opacity 0.7
-
-	> input[type=file]
-		display none
-
-	> footer
-		display flex
-		padding 8px
-
-		> button:not(.post)
-			color var(--text)
-
-			&:hover
-				color var(--textHighlighted)
-
-		> .post
-			display block
-			margin 0 0 0 auto
-			padding 0 10px
-			height 28px
-			color var(--primaryForeground)
-			background var(--primary) !important
-			outline none
-			border none
-			border-radius 4px
-			transition background 0.1s ease
-			cursor pointer
-
-			&:hover
-				background var(--primaryLighten10) !important
-
-			&:active
-				background var(--primaryDarken10) !important
-				transition background 0s ease
-
-</style>
diff --git a/src/client/app/common/views/widgets/posts-monitor.vue b/src/client/app/common/views/widgets/posts-monitor.vue
deleted file mode 100644
index 64c3b51540c9b7805de8b33abaa918b6f9b3d763..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/posts-monitor.vue
+++ /dev/null
@@ -1,203 +0,0 @@
-<template>
-<div class="mkw-posts-monitor">
-	<ui-container :show-header="props.design == 0" :naked="props.design == 2">
-		<template #header><fa icon="chart-line"/>{{ $t('title') }}</template>
-		<template #func><button @click="toggle" :title="$t('toggle')"><fa icon="sort"/></button></template>
-
-		<div class="qpdmibaztplkylerhdbllwcokyrfxeyj" :class="{ dual: props.view == 0 }">
-			<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" v-show="props.view != 2">
-				<defs>
-					<linearGradient :id="localGradientId" x1="0" x2="0" y1="1" y2="0">
-						<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
-						<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
-					</linearGradient>
-					<mask :id="localMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
-						<polygon
-							:points="localPolygonPoints"
-							fill="#fff"
-							fill-opacity="0.5"/>
-						<polyline
-							:points="localPolylinePoints"
-							fill="none"
-							stroke="#fff"
-							stroke-width="1"/>
-						<circle
-							:cx="localHeadX"
-							:cy="localHeadY"
-							r="1.5"
-							fill="#fff"/>
-					</mask>
-				</defs>
-				<rect
-					x="-2" y="-2"
-					:width="viewBoxX + 4" :height="viewBoxY + 4"
-					:style="`stroke: none; fill: url(#${ localGradientId }); mask: url(#${ localMaskId })`"/>
-				<text x="1" y="5">Local</text>
-			</svg>
-			<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" v-show="props.view != 1">
-				<defs>
-					<linearGradient :id="fediGradientId" x1="0" x2="0" y1="1" y2="0">
-						<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
-						<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
-					</linearGradient>
-					<mask :id="fediMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
-						<polygon
-							:points="fediPolygonPoints"
-							fill="#fff"
-							fill-opacity="0.5"/>
-						<polyline
-							:points="fediPolylinePoints"
-							fill="none"
-							stroke="#fff"
-							stroke-width="1"/>
-						<circle
-							:cx="fediHeadX"
-							:cy="fediHeadY"
-							r="1.5"
-							fill="#fff"/>
-					</mask>
-				</defs>
-				<rect
-					x="-2" y="-2"
-					:width="viewBoxX + 4" :height="viewBoxY + 4"
-					:style="`stroke: none; fill: url(#${ fediGradientId }); mask: url(#${ fediMaskId })`"/>
-				<text x="1" y="5">Fedi</text>
-			</svg>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-import { v4 as uuid } from 'uuid';
-
-export default define({
-	name: 'posts-monitor',
-	props: () => ({
-		design: 0,
-		view: 0
-	})
-}).extend({
-	i18n: i18n('common/views/widgets/posts-monitor.vue'),
-
-	data() {
-		return {
-			connection: null,
-			viewBoxY: 30,
-			stats: [],
-			fediGradientId: uuid(),
-			fediMaskId: uuid(),
-			localGradientId: uuid(),
-			localMaskId: uuid(),
-			fediPolylinePoints: '',
-			localPolylinePoints: '',
-			fediPolygonPoints: '',
-			localPolygonPoints: '',
-			fediHeadX: null,
-			fediHeadY: null,
-			localHeadX: null,
-			localHeadY: null
-		};
-	},
-	computed: {
-		viewBoxX(): number {
-			return this.props.view == 0 ? 50 : 100;
-		}
-	},
-	watch: {
-		viewBoxX() {
-			this.draw();
-		}
-	},
-	mounted() {
-		this.connection = this.$root.stream.useSharedConnection('notesStats');
-
-		this.connection.on('stats', this.onStats);
-		this.connection.on('statsLog', this.onStatsLog);
-		this.connection.send('requestLog',{
-			id: Math.random().toString().substr(2, 8)
-		});
-	},
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-	methods: {
-		toggle() {
-			if (this.props.view == 2) {
-				this.props.view = 0;
-			} else {
-				this.props.view++;
-			}
-			this.save();
-		},
-		func() {
-			if (this.props.design == 2) {
-				this.props.design = 0;
-			} else {
-				this.props.design++;
-			}
-			this.save();
-		},
-		draw() {
-			const stats = this.props.view == 0 ? this.stats.slice(-50) : this.stats;
-			const fediPeak = Math.max.apply(null, stats.map(x => x.all)) || 1;
-			const localPeak = Math.max.apply(null, stats.map(x => x.local)) || 1;
-
-			const fediPolylinePoints = stats.map((s, i) => [this.viewBoxX - ((stats.length - 1) - i), (1 - (s.all / fediPeak)) * this.viewBoxY]);
-			const localPolylinePoints = stats.map((s, i) => [this.viewBoxX - ((stats.length - 1) - i), (1 - (s.local / localPeak)) * this.viewBoxY]);
-			this.fediPolylinePoints = fediPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
-			this.localPolylinePoints = localPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
-
-			this.fediPolygonPoints = `${this.viewBoxX - (stats.length - 1)},${ this.viewBoxY } ${ this.fediPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
-			this.localPolygonPoints = `${this.viewBoxX - (stats.length - 1)},${ this.viewBoxY } ${ this.localPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
-
-			this.fediHeadX = fediPolylinePoints[fediPolylinePoints.length - 1][0];
-			this.fediHeadY = fediPolylinePoints[fediPolylinePoints.length - 1][1];
-			this.localHeadX = localPolylinePoints[localPolylinePoints.length - 1][0];
-			this.localHeadY = localPolylinePoints[localPolylinePoints.length - 1][1];
-		},
-		onStats(stats) {
-			this.stats.push(stats);
-			if (this.stats.length > 100) this.stats.shift();
-			this.draw();
-		},
-		onStatsLog(statsLog) {
-			for (const stats of statsLog) this.onStats(stats);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.qpdmibaztplkylerhdbllwcokyrfxeyj
-	&.dual
-		> svg
-			width 50%
-			float left
-
-			&:first-child
-				padding-right 5px
-
-			&:last-child
-				padding-left 5px
-
-	> svg
-		display block
-		padding 10px
-		width 100%
-
-		> text
-			font-size 5px
-			fill var(--chartCaption)
-
-			> tspan
-				opacity 0.5
-
-	&:after
-		content ""
-		display block
-		clear both
-
-</style>
diff --git a/src/client/app/common/views/widgets/queue.vue b/src/client/app/common/views/widgets/queue.vue
deleted file mode 100644
index 6e49f1efb0373395c645941a7f3f3333c659c0ee..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/queue.vue
+++ /dev/null
@@ -1,173 +0,0 @@
-<template>
-<div>
-	<ui-container :show-header="!props.compact">
-		<template #header><fa :icon="faTasks"/>Queue</template>
-
-		<div class="mntrproz">
-			<div>
-				<b>In</b>
-				<span v-if="latestStats">{{ latestStats.inbox.activeSincePrevTick | number }} / {{ latestStats.inbox.delayed | number }}</span>
-				<div ref="in"></div>
-			</div>
-			<div>
-				<b>Out</b>
-				<span v-if="latestStats">{{ latestStats.deliver.activeSincePrevTick | number }} / {{ latestStats.deliver.delayed | number }}</span>
-				<div ref="out"></div>
-			</div>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../define-widget';
-import { faTasks } from '@fortawesome/free-solid-svg-icons';
-import ApexCharts from 'apexcharts';
-
-export default define({
-	name: 'queue',
-	props: () => ({
-		compact: false
-	})
-}).extend({
-	data() {
-		return {
-			stats: [],
-			inChart: null,
-			outChart: null,
-			faTasks
-		};
-	},
-
-	watch: {
-		stats(stats) {
-			this.inChart.updateSeries([{
-				type: 'area',
-				data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick }))
-			}, {
-				type: 'area',
-				data: stats.map((x, i) => ({ x: i, y: x.inbox.active }))
-			}, {
-				type: 'line',
-				data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting }))
-			}, {
-				type: 'line',
-				data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
-			}]);
-			this.outChart.updateSeries([{
-				type: 'area',
-				data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick }))
-			}, {
-				type: 'area',
-				data: stats.map((x, i) => ({ x: i, y: x.deliver.active }))
-			}, {
-				type: 'line',
-				data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting }))
-			}, {
-				type: 'line',
-				data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
-			}]);
-		}
-	},
-
-	computed: {
-		latestStats(): any {
-			return this.stats[this.stats.length - 1];
-		}
-	},
-
-	mounted() {
-		const chartOpts = {
-			chart: {
-				type: 'area',
-				height: 70,
-				animations: {
-					dynamicAnimation: {
-						enabled: false
-					}
-				},
-				sparkline: {
-					enabled: true,
-				}
-			},
-			dataLabels: {
-				enabled: false
-			},
-			tooltip: {
-				enabled: false
-			},
-			stroke: {
-				curve: 'straight',
-				width: 1
-			},
-			colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'],
-			series: [{ data: [] }, { data: [] }, { data: [] }, { data: [] }] as any,
-			yaxis: {
-				min: 0,
-			}
-		};
-
-		this.inChart = new ApexCharts(this.$refs.in, chartOpts);
-		this.outChart = new ApexCharts(this.$refs.out, chartOpts);
-
-		this.inChart.render();
-		this.outChart.render();
-
-		const connection = this.$root.stream.useSharedConnection('queueStats');
-		connection.on('stats', this.onStats);
-		connection.on('statsLog', this.onStatsLog);
-		connection.send('requestLog', {
-			id: Math.random().toString().substr(2, 8),
-			length: 50
-		});
-
-		this.$once('hook:beforeDestroy', () => {
-			connection.dispose();
-			this.inChart.destroy();
-			this.outChart.destroy();
-		});
-	},
-
-	methods: {
-		func() {
-			this.props.compact = !this.props.compact;
-			this.save();
-		},
-
-		onStats(stats) {
-			this.stats.push(stats);
-			if (this.stats.length > 50) this.stats.shift();
-		},
-
-		onStatsLog(statsLog) {
-			for (const stats of statsLog.reverse()) {
-				this.onStats(stats);
-			}
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mntrproz
-	display flex
-	padding 4px
-
-	> div
-		width 50%
-		padding 4px
-
-		> b
-			display block
-			font-size 12px
-			color var(--text)
-
-		> span
-			position absolute
-			top 4px
-			right 4px
-			opacity 0.7
-			font-size 12px
-			color var(--text)
-
-</style>
diff --git a/src/client/app/common/views/widgets/rss.vue b/src/client/app/common/views/widgets/rss.vue
deleted file mode 100644
index c1a66bfebb4482ca7570224ecaeba739c9f81c32..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/rss.vue
+++ /dev/null
@@ -1,116 +0,0 @@
-<template>
-<div class="mkw-rss">
-	<ui-container :show-header="!props.compact">
-		<template #header><fa icon="rss-square"/>RSS</template>
-		<template #func><button title="設定" @click="setting"><fa icon="cog"/></button></template>
-
-		<div class="mkw-rss--body" :data-mobile="platform == 'mobile'">
-			<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
-			<div class="feed" v-else>
-				<a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
-			</div>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-
-export default define({
-	name: 'rss',
-	props: () => ({
-		compact: false,
-		url: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews'
-	})
-}).extend({
-	i18n: i18n(),
-	data() {
-		return {
-			items: [],
-			fetching: true,
-			clock: null
-		};
-	},
-	mounted() {
-		this.fetch();
-		this.clock = setInterval(this.fetch, 60000);
-	},
-	beforeDestroy() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		func() {
-			this.props.compact = !this.props.compact;
-			this.save();
-		},
-		fetch() {
-			fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, {
-			}).then(res => {
-				res.json().then(feed => {
-					this.items = feed.items;
-					this.fetching = false;
-				});
-			});
-		},
-		setting() {
-			this.$root.dialog({
-				title: 'URL',
-				input: {
-					type: 'url',
-					default: this.props.url
-				}
-			}).then(({ canceled, result: url }) => {
-				if (canceled) return;
-				this.props.url = url;
-				this.save();
-				this.fetch();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mkw-rss
-	.mkw-rss--body
-		.feed
-			padding 12px 16px
-			font-size 0.9em
-
-			> a
-				display block
-				padding 4px 0
-				color var(--text)
-				border-bottom dashed var(--lineWidth) var(--faceDivider)
-				white-space nowrap
-				text-overflow ellipsis
-				overflow hidden
-
-				&:last-child
-					border-bottom none
-
-		.fetching
-			margin 0
-			padding 16px
-			text-align center
-			color var(--text)
-
-			> [data-icon]
-				margin-right 4px
-
-		&[data-mobile]
-			background var(--face)
-
-			.feed
-				padding 0
-
-				> a
-					padding 8px 16px
-					border-bottom none
-
-					&:nth-child(even)
-						background rgba(#000, 0.05)
-
-</style>
diff --git a/src/client/app/common/views/widgets/server.cpu-memory.vue b/src/client/app/common/views/widgets/server.cpu-memory.vue
deleted file mode 100644
index 799773a70caf0cdef369ec2e21a68d9b3055b22e..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/server.cpu-memory.vue
+++ /dev/null
@@ -1,156 +0,0 @@
-<template>
-<div class="cpu-memory">
-	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
-		<defs>
-			<linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0">
-				<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
-				<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
-			</linearGradient>
-			<mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
-				<polygon
-					:points="cpuPolygonPoints"
-					fill="#fff"
-					fill-opacity="0.5"/>
-				<polyline
-					:points="cpuPolylinePoints"
-					fill="none"
-					stroke="#fff"
-					stroke-width="1"/>
-				<circle
-					:cx="cpuHeadX"
-					:cy="cpuHeadY"
-					r="1.5"
-					fill="#fff"/>
-			</mask>
-		</defs>
-		<rect
-			x="-2" y="-2"
-			:width="viewBoxX + 4" :height="viewBoxY + 4"
-			:style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"/>
-		<text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text>
-	</svg>
-	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
-		<defs>
-			<linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0">
-				<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
-				<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
-			</linearGradient>
-			<mask :id="memMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
-				<polygon
-					:points="memPolygonPoints"
-					fill="#fff"
-					fill-opacity="0.5"/>
-				<polyline
-					:points="memPolylinePoints"
-					fill="none"
-					stroke="#fff"
-					stroke-width="1"/>
-				<circle
-					:cx="memHeadX"
-					:cy="memHeadY"
-					r="1.5"
-					fill="#fff"/>
-			</mask>
-		</defs>
-		<rect
-			x="-2" y="-2"
-			:width="viewBoxX + 4" :height="viewBoxY + 4"
-			:style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"/>
-		<text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text>
-	</svg>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { v4 as uuid } from 'uuid';
-
-export default Vue.extend({
-	props: ['connection'],
-	data() {
-		return {
-			viewBoxX: 50,
-			viewBoxY: 30,
-			stats: [],
-			cpuGradientId: uuid(),
-			cpuMaskId: uuid(),
-			memGradientId: uuid(),
-			memMaskId: uuid(),
-			cpuPolylinePoints: '',
-			memPolylinePoints: '',
-			cpuPolygonPoints: '',
-			memPolygonPoints: '',
-			cpuHeadX: null,
-			cpuHeadY: null,
-			memHeadX: null,
-			memHeadY: null,
-			cpuP: '',
-			memP: ''
-		};
-	},
-	mounted() {
-		this.connection.on('stats', this.onStats);
-		this.connection.on('statsLog', this.onStatsLog);
-		this.connection.send('requestLog', {
-			id: Math.random().toString().substr(2, 8)
-		});
-	},
-	beforeDestroy() {
-		this.connection.off('stats', this.onStats);
-		this.connection.off('statsLog', this.onStatsLog);
-	},
-	methods: {
-		onStats(stats) {
-			this.stats.push(stats);
-			if (this.stats.length > 50) this.stats.shift();
-
-			const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu_usage) * this.viewBoxY]);
-			const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.used / s.mem.total)) * this.viewBoxY]);
-			this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
-			this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
-
-			this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.cpuPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
-			this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.memPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
-
-			this.cpuHeadX = cpuPolylinePoints[cpuPolylinePoints.length - 1][0];
-			this.cpuHeadY = cpuPolylinePoints[cpuPolylinePoints.length - 1][1];
-			this.memHeadX = memPolylinePoints[memPolylinePoints.length - 1][0];
-			this.memHeadY = memPolylinePoints[memPolylinePoints.length - 1][1];
-
-			this.cpuP = (stats.cpu_usage * 100).toFixed(0);
-			this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0);
-		},
-		onStatsLog(statsLog) {
-			for (const stats of statsLog.reverse()) this.onStats(stats);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.cpu-memory
-	> svg
-		display block
-		padding 10px
-		width 50%
-		float left
-
-		&:first-child
-			padding-right 5px
-
-		&:last-child
-			padding-left 5px
-
-		> text
-			font-size 5px
-			fill var(--chartCaption)
-
-			> tspan
-				opacity 0.5
-
-	&:after
-		content ""
-		display block
-		clear both
-
-</style>
diff --git a/src/client/app/common/views/widgets/server.cpu.vue b/src/client/app/common/views/widgets/server.cpu.vue
deleted file mode 100644
index c08971e11c216cf94def0f5bb7a2bef5560317f8..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/server.cpu.vue
+++ /dev/null
@@ -1,68 +0,0 @@
-<template>
-<div class="cpu">
-	<x-pie class="pie" :value="usage"/>
-	<div>
-		<p><fa icon="microchip"/>CPU</p>
-		<p>{{ meta.cpu.cores }} Logical cores</p>
-		<p>{{ meta.cpu.model }}</p>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import XPie from './server.pie.vue';
-
-export default Vue.extend({
-	components: {
-		XPie
-	},
-	props: ['connection', 'meta'],
-	data() {
-		return {
-			usage: 0
-		};
-	},
-	mounted() {
-		this.connection.on('stats', this.onStats);
-	},
-	beforeDestroy() {
-		this.connection.off('stats', this.onStats);
-	},
-	methods: {
-		onStats(stats) {
-			this.usage = stats.cpu_usage;
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.cpu
-	> .pie
-		padding 10px
-		height 100px
-		float left
-
-	> div
-		float left
-		width calc(100% - 100px)
-		padding 10px 10px 10px 0
-
-		> p
-			margin 0
-			font-size 12px
-			color var(--chartCaption)
-
-			&:first-child
-				font-weight bold
-
-				> [data-icon]
-					margin-right 4px
-
-	&:after
-		content ""
-		display block
-		clear both
-
-</style>
diff --git a/src/client/app/common/views/widgets/server.disk.vue b/src/client/app/common/views/widgets/server.disk.vue
deleted file mode 100644
index 039c4f5c2935ec1c30fd3dee8f91f731a24e1c1b..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/server.disk.vue
+++ /dev/null
@@ -1,76 +0,0 @@
-<template>
-<div class="disk">
-	<x-pie class="pie" :value="usage"/>
-	<div>
-		<p><fa :icon="['far', 'hdd']"/>Storage</p>
-		<p>Total: {{ total | bytes(1) }}</p>
-		<p>Free: {{ available | bytes(1) }}</p>
-		<p>Used: {{ used | bytes(1) }}</p>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import XPie from './server.pie.vue';
-
-export default Vue.extend({
-	components: {
-		XPie
-	},
-	props: ['connection'],
-	data() {
-		return {
-			usage: 0,
-			total: 0,
-			used: 0,
-			available: 0
-		};
-	},
-	mounted() {
-		this.connection.on('stats', this.onStats);
-	},
-	beforeDestroy() {
-		this.connection.off('stats', this.onStats);
-	},
-	methods: {
-		onStats(stats) {
-			stats.disk.used = stats.disk.total - stats.disk.free;
-			this.usage = stats.disk.used / stats.disk.total;
-			this.total = stats.disk.total;
-			this.used = stats.disk.used;
-			this.available = stats.disk.available;
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.disk
-	> .pie
-		padding 10px
-		height 100px
-		float left
-
-	> div
-		float left
-		width calc(100% - 100px)
-		padding 10px 10px 10px 0
-
-		> p
-			margin 0
-			font-size 12px
-			color var(--chartCaption)
-
-			&:first-child
-				font-weight bold
-
-				> [data-icon]
-					margin-right 4px
-
-	&:after
-		content ""
-		display block
-		clear both
-
-</style>
diff --git a/src/client/app/common/views/widgets/server.info.vue b/src/client/app/common/views/widgets/server.info.vue
deleted file mode 100644
index c6e0d68b113312c92cd9c6c26525b589efc1b6a5..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/server.info.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<template>
-<div class="info">
-	<p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p>
-	<p>Machine: {{ meta.machine }}</p>
-	<p>Node: {{ meta.node }}</p>
-	<p>PSQL: {{ meta.psql }}</p>
-	<p>Redis: {{ meta.redis }}</p>
-	<p>Version: {{ meta.version }} </p>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: ['meta']
-});
-</script>
-
-<style lang="stylus" scoped>
-.info
-	padding 10px 14px
-
-	> p
-		margin 0
-		font-size 12px
-		color var(--text)
-</style>
diff --git a/src/client/app/common/views/widgets/server.memory.vue b/src/client/app/common/views/widgets/server.memory.vue
deleted file mode 100644
index c3b2f3a1011e67d3600c60095ee6d31a16f89a5d..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/server.memory.vue
+++ /dev/null
@@ -1,76 +0,0 @@
-<template>
-<div class="memory">
-	<x-pie class="pie" :value="usage"/>
-	<div>
-		<p><fa icon="memory"/>Memory</p>
-		<p>Total: {{ total | bytes(1) }}</p>
-		<p>Used: {{ used | bytes(1) }}</p>
-		<p>Free: {{ free | bytes(1) }}</p>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import XPie from './server.pie.vue';
-
-export default Vue.extend({
-	components: {
-		XPie
-	},
-	props: ['connection'],
-	data() {
-		return {
-			usage: 0,
-			total: 0,
-			used: 0,
-			free: 0
-		};
-	},
-	mounted() {
-		this.connection.on('stats', this.onStats);
-	},
-	beforeDestroy() {
-		this.connection.off('stats', this.onStats);
-	},
-	methods: {
-		onStats(stats) {
-			stats.mem.free = stats.mem.total - stats.mem.used;
-			this.usage = stats.mem.used / stats.mem.total;
-			this.total = stats.mem.total;
-			this.used = stats.mem.used;
-			this.free = stats.mem.free;
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.memory
-	> .pie
-		padding 10px
-		height 100px
-		float left
-
-	> div
-		float left
-		width calc(100% - 100px)
-		padding 10px 10px 10px 0
-
-		> p
-			margin 0
-			font-size 12px
-			color var(--chartCaption)
-
-			&:first-child
-				font-weight bold
-
-				> [data-icon]
-					margin-right 4px
-
-	&:after
-		content ""
-		display block
-		clear both
-
-</style>
diff --git a/src/client/app/common/views/widgets/server.pie.vue b/src/client/app/common/views/widgets/server.pie.vue
deleted file mode 100644
index ce342fd41ba993929b727d54f8f6955a9aa8781d..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/server.pie.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<template>
-<svg viewBox="0 0 1 1" preserveAspectRatio="none">
-	<circle
-		:r="r"
-		cx="50%" cy="50%"
-		fill="none"
-		stroke-width="0.1"
-		stroke="rgba(0, 0, 0, 0.05)"/>
-	<circle
-		:r="r"
-		cx="50%" cy="50%"
-		:stroke-dasharray="Math.PI * (r * 2)"
-		:stroke-dashoffset="strokeDashoffset"
-		fill="none"
-		stroke-width="0.1"
-		:stroke="color"/>
-	<text x="50%" y="50%" dy="0.05" text-anchor="middle">{{ (value * 100).toFixed(0) }}%</text>
-</svg>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: {
-		value: {
-			type: Number,
-			required: true
-		}
-	},
-	data() {
-		return {
-			r: 0.4
-		};
-	},
-	computed: {
-		color(): string {
-			return `hsl(${180 - (this.value * 180)}, 80%, 70%)`;
-		},
-		strokeDashoffset(): number {
-			return (1 - this.value) * (Math.PI * (this.r * 2));
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-svg
-	display block
-	height 100%
-
-	> circle
-		transform-origin center
-		transform rotate(-90deg)
-		transition stroke-dashoffset 0.5s ease
-
-	> text
-		font-size 0.15px
-		fill var(--chartCaption)
-
-</style>
diff --git a/src/client/app/common/views/widgets/server.uptimes.vue b/src/client/app/common/views/widgets/server.uptimes.vue
deleted file mode 100644
index 0da5c4ec500487e08928384d1d25d3b3b6b8f8b4..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/server.uptimes.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<template>
-<div class="uptimes">
-	<p>Uptimes</p>
-	<p>Process: {{ process }}</p>
-	<p>OS: {{ os }}</p>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import formatUptime from '../../scripts/format-uptime';
-
-export default Vue.extend({
-	props: ['connection'],
-	data() {
-		return {
-			process: 0,
-			os: 0
-		};
-	},
-	mounted() {
-		this.connection.on('stats', this.onStats);
-	},
-	beforeDestroy() {
-		this.connection.off('stats', this.onStats);
-	},
-	methods: {
-		onStats(stats) {
-			this.process = formatUptime(stats.process_uptime);
-			this.os = formatUptime(stats.os_uptime);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.uptimes
-	padding 10px 14px
-
-	> p
-		margin 0
-		font-size 12px
-		color var(--text)
-
-		&:first-child
-			font-weight bold
-</style>
diff --git a/src/client/app/common/views/widgets/server.vue b/src/client/app/common/views/widgets/server.vue
deleted file mode 100644
index 90a0f0171b42ce52beac7bba7257f113e2a9c533..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/server.vue
+++ /dev/null
@@ -1,96 +0,0 @@
-<template>
-<div class="mkw-server">
-	<ui-container :show-header="props.design == 0" :naked="props.design == 2">
-		<template #header><fa icon="server"/>{{ $t('title') }}</template>
-		<template #func><button @click="toggle" :title="$t('toggle')"><fa icon="sort"/></button></template>
-
-		<p :class="$style.fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
-		<template v-if="!fetching">
-			<x-cpu-memory v-show="props.view == 0" :connection="connection"/>
-			<x-cpu v-show="props.view == 1" :connection="connection" :meta="meta"/>
-			<x-memory v-show="props.view == 2" :connection="connection"/>
-			<x-disk v-show="props.view == 3" :connection="connection"/>
-			<x-uptimes v-show="props.view == 4" :connection="connection"/>
-			<x-info v-show="props.view == 5" :connection="connection" :meta="meta"/>
-		</template>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-import XCpuMemory from './server.cpu-memory.vue';
-import XCpu from './server.cpu.vue';
-import XMemory from './server.memory.vue';
-import XDisk from './server.disk.vue';
-import XUptimes from './server.uptimes.vue';
-import XInfo from './server.info.vue';
-
-export default define({
-	name: 'server',
-	props: () => ({
-		design: 0,
-		view: 0
-	})
-}).extend({
-	i18n: i18n('common/views/widgets/server.vue'),
-
-	components: {
-		XCpuMemory,
-		XCpu,
-		XMemory,
-		XDisk,
-		XUptimes,
-		XInfo
-	},
-	data() {
-		return {
-			fetching: true,
-			meta: null,
-			connection: null
-		};
-	},
-	mounted() {
-		this.$root.getMeta().then(meta => {
-			this.meta = meta;
-			this.fetching = false;
-		});
-
-		this.connection = this.$root.stream.useSharedConnection('serverStats');
-	},
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-	methods: {
-		toggle() {
-			if (this.props.view == 5) {
-				this.props.view = 0;
-			} else {
-				this.props.view++;
-			}
-			this.save();
-		},
-		func() {
-			if (this.props.design == 2) {
-				this.props.design = 0;
-			} else {
-				this.props.design++;
-			}
-			this.save();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" module>
-.fetching
-	margin 0
-	padding 16px
-	text-align center
-	color var(--text)
-
-	> [data-icon]
-		margin-right 4px
-
-</style>
diff --git a/src/client/app/common/views/widgets/slideshow.vue b/src/client/app/common/views/widgets/slideshow.vue
deleted file mode 100644
index 23ccb9da6b221b7a58f45d0be477229287097a0f..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/slideshow.vue
+++ /dev/null
@@ -1,165 +0,0 @@
-<template>
-<div class="mkw-slideshow" :data-mobile="platform == 'mobile'">
-	<div @click="choose">
-		<p v-if="props.folder === undefined">
-			<template v-if="isCustomizeMode">{{ $t('folder-customize-mode') }}</template>
-			<template v-else>{{ $t('folder') }}</template>
-		</p>
-		<p v-if="props.folder !== undefined && images.length == 0 && !fetching">{{ $t('no-image') }}</p>
-		<div ref="slideA" class="slide a"></div>
-		<div ref="slideB" class="slide b"></div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import anime from 'animejs';
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-
-export default define({
-	name: 'slideshow',
-	props: () => ({
-		folder: undefined,
-		size: 0
-	})
-}).extend({
-	i18n: i18n('common/views/widgets/slideshow.vue'),
-
-	data() {
-		return {
-			images: [],
-			fetching: true,
-			clock: null
-		};
-	},
-	mounted() {
-		this.$nextTick(() => {
-			this.applySize();
-		});
-
-		if (this.props.folder !== undefined) {
-			this.fetch();
-		}
-
-		this.clock = setInterval(this.change, 10000);
-	},
-	beforeDestroy() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		func() {
-			this.resize();
-		},
-		applySize() {
-			let h;
-
-			if (this.props.size == 1) {
-				h = 250;
-			} else {
-				h = 170;
-			}
-
-			this.$el.style.height = `${h}px`;
-		},
-		resize() {
-			if (this.props.size == 1) {
-				this.props.size = 0;
-			} else {
-				this.props.size++;
-			}
-			this.save();
-
-			this.applySize();
-		},
-		change() {
-			if (this.images.length == 0) return;
-
-			const index = Math.floor(Math.random() * this.images.length);
-			const img = `url(${ this.images[index].url })`;
-
-			(this.$refs.slideB as any).style.backgroundImage = img;
-
-			anime({
-				targets: this.$refs.slideB,
-				opacity: 1,
-				duration: 1000,
-				easing: 'linear',
-				complete: () => {
-					// 既にこのウィジェットがunmountされていたら要素がない
-					if ((this.$refs.slideA as any) == null) return;
-
-					(this.$refs.slideA as any).style.backgroundImage = img;
-					anime({
-						targets: this.$refs.slideB,
-						opacity: 0,
-						duration: 0
-					});
-				}
-			});
-		},
-		fetch() {
-			this.fetching = true;
-
-			this.$root.api('drive/files', {
-				folderId: this.props.folder,
-				type: 'image/*',
-				limit: 100
-			}).then(images => {
-				this.images = images;
-				this.fetching = false;
-				(this.$refs.slideA as any).style.backgroundImage = '';
-				(this.$refs.slideB as any).style.backgroundImage = '';
-				this.change();
-			});
-		},
-		choose() {
-			this.$chooseDriveFolder().then(folder => {
-				this.props.folder = folder ? folder.id : null;
-				this.save();
-				this.fetch();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mkw-slideshow
-	overflow hidden
-	background #fff
-	border solid 1px rgba(#000, 0.075)
-	border-radius 6px
-
-	&[data-mobile]
-		border none
-		border-radius 8px
-		box-shadow 0 0 0 1px rgba(#000, 0.2)
-
-	> div
-		width 100%
-		height 100%
-		cursor pointer
-
-		> p
-			display block
-			margin 1em
-			text-align center
-			color #888
-
-		> *
-			pointer-events none
-
-		> .slide
-			position absolute
-			top 0
-			left 0
-			width 100%
-			height 100%
-			background-size cover
-			background-position center
-
-			&.b
-				opacity 0
-
-</style>
diff --git a/src/client/app/common/views/widgets/tips.vue b/src/client/app/common/views/widgets/tips.vue
deleted file mode 100644
index 9e047ef47c4c87d29d5cb439301b7ff15668c692..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/tips.vue
+++ /dev/null
@@ -1,109 +0,0 @@
-<template>
-<div class="mkw-tips">
-	<p ref="tip"><fa :icon="['far', 'lightbulb']"/><span v-html="tip"></span></p>
-</div>
-</template>
-
-<script lang="ts">
-import anime from 'animejs';
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-
-export default define({
-	name: 'tips'
-}).extend({
-	i18n: i18n('common/views/widgets/tips.vue'),
-
-	data() {
-		return {
-			tips: [],
-			tip: null,
-			clock: null
-		};
-	},
-	created() {
-		this.tips =  [
-			this.$t('tips-line1'),
-			this.$t('tips-line2'),
-			this.$t('tips-line3'),
-			this.$t('tips-line4'),
-			this.$t('tips-line5'),
-			this.$t('tips-line6'),
-			this.$t('tips-line7'),
-			this.$t('tips-line8'),
-			this.$t('tips-line9'),
-			this.$t('tips-line10'),
-			this.$t('tips-line11'),
-			this.$t('tips-line13'),
-			this.$t('tips-line14'),
-			this.$t('tips-line17'),
-			this.$t('tips-line19'),
-			this.$t('tips-line20'),
-			this.$t('tips-line21'),
-			this.$t('tips-line23'),
-			this.$t('tips-line24'),
-			this.$t('tips-line25')
-		];
-	},
-	mounted() {
-		this.$nextTick(() => {
-			this.set();
-		});
-
-		this.clock = setInterval(this.change, 20000);
-	},
-	beforeDestroy() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		set() {
-			this.tip = this.tips[Math.floor(Math.random() * this.tips.length)];
-		},
-		change() {
-			anime({
-				targets: this.$refs.tip,
-				opacity: 0,
-				duration: 500,
-				easing: 'linear',
-				complete: this.set
-			});
-
-			setTimeout(() => {
-				anime({
-					targets: this.$refs.tip,
-					opacity: 1,
-					duration: 500,
-					easing: 'linear'
-				});
-			}, 500);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mkw-tips
-	overflow visible !important
-	opacity 0.8
-
-	> p
-		display block
-		margin 0
-		padding 0 12px
-		text-align center
-		font-size 0.7em
-		color var(--text)
-
-		> [data-icon]
-			margin-right 4px
-
-		kbd
-			display inline
-			padding 0 6px
-			margin 0 2px
-			font-size 1em
-			font-family inherit
-			border solid 1px var(--text)
-			border-radius 2px
-
-</style>
diff --git a/src/client/app/common/views/widgets/version.vue b/src/client/app/common/views/widgets/version.vue
deleted file mode 100644
index e8f6c08f343ffca5cc1153c629bf84189c2f4e1e..0000000000000000000000000000000000000000
--- a/src/client/app/common/views/widgets/version.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-<template>
-<p>ver {{ version }} ({{ codename }})</p>
-</template>
-
-<script lang="ts">
-import { version, codename } from '../../../config';
-import define from '../../../common/define-widget';
-export default define({
-	name: 'version'
-}).extend({
-	data() {
-		return {
-			version,
-			codename
-		};
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-p
-	display block
-	margin 0
-	padding 0 12px
-	text-align center
-	font-size 0.7em
-	color var(--text)
-	opacity 0.8
-
-</style>
diff --git a/src/client/app/desktop/api/update-avatar.ts b/src/client/app/desktop/api/update-avatar.ts
deleted file mode 100644
index 6b88b51ef1a37630dc87db2d97a3b83d44cdd1f0..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/api/update-avatar.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import { apiUrl, locale } from '../../config';
-import ProgressDialog from '../views/components/progress-dialog.vue';
-
-export default ($root: any) => {
-
-	const cropImage = file => new Promise(async (resolve, reject) => {
-		const CropWindow = await import('../views/components/crop-window.vue').then(x => x.default);
-		const w = $root.new(CropWindow, {
-			image: file,
-			title: locale['desktop']['avatar-crop-title'],
-			aspectRatio: 1 / 1
-		});
-
-		w.$once('cropped', blob => {
-			const data = new FormData();
-			data.append('i', $root.$store.state.i.token);
-			data.append('file', blob, file.name + '.cropped.png');
-
-			$root.api('drive/folders/find', {
-				name: locale['desktop']['avatar']
-			}).then(avatarFolder => {
-				if (avatarFolder.length === 0) {
-					$root.api('drive/folders/create', {
-						name: locale['desktop']['avatar']
-					}).then(iconFolder => {
-						resolve(upload(data, iconFolder));
-					});
-				} else {
-					resolve(upload(data, avatarFolder[0]));
-				}
-			});
-		});
-
-		w.$once('skipped', () => {
-			resolve(file);
-		});
-
-		w.$once('cancelled', reject);
-
-		document.body.appendChild(w.$el);
-	});
-
-	const upload = (data, folder) => new Promise((resolve, reject) => {
-		const dialog = $root.new(ProgressDialog, {
-			title: locale['desktop']['uploading-avatar']
-		});
-		document.body.appendChild(dialog.$el);
-
-		if (folder) data.append('folderId', folder.id);
-
-		const xhr = new XMLHttpRequest();
-		xhr.open('POST', apiUrl + '/drive/files/create', true);
-		xhr.onload = e => {
-			const file = JSON.parse((e.target as any).response);
-			(dialog as any).close();
-			resolve(file);
-		};
-		xhr.onerror = reject;
-
-		xhr.upload.onprogress = e => {
-			if (e.lengthComputable) (dialog as any).update(e.loaded, e.total);
-		};
-
-		xhr.send(data);
-	});
-
-	const setAvatar = file => {
-		return $root.api('i/update', {
-			avatarId: file.id
-		}).then(i => {
-			$root.$store.commit('updateIKeyValue', {
-				key: 'avatarId',
-				value: i.avatarId
-			});
-			$root.$store.commit('updateIKeyValue', {
-				key: 'avatarUrl',
-				value: i.avatarUrl
-			});
-
-			$root.dialog({
-				title: locale['desktop']['avatar-updated'],
-				text: null
-			});
-
-			return i;
-		}).catch(err => {
-			switch (err.id) {
-				case 'f419f9f8-2f4d-46b1-9fb4-49d3a2fd7191':
-					$root.dialog({
-						type: 'error',
-						title: locale['desktop']['unable-to-process'],
-						text: locale['desktop']['invalid-filetype']
-					});
-					break;
-				default:
-					$root.dialog({
-						type: 'error',
-						text: locale['desktop']['unable-to-process']
-					});
-			}
-		});
-	};
-
-	return (file = null) => {
-		const selectedFile = file
-			? Promise.resolve(file)
-			: $root.$chooseDriveFile({
-				multiple: false,
-				type: 'image/*',
-				title: locale['desktop']['choose-avatar']
-			});
-
-		return selectedFile
-			.then(cropImage)
-			.then(setAvatar)
-			.catch(err => err && console.warn(err));
-	};
-};
diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts
deleted file mode 100644
index 09632b19418fcbaa3eafa04a612f053a3023de08..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/api/update-banner.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import { apiUrl, locale } from '../../config';
-import ProgressDialog from '../views/components/progress-dialog.vue';
-
-export default ($root: any) => {
-
-	const cropImage = file => new Promise(async (resolve, reject) => {
-		const CropWindow = await import('../views/components/crop-window.vue').then(x => x.default);
-		const w = $root.new(CropWindow, {
-			image: file,
-			title: locale['desktop']['banner-crop-title'],
-			aspectRatio: 16 / 9
-		});
-
-		w.$once('cropped', blob => {
-			const data = new FormData();
-			data.append('i', $root.$store.state.i.token);
-			data.append('file', blob, file.name + '.cropped.png');
-
-			$root.api('drive/folders/find', {
-				name: locale['desktop']['banner']
-			}).then(bannerFolder => {
-				if (bannerFolder.length === 0) {
-					$root.api('drive/folders/create', {
-						name: locale['desktop']['banner']
-					}).then(iconFolder => {
-						resolve(upload(data, iconFolder));
-					});
-				} else {
-					resolve(upload(data, bannerFolder[0]));
-				}
-			});
-		});
-
-		w.$once('skipped', () => {
-			resolve(file);
-		});
-
-		w.$once('cancelled', reject);
-
-		document.body.appendChild(w.$el);
-	});
-
-	const upload = (data, folder) => new Promise((resolve, reject) => {
-		const dialog = $root.new(ProgressDialog, {
-			title: locale['desktop']['uploading-banner']
-		});
-		document.body.appendChild(dialog.$el);
-
-		if (folder) data.append('folderId', folder.id);
-
-		const xhr = new XMLHttpRequest();
-		xhr.open('POST', apiUrl + '/drive/files/create', true);
-		xhr.onload = e => {
-			const file = JSON.parse((e.target as any).response);
-			(dialog as any).close();
-			resolve(file);
-		};
-		xhr.onerror = reject;
-
-		xhr.upload.onprogress = e => {
-			if (e.lengthComputable) (dialog as any).update(e.loaded, e.total);
-		};
-
-		xhr.send(data);
-	});
-
-	const setBanner = file => {
-		return $root.api('i/update', {
-			bannerId: file.id
-		}).then(i => {
-			$root.$store.commit('updateIKeyValue', {
-				key: 'bannerId',
-				value: i.bannerId
-			});
-			$root.$store.commit('updateIKeyValue', {
-				key: 'bannerUrl',
-				value: i.bannerUrl
-			});
-
-			$root.dialog({
-				title: locale['desktop']['banner-updated'],
-				text: null
-			});
-
-			return i;
-		}).catch(err => {
-			switch (err.id) {
-				case '75aedb19-2afd-4e6d-87fc-67941256fa60':
-					$root.dialog({
-						type: 'error',
-						title: locale['desktop']['unable-to-process'],
-						text: locale['desktop']['invalid-filetype']
-					});
-					break;
-				default:
-					$root.dialog({
-						type: 'error',
-						text: locale['desktop']['unable-to-process']
-					});
-			}
-		});
-	};
-
-	return (file = null) => {
-		const selectedFile = file
-			? Promise.resolve(file)
-			: $root.$chooseDriveFile({
-				multiple: false,
-				type: 'image/*',
-				title: locale['desktop']['choose-banner']
-			});
-
-		return selectedFile
-			.then(cropImage)
-			.then(setBanner)
-			.catch(err => err && console.warn(err));
-	};
-};
diff --git a/src/client/app/desktop/assets/grid.svg b/src/client/app/desktop/assets/grid.svg
deleted file mode 100644
index d1d72cd8cecde5c008a728c168ae67e3f73f4893..0000000000000000000000000000000000000000
Binary files a/src/client/app/desktop/assets/grid.svg and /dev/null differ
diff --git a/src/client/app/desktop/assets/header-icon.svg b/src/client/app/desktop/assets/header-icon.svg
deleted file mode 100644
index d677d2d16304709f93cc3d9be631cf686d724697..0000000000000000000000000000000000000000
Binary files a/src/client/app/desktop/assets/header-icon.svg and /dev/null differ
diff --git a/src/client/app/desktop/assets/index.jpg b/src/client/app/desktop/assets/index.jpg
deleted file mode 100644
index c054188159b19f4f96069d08af37576e97af19c6..0000000000000000000000000000000000000000
Binary files a/src/client/app/desktop/assets/index.jpg and /dev/null differ
diff --git a/src/client/app/desktop/assets/remove.png b/src/client/app/desktop/assets/remove.png
deleted file mode 100644
index c2e222a0fc7b983c77bc87fe36b1f90a848dfba8..0000000000000000000000000000000000000000
Binary files a/src/client/app/desktop/assets/remove.png and /dev/null differ
diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts
deleted file mode 100644
index 914e162c9a39888623dea43395c60b23dd3c6e1c..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/script.ts
+++ /dev/null
@@ -1,267 +0,0 @@
-/**
- * Desktop Client
- */
-
-import Vue from 'vue';
-import VueRouter from 'vue-router';
-
-// Style
-import './style.styl';
-
-import init from '../init';
-import composeNotification from '../common/scripts/compose-notification';
-
-import MkHome from './views/home/home.vue';
-import MkSelectDrive from './views/pages/selectdrive.vue';
-import MkDrive from './views/pages/drive.vue';
-import MkMessagingRoom from './views/pages/messaging-room.vue';
-import MkReversi from './views/pages/games/reversi.vue';
-import MkShare from '../common/views/pages/share.vue';
-import MkFollow from '../common/views/pages/follow.vue';
-import MkNotFound from '../common/views/pages/not-found.vue';
-import MkSettings from './views/pages/settings.vue';
-import DeckColumn from '../common/views/deck/deck.column-template.vue';
-
-import Ctx from './views/components/context-menu.vue';
-import RenoteFormWindow from './views/components/renote-form-window.vue';
-import MkChooseFileFromDriveWindow from './views/components/choose-file-from-drive-window.vue';
-import MkChooseFolderFromDriveWindow from './views/components/choose-folder-from-drive-window.vue';
-import MkHomeTimeline from './views/home/timeline.vue';
-import Notification from './views/components/ui-notification.vue';
-
-import { url } from '../config';
-import MiOS from '../mios';
-
-/**
- * init
- */
-init(async (launch, os) => {
-	Vue.mixin({
-		methods: {
-			$contextmenu(e, menu, opts?) {
-				const o = opts || {};
-				const vm = this.$root.new(Ctx, {
-					menu,
-					x: e.pageX - window.pageXOffset,
-					y: e.pageY - window.pageYOffset,
-				});
-				vm.$once('closed', () => {
-					if (o.closed) o.closed();
-				});
-			},
-
-			$post(opts) {
-				const o = opts || {};
-				if (o.renote) {
-					const vm = this.$root.new(RenoteFormWindow, {
-						note: o.renote,
-						animation: o.animation == null ? true : o.animation
-					});
-					if (o.cb) vm.$once('closed', o.cb);
-				} else {
-					this.$root.newAsync(() => import('./views/components/post-form-window.vue').then(m => m.default), {
-						reply: o.reply,
-						mention: o.mention,
-						animation: o.animation == null ? true : o.animation,
-						initialText: o.initialText,
-						instant: o.instant,
-						initialNote: o.initialNote,
-					}).then(vm => {
-						if (o.cb) vm.$once('closed', o.cb);
-					});
-				}
-			},
-
-			$chooseDriveFile(opts) {
-				return new Promise((res, rej) => {
-					const o = opts || {};
-
-					if (document.body.clientWidth > 800) {
-						const w = this.$root.new(MkChooseFileFromDriveWindow, {
-							title: o.title,
-							type: o.type,
-							multiple: o.multiple,
-							initFolder: o.currentFolder
-						});
-						w.$once('selected', file => {
-							res(file);
-						});
-					} else {
-						window['cb'] = file => {
-							res(file);
-						};
-
-						window.open(url + `/selectdrive?multiple=${o.multiple}`,
-							'choose_drive_window',
-							'height=500, width=800');
-					}
-				});
-			},
-
-			$chooseDriveFolder(opts) {
-				return new Promise((res, rej) => {
-					const o = opts || {};
-					const w = this.$root.new(MkChooseFolderFromDriveWindow, {
-						title: o.title,
-						initFolder: o.currentFolder
-					});
-					w.$once('selected', folder => {
-						res(folder);
-					});
-				});
-			},
-
-			$notify(message) {
-				this.$root.new(Notification, {
-					message
-				});
-			}
-		}
-	});
-
-	// Register directives
-	require('./views/directives');
-
-	// Register components
-	require('./views/components');
-	require('./views/widgets');
-
-	// Init router
-	const router = new VueRouter({
-		mode: 'history',
-		routes: [
-			os.store.state.device.inDeckMode
-				? { path: '/', name: 'index', component: () => import('../common/views/deck/deck.vue').then(m => m.default), children: [
-					{ path: '/@:user', component: () => import('../common/views/deck/deck.user-column.vue').then(m => m.default), children: [
-						{ path: '', name: 'user', component: () => import('../common/views/deck/deck.user-column.home.vue').then(m => m.default) },
-						{ path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
-						{ path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) },
-					]},
-					{ path: '/notes/:note', name: 'note', component: () => import('../common/views/deck/deck.note-column.vue').then(m => m.default) },
-					{ path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) },
-					{ path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) },
-					{ path: '/featured', name: 'featured', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/featured.vue').then(m => m.default), platform: 'deck' }) },
-					{ path: '/explore', name: 'explore', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) },
-					{ path: '/explore/tags/:tag', name: 'explore-tag', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) },
-					{ path: '/i/favorites', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/favorites.vue').then(m => m.default), platform: 'deck' }) },
-					{ path: '/i/pages', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) },
-					{ path: '/i/lists', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) },
-					{ path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) },
-					{ path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) },
-					{ path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) },
-					{ path: '/i/follow-requests', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) },
-					{ path: '/@:username/pages/:pageName', name: 'page', props: true, component: () => import('../common/views/deck/deck.page-column.vue').then(m => m.default) },
-				]}
-				: { path: '/', component: MkHome, children: [
-					{ path: '', name: 'index', component: MkHomeTimeline },
-					{ path: '/@:user', component: () => import('./views/home/user/index.vue').then(m => m.default), children: [
-						{ path: '', name: 'user', component: () => import('./views/home/user/user.home.vue').then(m => m.default) },
-						{ path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
-						{ path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) },
-					]},
-					{ path: '/notes/:note', name: 'note', component: () => import('./views/home/note.vue').then(m => m.default) },
-					{ path: '/search', component: () => import('./views/home/search.vue').then(m => m.default) },
-					{ path: '/tags/:tag', name: 'tag', component: () => import('./views/home/tag.vue').then(m => m.default) },
-					{ path: '/featured', name: 'featured', component: () => import('../common/views/pages/featured.vue').then(m => m.default), props: { platform: 'desktop' } },
-					{ path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
-					{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
-					{ path: '/i/favorites', component: () => import('../common/views/pages/favorites.vue').then(m => m.default), props: { platform: 'desktop' } },
-					{ path: '/i/pages', component: () => import('../common/views/pages/pages.vue').then(m => m.default) },
-					{ path: '/i/lists', component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) },
-					{ path: '/i/lists/:listId', props: true, component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default) },
-					{ path: '/i/groups', component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) },
-					{ path: '/i/groups/:groupId', props: true, component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default) },
-					{ path: '/i/follow-requests', component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) },
-					{ path: '/i/pages/new', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) },
-					{ path: '/i/pages/edit/:pageId', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initPageId: route.params.pageId }) },
-					{ path: '/@:user/pages/:page', component: () => import('../common/views/pages/page.vue').then(m => m.default), props: route => ({ pageName: route.params.page, username: route.params.user }) },
-					{ path: '/@:user/pages/:pageName/view-source', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
-				]},
-			{ path: '/i/pages/new', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) },
-			{ path: '/i/pages/edit/:pageId', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initPageId: route.params.pageId }) },
-			{ path: '/@:user/pages/:pageName/view-source', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
-			{ path: '/i/messaging/group/:group', component: MkMessagingRoom },
-			{ path: '/i/messaging/:user', component: MkMessagingRoom },
-			{ path: '/i/drive', component: MkDrive },
-			{ path: '/i/drive/folder/:folder', component: MkDrive },
-			{ path: '/i/settings', redirect: '/i/settings/profile' },
-			{ path: '/i/settings/:page', component: MkSettings },
-			{ path: '/selectdrive', component: MkSelectDrive },
-			{ path: '/@:acct/room', props: true, component: () => import('../common/views/pages/room/room.vue').then(m => m.default) },
-			{ path: '/share', component: MkShare },
-			{ path: '/games/reversi/:game?', component: MkReversi },
-			{ path: '/authorize-follow', component: MkFollow },
-			{ path: '/deck', redirect: '/' },
-			{ path: '*', component: MkNotFound }
-		],
-		scrollBehavior(to, from, savedPosition) {
-			return { x: 0, y: 0 };
-		}
-	});
-
-	// Launch the app
-	const [app, _] = launch(router);
-
-	/**
-	 * Init Notification
-	 */
-	if ('Notification' in window && os.store.getters.isSignedIn) {
-		// 許可を得ていなかったらリクエスト
-		if ((Notification as any).permission == 'default') {
-			await Notification.requestPermission();
-		}
-
-		if ((Notification as any).permission == 'granted') {
-			registerNotifications(os);
-		}
-	}
-}, true);
-
-function registerNotifications(os: MiOS) {
-	const stream = os.stream;
-
-	if (stream == null) return;
-
-	const connection = stream.useSharedConnection('main');
-
-	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('driveFileCreated', file => {
-		const _n = composeNotification('driveFileCreated', file);
-		const n = new Notification(_n.title, {
-			body: _n.body,
-			icon: _n.icon
-		});
-		setTimeout(n.close.bind(n), 5000);
-	});
-
-	connection.on('unreadMessagingMessage', message => {
-		const _n = composeNotification('unreadMessagingMessage', message);
-		const n = new Notification(_n.title, {
-			body: _n.body,
-			icon: _n.icon
-		});
-		n.onclick = () => {
-			n.close();
-			/*(riot as any).mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), {
-				user: message.user
-			});*/
-		};
-		setTimeout(n.close.bind(n), 7000);
-	});
-
-	connection.on('reversiInvited', matching => {
-		const _n = composeNotification('reversiInvited', matching);
-		const n = new Notification(_n.title, {
-			body: _n.body,
-			icon: _n.icon
-		});
-	});
-}
diff --git a/src/client/app/desktop/style.styl b/src/client/app/desktop/style.styl
deleted file mode 100644
index 249d3db2ed2551f2f40498ed8ab0b3bd7f11fbf2..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/style.styl
+++ /dev/null
@@ -1,40 +0,0 @@
-@import "../app"
-@import "../reset"
-
-*::input-placeholder
-	color #D8CBC5
-
-*:focus
-	outline none
-
-html
-	height 100%
-	background var(--bg)
-
-	&, div, textarea
-		scrollbar-width thin
-
-	&, *
-		scrollbar-color var(--scrollbarHandle) var(--scrollbarTrack)
-
-		&:hover
-			scrollbar-color var(--scrollbarHandleHover) var(--scrollbarTrack)
-
-		&:active
-			scrollbar-color var(--primary) var(--scrollbarTrack)
-
-		&::-webkit-scrollbar
-			width 6px
-			height 6px
-
-		&::-webkit-scrollbar-track
-			background var(--scrollbarTrack)
-
-		&::-webkit-scrollbar-thumb
-			background var(--scrollbarHandle)
-
-			&:hover
-				background var(--scrollbarHandleHover)
-
-			&:active
-				background var(--primary)
diff --git a/src/client/app/desktop/views/components/activity.calendar.vue b/src/client/app/desktop/views/components/activity.calendar.vue
deleted file mode 100644
index da74a97f68afbfbbad209c66e9855d3a915fdbc9..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/activity.calendar.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-<template>
-<svg viewBox="0 0 21 7">
-	<rect v-for="record in data" class="day"
-		width="1" height="1"
-		:x="record.x" :y="record.date.weekday"
-		rx="1" ry="1"
-		fill="transparent">
-		<title>{{ record.date.year }}/{{ record.date.month + 1 }}/{{ record.date.day }}</title>
-	</rect>
-	<rect v-for="record in data" class="day"
-		:width="record.v" :height="record.v"
-		:x="record.x + ((1 - record.v) / 2)" :y="record.date.weekday + ((1 - record.v) / 2)"
-		rx="1" ry="1"
-		:fill="record.color"
-		style="pointer-events: none;"/>
-	<rect class="today"
-		width="1" height="1"
-		:x="data[0].x" :y="data[0].date.weekday"
-		rx="1" ry="1"
-		fill="none"
-		stroke-width="0.1"
-		stroke="#f73520"/>
-</svg>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: ['data'],
-	created() {
-		for (const d of this.data) {
-			d.total = d.notes + d.replies + d.renotes;
-		}
-		const peak = Math.max.apply(null, this.data.map(d => d.total));
-
-		const now = new Date();
-		const year = now.getFullYear();
-		const month = now.getMonth();
-		const day = now.getDate();
-
-		let x = 20;
-		this.data.slice().forEach((d, i) => {
-			d.x = x;
-
-			const date = new Date(year, month, day - i);
-			d.date = {
-				year: date.getFullYear(),
-				month: date.getMonth(),
-				day: date.getDate(),
-				weekday: date.getDay()
-			};
-
-			d.v = peak == 0 ? 0 : d.total / (peak / 2);
-			if (d.v > 1) d.v = 1;
-			const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170;
-			const cs = d.v * 100;
-			const cl = 15 + ((1 - d.v) * 80);
-			d.color = `hsl(${ch}, ${cs}%, ${cl}%)`;
-
-			if (d.date.weekday == 0) x--;
-		});
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-svg
-	display block
-	padding 10px
-	width 100%
-
-	> rect
-		transform-origin center
-
-		&.day
-			&:hover
-				fill rgba(#000, 0.05)
-
-</style>
diff --git a/src/client/app/desktop/views/components/activity.chart.vue b/src/client/app/desktop/views/components/activity.chart.vue
deleted file mode 100644
index 648b64a3fe21f692bd2ab988df57876097b5cbd9..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/activity.chart.vue
+++ /dev/null
@@ -1,108 +0,0 @@
-<template>
-<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" @mousedown.prevent="onMousedown">
-	<title>{{ $t('total') }}<br/>{{ $t('notes') }}<br/>{{ $t('replies') }}<br/>{{ $t('renotes') }}</title>
-	<polyline
-		:points="pointsNote"
-		fill="none"
-		stroke-width="1"
-		stroke="#41ddde"/>
-	<polyline
-		:points="pointsReply"
-		fill="none"
-		stroke-width="1"
-		stroke="#f7796c"/>
-	<polyline
-		:points="pointsRenote"
-		fill="none"
-		stroke-width="1"
-		stroke="#a1de41"/>
-	<polyline
-		:points="pointsTotal"
-		fill="none"
-		stroke-width="1"
-		stroke="#555"
-		stroke-dasharray="2 2"/>
-</svg>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-function dragListen(fn) {
-	window.addEventListener('mousemove',  fn);
-	window.addEventListener('mouseleave', dragClear.bind(null, fn));
-	window.addEventListener('mouseup',    dragClear.bind(null, fn));
-}
-
-function dragClear(fn) {
-	window.removeEventListener('mousemove',  fn);
-	window.removeEventListener('mouseleave', dragClear);
-	window.removeEventListener('mouseup',    dragClear);
-}
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/activity.chart.vue'),
-	props: ['data'],
-	data() {
-		return {
-			viewBoxX: 147,
-			viewBoxY: 60,
-			zoom: 1,
-			pos: 0,
-			pointsNote: null,
-			pointsReply: null,
-			pointsRenote: null,
-			pointsTotal: null
-		};
-	},
-	created() {
-		for (const d of this.data) {
-			d.total = d.notes + d.replies + d.renotes;
-		}
-
-		this.render();
-	},
-	methods: {
-		render() {
-			const peak = Math.max.apply(null, this.data.map(d => d.total));
-			if (peak != 0) {
-				const data = this.data.slice().reverse();
-				this.pointsNote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' ');
-				this.pointsReply = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
-				this.pointsRenote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
-				this.pointsTotal = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
-			}
-		},
-		onMousedown(e) {
-			const clickX = e.clientX;
-			const clickY = e.clientY;
-			const baseZoom = this.zoom;
-			const basePos = this.pos;
-
-			// 動かした時
-			dragListen(me => {
-				let moveLeft = me.clientX - clickX;
-				let moveTop = me.clientY - clickY;
-
-				this.zoom = baseZoom + (-moveTop / 20);
-				this.pos = basePos + moveLeft;
-				if (this.zoom < 1) this.zoom = 1;
-				if (this.pos > 0) this.pos = 0;
-				if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX);
-
-				this.render();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-svg
-	display block
-	padding 10px
-	width 100%
-	cursor all-scroll
-
-</style>
diff --git a/src/client/app/desktop/views/components/activity.vue b/src/client/app/desktop/views/components/activity.vue
deleted file mode 100644
index 2cac125041322a95dd3a705fc40b3142f890cb1c..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/activity.vue
+++ /dev/null
@@ -1,86 +0,0 @@
-<template>
-<div class="mk-activity">
-	<ui-container :show-header="design == 0" :naked="design == 2">
-		<template #header><fa icon="chart-bar"/>{{ $t('title') }}</template>
-		<template #func><button :title="$t('toggle')" @click="toggle"><fa icon="sort"/></button></template>
-
-		<p :class="$style.fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
-		<template v-else>
-			<x-calendar v-show="view == 0" :data="[].concat(activity)"/>
-			<x-chart v-show="view == 1" :data="[].concat(activity)"/>
-		</template>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XCalendar from './activity.calendar.vue';
-import XChart from './activity.chart.vue';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/activity.vue'),
-	components: {
-		XCalendar,
-		XChart
-	},
-	props: {
-		design: {
-			default: 0
-		},
-		initView: {
-			default: 0
-		},
-		user: {
-			type: Object,
-			required: true
-		}
-	},
-	data() {
-		return {
-			fetching: true,
-			activity: null,
-			view: this.initView
-		};
-	},
-	mounted() {
-		this.$root.api('charts/user/notes', {
-			userId: this.user.id,
-			span: 'day',
-			limit: 7 * 21
-		}).then(activity => {
-			this.activity = activity.diffs.normal.map((_, i) => ({
-				total: activity.diffs.normal[i] + activity.diffs.reply[i] + activity.diffs.renote[i],
-				notes: activity.diffs.normal[i],
-				replies: activity.diffs.reply[i],
-				renotes: activity.diffs.renote[i]
-			}));
-			this.fetching = false;
-		});
-	},
-	methods: {
-		toggle() {
-			if (this.view == 1) {
-				this.view = 0;
-				this.$emit('viewChanged', this.view);
-			} else {
-				this.view++;
-				this.$emit('viewChanged', this.view);
-			}
-		}
-	}
-});
-</script>
-
-<style lang="stylus" module>
-.fetching
-	margin 0
-	padding 16px
-	text-align center
-	color var(--text)
-
-	> [data-icon]
-		margin-right 4px
-
-</style>
diff --git a/src/client/app/desktop/views/components/calendar.vue b/src/client/app/desktop/views/components/calendar.vue
deleted file mode 100644
index cdeac51638d61acfdff925e308b32d58aa4a9378..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/calendar.vue
+++ /dev/null
@@ -1,252 +0,0 @@
-<template>
-<div class="mk-calendar" :data-melt="design == 4 || design == 5" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
-	<template v-if="design == 0 || design == 1">
-		<button @click="prev" :title="$t('prev')"><fa icon="chevron-circle-left"/></button>
-		<p class="title">{{ $t('title', { year, month }) }}</p>
-		<button @click="next" :title="$t('next')"><fa icon="chevron-circle-right"/></button>
-	</template>
-
-	<div class="calendar">
-		<template v-if="design == 0 || design == 2 || design == 4">
-		<div class="weekday"
-			v-for="(day, i) in Array(7).fill(0)"
-			:data-today="year == today.getFullYear() && month == today.getMonth() + 1 && today.getDay() == i"
-			:data-is-weekend="i == 0 || i == 6"
-		>{{ weekdayText[i] }}</div>
-		</template>
-		<div v-for="n in paddingDays"></div>
-		<div class="day" v-for="(day, i) in days"
-			:data-today="isToday(i + 1)"
-			:data-selected="isSelected(i + 1)"
-			:data-is-out-of-range="isOutOfRange(i + 1)"
-			:data-is-weekend="isWeekend(i + 1)"
-			@click="go(i + 1)"
-			:title="isOutOfRange(i + 1) ? null : $t('go')"
-		>
-			<div>{{ i + 1 }}</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-const eachMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
-
-function isLeapYear(year) {
-	return !(year & (year % 25 ? 3 : 15));
-}
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/calendar.vue'),
-	props: {
-		design: {
-			default: 0
-		},
-		start: {
-			type: Date,
-			required: false
-		}
-	},
-	data() {
-		return {
-			today: new Date(),
-			year: new Date().getFullYear(),
-			month: new Date().getMonth() + 1,
-			selected: new Date(),
-			weekdayText: [
-				this.$t('@.weekday-short.sunday'),
-				this.$t('@.weekday-short.monday'),
-				this.$t('@.weekday-short.tuesday'),
-				this.$t('@.weekday-short.wednesday'),
-				this.$t('@.weekday-short.thursday'),
-				this.$t('@.weekday-short.friday'),
-				this.$t('@.weekday-short.saturday')
-			]
-		};
-	},
-	computed: {
-		paddingDays(): number {
-			const date = new Date(this.year, this.month - 1, 1);
-			return date.getDay();
-		},
-		days(): number {
-			let days = eachMonthDays[this.month - 1];
-
-			// うるう年なら+1日
-			if (this.month == 2 && isLeapYear(this.year)) days++;
-
-			return days;
-		}
-	},
-	methods: {
-		isToday(day) {
-			return this.year == this.today.getFullYear() && this.month == this.today.getMonth() + 1 && day == this.today.getDate();
-		},
-
-		isSelected(day) {
-			return this.year == this.selected.getFullYear() && this.month == this.selected.getMonth() + 1 && day == this.selected.getDate();
-		},
-
-		isOutOfRange(day) {
-			const test = (new Date(this.year, this.month - 1, day)).getTime();
-			return test > this.today.getTime() ||
-				(this.start ? test < (this.start as any).getTime() : false);
-		},
-
-		isWeekend(day) {
-			const weekday = (new Date(this.year, this.month - 1, day)).getDay();
-			return weekday == 0 || weekday == 6;
-		},
-
-		prev() {
-			if (this.month == 1) {
-				this.year = this.year - 1;
-				this.month = 12;
-			} else {
-				this.month--;
-			}
-		},
-
-		next() {
-			if (this.month == 12) {
-				this.year = this.year + 1;
-				this.month = 1;
-			} else {
-				this.month++;
-			}
-		},
-
-		go(day) {
-			if (this.isOutOfRange(day)) return;
-			const date = new Date(this.year, this.month - 1, day, 23, 59, 59, 999);
-			this.selected = date;
-			this.$emit('chosen', this.selected);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-calendar
-	color var(--calendarDay)
-	background var(--face)
-	overflow hidden
-
-	&.round
-		border-radius 6px
-
-	&.shadow
-		box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
-
-	&[data-melt]
-		background transparent !important
-		border none !important
-
-	> .title
-		z-index 1
-		margin 0
-		padding 0 16px
-		text-align center
-		line-height 42px
-		font-size 0.9em
-		font-weight bold
-		color var(--faceHeaderText)
-		background var(--faceHeader)
-		box-shadow 0 var(--lineWidth) rgba(#000, 0.07)
-
-		> [data-icon]
-			margin-right 4px
-
-	> button
-		position absolute
-		z-index 2
-		top 0
-		padding 0
-		width 42px
-		font-size 0.9em
-		line-height 42px
-		color var(--faceTextButton)
-
-		&:hover
-			color var(--faceTextButtonHover)
-
-		&:active
-			color var(--faceTextButtonActive)
-
-		&:first-of-type
-			left 0
-
-		&:last-of-type
-			right 0
-
-	> .calendar
-		display flex
-		flex-wrap wrap
-		padding 16px
-
-		*
-			user-select none
-
-		> div
-			width calc(100% * (1/7))
-			text-align center
-			line-height 32px
-			font-size 14px
-
-			&.weekday
-				color var(--calendarWeek)
-
-				&[data-is-weekend]
-					color var(--calendarSaturdayOrSunday)
-
-				&[data-today]
-					box-shadow 0 0 0 var(--lineWidth) var(--calendarWeek) inset
-					border-radius 6px
-
-					&[data-is-weekend]
-						box-shadow 0 0 0 var(--lineWidth) var(--calendarSaturdayOrSunday) inset
-
-			&.day
-				cursor pointer
-				color var(--calendarDay)
-
-				> div
-					border-radius 6px
-
-				&:hover > div
-					background var(--faceClearButtonHover)
-
-				&:active > div
-					background var(--faceClearButtonActive)
-
-				&[data-is-weekend]
-					color var(--calendarSaturdayOrSunday)
-
-				&[data-is-out-of-range]
-					cursor default
-					opacity 0.5
-
-				&[data-selected]
-					font-weight bold
-
-					> div
-						background var(--faceClearButtonHover)
-
-					&:active > div
-						background var(--faceClearButtonActive)
-
-				&[data-today]
-					> div
-						color var(--primaryForeground)
-						background var(--primary)
-
-					&:hover > div
-						background var(--primaryLighten10)
-
-					&:active > div
-						background var(--primaryDarken10)
-
-</style>
diff --git a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue
deleted file mode 100644
index 71c430edeb87893deda02da0be903aa6d8b7e17e..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue
+++ /dev/null
@@ -1,136 +0,0 @@
-<template>
-<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
-	<template #header>
-		<span class="jqiaciqv">
-			<span class="title">{{ $t('choose-prompt') }}</span>
-			<span class="count" v-if="multiple && files.length > 0">({{ $t('chosen-files', { count: files.length }) }})</span>
-		</span>
-	</template>
-
-	<div class="rqsvbumu">
-		<x-drive
-			ref="browser"
-			class="browser"
-			:type="type"
-			:multiple="multiple"
-			@selected="onSelected"
-			@change-selection="onChangeSelection"
-		/>
-		<div class="footer">
-			<button class="upload" :title="$t('title')" @click="upload"><fa icon="upload"/></button>
-			<ui-button inline @click="cancel" style="margin-right:16px;">{{ $t('cancel') }}</ui-button>
-			<ui-button inline primary :disabled="multiple && files.length == 0" @click="ok">{{ $t('ok') }}</ui-button>
-		</div>
-	</div>
-</mk-window>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/choose-file-from-drive-window.vue'),
-	components: {
-		XDrive: () => import('./drive.vue').then(m => m.default),
-	},
-	props: {
-		type: {
-			type: String,
-			required: false,
-			default: undefined 
-		},
-		multiple: {
-			default: false
-		}
-	},
-	data() {
-		return {
-			files: []
-		};
-	},
-	methods: {
-		onSelected(file) {
-			this.files = [file];
-			this.ok();
-		},
-		onChangeSelection(files) {
-			this.files = files;
-		},
-		upload() {
-			(this.$refs.browser as any).selectLocalFile();
-		},
-		ok() {
-			this.$emit('selected', this.multiple ? this.files : this.files[0]);
-			(this.$refs.window as any).close();
-		},
-		cancel() {
-			(this.$refs.window as any).close();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.jqiaciqv
-	.title
-		> [data-icon]
-			margin-right 4px
-
-	.count
-		margin-left 8px
-		opacity 0.7
-
-.rqsvbumu
-	display flex
-	flex-direction column
-	height 100%
-
-	.browser
-		flex 1
-		overflow auto
-
-	.footer
-		padding 16px
-		background var(--desktopPostFormBg)
-		text-align right
-
-	.upload
-		display inline-block
-		position absolute
-		top 8px
-		left 16px
-		cursor pointer
-		padding 0
-		margin 8px 4px 0 0
-		width 40px
-		height 40px
-		font-size 1em
-		color var(--primaryAlpha05)
-		background transparent
-		outline none
-		border solid 1px transparent
-		border-radius 4px
-
-		&:hover
-			background transparent
-			border-color var(--primaryAlpha03)
-
-		&:active
-			color var(--primaryAlpha06)
-			background transparent
-			border-color var(--primaryAlpha05)
-			//box-shadow 0 2px 4px rgba(var(--primaryDarken50), 0.15) inset
-
-		&:focus
-			&:after
-				content ""
-				pointer-events none
-				position absolute
-				top -5px
-				right -5px
-				bottom -5px
-				left -5px
-				border 2px solid var(--primaryAlpha03)
-				border-radius 8px
-
-</style>
diff --git a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue
deleted file mode 100644
index fe764365446707026c3a5b96e1dd0f1e76cae550..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-<template>
-<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
-	<template #header>
-		<span>{{ $t('choose-prompt') }}</span>
-	</template>
-
-	<div class="hllkpxxu">
-		<x-drive
-			ref="browser"
-			class="browser"
-			:multiple="false"
-		/>
-		<div class="footer">
-			<ui-button inline @click="cancel" style="margin-right:16px;">{{ $t('cancel') }}</ui-button>
-			<ui-button inline @click="ok" primary>{{ $t('ok') }}</ui-button>
-		</div>
-	</div>
-</mk-window>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/choose-folder-from-drive-window.vue'),
-	components: {
-		XDrive: () => import('./drive.vue').then(m => m.default),
-	},
-	methods: {
-		ok() {
-			this.$emit('selected', (this.$refs.browser as any).folder);
-			(this.$refs.window as any).close();
-		},
-		cancel() {
-			(this.$refs.window as any).close();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.hllkpxxu
-	display flex
-	flex-direction column
-	height 100%
-
-	.browser
-		flex 1
-		overflow auto
-
-	.footer
-		padding 16px
-		background var(--desktopPostFormBg)
-		text-align right
-
-</style>
diff --git a/src/client/app/desktop/views/components/context-menu.menu.vue b/src/client/app/desktop/views/components/context-menu.menu.vue
deleted file mode 100644
index f2bb3bec23d7134834ac0a1479540ac8b178cc56..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/context-menu.menu.vue
+++ /dev/null
@@ -1,121 +0,0 @@
-<template>
-<ul class="menu">
-	<li v-for="(item, i) in menu" :class="item ? item.type : item === null ? 'divider' : null">
-		<template v-if="item">
-			<template v-if="item.type == null || item.type == 'item'">
-				<p @click="click(item)"><i v-if="item.icon" :class="$style.icon"><fa :icon="item.icon"/></i>{{ item.text }}</p>
-			</template>
-			<template v-else-if="item.type == 'link'">
-				<a :href="item.href" :target="item.target" @click="click(item)" :download="item.download"><i v-if="item.icon" :class="$style.icon"><fa :icon="item.icon"/></i>{{ item.text }}</a>
-			</template>
-			<template v-else-if="item.type == 'nest'">
-				<p><i v-if="item.icon" :class="$style.icon"><fa :icon="item.icon"/></i>{{ item.text }}...<span class="caret"><fa icon="caret-right"/></span></p>
-				<me-nu :menu="item.menu" @x="click"/>
-			</template>
-		</template>
-	</li>
-</ul>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	name: 'me-nu',
-	props: ['menu'],
-	methods: {
-		click(item) {
-			this.$emit('x', item);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.menu
-	$width = 240px
-	$item-height = 38px
-	$padding = 10px
-
-	margin 0
-	padding $padding 0
-	list-style none
-
-	li
-		display block
-
-		&.divider
-			margin-top $padding
-			padding-top $padding
-			border-top solid var(--lineWidth) var(--faceDivider)
-
-		&.nest
-			> p
-				cursor default
-
-				> .caret
-					position absolute
-					top 0
-					right 8px
-
-					> *
-						line-height $item-height
-						width 28px
-						text-align center
-
-			&:hover > ul
-				visibility visible
-
-			&:active
-				> p, a
-					background var(--primary)
-
-		> p, a
-			display block
-			z-index 1
-			margin 0
-			padding 0 32px 0 38px
-			line-height $item-height
-			color var(--text)
-			text-decoration none
-			cursor pointer
-
-			&:hover
-				text-decoration none
-
-			*
-				pointer-events none
-
-		&:hover
-			> p, a
-				text-decoration none
-				background var(--primary)
-				color var(--primaryForeground)
-
-		&:active
-			> p, a
-				text-decoration none
-				background var(--primaryDarken10)
-				color var(--primaryForeground)
-
-	li > ul
-		visibility hidden
-		position absolute
-		top 0
-		left $width
-		margin-top -($padding)
-		width $width
-		background var(--popupBg)
-		border-radius 0 4px 4px 4px
-		box-shadow 2px 2px 8px rgba(#000, 0.2)
-		transition visibility 0s linear 0.2s
-
-</style>
-
-<style lang="stylus" module>
-.icon
-	display inline-block
-	width 28px
-	margin-left -28px
-	text-align center
-</style>
-
diff --git a/src/client/app/desktop/views/components/context-menu.vue b/src/client/app/desktop/views/components/context-menu.vue
deleted file mode 100644
index e79536fc0f6310d8db26362a0d22bb7d8829f541..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/context-menu.vue
+++ /dev/null
@@ -1,90 +0,0 @@
-<template>
-<div class="context-menu" @contextmenu.prevent="() => {}">
-	<x-menu :menu="menu" @x="click"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import anime from 'animejs';
-import contains from '../../../common/scripts/contains';
-import XMenu from './context-menu.menu.vue';
-
-export default Vue.extend({
-	components: {
-		XMenu
-	},
-	props: ['x', 'y', 'menu'],
-	mounted() {
-		this.$nextTick(() => {
-			const width = this.$el.offsetWidth;
-			const height = this.$el.offsetHeight;
-
-			let x = this.x;
-			let y = this.y;
-
-			if (x + width - window.pageXOffset > window.innerWidth) {
-				x = window.innerWidth - width + window.pageXOffset;
-			}
-
-			if (y + height - window.pageYOffset > window.innerHeight) {
-				y = window.innerHeight - height + window.pageYOffset;
-			}
-
-			this.$el.style.left = x + 'px';
-			this.$el.style.top = y + 'px';
-
-			for (const el of Array.from(document.querySelectorAll('body *'))) {
-				el.addEventListener('mousedown', this.onMousedown);
-			}
-
-			this.$el.style.display = 'block';
-
-			anime({
-				targets: this.$el,
-				opacity: [0, 1],
-				duration: 100,
-				easing: 'linear'
-			});
-		});
-	},
-	methods: {
-		onMousedown(e) {
-			e.preventDefault();
-			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
-			return false;
-		},
-		click(item) {
-			if (item.action) item.action();
-			this.close();
-		},
-		close() {
-			for (const el of Array.from(document.querySelectorAll('body *'))) {
-				el.removeEventListener('mousedown', this.onMousedown);
-			}
-
-			this.$emit('closed');
-			this.destroyDom();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.context-menu
-	$width = 240px
-	$item-height = 38px
-	$padding = 10px
-
-	position fixed
-	top 0
-	left 0
-	z-index 4096
-	width $width
-	font-size 0.8em
-	background var(--popupBg)
-	border-radius 0 4px 4px 4px
-	box-shadow 2px 2px 8px rgba(#000, 0.2)
-	opacity 0
-
-</style>
diff --git a/src/client/app/desktop/views/components/crop-window.vue b/src/client/app/desktop/views/components/crop-window.vue
deleted file mode 100644
index 856f889b02bb7d1131f23c8a32b6b57eddedf544..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/crop-window.vue
+++ /dev/null
@@ -1,189 +0,0 @@
-<template>
-	<mk-window ref="window" is-modal width="800px" :can-close="false">
-		<template #header><fa icon="crop"/>{{ title }}</template>
-		<div class="body">
-			<vue-cropper ref="cropper"
-				:src="imageUrl"
-				:view-mode="1"
-				:aspect-ratio="aspectRatio"
-				:container-style="{ width: '100%', 'max-height': '400px' }"
-			/>
-		</div>
-		<div :class="$style.actions">
-			<button :class="$style.skip" @click="skip">{{ $t('skip') }}</button>
-			<button :class="$style.cancel" @click="cancel">{{ $t('cancel') }}</button>
-			<button :class="$style.ok" @click="ok">{{ $t('ok') }}</button>
-		</div>
-	</mk-window>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import VueCropper from 'vue-cropperjs';
-import 'cropperjs/dist/cropper.css';
-import * as url from '../../../../../prelude/url';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/crop-window.vue'),
-	components: {
-		VueCropper
-	},
-	props: {
-		image: {
-			type: Object,
-			required: true
-		},
-		title: {
-			type: String,
-			required: true
-		},
-		aspectRatio: {
-			type: Number,
-			required: true
-		}
-	},
-	computed: {
-		imageUrl() {
-			return `/proxy/?${url.query({
-				url: this.image.url
-			})}`;
-		},
-	},
-	methods: {
-		ok() {
-			(this.$refs.cropper as any).getCroppedCanvas().toBlob(blob => {
-				this.$emit('cropped', blob);
-				(this.$refs.window as any).close();
-			});
-		},
-
-		skip() {
-			this.$emit('skipped');
-			(this.$refs.window as any).close();
-		},
-
-		cancel() {
-			this.$emit('canceled');
-			(this.$refs.window as any).close();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" module>
-
-
-.header
-	> [data-icon]
-		margin-right 4px
-
-.img
-	width 100%
-	max-height 400px
-
-.actions
-	height 72px
-	background var(--primaryLighten95)
-
-.ok
-.cancel
-.skip
-	display block
-	position absolute
-	bottom 16px
-	cursor pointer
-	padding 0
-	margin 0
-	height 40px
-	font-size 1em
-	outline none
-	border-radius 4px
-
-	&:focus
-		&:after
-			content ""
-			pointer-events none
-			position absolute
-			top -5px
-			right -5px
-			bottom -5px
-			left -5px
-			border 2px solid var(--primaryAlpha03)
-			border-radius 8px
-
-	&:disabled
-		opacity 0.7
-		cursor default
-
-.ok
-.cancel
-	width 120px
-
-.ok
-	right 16px
-	color var(--primaryForeground)
-	background linear-gradient(to bottom, var(--primaryLighten25) 0%, var(--primaryLighten10) 100%)
-	border solid 1px var(--primaryLighten15)
-
-	&:not(:disabled)
-		font-weight bold
-
-	&:hover:not(:disabled)
-		background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%)
-		border-color var(--primary)
-
-	&:active:not(:disabled)
-		background var(--primary)
-		border-color var(--primary)
-
-.cancel
-.skip
-	color #888
-	background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
-	border solid 1px #e2e2e2
-
-	&:hover
-		background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
-		border-color #dcdcdc
-
-	&:active
-		background #ececec
-		border-color #dcdcdc
-
-.cancel
-	right 148px
-
-.skip
-	left 16px
-	width 150px
-
-</style>
-
-<style lang="stylus">
-.cropper-modal {
-	opacity: 0.8;
-}
-
-.cropper-view-box {
-	outline-color: var(--primary);
-}
-
-.cropper-line, .cropper-point {
-	background-color: var(--primary);
-}
-
-.cropper-bg {
-	animation: cropper-bg 0.5s linear infinite;
-}
-
-@keyframes cropper-bg {
-	0% {
-		background-position: 0 0;
-	}
-
-	100% {
-		background-position: -8px -8px;
-	}
-}
-</style>
diff --git a/src/client/app/desktop/views/components/detail-notes.vue b/src/client/app/desktop/views/components/detail-notes.vue
deleted file mode 100644
index e50dda7c6ffbe628c99f9d19cb5211ad2c119b86..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/detail-notes.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-<template>
-<div class="ecsvsegy" v-if="!fetching">
-	<sequential-entrance animation="entranceFromTop" delay="25">
-		<template v-for="note in notes">
-			<mk-note-detail class="post" :note="note" :key="note.id"/>
-		</template>
-	</sequential-entrance>
-	<div class="more" v-if="more">
-		<ui-button inline @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import paging from '../../../common/scripts/paging';
-
-export default Vue.extend({
-	i18n: i18n(),
-
-	mixins: [
-		paging({
-			captureWindowScroll: true,
-		}),
-	],
-
-	props: {
-		pagination: {
-			required: true
-		},
-		extract: {
-			required: false
-		}
-	},
-
-	computed: {
-		notes() {
-			return this.extract ? this.extract(this.items) : this.items;
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.ecsvsegy
-	margin 0 auto
-
-	> * > .post
-		margin-bottom 16px
-
-	> .more
-		margin 32px 16px 16px 16px
-		text-align center
-
-</style>
diff --git a/src/client/app/desktop/views/components/drive-window.vue b/src/client/app/desktop/views/components/drive-window.vue
deleted file mode 100644
index 5f8a9316f35e41fbf2e08de20968e588c2358bec..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/drive-window.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<template>
-<mk-window ref="window" @closed="destroyDom" width="800px" height="500px" :popout-url="popout">
-	<template #header>
-		<p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> {{ $t('used') }}</p>
-		<span :class="$style.title"><fa icon="cloud"/>{{ $t('@.drive') }}</span>
-	</template>
-	<x-drive :class="$style.browser" multiple :init-folder="folder" ref="browser"/>
-</mk-window>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { url } from '../../../config';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/drive-window.vue'),
-	components: {
-		XDrive: () => import('./drive.vue').then(m => m.default),
-	},
-	props: ['folder'],
-	data() {
-		return {
-			usage: null
-		};
-	},
-	mounted() {
-		this.$root.api('drive').then(info => {
-			this.usage = info.usage / info.capacity * 100;
-		});
-	},
-	methods: {
-		popout() {
-			const folder = (this.$refs.browser as any) ? (this.$refs.browser as any).folder : null;
-			if (folder) {
-				return `${url}/i/drive/folder/${folder.id}`;
-			} else {
-				return `${url}/i/drive`;
-			}
-		}
-	}
-});
-</script>
-
-<style lang="stylus" module>
-.title
-	> [data-icon]
-		margin-right 4px
-
-.info
-	position absolute
-	top 0
-	left 16px
-	margin 0
-	font-size 80%
-
-.browser
-	height 100%
-
-</style>
-
diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue
deleted file mode 100644
index e34fdff4230524f14aab83d0d6b42541c95e6256..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/drive.file.vue
+++ /dev/null
@@ -1,339 +0,0 @@
-<template>
-<div class="gvfdktuvdgwhmztnuekzkswkjygptfcv"
-	:data-is-selected="isSelected"
-	:data-is-contextmenu-showing="isContextmenuShowing"
-	@click="onClick"
-	draggable="true"
-	@dragstart="onDragstart"
-	@dragend="onDragend"
-	@contextmenu.prevent.stop="onContextmenu"
-	:title="title"
->
-	<div class="label" v-if="$store.state.i.avatarId == file.id">
-		<img src="/assets/label.svg"/>
-		<p>{{ $t('avatar') }}</p>
-	</div>
-	<div class="label" v-if="$store.state.i.bannerId == file.id">
-		<img src="/assets/label.svg"/>
-		<p>{{ $t('banner') }}</p>
-	</div>
-	<div class="label red" v-if="file.isSensitive">
-		<img src="/assets/label-red.svg"/>
-		<p>{{ $t('nsfw') }}</p>
-	</div>
-
-	<x-file-thumbnail class="thumbnail" :file="file" fit="contain"/>
-
-	<p class="name">
-		<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
-		<span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
-	</p>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
-import updateAvatar from '../../api/update-avatar';
-import updateBanner from '../../api/update-banner';
-import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/drive.file.vue'),
-	props: ['file'],
-	components: {
-		XFileThumbnail
-	},
-	data() {
-		return {
-			isContextmenuShowing: false,
-			isDragging: false
-		};
-	},
-	computed: {
-		browser(): any {
-			return this.$parent;
-		},
-		isSelected(): boolean {
-			return this.browser.selectedFiles.some(f => f.id == this.file.id);
-		},
-		title(): string {
-			return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.size)}`;
-		}
-	},
-	methods: {
-		onClick() {
-			this.browser.chooseFile(this.file);
-		},
-
-		onContextmenu(e) {
-			this.isContextmenuShowing = true;
-			this.$contextmenu(e, [{
-				type: 'item',
-				text: this.$t('contextmenu.rename'),
-				icon: 'i-cursor',
-				action: this.rename
-			}, {
-				type: 'item',
-				text: this.file.isSensitive ? this.$t('contextmenu.unmark-as-sensitive') : this.$t('contextmenu.mark-as-sensitive'),
-				icon: this.file.isSensitive ? ['far', 'eye'] : ['far', 'eye-slash'],
-				action: this.toggleSensitive
-			}, null, {
-				type: 'item',
-				text: this.$t('contextmenu.copy-url'),
-				icon: 'link',
-				action: this.copyUrl
-			}, {
-				type: 'link',
-				href: this.file.url,
-				target: '_blank',
-				text: this.$t('contextmenu.download'),
-				icon: 'download',
-				download: this.file.name
-			}, null, {
-				type: 'item',
-				text: this.$t('@.delete'),
-				icon: ['far', 'trash-alt'],
-				action: this.deleteFile
-			}, null, {
-				type: 'nest',
-				text: this.$t('contextmenu.else-files'),
-				menu: [{
-					type: 'item',
-					text: this.$t('contextmenu.set-as-avatar'),
-					action: this.setAsAvatar
-				}, {
-					type: 'item',
-					text: this.$t('contextmenu.set-as-banner'),
-					action: this.setAsBanner
-				}]
-			}, /*{
-				type: 'nest',
-				text: this.$t('contextmenu.open-in-app'),
-				menu: [{
-					type: 'item',
-					text: '%i18n:@contextmenu.add-app%...',
-					action: this.addApp
-				}]
-			}*/], {
-				closed: () => {
-					this.isContextmenuShowing = false;
-				}
-			});
-		},
-
-		onDragstart(e) {
-			e.dataTransfer.effectAllowed = 'move';
-			e.dataTransfer.setData('mk_drive_file', JSON.stringify(this.file));
-			this.isDragging = true;
-
-			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
-			// (=あなたの子供が、ドラッグを開始しましたよ)
-			this.browser.isDragSource = true;
-		},
-
-		onDragend(e) {
-			this.isDragging = false;
-			this.browser.isDragSource = false;
-		},
-
-		onThumbnailLoaded() {
-			if (this.file.properties.avgColor) {
-				anime({
-					targets: this.$refs.thumbnail,
-					backgroundColor: 'transparent', // TODO fade
-					duration: 100,
-					easing: 'linear'
-				});
-			}
-		},
-
-		rename() {
-			this.$root.dialog({
-				title: this.$t('contextmenu.rename-file'),
-				input: {
-					placeholder: this.$t('contextmenu.input-new-file-name'),
-					default: this.file.name,
-					allowEmpty: false
-				}
-			}).then(({ canceled, result: name }) => {
-				if (canceled) return;
-				this.$root.api('drive/files/update', {
-					fileId: this.file.id,
-					name: name
-				});
-			});
-		},
-
-		toggleSensitive() {
-			this.$root.api('drive/files/update', {
-				fileId: this.file.id,
-				isSensitive: !this.file.isSensitive
-			});
-		},
-
-		copyUrl() {
-			copyToClipboard(this.file.url);
-			this.$root.dialog({
-				title: this.$t('contextmenu.copied'),
-				text: this.$t('contextmenu.copied-url-to-clipboard')
-			});
-		},
-
-		setAsAvatar() {
-			updateAvatar(this.$root)(this.file);
-		},
-
-		setAsBanner() {
-			updateBanner(this.$root)(this.file);
-		},
-
-		addApp() {
-			alert('not implemented yet');
-		},
-
-		deleteFile() {
-			this.$root.api('drive/files/delete', {
-				fileId: this.file.id
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.gvfdktuvdgwhmztnuekzkswkjygptfcv
-	padding 8px 0 0 0
-	min-height 180px
-	border-radius 4px
-
-	&, *
-		cursor pointer
-
-	&:hover
-		background rgba(#000, 0.05)
-
-		> .label
-			&:before
-			&:after
-				background #0b65a5
-
-			&.red
-				&:before
-				&:after
-					background #c12113
-
-	&:active
-		background rgba(#000, 0.1)
-
-		> .label
-			&:before
-			&:after
-				background #0b588c
-
-			&.red
-				&:before
-				&:after
-					background #ce2212
-
-	&[data-is-selected]
-		background var(--primary)
-
-		&:hover
-			background var(--primaryLighten10)
-
-		&:active
-			background var(--primaryDarken10)
-
-		> .label
-			&:before
-			&:after
-				display none
-
-		> .name
-			color var(--primaryForeground)
-
-		> .thumbnail
-			color var(--primaryForeground)
-
-	&[data-is-contextmenu-showing]
-		&:after
-			content ""
-			pointer-events none
-			position absolute
-			top -4px
-			right -4px
-			bottom -4px
-			left -4px
-			border 2px dashed var(--primaryAlpha03)
-			border-radius 4px
-
-	> .label
-		position absolute
-		top 0
-		left 0
-		pointer-events none
-
-		&:before
-		&:after
-			content ""
-			display block
-			position absolute
-			z-index 1
-			background #0c7ac9
-
-		&:before
-			top 0
-			left 57px
-			width 28px
-			height 8px
-
-		&:after
-			top 57px
-			left 0
-			width 8px
-			height 28px
-
-		&.red
-			&:before
-			&:after
-				background #c12113
-
-		> img
-			position absolute
-			z-index 2
-			top 0
-			left 0
-
-		> p
-			position absolute
-			z-index 3
-			top 19px
-			left -28px
-			width 120px
-			margin 0
-			text-align center
-			line-height 28px
-			color #fff
-			transform rotate(-45deg)
-
-	> .thumbnail
-		width 128px
-		height 128px
-		margin auto
-		color var(--driveFileIcon)
-
-	> .name
-		display block
-		margin 4px 0 0 0
-		font-size 0.8em
-		text-align center
-		word-break break-all
-		color var(--text)
-		overflow hidden
-
-		> .ext
-			opacity 0.5
-
-</style>
diff --git a/src/client/app/desktop/views/components/emoji-picker-dialog.vue b/src/client/app/desktop/views/components/emoji-picker-dialog.vue
deleted file mode 100644
index 4ea0f441a902c74d9cdec45d01873a867607518e..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/emoji-picker-dialog.vue
+++ /dev/null
@@ -1,84 +0,0 @@
-<template>
-<div class="gcafiosrssbtbnbzqupfmglvzgiaipyv">
-	<x-picker @chosen="chosen"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import contains from '../../../common/scripts/contains';
-
-export default Vue.extend({
-	components: {
-		XPicker: () => import('../../../common/views/components/emoji-picker.vue').then(m => m.default)
-	},
-
-	props: {
-		x: {
-			type: Number,
-			required: true
-		},
-		y: {
-			type: Number,
-			required: true
-		}
-	},
-
-	mounted() {
-		this.$nextTick(() => {
-			const width = this.$el.offsetWidth;
-			const height = this.$el.offsetHeight;
-
-			let x = this.x;
-			let y = this.y;
-
-			if (x + width - window.pageXOffset > window.innerWidth) {
-				x = window.innerWidth - width + window.pageXOffset;
-			}
-
-			if (y + height - window.pageYOffset > window.innerHeight) {
-				y = window.innerHeight - height + window.pageYOffset;
-			}
-
-			this.$el.style.left = x + 'px';
-			this.$el.style.top = y + 'px';
-
-			for (const el of Array.from(document.querySelectorAll('body *'))) {
-				el.addEventListener('mousedown', this.onMousedown);
-			}
-		});
-	},
-
-	methods: {
-		onMousedown(e) {
-			e.preventDefault();
-			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
-			return false;
-		},
-
-		chosen(emoji) {
-			this.$emit('chosen', emoji);
-			this.close();
-		},
-
-		close() {
-			for (const el of Array.from(document.querySelectorAll('body *'))) {
-				el.removeEventListener('mousedown', this.onMousedown);
-			}
-
-			this.$emit('closed');
-			this.destroyDom();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.gcafiosrssbtbnbzqupfmglvzgiaipyv
-	position absolute
-	top 0
-	left 0
-	z-index 3000
-	box-shadow 0 2px 12px 0 rgba(0, 0, 0, 0.3)
-
-</style>
diff --git a/src/client/app/desktop/views/components/game-window.vue b/src/client/app/desktop/views/components/game-window.vue
deleted file mode 100644
index 3dba4c3af4f2e6b7d126e3d4b2afc39589e1b027..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/game-window.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-<template>
-<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
-	<template #header><fa icon="gamepad"/> {{ $t('game') }}</template>
-	<x-reversi :class="$style.content" @gamed="g => game = g"/>
-</mk-window>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { url } from '../../../config';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/game-window.vue'),
-	components: {
-		XReversi: () => import('../../../common/views/components/games/reversi/reversi.vue').then(m => m.default)
-	},
-	data() {
-		return {
-			game: null
-		};
-	},
-	computed: {
-		popout(): string {
-			return this.game
-				? `${url}/games/reversi/${this.game.id}`
-				: `${url}/games/reversi`;
-		}
-	}
-});
-</script>
-
-<style lang="stylus" module>
-.content
-	height 100%
-	overflow auto
-
-</style>
diff --git a/src/client/app/desktop/views/components/index.ts b/src/client/app/desktop/views/components/index.ts
deleted file mode 100644
index 0cc44e1bbd4192a21f668c69521a1e33a175b530..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/index.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import Vue from 'vue';
-
-import ui from './ui.vue';
-import uiNotification from './ui-notification.vue';
-import note from './note.vue';
-import notes from './notes.vue';
-import subNoteContent from './sub-note-content.vue';
-import window from './window.vue';
-import renoteFormWindow from './renote-form-window.vue';
-import mediaVideo from './media-video.vue';
-import notifications from './notifications.vue';
-import renoteForm from './renote-form.vue';
-import notePreview from './note-preview.vue';
-import noteDetail from './note-detail.vue';
-import calendar from './calendar.vue';
-import activity from './activity.vue';
-import userListTimeline from './user-list-timeline.vue';
-import uiContainer from './ui-container.vue';
-
-Vue.component('mk-ui', ui);
-Vue.component('mk-ui-notification', uiNotification);
-Vue.component('mk-note', note);
-Vue.component('mk-notes', notes);
-Vue.component('mk-sub-note-content', subNoteContent);
-Vue.component('mk-window', window);
-Vue.component('mk-renote-form-window', renoteFormWindow);
-Vue.component('mk-media-video', mediaVideo);
-Vue.component('mk-notifications', notifications);
-Vue.component('mk-renote-form', renoteForm);
-Vue.component('mk-note-preview', notePreview);
-Vue.component('mk-note-detail', noteDetail);
-Vue.component('mk-calendar', calendar);
-Vue.component('mk-activity', activity);
-Vue.component('mk-user-list-timeline', userListTimeline);
-Vue.component('ui-container', uiContainer);
diff --git a/src/client/app/desktop/views/components/media-video-dialog.vue b/src/client/app/desktop/views/components/media-video-dialog.vue
deleted file mode 100644
index 9d2d0527efc66f27cecbf887bda58eb6f07ff011..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/media-video-dialog.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<template>
-<ui-modal v-hotkey.global="keymap">
-	<video :src="video.url" :title="video.name" controls autoplay ref="video" @volumechange="volumechange" />
-</ui-modal>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: ['video', 'start'],
-	mounted() {
-		const videoTag = this.$refs.video as HTMLVideoElement;
-		if (this.start) videoTag.currentTime = this.start
-		videoTag.volume = this.$store.state.device.mediaVolume;
-	},
-	computed: {
-		keymap(): any {
-			return {
-				'esc': this.close,
-			};
-		}
-	},
-	methods: {
-		close() {
-		},
-		volumechange() {
-			const videoTag = this.$refs.video as HTMLVideoElement;
-			this.$store.commit('device/set', { key: 'mediaVolume', value: videoTag.volume });
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-video
-	position fixed
-	z-index 2
-	top 0
-	right 0
-	bottom 0
-	left 0
-	max-width 80vw
-	max-height 80vh
-	margin auto
-
-</style>
diff --git a/src/client/app/desktop/views/components/media-video.vue b/src/client/app/desktop/views/components/media-video.vue
deleted file mode 100644
index c53da0f49edc7e4ea2c0cc1f8b25575b1217ffaf..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/media-video.vue
+++ /dev/null
@@ -1,102 +0,0 @@
-<template>
-<div class="uofhebxjdgksfmltszlxurtjnjjsvioh" v-if="video.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false">
-	<div>
-		<b><fa icon="exclamation-triangle"/> {{ $t('sensitive') }}</b>
-		<span>{{ $t('click-to-show') }}</span>
-	</div>
-</div>
-<div class="vwxdhznewyashiknzolsoihtlpicqepe" v-else>
-	<a class="thumbnail"
-		:href="video.url"
-		:style="imageStyle"
-		@click.prevent="onClick"
-		:title="video.name"
-	>
-		<fa :icon="['far', 'play-circle']"/>
-	</a>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import MkMediaVideoDialog from './media-video-dialog.vue';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/media-video.vue'),
-	props: {
-		video: {
-			type: Object,
-			required: true
-		},
-		inlinePlayable: {
-			default: false
-		}
-	},
-	data() {
-		return {
-			hide: true
-		};
-	},
-	computed: {
-		imageStyle(): any {
-			return {
-				'background-image': `url(${this.video.thumbnailUrl})`
-			};
-		}
-	},
-	methods: {
-		onClick() {
-			const videoTag = this.$refs.video as (HTMLVideoElement | null)
-			var start = 0
-			if (videoTag) {
-				start = videoTag.currentTime
-				videoTag.pause()
-			}
-			const viewer = this.$root.new(MkMediaVideoDialog, {
-				video: this.video,
-				start,
-			});
-			this.$once('hook:beforeDestroy', () => {
-				viewer.close();
-			});
-		}
-	}
-})
-</script>
-
-<style lang="stylus" scoped>
-.vwxdhznewyashiknzolsoihtlpicqepe
-	.video
-		display block
-		width 100%
-		height 100%
-		border-radius 4px
-
-	.thumbnail
-		display flex
-		justify-content center
-		align-items center
-		font-size 3.5em
-		cursor zoom-in
-		overflow hidden
-		background-position center
-		background-size cover
-		width 100%
-		height 100%
-
-.uofhebxjdgksfmltszlxurtjnjjsvioh
-	display flex
-	justify-content center
-	align-items center
-	background #111
-	color #fff
-
-	> div
-		display table-cell
-		text-align center
-		font-size 12px
-
-		> b
-			display block
-</style>
diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue
deleted file mode 100644
index 6c1708b59f758187e47bc24c194f6cbdfdbfca90..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/messaging-room-window.vue
+++ /dev/null
@@ -1,37 +0,0 @@
-<template>
-<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
-	<template #header><fa icon="comments"/> {{ $t('@.messaging') }}: <mk-user-name v-if="user" :user="user"/><span v-else>{{ group.name }}</span></template>
-	<x-messaging-room :user="user" :group="group" :class="$style.content"/>
-</mk-window>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { url } from '../../../config';
-import getAcct from '../../../../../misc/acct/render';
-
-export default Vue.extend({
-	i18n: i18n(),
-	components: {
-		XMessagingRoom: () => import('../../../common/views/components/messaging-room.vue').then(m => m.default)
-	},
-	props: ['user', 'group'],
-	computed: {
-		popout(): string {
-			if (this.user) {
-				return `${url}/i/messaging/${getAcct(this.user)}`;
-			} else if (this.group) {
-				return `${url}/i/messaging/group/${this.group.id}`;
-			}
-		}
-	}
-});
-</script>
-
-<style lang="stylus" module>
-.content
-	height 100%
-	overflow auto
-
-</style>
diff --git a/src/client/app/desktop/views/components/messaging-window.vue b/src/client/app/desktop/views/components/messaging-window.vue
deleted file mode 100644
index 7cec9484d6c0b5b9aa4cadd272ef28cd9fe6e685..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/messaging-window.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<template>
-<mk-window ref="window" width="500px" height="560px" @closed="destroyDom">
-	<template #header :class="$style.header"><fa icon="comments"/>{{ $t('@.messaging') }}</template>
-	<x-messaging :class="$style.content" @navigate="navigate" @navigateGroup="navigateGroup"/>
-</mk-window>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import MkMessagingRoomWindow from './messaging-room-window.vue';
-
-export default Vue.extend({
-	i18n: i18n(),
-	components: {
-		XMessaging: () => import('../../../common/views/components/messaging.vue').then(m => m.default)
-	},
-	methods: {
-		navigate(user) {
-			this.$root.new(MkMessagingRoomWindow, {
-				user: user
-			});
-		},
-		navigateGroup(group) {
-			this.$root.new(MkMessagingRoomWindow, {
-				group: group
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" module>
-.header
-	> [data-icon]
-		margin-right 4px
-
-.content
-	height 100%
-	overflow auto
-
-</style>
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
deleted file mode 100644
index e0ce5ce1c63e0dd0cb2f6689edd278da45de329d..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ /dev/null
@@ -1,356 +0,0 @@
-<template>
-<div class="mk-note-detail" :title="title" tabindex="-1" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
-	<button
-		class="read-more"
-		v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0"
-		:title="$t('title')"
-		@click="fetchConversation"
-		:disabled="conversationFetching"
-	>
-		<template v-if="!conversationFetching"><fa icon="ellipsis-v"/></template>
-		<template v-if="conversationFetching"><fa icon="spinner" pulse/></template>
-	</button>
-	<div class="conversation">
-		<x-sub v-for="note in conversation" :key="note.id" :note="note"/>
-	</div>
-	<div class="reply-to" v-if="appearNote.reply">
-		<x-sub :note="appearNote.reply"/>
-	</div>
-	<mk-renote class="renote" v-if="isRenote" :note="note"/>
-	<article>
-		<mk-avatar class="avatar" :user="appearNote.user"/>
-		<header>
-			<router-link class="name" :to="appearNote.user | userPage" v-user-preview="appearNote.user.id">
-				<mk-user-name :user="appearNote.user"/>
-			</router-link>
-			<span class="username"><mk-acct :user="appearNote.user"/></span>
-			<div class="info">
-				<router-link class="time" :to="appearNote | notePage">
-					<mk-time :time="appearNote.createdAt"/>
-				</router-link>
-				<div class="visibility-info">
-					<span class="visibility" v-if="appearNote.visibility != 'public'">
-						<fa v-if="appearNote.visibility == 'home'" icon="home"/>
-						<fa v-if="appearNote.visibility == 'followers'" icon="unlock"/>
-						<fa v-if="appearNote.visibility == 'specified'" icon="envelope"/>
-					</span>
-					<span class="localOnly" v-if="appearNote.localOnly == true"><fa icon="heart"/></span>
-				</div>
-			</div>
-		</header>
-		<div class="body">
-			<p v-if="appearNote.cw != null" class="cw">
-				<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" />
-				<mk-cw-button v-model="showContent" :note="appearNote"/>
-			</p>
-			<div class="content" v-show="appearNote.cw == null || showContent">
-				<div class="text">
-					<span v-if="appearNote.isHidden" style="opacity: 0.5">{{ $t('private') }}</span>
-					<span v-if="appearNote.deletedAt" style="opacity: 0.5">{{ $t('deleted') }}</span>
-					<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" />
-				</div>
-				<div class="files" v-if="appearNote.files.length > 0">
-					<mk-media-list :media-list="appearNote.files" :raw="true"/>
-				</div>
-				<mk-poll v-if="appearNote.poll" :note="appearNote"/>
-				<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
-				<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a>
-				<div class="map" v-if="appearNote.geo" ref="map"></div>
-				<div class="renote" v-if="appearNote.renote">
-					<mk-note-preview :note="appearNote.renote"/>
-				</div>
-			</div>
-		</div>
-		<footer>
-			<span class="app" v-if="note.app && $store.state.settings.showVia">via <b>{{ note.app.name }}</b></span>
-			<mk-reactions-viewer :note="appearNote"/>
-			<button class="replyButton" @click="reply()" :title="$t('reply')">
-				<template v-if="appearNote.reply"><fa icon="reply-all"/></template>
-				<template v-else><fa icon="reply"/></template>
-				<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
-			</button>
-			<button v-if="['public', 'home'].includes(appearNote.visibility)" class="renoteButton" @click="renote()" :title="$t('renote')">
-				<fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
-			</button>
-			<button v-else class="inhibitedButton">
-				<fa icon="ban"/>
-			</button>
-			<button v-if="!isMyNote && appearNote.myReaction == null" class="reactionButton" @click="react()" ref="reactButton" :title="$t('add-reaction')">
-				<fa icon="plus"/>
-			</button>
-			<button v-if="!isMyNote && appearNote.myReaction != null" class="reactionButton reacted" @click="undoReact(appearNote)" ref="reactButton" :title="$t('undo-reaction')">
-				<fa icon="minus"/>
-			</button>
-			<button @click="menu()" ref="menuButton">
-				<fa icon="ellipsis-h"/>
-			</button>
-		</footer>
-	</article>
-	<div class="replies" v-if="!compact">
-		<x-sub v-for="note in replies" :key="note.id" :note="note"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XSub from './note.sub.vue';
-import noteSubscriber from '../../../common/scripts/note-subscriber';
-import noteMixin from '../../../common/scripts/note-mixin';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/note-detail.vue'),
-
-	components: {
-		XSub
-	},
-
-	mixins: [noteMixin(), noteSubscriber('note')],
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		compact: {
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			conversation: [],
-			conversationFetching: false,
-			replies: []
-		};
-	},
-
-	mounted() {
-		// Get replies
-		if (!this.compact) {
-			this.$root.api('notes/children', {
-				noteId: this.appearNote.id,
-				limit: 30
-			}).then(replies => {
-				this.replies = replies;
-			});
-		}
-	},
-
-	methods: {
-		fetchConversation() {
-			this.conversationFetching = true;
-
-			// Fetch conversation
-			this.$root.api('notes/conversation', {
-				noteId: this.appearNote.replyId
-			}).then(conversation => {
-				this.conversationFetching = false;
-				this.conversation = conversation.reverse();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-note-detail
-	overflow hidden
-	text-align left
-	background var(--face)
-
-	&.round
-		border-radius 6px
-
-		> .read-more
-			border-radius 6px 6px 0 0
-
-	&.shadow
-		box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
-
-	> .read-more
-		display block
-		margin 0
-		padding 10px 0
-		width 100%
-		font-size 1em
-		text-align center
-		color #999
-		cursor pointer
-		background var(--subNoteBg)
-		outline none
-		border none
-		border-bottom solid 1px var(--faceDivider)
-
-		&:hover
-			box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
-
-		&:active
-			box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
-
-		&:disabled
-			cursor wait
-
-	> .conversation
-		> *
-			border-bottom 1px solid var(--faceDivider)
-
-	> .renote + article
-		padding-top 8px
-
-	> .reply-to
-		border-bottom 1px solid var(--faceDivider)
-
-	> article
-		padding 28px 32px 18px 32px
-
-		&:after
-			content ""
-			display block
-			clear both
-
-		&:hover
-			> footer > button
-				color var(--noteActionsHighlighted)
-
-		> .avatar
-			width 60px
-			height 60px
-			border-radius 8px
-
-		> header
-			position absolute
-			top 28px
-			left 108px
-			width calc(100% - 108px)
-
-			> .name
-				display inline-block
-				margin 0
-				line-height 24px
-				color var(--noteHeaderName)
-				font-size 18px
-				font-weight 700
-				text-align left
-				text-decoration none
-
-				&:hover
-					text-decoration underline
-
-			> .username
-				display block
-				text-align left
-				margin 0
-				color var(--noteHeaderAcct)
-
-			> .info
-				position absolute
-				top 0
-				right 32px
-				font-size 1em
-
-				> .time
-					color var(--noteHeaderInfo)
-
-				> .visibility-info
-					text-align: right
-					color var(--noteHeaderInfo)
-
-					> .localOnly
-						margin-left 4px
-
-		> .body
-			padding 8px 0
-
-			> .cw
-				cursor default
-				display block
-				margin 0
-				padding 0
-				overflow-wrap break-word
-				color var(--noteText)
-
-				> .text
-					margin-right 8px
-
-			> .content
-				> .text
-					cursor default
-					display block
-					margin 0
-					padding 0
-					overflow-wrap break-word
-					font-size 1.5em
-					color var(--noteText)
-
-				> .renote
-					margin 8px 0
-
-					> *
-						padding 16px
-						border dashed 1px var(--quoteBorder)
-						border-radius 8px
-
-				> .location
-					margin 4px 0
-					font-size 12px
-					color #ccc
-
-				> .map
-					width 100%
-					height 300px
-
-					&:empty
-						display none
-
-				> .mk-url-preview
-					margin-top 8px
-
-		> footer
-			font-size 1.2em
-
-			> .app
-				display block
-				font-size 0.8em
-				margin-left 0.5em
-				color var(--noteHeaderInfo)
-
-			> button
-				margin 0 28px 0 0
-				padding 8px
-				background transparent
-				border none
-				font-size 1em
-				color var(--noteActions)
-				cursor pointer
-
-				&:hover
-					color var(--noteActionsHover)
-
-				&.replyButton:hover
-					color var(--noteActionsReplyHover)
-
-				&.renoteButton:hover
-					color var(--noteActionsRenoteHover)
-
-				&.reactionButton:hover
-					color var(--noteActionsReactionHover)
-
-				&.inhibitedButton
-					cursor not-allowed
-
-				> .count
-					display inline
-					margin 0 0 0 8px
-					color var(--text)
-					opacity 0.7
-
-				&.reacted, &.reacted:hover
-					color var(--noteActionsReactionHover)
-
-	> .replies
-		> *
-			border-top 1px solid var(--faceDivider)
-
-</style>
diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue
deleted file mode 100644
index 3b1e71e16855f77654caddce4c022a07b85b22eb..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/note-preview.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-<template>
-<div class="qiziqtywpuaucsgarwajitwaakggnisj" :title="title">
-	<mk-avatar class="avatar" :user="note.user" v-if="!narrow"/>
-	<div class="main">
-		<mk-note-header class="header" :note="note" :mini="true"/>
-		<div class="body">
-			<p v-if="note.cw != null" class="cw">
-				<mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" />
-				<mk-cw-button v-model="showContent" :note="note"/>
-			</p>
-			<div class="content" v-show="note.cw == null || showContent">
-				<mk-sub-note-content class="text" :note="note"/>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-	},
-
-	inject: {
-		narrow: {
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			showContent: false
-		};
-	},
-
-	computed: {
-		title(): string {
-			return new Date(this.note.createdAt).toLocaleString();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.qiziqtywpuaucsgarwajitwaakggnisj
-	display flex
-	overflow hidden
-	font-size 0.9em
-
-	> .avatar
-		flex-shrink 0
-		display block
-		margin 0 12px 0 0
-		width 48px
-		height 48px
-		border-radius 8px
-
-	> .main
-		flex 1
-		min-width 0
-
-		> .body
-
-			> .cw
-				cursor default
-				display block
-				margin 0
-				padding 0
-				overflow-wrap break-word
-				color var(--noteText)
-
-				> .text
-					margin-right 8px
-
-			> .content
-				> .text
-					cursor default
-					margin 0
-					padding 0
-					color var(--subNoteText)
-
-</style>
diff --git a/src/client/app/desktop/views/components/note.sub.vue b/src/client/app/desktop/views/components/note.sub.vue
deleted file mode 100644
index bfecef3eb2a25b288f671b67ae3a8714b8c1b163..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/note.sub.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-<template>
-<div class="tkfdzaxtkdeianobciwadajxzbddorql" :class="{ mini: narrow }" :title="title">
-	<mk-avatar class="avatar" :user="note.user"/>
-	<div class="main">
-		<mk-note-header class="header" :note="note"/>
-		<div class="body">
-			<p v-if="note.cw != null" class="cw">
-				<mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" />
-				<mk-cw-button v-model="showContent" :note="note"/>
-			</p>
-			<div class="content" v-show="note.cw == null || showContent">
-				<mk-sub-note-content class="text" :note="note"/>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: {
-		note: {
-			type: Object,
-			required: true
-		}
-	},
-
-	inject: {
-		narrow: {
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			showContent: false
-		};
-	},
-
-	computed: {
-		title(): string {
-			return new Date(this.note.createdAt).toLocaleString();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.tkfdzaxtkdeianobciwadajxzbddorql
-	display flex
-	padding 16px 32px
-	font-size 0.9em
-	background var(--subNoteBg)
-
-	&.mini
-		padding 16px
-		font-size 10px
-
-		> .avatar
-			margin 0 8px 0 0
-			width 38px
-			height 38px
-
-	> .avatar
-		flex-shrink 0
-		display block
-		margin 0 12px 0 0
-		width 48px
-		height 48px
-		border-radius 8px
-
-	> .main
-		flex 1
-		min-width 0
-
-		> .header
-			margin-bottom 2px
-
-		> .body
-
-			> .cw
-				cursor default
-				display block
-				margin 0
-				padding 0
-				overflow-wrap break-word
-				color var(--noteText)
-
-				> .text
-					margin-right 8px
-
-			> .content
-				> .text
-					cursor default
-					margin 0
-					padding 0
-					color var(--subNoteText)
-					font-size calc(1em + var(--fontSize))
-
-					pre
-						max-height 120px
-						font-size 80%
-
-</style>
diff --git a/src/client/app/desktop/views/components/note.vue b/src/client/app/desktop/views/components/note.vue
deleted file mode 100644
index 1c00faed39f106658c756c912f5dfa4a95420ae9..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/note.vue
+++ /dev/null
@@ -1,323 +0,0 @@
-<template>
-<div
-	class="note"
-	:class="{ mini: narrow }"
-	v-show="(this.$store.state.settings.remainDeletedNote || appearNote.deletedAt == null) && !hideThisNote"
-	:tabindex="appearNote.deletedAt == null ? '-1' : null"
-	v-hotkey="keymap"
-	:title="title"
->
-	<x-sub v-for="note in conversation" :key="note.id" :note="note"/>
-	<div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
-		<x-sub :note="appearNote.reply"/>
-	</div>
-	<mk-renote class="renote" v-if="isRenote" :note="note"/>
-	<article class="article">
-		<mk-avatar class="avatar" :user="appearNote.user"/>
-		<div class="main">
-			<mk-note-header class="header" :note="appearNote" :mini="narrow"/>
-			<div class="body" v-if="appearNote.deletedAt == null">
-				<p v-if="appearNote.cw != null" class="cw">
-					<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" />
-					<mk-cw-button v-model="showContent" :note="appearNote"/>
-				</p>
-				<div class="content" v-show="appearNote.cw == null || showContent">
-					<div class="text">
-						<span v-if="appearNote.isHidden" style="opacity: 0.5">{{ $t('private') }}</span>
-						<a class="reply" v-if="appearNote.reply"><fa icon="reply"/></a>
-						<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
-						<a class="rp" v-if="appearNote.renote">RN:</a>
-					</div>
-					<div class="files" v-if="appearNote.files.length > 0">
-						<mk-media-list :media-list="appearNote.files"/>
-					</div>
-					<mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/>
-					<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> 位置情報</a>
-					<div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote"/></div>
-					<mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="compact"/>
-				</div>
-			</div>
-			<footer v-if="appearNote.deletedAt == null" class="footer">
-				<span class="app" v-if="appearNote.app && narrow && $store.state.settings.showVia">via <b>{{ appearNote.app.name }}</b></span>
-				<mk-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
-				<button class="replyButton button" @click="reply()" :title="$t('reply')">
-					<template v-if="appearNote.reply"><fa icon="reply-all"/></template>
-					<template v-else><fa icon="reply"/></template>
-					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
-				</button>
-				<button v-if="['public', 'home'].includes(appearNote.visibility)" class="renoteButton button" @click="renote()" :title="$t('renote')">
-					<fa icon="retweet"/>
-					<p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
-				</button>
-				<button v-else class="inhibitedButton button">
-					<fa icon="ban"/>
-				</button>
-				<button v-if="!isMyNote && appearNote.myReaction == null" class="reactionButton button" @click="react()" ref="reactButton" :title="$t('add-reaction')">
-					<fa icon="plus"/>
-					<p class="count" v-if="Object.values(appearNote.reactions).some(x => x)">{{ Object.values(appearNote.reactions).reduce((a, c) => a + c, 0) }}</p>
-				</button>
-				<button v-if="!isMyNote && appearNote.myReaction != null" class="reactionButton reacted button" @click="undoReact(appearNote)" ref="reactButton" :title="$t('undo-reaction')">
-					<fa icon="minus"/>
-					<p class="count" v-if="Object.values(appearNote.reactions).some(x => x)">{{ Object.values(appearNote.reactions).reduce((a, c) => a + c, 0) }}</p>
-				</button>
-				<button @click="menu()" ref="menuButton" class="button">
-					<fa icon="ellipsis-h"/>
-				</button>
-			</footer>
-			<div class="deleted" v-if="appearNote.deletedAt != null">{{ $t('deleted') }}</div>
-		</div>
-	</article>
-	<x-sub v-for="note in replies" :key="note.id" :note="note"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-import XSub from './note.sub.vue';
-import noteMixin from '../../../common/scripts/note-mixin';
-import noteSubscriber from '../../../common/scripts/note-subscriber';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/note.vue'),
-
-	components: {
-		XSub
-	},
-
-	mixins: [
-		noteMixin(),
-		noteSubscriber('note')
-	],
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		detail: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		compact: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	inject: {
-		narrow: {
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			conversation: [],
-			replies: []
-		};
-	},
-
-	created() {
-		if (this.detail) {
-			this.$root.api('notes/children', {
-				noteId: this.appearNote.id,
-				limit: 30
-			}).then(replies => {
-				this.replies = replies;
-			});
-
-			this.$root.api('notes/conversation', {
-				noteId: this.appearNote.replyId
-			}).then(conversation => {
-				this.conversation = conversation.reverse();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.note
-	margin 0
-	padding 0
-	overflow hidden
-	background var(--face)
-	border-bottom solid var(--lineWidth) var(--faceDivider)
-
-	&.mini
-		font-size 13px
-
-		> .renote
-			padding 8px 16px 0 16px
-
-			.avatar
-				width 20px
-				height 20px
-
-		> .article
-			padding 16px 16px 4px
-
-			> .avatar
-				margin 0 10px 8px 0
-				width 42px
-				height 42px
-
-	&:last-of-type
-		border-bottom none
-
-	&:focus
-		z-index 1
-
-		&:after
-			content ""
-			pointer-events none
-			position absolute
-			top 2px
-			right 2px
-			bottom 2px
-			left 2px
-			border 2px solid var(--primaryAlpha03)
-			border-radius 4px
-
-	> .renote + article
-		padding-top 8px
-
-	> .article
-		display flex
-		padding 28px 32px 18px 32px
-
-		&:hover
-			> .main > footer > button
-				color var(--noteActionsHighlighted)
-
-		> .avatar
-			flex-shrink 0
-			display block
-			margin 0 16px 10px 0
-			width 58px
-			height 58px
-			border-radius 8px
-			//position -webkit-sticky
-			//position sticky
-			//top 74px
-
-		> .main
-			flex 1
-			min-width 0
-
-			> .header
-				margin-bottom 4px
-
-			> .body
-
-				> .cw
-					cursor default
-					display block
-					margin 0
-					padding 0
-					overflow-wrap break-word
-					color var(--noteText)
-
-					> .text
-						margin-right 8px
-
-				> .content
-
-					> .text
-						cursor default
-						display block
-						margin 0
-						padding 0
-						overflow-wrap break-word
-						color var(--noteText)
-						font-size calc(1em + var(--fontSize))
-
-						> .reply
-							margin-right 8px
-							color var(--text)
-
-						> .rp
-							margin-left 4px
-							font-style oblique
-							color var(--renoteText)
-
-					> .location
-						margin 4px 0
-						font-size 12px
-						color #ccc
-
-					> .map
-						width 100%
-						height 300px
-
-						&:empty
-							display none
-
-					.mk-url-preview
-						margin-top 8px
-
-					> .mk-poll
-						font-size 80%
-
-					> .renote
-						margin 8px 0
-
-						> *
-							padding 16px
-							border dashed var(--lineWidth) var(--quoteBorder)
-							border-radius 8px
-
-			> .footer
-				> .app
-					display block
-					margin-top 0.5em
-					margin-left 0.5em
-					color var(--noteHeaderInfo)
-					font-size 0.8em
-
-				> .button
-					margin 0 28px 0 0
-					padding 0 8px
-					line-height 32px
-					font-size 1em
-					color var(--noteActions)
-					background transparent
-					border none
-					cursor pointer
-
-					&:last-child
-						margin-right 0
-
-					&:hover
-						color var(--noteActionsHover)
-
-					&.replyButton:hover
-						color var(--noteActionsReplyHover)
-
-					&.renoteButton:hover
-						color var(--noteActionsRenoteHover)
-
-					&.reactionButton:hover
-						color var(--noteActionsReactionHover)
-
-					&.inhibitedButton
-						cursor not-allowed
-
-					> .count
-						display inline
-						margin 0 0 0 8px
-						color var(--text)
-						opacity 0.7
-
-					&.reacted, &.reacted:hover
-						color var(--noteActionsReactionHover)
-
-			> .deleted
-				color var(--noteText)
-				opacity 0.7
-
-</style>
diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue
deleted file mode 100644
index 0820d5d80c40e51096fafe78df31d9d5cdc7699a..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/notes.vue
+++ /dev/null
@@ -1,182 +0,0 @@
-<template>
-<div class="mk-notes" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
-	<slot name="header"></slot>
-
-	<div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div>
-
-	<div class="empty" v-if="empty">{{ $t('@.no-notes') }}</div>
-
-	<mk-error v-if="error" @retry="init()"/>
-
-	<div class="placeholder" v-if="fetching">
-		<template v-for="i in 10">
-			<mk-note-skeleton :key="i"/>
-		</template>
-	</div>
-
-	<!-- トランジションを有効にするとなぜかメモリリークする -->
-	<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div" ref="notes">
-		<template v-for="(note, i) in _notes">
-			<mk-note :note="note" :key="note.id" :compact="true" ref="note"/>
-			<p class="date" :key="note.id + '_date'" v-if="i != items.length - 1 && note._date != _notes[i + 1]._date">
-				<span><fa icon="angle-up"/>{{ note._datetext }}</span>
-				<span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span>
-			</p>
-		</template>
-	</component>
-
-	<footer v-if="more">
-		<button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
-			<template v-if="!moreFetching">{{ $t('@.load-more') }}</template>
-			<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>
-		</button>
-	</footer>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import * as config from '../../../config';
-import shouldMuteNote from '../../../common/scripts/should-mute-note';
-import paging from '../../../common/scripts/paging';
-
-export default Vue.extend({
-	i18n: i18n(),
-
-	mixins: [
-		paging({
-			captureWindowScroll: true,
-
-			onQueueChanged: (self, x) => {
-				if (x.length > 0) {
-					self.$store.commit('indicate', true);
-				} else {
-					self.$store.commit('indicate', false);
-				}
-			},
-
-			onPrepend: (self, note, silent) => {
-				// 弾く
-				if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false;
-
-				// タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
-				if (document.hidden || !self.isScrollTop()) {
-					self.$store.commit('pushBehindNote', note);
-				}
-
-				if (self.isScrollTop()) {
-					// サウンドを再生する
-					if (self.$store.state.device.enableSounds && !silent) {
-						const sound = new Audio(`${config.url}/assets/post.mp3`);
-						sound.volume = self.$store.state.device.soundVolume;
-						sound.play();
-					}
-				}
-			},
-
-			onInited: (self) => {
-				self.$emit('loaded');
-			}
-		}),
-	],
-
-	props: {
-		pagination: {
-			required: true
-		},
-	},
-
-	computed: {
-		_notes(): any[] {
-			return (this.items as any).map(item => {
-				const date = new Date(item.createdAt).getDate();
-				const month = new Date(item.createdAt).getMonth() + 1;
-				item._date = date;
-				item._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
-				return item;
-			});
-		}
-	},
-
-	methods: {
-		focus() {
-			(this.$refs.notes as any).children[0].focus ? (this.$refs.notes as any).children[0].focus() : (this.$refs.notes as any).$el.children[0].focus();
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-notes
-	background var(--face)
-	overflow hidden
-
-	&.round
-		border-radius 6px
-
-	&.shadow
-		box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
-
-	.transition
-		.mk-notes-enter
-		.mk-notes-leave-to
-			opacity 0
-			transform translateY(-30px)
-
-		> *
-			transition transform .3s ease, opacity .3s ease
-
-	> .empty
-		padding 16px
-		text-align center
-		color var(--text)
-
-	> .placeholder
-		padding 32px
-		opacity 0.3
-
-	> .notes
-		> .date
-			display block
-			margin 0
-			line-height 32px
-			font-size 14px
-			text-align center
-			color var(--dateDividerFg)
-			background var(--dateDividerBg)
-			border-bottom solid var(--lineWidth) var(--faceDivider)
-
-			span
-				margin 0 16px
-
-			[data-icon]
-				margin-right 8px
-
-	> .newer-indicator
-		position -webkit-sticky
-		position sticky
-		z-index 100
-		height 3px
-		background var(--primary)
-
-	> footer
-		> button
-			display block
-			margin 0
-			padding 16px
-			width 100%
-			text-align center
-			color #ccc
-			background var(--face)
-			border-top solid var(--lineWidth) var(--faceDivider)
-			border-bottom-left-radius 6px
-			border-bottom-right-radius 6px
-
-			&:hover
-				box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
-
-			&:active
-				box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
-
-</style>
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
deleted file mode 100644
index a2504abe66c28e709699389a774ca958deb60396..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/notifications.vue
+++ /dev/null
@@ -1,379 +0,0 @@
-<template>
-<div class="mk-notifications">
-	<div class="placeholder" v-if="fetching">
-		<template v-for="i in 10">
-			<mk-note-skeleton :key="i"/>
-		</template>
-	</div>
-
-	<div class="notifications" v-if="!empty">
-		<!-- トランジションを有効にするとなぜかメモリリークする -->
-		<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition" tag="div">
-			<template v-for="(notification, i) in _notifications">
-				<div class="notification" :class="notification.type" :key="notification.id">
-					<template v-if="notification.type == 'reaction'">
-						<mk-avatar class="avatar" :user="notification.user"/>
-						<div class="text">
-							<header>
-								<mk-reaction-icon :reaction="notification.reaction" class="icon"/>
-								<router-link :to="notification.user | userPage" v-user-preview="notification.user.id" class="name">
-									<mk-user-name :user="notification.user"/>
-								</router-link>
-								<mk-time :time="notification.createdAt"/>
-							</header>
-							<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
-								<fa icon="quote-left"/>
-									<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
-								<fa icon="quote-right"/>
-							</router-link>
-						</div>
-					</template>
-
-					<template v-if="notification.type == 'renote'">
-						<mk-avatar class="avatar" :user="notification.note.user"/>
-						<div class="text">
-							<header>
-								<fa icon="retweet" class="icon"/>
-								<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId" class="name">
-									<mk-user-name :user="notification.note.user"/>
-								</router-link>
-								<mk-time :time="notification.createdAt"/>
-							</header>
-							<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)">
-								<fa icon="quote-left"/>
-									<mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :custom-emojis="notification.note.renote.emojis"/>
-								<fa icon="quote-right"/>
-							</router-link>
-						</div>
-					</template>
-
-					<template v-if="notification.type == 'quote'">
-						<mk-avatar class="avatar" :user="notification.note.user"/>
-						<div class="text">
-							<header>
-								<fa icon="quote-left" class="icon"/>
-								<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId" class="name">
-									<mk-user-name :user="notification.note.user"/>
-								</router-link>
-								<mk-time :time="notification.createdAt"/>
-							</header>
-							<router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
-								<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
-							</router-link>
-						</div>
-					</template>
-
-					<template v-if="notification.type == 'follow'">
-						<mk-avatar class="avatar" :user="notification.user"/>
-						<div class="text">
-							<header>
-								<fa icon="user-plus" class="icon"/>
-								<router-link :to="notification.user | userPage" v-user-preview="notification.user.id" class="name">
-									<mk-user-name :user="notification.user"/>
-								</router-link>
-								<mk-time :time="notification.createdAt"/>
-							</header>
-						</div>
-					</template>
-
-					<template v-if="notification.type == 'receiveFollowRequest'">
-						<mk-avatar class="avatar" :user="notification.user"/>
-						<div class="text">
-							<header>
-								<fa icon="user-clock" class="icon"/>
-								<router-link :to="notification.user | userPage" v-user-preview="notification.user.id" class="name">
-									<mk-user-name :user="notification.user"/>
-								</router-link>
-								<mk-time :time="notification.createdAt"/>
-							</header>
-						</div>
-					</template>
-
-					<template v-if="notification.type == 'reply'">
-						<mk-avatar class="avatar" :user="notification.note.user"/>
-						<div class="text">
-							<header>
-								<fa icon="reply" class="icon"/>
-								<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId" class="name">
-									<mk-user-name :user="notification.note.user"/>
-								</router-link>
-								<mk-time :time="notification.createdAt"/>
-							</header>
-							<router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
-								<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
-							</router-link>
-						</div>
-					</template>
-
-					<template v-if="notification.type == 'mention'">
-						<mk-avatar class="avatar" :user="notification.note.user"/>
-						<div class="text">
-							<header>
-								<fa icon="at" class="icon"/>
-								<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId" class="name">
-									<mk-user-name :user="notification.note.user"/>
-								</router-link>
-								<mk-time :time="notification.createdAt"/>
-							</header>
-							<router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
-								<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
-							</router-link>
-						</div>
-					</template>
-
-					<template v-if="notification.type == 'pollVote'">
-						<mk-avatar class="avatar" :user="notification.user"/>
-						<div class="text">
-							<header>
-								<fa icon="chart-pie" class="icon"/>
-								<router-link :to="notification.user | userPage" v-user-preview="notification.user.id" class="name">
-									<mk-user-name :user="notification.user"/>
-								</router-link>
-								<mk-time :time="notification.createdAt"/>
-							</header>
-							<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
-								<fa icon="quote-left"/>
-									<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
-								<fa icon="quote-right"/>
-							</router-link>
-						</div>
-					</template>
-				</div>
-
-				<p class="date" v-if="i != items.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
-					<span><fa icon="angle-up"/>{{ notification._datetext }}</span>
-					<span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span>
-				</p>
-			</template>
-		</component>
-	</div>
-	<button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMore" :disabled="moreFetching">
-		<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? $t('@.loading') : $t('@.load-more') }}
-	</button>
-	<p class="empty" v-if="empty">{{ $t('empty') }}</p>
-	<mk-error v-if="error" @retry="init()"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import getNoteSummary from '../../../../../misc/get-note-summary';
-import paging from '../../../common/scripts/paging';
-
-export default Vue.extend({
-	i18n: i18n(),
-
-	mixins: [
-		paging({
-			isContainer: true
-		}),
-	],
-
-	props: {
-		type: {
-			type: String,
-			required: false
-		}
-	},
-
-	data() {
-		return {
-			connection: null,
-			getNoteSummary,
-			pagination: {
-				endpoint: 'i/notifications',
-				limit: 10,
-				params: () => ({
-					includeTypes: this.type ? [this.type] : undefined
-				})
-			}
-		};
-	},
-
-	computed: {
-		_notifications(): any[] {
-			return (this.items as any).map(notification => {
-				const date = new Date(notification.createdAt).getDate();
-				const month = new Date(notification.createdAt).getMonth() + 1;
-				notification._date = date;
-				notification._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
-				return notification;
-			});
-		}
-	},
-
-	watch: {
-		type() {
-			this.reload();
-		}
-	},
-
-	mounted() {
-		this.connection = this.$root.stream.useSharedConnection('main');
-		this.connection.on('notification', this.onNotification);
-	},
-
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		onNotification(notification) {
-			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
-			this.$root.stream.send('readNotification', {
-				id: notification.id
-			});
-
-			this.prepend(notification);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-notifications
-	.transition
-		.mk-notifications-enter
-		.mk-notifications-leave-to
-			opacity 0
-			transform translateY(-30px)
-
-		> *
-			transition transform .3s ease, opacity .3s ease
-
-	> .placeholder
-		padding 16px
-		opacity 0.3
-
-	> .notifications
-		> div
-			> .notification
-				margin 0
-				padding 16px
-				overflow-wrap break-word
-				font-size 12px
-				border-bottom solid var(--lineWidth) var(--faceDivider)
-
-				&:last-child
-					border-bottom none
-
-				&:after
-					content ""
-					display block
-					clear both
-
-				> .avatar
-					display block
-					float left
-					position -webkit-sticky
-					position sticky
-					top 16px
-					width 36px
-					height 36px
-					border-radius 6px
-
-				> .text
-					float right
-					width calc(100% - 36px)
-					padding-left 8px
-
-					> header
-						display flex
-						align-items baseline
-						white-space nowrap
-
-						> .icon
-							margin-right 4px
-
-						> .name
-							overflow hidden
-							text-overflow ellipsis
-
-						> .mk-time
-							margin-left auto
-							color var(--noteHeaderInfo)
-							font-size 0.9em
-
-				.note-preview
-					color var(--noteText)
-					display inline-block
-					word-break break-word
-
-				.note-ref
-					color var(--noteText)
-					display inline-block
-					width: 100%
-					overflow hidden
-					white-space nowrap
-					text-overflow ellipsis
-
-					[data-icon]
-						font-size 1em
-						font-weight normal
-						font-style normal
-						display inline-block
-						margin-right 3px
-
-				&.reaction
-					.text header
-						align-items normal
-
-				&.renote, &.quote
-					.text header [data-icon]
-						color #77B255
-
-				&.follow
-					.text header [data-icon]
-						color #53c7ce
-
-				&.receiveFollowRequest
-					.text header [data-icon]
-						color #888
-
-				&.reply, &.mention
-					.text header [data-icon]
-						color #555
-
-			> .date
-				display block
-				margin 0
-				line-height 32px
-				text-align center
-				font-size 0.8em
-				color var(--dateDividerFg)
-				background var(--dateDividerBg)
-				border-bottom solid var(--lineWidth) var(--faceDivider)
-
-				span
-					margin 0 16px
-
-				[data-icon]
-					margin-right 8px
-
-	> .more
-		display block
-		width 100%
-		padding 16px
-		color var(--text)
-		border-top solid var(--lineWidth) rgba(#000, 0.05)
-
-		&:hover
-			background rgba(#000, 0.025)
-
-		&:active
-			background rgba(#000, 0.05)
-
-		&.fetching
-			cursor wait
-
-		> [data-icon]
-			margin-right 4px
-
-	> .empty
-		margin 0
-		padding 16px
-		text-align center
-		color var(--text)
-
-</style>
diff --git a/src/client/app/desktop/views/components/post-form-window.vue b/src/client/app/desktop/views/components/post-form-window.vue
deleted file mode 100644
index ff6f24b6e1ecdf08f4359ae177b3866f07951854..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/post-form-window.vue
+++ /dev/null
@@ -1,140 +0,0 @@
-<template>
-<mk-window class="mk-post-form-window" ref="window" is-modal @closed="onWindowClosed" :animation="animation">
-	<template #header>
-		<span class="mk-post-form-window--header">
-			<span class="icon" v-if="geo"><fa icon="map-marker-alt"/></span>
-			<span v-if="!reply">{{ $t('note') }}</span>
-			<span v-if="reply">{{ $t('reply') }}</span>
-			<span class="count" v-if="files.length != 0">{{ $t('attaches').replace('{}', files.length) }}</span>
-			<span class="count" v-if="uploadings.length != 0">{{ $t('uploading-media').replace('{}', uploadings.length) }}<mk-ellipsis/></span>
-		</span>
-	</template>
-
-	<div class="mk-post-form-window--body" :style="{ maxHeight: `${maxHeight}px` }">
-		<mk-note-preview v-if="reply" class="notePreview" :note="reply"/>
-		<x-post-form ref="form"
-			:reply="reply"
-			:mention="mention"
-			:initial-text="initialText"
-			:initial-note="initialNote"
-			:instant="instant"
-
-			@posted="onPosted"
-			@change-uploadings="onChangeUploadings"
-			@change-attached-files="onChangeFiles"
-			@geo-attached="onGeoAttached"
-			@geo-dettached="onGeoDettached"/>
-	</div>
-</mk-window>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XPostForm from './post-form.vue';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/post-form-window.vue'),
-
-	components: {
-		XPostForm
-	},
-
-	props: {
-		reply: {
-			type: Object,
-			required: false
-		},
-		mention: {
-			type: Object,
-			required: false
-		},
-
-		animation: {
-			type: Boolean,
-			required: false,
-			default: true
-		},
-
-		initialText: {
-			type: String,
-			required: false
-		},
-
-		initialNote: {
-			type: Object,
-			required: false
-		},
-
-		instant: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	data() {
-		return {
-			uploadings: [],
-			files: [],
-			geo: null
-		};
-	},
-
-	computed: {
-		maxHeight() {
-			return window.innerHeight - 50;
-		},
-	},
-
-	mounted() {
-		this.$nextTick(() => {
-			(this.$refs.form as any).focus();
-		});
-	},
-
-	methods: {
-		onChangeUploadings(files) {
-			this.uploadings = files;
-		},
-		onChangeFiles(files) {
-			this.files = files;
-		},
-		onGeoAttached(geo) {
-			this.geo = geo;
-		},
-		onGeoDettached() {
-			this.geo = null;
-		},
-		onPosted() {
-			(this.$refs.window as any).close();
-		},
-		onWindowClosed() {
-			this.$emit('closed');
-			this.destroyDom();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-post-form-window
-	.mk-post-form-window--header
-		.icon
-			margin-right 8px
-
-		.count
-			margin-left 8px
-			opacity 0.8
-
-			&:before
-				content '('
-
-			&:after
-				content ')'
-
-	.mk-post-form-window--body
-		.notePreview
-				margin 16px 22px
-
-</style>
diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
deleted file mode 100644
index b9c0624bd73f0e7d07b48bf996157e90de56fd1d..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/post-form.vue
+++ /dev/null
@@ -1,331 +0,0 @@
-<template>
-<div class="gjisdzwh"
-	@dragover.stop="onDragover"
-	@dragenter="onDragenter"
-	@dragleave="onDragleave"
-	@drop.stop="onDrop"
->
-	<div class="content">
-		<div class="hashtags" v-if="recentHashtags.length > 0 && $store.state.settings.suggestRecentHashtags">
-			<b>{{ $t('@.post-form.recent-tags') }}:</b>
-			<a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" :title="$t('@.post-form.click-to-tagging')">#{{ tag }}</a>
-		</div>
-		<div class="with-quote" v-if="quoteId"><fa icon="quote-left"/> {{ $t('@.post-form.quote-attached') }}<button @click="quoteId = null"><fa icon="times"/></button></div>
-		<div v-if="visibility === 'specified'" class="to-specified">
-			<fa icon="envelope"/> {{ $t('@.post-form.specified-recipient') }}
-			<div class="visibleUsers">
-				<span v-for="u in visibleUsers">
-					<mk-user-name :user="u"/>
-					<button @click="removeVisibleUser(u)"><fa icon="times"/></button>
-				</span>
-				<button @click="addVisibleUser">{{ $t('@.post-form.add-visible-user') }}</button>
-			</div>
-		</div>
-		<div class="local-only" v-if="localOnly === true"><fa icon="heart"/> {{ $t('@.post-form.local-only-message') }}</div>
-		<input v-show="useCw" ref="cw" v-model="cw" :placeholder="$t('@.post-form.cw-placeholder')" v-autocomplete="{ model: 'cw' }">
-		<div class="textarea">
-			<textarea :class="{ with: (files.length != 0 || poll) }"
-				ref="text" v-model="text" :disabled="posting"
-				@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
-				v-autocomplete="{ model: 'text' }"
-			></textarea>
-			<button class="emoji" @click="emoji" ref="emoji">
-				<fa :icon="['far', 'laugh']"/>
-			</button>
-			<x-post-form-attaches class="files" :class="{ with: poll }" :files="files"/>
-			<x-poll-editor class="poll-editor" v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/>
-		</div>
-	</div>
-	<mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
-	<button class="upload" :title="$t('@.post-form.attach-media-from-local')" @click="chooseFile"><fa icon="upload"/></button>
-	<button class="drive" :title="$t('@.post-form.attach-media-from-drive')" @click="chooseFileFromDrive"><fa icon="cloud"/></button>
-	<button class="kao" :title="$t('@.post-form.insert-a-kao')" @click="kao"><fa :icon="['far', 'smile']"/></button>
-	<button class="poll" :title="$t('@.post-form.create-poll')" @click="poll = !poll"><fa icon="chart-pie"/></button>
-	<button class="cw" :title="$t('@.post-form.hide-contents')" @click="useCw = !useCw"><fa :icon="['far', 'eye-slash']"/></button>
-	<button class="geo" :title="$t('@.post-form.attach-location-information')" @click="geo ? removeGeo() : setGeo()"><fa icon="map-marker-alt"/></button>
-	<button class="visibility" :title="$t('@.post-form.visibility')" @click="setVisibility" ref="visibilityButton">
-		<span v-if="visibility === 'public'"><fa icon="globe"/></span>
-		<span v-if="visibility === 'home'"><fa icon="home"/></span>
-		<span v-if="visibility === 'followers'"><fa icon="unlock"/></span>
-		<span v-if="visibility === 'specified'"><fa icon="envelope"/></span>
-	</button>
-	<p class="text-count" :class="{ over: trimmedLength(text) > maxNoteTextLength }">{{ maxNoteTextLength - trimmedLength(text) }}</p>
-	<ui-button primary :wait="posting" class="submit" :disabled="!canPost" @click="post">
-		{{ posting ? $t('@.post-form.posting') : submitText }}<mk-ellipsis v-if="posting"/>
-	</ui-button>
-	<input ref="file" type="file" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
-	<div class="dropzone" v-if="draghover"></div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import form from '../../../common/scripts/post-form';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/post-form.vue'),
-
-	mixins: [
-		form({
-			onSuccess: self => {
-				self.$notify(self.renote
-					? self.$t('reposted')
-					: self.reply
-						? self.$t('replied')
-						: self.$t('posted'));
-			},
-			onFailure: self => {
-				self.$notify(self.renote
-					? self.$t('renote-failed')
-					: self.reply
-						? self.$t('reply-failed')
-						: self.$t('note-failed'));
-			}
-		}),
-	],
-});
-</script>
-
-<style lang="stylus" scoped>
-.gjisdzwh
-	display block
-	padding 16px
-	background var(--desktopPostFormBg)
-	overflow hidden
-
-	&:after
-		content ""
-		display block
-		clear both
-
-	> .content
-		> input
-		> .textarea > textarea
-			display block
-			width 100%
-			padding 12px
-			font-size 16px
-			color var(--desktopPostFormTextareaFg)
-			background var(--desktopPostFormTextareaBg)
-			outline none
-			border solid 1px var(--primaryAlpha01)
-			border-radius 4px
-			transition border-color .2s ease
-			padding-right 30px
-
-			&:hover
-				border-color var(--primaryAlpha02)
-				transition border-color .1s ease
-
-			&:focus
-				border-color var(--primaryAlpha05)
-				transition border-color 0s ease
-
-			&:disabled
-				opacity 0.5
-
-			&::-webkit-input-placeholder
-				color var(--primaryAlpha03)
-
-		> input
-			margin-bottom 8px
-
-		> .textarea
-			> .emoji
-				position absolute
-				top 0
-				right 0
-				padding 10px
-				font-size 18px
-				color var(--text)
-				opacity 0.5
-
-				&:hover
-					color var(--textHighlighted)
-					opacity 1
-
-				&:active
-					color var(--primary)
-					opacity 1
-
-			> textarea
-				margin 0
-				max-width 100%
-				min-width 100%
-				min-height 84px
-
-				&:hover
-					& + * + *
-					& + * + * + *
-						border-color var(--primaryAlpha02)
-						transition border-color .1s ease
-
-				&:focus
-					& + * + *
-					& + * + * + *
-						border-color var(--primaryAlpha05)
-						transition border-color 0s ease
-
-					& + .emoji
-						opacity 0.7
-
-				&.with
-					border-bottom solid 1px var(--primaryAlpha01) !important
-					border-radius 4px 4px 0 0
-
-			> .files
-				margin 0
-				padding 0
-				background var(--desktopPostFormTextareaBg)
-				border solid 1px var(--primaryAlpha01)
-				border-top none
-				border-radius 0 0 4px 4px
-				transition border-color .3s ease
-
-				&.with
-					border-bottom solid 1px var(--primaryAlpha01) !important
-					border-radius 0
-
-			> .poll-editor
-				background var(--desktopPostFormTextareaBg)
-				border solid 1px var(--primaryAlpha01)
-				border-top none
-				border-radius 0 0 4px 4px
-				transition border-color .3s ease
-
-		> .hashtags
-			margin 0 0 8px 0
-			overflow hidden
-			white-space nowrap
-			font-size 14px
-
-			> b
-				color var(--primary)
-
-			> *
-				margin-right 8px
-				white-space nowrap
-
-		> .with-quote
-			margin 0 0 8px 0
-			color var(--primary)
-
-			> button
-				padding 4px 8px
-				color var(--primaryAlpha04)
-
-				&:hover
-					color var(--primaryAlpha06)
-
-				&:active
-					color var(--primaryDarken30)
-
-		> .to-specified
-			margin 0 0 8px 0
-			color var(--primary)
-
-			> .visibleUsers
-				display inline
-				top -1px
-				font-size 14px
-
-				> span
-					margin-left 14px
-
-					> button
-						padding 4px 8px
-						color var(--primaryAlpha04)
-
-						&:hover
-							color var(--primaryAlpha06)
-
-						&:active
-							color var(--primaryDarken30)
-
-		> .local-only
-			margin 0 0 8px 0
-			color var(--primary)
-
-	> .mk-uploader
-		margin 8px 0 0 0
-		padding 8px
-		border solid 1px var(--primaryAlpha02)
-		border-radius 4px
-
-	input[type='file']
-		display none
-
-	.submit
-		display block
-		position absolute
-		bottom 16px
-		right 16px
-		width 110px
-		height 40px
-
-	> .text-count
-		pointer-events none
-		display block
-		position absolute
-		bottom 16px
-		right 138px
-		margin 0
-		line-height 40px
-		color var(--primaryAlpha05)
-
-		&.over
-			color #ec3828
-
-	> .upload
-	> .drive
-	> .kao
-	> .poll
-	> .cw
-	> .geo
-	> .visibility
-		display inline-block
-		cursor pointer
-		padding 0
-		margin 8px 4px 0 0
-		width 40px
-		height 40px
-		font-size 1em
-		color var(--desktopPostFormTransparentButtonFg)
-		background transparent
-		outline none
-		border solid 1px transparent
-		border-radius 4px
-
-		&:hover
-			background transparent
-			border-color var(--primaryAlpha03)
-
-		&:active
-			color var(--primaryAlpha06)
-			background linear-gradient(to bottom, var(--desktopPostFormTransparentButtonActiveGradientStart) 0%, var(--desktopPostFormTransparentButtonActiveGradientEnd) 100%)
-			border-color var(--primaryAlpha05)
-			box-shadow 0 2px 4px rgba(#000, 0.15) inset
-
-		&:focus
-			&:after
-				content ""
-				pointer-events none
-				position absolute
-				top -5px
-				right -5px
-				bottom -5px
-				left -5px
-				border 2px solid var(--primaryAlpha03)
-				border-radius 8px
-
-	> .dropzone
-		position absolute
-		left 0
-		top 0
-		width 100%
-		height 100%
-		border dashed 2px var(--primaryAlpha05)
-		pointer-events none
-
-</style>
diff --git a/src/client/app/desktop/views/components/progress-dialog.vue b/src/client/app/desktop/views/components/progress-dialog.vue
deleted file mode 100644
index 28b35dbd97030f2a7b3dc5cafa4eb7aeb187f82c..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/progress-dialog.vue
+++ /dev/null
@@ -1,98 +0,0 @@
-<template>
-<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="destroyDom">
-	<template #header>{{ title }}<mk-ellipsis/></template>
-	<div :class="$style.body">
-		<p :class="$style.init" v-if="isNaN(value)">{{ $t('waiting') }}<mk-ellipsis/></p>
-		<p :class="$style.percentage" v-if="!isNaN(value)">{{ Math.floor((value / max) * 100) }}</p>
-		<progress :class="$style.progress"
-			v-if="!isNaN(value) && value < max"
-			:value="isNaN(value) ? 0 : value"
-			:max="max"
-		></progress>
-		<div :class="[$style.progress, $style.waiting]" v-if="value >= max"></div>
-	</div>
-</mk-window>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/progress-dialog.vue'),
-	props: ['title', 'initValue', 'initMax'],
-	data() {
-		return {
-			value: this.initValue,
-			max: this.initMax
-		};
-	},
-	methods: {
-		update(value, max) {
-			this.value = parseInt(value, 10);
-			this.max = parseInt(max, 10);
-		},
-		close() {
-			(this.$refs.window as any).close();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" module>
-
-
-.body
-	padding 18px 24px 24px 24px
-
-.init
-	display block
-	margin 0
-	text-align center
-	color rgba(#000, 0.7)
-
-.percentage
-	display block
-	margin 0 0 4px 0
-	text-align center
-	line-height 16px
-	color var(--primaryAlpha07)
-
-	&:after
-		content '%'
-
-.progress
-	display block
-	margin 0
-	width 100%
-	height 10px
-	background transparent
-	border none
-	border-radius 4px
-	overflow hidden
-
-	&::-webkit-progress-value
-		background var(--primary)
-
-	&::-webkit-progress-bar
-		background var(--primaryAlpha01)
-
-.waiting
-	background linear-gradient(
-		45deg,
-		var(--primaryLighten30) 25%,
-		var(--primary)               25%,
-		var(--primary)               50%,
-		var(--primaryLighten30) 50%,
-		var(--primaryLighten30) 75%,
-		var(--primary)               75%,
-		var(--primary)
-	)
-	background-size 32px 32px
-	animation progress-dialog-tag-progress-waiting 1.5s linear infinite
-
-	@keyframes progress-dialog-tag-progress-waiting
-		from {background-position: 0 0;}
-		to   {background-position: -64px 32px;}
-
-</style>
diff --git a/src/client/app/desktop/views/components/renote-form-window.vue b/src/client/app/desktop/views/components/renote-form-window.vue
deleted file mode 100644
index 0ca347b530fb0bc4d8e43938f849006422b5c51f..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/renote-form-window.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<template>
-<mk-window ref="window" is-modal @closed="onWindowClosed" :animation="animation">
-	<template #header :class="$style.header"><fa icon="retweet"/>{{ $t('title') }}</template>
-	<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled" v-hotkey.global="keymap"/>
-</mk-window>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/renote-form-window.vue'),
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-
-		animation: {
-			type: Boolean,
-			required: false,
-			default: true
-		}
-	},
-
-	computed: {
-		keymap(): any {
-			return {
-				'esc': this.close,
-				'enter': this.post,
-				'q': this.quote,
-			};
-		}
-	},
-
-	methods: {
-		post() {
-			(this.$refs.form as any).ok();
-		},
-		quote() {
-			(this.$refs.form as any).onQuote();
-		},
-		close() {
-			(this.$refs.window as any).close();
-		},
-		onPosted() {
-			(this.$refs.window as any).close();
-		},
-		onCanceled() {
-			(this.$refs.window as any).close();
-		},
-		onWindowClosed() {
-			this.$emit('closed');
-			this.destroyDom();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" module>
-.header
-	> [data-icon]
-		margin-right 4px
-
-</style>
diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue
deleted file mode 100644
index 53fbf0ff3027f173782762735de1f763ad8c9e74..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/renote-form.vue
+++ /dev/null
@@ -1,111 +0,0 @@
-<template>
-<div class="mk-renote-form">
-	<mk-note-preview class="preview" :note="note"/>
-	<template v-if="!quote">
-		<footer>
-			<a class="quote" v-if="!quote" @click="onQuote">{{ $t('quote') }}</a>
-			<ui-button class="button cancel" inline @click="cancel">{{ $t('cancel') }}</ui-button>
-			<ui-button class="button home" inline :primary="visibility != 'public'" @click="ok('home')"   :disabled="wait">{{ wait ? this.$t('reposting') : this.$t('renote-home') }}</ui-button>
-			<ui-button class="button ok"   inline :primary="visibility == 'public'" @click="ok('public')" :disabled="wait">{{ wait ? this.$t('reposting') : this.$t('renote') }}</ui-button>
-		</footer>
-	</template>
-	<template v-if="quote">
-		<x-post-form ref="form" :renote="note" @posted="onChildFormPosted"/>
-	</template>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/renote-form.vue'),
-
-	components: {
-		XPostForm: () => import('./post-form.vue').then(m => m.default)
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			wait: false,
-			quote: false,
-			visibility: this.$store.state.settings.defaultNoteVisibility
-		};
-	},
-
-	methods: {
-		ok(v: string) {
-			this.wait = true;
-			this.$root.api('notes/create', {
-				renoteId: this.note.id,
-				visibility: v || this.visibility
-			}).then(data => {
-				this.$emit('posted');
-				this.$notify(this.$t('success'));
-			}).catch(err => {
-				this.$notify(this.$t('failure'));
-			}).then(() => {
-				this.wait = false;
-			});
-		},
-
-		cancel() {
-			this.$emit('canceled');
-		},
-
-		onQuote() {
-			this.quote = true;
-
-			this.$nextTick(() => {
-				(this.$refs.form as any).focus();
-			});
-		},
-
-		onChildFormPosted() {
-			this.$emit('posted');
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-renote-form
-	> .preview
-		margin 16px 22px
-
-	> footer
-		height 72px
-		background var(--desktopRenoteFormFooter)
-
-		> .quote
-			position absolute
-			bottom 16px
-			left 28px
-			line-height 40px
-
-		> .button
-			display block
-			position absolute
-			bottom 16px
-			width 120px
-			height 40px
-
-			&.cancel
-				right 280px
-
-			&.home
-				right 148px
-				font-size 13px
-
-			&.ok
-				right 16px
-
-</style>
diff --git a/src/client/app/desktop/views/components/settings-window.vue b/src/client/app/desktop/views/components/settings-window.vue
deleted file mode 100644
index 9bfd5a14c79b82e996beef8cbb6a7a5df9d09a80..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/settings-window.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-<template>
-<mk-window ref="window" is-modal width="700px" height="550px" @closed="destroyDom">
-	<template #header :class="$style.header"><fa icon="cog"/>{{ $t('@.settings') }}</template>
-	<x-settings :initial-page="initialPage" @done="close"/>
-</mk-window>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/settings-window.vue'),
-
-	components: {
-		XSettings: () => import('./settings.vue').then(m => m.default)
-	},
-
-	props: {
-		initialPage: {
-			type: String,
-			required: false
-		}
-	},
-	methods: {
-		close() {
-			(this as any).$refs.window.close();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" module>
-.header
-	> [data-icon]
-		margin-right 4px
-
-</style>
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
deleted file mode 100644
index 65701cd5f388c54e353af452ec96281a93fab7ec..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/settings.vue
+++ /dev/null
@@ -1,86 +0,0 @@
-<template>
-<div class="mk-settings">
-	<div class="nav" :class="{ inWindow }">
-		<router-link to="/i/settings/profile" active-class="active"><fa icon="user" fixed-width/>{{ $t('@._settings.profile') }}</router-link>
-		<router-link to="/i/settings/appearance" active-class="active"><fa icon="palette" fixed-width/>{{ $t('@._settings.appearance') }}</router-link>
-		<router-link to="/i/settings/behavior" active-class="active"><fa icon="desktop" fixed-width/>{{ $t('@._settings.behavior') }}</router-link>
-		<router-link to="/i/settings/notification" active-class="active"><fa :icon="['far', 'bell']" fixed-width/>{{ $t('@._settings.notification') }}</router-link>
-		<router-link to="/i/settings/drive" active-class="active"><fa icon="cloud" fixed-width/>{{ $t('@.drive') }}</router-link>
-		<router-link to="/i/settings/hashtags" active-class="active"><fa icon="hashtag" fixed-width/>{{ $t('@._settings.tags') }}</router-link>
-		<router-link to="/i/settings/muteAndBlock" active-class="active"><fa icon="ban" fixed-width/>{{ $t('@._settings.mute-and-block') }}</router-link>
-		<router-link to="/i/settings/apps" active-class="active"><fa icon="puzzle-piece" fixed-width/>{{ $t('@._settings.apps') }}</router-link>
-		<router-link to="/i/settings/security" active-class="active"><fa icon="unlock-alt" fixed-width/>{{ $t('@._settings.security') }}</router-link>
-		<router-link to="/i/settings/api" active-class="active"><fa icon="key" fixed-width/>API</router-link>
-		<router-link to="/i/settings/other" active-class="active"><fa icon="cogs" fixed-width/>{{ $t('@._settings.other') }}</router-link>
-	</div>
-	<div class="pages">
-		<x-settings :page="page"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XSettings from '../../../common/views/components/settings/settings.vue';
-
-export default Vue.extend({
-	i18n: i18n(),
-	components: {
-		XSettings,
-	},
-	props: {
-		page: {
-			type: String,
-			required: true,
-		},
-		inWindow: {
-			type: Boolean,
-			required: false,
-			default: true
-		}
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-settings
-	display flex
-	width 100%
-	height 100%
-
-	> .nav
-		flex 0 0 200px
-		width 100%
-		height 100%
-		padding 16px 0 0 0
-		overflow auto
-		z-index 1
-		font-size 15px
-
-		> a
-			display block
-			padding 10px 16px
-			margin 0
-			color var(--desktopSettingsNavItem)
-			cursor pointer
-			user-select none
-			transition margin-left 0.2s ease
-
-			> [data-icon]
-				margin-right 4px
-
-			&:hover
-				color var(--desktopSettingsNavItemHover)
-
-			&.active
-				margin-left 8px
-				color var(--primary) !important
-
-	> .pages
-		width 100%
-		height 100%
-		flex auto
-		overflow auto
-
-</style>
diff --git a/src/client/app/desktop/views/components/sub-note-content.vue b/src/client/app/desktop/views/components/sub-note-content.vue
deleted file mode 100644
index 78f9a6034be4c7d5341286aac4ae590d7f3450e5..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/sub-note-content.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<template>
-<div class="mk-sub-note-content">
-	<div class="body">
-		<span v-if="note.isHidden" style="opacity: 0.5">{{ $t('private') }}</span>
-		<span v-if="note.deletedAt" style="opacity: 0.5">{{ $t('deleted') }}</span>
-		<a class="reply" v-if="note.replyId"><fa icon="reply"/></a>
-		<mfm v-if="note.text" :text="note.text" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis"/>
-		<a class="rp" v-if="note.renoteId" :href="`/notes/${note.renoteId}`">RN: ...</a>
-	</div>
-	<details v-if="note.files.length > 0">
-		<summary>({{ this.$t('media-count').replace('{}', note.files.length) }})</summary>
-		<mk-media-list :media-list="note.files"/>
-	</details>
-	<details v-if="note.poll">
-		<summary>{{ $t('poll') }}</summary>
-		<mk-poll :note="note"/>
-	</details>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/sub-note-content.vue'),
-	props: ['note']
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-sub-note-content
-	overflow-wrap break-word
-
-	> .body
-		> .reply
-			margin-right 6px
-			color #717171
-
-		> .rp
-			margin-left 4px
-			font-style oblique
-			color var(--renoteText)
-
-	mk-poll
-		font-size 80%
-
-</style>
diff --git a/src/client/app/desktop/views/components/ui-container.vue b/src/client/app/desktop/views/components/ui-container.vue
deleted file mode 100644
index 59954fee8eb7fff62152d9d8e006e91a4e5a72d8..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/ui-container.vue
+++ /dev/null
@@ -1,138 +0,0 @@
-<template>
-<div class="kedshtep" :class="{ naked, inNakedDeckColumn, shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
-	<header v-if="showHeader" :class="{ bodyTogglable }" @click="toggleContent(!showBody)">
-		<div class="title"><slot name="header"></slot></div>
-		<slot name="func"></slot>
-		<button v-if="bodyTogglable">
-			<template v-if="showBody"><fa icon="angle-up"/></template>
-			<template v-else><fa icon="angle-down"/></template>
-		</button>
-	</header>
-	<div v-show="showBody">
-		<slot></slot>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	props: {
-		showHeader: {
-			type: Boolean,
-			default: true
-		},
-		naked: {
-			type: Boolean,
-			default: false
-		},
-		bodyTogglable: {
-			type: Boolean,
-			default: false
-		},
-		expanded: {
-			type: Boolean,
-			default: true
-		},
-	},
-	inject: {
-		inNakedDeckColumn: {
-			default: false
-		}
-	},
-	data() {
-		return {
-			showBody: this.expanded
-		};
-	},
-	methods: {
-		toggleContent(show: boolean) {
-			if (!this.bodyTogglable) return;
-			this.showBody = show;
-			this.$emit('toggle', show);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.kedshtep
-	overflow hidden
-
-	&:not(.inNakedDeckColumn)
-		background var(--face)
-
-		&.round
-			border-radius 6px
-
-		&.shadow
-			box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
-
-		& + .kedshtep
-			margin-top 16px
-
-		&.naked
-			background transparent !important
-			box-shadow none !important
-
-		> header
-			background var(--faceHeader)
-
-			&.bodyTogglable
-				cursor pointer
-
-			> .title
-				z-index 1
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color var(--faceHeaderText)
-				box-shadow 0 var(--lineWidth) rgba(#000, 0.07)
-
-				> [data-icon]
-					margin-right 6px
-
-				&:empty
-					display none
-
-			> button
-				position absolute
-				z-index 2
-				top 0
-				right 0
-				padding 0
-				width 42px
-				font-size 0.9em
-				line-height 42px
-				color var(--faceTextButton)
-
-				&:hover
-					color var(--faceTextButtonHover)
-
-				&:active
-					color var(--faceTextButtonActive)
-
-	&.inNakedDeckColumn
-		background var(--face)
-
-		> header
-			margin 0
-			padding 8px 16px
-			font-size 12px
-			color var(--text)
-			background var(--deckColumnBg)
-
-			&.bodyTogglable
-				cursor pointer
-
-			> button
-				position absolute
-				top 0
-				right 8px
-				padding 8px 6px
-				font-size 14px
-				color var(--text)
-
-</style>
diff --git a/src/client/app/desktop/views/components/ui-notification.vue b/src/client/app/desktop/views/components/ui-notification.vue
deleted file mode 100644
index 52e8e1d6cb0ca0b44c775e43548d6cd54c9edc64..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/ui-notification.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<template>
-<div class="mk-ui-notification">
-	<p>{{ message }}</p>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import anime from 'animejs';
-
-export default Vue.extend({
-	props: ['message'],
-	mounted() {
-		this.$nextTick(() => {
-			anime({
-				targets: this.$el,
-				opacity: 1,
-				translateY: [-64, 0],
-				easing: 'easeOutElastic',
-				duration: 500
-			});
-
-			setTimeout(() => {
-				anime({
-					targets: this.$el,
-					opacity: 0,
-					translateY: -64,
-					duration: 500,
-					easing: 'easeInElastic',
-					complete: () => this.destroyDom()
-				});
-			}, 5000);
-		});
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-ui-notification
-	display block
-	position fixed
-	z-index 10000
-	top -128px
-	left 0
-	right 0
-	margin 0 auto
-	padding 128px 0 0 0
-	width 500px
-	color var(--desktopNotificationFg)
-	background var(--desktopNotificationBg)
-	border-radius 0 0 8px 8px
-	box-shadow 0 2px 4px var(--desktopNotificationShadow)
-	transform translateY(-64px)
-	opacity 0
-	pointer-events none
-
-	> p
-		margin 0
-		line-height 64px
-		text-align center
-
-</style>
diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue
deleted file mode 100644
index 690f3a5587df14f6497adef2da11ee3f3ee9fb5f..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/ui.header.account.vue
+++ /dev/null
@@ -1,343 +0,0 @@
-<template>
-<div class="account" v-hotkey.global="keymap">
-	<button class="header" :data-active="isOpen" @click="toggle">
-		<span class="username">{{ $store.state.i.username }}<template v-if="!isOpen"><fa icon="angle-down"/></template><template v-if="isOpen"><fa icon="angle-up"/></template></span>
-		<mk-avatar class="avatar" :user="$store.state.i"/>
-	</button>
-	<transition name="zoom-in-top">
-		<div class="menu" v-if="isOpen">
-			<ul>
-				<li>
-					<router-link :to="`/@${ $store.state.i.username }`">
-						<i><fa icon="user" fixed-width/></i>
-						<span>{{ $t('profile') }}</span>
-						<i><fa icon="angle-right"/></i>
-					</router-link>
-				</li>
-				<li @click="drive">
-					<p>
-						<i><fa icon="cloud" fixed-width/></i>
-						<span>{{ $t('@.drive') }}</span>
-						<i><fa icon="angle-right"/></i>
-					</p>
-				</li>
-				<li>
-					<router-link to="/i/favorites">
-						<i><fa icon="star" fixed-width/></i>
-						<span>{{ $t('@.favorites') }}</span>
-						<i><fa icon="angle-right"/></i>
-					</router-link>
-				</li>
-				<li>
-					<router-link to="/i/lists">
-						<i><fa icon="list" fixed-width/></i>
-						<span>{{ $t('lists') }}</span>
-						<i><fa icon="angle-right"/></i>
-					</router-link>
-				</li>
-				<li>
-					<router-link to="/i/groups">
-						<i><fa :icon="faUsers" fixed-width/></i>
-						<span>{{ $t('groups') }}</span>
-						<i><fa icon="angle-right"/></i>
-					</router-link>
-				</li>
-				<li>
-					<router-link to="/i/pages">
-						<i><fa :icon="faStickyNote" fixed-width/></i>
-						<span>{{ $t('@.pages') }}</span>
-						<i><fa icon="angle-right"/></i>
-					</router-link>
-				</li>
-				<li v-if="($store.state.i.isLocked || $store.state.i.carefulBot)">
-					<router-link to="/i/follow-requests">
-						<i><fa :icon="['far', 'envelope']" fixed-width/></i>
-						<span>{{ $t('follow-requests') }}<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></span>
-						<i><fa icon="angle-right"/></i>
-					</router-link>
-				</li>
-				<li>
-					<router-link :to="`/@${ $store.state.i.username }/room`">
-						<i><fa :icon="faDoorOpen" fixed-width/></i>
-						<span>{{ $t('room') }}</span>
-						<i><fa icon="angle-right"/></i>
-					</router-link>
-				</li>
-			</ul>
-			<ul>
-				<li>
-					<router-link to="/i/settings">
-						<i><fa icon="cog" fixed-width/></i>
-						<span>{{ $t('@.settings') }}</span>
-						<i><fa icon="angle-right"/></i>
-					</router-link>
-				</li>
-				<li v-if="$store.state.i.isAdmin || $store.state.i.isModerator">
-					<a href="/admin">
-						<i><fa icon="terminal" fixed-width/></i>
-						<span>{{ $t('admin') }}</span>
-						<i><fa icon="angle-right"/></i>
-					</a>
-				</li>
-			</ul>
-			<ul>
-				<li @click="toggleDeckMode">
-					<p>
-						<template v-if="$store.state.device.inDeckMode"><span>{{ $t('@.home') }}</span><i><fa :icon="faHome"/></i></template>
-						<template v-else><span>{{ $t('@.deck') }}</span><i><fa :icon="faColumns"/></i></template>
-					</p>
-				</li>
-				<li @click="dark">
-					<p>
-						<span>{{ $store.state.device.darkmode ? $t('@.turn-off-darkmode') : $t('@.turn-on-darkmode') }}</span>
-						<template><i><fa :icon="$store.state.device.darkmode ? faSun : faMoon"/></i></template>
-					</p>
-				</li>
-			</ul>
-			<ul>
-				<li @click="signout">
-					<p class="signout">
-						<i><fa icon="power-off" fixed-width/></i>
-						<span>{{ $t('@.signout') }}</span>
-					</p>
-				</li>
-			</ul>
-		</div>
-	</transition>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-// import MkSettingsWindow from './settings-window.vue';
-import MkDriveWindow from './drive-window.vue';
-import contains from '../../../common/scripts/contains';
-import { faHome, faColumns, faUsers, faDoorOpen } from '@fortawesome/free-solid-svg-icons';
-import { faMoon, faSun, faStickyNote } from '@fortawesome/free-regular-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/ui.header.account.vue'),
-	data() {
-		return {
-			isOpen: false,
-			faHome, faColumns, faMoon, faSun, faStickyNote, faUsers, faDoorOpen
-		};
-	},
-	computed: {
-		keymap(): any {
-			return {
-				'a|m': this.toggle
-			};
-		}
-	},
-	beforeDestroy() {
-		this.close();
-	},
-	methods: {
-		toggle() {
-			this.isOpen ? this.close() : this.open();
-		},
-		open() {
-			this.isOpen = true;
-			for (const el of Array.from(document.querySelectorAll('body *'))) {
-				el.addEventListener('mousedown', this.onMousedown);
-			}
-		},
-		close() {
-			this.isOpen = false;
-			for (const el of Array.from(document.querySelectorAll('body *'))) {
-				el.removeEventListener('mousedown', this.onMousedown);
-			}
-		},
-		onMousedown(e) {
-			e.preventDefault();
-			if (!contains(this.$el, e.target) && this.$el != e.target) this.close();
-			return false;
-		},
-		drive() {
-			this.close();
-			this.$root.new(MkDriveWindow);
-		},
-		signout() {
-			this.$root.signout();
-		},
-		dark() {
-			this.$store.commit('device/set', {
-				key: 'darkmode',
-				value: !this.$store.state.device.darkmode
-			});
-		},
-		toggleDeckMode() {
-			this.$store.commit('device/set', { key: 'deckMode', value: !this.$store.state.device.inDeckMode });
-			location.replace('/');
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.account
-	> .header
-		display block
-		margin 0
-		padding 0
-		color var(--desktopHeaderFg)
-		border none
-		background transparent
-		cursor pointer
-
-		*
-			pointer-events none
-
-		&:hover
-		&[data-active='true']
-			color var(--desktopHeaderHoverFg)
-
-			> .avatar
-				filter saturate(150%)
-
-		> .username
-			display block
-			float left
-			margin 0 12px 0 16px
-			max-width 16em
-			line-height 48px
-			font-weight bold
-			text-decoration none
-
-			@media (max-width 1100px)
-				display none
-
-			[data-icon]
-				margin-left 8px
-
-		> .avatar
-			display block
-			float left
-			min-width 32px
-			max-width 32px
-			min-height 32px
-			max-height 32px
-			margin 8px 8px 8px 0
-			border-radius 4px
-			transition filter 100ms ease
-
-			@media (max-width 1100px)
-				margin-left 8px
-
-	> .menu
-		$bgcolor = var(--face)
-		display block
-		position absolute
-		top 56px
-		right -2px
-		width 230px
-		font-size 0.8em
-		background $bgcolor
-		border-radius 4px
-		box-shadow 0 var(--lineWidth) 4px rgba(#000, 0.25)
-
-		&:before
-			content ""
-			pointer-events none
-			display block
-			position absolute
-			top -28px
-			right 12px
-			border-top solid 14px transparent
-			border-right solid 14px transparent
-			border-bottom solid 14px rgba(#000, 0.1)
-			border-left solid 14px transparent
-
-		&:after
-			content ""
-			pointer-events none
-			display block
-			position absolute
-			top -27px
-			right 12px
-			border-top solid 14px transparent
-			border-right solid 14px transparent
-			border-bottom solid 14px $bgcolor
-			border-left solid 14px transparent
-
-		ul
-			display block
-			margin 10px 0
-			padding 0
-			list-style none
-
-			& + ul
-				padding-top 10px
-				border-top solid var(--lineWidth) var(--faceDivider)
-
-			> li
-				display block
-				margin 0
-				padding 0
-
-				> a
-				> p
-					display block
-					z-index 1
-					padding 0 28px
-					margin 0
-					line-height 40px
-					color var(--text)
-					cursor pointer
-
-					*
-						pointer-events none
-
-					> span:first-child
-						padding-left 22px
-
-					> span:nth-child(2)
-						> i
-							margin-left 4px
-							padding 2px 8px
-							font-size 90%
-							font-style normal
-							background var(--primary)
-							color var(--primaryForeground)
-							border-radius 8px
-
-					> i:first-child
-						margin-right 6px
-						width 16px
-
-					> i:last-child
-						display block
-						position absolute
-						top 0
-						right 8px
-						z-index 1
-						padding 0 20px
-						font-size 1.2em
-						line-height 40px
-
-					&:hover, &:active
-						text-decoration none
-						background var(--primary)
-						color var(--primaryForeground)
-
-					&:active
-						background var(--primaryDarken10)
-
-					&.signout
-						$color = #e64137
-
-						&:hover, &:active
-							background $color
-							color #fff
-
-						&:active
-							background darken($color, 10%)
-
-.zoom-in-top-enter-active,
-.zoom-in-top-leave-active {
-	transform-origin: center -16px;
-}
-
-</style>
diff --git a/src/client/app/desktop/views/components/ui.header.clock.vue b/src/client/app/desktop/views/components/ui.header.clock.vue
deleted file mode 100644
index b8b638bc414fe74679613e403a2d4bae54b4534f..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/ui.header.clock.vue
+++ /dev/null
@@ -1,109 +0,0 @@
-<template>
-<div class="clock">
-	<div class="header">
-		<time ref="time">
-			<span class="yyyymmdd">{{ yyyy }}/{{ mm }}/{{ dd }}</span>
-			<br>
-			<span class="hhnn">{{ hh }}<span :style="{ visibility: now.getSeconds() % 2 == 0 ? 'visible' : 'hidden' }">:</span>{{ nn }}</span>
-		</time>
-	</div>
-	<div class="content">
-		<mk-analog-clock :dark="true"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	data() {
-		return {
-			now: new Date(),
-			clock: null
-		};
-	},
-	computed: {
-		yyyy(): number {
-			return this.now.getFullYear();
-		},
-		mm(): string {
-			return ('0' + (this.now.getMonth() + 1)).slice(-2);
-		},
-		dd(): string {
-			return ('0' + this.now.getDate()).slice(-2);
-		},
-		hh(): string {
-			return ('0' + this.now.getHours()).slice(-2);
-		},
-		nn(): string {
-			return ('0' + this.now.getMinutes()).slice(-2);
-		}
-	},
-	mounted() {
-		this.tick();
-		this.clock = setInterval(this.tick, 1000);
-	},
-	beforeDestroy() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		tick() {
-			this.now = new Date();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.clock
-	display inline-block
-	overflow visible
-
-	> .header
-		padding 0 12px
-		text-align center
-		font-size 10px
-
-		&, *
-			cursor: default
-
-		&:hover
-			background #899492
-
-			& + .content
-				visibility visible
-
-			> time
-				color #fff !important
-
-				*
-					color #fff !important
-
-		&:after
-			content ""
-			display block
-			clear both
-
-		> time
-			display table-cell
-			vertical-align middle
-			height 48px
-			color var(--desktopHeaderFg)
-
-			> .yyyymmdd
-				opacity 0.7
-
-	> .content
-		visibility hidden
-		display block
-		position absolute
-		top auto
-		right 0
-		z-index 3
-		margin 0
-		padding 0
-		width 256px
-		background #899492
-
-</style>
diff --git a/src/client/app/desktop/views/components/ui.header.messaging.vue b/src/client/app/desktop/views/components/ui.header.messaging.vue
deleted file mode 100644
index c5d1da3a3d5fce4e763ac8c0e614793382c35c14..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/ui.header.messaging.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-<template>
-<div class="toltmoik">
-	<button @click="open()" :title="$t('@.messaging')">
-		<i class="bell"><fa :icon="faComments"/></i>
-		<i class="circle" v-if="hasUnreadMessagingMessage"><fa icon="circle"/></i>
-	</button>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import MkMessagingWindow from './messaging-window.vue';
-import { faComments } from '@fortawesome/free-regular-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n(),
-
-	data() {
-		return {
-			faComments
-		};
-	},
-
-	computed: {
-		hasUnreadMessagingMessage(): boolean {
-			return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage;
-		}
-	},
-
-	methods: {
-		open() {
-			this.$root.new(MkMessagingWindow);
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.toltmoik
-	> button
-		display block
-		margin 0
-		padding 0
-		width 32px
-		color var(--desktopHeaderFg)
-		border none
-		background transparent
-		cursor pointer
-
-		*
-			pointer-events none
-
-		&:hover
-		&[data-active='true']
-			color var(--desktopHeaderHoverFg)
-
-		> i.bell
-			font-size 1.2em
-			line-height 48px
-
-		> i.circle
-			margin-left -5px
-			vertical-align super
-			font-size 10px
-			color var(--notificationIndicator)
-			animation blink 1s infinite
-
-</style>
diff --git a/src/client/app/desktop/views/components/ui.header.nav.vue b/src/client/app/desktop/views/components/ui.header.nav.vue
deleted file mode 100644
index 2bd3cf87729ac47d972c634a58fd435273c8c033..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/ui.header.nav.vue
+++ /dev/null
@@ -1,141 +0,0 @@
-<template>
-<div class="nav">
-	<ul>
-		<li class="timeline" :class="{ active: $route.name == 'index' }" @click="goToTop">
-			<router-link to="/"><fa icon="home"/><p>{{ $t('@.timeline') }}</p></router-link>
-		</li>
-		<li class="featured" :class="{ active: $route.name == 'featured' }">
-			<router-link to="/featured"><fa :icon="faNewspaper"/><p>{{ $t('@.featured-notes') }}</p></router-link>
-		</li>
-		<li class="explore" :class="{ active: $route.name == 'explore' || $route.name == 'explore-tag' }">
-			<router-link to="/explore"><fa :icon="faHashtag"/><p>{{ $t('@.explore') }}</p></router-link>
-		</li>
-		<li class="game">
-			<a @click="game">
-				<fa icon="gamepad"/>
-				<p>{{ $t('game') }}</p>
-				<template v-if="hasGameInvitations"><fa icon="circle"/></template>
-			</a>
-		</li>
-	</ul>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import MkGameWindow from './game-window.vue';
-import { faNewspaper, faHashtag } from '@fortawesome/free-solid-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/ui.header.nav.vue'),
-	data() {
-		return {
-			hasGameInvitations: false,
-			connection: null,
-			faNewspaper, faHashtag
-		};
-	},
-	mounted() {
-		if (this.$store.getters.isSignedIn) {
-			this.connection = this.$root.stream.useSharedConnection('main');
-
-			this.connection.on('reversiInvited', this.onReversiInvited);
-			this.connection.on('reversiNoInvites', this.onReversiNoInvites);
-		}
-	},
-	beforeDestroy() {
-		if (this.$store.getters.isSignedIn) {
-			this.connection.dispose();
-		}
-	},
-	methods: {
-		onReversiInvited() {
-			this.hasGameInvitations = true;
-		},
-
-		onReversiNoInvites() {
-			this.hasGameInvitations = false;
-		},
-
-		game() {
-			this.$root.new(MkGameWindow);
-		},
-
-		goToTop() {
-			window.scrollTo({
-				top: 0,
-				behavior: 'smooth'
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.nav
-	display inline-block
-	margin 0
-	padding 0
-	line-height 3rem
-	vertical-align top
-
-	> ul
-		display inline-block
-		margin 0
-		padding 0
-		vertical-align top
-		line-height 3rem
-		list-style none
-
-		> li
-			display inline-block
-			vertical-align top
-			height 48px
-			line-height 48px
-
-			&.active
-				> a
-					border-bottom solid 3px var(--primary)
-
-			> a
-				display inline-block
-				z-index 1
-				height 100%
-				padding 0 20px
-				font-size 13px
-				font-variant small-caps
-				color var(--desktopHeaderFg)
-				text-decoration none
-				transition none
-				cursor pointer
-
-				*
-					pointer-events none
-
-				&:hover
-					color var(--desktopHeaderHoverFg)
-					text-decoration none
-
-				> [data-icon]:first-child
-					margin-right 8px
-
-				> [data-icon]:last-child
-					margin-left 5px
-					font-size 10px
-					color var(--notificationIndicator)
-
-					@media (max-width 1100px)
-						margin-left -5px
-
-				> p
-					display inline
-					margin 0
-
-					@media (max-width 1100px)
-						display none
-
-				@media (max-width 700px)
-					padding 0 12px
-
-</style>
diff --git a/src/client/app/desktop/views/components/ui.header.notifications.vue b/src/client/app/desktop/views/components/ui.header.notifications.vue
deleted file mode 100644
index d3316d6a896312120712f3b4936a5a3752349717..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/ui.header.notifications.vue
+++ /dev/null
@@ -1,136 +0,0 @@
-<template>
-<div class="notifications" v-hotkey.global="keymap">
-	<button :data-active="isOpen" @click="toggle" :title="$t('title')">
-		<i class="bell"><fa :icon="['far', 'bell']"/></i>
-		<i class="circle" v-if="hasUnreadNotification"><fa icon="circle"/></i>
-	</button>
-	<div class="pop" v-if="isOpen">
-		<mk-notifications/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import contains from '../../../common/scripts/contains';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/ui.header.notifications.vue'),
-	data() {
-		return {
-			isOpen: false
-		};
-	},
-
-	computed: {
-		hasUnreadNotification(): boolean {
-			return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification;
-		},
-
-		keymap(): any {
-			return {
-				'shift+n': this.toggle
-			};
-		}
-	},
-
-	methods: {
-		toggle() {
-			this.isOpen ? this.close() : this.open();
-		},
-
-		open() {
-			this.isOpen = true;
-			for (const el of Array.from(document.querySelectorAll('body *'))) {
-				el.addEventListener('mousedown', this.onMousedown);
-			}
-		},
-
-		close() {
-			this.isOpen = false;
-			for (const el of Array.from(document.querySelectorAll('body *'))) {
-				el.removeEventListener('mousedown', this.onMousedown);
-			}
-		},
-
-		onMousedown(e) {
-			e.preventDefault();
-			if (!contains(this.$el, e.target) && this.$el != e.target) this.close();
-			return false;
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.notifications
-	> button
-		display block
-		margin 0
-		padding 0
-		width 32px
-		color var(--desktopHeaderFg)
-		border none
-		background transparent
-		cursor pointer
-
-		*
-			pointer-events none
-
-		&:hover
-		&[data-active='true']
-			color var(--desktopHeaderHoverFg)
-
-		> i.bell
-			font-size 1.2em
-			line-height 48px
-
-		> i.circle
-			margin-left -5px
-			vertical-align super
-			font-size 10px
-			color var(--notificationIndicator)
-			animation blink 1s infinite
-
-	> .pop
-		$bgcolor = var(--face)
-		display block
-		position absolute
-		top 56px
-		right -72px
-		width 300px
-		background $bgcolor
-		border-radius 4px
-		box-shadow 0 1px 4px rgba(#000, 0.25)
-
-		&:before
-			content ""
-			pointer-events none
-			display block
-			position absolute
-			top -28px
-			right 74px
-			border-top solid 14px transparent
-			border-right solid 14px transparent
-			border-bottom solid 14px rgba(#000, 0.1)
-			border-left solid 14px transparent
-
-		&:after
-			content ""
-			pointer-events none
-			display block
-			position absolute
-			top -27px
-			right 74px
-			border-top solid 14px transparent
-			border-right solid 14px transparent
-			border-bottom solid 14px $bgcolor
-			border-left solid 14px transparent
-
-		> .mk-notifications
-			max-height 350px
-			font-size 1rem
-			overflow auto
-
-</style>
diff --git a/src/client/app/desktop/views/components/ui.header.post.vue b/src/client/app/desktop/views/components/ui.header.post.vue
deleted file mode 100644
index b273ad8d4d88dd27ef8b1cfecc499d0f9d2fd287..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/ui.header.post.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<template>
-<div class="note">
-	<button @click="post" :title="$t('post')"><fa icon="pencil-alt"/></button>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/ui.header.post.vue'),
-	methods: {
-		post() {
-			this.$post();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.note
-	display inline-block
-	padding 8px
-	height 100%
-	vertical-align top
-
-	> button
-		display inline-block
-		margin 0
-		padding 0 10px
-		height 100%
-		font-size 1.2em
-		font-weight normal
-		text-decoration none
-		color var(--primaryForeground)
-		background var(--primary) !important
-		outline none
-		border none
-		border-radius 4px
-		transition background 0.1s ease
-		cursor pointer
-
-		*
-			pointer-events none
-
-		&:hover
-			background var(--primaryLighten10) !important
-
-		&:active
-			background var(--primaryDarken10) !important
-			transition background 0s ease
-
-</style>
diff --git a/src/client/app/desktop/views/components/ui.header.search.vue b/src/client/app/desktop/views/components/ui.header.search.vue
deleted file mode 100644
index 0cf5ca6f321880e50b82a3828033709b6f9f5a67..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/ui.header.search.vue
+++ /dev/null
@@ -1,82 +0,0 @@
-<template>
-<form class="wlvfdpkp" @submit.prevent="onSubmit">
-	<i><fa icon="search"/></i>
-	<input v-model="q" type="search" :placeholder="$t('placeholder')" v-autocomplete="{ model: 'q' }"/>
-	<div class="result"></div>
-</form>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { search } from '../../../common/scripts/search';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/ui.header.search.vue'),
-	data() {
-		return {
-			q: '',
-			wait: false
-		};
-	},
-	methods: {
-		async onSubmit() {
-			if (this.wait) return;
-
-			this.wait = true;
-			search(this, this.q).finally(() => {
-				this.wait = false;
-				this.q = '';
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.wlvfdpkp
-	@media (max-width 800px)
-		display none !important
-
-	> i
-		display block
-		position absolute
-		top 0
-		left 0
-		width 48px
-		text-align center
-		line-height 48px
-		color var(--desktopHeaderFg)
-		pointer-events none
-
-		> *
-			vertical-align middle
-
-	> input
-		user-select text
-		cursor auto
-		margin 8px 0 0 0
-		padding 6px 18px 6px 36px
-		width 14em
-		height 32px
-		font-size 1em
-		background var(--desktopHeaderSearchBg)
-		outline none
-		border none
-		border-radius 16px
-		transition color 0.5s ease, border 0.5s ease
-		color var(--desktopHeaderSearchFg)
-
-		@media (max-width 1000px)
-			width 10em
-
-		&::placeholder
-			color var(--desktopHeaderFg)
-
-		&:hover
-			background var(--desktopHeaderSearchHoverBg)
-
-		&:focus
-			box-shadow 0 0 0 2px var(--primaryAlpha05) !important
-
-</style>
diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue
deleted file mode 100644
index 14a732155224bc0d5bff43a8a97923e94b5397fe..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/ui.header.vue
+++ /dev/null
@@ -1,161 +0,0 @@
-<template>
-<div class="header" :style="style">
-	<p class="warn" v-if="env != 'production'">{{ $t('@.do-not-use-in-production') }} <a href="/assets/flush.html?force">Flush</a></p>
-	<div class="main" ref="main">
-		<div class="backdrop"></div>
-		<div class="main">
-			<div class="container" ref="mainContainer">
-				<div class="left">
-					<x-nav/>
-				</div>
-				<div class="center">
-					<div class="icon" @click="goToTop">
-						<img svg-inline src="../../assets/header-icon.svg"/>
-					</div>
-				</div>
-				<div class="right">
-					<x-search/>
-					<x-account v-if="$store.getters.isSignedIn"/>
-					<x-messaging v-if="$store.getters.isSignedIn"/>
-					<x-notifications v-if="$store.getters.isSignedIn"/>
-					<x-post v-if="$store.getters.isSignedIn"/>
-					<x-clock v-if="$store.state.settings.showClockOnHeader" class="clock"/>
-				</div>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { env } from '../../../config';
-
-import XNav from './ui.header.nav.vue';
-import XSearch from './ui.header.search.vue';
-import XAccount from './ui.header.account.vue';
-import XNotifications from './ui.header.notifications.vue';
-import XPost from './ui.header.post.vue';
-import XClock from './ui.header.clock.vue';
-import XMessaging from './ui.header.messaging.vue';
-
-export default Vue.extend({
-	i18n: i18n(),
-	components: {
-		XNav,
-		XSearch,
-		XAccount,
-		XNotifications,
-		XMessaging,
-		XPost,
-		XClock
-	},
-
-	data() {
-		return {
-			env: env
-		};
-	},
-
-	computed: {
-		style(): any {
-			return {
-				'box-shadow': this.$store.state.device.useShadow ? '0 0px 8px rgba(0, 0, 0, 0.2)' : 'none'
-			};
-		}
-	},
-
-	mounted() {
-		this.$store.commit('setUiHeaderHeight', this.$el.offsetHeight);
-	},
-
-	methods: {
-		goToTop() {
-			window.scrollTo({
-				top: 0,
-				behavior: 'smooth'
-			});
-		}
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-.header
-	position fixed
-	top 0
-	z-index 1000
-	width 100%
-
-	> .warn
-		display block
-		margin 0
-		padding 4px
-		text-align center
-		font-size 12px
-		background #f00
-		color #fff
-
-	> .main
-		height 48px
-
-		> .backdrop
-			position absolute
-			top 0
-			z-index 1000
-			width 100%
-			height 48px
-			background var(--desktopHeaderBg)
-
-		> .main
-			z-index 1001
-			margin 0
-			padding 0
-			background-clip content-box
-			font-size 0.9rem
-			user-select none
-
-			> .container
-				display flex
-				width 100%
-				max-width 1208px
-				margin 0 auto
-
-				> *
-					position absolute
-					height 48px
-
-				> .center
-					right 0
-
-					> .icon
-						margin auto
-						display block
-						width 48px
-						text-align center
-						cursor pointer
-						opacity 0.5
-
-						> svg
-							width 24px
-							height 48px
-							vertical-align top
-							fill var(--desktopHeaderFg)
-
-				> .left,
-				> .center
-					left 0
-
-				> .right
-					right 0
-
-					> *
-						display inline-block
-						vertical-align top
-
-					@media (max-width 1100px)
-						> .clock
-							display none
-
-</style>
diff --git a/src/client/app/desktop/views/components/ui.sidebar.vue b/src/client/app/desktop/views/components/ui.sidebar.vue
deleted file mode 100644
index d1ceec51984f71ae4ebe8c293562851ed155b834..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/ui.sidebar.vue
+++ /dev/null
@@ -1,363 +0,0 @@
-<template>
-<div class="header" :class="navbar" :data-shadow="$store.state.device.useShadow">
-	<div class="body">
-		<div class="post">
-			<button @click="post" :title="$t('title')"><fa icon="pencil-alt"/></button>
-		</div>
-
-		<div class="nav" v-if="$store.getters.isSignedIn">
-			<div class="home" :class="{ active: $route.name == 'index' }" @click="goToTop">
-				<router-link to="/"><fa icon="home"/></router-link>
-			</div>
-			<div class="featured" :class="{ active: $route.name == 'featured' }">
-				<router-link to="/featured"><fa :icon="faNewspaper"/></router-link>
-			</div>
-			<div class="explore" :class="{ active: $route.name == 'explore' || $route.name == 'explore-tag' }">
-				<router-link to="/explore"><fa :icon="faHashtag"/></router-link>
-			</div>
-			<div class="game">
-				<a @click="game"><fa icon="gamepad"/><template v-if="hasGameInvitations"><fa icon="circle"/></template></a>
-			</div>
-		</div>
-
-		<div class="nav bottom" v-if="$store.getters.isSignedIn">
-			<div>
-				<a @click="drive"><fa icon="cloud"/></a>
-			</div>
-			<div ref="notificationsButton" :class="{ active: showNotifications }">
-				<a @click="notifications"><fa :icon="['far', 'bell']"/></a>
-			</div>
-			<div class="messaging">
-				<a @click="messaging"><fa icon="comments"/><template v-if="hasUnreadMessagingMessage"><fa icon="circle"/></template></a>
-			</div>
-			<div>
-				<a @click="settings"><fa icon="cog"/></a>
-			</div>
-			<div class="signout">
-				<a @click="signout"><fa icon="power-off"/></a>
-			</div>
-			<div>
-				<router-link to="/i/favorites"><fa icon="star"/></router-link>
-			</div>
-			<div v-if="($store.state.i.isLocked || $store.state.i.carefulBot)">
-				<a @click="followRequests"><fa :icon="['far', 'envelope']"/><i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></a>
-			</div>
-			<div class="account">
-				<router-link :to="`/@${ $store.state.i.username }`">
-					<mk-avatar class="avatar" :user="$store.state.i"/>
-				</router-link>
-			</div>
-			<div>
-				<template v-if="$store.state.device.inDeckMode">
-					<a @click="toggleDeckMode(false)"><fa icon="home"/></a>
-				</template>
-				<template v-else>
-					<a @click="toggleDeckMode(true)"><fa icon="columns"/></a>
-				</template>
-			</div>
-			<div>
-				<a @click="dark"><template v-if="$store.state.device.darkmode"><fa icon="moon"/></template><template v-else><fa :icon="['far', 'moon']"/></template></a>
-			</div>
-		</div>
-	</div>
-
-	<transition :name="`slide-${navbar}`">
-		<div class="notifications" v-if="showNotifications" ref="notifications" :class="navbar" :data-shadow="$store.state.device.useShadow">
-			<mk-notifications/>
-		</div>
-	</transition>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import MkSettingsWindow from './settings-window.vue';
-import MkDriveWindow from './drive-window.vue';
-import MkMessagingWindow from './messaging-window.vue';
-import MkGameWindow from './game-window.vue';
-import contains from '../../../common/scripts/contains';
-import { faNewspaper, faHashtag } from '@fortawesome/free-solid-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/ui.sidebar.vue'),
-	data() {
-		return {
-			hasGameInvitations: false,
-			connection: null,
-			showNotifications: false,
-			faNewspaper, faHashtag
-		};
-	},
-
-	computed: {
-		hasUnreadMessagingMessage(): boolean {
-			return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage;
-		},
-
-		navbar(): string {
-			return this.$store.state.device.navbar;
-		},
-	},
-
-	mounted() {
-		if (this.$store.getters.isSignedIn) {
-			this.connection = this.$root.stream.useSharedConnection('main');
-
-			this.connection.on('reversiInvited', this.onReversiInvited);
-			this.connection.on('reversiNoInvites', this.onReversiNoInvites);
-		}
-	},
-
-	beforeDestroy() {
-		if (this.$store.getters.isSignedIn) {
-			this.connection.dispose();
-		}
-	},
-
-	methods: {
-		toggleDeckMode(deck) {
-			this.$store.commit('device/set', { key: 'deckMode', value: deck });
-			location.replace('/');
-		},
-
-		onReversiInvited() {
-			this.hasGameInvitations = true;
-		},
-
-		onReversiNoInvites() {
-			this.hasGameInvitations = false;
-		},
-
-		messaging() {
-			this.$root.new(MkMessagingWindow);
-		},
-
-		game() {
-			this.$root.new(MkGameWindow);
-		},
-
-		post() {
-			this.$post();
-		},
-
-		drive() {
-			this.$root.new(MkDriveWindow);
-		},
-
-		list() {
-			this.$root.new(MkUserListsWindow);
-		},
-
-		followRequests() {
-			this.$root.new(MkFollowRequestsWindow);
-		},
-
-		settings() {
-			this.$root.new(MkSettingsWindow);
-		},
-
-		signout() {
-			this.$root.signout();
-		},
-
-		notifications() {
-			this.showNotifications ? this.closeNotifications() : this.openNotifications();
-		},
-
-		openNotifications() {
-			this.showNotifications = true;
-			for (const el of Array.from(document.querySelectorAll('body *'))) {
-				el.addEventListener('mousedown', this.onMousedown);
-			}
-		},
-
-		closeNotifications() {
-			this.showNotifications = false;
-			for (const el of Array.from(document.querySelectorAll('body *'))) {
-				el.removeEventListener('mousedown', this.onMousedown);
-			}
-		},
-
-		onMousedown(e) {
-			e.preventDefault();
-			if (
-				!contains(this.$refs.notifications, e.target) &&
-				this.$refs.notifications != e.target &&
-				!contains(this.$refs.notificationsButton, e.target) &&
-				this.$refs.notificationsButton != e.target
-			) {
-				this.closeNotifications();
-			}
-			return false;
-		},
-
-		dark() {
-			this.$store.commit('device/set', {
-				key: 'darkmode',
-				value: !this.$store.state.device.darkmode
-			});
-		},
-
-		goToTop() {
-			window.scrollTo({
-				top: 0,
-				behavior: 'smooth'
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.header
-	$width = 68px
-
-	position fixed
-	top 0
-	z-index 1000
-	width $width
-	height 100%
-
-	&.left
-		left 0
-
-		&[data-shadow]
-			box-shadow 4px 0 4px rgba(0, 0, 0, 0.1)
-
-	&.right
-		right 0
-
-		&[data-shadow]
-			box-shadow -4px 0 4px rgba(0, 0, 0, 0.1)
-
-	> .body
-		position fixed
-		top 0
-		z-index 1
-		width $width
-		height 100%
-		background var(--desktopHeaderBg)
-
-		> .post
-			width $width
-			height $width
-			padding 12px
-
-			> button
-				display inline-block
-				margin 0
-				padding 0
-				height 100%
-				width 100%
-				font-size 1.2em
-				font-weight normal
-				text-decoration none
-				color var(--primaryForeground)
-				background var(--primary) !important
-				outline none
-				border none
-				border-radius 100%
-				transition background 0.1s ease
-				cursor pointer
-
-				*
-					pointer-events none
-
-				&:hover
-					background var(--primaryLighten10) !important
-
-				&:active
-					background var(--primaryDarken10) !important
-					transition background 0s ease
-
-		> .nav.bottom
-			position absolute
-			bottom 0
-			left 0
-
-			> .account
-				width $width
-				height $width
-				padding 14px
-
-				> *
-					display block
-					width 100%
-					height 100%
-
-					> .avatar
-						pointer-events none
-						width 100%
-						height 100%
-
-	> .notifications
-		position fixed
-		top 0
-		width 350px
-		height 100%
-		overflow auto
-		background var(--face)
-
-		&.left
-			left $width
-
-			&[data-shadow]
-				box-shadow 4px 0 4px rgba(0, 0, 0, 0.1)
-
-		&.right
-			right $width
-
-			&[data-shadow]
-				box-shadow -4px 0 4px rgba(0, 0, 0, 0.1)
-
-	.nav
-		> *
-			> *
-				display block
-				width $width
-				line-height 52px
-				text-align center
-				font-size 18px
-				color var(--desktopHeaderFg)
-
-				&:hover
-					background rgba(0, 0, 0, 0.05)
-					color var(--desktopHeaderHoverFg)
-					text-decoration none
-
-				&:active
-					background rgba(0, 0, 0, 0.1)
-
-	&.left
-		.nav
-			> *
-				&.active
-					box-shadow -4px 0 var(--primary) inset
-
-	&.right
-		.nav
-			> *
-				&.active
-					box-shadow 4px 0 var(--primary) inset
-
-.slide-left-enter-active,
-.slide-left-leave-active {
-	transition: all 0.2s ease;
-}
-
-.slide-left-enter, .slide-left-leave-to {
-	transform: translateX(-16px);
-	opacity: 0;
-}
-
-.slide-right-enter-active,
-.slide-right-leave-active {
-	transition: all 0.2s ease;
-}
-
-.slide-right-enter, .slide-right-leave-to {
-	transform: translateX(16px);
-	opacity: 0;
-}
-</style>
diff --git a/src/client/app/desktop/views/components/ui.vue b/src/client/app/desktop/views/components/ui.vue
deleted file mode 100644
index f7961d508378b5d5c6ebdc178755008b4002bf61..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/ui.vue
+++ /dev/null
@@ -1,108 +0,0 @@
-<template>
-<div class="mk-ui" v-hotkey.global="keymap">
-	<div class="bg" v-if="$store.getters.isSignedIn && $store.state.settings.wallpaper" :style="style"></div>
-	<x-header class="header" v-if="navbar == 'top'" v-show="!zenMode" ref="header"/>
-	<x-sidebar class="sidebar" v-if="navbar != 'top'" v-show="!zenMode" ref="sidebar"/>
-	<div class="content" :class="[{ sidebar: navbar != 'top', zen: zenMode }, navbar]">
-		<slot></slot>
-	</div>
-	<mk-stream-indicator v-if="$store.getters.isSignedIn"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import XHeader from './ui.header.vue';
-import XSidebar from './ui.sidebar.vue';
-
-export default Vue.extend({
-	components: {
-		XHeader,
-		XSidebar
-	},
-
-	data() {
-		return {
-			zenMode: false
-		};
-	},
-
-	computed: {
-		navbar(): string {
-			return this.$store.state.device.navbar;
-		},
-
-		style(): any {
-			if (!this.$store.getters.isSignedIn || this.$store.state.settings.wallpaper == null) return {};
-			return {
-				backgroundImage: `url(${ this.$store.state.settings.wallpaper })`
-			};
-		},
-
-		keymap(): any {
-			return {
-				'p': this.post,
-				'n': this.post,
-				'z': this.toggleZenMode
-			};
-		}
-	},
-
-	watch: {
-		'$store.state.uiHeaderHeight'() {
-			this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px';
-		},
-
-		navbar() {
-			if (this.navbar != 'top') {
-				this.$store.commit('setUiHeaderHeight', 0);
-			}
-		}
-	},
-
-	mounted() {
-		this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px';
-	},
-
-	methods: {
-		post() {
-			this.$post();
-		},
-
-		toggleZenMode() {
-			this.zenMode = !this.zenMode;
-			this.$nextTick(() => {
-				if (this.$refs.header) {
-					this.$store.commit('setUiHeaderHeight', this.$refs.header.$el.offsetHeight);
-				}
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-ui
-	min-height 100vh
-	padding-top 48px
-
-	> .bg
-		position fixed
-		top 0
-		left 0
-		width 100%
-		height 100vh
-		background-size cover
-		background-position center
-		background-attachment fixed
-
-	> .content.sidebar.left
-		padding-left 68px
-
-	> .content.sidebar.right
-		padding-right 68px
-
-	> .content.zen
-		padding 0 !important
-
-</style>
diff --git a/src/client/app/desktop/views/components/user-list-timeline.vue b/src/client/app/desktop/views/components/user-list-timeline.vue
deleted file mode 100644
index dae282ec5cc75d4506929c6aca47533a8ecb5405..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/user-list-timeline.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-<template>
-<div>
-	<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')">
-		<template #header>
-			<slot></slot>
-		</template>
-	</mk-notes>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: ['list'],
-	data() {
-		return {
-			connection: null,
-			date: null,
-			pagination: {
-				endpoint: 'notes/user-list-timeline',
-				limit: 10,
-				params: init => ({
-					listId: this.list.id,
-					untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
-					includeMyRenotes: this.$store.state.settings.showMyRenotes,
-					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
-					includeLocalRenotes: this.$store.state.settings.showLocalRenotes
-				})
-			}
-		};
-	},
-	watch: {
-		$route: 'init'
-	},
-	mounted() {
-		this.init();
-		this.$root.$on('warp', this.warp);
-		this.$once('hook:beforeDestroy', () => {
-			this.$root.$off('warp', this.warp);
-		});
-	},
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-	methods: {
-		init() {
-			if (this.connection) this.connection.dispose();
-			this.connection = this.$root.stream.connectToChannel('userList', {
-				listId: this.list.id
-			});
-			this.connection.on('note', this.onNote);
-			this.connection.on('userAdded', this.onUserAdded);
-			this.connection.on('userRemoved', this.onUserRemoved);
-		},
-		onNote(note) {
-			(this.$refs.timeline as any).prepend(note);
-		},
-		onUserAdded() {
-			(this.$refs.timeline as any).reload();
-		},
-		onUserRemoved() {
-			(this.$refs.timeline as any).reload();
-		},
-		warp(date) {
-			this.date = date;
-			(this.$refs.timeline as any).reload();
-		}
-	}
-});
-</script>
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
deleted file mode 100644
index 9328648ccb43bfc3d8d389a9634c7a7f749da50a..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ /dev/null
@@ -1,164 +0,0 @@
-<template>
-<div class="mk-user-preview">
-	<template v-if="u != null">
-		<div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl})` : ''"></div>
-		<mk-avatar class="avatar" :user="u" :disable-preview="true"/>
-		<div class="title">
-			<router-link class="name" :to="u | userPage"><mk-user-name :user="u" :nowrap="false"/></router-link>
-			<p class="username"><mk-acct :user="u"/></p>
-		</div>
-		<div class="description">
-			<mfm v-if="u.description" :text="u.description" :author="u" :i="$store.state.i" :custom-emojis="u.emojis"/>
-		</div>
-		<div class="status">
-			<div>
-				<p>{{ $t('notes') }}</p><span>{{ u.notesCount }}</span>
-			</div>
-			<div>
-				<p>{{ $t('following') }}</p><span>{{ u.followingCount }}</span>
-			</div>
-			<div>
-				<p>{{ $t('followers') }}</p><span>{{ u.followersCount }}</span>
-			</div>
-		</div>
-		<mk-follow-button class="koudoku-button" v-if="$store.getters.isSignedIn && u.id != $store.state.i.id" :user="u" mini/>
-	</template>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import anime from 'animejs';
-import parseAcct from '../../../../../misc/acct/parse';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/user-preview.vue'),
-	props: {
-		user: {
-			type: [Object, String],
-			required: true
-		}
-	},
-	data() {
-		return {
-			u: null
-		};
-	},
-	mounted() {
-		if (typeof this.user == 'object') {
-			this.u = this.user;
-			this.$nextTick(() => {
-				this.open();
-			});
-		} else {
-			const query = this.user.startsWith('@') ?
-				parseAcct(this.user.substr(1)) :
-				{ userId: this.user };
-
-			this.$root.api('users/show', query).then(user => {
-				this.u = user;
-				this.open();
-			});
-		}
-	},
-	methods: {
-		open() {
-			anime({
-				targets: this.$el,
-				opacity: 1,
-				'margin-top': 0,
-				duration: 200,
-				easing: 'easeOutQuad'
-			});
-		},
-		close() {
-			anime({
-				targets: this.$el,
-				opacity: 0,
-				'margin-top': '-8px',
-				duration: 200,
-				easing: 'easeOutQuad',
-				complete: () => this.destroyDom()
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-user-preview
-	position absolute
-	z-index 2048
-	margin-top -8px
-	width 250px
-	background var(--face)
-	background-clip content-box
-	border solid 1px rgba(#000, 0.1)
-	border-radius 4px
-	overflow hidden
-	opacity 0
-
-	> .banner
-		height 84px
-		background-color rgba(0, 0, 0, 0.1)
-		background-size cover
-		background-position center
-
-	> .avatar
-		display block
-		position absolute
-		top 62px
-		left 13px
-		z-index 2
-		width 58px
-		height 58px
-		border solid 3px var(--face)
-		border-radius 8px
-
-	> .title
-		display block
-		padding 8px 0 8px 82px
-
-		> .name
-			display inline-block
-			margin 0
-			font-weight bold
-			line-height 16px
-			color var(--text)
-
-		> .username
-			display block
-			margin 0
-			line-height 16px
-			font-size 0.8em
-			color var(--text)
-			opacity 0.7
-
-	> .description
-		padding 0 16px
-		font-size 0.7em
-		color var(--text)
-
-	> .status
-		padding 8px 16px
-
-		> div
-			display inline-block
-			width 33%
-
-			> p
-				margin 0
-				font-size 0.7em
-				color var(--text)
-
-			> span
-				font-size 1em
-				color var(--primary)
-
-	> .koudoku-button
-		position absolute
-		top 8px
-		right 8px
-
-</style>
diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue
deleted file mode 100644
index 499f4e7c91584141de0ed00e0d75c9c29603fb94..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/components/window.vue
+++ /dev/null
@@ -1,620 +0,0 @@
-<template>
-<div class="mk-window" :data-flexible="isFlexible" @dragover="onDragover">
-	<div class="bg" ref="bg" v-show="isModal" @click="onBgClick"></div>
-	<div class="main" ref="main" tabindex="-1" :data-is-modal="isModal" @mousedown="onBodyMousedown" @keydown="onKeydown" :style="{ width, height }">
-		<div class="body">
-			<header ref="header"
-				@contextmenu.prevent="() => {}" @mousedown.prevent="onHeaderMousedown"
-			>
-				<h1><slot name="header"></slot></h1>
-				<div>
-					<button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" :title="$t('popout')">
-						<i><fa :icon="['far', 'window-restore']"/></i>
-					</button>
-					<button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" :title="$t('close')">
-						<i><fa icon="times"/></i>
-					</button>
-				</div>
-			</header>
-			<div class="content">
-				<slot></slot>
-			</div>
-		</div>
-		<template v-if="canResize">
-			<div class="handle top" @mousedown.prevent="onTopHandleMousedown"></div>
-			<div class="handle right" @mousedown.prevent="onRightHandleMousedown"></div>
-			<div class="handle bottom" @mousedown.prevent="onBottomHandleMousedown"></div>
-			<div class="handle left" @mousedown.prevent="onLeftHandleMousedown"></div>
-			<div class="handle top-left" @mousedown.prevent="onTopLeftHandleMousedown"></div>
-			<div class="handle top-right" @mousedown.prevent="onTopRightHandleMousedown"></div>
-			<div class="handle bottom-right" @mousedown.prevent="onBottomRightHandleMousedown"></div>
-			<div class="handle bottom-left" @mousedown.prevent="onBottomLeftHandleMousedown"></div>
-		</template>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import anime from 'animejs';
-import contains from '../../../common/scripts/contains';
-
-const minHeight = 40;
-const minWidth = 200;
-
-function dragListen(fn) {
-	window.addEventListener('mousemove',  fn);
-	window.addEventListener('mouseleave', dragClear.bind(null, fn));
-	window.addEventListener('mouseup',    dragClear.bind(null, fn));
-}
-
-function dragClear(fn) {
-	window.removeEventListener('mousemove',  fn);
-	window.removeEventListener('mouseleave', dragClear);
-	window.removeEventListener('mouseup',    dragClear);
-}
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/window.vue'),
-	props: {
-		isModal: {
-			type: Boolean,
-			default: false
-		},
-		canClose: {
-			type: Boolean,
-			default: true
-		},
-		width: {
-			type: String,
-			default: '530px'
-		},
-		height: {
-			type: String,
-			default: 'auto'
-		},
-		popoutUrl: {
-			type: [String, Function],
-			default: null
-		},
-		name: {
-			type: String,
-			default: null
-		},
-		animation: {
-			type: Boolean,
-			required: false,
-			default: true
-		}
-	},
-
-	computed: {
-		isFlexible(): boolean {
-			return this.height == 'auto';
-		},
-		canResize(): boolean {
-			return !this.isFlexible;
-		}
-	},
-
-	created() {
-		// ウィンドウをウィンドウシステムに登録
-		this.$root.os.windows.add(this);
-	},
-
-	mounted() {
-		this.$nextTick(() => {
-			const main = this.$refs.main as any;
-			main.style.top = '15%';
-			main.style.left = (window.innerWidth / 2) - (main.offsetWidth / 2) + 'px';
-
-			window.addEventListener('resize', this.onBrowserResize);
-
-			this.open();
-		});
-	},
-
-	destroyed() {
-		// ウィンドウをウィンドウシステムから削除
-		this.$root.os.windows.remove(this);
-
-		window.removeEventListener('resize', this.onBrowserResize);
-	},
-
-	methods: {
-		open() {
-			this.$emit('opening');
-
-			this.top();
-
-			const bg = this.$refs.bg as any;
-			const main = this.$refs.main as any;
-
-			if (this.isModal) {
-				bg.style.pointerEvents = 'auto';
-				anime({
-					targets: bg,
-					opacity: 1,
-					duration: this.animation ? 100 : 0,
-					easing: 'linear'
-				});
-			}
-
-			main.style.pointerEvents = 'auto';
-			anime({
-				targets: main,
-				opacity: 1,
-				scale: [1.1, 1],
-				duration: this.animation ? 200 : 0,
-				easing: 'easeOutQuad'
-			});
-
-			if (focus) main.focus();
-
-			setTimeout(() => {
-				this.$emit('opened');
-			}, this.animation ? 300 : 0);
-		},
-
-		close() {
-			this.$emit('before-close');
-
-			const bg = this.$refs.bg as any;
-			const main = this.$refs.main as any;
-
-			if (this.isModal) {
-				bg.style.pointerEvents = 'none';
-				anime({
-					targets: bg,
-					opacity: 0,
-					duration: this.animation ? 300 : 0,
-					easing: 'linear'
-				});
-			}
-
-			main.style.pointerEvents = 'none';
-
-			anime({
-				targets: main,
-				opacity: 0,
-				scale: 0.8,
-				duration: this.animation ? 300 : 0,
-				easing: 'cubicBezier(0.5, -0.5, 1, 0.5)'
-			});
-
-			setTimeout(() => {
-				this.$emit('closed');
-				this.destroyDom();
-			}, this.animation ? 300 : 0);
-		},
-
-		popout() {
-			const url = typeof this.popoutUrl == 'function' ? this.popoutUrl() : this.popoutUrl;
-
-			const main = this.$refs.main as any;
-
-			if (main) {
-				const position = main.getBoundingClientRect();
-
-				const width = parseInt(getComputedStyle(main, '').width, 10);
-				const height = parseInt(getComputedStyle(main, '').height, 10);
-				const x = window.screenX + position.left;
-				const y = window.screenY + position.top;
-
-				window.open(url, url,
-					`width=${width}, height=${height}, top=${y}, left=${x}`);
-
-				this.close();
-			} else {
-				const x = window.top.outerHeight / 2 + window.top.screenY - (parseInt(this.height, 10) / 2);
-				const y = window.top.outerWidth / 2 + window.top.screenX - (parseInt(this.width, 10) / 2);
-				window.open(url, url,
-					`width=${this.width}, height=${this.height}, top=${x}, left=${y}`);
-			}
-		},
-
-		// 最前面へ移動
-		top() {
-			let z = 0;
-
-			const ws = Array.from(this.$root.os.windows.getAll()).filter(w => w != this);
-			for (const w of ws) {
-				const m = w.$refs.main;
-				const mz = Number(document.defaultView.getComputedStyle(m, null).zIndex);
-				if (mz > z) z = mz;
-			}
-
-			if (z > 0) {
-				(this.$refs.main as any).style.zIndex = z + 1;
-				if (this.isModal) (this.$refs.bg as any).style.zIndex = z + 1;
-			}
-		},
-
-		onBgClick() {
-			if (this.canClose) this.close();
-		},
-
-		onBodyMousedown() {
-			this.top();
-		},
-
-		onHeaderMousedown(e) {
-			const main = this.$refs.main as any;
-
-			if (!contains(main, document.activeElement)) main.focus();
-
-			const position = main.getBoundingClientRect();
-
-			const clickX = e.clientX;
-			const clickY = e.clientY;
-			const moveBaseX = clickX - position.left;
-			const moveBaseY = clickY - position.top;
-			const browserWidth = window.innerWidth;
-			const browserHeight = window.innerHeight;
-			const windowWidth = main.offsetWidth;
-			const windowHeight = main.offsetHeight;
-
-			// 動かした時
-			dragListen(me => {
-				let moveLeft = me.clientX - moveBaseX;
-				let moveTop = me.clientY - moveBaseY;
-
-				// 下はみ出し
-				if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight;
-
-				// 左はみ出し
-				if (moveLeft < 0) moveLeft = 0;
-
-				// 上はみ出し
-				if (moveTop < 0) moveTop = 0;
-
-				// 右はみ出し
-				if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth;
-
-				main.style.left = moveLeft + 'px';
-				main.style.top = moveTop + 'px';
-			});
-		},
-
-		// 上ハンドル掴み時
-		onTopHandleMousedown(e) {
-			const main = this.$refs.main as any;
-
-			const base = e.clientY;
-			const height = parseInt(getComputedStyle(main, '').height, 10);
-			const top = parseInt(getComputedStyle(main, '').top, 10);
-
-			// 動かした時
-			dragListen(me => {
-				const move = me.clientY - base;
-				if (top + move > 0) {
-					if (height + -move > minHeight) {
-						this.applyTransformHeight(height + -move);
-						this.applyTransformTop(top + move);
-					} else { // 最小の高さより小さくなろうとした時
-						this.applyTransformHeight(minHeight);
-						this.applyTransformTop(top + (height - minHeight));
-					}
-				} else { // 上のはみ出し時
-					this.applyTransformHeight(top + height);
-					this.applyTransformTop(0);
-				}
-			});
-		},
-
-		// 右ハンドル掴み時
-		onRightHandleMousedown(e) {
-			const main = this.$refs.main as any;
-
-			const base = e.clientX;
-			const width = parseInt(getComputedStyle(main, '').width, 10);
-			const left = parseInt(getComputedStyle(main, '').left, 10);
-			const browserWidth = window.innerWidth;
-
-			// 動かした時
-			dragListen(me => {
-				const move = me.clientX - base;
-				if (left + width + move < browserWidth) {
-					if (width + move > minWidth) {
-						this.applyTransformWidth(width + move);
-					} else { // 最小の幅より小さくなろうとした時
-						this.applyTransformWidth(minWidth);
-					}
-				} else { // 右のはみ出し時
-					this.applyTransformWidth(browserWidth - left);
-				}
-			});
-		},
-
-		// 下ハンドル掴み時
-		onBottomHandleMousedown(e) {
-			const main = this.$refs.main as any;
-
-			const base = e.clientY;
-			const height = parseInt(getComputedStyle(main, '').height, 10);
-			const top = parseInt(getComputedStyle(main, '').top, 10);
-			const browserHeight = window.innerHeight;
-
-			// 動かした時
-			dragListen(me => {
-				const move = me.clientY - base;
-				if (top + height + move < browserHeight) {
-					if (height + move > minHeight) {
-						this.applyTransformHeight(height + move);
-					} else { // 最小の高さより小さくなろうとした時
-						this.applyTransformHeight(minHeight);
-					}
-				} else { // 下のはみ出し時
-					this.applyTransformHeight(browserHeight - top);
-				}
-			});
-		},
-
-		// 左ハンドル掴み時
-		onLeftHandleMousedown(e) {
-			const main = this.$refs.main as any;
-
-			const base = e.clientX;
-			const width = parseInt(getComputedStyle(main, '').width, 10);
-			const left = parseInt(getComputedStyle(main, '').left, 10);
-
-			// 動かした時
-			dragListen(me => {
-				const move = me.clientX - base;
-				if (left + move > 0) {
-					if (width + -move > minWidth) {
-						this.applyTransformWidth(width + -move);
-						this.applyTransformLeft(left + move);
-					} else { // 最小の幅より小さくなろうとした時
-						this.applyTransformWidth(minWidth);
-						this.applyTransformLeft(left + (width - minWidth));
-					}
-				} else { // 左のはみ出し時
-					this.applyTransformWidth(left + width);
-					this.applyTransformLeft(0);
-				}
-			});
-		},
-
-		// 左上ハンドル掴み時
-		onTopLeftHandleMousedown(e) {
-			this.onTopHandleMousedown(e);
-			this.onLeftHandleMousedown(e);
-		},
-
-		// 右上ハンドル掴み時
-		onTopRightHandleMousedown(e) {
-			this.onTopHandleMousedown(e);
-			this.onRightHandleMousedown(e);
-		},
-
-		// 右下ハンドル掴み時
-		onBottomRightHandleMousedown(e) {
-			this.onBottomHandleMousedown(e);
-			this.onRightHandleMousedown(e);
-		},
-
-		// 左下ハンドル掴み時
-		onBottomLeftHandleMousedown(e) {
-			this.onBottomHandleMousedown(e);
-			this.onLeftHandleMousedown(e);
-		},
-
-		// 高さを適用
-		applyTransformHeight(height) {
-			(this.$refs.main as any).style.height = height + 'px';
-		},
-
-		// 幅を適用
-		applyTransformWidth(width) {
-			(this.$refs.main as any).style.width = width + 'px';
-		},
-
-		// Y座標を適用
-		applyTransformTop(top) {
-			(this.$refs.main as any).style.top = top + 'px';
-		},
-
-		// X座標を適用
-		applyTransformLeft(left) {
-			(this.$refs.main as any).style.left = left + 'px';
-		},
-
-		onDragover(e) {
-			e.dataTransfer.dropEffect = 'none';
-		},
-
-		onKeydown(e) {
-			if (e.which == 27) { // Esc
-				if (this.canClose) {
-					e.preventDefault();
-					e.stopPropagation();
-					this.close();
-				}
-			}
-		},
-
-		onBrowserResize() {
-			const main = this.$refs.main as any;
-			const position = main.getBoundingClientRect();
-			const browserWidth = window.innerWidth;
-			const browserHeight = window.innerHeight;
-			const windowWidth = main.offsetWidth;
-			const windowHeight = main.offsetHeight;
-			if (position.left < 0) main.style.left = 0;     // 左はみ出し
-			if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px';  // 下はみ出し
-			if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px';    // 右はみ出し
-			if (position.top < 0) main.style.top = 0;       // 上はみ出し
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-window
-	display block
-
-	> .bg
-		display block
-		position fixed
-		z-index 2000
-		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 2000
-		top 15%
-		left 0
-		margin 0
-		opacity 0
-		pointer-events none
-
-		&:focus
-			&:not([data-is-modal])
-				> .body
-						box-shadow 0 0 0 1px var(--primaryAlpha05), 0 2px 12px 0 var(--desktopWindowShadow)
-
-		> .handle
-			$size = 8px
-
-			position absolute
-
-			&.top
-				top -($size)
-				left 0
-				width 100%
-				height $size
-				cursor ns-resize
-
-			&.right
-				top 0
-				right -($size)
-				width $size
-				height 100%
-				cursor ew-resize
-
-			&.bottom
-				bottom -($size)
-				left 0
-				width 100%
-				height $size
-				cursor ns-resize
-
-			&.left
-				top 0
-				left -($size)
-				width $size
-				height 100%
-				cursor ew-resize
-
-			&.top-left
-				top -($size)
-				left -($size)
-				width $size * 2
-				height $size * 2
-				cursor nwse-resize
-
-			&.top-right
-				top -($size)
-				right -($size)
-				width $size * 2
-				height $size * 2
-				cursor nesw-resize
-
-			&.bottom-right
-				bottom -($size)
-				right -($size)
-				width $size * 2
-				height $size * 2
-				cursor nwse-resize
-
-			&.bottom-left
-				bottom -($size)
-				left -($size)
-				width $size * 2
-				height $size * 2
-				cursor nesw-resize
-
-		> .body
-			height 100%
-			overflow hidden
-			background var(--face)
-			border-radius 6px
-			box-shadow 0 2px 12px 0 rgba(#000, 0.5)
-
-			> header
-				$header-height = 40px
-
-				z-index 1001
-				height $header-height
-				overflow hidden
-				white-space nowrap
-				cursor move
-				background var(--faceHeader)
-				border-radius 6px 6px 0 0
-				box-shadow 0 1px 0 rgba(#000, 0.1)
-
-				&, *
-					user-select none
-
-				> h1
-					pointer-events none
-					display block
-					margin 0 auto
-					overflow hidden
-					height $header-height
-					text-overflow ellipsis
-					text-align center
-					font-size 1em
-					line-height $header-height
-					font-weight normal
-					color var(--desktopWindowTitle)
-
-				> div:last-child
-					position absolute
-					top 0
-					right 0
-					display block
-					z-index 1
-
-					> *
-						display inline-block
-						margin 0
-						padding 0
-						cursor pointer
-						font-size 1em
-						color var(--faceTextButton)
-						border none
-						outline none
-						background transparent
-
-						&:hover
-							color var(--faceTextButtonHover)
-
-						&:active
-							color var(--faceTextButtonActive)
-
-						> i
-							display inline-block
-							padding 0
-							width $header-height
-							line-height $header-height
-							text-align center
-
-			> .content
-				height 100%
-				overflow auto
-
-	&:not([flexible])
-		> .main > .body > .content
-			height calc(100% - 40px)
-
-</style>
diff --git a/src/client/app/desktop/views/home/home.vue b/src/client/app/desktop/views/home/home.vue
deleted file mode 100644
index ec166d42c8aba844cb6f667b4ec7516c9299690c..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/home/home.vue
+++ /dev/null
@@ -1,406 +0,0 @@
-<template>
-<component :is="customize ? 'mk-dummy' : 'mk-ui'" v-hotkey.global="keymap" v-if="$store.getters.isSignedIn || $route.name != 'index'">
-	<div class="wqsofvpm" :data-customize="customize">
-		<div class="customize" v-if="customize">
-			<a @click="done()"><fa icon="check"/>{{ $t('done') }}</a>
-			<div>
-				<div class="adder">
-					<p>{{ $t('add-widget') }}</p>
-					<select v-model="widgetAdderSelected">
-						<option value="profile">{{ $t('@.widgets.profile') }}</option>
-						<option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option>
-						<option value="calendar">{{ $t('@.widgets.calendar') }}</option>
-						<option value="timemachine">{{ $t('@.widgets.timemachine') }}</option>
-						<option value="activity">{{ $t('@.widgets.activity') }}</option>
-						<option value="rss">{{ $t('@.widgets.rss') }}</option>
-						<option value="trends">{{ $t('@.widgets.trends') }}</option>
-						<option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option>
-						<option value="slideshow">{{ $t('@.widgets.slideshow') }}</option>
-						<option value="version">{{ $t('@.widgets.version') }}</option>
-						<option value="broadcast">{{ $t('@.widgets.broadcast') }}</option>
-						<option value="notifications">{{ $t('@.widgets.notifications') }}</option>
-						<option value="users">{{ $t('@.widgets.users') }}</option>
-						<option value="polls">{{ $t('@.widgets.polls') }}</option>
-						<option value="post-form">{{ $t('@.widgets.post-form') }}</option>
-						<option value="messaging">{{ $t('@.messaging') }}</option>
-						<option value="memo">{{ $t('@.widgets.memo') }}</option>
-						<option value="hashtags">{{ $t('@.widgets.hashtags') }}</option>
-						<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option>
-						<option value="server">{{ $t('@.widgets.server') }}</option>
-						<option value="queue">{{ $t('@.widgets.queue') }}</option>
-						<option value="nav">{{ $t('@.widgets.nav') }}</option>
-						<option value="tips">{{ $t('@.widgets.tips') }}</option>
-					</select>
-					<button @click="addWidget">{{ $t('add') }}</button>
-				</div>
-				<div class="trash">
-					<x-draggable v-model="trash" group="x" @add="onTrash"></x-draggable>
-					<p>{{ $t('@.trash') }}</p>
-				</div>
-			</div>
-		</div>
-		<div class="main" :class="{ side: widgets.left.length == 0 || widgets.right.length == 0 }">
-			<template v-if="customize">
-				<x-draggable v-for="place in ['left', 'right']"
-					:list="widgets[place]"
-					:class="place"
-					:data-place="place"
-					group="x"
-					animation="150"
-					@sort="onWidgetSort"
-					:key="place"
-				>
-					<div v-for="widget in widgets[place]" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
-						<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="desktop"/>
-					</div>
-				</x-draggable>
-				<div class="main">
-					<a @click="hint">{{ $t('@.customization-tips.title') }}</a>
-					<div>
-						<x-timeline/>
-					</div>
-				</div>
-			</template>
-			<template v-else>
-				<div v-for="place in ['left', 'right']" :class="place" :key="place">
-					<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="desktop"/>
-				</div>
-				<div class="main">
-					<router-view ref="content"></router-view>
-				</div>
-			</template>
-		</div>
-	</div>
-</component>
-<x-welcome v-else/>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import * as XDraggable from 'vuedraggable';
-import { v4 as uuid } from 'uuid';
-import XWelcome from '../pages/welcome.vue';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/home.vue'),
-
-	components: {
-		XDraggable,
-		XWelcome
-	},
-
-	data() {
-		return {
-			customize: window.location.search == '?customize',
-			connection: null,
-			widgetAdderSelected: null,
-			trash: [],
-			view: null
-		};
-	},
-
-	computed: {
-		home(): any[] {
-			if (this.$store.getters.isSignedIn) {
-				return this.$store.getters.home || [];
-			} else {
-				return [{
-					name: 'instance',
-					place: 'right'
-				}, {
-					name: 'broadcast',
-					place: 'right',
-					data: {}
-				}, {
-					name: 'hashtags',
-					place: 'right',
-					data: {}
-				}];
-			}
-		},
-		left(): any[] {
-			return this.home.filter(w => w.place == 'left');
-		},
-		right(): any[] {
-			return this.home.filter(w => w.place == 'right');
-		},
-		widgets(): any {
-			return {
-				left: this.left,
-				right: this.right
-			};
-		},
-		keymap(): any {
-			return {
-				't': this.focus
-			};
-		}
-	},
-
-	created() {
-		if (!this.$store.getters.isSignedIn) return;
-
-		if (this.$store.getters.home == null) {
-			const defaultDesktopHomeWidgets = {
-				left: [
-					'profile',
-					'calendar',
-					'activity',
-					'rss',
-					'hashtags',
-					'photo-stream',
-					'version'
-				],
-				right: [
-					'customize',
-					'broadcast',
-					'notifications',
-					'users',
-					'polls',
-					'server',
-					'nav',
-					'tips'
-				]
-			};
-
-			//#region Construct home data
-			const _defaultDesktopHomeWidgets = [];
-
-			for (const widget of defaultDesktopHomeWidgets.left) {
-				_defaultDesktopHomeWidgets.push({
-					name: widget,
-					id: uuid(),
-					place: 'left',
-					data: {}
-				});
-			}
-
-			for (const widget of defaultDesktopHomeWidgets.right) {
-				_defaultDesktopHomeWidgets.push({
-					name: widget,
-					id: uuid(),
-					place: 'right',
-					data: {}
-				});
-			}
-			//#endregion
-
-			this.$store.commit('setHome', _defaultDesktopHomeWidgets);
-		}
-	},
-
-	mounted() {
-		this.connection = this.$root.stream.useSharedConnection('main');
-	},
-
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		hint() {
-			this.$root.dialog({
-				title: this.$t('@.customization-tips.title'),
-				text: this.$t('@.customization-tips.paragraph')
-			});
-		},
-
-		onTlLoaded() {
-			this.$emit('loaded');
-		},
-
-		onWidgetContextmenu(widgetId) {
-			const w = (this.$refs[widgetId] as any)[0];
-			if (w.func) w.func();
-		},
-
-		onWidgetSort() {
-			this.saveHome();
-		},
-
-		onTrash(evt) {
-			this.saveHome();
-		},
-
-		addWidget() {
-			if(this.widgetAdderSelected == null) return;
-
-			this.$store.commit('addHomeWidget', {
-				name: this.widgetAdderSelected,
-				id: uuid(),
-				place: 'left',
-				data: {}
-			});
-		},
-
-		saveHome() {
-			const left = this.widgets.left;
-			const right = this.widgets.right;
-			this.$store.commit('setHome', left.concat(right));
-			for (const w of left) w.place = 'left';
-			for (const w of right) w.place = 'right';
-		},
-
-		done() {
-			location.href = '/';
-		},
-
-		focus() {
-			(this.$refs.content as any).focus();
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.wqsofvpm
-	display block
-
-	&[data-customize]
-		padding-top 48px
-		background-image url('/assets/desktop/grid.svg')
-
-		> .main > .main
-			> a
-				display block
-				margin-bottom 8px
-				text-align center
-
-			> div
-				cursor not-allowed !important
-
-				> *
-					pointer-events none
-
-	&:not([data-customize])
-		> .main > *:not(.main):empty
-			display none
-
-	> .customize
-		position fixed
-		z-index 1000
-		top 0
-		left 0
-		width 100%
-		height 48px
-		color var(--text)
-		background var(--desktopHeaderBg)
-		box-shadow 0 1px 1px rgba(#000, 0.075)
-
-		> a
-			display block
-			position absolute
-			z-index 1001
-			top 0
-			right 0
-			padding 0 16px
-			line-height 48px
-			text-decoration none
-			color var(--primaryForeground)
-			background var(--primary)
-			transition background 0.1s ease
-
-			&:hover
-				background var(--primaryLighten10)
-
-			&:active
-				background var(--primaryDarken10)
-				transition background 0s ease
-
-			> [data-icon]
-				margin-right 8px
-
-		> div
-			display flex
-			margin 0 auto
-			max-width 1220px - 32px
-
-			> div
-				width 50%
-
-				&.adder
-					> p
-						display inline
-						line-height 48px
-
-				&.trash
-					border-left solid 1px var(--faceDivider)
-
-					> div
-						width 100%
-						height 100%
-
-					> p
-						position absolute
-						top 0
-						left 0
-						width 100%
-						line-height 48px
-						margin 0
-						text-align center
-						pointer-events none
-
-	> .main
-		display flex
-		justify-content center
-		margin 0 auto
-		max-width 1240px
-
-		> *
-			.customize-container
-				cursor move
-				border-radius 6px
-
-				&:hover
-					box-shadow 0 0 8px rgba(64, 120, 200, 0.3)
-
-				> *
-					pointer-events none
-
-		> .main
-			padding 16px
-			width calc(100% - 280px * 2)
-			order 2
-
-		&.side
-			> .main
-				width calc(100% - 280px)
-				max-width 680px
-
-		> *:not(.main)
-			width 280px
-			padding 16px 0 16px 0
-
-			> *:not(:last-child)
-				margin-bottom 16px
-
-		> .left
-			padding-left 16px
-			order 1
-
-		> .right
-			padding-right 16px
-			order 3
-
-		&.side
-			@media (max-width 1000px)
-				> *:not(.main)
-					display none
-
-				> .main
-					width 100%
-					max-width 700px
-					margin 0 auto
-
-		&:not(.side)
-			@media (max-width 1100px)
-				> *:not(.main)
-					display none
-
-				> .main
-					width 100%
-					max-width 700px
-					margin 0 auto
-
-</style>
diff --git a/src/client/app/desktop/views/home/note.vue b/src/client/app/desktop/views/home/note.vue
deleted file mode 100644
index c19f58cd2b0f8fe07353c337f71e16819b0ab450..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/home/note.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-<template>
-<div v-if="!fetching" class="kcthdwmv">
-	<mk-note-detail :note="note" :key="note.id"/>
-	<footer>
-		<router-link v-if="note.next" :to="note.next"><fa icon="angle-left"/> {{ $t('next') }}</router-link>
-		<router-link v-if="note.prev" :to="note.prev">{{ $t('prev') }} <fa icon="angle-right"/></router-link>
-	</footer>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import Progress from '../../../common/scripts/loading';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/pages/note.vue'),
-	data() {
-		return {
-			fetching: true,
-			note: null
-		};
-	},
-	watch: {
-		$route: 'fetch'
-	},
-	created() {
-		this.fetch();
-	},
-	methods: {
-		fetch() {
-			Progress.start();
-			this.fetching = true;
-
-			this.$root.api('notes/show', {
-				noteId: this.$route.params.note
-			}).then(note => {
-				this.note = note;
-				this.fetching = false;
-
-				Progress.done();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.kcthdwmv
-	text-align center
-
-	> footer
-		margin-top 16px
-
-		> a
-			display inline-block
-			margin 0 16px
-
-</style>
diff --git a/src/client/app/desktop/views/home/search.vue b/src/client/app/desktop/views/home/search.vue
deleted file mode 100644
index 06b354b133045d7e71c152dcc6385190efc84a35..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/home/search.vue
+++ /dev/null
@@ -1,76 +0,0 @@
-<template>
-<div>
-	<mk-notes ref="timeline" :pagination="pagination" @inited="inited">
-		<template #header>
-			<header class="oxgbmvii">
-				<span><fa icon="search"/> {{ q }}</span>
-			</header>
-		</template>
-	</mk-notes>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import Progress from '../../../common/scripts/loading';
-import { genSearchQuery } from '../../../common/scripts/gen-search-query';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/pages/search.vue'),
-	data() {
-		return {
-			pagination: {
-				endpoint: 'notes/search',
-				limit: 20,
-				params: () => genSearchQuery(this, this.q)
-			}
-		};
-	},
-	computed: {
-		q(): string {
-			return this.$route.query.q;
-		}
-	},
-	watch: {
-		$route() {
-			this.$refs.timeline.reload();
-		}
-	},
-	mounted() {
-		document.addEventListener('keydown', this.onDocumentKeydown);
-		window.addEventListener('scroll', this.onScroll, { passive: true });
-		Progress.start();
-	},
-	beforeDestroy() {
-		document.removeEventListener('keydown', this.onDocumentKeydown);
-		window.removeEventListener('scroll', this.onScroll);
-	},
-	methods: {
-		onDocumentKeydown(e) {
-			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
-				if (e.which == 84) { // t
-					(this.$refs.timeline as any).focus();
-				}
-			}
-		},
-		inited() {
-			Progress.done();
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.oxgbmvii
-	padding 0 8px
-	z-index 10
-	background var(--faceHeader)
-	box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow)
-
-	> span
-		padding 0 8px
-		font-size 0.9em
-		line-height 42px
-		color var(--text)
-</style>
diff --git a/src/client/app/desktop/views/home/tag.vue b/src/client/app/desktop/views/home/tag.vue
deleted file mode 100644
index 343b4ce9519b88d1e2d7fe26ba82e9b1e3fee81b..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/home/tag.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-<div>
-	<mk-notes ref="timeline" :pagination="pagination" @loaded="inited">
-		<template #header>
-			<header class="wqraeznr">
-				<span><fa icon="hashtag"/> {{ $route.params.tag }}</span>
-			</header>
-		</template>
-	</mk-notes>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import Progress from '../../../common/scripts/loading';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/pages/tag.vue'),
-	computed: {
-		pagination() {
-			return {
-				endpoint: 'notes/search-by-tag',
-				limit: 20,
-				params: {
-					tag: this.$route.params.tag
-				}
-			};
-		}
-	},
-	mounted() {
-		document.addEventListener('keydown', this.onDocumentKeydown);
-		Progress.start();
-	},
-	beforeDestroy() {
-		document.removeEventListener('keydown', this.onDocumentKeydown);
-	},
-	methods: {
-		onDocumentKeydown(e) {
-			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
-				if (e.which == 84) { // t
-					(this.$refs.timeline as any).focus();
-				}
-			}
-		},
-		inited() {
-			Progress.done();
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.wqraeznr
-	padding 0 8px
-	z-index 10
-	background var(--faceHeader)
-	box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow)
-
-	> span
-		padding 0 8px
-		font-size 0.9em
-		line-height 42px
-		color var(--text)
-</style>
diff --git a/src/client/app/desktop/views/home/timeline.core.vue b/src/client/app/desktop/views/home/timeline.core.vue
deleted file mode 100644
index aae7dbc60e42185750001c4c3d20300a43b3adf7..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/home/timeline.core.vue
+++ /dev/null
@@ -1,144 +0,0 @@
-<template>
-<div>
-	<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')">
-		<template #header>
-			<slot></slot>
-			<div v-if="src == 'home' && alone" class="ibpylqas">
-				<p>{{ $t('@.empty-timeline-info.follow-users-to-make-your-timeline') }}</p>
-				<router-link to="/explore">{{ $t('@.empty-timeline-info.explore') }}</router-link>
-			</div>
-		</template>
-	</mk-notes>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/timeline.core.vue'),
-
-	props: {
-		src: {
-			type: String,
-			required: true
-		},
-		tagTl: {
-			required: false
-		}
-	},
-
-	data() {
-		return {
-			connection: null,
-			date: null,
-			baseQuery: {
-				includeMyRenotes: this.$store.state.settings.showMyRenotes,
-				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
-				includeLocalRenotes: this.$store.state.settings.showLocalRenotes
-			},
-			query: {},
-			endpoint: null,
-			pagination: null
-		};
-	},
-
-	computed: {
-		alone(): boolean {
-			return this.$store.state.i.followingCount == 0;
-		}
-	},
-
-	created() {
-		this.$root.$on('warp', this.warp);
-		this.$once('hook:beforeDestroy', () => {
-			this.$root.$off('warp', this.warp);
-			this.connection.dispose();
-		});
-
-		const prepend = note => {
-			(this.$refs.timeline as any).prepend(note);
-		};
-
-		if (this.src == 'tag') {
-			this.endpoint = 'notes/search-by-tag';
-			this.query = {
-				query: this.tagTl.query
-			};
-			this.connection = this.$root.stream.connectToChannel('hashtag', { q: this.tagTl.query });
-			this.connection.on('note', prepend);
-		} else if (this.src == 'home') {
-			this.endpoint = 'notes/timeline';
-			const onChangeFollowing = () => {
-				this.fetch();
-			};
-			this.connection = this.$root.stream.useSharedConnection('homeTimeline');
-			this.connection.on('note', prepend);
-			this.connection.on('follow', onChangeFollowing);
-			this.connection.on('unfollow', onChangeFollowing);
-		} else if (this.src == 'local') {
-			this.endpoint = 'notes/local-timeline';
-			this.connection = this.$root.stream.useSharedConnection('localTimeline');
-			this.connection.on('note', prepend);
-		} else if (this.src == 'hybrid') {
-			this.endpoint = 'notes/hybrid-timeline';
-			this.connection = this.$root.stream.useSharedConnection('hybridTimeline');
-			this.connection.on('note', prepend);
-		} else if (this.src == 'global') {
-			this.endpoint = 'notes/global-timeline';
-			this.connection = this.$root.stream.useSharedConnection('globalTimeline');
-			this.connection.on('note', prepend);
-		} else if (this.src == 'mentions') {
-			this.endpoint = 'notes/mentions';
-			this.connection = this.$root.stream.useSharedConnection('main');
-			this.connection.on('mention', prepend);
-		} else if (this.src == 'messages') {
-			this.endpoint = 'notes/mentions';
-			this.query = {
-				visibility: 'specified'
-			};
-			const onNote = note => {
-				if (note.visibility == 'specified') {
-					prepend(note);
-				}
-			};
-			this.connection = this.$root.stream.useSharedConnection('main');
-			this.connection.on('mention', onNote);
-		}
-
-		this.pagination = {
-			endpoint: this.endpoint,
-			limit: 10,
-			params: init => ({
-				untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
-				...this.baseQuery, ...this.query
-			})
-		};
-	},
-
-	methods: {
-		focus() {
-			(this.$refs.timeline as any).focus();
-		},
-
-		warp(date) {
-			this.date = date;
-			(this.$refs.timeline as any).reload();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.ibpylqas
-	padding 16px
-	text-align center
-	color var(--text)
-	border-bottom solid var(--lineWidth) var(--faceDivider)
-	font-size 14px
-
-	> p
-		margin 0 0 8px 0
-
-</style>
diff --git a/src/client/app/desktop/views/home/timeline.vue b/src/client/app/desktop/views/home/timeline.vue
deleted file mode 100644
index 224b937997bee9c09dc86c39fc7eba44a093d0dd..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/home/timeline.vue
+++ /dev/null
@@ -1,278 +0,0 @@
-<template>
-<div class="pwbzawku">
-	<x-post-form class="form" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }" v-if="$store.state.settings.showPostFormOnTopOfTl"/>
-	<div class="main">
-		<component :is="src == 'list' ? 'mk-user-list-timeline' : 'x-core'" ref="tl" v-bind="options">
-			<header class="zahtxcqi">
-				<div :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</div>
-				<div :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</div>
-				<div :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</div>
-				<div :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</div>
-				<div :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</div>
-				<div :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.name }}</div>
-				<div class="buttons">
-					<button :data-active="src == 'mentions'" @click="src = 'mentions'" :title="$t('mentions')"><fa icon="at"/><i class="indicator" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button>
-					<button :data-active="src == 'messages'" @click="src = 'messages'" :title="$t('messages')"><fa :icon="['far', 'envelope']"/><i class="indicator" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button>
-					<button @click="chooseTag" :title="$t('hashtag')" ref="tagButton"><fa icon="hashtag"/></button>
-					<button @click="chooseList" :title="$t('list')" ref="listButton"><fa icon="list"/></button>
-				</div>
-			</header>
-		</component>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XCore from './timeline.core.vue';
-import Menu from '../../../common/views/components/menu.vue';
-import MkSettingsWindow from '../components/settings-window.vue';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/components/timeline.vue'),
-
-	components: {
-		XCore,
-		XPostForm: () => import('../components/post-form.vue').then(m => m.default)
-	},
-
-	data() {
-		return {
-			src: 'home',
-			list: null,
-			tagTl: null,
-			enableLocalTimeline: false,
-			enableGlobalTimeline: false,
-		};
-	},
-
-	computed: {
-		options(): any {
-			return {
-				...(this.src == 'list' ? { list: this.list } : { src: this.src }),
-				...(this.src == 'tag' ? { tagTl: this.tagTl } : {}),
-				key: this.src == 'list' ? this.list.id : this.src
-			}
-		}
-	},
-
-	watch: {
-		src() {
-			this.saveSrc();
-		},
-
-		list(x) {
-			this.saveSrc();
-			if (x != null) this.tagTl = null;
-		},
-
-		tagTl(x) {
-			this.saveSrc();
-			if (x != null) this.list = null;
-		}
-	},
-
-	created() {
-		this.$root.getMeta().then((meta: Record<string, any>) => {
-			if (!(
-				this.enableGlobalTimeline = !meta.disableGlobalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin
-			) && this.src === 'global') this.src = 'local';
-			if (!(
-				this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin
-			) && ['local', 'hybrid'].includes(this.src)) this.src = 'home';
-		});
-
-		if (this.$store.state.device.tl) {
-			this.src = this.$store.state.device.tl.src;
-			if (this.src == 'list') {
-				this.list = this.$store.state.device.tl.arg;
-			} else if (this.src == 'tag') {
-				this.tagTl = this.$store.state.device.tl.arg;
-			}
-		}
-	},
-
-	mounted() {
-		document.title = this.$root.instanceName;
-
-		(this.$refs.tl as any).$once('loaded', () => {
-			this.$emit('loaded');
-		});
-	},
-
-	methods: {
-		saveSrc() {
-			this.$store.commit('device/setTl', {
-				src: this.src,
-				arg: this.src == 'list' ? this.list : this.tagTl
-			});
-		},
-
-		focus() {
-			(this.$refs.tl as any).focus();
-		},
-
-		warp(date) {
-			(this.$refs.tl as any).warp(date);
-		},
-
-		async chooseList() {
-			const lists = await this.$root.api('users/lists/list');
-
-			let menu = [{
-				icon: 'plus',
-				text: this.$t('add-list'),
-				action: () => {
-					this.$root.dialog({
-						title: this.$t('list-name'),
-						input: true
-					}).then(async ({ canceled, result: name }) => {
-						if (canceled) return;
-						const list = await this.$root.api('users/lists/create', {
-							name
-						});
-
-						this.list = list;
-						this.src = 'list';
-					});
-				}
-			}];
-
-			if (lists.length > 0) {
-				menu.push(null);
-			}
-
-			menu = menu.concat(lists.map(list => ({
-				icon: 'list',
-				text: list.name,
-				action: () => {
-					this.list = list;
-					this.src = 'list';
-				}
-			})));
-
-			this.$root.new(Menu, {
-				source: this.$refs.listButton,
-				items: menu
-			});
-		},
-
-		chooseTag() {
-			let menu = [{
-				icon: 'plus',
-				text: this.$t('add-tag-timeline'),
-				action: () => {
-					this.$root.new(MkSettingsWindow, {
-						initialPage: 'hashtags'
-					});
-				}
-			}];
-
-			if (this.$store.state.settings.tagTimelines.length > 0) {
-				menu.push(null);
-			}
-
-			menu = menu.concat(this.$store.state.settings.tagTimelines.map(t => ({
-				icon: 'hashtag',
-				text: t.title,
-				action: () => {
-					this.tagTl = t;
-					this.src = 'tag';
-				}
-			})));
-
-			this.$root.new(Menu, {
-				source: this.$refs.tagButton,
-				items: menu
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.pwbzawku
-	> .form
-		margin-bottom 16px
-
-		&.round
-			border-radius 6px
-
-		&.shadow
-			box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
-
-	header.zahtxcqi
-		display flex
-		flex-wrap wrap
-		padding 0 8px
-		z-index 10
-		background var(--faceHeader)
-		box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow)
-
-		> *
-			flex-shrink 0
-
-		> .buttons
-			margin-left auto
-
-			> button
-				padding 0 8px
-				font-size 0.9em
-				line-height 42px
-				color var(--faceTextButton)
-
-				> .indicator
-					position absolute
-					top -4px
-					right 4px
-					font-size 10px
-					color var(--notificationIndicator)
-					animation blink 1s infinite
-
-				&:hover
-					color var(--faceTextButtonHover)
-
-				&[data-active]
-					color var(--primary)
-					cursor default
-
-					&:before
-						content ""
-						display block
-						position absolute
-						bottom 0
-						left 0
-						width 100%
-						height 2px
-						background var(--primary)
-
-		> div:not(.buttons)
-			padding 0 10px
-			line-height 42px
-			font-size 12px
-			user-select none
-
-			&[data-active]
-				color var(--primary)
-				cursor default
-				font-weight bold
-
-				&:before
-					content ""
-					display block
-					position absolute
-					bottom 0
-					left -8px
-					width calc(100% + 16px)
-					height 2px
-					background var(--primary)
-
-			&:not([data-active])
-				color var(--desktopTimelineSrc)
-				cursor pointer
-
-				&:hover
-					color var(--desktopTimelineSrcHover)
-
-</style>
diff --git a/src/client/app/desktop/views/home/user/index.vue b/src/client/app/desktop/views/home/user/index.vue
deleted file mode 100644
index 98ad165d936092196b02f5934198daab0fe53623..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/home/user/index.vue
+++ /dev/null
@@ -1,90 +0,0 @@
-<template>
-<div class="omechnps" v-if="!fetching">
-	<div class="is-suspended" v-if="user.isSuspended" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
-		<fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }}
-	</div>
-	<div class="is-remote" v-if="user.host != null" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
-		<fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url" rel="nofollow noopener" target="_blank">{{ $t('@.view-on-remote') }}</a>
-	</div>
-	<div class="main">
-		<x-header class="header" :user="user"/>
-		<router-view :user="user"></router-view>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import parseAcct from '../../../../../../misc/acct/parse';
-import Progress from '../../../../common/scripts/loading';
-import XHeader from './user.header.vue';
-
-export default Vue.extend({
-	i18n: i18n(),
-	components: {
-		XHeader
-	},
-	data() {
-		return {
-			fetching: true,
-			user: null
-		};
-	},
-	watch: {
-		$route: 'fetch'
-	},
-	created() {
-		this.fetch();
-	},
-	methods: {
-		fetch() {
-			this.fetching = true;
-			Progress.start();
-			this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
-				this.user = user;
-				this.fetching = false;
-				Progress.done();
-			});
-		},
-
-		warp(date) {
-			(this.$refs.tl as any).warp(date);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.omechnps
-	width 100%
-	margin 0 auto
-
-	> .is-suspended
-	> .is-remote
-		margin-bottom 16px
-		padding 14px 16px
-		font-size 14px
-
-		&.round
-			border-radius 6px
-
-		&.shadow
-			box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
-
-		&.is-suspended
-			color var(--suspendedInfoFg)
-			background var(--suspendedInfoBg)
-
-		&.is-remote
-			color var(--remoteInfoFg)
-			background var(--remoteInfoBg)
-
-		> a
-			font-weight bold
-
-	> .main
-		> .header
-			margin-bottom 16px
-
-</style>
diff --git a/src/client/app/desktop/views/home/user/user.header.vue b/src/client/app/desktop/views/home/user/user.header.vue
deleted file mode 100644
index c8e777967845ee334941bc7c07ebcbb107786c96..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/home/user/user.header.vue
+++ /dev/null
@@ -1,292 +0,0 @@
-<template>
-<div class="header" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
-	<div class="banner-container" :style="style">
-		<div class="banner" ref="banner" :style="style"></div>
-		<div class="fade"></div>
-		<div class="title">
-			<p class="name">
-				<mk-user-name :user="user" :nowrap="false"/>
-			</p>
-			<div>
-				<span class="username"><mk-acct :user="user" :detail="true" /></span>
-				<span v-if="user.isBot" :title="$t('is-bot')"><fa icon="robot"/></span>
-			</div>
-		</div>
-		<span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('follows-you') }}</span>
-		<div class="actions" v-if="$store.getters.isSignedIn">
-			<button @click="menu" class="menu" ref="menu"><fa icon="ellipsis-h"/></button>
-			<mk-follow-button v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" class="follow"/>
-		</div>
-	</div>
-	<mk-avatar class="avatar" :user="user" :disable-preview="true"/>
-	<div class="body">
-		<div class="description">
-			<mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
-			<p v-else class="empty">{{ $t('no-description') }}</p>
-			<x-integrations :user="user" style="margin-top:16px;"/>
-		</div>
-		<div class="fields" v-if="user.fields" :key="user.id">
-			<dl class="field" v-for="(field, i) in user.fields" :key="i">
-				<dt class="name">
-					<mfm :text="field.name" :plain="true" :custom-emojis="user.emojis"/>
-				</dt>
-				<dd class="value">
-					<mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
-				</dd>
-			</dl>
-		</div>
-		<div class="info">
-			<span class="location" v-if="user.host === null && user.location"><fa icon="map-marker"/> {{ user.location }}</span>
-			<span class="birthday" v-if="user.host === null && user.birthday"><fa icon="birthday-cake"/> {{ user.birthday.replace('-', $t('year')).replace('-', $t('month')) + $t('day') }} ({{ $t('years-old', { age }) }})</span>
-		</div>
-		<div class="status">
-			<router-link :to="user | userPage()" class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</router-link>
-			<router-link :to="user | userPage('following')" class="following clickable"><b>{{ user.followingCount | number }}</b>{{ $t('following') }}</router-link>
-			<router-link :to="user | userPage('followers')" class="followers clickable"><b>{{ user.followersCount | number }}</b>{{ $t('followers') }}</router-link>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import * as age from 's-age';
-import XUserMenu from '../../../../common/views/components/user-menu.vue';
-import XIntegrations from '../../../../common/views/components/integrations.vue';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/pages/user/user.header.vue'),
-	components: {
-		XIntegrations
-	},
-	props: ['user'],
-	computed: {
-		style(): any {
-			if (this.user.bannerUrl == null) return {};
-			return {
-				backgroundColor: this.user.bannerColor,
-				backgroundImage: `url(${ this.user.bannerUrl })`
-			};
-		},
-
-		age(): number {
-			return age(this.user.birthday);
-		}
-	},
-	mounted() {
-		if (this.user.bannerUrl) {
-			//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);
-		}
-	},
-	methods: {
-		mention() {
-			this.$post({ mention: this.user });
-		},
-		onScroll() {
-			const banner = this.$refs.banner as any;
-
-			const top = window.scrollY;
-
-			const z = 1.25; // 奥行き(小さいほど奥)
-			const pos = -(top / z);
-			banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
-
-			const blur = top / 32
-			if (blur <= 10) banner.style.filter = `blur(${blur}px)`;
-		},
-
-		menu() {
-			const w = this.$root.new(XUserMenu, {
-				source: this.$refs.menu,
-				user: this.user
-			});
-			this.$once('hook:beforeDestroy', () => {
-				w.destroyDom();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.header
-	background var(--face)
-	overflow hidden
-
-	&.round
-		border-radius 6px
-
-	&.shadow
-		box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
-
-	> .banner-container
-		height 250px
-		overflow hidden
-		background-size cover
-		background-position center
-
-		> .banner
-			height 100%
-			background-color #383838
-			background-size cover
-			background-position center
-			box-shadow 0 0 128px rgba(0, 0, 0, 0.5) inset
-
-		> .fade
-			position absolute
-			bottom 0
-			left 0
-			width 100%
-			height 78px
-			background linear-gradient(transparent, rgba(#000, 0.7))
-
-		> .followed
-			position absolute
-			top 12px
-			left 12px
-			padding 4px 6px
-			color #fff
-			background rgba(0, 0, 0, 0.7)
-			font-size 12px
-
-		> .actions
-			position absolute
-			top 12px
-			right 12px
-
-			> .menu
-				height 100%
-				padding 0 14px
-				color #fff
-				text-shadow 0 0 8px #000
-				font-size 16px
-
-		> .title
-			position absolute
-			bottom 0
-			left 0
-			width 100%
-			padding 0 0 8px 154px
-			color #fff
-
-			> .name
-				display block
-				margin 0
-				line-height 32px
-				font-weight bold
-				font-size 1.8em
-				text-shadow 0 0 8px #000
-
-			> 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)
-
-		> &.cat::before,
-		> &.cat::after
-			border-width 8px
-
-	> .body
-		padding 16px 16px 16px 154px
-		color var(--text)
-
-		> .description
-			font-size 15px
-
-			> .empty
-				margin 0
-				opacity 0.5
-
-		> .fields
-			margin-top 16px
-
-			> .field
-				display flex
-				padding 0
-				margin 0
-				align-items center
-
-				> .name
-					border-right solid 1px var(--faceDivider)
-					padding 4px
-					margin 4px
-					width 30%
-					overflow hidden
-					white-space nowrap
-					text-overflow ellipsis
-					font-weight bold
-					text-align center
-
-				> .value
-					padding 4px
-					margin 4px
-					width 70%
-					overflow hidden
-					white-space nowrap
-					text-overflow ellipsis
-
-		> .info
-			margin-top 16px
-			padding-top 16px
-			border-top solid 1px var(--faceDivider)
-			font-size 15px
-
-			&:empty
-				display none
-
-			> *
-				margin-right 16px
-
-		> .status
-			margin-top 16px
-			padding-top 16px
-			border-top solid 1px var(--faceDivider)
-			font-size 80%
-
-			> *
-				display inline-block
-				padding-right 16px
-				margin-right 16px
-				color inherit
-
-				&:not(:last-child)
-					border-right solid 1px var(--faceDivider)
-
-				&.clickable
-					cursor pointer
-
-					&:hover
-						color var(--faceTextButtonHover)
-
-				> b
-					margin-right 4px
-					font-size 1rem
-					font-weight bold
-					color var(--primary)
-
-</style>
diff --git a/src/client/app/desktop/views/home/user/user.home.vue b/src/client/app/desktop/views/home/user/user.home.vue
deleted file mode 100644
index c47e0a0771d7e721ceccea1e246a2bb5e1511c70..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/home/user/user.home.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<template>
-<div class="lnctpgve">
-	<x-page v-if="user.pinnedPage" :page="user.pinnedPage" :key="user.pinnedPage.id" :show-title="!user.pinnedPage.hideTitleWhenPinned"/>
-	<mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
-	<!--<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>-->
-	<div class="activity">
-		<ui-container :body-togglable="true"
-			:expanded="$store.state.device.expandUsersActivity"
-			@toggle="expanded => $store.commit('device/set', { key: 'expandUsersActivity', value: expanded })">
-			<template #header><fa icon="chart-bar"/>{{ $t('activity') }}</template>
-			<x-activity :user="user" :limit="35" style="padding: 16px;"/>
-		</ui-container>
-	</div>
-	<x-photos :user="user"/>
-	<x-timeline ref="tl" :user="user"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import XTimeline from './user.timeline.vue';
-import XPhotos from './user.photos.vue';
-import XActivity from '../../../../common/views/components/activity.vue';
-import XPage from '../../../../common/views/components/page/page.vue';
-
-export default Vue.extend({
-	i18n: i18n(),
-	components: {
-		XTimeline,
-		XPhotos,
-		XActivity,
-		XPage,
-	},
-	props: {
-		user: {
-			type: Object,
-			required: true
-		}
-	},
-	methods: {
-		warp(date) {
-			(this.$refs.tl as any).warp(date);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.lnctpgve
-	> *
-		margin-bottom 16px
-
-</style>
diff --git a/src/client/app/desktop/views/home/user/user.photos.vue b/src/client/app/desktop/views/home/user/user.photos.vue
deleted file mode 100644
index 03abcf865c0a5aedc64486906fcd186d0f275f24..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/home/user/user.photos.vue
+++ /dev/null
@@ -1,98 +0,0 @@
-<template>
-<ui-container :body-togglable="true"
-	:expanded="$store.state.device.expandUsersPhotos"
-	@toggle="expanded => $store.commit('device/set', { key: 'expandUsersPhotos', value: expanded })">
-	<template #header><fa icon="camera"/> {{ $t('title') }}</template>
-
-	<div class="dzsuvbsrrrwobdxifudxuefculdfiaxd">
-		<p class="initializing" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('loading') }}<mk-ellipsis/></p>
-		<div class="stream" v-if="!fetching && images.length > 0">
-			<router-link v-for="image in images" class="img"
-				:style="`background-image: url(${image.thumbnailUrl})`"
-				:key="`${image.id}:${image._note.id}`"
-				:to="image._note | notePage"
-				:title="`${image.name}\n${(new Date(image.createdAt)).toLocaleString()}`"
-			></router-link>
-		</div>
-		<p class="empty" v-if="!fetching && images.length == 0">{{ $t('no-photos') }}</p>
-	</div>
-</ui-container>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import { getStaticImageUrl } from '../../../../common/scripts/get-static-image-url';
-import { concat } from '../../../../../../prelude/array';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/pages/user/user.photos.vue'),
-	props: ['user'],
-	data() {
-		return {
-			images: [],
-			fetching: true
-		};
-	},
-	mounted() {
-		const image = [
-			'image/jpeg',
-			'image/png',
-			'image/gif',
-			'image/apng',
-			'image/vnd.mozilla.apng',
-		];
-
-		this.$root.api('users/notes', {
-			userId: this.user.id,
-			fileType: image,
-			excludeNsfw: !this.$store.state.device.alwaysShowNsfw,
-			limit: 9,
-		}).then(notes => {
-			for (const note of notes) {
-				for (const file of note.files) {
-					file._note = note;
-				}
-			}
-			const files = concat(notes.map((n: any): any[] => n.files));
-			this.images = files.filter(f => image.includes(f.type)).slice(0, 9);
-			this.fetching = false;
-		});
-	},
-	methods: {
-		thumbnail(image: any): string {
-			return this.$store.state.device.disableShowingAnimatedImages
-				? getStaticImageUrl(image.thumbnailUrl)
-				: image.thumbnailUrl;
-		},
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-.dzsuvbsrrrwobdxifudxuefculdfiaxd
-	> .stream
-		display grid
-		grid-template-columns 1fr 1fr 1fr
-		gap 8px
-		padding 16px
-		background var(--face)
-
-		> *
-			height 120px
-			background-position center center
-			background-size cover
-			background-clip content-box
-			border-radius 4px
-
-	> .initializing
-	> .empty
-		margin 0
-		padding 16px
-		text-align center
-		color var(--text)
-
-		> i
-			margin-right 4px
-
-</style>
diff --git a/src/client/app/desktop/views/home/user/user.timeline.vue b/src/client/app/desktop/views/home/user/user.timeline.vue
deleted file mode 100644
index 2a97f2c96e2b64c443de27cb11a689c2ce9a00af..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/home/user/user.timeline.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-<template>
-<div>
-	<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')">
-		<template #header>
-			<header class="kugajpep">
-				<span :data-active="mode == 'default'" @click="mode = 'default'"><fa :icon="['far', 'comment-alt']"/> {{ $t('default') }}</span>
-				<span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'"><fa icon="comments"/> {{ $t('with-replies') }}</span>
-				<span :data-active="mode == 'with-media'" @click="mode = 'with-media'"><fa :icon="['far', 'images']"/> {{ $t('with-media') }}</span>
-				<span :data-active="mode == 'my-posts'" @click="mode = 'my-posts'"><fa icon="user"/> {{ $t('my-posts') }}</span>
-			</header>
-		</template>
-	</mk-notes>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/pages/user/user.timeline.vue'),
-
-	props: ['user'],
-
-	data() {
-		return {
-			fetching: true,
-			mode: 'default',
-			unreadCount: 0,
-			date: null,
-			pagination: {
-				endpoint: 'users/notes',
-				limit: 10,
-				params: init => ({
-					userId: this.user.id,
-					untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
-					includeReplies: this.mode == 'with-replies',
-					includeMyRenotes: this.mode != 'my-posts',
-					withFiles: this.mode == 'with-media',
-				})
-			}
-		};
-	},
-
-	watch: {
-		mode() {
-			(this.$refs.timeline as any).reload();
-		}
-	},
-
-	mounted() {
-		document.addEventListener('keydown', this.onDocumentKeydown);
-		this.$root.$on('warp', this.warp);
-		this.$once('hook:beforeDestroy', () => {
-			this.$root.$off('warp', this.warp);
-			document.removeEventListener('keydown', this.onDocumentKeydown);
-		});
-	},
-
-	methods: {
-		onDocumentKeydown(e) {
-			if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') {
-				if (e.which == 84) { // [t]
-					(this.$refs.timeline as any).focus();
-				}
-			}
-		},
-
-		warp(date) {
-			this.date = date;
-			(this.$refs.timeline as any).reload();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.kugajpep
-	padding 0 8px
-	z-index 10
-	background var(--faceHeader)
-	box-shadow 0 1px var(--desktopTimelineHeaderShadow)
-
-	> span
-		display inline-block
-		padding 0 10px
-		line-height 42px
-		font-size 12px
-		user-select none
-
-		&[data-active]
-			color var(--primary)
-			cursor default
-			font-weight bold
-
-			&:before
-				content ""
-				display block
-				position absolute
-				bottom 0
-				left -8px
-				width calc(100% + 16px)
-				height 2px
-				background var(--primary)
-
-		&:not([data-active])
-			color var(--desktopTimelineSrc)
-			cursor pointer
-
-			&:hover
-				color var(--desktopTimelineSrcHover)
-
-</style>
diff --git a/src/client/app/desktop/views/pages/drive.vue b/src/client/app/desktop/views/pages/drive.vue
deleted file mode 100644
index b389392ec800208c119b9e03dd375d83308cd791..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/pages/drive.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<template>
-<div class="mk-drive-page">
-	<x-drive :init-folder="folder" @move-root="onMoveRoot" @open-folder="onOpenFolder"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/pages/drive.vue'),
-	components: {
-		XDrive: () => import('../components/drive.vue').then(m => m.default),
-	},
-	data() {
-		return {
-			folder: null
-		};
-	},
-	created() {
-		this.folder = this.$route.params.folder;
-	},
-	mounted() {
-		document.title = this.$t('title');
-	},
-	methods: {
-		onMoveRoot() {
-			const title = this.$t('title');
-
-			// Rewrite URL
-			history.pushState(null, title, '/i/drive');
-
-			document.title = title;
-		},
-		onOpenFolder(folder) {
-			const title = `${folder.name} | ${this.$t('title')}`;
-
-			// Rewrite URL
-			history.pushState(null, title, `/i/drive/folder/${folder.id}`);
-
-			document.title = title;
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-drive-page
-	position fixed
-	width 100%
-	height 100%
-	background #fff
-
-	> .mk-drive
-		height 100%
-</style>
diff --git a/src/client/app/desktop/views/pages/games/reversi.vue b/src/client/app/desktop/views/pages/games/reversi.vue
deleted file mode 100644
index b859b95d7fe217e75f3d4da42f369a5bea128364..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/pages/games/reversi.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-<template>
-<component :is="ui ? 'mk-ui' : 'div'">
-	<x-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/>
-</component>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	components: {
-		XReversi: () => import('../../../../common/views/components/games/reversi/reversi.vue').then(m => m.default)
-	},
-	props: {
-		ui: {
-			default: false
-		}
-	},
-	methods: {
-		nav(game, actualNav) {
-			if (actualNav) {
-				this.$router.push(`/games/reversi/${game.id}`);
-			} else {
-				// TODO: https://github.com/vuejs/vue-router/issues/703
-				this.$router.push(`/games/reversi/${game.id}`);
-			}
-		}
-	}
-});
-</script>
diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue
deleted file mode 100644
index c725074b7d92dcde47a83c6b27a5baf769e075e5..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/pages/messaging-room.vue
+++ /dev/null
@@ -1,82 +0,0 @@
-<template>
-<div class="mk-messaging-room-page">
-	<x-messaging-room v-if="user || group" :user="user" :group="group" :is-naked="true"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import Progress from '../../../common/scripts/loading';
-import parseAcct from '../../../../../misc/acct/parse';
-import getUserName from '../../../../../misc/get-user-name';
-
-export default Vue.extend({
-	i18n: i18n(),
-	components: {
-		XMessagingRoom: () => import('../../../common/views/components/messaging-room.vue').then(m => m.default)
-	},
-	data() {
-		return {
-			fetching: true,
-			user: null,
-			group: null
-		};
-	},
-	watch: {
-		$route: 'fetch'
-	},
-	created() {
-		const applyBg = v =>
-			document.documentElement.style.setProperty('background', v ? '#191b22' : '#fff', 'important');
-
-		applyBg(this.$store.state.device.darkmode);
-
-		this.unwatchDarkmode = this.$store.watch(s => {
-			return s.device.darkmode;
-		}, applyBg);
-
-		this.fetch();
-	},
-	beforeDestroy() {
-		document.documentElement.style.removeProperty('background');
-		document.documentElement.style.removeProperty('background-color'); // for safari's bug
-		this.unwatchDarkmode();
-	},
-	methods: {
-		fetch() {
-			Progress.start();
-			this.fetching = true;
-
-			if (this.$route.params.user) {
-				this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
-					this.user = user;
-					this.fetching = false;
-
-					document.title = this.$t('@.messaging') + ': ' + getUserName(this.user);
-
-					Progress.done();
-				});
-			} else {
-				this.$root.api('users/groups/show', { groupId: this.$route.params.group }).then(group => {
-					this.group = group;
-					this.fetching = false;
-
-					document.title = this.$t('@.messaging') + ': ' + this.group.name;
-
-					Progress.done();
-				});
-			}
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-messaging-room-page
-	display flex
-	flex 1
-	flex-direction column
-	min-height 100%
-
-</style>
diff --git a/src/client/app/desktop/views/pages/selectdrive.vue b/src/client/app/desktop/views/pages/selectdrive.vue
deleted file mode 100644
index 7e6a8e1937729dc19296303682a165bddb7f283b..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/pages/selectdrive.vue
+++ /dev/null
@@ -1,182 +0,0 @@
-<template>
-<div class="mkp-selectdrive">
-	<x-drive ref="browser"
-		:multiple="multiple"
-		@selected="onSelected"
-		@change-selection="onChangeSelection"
-	/>
-	<footer>
-		<button class="upload" :title="$t('upload')" @click="upload"><fa icon="upload"/></button>
-		<button class="cancel" @click="close">{{ $t('cancel') }}</button>
-		<button class="ok" @click="ok">{{ $t('ok') }}</button>
-	</footer>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/pages/selectdrive.vue'),
-	components: {
-		XDrive: () => import('../components/drive.vue').then(m => m.default),
-	},
-	data() {
-		return {
-			files: []
-		};
-	},
-	computed: {
-		multiple(): boolean {
-			const q = (new URL(location.toString())).searchParams;
-			return q.get('multiple') == 'true';
-		}
-	},
-	mounted() {
-		document.title = this.$t('title');
-	},
-	methods: {
-		onSelected(file) {
-			this.files = [file];
-			this.ok();
-		},
-		onChangeSelection(files) {
-			this.files = files;
-		},
-		upload() {
-			(this.$refs.browser as any).selectLocalFile();
-		},
-		close() {
-			window.close();
-		},
-		ok() {
-			window.opener.cb(this.multiple ? this.files : this.files[0]);
-			this.close();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-
-
-.mkp-selectdrive
-	display block
-	position fixed
-	width 100%
-	height 100%
-	background #fff
-
-	> .mk-drive
-		height calc(100% - 72px)
-
-	> footer
-		position fixed
-		bottom 0
-		left 0
-		width 100%
-		height 72px
-		background var(--primaryLighten95)
-
-		.upload
-			display inline-block
-			position absolute
-			top 8px
-			left 16px
-			cursor pointer
-			padding 0
-			margin 8px 4px 0 0
-			width 40px
-			height 40px
-			font-size 1em
-			color var(--primaryAlpha05)
-			background transparent
-			outline none
-			border solid 1px transparent
-			border-radius 4px
-
-			&:hover
-				background transparent
-				border-color var(--primaryAlpha03)
-
-			&:active
-				color var(--primaryAlpha06)
-				background transparent
-				border-color var(--primaryAlpha05)
-				//box-shadow 0 2px 4px rgba(var(--primaryDarken50), 0.15) inset
-
-			&:focus
-				&:after
-					content ""
-					pointer-events none
-					position absolute
-					top -5px
-					right -5px
-					bottom -5px
-					left -5px
-					border 2px solid var(--primaryAlpha03)
-					border-radius 8px
-
-		.ok
-		.cancel
-			display block
-			position absolute
-			bottom 16px
-			cursor pointer
-			padding 0
-			margin 0
-			width 120px
-			height 40px
-			font-size 1em
-			outline none
-			border-radius 4px
-
-			&:focus
-				&:after
-					content ""
-					pointer-events none
-					position absolute
-					top -5px
-					right -5px
-					bottom -5px
-					left -5px
-					border 2px solid var(--primaryAlpha03)
-					border-radius 8px
-
-			&:disabled
-				opacity 0.7
-				cursor default
-
-		.ok
-			right 16px
-			color var(--primaryForeground)
-			background linear-gradient(to bottom, var(--primaryLighten25) 0%, var(--primaryLighten10) 100%)
-			border solid 1px var(--primaryLighten15)
-
-			&:not(:disabled)
-				font-weight bold
-
-			&:hover:not(:disabled)
-				background linear-gradient(to bottom, var(--primaryLighten8) 0%, var(--primaryDarken8) 100%)
-				border-color var(--primary)
-
-			&:active:not(:disabled)
-				background var(--primary)
-				border-color var(--primary)
-
-		.cancel
-			right 148px
-			color #888
-			background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
-			border solid 1px #e2e2e2
-
-			&:hover
-				background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
-				border-color #dcdcdc
-
-			&:active
-				background #ececec
-				border-color #dcdcdc
-
-</style>
diff --git a/src/client/app/desktop/views/pages/settings.vue b/src/client/app/desktop/views/pages/settings.vue
deleted file mode 100644
index 826fae25290de5049b3892b37511a6eaf0ed551d..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/pages/settings.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<template>
-<mk-ui>
-	<main>
-		<x-settings :in-window="false" :page="$route.params.page" />
-	</main>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	components: {
-		XSettings: () => import('../components/settings.vue').then(m => m.default)
-	},
-	mounted() {
-		document.title = this.$root.instanceName;
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-main
-	margin 0 auto
-	max-width 900px
-
-</style>
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
deleted file mode 100644
index 511e1548e5d8704ff0fc59e991dd110847d5fac6..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ /dev/null
@@ -1,509 +0,0 @@
-<template>
-<div class="mk-welcome">
-	<div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div>
-
-	<button @click="dark">
-		<template v-if="$store.state.device.darkmode"><fa icon="moon"/></template>
-		<template v-else><fa :icon="['far', 'moon']"/></template>
-	</button>
-
-	<mk-forkit class="forkit"/>
-
-	<main>
-		<div class="body">
-			<div class="main block">
-				<div>
-					<h1 v-if="name != null && name != ''">{{ name }}</h1>
-					<h1 v-else><img svg-inline src="../../../../assets/title.svg" alt="Misskey"></h1>
-
-					<div class="info">
-						<span><b>{{ host }}</b> - <span v-html="$t('powered-by-misskey')"></span></span>
-						<span class="stats" v-if="stats">
-							<span><fa icon="user"/> {{ stats.originalUsersCount | number }}</span>
-							<span><fa icon="pencil-alt"/> {{ stats.originalNotesCount | number }}</span>
-						</span>
-					</div>
-
-					<div class="desc">
-						<span class="desc" v-html="description || $t('@.about')"></span>
-						<a class="about" @click="about">{{ $t('about') }}</a>
-					</div>
-
-					<p class="sign">
-						<span class="signup" @click="signup">{{ $t('@.signup') }}</span>
-						<span class="divider">|</span>
-						<span class="signin" @click="signin">{{ $t('@.signin') }}</span>
-					</p>
-
-					<img v-if="meta" :src="meta.mascotImageUrl" alt="" title="藍" class="char">
-				</div>
-			</div>
-
-			<div class="announcements block">
-				<header><fa icon="broadcast-tower"/> {{ $t('announcements') }}</header>
-				<div v-if="announcements && announcements.length > 0">
-					<div v-for="announcement in announcements">
-						<h1 v-html="announcement.title"></h1>
-						<mfm :text="announcement.text"/>
-						<img v-if="announcement.image" :src="announcement.image" alt="" style="display: block; max-height: 130px; max-width: 100%;"/>
-					</div>
-				</div>
-			</div>
-
-			<div class="photos block">
-				<header><fa :icon="['far', 'images']"/> {{ $t('photos') }}</header>
-				<div>
-					<div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div>
-				</div>
-			</div>
-
-			<div class="tag-cloud block">
-				<div>
-					<mk-tag-cloud/>
-				</div>
-			</div>
-
-			<div class="nav block">
-				<div>
-					<mk-nav class="nav"/>
-				</div>
-			</div>
-
-			<div class="side">
-				<div class="trends block">
-					<div>
-						<mk-trends/>
-					</div>
-				</div>
-
-				<div class="tl block">
-					<header><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</header>
-					<div>
-						<mk-welcome-timeline class="tl" :max="20"/>
-					</div>
-				</div>
-
-				<div class="info block">
-					<header><fa icon="info-circle"/> {{ $t('info') }}</header>
-					<div>
-						<div v-if="meta" class="body">
-							<p>Version: <b>{{ meta.version }}</b></p>
-							<p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p>
-						</div>
-					</div>
-				</div>
-			</div>
-		</div>
-	</main>
-
-	<modal name="about" class="about modal" width="800px" height="auto" scrollable>
-		<article class="fpdezooorhntlzyeszemrsqdlgbysvxq">
-			<h1>{{ $t('@.intro.title') }}</h1>
-			<p v-html="this.$t('@.intro.about')"></p>
-			<section>
-				<h2>{{ $t('@.intro.features') }}</h2>
-				<section>
-					<div class="body">
-						<h3>{{ $t('@.intro.rich-contents') }}</h3>
-						<p v-html="this.$t('@.intro.rich-contents-desc')"></p>
-					</div>
-					<div class="image"><img src="/assets/about/post.png" alt=""></div>
-				</section>
-				<section>
-					<div class="body">
-						<h3>{{ $t('@.intro.reaction') }}</h3>
-						<p v-html="this.$t('@.intro.reaction-desc')"></p>
-					</div>
-					<div class="image"><img src="/assets/about/reaction.png" alt=""></div>
-				</section>
-				<section>
-					<div class="body">
-						<h3>{{ $t('@.intro.ui') }}</h3>
-						<p v-html="this.$t('@.intro.ui-desc')"></p>
-					</div>
-					<div class="image"><img src="/assets/about/ui.png" alt=""></div>
-				</section>
-				<section>
-					<div class="body">
-						<h3>{{ $t('@.intro.drive') }}</h3>
-						<p v-html="this.$t('@.intro.drive-desc')"></p>
-					</div>
-					<div class="image"><img src="/assets/about/drive.png" alt=""></div>
-				</section>
-			</section>
-			<p v-html="this.$t('@.intro.outro')"></p>
-		</article>
-	</modal>
-
-	<modal name="signup" class="modal" width="450px" height="auto" scrollable>
-		<header class="formHeader">{{ $t('@.signup') }}</header>
-		<mk-signup class="form"/>
-	</modal>
-
-	<modal name="signin" class="modal" width="450px" height="auto" scrollable>
-		<header class="formHeader">{{ $t('@.signin') }}</header>
-		<mk-signin class="form"/>
-	</modal>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { host, copyright } from '../../../config';
-import { concat } from '../../../../../prelude/array';
-import { toUnicode } from 'punycode';
-
-export default Vue.extend({
-	i18n: i18n('desktop/views/pages/welcome.vue'),
-	data() {
-		return {
-			meta: null,
-			stats: null,
-			banner: null,
-			copyright,
-			host: toUnicode(host),
-			name: null,
-			description: '',
-			announcements: [],
-			photos: []
-		};
-	},
-
-	created() {
-		this.$root.getMeta().then(meta => {
-			this.meta = meta;
-			this.name = meta.name;
-			this.description = meta.description;
-			this.announcements = meta.announcements;
-			this.banner = meta.bannerUrl;
-		});
-
-		this.$root.api('stats').then(stats => {
-			this.stats = stats;
-		});
-
-		const image = [
-			'image/jpeg',
-			'image/png',
-			'image/gif',
-			'image/apng',
-			'image/vnd.mozilla.apng',
-		];
-
-		this.$root.api('notes/local-timeline', {
-			fileType: image,
-			excludeNsfw: true,
-			limit: 6
-		}).then((notes: any[]) => {
-			const files = concat(notes.map((n: any): any[] => n.files));
-			this.photos = files.filter(f => image.includes(f.type)).slice(0, 6);
-		});
-	},
-
-	methods: {
-		about() {
-			this.$modal.show('about');
-		},
-
-		signup() {
-			this.$modal.show('signup');
-		},
-
-		signin() {
-			this.$modal.show('signin');
-		},
-
-		dark() {
-			this.$store.commit('device/set', {
-				key: 'darkmode',
-				value: !this.$store.state.device.darkmode
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus">
-#wait
-	right auto
-	left 15px
-
-.v--modal-overlay
-	background rgba(0, 0, 0, 0.6)
-
-.modal
-	.form
-		padding 24px 48px 48px 48px
-
-	.formHeader
-		text-align center
-		padding 48px 0 12px 0
-		margin 0 48px
-		font-size 1.5em
-
-	.v--modal-box
-		background var(--face)
-		color var(--text)
-
-		.formHeader
-			border-bottom solid 1px rgba(#000, 0.2)
-
-.v--modal-overlay.about
-	.v--modal-box.v--modal
-		margin 32px 0
-
-.fpdezooorhntlzyeszemrsqdlgbysvxq
-	padding 64px
-
-	> p:last-child
-		margin-bottom 0
-
-	> h1
-		margin-top 0
-
-	> section
-		> h2
-			border-bottom 1px solid var(--faceDivider)
-
-		> section
-			display grid
-			grid-template-rows 1fr
-			grid-template-columns 180px 1fr
-			gap 32px
-			margin-bottom 32px
-			padding-bottom 32px
-			border-bottom 1px solid var(--faceDivider)
-
-			&:nth-child(odd)
-				grid-template-columns 1fr 180px
-
-				> .body
-					grid-column 1
-
-				> .image
-					grid-column 2
-
-			> .body
-				grid-row 1
-				grid-column 2
-
-			> .image
-				grid-row 1
-				grid-column 1
-
-				> img
-					display block
-					width 100%
-					height 100%
-					object-fit cover
-</style>
-
-<style lang="stylus" scoped>
-.mk-welcome
-	display flex
-	min-height 100vh
-
-	> .banner
-		position absolute
-		top 0
-		left 0
-		width 100%
-		height 400px
-		background-position center
-		background-size cover
-		opacity 0.7
-
-		&:after
-			content ""
-			display block
-			position absolute
-			bottom 0
-			left 0
-			width 100%
-			height 100px
-			background linear-gradient(transparent, var(--bg))
-
-	> .forkit
-		position absolute
-		top 0
-		right 0
-
-	> button
-		position fixed
-		z-index 1
-		bottom 16px
-		left 16px
-		padding 16px
-		font-size 18px
-		color var(--text)
-
-	> main
-		margin 0 auto
-		padding 64px
-		width 100%
-		max-width 1200px
-
-		.block
-			color var(--text)
-			background var(--face)
-			overflow auto
-
-			> header
-				z-index 1
-				padding 0 16px
-				line-height 48px
-				background var(--faceHeader)
-				box-shadow 0 1px 0 rgba(0, 0, 0, 0.1)
-
-				& + div
-					max-height calc(100% - 48px)
-
-			> div
-				overflow auto
-
-		> .body
-			display grid
-			grid-template-rows 390px 1fr 256px 64px
-			grid-template-columns 1fr 1fr 350px
-			gap 16px
-			height 1150px
-
-			> .main
-				grid-row 1
-				grid-column 1 / 3
-
-				> div
-					padding 32px
-					min-height 100%
-
-					> h1
-						margin 0
-
-						> svg
-							margin -8px 0 0 -16px
-							width 280px
-							height 100px
-							fill currentColor
-
-					> .info
-						margin 0 auto 16px auto
-						width $width
-						font-size 14px
-
-						> .stats
-							margin-left 16px
-							padding-left 16px
-							border-left solid 1px var(--faceDivider)
-
-							> *
-								margin-right 16px
-
-					> .desc
-						max-width calc(100% - 150px)
-
-					> .sign
-						font-size 120%
-						margin-bottom 0
-
-						> .divider
-							margin 0 16px
-
-						> .signin
-						> .signup
-							cursor pointer
-
-							&:hover
-								color var(--primary)
-
-					> .char
-						display block
-						position absolute
-						right 16px
-						bottom 0
-						height 320px
-						opacity 0.7
-
-					> *:not(.char)
-						z-index 1
-
-			> .announcements
-				grid-row 2
-				grid-column 1
-
-				> div
-					padding 32px
-
-					> div
-						padding 0 0 16px 0
-						margin 0 0 16px 0
-						border-bottom 1px solid var(--faceDivider)
-
-						> h1
-							margin 0
-							font-size 1.25em
-
-			> .photos
-				grid-row 2
-				grid-column 2
-
-				> div
-					display grid
-					grid-template-rows 1fr 1fr 1fr
-					grid-template-columns 1fr 1fr
-					gap 8px
-					height 100%
-					padding 16px
-
-					> div
-						//border-radius 4px
-						background-position center center
-						background-size cover
-
-			> .tag-cloud
-				grid-row 3
-				grid-column 1 / 3
-
-				> div
-					height 256px
-					padding 32px
-
-			> .nav
-				display flex
-				justify-content center
-				align-items center
-				grid-row 4
-				grid-column 1 / 3
-				font-size 14px
-
-			> .side
-				display grid
-				grid-row 1 / 5
-				grid-column 3
-				grid-template-rows 1fr 350px
-				grid-template-columns 1fr
-				gap 16px
-
-				> .tl
-					grid-row 1
-					grid-column 1
-					overflow auto
-
-				> .trends
-					grid-row 2
-					grid-column 1
-					padding 8px
-
-				> .info
-					grid-row 3
-					grid-column 1
-
-					> div
-						padding 16px
-
-						> .body
-							> p
-								display block
-								margin 0
-
-</style>
diff --git a/src/client/app/desktop/views/widgets/activity.vue b/src/client/app/desktop/views/widgets/activity.vue
deleted file mode 100644
index 73c6d0ef647be3ffb07d8be0ff83b5c4c50fe45c..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/widgets/activity.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-<template>
-<mk-activity
-	:design="props.design"
-	:init-view="props.view"
-	:user="$store.state.i"
-	@view-changed="viewChanged"/>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-export default define({
-	name: 'activity',
-	props: () => ({
-		design: 0,
-		view: 0
-	})
-}).extend({
-	methods: {
-		func() {
-			if (this.props.design == 2) {
-				this.props.design = 0;
-			} else {
-				this.props.design++;
-			}
-			this.save();
-		},
-		viewChanged(view) {
-			this.props.view = view;
-			this.save();
-		}
-	}
-});
-</script>
diff --git a/src/client/app/desktop/views/widgets/customize.vue b/src/client/app/desktop/views/widgets/customize.vue
deleted file mode 100644
index eb719103820832e5815b4da1e46542f2db0697a2..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/widgets/customize.vue
+++ /dev/null
@@ -1,21 +0,0 @@
-<template>
-<div class="mkw-customize">
-	<ui-button @click="customize()">{{ $t('@.customize-home') }}</ui-button>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-
-export default define({
-	name: 'customize',
-}).extend({
-	i18n: i18n(),
-	methods: {
-		customize(date) {
-			location.href = '/?customize';
-		}
-	}
-});
-</script>
diff --git a/src/client/app/desktop/views/widgets/index.ts b/src/client/app/desktop/views/widgets/index.ts
deleted file mode 100644
index c00cd2ff4dba8ad1ed59d2139f031db7bd83dd5c..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/widgets/index.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import Vue from 'vue';
-
-import wNotifications from './notifications.vue';
-import wTimemachine from './timemachine.vue';
-import wActivity from './activity.vue';
-import wTrends from './trends.vue';
-import wUsers from './users.vue';
-import wPolls from './polls.vue';
-import wMessaging from './messaging.vue';
-import wProfile from './profile.vue';
-import wCustomize from './customize.vue';
-
-Vue.component('mkw-notifications', wNotifications);
-Vue.component('mkw-timemachine', wTimemachine);
-Vue.component('mkw-activity', wActivity);
-Vue.component('mkw-trends', wTrends);
-Vue.component('mkw-users', wUsers);
-Vue.component('mkw-polls', wPolls);
-Vue.component('mkw-messaging', wMessaging);
-Vue.component('mkw-profile', wProfile);
-Vue.component('mkw-customize', wCustomize);
diff --git a/src/client/app/desktop/views/widgets/messaging.vue b/src/client/app/desktop/views/widgets/messaging.vue
deleted file mode 100644
index e94e745c197a82cb89d69f310a9b22b6bc70bc2c..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/widgets/messaging.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
-<div class="mkw-messaging">
-	<ui-container :show-header="props.design == 0">
-		<template #header><fa icon="comments"/>{{ $t('@.messaging') }}</template>
-		<template #func><button @click="add"><fa icon="plus"/></button></template>
-
-		<x-messaging ref="index" compact @navigate="navigate" @navigateGroup="navigateGroup"/>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-import MkMessagingRoomWindow from '../components/messaging-room-window.vue';
-import MkMessagingWindow from '../components/messaging-window.vue';
-
-export default define({
-	name: 'messaging',
-	props: () => ({
-		design: 0
-	})
-}).extend({
-	i18n: i18n(''),
-	components: {
-		XMessaging: () => import('../../../common/views/components/messaging.vue').then(m => m.default)
-	},
-	methods: {
-		navigate(user) {
-			this.$root.new(MkMessagingRoomWindow, {
-				user: user
-			});
-		},
-		navigateGroup(group) {
-			this.$root.new(MkMessagingRoomWindow, {
-				group: group
-			});
-		},
-		add() {
-			this.$root.new(MkMessagingWindow);
-		},
-		func() {
-			if (this.props.design == 1) {
-				this.props.design = 0;
-			} else {
-				this.props.design++;
-			}
-			this.save();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mkw-messaging
-	.mk-messaging
-		max-height 250px
-		overflow auto
-
-</style>
diff --git a/src/client/app/desktop/views/widgets/notifications.vue b/src/client/app/desktop/views/widgets/notifications.vue
deleted file mode 100644
index 5a84f7b371311c28314e9e00abf9f23430eafb5c..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/widgets/notifications.vue
+++ /dev/null
@@ -1,55 +0,0 @@
-<template>
-<div class="mkw-notifications">
-	<ui-container :show-header="!props.compact">
-		<template #header><fa :icon="['far', 'bell']"/>{{ props.type === 'all' ? $t('title') : $t('@.notification-types.' + props.type) }}</template>
-		<template #func><button :title="$t('@.notification-type')" @click="settings"><fa icon="cog"/></button></template>
-
-		<mk-notifications :class="$style.notifications" :type="props.type === 'all' ? null : props.type"/>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-
-export default define({
-	name: 'notifications',
-	props: () => ({
-		compact: false,
-		type: 'all'
-	})
-}).extend({
-	i18n: i18n('desktop/views/widgets/notifications.vue'),
-	methods: {
-		settings() {
-			this.$root.dialog({
-				title: this.$t('@.notification-type'),
-				type: null,
-				select: {
-					items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({
-						value: x, text: this.$t('@.notification-types.' + x)
-					}))
-					default: this.props.type,
-				},
-				showCancelButton: true
-			}).then(({ canceled, result: type }) => {
-				if (canceled) return;
-				this.props.type = type;
-				this.save();
-			});
-		},
-		func() {
-			this.props.compact = !this.props.compact;
-			this.save();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" module>
-.notifications
-	max-height 300px
-	overflow auto
-
-</style>
diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue
deleted file mode 100644
index c77762ecdfa519b1a4c2f3dcec75726009f06948..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/widgets/polls.vue
+++ /dev/null
@@ -1,110 +0,0 @@
-<template>
-<div class="mkw-polls">
-	<ui-container :show-header="!props.compact">
-		<template #header><fa icon="chart-pie"/>{{ $t('title') }}</template>
-		<template #func>
-			<button :title="$t('title')" @click="fetch">
-				<fa v-if="!fetching && more" icon="arrow-right"/>
-				<fa v-if="!fetching && !more" icon="sync"/>
-			</button>
-		</template>
-
-		<div class="mkw-polls--body">
-			<div class="poll" v-if="!fetching && poll != null">
-				<p v-if="poll.text"><router-link :to="poll | notePage">
-					<mfm :text="poll.text" :author="poll.user" :custom-emojis="poll.emojis"/>
-				</router-link></p>
-				<p v-if="!poll.text"><router-link :to="poll | notePage"><fa icon="link"/></router-link></p>
-				<mk-poll :note="poll"/>
-			</div>
-			<p class="empty" v-if="!fetching && poll == null">{{ $t('nothing') }}</p>
-			<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-
-export default define({
-	name: 'polls',
-	props: () => ({
-		compact: false
-	})
-}).extend({
-	i18n: i18n('desktop/views/widgets/polls.vue'),
-	data() {
-		return {
-			poll: null,
-			fetching: true,
-			more: true,
-			offset: 0
-		};
-	},
-	mounted() {
-		this.fetch();
-	},
-	methods: {
-		func() {
-			this.props.compact = !this.props.compact;
-			this.save();
-		},
-		fetch() {
-			this.fetching = true;
-			this.poll = null;
-
-			this.$root.api('notes/polls/recommendation', {
-				limit: 1,
-				offset: this.offset
-			}).then(notes => {
-				const poll = notes ? notes[0] : null;
-				if (poll == null) {
-					this.more = false;
-					this.offset = 0;
-				} else {
-					this.more = true;
-					this.offset++;
-				}
-				this.poll = poll;
-				this.fetching = false;
-			}).catch(() => {
-				this.poll = null;
-				this.fetching = false;
-				this.more = false;
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mkw-polls--body
-	> .poll
-		padding 16px
-		font-size 12px
-		color var(--text)
-
-		> p
-			margin 0 0 8px 0
-
-			> a
-				color inherit
-
-	> .empty
-		margin 0
-		padding 16px
-		text-align center
-		color var(--text)
-
-	> .fetching
-		margin 0
-		padding 16px
-		text-align center
-		color var(--text)
-
-		> [data-icon]
-			margin-right 4px
-
-</style>
diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue
deleted file mode 100644
index bad1925f69cad8e55128291fefa3c2e883e154a6..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/widgets/profile.vue
+++ /dev/null
@@ -1,132 +0,0 @@
-<template>
-<div class="egwyvoaaryotefqhqtmiyawwefemjfsd">
-	<ui-container :show-header="false" :naked="props.design == 2">
-		<div class="egwyvoaaryotefqhqtmiyawwefemjfsd-body"
-			:data-compact="props.design == 1 || props.design == 2"
-			:data-melt="props.design == 2"
-		>
-			<div class="banner"
-				:style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl})` : ''"
-				:title="$t('update-banner')"
-				@click="updateBanner()"
-			></div>
-			<mk-avatar class="avatar" :user="$store.state.i"
-				:disable-link="true"
-				@click="updateAvatar()"
-				:title="$t('update-avatar')"
-			/>
-			<router-link class="name" :to="$store.state.i | userPage"><mk-user-name :user="$store.state.i"/></router-link>
-			<p class="username">@{{ $store.state.i | acct }}</p>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-import updateAvatar from '../../api/update-avatar';
-import updateBanner from '../../api/update-banner';
-
-export default define({
-	name: 'profile',
-	props: () => ({
-		design: 0
-	})
-}).extend({
-	i18n: i18n('desktop/views/widgets/profile.vue'),
-	methods: {
-		func() {
-			if (this.props.design == 2) {
-				this.props.design = 0;
-			} else {
-				this.props.design++;
-			}
-			this.save();
-		},
-		updateAvatar() {
-			updateAvatar(this.$root)();
-		},
-		updateBanner() {
-			updateBanner(this.$root)();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.egwyvoaaryotefqhqtmiyawwefemjfsd-body
-	&[data-compact]
-		> .banner:before
-			content ""
-			display block
-			width 100%
-			height 100%
-			background rgba(#000, 0.5)
-
-		> .avatar
-			top ((100px - 58px) / 2)
-			left ((100px - 58px) / 2)
-			border none
-			border-radius 100%
-			box-shadow 0 0 16px rgba(#000, 0.5)
-
-		> .name
-			position absolute
-			top 0
-			left 92px
-			margin 0
-			line-height 100px
-			color #fff
-			text-shadow 0 0 8px rgba(#000, 0.5)
-
-		> .username
-			display none
-
-	&[data-melt]
-		> .banner
-			visibility hidden
-
-		> .avatar
-			box-shadow none
-
-		> .name
-			color #666
-			text-shadow none
-
-	> .banner
-		height 100px
-		background-color var(--primaryAlpha01)
-		background-size cover
-		background-position center
-		cursor pointer
-
-	> .avatar
-		display block
-		position absolute
-		top 76px
-		left 16px
-		width 58px
-		height 58px
-		border solid 3px var(--face)
-		border-radius 8px
-		cursor pointer
-
-	> .name
-		display block
-		margin 10px 0 0 84px
-		line-height 16px
-		font-weight bold
-		color var(--text)
-		overflow hidden
-		text-overflow ellipsis
-
-	> .username
-		display block
-		margin 4px 0 8px 84px
-		line-height 16px
-		font-size 0.9em
-		color var(--text)
-		opacity 0.7
-
-</style>
diff --git a/src/client/app/desktop/views/widgets/timemachine.vue b/src/client/app/desktop/views/widgets/timemachine.vue
deleted file mode 100644
index 854b01c13e51935596334f08c46c7276dadfcc2d..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/widgets/timemachine.vue
+++ /dev/null
@@ -1,29 +0,0 @@
-<template>
-<div class="mkw-timemachine">
-	<mk-calendar :design="props.design" @chosen="chosen"/>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-export default define({
-	name: 'timemachine',
-	props: () => ({
-		design: 0
-	})
-}).extend({
-	methods: {
-		chosen(date) {
-			this.$root.$emit('warp', date);
-		},
-		func() {
-			if (this.props.design == 5) {
-				this.props.design = 0;
-			} else {
-				this.props.design++;
-			}
-			this.save();
-		}
-	}
-});
-</script>
diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue
deleted file mode 100644
index c512945895167f817ac945db35bf174654de3e75..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/widgets/trends.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-<template>
-<div class="mkw-trends">
-	<ui-container :show-header="!props.compact">
-		<template #header><fa icon="fire"/>{{ $t('title') }}</template>
-		<template #func><button :title="$t('title')" @click="fetch"><fa icon="sync"/></button></template>
-
-		<div class="mkw-trends--body">
-			<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
-			<div class="note" v-else-if="note != null">
-				<p class="text"><router-link :to="note | notePage">{{ note.text }}</router-link></p>
-				<p class="author">―<router-link :to="note.user | userPage">@{{ note.user | acct }}</router-link></p>
-			</div>
-			<p class="empty" v-else>{{ $t('nothing') }}</p>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-
-export default define({
-	name: 'trends',
-	props: () => ({
-		compact: false
-	})
-}).extend({
-	i18n: i18n('desktop/views/widgets/trends.vue'),
-	data() {
-		return {
-			note: null,
-			fetching: true,
-			offset: 0
-		};
-	},
-	mounted() {
-		this.fetch();
-	},
-	methods: {
-		func() {
-			this.props.compact = !this.props.compact;
-			this.save();
-		},
-		fetch() {
-			this.fetching = true;
-			this.note = null;
-
-			this.$root.api('notes/trend', {
-				limit: 1,
-				offset: this.offset,
-				renote: false,
-				reply: false,
-				file: false,
-				poll: false
-			}).then(notes => {
-				const note = notes ? notes[0] : null;
-				if (note == null) {
-					this.offset = 0;
-				} else {
-					this.offset++;
-				}
-				this.note = note;
-				this.fetching = false;
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mkw-trends
-	.mkw-trends--body
-		> .note
-			padding 16px
-			font-size 12px
-			font-style oblique
-			color #555
-
-			> p
-				margin 0
-
-			> .text,
-			> .author
-				> a
-					color inherit
-
-		> .empty
-			margin 0
-			padding 16px
-			text-align center
-			color var(--text)
-
-		> .fetching
-			margin 0
-			padding 16px
-			text-align center
-			color var(--text)
-
-			> [data-icon]
-				margin-right 4px
-
-</style>
diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue
deleted file mode 100644
index 85902fc20c85734c1ab394dee3a63955eb16a3bb..0000000000000000000000000000000000000000
--- a/src/client/app/desktop/views/widgets/users.vue
+++ /dev/null
@@ -1,145 +0,0 @@
-<template>
-<div class="mkw-users">
-	<ui-container :show-header="!props.compact">
-		<template #header><fa icon="users"/>{{ $t('title') }}</template>
-		<template #func>
-			<button :title="$t('title')" @click="refresh">
-				<fa v-if="!fetching && more" icon="arrow-right"/>
-				<fa v-if="!fetching && !more" icon="sync"/>
-			</button>
-		</template>
-
-		<div class="mkw-users--body">
-			<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
-			<template v-else-if="users.length != 0">
-				<div class="user" v-for="_user in users">
-					<mk-avatar class="avatar" :user="_user"/>
-					<div class="body">
-						<router-link class="name" :to="_user | userPage" v-user-preview="_user.id"><mk-user-name :user="_user"/></router-link>
-						<p class="username">@{{ _user | acct }}</p>
-					</div>
-				</div>
-			</template>
-			<p class="empty" v-else>{{ $t('no-one') }}</p>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-
-const limit = 3;
-
-export default define({
-	name: 'users',
-	props: () => ({
-		compact: false
-	})
-}).extend({
-	i18n: i18n('desktop/views/widgets/users.vue'),
-	data() {
-		return {
-			users: [],
-			fetching: true,
-			more: true,
-			page: 0
-		};
-	},
-	mounted() {
-		this.fetch();
-	},
-	methods: {
-		func() {
-			this.props.compact = !this.props.compact;
-			this.save();
-		},
-		fetch() {
-			this.fetching = true;
-			this.users = [];
-
-			this.$root.api('users/recommendation', {
-				limit: limit,
-				offset: limit * this.page
-			}).then(users => {
-				this.users = users;
-				this.fetching = false;
-			}).catch(() => {
-				this.users = [];
-				this.fetching = false;
-				this.more = false;
-				this.page = 0;
-			});
-		},
-		refresh() {
-			if (this.users.length < limit) {
-				this.more = false;
-				this.page = 0;
-			} else {
-				this.more = true;
-				this.page++;
-			}
-			this.fetch();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mkw-users
-	.mkw-users--body
-		> .user
-			padding 16px
-			border-bottom solid 1px var(--faceDivider)
-
-			&:last-child
-				border-bottom none
-
-			&:after
-				content ""
-				display block
-				clear both
-
-			> .avatar
-				display block
-				float left
-				margin 0 12px 0 0
-				width 42px
-				height 42px
-				border-radius 8px
-
-			> .body
-				float left
-				width calc(100% - 54px)
-
-				> .name
-					margin 0
-					font-size 16px
-					line-height 24px
-					color var(--text)
-
-				> .username
-					display block
-					margin 0
-					font-size 15px
-					line-height 16px
-					color var(--text)
-					opacity 0.7
-
-		> .empty
-			margin 0
-			padding 16px
-			text-align center
-			color var(--text)
-
-		> .fetching
-			margin 0
-			padding 16px
-			text-align center
-			color var(--text)
-
-			> [data-icon]
-				margin-right 4px
-
-</style>
diff --git a/src/client/app/dev/script.ts b/src/client/app/dev/script.ts
deleted file mode 100644
index 9adcb84d7c01027cdd849be7e97a1822c2e2ee2f..0000000000000000000000000000000000000000
--- a/src/client/app/dev/script.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * Developer Center
- */
-
-import Vue from 'vue';
-import VueRouter from 'vue-router';
-import BootstrapVue from 'bootstrap-vue';
-import 'bootstrap/dist/css/bootstrap.css';
-import 'bootstrap-vue/dist/bootstrap-vue.css';
-
-// Style
-import './style.styl';
-
-import init from '../init';
-
-import Index from './views/index.vue';
-import Apps from './views/apps.vue';
-import AppNew from './views/new-app.vue';
-import App from './views/app.vue';
-import ui from './views/ui.vue';
-import NotFound from '../common/views/pages/not-found.vue';
-
-Vue.use(BootstrapVue);
-
-Vue.component('mk-ui', ui);
-
-/**
- * init
- */
-init(launch => {
-	// Init router
-	const router = new VueRouter({
-		mode: 'history',
-		base: '/dev/',
-		routes: [
-			{ path: '/', component: Index },
-			{ path: '/apps', component: Apps },
-			{ path: '/app/new', component: AppNew },
-			{ path: '/app/:id', component: App },
-			{ path: '*', component: NotFound }
-		]
-	});
-
-	// Launch the app
-	launch(router);
-});
diff --git a/src/client/app/dev/style.styl b/src/client/app/dev/style.styl
deleted file mode 100644
index e635897b170fee1f653983f1676d1d960350d35f..0000000000000000000000000000000000000000
--- a/src/client/app/dev/style.styl
+++ /dev/null
@@ -1,10 +0,0 @@
-@import "../app"
-@import "../reset"
-
-// Bootstrapのデザインを崩すので:
-*
-	position initial
-	background-clip initial !important
-
-html
-	background-color #fff
diff --git a/src/client/app/dev/views/app.vue b/src/client/app/dev/views/app.vue
deleted file mode 100644
index 2379d71aa588cec22f81336951297a9673b2a7b7..0000000000000000000000000000000000000000
--- a/src/client/app/dev/views/app.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<template>
-<mk-ui>
-	<p v-if="fetching">{{ $t('@.loading') }}</p>
-	<b-card v-if="!fetching" :header="app.name">
-		<b-form-group label="App Secret">
-			<b-input :value="app.secret" readonly/>
-		</b-form-group>
-	</b-card>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-export default Vue.extend({
-	i18n: i18n(),
-	data() {
-		return {
-			fetching: true,
-			app: null
-		};
-	},
-	watch: {
-		$route: 'fetch'
-	},
-	mounted() {
-		this.fetch();
-	},
-	methods: {
-		fetch() {
-			this.fetching = true;
-			this.$root.api('app/show', {
-				appId: this.$route.params.id
-			}).then(app => {
-				this.app = app;
-				this.fetching = false;
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/app/dev/views/apps.vue b/src/client/app/dev/views/apps.vue
deleted file mode 100644
index b99ccdf5763708759f49b34d313f50c604b53690..0000000000000000000000000000000000000000
--- a/src/client/app/dev/views/apps.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-<template>
-<mk-ui>
-	<b-card :header="$t('manage-apps')">
-		<b-button to="/app/new" variant="primary">{{ $t('create-app') }}</b-button>
-		<hr>
-		<div class="apps">
-			<p v-if="fetching">{{ $t('@.loading') }}</p>
-			<template v-if="!fetching">
-				<b-alert v-if="apps.length == 0">{{ $t('app-missing') }}</b-alert>
-				<b-list-group v-else>
-					<b-list-group-item v-for="app in apps" :key="app.id" :to="`/app/${app.id}`">
-						{{ app.name }}
-					</b-list-group-item>
-				</b-list-group>
-			</template>
-		</div>
-	</b-card>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-export default Vue.extend({
-	i18n: i18n('dev/views/apps.vue'),
-	data() {
-		return {
-			fetching: true,
-			apps: []
-		};
-	},
-	mounted() {
-		this.$root.api('my/apps').then(apps => {
-			this.apps = apps;
-			this.fetching = false;
-		});
-	}
-});
-</script>
diff --git a/src/client/app/dev/views/index.vue b/src/client/app/dev/views/index.vue
deleted file mode 100644
index db0e4d57c240f2dc15e6e29fccd98659c283ed99..0000000000000000000000000000000000000000
--- a/src/client/app/dev/views/index.vue
+++ /dev/null
@@ -1,13 +0,0 @@
-<template>
-<mk-ui>
-	<b-button to="/apps" variant="primary">{{ $t('manage-apps') }}</b-button>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-export default Vue.extend({
-	i18n: i18n('dev/views/index.vue')
-});
-</script>
diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue
deleted file mode 100644
index dbb41211ccff6387bcef14eac14dbbeb6a8a5e82..0000000000000000000000000000000000000000
--- a/src/client/app/dev/views/new-app.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<template>
-<mk-ui>
-	<b-card :header="$t('new-app')">
-		<b-alert show variant="info"><fa icon="info-circle"/> {{ $t('new-app-info') }}</b-alert>
-		<b-form @submit.prevent="onSubmit" autocomplete="off">
-			<b-form-group :label="$t('app-name')" :description="$t('app-name-desc')">
-				<b-form-input v-model="name" type="text" :placeholder="$t('app-name-placeholder')" autocomplete="off" required/>
-			</b-form-group>
-			<b-form-group :label="$t('app-overview')" :description="$t('app-overview-desc')">
-				<b-textarea v-model="description" :placeholder="$t('app-overview-placeholder')" autocomplete="off" required></b-textarea>
-			</b-form-group>
-			<b-form-group :label="$t('callback-url')" :description="$t('callback-url-desc')">
-				<b-input v-model="cb" type="url" :placeholder="$t('callback-url-placeholder')" autocomplete="off"/>
-			</b-form-group>
-			<b-card :header="$t('authority')">
-				<b-form-group :description="$t('authority-desc')">
-					<b-alert show variant="warning"><fa icon="exclamation-triangle"/> {{ $t('authority-warning') }}</b-alert>
-					<b-form-checkbox-group v-model="permission" stacked>
-						<b-form-checkbox v-for="v in permissionsList" :value="v" :key="v">{{ $t(`@.permissions.${v}`) }} ({{ v }})</b-form-checkbox>
-					</b-form-checkbox-group>
-				</b-form-group>
-			</b-card>
-			<hr>
-			<b-button type="submit" variant="primary">{{ $t('create-app') }}</b-button>
-		</b-form>
-	</b-card>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../i18n';
-import { kinds } from '../../../../server/api/kinds';
-
-export default Vue.extend({
-	i18n: i18n('dev/views/new-app.vue'),
-	data() {
-		return {
-			name: '',
-			description: '',
-			cb: '',
-			nidState: null,
-			permission: [],
-			permissionsList: kinds
-		};
-	},
-	methods: {
-		onSubmit() {
-			this.$root.api('app/create', {
-				name: this.name,
-				description: this.description,
-				callbackUrl: this.cb,
-				permission: this.permission
-			}).then(() => {
-				location.href = '/dev/apps';
-			}).catch(() => {
-				alert(this.$t('@.dev.failed-to-create'));
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/app/dev/views/ui.vue b/src/client/app/dev/views/ui.vue
deleted file mode 100644
index f1e001909f422444965b25997c5a16a3264bf71a..0000000000000000000000000000000000000000
--- a/src/client/app/dev/views/ui.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-<template>
-<div>
-	<b-navbar toggleable="md" type="dark" variant="info">
-		<b-navbar-brand>Developers</b-navbar-brand>
-		<b-navbar-nav>
-			<b-nav-item to="/">Home</b-nav-item>
-			<b-nav-item to="/apps">Apps</b-nav-item>
-		</b-navbar-nav>
-	</b-navbar>
-	<main>
-		<slot></slot>
-	</main>
-</div>
-</template>
-
-<style lang="stylus" scoped>
-main
-	padding 32px
-	max-width 700px
-</style>
diff --git a/src/client/app/i18n.ts b/src/client/app/i18n.ts
deleted file mode 100644
index 2d0d9ba5500aee9df82b340bc1ac59968b61e4a2..0000000000000000000000000000000000000000
--- a/src/client/app/i18n.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { lang, locale } from './config';
-
-export default function(scope?: string) {
-	const texts = scope ? locale[scope] || {} : {};
-	texts['@'] = locale['common'];
-	texts['@deck'] = locale['deck'];
-	return {
-		sync: false,
-		locale: lang,
-		messages: {
-			[lang]: texts
-		}
-	};
-}
diff --git a/src/client/app/init.css b/src/client/app/init.css
deleted file mode 100644
index db5e23c56d41e9c26c525639f373b4fdd0af29f9..0000000000000000000000000000000000000000
--- a/src/client/app/init.css
+++ /dev/null
@@ -1,57 +0,0 @@
-@charset "utf-8";
-
-/**
- * Boot screen style
- */
-
-html {
-	font-family: Roboto, HelveticaNeue, Arial, sans-serif;
-}
-
-body > noscript {
-	position: fixed;
-	z-index: 2;
-	top: 0;
-	left: 0;
-	width: 100%;
-	height: 100%;
-	text-align: center;
-	background: #fff;
-}
-	body > noscript > p {
-		display: block;
-		margin: 32px;
-		font-size: 2em;
-		color: #555;
-	}
-
-#ini {
-	position: fixed;
-	z-index: 1;
-	top: 0;
-	left: 0;
-	width: 100%;
-	height: 100%;
-	background: var(--bg);
-	cursor: wait;
-}
-	#ini > svg {
-		position: absolute;
-		top: 0;
-		right: 0;
-		bottom: 0;
-		left: 0;
-		margin: auto;
-		width: 64px;
-		height: 64px;
-		animation: ini 0.6s infinite linear;
-	}
-
-@keyframes ini {
-	from {
-		transform: rotate(0deg);
-	}
-	to {
-		transform: rotate(360deg);
-	}
-}
diff --git a/src/client/app/init.ts b/src/client/app/init.ts
deleted file mode 100644
index 6fd11b9b75ee98f6ca57f812918a43157f7b0b3b..0000000000000000000000000000000000000000
--- a/src/client/app/init.ts
+++ /dev/null
@@ -1,512 +0,0 @@
-/**
- * App initializer
- */
-
-import Vue from 'vue';
-import Vuex from 'vuex';
-import VueRouter from 'vue-router';
-import VAnimateCss from 'v-animate-css';
-import VModal from 'vue-js-modal';
-import VueI18n from 'vue-i18n';
-import SequentialEntrance from 'vue-sequential-entrance';
-
-import VueHotkey from './common/hotkey';
-import VueSize from './common/size';
-import App from './app.vue';
-import checkForUpdate from './common/scripts/check-for-update';
-import MiOS from './mios';
-import { version, codename, lang, locale } from './config';
-import { builtinThemes, applyTheme, futureTheme } from './theme';
-import Dialog from './common/views/components/dialog.vue';
-
-if (localStorage.getItem('theme') == null) {
-	applyTheme(futureTheme);
-}
-
-//#region FontAwesome
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
-
-import {
-	faRetweet,
-	faPlus,
-	faUser,
-	faCog,
-	faCheck,
-	faStar,
-	faReply,
-	faEllipsisH,
-	faQuoteLeft,
-	faQuoteRight,
-	faAngleUp,
-	faAngleDown,
-	faAt,
-	faHashtag,
-	faHome,
-	faGlobe,
-	faCircle,
-	faList,
-	faHeart,
-	faUnlock,
-	faRssSquare,
-	faSort,
-	faChartPie,
-	faChartBar,
-	faPencilAlt,
-	faColumns,
-	faComments,
-	faGamepad,
-	faCloud,
-	faPowerOff,
-	faChevronCircleLeft,
-	faChevronCircleRight,
-	faShareAlt,
-	faTimes,
-	faThumbtack,
-	faSearch,
-	faAngleRight,
-	faWrench,
-	faTerminal,
-	faMoon,
-	faPalette,
-	faSlidersH,
-	faDesktop,
-	faVolumeUp,
-	faLanguage,
-	faInfoCircle,
-	faExclamationTriangle,
-	faKey,
-	faBan,
-	faCogs,
-	faUnlockAlt,
-	faPuzzlePiece,
-	faMobileAlt,
-	faSignInAlt,
-	faSyncAlt,
-	faPaperPlane,
-	faUpload,
-	faMapMarkerAlt,
-	faEnvelope,
-	faLock,
-	faFolderOpen,
-	faBirthdayCake,
-	faImage,
-	faEye,
-	faDownload,
-	faFileImport,
-	faLink,
-	faArrowRight,
-	faICursor,
-	faCaretRight,
-	faReplyAll,
-	faCamera,
-	faMinus,
-	faCaretDown,
-	faCalculator,
-	faUsers,
-	faBars,
-	faFileImage,
-	faPollH,
-	faFolder,
-	faMicrochip,
-	faMemory,
-	faServer,
-	faExclamationCircle,
-	faSpinner,
-	faBroadcastTower,
-	faChartLine,
-	faEllipsisV,
-	faStickyNote,
-	faUserClock,
-	faUserPlus,
-	faExternalLinkSquareAlt,
-	faSync,
-	faArrowLeft,
-	faMapMarker,
-	faRobot,
-	faHourglassHalf,
-	faGavel,
-	faUndoAlt,
-} from '@fortawesome/free-solid-svg-icons';
-
-import {
-	faBell as farBell,
-	faEnvelope as farEnvelope,
-	faComments as farComments,
-	faTrashAlt as farTrashAlt,
-	faWindowRestore as farWindowRestore,
-	faFolder as farFolder,
-	faLaugh as farLaugh,
-	faSmile as farSmile,
-	faEyeSlash as farEyeSlash,
-	faFolderOpen as farFolderOpen,
-	faSave as farSave,
-	faImages as farImages,
-	faChartBar as farChartBar,
-	faCommentAlt as farCommentAlt,
-	faClock as farClock,
-	faCalendarAlt as farCalendarAlt,
-	faHdd as farHdd,
-	faMoon as farMoon,
-	faPlayCircle as farPlayCircle,
-	faLightbulb as farLightbulb,
-	faStickyNote as farStickyNote,
-} from '@fortawesome/free-regular-svg-icons';
-
-import {
-	faTwitter as fabTwitter,
-	faGithub as fabGithub,
-	faDiscord as fabDiscord
-} from '@fortawesome/free-brands-svg-icons';
-import i18n from './i18n';
-
-library.add(
-	faRetweet,
-	faPlus,
-	faUser,
-	faCog,
-	faCheck,
-	faStar,
-	faReply,
-	faEllipsisH,
-	faQuoteLeft,
-	faQuoteRight,
-	faAngleUp,
-	faAngleDown,
-	faAt,
-	faHashtag,
-	faHome,
-	faGlobe,
-	faCircle,
-	faList,
-	faHeart,
-	faUnlock,
-	faRssSquare,
-	faSort,
-	faChartPie,
-	faChartBar,
-	faPencilAlt,
-	faColumns,
-	faComments,
-	faGamepad,
-	faCloud,
-	faPowerOff,
-	faChevronCircleLeft,
-	faChevronCircleRight,
-	faShareAlt,
-	faTimes,
-	faThumbtack,
-	faSearch,
-	faAngleRight,
-	faWrench,
-	faTerminal,
-	faMoon,
-	faPalette,
-	faSlidersH,
-	faDesktop,
-	faVolumeUp,
-	faLanguage,
-	faInfoCircle,
-	faExclamationTriangle,
-	faKey,
-	faBan,
-	faCogs,
-	faUnlockAlt,
-	faPuzzlePiece,
-	faMobileAlt,
-	faSignInAlt,
-	faSyncAlt,
-	faPaperPlane,
-	faUpload,
-	faMapMarkerAlt,
-	faEnvelope,
-	faLock,
-	faFolderOpen,
-	faBirthdayCake,
-	faImage,
-	faEye,
-	faDownload,
-	faFileImport,
-	faLink,
-	faArrowRight,
-	faICursor,
-	faCaretRight,
-	faReplyAll,
-	faCamera,
-	faMinus,
-	faCaretDown,
-	faCalculator,
-	faUsers,
-	faBars,
-	faFileImage,
-	faPollH,
-	faFolder,
-	faMicrochip,
-	faMemory,
-	faServer,
-	faExclamationCircle,
-	faSpinner,
-	faBroadcastTower,
-	faChartLine,
-	faEllipsisV,
-	faStickyNote,
-	faUserClock,
-	faUserPlus,
-	faExternalLinkSquareAlt,
-	faSync,
-	faArrowLeft,
-	faMapMarker,
-	faRobot,
-	faHourglassHalf,
-	faGavel,
-	faUndoAlt,
-
-	farBell,
-	farEnvelope,
-	farComments,
-	farTrashAlt,
-	farWindowRestore,
-	farFolder,
-	farLaugh,
-	farSmile,
-	farEyeSlash,
-	farFolderOpen,
-	farSave,
-	farImages,
-	farChartBar,
-	farCommentAlt,
-	farClock,
-	farCalendarAlt,
-	farHdd,
-	farMoon,
-	farPlayCircle,
-	farLightbulb,
-	farStickyNote,
-
-	fabTwitter,
-	fabGithub,
-	fabDiscord
-);
-//#endregion
-
-Vue.use(Vuex);
-Vue.use(VueRouter);
-Vue.use(VAnimateCss);
-Vue.use(VModal);
-Vue.use(VueHotkey);
-Vue.use(VueSize);
-Vue.use(VueI18n);
-Vue.use(SequentialEntrance);
-
-Vue.component('fa', FontAwesomeIcon);
-
-// Register global directives
-require('./common/views/directives');
-
-// Register global components
-require('./common/views/components');
-require('./common/views/widgets');
-
-// Register global filters
-require('./common/views/filters');
-
-Vue.mixin({
-	methods: {
-		destroyDom() {
-			this.$destroy();
-
-			if (this.$el.parentNode) {
-				this.$el.parentNode.removeChild(this.$el);
-			}
-		}
-	}
-});
-
-/**
- * APP ENTRY POINT!
- */
-
-console.info(`Misskey v${version} (${codename})`);
-console.info(
-	`%c${locale['common']['do-not-copy-paste']}`,
-	'color: red; background: yellow; font-size: 16px; font-weight: bold;');
-
-// BootTimer解除
-window.clearTimeout((window as any).mkBootTimer);
-delete (window as any).mkBootTimer;
-
-//#region Set lang attr
-const html = document.documentElement;
-html.setAttribute('lang', lang);
-//#endregion
-
-// iOSでプライベートモードだとlocalStorageが使えないので既存のメソッドを上書きする
-try {
-	localStorage.setItem('kyoppie', 'yuppie');
-} catch (e) {
-	Storage.prototype.setItem = () => { }; // noop
-}
-
-// クライアントを更新すべきならする
-if (localStorage.getItem('should-refresh') == 'true') {
-	localStorage.removeItem('should-refresh');
-	location.reload(true);
-}
-
-// MiOSを初期化してコールバックする
-export default (callback: (launch: (router: VueRouter) => [Vue, MiOS], os: MiOS) => void, sw = false) => {
-	const os = new MiOS(sw);
-
-	os.init(() => {
-		// アプリ基底要素マウント
-		document.body.innerHTML = '<div id="app"></div>';
-
-		const launch = (router: VueRouter) => {
-			//#region theme
-			os.store.watch(s => {
-				return s.device.darkmode;
-			}, v => {
-				const themes = os.store.state.device.themes.concat(builtinThemes);
-				const dark = themes.find(t => t.id == os.store.state.device.darkTheme);
-				const light = themes.find(t => t.id == os.store.state.device.lightTheme);
-				applyTheme(v ? dark : light);
-			});
-			os.store.watch(s => {
-				return s.device.lightTheme;
-			}, v => {
-				const themes = os.store.state.device.themes.concat(builtinThemes);
-				const theme = themes.find(t => t.id == v);
-				if (!os.store.state.device.darkmode) {
-					applyTheme(theme);
-				}
-			});
-			os.store.watch(s => {
-				return s.device.darkTheme;
-			}, v => {
-				const themes = os.store.state.device.themes.concat(builtinThemes);
-				const theme = themes.find(t => t.id == v);
-				if (os.store.state.device.darkmode) {
-					applyTheme(theme);
-				}
-			});
-			//#endregion
-
-			/*// Reapply current theme
-			try {
-				const themeName = os.store.state.device.darkmode ? os.store.state.device.darkTheme : os.store.state.device.lightTheme;
-				const themes = os.store.state.device.themes.concat(builtinThemes);
-				const theme = themes.find(t => t.id == themeName);
-				if (theme) {
-					applyTheme(theme);
-				}
-			} catch (e) {
-				console.log(`Cannot reapply theme. ${e}`);
-			}*/
-
-			//#region line width
-			document.documentElement.style.setProperty('--lineWidth', `${os.store.state.device.lineWidth}px`);
-			os.store.watch(s => {
-				return s.device.lineWidth;
-			}, v => {
-				document.documentElement.style.setProperty('--lineWidth', `${os.store.state.device.lineWidth}px`);
-			});
-			//#endregion
-
-			//#region fontSize
-			document.documentElement.style.setProperty('--fontSize', `${os.store.state.device.fontSize}px`);
-			os.store.watch(s => {
-				return s.device.fontSize;
-			}, v => {
-				document.documentElement.style.setProperty('--fontSize', `${os.store.state.device.fontSize}px`);
-			});
-			//#endregion
-
-			document.addEventListener('visibilitychange', () => {
-				if (!document.hidden) {
-					os.store.commit('clearBehindNotes');
-				}
-			}, false);
-
-			window.addEventListener('scroll', () => {
-				if (window.scrollY <= 8) {
-					os.store.commit('clearBehindNotes');
-				}
-			}, { passive: true });
-
-			const app = new Vue({
-				i18n: i18n(),
-				store: os.store,
-				data() {
-					return {
-						os: {
-							windows: os.windows
-						},
-						stream: os.stream,
-						instanceName: os.instanceName
-					};
-				},
-				methods: {
-					api: os.api,
-					getMeta: os.getMeta,
-					getMetaSync: os.getMetaSync,
-					signout: os.signout,
-					new(vm, props) {
-						const x = new vm({
-							parent: this,
-							propsData: props
-						}).$mount();
-						document.body.appendChild(x.$el);
-						return x;
-					},
-					newAsync(vm, props) {
-						return new Promise((res) => {
-							vm().then(vm => {
-								const x = new vm({
-									parent: this,
-									propsData: props
-								}).$mount();
-								document.body.appendChild(x.$el);
-								res(x);
-							});
-						});
-					},
-					dialog(opts) {
-						const vm = this.new(Dialog, opts);
-						const p: any = new Promise((res) => {
-							vm.$once('ok', result => res({ canceled: false, result }));
-							vm.$once('cancel', () => res({ canceled: true }));
-						});
-						p.close = () => {
-							vm.close();
-						};
-						return p;
-					}
-				},
-				router,
-				render: createEl => createEl(App)
-			});
-
-			os.app = app;
-
-			// マウント
-			app.$mount('#app');
-
-			//#region 更新チェック
-			setTimeout(() => {
-				checkForUpdate(app);
-			}, 3000);
-			//#endregion
-
-			return [app, os] as [Vue, MiOS];
-		};
-
-		// Deck mode
-		os.store.commit('device/set', {
-			key: 'inDeckMode',
-			value: os.store.getters.isSignedIn && os.store.state.device.deckMode
-				&& (document.location.pathname === '/' || window.performance.navigation.type === 1)
-		});
-
-		callback(launch, os);
-	});
-};
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts
deleted file mode 100644
index 26ef7408113455cf56d48fb8127648f9c3f5ed7d..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/script.ts
+++ /dev/null
@@ -1,184 +0,0 @@
-/**
- * Mobile Client
- */
-
-import Vue from 'vue';
-import VueRouter from 'vue-router';
-
-// Style
-import './style.styl';
-
-import init from '../init';
-
-import MkIndex from './views/pages/index.vue';
-import MkSignup from './views/pages/signup.vue';
-import MkSelectDrive from './views/pages/selectdrive.vue';
-import MkDrive from './views/pages/drive.vue';
-import MkNotifications from './views/pages/notifications.vue';
-import MkMessaging from './views/pages/messaging.vue';
-import MkMessagingRoom from './views/pages/messaging-room.vue';
-import MkNote from './views/pages/note.vue';
-import MkSearch from './views/pages/search.vue';
-import UI from './views/pages/ui.vue';
-import MkReversi from './views/pages/games/reversi.vue';
-import MkTag from './views/pages/tag.vue';
-import MkShare from '../common/views/pages/share.vue';
-import MkFollow from '../common/views/pages/follow.vue';
-import MkNotFound from '../common/views/pages/not-found.vue';
-import DeckColumn from '../common/views/deck/deck.column-template.vue';
-import PostFormDialog from './views/components/post-form-dialog.vue';
-
-import FileChooser from './views/components/drive-file-chooser.vue';
-import FolderChooser from './views/components/drive-folder-chooser.vue';
-
-/**
- * init
- */
-init((launch, os) => {
-	Vue.mixin({
-		data() {
-			return {
-				isMobile: true
-			};
-		},
-
-		methods: {
-			$post(opts) {
-				const o = opts || {};
-
-				document.documentElement.style.overflow = 'hidden';
-
-				function recover() {
-					document.documentElement.style.overflow = 'auto';
-				}
-
-				const vm = this.$root.new(PostFormDialog, {
-					reply: o.reply,
-					mention: o.mention,
-					renote: o.renote,
-					initialText: o.initialText,
-					instant: o.instant,
-					initialNote: o.initialNote,
-				});
-				vm.$once('cancel', recover);
-				vm.$once('posted', recover);
-				if (o.cb) vm.$once('closed', o.cb);
-				(vm as any).focus();
-			},
-
-			$chooseDriveFile(opts) {
-				return new Promise((res, rej) => {
-					const o = opts || {};
-					const vm = this.$root.new(FileChooser, {
-						title: o.title,
-						multiple: o.multiple,
-						initFolder: o.currentFolder
-					});
-					vm.$once('selected', file => {
-						res(file);
-					});
-				});
-			},
-
-			$chooseDriveFolder(opts) {
-				return new Promise((res, rej) => {
-					const o = opts || {};
-					const vm = this.$root.new(FolderChooser, {
-						title: o.title,
-						initFolder: o.currentFolder
-					});
-					vm.$once('selected', folder => {
-						res(folder);
-					});
-				});
-			},
-
-			$notify(message) {
-				alert(message);
-			}
-		}
-	});
-
-	// Register directives
-	require('./views/directives');
-
-	// Register components
-	require('./views/components');
-	require('./views/widgets');
-
-	// http://qiita.com/junya/items/3ff380878f26ca447f85
-	document.body.setAttribute('ontouchstart', '');
-
-	// Init router
-	const router = new VueRouter({
-		mode: 'history',
-		routes: [
-			...(os.store.state.device.inDeckMode
-				? [{ path: '/', name: 'index', component: () => import('../common/views/deck/deck.vue').then(m => m.default), children: [
-					{ path: '/@:user', component: () => import('../common/views/deck/deck.user-column.vue').then(m => m.default), children: [
-						{ path: '', name: 'user', component: () => import('../common/views/deck/deck.user-column.home.vue').then(m => m.default) },
-						{ path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
-						{ path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) },
-					]},
-					{ path: '/notes/:note', name: 'note', component: () => import('../common/views/deck/deck.note-column.vue').then(m => m.default) },
-					{ path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) },
-					{ path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) },
-					{ path: '/featured', name: 'featured', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/featured.vue').then(m => m.default), platform: 'deck' }) },
-					{ path: '/explore', name: 'explore', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) },
-					{ path: '/explore/tags/:tag', name: 'explore-tag', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) },
-					{ path: '/i/favorites', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/favorites.vue').then(m => m.default), platform: 'deck' }) },
-					{ path: '/i/pages', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) },
-					{ path: '/i/lists', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) },
-					{ path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) },
-					{ path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) },
-					{ path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) },
-					{ path: '/i/follow-requests', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) },
-					{ path: '/@:username/pages/:pageName', name: 'page', props: true, component: () => import('../common/views/deck/deck.page-column.vue').then(m => m.default) },
-				]}]
-			: [
-				{ path: '/', name: 'index', component: MkIndex },
-		]),
-			{ path: '/signup', name: 'signup', component: MkSignup },
-			{ path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) },
-			{ path: '/i/settings/:page', redirect: '/i/settings' },
-			{ path: '/i/favorites', name: 'favorites', component: UI, props: route => ({ component: () => import('../common/views/pages/favorites.vue').then(m => m.default), platform: 'mobile' }) },
-			{ path: '/i/pages', name: 'pages', component: UI, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) },
-			{ path: '/i/lists', name: 'user-lists', component: UI, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) },
-			{ path: '/i/lists/:list', component: UI, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.list }) },
-			{ path: '/i/groups', name: 'user-groups', component: UI, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) },
-			{ path: '/i/groups/:group', component: UI, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.group }) },
-			{ path: '/i/follow-requests', name: 'follow-requests', component: UI, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) },
-			{ path: '/i/widgets', name: 'widgets', component: () => import('./views/pages/widgets.vue').then(m => m.default) },
-			{ path: '/i/notifications', name: 'notifications', component: MkNotifications },
-			{ path: '/i/messaging', name: 'messaging', component: MkMessaging },
-			{ path: '/i/messaging/group/:group', component: MkMessagingRoom },
-			{ path: '/i/messaging/:user', component: MkMessagingRoom },
-			{ path: '/i/drive', name: 'drive', component: MkDrive },
-			{ path: '/i/drive/folder/:folder', component: MkDrive },
-			{ path: '/i/drive/file/:file', component: MkDrive },
-			{ path: '/i/pages/new', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) }) },
-			{ path: '/i/pages/edit/:pageId', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), initPageId: route.params.pageId }) },
-			{ path: '/selectdrive', component: MkSelectDrive },
-			{ path: '/search', component: MkSearch },
-			{ path: '/tags/:tag', component: MkTag },
-			{ path: '/featured', name: 'featured', component: UI, props: route => ({ component: () => import('../common/views/pages/featured.vue').then(m => m.default), platform: 'mobile' }) },
-			{ path: '/explore', name: 'explore', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) },
-			{ path: '/explore/tags/:tag', name: 'explore-tag', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) },
-			{ path: '/share', component: MkShare },
-			{ path: '/games/reversi/:game?', name: 'reversi', component: MkReversi },
-			{ path: '/@:user', name: 'user', component: () => import('./views/pages/user/index.vue').then(m => m.default), children: [
-				{ path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
-				{ path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) },
-			]},
-			{ path: '/@:user/pages/:page', component: UI, props: route => ({ component: () => import('../common/views/pages/page.vue').then(m => m.default), pageName: route.params.page, username: route.params.user }) },
-			{ path: '/@:user/pages/:pageName/view-source', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), initUser: route.params.user, initPageName: route.params.pageName }) },
-			{ path: '/@:acct/room', props: true, component: () => import('../common/views/pages/room/room.vue').then(m => m.default) },
-			{ path: '/notes/:note', component: MkNote },
-			{ path: '/authorize-follow', component: MkFollow },
-			{ path: '*', component: MkNotFound }
-		]
-	});
-
-	// Launch the app
-	launch(router);
-}, true);
diff --git a/src/client/app/mobile/style.styl b/src/client/app/mobile/style.styl
deleted file mode 100644
index 3a4fc9c0c6198cfea66611d38552ec16b00d117f..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/style.styl
+++ /dev/null
@@ -1,23 +0,0 @@
-@import "../app"
-@import "../reset"
-
-#wait
-	top auto
-	bottom 15px
-	left 15px
-
-html
-	height 100%
-	background var(--bg)
-
-main
-	width 100%
-	max-width 680px
-	margin 0 auto
-	padding 8px
-
-	@media (min-width 500px)
-		padding 16px
-
-	@media (min-width 600px)
-		padding 32px
diff --git a/src/client/app/mobile/views/components/detail-notes.vue b/src/client/app/mobile/views/components/detail-notes.vue
deleted file mode 100644
index bab79495342a2540fcc093751e96f30ce3d3826d..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/detail-notes.vue
+++ /dev/null
@@ -1,52 +0,0 @@
-<template>
-<div class="fdcvngpy">
-	<sequential-entrance animation="entranceFromTop" delay="25">
-		<template v-for="note in notes">
-			<mk-note-detail class="post" :note="note" :key="note.id"/>
-		</template>
-	</sequential-entrance>
-	<ui-button v-if="more" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import paging from '../../../common/scripts/paging';
-
-export default Vue.extend({
-	i18n: i18n(),
-
-	mixins: [
-		paging({
-			captureWindowScroll: true,
-		}),
-	],
-
-	props: {
-		pagination: {
-			required: true
-		},
-		extract: {
-			required: false
-		}
-	},
-
-	computed: {
-		notes() {
-			return this.extract ? this.extract(this.items) : this.items;
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.fdcvngpy
-	> * > .post
-		margin-bottom 8px
-
-	@media (min-width 500px)
-		> * > .post
-			margin-bottom 16px
-
-</style>
diff --git a/src/client/app/mobile/views/components/drive-file-chooser.vue b/src/client/app/mobile/views/components/drive-file-chooser.vue
deleted file mode 100644
index 8795102f97bf4997714ea338057ed358f676a4c9..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/drive-file-chooser.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-<template>
-<div class="cdxzvcfawjxdyxsekbxbfgtplebnoneb">
-	<div class="body">
-		<header>
-			<h1>{{ $t('select-file') }}<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1>
-			<button class="close" @click="cancel"><fa icon="times"/></button>
-			<button v-if="multiple" class="ok" @click="ok"><fa icon="check"/></button>
-		</header>
-		<x-drive class="drive" ref="browser"
-			:select-file="true"
-			:type="type"
-			:multiple="multiple"
-			@change-selection="onChangeSelection"
-			@selected="onSelected"
-		/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/components/drive-file-chooser.vue'),
-	components: {
-		XDrive: () => import('./drive.vue').then(m => m.default),
-	},
-	props: ['type', 'multiple'],
-	data() {
-		return {
-			files: []
-		};
-	},
-	methods: {
-		onChangeSelection(files) {
-			this.files = files;
-		},
-		onSelected(file) {
-			this.$emit('selected', file);
-			this.destroyDom();
-		},
-		cancel() {
-			this.$emit('canceled');
-			this.destroyDom();
-		},
-		ok() {
-			this.$emit('selected', this.files);
-			this.destroyDom();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.cdxzvcfawjxdyxsekbxbfgtplebnoneb
-	position fixed
-	z-index 20000
-	top 0
-	left 0
-	width 100%
-	height 100%
-	padding 8px
-	background rgba(#000, 0.2)
-
-	> .body
-		width 100%
-		height 100%
-		background var(--faceHeader)
-
-		> header
-			border-bottom solid 1px var(--faceDivider)
-			color var(--text)
-
-			> h1
-				margin 0
-				padding 0
-				text-align center
-				line-height 42px
-				font-size 1em
-				font-weight normal
-
-				> .count
-					margin-left 4px
-					opacity 0.5
-
-			> .close
-				position absolute
-				top 0
-				left 0
-				line-height 42px
-				width 42px
-
-			> .ok
-				position absolute
-				top 0
-				right 0
-				line-height 42px
-				width 42px
-
-		> .drive
-			height calc(100% - 42px)
-			overflow scroll
-			-webkit-overflow-scrolling touch
-
-</style>
diff --git a/src/client/app/mobile/views/components/drive-folder-chooser.vue b/src/client/app/mobile/views/components/drive-folder-chooser.vue
deleted file mode 100644
index 250a7aca2c54213557a97c76f8454bebed40d971..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/drive-folder-chooser.vue
+++ /dev/null
@@ -1,83 +0,0 @@
-<template>
-<div class="mk-drive-folder-chooser">
-	<div class="body">
-		<header>
-			<h1>{{ $t('select-folder') }}</h1>
-			<button class="close" @click="cancel"><fa icon="times"/></button>
-			<button class="ok" @click="ok"><fa icon="check"/></button>
-		</header>
-		<x-drive ref="browser"
-			select-folder
-		/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-export default Vue.extend({
-	i18n: i18n('mobile/views/components/drive-folder-chooser.vue'),
-	components: {
-		XDrive: () => import('./drive.vue').then(m => m.default),
-	},
-	methods: {
-		cancel() {
-			this.$emit('canceled');
-			this.destroyDom();
-		},
-		ok() {
-			this.$emit('selected', (this.$refs.browser as any).folder);
-			this.destroyDom();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-drive-folder-chooser
-	position fixed
-	z-index 2048
-	top 0
-	left 0
-	width 100%
-	height 100%
-	padding 8px
-	background rgba(#000, 0.2)
-
-	> .body
-		width 100%
-		height 100%
-		background #fff
-
-		> header
-			border-bottom solid 1px #eee
-
-			> h1
-				margin 0
-				padding 0
-				text-align center
-				line-height 42px
-				font-size 1em
-				font-weight normal
-
-			> .close
-				position absolute
-				top 0
-				left 0
-				line-height 42px
-				width 42px
-
-			> .ok
-				position absolute
-				top 0
-				right 0
-				line-height 42px
-				width 42px
-
-		> .mk-drive
-			height calc(100% - 42px)
-			overflow scroll
-			-webkit-overflow-scrolling touch
-
-</style>
diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue
deleted file mode 100644
index 328982a16bfb412d27ac544c8b7238b5b3702edc..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/drive.file-detail.vue
+++ /dev/null
@@ -1,253 +0,0 @@
-<template>
-<div class="pyvicwrksnfyhpfgkjwqknuururpaztw">
-	<div class="preview">
-		<x-file-thumbnail class="preview" :file="file" :detail="true"/>
-		<template v-if="kind != 'image'"><fa icon="file"/></template>
-		<footer v-if="kind == 'image' && file.properties && file.properties.width && file.properties.height">
-			<span class="size">
-				<span class="width">{{ file.properties.width }}</span>
-				<span class="time">×</span>
-				<span class="height">{{ file.properties.height }}</span>
-				<span class="px">px</span>
-			</span>
-			<span class="separator"></span>
-			<span class="aspect-ratio">
-				<span class="width">{{ file.properties.width / gcd(file.properties.width, file.properties.height) }}</span>
-				<span class="colon">:</span>
-				<span class="height">{{ file.properties.height / gcd(file.properties.width, file.properties.height) }}</span>
-			</span>
-		</footer>
-	</div>
-	<div class="info">
-		<div>
-			<span class="type"><mk-file-type-icon :type="file.type"/> {{ file.type }}</span>
-			<span class="separator"></span>
-			<span class="data-size">{{ file.size | bytes }}</span>
-			<span class="separator"></span>
-			<span class="created-at" @click="showCreatedAt"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span>
-			<template v-if="file.isSensitive">
-				<span class="separator"></span>
-				<span class="nsfw"><fa :icon="['far', 'eye-slash']"/> {{ $t('nsfw') }}</span>
-			</template>
-		</div>
-	</div>
-	<div class="menu">
-		<div>
-			<ui-input readonly :value="file.url">URL</ui-input>
-			<ui-button link :href="dlUrl" :download="file.name"><fa icon="download"/> {{ $t('download') }}</ui-button>
-			<ui-button @click="rename"><fa icon="pencil-alt"/> {{ $t('rename') }}</ui-button>
-			<ui-button @click="move"><fa :icon="['far', 'folder-open']"/> {{ $t('move') }}</ui-button>
-			<ui-button @click="toggleSensitive" v-if="file.isSensitive"><fa :icon="['far', 'eye']"/> {{ $t('unmark-as-sensitive') }}</ui-button>
-			<ui-button @click="toggleSensitive" v-else><fa :icon="['far', 'eye-slash']"/> {{ $t('mark-as-sensitive') }}</ui-button>
-			<ui-button @click="del"><fa :icon="['far', 'trash-alt']"/> {{ $t('delete') }}</ui-button>
-		</div>
-	</div>
-	<div class="hash">
-		<div>
-			<p>
-				<fa icon="hashtag"/>{{ $t('hash') }}
-			</p>
-			<code>{{ file.md5 }}</code>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { gcd } from '../../../../../prelude/math';
-import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/components/drive.file-detail.vue'),
-	props: ['file'],
-
-	components: {
-		XFileThumbnail
-	},
-
-	data() {
-		return {
-			gcd,
-			exif: null
-		};
-	},
-
-	computed: {
-		browser(): any {
-			return this.$parent;
-		},
-
-		kind(): string {
-			return this.file.type.split('/')[0];
-		},
-
-		style(): any {
-			return this.file.properties.avgColor ? {
-				'background-color': this.file.properties.avgColor
-			} : {};
-		},
-
-		dlUrl(): string {
-			return this.file.url;
-		}
-	},
-
-	methods: {
-		rename() {
-			const name = window.prompt(this.$t('rename'), this.file.name);
-			if (name == null || name == '' || name == this.file.name) return;
-			this.$root.api('drive/files/update', {
-				fileId: this.file.id,
-				name: name
-			}).then(() => {
-				this.browser.cf(this.file, true);
-			});
-		},
-
-		move() {
-			this.$chooseDriveFolder().then(folder => {
-				this.$root.api('drive/files/update', {
-					fileId: this.file.id,
-					folderId: folder == null ? null : folder.id
-				}).then(() => {
-					this.browser.cf(this.file, true);
-				});
-			});
-		},
-
-		del() {
-			this.$root.api('drive/files/delete', {
-				fileId: this.file.id
-			}).then(() => {
-				this.browser.cd(this.file.folderId);
-			});
-		},
-
-		toggleSensitive() {
-			this.$root.api('drive/files/update', {
-				fileId: this.file.id,
-				isSensitive: !this.file.isSensitive
-			});
-
-			this.file.isSensitive = !this.file.isSensitive;
-		},
-
-		showCreatedAt() {
-			this.$root.dialog({
-				type: 'info',
-				text: (new Date(this.file.createdAt)).toLocaleString()
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.pyvicwrksnfyhpfgkjwqknuururpaztw
-	> .preview
-		padding 8px
-		background var(--bg)
-
-		> .preview
-			width fit-content
-			width -moz-fit-content
-			max-width 100%
-			margin 0 auto
-			box-shadow 1px 1px 4px rgba(#000, 0.2)
-			overflow hidden
-			color var(--driveFileIcon)
-			justify-content center
-
-		> footer
-			padding 8px 8px 0 8px
-			text-align center
-			font-size 0.8em
-			color var(--text)
-			opacity 0.7
-
-			> .separator
-				display inline
-				padding 0 4px
-
-			> .size
-				display inline
-
-				.time
-					margin 0 2px
-
-				.px
-					margin-left 4px
-
-			> .aspect-ratio
-				display inline
-				opacity 0.7
-
-				&:before
-					content "("
-
-				&:after
-					content ")"
-
-	> .info
-		padding 14px
-		font-size 0.8em
-		border-top solid 1px var(--faceDivider)
-
-		> div
-			max-width 500px
-			margin 0 auto
-			color var(--text)
-
-			> .separator
-				padding 0 4px
-
-			> .created-at
-
-				> [data-icon]
-					margin-right 2px
-
-			> .nsfw
-				color #bf4633
-
-	> .menu
-		padding 0 14px 14px 14px
-		border-top solid 1px var(--faceDivider)
-
-		> div
-			max-width 500px
-			margin 0 auto
-
-	> .hash
-		padding 14px
-		border-top solid 1px var(--faceDivider)
-
-		> div
-			max-width 500px
-			margin 0 auto
-
-			> p
-				display block
-				margin 0
-				padding 0
-				color var(--text)
-				font-size 0.9em
-
-				> [data-icon]
-					margin-right 4px
-
-			> code
-				display block
-				width 100%
-				margin 6px 0 0 0
-				padding 8px
-				white-space nowrap
-				overflow auto
-				font-size 0.8em
-				color #222
-				border solid 1px #dfdfdf
-				border-radius 2px
-				background #f5f5f5
-
-</style>
diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue
deleted file mode 100644
index ed95537f9cbf8be00309b1302129347bbc0ec24f..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/drive.file.vue
+++ /dev/null
@@ -1,155 +0,0 @@
-<template>
-<a class="vupkuhvjnjyqaqhsiogfbywvjxynrgsm" @click.prevent="onClick" :href="`/i/drive/file/${ file.id }`" :data-is-selected="isSelected">
-	<div class="container">
-		<x-file-thumbnail class="thumbnail" :file="file" fit="cover"/>
-		<div class="body">
-			<p class="name">
-				<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
-				<span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
-			</p>
-			<footer>
-				<span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span>
-				<span class="separator"></span>
-				<span class="data-size">{{ file.size | bytes }}</span>
-				<span class="separator"></span>
-				<span class="created-at"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span>
-				<template v-if="file.isSensitive">
-					<span class="separator"></span>
-					<span class="nsfw"><fa :icon="['far', 'eye-slash']"/> {{ $t('nsfw') }}</span>
-				</template>
-			</footer>
-		</div>
-	</div>
-</a>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XFileThumbnail from '../../../common/views/components/drive-file-thumbnail.vue';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/components/drive.file.vue'),
-	props: ['file'],
-	components: {
-		XFileThumbnail
-	},
-	data() {
-		return {
-			isSelected: false
-		};
-	},
-	computed: {
-		browser(): any {
-			return this.$parent;
-		}
-	},
-	created() {
-		this.isSelected = this.browser.selectedFiles.some(f => f.id == this.file.id)
-
-		this.browser.$on('change-selection', this.onBrowserChangeSelection);
-	},
-	beforeDestroy() {
-		this.browser.$off('change-selection', this.onBrowserChangeSelection);
-	},
-	methods: {
-		onBrowserChangeSelection(selections) {
-			this.isSelected = selections.some(f => f.id == this.file.id);
-		},
-		onClick() {
-			this.browser.chooseFile(this.file);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.vupkuhvjnjyqaqhsiogfbywvjxynrgsm
-	display block
-	text-decoration none !important
-
-	*
-		user-select none
-		pointer-events none
-
-	> .container
-		display grid
-		max-width 500px
-		margin 0 auto
-		padding 16px
-		grid-template-columns 64px 1fr
-		grid-column-gap 10px
-
-		&:after
-			content ""
-			display block
-			clear both
-
-		> .thumbnail
-			width 64px
-			height 64px
-			color var(--driveFileIcon)
-
-		> .body
-			display block
-			word-break break-all
-
-			> .name
-				display block
-				margin 0
-				padding 0
-				font-size 0.9em
-				font-weight bold
-				color var(--text)
-				word-break break-word
-
-				> .ext
-					opacity 0.5
-
-			> .tags
-				display block
-				margin 4px 0 0 0
-				padding 0
-				list-style none
-				font-size 0.5em
-
-				> .tag
-					display inline-block
-					margin 0 5px 0 0
-					padding 1px 5px
-					border-radius 2px
-
-			> footer
-				display block
-				margin 4px 0 0 0
-				font-size 0.7em
-				color var(--text)
-
-				> .separator
-					padding 0 4px
-
-				> .type
-					opacity 0.7
-
-					> .mk-file-type-icon
-						margin-right 4px
-
-				> .data-size
-					opacity 0.7
-
-				> .created-at
-					opacity 0.7
-
-					> [data-icon]
-						margin-right 2px
-
-				> .nsfw
-					color #bf4633
-
-	&[data-is-selected]
-		background var(--primary)
-
-		&, *
-			color var(--primaryForeground) !important
-
-</style>
diff --git a/src/client/app/mobile/views/components/drive.folder.vue b/src/client/app/mobile/views/components/drive.folder.vue
deleted file mode 100644
index 0959c1e7d4ff5c2cfd2ab833093569d88bf3586b..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/drive.folder.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-<template>
-<a class="jvwxssxsytqlqvrpiymarjlzlsxskqsr" @click.prevent="onClick" :href="`/i/drive/folder/${ folder.id }`">
-	<div class="container">
-		<p class="name"><fa icon="folder"/>{{ folder.name }}</p><fa icon="angle-right"/>
-	</div>
-</a>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	props: ['folder'],
-	computed: {
-		browser(): any {
-			return this.$parent;
-		}
-	},
-	methods: {
-		onClick() {
-			this.browser.cd(this.folder);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.jvwxssxsytqlqvrpiymarjlzlsxskqsr
-	display block
-	color var(--text)
-	text-decoration none !important
-
-	*
-		user-select none
-		pointer-events none
-
-	> .container
-		max-width 500px
-		margin 0 auto
-		padding 16px
-
-		> .name
-			display block
-			margin 0
-			padding 0
-
-			> [data-icon]
-				margin-right 6px
-
-		> [data-icon]
-			position absolute
-			top 0
-			bottom 0
-			right 20px
-			height 100%
-
-</style>
diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue
deleted file mode 100644
index fe193f311a06713f375138081042962b6176cdd2..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/drive.vue
+++ /dev/null
@@ -1,618 +0,0 @@
-<template>
-<div class="kmmwchoexgckptowjmjgfsygeltxfeqs">
-	<nav ref="nav">
-		<a @click.prevent="goRoot()" href="/i/drive"><fa icon="cloud"/>{{ $t('@.drive') }}</a>
-		<template v-for="folder in hierarchyFolders">
-			<span :key="folder.id + '>'"><fa icon="angle-right"/></span>
-			<a :key="folder.id" @click.prevent="cd(folder)" :href="`/i/drive/folder/${folder.id}`">{{ folder.name }}</a>
-		</template>
-		<template v-if="folder != null">
-			<span><fa icon="angle-right"/></span>
-			<p>{{ folder.name }}</p>
-		</template>
-		<template v-if="file != null">
-			<span><fa icon="angle-right"/></span>
-			<p>{{ file.name }}</p>
-		</template>
-	</nav>
-	<mk-uploader ref="uploader"/>
-	<div class="browser" :class="{ fetching }" v-if="file == null">
-		<div class="info" v-if="info">
-			<p v-if="folder == null">{{ (info.usage / info.capacity * 100).toFixed(1) }}% {{ $t('used') }}</p>
-			<p v-if="folder != null && (folder.foldersCount > 0 || folder.filesCount > 0)">
-				<template v-if="folder.foldersCount > 0">{{ folder.foldersCount }} {{ $t('folder-count') }}</template>
-				<template v-if="folder.foldersCount > 0 && folder.filesCount > 0">{{ $t('count-separator') }}</template>
-				<template v-if="folder.filesCount > 0">{{ folder.filesCount }} {{ $t('file-count') }}</template>
-			</p>
-		</div>
-		<div class="folders" v-if="folders.length > 0 || moreFolders">
-			<x-folder class="folder" v-for="folder in folders" :key="folder.id" :folder="folder"/>
-			<p v-if="moreFolders">{{ $t('@.load-more') }}</p>
-		</div>
-		<div class="files" v-if="files.length > 0 || moreFiles">
-			<x-file class="file" v-for="file in files" :key="file.id" :file="file"/>
-			<button class="more" v-if="moreFiles" @click="fetchMoreFiles">
-				{{ fetchingMoreFiles ? this.$t('@.loading') : this.$t('@.load-more') }}
-			</button>
-		</div>
-		<div class="empty" v-if="files.length == 0 && !moreFiles && folders.length == 0 && !moreFolders && !fetching">
-			<p v-if="folder == null">{{ $t('nothing-in-drive') }}</p>
-			<p v-if="folder != null">{{ $t('folder-is-empty') }}</p>
-		</div>
-	</div>
-	<div class="fetching" v-if="fetching && file == null && files.length == 0 && folders.length == 0">
-		<div class="spinner">
-			<div class="dot1"></div>
-			<div class="dot2"></div>
-		</div>
-	</div>
-	<input ref="file" class="file" type="file" multiple="multiple" @change="onChangeLocalFile"/>
-	<x-file-detail v-if="file != null" :file="file"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XFolder from './drive.folder.vue';
-import XFile from './drive.file.vue';
-import XFileDetail from './drive.file-detail.vue';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/components/drive.vue'),
-	components: {
-		XFolder,
-		XFile,
-		XFileDetail
-	},
-	props: ['initFolder', 'initFile', 'selectFile', 'multiple', 'isNaked', 'top'],
-	data() {
-		return {
-			/**
-			 * 現在の階層(フォルダ)
-			 * * null でルートを表す
-			 */
-			folder: null,
-
-			file: null,
-
-			files: [],
-			folders: [],
-			moreFiles: false,
-			moreFolders: false,
-			hierarchyFolders: [],
-			selectedFiles: [],
-			info: null,
-			connection: null,
-
-			fetching: true,
-			fetchingMoreFiles: false,
-			fetchingMoreFolders: false
-		};
-	},
-	computed: {
-		isFileSelectMode(): boolean {
-			return this.selectFile;
-		}
-	},
-	watch: {
-		top() {
-			if (this.isNaked) {
-				(this.$refs.nav as any).style.top = `${this.top}px`;
-			}
-		}
-	},
-	mounted() {
-		this.connection = this.$root.stream.useSharedConnection('drive');
-
-		this.connection.on('fileCreated', this.onStreamDriveFileCreated);
-		this.connection.on('fileUpdated', this.onStreamDriveFileUpdated);
-		this.connection.on('fileDeleted', this.onStreamDriveFileDeleted);
-		this.connection.on('folderCreated', this.onStreamDriveFolderCreated);
-		this.connection.on('folderUpdated', this.onStreamDriveFolderUpdated);
-
-		if (this.initFolder) {
-			this.cd(this.initFolder, true);
-		} else if (this.initFile) {
-			this.cf(this.initFile, true);
-		} else {
-			this.fetch();
-		}
-
-		if (this.isNaked) {
-			(this.$refs.nav as any).style.top = `${this.top}px`;
-		}
-	},
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-	methods: {
-		onStreamDriveFileCreated(file) {
-			this.addFile(file, true);
-		},
-
-		onStreamDriveFileUpdated(file) {
-			const current = this.folder ? this.folder.id : null;
-			if (current != file.folderId) {
-				this.removeFile(file);
-			} else {
-				this.addFile(file, true);
-			}
-		},
-
-		onStreamDriveFileDeleted(fileId) {
-			this.removeFile(fileId);
-		},
-
-		onStreamDriveFolderCreated(folder) {
-			this.addFolder(folder, true);
-		},
-
-		onStreamDriveFolderUpdated(folder) {
-			const current = this.folder ? this.folder.id : null;
-			if (current != folder.parentId) {
-				this.removeFolder(folder);
-			} else {
-				this.addFolder(folder, true);
-			}
-		},
-
-		dive(folder) {
-			this.hierarchyFolders.unshift(folder);
-			if (folder.parent) this.dive(folder.parent);
-		},
-
-		cd(target, silent = false) {
-			if (target == null) {
-				this.goRoot(silent);
-				return;
-			} else if (typeof target == 'object') {
-				target = target.id;
-			}
-
-			this.file = null;
-			this.fetching = true;
-
-			this.$root.api('drive/folders/show', {
-				folderId: target
-			}).then(folder => {
-				this.folder = folder;
-				this.hierarchyFolders = [];
-
-				if (folder.parent) this.dive(folder.parent);
-
-				this.$emit('open-folder', this.folder, silent);
-				this.fetch();
-			});
-		},
-
-		addFolder(folder, unshift = false) {
-			const current = this.folder ? this.folder.id : null;
-			// 追加しようとしているフォルダが、今居る階層とは違う階層のものだったら中断
-			if (current != folder.parentId) return;
-
-			// 追加しようとしているフォルダを既に所有してたら中断
-			if (this.folders.some(f => f.id == folder.id)) return;
-
-			if (unshift) {
-				this.folders.unshift(folder);
-			} else {
-				this.folders.push(folder);
-			}
-		},
-
-		addFile(file, unshift = false) {
-			const current = this.folder ? this.folder.id : null;
-			// 追加しようとしているファイルが、今居る階層とは違う階層のものだったら中断
-			if (current != file.folderId) return;
-
-			if (this.files.some(f => f.id == file.id)) {
-				const exist = this.files.map(f => f.id).indexOf(file.id);
-				Vue.set(this.files, exist, file);
-				return;
-			}
-
-			if (unshift) {
-				this.files.unshift(file);
-			} else {
-				this.files.push(file);
-			}
-		},
-
-		removeFolder(folder) {
-			if (typeof folder == 'object') folder = folder.id;
-			this.folders = this.folders.filter(f => f.id != folder);
-		},
-
-		removeFile(file) {
-			if (typeof file == 'object') file = file.id;
-			this.files = this.files.filter(f => f.id != file);
-		},
-
-		appendFile(file) {
-			this.addFile(file);
-		},
-		appendFolder(folder) {
-			this.addFolder(folder);
-		},
-		prependFile(file) {
-			this.addFile(file, true);
-		},
-		prependFolder(folder) {
-			this.addFolder(folder, true);
-		},
-
-		goRoot(silent = false) {
-			// すでにrootにいるなら何もしない
-			if (this.folder == null && this.file == null) return;
-			
-			this.file = null;
-			this.folder = null;
-			this.hierarchyFolders = [];
-			this.$emit('move-root', silent);
-			this.fetch();
-		},
-
-		fetch() {
-			this.folders = [];
-			this.files = [];
-			this.moreFolders = false;
-			this.moreFiles = false;
-			this.fetching = true;
-
-			this.$emit('begin-fetch');
-
-			let fetchedFolders = null;
-			let fetchedFiles = null;
-
-			const foldersMax = 20;
-			const filesMax = 20;
-
-			// フォルダ一覧取得
-			this.$root.api('drive/folders', {
-				folderId: this.folder ? this.folder.id : null,
-				limit: foldersMax + 1
-			}).then(folders => {
-				if (folders.length == foldersMax + 1) {
-					this.moreFolders = true;
-					folders.pop();
-				}
-				fetchedFolders = folders;
-				complete();
-			});
-
-			// ファイル一覧取得
-			this.$root.api('drive/files', {
-				folderId: this.folder ? this.folder.id : null,
-				limit: filesMax + 1
-			}).then(files => {
-				if (files.length == filesMax + 1) {
-					this.moreFiles = true;
-					files.pop();
-				}
-				fetchedFiles = files;
-				complete();
-			});
-
-			let flag = false;
-			const complete = () => {
-				if (flag) {
-					for (const x of fetchedFolders) this.appendFolder(x);
-					for (const x of fetchedFiles) this.appendFile(x);
-					this.fetching = false;
-
-					// 一連の読み込みが完了したイベントを発行
-					this.$emit('fetched');
-				} else {
-					flag = true;
-					// 一連の読み込みが半分完了したイベントを発行
-					this.$emit('fetch-mid');
-				}
-			};
-
-			if (this.folder == null) {
-				// Fetch addtional drive info
-				this.$root.api('drive').then(info => {
-					this.info = info;
-				});
-			}
-		},
-
-		fetchMoreFiles() {
-			this.fetching = true;
-			this.fetchingMoreFiles = true;
-
-			const max = 30;
-
-			// ファイル一覧取得
-			this.$root.api('drive/files', {
-				folderId: this.folder ? this.folder.id : null,
-				limit: max + 1,
-				untilId: this.files[this.files.length - 1].id
-			}).then(files => {
-				if (files.length == max + 1) {
-					this.moreFiles = true;
-					files.pop();
-				} else {
-					this.moreFiles = false;
-				}
-				for (const x of files) this.appendFile(x);
-				this.fetching = false;
-				this.fetchingMoreFiles = false;
-			});
-		},
-
-		chooseFile(file) {
-			if (this.isFileSelectMode) {
-				if (this.multiple) {
-					if (this.selectedFiles.some(f => f.id == file.id)) {
-						this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id);
-					} else {
-						this.selectedFiles.push(file);
-					}
-					this.$emit('change-selection', this.selectedFiles);
-				} else {
-					this.$emit('selected', file);
-				}
-			} else {
-				this.cf(file);
-			}
-		},
-
-		cf(file, silent = false) {
-			if (typeof file == 'object') file = file.id;
-
-			this.fetching = true;
-
-			this.$root.api('drive/files/show', {
-				fileId: file
-			}).then(file => {
-				this.file = file;
-				this.folder = null;
-				this.hierarchyFolders = [];
-
-				if (file.folder) this.dive(file.folder);
-
-				this.fetching = false;
-
-				this.$emit('open-file', this.file, silent);
-			});
-		},
-
-		selectLocalFile() {
-			(this.$refs.file as any).click();
-		},
-
-		createFolder() {
-			this.$root.dialog({
-				title: this.$t('folder-name'),
-				input: {
-					default: this.folder.name
-				}
-			}).then(({ result: name }) => {
-				if (!name) {
-					this.$root.dialog({
-						type: 'error',
-						text: this.$t('folder-name-cannot-empty')
-					});
-					return;
-				}
-				this.$root.api('drive/folders/create', {
-					name: name,
-					parentId: this.folder ? this.folder.id : undefined
-				}).then(folder => {
-					this.addFolder(folder, true);
-				});
-			});
-		},
-
-		renameFolder() {
-			if (this.folder == null) {
-				this.$root.dialog({
-					type: 'error',
-					text: this.$t('here-is-root')
-				});
-				return;
-			}
-			this.$root.dialog({
-				title: this.$t('folder-name'),
-				input: {
-					default: this.folder.name
-				}
-			}).then(({ result: name }) => {
-				if (!name) {
-					this.$root.dialog({
-						type: 'error',
-						text: this.$t('cannot-empty')
-					});
-					return;
-				}
-				this.$root.api('drive/folders/update', {
-					name: name,
-					folderId: this.folder.id
-				}).then(folder => {
-					this.cd(folder);
-				});
-			});
-		},
-
-		moveFolder() {
-			if (this.folder == null) {
-				this.$root.dialog({
-					type: 'error',
-					text: this.$t('here-is-root')
-				});
-				return;
-			}
-			this.$chooseDriveFolder().then(folder => {
-				this.$root.api('drive/folders/update', {
-					parentId: folder ? folder.id : null,
-					folderId: this.folder.id
-				}).then(folder => {
-					this.cd(folder);
-				});
-			});
-		},
-
-		urlUpload() {
-			const url = window.prompt(this.$t('url-prompt'));
-			if (url == null || url == '') return;
-			this.$root.api('drive/files/upload_from_url', {
-				url: url,
-				folderId: this.folder ? this.folder.id : undefined
-			});
-			this.$root.dialog({
-				type: 'info',
-				text: this.$t('uploading')
-			});
-		},
-
-		onChangeLocalFile() {
-			for (const f of Array.from((this.$refs.file as any).files)) {
-				(this.$refs.uploader as any).upload(f, this.folder);
-			}
-		},
-
-		deleteFolder() {
-			if (this.folder == null) {
-				this.$root.dialog({
-					type: 'error',
-					text: this.$t('here-is-root')
-				});
-				return;
-			}
-			this.$root.api('drive/folders/delete', {
-				folderId: this.folder.id
-			}).then(folder => {
-				this.cd(this.folder.parentId);
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.kmmwchoexgckptowjmjgfsygeltxfeqs
-	background var(--face)
-
-	> nav
-		display block
-		position sticky
-		position -webkit-sticky
-		top 0
-		z-index 1
-		width 100%
-		padding 10px 12px
-		overflow auto
-		white-space nowrap
-		font-size 0.9em
-		color var(--text)
-		-webkit-backdrop-filter blur(12px)
-		backdrop-filter blur(12px)
-		background-color var(--mobileDriveNavBg)
-		border-bottom solid 1px rgba(#000, 0.13)
-
-		> p
-		> a
-			display inline
-			margin 0
-			padding 0
-			text-decoration none !important
-			color inherit
-
-			&:last-child
-				font-weight bold
-
-			> [data-icon]
-				margin-right 4px
-
-		> span
-			margin 0 8px
-			opacity 0.5
-
-	> .browser
-		&.fetching
-			opacity 0.5
-
-		> .info
-			border-bottom solid 1px var(--faceDivider)
-
-			&:empty
-				display none
-
-			> p
-				display block
-				max-width 500px
-				margin 0 auto
-				padding 4px 16px
-				font-size 10px
-				color var(--text)
-
-		> .folders
-			> .folder
-				border-bottom solid 1px var(--faceDivider)
-
-		> .files
-			> .file
-				border-bottom solid 1px var(--faceDivider)
-
-			> .more
-				display block
-				width 100%
-				padding 16px
-				font-size 16px
-				color #555
-
-		> .empty
-			padding 16px
-			text-align center
-			color #999
-			pointer-events none
-
-			> p
-				margin 0
-
-	> .fetching
-		.spinner
-			margin 100px auto
-			width 40px
-			height 40px
-			text-align center
-
-			animation sk-rotate 2.0s infinite linear
-
-		.dot1, .dot2
-			width 60%
-			height 60%
-			display inline-block
-			position absolute
-			top 0
-			background rgba(#000, 0.2)
-			border-radius 100%
-
-			animation sk-bounce 2.0s infinite ease-in-out
-
-		.dot2
-			top auto
-			bottom 0
-			animation-delay -1.0s
-
-		@keyframes sk-rotate {
-			100% {
-				transform: rotate(360deg);
-			}
-		}
-
-		@keyframes sk-bounce {
-			0%, 100% {
-				transform: scale(0.0);
-			}
-			50% {
-				transform: scale(1.0);
-			}
-		}
-
-	> .file
-		display none
-
-</style>
diff --git a/src/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts
deleted file mode 100644
index 4e10d80f926c0844b6edf253018e29a5fc77020c..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/index.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import Vue from 'vue';
-
-import ui from './ui.vue';
-import note from './note.vue';
-import notes from './notes.vue';
-import mediaVideo from './media-video.vue';
-import notePreview from './note-preview.vue';
-import subNoteContent from './sub-note-content.vue';
-import noteCard from './note-card.vue';
-import noteDetail from './note-detail.vue';
-import notification from './notification.vue';
-import notifications from './notifications.vue';
-import notificationPreview from './notification-preview.vue';
-import userTimeline from './user-timeline.vue';
-import userListTimeline from './user-list-timeline.vue';
-import uiContainer from './ui-container.vue';
-
-Vue.component('mk-ui', ui);
-Vue.component('mk-note', note);
-Vue.component('mk-notes', notes);
-Vue.component('mk-media-video', mediaVideo);
-Vue.component('mk-note-preview', notePreview);
-Vue.component('mk-sub-note-content', subNoteContent);
-Vue.component('mk-note-card', noteCard);
-Vue.component('mk-note-detail', noteDetail);
-Vue.component('mk-notification', notification);
-Vue.component('mk-notifications', notifications);
-Vue.component('mk-notification-preview', notificationPreview);
-Vue.component('mk-user-timeline', userTimeline);
-Vue.component('mk-user-list-timeline', userListTimeline);
-Vue.component('ui-container', uiContainer);
diff --git a/src/client/app/mobile/views/components/note-card.vue b/src/client/app/mobile/views/components/note-card.vue
deleted file mode 100644
index 270482031873f32887fe50309b4d478b16692cb8..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/note-card.vue
+++ /dev/null
@@ -1,93 +0,0 @@
-<template>
-<div class="mk-note-card">
-	<a :href="note | notePage">
-		<header>
-			<img :src="avator" alt="avatar"/>
-			<h3><mk-user-name :user="note.user"/></h3>
-		</header>
-		<div>
-			{{ text }}
-		</div>
-		<mk-time :time="note.createdAt"/>
-	</a>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import summary from '../../../../../misc/get-note-summary';
-import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url';
-
-export default Vue.extend({
-	props: ['note'],
-	computed: {
-		text(): string {
-			return summary(this.note);
-		},
-		avator(): string {
-			return this.$store.state.device.disableShowingAnimatedImages
-				? getStaticImageUrl(this.note.user.avatarUrl)
-				: this.note.user.avatarUrl;
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-note-card
-	display inline-block
-	width 150px
-	//height 120px
-	font-size 12px
-	background var(--face)
-	border-radius 4px
-	box-shadow 0 2px 8px rgba(0, 0, 0, 0.2)
-
-	> a
-		display block
-		color var(--noteText)
-
-		&:hover
-			text-decoration none
-
-		> header
-			> img
-				position absolute
-				top 8px
-				left 8px
-				width 28px
-				height 28px
-				border-radius 6px
-
-			> h3
-				display inline-block
-				overflow hidden
-				width calc(100% - 45px)
-				margin 8px 0 0 42px
-				line-height 28px
-				white-space nowrap
-				text-overflow ellipsis
-				font-size 12px
-
-		> div
-			padding 2px 8px 8px 8px
-			height 60px
-			overflow hidden
-			white-space normal
-
-			&:after
-				content ""
-				display block
-				position absolute
-				top 40px
-				left 0
-				width 100%
-				height 20px
-				background linear-gradient(to bottom, transparent 0%, var(--face) 100%)
-
-		> .mk-time
-			display inline-block
-			padding 8px
-			color var(--text)
-
-</style>
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
deleted file mode 100644
index 358b827a5c4878c3c6321748fd8c946007d65152..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ /dev/null
@@ -1,351 +0,0 @@
-<template>
-<div class="mk-note-detail" tabindex="-1" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
-	<button
-		class="more"
-		v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0"
-		@click="fetchConversation"
-		:disabled="conversationFetching"
-	>
-		<template v-if="!conversationFetching"><fa icon="ellipsis-v"/></template>
-		<template v-if="conversationFetching"><fa icon="spinner" pulse/></template>
-	</button>
-	<div class="conversation">
-		<x-sub v-for="note in conversation" :key="note.id" :note="note"/>
-	</div>
-	<div class="reply-to" v-if="appearNote.reply">
-		<x-sub :note="appearNote.reply"/>
-	</div>
-	<mk-renote class="renote" v-if="isRenote" :note="note" mini/>
-	<article>
-		<header>
-			<mk-avatar class="avatar" :user="appearNote.user"/>
-			<div>
-				<router-link class="name" :to="appearNote.user | userPage"><mk-user-name :user="appearNote.user"/></router-link>
-				<span class="username"><mk-acct :user="appearNote.user"/></span>
-			</div>
-		</header>
-		<div class="body">
-			<p v-if="appearNote.cw != null" class="cw">
-				<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" />
-				<mk-cw-button v-model="showContent" :note="appearNote"/>
-			</p>
-			<div class="content" v-show="appearNote.cw == null || showContent">
-				<div class="text">
-					<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
-					<span v-if="appearNote.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span>
-					<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
-				</div>
-				<div class="files" v-if="appearNote.files.length > 0">
-					<mk-media-list :media-list="appearNote.files" :raw="true"/>
-				</div>
-				<mk-poll v-if="appearNote.poll" :note="appearNote"/>
-				<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
-				<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a>
-				<div class="map" v-if="appearNote.geo" ref="map"></div>
-				<div class="renote" v-if="appearNote.renote">
-					<mk-note-preview :note="appearNote.renote"/>
-				</div>
-			</div>
-		</div>
-		<router-link class="time" :to="appearNote | notePage">
-			<mk-time :time="appearNote.createdAt" mode="detail"/>
-		</router-link>
-		<div class="visibility-info">
-			<span class="visibility" v-if="appearNote.visibility != 'public'">
-				<fa v-if="appearNote.visibility == 'home'" icon="home"/>
-				<fa v-if="appearNote.visibility == 'followers'" icon="unlock"/>
-				<fa v-if="appearNote.visibility == 'specified'" icon="envelope"/>
-			</span>
-			<span class="localOnly" v-if="appearNote.localOnly == true"><fa icon="heart"/></span>
-		</div>
-		<footer>
-			<mk-reactions-viewer :note="appearNote"/>
-			<button @click="reply()" :title="$t('title')">
-				<template v-if="appearNote.reply"><fa icon="reply-all"/></template>
-				<template v-else><fa icon="reply"/></template>
-				<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
-			</button>
-			<button v-if="['public', 'home'].includes(appearNote.visibility)" @click="renote()" title="Renote">
-				<fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
-			</button>
-			<button v-else>
-				<fa icon="ban"/>
-			</button>
-			<button v-if="!isMyNote && appearNote.myReaction == null" class="reactionButton" @click="react()" ref="reactButton">
-				<fa icon="plus"/>
-			</button>
-			<button v-if="!isMyNote && appearNote.myReaction != null" class="reactionButton reacted" @click="undoReact(appearNote)" ref="reactButton">
-				<fa icon="minus"/>
-			</button>
-			<button @click="menu()" ref="menuButton">
-				<fa icon="ellipsis-h"/>
-			</button>
-		</footer>
-	</article>
-	<div class="replies" v-if="!compact">
-		<x-sub v-for="note in replies" :key="note.id" :note="note"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XSub from './note.sub.vue';
-import noteSubscriber from '../../../common/scripts/note-subscriber';
-import noteMixin from '../../../common/scripts/note-mixin';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/components/note-detail.vue'),
-
-	components: {
-		XSub
-	},
-
-	mixins: [noteMixin(), noteSubscriber('note')],
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		compact: {
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			conversation: [],
-			conversationFetching: false,
-			replies: []
-		};
-	},
-
-	watch: {
-		note() {
-			this.fetchReplies();
-		}
-	},
-
-	mounted() {
-		this.fetchReplies();
-	},
-
-	methods: {
-		fetchReplies() {
-			if (this.compact) return;
-			this.$root.api('notes/children', {
-				noteId: this.appearNote.id,
-				limit: 30
-			}).then(replies => {
-				this.replies = replies;
-			});
-		},
-
-		fetchConversation() {
-			this.conversationFetching = true;
-
-			// Fetch conversation
-			this.$root.api('notes/conversation', {
-				noteId: this.appearNote.replyId
-			}).then(conversation => {
-				this.conversationFetching = false;
-				this.conversation = conversation.reverse();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-note-detail
-	overflow hidden
-	width 100%
-	text-align left
-	background var(--face)
-
-	&.round
-		border-radius 8px
-
-	&.shadow
-		box-shadow 0 4px 16px rgba(#000, 0.1)
-
-		@media (min-width 500px)
-			box-shadow 0 8px 32px rgba(#000, 0.1)
-
-	> .fetching
-		padding 64px 0
-
-	> .more
-		display block
-		margin 0
-		padding 10px 0
-		width 100%
-		font-size 1em
-		text-align center
-		color var(--text)
-		cursor pointer
-		background var(--subNoteBg)
-		outline none
-		border none
-		border-bottom solid 1px var(--faceDivider)
-		border-radius 6px 6px 0 0
-		box-shadow none
-
-		&:hover
-			box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.05)
-
-		&:active
-			box-shadow 0 0 0 100px inset rgba(0, 0, 0, 0.1)
-
-	> .conversation
-		> *
-			border-bottom 1px solid var(--faceDivider)
-
-	> .renote + article
-		padding-top 8px
-
-	> .reply-to
-		border-bottom 1px solid var(--faceDivider)
-
-	> article
-		padding 14px 16px 9px 16px
-
-		@media (min-width 500px)
-			padding 28px 32px 18px 32px
-
-		> header
-			display flex
-			line-height 1.1em
-
-			> .avatar
-				display block
-				margin 0 12px 0 0
-				width 54px
-				height 54px
-				border-radius 8px
-
-				@media (min-width 500px)
-					width 60px
-					height 60px
-
-			> div
-				min-width 0
-
-				> .name
-					display inline-block
-					margin .4em 0
-					color var(--noteHeaderName)
-					font-size 16px
-					font-weight bold
-					text-align left
-					text-decoration none
-
-					&:hover
-						text-decoration underline
-
-				> .username
-					display block
-					text-align left
-					margin 0
-					color var(--noteHeaderAcct)
-
-		> .body
-			padding 8px 0
-
-			> .cw
-				cursor default
-				display block
-				margin 0
-				padding 0
-				overflow-wrap break-word
-				color var(--noteText)
-
-				> .text
-					margin-right 8px
-
-			> .content
-
-				> .text
-					display block
-					margin 0
-					padding 0
-					overflow-wrap break-word
-					font-size 16px
-					color var(--noteText)
-
-					@media (min-width 500px)
-						font-size 24px
-
-				> .renote
-					margin 8px 0
-
-					> *
-						padding 16px
-						border dashed 1px var(--quoteBorder)
-						border-radius 8px
-
-				> .location
-					margin 4px 0
-					font-size 12px
-					color var(--text)
-
-				> .map
-					width 100%
-					height 200px
-
-					&:empty
-						display none
-
-				> .mk-url-preview
-					margin-top 8px
-
-				> .files
-					> img
-						display block
-						max-width 100%
-
-		> .time
-			font-size 16px
-			color var(--noteHeaderInfo)
-
-		> .visibility-info
-			color var(--noteHeaderInfo)
-
-			> .localOnly
-				margin-left 4px
-
-		> footer
-			font-size 1.2em
-
-			> button
-				margin 0
-				padding 8px
-				background transparent
-				border none
-				box-shadow none
-				font-size 1em
-				color var(--noteActions)
-				cursor pointer
-
-				&:not(:last-child)
-					margin-right 28px
-
-				&:hover
-					color var(--noteActionsHover)
-
-				> .count
-					display inline
-					margin 0 0 0 8px
-					color var(--text)
-					opacity 0.7
-
-				&.reacted
-					color var(--primary)
-
-	> .replies
-		> *
-			border-top 1px solid var(--faceDivider)
-
-</style>
diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue
deleted file mode 100644
index 1dbbddaa6264566e1655178fc47bc3833706af15..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/note-preview.vue
+++ /dev/null
@@ -1,114 +0,0 @@
-<template>
-<div class="yohlumlkhizgfkvvscwfcrcggkotpvry" :class="{ smart: $store.state.device.postStyle == 'smart', mini: narrow }">
-	<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart' && !narrow"/>
-	<div class="main">
-		<mk-note-header class="header" :note="note" :mini="true"/>
-		<div class="body">
-			<p v-if="note.cw != null" class="cw">
-				<span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
-				<mk-cw-button v-model="showContent" :note="note"/>
-			</p>
-			<div class="content" v-show="note.cw == null || showContent">
-				<mk-sub-note-content class="text" :note="note"/>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: {
-		note: {
-			type: Object,
-			required: true
-		}
-	},
-
-	inject: {
-		narrow: {
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			showContent: false
-		};
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.yohlumlkhizgfkvvscwfcrcggkotpvry
-	display flex
-	margin 0
-	padding 0
-	overflow hidden
-	font-size 10px
-
-	&:not(.mini)
-
-		@media (min-width 350px)
-			font-size 12px
-
-		@media (min-width 500px)
-			font-size 14px
-
-		> .avatar
-
-			@media (min-width 350px)
-				margin 0 10px 0 0
-				width 44px
-				height 44px
-
-			@media (min-width 500px)
-				margin 0 12px 0 0
-				width 48px
-				height 48px
-
-	&.smart
-		> .main
-			width 100%
-
-			> header
-				align-items center
-
-	> .avatar
-		flex-shrink 0
-		display block
-		margin 0 10px 0 0
-		width 40px
-		height 40px
-		border-radius 8px
-
-	> .main
-		flex 1
-		min-width 0
-
-		> .header
-			margin-bottom 2px
-
-		> .body
-
-			> .cw
-				cursor default
-				display block
-				margin 0
-				padding 0
-				overflow-wrap break-word
-				color var(--noteText)
-
-				> .text
-					margin-right 8px
-
-			> .content
-				> .text
-					cursor default
-					margin 0
-					padding 0
-					color var(--subNoteText)
-
-</style>
diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue
deleted file mode 100644
index b951947f2af88916bb53522c93c4a376e537f0e6..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/note.sub.vue
+++ /dev/null
@@ -1,124 +0,0 @@
-<template>
-<div class="zlrxdaqttccpwhpaagdmkawtzklsccam" :class="{ smart: $store.state.device.postStyle == 'smart', mini: narrow }">
-	<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/>
-	<div class="main">
-		<mk-note-header class="header" :note="note" :mini="true"/>
-		<div class="body">
-			<p v-if="note.cw != null" class="cw">
-				<mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" />
-				<mk-cw-button v-model="showContent" :note="note"/>
-			</p>
-			<div class="content" v-show="note.cw == null || showContent">
-				<mk-sub-note-content class="text" :note="note"/>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		// TODO
-		truncate: {
-			type: Boolean,
-			default: true
-		}
-	},
-
-	inject: {
-		narrow: {
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			showContent: false
-		};
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.zlrxdaqttccpwhpaagdmkawtzklsccam
-	display flex
-	padding 16px
-	font-size 10px
-	background var(--subNoteBg)
-
-	&:not(.mini)
-
-		@media (min-width 350px)
-			font-size 12px
-
-		@media (min-width 500px)
-			font-size 14px
-
-		@media (min-width 600px)
-			padding 24px 32px
-
-		> .avatar
-
-			@media (min-width 350px)
-				margin-right 10px
-				width 42px
-				height 42px
-
-			@media (min-width 500px)
-				margin-right 14px
-				width 50px
-				height 50px
-
-	&.smart
-		> .main
-			width 100%
-
-			> header
-				align-items center
-
-	> .avatar
-		flex-shrink 0
-		display block
-		margin 0 8px 0 0
-		width 38px
-		height 38px
-		border-radius 8px
-
-	> .main
-		flex 1
-		min-width 0
-
-		> .header
-			margin-bottom 2px
-
-		> .body
-			> .cw
-				cursor default
-				display block
-				margin 0
-				padding 0
-				overflow-wrap break-word
-				color var(--noteText)
-
-				> .text
-					margin-right 8px
-
-			> .content
-				> .text
-					margin 0
-					padding 0
-					color var(--subNoteText)
-					font-size calc(1em + var(--fontSize))
-
-					pre
-						max-height 120px
-						font-size 80%
-
-</style>
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
deleted file mode 100644
index 01514f05fc2e3097d72a7eee91a9c636d309a04a..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/note.vue
+++ /dev/null
@@ -1,302 +0,0 @@
-<template>
-<div
-	class="note"
-	v-show="appearNote.deletedAt == null && !hideThisNote"
-	:tabindex="appearNote.deletedAt == null ? '-1' : null"
-	:class="{ renote: isRenote, smart: $store.state.device.postStyle == 'smart', mini: narrow }"
-	v-hotkey="keymap"
->
-	<x-sub v-for="note in conversation" :key="note.id" :note="note"/>
-	<div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
-		<x-sub :note="appearNote.reply"/>
-	</div>
-	<mk-renote class="renote" v-if="isRenote" :note="note"/>
-	<article class="article">
-		<mk-avatar class="avatar" :user="appearNote.user" v-if="$store.state.device.postStyle != 'smart'"/>
-		<div class="main">
-			<mk-note-header class="header" :note="appearNote" :mini="true"/>
-			<div class="body" v-if="appearNote.deletedAt == null">
-				<p v-if="appearNote.cw != null" class="cw">
-				<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" />
-					<mk-cw-button v-model="showContent" :note="appearNote"/>
-				</p>
-				<div class="content" v-show="appearNote.cw == null || showContent">
-					<div class="text">
-						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
-						<a class="reply" v-if="appearNote.reply"><fa icon="reply"/></a>
-						<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
-						<a class="rp" v-if="appearNote.renote != null">RN:</a>
-					</div>
-					<div class="files" v-if="appearNote.files.length > 0">
-						<mk-media-list :media-list="appearNote.files"/>
-					</div>
-					<mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/>
-					<mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="true"/>
-					<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a>
-					<div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote"/></div>
-				</div>
-				<span class="app" v-if="appearNote.app && $store.state.settings.showVia">via <b>{{ appearNote.app.name }}</b></span>
-			</div>
-			<footer v-if="appearNote.deletedAt == null" class="footer">
-				<mk-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
-				<button @click="reply()" class="button">
-					<template v-if="appearNote.reply"><fa icon="reply-all"/></template>
-					<template v-else><fa icon="reply"/></template>
-					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
-				</button>
-				<button v-if="['public', 'home'].includes(appearNote.visibility)" @click="renote()" title="Renote" class="button">
-					<fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
-				</button>
-				<button v-else class="button">
-					<fa icon="ban"/>
-				</button>
-				<button v-if="!isMyNote && appearNote.myReaction == null" class="button" @click="react()" ref="reactButton">
-					<fa icon="plus"/>
-				</button>
-				<button v-if="!isMyNote && appearNote.myReaction != null" class="button reacted" @click="undoReact(appearNote)" ref="reactButton">
-					<fa icon="minus"/>
-				</button>
-				<button class="button" @click="menu()" ref="menuButton">
-					<fa icon="ellipsis-h"/>
-				</button>
-			</footer>
-			<div class="deleted" v-if="appearNote.deletedAt != null">{{ $t('deleted') }}</div>
-		</div>
-	</article>
-	<x-sub v-for="note in replies" :key="note.id" :note="note"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-import XSub from './note.sub.vue';
-import noteMixin from '../../../common/scripts/note-mixin';
-import noteSubscriber from '../../../common/scripts/note-subscriber';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/components/note.vue'),
-	components: {
-		XSub
-	},
-
-	mixins: [
-		noteMixin({
-			mobile: true
-		}),
-		noteSubscriber('note')
-	],
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		detail: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	inject: {
-		narrow: {
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			conversation: [],
-			replies: []
-		};
-	},
-
-	created() {
-		if (this.detail) {
-			this.$root.api('notes/children', {
-				noteId: this.appearNote.id,
-				limit: 30
-			}).then(replies => {
-				this.replies = replies;
-			});
-
-			this.$root.api('notes/conversation', {
-				noteId: this.appearNote.replyId
-			}).then(conversation => {
-				this.conversation = conversation.reverse();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.note
-	overflow hidden
-	font-size 13px
-	border-bottom solid var(--lineWidth) var(--faceDivider)
-
-	&:last-of-type
-		border-bottom none
-
-	&:not(.mini)
-
-		@media (min-width 350px)
-			font-size 14px
-
-		@media (min-width 500px)
-			font-size 16px
-
-		> .article
-			@media (min-width 600px)
-				padding 32px 32px 22px
-
-			> .avatar
-				@media (min-width 350px)
-					width 48px
-					height 48px
-					border-radius 6px
-
-				@media (min-width 500px)
-					margin-right 16px
-					width 58px
-					height 58px
-					border-radius 8px
-
-			> .main
-				> .header
-					@media (min-width 500px)
-						margin-bottom 2px
-
-				> .body
-					@media (min-width 700px)
-						font-size 1.1em
-
-	&.smart
-		> .article
-			> .main
-				> header
-					align-items center
-					margin-bottom 4px
-
-	> .renote + .article
-		padding-top 8px
-
-	> .article
-		display flex
-		padding 16px 16px 9px
-
-		> .avatar
-			flex-shrink 0
-			display block
-			margin 0 10px 8px 0
-			width 42px
-			height 42px
-			border-radius 6px
-			//position -webkit-sticky
-			//position sticky
-			//top 62px
-
-		> .main
-			flex 1
-			min-width 0
-
-			> .body
-				> .cw
-					cursor default
-					display block
-					margin 0
-					padding 0
-					overflow-wrap break-word
-					color var(--noteText)
-
-					> .text
-						margin-right 8px
-
-				> .content
-
-					> .text
-						display block
-						margin 0
-						padding 0
-						overflow-wrap break-word
-						color var(--noteText)
-						font-size calc(1em + var(--fontSize))
-
-						> .reply
-							margin-right 8px
-							color var(--noteText)
-
-						> .rp
-							margin-left 4px
-							font-style oblique
-							color var(--renoteText)
-
-					.mk-url-preview
-						margin-top 8px
-
-					> .files
-						> img
-							display block
-							max-width 100%
-
-					> .location
-						margin 4px 0
-						font-size 12px
-						color #ccc
-
-					> .map
-						width 100%
-						height 200px
-
-						&:empty
-							display none
-
-					> .mk-poll
-						font-size 80%
-
-					> .renote
-						margin 8px 0
-
-						> *
-							padding 16px
-							border dashed var(--lineWidth) var(--quoteBorder)
-							border-radius 8px
-
-				> .app
-					font-size 12px
-					color #ccc
-
-			> .footer
-				> .button
-					margin 0
-					padding 8px
-					background transparent
-					border none
-					box-shadow none
-					font-size 1em
-					color var(--noteActions)
-					cursor pointer
-
-					&:not(:last-child)
-						margin-right 28px
-
-					&:hover
-						color var(--noteActionsHover)
-
-					> .count
-						display inline
-						margin 0 0 0 8px
-						color var(--text)
-						opacity 0.7
-
-					&.reacted
-						color var(--primary)
-
-			> .deleted
-				color var(--noteText)
-				opacity 0.7
-
-</style>
diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue
deleted file mode 100644
index 1a0cd5cc240835d68240f1cfdfa32ef6a4816ccc..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/notes.vue
+++ /dev/null
@@ -1,167 +0,0 @@
-<template>
-<div class="ivaojijs" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
-	<div class="empty" v-if="empty">{{ $t('@.no-notes') }}</div>
-
-	<mk-error v-if="error" @retry="init()"/>
-
-	<div class="placeholder" v-if="fetching">
-		<template v-for="i in 10">
-			<mk-note-skeleton :key="i"/>
-		</template>
-	</div>
-
-	<!-- トランジションを有効にするとなぜかメモリリークする -->
-	<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div">
-		<template v-for="(note, i) in _notes">
-			<mk-note :note="note" :key="note.id"/>
-			<p class="date" :key="note.id + '_date'" v-if="i != items.length - 1 && note._date != _notes[i + 1]._date">
-				<span><fa icon="angle-up"/>{{ note._datetext }}</span>
-				<span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span>
-			</p>
-		</template>
-	</component>
-
-	<footer v-if="more">
-		<button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
-			<template v-if="!moreFetching">{{ $t('@.load-more') }}</template>
-			<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>
-		</button>
-	</footer>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import shouldMuteNote from '../../../common/scripts/should-mute-note';
-import paging from '../../../common/scripts/paging';
-
-export default Vue.extend({
-	i18n: i18n(),
-
-	mixins: [
-		paging({
-			captureWindowScroll: true,
-
-			onQueueChanged: (self, x) => {
-				if (x.length > 0) {
-					self.$store.commit('indicate', true);
-				} else {
-					self.$store.commit('indicate', false);
-				}
-			},
-
-			onPrepend: (self, note) => {
-				// 弾く
-				if (shouldMuteNote(self.$store.state.i, self.$store.state.settings, note)) return false;
-
-				// タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
-				if (document.hidden || !self.isScrollTop()) {
-					self.$store.commit('pushBehindNote', note);
-				}
-			},
-
-			onInited: (self) => {
-				self.$emit('loaded');
-			}
-		}),
-	],
-
-	props: {
-		pagination: {
-			required: true
-		},
-	},
-
-	computed: {
-		_notes(): any[] {
-			return (this.items as any).map(item => {
-				const date = new Date(item.createdAt).getDate();
-				const month = new Date(item.createdAt).getMonth() + 1;
-				item._date = date;
-				item._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
-				return item;
-			});
-		}
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-.ivaojijs
-	overflow hidden
-	background var(--face)
-
-	&.round
-		border-radius 8px
-
-	&.shadow
-		box-shadow 0 4px 16px rgba(#000, 0.1)
-
-		@media (min-width 500px)
-			box-shadow 0 8px 32px rgba(#000, 0.1)
-
-	> .empty
-		padding 16px
-		text-align center
-		color var(--text)
-
-	.transition
-		.mk-notes-enter
-		.mk-notes-leave-to
-			opacity 0
-			transform translateY(-30px)
-
-		> *
-			transition transform .3s ease, opacity .3s ease
-
-		> .date
-			display block
-			margin 0
-			line-height 32px
-			text-align center
-			font-size 0.9em
-			color var(--dateDividerFg)
-			background var(--dateDividerBg)
-			border-bottom solid var(--lineWidth) var(--faceDivider)
-
-			span
-				margin 0 16px
-
-			[data-icon]
-				margin-right 8px
-
-	> .placeholder
-		padding 16px
-		opacity 0.3
-
-		@media (min-width 500px)
-			padding 32px
-
-	> .empty
-		margin 0 auto
-		padding 32px
-		max-width 400px
-		text-align center
-		color var(--text)
-
-	> footer
-		text-align center
-		border-top solid var(--lineWidth) var(--faceDivider)
-
-		&:empty
-			display none
-
-		> button
-			margin 0
-			padding 16px
-			width 100%
-			color var(--text)
-
-			@media (min-width 500px)
-				padding 20px
-
-			&:disabled
-				opacity 0.7
-
-</style>
diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue
deleted file mode 100644
index 8422c7342064b87b1cd5729954becad705a82a2d..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/notification-preview.vue
+++ /dev/null
@@ -1,137 +0,0 @@
-<template>
-<div class="mk-notification-preview" :class="notification.type">
-	<template v-if="notification.type == 'reaction'">
-		<mk-avatar class="avatar" :user="notification.user"/>
-		<div class="text">
-			<p><mk-reaction-icon :reaction="notification.reaction"/><mk-user-name :user="notification.user"/></p>
-			<p class="note-ref"><fa icon="quote-left"/>{{ getNoteSummary(notification.note) }}<fa icon="quote-right"/></p>
-		</div>
-	</template>
-
-	<template v-if="notification.type == 'renote'">
-		<mk-avatar class="avatar" :user="notification.note.user"/>
-		<div class="text">
-			<p><fa icon="retweet"/><mk-user-name :user="notification.note.user"/></p>
-			<p class="note-ref"><fa icon="quote-left"/>{{ getNoteSummary(notification.note.renote) }}<fa icon="quote-right"/></p>
-		</div>
-	</template>
-
-	<template v-if="notification.type == 'quote'">
-		<mk-avatar class="avatar" :user="notification.note.user"/>
-		<div class="text">
-			<p><fa icon="quote-left"/><mk-user-name :user="notification.note.user"/></p>
-			<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
-		</div>
-	</template>
-
-	<template v-if="notification.type == 'follow'">
-		<mk-avatar class="avatar" :user="notification.user"/>
-		<div class="text">
-			<p><fa icon="user-plus"/><mk-user-name :user="notification.user"/></p>
-		</div>
-	</template>
-
-	<template v-if="notification.type == 'receiveFollowRequest'">
-		<mk-avatar class="avatar" :user="notification.user"/>
-		<div class="text">
-			<p><fa icon="user-clock"/><mk-user-name :user="notification.user"/></p>
-		</div>
-	</template>
-
-	<template v-if="notification.type == 'reply'">
-		<mk-avatar class="avatar" :user="notification.note.user"/>
-		<div class="text">
-			<p><fa icon="reply"/><mk-user-name :user="notification.note.user"/></p>
-			<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
-		</div>
-	</template>
-
-	<template v-if="notification.type == 'mention'">
-		<mk-avatar class="avatar" :user="notification.note.user"/>
-		<div class="text">
-			<p><fa icon="at"/><mk-user-name :user="notification.note.user"/></p>
-			<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
-		</div>
-	</template>
-
-	<template v-if="notification.type == 'pollVote'">
-		<mk-avatar class="avatar" :user="notification.user"/>
-		<div class="text">
-			<p><fa icon="chart-pie"/><mk-user-name :user="notification.user"/></p>
-			<p class="note-ref"><fa icon="quote-left"/>{{ getNoteSummary(notification.note) }}<fa icon="quote-right"/></p>
-		</div>
-	</template>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import getNoteSummary from '../../../../../misc/get-note-summary';
-
-export default Vue.extend({
-	props: ['notification'],
-	data() {
-		return {
-			getNoteSummary
-		};
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-notification-preview
-	margin 0
-	padding 8px
-	color #fff
-	overflow-wrap break-word
-
-	&:after
-		content ""
-		display block
-		clear both
-
-	> .avatar
-		display block
-		float left
-		width 36px
-		height 36px
-		border-radius 6px
-
-	> .text
-		float right
-		width calc(100% - 36px)
-		padding-left 8px
-
-		p
-			margin 0
-
-			[data-icon], mk-reaction-icon
-				margin-right 4px
-
-	.note-ref
-
-		[data-icon]
-			font-size 1em
-			font-weight normal
-			font-style normal
-			display inline-block
-			margin-right 3px
-
-	&.renote, &.quote
-		.text p [data-icon]
-			color #77B255
-
-	&.follow
-		.text p [data-icon]
-			color #53c7ce
-
-	&.receiveFollowRequest
-		.text p [data-icon]
-			color #888
-
-	&.reply, &.mention
-		.text p [data-icon]
-			color #fff
-
-</style>
-
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
deleted file mode 100644
index 2defef477755d7dc9fa10641e40efdad096cb509..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/notification.vue
+++ /dev/null
@@ -1,199 +0,0 @@
-<template>
-<div class="mk-notification">
-	<div class="notification reaction" v-if="notification.type == 'reaction'">
-		<mk-avatar class="avatar" :user="notification.user"/>
-		<div>
-			<header>
-				<mk-reaction-icon :reaction="notification.reaction"/>
-				<router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link>
-				<mk-time :time="notification.createdAt"/>
-			</header>
-			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
-				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
-				<fa icon="quote-right"/>
-			</router-link>
-		</div>
-	</div>
-
-	<div class="notification renote" v-if="notification.type == 'renote'">
-		<mk-avatar class="avatar" :user="notification.user"/>
-		<div>
-			<header>
-				<fa icon="retweet"/>
-				<router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link>
-				<mk-time :time="notification.createdAt"/>
-			</header>
-			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)">
-				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :custom-emojis="notification.note.renote.emojis"/>
-				<fa icon="quote-right"/>
-			</router-link>
-		</div>
-	</div>
-
-	<div class="notification follow" v-if="notification.type == 'follow'">
-		<mk-avatar class="avatar" :user="notification.user"/>
-		<div>
-			<header>
-				<fa icon="user-plus"/>
-				<router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link>
-				<mk-time :time="notification.createdAt"/>
-			</header>
-		</div>
-	</div>
-
-	<div class="notification followRequest" v-if="notification.type == 'receiveFollowRequest'">
-		<mk-avatar class="avatar" :user="notification.user"/>
-		<div>
-			<header>
-				<fa icon="user-clock"/>
-				<router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link>
-				<mk-time :time="notification.createdAt"/>
-			</header>
-		</div>
-	</div>
-
-	<div class="notification pollVote" v-if="notification.type == 'pollVote'">
-		<mk-avatar class="avatar" :user="notification.user"/>
-		<div>
-			<header>
-				<fa icon="chart-pie"/>
-				<router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link>
-				<mk-time :time="notification.createdAt"/>
-			</header>
-			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
-				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
-				<fa icon="quote-right"/>
-			</router-link>
-		</div>
-	</div>
-
-	<template v-if="notification.type == 'quote'">
-		<mk-note :note="notification.note"/>
-	</template>
-
-	<template v-if="notification.type == 'reply'">
-		<mk-note :note="notification.note"/>
-	</template>
-
-	<template v-if="notification.type == 'mention'">
-		<mk-note :note="notification.note"/>
-	</template>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import getNoteSummary from '../../../../../misc/get-note-summary';
-
-export default Vue.extend({
-	props: ['notification'],
-	data() {
-		return {
-			getNoteSummary
-		};
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-notification
-
-	&.wide
-		> .notification
-			@media (min-width 350px)
-				font-size 14px
-
-			@media (min-width 500px)
-				font-size 16px
-
-			@media (min-width 600px)
-				padding 24px 32px
-
-			> .avatar
-				@media (min-width 500px)
-					width 42px
-					height 42px
-
-			> div
-				@media (min-width 500px)
-					width calc(100% - 42px)
-
-	> .notification
-		padding 16px
-		font-size 12px
-		overflow-wrap break-word
-
-		&:after
-			content ""
-			display block
-			clear both
-
-		> .avatar
-			display block
-			float left
-			width 36px
-			height 36px
-			border-radius 6px
-
-		> div
-			float right
-			width calc(100% - 36px)
-			padding-left 8px
-
-			> header
-				display flex
-				align-items baseline
-				white-space nowrap
-
-				[data-icon], .mk-reaction-icon
-					margin-right 4px
-
-				> .name
-					text-overflow ellipsis
-					white-space nowrap
-					min-width 0
-					overflow hidden
-
-				> .mk-time
-					margin-left auto
-					color var(--noteHeaderInfo)
-					font-size 0.9em
-
-			> .note-preview
-				color var(--noteText)
-
-			> .note-ref
-				color var(--noteText)
-				display inline-block
-				width: 100%
-				overflow hidden
-				white-space nowrap
-				text-overflow ellipsis
-
-				[data-icon]
-					font-size 1em
-					font-weight normal
-					font-style normal
-					display inline-block
-					margin-right 3px
-
-		&.reaction
-			> div > header
-				align-items normal
-
-		&.renote
-			> div > header [data-icon]
-				color #77B255
-
-		&.follow
-			> div > header [data-icon]
-				color #53c7ce
-
-		&.receiveFollowRequest
-			> div > header [data-icon]
-				color #888
-
-</style>
diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue
deleted file mode 100644
index ca6a8beca328e4ee5dfcd7f51bd9e6ac7fa8ff11..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/notifications.vue
+++ /dev/null
@@ -1,167 +0,0 @@
-<template>
-<div class="mk-notifications">
-	<div class="placeholder" v-if="fetching">
-		<template v-for="i in 10">
-			<mk-note-skeleton :key="i"/>
-		</template>
-	</div>
-
-	<!-- トランジションを有効にするとなぜかメモリリークする -->
-	<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications" tag="div">
-		<template v-for="(notification, i) in _notifications">
-			<mk-notification :notification="notification" :key="notification.id" :class="{ wide: wide }"/>
-			<p class="date" :key="notification.id + '_date'" v-if="i != items.length - 1 && notification._date != _notifications[i + 1]._date">
-				<span><fa icon="angle-up"/>{{ notification._datetext }}</span>
-				<span><fa icon="angle-down"/>{{ _notifications[i + 1]._datetext }}</span>
-			</p>
-		</template>
-	</component>
-
-	<button class="more" v-if="more" @click="fetchMore" :disabled="moreFetching">
-		<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>
-		{{ moreFetching ? $t('@.loading') : $t('@.load-more') }}
-	</button>
-
-	<p class="empty" v-if="empty">{{ $t('empty') }}</p>
-
-	<mk-error v-if="error" @retry="init()"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import paging from '../../../common/scripts/paging';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/components/notifications.vue'),
-
-	mixins: [
-		paging({
-			beforeInit: (self) => {
-				self.$emit('beforeInit');
-			},
-			onInited: (self) => {
-				self.$emit('inited');
-			}
-		}),
-	],
-
-	props: {
-		type: {
-			type: String,
-			required: false
-		},
-		wide: {
-			type: Boolean,
-			required: false,
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			connection: null,
-			pagination: {
-				endpoint: 'i/notifications',
-				limit: 15,
-				params: () => ({
-					includeTypes: this.type ? [this.type] : undefined
-				})
-			}
-		};
-	},
-
-	computed: {
-		_notifications(): any[] {
-			return (this.items as any).map(notification => {
-				const date = new Date(notification.createdAt).getDate();
-				const month = new Date(notification.createdAt).getMonth() + 1;
-				notification._date = date;
-				notification._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
-				return notification;
-			});
-		}
-	},
-
-	watch: {
-		type() {
-			this.reload();
-		}
-	},
-
-	mounted() {
-		this.connection = this.$root.stream.useSharedConnection('main');
-		this.connection.on('notification', this.onNotification);
-	},
-
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		onNotification(notification) {
-			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
-			this.$root.stream.send('readNotification', {
-				id: notification.id
-			});
-
-			this.prepend(notification);
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-notifications
-	.transition
-		.mk-notifications-enter
-		.mk-notifications-leave-to
-			opacity 0
-			transform translateY(-30px)
-
-		> *
-			transition transform .3s ease, opacity .3s ease
-
-	> .notifications
-
-		> .mk-notification:not(:last-child)
-			border-bottom solid var(--lineWidth) var(--faceDivider)
-
-		> .date
-			display block
-			margin 0
-			line-height 32px
-			text-align center
-			font-size 0.8em
-			color var(--dateDividerFg)
-			background var(--dateDividerBg)
-			border-bottom solid var(--lineWidth) var(--faceDivider)
-
-			span
-				margin 0 16px
-
-			[data-icon]
-				margin-right 8px
-
-	> .more
-		display block
-		width 100%
-		padding 16px
-		color var(--text)
-		border-top solid var(--lineWidth) rgba(#000, 0.05)
-
-		> [data-icon]
-			margin-right 4px
-
-	> .empty
-		margin 0
-		padding 16px
-		text-align center
-		color var(--text)
-
-	> .placeholder
-		padding 32px
-		opacity 0.3
-
-</style>
diff --git a/src/client/app/mobile/views/components/notify.vue b/src/client/app/mobile/views/components/notify.vue
deleted file mode 100644
index c6e1df0fde254f512126c27aeb837b16b89a29f3..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/notify.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-<template>
-<div class="mk-notify" :class="pos">
-	<div>
-		<mk-notification-preview :notification="notification"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import anime from 'animejs';
-
-export default Vue.extend({
-	props: ['notification'],
-	computed: {
-		pos() {
-			return this.$store.state.device.mobileNotificationPosition;
-		}
-	},
-	mounted() {
-		this.$nextTick(() => {
-			anime({
-				targets: this.$el,
-				[this.pos]: '0px',
-				duration: 500,
-				easing: 'easeOutQuad'
-			});
-
-			setTimeout(() => {
-				anime({
-					targets: this.$el,
-					[this.pos]: `-${this.$el.offsetHeight}px`,
-					duration: 500,
-					easing: 'easeOutQuad',
-					complete: () => this.destroyDom()
-				});
-			}, 6000);
-		});
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-notify
-	$height = 78px
-
-	position fixed
-	z-index 10000
-	left 0
-	right 0
-	width 100%
-	max-width 500px
-	height $height
-	margin 0 auto
-	padding 8px
-	pointer-events none
-	font-size 80%
-
-	&.bottom
-		bottom -($height)
-
-	&.top
-		top -($height)
-
-	> div
-		height 100%
-		-webkit-backdrop-filter blur(2px)
-		backdrop-filter blur(2px)
-		background-color rgba(#000, 0.5)
-		border-radius 7px
-		overflow hidden
-
-</style>
diff --git a/src/client/app/mobile/views/components/post-form-dialog.vue b/src/client/app/mobile/views/components/post-form-dialog.vue
deleted file mode 100644
index 4ae79dbd7b52a377310dc7b8b0a8b338ab5e3c9f..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/post-form-dialog.vue
+++ /dev/null
@@ -1,120 +0,0 @@
-<template>
-<ui-modal
-	ref="modal"
-	:close-on-bg-click="false"
-	:close-anime-duration="300"
-	@before-close="onBeforeClose">
-	<div class="main" ref="main">
-		<x-post-form ref="form"
-			:reply="reply"
-			:renote="renote"
-			:mention="mention"
-			:initial-text="initialText"
-			:initial-note="initialNote"
-			:instant="instant"
-			@posted="onPosted"
-			@cancel="onCanceled"/>
-	</div>
-</ui-modal>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import anime from 'animejs';
-import XPostForm from './post-form.vue';
-
-export default Vue.extend({
-	components: {
-		XPostForm
-	},
-
-	props: {
-		reply: {
-			type: Object,
-			required: false
-		},
-		renote: {
-			type: Object,
-			required: false
-		},
-		mention: {
-			type: Object,
-			required: false
-		},
-		initialText: {
-			type: String,
-			required: false
-		},
-		initialNote: {
-			type: Object,
-			required: false
-		},
-		instant: {
-			type: Boolean,
-			required: false,
-			default: false
-		}
-	},
-
-	mounted() {
-		this.$nextTick(() => {
-			anime({
-				targets: this.$refs.main,
-				opacity: 1,
-				translateY: [-16, 0],
-				duration: 300,
-				easing: 'easeOutQuad'
-			});
-		});
-	},
-
-	methods: {
-		focus() {
-			this.$refs.form.focus();
-		},
-
-		onBeforeClose() {
-			(this.$refs.main as any).style.pointerEvents = 'none';
-
-			anime({
-				targets: this.$refs.main,
-				opacity: 0,
-				translateY: 16,
-				duration: 300,
-				easing: 'easeOutQuad'
-			});
-		},
-
-		close() {
-			(this.$refs.modal as any).close();
-		},
-
-		onPosted() {
-			this.$emit('posted');
-			this.close();
-		},
-
-		onCanceled() {
-			this.$emit('cancel');
-			this.close();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-
-.main
-	display block
-	position fixed
-	z-index 10000
-	top 0
-	left 0
-	right 0
-	height 100%
-	overflow auto
-	margin 0 auto 0 auto
-	opacity 0
-	transform translateY(-16px)
-
-</style>
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
deleted file mode 100644
index 38c6a42dd569a33143e601d1c68ed22ad5209c94..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/post-form.vue
+++ /dev/null
@@ -1,244 +0,0 @@
-<template>
-<div class="gafaadew">
-	<div class="form"
-		@dragover.stop="onDragover"
-		@dragenter="onDragenter"
-		@dragleave="onDragleave"
-		@drop.stop="onDrop"
-	>
-		<header>
-			<button class="cancel" @click="cancel"><fa icon="times"/></button>
-			<div>
-				<span class="text-count" :class="{ over: trimmedLength(text) > maxNoteTextLength }">{{ maxNoteTextLength - trimmedLength(text) }}</span>
-				<span class="geo" v-if="geo"><fa icon="map-marker-alt"/></span>
-				<button class="submit" :disabled="!canPost" @click="post">{{ submitText }}</button>
-			</div>
-		</header>
-		<div class="form">
-			<mk-note-preview class="preview" v-if="reply" :note="reply"/>
-			<mk-note-preview class="preview" v-if="renote" :note="renote"/>
-			<div class="with-quote" v-if="quoteId"><fa icon="quote-left"/> {{ $t('@.post-form.quote-attached') }}<button @click="quoteId = null"><fa icon="times"/></button></div>
-			<div v-if="visibility === 'specified'" class="to-specified">
-				<fa icon="envelope"/> {{ $t('@.post-form.specified-recipient') }}
-				<div class="visibleUsers">
-					<span v-for="u in visibleUsers">
-						<mk-user-name :user="u"/>
-						<button @click="removeVisibleUser(u)"><fa icon="times"/></button>
-					</span>
-					<button @click="addVisibleUser">{{ $t('@.post-form.add-visible-user') }}</button>
-				</div>
-			</div>
-			<div class="local-only" v-if="localOnly === true"><fa icon="heart"/> {{ $t('@.post-form.local-only-message') }}</div>
-			<input v-show="useCw" ref="cw" v-model="cw" :placeholder="$t('@.post-form.cw-placeholder')" v-autocomplete="{ model: 'cw' }">
-			<textarea v-model="text" ref="text" :disabled="posting" :placeholder="placeholder" v-autocomplete="{ model: 'text' }" @paste="onPaste"></textarea>
-			<x-post-form-attaches class="attaches" :files="files"/>
-			<x-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/>
-			<mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
-			<footer>
-				<button class="upload" @click="chooseFile"><fa icon="upload"/></button>
-				<button class="drive" @click="chooseFileFromDrive"><fa icon="cloud"/></button>
-				<button class="kao" @click="kao"><fa :icon="['far', 'smile']"/></button>
-				<button class="poll" @click="poll = true"><fa icon="chart-pie"/></button>
-				<button class="poll" @click="useCw = !useCw"><fa :icon="['far', 'eye-slash']"/></button>
-				<button class="geo" @click="geo ? removeGeo() : setGeo()"><fa icon="map-marker-alt"/></button>
-				<button class="visibility" @click="setVisibility" ref="visibilityButton">
-					<span v-if="visibility === 'public'"><fa icon="globe"/></span>
-					<span v-if="visibility === 'home'"><fa icon="home"/></span>
-					<span v-if="visibility === 'followers'"><fa icon="unlock"/></span>
-					<span v-if="visibility === 'specified'"><fa icon="envelope"/></span>
-				</button>
-			</footer>
-			<input ref="file" class="file" type="file" multiple="multiple" @change="onChangeFile"/>
-		</div>
-	</div>
-	<div class="hashtags" v-if="recentHashtags.length > 0 && $store.state.settings.suggestRecentHashtags">
-		<a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)">#{{ tag }}</a>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import form from '../../../common/scripts/post-form';
-
-export default Vue.extend({
-	i18n: i18n(),
-
-	mixins: [
-		form({
-			mobile: true
-		}),
-	],
-
-	methods: {
-		cancel() {
-			this.$emit('cancel');
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.gafaadew
-	max-width 500px
-	width calc(100% - 16px)
-	margin 8px auto
-
-	@media (min-width 500px)
-		margin 16px auto
-		width calc(100% - 32px)
-
-		> .form
-			box-shadow 0 8px 32px rgba(#000, 0.1)
-
-	@media (min-width 600px)
-		margin 32px auto
-
-	> .form
-		background var(--face)
-		border-radius 8px
-		box-shadow 0 0 2px rgba(#000, 0.1)
-
-		> header
-			z-index 1000
-			height 50px
-			box-shadow 0 1px 0 0 var(--mobilePostFormDivider)
-
-			> .cancel
-				padding 0
-				width 50px
-				line-height 50px
-				font-size 24px
-				color var(--text)
-
-			> div
-				position absolute
-				top 0
-				right 0
-				color var(--text)
-
-				> .text-count
-					line-height 50px
-
-				> .geo
-					margin 0 8px
-					line-height 50px
-
-				> .submit
-					margin 8px
-					padding 0 16px
-					line-height 34px
-					vertical-align bottom
-					color var(--primaryForeground)
-					background var(--primary)
-					border-radius 4px
-
-					&:disabled
-						opacity 0.7
-
-		> .form
-			max-width 500px
-			margin 0 auto
-
-			> .preview
-				padding 16px
-
-			> .with-quote
-				margin 0 0 8px 0
-				color var(--primary)
-
-				> button
-					padding 4px 8px
-					color var(--primaryAlpha04)
-
-					&:hover
-						color var(--primaryAlpha06)
-
-					&:active
-						color var(--primaryDarken30)
-
-			> .to-specified
-				margin 0 0 8px 0
-				color var(--primary)
-
-				> .visibleUsers
-					display inline
-					top -1px
-					font-size 14px
-
-					> span
-						margin-left 14px
-
-						> button
-							padding 4px 8px
-							color var(--primaryAlpha04)
-
-							&:hover
-								color var(--primaryAlpha06)
-
-							&:active
-								color var(--primaryDarken30)
-
-			> .local-only
-				margin 0 0 8px 0
-				color var(--primary)
-
-			> input
-				z-index 1
-
-			> input
-			> textarea
-				display block
-				padding 12px
-				margin 0
-				width 100%
-				font-size 16px
-				color var(--inputText)
-				background var(--mobilePostFormTextareaBg)
-				border none
-				border-radius 0
-				box-shadow 0 1px 0 0 var(--mobilePostFormDivider)
-
-				&:disabled
-					opacity 0.5
-
-			> textarea
-				max-width 100%
-				min-width 100%
-				min-height 80px
-
-			> .mk-uploader
-				margin 8px 0 0 0
-				padding 8px
-
-			> .file
-				display none
-
-			> footer
-				white-space nowrap
-				overflow auto
-				-webkit-overflow-scrolling touch
-				overflow-scrolling touch
-
-				> *
-					display inline-block
-					padding 0
-					margin 0
-					width 48px
-					height 48px
-					font-size 20px
-					color var(--mobilePostFormButton)
-					background transparent
-					outline none
-					border none
-					border-radius 0
-					box-shadow none
-
-	> .hashtags
-		margin 8px
-
-		> *
-			margin-right 8px
-
-</style>
diff --git a/src/client/app/mobile/views/components/sub-note-content.vue b/src/client/app/mobile/views/components/sub-note-content.vue
deleted file mode 100644
index 66dbb90ebbbe45d0e8736a8f09c51c014f7cc622..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/sub-note-content.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<template>
-<div class="mk-sub-note-content">
-	<div class="body">
-		<span v-if="note.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
-		<span v-if="note.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span>
-		<a class="reply" v-if="note.replyId"><fa icon="reply"/></a>
-		<mfm v-if="note.text" :text="note.text" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis"/>
-		<a class="rp" v-if="note.renoteId">RN: ...</a>
-	</div>
-	<details v-if="note.files.length > 0">
-		<summary>({{ $t('media-count').replace('{}', note.files.length) }})</summary>
-		<mk-media-list :media-list="note.files"/>
-	</details>
-	<details v-if="note.poll">
-		<summary>{{ $t('poll') }}</summary>
-		<mk-poll :note="note"/>
-	</details>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-export default Vue.extend({
-	i18n: i18n('mobile/views/components/sub-note-content.vue'),
-	props: ['note']
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-sub-note-content
-	overflow-wrap break-word
-
-	> .body
-		> .reply
-			margin-right 6px
-			color #717171
-
-		> .rp
-			margin-left 4px
-			font-style oblique
-			color var(--renoteText)
-
-	mk-poll
-		font-size 80%
-
-</style>
diff --git a/src/client/app/mobile/views/components/ui-container.vue b/src/client/app/mobile/views/components/ui-container.vue
deleted file mode 100644
index 08af7035f9e385f316ad887c99eb924cce369d1b..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/ui-container.vue
+++ /dev/null
@@ -1,127 +0,0 @@
-<template>
-<div class="ukygtjoj" :class="{ naked, inNakedDeckColumn, hideHeader: !showHeader, shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
-	<header v-if="showHeader" @click="() => showBody = !showBody">
-		<div class="title"><slot name="header"></slot></div>
-		<slot name="func"></slot>
-		<button v-if="bodyTogglable">
-			<template v-if="showBody"><fa icon="angle-up"/></template>
-			<template v-else><fa icon="angle-down"/></template>
-		</button>
-	</header>
-	<div v-show="showBody">
-		<slot></slot>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	props: {
-		showHeader: {
-			type: Boolean,
-			default: true
-		},
-		naked: {
-			type: Boolean,
-			default: false
-		},
-		bodyTogglable: {
-			type: Boolean,
-			default: false
-		},
-		expanded: {
-			type: Boolean,
-			default: true
-		},
-	},
-	inject: {
-		inNakedDeckColumn: {
-			default: false
-		}
-	},
-	data() {
-		return {
-			showBody: this.expanded
-		};
-	},
-	methods: {
-		toggleContent(show: boolean) {
-			if (!this.bodyTogglable) return;
-			this.showBody = show;
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.ukygtjoj
-	overflow hidden
-
-	&:not(.inNakedDeckColumn)
-		background var(--face)
-
-		&.round
-			border-radius 8px
-
-		&.shadow
-			box-shadow 0 4px 16px rgba(#000, 0.1)
-
-		& + .ukygtjoj
-			margin-top 16px
-
-			@media (max-width 500px)
-				margin-top 8px
-
-		&.naked
-			background transparent !important
-			box-shadow none !important
-
-		> header
-			> .title
-				margin 0
-				padding 8px 10px
-				font-size 15px
-				font-weight normal
-				color var(--faceHeaderText)
-				background var(--faceHeader)
-
-				> [data-icon]
-					margin-right 6px
-
-				&:empty
-					display none
-
-			> button
-				position absolute
-				z-index 2
-				top 0
-				right 0
-				padding 0
-				width 42px
-				height 100%
-				font-size 15px
-				color var(--faceTextButton)
-
-		> div
-			color var(--text)
-
-	&.inNakedDeckColumn
-		background var(--face)
-
-		> header
-			margin 0
-			padding 8px 16px
-			font-size 12px
-			color var(--text)
-			background var(--deckColumnBg)
-
-			> button
-				position absolute
-				top 0
-				right 8px
-				padding 8px 6px
-				font-size 14px
-				color var(--text)
-
-</style>
diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue
deleted file mode 100644
index f20f64e7ff102eaaf66808d7cb910ccfef9bec63..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/ui.header.vue
+++ /dev/null
@@ -1,142 +0,0 @@
-<template>
-<div class="header" ref="root" :class="{ shadow: $store.state.device.useShadow }">
-	<div class="main" ref="main">
-		<div class="backdrop"></div>
-		<div class="content" ref="mainContainer">
-			<button class="nav" @click="$parent.isDrawerOpening = true"><fa icon="bars"/></button>
-			<i v-if="$parent.indicate" class="circle"><fa icon="circle"/></i>
-			<h1>
-				<slot>{{ $root.instanceName }}</slot>
-			</h1>
-			<slot name="func"></slot>
-		</div>
-	</div>
-	<div class="indicator" v-show="$store.state.indicate"></div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { env } from '../../../config';
-
-export default Vue.extend({
-	i18n: i18n(),
-	props: ['func'],
-
-	data() {
-		return {
-			env: env
-		};
-	},
-
-	mounted() {
-		this.$store.commit('setUiHeaderHeight', 48);
-	},
-});
-</script>
-
-<style lang="stylus" scoped>
-.header
-	$height = 48px
-
-	position fixed
-	top 0
-	left -8px
-	z-index 1024
-	width calc(100% + 16px)
-	padding 0 8px
-
-	&.shadow
-		box-shadow 0 0 8px rgba(0, 0, 0, 0.25)
-
-	&, *
-		user-select none
-
-	> .indicator
-		height 3px
-		background var(--primary)
-
-	> .warn
-		display block
-		margin 0
-		padding 4px
-		text-align center
-		font-size 12px
-		background #f00
-		color #fff
-
-	> .main
-		color var(--mobileHeaderFg)
-
-		> .backdrop
-			position absolute
-			top 0
-			z-index 1000
-			width 100%
-			height $height
-			-webkit-backdrop-filter blur(12px)
-			backdrop-filter blur(12px)
-			background-color var(--mobileHeaderBg)
-
-		> .content
-			z-index 1001
-
-			> h1
-				display block
-				margin 0 auto
-				padding 0
-				width 100%
-				max-width calc(100% - 112px)
-				text-align center
-				font-size 1.1em
-				font-weight normal
-				line-height $height
-				white-space nowrap
-				overflow hidden
-				text-overflow ellipsis
-
-				> img
-					display inline-block
-					vertical-align bottom
-					width ($height - 16px)
-					height ($height - 16px)
-					margin 8px
-					border-radius 6px
-
-			> .nav
-				display block
-				position absolute
-				top 0
-				left 0
-				padding 0
-				width $height
-				font-size 1.4em
-				line-height $height
-				border-right solid 1px rgba(#000, 0.1)
-
-				> [data-icon]
-					transition all 0.2s ease
-
-			> i.circle
-				position absolute
-				top 8px
-				left 8px
-				pointer-events none
-				font-size 10px
-				color var(--notificationIndicator)
-
-			> button:last-child
-				display block
-				position absolute
-				top 0
-				right 0
-				padding 0
-				width $height
-				text-align center
-				font-size 1.4em
-				color inherit
-				line-height $height
-				border-left solid 1px rgba(#000, 0.1)
-
-</style>
diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
deleted file mode 100644
index db250ec6f81eeb5037d4ef43b058b382716b236e..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ /dev/null
@@ -1,346 +0,0 @@
-<template>
-<div class="fquwcbxs">
-	<transition name="back">
-		<div class="backdrop"
-			v-if="isOpen"
-			@click="$parent.isDrawerOpening = false"
-			@touchstart="$parent.isDrawerOpening = false"
-		></div>
-	</transition>
-	<transition name="nav">
-		<div class="body" :class="{ notifications: showNotifications }" v-if="isOpen">
-			<div class="nav" v-show="!showNotifications">
-				<router-link class="me" v-if="$store.getters.isSignedIn" :to="`/@${$store.state.i.username}`">
-					<img class="avatar" :src="$store.state.i.avatarUrl" alt="avatar"/>
-					<p class="name"><mk-user-name :user="$store.state.i"/></p>
-				</router-link>
-				<div class="links">
-					<ul>
-						<li><router-link to="/" :data-active="$route.name == 'index'"><i><fa icon="home" fixed-width/></i>{{ $t('timeline') }}<i><fa icon="angle-right"/></i></router-link></li>
-						<li v-if="$store.state.device.enableMobileQuickNotificationView"><p @click="showNotifications = true"><i><fa :icon="faBell" fixed-width/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></p></li>
-						<li v-else><router-link to="/i/notifications" :data-active="$route.name == 'notifications'"><i><fa :icon="faBell" fixed-width/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
-						<li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'"><i><fa :icon="['far', 'comments']" fixed-width/></i>{{ $t('@.messaging') }}<i v-if="hasUnreadMessagingMessage" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
-						<li v-if="$store.getters.isSignedIn && ($store.state.i.isLocked || $store.state.i.carefulBot)"><router-link to="/i/follow-requests" :data-active="$route.name == 'follow-requests'"><i><fa :icon="['far', 'envelope']" fixed-width/></i>{{ $t('follow-requests') }}<i v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
-						<li><router-link to="/featured" :data-active="$route.name == 'featured'"><i><fa :icon="faNewspaper" fixed-width/></i>{{ $t('@.featured-notes') }}<i><fa icon="angle-right"/></i></router-link></li>
-						<li><router-link to="/explore" :data-active="$route.name == 'explore' || $route.name == 'explore-tag'"><i><fa :icon="faHashtag" fixed-width/></i>{{ $t('@.explore') }}<i><fa icon="angle-right"/></i></router-link></li>
-						<li><router-link to="/games/reversi" :data-active="$route.name == 'reversi'"><i><fa icon="gamepad" fixed-width/></i>{{ $t('game') }}<i v-if="hasGameInvitation" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
-					</ul>
-					<ul>
-						<li><router-link to="/i/widgets" :data-active="$route.name == 'widgets'"><i><fa :icon="['far', 'calendar-alt']" fixed-width/></i>{{ $t('widgets') }}<i><fa icon="angle-right"/></i></router-link></li>
-						<li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'"><i><fa icon="star" fixed-width/></i>{{ $t('@.favorites') }}<i><fa icon="angle-right"/></i></router-link></li>
-						<li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'"><i><fa icon="list" fixed-width/></i>{{ $t('user-lists') }}<i><fa icon="angle-right"/></i></router-link></li>
-						<li><router-link to="/i/groups" :data-active="$route.name == 'user-groups'"><i><fa :icon="faUsers" fixed-width/></i>{{ $t('user-groups') }}<i><fa icon="angle-right"/></i></router-link></li>
-						<li><router-link to="/i/drive" :data-active="$route.name == 'drive'"><i><fa icon="cloud" fixed-width/></i>{{ $t('@.drive') }}<i><fa icon="angle-right"/></i></router-link></li>
-						<li><router-link to="/i/pages" :data-active="$route.name == 'pages'"><i><fa :icon="faStickyNote" fixed-width/></i>{{ $t('@.pages') }}<i><fa icon="angle-right"/></i></router-link></li>
-					</ul>
-					<ul>
-						<li><a @click="search"><i><fa icon="search" fixed-width/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li>
-						<li><router-link to="/i/settings" :data-active="$route.name == 'settings'"><i><fa icon="cog" fixed-width/></i>{{ $t('@.settings') }}<i><fa icon="angle-right"/></i></router-link></li>
-						<li v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)"><a href="/admin"><i><fa icon="terminal" fixed-width/></i><span>{{ $t('admin') }}</span><i><fa icon="angle-right"/></i></a></li>
-					</ul>
-					<ul>
-						<li @click="toggleDeckMode"><p><i><fa :icon="$store.state.device.inDeckMode ? faHome : faColumns" fixed-width/></i><span>{{ $store.state.device.inDeckMode ? $t('@.home') : $t('@.deck') }}</span></p></li>
-						<li @click="dark"><p><i><fa :icon="$store.state.device.darkmode ? faSun : faMoon" fixed-width/></i><span>{{ $store.state.device.darkmode ? $t('@.turn-off-darkmode') : $t('@.turn-on-darkmode') }}</span></p></li>
-					</ul>
-				</div>
-				<div class="announcements" v-if="announcements && announcements.length > 0">
-					<article v-for="announcement in announcements">
-						<span v-html="announcement.title" class="title"></span>
-						<div><mfm :text="announcement.text"/></div>
-						<img v-if="announcement.image" :src="announcement.image" alt="" style="display: block; max-height: 120px; max-width: 100%;"/>
-					</article>
-				</div>
-				<a :href="aboutUrl"><p class="about">{{ $t('about') }}</p></a>
-			</div>
-			<div class="notifications" v-if="showNotifications">
-				<header>
-					<button @click="showNotifications = false"><fa icon="times"/></button>
-					<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i>
-				</header>
-				<mk-notifications/>
-			</div>
-		</div>
-	</transition>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { lang } from '../../../config';
-import { faNewspaper, faHashtag, faHome, faColumns, faUsers } from '@fortawesome/free-solid-svg-icons';
-import { faMoon, faSun, faStickyNote, faBell } from '@fortawesome/free-regular-svg-icons';
-import { search } from '../../../common/scripts/search';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/components/ui.nav.vue'),
-
-	props: ['isOpen'],
-
-	provide: {
-		narrow: true
-	},
-
-	data() {
-		return {
-			hasGameInvitation: false,
-			connection: null,
-			aboutUrl: `/docs/${lang}/about`,
-			announcements: [],
-			searching: false,
-			showNotifications: false,
-			faNewspaper, faHashtag, faMoon, faSun, faHome, faColumns, faStickyNote, faUsers, faBell,
-		};
-	},
-
-	computed: {
-		hasUnreadNotification(): boolean {
-			return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification;
-		},
-
-		hasUnreadMessagingMessage(): boolean {
-			return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage;
-		}
-	},
-
-	watch: {
-		isOpen() {
-			this.showNotifications = false;
-		}
-	},
-
-	mounted() {
-		this.$root.getMeta().then(meta => {
-			this.announcements = meta.announcements;
-		});
-
-		if (this.$store.getters.isSignedIn) {
-			this.connection = this.$root.stream.useSharedConnection('main');
-
-			this.connection.on('reversiInvited', this.onReversiInvited);
-			this.connection.on('reversiNoInvites', this.onReversiNoInvites);
-		}
-	},
-
-	beforeDestroy() {
-		if (this.$store.getters.isSignedIn) {
-			this.connection.dispose();
-		}
-	},
-
-	methods: {
-		search() {
-			if (this.searching) return;
-
-			this.$root.dialog({
-				title: this.$t('search'),
-				input: true
-			}).then(async ({ canceled, result: query }) => {
-				if (canceled) return;
-
-				this.searching = true;
-				search(this, query).finally(() => {
-					this.searching = false;
-				});
-			});
-		},
-
-		onReversiInvited() {
-			this.hasGameInvitation = true;
-		},
-
-		onReversiNoInvites() {
-			this.hasGameInvitation = false;
-		},
-
-		dark() {
-			this.$store.commit('device/set', {
-				key: 'darkmode',
-				value: !this.$store.state.device.darkmode
-			});
-		},
-
-		toggleDeckMode() {
-			this.$store.commit('device/set', { key: 'deckMode', value: !this.$store.state.device.inDeckMode });
-			location.replace('/');
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.fquwcbxs
-	$color = var(--text)
-
-	.backdrop
-		position fixed
-		top 0
-		left 0
-		z-index 1025
-		width 100%
-		height 100%
-		background var(--mobileNavBackdrop)
-
-	.body
-		position fixed
-		top 0
-		left 0
-		z-index 1026
-		width 240px
-		height 100%
-		overflow auto
-		-webkit-overflow-scrolling touch
-		background var(--secondary)
-		font-size 15px
-
-		&.notifications
-			width 330px
-
-		> .notifications
-			padding-top 42px
-
-			> header
-				position fixed
-				top 0
-				left 0
-				z-index 1000
-				width 330px
-				line-height 42px
-				background var(--secondary)
-
-				> button
-					display block
-					padding 0 14px
-					font-size 20px
-					line-height 42px
-					color var(--text)
-
-				> i
-					position absolute
-					top 0
-					right 16px
-					font-size 12px
-					color var(--notificationIndicator)
-
-		> .nav
-
-			> .me
-				display block
-				margin 0
-				padding 16px
-
-				.avatar
-					display inline
-					max-width 64px
-					border-radius 32px
-					vertical-align middle
-
-				.name
-					display block
-					margin 0 16px
-					position absolute
-					top 0
-					left 80px
-					padding 0
-					width calc(100% - 112px)
-					color $color
-					line-height 96px
-					overflow hidden
-					text-overflow ellipsis
-					white-space nowrap
-
-			ul
-				display block
-				margin 16px 0
-				padding 0
-				list-style none
-
-				&:first-child
-					margin-top 0
-
-				&:last-child
-					margin-bottom 0
-
-				> li
-					display block
-					font-size 1em
-					line-height 1em
-
-					a, p
-						display block
-						margin 0
-						padding 0 20px
-						line-height 3rem
-						line-height calc(1rem + 30px)
-						color $color
-						text-decoration none
-
-						&[data-active]
-							color var(--primaryForeground)
-							background var(--primary)
-
-							> i:last-child
-								color var(--primaryForeground)
-
-						> i:first-child
-							margin-right 0.5em
-							width 20px
-							text-align center
-
-						> i.circle
-							margin-left 6px
-							font-size 10px
-							color var(--notificationIndicator)
-
-						> i:last-child
-							position absolute
-							top 0
-							right 0
-							padding 0 20px
-							font-size 1.2em
-							line-height calc(1rem + 30px)
-							color $color
-							opacity 0.5
-
-			.announcements
-				> article
-					background var(--mobileAnnouncement)
-					color var(--mobileAnnouncementFg)
-					padding 16px
-					margin 8px 0
-					font-size 12px
-
-					> .title
-						font-weight bold
-
-			.about
-				margin 0 0 8px 0
-				padding 1em 0
-				text-align center
-				font-size 0.8em
-				color $color
-				opacity 0.5
-
-.nav-enter-active,
-.nav-leave-active {
-	opacity: 1;
-	transform: translateX(0);
-	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-.nav-enter,
-.nav-leave-active {
-	opacity: 0;
-	transform: translateX(-240px);
-}
-
-.back-enter-active,
-.back-leave-active {
-	opacity: 1;
-	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-.back-enter,
-.back-leave-active {
-	opacity: 0;
-}
-
-</style>
diff --git a/src/client/app/mobile/views/components/ui.vue b/src/client/app/mobile/views/components/ui.vue
deleted file mode 100644
index 05c886a497dae4f51741ff573e27beeda9b76ba7..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/ui.vue
+++ /dev/null
@@ -1,136 +0,0 @@
-<template>
-<div class="mk-ui" :class="{ deck: $store.state.device.inDeckMode }">
-	<x-header v-if="!$store.state.device.inDeckMode">
-		<template #func><slot name="func"></slot></template>
-		<slot name="header"></slot>
-	</x-header>
-	<x-nav :is-open="isDrawerOpening"/>
-	<div class="content">
-		<slot></slot>
-	</div>
-	<mk-stream-indicator v-if="$store.getters.isSignedIn"/>
-	<button class="nav button" v-if="$store.state.device.inDeckMode" @click="isDrawerOpening = !isDrawerOpening"><fa icon="bars"/><i v-if="indicate"><fa icon="circle"/></i></button>
-	<button class="post button" v-if="$store.state.device.inDeckMode" @click="$post()"><fa icon="pencil-alt"/></button>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import MkNotify from './notify.vue';
-import XHeader from './ui.header.vue';
-import XNav from './ui.nav.vue';
-
-export default Vue.extend({
-	components: {
-		XHeader,
-		XNav
-	},
-
-	props: ['title'],
-
-	data() {
-		return {
-			hasGameInvitation: false,
-			isDrawerOpening: false,
-			connection: null
-		};
-	},
-
-	computed: {
-		hasUnreadNotification(): boolean {
-			return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification;
-		},
-
-		hasUnreadMessagingMessage(): boolean {
-			return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage;
-		},
-
-		indicate(): boolean {
-			return this.hasUnreadNotification || this.hasUnreadMessagingMessage || this.hasGameInvitation;
-		}
-	},
-
-	watch: {
-		'$store.state.uiHeaderHeight'() {
-			this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px';
-		}
-	},
-
-	mounted() {
-		this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px';
-
-		if (this.$store.getters.isSignedIn) {
-			this.connection = this.$root.stream.useSharedConnection('main');
-
-			this.connection.on('notification', this.onNotification);
-			this.connection.on('reversiInvited', this.onReversiInvited);
-			this.connection.on('reversiNoInvites', this.onReversiNoInvites);
-		}
-	},
-
-	beforeDestroy() {
-		if (this.$store.getters.isSignedIn) {
-			this.connection.dispose();
-		}
-	},
-
-	methods: {
-		onNotification(notification) {
-			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
-			this.$root.stream.send('readNotification', {
-				id: notification.id
-			});
-
-			this.$root.new(MkNotify, {
-				notification
-			});
-		},
-
-		onReversiInvited() {
-			this.hasGameInvitation = true;
-		},
-
-		onReversiNoInvites() {
-			this.hasGameInvitation = false;
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-ui
-	&:not(.deck)
-		padding-top 48px
-
-	> .button
-		position fixed
-		z-index 1000
-		bottom 28px
-		padding 0
-		width 64px
-		height 64px
-		border-radius 100%
-		box-shadow 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12)
-
-		> *
-			font-size 24px
-
-		&.nav
-			left 28px
-			background var(--secondary)
-			color var(--text)
-
-			> i
-				position absolute
-				top 0
-				left 0
-				color var(--notificationIndicator)
-				font-size 16px
-				animation blink 1s infinite
-
-		&.post
-			right 28px
-			background var(--primary)
-			color var(--primaryForeground)
-
-</style>
diff --git a/src/client/app/mobile/views/components/user-list-timeline.vue b/src/client/app/mobile/views/components/user-list-timeline.vue
deleted file mode 100644
index d9aa1dad8ac76eafc282a8558e7bcb6badf0a60d..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/user-list-timeline.vue
+++ /dev/null
@@ -1,76 +0,0 @@
-<template>
-<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: ['list'],
-
-	data() {
-		return {
-			connection: null,
-			date: null,
-			pagination: {
-				endpoint: 'notes/user-list-timeline',
-				limit: 10,
-				params: init => ({
-					listId: this.list.id,
-					untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
-					includeMyRenotes: this.$store.state.settings.showMyRenotes,
-					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
-					includeLocalRenotes: this.$store.state.settings.showLocalRenotes
-				})
-			}
-		};
-	},
-
-	watch: {
-		$route: 'init'
-	},
-
-	mounted() {
-		this.init();
-
-		this.$root.$on('warp', this.warp);
-		this.$once('hook:beforeDestroy', () => {
-			this.$root.$off('warp', this.warp);
-		});
-	},
-
-	beforeDestroy() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		init() {
-			if (this.connection) this.connection.dispose();
-			this.connection = this.$root.stream.connectToChannel('userList', {
-				listId: this.list.id
-			});
-			this.connection.on('note', this.onNote);
-			this.connection.on('userAdded', this.onUserAdded);
-			this.connection.on('userRemoved', this.onUserRemoved);
-		},
-
-		onNote(note) {
-			// Prepend a note
-			(this.$refs.timeline as any).prepend(note);
-		},
-
-		onUserAdded() {
-			(this.$refs.timeline as any).reload();
-		},
-
-		onUserRemoved() {
-			(this.$refs.timeline as any).reload();
-		},
-
-		warp(date) {
-			this.date = date;
-			(this.$refs.timeline as any).reload();
-		}
-	}
-});
-</script>
diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue
deleted file mode 100644
index 3b6baa76befa6164bb263538cfcfc9350d7b1af5..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/components/user-timeline.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<template>
-<mk-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/components/user-timeline.vue'),
-
-	props: ['user', 'withMedia'],
-
-	data() {
-		return {
-			date: null,
-			pagination: {
-				endpoint: 'users/notes',
-				limit: 10,
-				params: init => ({
-					userId: this.user.id,
-					withFiles: this.withMedia,
-					untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
-				})
-			}
-		};
-	},
-
-	created() {
-		this.$root.$on('warp', this.warp);
-		this.$once('hook:beforeDestroy', () => {
-			this.$root.$off('warp', this.warp);
-		});
-	},
-
-	methods: {
-		warp(date) {
-			this.date = date;
-			(this.$refs.timeline as any).reload();
-		}
-	}
-});
-</script>
diff --git a/src/client/app/mobile/views/directives/index.ts b/src/client/app/mobile/views/directives/index.ts
deleted file mode 100644
index 324e07596d90b0d6f6263488fbb6c1c17f3987cd..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/directives/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import Vue from 'vue';
-
-import userPreview from './user-preview';
-
-Vue.directive('userPreview', userPreview);
-Vue.directive('user-preview', userPreview);
diff --git a/src/client/app/mobile/views/directives/user-preview.ts b/src/client/app/mobile/views/directives/user-preview.ts
deleted file mode 100644
index 1a54abc20d7065beff4575a4255f6a396a3c02f5..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/directives/user-preview.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-// nope
-export default {};
diff --git a/src/client/app/mobile/views/pages/drive.vue b/src/client/app/mobile/views/pages/drive.vue
deleted file mode 100644
index 05163c6ed9319965f4015095d3c142d4987ad1e3..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/drive.vue
+++ /dev/null
@@ -1,147 +0,0 @@
-<template>
-<mk-ui>
-	<template #header>
-		<template v-if="folder"><span style="margin-right:4px;"><fa :icon="['far', 'folder-open']"/></span>{{ folder.name }}</template>
-		<template v-if="file"><mk-file-type-icon data-icon :type="file.type" style="margin-right:4px;"/>{{ file.name }}</template>
-		<template v-if="!folder && !file"><span style="margin-right:4px;"><fa icon="cloud"/></span>{{ $t('@.drive') }}</template>
-	</template>
-	<template #func v-if="folder || (!folder && !file)"><button @click="openContextMenu" ref="contextSource"><fa icon="ellipsis-h"/></button></template>
-	<x-drive
-		ref="browser"
-		:init-folder="initFolder"
-		:init-file="initFile"
-		:is-naked="true"
-		:top="$store.state.uiHeaderHeight"
-		@begin-fetch="Progress.start()"
-		@fetched-mid="Progress.set(0.5)"
-		@fetched="Progress.done()"
-		@move-root="onMoveRoot"
-		@open-folder="onOpenFolder"
-		@open-file="onOpenFile"
-	/>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import Progress from '../../../common/scripts/loading';
-import XMenu from '../../../common/views/components/menu.vue';
-import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
-import { faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/pages/drive.vue'),
-	components: {
-		XDrive: () => import('../components/drive.vue').then(m => m.default),
-	},
-	data() {
-		return {
-			Progress,
-			folder: null,
-			file: null,
-			initFolder: null,
-			initFile: null
-		};
-	},
-	created() {
-		this.initFolder = this.$route.params.folder;
-		this.initFile = this.$route.params.file;
-
-		window.addEventListener('popstate', this.onPopState);
-	},
-	mounted() {
-		document.title = `${this.$root.instanceName} Drive`;
-	},
-	beforeDestroy() {
-		window.removeEventListener('popstate', this.onPopState);
-	},
-	methods: {
-		onPopState() {
-			if (this.$route.params.folder) {
-				(this.$refs as any).browser.cd(this.$route.params.folder, true);
-			} else if (this.$route.params.file) {
-				(this.$refs as any).browser.cf(this.$route.params.file, true);
-			} else {
-				(this.$refs as any).browser.goRoot(true);
-			}
-		},
-		onMoveRoot(silent) {
-			const title = `${this.$root.instanceName} Drive`;
-
-			if (!silent) {
-				// Rewrite URL
-				history.pushState(null, title, '/i/drive');
-			}
-
-			document.title = title;
-
-			this.file = null;
-			this.folder = null;
-		},
-		onOpenFolder(folder, silent) {
-			const title = `${folder.name} | ${this.$root.instanceName} Drive`;
-
-			if (!silent) {
-				// Rewrite URL
-				history.pushState(null, title, `/i/drive/folder/${folder.id}`);
-			}
-
-			document.title = title;
-
-			this.file = null;
-			this.folder = folder;
-		},
-		onOpenFile(file, silent) {
-			const title = `${file.name} | ${this.$root.instanceName} Drive`;
-
-			if (!silent) {
-				// Rewrite URL
-				history.pushState(null, title, `/i/drive/file/${file.id}`);
-			}
-
-			document.title = title;
-
-			this.file = file;
-			this.folder = null;
-		},
-		openContextMenu() {
-			this.$root.new(XMenu, {
-				items: [{
-					type: 'item',
-					text: this.$t('contextmenu.upload'),
-					icon: 'upload',
-					action: this.$refs.browser.selectLocalFile
-				}, {
-					type: 'item',
-					text: this.$t('contextmenu.url-upload'),
-					icon: faCloudUploadAlt,
-					action: this.$refs.browser.urlUpload
-				}, {
-					type: 'item',
-					text: this.$t('contextmenu.create-folder'),
-					icon: ['far', 'folder'],
-					action: this.$refs.browser.createFolder
-				}, ...(this.folder ? [{
-					type: 'item',
-					text: this.$t('contextmenu.rename-folder'),
-					icon: 'i-cursor',
-					action: this.$refs.browser.renameFolder
-				}, {
-					type: 'item',
-					text: this.$t('contextmenu.move-folder'),
-					icon: ['far', 'folder-open'],
-					action: this.$refs.browser.moveFolder
-				}, {
-					type: 'item',
-					text: this.$t('contextmenu.delete-folder'),
-					icon: faTrashAlt,
-					action: this.$refs.browser.deleteFolder
-				}] : [])],
-				source: this.$refs.contextSource,
-			});
-		}
-	}
-});
-</script>
-
diff --git a/src/client/app/mobile/views/pages/games/reversi.vue b/src/client/app/mobile/views/pages/games/reversi.vue
deleted file mode 100644
index 69b7bdffb4386cf134ea27ae7d90f7249b444bda..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/games/reversi.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<template>
-<mk-ui>
-	<template #header><span style="margin-right:4px;"><fa icon="gamepad"/></span>{{ $t('reversi') }}</template>
-	<x-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/pages/games/reversi.vue'),
-	components: {
-		XReversi: () => import('../../../../common/views/components/games/reversi/reversi.vue').then(m => m.default)
-	},
-	mounted() {
-		document.title = `${this.$root.instanceName} | ${this.$t('reversi')}`;
-	},
-	methods: {
-		nav(game, actualNav) {
-			if (actualNav) {
-				this.$router.push(`/games/reversi/${game.id}`);
-			} else {
-				// TODO: https://github.com/vuejs/vue-router/issues/703
-				this.$router.push(`/games/reversi/${game.id}`);
-			}
-		}
-	}
-});
-</script>
diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue
deleted file mode 100644
index f115458092ce9243ca765c74a382497c94b6716a..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/home.timeline.vue
+++ /dev/null
@@ -1,143 +0,0 @@
-<template>
-<div>
-	<ui-container v-if="src == 'home' && alone" :show-header="false" style="margin-bottom:8px;">
-		<div class="zrzngnxs">
-			<p>{{ $t('@.empty-timeline-info.follow-users-to-make-your-timeline') }}</p>
-			<router-link to="/explore">{{ $t('@.empty-timeline-info.explore') }}</router-link>
-		</div>
-	</ui-container>
-
-	<mk-notes ref="timeline" :pagination="pagination" @loaded="() => $emit('loaded')"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/pages/home.timeline.vue'),
-
-	props: {
-		src: {
-			type: String,
-			required: true
-		},
-		tagTl: {
-			required: false
-		}
-	},
-
-	data() {
-		return {
-			streamManager: null,
-			connection: null,
-			unreadCount: 0,
-			date: null,
-			baseQuery: {
-				includeMyRenotes: this.$store.state.settings.showMyRenotes,
-				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
-				includeLocalRenotes: this.$store.state.settings.showLocalRenotes
-			},
-			query: {},
-			endpoint: null,
-			pagination: null
-		};
-	},
-
-	computed: {
-		alone(): boolean {
-			return this.$store.state.i.followingCount == 0;
-		}
-	},
-
-	created() {
-		this.$root.$on('warp', this.warp);
-		this.$once('hook:beforeDestroy', () => {
-			this.$root.$off('warp', this.warp);
-			this.connection.dispose();
-		});
-
-		const prepend = note => {
-			(this.$refs.timeline as any).prepend(note);
-		};
-
-		if (this.src == 'tag') {
-			this.endpoint = 'notes/search-by-tag';
-			this.query = {
-				query: this.tagTl.query
-			};
-			this.connection = this.$root.stream.connectToChannel('hashtag', { q: this.tagTl.query });
-			this.connection.on('note', prepend);
-		} else if (this.src == 'home') {
-			this.endpoint = 'notes/timeline';
-			const onChangeFollowing = () => {
-				this.fetch();
-			};
-			this.connection = this.$root.stream.useSharedConnection('homeTimeline');
-			this.connection.on('note', prepend);
-			this.connection.on('follow', onChangeFollowing);
-			this.connection.on('unfollow', onChangeFollowing);
-		} else if (this.src == 'local') {
-			this.endpoint = 'notes/local-timeline';
-			this.connection = this.$root.stream.useSharedConnection('localTimeline');
-			this.connection.on('note', prepend);
-		} else if (this.src == 'hybrid') {
-			this.endpoint = 'notes/hybrid-timeline';
-			this.connection = this.$root.stream.useSharedConnection('hybridTimeline');
-			this.connection.on('note', prepend);
-		} else if (this.src == 'global') {
-			this.endpoint = 'notes/global-timeline';
-			this.connection = this.$root.stream.useSharedConnection('globalTimeline');
-			this.connection.on('note', prepend);
-		} else if (this.src == 'mentions') {
-			this.endpoint = 'notes/mentions';
-			this.connection = this.$root.stream.useSharedConnection('main');
-			this.connection.on('mention', prepend);
-		} else if (this.src == 'messages') {
-			this.endpoint = 'notes/mentions';
-			this.query = {
-				visibility: 'specified'
-			};
-			const onNote = note => {
-				if (note.visibility == 'specified') {
-					prepend(note);
-				}
-			};
-			this.connection = this.$root.stream.useSharedConnection('main');
-			this.connection.on('mention', onNote);
-		}
-
-		this.pagination = {
-			endpoint: this.endpoint,
-			limit: 10,
-			params: init => ({
-				untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
-				...this.baseQuery, ...this.query
-			})
-		};
-	},
-
-	methods: {
-		focus() {
-			(this.$refs.timeline as any).focus();
-		},
-
-		warp(date) {
-			this.date = date;
-			(this.$refs.timeline as any).reload();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.zrzngnxs
-	padding 16px
-	text-align center
-	font-size 14px
-
-	> p
-		margin 0 0 8px 0
-
-</style>
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
deleted file mode 100644
index 0d110bf2ee4586387cfa89da5c66aa42400024cd..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/home.vue
+++ /dev/null
@@ -1,249 +0,0 @@
-<template>
-<mk-ui>
-	<template #header>
-		<span @click="showNav = true">
-			<span :class="$style.title">
-				<span v-if="src == 'home'"><fa icon="home"/>{{ $t('home') }}</span>
-				<span v-if="src == 'local'"><fa :icon="['far', 'comments']"/>{{ $t('local') }}</span>
-				<span v-if="src == 'hybrid'"><fa icon="share-alt"/>{{ $t('hybrid') }}</span>
-				<span v-if="src == 'global'"><fa icon="globe"/>{{ $t('global') }}</span>
-				<span v-if="src == 'mentions'"><fa icon="at"/>{{ $t('mentions') }}</span>
-				<span v-if="src == 'messages'"><fa :icon="['far', 'envelope']"/>{{ $t('messages') }}</span>
-				<span v-if="src == 'list'"><fa icon="list"/>{{ list.name }}</span>
-				<span v-if="src == 'tag'"><fa icon="hashtag"/>{{ tagTl.title }}</span>
-			</span>
-			<span style="margin-left:8px">
-				<template v-if="!showNav"><fa icon="angle-down"/></template>
-				<template v-else><fa icon="angle-up"/></template>
-			</span>
-			<i :class="$style.badge" v-if="$store.state.i.hasUnreadMentions || $store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i>
-		</span>
-	</template>
-
-	<template #func>
-		<button @click="fn"><fa icon="pencil-alt"/></button>
-	</template>
-
-	<main>
-		<div class="nav" v-if="showNav">
-			<div class="bg" @click="showNav = false"></div>
-			<div class="pointer"></div>
-			<div class="body">
-				<div>
-					<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span>
-					<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span>
-					<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span>
-					<span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span>
-					<div class="hr"></div>
-					<span :data-active="src == 'mentions'" @click="src = 'mentions'"><fa icon="at"/> {{ $t('mentions') }}<i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></span>
-					<span :data-active="src == 'messages'" @click="src = 'messages'"><fa :icon="['far', 'envelope']"/> {{ $t('messages') }}<i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></span>
-					<template v-if="lists">
-						<div class="hr" v-if="lists.length > 0"></div>
-						<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id"><fa icon="list"/> {{ l.name }}</span>
-					</template>
-					<div class="hr" v-if="$store.state.settings.tagTimelines && $store.state.settings.tagTimelines.length > 0"></div>
-					<span v-for="tl in $store.state.settings.tagTimelines" :data-active="src == 'tag' && tagTl == tl" @click="src = 'tag'; tagTl = tl" :key="tl.id"><fa icon="hashtag"/> {{ tl.title }}</span>
-				</div>
-			</div>
-		</div>
-
-		<div class="tl">
-			<x-tl v-if="src == 'home'" ref="tl" key="home" src="home"/>
-			<x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/>
-			<x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
-			<x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/>
-			<x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
-			<x-tl v-if="src == 'messages'" ref="tl" key="messages" src="messages"/>
-			<x-tl v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/>
-			<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
-		</div>
-	</main>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import Progress from '../../../common/scripts/loading';
-import XTl from './home.timeline.vue';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/pages/home.vue'),
-
-	components: {
-		XTl
-	},
-
-	data() {
-		return {
-			src: 'home',
-			list: null,
-			lists: null,
-			tagTl: null,
-			showNav: false,
-			enableLocalTimeline: false,
-			enableGlobalTimeline: false,
-		};
-	},
-
-	watch: {
-		src() {
-			this.showNav = false;
-			this.saveSrc();
-		},
-
-		list(x) {
-			this.showNav = false;
-			this.saveSrc();
-			if (x != null) this.tagTl = null;
-		},
-
-		tagTl(x) {
-			this.showNav = false;
-			this.saveSrc();
-			if (x != null) this.list = null;
-		},
-
-		showNav(v) {
-			if (v && this.lists === null) {
-				this.$root.api('users/lists/list').then(lists => {
-					this.lists = lists;
-				});
-			}
-		}
-	},
-
-	created() {
-		this.$root.getMeta().then((meta: Record<string, any>) => {
-			if (!(
-				this.enableGlobalTimeline = !meta.disableGlobalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin
-			) && this.src === 'global') this.src = 'local';
-			if (!(
-				this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin
-			) && ['local', 'hybrid'].includes(this.src)) this.src = 'home';
-		});
-
-		if (this.$store.state.device.tl) {
-			this.src = this.$store.state.device.tl.src;
-			if (this.src == 'list') {
-				this.list = this.$store.state.device.tl.arg;
-			} else if (this.src == 'tag') {
-				this.tagTl = this.$store.state.device.tl.arg;
-			}
-		}
-	},
-
-	mounted() {
-		document.title = this.$root.instanceName;
-
-		Progress.start();
-
-		(this.$refs.tl as any).$once('loaded', () => {
-			Progress.done();
-		});
-	},
-
-	methods: {
-		fn() {
-			this.$post();
-		},
-
-		saveSrc() {
-			this.$store.commit('device/setTl', {
-				src: this.src,
-				arg: this.src == 'list' ? this.list : this.tagTl
-			});
-		},
-
-		warp() {
-
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-main
-	> .nav
-		> .pointer
-			position fixed
-			z-index 10002
-			top 56px
-			left 0
-			right 0
-
-			$size = 16px
-
-			&:after
-				content ""
-				display block
-				position absolute
-				top -($size * 2)
-				left s('calc(50% - %s)', $size)
-				border-top solid $size transparent
-				border-left solid $size transparent
-				border-right solid $size transparent
-				border-bottom solid $size var(--popupBg)
-
-		> .bg
-			position fixed
-			z-index 10000
-			top 0
-			left 0
-			width 100%
-			height 100%
-			background rgba(#000, 0.5)
-
-		> .body
-			position fixed
-			z-index 10001
-			top 56px
-			left 0
-			right 0
-			width 300px
-			max-height calc(100% - 70px)
-			margin 0 auto
-			overflow auto
-			-webkit-overflow-scrolling touch
-			background var(--popupBg)
-			border-radius 8px
-			box-shadow 0 0 16px rgba(#000, 0.1)
-
-			> div
-				padding 8px 0
-
-				> .hr
-					margin 8px 0
-					border-top solid 1px var(--faceDivider)
-
-				> *:not(.hr)
-					display block
-					padding 8px 16px
-					color var(--text)
-
-					&[data-active]
-						color var(--primaryForeground)
-						background var(--primary)
-
-					&:not([data-active]):hover
-						background var(--mobileHomeTlItemHover)
-
-					> .badge
-						margin-left 6px
-						font-size 10px
-						color var(--notificationIndicator)
-
-</style>
-
-<style lang="stylus" module>
-.title
-	[data-icon]
-		margin-right 4px
-
-.badge
-	margin-left 6px
-	font-size 10px
-	color var(--notificationIndicator)
-	vertical-align middle
-
-</style>
diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue
deleted file mode 100644
index 7872847127db6f72ce948c647e9514d63ccb0352..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/messaging-room.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-<template>
-<mk-ui>
-	<template #header>
-		<template v-if="user"><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span><mk-user-name :user="user"/></template>
-		<template v-else-if="group"><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span>{{ group.name }}</template>
-		<template v-else><mk-ellipsis/></template>
-	</template>
-	<x-messaging-room v-if="!fetching" :user="user" :group="group" :is-naked="true"/>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import parseAcct from '../../../../../misc/acct/parse';
-
-export default Vue.extend({
-	i18n: i18n(),
-	components: {
-		XMessagingRoom: () => import('../../../common/views/components/messaging-room.vue').then(m => m.default)
-	},
-	data() {
-		return {
-			fetching: true,
-			user: null,
-			group: null,
-			unwatchDarkmode: null
-		};
-	},
-	watch: {
-		$route: 'fetch'
-	},
-	created() {
-		const applyBg = v =>
-			document.documentElement.style.setProperty('background', v ? '#191b22' : '#fff', 'important');
-
-		applyBg(this.$store.state.device.darkmode);
-
-		this.unwatchDarkmode = this.$store.watch(s => {
-			return s.device.darkmode;
-		}, applyBg);
-
-		this.fetch();
-	},
-	beforeDestroy() {
-		document.documentElement.style.removeProperty('background');
-		document.documentElement.style.removeProperty('background-color'); // for safari's bug
-		this.unwatchDarkmode();
-	},
-	methods: {
-		fetch() {
-			this.fetching = true;
-			if (this.$route.params.user) {
-				this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
-					this.user = user;
-					this.fetching = false;
-
-					document.title = `${this.$t('@.messaging')}: ${Vue.filter('userName')(this.user)} | ${this.$root.instanceName}`;
-				});
-			} else {
-				this.$root.api('users/groups/show', { groupId: this.$route.params.group }).then(group => {
-					this.group = group;
-					this.fetching = false;
-
-					document.title = this.$t('@.messaging') + ': ' + this.group.name;
-				});
-			}
-		}
-	}
-});
-</script>
diff --git a/src/client/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue
deleted file mode 100644
index ff66ae06e6489a51f453a50198f62a3fbb397a6d..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/messaging.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-<template>
-<mk-ui>
-	<template #header><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span>{{ $t('@.messaging') }}</template>
-	<x-messaging @navigate="navigate" @navigateGroup="navigateGroup" :header-top="48"/>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import getAcct from '../../../../../misc/acct/render';
-
-export default Vue.extend({
-	i18n: i18n(),
-	components: {
-		XMessaging: () => import('../../../common/views/components/messaging.vue').then(m => m.default)
-	},
-	mounted() {
-		document.title = `${this.$root.instanceName} ${this.$t('@.messaging')}`;
-	},
-	methods: {
-		navigate(user) {
-			(this as any).$router.push(`/i/messaging/${getAcct(user)}`);
-		},
-		navigateGroup(group) {
-			(this as any).$router.push(`/i/messaging/group/${group.id}`);
-		}
-	}
-});
-</script>
diff --git a/src/client/app/mobile/views/pages/note.vue b/src/client/app/mobile/views/pages/note.vue
deleted file mode 100644
index 090851fc4e033646b36b83065fb118224c969681..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/note.vue
+++ /dev/null
@@ -1,67 +0,0 @@
-<template>
-<mk-ui>
-	<template #header><span style="margin-right:4px;"><fa :icon="['far', 'sticky-note']"/></span>{{ $t('title') }}</template>
-	<main v-if="!fetching">
-		<div>
-			<mk-note-detail :note="note" :key="note.id"/>
-		</div>
-		<footer>
-			<router-link v-if="note.prev" :to="note.prev"><fa icon="angle-left"/> {{ $t('prev') }}</router-link>
-			<router-link v-if="note.next" :to="note.next">{{ $t('next') }} <fa icon="angle-right"/></router-link>
-		</footer>
-	</main>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import Progress from '../../../common/scripts/loading';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/pages/note.vue'),
-	data() {
-		return {
-			fetching: true,
-			note: null
-		};
-	},
-	watch: {
-		$route: 'fetch'
-	},
-	created() {
-		this.fetch();
-	},
-	mounted() {
-		document.title = this.$root.instanceName;
-	},
-	methods: {
-		fetch() {
-			Progress.start();
-			this.fetching = true;
-
-			this.$root.api('notes/show', {
-				noteId: this.$route.params.note
-			}).then(note => {
-				this.note = note;
-				this.fetching = false;
-
-				Progress.done();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-main
-	text-align center
-
-	> footer
-		margin-top 16px
-
-		> a
-			display inline-block
-			margin 0 16px
-
-</style>
diff --git a/src/client/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue
deleted file mode 100644
index 24f8f79ccc9085cd5a090259e738e9de1bf3e529..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/notifications.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-<template>
-<mk-ui>
-	<template #header><fa :icon="faBell"/> {{ $t('notifications') }}</template>
-	<template #func>
-		<button @click="filter()"><fa icon="cog"/></button>
-	</template>
-
-	<main>
-		<mk-notifications @before-init="beforeInit()" @inited="inited()" :type="type === 'all' ? null : type" :wide="true" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"/>
-	</main>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { faBell } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../../../i18n';
-import Progress from '../../../common/scripts/loading';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/pages/notifications.vue'),
-	data() {
-		return {
-			type: 'all',
-			faBell,
-		};
-	},
-	mounted() {
-		document.title = this.$root.instanceName;
-	},
-	methods: {
-		beforeInit() {
-			Progress.start();
-		},
-		inited() {
-			Progress.done();
-		},
-		filter() {
-			this.$root.dialog({
-				title: this.$t('@.notification-type'),
-				type: null,
-				select: {
-					items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({
-						value: x, text: this.$t('@.notification-types.' + x)
-					}))
-					default: this.type,
-				},
-				showCancelButton: true
-			}).then(({ canceled, result: type }) => {
-				if (canceled) return;
-				this.type = type;
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-main > *
-	overflow hidden
-	background var(--face)
-
-	&.round
-		border-radius 8px
-
-	&.shadow
-		box-shadow 0 4px 16px rgba(#000, 0.1)
-
-		@media (min-width 500px)
-			box-shadow 0 8px 32px rgba(#000, 0.1)
-</style>
diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue
deleted file mode 100644
index dca1ffd40a9a1bd7d995aa63230dac8fc3b2d295..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/search.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<template>
-<mk-ui>
-	<template #header><fa icon="search"/> {{ q }}</template>
-
-	<main>
-		<mk-notes ref="timeline" :pagination="pagination" @inited="inited"/>
-	</main>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import Progress from '../../../common/scripts/loading';
-import { genSearchQuery } from '../../../common/scripts/gen-search-query';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/pages/search.vue'),
-	data() {
-		return {
-			pagination: {
-				endpoint: 'notes/search',
-				limit: 20,
-				params: () => genSearchQuery(this, this.q)
-			}
-		};
-	},
-	computed: {
-		q(): string {
-			return this.$route.query.q;
-		}
-	},
-	watch: {
-		$route() {
-			this.$refs.timeline.reload();
-		}
-	},
-	mounted() {
-		document.title = `${this.$t('search')}: ${this.q} | ${this.$root.instanceName}`;
-	},
-	methods: {
-		inited() {
-			Progress.done();
-		},
-	}
-});
-</script>
diff --git a/src/client/app/mobile/views/pages/selectdrive.vue b/src/client/app/mobile/views/pages/selectdrive.vue
deleted file mode 100644
index 095c19cf2c4e98b20b048fd7ba362fbad694fd69..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/selectdrive.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<template>
-<div class="mk-selectdrive">
-	<header>
-		<h1>{{ $t('select-file') }}<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1>
-		<button class="upload" @click="upload"><fa icon="upload"/></button>
-		<button v-if="multiple" class="ok" @click="ok"><fa icon="check"/></button>
-	</header>
-	<x-drive ref="browser" select-file :multiple="multiple" is-naked :top="$store.state.uiHeaderHeight"/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/pages/selectdrive.vue'),
-	components: {
-		XDrive: () => import('../components/drive.vue').then(m => m.default),
-	},
-	data() {
-		return {
-			files: []
-		};
-	},
-	computed: {
-		multiple(): boolean {
-			const q = (new URL(location.toString())).searchParams;
-			return q.get('multiple') == 'true';
-		}
-	},
-	mounted() {
-		document.title = this.$t('title');
-	},
-	methods: {
-		onSelected(file) {
-			this.files = [file];
-			this.ok();
-		},
-		onChangeSelection(files) {
-			this.files = files;
-		},
-		upload() {
-			(this.$refs.browser as any).selectLocalFile();
-		},
-		close() {
-			window.close();
-		},
-		ok() {
-			window.opener.cb(this.multiple ? this.files : this.files[0]);
-			this.close();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-selectdrive
-	width 100%
-	height 100%
-	background #fff
-
-	> header
-		position fixed
-		top 0
-		left 0
-		width 100%
-		z-index 1000
-		background #fff
-		box-shadow 0 1px rgba(#000, 0.1)
-
-		> h1
-			margin 0
-			padding 0
-			text-align center
-			line-height 42px
-			font-size 1em
-			font-weight normal
-
-			> .count
-				margin-left 4px
-				opacity 0.5
-
-		> .upload
-			position absolute
-			top 0
-			left 0
-			line-height 42px
-			width 42px
-
-		> .ok
-			position absolute
-			top 0
-			right 0
-			line-height 42px
-			width 42px
-
-	> .mk-drive
-		top 42px
-
-</style>
diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
deleted file mode 100644
index c24a56be7b5be16cbd900147c3ae779194b7716e..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/settings.vue
+++ /dev/null
@@ -1,86 +0,0 @@
-<template>
-<mk-ui>
-	<template #header><span style="margin-right:4px;"><fa icon="cog"/></span>{{ $t('@.settings') }}</template>
-	<main>
-		<div class="signed-in-as" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
-			<mfm :text="$t('signed-in-as').replace('{}', name)" :plain="true" :custom-emojis="$store.state.i.emojis"/>
-		</div>
-
-		<x-settings/>
-
-		<div class="signout" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }" @click="signout">{{ $t('@.signout') }}</div>
-
-		<footer>
-			<small>ver {{ version }} ({{ codename }})</small>
-		</footer>
-	</main>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import XSettings from '../../../common/views/components/settings/settings.vue';
-import { version, codename } from '../../../config';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/pages/settings.vue'),
-	components: {
-		XSettings,
-	},
-	data() {
-		return {
-			version,
-			codename,
-		};
-	},
-	computed: {
-		name(): string {
-			return Vue.filter('userName')(this.$store.state.i);
-		},
-	},
-	methods: {
-		signout() {
-			this.$root.signout();
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-main
-
-	> .signed-in-as
-		margin 16px
-		padding 16px
-		text-align center
-		color var(--mobileSignedInAsFg)
-		background var(--mobileSignedInAsBg)
-		font-weight bold
-
-		&.round
-			border-radius 6px
-
-		&.shadow
-			box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12)
-
-	> .signout
-		margin 16px
-		padding 16px
-		text-align center
-		color var(--mobileSignedInAsFg)
-		background var(--mobileSignedInAsBg)
-
-		&.round
-			border-radius 6px
-
-		&.shadow
-			box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12)
-
-	> footer
-		margin 16px
-		text-align center
-		color var(--text)
-		opacity 0.7
-
-</style>
diff --git a/src/client/app/mobile/views/pages/signup.vue b/src/client/app/mobile/views/pages/signup.vue
deleted file mode 100644
index 81d2741ae565ef2fdc8f7039638641fccc033279..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/signup.vue
+++ /dev/null
@@ -1,29 +0,0 @@
-<template>
-<div class="signup">
-	<h1>{{ $t('lets-start') }}</h1>
-	<mk-signup/>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-export default Vue.extend({
-	i18n: i18n('mobile/views/pages/signup.vue')
-});
-</script>
-
-<style lang="stylus" scoped>
-.signup
-	padding 32px
-	margin 0 auto
-	max-width 500px
-
-	h1
-		margin 0
-		padding 8px 0 0 0
-		font-size 1.5em
-		font-weight bold
-		color var(--text)
-
-</style>
diff --git a/src/client/app/mobile/views/pages/tag.vue b/src/client/app/mobile/views/pages/tag.vue
deleted file mode 100644
index 19482ec382175f3a99049eab768738ab2d9a3a60..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/tag.vue
+++ /dev/null
@@ -1,40 +0,0 @@
-<template>
-<mk-ui>
-	<template #header><span style="margin-right:4px;"><fa icon="hashtag"/></span>{{ $route.params.tag }}</template>
-
-	<main>
-		<mk-notes ref="timeline" :pagination="pagination" @inited="inited"/>
-	</main>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import Progress from '../../../common/scripts/loading';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/pages/tag.vue'),
-	data() {
-		return {
-			pagination: {
-				endpoint: 'notes/search-by-tag',
-				limit: 20,
-				params: {
-					tag: this.$route.params.tag
-				}
-			}
-		};
-	},
-	watch: {
-		$route() {
-			this.$refs.timeline.reload();
-		}
-	},
-	methods: {
-		inited() {
-			Progress.done();
-		},
-	}
-});
-</script>
diff --git a/src/client/app/mobile/views/pages/ui.vue b/src/client/app/mobile/views/pages/ui.vue
deleted file mode 100644
index 397ba5df07110ee3d6e19227802da2f23183ca05..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/ui.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-<template>
-<mk-ui>
-	<template #header><span style="margin-right:4px;" v-if="icon"><fa :icon="icon"/></span>{{ title }}</template>
-
-	<main>
-		<component :is="component" @init="init" v-bind="$attrs"/>
-	</main>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: {
-		component: {
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			title: null,
-			icon: null,
-		};
-	},
-
-	mounted() {
-	},
-
-	methods: {
-		init(v) {
-			this.title = v.title;
-			this.icon = v.icon;
-		}
-	}
-});
-</script>
diff --git a/src/client/app/mobile/views/pages/user/home.notes.vue b/src/client/app/mobile/views/pages/user/home.notes.vue
deleted file mode 100644
index 9abe5b893c5b1034f78f9246dd6b083d89983f7b..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/user/home.notes.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-<template>
-<div class="root notes">
-	<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
-	<div v-if="!fetching && notes.length > 0">
-		<mk-note-card v-for="note in notes" :key="note.id" :note="note"/>
-	</div>
-	<p class="empty" v-if="!fetching && notes.length == 0">{{ $t('@.no-notes') }}</p>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-export default Vue.extend({
-	i18n: i18n('mobile/views/pages/user/home.notes.vue'),
-	props: ['user'],
-	data() {
-		return {
-			fetching: true,
-			notes: []
-		};
-	},
-	mounted() {
-		this.$root.api('users/notes', {
-			userId: this.user.id,
-		}).then(notes => {
-			this.notes = notes;
-			this.fetching = false;
-		});
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.root.notes
-
-	> div
-		overflow-x scroll
-		-webkit-overflow-scrolling touch
-		white-space nowrap
-		padding 8px
-
-		> *
-			vertical-align top
-
-			&:not(:last-child)
-				margin-right 8px
-
-	> .fetching
-	> .empty
-		margin 0
-		padding 16px
-		text-align center
-		color var(--text)
-
-		> i
-			margin-right 4px
-
-</style>
diff --git a/src/client/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue
deleted file mode 100644
index 316b2a12fe1afa854d5ff4ca55ac44030bd2e429..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/user/home.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-<template>
-<div class="wojmldye">
-	<x-page class="page" v-if="user.pinnedPage" :page="user.pinnedPage" :key="user.pinnedPage.id" :show-title="!user.pinnedPage.hideTitleWhenPinned"/>
-	<mk-note-detail class="note" v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
-	<ui-container :body-togglable="true">
-		<template #header><fa :icon="['far', 'comments']"/>{{ $t('recent-notes') }}</template>
-		<div>
-			<x-notes :user="user"/>
-		</div>
-	</ui-container>
-	<ui-container :body-togglable="true">
-		<template #header><fa icon="image"/>{{ $t('images') }}</template>
-		<div>
-			<x-photos :user="user"/>
-		</div>
-	</ui-container>
-	<ui-container :body-togglable="true">
-		<template #header><fa icon="chart-bar"/>{{ $t('activity') }}</template>
-		<div style="padding:8px;">
-			<x-activity :user="user"/>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import XNotes from './home.notes.vue';
-import XPhotos from './home.photos.vue';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/pages/user/home.vue'),
-	components: {
-		XNotes,
-		XPhotos,
-		XPage: () => import('../../../../common/views/components/page/page.vue').then(m => m.default),
-		XActivity: () => import('../../../../common/views/components/activity.vue').then(m => m.default)
-	},
-	props: ['user'],
-	data() {
-		return {
-			makeFrequentlyRepliedUsersPromise: () => this.$root.api('users/get_frequently_replied_users', {
-				userId: this.user.id
-			}).then(res => res.map(x => x.user)),
-			makeFollowersYouKnowPromise: () => this.$root.api('users/followers', {
-				userId: this.user.id,
-				iknow: true,
-				limit: 30
-			}).then(res => res.users),
-		};
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.wojmldye
-	> .page
-		margin 0 0 8px 0
-
-		@media (min-width 500px)
-			margin 0 0 16px 0
-	
-	> .note
-		margin 0 0 8px 0
-
-		@media (min-width 500px)
-			margin 0 0 16px 0
-
-</style>
diff --git a/src/client/app/mobile/views/pages/user/index.vue b/src/client/app/mobile/views/pages/user/index.vue
deleted file mode 100644
index b8a79a6b3488a727e83fe973d6bb6f80846d15fa..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/user/index.vue
+++ /dev/null
@@ -1,349 +0,0 @@
-<template>
-<mk-ui>
-	<template #header v-if="!fetching">
-		<img :src="avator" alt=""><mk-user-name :user="user" :key="user.id"/>
-	</template>
-	<div class="wwtwuxyh" v-if="!fetching">
-		<div class="is-suspended" v-if="user.isSuspended"><p><fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }}</p></div>
-		<div class="is-remote" v-if="user.host != null"><p><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url" rel="nofollow noopener" target="_blank">{{ $t('@.view-on-remote') }}</a></p></div>
-		<header>
-			<div class="banner" :style="style"></div>
-			<div class="body">
-				<div class="top">
-					<a class="avatar">
-						<img :src="avator" alt="avatar"/>
-					</a>
-					<button class="menu" ref="menu" @click="menu"><fa icon="ellipsis-h"/></button>
-					<mk-follow-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
-				</div>
-				<div class="title">
-					<h1><mk-user-name :user="user" :key="user.id" :nowrap="false"/></h1>
-					<span class="username"><mk-acct :user="user" :detail="true" :key="user.id"/></span>
-					<span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span>
-				</div>
-				<div class="description">
-					<mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :key="user.id"/>
-					<x-integrations :user="user" style="margin:20px 0;"/>
-				</div>
-				<div class="fields" v-if="user.fields" :key="user.id">
-					<dl class="field" v-for="(field, i) in user.fields" :key="i">
-						<dt class="name">
-							<mfm :text="field.name" :plain="true" :custom-emojis="user.emojis"/>
-						</dt>
-						<dd class="value">
-							<mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
-						</dd>
-					</dl>
-				</div>
-				<div class="info">
-					<p class="location" v-if="user.host === null && user.location">
-						<fa icon="map-marker"/>{{ user.location }}
-					</p>
-					<p class="birthday" v-if="user.host === null && user.birthday">
-						<fa icon="birthday-cake"/>{{ user.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ $t('years-old', { age }) }})
-					</p>
-				</div>
-				<div class="status">
-					<router-link :to="user | userPage()">
-						<b>{{ user.notesCount | number }}</b>
-						<i>{{ $t('notes') }}</i>
-					</router-link>
-					<router-link :to="user | userPage('following')">
-						<b>{{ user.followingCount | number }}</b>
-						<i>{{ $t('following') }}</i>
-					</router-link>
-					<router-link :to="user | userPage('followers')">
-						<b>{{ user.followersCount | number }}</b>
-						<i>{{ $t('followers') }}</i>
-					</router-link>
-				</div>
-			</div>
-		</header>
-		<nav v-if="$route.name == 'user'" :class="{ shadow: $store.state.device.useShadow }">
-			<div class="nav-container">
-				<a :data-active="page == 'home'" @click="page = 'home'"><fa icon="home"/> {{ $t('overview') }}</a>
-				<a :data-active="page == 'notes'" @click="page = 'notes'"><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</a>
-				<a :data-active="page == 'media'" @click="page = 'media'"><fa icon="image"/> {{ $t('media') }}</a>
-			</div>
-		</nav>
-		<main>
-			<template v-if="$route.name == 'user'">
-				<x-home v-if="page == 'home'" :user="user" :key="user.id"/>
-				<mk-user-timeline v-if="page == 'notes'" :user="user" :key="`tl:${user.id}`"/>
-				<mk-user-timeline v-if="page == 'media'" :user="user" :with-media="true" :key="`media:${user.id}`"/>
-			</template>
-			<router-view :user="user"></router-view>
-		</main>
-	</div>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../../i18n';
-import * as age from 's-age';
-import parseAcct from '../../../../../../misc/acct/parse';
-import Progress from '../../../../common/scripts/loading';
-import XUserMenu from '../../../../common/views/components/user-menu.vue';
-import XHome from './home.vue';
-import { getStaticImageUrl } from '../../../../common/scripts/get-static-image-url';
-import XIntegrations from '../../../../common/views/components/integrations.vue';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/pages/user.vue'),
-	components: {
-		XHome,
-		XIntegrations
-	},
-	data() {
-		return {
-			fetching: true,
-			user: null,
-			page: this.$route.name == 'user' ? 'home' : null
-		};
-	},
-	computed: {
-		age(): number {
-			return age(this.user.birthday);
-		},
-		avator(): string {
-			return this.$store.state.device.disableShowingAnimatedImages
-				? getStaticImageUrl(this.user.avatarUrl)
-				: this.user.avatarUrl;
-		},
-		style(): any {
-			if (this.user.bannerUrl == null) return {};
-			return {
-				backgroundColor: this.user.bannerColor,
-				backgroundImage: `url(${ this.user.bannerUrl })`
-			};
-		}
-	},
-	watch: {
-		$route: 'fetch'
-	},
-	created() {
-		this.fetch();
-	},
-	methods: {
-		fetch() {
-			Progress.start();
-
-			this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
-				this.user = user;
-				this.fetching = false;
-
-				Progress.done();
-				document.title = `${Vue.filter('userName')(this.user)} | ${this.$root.instanceName}`;
-			});
-		},
-
-		menu() {
-			this.$root.new(XUserMenu, {
-				source: this.$refs.menu,
-				user: this.user
-			});
-		},
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.wwtwuxyh
-	$bg = var(--face)
-
-	> .is-suspended
-	> .is-remote
-		&.is-suspended
-			color #570808
-			background #ffdbdb
-
-		&.is-remote
-			color #573c08
-			background #fff0db
-
-		> p
-			margin 0 auto
-			padding 14px
-			max-width 600px
-			font-size 14px
-
-			> a
-				font-weight bold
-
-			@media (max-width 500px)
-				padding 12px
-				font-size 12px
-
-	> header
-		background $bg
-
-		> .banner
-			padding-bottom 33.3%
-			background-color rgba(0, 0, 0, 0.1)
-			background-size cover
-			background-position center
-
-		> .body
-			padding 12px
-			margin 0 auto
-			max-width 600px
-
-			> .top
-				display flex
-
-				> .avatar
-					display block
-					width 25%
-					height 40px
-
-					> img
-						display block
-						position absolute
-						left -2px
-						bottom -2px
-						width 100%
-						background $bg
-						border 3px solid $bg
-						border-radius 6px
-
-						@media (min-width 500px)
-							left -4px
-							bottom -4px
-							border 4px solid $bg
-							border-radius 12px
-
-				> .menu
-					margin 0 0 0 auto
-					padding 8px
-					margin-right 8px
-					font-size 18px
-					color var(--text)
-
-			> .title
-				margin 8px 0
-
-				> h1
-					margin 0
-					line-height 22px
-					font-size 20px
-					color var(--mobileUserPageName)
-
-				> .username
-					display inline-block
-					line-height 20px
-					font-size 16px
-					font-weight bold
-					color var(--mobileUserPageAcct)
-
-				> .followed
-					margin-left 8px
-					padding 2px 4px
-					font-size 12px
-					color var(--mobileUserPageFollowedFg)
-					background var(--mobileUserPageFollowedBg)
-					border-radius 4px
-
-			> .description
-				margin 8px 0
-				color var(--mobileUserPageDescription)
-
-				@media (max-width 450px)
-					font-size 15px
-
-			> .fields
-				margin 8px 0
-
-				> .field
-					display flex
-					padding 0
-					margin 0
-					align-items center
-
-					> .name
-						padding 4px
-						margin 4px
-						width 30%
-						overflow hidden
-						white-space nowrap
-						text-overflow ellipsis
-						font-weight bold
-						color var(--mobileUserPageStatusHighlight)
-
-					> .value
-						padding 4px
-						margin 4px
-						width 70%
-						overflow hidden
-						white-space nowrap
-						text-overflow ellipsis
-						color var(--mobileUserPageStatusHighlight)
-
-			> .info
-				margin 8px 0
-
-				@media (max-width 450px)
-					font-size 15px
-
-				> p
-					display inline
-					margin 0 16px 0 0
-					color var(--text)
-
-					> i
-						margin-right 4px
-
-			> .status
-				> a
-					color var(--text)
-
-					&:not(:last-child)
-						margin-right 16px
-
-					> b
-						margin-right 4px
-						font-size 16px
-						color var(--mobileUserPageStatusHighlight)
-
-					> i
-						font-size 14px
-
-				> button
-					color var(--text)
-
-	> nav
-		position -webkit-sticky
-		position sticky
-		top 47px
-		background-color $bg
-		z-index 2
-
-		&.shadow
-			box-shadow 0 4px 4px var(--mobileUserPageHeaderShadow)
-
-		> .nav-container
-			display flex
-			justify-content center
-			margin 0 auto
-			max-width 616px
-
-			> a
-				display block
-				flex 1 1
-				text-align center
-				line-height 48px
-				font-size 12px
-				text-decoration none
-				color var(--text)
-				border-bottom solid 2px transparent
-
-				@media (min-width 400px)
-					line-height 52px
-					font-size 14px
-
-				&[data-active]
-					font-weight bold
-					color var(--primary)
-					border-color var(--primary)
-
-</style>
diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue
deleted file mode 100644
index 6cf4a36f90eb8c73eb8a58ed4f4e6be5819fd96f..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/welcome.vue
+++ /dev/null
@@ -1,310 +0,0 @@
-<template>
-<div class="wgwfgvvimdjvhjfwxropcwksnzftjqes">
-	<div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div>
-
-	<div>
-		<img svg-inline src="../../../../assets/title.svg" alt="Misskey">
-		<p class="host">{{ host }}</p>
-		<div class="about">
-			<h2>{{ name || 'Misskey' }}</h2>
-			<p v-html="description || this.$t('@.about')"></p>
-			<router-link class="signup" to="/signup">{{ $t('@.signup') }}</router-link>
-		</div>
-		<div class="signin">
-			<a href="/signin" @click.prevent="signin()">{{ $t('@.signin') }}</a>
-		</div>
-		<div class="tl">
-			<mk-welcome-timeline/>
-		</div>
-		<div class="hashtags">
-			<mk-tag-cloud/>
-		</div>
-		<div class="photos">
-			<div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div>
-		</div>
-		<div class="stats" v-if="stats">
-			<span><fa icon="user"/> {{ stats.originalUsersCount | number }}</span>
-			<span><fa icon="pencil-alt"/> {{ stats.originalNotesCount | number }}</span>
-		</div>
-		<div class="announcements" v-if="announcements && announcements.length > 0">
-			<article v-for="announcement in announcements">
-				<span class="title" v-html="announcement.title"></span>
-				<mfm :text="announcement.text"/>
-				<img v-if="announcement.image" :src="announcement.image" alt="" style="display: block; max-height: 120px; max-width: 100%;"/>
-			</article>
-		</div>
-		<article class="about-misskey">
-			<h1>{{ $t('@.intro.title') }}</h1>
-			<p v-html="this.$t('@.intro.about')"></p>
-			<section>
-				<h2>{{ $t('@.intro.features') }}</h2>
-				<section>
-					<h3>{{ $t('@.intro.rich-contents') }}</h3>
-					<div class="image"><img src="/assets/about/post.png" alt=""></div>
-					<p v-html="this.$t('@.intro.rich-contents-desc')"></p>
-				</section>
-				<section>
-					<h3>{{ $t('@.intro.reaction') }}</h3>
-					<div class="image"><img src="/assets/about/reaction.png" alt=""></div>
-					<p v-html="this.$t('@.intro.reaction-desc')"></p>
-				</section>
-				<section>
-					<h3>{{ $t('@.intro.ui') }}</h3>
-					<div class="image"><img src="/assets/about/ui.png" alt=""></div>
-					<p v-html="this.$t('@.intro.ui-desc')"></p>
-				</section>
-				<section>
-					<h3>{{ $t('@.intro.drive') }}</h3>
-					<div class="image"><img src="/assets/about/drive.png" alt=""></div>
-					<p v-html="this.$t('@.intro.drive-desc')"></p>
-				</section>
-			</section>
-			<p v-html="this.$t('@.intro.outro')"></p>
-		</article>
-		<div class="info" v-if="meta">
-			<p>Version: <b>{{ meta.version }}</b></p>
-			<p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p>
-		</div>
-		<footer>
-			<small>{{ copyright }}</small>
-		</footer>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { copyright, host } from '../../../config';
-import { concat } from '../../../../../prelude/array';
-import { toUnicode } from 'punycode';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/pages/welcome.vue'),
-	data() {
-		return {
-			meta: null,
-			copyright,
-			stats: null,
-			banner: null,
-			host: toUnicode(host),
-			name: null,
-			description: '',
-			photos: [],
-			announcements: []
-		};
-	},
-	created() {
-		this.$root.getMeta().then(meta => {
-			this.meta = meta;
-			this.name = meta.name;
-			this.description = meta.description;
-			this.announcements = meta.announcements;
-			this.banner = meta.bannerUrl;
-		});
-
-		this.$root.api('stats').then(stats => {
-			this.stats = stats;
-		});
-
-		const image = [
-			'image/jpeg',
-			'image/png',
-			'image/gif',
-			'image/apng',
-			'image/vnd.mozilla.apng',
-		];
-
-		this.$root.api('notes/local-timeline', {
-			fileType: image,
-			excludeNsfw: true,
-			limit: 6
-		}).then((notes: any[]) => {
-			const files = concat(notes.map((n: any): any[] => n.files));
-			this.photos = files.filter(f => image.includes(f.type)).slice(0, 6);
-		});
-	},
-	methods: {
-		signin() {
-			this.$root.dialog({
-				type: 'signin'
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.wgwfgvvimdjvhjfwxropcwksnzftjqes
-	text-align center
-
-	> .banner
-		position absolute
-		top 0
-		left 0
-		width 100%
-		height 300px
-		background-position center
-		background-size cover
-		opacity 0.7
-
-		&:after
-			content ""
-			display block
-			position absolute
-			bottom 0
-			left 0
-			width 100%
-			height 100px
-			background linear-gradient(transparent, var(--bg))
-
-	> div:not(.banner)
-		padding 32px
-		margin 0 auto
-		max-width 500px
-
-		> svg
-			display block
-			width 200px
-			height 50px
-			margin 0 auto
-
-		> .host
-			display block
-			text-align center
-			padding 6px 12px
-			line-height 32px
-			font-weight bold
-			color #333
-			background rgba(#000, 0.035)
-			border-radius 6px
-
-		> .about
-			margin-top 16px
-			padding 16px
-			color var(--text)
-			background var(--face)
-			border-radius 6px
-
-			> h2
-				margin 0
-
-			> p
-				margin 8px
-
-			> .signup
-				font-weight bold
-
-		> .signin
-			margin 16px 0
-
-		> .tl
-			margin 16px 0
-
-			> *
-				max-height 300px
-				border-radius 6px
-				overflow auto
-				-webkit-overflow-scrolling touch
-
-		> .hashtags
-			padding 0 8px
-			height 200px
-
-		> .photos
-			display grid
-			grid-template-rows 1fr 1fr 1fr
-			grid-template-columns 1fr 1fr
-			gap 8px
-			height 300px
-			margin-top 16px
-
-			> div
-				border-radius 4px
-				background-position center center
-				background-size cover
-
-		> .stats
-			margin 16px 0
-			padding 8px
-			font-size 14px
-			color var(--text)
-			background rgba(#000, 0.1)
-			border-radius 6px
-
-			> *
-				margin 0 8px
-
-		> .announcements
-			margin 16px 0
-
-			> article
-				background var(--mobileAnnouncement)
-				border-radius 6px
-				color var(--mobileAnnouncementFg)
-				padding 16px
-				margin 8px 0
-				font-size 12px
-
-				> .title
-					font-weight bold
-
-		> .about-misskey
-			margin 16px 0
-			padding 32px
-			font-size 14px
-			background var(--face)
-			border-radius 6px
-			overflow hidden
-			color var(--text)
-
-			> h1
-				margin 0
-
-				& + p
-					margin-top 8px
-
-			> p:last-child
-				margin-bottom 0
-
-			> section
-				> h2
-					border-bottom 1px solid var(--faceDivider)
-
-				> section
-					margin-bottom 16px
-					padding-bottom 16px
-					border-bottom 1px solid var(--faceDivider)
-
-					> h3
-						margin-bottom 8px
-
-					> p
-						margin-bottom 0
-
-					> .image
-						> img
-							display block
-							width 100%
-							height 120px
-							object-fit cover
-
-		> .info
-			padding 16px 0
-			border solid 2px rgba(0, 0, 0, 0.1)
-			border-radius 8px
-			color var(--text)
-
-			> *
-				margin 0 16px
-
-		> footer
-			text-align center
-			color var(--text)
-
-			> small
-				display block
-				margin 16px 0 0 0
-				opacity 0.7
-
-</style>
diff --git a/src/client/app/mobile/views/pages/widgets.vue b/src/client/app/mobile/views/pages/widgets.vue
deleted file mode 100644
index 19df613b3aa44a62de1a6b806e3cf341cd813a3c..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/pages/widgets.vue
+++ /dev/null
@@ -1,192 +0,0 @@
-<template>
-<mk-ui>
-	<template #header><span style="margin-right:4px;"><fa icon="home"/></span>{{ $t('dashboard') }}</template>
-	<template #func>
-		<button @click="customizing = !customizing"><fa icon="cog"/></button>
-	</template>
-	<main>
-		<template v-if="customizing">
-			<header>
-				<select v-model="widgetAdderSelected">
-					<option value="profile">{{ $t('@.widgets.profile') }}</option>
-					<option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option>
-					<option value="calendar">{{ $t('@.widgets.calendar') }}</option>
-					<option value="activity">{{ $t('@.widgets.activity') }}</option>
-					<option value="rss">{{ $t('@.widgets.rss') }}</option>
-					<option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option>
-					<option value="slideshow">{{ $t('@.widgets.slideshow') }}</option>
-					<option value="hashtags">{{ $t('@.widgets.hashtags') }}</option>
-					<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option>
-					<option value="version">{{ $t('@.widgets.version') }}</option>
-					<option value="server">{{ $t('@.widgets.server') }}</option>
-					<option value="queue">{{ $t('@.widgets.queue') }}</option>
-					<option value="memo">{{ $t('@.widgets.memo') }}</option>
-					<option value="nav">{{ $t('@.widgets.nav') }}</option>
-					<option value="tips">{{ $t('@.widgets.tips') }}</option>
-				</select>
-				<button @click="addWidget">{{ $t('add-widget') }}</button>
-				<p><a @click="hint">{{ $t('customization-tips') }}</a></p>
-			</header>
-			<x-draggable
-				:list="widgets"
-				handle=".handle"
-				animation="150"
-				@sort="onWidgetSort"
-			>
-				<div v-for="widget in widgets" class="customize-container" :key="widget.id">
-					<header>
-						<span class="handle"><fa icon="bars"/></span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)"><fa icon="times"/></button>
-					</header>
-					<div @click="widgetFunc(widget.id)">
-						<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="mobile"/>
-					</div>
-				</div>
-			</x-draggable>
-		</template>
-		<template v-else>
-			<component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="mobile"/>
-		</template>
-	</main>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import * as XDraggable from 'vuedraggable';
-import { v4 as uuid } from 'uuid';
-
-export default Vue.extend({
-	i18n: i18n('mobile/views/pages/widgets.vue'),
-	components: {
-		XDraggable
-	},
-
-	data() {
-		return {
-			showNav: false,
-			customizing: false,
-			widgetAdderSelected: null
-		};
-	},
-
-	computed: {
-		widgets(): any[] {
-			return this.$store.getters.mobileHome || [];
-		}
-	},
-
-	created() {
-		if (this.widgets.length == 0) {
-			this.$store.commit('setMobileHome', [{
-				name: 'calendar',
-				id: 'a', data: {}
-			}, {
-				name: 'activity',
-				id: 'b', data: {}
-			}, {
-				name: 'rss',
-				id: 'c', data: {}
-			}, {
-				name: 'photo-stream',
-				id: 'd', data: {}
-			}, {
-				name: 'nav',
-				id: 'f', data: {}
-			}, {
-				name: 'version',
-				id: 'g', data: {}
-			}]);
-		}
-	},
-
-	mounted() {
-		document.title = this.$root.instanceName;
-	},
-
-	methods: {
-		hint() {
-			this.$root.dialog({
-				type: 'info',
-				text: this.$t('widgets-hints')
-			});
-		},
-
-		widgetFunc(id) {
-			const w = this.$refs[id][0];
-			if (w.func) w.func();
-		},
-
-		onWidgetSort() {
-			this.saveHome();
-		},
-
-		addWidget() {
-			if(this.widgetAdderSelected == null) return;
-
-			this.$store.commit('addMobileHomeWidget', {
-				name: this.widgetAdderSelected,
-				id: uuid(),
-				data: {}
-			});
-		},
-
-		removeWidget(widget) {
-			this.$store.commit('removeMobileHomeWidget', widget);
-		},
-
-		saveHome() {
-			this.$store.commit('setMobileHome', this.widgets);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-main
-	margin 0 auto
-	padding 8px
-	max-width 500px
-	width 100%
-
-	@media (min-width 500px)
-		padding 16px 8px
-
-	@media (min-width 600px)
-		padding 32px 8px
-
-	> header
-		padding 8px
-		background #fff
-
-	.widget
-		margin-bottom 8px
-
-		@media (min-width 600px)
-			margin-bottom 16px
-
-	.customize-container
-		margin 8px
-		background #fff
-
-		> header
-			line-height 32px
-			background #eee
-
-			> .handle
-				padding 0 8px
-
-			> .remove
-				position absolute
-				top 0
-				right 0
-				padding 0 8px
-				line-height 32px
-
-		> div
-			padding 8px
-
-			> *
-				pointer-events none
-
-</style>
diff --git a/src/client/app/mobile/views/widgets/activity.vue b/src/client/app/mobile/views/widgets/activity.vue
deleted file mode 100644
index 047784deacd9f33128e0fbc51d56faf0ab22c7b0..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/widgets/activity.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-<template>
-<div class="mkw-activity">
-	<ui-container :show-header="!props.compact">
-		<template #header><fa icon="chart-bar"/>{{ $t('activity') }}</template>
-		<div :class="$style.body">
-			<x-activity :user="$store.state.i"/>
-		</div>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-
-export default define({
-	name: 'activity',
-	props: () => ({
-		compact: false
-	})
-}).extend({
-	i18n: i18n(),
-	components: {
-		XActivity: () => import('../../../common/views/components/activity.vue').then(m => m.default)
-	},
-	methods: {
-		func() {
-			this.props.compact = !this.props.compact;
-			this.save();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" module>
-.body
-	padding 8px
-</style>
diff --git a/src/client/app/mobile/views/widgets/index.ts b/src/client/app/mobile/views/widgets/index.ts
deleted file mode 100644
index 4de912b64c6d736f5e86125ea75e4a86ce0b0687..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/widgets/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import Vue from 'vue';
-
-import wActivity from './activity.vue';
-import wProfile from './profile.vue';
-
-Vue.component('mkw-activity', wActivity);
-Vue.component('mkw-profile', wProfile);
diff --git a/src/client/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue
deleted file mode 100644
index d4ccc87e572d296e15c810b5a3ed850cb3bc360a..0000000000000000000000000000000000000000
--- a/src/client/app/mobile/views/widgets/profile.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-<div class="mkw-profile">
-	<ui-container>
-		<div :class="$style.banner"
-			:style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl})` : ''"
-		></div>
-		<img :class="$style.avatar"
-			:src="$store.state.i.avatarUrl"
-			alt="avatar"
-		/>
-		<router-link :class="$style.name" :to="$store.state.i | userPage">
-			<mk-user-name :user="$store.state.i"/>
-		</router-link>
-	</ui-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-
-export default define({
-	name: 'profile'
-});
-</script>
-
-<style lang="stylus" module>
-.banner
-	height 100px
-	background-color #f5f5f5
-	background-size cover
-	background-position center
-	cursor pointer
-
-.banner:before
-	content ""
-	display block
-	width 100%
-	height 100%
-	background rgba(#000, 0.5)
-
-.avatar
-	display block
-	position absolute
-	width 58px
-	height 58px
-	margin 0
-	vertical-align bottom
-	top ((100px - 58px) / 2)
-	left ((100px - 58px) / 2)
-	border none
-	border-radius 100%
-	box-shadow 0 0 16px rgba(#000, 0.5)
-
-.name
-	display block
-	position absolute
-	top 0
-	left 92px
-	margin 0
-	line-height 100px
-	color #fff
-	font-weight bold
-	text-shadow 0 0 8px rgba(#000, 0.5)
-
-</style>
diff --git a/src/client/app/reset.styl b/src/client/app/reset.styl
deleted file mode 100644
index 614f29a835c2e250c206fb9a13b2c310f6940d43..0000000000000000000000000000000000000000
--- a/src/client/app/reset.styl
+++ /dev/null
@@ -1,37 +0,0 @@
-input
-	min-width 0
-
-input:not([type])
-input[type='text']
-input[type='password']
-input[type='search']
-input[type='email']
-textarea
-button
-progress
-	-webkit-appearance none
-	-moz-appearance none
-	appearance none
-	box-shadow none
-
-textarea
-	font-family Roboto, HelveticaNeue, Arial, sans-serif
-
-button
-	margin 0
-	background transparent
-	border none
-	cursor pointer
-	color inherit
-	touch-action manipulation
-
-	*
-		pointer-events none
-		user-select none
-
-	&[disabled]
-		cursor default
-
-pre
-	overflow auto
-	white-space pre
diff --git a/src/client/app/safe.js b/src/client/app/safe.js
deleted file mode 100644
index 88c603f6b9086a5f144a6b805d03853dfeca11c0..0000000000000000000000000000000000000000
--- a/src/client/app/safe.js
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * ブラウザの検証
- */
-
-// Detect an old browser
-if (!('fetch' in window)) {
-	alert(
-		'お使いのブラウザ(またはOS)のバージョンが旧式のため、Misskeyを動作させることができません。' +
-		'バージョンを最新のものに更新するか、別のブラウザをお試しください。' +
-		'\n\n' +
-		'Your browser (or your OS) seems outdated. ' +
-		'To run Misskey, please update your browser to latest version or try other browsers.');
-}
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
deleted file mode 100644
index fd3aceb72814ca9062602b7b8f8e2f94ba797b35..0000000000000000000000000000000000000000
--- a/src/client/app/store.ts
+++ /dev/null
@@ -1,463 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import createPersistedState from 'vuex-persistedstate';
-import * as nestedProperty from 'nested-property';
-
-import MiOS from './mios';
-import { erase } from '../../prelude/array';
-import getNoteSummary from '../../misc/get-note-summary';
-
-const defaultSettings = {
-	keepCw: false,
-	tagTimelines: [],
-	fetchOnScroll: true,
-	remainDeletedNote: false,
-	showPostFormOnTopOfTl: false,
-	suggestRecentHashtags: true,
-	showClockOnHeader: true,
-	circleIcons: true,
-	contrastedAcct: true,
-	showFullAcct: false,
-	showVia: true,
-	showReplyTarget: true,
-	showMyRenotes: true,
-	showRenotedMyNotes: true,
-	showLocalRenotes: true,
-	loadRemoteMedia: true,
-	disableViaMobile: false,
-	memo: null,
-	iLikeSushi: false,
-	rememberNoteVisibility: false,
-	defaultNoteVisibility: 'public',
-	wallpaper: null,
-	webSearchEngine: 'https://www.google.com/?#q={{query}}',
-	mutedWords: [],
-	gamesReversiShowBoardLabels: false,
-	gamesReversiUseAvatarStones: true,
-	disableAnimatedMfm: false,
-	homeProfiles: {},
-	mobileHomeProfiles: {},
-	deckProfiles: {},
-	uploadFolder: null,
-	pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
-	pasteDialog: false,
-	reactions: ['like', 'love', 'laugh', 'hmm', 'surprise', 'congrats', 'angry', 'confused', 'rip', 'pudding']
-};
-
-const defaultDeviceSettings = {
-	homeProfile: 'Default',
-	mobileHomeProfile: 'Default',
-	deckProfile: 'Default',
-	deckMode: false,
-	deckColumnAlign: 'center',
-	deckColumnWidth: 'normal',
-	useShadow: false,
-	roundedCorners: true,
-	reduceMotion: false,
-	darkmode: true,
-	darkTheme: 'bb5a8287-a072-4b0a-8ae5-ea2a0d33f4f2',
-	lightTheme: 'light',
-	lineWidth: 1,
-	fontSize: 0,
-	themes: [],
-	enableSounds: true,
-	soundVolume: 0.5,
-	mediaVolume: 0.5,
-	lang: null,
-	appTypeForce: 'auto',
-	debug: false,
-	lightmode: false,
-	loadRawImages: false,
-	alwaysShowNsfw: false,
-	postStyle: 'standard',
-	navbar: 'top',
-	mobileNotificationPosition: 'bottom',
-	useOsDefaultEmojis: false,
-	disableShowingAnimatedImages: false,
-	expandUsersPhotos: true,
-	expandUsersActivity: true,
-	enableMobileQuickNotificationView: false,
-	roomGraphicsQuality: 'medium',
-	roomUseOrthographicCamera: true,
-	activeEmojiCategoryName: undefined,
-	recentEmojis: [],
-};
-
-export default (os: MiOS) => new Vuex.Store({
-	plugins: [createPersistedState({
-		paths: ['i', 'device', 'settings']
-	})],
-
-	state: {
-		i: null,
-		indicate: false,
-		uiHeaderHeight: 0,
-		behindNotes: []
-	},
-
-	getters: {
-		isSignedIn: state => state.i != null,
-
-		home: state => state.settings.homeProfiles[state.device.homeProfile],
-
-		mobileHome: state => state.settings.mobileHomeProfiles[state.device.mobileHomeProfile],
-
-		deck: state => state.settings.deckProfiles[state.device.deckProfile],
-	},
-
-	mutations: {
-		updateI(state, x) {
-			state.i = x;
-		},
-
-		updateIKeyValue(state, x) {
-			state.i[x.key] = x.value;
-		},
-
-		indicate(state, x) {
-			state.indicate = x;
-		},
-
-		setUiHeaderHeight(state, height) {
-			state.uiHeaderHeight = height;
-		},
-
-		pushBehindNote(state, note) {
-			if (note.userId === state.i.id) return;
-			if (state.behindNotes.some(n => n.id === note.id)) return;
-			state.behindNotes.push(note);
-			document.title = `(${state.behindNotes.length}) ${getNoteSummary(note)}`;
-		},
-
-		clearBehindNotes(state) {
-			state.behindNotes = [];
-			document.title = os.instanceName;
-		},
-
-		setHome(state, data) {
-			Vue.set(state.settings.homeProfiles, state.device.homeProfile, data);
-			os.store.dispatch('settings/updateHomeProfile');
-		},
-
-		setDeck(state, data) {
-			Vue.set(state.settings.deckProfiles, state.device.deckProfile, data);
-			os.store.dispatch('settings/updateDeckProfile');
-		},
-
-		addHomeWidget(state, widget) {
-			state.settings.homeProfiles[state.device.homeProfile].unshift(widget);
-			os.store.dispatch('settings/updateHomeProfile');
-		},
-
-		setMobileHome(state, data) {
-			Vue.set(state.settings.mobileHomeProfiles, state.device.mobileHomeProfile, data);
-			os.store.dispatch('settings/updateMobileHomeProfile');
-		},
-
-		updateWidget(state, x) {
-			let w;
-
-			//#region Desktop home
-			const home = state.settings.homeProfiles[state.device.homeProfile];
-			if (home) {
-				w = home.find(w => w.id == x.id);
-				if (w) {
-					w.data = x.data;
-					os.store.dispatch('settings/updateHomeProfile');
-				}
-			}
-			//#endregion
-
-			//#region Mobile home
-			const mobileHome = state.settings.mobileHomeProfiles[state.device.mobileHomeProfile];
-			if (mobileHome) {
-				w = mobileHome.find(w => w.id == x.id);
-				if (w) {
-					w.data = x.data;
-					os.store.dispatch('settings/updateMobileHomeProfile');
-				}
-			}
-			//#endregion
-		},
-
-		addMobileHomeWidget(state, widget) {
-			state.settings.mobileHomeProfiles[state.device.mobileHomeProfile].unshift(widget);
-			os.store.dispatch('settings/updateMobileHomeProfile');
-		},
-
-		removeMobileHomeWidget(state, widget) {
-			Vue.set('state.settings.mobileHomeProfiles', state.device.mobileHomeProfile, state.settings.mobileHomeProfiles[state.device.mobileHomeProfile].filter(w => w.id != widget.id));
-			os.store.dispatch('settings/updateMobileHomeProfile');
-		},
-
-		addDeckColumn(state, column) {
-			const deck = state.settings.deckProfiles[state.device.deckProfile];
-			if (column.name == undefined) column.name = null;
-			deck.columns.push(column);
-			deck.layout.push([column.id]);
-			os.store.dispatch('settings/updateDeckProfile');
-		},
-
-		removeDeckColumn(state, id) {
-			const deck = state.settings.deckProfiles[state.device.deckProfile];
-			deck.columns = deck.columns.filter(c => c.id != id);
-			deck.layout = deck.layout.map(ids => erase(id, ids));
-			deck.layout = deck.layout.filter(ids => ids.length > 0);
-			os.store.dispatch('settings/updateDeckProfile');
-		},
-
-		swapDeckColumn(state, x) {
-			const deck = state.settings.deckProfiles[state.device.deckProfile];
-			const a = x.a;
-			const b = x.b;
-			const aX = deck.layout.findIndex(ids => ids.indexOf(a) != -1);
-			const aY = deck.layout[aX].findIndex(id => id == a);
-			const bX = deck.layout.findIndex(ids => ids.indexOf(b) != -1);
-			const bY = deck.layout[bX].findIndex(id => id == b);
-			deck.layout[aX][aY] = b;
-			deck.layout[bX][bY] = a;
-			os.store.dispatch('settings/updateDeckProfile');
-		},
-
-		swapLeftDeckColumn(state, id) {
-			const deck = state.settings.deckProfiles[state.device.deckProfile];
-			deck.layout.some((ids, i) => {
-				if (ids.indexOf(id) != -1) {
-					const left = deck.layout[i - 1];
-					if (left) {
-						// https://vuejs.org/v2/guide/list.html#Caveats
-						//state.deck.layout[i - 1] = state.deck.layout[i];
-						//state.deck.layout[i] = left;
-						deck.layout.splice(i - 1, 1, deck.layout[i]);
-						deck.layout.splice(i, 1, left);
-					}
-					return true;
-				}
-			});
-			os.store.dispatch('settings/updateDeckProfile');
-		},
-
-		swapRightDeckColumn(state, id) {
-			const deck = state.settings.deckProfiles[state.device.deckProfile];
-			deck.layout.some((ids, i) => {
-				if (ids.indexOf(id) != -1) {
-					const right = deck.layout[i + 1];
-					if (right) {
-						// https://vuejs.org/v2/guide/list.html#Caveats
-						//state.deck.layout[i + 1] = state.deck.layout[i];
-						//state.deck.layout[i] = right;
-						deck.layout.splice(i + 1, 1, deck.layout[i]);
-						deck.layout.splice(i, 1, right);
-					}
-					return true;
-				}
-			});
-			os.store.dispatch('settings/updateDeckProfile');
-		},
-
-		swapUpDeckColumn(state, id) {
-			const deck = state.settings.deckProfiles[state.device.deckProfile];
-			const ids = deck.layout.find(ids => ids.indexOf(id) != -1);
-			ids.some((x, i) => {
-				if (x == id) {
-					const up = ids[i - 1];
-					if (up) {
-						// https://vuejs.org/v2/guide/list.html#Caveats
-						//ids[i - 1] = id;
-						//ids[i] = up;
-						ids.splice(i - 1, 1, id);
-						ids.splice(i, 1, up);
-					}
-					return true;
-				}
-			});
-			os.store.dispatch('settings/updateDeckProfile');
-		},
-
-		swapDownDeckColumn(state, id) {
-			const deck = state.settings.deckProfiles[state.device.deckProfile];
-			const ids = deck.layout.find(ids => ids.indexOf(id) != -1);
-			ids.some((x, i) => {
-				if (x == id) {
-					const down = ids[i + 1];
-					if (down) {
-						// https://vuejs.org/v2/guide/list.html#Caveats
-						//ids[i + 1] = id;
-						//ids[i] = down;
-						ids.splice(i + 1, 1, id);
-						ids.splice(i, 1, down);
-					}
-					return true;
-				}
-			});
-			os.store.dispatch('settings/updateDeckProfile');
-		},
-
-		stackLeftDeckColumn(state, id) {
-			const deck = state.settings.deckProfiles[state.device.deckProfile];
-			const i = deck.layout.findIndex(ids => ids.indexOf(id) != -1);
-			deck.layout = deck.layout.map(ids => erase(id, ids));
-			const left = deck.layout[i - 1];
-			if (left) deck.layout[i - 1].push(id);
-			deck.layout = deck.layout.filter(ids => ids.length > 0);
-			os.store.dispatch('settings/updateDeckProfile');
-		},
-
-		popRightDeckColumn(state, id) {
-			const deck = state.settings.deckProfiles[state.device.deckProfile];
-			const i = deck.layout.findIndex(ids => ids.indexOf(id) != -1);
-			deck.layout = deck.layout.map(ids => erase(id, ids));
-			deck.layout.splice(i + 1, 0, [id]);
-			deck.layout = deck.layout.filter(ids => ids.length > 0);
-			os.store.dispatch('settings/updateDeckProfile');
-		},
-
-		addDeckWidget(state, x) {
-			const deck = state.settings.deckProfiles[state.device.deckProfile];
-			const column = deck.columns.find(c => c.id == x.id);
-			if (column == null) return;
-			column.widgets.unshift(x.widget);
-			os.store.dispatch('settings/updateDeckProfile');
-		},
-
-		removeDeckWidget(state, x) {
-			const deck = state.settings.deckProfiles[state.device.deckProfile];
-			const column = deck.columns.find(c => c.id == x.id);
-			if (column == null) return;
-			column.widgets = column.widgets.filter(w => w.id != x.widget.id);
-			os.store.dispatch('settings/updateDeckProfile');
-		},
-
-		renameDeckColumn(state, x) {
-			const deck = state.settings.deckProfiles[state.device.deckProfile];
-			const column = deck.columns.find(c => c.id == x.id);
-			if (column == null) return;
-			column.name = x.name;
-			os.store.dispatch('settings/updateDeckProfile');
-		},
-
-		updateDeckColumn(state, x) {
-			const deck = state.settings.deckProfiles[state.device.deckProfile];
-			let column = deck.columns.find(c => c.id == x.id);
-			if (column == null) return;
-			column = x;
-			os.store.dispatch('settings/updateDeckProfile');
-		}
-	},
-
-	actions: {
-		login(ctx, i) {
-			ctx.commit('updateI', i);
-			ctx.dispatch('settings/merge', i.clientData);
-		},
-
-		logout(ctx) {
-			ctx.commit('updateI', null);
-			document.cookie = `i=; max-age=0; domain=${document.location.hostname}`;
-			localStorage.removeItem('i');
-		},
-
-		mergeMe(ctx, me) {
-			for (const [key, value] of Object.entries(me)) {
-				ctx.commit('updateIKeyValue', { key, value });
-			}
-
-			if (me.clientData) {
-				ctx.dispatch('settings/merge', me.clientData);
-			}
-		},
-	},
-
-	modules: {
-		device: {
-			namespaced: true,
-
-			state: defaultDeviceSettings,
-
-			mutations: {
-				set(state, x: { key: string; value: any }) {
-					state[x.key] = x.value;
-				},
-
-				setTl(state, x) {
-					state.tl = {
-						src: x.src,
-						arg: x.arg
-					};
-				},
-
-				setVisibility(state, visibility) {
-					state.visibility = visibility;
-				},
-			}
-		},
-
-		settings: {
-			namespaced: true,
-
-			state: defaultSettings,
-
-			mutations: {
-				set(state, x: { key: string; value: any }) {
-					nestedProperty.set(state, x.key, x.value);
-				},
-			},
-
-			actions: {
-				merge(ctx, settings) {
-					if (settings == null) return;
-					for (const [key, value] of Object.entries(settings)) {
-						ctx.commit('set', { key, value });
-					}
-				},
-
-				set(ctx, x) {
-					ctx.commit('set', x);
-
-					if (ctx.rootGetters.isSignedIn) {
-						os.api('i/update-client-setting', {
-							name: x.key,
-							value: x.value
-						});
-					}
-				},
-
-				updateHomeProfile(ctx) {
-					const profiles = ctx.state.homeProfiles;
-					ctx.commit('set', {
-						key: 'homeProfiles',
-						value: profiles
-					});
-					os.api('i/update-client-setting', {
-						name: 'homeProfiles',
-						value: profiles
-					});
-				},
-
-				updateMobileHomeProfile(ctx) {
-					const profiles = ctx.state.mobileHomeProfiles;
-					ctx.commit('set', {
-						key: 'mobileHomeProfiles',
-						value: profiles
-					});
-					os.api('i/update-client-setting', {
-						name: 'mobileHomeProfiles',
-						value: profiles
-					});
-				},
-
-				updateDeckProfile(ctx) {
-					const profiles = ctx.state.deckProfiles;
-					ctx.commit('set', {
-						key: 'deckProfiles',
-						value: profiles
-					});
-					os.api('i/update-client-setting', {
-						name: 'deckProfiles',
-						value: profiles
-					});
-				},
-			}
-		}
-	}
-});
diff --git a/src/client/app/theme.ts b/src/client/app/theme.ts
deleted file mode 100644
index b16fcdff4bfbedb5d65302fdc9664f6b328a8510..0000000000000000000000000000000000000000
--- a/src/client/app/theme.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import * as tinycolor from 'tinycolor2';
-
-export type Theme = {
-	id: string;
-	name: string;
-	author: string;
-	desc?: string;
-	base?: 'dark' | 'light';
-	vars: { [key: string]: string };
-	props: { [key: string]: string };
-};
-
-export const lightTheme: Theme = require('../themes/light.json5');
-export const darkTheme: Theme = require('../themes/dark.json5');
-export const lavenderTheme: Theme = require('../themes/lavender.json5');
-export const futureTheme: Theme = require('../themes/future.json5');
-export const halloweenTheme: Theme = require('../themes/halloween.json5');
-export const cafeTheme: Theme = require('../themes/cafe.json5');
-export const japaneseSushiSetTheme: Theme = require('../themes/japanese-sushi-set.json5');
-export const gruvboxDarkTheme: Theme = require('../themes/gruvbox-dark.json5');
-export const monokaiTheme: Theme = require('../themes/monokai.json5');
-export const vividTheme: Theme = require('../themes/vivid.json5');
-export const rainyTheme: Theme = require('../themes/rainy.json5');
-export const mauveTheme: Theme = require('../themes/mauve.json5');
-export const grayTheme: Theme = require('../themes/gray.json5');
-export const tweetDeckTheme: Theme = require('../themes/tweet-deck.json5');
-
-export const builtinThemes = [
-	lightTheme,
-	darkTheme,
-	lavenderTheme,
-	futureTheme,
-	halloweenTheme,
-	cafeTheme,
-	japaneseSushiSetTheme,
-	gruvboxDarkTheme,
-	monokaiTheme,
-	vividTheme,
-	rainyTheme,
-	mauveTheme,
-	grayTheme,
-	tweetDeckTheme,
-];
-
-export function applyTheme(theme: Theme, persisted = true) {
-	document.documentElement.classList.add('changing-theme');
-
-	setTimeout(() => {
-		document.documentElement.classList.remove('changing-theme');
-	}, 1000);
-
-	// Deep copy
-	const _theme = JSON.parse(JSON.stringify(theme));
-
-	if (_theme.base) {
-		const base = [lightTheme, darkTheme].find(x => x.id == _theme.base);
-		_theme.vars = Object.assign({}, base.vars, _theme.vars);
-		_theme.props = Object.assign({}, base.props, _theme.props);
-	}
-
-	const props = compile(_theme);
-
-	for (const [k, v] of Object.entries(props)) {
-		document.documentElement.style.setProperty(`--${k}`, v.toString());
-	}
-
-	if (persisted) {
-		localStorage.setItem('theme', JSON.stringify(props));
-	}
-}
-
-function compile(theme: Theme): { [key: string]: string } {
-	function getColor(code: string): tinycolor.Instance {
-		// ref
-		if (code[0] == '@') {
-			return getColor(theme.props[code.substr(1)]);
-		}
-		if (code[0] == '$') {
-			return getColor(theme.vars[code.substr(1)]);
-		}
-
-		// func
-		if (code[0] == ':') {
-			const parts = code.split('<');
-			const func = parts.shift().substr(1);
-			const arg = parseFloat(parts.shift());
-			const color = getColor(parts.join('<'));
-
-			switch (func) {
-				case 'darken': return color.darken(arg);
-				case 'lighten': return color.lighten(arg);
-				case 'alpha': return color.setAlpha(arg);
-			}
-		}
-
-		return tinycolor(code);
-	}
-
-	const props = {};
-
-	for (const [k, v] of Object.entries(theme.props)) {
-		props[k] = genValue(getColor(v));
-	}
-
-	const primary = getColor(props['primary']);
-
-	for (let i = 1; i < 10; i++) {
-		const color = primary.clone().setAlpha(i / 10);
-		props['primaryAlpha0' + i] = genValue(color);
-	}
-
-	for (let i = 5; i < 100; i += 5) {
-		const color = primary.clone().lighten(i);
-		props['primaryLighten' + i] = genValue(color);
-	}
-
-	for (let i = 5; i < 100; i += 5) {
-		const color = primary.clone().darken(i);
-		props['primaryDarken' + i] = genValue(color);
-	}
-
-	return props;
-}
-
-function genValue(c: tinycolor.Instance): string {
-	return c.toRgbString();
-}
diff --git a/src/client/assets/error.jpg b/src/client/assets/error.jpg
deleted file mode 100644
index 24d92f3803bad3c3e2518fc684e7104c2d0480dc..0000000000000000000000000000000000000000
Binary files a/src/client/assets/error.jpg and /dev/null differ
diff --git a/src/client/assets/fedi.jpg b/src/client/assets/fedi.jpg
deleted file mode 100644
index cbf3748eb84c67869a7d40ad3ceed79b32be2d41..0000000000000000000000000000000000000000
Binary files a/src/client/assets/fedi.jpg and /dev/null differ
diff --git a/src/client/assets/flush.html b/src/client/assets/flush.html
deleted file mode 100644
index 27725268f91861843e42902e2c270333eff39f70..0000000000000000000000000000000000000000
--- a/src/client/assets/flush.html
+++ /dev/null
@@ -1,16 +0,0 @@
-<!DOCTYPE html>
-
-<html>
-	<head>
-		<meta charset="utf-8">
-		<title>Misskeyのリカバリ</title>
-		<script>
-			const yn = location.search === '?force' || window.confirm('キャッシュをクリアしますか?\n\nDo you want to clear caches?');
-			if (yn) {
-				localStorage.setItem('shouldFlush', 'true');
-			}
-
-			location.href = '/';
-		</script>
-	</head>
-</html>
diff --git a/src/client/assets/manifest.json b/src/client/assets/manifest.json
index 895afbed3625999b688479a95e1483a5c4d7d3fb..f5a1d47a8aa85a810768adbc8279474df1393806 100644
--- a/src/client/assets/manifest.json
+++ b/src/client/assets/manifest.json
@@ -4,38 +4,13 @@
 	"start_url": "/",
 	"display": "standalone",
 	"background_color": "#313a42",
-	"theme_color": "#fb4e4e",
+	"theme_color": "#86b300",
 	"icons": [
-		{
-			"src": "/assets/icons/16.png",
-			"sizes": "16x16",
-			"type": "image/png"
-		},
-		{
-			"src": "/assets/icons/32.png",
-			"sizes": "32x32",
-			"type": "image/png"
-		},
-		{
-			"src": "/assets/icons/64.png",
-			"sizes": "64x64",
-			"type": "image/png"
-		},
-		{
-			"src": "/assets/icons/128.png",
-			"sizes": "128x128",
-			"type": "image/png"
-		},
 		{
 			"src": "/assets/icons/192.png",
 			"sizes": "192x192",
 			"type": "image/png"
 		},
-		{
-			"src": "/assets/icons/256.png",
-			"sizes": "256x256",
-			"type": "image/png"
-		},
 		{
 			"src": "/assets/icons/512.png",
 			"sizes": "512x512",
diff --git a/src/client/assets/message.mp3 b/src/client/assets/message.mp3
deleted file mode 100644
index 64277444759d7d0be1c34574a2f13ca4a84a6e8b..0000000000000000000000000000000000000000
Binary files a/src/client/assets/message.mp3 and /dev/null differ
diff --git a/src/client/assets/misskey-php-like-logo.png b/src/client/assets/misskey-php-like-logo.png
deleted file mode 100644
index 882ef6708b0724d6190eeb967d666a75c0527489..0000000000000000000000000000000000000000
Binary files a/src/client/assets/misskey-php-like-logo.png and /dev/null differ
diff --git a/src/client/assets/pointer.png b/src/client/assets/pointer.png
deleted file mode 100644
index 255e0c8a4f12b4f30a3e735bc9a35f813008d31a..0000000000000000000000000000000000000000
Binary files a/src/client/assets/pointer.png and /dev/null differ
diff --git a/src/client/assets/post.mp3 b/src/client/assets/post.mp3
deleted file mode 100644
index d3da88a933265ea66d24f31db01dbad15b92d932..0000000000000000000000000000000000000000
Binary files a/src/client/assets/post.mp3 and /dev/null differ
diff --git a/src/client/assets/redoc.html b/src/client/assets/redoc.html
deleted file mode 100644
index 9803464cb1d785b3152b85a7b44fa8361de99cee..0000000000000000000000000000000000000000
--- a/src/client/assets/redoc.html
+++ /dev/null
@@ -1,24 +0,0 @@
-<!DOCTYPE html>
-<html>
-	<head>
-		<title>Misskey API</title>
-		<!-- needed for adaptive design -->
-		<meta charset="utf-8"/>
-		<meta name="viewport" content="width=device-width, initial-scale=1">
-		<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
-
-		<!--
-		ReDoc doesn't change outer page styles
-		-->
-		<style>
-			body {
-				margin: 0;
-				padding: 0;
-			}
-		</style>
-	</head>
-	<body>
-		<redoc spec-url='/api.json'></redoc>
-		<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
-	</body>
-</html>
diff --git a/src/client/assets/reversi-put-me.mp3 b/src/client/assets/reversi-put-me.mp3
deleted file mode 100644
index 4e0e72091c6b5092c1ee9f3f352dcd276a343a47..0000000000000000000000000000000000000000
Binary files a/src/client/assets/reversi-put-me.mp3 and /dev/null differ
diff --git a/src/client/assets/reversi-put-you.mp3 b/src/client/assets/reversi-put-you.mp3
deleted file mode 100644
index 9244189c2d5e5c0cf1064196e92b0843d5dacad1..0000000000000000000000000000000000000000
Binary files a/src/client/assets/reversi-put-you.mp3 and /dev/null differ
diff --git a/src/client/assets/room/furnitures/bed/bed.blend b/src/client/assets/room/furnitures/bed/bed.blend
deleted file mode 100644
index 731df76d0cd735d34f95bfaf05331ca2a6aa6909..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/bed/bed.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/bed/bed.glb b/src/client/assets/room/furnitures/bed/bed.glb
deleted file mode 100644
index f35ecb9ef4125801c5f2ff036bc7463abfdabe70..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/bed/bed.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/bin/bin.blend b/src/client/assets/room/furnitures/bin/bin.blend
deleted file mode 100644
index 8d459a0869ec309ac72a57a0f50f7e2f78b26965..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/bin/bin.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/bin/bin.glb b/src/client/assets/room/furnitures/bin/bin.glb
deleted file mode 100644
index b45f203802511026d6c4760155b281d5307105c6..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/bin/bin.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/book/book.blend b/src/client/assets/room/furnitures/book/book.blend
deleted file mode 100644
index 0d4899d4aec991e1f53e6f77d8c89d58159bf948..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/book/book.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/book/book.glb b/src/client/assets/room/furnitures/book/book.glb
deleted file mode 100644
index 546893da06b256e8a5e9710525961a01dc9439bb..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/book/book.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/book2/barcode.png b/src/client/assets/room/furnitures/book2/barcode.png
deleted file mode 100644
index 37cfe5add363f5324cf8ed15c4f06c35d2f030c4..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/book2/barcode.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/book2/book2.blend b/src/client/assets/room/furnitures/book2/book2.blend
deleted file mode 100644
index e0fdb4810197caad280ed2d1e715d9e254827a04..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/book2/book2.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/book2/book2.glb b/src/client/assets/room/furnitures/book2/book2.glb
deleted file mode 100644
index 2b26402f8ca80bf96517241bf1d0ba1c359605e2..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/book2/book2.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/book2/texture.afdesign b/src/client/assets/room/furnitures/book2/texture.afdesign
deleted file mode 100644
index b63771607a035da290e5e27f7d9b5824857440c2..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/book2/texture.afdesign and /dev/null differ
diff --git a/src/client/assets/room/furnitures/book2/texture.png b/src/client/assets/room/furnitures/book2/texture.png
deleted file mode 100644
index 5aa84f0340b1e91f341761f605b50dda53dccaeb..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/book2/texture.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/book2/uv.png b/src/client/assets/room/furnitures/book2/uv.png
deleted file mode 100644
index 61c4fb040024b5647c98e667314af008fe127141..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/book2/uv.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/cardboard-box/cardboard-box.blend b/src/client/assets/room/furnitures/cardboard-box/cardboard-box.blend
deleted file mode 100644
index 3a528de32a376053a00fc5c9564ed9ebca1a011f..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/cardboard-box/cardboard-box.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/cardboard-box/cardboard-box.glb b/src/client/assets/room/furnitures/cardboard-box/cardboard-box.glb
deleted file mode 100644
index bed372e94f8b7bd18a272e8a38f5835cbc3a20ca..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/cardboard-box/cardboard-box.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/cardboard-box2/cardboard-box2.blend b/src/client/assets/room/furnitures/cardboard-box2/cardboard-box2.blend
deleted file mode 100644
index 5f146267acf22370b4b7a6d60ace5217b3db91f7..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/cardboard-box2/cardboard-box2.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/cardboard-box2/cardboard-box2.glb b/src/client/assets/room/furnitures/cardboard-box2/cardboard-box2.glb
deleted file mode 100644
index 85fcb5c0b60ebf15658dc49b687c7bda51843a5d..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/cardboard-box2/cardboard-box2.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/cardboard-box2/texture.png b/src/client/assets/room/furnitures/cardboard-box2/texture.png
deleted file mode 100644
index e498d8f65b0d3816157eac2d91f22bace1cbf1e2..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/cardboard-box2/texture.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/cardboard-box2/uv.png b/src/client/assets/room/furnitures/cardboard-box2/uv.png
deleted file mode 100644
index d547843ee015eff0edaf60ff86c5eebf2c7b5646..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/cardboard-box2/uv.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/cardboard-box3/cardboard-box3.blend b/src/client/assets/room/furnitures/cardboard-box3/cardboard-box3.blend
deleted file mode 100644
index 00681a3cfd5060e42378fd2e2fe6646074a6f862..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/cardboard-box3/cardboard-box3.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/cardboard-box3/cardboard-box3.glb b/src/client/assets/room/furnitures/cardboard-box3/cardboard-box3.glb
deleted file mode 100644
index 1ef04276894f0c0f7b29533074d5716fb18d66a9..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/cardboard-box3/cardboard-box3.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/cardboard-box3/texture.png b/src/client/assets/room/furnitures/cardboard-box3/texture.png
deleted file mode 100644
index 56c914cb9d8094a4ceacad5ff711f63f3df1b582..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/cardboard-box3/texture.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/cardboard-box3/texture.xcf b/src/client/assets/room/furnitures/cardboard-box3/texture.xcf
deleted file mode 100644
index 7ffb3e3439d30c22841d77510c8d85815ced9475..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/cardboard-box3/texture.xcf and /dev/null differ
diff --git a/src/client/assets/room/furnitures/cardboard-box3/uv.png b/src/client/assets/room/furnitures/cardboard-box3/uv.png
deleted file mode 100644
index 797ac509dbacefd19ff3a9dbbcde418a9e42a2ca..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/cardboard-box3/uv.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/carpet-stripe/carpet-stripe.blend b/src/client/assets/room/furnitures/carpet-stripe/carpet-stripe.blend
deleted file mode 100644
index 750343d4f00fc9af30c5158cfc0c517d73e2e02a..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/carpet-stripe/carpet-stripe.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/carpet-stripe/carpet-stripe.glb b/src/client/assets/room/furnitures/carpet-stripe/carpet-stripe.glb
deleted file mode 100644
index 3066a69e353c4ab6668f6c010a804ac258e6f26a..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/carpet-stripe/carpet-stripe.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/chair/chair.blend b/src/client/assets/room/furnitures/chair/chair.blend
deleted file mode 100644
index 79c29a840145a21373a2bb7a635b57745a57007f..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/chair/chair.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/chair/chair.glb b/src/client/assets/room/furnitures/chair/chair.glb
deleted file mode 100644
index 08ee1a0bb0fc569dad8b900fb8c0d5ab2cef4716..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/chair/chair.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/chair2/chair2.blend b/src/client/assets/room/furnitures/chair2/chair2.blend
deleted file mode 100644
index c6a1acd96f5f5347536e31d99ee36d25e8e775d6..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/chair2/chair2.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/chair2/chair2.glb b/src/client/assets/room/furnitures/chair2/chair2.glb
deleted file mode 100644
index 5ea2f3518b692efe6e5237aae464ba594c3f755c..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/chair2/chair2.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/color-box/color-box.blend b/src/client/assets/room/furnitures/color-box/color-box.blend
deleted file mode 100644
index f96a4ff76633b20fb292504702c0816e06859ce7..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/color-box/color-box.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/color-box/color-box.glb b/src/client/assets/room/furnitures/color-box/color-box.glb
deleted file mode 100644
index 43f2abcae8ff2471236a5936844179edb4f154ec..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/color-box/color-box.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/corkboard/corkboard.blend b/src/client/assets/room/furnitures/corkboard/corkboard.blend
deleted file mode 100644
index 9a7e1878cda339fc9f22151d9904bdddde7f892f..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/corkboard/corkboard.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/corkboard/corkboard.glb b/src/client/assets/room/furnitures/corkboard/corkboard.glb
deleted file mode 100644
index fee108fb91519ca8ec3f7dc9637db5dde02c6a6d..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/corkboard/corkboard.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/cube/cube.blend b/src/client/assets/room/furnitures/cube/cube.blend
deleted file mode 100644
index 1af5bf40a9d3c63d4a8345bc34b36f4e36f8fdfe..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/cube/cube.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/cube/cube.glb b/src/client/assets/room/furnitures/cube/cube.glb
deleted file mode 100644
index 4ac8b6036d22ea22981582ee87e529277acab4c5..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/cube/cube.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/cup-noodle/cup-noodle.blend b/src/client/assets/room/furnitures/cup-noodle/cup-noodle.blend
deleted file mode 100644
index 37ca8868c7c7e9022d00ca7dbe5fb78f2b6bfd93..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/cup-noodle/cup-noodle.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/cup-noodle/cup-noodle.glb b/src/client/assets/room/furnitures/cup-noodle/cup-noodle.glb
deleted file mode 100644
index 58efb1b3b42ee3188dc70e41307e2a88490a372c..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/cup-noodle/cup-noodle.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/cup-noodle/noodle.png b/src/client/assets/room/furnitures/cup-noodle/noodle.png
deleted file mode 100644
index 1d74e0bbe77d09732066802aff67ce4cf5d4963b..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/cup-noodle/noodle.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/desk/desk.blend b/src/client/assets/room/furnitures/desk/desk.blend
deleted file mode 100644
index c88d01f0b233ec40c4475b2555d21386d9037691..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/desk/desk.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/desk/desk.glb b/src/client/assets/room/furnitures/desk/desk.glb
deleted file mode 100644
index 4a58513095884e46661b178bc50b41140eeb3a47..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/desk/desk.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/energy-drink/energy-drink.blend b/src/client/assets/room/furnitures/energy-drink/energy-drink.blend
deleted file mode 100644
index 65fc41273e60c3ce5ecb60838b596fb73c6f50fa..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/energy-drink/energy-drink.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/energy-drink/energy-drink.glb b/src/client/assets/room/furnitures/energy-drink/energy-drink.glb
deleted file mode 100644
index 7fb1c278368ad14c7f18139524b172090e8aca07..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/energy-drink/energy-drink.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/energy-drink/texture.afdesign b/src/client/assets/room/furnitures/energy-drink/texture.afdesign
deleted file mode 100644
index 8c117a49b18882c3ff5b87fd6a2299a097d3d441..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/energy-drink/texture.afdesign and /dev/null differ
diff --git a/src/client/assets/room/furnitures/energy-drink/texture.png b/src/client/assets/room/furnitures/energy-drink/texture.png
deleted file mode 100644
index 484ca0f96ffd29bbfda5a16039a924dfd0e22901..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/energy-drink/texture.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/energy-drink/uv.png b/src/client/assets/room/furnitures/energy-drink/uv.png
deleted file mode 100644
index 2a3f20c999a041dd392611fe5fb921adac8be4c5..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/energy-drink/uv.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/eraser/cover.png b/src/client/assets/room/furnitures/eraser/cover.png
deleted file mode 100644
index 932a3fc62eb19dbafe95f91369249a7b232501da..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/eraser/cover.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/eraser/cover.psd b/src/client/assets/room/furnitures/eraser/cover.psd
deleted file mode 100644
index c393337833d4d76f28a4f2de33ace2ce863f3b1c..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/eraser/cover.psd and /dev/null differ
diff --git a/src/client/assets/room/furnitures/eraser/eraser-uv.png b/src/client/assets/room/furnitures/eraser/eraser-uv.png
deleted file mode 100644
index 89e4ea4c4508c20514f84977b3320f53c171e71f..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/eraser/eraser-uv.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/eraser/eraser.blend b/src/client/assets/room/furnitures/eraser/eraser.blend
deleted file mode 100644
index 103c54fbae3f04db34ad3ad7cd36102890a393fb..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/eraser/eraser.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/eraser/eraser.glb b/src/client/assets/room/furnitures/eraser/eraser.glb
deleted file mode 100644
index 016b60df202459182cb66c6f12b13222f8a88ae8..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/eraser/eraser.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/facial-tissue/facial-tissue-uv.png b/src/client/assets/room/furnitures/facial-tissue/facial-tissue-uv.png
deleted file mode 100644
index e3865ad15e6798f43ec910b34aee19ccbe3f8d6e..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/facial-tissue/facial-tissue-uv.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/facial-tissue/facial-tissue.blend b/src/client/assets/room/furnitures/facial-tissue/facial-tissue.blend
deleted file mode 100644
index d59f87c1ee5482a6f375585fba575a60a4d454a8..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/facial-tissue/facial-tissue.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/facial-tissue/facial-tissue.glb b/src/client/assets/room/furnitures/facial-tissue/facial-tissue.glb
deleted file mode 100644
index 48b36ef34729727ff186714aaf6f08f473115eaa..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/facial-tissue/facial-tissue.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/facial-tissue/facial-tissue.png b/src/client/assets/room/furnitures/facial-tissue/facial-tissue.png
deleted file mode 100644
index 7cee4b1859e5ed39b7abb861a05ca05d0dc16c7e..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/facial-tissue/facial-tissue.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/facial-tissue/facial-tissue.psd b/src/client/assets/room/furnitures/facial-tissue/facial-tissue.psd
deleted file mode 100644
index cd59fc007bb0d4c2472a99ec2326b8f8284cae87..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/facial-tissue/facial-tissue.psd and /dev/null differ
diff --git a/src/client/assets/room/furnitures/fan/fan.blend b/src/client/assets/room/furnitures/fan/fan.blend
deleted file mode 100644
index 8c8106e5fe61f92fce0e3e3f359bba90474a5576..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/fan/fan.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/fan/fan.glb b/src/client/assets/room/furnitures/fan/fan.glb
deleted file mode 100644
index d9367f353407b2a9f66841ef2ac373e322ca64b5..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/fan/fan.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/holo-display/holo-display.blend b/src/client/assets/room/furnitures/holo-display/holo-display.blend
deleted file mode 100644
index 56d2e1f8191d1970cb262127f917642ef3a3dc57..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/holo-display/holo-display.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/holo-display/holo-display.glb b/src/client/assets/room/furnitures/holo-display/holo-display.glb
deleted file mode 100644
index 4d042a59b31f7fc04d6b6359af72f297903acfc0..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/holo-display/holo-display.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/holo-display/ray-uv.png b/src/client/assets/room/furnitures/holo-display/ray-uv.png
deleted file mode 100644
index aa7e817e0f7840f2bbda96c031182ade732026f2..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/holo-display/ray-uv.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/holo-display/ray.png b/src/client/assets/room/furnitures/holo-display/ray.png
deleted file mode 100644
index 6a5d24e143bfc5ecde69ba0db283b20a5cdfe29c..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/holo-display/ray.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/keyboard/keyboard.blend b/src/client/assets/room/furnitures/keyboard/keyboard.blend
deleted file mode 100644
index ab33d134b3d99491c34b8557628c944948d01ce9..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/keyboard/keyboard.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/keyboard/keyboard.glb b/src/client/assets/room/furnitures/keyboard/keyboard.glb
deleted file mode 100644
index 15dc69f47a4d5390dd2f5e83045cf1577ae98a75..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/keyboard/keyboard.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/low-table/low-table.blend b/src/client/assets/room/furnitures/low-table/low-table.blend
deleted file mode 100644
index e1592174d9963b9996cdf1969ecc96885c31d2a5..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/low-table/low-table.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/low-table/low-table.glb b/src/client/assets/room/furnitures/low-table/low-table.glb
deleted file mode 100644
index c69bf35d7b52eda2434c4a3db91a3c5f695e4adf..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/low-table/low-table.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/mat/mat.blend b/src/client/assets/room/furnitures/mat/mat.blend
deleted file mode 100644
index a1e1a68c554e93b9b63f54801310d6f34a83d161..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/mat/mat.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/mat/mat.glb b/src/client/assets/room/furnitures/mat/mat.glb
deleted file mode 100644
index 87ccd44e1a2fcb07c7ea55c7561bff356c5f6d70..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/mat/mat.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/milk/milk-uv.png b/src/client/assets/room/furnitures/milk/milk-uv.png
deleted file mode 100644
index 258fd546380d4ba7f7f5d3611e792b0192101342..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/milk/milk-uv.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/milk/milk.blend b/src/client/assets/room/furnitures/milk/milk.blend
deleted file mode 100644
index 2df508d5b905dbc1ee0df0f5b755c5949dc66681..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/milk/milk.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/milk/milk.glb b/src/client/assets/room/furnitures/milk/milk.glb
deleted file mode 100644
index b335fe3d021b227bcec7f82d13133442f1fe84cf..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/milk/milk.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/milk/milk.png b/src/client/assets/room/furnitures/milk/milk.png
deleted file mode 100644
index 35181c8c8c2b3ccd614bdc4570475f3c32f2d28f..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/milk/milk.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/milk/milk.psd b/src/client/assets/room/furnitures/milk/milk.psd
deleted file mode 100644
index f31e439277821d8ad26e4a6f68c00d80cac2f8ba..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/milk/milk.psd and /dev/null differ
diff --git a/src/client/assets/room/furnitures/monitor/monitor.blend b/src/client/assets/room/furnitures/monitor/monitor.blend
deleted file mode 100644
index 6c042ccdd87628352089ef4a698546bc55b96b86..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/monitor/monitor.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/monitor/monitor.glb b/src/client/assets/room/furnitures/monitor/monitor.glb
deleted file mode 100644
index fc33286a15226afe45cdd17bfcf90dcb668c7b65..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/monitor/monitor.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/monitor/monitor.psd b/src/client/assets/room/furnitures/monitor/monitor.psd
deleted file mode 100644
index 57afff9cd9ef9717b57fee8df33bc5bd4814da1e..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/monitor/monitor.psd and /dev/null differ
diff --git a/src/client/assets/room/furnitures/monitor/screen-uv.png b/src/client/assets/room/furnitures/monitor/screen-uv.png
deleted file mode 100644
index 35f74de8aa6a11d80b7710c20761ab0f81048510..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/monitor/screen-uv.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/monitor/screen.jpg b/src/client/assets/room/furnitures/monitor/screen.jpg
deleted file mode 100644
index 4004a1ede9979af67367bab49c48a5499c24bb77..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/monitor/screen.jpg and /dev/null differ
diff --git a/src/client/assets/room/furnitures/moon/moon.blend b/src/client/assets/room/furnitures/moon/moon.blend
deleted file mode 100644
index 4ff3deab8ec8c6cdc7ade407b72a8fe5ba28c782..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/moon/moon.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/moon/moon.glb b/src/client/assets/room/furnitures/moon/moon.glb
deleted file mode 100644
index 07fa7e4c02a9b1902ca137b08e10266c040bfa48..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/moon/moon.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/moon/moon.jpg b/src/client/assets/room/furnitures/moon/moon.jpg
deleted file mode 100644
index 8988ac64b9b5d1c993d6007864ce83c3d58a4d97..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/moon/moon.jpg and /dev/null differ
diff --git a/src/client/assets/room/furnitures/mousepad/mousepad.blend b/src/client/assets/room/furnitures/mousepad/mousepad.blend
deleted file mode 100644
index 14bd139c94a03e0b26685de5431e6fe9893d87ed..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/mousepad/mousepad.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/mousepad/mousepad.glb b/src/client/assets/room/furnitures/mousepad/mousepad.glb
deleted file mode 100644
index 681ada49cd93e18d9b01f111c1de8a0fd6e7b460..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/mousepad/mousepad.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/pc/motherboard-uv.png b/src/client/assets/room/furnitures/pc/motherboard-uv.png
deleted file mode 100644
index 355009fe7c75491c71de252a7cb649da77d75581..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/pc/motherboard-uv.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/pc/motherboard-uv.psd b/src/client/assets/room/furnitures/pc/motherboard-uv.psd
deleted file mode 100644
index 971f33f79eadfdda3e206d09a23aa17d04ac16fa..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/pc/motherboard-uv.psd and /dev/null differ
diff --git a/src/client/assets/room/furnitures/pc/motherboard.jpg b/src/client/assets/room/furnitures/pc/motherboard.jpg
deleted file mode 100644
index d894e4efcf13695b3d308c5481939a96ec023371..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/pc/motherboard.jpg and /dev/null differ
diff --git a/src/client/assets/room/furnitures/pc/pc.blend b/src/client/assets/room/furnitures/pc/pc.blend
deleted file mode 100644
index 13dfec6ccc27bdd13bfdcb5d9eec42fcaf75b7b0..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/pc/pc.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/pc/pc.glb b/src/client/assets/room/furnitures/pc/pc.glb
deleted file mode 100644
index 44a48b18ae6031bf312552636e50c9014391e667..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/pc/pc.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/pencil/pencil.blend b/src/client/assets/room/furnitures/pencil/pencil.blend
deleted file mode 100644
index 0fc6bdd77699df7874b90b5694ff6e17c0d71a3c..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/pencil/pencil.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/pencil/pencil.glb b/src/client/assets/room/furnitures/pencil/pencil.glb
deleted file mode 100644
index a938b5cdcc759f46990adfa9e2656587e7070400..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/pencil/pencil.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/photoframe/photo-uv.png b/src/client/assets/room/furnitures/photoframe/photo-uv.png
deleted file mode 100644
index 9b94906413c704cf62acd86eda0afef773e932e6..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/photoframe/photo-uv.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/photoframe/photo.jpg b/src/client/assets/room/furnitures/photoframe/photo.jpg
deleted file mode 100644
index af14f0f36a03e15c1e1f12cfb0d69e319378f625..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/photoframe/photo.jpg and /dev/null differ
diff --git a/src/client/assets/room/furnitures/photoframe/photoframe.blend b/src/client/assets/room/furnitures/photoframe/photoframe.blend
deleted file mode 100644
index 4224cde45b42b17acd08e3a98eb4d990d543e27f..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/photoframe/photoframe.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/photoframe/photoframe.glb b/src/client/assets/room/furnitures/photoframe/photoframe.glb
deleted file mode 100644
index 4255a77de6ccaacec292f79c0dec650bb22137c7..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/photoframe/photoframe.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/piano/piano.blend b/src/client/assets/room/furnitures/piano/piano.blend
deleted file mode 100644
index 7653cdf672f6599d66e43aa71a1cf657c54e7f6f..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/piano/piano.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/piano/piano.glb b/src/client/assets/room/furnitures/piano/piano.glb
deleted file mode 100644
index 7242e78ceb4a382243d37cc5c326a26684033461..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/piano/piano.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/pinguin/pinguin.blend b/src/client/assets/room/furnitures/pinguin/pinguin.blend
deleted file mode 100644
index 514c713e4cd6cdddcef2d2b775712565a950f931..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/pinguin/pinguin.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/pinguin/pinguin.glb b/src/client/assets/room/furnitures/pinguin/pinguin.glb
deleted file mode 100644
index 6df34c06e9bb2be58974941b90119e6c13ebb071..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/pinguin/pinguin.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/plant/plant-soil-uv.png b/src/client/assets/room/furnitures/plant/plant-soil-uv.png
deleted file mode 100644
index d4971a896cd210372a2532bf73d92a4c3dadf089..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/plant/plant-soil-uv.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/plant/plant-soil.png b/src/client/assets/room/furnitures/plant/plant-soil.png
deleted file mode 100644
index e79ccd240e5fb4710c29aaceafffb8d3bc150170..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/plant/plant-soil.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/plant/plant-soil.psd b/src/client/assets/room/furnitures/plant/plant-soil.psd
deleted file mode 100644
index 1457b7ea5b3086877beb83ab3bb822648c8a8d4f..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/plant/plant-soil.psd and /dev/null differ
diff --git a/src/client/assets/room/furnitures/plant/plant.blend b/src/client/assets/room/furnitures/plant/plant.blend
deleted file mode 100644
index aa38c7b54e4e6656a713b97964a1de0b8ea36b48..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/plant/plant.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/plant/plant.glb b/src/client/assets/room/furnitures/plant/plant.glb
deleted file mode 100644
index 38422b4a9b2940de45816684ae45c77599382995..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/plant/plant.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/plant2/plant2.blend b/src/client/assets/room/furnitures/plant2/plant2.blend
deleted file mode 100644
index 6592c5d98d91007550d41cdfd27e3da08f17bb3e..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/plant2/plant2.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/plant2/plant2.glb b/src/client/assets/room/furnitures/plant2/plant2.glb
deleted file mode 100644
index 223e6f58340f9a9ba76c8aac4777ca3ccfb51a0c..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/plant2/plant2.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/plant2/soil.png b/src/client/assets/room/furnitures/plant2/soil.png
deleted file mode 100644
index e79ccd240e5fb4710c29aaceafffb8d3bc150170..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/plant2/soil.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/poster-h/poster-h.blend b/src/client/assets/room/furnitures/poster-h/poster-h.blend
deleted file mode 100644
index 40f944f3c115f9d3aaf966f6b7f06e436c85d9e4..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/poster-h/poster-h.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/poster-h/poster-h.glb b/src/client/assets/room/furnitures/poster-h/poster-h.glb
deleted file mode 100644
index c6032c100979c713e6d6a4a15f30f7dcec160e6d..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/poster-h/poster-h.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/poster-h/uv.png b/src/client/assets/room/furnitures/poster-h/uv.png
deleted file mode 100644
index f854231e0bcea8d17145035c1859ba980fe5e978..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/poster-h/uv.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/poster-v/poster-v.blend b/src/client/assets/room/furnitures/poster-v/poster-v.blend
deleted file mode 100644
index 07fe971634f2f9df6ab8802f670ebd09c41adf65..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/poster-v/poster-v.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/poster-v/poster-v.glb b/src/client/assets/room/furnitures/poster-v/poster-v.glb
deleted file mode 100644
index 6e3782f19364233453bfee6d4af8451614af6de5..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/poster-v/poster-v.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/poster-v/uv.png b/src/client/assets/room/furnitures/poster-v/uv.png
deleted file mode 100644
index 7bb2bf809e000338978284c7e25a56a6cbb4e15b..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/poster-v/uv.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/pudding/pudding.blend b/src/client/assets/room/furnitures/pudding/pudding.blend
deleted file mode 100644
index bba40ce161d37b08e974cbc875ee5193ffbfe862..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/pudding/pudding.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/pudding/pudding.glb b/src/client/assets/room/furnitures/pudding/pudding.glb
deleted file mode 100644
index 06c9ed80cc1507fc8562d12b7b3cf849897580fa..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/pudding/pudding.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/rubik-cube/rubik-cube.blend b/src/client/assets/room/furnitures/rubik-cube/rubik-cube.blend
deleted file mode 100644
index 6c09067e78bfce812853e715ed6b467fd87ab2d4..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/rubik-cube/rubik-cube.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/rubik-cube/rubik-cube.glb b/src/client/assets/room/furnitures/rubik-cube/rubik-cube.glb
deleted file mode 100644
index d640df9b06e50757d021866fb6eeda54ad272296..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/rubik-cube/rubik-cube.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/server/rack-uv.png b/src/client/assets/room/furnitures/server/rack-uv.png
deleted file mode 100644
index 65bdb0ffd91b3b4dccbc0e83f1641c42c99afe59..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/server/rack-uv.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/server/rack.png b/src/client/assets/room/furnitures/server/rack.png
deleted file mode 100644
index b851295cfa1c5748c4b42b13ee9296c358be3544..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/server/rack.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/server/server.blend b/src/client/assets/room/furnitures/server/server.blend
deleted file mode 100644
index 6675dfbdc2f020c98cb755bd1989de2a753b107a..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/server/server.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/server/server.glb b/src/client/assets/room/furnitures/server/server.glb
deleted file mode 100644
index a8b530a2d22ee5cce3e76ac644c55f64e7e2f041..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/server/server.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/server/server.png b/src/client/assets/room/furnitures/server/server.png
deleted file mode 100644
index 8e9a0d716c928c335b09511e92ab308e000ab7f6..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/server/server.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/server/uv.png b/src/client/assets/room/furnitures/server/uv.png
deleted file mode 100644
index ca2e747d16227f000de4266c0e04c3888ba982ce..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/server/uv.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/sofa/sofa.blend b/src/client/assets/room/furnitures/sofa/sofa.blend
deleted file mode 100644
index fb5aa51a2c05e9bed15a29e3e556775d75163d6e..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/sofa/sofa.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/sofa/sofa.glb b/src/client/assets/room/furnitures/sofa/sofa.glb
deleted file mode 100644
index 6ce77d94ac3aff8c008ac139367827cf2c93c362..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/sofa/sofa.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/spiral/spiral.blend b/src/client/assets/room/furnitures/spiral/spiral.blend
deleted file mode 100644
index 9d3be77bce7e2f915a4840881ea0aaa302d3910c..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/spiral/spiral.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/spiral/spiral.glb b/src/client/assets/room/furnitures/spiral/spiral.glb
deleted file mode 100644
index ee8e3c23b1558750f1fa56c8a9fdeb4cabf476d9..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/spiral/spiral.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/tv/screen-uv.png b/src/client/assets/room/furnitures/tv/screen-uv.png
deleted file mode 100644
index 4bb74f031f857cc5a2f0fd0cbbf082c6f45453bb..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/tv/screen-uv.png and /dev/null differ
diff --git a/src/client/assets/room/furnitures/tv/tv.blend b/src/client/assets/room/furnitures/tv/tv.blend
deleted file mode 100644
index 490e298e7b24dd1928fd4d39d7f54b7fdea63250..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/tv/tv.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/tv/tv.glb b/src/client/assets/room/furnitures/tv/tv.glb
deleted file mode 100644
index b9bd23896b394fd59991885296acea68b6b37dcd..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/tv/tv.glb and /dev/null differ
diff --git a/src/client/assets/room/furnitures/wall-clock/wall-clock.blend b/src/client/assets/room/furnitures/wall-clock/wall-clock.blend
deleted file mode 100644
index 0a61c8f01e3eab209e3a2ed73fbfc13feaaa84b7..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/wall-clock/wall-clock.blend and /dev/null differ
diff --git a/src/client/assets/room/furnitures/wall-clock/wall-clock.glb b/src/client/assets/room/furnitures/wall-clock/wall-clock.glb
deleted file mode 100644
index b9f0093a8dc470e2783a9a53b97a021e330ee037..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/furnitures/wall-clock/wall-clock.glb and /dev/null differ
diff --git a/src/client/assets/room/rooms/default/default.blend b/src/client/assets/room/rooms/default/default.blend
deleted file mode 100644
index 661154724aafc4b46db0d6100e0be6a9458335a7..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/rooms/default/default.blend and /dev/null differ
diff --git a/src/client/assets/room/rooms/default/default.glb b/src/client/assets/room/rooms/default/default.glb
deleted file mode 100644
index 3d378deee2a9c63add57a19753481a18e409c81a..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/rooms/default/default.glb and /dev/null differ
diff --git a/src/client/assets/room/rooms/washitsu/husuma-uv.png b/src/client/assets/room/rooms/washitsu/husuma-uv.png
deleted file mode 100644
index ae2fca3911bdd50d5d80f21a8e69e269b4af92a9..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/rooms/washitsu/husuma-uv.png and /dev/null differ
diff --git a/src/client/assets/room/rooms/washitsu/husuma.png b/src/client/assets/room/rooms/washitsu/husuma.png
deleted file mode 100644
index 084cbed67c89845583ab8176a325aa01a86fa4e7..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/rooms/washitsu/husuma.png and /dev/null differ
diff --git a/src/client/assets/room/rooms/washitsu/tatami-single1600.png b/src/client/assets/room/rooms/washitsu/tatami-single1600.png
deleted file mode 100644
index c0e684d743d06352b35472a2bfce21e4a385947a..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/rooms/washitsu/tatami-single1600.png and /dev/null differ
diff --git a/src/client/assets/room/rooms/washitsu/tatami-uv.png b/src/client/assets/room/rooms/washitsu/tatami-uv.png
deleted file mode 100644
index 5b16c66091db13632800d0eea7798c93162ae48e..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/rooms/washitsu/tatami-uv.png and /dev/null differ
diff --git a/src/client/assets/room/rooms/washitsu/tatami.afdesign b/src/client/assets/room/rooms/washitsu/tatami.afdesign
deleted file mode 100644
index 9300a26950a1ab857fe71d41893c8eb2b5429ba1..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/rooms/washitsu/tatami.afdesign and /dev/null differ
diff --git a/src/client/assets/room/rooms/washitsu/tatami.png b/src/client/assets/room/rooms/washitsu/tatami.png
deleted file mode 100644
index 8894d040ae35bb522c503ea771943e00d8194613..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/rooms/washitsu/tatami.png and /dev/null differ
diff --git a/src/client/assets/room/rooms/washitsu/washitsu.blend b/src/client/assets/room/rooms/washitsu/washitsu.blend
deleted file mode 100644
index 84dc11374d63314bc95b4556ad0c588dc420cb98..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/rooms/washitsu/washitsu.blend and /dev/null differ
diff --git a/src/client/assets/room/rooms/washitsu/washitsu.glb b/src/client/assets/room/rooms/washitsu/washitsu.glb
deleted file mode 100644
index 5b4767bc73d5c27e677c6b3832e7900ecbeb7604..0000000000000000000000000000000000000000
Binary files a/src/client/assets/room/rooms/washitsu/washitsu.glb and /dev/null differ
diff --git a/src/client/assets/thumbnail-not-available.png b/src/client/assets/thumbnail-not-available.png
deleted file mode 100644
index 07cad9919c5a1c6a9398799fc75c989c602386ff..0000000000000000000000000000000000000000
Binary files a/src/client/assets/thumbnail-not-available.png and /dev/null differ
diff --git a/src/client/assets/title.svg b/src/client/assets/title.svg
deleted file mode 100644
index 0e4e0b8b3bff4a0574813e5b527f055280a84fa0..0000000000000000000000000000000000000000
Binary files a/src/client/assets/title.svg and /dev/null differ
diff --git a/src/client/assets/unread.svg b/src/client/assets/unread.svg
deleted file mode 100644
index 8c3cc9f4758c0be0f6f5a12c2929cfa2204acbf8..0000000000000000000000000000000000000000
Binary files a/src/client/assets/unread.svg and /dev/null differ
diff --git a/src/client/assets/version.html b/src/client/assets/version.html
deleted file mode 100644
index 177d37db8f18d1615a254cc6ac9b09328bc1e6db..0000000000000000000000000000000000000000
--- a/src/client/assets/version.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-
-<html>
-	<head>
-		<meta charset="utf-8">
-		<title>Misskeyのリカバリ</title>
-		<script>
-			const v = window.prompt('Enter version:');
-			if (v) {
-				localStorage.setItem('v', v);
-			}
-
-			setTimeout(() => {
-				location.href = '/';
-			}, 500);
-		</script>
-	</head>
-</html>
diff --git a/src/client/assets/welcome-bg.dark.svg b/src/client/assets/welcome-bg.dark.svg
deleted file mode 100644
index 1866170327c30dbd279bec26779b2962d595c87e..0000000000000000000000000000000000000000
Binary files a/src/client/assets/welcome-bg.dark.svg and /dev/null differ
diff --git a/src/client/assets/welcome-bg.light.svg b/src/client/assets/welcome-bg.light.svg
deleted file mode 100644
index ebccb648ea53e2bf49199a19e7278f4782d6ccaa..0000000000000000000000000000000000000000
Binary files a/src/client/assets/welcome-bg.light.svg and /dev/null differ
diff --git a/src/client/components/acct.vue b/src/client/components/acct.vue
new file mode 100644
index 0000000000000000000000000000000000000000..250e8b2371e088ea3a19a8673bbf183c0160e980
--- /dev/null
+++ b/src/client/components/acct.vue
@@ -0,0 +1,29 @@
+<template>
+<span class="mk-acct" v-once>
+	<span class="name">@{{ user.username }}</span>
+	<span class="host" v-if="user.host || detail || $store.state.settings.showFullAcct">@{{ user.host || host }}</span>
+</span>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { toUnicode } from 'punycode';
+import { host } from '../config';
+
+export default Vue.extend({
+	props: ['user', 'detail'],
+	data() {
+		return {
+			host: toUnicode(host),
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-acct {
+	> .host {
+		opacity: 0.5;
+	}
+}
+</style>
diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/components/autocomplete.vue
similarity index 78%
rename from src/client/app/common/views/components/autocomplete.vue
rename to src/client/components/autocomplete.vue
index bbfb7896ae49701391d4859b1811a672bb6051c5..232b25dd61bcf1b543227316d4bcfad5b7d44de2 100644
--- a/src/client/app/common/views/components/autocomplete.vue
+++ b/src/client/components/autocomplete.vue
@@ -28,10 +28,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import { emojilist } from '../../../../../misc/emojilist';
-import contains from '../../../common/scripts/contains';
-import { twemojiSvgBase } from '../../../../../misc/twemoji-base';
-import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url';
+import { emojilist } from '../../misc/emojilist';
+import contains from '../scripts/contains';
+import { twemojiSvgBase } from '../../misc/twemoji-base';
+import { getStaticImageUrl } from '../scripts/get-static-image-url';
 
 type EmojiDef = {
 	emoji: string;
@@ -73,42 +73,7 @@ for (const x of lib) {
 emjdb.sort((a, b) => a.name.length - b.name.length);
 
 export default Vue.extend({
-	props: {
-		type: {
-			type: String,
-			required: true,
-		},
-
-		q: {
-			type: String,
-			required: true,
-		},
-
-		textarea: {
-			type: Object,
-			required: true,
-		},
-
-		complete: {
-			type: Function,
-			required: true,
-		},
-
-		close: {
-			type: Function,
-			required: true,
-		},
-
-		x: {
-			type: Number,
-			required: true,
-		},
-
-		y: {
-			type: Number,
-			required: true,
-		},
-	},
+	props: ['type', 'q', 'textarea', 'complete', 'close', 'x', 'y'],
 
 	data() {
 		return {
@@ -184,7 +149,7 @@ export default Vue.extend({
 
 		this.textarea.addEventListener('keydown', this.onKeydown);
 
-		for (const el of Array.from(document.querySelectorAll('body *'))) {
+		for (const el of Array.from(document.querySelectorAll('*'))) {
 			el.addEventListener('mousedown', this.onMousedown);
 		}
 
@@ -202,7 +167,7 @@ export default Vue.extend({
 	beforeDestroy() {
 		this.textarea.removeEventListener('keydown', this.onKeydown);
 
-		for (const el of Array.from(document.querySelectorAll('body *'))) {
+		for (const el of Array.from(document.querySelectorAll('*'))) {
 			el.removeEventListener('mousedown', this.onMousedown);
 		}
 	},
@@ -363,96 +328,116 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.mk-autocomplete
-	position fixed
-	z-index 65535
-	max-width 100%
-	margin-top calc(1em + 8px)
-	overflow hidden
-	background var(--faceHeader)
-	border solid 1px rgba(#000, 0.1)
-	border-radius 4px
-	transition top 0.1s ease, left 0.1s ease
-
-	> ol
-		display block
-		margin 0
-		padding 4px 0
-		max-height 190px
-		max-width 500px
-		overflow auto
-		list-style none
-
-		> li
-			display flex
-			align-items center
-			padding 4px 12px
-			white-space nowrap
-			overflow hidden
-			font-size 0.9em
-			color rgba(#000, 0.8)
-			cursor default
-
-			&, *
-				user-select none
-
-			*
-				overflow hidden
-				text-overflow ellipsis
-
-			&:hover
-				background var(--autocompleteItemHoverBg)
-
-			&[data-selected='true']
-				background var(--primary)
-
-				&, *
-					color #fff !important
-
-			&:active
-				background var(--primaryDarken10)
-
-				&, *
-					color #fff !important
-
-	> .users > li
-
-		.avatar
-			min-width 28px
-			min-height 28px
-			max-width 28px
-			max-height 28px
-			margin 0 8px 0 0
-			border-radius 100%
-
-		.name
-			margin 0 8px 0 0
-			color var(--autocompleteItemText)
-
-		.username
-			color var(--autocompleteItemTextSub)
-
-	> .hashtags > li
-
-		.name
-			color var(--autocompleteItemText)
-
-	> .emojis > li
-
-		.emoji
-			display inline-block
-			margin 0 4px 0 0
-			width 24px
-
-			> img
-				width 24px
-				vertical-align bottom
-
-		.name
-			color var(--autocompleteItemText)
-
-		.alias
-			margin 0 0 0 8px
-			color var(--autocompleteItemTextSub)
+<style lang="scss" scoped>
+.mk-autocomplete {
+	position: fixed;
+	z-index: 65535;
+	max-width: 100%;
+	margin-top: calc(1em + 8px);
+	overflow: hidden;
+	background: var(--panel);
+	border: solid 1px rgba(#000, 0.1);
+	border-radius: 4px;
+	transition: top 0.1s ease, left 0.1s ease;
+
+	> ol {
+		display: block;
+		margin: 0;
+		padding: 4px 0;
+		max-height: 190px;
+		max-width: 500px;
+		overflow: auto;
+		list-style: none;
+
+		> li {
+			display: flex;
+			align-items: center;
+			padding: 4px 12px;
+			white-space: nowrap;
+			overflow: hidden;
+			font-size: 0.9em;
+			cursor: default;
+
+			&, * {
+				user-select: none;
+			}
+
+			* {
+				overflow: hidden;
+				text-overflow: ellipsis;
+			}
+
+			&:hover {
+				background: var(--yrnqrguo);
+			}
+
+			&[data-selected='true'] {
+				background: var(--accent);
+
+				&, * {
+					color: #fff !important;
+				}
+			}
+
+			&:active {
+				background: var(--accentDarken);
+
+				&, * {
+					color: #fff !important;
+				}
+			}
+		}
+	}
+
+	> .users > li {
+
+		.avatar {
+			min-width: 28px;
+			min-height: 28px;
+			max-width: 28px;
+			max-height: 28px;
+			margin: 0 8px 0 0;
+			border-radius: 100%;
+		}
+
+		.name {
+			margin: 0 8px 0 0;
+			color: var(--autocompleteItemText);
+		}
+
+		.username {
+			color: var(--autocompleteItemTextSub);
+		}
+	}
+
+	> .hashtags > li {
+
+		.name {
+			color: var(--autocompleteItemText);
+		}
+	}
+
+	> .emojis > li {
+
+		.emoji {
+			display: inline-block;
+			margin: 0 4px 0 0;
+			width: 24px;
+
+			> img {
+				width: 24px;
+				vertical-align: bottom;
+			}
+		}
+
+		.name {
+			color: var(--autocompleteItemText);
+		}
+
+		.alias {
+			margin: 0 0 0 8px;
+			color: var(--autocompleteItemTextSub);
+		}
+	}
+}
 </style>
diff --git a/src/client/components/avatar.vue b/src/client/components/avatar.vue
new file mode 100644
index 0000000000000000000000000000000000000000..12cbb82478b3cfb0b5eb2ac7927d52d43486fdbc
--- /dev/null
+++ b/src/client/components/avatar.vue
@@ -0,0 +1,116 @@
+<template>
+<span class="mk-avatar" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick">
+	<span class="inner" :style="icon"></span>
+</span>
+<span class="mk-avatar" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick">
+	<span class="inner" :style="icon"></span>
+</span>
+<router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id">
+	<span class="inner" :style="icon"></span>
+</router-link>
+<router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview">
+	<span class="inner" :style="icon"></span>
+</router-link>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { getStaticImageUrl } from '../scripts/get-static-image-url';
+
+export default Vue.extend({
+	props: {
+		user: {
+			type: Object,
+			required: true
+		},
+		target: {
+			required: false,
+			default: null
+		},
+		disableLink: {
+			required: false,
+			default: false
+		},
+		disablePreview: {
+			required: false,
+			default: false
+		}
+	},
+	computed: {
+		cat(): boolean {
+			return this.user.isCat;
+		},
+		url(): string {
+			return this.$store.state.device.disableShowingAnimatedImages
+				? getStaticImageUrl(this.user.avatarUrl)
+				: this.user.avatarUrl;
+		},
+		icon(): any {
+			return {
+				backgroundColor: this.user.avatarColor,
+				backgroundImage: `url(${this.url})`,
+			};
+		}
+	},
+	watch: {
+		'user.avatarColor'() {
+			this.$el.style.color = this.user.avatarColor;
+		}
+	},
+	mounted() {
+		if (this.user.avatarColor) {
+			this.$el.style.color = this.user.avatarColor;
+		}
+	},
+	methods: {
+		onClick(e) {
+			this.$emit('click', e);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-avatar {
+	position: relative;
+	display: inline-block;
+	vertical-align: bottom;
+	flex-shrink: 0;
+	border-radius: 100%;
+	line-height: 16px;
+
+	&.cat {
+		&:before, &:after {
+			background: #df548f;
+			border: solid 4px currentColor;
+			box-sizing: border-box;
+			content: '';
+			display: inline-block;
+			height: 50%;
+			width: 50%;
+		}
+
+		&:before {
+			border-radius: 0 75% 75%;
+			transform: rotate(37.5deg) skew(30deg);
+		}
+
+		&:after {
+			border-radius: 75% 0 75% 75%;
+			transform: rotate(-37.5deg) skew(-30deg);
+		}
+	}
+	
+	.inner {
+		background-position: center center;
+		background-size: cover;
+		bottom: 0;
+		left: 0;
+		position: absolute;
+		right: 0;
+		top: 0;
+		border-radius: 100%;
+		z-index: 1;
+	}
+}
+</style>
diff --git a/src/client/app/common/views/components/avatars.vue b/src/client/components/avatars.vue
similarity index 100%
rename from src/client/app/common/views/components/avatars.vue
rename to src/client/components/avatars.vue
diff --git a/src/client/app/common/views/components/code-core.vue b/src/client/components/code-core.vue
similarity index 99%
rename from src/client/app/common/views/components/code-core.vue
rename to src/client/components/code-core.vue
index 219ed1d80a40cab95e45c4112cb64a6c2835fa95..a9253528d9613c34cfa6277a6a4114a0a79cca2b 100644
--- a/src/client/app/common/views/components/code-core.vue
+++ b/src/client/components/code-core.vue
@@ -7,7 +7,6 @@ import Vue from 'vue';
 import 'prismjs';
 import 'prismjs/themes/prism-okaidia.css';
 import XPrism from 'vue-prism-component';
-
 export default Vue.extend({
 	components: {
 		XPrism
@@ -26,7 +25,6 @@ export default Vue.extend({
 			required: false
 		}
 	},
-
 	computed: {
 		prismLang() {
 			return Prism.languages[this.lang] ? this.lang : 'js';
diff --git a/src/client/app/common/views/components/code.vue b/src/client/components/code.vue
similarity index 99%
rename from src/client/app/common/views/components/code.vue
rename to src/client/components/code.vue
index d52c9f7bc2da80e41eb7eae18cf771e410ffc690..94cad57be4d00fd3955e4b4924a9adeccbd42898 100644
--- a/src/client/app/common/views/components/code.vue
+++ b/src/client/components/code.vue
@@ -4,12 +4,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
-
 export default Vue.extend({
 	components: {
 		XCode: () => import('./code-core.vue').then(m => m.default)
 	},
-
 	props: {
 		code: {
 			type: String,
diff --git a/src/client/components/cw-button.vue b/src/client/components/cw-button.vue
new file mode 100644
index 0000000000000000000000000000000000000000..4516e5210c964c149fdd33af8709559beb9b0d0f
--- /dev/null
+++ b/src/client/components/cw-button.vue
@@ -0,0 +1,73 @@
+<template>
+<button class="nrvgflfuaxwgkxoynpnumyookecqrrvh _button" @click="toggle">
+	<b>{{ value ? this.$t('_cw.hide') : this.$t('_cw.show') }}</b>
+	<span v-if="!value">{{ this.label }}</span>
+</button>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import { length } from 'stringz';
+import { concat } from '../../prelude/array';
+
+export default Vue.extend({
+	i18n,
+
+	props: {
+		value: {
+			type: Boolean,
+			required: true
+		},
+		note: {
+			type: Object,
+			required: true
+		}
+	},
+
+	computed: {
+		label(): string {
+			return concat([
+				this.note.text ? [this.$t('_cw.chars', { count: length(this.note.text) })] : [],
+				this.note.files && this.note.files.length !== 0 ? [this.$t('_cw.files', { count: this.note.files.length }) ] : [],
+				this.note.poll != null ? [this.$t('_cw.poll')] : []
+			] as string[][]).join(' / ');
+		}
+	},
+
+	methods: {
+		length,
+
+		toggle() {
+			this.$emit('input', !this.value);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.nrvgflfuaxwgkxoynpnumyookecqrrvh {
+	display: inline-block;
+	padding: 4px 8px;
+	font-size: 0.7em;
+	color: var(--cwFg);
+	background: var(--cwBg);
+	border-radius: 2px;
+
+	&:hover {
+		background: var(--cwHoverBg);
+	}
+
+	> span {
+		margin-left: 4px;
+
+		&:before {
+			content: '(';
+		}
+
+		&:after {
+			content: ')';
+		}
+	}
+}
+</style>
diff --git a/src/client/components/date-separated-list.vue b/src/client/components/date-separated-list.vue
new file mode 100644
index 0000000000000000000000000000000000000000..00c3cd6643cdaba3ebe15a7f3bcded6f077d62b4
--- /dev/null
+++ b/src/client/components/date-separated-list.vue
@@ -0,0 +1,94 @@
+<template>
+<sequential-entrance class="sqadhkmv" ref="list" :direction="direction">
+	<template v-for="(item, i) in items">
+		<slot :item="item" :i="i"></slot>
+		<div class="separator" :key="item.id + '_date'" :data-index="i" v-if="i != items.length - 1 && new Date(item.createdAt).getDate() != new Date(items[i + 1].createdAt).getDate()">
+			<p class="date">
+				<span><fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span>
+				<span>{{ getDateText(items[i + 1].createdAt) }}<fa class="icon" :icon="faAngleDown"/></span>
+			</p>
+		</div>
+	</template>
+</sequential-entrance>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
+
+export default Vue.extend({
+	i18n,
+
+	props: {
+		items: {
+			type: Array,
+			required: true,
+		},
+		direction: {
+			type: String,
+			required: false
+		}
+	},
+
+	data() {
+		return {
+			faAngleUp, faAngleDown
+		};
+	},
+
+	methods: {
+		getDateText(time: string) {
+			const date = new Date(time).getDate();
+			const month = new Date(time).getMonth() + 1;
+			return this.$t('monthAndDay', {
+				month: month.toString(),
+				day: date.toString()
+			});
+		},
+
+		focus() {
+			this.$refs.list.focus();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.sqadhkmv {
+	> .separator {
+		text-align: center;
+
+		> .date {
+			display: inline-block;
+			position: relative;
+			margin: 0;
+			padding: 0 16px;
+			line-height: 32px;
+			text-align: center;
+			font-size: 12px;
+			border-radius: 64px;
+			background: var(--dateLabelBg);
+			color: var(--dateLabelFg);
+
+			> span {
+				&:first-child {
+					margin-right: 8px;
+
+					> .icon {
+						margin-right: 8px;
+					}
+				}
+
+				&:last-child {
+					margin-left: 8px;
+
+					> .icon {
+						margin-left: 8px;
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/components/dialog.vue b/src/client/components/dialog.vue
new file mode 100644
index 0000000000000000000000000000000000000000..5311611575d57a6512daf754c86e86c1e67e76f2
--- /dev/null
+++ b/src/client/components/dialog.vue
@@ -0,0 +1,320 @@
+<template>
+<div class="mk-dialog" :class="{ iconOnly }">
+	<transition name="bg-fade" appear>
+		<div class="bg" ref="bg" @click="onBgClick" v-if="show"></div>
+	</transition>
+	<transition name="dialog" appear @after-leave="() => { destroyDom(); }">
+		<div class="main" ref="main" v-if="show">
+			<template v-if="type == 'signin'">
+				<mk-signin/>
+			</template>
+			<template v-else>
+				<div class="icon" v-if="icon">
+					<fa :icon="icon"/>
+				</div>
+				<div class="icon" v-else-if="!input && !select && !user" :class="type">
+					<fa :icon="faCheck" v-if="type === 'success'"/>
+					<fa :icon="faTimesCircle" v-if="type === 'error'"/>
+					<fa :icon="faExclamationTriangle" v-if="type === 'warning'"/>
+					<fa :icon="faInfoCircle" v-if="type === 'info'"/>
+					<fa :icon="faQuestionCircle" v-if="type === 'question'"/>
+					<fa :icon="faSpinner" pulse v-if="type === 'waiting'"/>
+				</div>
+				<header v-if="title" v-html="title"></header>
+				<header v-if="title == null && user">{{ $t('enterUsername') }}</header>
+				<div class="body" v-if="text" v-html="text"></div>
+				<mk-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></mk-input>
+				<mk-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></mk-input>
+				<mk-select v-if="select" v-model="selectedValue" autofocus>
+					<template v-if="select.items">
+						<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
+					</template>
+					<template v-else>
+						<optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label">
+							<option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option>
+						</optgroup>
+					</template>
+				</mk-select>
+				<div class="buttons" v-if="!iconOnly && (showOkButton || showCancelButton) && !actions">
+					<mk-button inline @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user" :disabled="!canOk">{{ (showCancelButton || input || select || user) ? $t('ok') : $t('gotIt') }}</mk-button>
+					<mk-button inline @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('cancel') }}</mk-button>
+				</div>
+				<div class="buttons" v-if="actions">
+					<mk-button v-for="action in actions" inline @click="() => { action.callback(); close(); }" :primary="action.primary" :key="action.text">{{ action.text }}</mk-button>
+				</div>
+			</template>
+		</div>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faSpinner, faInfoCircle, faExclamationTriangle, faCheck } from '@fortawesome/free-solid-svg-icons';
+import { faTimesCircle, faQuestionCircle } from '@fortawesome/free-regular-svg-icons';
+import MkButton from './ui/button.vue';
+import MkInput from './ui/input.vue';
+import MkSelect from './ui/select.vue';
+import parseAcct from '../../misc/acct/parse';
+import i18n from '../i18n';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		MkButton,
+		MkInput,
+		MkSelect,
+	},
+
+	props: {
+		type: {
+			type: String,
+			required: false,
+			default: 'info'
+		},
+		title: {
+			type: String,
+			required: false
+		},
+		text: {
+			type: String,
+			required: false
+		},
+		input: {
+			required: false
+		},
+		select: {
+			required: false
+		},
+		user: {
+			required: false
+		},
+		icon: {
+			required: false
+		},
+		actions: {
+			required: false
+		},
+		showOkButton: {
+			type: Boolean,
+			default: true
+		},
+		showCancelButton: {
+			type: Boolean,
+			default: false
+		},
+		cancelableByBgClick: {
+			type: Boolean,
+			default: true
+		},
+		iconOnly: {
+			type: Boolean,
+			default: false
+		},
+		autoClose: {
+			type: Boolean,
+			default: false
+		}
+	},
+
+	data() {
+		return {
+			show: true,
+			inputValue: this.input && this.input.default ? this.input.default : null,
+			userInputValue: null,
+			selectedValue: this.select ? this.select.default ? this.select.default : this.select.items ? this.select.items[0].value : this.select.groupedItems[0].items[0].value : null,
+			canOk: true,
+			faTimesCircle, faQuestionCircle, faSpinner, faInfoCircle, faExclamationTriangle, faCheck
+		};
+	},
+
+	watch: {
+		userInputValue() {
+			if (this.user) {
+				this.$root.api('users/show', parseAcct(this.userInputValue)).then(u => {
+					this.canOk = u != null;
+				}).catch(() => {
+					this.canOk = false;
+				});
+			}
+		}
+	},
+
+	mounted() {
+		if (this.user) this.canOk = false;
+
+		if (this.autoClose) {
+			setTimeout(() => {
+				this.close();
+			}, 1000);
+		}
+
+		document.addEventListener('keydown', this.onKeydown);
+	},
+
+	beforeDestroy() {
+		document.removeEventListener('keydown', this.onKeydown);
+	},
+
+	methods: {
+		async ok() {
+			if (!this.canOk) return;
+			if (!this.showOkButton) return;
+
+			if (this.user) {
+				const user = await this.$root.api('users/show', parseAcct(this.userInputValue));
+				if (user) {
+					this.$emit('ok', user);
+					this.close();
+				}
+			} else {
+				const result =
+					this.input ? this.inputValue :
+					this.select ? this.selectedValue :
+					true;
+				this.$emit('ok', result);
+				this.close();
+			}
+		},
+
+		cancel() {
+			this.$emit('cancel');
+			this.close();
+		},
+
+		close() {
+			if (!this.show) return;
+			this.show = false;
+			this.$el.style.pointerEvents = 'none';
+			(this.$refs.bg as any).style.pointerEvents = 'none';
+			(this.$refs.main as any).style.pointerEvents = 'none';
+		},
+
+		onBgClick() {
+			if (this.cancelableByBgClick) {
+				this.cancel();
+			}
+		},
+
+		onKeydown(e) {
+			if (e.which === 27) { // ESC
+				this.cancel();
+			}
+		},
+
+		onInputKeydown(e) {
+			if (e.which === 13) { // Enter
+				e.preventDefault();
+				e.stopPropagation();
+				this.ok();
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.dialog-enter-active, .dialog-leave-active {
+	transition: opacity 0.3s, transform 0.3s !important;
+}
+.dialog-enter, .dialog-leave-to {
+	opacity: 0;
+	transform: scale(0.9);
+}
+
+.bg-fade-enter-active, .bg-fade-leave-active {
+	transition: opacity 0.3s !important;
+}
+.bg-fade-enter, .bg-fade-leave-to {
+	opacity: 0;
+}
+
+.mk-dialog {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	position: fixed;
+	z-index: 30000;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+
+	&.iconOnly > .main {
+		min-width: 0;
+		width: initial;
+	}
+
+	> .bg {
+		display: block;
+		position: fixed;
+		top: 0;
+		left: 0;
+		width: 100%;
+		height: 100%;
+		background: rgba(0,0,0,0.7);
+	}
+
+	> .main {
+		display: block;
+		position: fixed;
+		margin: auto;
+		padding: 32px;
+		min-width: 320px;
+		max-width: 480px;
+		box-sizing: border-box;
+		width: calc(100% - 32px);
+		text-align: center;
+		background: var(--panel);
+		border-radius: var(--radius);
+
+		> .icon {
+			font-size: 32px;
+
+			&.success {
+				color: var(--accent);
+			}
+
+			&.error {
+				color: #ec4137;
+			}
+
+			&.warning {
+				color: #ecb637;
+			}
+
+			> * {
+				display: block;
+				margin: 0 auto;
+			}
+
+			& + header {
+				margin-top: 16px;
+			}
+		}
+
+		> header {
+			margin: 0 0 8px 0;
+			font-weight: bold;
+			font-size: 20px;
+
+			& + .body {
+				margin-top: 8px;
+			}
+		}
+
+		> .body {
+			margin: 16px 0 0 0;
+		}
+
+		> .buttons {
+			margin-top: 16px;
+
+			> * {
+				margin: 0 8px;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/app/common/views/components/drive-file-thumbnail.vue b/src/client/components/drive-file-thumbnail.vue
similarity index 83%
rename from src/client/app/common/views/components/drive-file-thumbnail.vue
rename to src/client/components/drive-file-thumbnail.vue
index f44223ad6f33924f8817ff1d2d890c11da96e44b..37a884dc3d9f915b0e63d9962d8fc6402194e73e 100644
--- a/src/client/app/common/views/components/drive-file-thumbnail.vue
+++ b/src/client/components/drive-file-thumbnail.vue
@@ -36,7 +36,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import anime from 'animejs';
 import {
 	faFile,
 	faFileAlt,
@@ -120,12 +119,7 @@ export default Vue.extend({
 	methods: {
 		onThumbnailLoaded() {
 			if (this.file.properties.avgColor) {
-				anime({
-					targets: this.$refs.thumbnail,
-					backgroundColor: 'transparent', // TODO fade
-					duration: 100,
-					easing: 'linear'
-				});
+				this.$refs.thumbnail.style.backgroundColor = 'transparent';
 			}
 		},
 		volumechange() {
@@ -136,49 +130,59 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.zdjebgpv
-	display flex
+<style lang="scss" scoped>
+.zdjebgpv {
+	display: flex;
 
 	> img,
-	> .icon
-		pointer-events none
+	> .icon {
+		pointer-events: none;
+	}
 
-	> .icon-sub
-		position absolute
-		width 30%
-		height auto
-		margin 0
-		right 4%
-		bottom 4%
+	> .icon-sub {
+		position: absolute;
+		width: 30%;
+		height: auto;
+		margin: 0;
+		right: 4%;
+		bottom: 4%;
+	}
 
-	> *
-		margin auto
+	> * {
+		margin: auto;
+	}
 
-	&:not(.detail)
-		> img
-			height 100%
-			width 100%
-			object-fit cover
+	&:not(.detail) {
+		> img {
+			height: 100%;
+			width: 100%;
+			object-fit: cover;
+		}
 
-		> .icon
-			height 65%
-			width 65%
+		> .icon {
+			height: 65%;
+			width: 65%;
+		}
 
 		> video,
-		> audio
-			width 100%
-
-	&.detail
-		> .icon
-			height 100px
-			width 100px
-			margin 16px
+		> audio {
+			width: 100%;
+		}
+	}
 
-		> *:not(.icon)
-			max-height 300px
-			max-width 100%
-			height 100%
-			object-fit contain
+	&.detail {
+		> .icon {
+			height: 100px;
+			width: 100px;
+			margin: 16px;
+		}
 
+		> *:not(.icon) {
+			max-height: 300px;
+			max-width: 100%;
+			height: 100%;
+			object-fit: contain;
+		}
+	}
+}
 </style>
diff --git a/src/client/components/drive-window.vue b/src/client/components/drive-window.vue
new file mode 100644
index 0000000000000000000000000000000000000000..64c4cee0c12cf0d7a6541bba58145db15139dba5
--- /dev/null
+++ b/src/client/components/drive-window.vue
@@ -0,0 +1,53 @@
+<template>
+<x-window ref="window" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="selected.length === 0" @ok="ok()">
+	<template #header>{{ multiple ? $t('selectFiles') : $t('selectFile') }}<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ selected.length | number }})</span></template>
+	<div>
+		<x-drive :multiple="multiple" @change-selection="onChangeSelection" :select-mode="true"/>
+	</div>
+</x-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import XDrive from './drive.vue';
+import XWindow from './window.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XDrive,
+		XWindow,
+	},
+
+	props: {
+		type: {
+			type: String,
+			required: false,
+			default: undefined 
+		},
+		multiple: {
+			type: Boolean,
+			default: false
+		}
+	},
+
+	data() {
+		return {
+			selected: []
+		};
+	},
+
+	methods: {
+		ok() {
+			this.$emit('selected', this.selected);
+			this.$refs.window.close();
+		},
+
+		onChangeSelection(files) {
+			this.selected = files;
+		}
+	}
+});
+</script>
diff --git a/src/client/components/drive.file.vue b/src/client/components/drive.file.vue
new file mode 100644
index 0000000000000000000000000000000000000000..22fc8c6fb713a6cf012e63f3c3e11feda10d1d54
--- /dev/null
+++ b/src/client/components/drive.file.vue
@@ -0,0 +1,368 @@
+<template>
+<div class="ncvczrfv"
+	:data-is-selected="isSelected"
+	@click="onClick"
+	draggable="true"
+	@dragstart="onDragstart"
+	@dragend="onDragend"
+	:title="title"
+>
+	<div class="label" v-if="$store.state.i.avatarId == file.id">
+		<img src="/assets/label.svg"/>
+		<p>{{ $t('avatar') }}</p>
+	</div>
+	<div class="label" v-if="$store.state.i.bannerId == file.id">
+		<img src="/assets/label.svg"/>
+		<p>{{ $t('banner') }}</p>
+	</div>
+	<div class="label red" v-if="file.isSensitive">
+		<img src="/assets/label-red.svg"/>
+		<p>{{ $t('nsfw') }}</p>
+	</div>
+
+	<x-file-thumbnail class="thumbnail" :file="file" fit="contain"/>
+
+	<p class="name">
+		<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
+		<span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
+	</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
+import i18n from '../i18n';
+import copyToClipboard from '../scripts/copy-to-clipboard';
+//import updateAvatar from '../api/update-avatar';
+//import updateBanner from '../api/update-banner';
+import XFileThumbnail from './drive-file-thumbnail.vue';
+import { faDownload, faLink, faICursor, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
+
+export default Vue.extend({
+	i18n,
+
+	props: {
+		file: {
+			type: Object,
+			required: true,
+		},
+		selectMode: {
+			type: Boolean,
+			required: false,
+			default: false,
+		}
+	},
+
+	components: {
+		XFileThumbnail
+	},
+
+	data() {
+		return {
+			isDragging: false
+		};
+	},
+
+	computed: {
+		browser(): any {
+			return this.$parent;
+		},
+		isSelected(): boolean {
+			return this.browser.selectedFiles.some(f => f.id == this.file.id);
+		},
+		title(): string {
+			return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.size)}`;
+		}
+	},
+
+	methods: {
+		onClick(ev) {
+			if (this.selectMode) {
+				this.browser.chooseFile(this.file);
+			} else {
+				this.$root.menu({
+					items: [{
+						type: 'item',
+						text: this.$t('rename'),
+						icon: faICursor,
+						action: this.rename
+					}, {
+						type: 'item',
+						text: this.file.isSensitive ? this.$t('unmarkAsSensitive') : this.$t('markAsSensitive'),
+						icon: this.file.isSensitive ? faEye : faEyeSlash,
+						action: this.toggleSensitive
+					}, null, {
+						type: 'item',
+						text: this.$t('copyUrl'),
+						icon: faLink,
+						action: this.copyUrl
+					}, {
+						type: 'a',
+						href: this.file.url,
+						target: '_blank',
+						text: this.$t('download'),
+						icon: faDownload,
+						download: this.file.name
+					}, null, {
+						type: 'item',
+						text: this.$t('delete'),
+						icon: faTrashAlt,
+						action: this.deleteFile
+					}, null, {
+						type: 'nest',
+						text: this.$t('contextmenu.else-files'),
+						menu: [{
+							type: 'item',
+							text: this.$t('contextmenu.set-as-avatar'),
+							action: this.setAsAvatar
+						}, {
+							type: 'item',
+							text: this.$t('contextmenu.set-as-banner'),
+							action: this.setAsBanner
+						}]
+					}],
+					source: ev.currentTarget || ev.target,
+				});
+			}
+		},
+
+		onDragstart(e) {
+			e.dataTransfer.effectAllowed = 'move';
+			e.dataTransfer.setData('mk_drive_file', JSON.stringify(this.file));
+			this.isDragging = true;
+
+			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
+			// (=あなたの子供が、ドラッグを開始しましたよ)
+			this.browser.isDragSource = true;
+		},
+
+		onDragend(e) {
+			this.isDragging = false;
+			this.browser.isDragSource = false;
+		},
+
+		onThumbnailLoaded() {
+			if (this.file.properties.avgColor) {
+				anime({
+					targets: this.$refs.thumbnail,
+					backgroundColor: 'transparent', // TODO fade
+					duration: 100,
+					easing: 'linear'
+				});
+			}
+		},
+
+		rename() {
+			this.$root.dialog({
+				title: this.$t('contextmenu.rename-file'),
+				input: {
+					placeholder: this.$t('contextmenu.input-new-file-name'),
+					default: this.file.name,
+					allowEmpty: false
+				}
+			}).then(({ canceled, result: name }) => {
+				if (canceled) return;
+				this.$root.api('drive/files/update', {
+					fileId: this.file.id,
+					name: name
+				});
+			});
+		},
+
+		toggleSensitive() {
+			this.$root.api('drive/files/update', {
+				fileId: this.file.id,
+				isSensitive: !this.file.isSensitive
+			});
+		},
+
+		copyUrl() {
+			copyToClipboard(this.file.url);
+			this.$root.dialog({
+				type: 'success',
+				iconOnly: true, autoClose: true
+			});
+		},
+
+		setAsAvatar() {
+			updateAvatar(this.$root)(this.file);
+		},
+
+		setAsBanner() {
+			updateBanner(this.$root)(this.file);
+		},
+
+		addApp() {
+			alert('not implemented yet');
+		},
+
+		async deleteFile() {
+			const { canceled } = await this.$root.dialog({
+				type: 'warning',
+				text: this.$t('driveFileDeleteConfirm', { name: this.file.name }),
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			this.$root.api('drive/files/delete', {
+				fileId: this.file.id
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ncvczrfv {
+	position: relative;
+	padding: 8px 0 0 0;
+	min-height: 180px;
+	border-radius: 4px;
+
+	&, * {
+		cursor: pointer;
+	}
+
+	&:hover {
+		background: rgba(#000, 0.05);
+
+		> .label {
+			&:before,
+			&:after {
+				background: #0b65a5;
+			}
+
+			&.red {
+				&:before,
+				&:after {
+					background: #c12113;
+				}
+			}
+		}
+	}
+
+	&:active {
+		background: rgba(#000, 0.1);
+
+		> .label {
+			&:before,
+			&:after {
+				background: #0b588c;
+			}
+
+			&.red {
+				&:before,
+				&:after {
+					background: #ce2212;
+				}
+			}
+		}
+	}
+
+	&[data-is-selected] {
+		background: var(--accent);
+
+		&:hover {
+			background: var(--accentLighten);
+		}
+
+		&:active {
+			background: var(--accentDarken);
+		}
+
+		> .label {
+			&:before,
+			&:after {
+				display: none;
+			}
+		}
+
+		> .name {
+			color: #fff;
+		}
+
+		> .thumbnail {
+			color: #fff;
+		}
+	}
+
+	> .label {
+		position: absolute;
+		top: 0;
+		left: 0;
+		pointer-events: none;
+
+		&:before,
+		&:after {
+			content: "";
+			display: block;
+			position: absolute;
+			z-index: 1;
+			background: #0c7ac9;
+		}
+
+		&:before {
+			top: 0;
+			left: 57px;
+			width: 28px;
+			height: 8px;
+		}
+
+		&:after {
+			top: 57px;
+			left: 0;
+			width: 8px;
+			height: 28px;
+		}
+
+		&.red {
+			&:before,
+			&:after {
+				background: #c12113;
+			}
+		}
+
+		> img {
+			position: absolute;
+			z-index: 2;
+			top: 0;
+			left: 0;
+		}
+
+		> p {
+			position: absolute;
+			z-index: 3;
+			top: 19px;
+			left: -28px;
+			width: 120px;
+			margin: 0;
+			text-align: center;
+			line-height: 28px;
+			color: #fff;
+			transform: rotate(-45deg);
+		}
+	}
+
+	> .thumbnail {
+		width: 128px;
+		height: 128px;
+		margin: auto;
+		color: var(--driveFileIcon);
+	}
+
+	> .name {
+		display: block;
+		margin: 4px 0 0 0;
+		font-size: 0.8em;
+		text-align: center;
+		word-break: break-all;
+		color: var(--fg);
+		overflow: hidden;
+
+		> .ext {
+			opacity: 0.5;
+		}
+	}
+}
+</style>
diff --git a/src/client/app/desktop/views/components/drive.folder.vue b/src/client/components/drive.folder.vue
similarity index 69%
rename from src/client/app/desktop/views/components/drive.folder.vue
rename to src/client/components/drive.folder.vue
index cf59d51b01dcc909944d4cef7c8e8cf85440ed9d..39a9588772ca88947015be967e96951459b38b96 100644
--- a/src/client/app/desktop/views/components/drive.folder.vue
+++ b/src/client/components/drive.folder.vue
@@ -1,6 +1,5 @@
 <template>
-<div class="ynntpczxvnusfwdyxsfuhvcmuypqopdd"
-	:data-is-contextmenu-showing="isContextmenuShowing"
+<div class="rghtznwe"
 	:data-draghover="draghover"
 	@click="onClick"
 	@mouseover="onMouseover"
@@ -12,12 +11,11 @@
 	draggable="true"
 	@dragstart="onDragstart"
 	@dragend="onDragend"
-	@contextmenu.prevent.stop="onContextmenu"
 	:title="title"
 >
 	<p class="name">
-		<template v-if="hover"><fa :icon="['far', 'folder-open']" fixed-width/></template>
-		<template v-if="!hover"><fa :icon="['far', 'folder']" fixed-width/></template>
+		<template v-if="hover"><fa :icon="faFolderOpen" fixed-width/></template>
+		<template v-if="!hover"><fa :icon="faFolder" fixed-width/></template>
 		{{ folder.name }}
 	</p>
 	<p class="upload" v-if="$store.state.settings.uploadFolder == folder.id">
@@ -28,19 +26,28 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../../i18n';
+import { faFolder, faFolderOpen } from '@fortawesome/free-regular-svg-icons';
+import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n: i18n('desktop/views/components/drive.folder.vue'),
-	props: ['folder'],
+	i18n,
+
+	props: {
+		folder: {
+			type: Object,
+			required: true,
+		}
+	},
+
 	data() {
 		return {
 			hover: false,
 			draghover: false,
 			isDragging: false,
-			isContextmenuShowing: false
+			faFolder, faFolderOpen
 		};
 	},
+
 	computed: {
 		browser(): any {
 			return this.$parent;
@@ -54,43 +61,6 @@ export default Vue.extend({
 			this.browser.move(this.folder);
 		},
 
-		onContextmenu(e) {
-			this.isContextmenuShowing = true;
-			this.$contextmenu(e, [{
-				type: 'item',
-				text: this.$t('contextmenu.move-to-this-folder'),
-				icon: 'arrow-right',
-				action: this.go
-			}, {
-				type: 'item',
-				text: this.$t('contextmenu.show-in-new-window'),
-				icon: ['far', 'window-restore'],
-				action: this.newWindow
-			}, null, {
-				type: 'item',
-				text: this.$t('contextmenu.rename'),
-				icon: 'i-cursor',
-				action: this.rename
-			}, null, {
-				type: 'item',
-				text: this.$t('@.delete'),
-				icon: ['far', 'trash-alt'],
-				action: this.deleteFolder
-			}, null, {
-				type: 'nest',
-				text: this.$t('contextmenu.else-folders'),
-				menu: [{
-					type: 'item',
-					text: this.$t('contextmenu.set-as-upload-folder'),
-					action: this.setAsUploadFolder
-				}]
-			}], {
-				closed: () => {
-					this.isContextmenuShowing = false;
-				}
-			});
-		},
-
 		onMouseover() {
 			this.hover = true;
 		},
@@ -259,55 +229,53 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.ynntpczxvnusfwdyxsfuhvcmuypqopdd
-	padding 8px
-	height 64px
-	background var(--desktopDriveFolderBg)
-	border-radius 4px
-
-	&, *
-		cursor pointer
-
-	*
-		pointer-events none
-
-	&:hover
-		background var(--desktopDriveFolderHoverBg)
-
-	&:active
-		background var(--desktopDriveFolderActiveBg)
-
-	&[data-is-contextmenu-showing]
-	&[data-draghover]
-		&:after
-			content ""
-			pointer-events none
-			position absolute
-			top -4px
-			right -4px
-			bottom -4px
-			left -4px
-			border 2px dashed var(--primaryAlpha03)
-			border-radius 4px
-
-	&[data-draghover]
-		background var(--desktopDriveFolderActiveBg)
-
-	> .name
-		margin 0
-		font-size 0.9em
-		color var(--desktopDriveFolderFg)
-
-		> [data-icon]
-			margin-right 4px
-			margin-left 2px
-			text-align left
-
-	> .upload
-		margin 4px 4px
-		font-size 0.8em
-		text-align right
-		color var(--desktopDriveFolderFg)
+<style lang="scss" scoped>
+.rghtznwe {
+	position: relative;
+	padding: 8px;
+	height: 64px;
+	background: var(--driveFolderBg);
+	border-radius: 4px;
+
+	&, * {
+		cursor: pointer;
+	}
 
+	* {
+		pointer-events: none;
+	}
+
+	&[data-draghover] {
+		&:after {
+			content: "";
+			pointer-events: none;
+			position: absolute;
+			top: -4px;
+			right: -4px;
+			bottom: -4px;
+			left: -4px;
+			border: 2px dashed var(--focus);
+			border-radius: 4px;
+		}
+	}
+
+	> .name {
+		margin: 0;
+		font-size: 0.9em;
+		color: var(--desktopDriveFolderFg);
+
+		> [data-icon] {
+			margin-right: 4px;
+			margin-left: 2px;
+			text-align: left;
+		}
+	}
+
+	> .upload {
+		margin: 4px 4px;
+		font-size: 0.8em;
+		text-align: right;
+		color: var(--desktopDriveFolderFg);
+	}
+}
 </style>
diff --git a/src/client/app/desktop/views/components/drive.nav-folder.vue b/src/client/components/drive.nav-folder.vue
similarity index 83%
rename from src/client/app/desktop/views/components/drive.nav-folder.vue
rename to src/client/components/drive.nav-folder.vue
index 14ab46764218f0e855cd4df2431f4eeb16883213..0689faecd24897a6314daf4fc9cb2a860e2fee9c 100644
--- a/src/client/app/desktop/views/components/drive.nav-folder.vue
+++ b/src/client/components/drive.nav-folder.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="root nav-folder"
+<div class="drylbebk"
 	:data-draghover="draghover"
 	@click="onClick"
 	@dragover.prevent.stop="onDragover"
@@ -7,38 +7,53 @@
 	@dragleave="onDragleave"
 	@drop.stop="onDrop"
 >
-	<i v-if="folder == null" class="cloud"><fa icon="cloud"/></i>
-	<span>{{ folder == null ? $t('@.drive') : folder.name }}</span>
+	<i v-if="folder == null"><fa :icon="faCloud"/></i>
+	<span>{{ folder == null ? $t('drive') : folder.name }}</span>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../../i18n';
+import { faCloud } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
+
 export default Vue.extend({
-	i18n: i18n(),
-	props: ['folder'],
+	i18n,
+
+	props: {
+		folder: {
+			type: Object,
+			required: false,
+		}
+	},
+
 	data() {
 		return {
 			hover: false,
-			draghover: false
+			draghover: false,
+			faCloud
 		};
 	},
+
 	computed: {
 		browser(): any {
 			return this.$parent;
 		}
 	},
+
 	methods: {
 		onClick() {
 			this.browser.move(this.folder);
 		},
+
 		onMouseover() {
 			this.hover = true;
 		},
+
 		onMouseout() {
 			this.hover = false;
 		},
+
 		onDragover(e) {
 			// このフォルダがルートかつカレントディレクトリならドロップ禁止
 			if (this.folder == null && this.browser.folder == null) {
@@ -57,12 +72,15 @@ export default Vue.extend({
 
 			return false;
 		},
+
 		onDragenter() {
 			if (this.folder || this.browser.folder) this.draghover = true;
 		},
+
 		onDragleave() {
 			if (this.folder || this.browser.folder) this.draghover = false;
 		},
+
 		onDrop(e) {
 			this.draghover = false;
 
@@ -104,15 +122,18 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.root.nav-folder
-	> *
-		pointer-events none
-
-	&[data-draghover]
-		background #eee
+<style lang="scss" scoped>
+.drylbebk {
+	> * {
+		pointer-events: none;
+	}
 
-	i.cloud
-		margin-right 4px
+	&[data-draghover] {
+		background: #eee;
+	}
 
+	> i {
+		margin-right: 4px;
+	}
+}
 </style>
diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/components/drive.vue
similarity index 66%
rename from src/client/app/desktop/views/components/drive.vue
rename to src/client/components/drive.vue
index ff4ff18e6ed54fcbb5757a74a0e0a16383814b10..2279e2eb6e548acf12bb85fe838517ba9745418f 100644
--- a/src/client/app/desktop/views/components/drive.vue
+++ b/src/client/components/drive.vue
@@ -1,76 +1,71 @@
 <template>
-<div class="mk-drive">
+<div class="yfudmmck">
 	<nav>
 		<div class="path" @contextmenu.prevent.stop="() => {}">
 			<x-nav-folder :class="{ current: folder == null }"/>
 			<template v-for="folder in hierarchyFolders">
-				<span class="separator"><fa icon="angle-right"/></span>
+				<span class="separator"><fa :icon="faAngleRight"/></span>
 				<x-nav-folder :folder="folder" :key="folder.id"/>
 			</template>
-			<span class="separator" v-if="folder != null"><fa icon="angle-right"/></span>
+			<span class="separator" v-if="folder != null"><fa :icon="faAngleRight"/></span>
 			<span class="folder current" v-if="folder != null">{{ folder.name }}</span>
 		</div>
 	</nav>
 	<div class="main" :class="{ uploading: uploadings.length > 0, fetching }"
 		ref="main"
-		@mousedown="onMousedown"
 		@dragover.prevent.stop="onDragover"
 		@dragenter="onDragenter"
 		@dragleave="onDragleave"
 		@drop.prevent.stop="onDrop"
-		@contextmenu.prevent.stop="onContextmenu"
 	>
-		<div class="selection" ref="selection"></div>
 		<div class="contents" ref="contents">
-			<div class="folders" ref="foldersContainer" v-if="folders.length > 0 || moreFolders">
+			<div class="folders" ref="foldersContainer" v-if="folders.length > 0">
 				<x-folder v-for="folder in folders" :key="folder.id" class="folder" :folder="folder"/>
 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
 				<div class="padding" v-for="n in 16"></div>
-				<ui-button v-if="moreFolders">{{ $t('@.load-more') }}</ui-button>
+				<mk-button v-if="moreFolders">{{ $t('@.load-more') }}</mk-button>
 			</div>
-			<div class="files" ref="filesContainer" v-if="files.length > 0 || moreFiles">
-				<x-file v-for="file in files" :key="file.id" class="file" :file="file"/>
+			<div class="files" ref="filesContainer" v-if="files.length > 0">
+				<x-file v-for="file in files" :key="file.id" class="file" :file="file" :select-mode="selectMode"/>
 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
 				<div class="padding" v-for="n in 16"></div>
-				<ui-button v-if="moreFiles" @click="fetchMoreFiles">{{ $t('@.load-more') }}</ui-button>
+				<mk-button v-if="moreFiles" @click="fetchMoreFiles">{{ $t('@.load-more') }}</mk-button>
 			</div>
-			<div class="empty" v-if="files.length == 0 && !moreFiles && folders.length == 0 && !moreFolders && !fetching">
+			<div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching">
 				<p v-if="draghover">{{ $t('empty-draghover') }}</p>
-				<p v-if="!draghover && folder == null"><strong>{{ $t('empty-drive') }}</strong><br/>{{ $t('empty-drive-description') }}</p>
-				<p v-if="!draghover && folder != null">{{ $t('empty-folder') }}</p>
-			</div>
-		</div>
-		<div class="fetching" v-if="fetching">
-			<div class="spinner">
-				<div class="dot1"></div>
-				<div class="dot2"></div>
+				<p v-if="!draghover && folder == null"><strong>{{ $t('emptyDrive') }}</strong><br/>{{ $t('empty-drive-description') }}</p>
+				<p v-if="!draghover && folder != null">{{ $t('emptyFolder') }}</p>
 			</div>
 		</div>
+		<mk-loading v-if="fetching"/>
 	</div>
 	<div class="dropzone" v-if="draghover"></div>
-	<mk-uploader ref="uploader" @change="onChangeUploaderUploads" @uploaded="onUploaderUploaded"/>
+	<x-uploader ref="uploader" @change="onChangeUploaderUploads" @uploaded="onUploaderUploaded"/>
 	<input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../../i18n';
-import MkDriveWindow from './drive-window.vue';
+import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
 import XNavFolder from './drive.nav-folder.vue';
 import XFolder from './drive.folder.vue';
 import XFile from './drive.file.vue';
-import contains from '../../../common/scripts/contains';
-import { url } from '../../../config';
-import { faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons';
+import XUploader from './uploader.vue';
+import MkButton from './ui/button.vue';
 
 export default Vue.extend({
-	i18n: i18n('desktop/views/components/drive.vue'),
+	i18n,
+
 	components: {
 		XNavFolder,
 		XFolder,
-		XFile
+		XFile,
+		XUploader,
+		MkButton,
 	},
+
 	props: {
 		initFolder: {
 			type: Object,
@@ -79,13 +74,20 @@ export default Vue.extend({
 		type: {
 			type: String,
 			required: false,
-			default: undefined 
+			default: undefined
 		},
 		multiple: {
 			type: Boolean,
+			required: false,
+			default: false
+		},
+		selectMode: {
+			type: Boolean,
+			required: false,
 			default: false
 		}
 	},
+
 	data() {
 		return {
 			/**
@@ -114,9 +116,18 @@ export default Vue.extend({
 			 */
 			isDragSource: false,
 
-			fetching: true
+			fetching: true,
+
+			faAngleRight
 		};
 	},
+
+	watch: {
+		folder() {
+			this.$emit('cd', this.folder);
+		}
+	},
+
 	mounted() {
 		this.connection = this.$root.stream.useSharedConnection('drive');
 
@@ -133,29 +144,12 @@ export default Vue.extend({
 			this.fetch();
 		}
 	},
+
 	beforeDestroy() {
 		this.connection.dispose();
 	},
-	methods: {
-		onContextmenu(e) {
-			this.$contextmenu(e, [{
-				type: 'item',
-				text: this.$t('contextmenu.create-folder'),
-				icon: ['far', 'folder'],
-				action: this.createFolder
-			}, {
-				type: 'item',
-				text: this.$t('contextmenu.upload'),
-				icon: 'upload',
-				action: this.selectLocalFile
-			}, {
-				type: 'item',
-				text: this.$t('contextmenu.url-upload'),
-				icon: faCloudUploadAlt,
-				action: this.urlUpload
-			}]);
-		},
 
+	methods: {
 		onStreamDriveFileCreated(file) {
 			this.addFile(file, true);
 		},
@@ -198,53 +192,6 @@ export default Vue.extend({
 			this.addFile(file, true);
 		},
 
-		onMousedown(e): any {
-			if (contains(this.$refs.foldersContainer, e.target) || contains(this.$refs.filesContainer, e.target)) return true;
-
-			const main = this.$refs.main as any;
-			const selection = this.$refs.selection as any;
-
-			const rect = main.getBoundingClientRect();
-
-			const left = e.pageX + main.scrollLeft - rect.left - window.pageXOffset
-			const top = e.pageY + main.scrollTop - rect.top - window.pageYOffset
-
-			const move = e => {
-				selection.style.display = 'block';
-
-				const cursorX = e.pageX + main.scrollLeft - rect.left - window.pageXOffset;
-				const cursorY = e.pageY + main.scrollTop - rect.top - window.pageYOffset;
-				const w = cursorX - left;
-				const h = cursorY - top;
-
-				if (w > 0) {
-					selection.style.width = w + 'px';
-					selection.style.left = left + 'px';
-				} else {
-					selection.style.width = -w + 'px';
-					selection.style.left = cursorX + 'px';
-				}
-
-				if (h > 0) {
-					selection.style.height = h + 'px';
-					selection.style.top = top + 'px';
-				} else {
-					selection.style.height = -h + 'px';
-					selection.style.top = cursorY + 'px';
-				}
-			};
-
-			const up = e => {
-				document.documentElement.removeEventListener('mousemove', move);
-				document.documentElement.removeEventListener('mouseup', up);
-
-				selection.style.display = 'none';
-			};
-
-			document.documentElement.addEventListener('mousemove', move);
-			document.documentElement.addEventListener('mouseup', up);
-		},
-
 		onDragover(e): any {
 			// ドラッグ元が自分自身の所有するアイテムだったら
 			if (this.isDragSource) {
@@ -402,18 +349,6 @@ export default Vue.extend({
 			}
 		},
 
-		newWindow(folder) {
-			if (document.body.clientWidth > 800) {
-				this.$root.new(MkDriveWindow, {
-					folder: folder
-				});
-			} else {
-				window.open(`${url}/i/drive/folder/${folder.id}`,
-					'drive_window',
-					'height=500, width=800');
-			}
-		},
-
 		move(target) {
 			if (target == null) {
 				this.goRoot();
@@ -590,171 +525,140 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.mk-drive
-	> nav
-		display block
-		z-index 2
-		width 100%
-		overflow auto
-		font-size 0.9em
-		color var(--text)
-		background var(--face)
-		box-shadow 0 1px 0 rgba(#000, 0.05)
-
-		&, *
-			user-select none
-
-		> .path
-			display inline-block
-			vertical-align bottom
-			margin 0
-			padding 0 8px
-			width calc(100% - 200px)
-			line-height 38px
-			white-space nowrap
-
-			> *
-				display inline-block
-				margin 0
-				padding 0 8px
-				line-height 38px
-				cursor pointer
-
-				*
-					pointer-events none
-
-				&:hover
-					text-decoration underline
-
-				&.current
-					font-weight bold
-					cursor default
-
-					&:hover
-						text-decoration none
-
-				&.separator
-					margin 0
-					padding 0
-					opacity 0.5
-					cursor default
-
-					> [data-icon]
-						margin 0
-
-	> .main
-		padding 8px
-		height calc(100% - 38px)
-		overflow auto
-		background var(--desktopDriveBg)
-
-		&, *
-			user-select none
-
-		&.fetching
-			cursor wait !important
-
-			*
-				pointer-events none
-
-			> .contents
-				opacity 0.5
-
-		&.uploading
-			height calc(100% - 38px - 100px)
-
-		> .selection
-			display none
-			position absolute
-			z-index 128
-			top 0
-			left 0
-			border solid 1px var(--primary)
-			background var(--primaryAlpha05)
-			pointer-events none
-
-		> .contents
-
-			> .folders
-			> .files
-				display flex
-				flex-wrap wrap
-
-				> .folder
-				> .file
-					flex-grow 1
-					width 144px
-					margin 4px
-
-				> .padding
-					flex-grow 1
-					pointer-events none
-					width 144px + 8px // 8px is margin
-
-			> .empty
-				padding 16px
-				text-align center
-				color #999
-				pointer-events none
-
-				> p
-					margin 0
-
-		> .fetching
-			.spinner
-				margin 100px auto
-				width 40px
-				height 40px
-				text-align center
-
-				animation sk-rotate 2.0s infinite linear
-
-			.dot1, .dot2
-				width 60%
-				height 60%
-				display inline-block
-				position absolute
-				top 0
-				background-color rgba(#000, 0.3)
-				border-radius 100%
-
-				animation sk-bounce 2.0s infinite ease-in-out
-
-			.dot2
-				top auto
-				bottom 0
-				animation-delay -1.0s
-
-			@keyframes sk-rotate {
-				100% {
-					transform: rotate(360deg);
+<style lang="scss" scoped>
+.yfudmmck {
+	> nav {
+		display: block;
+		z-index: 2;
+		width: 100%;
+		overflow: auto;
+		font-size: 0.9em;
+		box-shadow: 0 1px 0 var(--divider);
+
+		&, * {
+			user-select: none;
+		}
+
+		> .path {
+			display: inline-block;
+			vertical-align: bottom;
+			line-height: 38px;
+			white-space: nowrap;
+
+			> * {
+				display: inline-block;
+				margin: 0;
+				padding: 0 8px;
+				line-height: 38px;
+				cursor: pointer;
+
+				* {
+					pointer-events: none;
+				}
+
+				&:hover {
+					text-decoration: underline;
+				}
+
+				&.current {
+					font-weight: bold;
+					cursor: default;
+
+					&:hover {
+						text-decoration: none;
+					}
+				}
+
+				&.separator {
+					margin: 0;
+					padding: 0;
+					opacity: 0.5;
+					cursor: default;
+
+					> [data-icon] {
+						margin: 0;
+					}
 				}
 			}
+		}
+	}
+
+	> .main {
+		padding: 8px 0;
+		overflow: auto;
+
+		&, * {
+			user-select: none;
+		}
 
-			@keyframes sk-bounce {
-				0%, 100% {
-					transform: scale(0.0);
+		&.fetching {
+			cursor: wait !important;
+
+			* {
+				pointer-events: none;
+			}
+
+			> .contents {
+				opacity: 0.5;
+			}
+		}
+
+		&.uploading {
+			height: calc(100% - 38px - 100px);
+		}
+
+		> .contents {
+
+			> .folders,
+			> .files {
+				display: flex;
+				flex-wrap: wrap;
+
+				> .folder,
+				> .file {
+					flex-grow: 1;
+					width: 144px;
+					margin: 4px;
+					box-sizing: border-box;
 				}
-				50% {
-					transform: scale(1.0);
+
+				> .padding {
+					flex-grow: 1;
+					pointer-events: none;
+					width: 144px + 8px;
 				}
 			}
 
-	> .dropzone
-		position absolute
-		left 0
-		top 38px
-		width 100%
-		height calc(100% - 38px)
-		border dashed 2px var(--primaryAlpha05)
-		pointer-events none
+			> .empty {
+				padding: 16px;
+				text-align: center;
+				pointer-events: none;
+				opacity: 0.5;
 
-	> .mk-uploader
-		height 100px
-		padding 16px
+				> p {
+					margin: 0;
+				}
+			}
+		}
+	}
 
-	> input
-		display none
+	> .dropzone {
+		position: absolute;
+		left: 0;
+		top: 38px;
+		width: 100%;
+		height: calc(100% - 38px);
+		border: dashed 2px var(--focus);
+		pointer-events: none;
+	}
+
+	> .mk-uploader {
+		height: 100px;
+		padding: 16px;
+	}
 
+	> input {
+		display: none;
+	}
+}
 </style>
diff --git a/src/client/components/ellipsis.vue b/src/client/components/ellipsis.vue
new file mode 100644
index 0000000000000000000000000000000000000000..0a46f486d6875862a0821677898d290702102710
--- /dev/null
+++ b/src/client/components/ellipsis.vue
@@ -0,0 +1,34 @@
+<template>
+	<span class="mk-ellipsis">
+		<span>.</span><span>.</span><span>.</span>
+	</span>
+</template>
+
+<style lang="scss" scoped>
+.mk-ellipsis {
+	> span {
+		animation: ellipsis 1.4s infinite ease-in-out both;
+
+		&:nth-child(1) {
+			animation-delay: 0s;
+		}
+
+		&:nth-child(2) {
+			animation-delay: 0.16s;
+		}
+
+		&:nth-child(3) {
+			animation-delay: 0.32s;
+		}
+	}
+}
+
+@keyframes ellipsis {
+	0%, 80%, 100% {
+		opacity: 1;
+	}
+	40% {
+		opacity: 0;
+	}
+}
+</style>
diff --git a/src/client/components/emoji-picker.vue b/src/client/components/emoji-picker.vue
new file mode 100644
index 0000000000000000000000000000000000000000..61d641a023d6a53de79c2e722d565036282cd635
--- /dev/null
+++ b/src/client/components/emoji-picker.vue
@@ -0,0 +1,268 @@
+<template>
+<x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }">
+	<div class="omfetrab">
+		<header>
+			<button v-for="category in categories"
+				class="_button"
+				:title="category.text"
+				@click="go(category)"
+				:class="{ active: category.isActive }"
+				:key="category.text"
+			>
+				<fa :icon="category.icon" fixed-width/>
+			</button>
+		</header>
+
+		<div class="emojis">
+			<template v-if="categories[0].isActive">
+				<header class="category"><fa :icon="faHistory" fixed-width/> {{ $t('recentUsedEmojis') }}</header>
+				<div class="list">
+					<button v-for="(emoji, i) in ($store.state.device.recentEmojis || [])"
+						class="_button"
+						:title="emoji.name"
+						@click="chosen(emoji)"
+						:key="i"
+					>
+						<mk-emoji v-if="emoji.char != null" :emoji="emoji.char"/>
+						<img v-else :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
+					</button>
+				</div>
+			</template>
+
+			<header class="category"><fa :icon="categories.find(x => x.isActive).icon" fixed-width/> {{ categories.find(x => x.isActive).text }}</header>
+			<template v-if="categories.find(x => x.isActive).name">
+				<div class="list">
+					<button v-for="emoji in emojilist.filter(e => e.category === categories.find(x => x.isActive).name)"
+						class="_button"
+						:title="emoji.name"
+						@click="chosen(emoji)"
+						:key="emoji.name"
+					>
+						<mk-emoji :emoji="emoji.char"/>
+					</button>
+				</div>
+			</template>
+			<template v-else>
+				<div v-for="(key, i) in Object.keys(customEmojis)" :key="i">
+					<header class="sub" v-if="key">{{ key }}</header>
+					<div class="list">
+						<button v-for="emoji in customEmojis[key]"
+							class="_button"
+							:title="emoji.name"
+							@click="chosen(emoji)"
+							:key="emoji.name"
+						>
+							<img :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
+						</button>
+					</div>
+				</div>
+			</template>
+		</div>
+	</div>
+</x-popup>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import { emojilist } from '../../misc/emojilist';
+import { getStaticImageUrl } from '../scripts/get-static-image-url';
+import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice, faGlobe, faHistory } from '@fortawesome/free-solid-svg-icons';
+import { faHeart, faFlag, faLaugh } from '@fortawesome/free-regular-svg-icons';
+import { groupByX } from '../../prelude/array';
+import XPopup from './popup.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XPopup,
+	},
+
+	props: {
+		source: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			emojilist,
+			getStaticImageUrl,
+			customEmojis: {},
+			faGlobe, faHistory,
+			categories: [{
+				text: this.$t('customEmoji'),
+				icon: faAsterisk,
+				isActive: true
+			}, {
+				name: 'people',
+				text: this.$t('people'),
+				icon: faLaugh,
+				isActive: false
+			}, {
+				name: 'animals_and_nature',
+				text: this.$t('animals-and-nature'),
+				icon: faLeaf,
+				isActive: false
+			}, {
+				name: 'food_and_drink',
+				text: this.$t('food-and-drink'),
+				icon: faUtensils,
+				isActive: false
+			}, {
+				name: 'activity',
+				text: this.$t('activity'),
+				icon: faFutbol,
+				isActive: false
+			}, {
+				name: 'travel_and_places',
+				text: this.$t('travel-and-places'),
+				icon: faCity,
+				isActive: false
+			}, {
+				name: 'objects',
+				text: this.$t('objects'),
+				icon: faDice,
+				isActive: false
+			}, {
+				name: 'symbols',
+				text: this.$t('symbols'),
+				icon: faHeart,
+				isActive: false
+			}, {
+				name: 'flags',
+				text: this.$t('flags'),
+				icon: faFlag,
+				isActive: false
+			}]
+		};
+	},
+
+	created() {
+		let local = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
+		local = groupByX(local, (x: any) => x.category || '');
+		this.customEmojis = local;
+	},
+
+	methods: {
+		go(category: any) {
+			this.goCategory(category.name);
+		},
+
+		goCategory(name: string) {
+			let matched = false;
+			for (const c of this.categories) {
+				c.isActive = c.name === name;
+				if (c.isActive) {
+					matched = true;
+				}
+			}
+			if (!matched) {
+				this.categories[0].isActive = true;
+			}
+		},
+
+		chosen(emoji: any) {
+			const getKey = (emoji: any) => emoji.char || `:${emoji.name}:`;
+			let recents = this.$store.state.device.recentEmojis || [];
+			recents = recents.filter((e: any) => getKey(e) !== getKey(emoji));
+			recents.unshift(emoji)
+			this.$store.commit('device/set', { key: 'recentEmojis', value: recents.splice(0, 16) });
+			this.$emit('chosen', getKey(emoji));
+		},
+
+		close() {
+			this.$refs.popup.close();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.omfetrab {
+	width: 350px;
+
+	> header {
+		display: flex;
+
+		> button {
+			flex: 1;
+			padding: 10px 0;
+			font-size: 16px;
+			transition: color 0.2s ease;
+
+			&:hover {
+				color: var(--textHighlighted);
+				transition: color 0s;
+			}
+
+			&.active {
+				color: var(--accent);
+				transition: color 0s;
+			}
+		}
+	}
+
+	> .emojis {
+		height: 300px;
+		overflow-y: auto;
+		overflow-x: hidden;
+
+		> header.category {
+			position: sticky;
+			top: 0;
+			left: 0;
+			z-index: 1;
+			padding: 8px;
+			background: var(--panel);
+			font-size: 12px;
+		}
+
+		header.sub {
+			padding: 4px 8px;
+			font-size: 12px;
+		}
+
+		div.list {
+			display: grid;
+			grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
+			gap: 4px;
+			padding: 8px;
+
+			> button {
+				position: relative;
+				padding: 0;
+				width: 100%;
+
+				&:before {
+					content: '';
+					display: block;
+					width: 1px;
+					height: 0;
+					padding-bottom: 100%;
+				}
+
+				&:hover {
+					> * {
+						transform: scale(1.2);
+						transition: transform 0s;
+					}
+				}
+
+				> * {
+					position: absolute;
+					top: 0;
+					left: 0;
+					width: 100%;
+					height: 100%;
+					object-fit: contain;
+					font-size: 28px;
+					transition: transform 0.2s ease;
+					pointer-events: none;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/app/common/views/components/emoji.vue b/src/client/components/emoji.vue
similarity index 71%
rename from src/client/app/common/views/components/emoji.vue
rename to src/client/components/emoji.vue
index 26992c5f7e792442c0729273faabfb4af4decff0..2e8bddb8034f8842b7e67d8410ae8e5ae0ed99d6 100644
--- a/src/client/app/common/views/components/emoji.vue
+++ b/src/client/components/emoji.vue
@@ -1,14 +1,14 @@
 <template>
-<img v-if="customEmoji" class="fvgwvorwhxigeolkkrcderjzcawqrscl custom" :class="{ normal: normal }" :src="url" :alt="alt" :title="alt"/>
-<img v-else-if="char && !useOsDefaultEmojis" class="fvgwvorwhxigeolkkrcderjzcawqrscl" :src="url" :alt="alt" :title="alt"/>
+<img v-if="customEmoji" class="mk-emoji custom" :class="{ normal, noStyle }" :src="url" :alt="alt" :title="alt"/>
+<img v-else-if="char && !useOsDefaultEmojis" class="mk-emoji" :src="url" :alt="alt" :title="alt"/>
 <span v-else-if="char && useOsDefaultEmojis">{{ char }}</span>
 <span v-else>:{{ name }}:</span>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url';
-import { twemojiSvgBase } from '../../../../../misc/twemoji-base';
+import { getStaticImageUrl } from '../scripts/get-static-image-url';
+import { twemojiSvgBase } from '../../misc/twemoji-base';
 
 export default Vue.extend({
 	props: {
@@ -25,6 +25,11 @@ export default Vue.extend({
 			required: false,
 			default: false
 		},
+		noStyle: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
 		customEmojis: {
 			required: false,
 			default: () => []
@@ -96,24 +101,32 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.fvgwvorwhxigeolkkrcderjzcawqrscl
-	height 1.25em
-	vertical-align -0.25em
+<style lang="scss" scoped>
+.mk-emoji {
+	height: 1.25em;
+	vertical-align: -0.25em;
 
-	&.custom
-		height 2.5em
-		vertical-align middle
-		transition transform 0.2s ease
+	&.custom {
+		height: 2.5em;
+		vertical-align: middle;
+		transition: transform 0.2s ease;
 
-		&:hover
-			transform scale(1.2)
+		&:hover {
+			transform: scale(1.2);
+		}
 
-		&.normal
-			height 1.25em
-			vertical-align -0.25em
+		&.normal {
+			height: 1.25em;
+			vertical-align: -0.25em;
 
-			&:hover
-				transform none
+			&:hover {
+				transform: none;
+			}
+		}
+	}
 
+	&.noStyle {
+		height: auto !important;
+	}
+}
 </style>
diff --git a/src/client/components/error.vue b/src/client/components/error.vue
new file mode 100644
index 0000000000000000000000000000000000000000..1dc21dbb1934c85652cb509005f613b8fe5d5c9e
--- /dev/null
+++ b/src/client/components/error.vue
@@ -0,0 +1,42 @@
+<template>
+<div class="wjqjnyhzogztorhrdgcpqlkxhkmuetgj _panel">
+	<p><fa :icon="faExclamationTriangle"/> {{ $t('error') }}</p>
+	<mk-button @click="() => $emit('retry')" class="button">{{ $t('retry') }}</mk-button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
+import MkButton from './ui/button.vue';
+
+export default Vue.extend({
+	i18n,
+	components: {
+		MkButton,
+	},
+	data() {
+		return {
+			faExclamationTriangle
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.wjqjnyhzogztorhrdgcpqlkxhkmuetgj {
+	max-width: 350px;
+	margin: 0 auto;
+	padding: 32px;
+	text-align: center;
+
+	> p {
+		margin: 0 0 8px 0;
+	}
+
+	> .button {
+		margin: 0 auto;
+	}
+}
+</style>
diff --git a/src/client/components/file-type-icon.vue b/src/client/components/file-type-icon.vue
new file mode 100644
index 0000000000000000000000000000000000000000..8492567ad71056693b784b4d6efc0d7c294bcc6c
--- /dev/null
+++ b/src/client/components/file-type-icon.vue
@@ -0,0 +1,29 @@
+<template>
+<span class="mk-file-type-icon">
+	<template v-if="kind == 'image'"><fa :icon="faFileImage"/></template>
+</span>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faFileImage } from '@fortawesome/free-solid-svg-icons';
+
+export default Vue.extend({
+	props: {
+		type: {
+			type: String,
+			required: true,
+		}
+	},
+	data() {
+		return {
+			faFileImage
+		};
+	},
+	computed: {
+		kind(): string {
+			return this.type.split('/')[0];
+		}
+	}
+});
+</script>
diff --git a/src/client/components/follow-button.vue b/src/client/components/follow-button.vue
new file mode 100644
index 0000000000000000000000000000000000000000..4b57a2bd88d6b2978af4925118b491e6a20bf8b3
--- /dev/null
+++ b/src/client/components/follow-button.vue
@@ -0,0 +1,162 @@
+<template>
+<button class="wfliddvnhxvyusikowhxozkyxyenqxqr _button"
+	:class="{ wait, active: isFollowing || hasPendingFollowRequestFromYou }"
+	@click="onClick"
+	:disabled="wait"
+>
+	<template v-if="!wait">
+		<fa v-if="hasPendingFollowRequestFromYou && user.isLocked" :icon="faHourglassHalf"/>
+		<fa v-else-if="hasPendingFollowRequestFromYou && !user.isLocked" :icon="faSpinner" pulse/>
+		<fa v-else-if="isFollowing" :icon="faMinus"/>
+		<fa v-else-if="!isFollowing && user.isLocked" :icon="faPlus"/>
+		<fa v-else-if="!isFollowing && !user.isLocked" :icon="faPlus"/>
+	</template>
+	<template v-else><fa :icon="faSpinner" pulse fixed-width/></template>
+</button>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import { faSpinner, faPlus, faMinus, faHourglassHalf } from '@fortawesome/free-solid-svg-icons';
+
+export default Vue.extend({
+	i18n,
+
+	props: {
+		user: {
+			type: Object,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			isFollowing: this.user.isFollowing,
+			hasPendingFollowRequestFromYou: this.user.hasPendingFollowRequestFromYou,
+			wait: false,
+			connection: null,
+			faSpinner, faPlus, faMinus, faHourglassHalf
+		};
+	},
+
+	mounted() {
+		this.connection = this.$root.stream.useSharedConnection('main');
+
+		this.connection.on('follow', this.onFollowChange);
+		this.connection.on('unfollow', this.onFollowChange);
+	},
+
+	beforeDestroy() {
+		this.connection.dispose();
+	},
+
+	methods: {
+		onFollowChange(user) {
+			if (user.id == this.user.id) {
+				this.isFollowing = user.isFollowing;
+				this.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
+			}
+		},
+
+		async onClick() {
+			this.wait = true;
+
+			try {
+				if (this.isFollowing) {
+					const { canceled } = await this.$root.dialog({
+						type: 'warning',
+						text: this.$t('unfollowConfirm', { name: this.user.name || this.user.username }),
+						showCancelButton: true
+					});
+
+					if (canceled) return;
+
+					await this.$root.api('following/delete', {
+						userId: this.user.id
+					});
+				} else {
+					if (this.hasPendingFollowRequestFromYou) {
+						await this.$root.api('following/requests/cancel', {
+							userId: this.user.id
+						});
+					} else if (this.user.isLocked) {
+						await this.$root.api('following/create', {
+							userId: this.user.id
+						});
+						this.hasPendingFollowRequestFromYou = true;
+					} else {
+						await this.$root.api('following/create', {
+							userId: this.user.id
+						});
+						this.hasPendingFollowRequestFromYou = true;
+					}
+				}
+			} catch (e) {
+				console.error(e);
+			} finally {
+				this.wait = false;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.wfliddvnhxvyusikowhxozkyxyenqxqr {
+	position: relative;
+	display: inline-block;
+	font-weight: bold;
+	color: var(--accent);
+	background: transparent;
+	border: solid 1px var(--accent);
+	padding: 0;
+	width: 31px;
+	height: 31px;
+	font-size: 16px;
+	border-radius: 100%;
+	background: #fff;
+
+	&:focus {
+		&:after {
+			content: "";
+			pointer-events: none;
+			position: absolute;
+			top: -5px;
+			right: -5px;
+			bottom: -5px;
+			left: -5px;
+			border: 2px solid var(--focus);
+			border-radius: 100%;
+		}
+	}
+
+	&:hover {
+		//background: mix($primary, #fff, 20);
+	}
+
+	&:active {
+		//background: mix($primary, #fff, 40);
+	}
+
+	&.active {
+		color: #fff;
+		background: var(--accent);
+
+		&:hover {
+			background: var(--accentLighten);
+			border-color: var(--accentLighten);
+		}
+
+		&:active {
+			background: var(--accentDarken);
+			border-color: var(--accentDarken);
+		}
+	}
+
+	&.wait {
+		cursor: wait !important;
+		opacity: 0.7;
+	}
+}
+</style>
diff --git a/src/client/app/common/views/components/formula-core.vue b/src/client/components/formula-core.vue
similarity index 88%
rename from src/client/app/common/views/components/formula-core.vue
rename to src/client/components/formula-core.vue
index 69697d6df05118726652d6c3dc58006a657a35a8..45b27f902699dd8a9fa06c867e1ca3bf7c81e7a6 100644
--- a/src/client/app/common/views/components/formula-core.vue
+++ b/src/client/components/formula-core.vue
@@ -1,3 +1,4 @@
+
 <template>
 <div v-if="block" v-html="compiledFormula"></div>
 <span v-else v-html="compiledFormula"></span>
@@ -6,7 +7,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as katex from 'katex';
-
 export default Vue.extend({
 	props: {
 		formula: {
@@ -29,5 +29,5 @@ export default Vue.extend({
 </script>
 
 <style>
-@import "../../../../../../node_modules/katex/dist/katex.min.css";
+@import "../../../node_modules/katex/dist/katex.min.css";
 </style>
diff --git a/src/client/app/common/views/components/formula.vue b/src/client/components/formula.vue
similarity index 99%
rename from src/client/app/common/views/components/formula.vue
rename to src/client/components/formula.vue
index 73572b72c6556d902af4b3643f3f51cfea124f40..4aaad1bf3e082412a37ab425d9c5b0811f8a71cf 100644
--- a/src/client/app/common/views/components/formula.vue
+++ b/src/client/components/formula.vue
@@ -4,12 +4,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
-
 export default Vue.extend({
 	components: {
 		XFormula: () => import('./formula-core.vue').then(m => m.default)
 	},
-
 	props: {
 		formula: {
 			type: String,
diff --git a/src/client/components/google.vue b/src/client/components/google.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e6ef7f7d9067ab662f3cdd50a58427c60c752e0e
--- /dev/null
+++ b/src/client/components/google.vue
@@ -0,0 +1,71 @@
+<template>
+<div class="mk-google">
+	<input type="search" v-model="query" :placeholder="q">
+	<button @click="search"><fa icon="search"/> {{ $t('@.search') }}</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+
+export default Vue.extend({
+	i18n,
+	props: ['q'],
+	data() {
+		return {
+			query: null
+		};
+	},
+	mounted() {
+		this.query = this.q;
+	},
+	methods: {
+		search() {
+			const engine = this.$store.state.settings.webSearchEngine ||
+				'https://www.google.com/?#q={{query}}';
+			const url = engine.replace('{{query}}', this.query)
+			window.open(url, '_blank');
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-google {
+	display: flex;
+	margin: 8px 0;
+
+	> input {
+		flex-shrink: 1;
+		padding: 10px;
+		width: 100%;
+		height: 40px;
+		font-size: 16px;
+		color: var(--googleSearchFg);
+		background: var(--googleSearchBg);
+		border: solid 1px var(--googleSearchBorder);
+		border-radius: 4px 0 0 4px;
+
+		&:hover {
+			border-color: var(--googleSearchHoverBorder);
+		}
+	}
+
+	> button {
+		flex-shrink: 0;
+		padding: 0 16px;
+		border: solid 1px var(--googleSearchBorder);
+		border-left: none;
+		border-radius: 0 4px 4px 0;
+
+		&:hover {
+			background-color: var(--googleSearchHoverButton);
+		}
+
+		&:active {
+			box-shadow: 0 2px 4px rgba(#000, 0.15) inset;
+		}
+	}
+}
+</style>
diff --git a/src/client/components/index.ts b/src/client/components/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9385c2af73424bda52231baa10d56581642af69f
--- /dev/null
+++ b/src/client/components/index.ts
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+
+import mfm from './misskey-flavored-markdown.vue';
+import acct from './acct.vue';
+import avatar from './avatar.vue';
+import emoji from './emoji.vue';
+import userName from './user-name.vue';
+import ellipsis from './ellipsis.vue';
+import time from './time.vue';
+import url from './url.vue';
+import loading from './loading.vue';
+import SequentialEntrance from './sequential-entrance.vue';
+import error from './error.vue';
+
+Vue.component('mfm', mfm);
+Vue.component('mk-acct', acct);
+Vue.component('mk-avatar', avatar);
+Vue.component('mk-emoji', emoji);
+Vue.component('mk-user-name', userName);
+Vue.component('mk-ellipsis', ellipsis);
+Vue.component('mk-time', time);
+Vue.component('mk-url', url);
+Vue.component('mk-loading', loading);
+Vue.component('mk-error', error);
+Vue.component('sequential-entrance', SequentialEntrance);
diff --git a/src/client/components/loading.vue b/src/client/components/loading.vue
new file mode 100644
index 0000000000000000000000000000000000000000..88d1ed77fac1f32be31b0ef3362495d07f476c33
--- /dev/null
+++ b/src/client/components/loading.vue
@@ -0,0 +1,30 @@
+<template>
+<div class="yxspomdl">
+	<fa :icon="faSpinner" pulse fixed-width class="icon"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faSpinner } from '@fortawesome/free-solid-svg-icons';
+
+export default Vue.extend({
+	data() {
+		return {
+			faSpinner
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.yxspomdl {
+	padding: 32px;
+	text-align: center;
+
+	> .icon {
+		font-size: 32px;
+		opacity: 0.5;
+	}
+}
+</style>
diff --git a/src/client/app/common/views/components/media-banner.vue b/src/client/components/media-banner.vue
similarity index 57%
rename from src/client/app/common/views/components/media-banner.vue
rename to src/client/components/media-banner.vue
index 4e459ad666dd6a588d480c6f1acb02fcac2e3cfa..088c11fab7a9f113f278afb16df3dcb8d9da9845 100644
--- a/src/client/app/common/views/components/media-banner.vue
+++ b/src/client/components/media-banner.vue
@@ -1,9 +1,9 @@
 <template>
 <div class="mk-media-banner">
 	<div class="sensitive" v-if="media.isSensitive && hide" @click="hide = false">
-		<span class="icon"><fa icon="exclamation-triangle"/></span>
+		<span class="icon"><fa :icon="faExclamationTriangle"/></span>
 		<b>{{ $t('sensitive') }}</b>
-		<span>{{ $t('click-to-show') }}</span>
+		<span>{{ $t('clickToShow') }}</span>
 	</div>
 	<div class="audio" v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'">
 		<audio class="audio"
@@ -27,10 +27,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../../i18n';
+import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n: i18n('common/views/components/media-banner.vue'),
+	i18n,
 	props: {
 		media: {
 			type: Object,
@@ -39,7 +40,8 @@ export default Vue.extend({
 	},
 	data() {
 		return {
-			hide: true
+			hide: true,
+			faExclamationTriangle
 		};
 	},
 	mounted() {
@@ -55,44 +57,53 @@ export default Vue.extend({
 })
 </script>
 
-<style lang="stylus" scoped>
-.mk-media-banner
-	width 100%
-	border-radius 4px
-	margin-top 4px
-	overflow hidden
+<style lang="scss" scoped>
+.mk-media-banner {
+	width: 100%;
+	border-radius: 4px;
+	margin-top: 4px;
+	overflow: hidden;
 
 	> .download,
-	> .sensitive
-		display flex
-		align-items center
-		font-size 12px
-		padding 8px 12px
-		white-space nowrap
+	> .sensitive {
+		display: flex;
+		align-items: center;
+		font-size: 12px;
+		padding: 8px 12px;
+		white-space: nowrap;
 
-		> *
-			display block
-
-		> b
-			overflow hidden
-			text-overflow ellipsis
+		> * {
+			display: block;
+		}
 
-		> *:not(:last-child)
-			margin-right .2em
+		> b {
+			overflow: hidden;
+			text-overflow: ellipsis;
+		}
 
-		> .icon
-			font-size 1.6em
+		> *:not(:last-child) {
+			margin-right: .2em;
+		}
 
-	> .download
-		background var(--noteAttachedFile)
+		> .icon {
+			font-size: 1.6em;
+		}
+	}
 
-	> .sensitive
-		background #111
-		color #fff
+	> .download {
+		background: var(--noteAttachedFile);
+	}
 
-	> .audio
-		.audio
-			display block
-			width 100%
+	> .sensitive {
+		background: #111;
+		color: #fff;
+	}
 
+	> .audio {
+		.audio {
+			display: block;
+			width: 100%;
+		}
+	}
+}
 </style>
diff --git a/src/client/components/media-image.vue b/src/client/components/media-image.vue
new file mode 100644
index 0000000000000000000000000000000000000000..5ae167d490acc2eecb7ffadfc4daf64303469ea3
--- /dev/null
+++ b/src/client/components/media-image.vue
@@ -0,0 +1,113 @@
+<template>
+<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false">
+	<div>
+		<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b>
+		<span>{{ $t('clickToShow') }}</span>
+	</div>
+</div>
+<a class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else
+	:href="image.url"
+	:style="style"
+	:title="image.name"
+	@click.prevent="onClick"
+>
+	<div v-if="image.type === 'image/gif'">GIF</div>
+</a>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
+import { getStaticImageUrl } from '../scripts/get-static-image-url';
+
+export default Vue.extend({
+	i18n,
+	props: {
+		image: {
+			type: Object,
+			required: true
+		},
+		raw: {
+			default: false
+		}
+	},
+	data() {
+		return {
+			hide: true,
+			faExclamationTriangle
+		};
+	},
+	computed: {
+		style(): any {
+			let url = `url(${
+				this.$store.state.device.disableShowingAnimatedImages
+					? getStaticImageUrl(this.image.thumbnailUrl)
+					: this.image.thumbnailUrl
+			})`;
+
+			if (this.$store.state.device.loadRemoteMedia) {
+				url = null;
+			} else if (this.raw || this.$store.state.device.loadRawImages) {
+				url = `url(${this.image.url})`;
+			}
+
+			return {
+				'background-color': this.image.properties.avgColor || 'transparent',
+				'background-image': url
+			};
+		}
+	},
+	methods: {
+		onClick() {
+			window.open(this.image.url, '_blank');
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.gqnyydlzavusgskkfvwvjiattxdzsqlf {
+	display: block;
+	cursor: zoom-in;
+	overflow: hidden;
+	width: 100%;
+	height: 100%;
+	background-position: center;
+	background-size: contain;
+	background-repeat: no-repeat;
+
+	> div {
+		background-color: var(--fg);
+		border-radius: 6px;
+		color: var(--secondary);
+		display: inline-block;
+		font-size: 14px;
+		font-weight: bold;
+		left: 12px;
+		opacity: .5;
+		padding: 0 6px;
+		text-align: center;
+		top: 12px;
+		pointer-events: none;
+	}
+}
+
+.qjewsnkgzzxlxtzncydssfbgjibiehcy {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	background: #111;
+	color: #fff;
+
+	> div {
+		display: table-cell;
+		text-align: center;
+		font-size: 12px;
+
+		> * {
+			display: block;
+		}
+	}
+}
+</style>
diff --git a/src/client/components/media-list.vue b/src/client/components/media-list.vue
new file mode 100644
index 0000000000000000000000000000000000000000..08722ff91a0a4923ef5df1997e5334e425ec1c98
--- /dev/null
+++ b/src/client/components/media-list.vue
@@ -0,0 +1,130 @@
+<template>
+<div class="mk-media-list">
+	<template v-for="media in mediaList.filter(media => !previewable(media))">
+		<x-banner :media="media" :key="media.id"/>
+	</template>
+	<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container">
+		<div :data-count="mediaList.filter(media => previewable(media)).length" ref="grid">
+			<template v-for="media in mediaList">
+				<x-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/>
+				<x-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/>
+			</template>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XBanner from './media-banner.vue';
+import XImage from './media-image.vue';
+import XVideo from './media-video.vue';
+
+export default Vue.extend({
+	components: {
+		XBanner,
+		XImage,
+		XVideo,
+	},
+	props: {
+		mediaList: {
+			required: true
+		},
+		raw: {
+			default: false
+		}
+	},
+	mounted() {
+		//#region for Safari bug
+		if (this.$refs.grid) {
+			this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px`
+				: '287px';
+		}
+		//#endregion
+	},
+	methods: {
+		previewable(file) {
+			return file.type.startsWith('video') || file.type.startsWith('image');
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-media-list {
+	> .gird-container {
+		position: relative;
+		width: 100%;
+		margin-top: 4px;
+
+		&:before {
+			content: '';
+			display: block;
+			padding-top: 56.25% // 16:9;
+		}
+
+		> div {
+			position: absolute;
+			top: 0;
+			right: 0;
+			bottom: 0;
+			left: 0;
+			display: grid;
+			grid-gap: 4px;
+
+			> * {
+				overflow: hidden;
+				border-radius: 4px;
+			}
+
+			&[data-count="1"] {
+				grid-template-rows: 1fr;
+			}
+
+			&[data-count="2"] {
+				grid-template-columns: 1fr 1fr;
+				grid-template-rows: 1fr;
+			}
+
+			&[data-count="3"] {
+				grid-template-columns: 1fr 0.5fr;
+				grid-template-rows: 1fr 1fr;
+
+				> *:nth-child(1) {
+					grid-row: 1 / 3;
+				}
+
+				> *:nth-child(3) {
+					grid-column: 2 / 3;
+					grid-row: 2 / 3;
+				}
+			}
+
+			&[data-count="4"] {
+				grid-template-columns: 1fr 1fr;
+				grid-template-rows: 1fr 1fr;
+			}
+
+			> *:nth-child(1) {
+				grid-column: 1 / 2;
+				grid-row: 1 / 2;
+			}
+
+			> *:nth-child(2) {
+				grid-column: 2 / 3;
+				grid-row: 1 / 2;
+			}
+
+			> *:nth-child(3) {
+				grid-column: 1 / 2;
+				grid-row: 2 / 3;
+			}
+
+			> *:nth-child(4) {
+				grid-column: 2 / 3;
+				grid-row: 2 / 3;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/app/mobile/views/components/media-video.vue b/src/client/components/media-video.vue
similarity index 51%
rename from src/client/app/mobile/views/components/media-video.vue
rename to src/client/components/media-video.vue
index 044bb4c106b4c7c7eb81d61e528031041dc67ee5..f96e90297603705ff4f61a9771a5dfef8acf737d 100644
--- a/src/client/app/mobile/views/components/media-video.vue
+++ b/src/client/components/media-video.vue
@@ -2,7 +2,7 @@
 <div class="icozogqfvdetwohsdglrbswgrejoxbdj" v-if="video.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false">
 	<div>
 		<b><fa icon="exclamation-triangle"/> {{ $t('sensitive') }}</b>
-		<span>{{ $t('click-to-show') }}</span>
+		<span>{{ $t('clickToShow') }}</span>
 	</div>
 </div>
 <a class="kkjnbbplepmiyuadieoenjgutgcmtsvu" v-else
@@ -12,16 +12,17 @@
 	:style="imageStyle"
 	:title="video.name"
 >
-	<fa :icon="['far', 'play-circle']"/>
+	<fa :icon="faPlayCircle"/>
 </a>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../../i18n';
+import { faPlayCircle } from '@fortawesome/free-regular-svg-icons';
+import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n: i18n('mobile/views/components/media-video.vue'),
+	i18n,
 	props: {
 		video: {
 			type: Object,
@@ -30,7 +31,8 @@ export default Vue.extend({
 	},
 	data() {
 		return {
-			hide: true
+			hide: true,
+			faPlayCircle
 		};
 	},
 	computed: {
@@ -43,32 +45,35 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.kkjnbbplepmiyuadieoenjgutgcmtsvu
-	display flex
-	justify-content center
-	align-items center
+<style lang="scss" scoped>
+.kkjnbbplepmiyuadieoenjgutgcmtsvu {
+	display: flex;
+	justify-content: center;
+	align-items: center;
 
-	font-size 3.5em
-	overflow hidden
-	background-position center
-	background-size cover
-	width 100%
-	height 100%
+	font-size: 3.5em;
+	overflow: hidden;
+	background-position: center;
+	background-size: cover;
+	width: 100%;
+	height: 100%;
+}
 
-.icozogqfvdetwohsdglrbswgrejoxbdj
-	display flex
-	justify-content center
-	align-items center
-	background #111
-	color #fff
+.icozogqfvdetwohsdglrbswgrejoxbdj {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	background: #111;
+	color: #fff;
 
-	> div
-		display table-cell
-		text-align center
-		font-size 12px
-
-		> b
-			display block
+	> div {
+		display: table-cell;
+		text-align: center;
+		font-size: 12px;
 
+		> b {
+			display: block;
+		}
+	}
+}
 </style>
diff --git a/src/client/app/common/views/components/mention.vue b/src/client/components/mention.vue
similarity index 58%
rename from src/client/app/common/views/components/mention.vue
rename to src/client/components/mention.vue
index 4e9f9e90d6d40159f370c2ba62fea099212677b1..06dcf1288787ee9a896ae888c049de457660cea9 100644
--- a/src/client/app/common/views/components/mention.vue
+++ b/src/client/components/mention.vue
@@ -1,27 +1,27 @@
 <template>
 <router-link class="ldlomzub" :to="url" v-user-preview="canonical" v-if="url.startsWith('/')">
-	<span class="me" v-if="isMe">{{ $t('@.you') }}</span>
+	<span class="me" v-if="isMe">{{ $t('you') }}</span>
 	<span class="main">
 		<span class="username">@{{ username }}</span>
-		<span class="host" :class="{ fade: $store.state.settings.contrastedAcct }" v-if="(host != localHost) || $store.state.settings.showFullAcct">@{{ toUnicode(host) }}</span>
+		<span class="host" v-if="(host != localHost) || $store.state.settings.showFullAcct">@{{ toUnicode(host) }}</span>
 	</span>
 </router-link>
 <a class="ldlomzub" :href="url" target="_blank" rel="noopener" v-else>
 	<span class="main">
 		<span class="username">@{{ username }}</span>
-		<span class="host" :class="{ fade: $store.state.settings.contrastedAcct }">@{{ toUnicode(host) }}</span>
+		<span class="host">@{{ toUnicode(host) }}</span>
 	</span>
 </a>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../../i18n';
+import i18n from '../i18n';
 import { toUnicode } from 'punycode';
-import { host as localHost } from '../../../config';
+import { host as localHost } from '../config';
 
 export default Vue.extend({
-	i18n: i18n(),
+	i18n,
 	props: {
 		username: {
 			type: String,
@@ -62,26 +62,21 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.ldlomzub
-	color var(--mfmMention)
-
-	> .me
-		pointer-events none
-		user-select none
-		padding 0 4px
-		background var(--mfmMention)
-		border solid var(--lineWidth) var(--mfmMention)
-		border-radius 4px 0 0 4px
-		color var(--mfmMentionForeground)
-
-		& + .main
-			padding 0 4px
-			border solid var(--lineWidth) var(--mfmMention)
-			border-radius 0 4px 4px 0
-
-	> .main
-		> .host.fade
-			opacity 0.5
+<style lang="scss" scoped>
+.ldlomzub {
+	color: var(--mention);
+	
+	> .me {
+		pointer-events: none;
+		user-select: none;
+		font-size: 70%;
+		vertical-align: top;
+	}
 
+	> .main {
+		> .host {
+			opacity: 0.5;
+		}
+	}
+}
 </style>
diff --git a/src/client/components/menu.vue b/src/client/components/menu.vue
new file mode 100644
index 0000000000000000000000000000000000000000..c1c5ceaee721d82e30debbca0b5b2c251adea910
--- /dev/null
+++ b/src/client/components/menu.vue
@@ -0,0 +1,165 @@
+<template>
+<x-popup :source="source" :no-center="noCenter" :fixed="fixed" :width="width" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }">
+	<sequential-entrance class="rrevdjwt" :class="{ left: align === 'left' }" :delay="15" :direction="direction">
+		<template v-for="(item, i) in items.filter(item => item !== undefined)">
+			<div v-if="item === null" class="divider" :key="i" :data-index="i"></div>
+			<span v-else-if="item.type === 'label'" class="label item" :key="i" :data-index="i">
+				<span>{{ item.text }}</span>
+			</span>
+			<router-link v-else-if="item.type === 'link'" :to="item.to" @click.native="close()" :tabindex="i" class="_button item" :key="i" :data-index="i">
+				<fa v-if="item.icon" :icon="item.icon" fixed-width/>
+				<mk-avatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
+				<span>{{ item.text }}</span>
+				<i v-if="item.indicate"><fa :icon="faCircle"/></i>
+			</router-link>
+			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" @click="close()" :tabindex="i" class="_button item" :key="i" :data-index="i">
+				<fa v-if="item.icon" :icon="item.icon" fixed-width/>
+				<span>{{ item.text }}</span>
+				<i v-if="item.indicate"><fa :icon="faCircle"/></i>
+			</a>
+			<button v-else-if="item.type === 'user'" @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i" :data-index="i">
+				<mk-avatar :user="item.user" class="avatar"/><mk-user-name :user="item.user"/>
+				<i v-if="item.indicate"><fa :icon="faCircle"/></i>
+			</button>
+			<button v-else @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i" :data-index="i">
+				<fa v-if="item.icon" :icon="item.icon" fixed-width/>
+				<mk-avatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
+				<span>{{ item.text }}</span>
+				<i v-if="item.indicate"><fa :icon="faCircle"/></i>
+			</button>
+		</template>
+	</sequential-entrance>
+</x-popup>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faCircle } from '@fortawesome/free-solid-svg-icons';
+import XPopup from './popup.vue';
+
+export default Vue.extend({
+	components: {
+		XPopup
+	},
+	props: {
+		source: {
+			required: true
+		},
+		items: {
+			type: Array,
+			required: true
+		},
+		align: {
+			type: String,
+			required: false
+		},
+		noCenter: {
+			type: Boolean,
+			required: false
+		},
+		fixed: {
+			type: Boolean,
+			required: false
+		},
+		width: {
+			type: Number,
+			required: false
+		},
+		direction: {
+			type: String,
+			required: false
+		},
+	},
+	data() {
+		return {
+			faCircle
+		};
+	},
+	methods: {
+		clicked(fn) {
+			fn();
+			this.close();
+		},
+		close() {
+			this.$refs.popup.close();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+@keyframes blink {
+	0% { opacity: 1; }
+	30% { opacity: 1; }
+	90% { opacity: 0; }
+}
+
+.rrevdjwt {
+	padding: 8px 0;
+
+	&.left {
+		> .item {
+			text-align: left;
+		}
+	}
+
+	> .item {
+		display: block;
+		padding: 8px 16px;
+		width: 100%;
+		box-sizing: border-box;
+		white-space: nowrap;
+		font-size: 0.9em;
+		text-align: center;
+		overflow: hidden;
+		text-overflow: ellipsis;
+
+		&:hover {
+			color: #fff;
+			background: var(--accent);
+			text-decoration: none;
+		}
+
+		&:active {
+			color: #fff;
+			background: var(--accentDarken);
+		}
+
+		&.label {
+			pointer-events: none;
+			font-size: 0.7em;
+			padding-bottom: 4px;
+
+			> span {
+				opacity: 0.7;
+			}
+		}
+
+		> [data-icon] {
+			margin-right: 4px;
+			width: 20px;
+		}
+
+		> .avatar {
+			margin-right: 4px;
+			width: 20px;
+			height: 20px;
+		}
+
+		> i {
+			position: absolute;
+			top: 5px;
+			left: 13px;
+			color: var(--accent);
+			font-size: 12px;
+			animation: blink 1s infinite;
+		}
+	}
+
+	> .divider {
+		margin: 8px 0;
+		height: 1px;
+		background: var(--divider);
+	}
+}
+</style>
diff --git a/src/client/app/common/views/components/mfm.ts b/src/client/components/mfm.ts
similarity index 80%
rename from src/client/app/common/views/components/mfm.ts
rename to src/client/components/mfm.ts
index 561c3d8e30e11408f5dd671e6a62b9e40ad66433..932beb907f5172ba2f756066bdaf16536f0bff3d 100644
--- a/src/client/app/common/views/components/mfm.ts
+++ b/src/client/components/mfm.ts
@@ -1,20 +1,13 @@
 import Vue, { VNode } from 'vue';
-import { length } from 'stringz';
-import { MfmForest } from '../../../../../mfm/types';
-import { parse, parsePlain } from '../../../../../mfm/parse';
+import { MfmForest } from '../../mfm/types';
+import { parse, parsePlain } from '../../mfm/parse';
 import MkUrl from './url.vue';
 import MkMention from './mention.vue';
-import { concat, sum } from '../../../../../prelude/array';
+import { concat } from '../../prelude/array';
 import MkFormula from './formula.vue';
 import MkCode from './code.vue';
 import MkGoogle from './google.vue';
-import { host } from '../../../config';
-import { preorderF, countNodesF } from '../../../../../prelude/tree';
-
-function sumTextsLength(ts: MfmForest): number {
-	const textNodes = preorderF(ts).filter(n => n.type === 'text');
-	return sum(textNodes.map(x => length(x.props.text)));
-}
+import { host } from '../config';
 
 export default Vue.component('misskey-flavored-markdown', {
 	props: {
@@ -52,9 +45,6 @@ export default Vue.component('misskey-flavored-markdown', {
 
 		const ast = (this.plain ? parsePlain : parse)(this.text);
 
-		let bigCount = 0;
-		let motionCount = 0;
-
 		const genEl = (ast: MfmForest) => concat(ast.map((token): VNode[] => {
 			switch (token.node.type) {
 				case 'text': {
@@ -87,14 +77,11 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'big': {
-					bigCount++;
-					const isLong = sumTextsLength(token.children) > 15 || countNodesF(token.children) > 5;
-					const isMany = bigCount > 3;
 					return (createElement as any)('strong', {
 						attrs: {
-							style: `display: inline-block; font-size: ${ isMany ? '100%' : '150%' };`
+							style: `display: inline-block; font-size: 150% };`
 						},
-						directives: [this.$store.state.settings.disableAnimatedMfm || isLong || isMany ? {} : {
+						directives: [this.$store.state.settings.disableAnimatedMfm ? {} : {
 							name: 'animate-css',
 							value: { classes: 'tada', iteration: 'infinite' }
 						}]
@@ -118,14 +105,11 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'motion': {
-					motionCount++;
-					const isLong = sumTextsLength(token.children) > 15 || countNodesF(token.children) > 5;
-					const isMany = motionCount > 5;
 					return (createElement as any)('span', {
 						attrs: {
 							style: 'display: inline-block;'
 						},
-						directives: [this.$store.state.settings.disableAnimatedMfm || isLong || isMany ? {} : {
+						directives: [this.$store.state.settings.disableAnimatedMfm ? {} : {
 							name: 'animate-css',
 							value: { classes: 'rubberBand', iteration: 'infinite' }
 						}]
@@ -133,14 +117,11 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'spin': {
-					motionCount++;
-					const isLong = sumTextsLength(token.children) > 10 || countNodesF(token.children) > 5;
-					const isMany = motionCount > 5;
 					const direction =
 						token.node.props.attr == 'left' ? 'reverse' :
 						token.node.props.attr == 'alternate' ? 'alternate' :
 						'normal';
-					const style = (this.$store.state.settings.disableAnimatedMfm || isLong || isMany)
+					const style = (this.$store.state.settings.disableAnimatedMfm)
 						? ''
 						: `animation: spin 1.5s linear infinite; animation-direction: ${direction};`;
 					return (createElement as any)('span', {
@@ -151,12 +132,9 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'jump': {
-					motionCount++;
-					const isLong = sumTextsLength(token.children) > 30 || countNodesF(token.children) > 5;
-					const isMany = motionCount > 5;
 					return (createElement as any)('span', {
 						attrs: {
-							style: (this.$store.state.settings.disableAnimatedMfm || isLong || isMany) ? 'display: inline-block;' : 'display: inline-block; animation: jump 0.75s linear infinite;'
+							style: (this.$store.state.settings.disableAnimatedMfm) ? 'display: inline-block;' : 'display: inline-block; animation: jump 0.75s linear infinite;'
 						},
 					}, genEl(token.children));
 				}
@@ -177,7 +155,7 @@ export default Vue.component('misskey-flavored-markdown', {
 							rel: 'nofollow noopener',
 						},
 						attrs: {
-							style: 'color:var(--mfmUrl);'
+							style: 'color:var(--link);'
 						}
 					})];
 				}
@@ -190,7 +168,7 @@ export default Vue.component('misskey-flavored-markdown', {
 							rel: 'nofollow noopener',
 							target: '_blank',
 							title: token.node.props.url,
-							style: 'color:var(--mfmLink);'
+							style: 'color:var(--link);'
 						}
 					}, genEl(token.children))];
 				}
@@ -210,7 +188,7 @@ export default Vue.component('misskey-flavored-markdown', {
 						key: Math.random(),
 						attrs: {
 							to: this.isNote ? `/tags/${encodeURIComponent(token.node.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.node.props.hashtag)}`,
-							style: 'color:var(--mfmHashtag);'
+							style: 'color:var(--hashtag);'
 						}
 					}, `#${token.node.props.hashtag}`)];
 				}
diff --git a/src/client/components/misskey-flavored-markdown.vue b/src/client/components/misskey-flavored-markdown.vue
new file mode 100644
index 0000000000000000000000000000000000000000..c8eee8c1269d8c8361e2fc57ce86f21ca1a4bddb
--- /dev/null
+++ b/src/client/components/misskey-flavored-markdown.vue
@@ -0,0 +1,35 @@
+<template>
+<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }" v-once/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import MfmCore from './mfm';
+
+export default Vue.extend({
+	components: {
+		MfmCore
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.havbbuyv {
+	white-space: pre-wrap;
+
+	&.nowrap {
+		white-space: pre;
+		word-wrap: normal; // https://codeday.me/jp/qa/20190424/690106.html
+		overflow: hidden;
+		text-overflow: ellipsis;
+	}
+
+	::v-deep .quote {
+		display: block;
+		margin: 8px;
+		padding: 6px 0 6px 12px;
+		color: var(--mfmQuote);
+		border-left: solid 3px var(--mfmQuoteLine);
+	}
+}
+</style>
diff --git a/src/client/components/modal.vue b/src/client/components/modal.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b7e6a336d751968918418f148cd5837cc5842904
--- /dev/null
+++ b/src/client/components/modal.vue
@@ -0,0 +1,84 @@
+<template>
+<div class="mk-modal">
+	<transition name="bg-fade" appear>
+		<div class="bg" ref="bg" v-if="show" @click="close()"></div>
+	</transition>
+	<transition name="modal" appear @after-leave="() => { $emit('closed'); destroyDom(); }">
+		<div class="content" ref="content" v-if="show" @click.self="close()"><slot></slot></div>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: {
+	},
+	data() {
+		return {
+			show: true,
+		};
+	},
+	methods: {
+		close() {
+			this.show = false;
+			(this.$refs.bg as any).style.pointerEvents = 'none';
+			(this.$refs.content as any).style.pointerEvents = 'none';
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.modal-enter-active, .modal-leave-active {
+	transition: opacity 0.3s, transform 0.3s !important;
+}
+.modal-enter, .modal-leave-to {
+	opacity: 0;
+	transform: scale(0.9);
+}
+
+.bg-fade-enter-active, .bg-fade-leave-active {
+	transition: opacity 0.3s !important;
+}
+.bg-fade-enter, .bg-fade-leave-to {
+	opacity: 0;
+}
+
+.mk-modal {
+	> .bg {
+		position: fixed;
+		top: 0;
+		left: 0;
+		z-index: 10000;
+		width: 100%;
+		height: 100%;
+		background: var(--modalBg)
+	}
+
+	> .content {
+		position: fixed;
+		z-index: 10000;
+		top: 0;
+		bottom: 0;
+		left: 0;
+		right: 0;
+		max-width: calc(100% - 16px);
+		max-height: calc(100% - 16px);
+		overflow: auto;
+		margin: auto;
+
+		::v-deep > * {
+			position: absolute;
+			top: 0;
+			bottom: 0;
+			left: 0;
+			right: 0;
+			margin: auto;
+			max-height: 100%;
+			max-width: 100%;
+		}
+	}
+}
+</style>
diff --git a/src/client/components/note-header.vue b/src/client/components/note-header.vue
new file mode 100644
index 0000000000000000000000000000000000000000..30ecb80834c220fab9da9aafe4f5a74708f6cea8
--- /dev/null
+++ b/src/client/components/note-header.vue
@@ -0,0 +1,99 @@
+<template>
+<header class="kkwtjztg">
+	<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">
+		<mk-user-name :user="note.user"/>
+	</router-link>
+	<span class="is-bot" v-if="note.user.isBot">bot</span>
+	<span class="username"><mk-acct :user="note.user"/></span>
+	<div class="info">
+		<span class="mobile" v-if="note.viaMobile"><fa :icon="faMobileAlt"/></span>
+		<router-link class="created-at" :to="note | notePage">
+			<mk-time :time="note.createdAt"/>
+		</router-link>
+		<span class="visibility" v-if="note.visibility != 'public'">
+			<fa v-if="note.visibility == 'home'" :icon="faHome"/>
+			<fa v-if="note.visibility == 'followers'" :icon="faUnlock"/>
+			<fa v-if="note.visibility == 'specified'" :icon="faEnvelope"/>
+		</span>
+	</div>
+</header>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faHome, faUnlock, faEnvelope, faMobileAlt } from '@fortawesome/free-solid-svg-icons';
+
+export default Vue.extend({
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			faHome, faUnlock, faEnvelope, faMobileAlt
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.kkwtjztg {
+	display: flex;
+	align-items: baseline;
+	white-space: nowrap;
+
+	> .name {
+		display: block;
+		margin: 0 .5em 0 0;
+		padding: 0;
+		overflow: hidden;
+		color: var(--noteHeaderName);
+		font-size: 1em;
+		font-weight: bold;
+		text-decoration: none;
+		text-overflow: ellipsis;
+
+		&:hover {
+			text-decoration: underline;
+		}
+	}
+
+	> .is-bot {
+		flex-shrink: 0;
+		align-self: center;
+		margin: 0 .5em 0 0;
+		padding: 1px 6px;
+		font-size: 80%;
+		color: var(--noteHeaderBadgeFg);
+		background: var(--noteHeaderBadgeBg);
+		border-radius: 3px;
+	}
+
+	> .username {
+		margin: 0 .5em 0 0;
+		overflow: hidden;
+		text-overflow: ellipsis;
+		color: var(--noteHeaderAcct);
+	}
+
+	> .info {
+		margin-left: auto;
+		font-size: 0.9em;
+
+		> * {
+			color: var(--noteHeaderInfo);
+		}
+
+		> .mobile {
+			margin-right: 8px;
+		}
+
+		> .visibility {
+			margin-left: 8px;
+		}
+	}
+}
+</style>
diff --git a/src/client/app/common/views/components/note-menu.vue b/src/client/components/note-menu.vue
similarity index 61%
rename from src/client/app/common/views/components/note-menu.vue
rename to src/client/components/note-menu.vue
index 1dcf58dd36b9097193e8dbcd8d3874420b07a12b..dd7b062f159cc1cccddd895be32a7284d75383ce 100644
--- a/src/client/app/common/views/components/note-menu.vue
+++ b/src/client/components/note-menu.vue
@@ -1,18 +1,21 @@
 <template>
-<div style="position:initial">
-	<mk-menu :source="source" :items="items" @closed="closed"/>
-</div>
+<x-menu :source="source" :items="items" @closed="closed"/>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../../i18n';
-import { url } from '../../../config';
-import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
-import { faCopy, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
+import { faStar, faLink, faThumbtack, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
+import { faCopy, faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
+import i18n from '../i18n';
+import { url } from '../config';
+import copyToClipboard from '../scripts/copy-to-clipboard';
+import XMenu from './menu.vue';
 
 export default Vue.extend({
-	i18n: i18n('common/views/components/note-menu.vue'),
+	i18n,
+	components: {
+		XMenu
+	},
 	props: ['note', 'source'],
 	data() {
 		return {
@@ -24,35 +27,27 @@ export default Vue.extend({
 		items(): any[] {
 			if (this.$store.getters.isSignedIn) {
 				return [{
-					icon: 'at',
-					text: this.$t('mention'),
-					action: this.mention
-				}, null, {
-					icon: 'info-circle',
-					text: this.$t('detail'),
-					action: this.detail
-				}, {
 					icon: faCopy,
-					text: this.$t('copy-content'),
+					text: this.$t('copyContent'),
 					action: this.copyContent
 				}, {
-					icon: 'link',
-					text: this.$t('copy-link'),
+					icon: faLink,
+					text: this.$t('copyLink'),
 					action: this.copyLink
 				}, this.note.uri ? {
-					icon: 'external-link-square-alt',
-					text: this.$t('remote'),
+					icon: faExternalLinkSquareAlt,
+					text: this.$t('showOnRemote'),
 					action: () => {
 						window.open(this.note.uri, '_blank');
 					}
 				} : undefined,
 				null,
 				this.isFavorited ? {
-					icon: 'star',
+					icon: faStar,
 					text: this.$t('unfavorite'),
 					action: () => this.toggleFavorite(false)
 				} : {
-					icon: 'star',
+					icon: faStar,
 					text: this.$t('favorite'),
 					action: () => this.toggleFavorite(true)
 				},
@@ -66,23 +61,18 @@ export default Vue.extend({
 					action: () => this.toggleWatch(true)
 				} : undefined,
 				this.note.userId == this.$store.state.i.id ? (this.$store.state.i.pinnedNoteIds || []).includes(this.note.id) ? {
-					icon: 'thumbtack',
+					icon: faThumbtack,
 					text: this.$t('unpin'),
 					action: () => this.togglePin(false)
 				} : {
-					icon: 'thumbtack',
+					icon: faThumbtack,
 					text: this.$t('pin'),
 					action: () => this.togglePin(true)
 				} : undefined,
-				...(this.note.userId == this.$store.state.i.id || this.$store.state.i.isAdmin || this.$store.state.i.isModerator ? [
+				...(this.note.userId == this.$store.state.i.id ? [
 					null,
-					this.note.userId == this.$store.state.i.id ? {
-						icon: 'undo-alt',
-						text: this.$t('delete-and-edit'),
-						action: this.deleteAndEdit
-					} : undefined,
 					{
-						icon: ['far', 'trash-alt'],
+						icon: faTrashAlt,
 						text: this.$t('delete'),
 						action: this.del
 					}]
@@ -91,20 +81,16 @@ export default Vue.extend({
 				.filter(x => x !== undefined);
 			} else {
 				return [{
-					icon: 'info-circle',
-					text: this.$t('detail'),
-					action: this.detail
-				}, {
 					icon: faCopy,
-					text: this.$t('copy-content'),
+					text: this.$t('copyContent'),
 					action: this.copyContent
 				}, {
-					icon: 'link',
-					text: this.$t('copy-link'),
+					icon: faLink,
+					text: this.$t('copyLink'),
 					action: this.copyLink
 				}, this.note.uri ? {
-					icon: 'external-link-square-alt',
-					text: this.$t('remote'),
+					icon: faExternalLinkSquareAlt,
+					text: this.$t('showOnRemote'),
 					action: () => {
 						window.open(this.note.uri, '_blank');
 					}
@@ -124,19 +110,11 @@ export default Vue.extend({
 	},
 
 	methods: {
-		mention() {
-			this.$post({ mention: this.note.user });
-		},
-
-		detail() {
-			this.$router.push(`/notes/${this.note.id}`);
-		},
-
 		copyContent() {
 			copyToClipboard(this.note.text);
 			this.$root.dialog({
 				type: 'success',
-				splash: true
+				iconOnly: true, autoClose: true
 			});
 		},
 
@@ -144,7 +122,7 @@ export default Vue.extend({
 			copyToClipboard(`${url}/notes/${this.note.id}`);
 			this.$root.dialog({
 				type: 'success',
-				splash: true
+				iconOnly: true, autoClose: true
 			});
 		},
 
@@ -154,14 +132,15 @@ export default Vue.extend({
 			}).then(() => {
 				this.$root.dialog({
 					type: 'success',
-					splash: true
+					iconOnly: true, autoClose: true
 				});
+				this.$emit('closed');
 				this.destroyDom();
 			}).catch(e => {
 				if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
 					this.$root.dialog({
 						type: 'error',
-						text: this.$t('pin-limit-exceeded')
+						text: this.$t('pinLimitExceeded')
 					});
 				}
 			});
@@ -170,7 +149,7 @@ export default Vue.extend({
 		del() {
 			this.$root.dialog({
 				type: 'warning',
-				text: this.$t('delete-confirm'),
+				text: this.$t('noteDeleteConfirm'),
 				showCancelButton: true
 			}).then(({ canceled }) => {
 				if (canceled) return;
@@ -178,38 +157,21 @@ export default Vue.extend({
 				this.$root.api('notes/delete', {
 					noteId: this.note.id
 				}).then(() => {
+					this.$emit('closed');
 					this.destroyDom();
 				});
 			});
 		},
 
-		deleteAndEdit() {
-			this.$root.dialog({
-				type: 'warning',
-				text: this.$t('delete-and-edit-confirm'),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-				this.$root.api('notes/delete', {
-					noteId: this.note.id
-				}).then(() => {
-					this.destroyDom();
-				});
-				this.$post({
-					initialNote: this.note,
-					reply: this.note.reply,
-				});
-			});
-		},
-
 		toggleFavorite(favorite: boolean) {
 			this.$root.api(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
 				noteId: this.note.id
 			}).then(() => {
 				this.$root.dialog({
 					type: 'success',
-					splash: true
+					iconOnly: true, autoClose: true
 				});
+				this.$emit('closed');
 				this.destroyDom();
 			});
 		},
@@ -220,7 +182,7 @@ export default Vue.extend({
 			}).then(() => {
 				this.$root.dialog({
 					type: 'success',
-					splash: true
+					iconOnly: true, autoClose: true
 				});
 				this.destroyDom();
 			});
diff --git a/src/client/components/note-preview.vue b/src/client/components/note-preview.vue
new file mode 100644
index 0000000000000000000000000000000000000000..17ff5be8683421a393b863d830f306f4b3dbb304
--- /dev/null
+++ b/src/client/components/note-preview.vue
@@ -0,0 +1,121 @@
+<template>
+<div class="yohlumlkhizgfkvvscwfcrcggkotpvry">
+	<mk-avatar class="avatar" :user="note.user"/>
+	<div class="main">
+		<x-note-header class="header" :note="note" :mini="true"/>
+		<div class="body">
+			<p v-if="note.cw != null" class="cw">
+				<span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
+				<x-cw-button v-model="showContent" :note="note"/>
+			</p>
+			<div class="content" v-show="note.cw == null || showContent">
+				<x-sub-note-content class="text" :note="note"/>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XNoteHeader from './note-header.vue';
+import XSubNoteContent from './sub-note-content.vue';
+import XCwButton from './cw-button.vue';
+
+export default Vue.extend({
+	components: {
+		XNoteHeader,
+		XSubNoteContent,
+		XCwButton,
+	},
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			showContent: false
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.yohlumlkhizgfkvvscwfcrcggkotpvry {
+	display: flex;
+	margin: 0;
+	padding: 0;
+	overflow: hidden;
+	font-size: 10px;
+
+	@media (min-width: 350px) {
+		font-size: 12px;
+	}
+
+	@media (min-width: 500px) {
+		font-size: 14px;
+	}
+
+	> .avatar {
+
+		@media (min-width: 350px) {
+			margin: 0 10px 0 0;
+			width: 44px;
+			height: 44px;
+		}
+
+		@media (min-width: 500px) {
+			margin: 0 12px 0 0;
+			width: 48px;
+			height: 48px;
+		}
+	}
+
+	> .avatar {
+		flex-shrink: 0;
+		display: block;
+		margin: 0 10px 0 0;
+		width: 40px;
+		height: 40px;
+		border-radius: 8px;
+	}
+
+	> .main {
+		flex: 1;
+		min-width: 0;
+
+		> .header {
+			margin-bottom: 2px;
+		}
+
+		> .body {
+
+			> .cw {
+				cursor: default;
+				display: block;
+				margin: 0;
+				padding: 0;
+				overflow-wrap: break-word;
+				color: var(--noteText);
+
+				> .text {
+					margin-right: 8px;
+				}
+			}
+
+			> .content {
+				> .text {
+					cursor: default;
+					margin: 0;
+					padding: 0;
+					color: var(--subNoteText);
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/components/note.sub.vue b/src/client/components/note.sub.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7f6f97289655d6e59c6093ef0b6827fb8081f2cb
--- /dev/null
+++ b/src/client/components/note.sub.vue
@@ -0,0 +1,108 @@
+<template>
+<div class="zlrxdaqttccpwhpaagdmkawtzklsccam">
+	<mk-avatar class="avatar" :user="note.user"/>
+	<div class="main">
+		<x-note-header class="header" :note="note" :mini="true"/>
+		<div class="body">
+			<p v-if="note.cw != null" class="cw">
+				<mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" />
+				<x-cw-button v-model="showContent" :note="note"/>
+			</p>
+			<div class="content" v-show="note.cw == null || showContent">
+				<x-sub-note-content class="text" :note="note"/>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XNoteHeader from './note-header.vue';
+import XSubNoteContent from './sub-note-content.vue';
+import XCwButton from './cw-button.vue';
+
+export default Vue.extend({
+	components: {
+		XNoteHeader,
+		XSubNoteContent,
+		XCwButton,
+	},
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+		// TODO
+		truncate: {
+			type: Boolean,
+			default: true
+		}
+	},
+
+	inject: {
+		narrow: {
+			default: false
+		}
+	},
+
+	data() {
+		return {
+			showContent: false
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.zlrxdaqttccpwhpaagdmkawtzklsccam {
+	display: flex;
+	padding: 16px 32px;
+	font-size: 0.9em;
+	background: rgba(0, 0, 0, 0.03);
+
+	@media (max-width: 450px) {
+		padding: 14px 16px;
+	}
+
+	> .avatar {
+		flex-shrink: 0;
+		display: block;
+		margin: 0 8px 0 0;
+		width: 38px;
+		height: 38px;
+		border-radius: 8px;
+	}
+
+	> .main {
+		flex: 1;
+		min-width: 0;
+
+		> .header {
+			margin-bottom: 2px;
+		}
+
+		> .body {
+			> .cw {
+				cursor: default;
+				display: block;
+				margin: 0;
+				padding: 0;
+				overflow-wrap: break-word;
+
+				> .text {
+					margin-right: 8px;
+				}
+			}
+
+			> .content {
+				> .text {
+					margin: 0;
+					padding: 0;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
new file mode 100644
index 0000000000000000000000000000000000000000..8b3fa61a6533a446d600814e4f3f8467e80ca80f
--- /dev/null
+++ b/src/client/components/note.vue
@@ -0,0 +1,729 @@
+<template>
+<div
+	class="note _panel"
+	v-show="appearNote.deletedAt == null && !hideThisNote"
+	:tabindex="appearNote.deletedAt == null ? '-1' : null"
+	:class="{ renote: isRenote }"
+	v-hotkey="keymap"
+	v-size="[{ max: 500 }, { max: 450 }, { max: 350 }, { max: 300 }]"
+>
+	<x-sub v-for="note in conversation" :key="note.id" :note="note"/>
+	<x-sub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
+	<div class="pinned" v-if="pinned"><fa :icon="faThumbtack"/> {{ $t('pinnedNote') }}</div>
+	<div class="renote" v-if="isRenote">
+		<mk-avatar class="avatar" :user="note.user"/>
+		<fa :icon="faRetweet"/>
+		<i18n path="renotedBy" tag="span">
+			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId" place="user">
+				<mk-user-name :user="note.user"/>
+			</router-link>
+		</i18n>
+		<div class="info">
+			<mk-time :time="note.createdAt"/>
+			<span class="visibility" v-if="note.visibility != 'public'">
+				<fa v-if="note.visibility == 'home'" :icon="faHome"/>
+				<fa v-if="note.visibility == 'followers'" :icon="faUnlock"/>
+				<fa v-if="note.visibility == 'specified'" :icon="faEnvelope"/>
+			</span>
+		</div>
+	</div>
+	<article class="article">
+		<mk-avatar class="avatar" :user="appearNote.user"/>
+		<div class="main">
+			<x-note-header class="header" :note="appearNote" :mini="true"/>
+			<div class="body" v-if="appearNote.deletedAt == null">
+				<p v-if="appearNote.cw != null" class="cw">
+				<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" />
+					<x-cw-button v-model="showContent" :note="appearNote"/>
+				</p>
+				<div class="content" v-show="appearNote.cw == null || showContent">
+					<div class="text">
+						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
+						<router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><fa :icon="faReply"/></router-link>
+						<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
+						<a class="rp" v-if="appearNote.renote != null">RN:</a>
+					</div>
+					<div class="files" v-if="appearNote.files.length > 0">
+						<x-media-list :media-list="appearNote.files"/>
+					</div>
+					<x-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/>
+					<x-url-preview v-for="url in urls" :url="url" :key="url" :compact="true" class="url-preview"/>
+					<div class="renote" v-if="appearNote.renote"><x-note-preview :note="appearNote.renote"/></div>
+				</div>
+			</div>
+			<footer v-if="appearNote.deletedAt == null" class="footer">
+				<x-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
+				<button @click="reply()" class="button _button">
+					<template v-if="appearNote.reply"><fa :icon="faReplyAll"/></template>
+					<template v-else><fa :icon="faReply"/></template>
+					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
+				</button>
+				<button v-if="['public', 'home'].includes(appearNote.visibility)" @click="renote()" class="button _button" ref="renoteButton">
+					<fa :icon="faRetweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
+				</button>
+				<button v-else class="button _button">
+					<fa :icon="faBan"/>
+				</button>
+				<button v-if="!isMyNote && appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
+					<fa :icon="faPlus"/>
+				</button>
+				<button v-if="!isMyNote && appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton">
+					<fa :icon="faMinus"/>
+				</button>
+				<button class="button _button" @click="menu()" ref="menuButton">
+					<fa :icon="faEllipsisH"/>
+				</button>
+			</footer>
+			<div class="deleted" v-if="appearNote.deletedAt != null">{{ $t('deleted') }}</div>
+		</div>
+	</article>
+	<x-sub v-for="note in replies" :key="note.id" :note="note"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan } from '@fortawesome/free-solid-svg-icons';
+import { parse } from '../../mfm/parse';
+import { sum, unique } from '../../prelude/array';
+import i18n from '../i18n';
+import XSub from './note.sub.vue';
+import XNoteHeader from './note-header.vue';
+import XNotePreview from './note-preview.vue';
+import XReactionsViewer from './reactions-viewer.vue';
+import XMediaList from './media-list.vue';
+import XCwButton from './cw-button.vue';
+import XPoll from './poll.vue';
+import XUrlPreview from './url-preview.vue';
+import MkNoteMenu from './note-menu.vue';
+import MkReactionPicker from './reaction-picker.vue';
+import MkRenotePicker from './renote-picker.vue';
+import pleaseLogin from '../scripts/please-login';
+
+function focus(el, fn) {
+	const target = fn(el);
+	if (target) {
+		if (target.hasAttribute('tabindex')) {
+			target.focus();
+		} else {
+			focus(target, fn);
+		}
+	}
+}
+
+export default Vue.extend({
+	i18n,
+	
+	components: {
+		XSub,
+		XNoteHeader,
+		XNotePreview,
+		XReactionsViewer,
+		XMediaList,
+		XCwButton,
+		XPoll,
+		XUrlPreview,
+	},
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+		detail: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		pinned: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+
+	data() {
+		return {
+			connection: null,
+			conversation: [],
+			replies: [],
+			showContent: false,
+			hideThisNote: false,
+			openingMenu: false,
+			faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan
+		};
+	},
+
+	computed: {
+		keymap(): any {
+			return {
+				'r': () => this.reply(true),
+				'e|a|plus': () => this.react(true),
+				'q': () => this.renote(true),
+				'f|b': this.favorite,
+				'delete|ctrl+d': this.del,
+				'ctrl+q': this.renoteDirectly,
+				'up|k|shift+tab': this.focusBefore,
+				'down|j|tab': this.focusAfter,
+				'esc': this.blur,
+				'm|o': () => this.menu(true),
+				's': this.toggleShowContent,
+				'1': () => this.reactDirectly(this.$store.state.settings.reactions[0]),
+				'2': () => this.reactDirectly(this.$store.state.settings.reactions[1]),
+				'3': () => this.reactDirectly(this.$store.state.settings.reactions[2]),
+				'4': () => this.reactDirectly(this.$store.state.settings.reactions[3]),
+				'5': () => this.reactDirectly(this.$store.state.settings.reactions[4]),
+				'6': () => this.reactDirectly(this.$store.state.settings.reactions[5]),
+				'7': () => this.reactDirectly(this.$store.state.settings.reactions[6]),
+				'8': () => this.reactDirectly(this.$store.state.settings.reactions[7]),
+				'9': () => this.reactDirectly(this.$store.state.settings.reactions[8]),
+				'0': () => this.reactDirectly(this.$store.state.settings.reactions[9]),
+			};
+		},
+
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.fileIds.length == 0 &&
+				this.note.poll == null);
+		},
+
+		appearNote(): any {
+			return this.isRenote ? this.note.renote : this.note;
+		},
+
+		isMyNote(): boolean {
+			return this.$store.getters.isSignedIn && (this.$store.state.i.id === this.appearNote.userId);
+		},
+
+		reactionsCount(): number {
+			return this.appearNote.reactions
+				? sum(Object.values(this.appearNote.reactions))
+				: 0;
+		},
+
+		title(): string {
+			return '';
+		},
+
+		urls(): string[] {
+			if (this.appearNote.text) {
+				const ast = parse(this.appearNote.text);
+				// TODO: 再帰的にURL要素がないか調べる
+				const urls = unique(ast
+					.filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent))
+					.map(t => t.node.props.url));
+
+				// unique without hash
+				// [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ]
+				const removeHash = x => x.replace(/#[^#]*$/, '');
+
+				return urls.reduce((array, url) => {
+					const removed = removeHash(url);
+					if (!array.map(x => removeHash(x)).includes(removed)) array.push(url);
+					return array;
+				}, []);
+			} else {
+				return null;
+			}
+		}
+	},
+
+	created() {
+		if (this.$store.getters.isSignedIn) {
+			this.connection = this.$root.stream;
+		}
+
+		if (this.detail) {
+			this.$root.api('notes/children', {
+				noteId: this.appearNote.id,
+				limit: 30
+			}).then(replies => {
+				this.replies = replies;
+			});
+
+			if (this.appearNote.replyId) {
+				this.$root.api('notes/conversation', {
+					noteId: this.appearNote.replyId
+				}).then(conversation => {
+					this.conversation = conversation.reverse();
+				});
+			}
+		}
+	},
+
+	mounted() {
+		this.capture(true);
+
+		if (this.$store.getters.isSignedIn) {
+			this.connection.on('_connected_', this.onStreamConnected);
+		}
+	},
+
+	beforeDestroy() {
+		this.decapture(true);
+
+		if (this.$store.getters.isSignedIn) {
+			this.connection.off('_connected_', this.onStreamConnected);
+		}
+	},
+
+	methods: {
+		capture(withHandler = false) {
+			if (this.$store.getters.isSignedIn) {
+				this.connection.send('sn', { id: this.appearNote.id });
+				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
+			}
+		},
+
+		decapture(withHandler = false) {
+			if (this.$store.getters.isSignedIn) {
+				this.connection.send('un', {
+					id: this.appearNote.id
+				});
+				if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated);
+			}
+		},
+
+		onStreamConnected() {
+			this.capture();
+		},
+
+		onStreamNoteUpdated(data) {
+			const { type, id, body } = data;
+
+			if (id !== this.appearNote.id) return;
+
+			switch (type) {
+				case 'reacted': {
+					const reaction = body.reaction;
+
+					if (this.appearNote.reactions == null) {
+						Vue.set(this.appearNote, 'reactions', {});
+					}
+
+					if (this.appearNote.reactions[reaction] == null) {
+						Vue.set(this.appearNote.reactions, reaction, 0);
+					}
+
+					// Increment the count
+					this.appearNote.reactions[reaction]++;
+
+					if (body.userId == this.$store.state.i.id) {
+						Vue.set(this.appearNote, 'myReaction', reaction);
+					}
+					break;
+				}
+
+				case 'unreacted': {
+					const reaction = body.reaction;
+
+					if (this.appearNote.reactions == null) {
+						return;
+					}
+
+					if (this.appearNote.reactions[reaction] == null) {
+						return;
+					}
+
+					// Decrement the count
+					if (this.appearNote.reactions[reaction] > 0) this.appearNote.reactions[reaction]--;
+
+					if (body.userId == this.$store.state.i.id) {
+						Vue.set(this.appearNote, 'myReaction', null);
+					}
+					break;
+				}
+
+				case 'pollVoted': {
+					const choice = body.choice;
+					this.appearNote.poll.choices[choice].votes++;
+					if (body.userId == this.$store.state.i.id) {
+						Vue.set(this.appearNote.poll.choices[choice], 'isVoted', true);
+					}
+					break;
+				}
+
+				case 'deleted': {
+					Vue.set(this.appearNote, 'deletedAt', body.deletedAt);
+					Vue.set(this.appearNote, 'renote', null);
+					this.appearNote.text = null;
+					this.appearNote.fileIds = [];
+					this.appearNote.poll = null;
+					this.appearNote.cw = null;
+					break;
+				}
+			}
+		},
+
+		reply(viaKeyboard = false) {
+			pleaseLogin(this.$root);
+			this.$root.post({
+				reply: this.appearNote,
+				animation: !viaKeyboard,
+			}, () => {
+				this.focus();
+			});
+		},
+
+		renote() {
+			pleaseLogin(this.$root);
+			this.blur();
+			this.$root.new(MkRenotePicker, {
+				source: this.$refs.renoteButton,
+				note: this.appearNote,
+			}).$once('closed', this.focus);
+		},
+
+		renoteDirectly() {
+			(this as any).$root.api('notes/create', {
+				renoteId: this.appearNote.id
+			});
+		},
+
+		react(viaKeyboard = false) {
+			pleaseLogin(this.$root);
+			this.blur();
+			const picker = this.$root.new(MkReactionPicker, {
+				source: this.$refs.reactButton,
+				showFocus: viaKeyboard,
+			});
+			picker.$once('chosen', reaction => {
+				this.$root.api('notes/reactions/create', {
+					noteId: this.appearNote.id,
+					reaction: reaction
+				}).then(() => {
+					picker.close();
+				});
+			});
+			picker.$once('closed', this.focus);
+		},
+
+		reactDirectly(reaction) {
+			this.$root.api('notes/reactions/create', {
+				noteId: this.appearNote.id,
+				reaction: reaction
+			});
+		},
+
+		undoReact(note) {
+			const oldReaction = note.myReaction;
+			if (!oldReaction) return;
+			this.$root.api('notes/reactions/delete', {
+				noteId: note.id
+			});
+		},
+
+		favorite() {
+			pleaseLogin(this.$root);
+			this.$root.api('notes/favorites/create', {
+				noteId: this.appearNote.id
+			}).then(() => {
+				this.$root.dialog({
+					type: 'success',
+					iconOnly: true, autoClose: true
+				});
+			});
+		},
+
+		del() {
+			this.$root.dialog({
+				type: 'warning',
+				text: this.$t('noteDeleteConfirm'),
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+
+				this.$root.api('notes/delete', {
+					noteId: this.appearNote.id
+				});
+			});
+		},
+
+		menu(viaKeyboard = false) {
+			if (this.openingMenu) return;
+			this.openingMenu = true;
+			const w = this.$root.new(MkNoteMenu, {
+				source: this.$refs.menuButton,
+				note: this.appearNote,
+				animation: !viaKeyboard
+			}).$once('closed', () => {
+				this.openingMenu = false;
+				this.focus();
+			});
+		},
+
+		toggleShowContent() {
+			this.showContent = !this.showContent;
+		},
+
+		focus() {
+			this.$el.focus();
+		},
+
+		blur() {
+			this.$el.blur();
+		},
+
+		focusBefore() {
+			focus(this.$el, e => e.previousElementSibling);
+		},
+
+		focusAfter() {
+			focus(this.$el, e => e.nextElementSibling);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.note {
+	position: relative;
+	transition: box-shadow 0.1s ease;
+
+	&.max-width_500px {
+		font-size: 0.9em;
+	}
+
+	&.max-width_450px {
+		> .renote {
+			padding: 8px 16px 0 16px;
+		}
+
+		> .article {
+			padding: 14px 16px 9px;
+
+			> .avatar {
+				margin: 0 10px 8px 0;
+				width: 50px;
+				height: 50px;
+			}
+		}
+	}
+
+	&.max-width_350px {
+		> .article {
+			> .main {
+				> .footer {
+					> .button {
+						&:not(:last-child) {
+							margin-right: 18px;
+						}
+					}
+				}
+			}
+		}
+	}
+
+	&.max-width_300px {
+		font-size: 0.825em;
+
+		> .article {
+			> .avatar {
+				width: 44px;
+				height: 44px;
+			}
+
+			> .main {
+				> .footer {
+					> .button {
+						&:not(:last-child) {
+							margin-right: 12px;
+						}
+					}
+				}
+			}
+		}
+	}
+
+	&:focus {
+		outline: none;
+		box-shadow: 0 0 0 3px var(--focus);
+	}
+
+	&:hover > .article > .main > .footer > .button {
+		opacity: 1;
+	}
+
+	> *:first-child {
+		border-radius: var(--radius) var(--radius) 0 0;
+	}
+
+	> *:last-child {
+		border-radius: 0 0 var(--radius) var(--radius);
+	}
+
+	> .pinned {
+		padding: 16px 32px 8px 32px;
+		line-height: 24px;
+		font-size: 90%;
+		white-space: pre;
+		color: #d28a3f;
+
+		@media (max-width: 450px) {
+			padding: 8px 16px 0 16px;
+		}
+
+		> [data-icon] {
+			margin-right: 4px;
+		}
+	}
+
+	> .pinned + .article {
+		padding-top: 8px;
+	}
+
+	> .renote {
+		display: flex;
+		align-items: center;
+		padding: 16px 32px 8px 32px;
+		line-height: 28px;
+		white-space: pre;
+		color: var(--renote);
+
+		> .avatar {
+			flex-shrink: 0;
+			display: inline-block;
+			width: 28px;
+			height: 28px;
+			margin: 0 8px 0 0;
+			border-radius: 6px;
+		}
+
+		> [data-icon] {
+			margin-right: 4px;
+		}
+
+		> span {
+			overflow: hidden;
+			flex-shrink: 1;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+
+			> .name {
+				font-weight: bold;
+			}
+		}
+
+		> .info {
+			margin-left: auto;
+			font-size: 0.9em;
+
+			> .mk-time {
+				flex-shrink: 0;
+			}
+
+			> .visibility {
+				margin-left: 8px;
+
+				[data-icon] {
+					margin-right: 0;
+				}
+			}
+		}
+	}
+
+	> .renote + .article {
+		padding-top: 8px;
+	}
+
+	> .article {
+		display: flex;
+		padding: 28px 32px 18px;
+
+		> .avatar {
+			flex-shrink: 0;
+			display: block;
+			//position: sticky;
+			//top: 72px;
+			margin: 0 14px 8px 0;
+			width: 58px;
+			height: 58px;
+		}
+
+		> .main {
+			flex: 1;
+			min-width: 0;
+
+			> .body {
+				> .cw {
+					cursor: default;
+					display: block;
+					margin: 0;
+					padding: 0;
+					overflow-wrap: break-word;
+
+					> .text {
+						margin-right: 8px;
+					}
+				}
+
+				> .content {
+					> .text {
+						overflow-wrap: break-word;
+
+						> .reply {
+							color: var(--accent);
+							margin-right: 0.5em;
+						}
+
+						> .rp {
+							margin-left: 4px;
+							font-style: oblique;
+							color: var(--renote);
+						}
+					}
+
+					> .url-preview {
+						margin-top: 8px;
+					}
+
+					> .mk-poll {
+						font-size: 80%;
+					}
+
+					> .renote {
+						padding: 8px 0;
+
+						> * {
+							padding: 16px;
+							border: dashed 1px var(--renote);
+							border-radius: 8px;
+						}
+					}
+				}
+			}
+
+			> .footer {
+				> .button {
+					margin: 0;
+					padding: 8px;
+					opacity: 0.7;
+
+					&:not(:last-child) {
+						margin-right: 28px;
+					}
+
+					&:hover {
+						color: var(--mkykhqkw);
+					}
+
+					> .count {
+						display: inline;
+						margin: 0 0 0 8px;
+						opacity: 0.7;
+					}
+
+					&.reacted {
+						color: var(--accent);
+					}
+				}
+			}
+
+			> .deleted {
+				opacity: 0.7;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7cf2aa2b02a8b08ed8284e4e3d13ff68706133b0
--- /dev/null
+++ b/src/client/components/notes.vue
@@ -0,0 +1,144 @@
+<template>
+<div class="mk-notes" v-size="[{ max: 500 }]">
+	<div class="empty" v-if="empty">{{ $t('noNotes') }}</div>
+
+	<mk-error v-if="error" @retry="init()"/>
+
+	<x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note, i }">
+		<x-note :note="note" :detail="detail" :key="note.id" :data-index="i"/>
+	</x-list>
+
+	<footer v-if="more">
+		<button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" class="_buttonPrimary">
+			<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
+			<template v-if="moreFetching"><fa :icon="faSpinner" pulse fixed-width/></template>
+		</button>
+	</footer>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faSpinner } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
+import paging from '../scripts/paging';
+import XNote from './note.vue';
+import XList from './date-separated-list.vue';
+import getUserName from '../../misc/get-user-name';
+import getNoteSummary from '../../misc/get-note-summary';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XNote, XList
+	},
+
+	mixins: [
+		paging({
+			onPrepend: (self, note) => {
+				// タブが非表示なら通知
+				if (document.hidden) {
+					if ('Notification' in window && Notification.permission === 'granted') {
+						new Notification(getUserName(note.user), {
+							body: getNoteSummary(note),
+							icon: note.user.avatarUrl,
+							tag: 'newNote'
+						});
+					}
+				}
+			},
+
+			before: (self) => {
+				self.$emit('before');
+			},
+
+			after: (self, e) => {
+				self.$emit('after', e);
+			}
+		}),
+	],
+
+	props: {
+		pagination: {
+			required: true
+		},
+
+		detail: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+
+		extract: {
+			required: false
+		}
+	},
+
+	data() {
+		return {
+			faSpinner
+		};
+	},
+
+	computed: {
+		notes(): any[] {
+			return this.extract ? this.extract(this.items) : this.items;
+		},
+	},
+
+	methods: {
+		focus() {
+			this.$refs.notes.focus();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-notes {
+	> .empty {
+		margin: 0 auto;
+		padding: 32px;
+		text-align: center;
+		background: rgba(0, 0, 0, 0.3);
+		color: #fff;
+		-webkit-backdrop-filter: blur(16px);
+		backdrop-filter: blur(16px);
+		border-radius: 6px;
+	}
+
+	> .notes {
+		> ::v-deep * {
+			margin-bottom: var(--marginFull);
+		}
+	}
+
+	&.max-width_500px {
+		> .notes {
+			> ::v-deep * {
+				margin-bottom: var(--marginHalf);
+			}
+		}
+	}
+
+	> footer {
+		text-align: center;
+
+		&:empty {
+			display: none;
+		}
+
+		> button {
+			margin: 0;
+			padding: 16px;
+			width: 100%;
+			border-radius: var(--radius);
+
+			&:disabled {
+				opacity: 0.7;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/components/notification.vue b/src/client/components/notification.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e325f0adb69c45074a42cb62dd0cacc4ddb7aa5b
--- /dev/null
+++ b/src/client/components/notification.vue
@@ -0,0 +1,219 @@
+<template>
+<div class="mk-notification" :class="notification.type">
+	<div class="head">
+		<mk-avatar class="avatar" :user="notification.user"/>
+		<div class="icon" :class="notification.type">
+			<fa :icon="faPlus" v-if="notification.type === 'follow'"/>
+			<fa :icon="faClock" v-if="notification.type === 'receiveFollowRequest'"/>
+			<fa :icon="faCheck" v-if="notification.type === 'followRequestAccepted'"/>
+			<fa :icon="faRetweet" v-if="notification.type === 'renote'"/>
+			<fa :icon="faReply" v-if="notification.type === 'reply'"/>
+			<fa :icon="faAt" v-if="notification.type === 'mention'"/>
+			<fa :icon="faQuoteLeft" v-if="notification.type === 'quote'"/>
+			<x-reaction-icon v-if="notification.type === 'reaction'" :reaction="notification.reaction" :no-style="true"/>
+		</div>
+	</div>
+	<div class="tail">
+		<header>
+			<router-link class="name" :to="notification.user | userPage" v-user-preview="notification.user.id"><mk-user-name :user="notification.user"/></router-link>
+			<mk-time :time="notification.createdAt" v-if="withTime"/>
+		</header>
+		<router-link v-if="notification.type === 'reaction'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
+			<fa :icon="faQuoteLeft"/>
+			<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="nowrap" :custom-emojis="notification.note.emojis"/>
+			<fa :icon="faQuoteRight"/>
+		</router-link>
+		<router-link v-if="notification.type === 'renote'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)">
+			<fa :icon="faQuoteLeft"/>
+			<mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="nowrap" :custom-emojis="notification.note.renote.emojis"/>
+			<fa :icon="faQuoteRight"/>
+		</router-link>
+		<router-link v-if="notification.type === 'reply'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
+			<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="nowrap" :custom-emojis="notification.note.emojis"/>
+		</router-link>
+		<router-link v-if="notification.type === 'mention'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
+			<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="nowrap" :custom-emojis="notification.note.emojis"/>
+		</router-link>
+		<router-link v-if="notification.type === 'quote'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
+			<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="nowrap" :custom-emojis="notification.note.emojis"/>
+		</router-link>
+		<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ $t('youGotNewFollower') }}</span>
+		<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ $t('followRequestAccepted') }}</span>
+		<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ $t('receiveFollowRequest') }}<div v-if="!nowrap && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ $t('reject') }}</button></div></span>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faPlus, faQuoteLeft, faQuoteRight, faRetweet, faReply, faAt, faCheck } from '@fortawesome/free-solid-svg-icons';
+import { faClock } from '@fortawesome/free-regular-svg-icons';
+import getNoteSummary from '../../misc/get-note-summary';
+import XReactionIcon from './reaction-icon.vue';
+
+export default Vue.extend({
+	components: {
+		XReactionIcon
+	},
+	props: {
+		notification: {
+			type: Object,
+			required: true,
+		},
+		withTime: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+		nowrap: {
+			type: Boolean,
+			required: false,
+			default: true,
+		},
+	},
+	data() {
+		return {
+			getNoteSummary,
+			followRequestDone: false,
+			faPlus, faQuoteLeft, faQuoteRight, faRetweet, faReply, faAt, faClock, faCheck
+		};
+	},
+	methods: {
+		acceptFollowRequest() {
+			this.followRequestDone = true;
+			this.$root.api('following/requests/accept', { userId: this.notification.user.id });
+		},
+		rejectFollowRequest() {
+			this.followRequestDone = true;
+			this.$root.api('following/requests/reject', { userId: this.notification.user.id });
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-notification {
+	position: relative;
+	box-sizing: border-box;
+	padding: 16px;
+	font-size: 0.9em;
+	overflow-wrap: break-word;
+	display: flex;
+
+	@media (max-width: 500px) {
+		padding: 12px;
+		font-size: 0.8em;
+	}
+
+	&:after {
+		content: "";
+		display: block;
+		clear: both;
+	}
+
+	> .head {
+		position: sticky;
+		top: 0;
+		flex-shrink: 0;
+		width: 42px;
+		height: 42px;
+		margin-right: 8px;
+
+		> .avatar {
+			display: block;
+			width: 100%;
+			height: 100%;
+			border-radius: 6px;
+		}
+
+		> .icon {
+			position: absolute;
+			z-index: 1;
+			bottom: -2px;
+			right: -2px;
+			width: 20px;
+			height: 20px;
+			box-sizing: border-box;
+			border-radius: 100%;
+			background: var(--panel);
+			box-shadow: 0 0 0 3px var(--panel);
+			font-size: 12px;
+			pointer-events: none;
+
+			> * {
+				color: #fff;
+				width: 100%;
+				height: 100%;
+			}
+
+			&.follow, &.followRequestAccepted, &.receiveFollowRequest {
+				padding: 3px;
+				background: #36aed2;
+			}
+
+			&.retweet {
+				padding: 3px;
+				background: #36d298;
+			}
+
+			&.quote {
+				padding: 3px;
+				background: #36d298;
+			}
+
+			&.reply {
+				padding: 3px;
+				background: #007aff;
+			}
+
+			&.mention {
+				padding: 3px;
+				background: #88a6b7;
+			}
+		}
+	}
+
+	> .tail {
+		flex: 1;
+		min-width: 0;
+
+		> header {
+			display: flex;
+			align-items: baseline;
+			white-space: nowrap;
+
+			> .name {
+				text-overflow: ellipsis;
+				white-space: nowrap;
+				min-width: 0;
+				overflow: hidden;
+			}
+
+			> .mk-time {
+				margin-left: auto;
+				font-size: 0.9em;
+			}
+		}
+
+		> .text {
+			white-space: nowrap;
+			overflow: hidden;
+			text-overflow: ellipsis;
+
+			> [data-icon] {
+				vertical-align: super;
+				font-size: 50%;
+				opacity: 0.5;
+			}
+
+			> [data-icon]:first-child {
+				margin-right: 4px;
+			}
+
+			> [data-icon]:last-child {
+				margin-left: 4px;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/components/notifications.vue b/src/client/components/notifications.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ad82913380158e09ac5fae915fc949924c96e24a
--- /dev/null
+++ b/src/client/components/notifications.vue
@@ -0,0 +1,136 @@
+<template>
+<div class="mk-notifications">
+	<div class="contents">
+		<x-list class="notifications" :items="items" v-slot="{ item: notification, i }">
+			<x-notification :notification="notification" :with-time="true" :nowrap="false" class="notification" :key="notification.id" :data-index="i"/>
+		</x-list>
+
+		<button class="more _button" v-if="more" @click="fetchMore" :disabled="moreFetching">
+			<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
+			<template v-if="moreFetching"><fa :icon="faSpinner" pulse fixed-width/></template>
+		</button>
+
+		<p class="empty" v-if="empty">{{ $t('noNotifications') }}</p>
+
+		<mk-error v-if="error" @retry="init()"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faSpinner } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
+import paging from '../scripts/paging';
+import XNotification from './notification.vue';
+import XList from './date-separated-list.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XNotification,
+		XList,
+	},
+
+	mixins: [
+		paging({}),
+	],
+
+	props: {
+		type: {
+			type: String,
+			required: false
+		},
+		wide: {
+			type: Boolean,
+			required: false,
+			default: false
+		}
+	},
+
+	data() {
+		return {
+			connection: null,
+			pagination: {
+				endpoint: 'i/notifications',
+				limit: 10,
+				params: () => ({
+					includeTypes: this.type ? [this.type] : undefined
+				})
+			},
+			faSpinner
+		};
+	},
+
+	watch: {
+		type() {
+			this.reload();
+		}
+	},
+
+	mounted() {
+		this.connection = this.$root.stream.useSharedConnection('main');
+		this.connection.on('notification', this.onNotification);
+	},
+
+	beforeDestroy() {
+		this.connection.dispose();
+	},
+
+	methods: {
+		onNotification(notification) {
+			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
+			this.$root.stream.send('readNotification', {
+				id: notification.id
+			});
+
+			this.prepend(notification);
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-notifications {
+	> .contents {
+		overflow: auto;
+		height: 100%;
+		padding: 8px 8px 0 8px;
+
+		> .notifications {
+			> ::v-deep * {
+				margin-bottom: 8px;
+			}
+
+			> .notification {
+				background: var(--panel);
+				border-radius: 6px;
+				box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+			}
+		}
+
+		> .more {
+			display: block;
+			width: 100%;
+			padding: 16px;
+
+			> [data-icon] {
+				margin-right: 4px;
+			}
+		}
+
+		> .empty {
+			margin: 0;
+			padding: 16px;
+			text-align: center;
+			color: var(--fg);
+		}
+
+		> .placeholder {
+			padding: 32px;
+			opacity: 0.3;
+		}
+	}
+}
+</style>
diff --git a/src/client/components/page-preview.vue b/src/client/components/page-preview.vue
new file mode 100644
index 0000000000000000000000000000000000000000..5ba226c481d62522f61cf5a173d2229e188162f3
--- /dev/null
+++ b/src/client/components/page-preview.vue
@@ -0,0 +1,163 @@
+<template>
+<router-link :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1">
+	<div class="thumbnail" v-if="page.eyeCatchingImage" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
+	<article>
+		<header>
+			<h1 :title="page.title">{{ page.title }}</h1>
+		</header>
+		<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p>
+		<footer>
+			<img class="icon" :src="page.user.avatarUrl"/>
+			<p>{{ page.user | userName }}</p>
+		</footer>
+	</article>
+</router-link>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: {
+		page: {
+			type: Object,
+			required: true
+		},
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.vhpxefrj {
+	display: block;
+	overflow: hidden;
+	width: 100%;
+	border: solid var(--lineWidth) var(--urlPreviewBorder);
+	border-radius: 4px;
+	overflow: hidden;
+
+	&:hover {
+		text-decoration: none;
+		border-color: var(--urlPreviewBorderHover);
+	}
+
+	> .thumbnail {
+		position: absolute;
+		width: 100px;
+		height: 100%;
+		background-position: center;
+		background-size: cover;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+
+		> button {
+			font-size: 3.5em;
+			opacity: 0.7;
+
+			&:hover {
+				font-size: 4em;
+				opacity: 0.9;
+			}
+		}
+
+		& + article {
+			left: 100px;
+			width: calc(100% - 100px);
+		}
+	}
+
+	> article {
+		padding: 16px;
+
+		> header {
+			margin-bottom: 8px;
+
+			> h1 {
+				margin: 0;
+				font-size: 1em;
+				color: var(--urlPreviewTitle);
+			}
+		}
+
+		> p {
+			margin: 0;
+			color: var(--urlPreviewText);
+			font-size: 0.8em;
+		}
+
+		> footer {
+			margin-top: 8px;
+			height: 16px;
+
+			> img {
+				display: inline-block;
+				width: 16px;
+				height: 16px;
+				margin-right: 4px;
+				vertical-align: top;
+			}
+
+			> p {
+				display: inline-block;
+				margin: 0;
+				color: var(--urlPreviewInfo);
+				font-size: 0.8em;
+				line-height: 16px;
+				vertical-align: top;
+			}
+		}
+	}
+
+	@media (max-width: 700px) {
+		> .thumbnail {
+			position: relative;
+			width: 100%;
+			height: 100px;
+
+			& + article {
+				left: 0;
+				width: 100%;
+			}
+		}
+	}
+
+	@media (max-width: 550px) {
+		font-size: 12px;
+
+		> .thumbnail {
+			height: 80px;
+		}
+
+		> article {
+			padding: 12px;
+		}
+	}
+
+	@media (max-width: 500px) {
+		font-size: 10px;
+
+		> .thumbnail {
+			height: 70px;
+		}
+
+		> article {
+			padding: 8px;
+
+			> header {
+				margin-bottom: 4px;
+			}
+
+			> footer {
+				margin-top: 4px;
+
+				> img {
+					width: 12px;
+					height: 12px;
+				}
+			}
+		}
+	}
+}
+
+</style>
diff --git a/src/client/app/common/views/components/page/page.block.vue b/src/client/components/page/page.block.vue
similarity index 99%
rename from src/client/app/common/views/components/page/page.block.vue
rename to src/client/components/page/page.block.vue
index 56d18220134bbffb55e105c15064e5a05b88889c..c1d046fa2ec4e156b0eb813373d45ed3125c2365 100644
--- a/src/client/app/common/views/components/page/page.block.vue
+++ b/src/client/components/page/page.block.vue
@@ -22,7 +22,6 @@ export default Vue.extend({
 	components: {
 		XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton
 	},
-
 	props: {
 		value: {
 			required: true
diff --git a/src/client/app/common/views/components/page/page.button.vue b/src/client/components/page/page.button.vue
similarity index 74%
rename from src/client/app/common/views/components/page/page.button.vue
rename to src/client/components/page/page.button.vue
index 87112aca0d33dadffa7156a6adbbfa02bde8d539..eeb56d5eca1dbd5f710317c534c941202bd697ca 100644
--- a/src/client/app/common/views/components/page/page.button.vue
+++ b/src/client/components/page/page.button.vue
@@ -1,13 +1,17 @@
 <template>
 <div>
-	<ui-button class="kudkigyw" @click="click()" :primary="value.primary">{{ script.interpolate(value.text) }}</ui-button>
+	<mk-button class="kudkigyw" @click="click()" :primary="value.primary">{{ script.interpolate(value.text) }}</mk-button>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import MkButton from '../ui/button.vue';
 
 export default Vue.extend({
+	components: {
+		MkButton
+	},
 	props: {
 		value: {
 			required: true
@@ -16,7 +20,6 @@ export default Vue.extend({
 			required: true
 		}
 	},
-
 	methods: {
 		click() {
 			if (this.value.action === 'dialog') {
@@ -46,10 +49,11 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.kudkigyw
-	display inline-block
-	min-width 200px
-	max-width 450px
-	margin 8px 0
+<style lang="scss" scoped>
+.kudkigyw {
+	display: inline-block;
+	min-width: 200px;
+	max-width: 450px;
+	margin: 8px 0;
+}
 </style>
diff --git a/src/client/app/common/views/components/page/page.counter.vue b/src/client/components/page/page.counter.vue
similarity index 60%
rename from src/client/app/common/views/components/page/page.counter.vue
rename to src/client/components/page/page.counter.vue
index 8d55319fe97f707672083bdbfe3aff0471ea1df4..781a1bd549f61c306ae549b247f6ce50a97d960c 100644
--- a/src/client/app/common/views/components/page/page.counter.vue
+++ b/src/client/components/page/page.counter.vue
@@ -1,13 +1,17 @@
 <template>
 <div>
-	<ui-button class="llumlmnx" @click="click()">{{ script.interpolate(value.text) }}</ui-button>
+	<mk-button class="llumlmnx" @click="click()">{{ script.interpolate(value.text) }}</mk-button>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import MkButton from '../ui/button.vue';
 
 export default Vue.extend({
+	components: {
+		MkButton
+	},
 	props: {
 		value: {
 			required: true
@@ -16,20 +20,17 @@ export default Vue.extend({
 			required: true
 		}
 	},
-
 	data() {
 		return {
 			v: 0,
 		};
 	},
-
 	watch: {
 		v() {
 			this.script.aiScript.updatePageVar(this.value.name, this.v);
 			this.script.eval();
 		}
 	},
-
 	methods: {
 		click() {
 			this.v = this.v + (this.value.inc || 1);
@@ -38,10 +39,11 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.llumlmnx
-	display inline-block
-	min-width 300px
-	max-width 450px
-	margin 8px 0
+<style lang="scss" scoped>
+.llumlmnx {
+	display: inline-block;
+	min-width: 300px;
+	max-width: 450px;
+	margin: 8px 0;
+}
 </style>
diff --git a/src/client/app/common/views/components/page/page.if.vue b/src/client/components/page/page.if.vue
similarity index 98%
rename from src/client/app/common/views/components/page/page.if.vue
rename to src/client/components/page/page.if.vue
index 417ef0c553c3185b268957c99b806e039a33e154..a714a522e895b53d75d5ae1155f8228589265447 100644
--- a/src/client/app/common/views/components/page/page.if.vue
+++ b/src/client/components/page/page.if.vue
@@ -22,9 +22,8 @@ export default Vue.extend({
 			required: true
 		}
 	},
-
 	beforeCreate() {
-		this.$options.components.XBlock = require('./page.block.vue').default
+		this.$options.components.XBlock = require('./page.block.vue').default;
 	},
 });
 </script>
diff --git a/src/client/app/common/views/components/page/page.image.vue b/src/client/components/page/page.image.vue
similarity index 75%
rename from src/client/app/common/views/components/page/page.image.vue
rename to src/client/components/page/page.image.vue
index 1285445eb0ebf0bea45232092d1e10bc12c4f5b3..f0d7c7b30f4f33fb23d438e56ddee4200476d778 100644
--- a/src/client/app/common/views/components/page/page.image.vue
+++ b/src/client/components/page/page.image.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="lzyxtsnt">
-	<img v-if="image" :src="image.url"/>
+	<img v-if="image" :src="image.url" alt=""/>
 </div>
 </template>
 
@@ -16,21 +16,21 @@ export default Vue.extend({
 			required: true
 		},
 	},
-
 	data() {
 		return {
 			image: null,
 		};
 	},
-
 	created() {
 		this.image = this.page.attachedFiles.find(x => x.id === this.value.fileId);
 	}
 });
 </script>
 
-<style lang="stylus" scoped>
-.lzyxtsnt
-	> img
-		max-width 100%
+<style lang="scss" scoped>
+.lzyxtsnt {
+	> img {
+		max-width: 100%;
+	}
+}
 </style>
diff --git a/src/client/app/common/views/components/page/page.number-input.vue b/src/client/components/page/page.number-input.vue
similarity index 56%
rename from src/client/app/common/views/components/page/page.number-input.vue
rename to src/client/components/page/page.number-input.vue
index 31da37330a0c01f407ed69baab5c15d57ed33123..9ee2730fac9438ebb18f7a8e7d2cf94b11e5ec8d 100644
--- a/src/client/app/common/views/components/page/page.number-input.vue
+++ b/src/client/components/page/page.number-input.vue
@@ -1,13 +1,17 @@
 <template>
 <div>
-	<ui-input class="kudkigyw" v-model="v" type="number">{{ script.interpolate(value.text) }}</ui-input>
+	<mk-input class="kudkigyw" v-model="v" type="number">{{ script.interpolate(value.text) }}</mk-input>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import MkInput from '../ui/input.vue';
 
 export default Vue.extend({
+	components: {
+		MkInput
+	},
 	props: {
 		value: {
 			required: true
@@ -16,13 +20,11 @@ export default Vue.extend({
 			required: true
 		}
 	},
-
 	data() {
 		return {
 			v: this.value.default,
 		};
 	},
-
 	watch: {
 		v() {
 			this.script.aiScript.updatePageVar(this.value.name, this.v);
@@ -32,10 +34,11 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.kudkigyw
-	display inline-block
-	min-width 300px
-	max-width 450px
-	margin 8px 0
+<style lang="scss" scoped>
+.kudkigyw {
+	display: inline-block;
+	min-width: 300px;
+	max-width: 450px;
+	margin: 8px 0;
+}
 </style>
diff --git a/src/client/components/page/page.post.vue b/src/client/components/page/page.post.vue
new file mode 100644
index 0000000000000000000000000000000000000000..010a96c855757c93fb130fd032225bfa30a4fa27
--- /dev/null
+++ b/src/client/components/page/page.post.vue
@@ -0,0 +1,75 @@
+<template>
+<div class="ngbfujlo">
+	<mk-textarea class="textarea" :value="text" readonly></mk-textarea>
+	<mk-button primary @click="post()" :disabled="posting || posted">{{ posted ? $t('posted-from-post-form') : $t('post-from-post-form') }}</mk-button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../../i18n';
+import MkTextarea from '../ui/textarea.vue';
+import MkButton from '../ui/button.vue';
+
+export default Vue.extend({
+	i18n,
+	components: {
+		MkTextarea,
+		MkButton,
+	},
+	props: {
+		value: {
+			required: true
+		},
+		script: {
+			required: true
+		}
+	},
+	data() {
+		return {
+			text: this.script.interpolate(this.value.text),
+			posted: false,
+			posting: false,
+		};
+	},
+	watch: {
+		'script.vars': {
+			handler() {
+				this.text = this.script.interpolate(this.value.text);
+			},
+			deep: true
+		}
+	},
+	methods: {
+		post() {
+			this.posting = true;
+			this.$root.api('notes/create', {
+				text: this.text,
+			}).then(() => {
+				this.posted = true;
+				this.$root.dialog({
+					type: 'success',
+					iconOnly: true, autoClose: true
+				});
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ngbfujlo {
+	padding: 0 32px 32px 32px;
+	border: solid 2px var(--divider);
+	border-radius: 6px;
+
+	@media (max-width: 600px) {
+		padding: 0 16px 16px 16px;
+
+		> .textarea {
+			margin-top: 16px;
+			margin-bottom: 16px;
+		}
+	}
+}
+</style>
diff --git a/src/client/app/common/views/components/page/page.radio-button.vue b/src/client/components/page/page.radio-button.vue
similarity index 73%
rename from src/client/app/common/views/components/page/page.radio-button.vue
rename to src/client/components/page/page.radio-button.vue
index 27c11bebad936495011300ade2757f8cd6f6a66e..fda0a03927bf45a7b2f43a85f187c0a69450d2e7 100644
--- a/src/client/app/common/views/components/page/page.radio-button.vue
+++ b/src/client/components/page/page.radio-button.vue
@@ -1,14 +1,18 @@
 <template>
 <div>
 	<div>{{ script.interpolate(value.title) }}</div>
-	<ui-radio v-for="x in value.values" v-model="v" :value="x" :key="x">{{ x }}</ui-radio>
+	<mk-radio v-for="x in value.values" v-model="v" :value="x" :key="x">{{ x }}</mk-radio>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import MkRadio from '../ui/radio.vue';
 
 export default Vue.extend({
+	components: {
+		MkRadio
+	},
 	props: {
 		value: {
 			required: true
@@ -17,13 +21,11 @@ export default Vue.extend({
 			required: true
 		}
 	},
-
 	data() {
 		return {
 			v: this.value.default,
 		};
 	},
-
 	watch: {
 		v() {
 			this.script.aiScript.updatePageVar(this.value.name, this.v);
@@ -32,6 +34,3 @@ export default Vue.extend({
 	}
 });
 </script>
-
-<style lang="stylus" scoped>
-</style>
diff --git a/src/client/app/common/views/components/page/page.section.vue b/src/client/components/page/page.section.vue
similarity index 71%
rename from src/client/app/common/views/components/page/page.section.vue
rename to src/client/components/page/page.section.vue
index 03c009d9c3e83e42bc00cb56c917828ab48c65a7..b83c773f7109fc261355db93b3711280498656eb 100644
--- a/src/client/app/common/views/components/page/page.section.vue
+++ b/src/client/components/page/page.section.vue
@@ -26,30 +26,33 @@ export default Vue.extend({
 			required: true
 		}
 	},
-
 	beforeCreate() {
-		this.$options.components.XBlock = require('./page.block.vue').default
+		this.$options.components.XBlock = require('./page.block.vue').default;
 	},
 });
 </script>
 
-<style lang="stylus" scoped>
-.sdgxphyu
-	margin 1.5em 0
+<style lang="scss" scoped>
+.sdgxphyu {
+	margin: 1.5em 0;
 
-	> h2
-		font-size 1.35em
-		margin 0 0 0.5em 0
+	> h2 {
+		font-size: 1.35em;
+		margin: 0 0 0.5em 0;
+	}
 
-	> h3
-		font-size 1em
-		margin 0 0 0.5em 0
+	> h3 {
+		font-size: 1em;
+		margin: 0 0 0.5em 0;
+	}
 
-	> h4
-		font-size 1em
-		margin 0 0 0.5em 0
+	> h4 {
+		font-size: 1em;
+		margin: 0 0 0.5em 0;
+	}
 
-	> .children
+	> .children {
 		//padding 16px
-
+	}
+}
 </style>
diff --git a/src/client/app/common/views/components/page/page.switch.vue b/src/client/components/page/page.switch.vue
similarity index 61%
rename from src/client/app/common/views/components/page/page.switch.vue
rename to src/client/components/page/page.switch.vue
index 53695f1b36760202fb5f4bec98bde070e9b94308..416c36e9ad00dbca5422dc6c30a48c2004d1a114 100644
--- a/src/client/app/common/views/components/page/page.switch.vue
+++ b/src/client/components/page/page.switch.vue
@@ -1,13 +1,17 @@
 <template>
 <div class="hkcxmtwj">
-	<ui-switch v-model="v">{{ script.interpolate(value.text) }}</ui-switch>
+	<mk-switch v-model="v">{{ script.interpolate(value.text) }}</mk-switch>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import MkSwitch from '../ui/switch.vue';
 
 export default Vue.extend({
+	components: {
+		MkSwitch
+	},
 	props: {
 		value: {
 			required: true
@@ -16,13 +20,11 @@ export default Vue.extend({
 			required: true
 		}
 	},
-
 	data() {
 		return {
 			v: this.value.default,
 		};
 	},
-
 	watch: {
 		v() {
 			this.script.aiScript.updatePageVar(this.value.name, this.v);
@@ -32,12 +34,13 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.hkcxmtwj
-	display inline-block
-	margin 16px auto
-
-	& + .hkcxmtwj
-		margin-left 16px
+<style lang="scss" scoped>
+.hkcxmtwj {
+	display: inline-block;
+	margin: 16px auto;
 
+	& + .hkcxmtwj {
+		margin-left: 16px;
+	}
+}
 </style>
diff --git a/src/client/app/common/views/components/page/page.text-input.vue b/src/client/components/page/page.text-input.vue
similarity index 57%
rename from src/client/app/common/views/components/page/page.text-input.vue
rename to src/client/components/page/page.text-input.vue
index cf917dd5a8ee3bbb70895322a276d55dc6ad65bc..fcc181d67371fb2a31705c9ffe00129fcae8734c 100644
--- a/src/client/app/common/views/components/page/page.text-input.vue
+++ b/src/client/components/page/page.text-input.vue
@@ -1,13 +1,17 @@
 <template>
 <div>
-	<ui-input class="kudkigyw" v-model="v" type="text">{{ script.interpolate(value.text) }}</ui-input>
+	<mk-input class="kudkigyw" v-model="v" type="text">{{ script.interpolate(value.text) }}</mk-input>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import MkInput from '../ui/input.vue';
 
 export default Vue.extend({
+	components: {
+		MkInput
+	},
 	props: {
 		value: {
 			required: true
@@ -16,13 +20,11 @@ export default Vue.extend({
 			required: true
 		}
 	},
-
 	data() {
 		return {
 			v: this.value.default,
 		};
 	},
-
 	watch: {
 		v() {
 			this.script.aiScript.updatePageVar(this.value.name, this.v);
@@ -32,10 +34,11 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.kudkigyw
-	display inline-block
-	min-width 300px
-	max-width 450px
-	margin 8px 0
+<style lang="scss" scoped>
+.kudkigyw {
+	display: inline-block;
+	min-width: 300px;
+	max-width: 450px;
+	margin: 8px 0;
+}
 </style>
diff --git a/src/client/app/common/views/components/page/page.text.vue b/src/client/components/page/page.text.vue
similarity index 67%
rename from src/client/app/common/views/components/page/page.text.vue
rename to src/client/components/page/page.text.vue
index 326fd3905033457c9db99193d632ede82ce6fbc5..aeab31225ecaea62c92adb7caa43644f15eac0b1 100644
--- a/src/client/app/common/views/components/page/page.text.vue
+++ b/src/client/components/page/page.text.vue
@@ -1,15 +1,14 @@
 <template>
 <div class="mrdgzndn">
 	<mfm :text="text" :is-note="false" :i="$store.state.i" :key="text"/>
-
 	<mk-url-preview v-for="url in urls" :url="url" :key="url" class="url"/>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import { parse } from '../../../../../../mfm/parse';
-import { unique } from '../../../../../../prelude/array';
+import { parse } from '../../../mfm/parse';
+import { unique } from '../../../prelude/array';
 
 export default Vue.extend({
 	props: {
@@ -20,13 +19,11 @@ export default Vue.extend({
 			required: true
 		}
 	},
-
 	data() {
 		return {
 			text: this.script.interpolate(this.value.text),
 		};
 	},
-
 	computed: {
 		urls(): string[] {
 			if (this.text) {
@@ -40,23 +37,29 @@ export default Vue.extend({
 			}
 		}
 	},
-
-	created() {
-		this.$watch('script.vars', () => {
-			this.text = this.script.interpolate(this.value.text);
-		}, { deep: true });
-	}
+	watch: {
+		'script.vars': {
+			handler() {
+				this.text = this.script.interpolate(this.value.text);
+			},
+			deep: true
+		}
+	},
 });
 </script>
 
-<style lang="stylus" scoped>
-.mrdgzndn
-	&:not(:first-child)
-		margin-top 0.5em
+<style lang="scss" scoped>
+.mrdgzndn {
+	&:not(:first-child) {
+		margin-top: 0.5em;
+	}
 
-	&:not(:last-child)
-		margin-bottom 0.5em
+	&:not(:last-child) {
+		margin-bottom: 0.5em;
+	}
 
-	> .url
-		margin 0.5em 0
+	> .url {
+		margin: 0.5em 0;
+	}
+}
 </style>
diff --git a/src/client/app/common/views/components/page/page.textarea-input.vue b/src/client/components/page/page.textarea-input.vue
similarity index 70%
rename from src/client/app/common/views/components/page/page.textarea-input.vue
rename to src/client/components/page/page.textarea-input.vue
index eece59fefb6a2d90e9f451988312325a6648f156..d1cf9813c4088c6cccffd220d1b5a0433a3bc37b 100644
--- a/src/client/app/common/views/components/page/page.textarea-input.vue
+++ b/src/client/components/page/page.textarea-input.vue
@@ -1,13 +1,17 @@
 <template>
 <div>
-	<ui-textarea class="" v-model="v">{{ script.interpolate(value.text) }}</ui-textarea>
+	<mk-textarea v-model="v">{{ script.interpolate(value.text) }}</mk-textarea>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import MkTextarea from '../ui/textarea.vue';
 
 export default Vue.extend({
+	components: {
+		MkTextarea
+	},
 	props: {
 		value: {
 			required: true
@@ -16,13 +20,11 @@ export default Vue.extend({
 			required: true
 		}
 	},
-
 	data() {
 		return {
 			v: this.value.default,
 		};
 	},
-
 	watch: {
 		v() {
 			this.script.aiScript.updatePageVar(this.value.name, this.v);
@@ -31,6 +33,3 @@ export default Vue.extend({
 	}
 });
 </script>
-
-<style lang="stylus" scoped>
-</style>
diff --git a/src/client/app/common/views/components/page/page.textarea.vue b/src/client/components/page/page.textarea.vue
similarity index 51%
rename from src/client/app/common/views/components/page/page.textarea.vue
rename to src/client/components/page/page.textarea.vue
index 03c8542cb06adef44edbfd22abf5a85856ca87b9..78b74dd64c74b73351b6207f502a85b162e30c4c 100644
--- a/src/client/app/common/views/components/page/page.textarea.vue
+++ b/src/client/components/page/page.textarea.vue
@@ -1,11 +1,15 @@
 <template>
-<ui-textarea class="" :value="text" readonly></ui-textarea>
+<mk-textarea :value="text" readonly></mk-textarea>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import MkTextarea from '../ui/textarea.vue';
 
 export default Vue.extend({
+	components: {
+		MkTextarea
+	},
 	props: {
 		value: {
 			required: true
@@ -14,20 +18,18 @@ export default Vue.extend({
 			required: true
 		}
 	},
-
 	data() {
 		return {
 			text: this.script.interpolate(this.value.text),
 		};
 	},
-
-	created() {
-		this.$watch('script.vars', () => {
-			this.text = this.script.interpolate(this.value.text);
-		}, { deep: true });
+	watch: {
+		'script.vars': {
+			handler() {
+				this.text = this.script.interpolate(this.value.text);
+			},
+			deep: true
+		}
 	}
 });
 </script>
-
-<style lang="stylus" scoped>
-</style>
diff --git a/src/client/app/common/views/components/page/page.vue b/src/client/components/page/page.vue
similarity index 66%
rename from src/client/app/common/views/components/page/page.vue
rename to src/client/components/page/page.vue
index 1bfb93780ff7f4005a702becdd6c3e394d0904ed..bd7831347558da3f82d9384aa9584d4500016ebb 100644
--- a/src/client/app/common/views/components/page/page.vue
+++ b/src/client/components/page/page.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="iroscrza" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners, center: page.alignCenter, serif: page.font === 'serif' }">
+<div class="iroscrza" :class="{ center: page.alignCenter, serif: page.font === 'serif' }">
 	<header v-if="showTitle">
 		<div class="title">{{ page.title }}</div>
 	</header>
@@ -11,7 +11,7 @@
 	<footer v-if="showFooter">
 		<small>@{{ page.user.username }}</small>
 		<template v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId">
-			<router-link :to="`/i/pages/edit/${page.id}`">{{ $t('edit-this-page') }}</router-link>
+			<router-link :to="`/my/pages/edit/${page.id}`">{{ $t('edit-this-page') }}</router-link>
 			<a v-if="$store.state.i.pinnedPageId === page.id" @click="pin(false)">{{ $t('unpin-this-page') }}</a>
 			<a v-else @click="pin(true)">{{ $t('pin-this-page') }}</a>
 		</template>
@@ -27,13 +27,13 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../../../i18n';
+import i18n from '../../i18n';
 import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons';
 import { faHeart } from '@fortawesome/free-regular-svg-icons';
 import XBlock from './page.block.vue';
-import { ASEvaluator } from '../../../../../../misc/aiscript/evaluator';
-import { collectPageVars } from '../../../scripts/collect-page-vars';
-import { url } from '../../../../config';
+import { ASEvaluator } from '../../scripts/aiscript/evaluator';
+import { collectPageVars } from '../../scripts/collect-page-vars';
+import { url } from '../../config';
 
 class Script {
 	public aiScript: ASEvaluator;
@@ -66,7 +66,7 @@ class Script {
 }
 
 export default Vue.extend({
-	i18n: i18n('pages'),
+	i18n,
 
 	components: {
 		XBlock
@@ -146,77 +146,85 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.iroscrza
-	overflow hidden
-	background var(--face)
-	
-	&.serif
-		> div
-			font-family serif
-
-	&.center
-		text-align center
-
-	&.round
-		border-radius 6px
-
-	&.shadow
-		box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
-
-	> header
-		> .title
-			z-index 1
-			margin 0
-			padding 16px 32px
-			font-size 20px
-			font-weight bold
-			color var(--text)
-			box-shadow 0 var(--lineWidth) rgba(#000, 0.07)
-
-			@media (max-width 600px)
-				padding 16px 32px
-				font-size 20px
+<style lang="scss" scoped>
+.iroscrza {
+	&.serif {
+		> div {
+			font-family: serif;
+		}
+	}
 
-			@media (max-width 400px)
-				padding 10px 20px
-				font-size 16px
+	&.center {
+		text-align: center;
+	}
 
-	> div
-		color var(--text)
-		padding 24px 32px
-		font-size 16px
+	> header {
+		> .title {
+			z-index: 1;
+			margin: 0;
+			padding: 16px 32px;
+			font-size: 20px;
+			font-weight: bold;
+			color: var(--text);
+			box-shadow: 0 var(--lineWidth) rgba(#000, 0.07);
+
+			@media (max-width: 600px) {
+				padding: 16px 32px;
+				font-size: 20px;
+			}
+
+			@media (max-width: 400px) {
+				padding: 10px 20px;
+				font-size: 16px;
+			}
+		}
+	}
 
-		@media (max-width 600px)
-			padding 24px 32px
-			font-size 16px
+	> div {
+		color: var(--text);
+		padding: 24px 32px;
+		font-size: 16px;
 
-		@media (max-width 400px)
-			padding 20px 20px
-			font-size 15px
+		@media (max-width: 600px) {
+			padding: 24px 32px;
+			font-size: 16px;
+		}
 
-	> footer
-		color var(--text)
-		padding 0 32px 28px 32px
+		@media (max-width: 400px) {
+			padding: 20px 20px;
+			font-size: 15px;
+		}
+	}
 
-		@media (max-width 600px)
-			padding 0 32px 28px 32px
+	> footer {
+		color: var(--text);
+		padding: 0 32px 28px 32px;
 
-		@media (max-width 400px)
-			padding 0 20px 20px 20px
-			font-size 14px
+		@media (max-width: 600px) {
+			padding: 0 32px 28px 32px;
+		}
 
-		> small
-			display block
-			opacity 0.5
+		@media (max-width: 400px) {
+			padding: 0 20px 20px 20px;
+			font-size: 14px;
+		}
 
-		> a
-			font-size 90%
+		> small {
+			display: block;
+			opacity: 0.5;
+		}
 
-		> a + a
-			margin-left 8px
+		> a {
+			font-size: 90%;
+		}
 
-		> .like
-			margin-top 16px
+		> a + a {
+			margin-left: 8px;
+		}
 
+		> .like {
+			margin-top: 16px;
+		}
+	}
+}
 </style>
diff --git a/src/client/components/poll-editor.vue b/src/client/components/poll-editor.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b5b8c2c02d7dd31bdb0cce3d3f6791ee0f95a507
--- /dev/null
+++ b/src/client/components/poll-editor.vue
@@ -0,0 +1,218 @@
+<template>
+<div class="zmdxowus">
+	<p class="caution" v-if="choices.length < 2">
+		<fa :icon="faExclamationTriangle"/>{{ $t('_poll.noOnlyOneChoice') }}
+	</p>
+	<ul ref="choices">
+		<li v-for="(choice, i) in choices" :key="i">
+			<mk-input class="input" :value="choice" @input="onInput(i, $event)">
+				<span>{{ $t('_poll.choiceN', { n: i + 1 }) }}</span>
+			</mk-input>
+			<button @click="remove(i)" class="_button">
+				<fa :icon="faTimes"/>
+			</button>
+		</li>
+	</ul>
+	<mk-button class="add" v-if="choices.length < 10" @click="add">{{ $t('add') }}</mk-button>
+	<mk-button class="add" v-else disabled>{{ $t('_poll.noMore') }}</mk-button>
+	<section>
+		<mk-switch v-model="multiple">{{ $t('_poll.canMultipleVote') }}</mk-switch>
+		<div>
+			<mk-select v-model="expiration">
+				<template #label>{{ $t('_poll.expiration') }}</template>
+				<option value="infinite">{{ $t('_poll.infinite') }}</option>
+				<option value="at">{{ $t('_poll.at') }}</option>
+				<option value="after">{{ $t('_poll.after') }}</option>
+			</mk-select>
+			<section v-if="expiration === 'at'">
+				<mk-input v-model="atDate" type="date" class="input">
+					<span>{{ $t('_poll.deadlineDate') }}</span>
+				</mk-input>
+				<mk-input v-model="atTime" type="time" class="input">
+					<span>{{ $t('_poll.deadlineTime') }}</span>
+				</mk-input>
+			</section>
+			<section v-if="expiration === 'after'">
+				<mk-input v-model="after" type="number" class="input">
+					<span>{{ $t('_poll.duration') }}</span>
+				</mk-input>
+				<mk-select v-model="unit">
+					<option value="second">{{ $t('_time.second') }}</option>
+					<option value="minute">{{ $t('_time.minute') }}</option>
+					<option value="hour">{{ $t('_time.hour') }}</option>
+					<option value="day">{{ $t('_time.day') }}</option>
+				</mk-select>
+			</section>
+		</div>
+	</section>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faExclamationTriangle, faTimes } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
+import { erase } from '../../prelude/array';
+import { addTimespan } from '../../prelude/time';
+import { formatDateTimeString } from '../../misc/format-time-string';
+import MkInput from './ui/input.vue';
+import MkSelect from './ui/select.vue';
+import MkSwitch from './ui/switch.vue';
+import MkButton from './ui/button.vue';
+
+export default Vue.extend({
+	i18n,
+	components: {
+		MkInput,
+		MkSelect,
+		MkSwitch,
+		MkButton,
+	},
+	data() {
+		return {
+			choices: ['', ''],
+			multiple: false,
+			expiration: 'infinite',
+			atDate: formatDateTimeString(addTimespan(new Date(), 1, 'days'), 'yyyy-MM-dd'),
+			atTime: '00:00',
+			after: 0,
+			unit: 'second',
+			faExclamationTriangle, faTimes
+		};
+	},
+	watch: {
+		choices() {
+			this.$emit('updated');
+		}
+	},
+	methods: {
+		onInput(i, e) {
+			Vue.set(this.choices, i, e);
+		},
+
+		add() {
+			this.choices.push('');
+			this.$nextTick(() => {
+				(this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
+			});
+		},
+
+		remove(i) {
+			this.choices = this.choices.filter((_, _i) => _i != i);
+		},
+
+		get() {
+			const at = () => {
+				return new Date(`${this.atDate} ${this.atTime}`).getTime();
+			};
+
+			const after = () => {
+				let base = parseInt(this.after);
+				switch (this.unit) {
+					case 'day': base *= 24;
+					case 'hour': base *= 60;
+					case 'minute': base *= 60;
+					case 'second': return base *= 1000;
+					default: return null;
+				}
+			};
+
+			return {
+				choices: erase('', this.choices),
+				multiple: this.multiple,
+				...(
+					this.expiration === 'at' ? { expiresAt: at() } :
+					this.expiration === 'after' ? { expiredAfter: after() } : {})
+			};
+		},
+
+		set(data) {
+			if (data.choices.length == 0) return;
+			this.choices = data.choices;
+			if (data.choices.length == 1) this.choices = this.choices.concat('');
+			this.multiple = data.multiple;
+			if (data.expiresAt) {
+				this.expiration = 'at';
+				this.atDate = this.atTime = data.expiresAt;
+			} else if (typeof data.expiredAfter === 'number') {
+				this.expiration = 'after';
+				this.after = data.expiredAfter;
+			} else {
+				this.expiration = 'infinite';
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.zmdxowus {
+	padding: 8px;
+
+	> .caution {
+		margin: 0 0 8px 0;
+		font-size: 0.8em;
+		color: #f00;
+
+		> [data-icon] {
+			margin-right: 4px;
+		}
+	}
+
+	> ul {
+		display: block;
+		margin: 0;
+		padding: 0;
+		list-style: none;
+
+		> li {
+			display: flex;
+			margin: 8px 0;
+			padding: 0;
+			width: 100%;
+
+			> .input {
+				flex: 1;
+				margin-top: 16px;
+				margin-bottom: 0;
+			}
+
+			> button {
+				width: 32px;
+				padding: 4px 0;
+			}
+		}
+	}
+
+	> .add {
+		margin: 8px 0 0 0;
+		z-index: 1;
+	}
+
+	> section {
+		margin: 16px 0 -16px 0;
+
+		> div {
+			margin: 0 8px;
+
+			&:last-child {
+				flex: 1 0 auto;
+
+				> section {
+					align-items: center;
+					display: flex;
+					margin: -32px 0 0;
+
+					> &:first-child {
+						margin-right: 16px;
+					}
+
+					> .input {
+						flex: 1 0 auto;
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/components/poll.vue b/src/client/components/poll.vue
new file mode 100644
index 0000000000000000000000000000000000000000..15be1b282d3b66d8210effca8ba2d664748ac2b1
--- /dev/null
+++ b/src/client/components/poll.vue
@@ -0,0 +1,174 @@
+<template>
+<div class="mk-poll" :data-done="closed || isVoted">
+	<ul>
+		<li v-for="(choice, i) in poll.choices" :key="i" @click="vote(i)" :class="{ voted: choice.voted }">
+			<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
+			<span>
+				<template v-if="choice.isVoted"><fa :icon="faCheck"/></template>
+				<mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/>
+				<span class="votes" v-if="showResult">({{ $t('_poll.votesCount', { n: choice.votes }) }})</span>
+			</span>
+		</li>
+	</ul>
+	<p>
+		<span>{{ $t('_poll.totalVotes', { n: total }) }}</span>
+		<span> · </span>
+		<a v-if="!closed && !isVoted" @click="toggleShowResult">{{ showResult ? $t('_poll.vote') : $t('_poll.showResult') }}</a>
+		<span v-if="isVoted">{{ $t('_poll.voted') }}</span>
+		<span v-else-if="closed">{{ $t('_poll.closed') }}</span>
+		<span v-if="remaining > 0"> · {{ timer }}</span>
+	</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faCheck } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
+import { sum } from '../../prelude/array';
+
+export default Vue.extend({
+	i18n,
+	props: {
+		note: {
+			type: Object,
+			required: true
+		}
+	},
+	data() {
+		return {
+			remaining: -1,
+			showResult: false,
+			faCheck
+		};
+	},
+	computed: {
+		poll(): any {
+			return this.note.poll;
+		},
+		total(): number {
+			return sum(this.poll.choices.map(x => x.votes));
+		},
+		closed(): boolean {
+			return !this.remaining;
+		},
+		timer(): string {
+			return this.$t(
+				this.remaining > 86400 ? '_poll.remainingDays' :
+				this.remaining > 3600 ? '_poll.remainingHours' :
+				this.remaining > 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', {
+					s: Math.floor(this.remaining % 60),
+					m: Math.floor(this.remaining / 60) % 60,
+					h: Math.floor(this.remaining / 3600) % 24,
+					d: Math.floor(this.remaining / 86400)
+				});
+		},
+		isVoted(): boolean {
+			return !this.poll.multiple && this.poll.choices.some(c => c.isVoted);
+		}
+	},
+	created() {
+		this.showResult = this.isVoted;
+
+		if (this.note.poll.expiresAt) {
+			const update = () => {
+				if (this.remaining = Math.floor(Math.max(new Date(this.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000))
+					requestAnimationFrame(update);
+				else
+					this.showResult = true;
+			};
+
+			update();
+		}
+	},
+	methods: {
+		toggleShowResult() {
+			this.showResult = !this.showResult;
+		},
+		vote(id) {
+			if (this.closed || !this.poll.multiple && this.poll.choices.some(c => c.isVoted)) return;
+			this.$root.api('notes/polls/vote', {
+				noteId: this.note.id,
+				choice: id
+			}).then(() => {
+				if (!this.showResult) this.showResult = !this.poll.multiple;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-poll {
+	> ul {
+		display: block;
+		margin: 0;
+		padding: 0;
+		list-style: none;
+
+		> li {
+			display: block;
+			position: relative;
+			margin: 4px 0;
+			padding: 4px 8px;
+			width: 100%;
+			color: var(--pollChoiceText);
+			border: solid 1px var(--pollChoiceBorder);
+			border-radius: 4px;
+			overflow: hidden;
+			cursor: pointer;
+
+			&:hover {
+				background: rgba(#000, 0.05);
+			}
+
+			&:active {
+				background: rgba(#000, 0.1);
+			}
+
+			> .backdrop {
+				position: absolute;
+				top: 0;
+				left: 0;
+				height: 100%;
+				background: var(--accent);
+				transition: width 1s ease;
+			}
+
+			> span {
+				position: relative;
+
+				> [data-icon] {
+					margin-right: 4px;
+				}
+
+				> .votes {
+					margin-left: 4px;
+				}
+			}
+		}
+	}
+
+	> p {
+		color: var(--fg);
+
+		a {
+			color: inherit;
+		}
+	}
+
+	&[data-done] {
+		> ul > li {
+			cursor: default;
+
+			&:hover {
+				background: transparent;
+			}
+
+			&:active {
+				background: transparent;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/components/popup.vue b/src/client/components/popup.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d5b1f9423bcc0ffbe41baff7a62da2893e9c380f
--- /dev/null
+++ b/src/client/components/popup.vue
@@ -0,0 +1,147 @@
+<template>
+<div class="mk-popup">
+	<transition name="bg-fade" appear>
+		<div class="bg" ref="bg" @click="close()" v-if="show"></div>
+	</transition>
+	<transition name="popup" appear @after-leave="() => { $emit('closed'); destroyDom(); }">
+		<div class="content" :class="{ fixed }" ref="content" v-if="show" :style="{ width: width ? width + 'px' : 'auto' }"><slot></slot></div>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: {
+		source: {
+			required: true
+		},
+		noCenter: {
+			type: Boolean,
+			required: false
+		},
+		fixed: {
+			type: Boolean,
+			required: false
+		},
+		width: {
+			type: Number,
+			required: false
+		}
+	},
+	data() {
+		return {
+			show: true,
+		};
+	},
+	mounted() {
+		this.$nextTick(() => {
+			const popover = this.$refs.content as any;
+
+			const rect = this.source.getBoundingClientRect();
+			const width = popover.offsetWidth;
+			const height = popover.offsetHeight;
+
+			let left;
+			let top;
+
+			if (this.$root.isMobile && !this.noCenter) {
+				const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.source.offsetWidth / 2);
+				const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + (this.source.offsetHeight / 2);
+				left = (x - (width / 2));
+				top = (y - (height / 2));
+				popover.style.transformOrigin = 'center';
+			} else {
+				const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.source.offsetWidth / 2);
+				const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + this.source.offsetHeight;
+				left = (x - (width / 2));
+				top = y;
+			}
+
+			if (this.fixed) {
+				if (left + width > window.innerWidth) {
+					left = window.innerWidth - width;
+					popover.style.transformOrigin = 'center';
+				}
+
+				if (top + height > window.innerHeight) {
+					top = window.innerHeight - height;
+					popover.style.transformOrigin = 'center';
+				}
+			} else {
+				if (left + width - window.pageXOffset > window.innerWidth) {
+					left = window.innerWidth - width + window.pageXOffset;
+					popover.style.transformOrigin = 'center';
+				}
+
+				if (top + height - window.pageYOffset > window.innerHeight) {
+					top = window.innerHeight - height + window.pageYOffset;
+					popover.style.transformOrigin = 'center';
+				}
+			}
+
+			if (top < 0) {
+				top = 0;
+			}
+
+			if (left < 0) {
+				left = 0;
+			}
+
+			popover.style.left = left + 'px';
+			popover.style.top = top + 'px';
+		});
+	},
+	methods: {
+		close() {
+			this.show = false;
+			(this.$refs.bg as any).style.pointerEvents = 'none';
+			(this.$refs.content as any).style.pointerEvents = 'none';
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.popup-enter-active, .popup-leave-active {
+	transition: opacity 0.3s, transform 0.3s !important;
+}
+.popup-enter, .popup-leave-to {
+	opacity: 0;
+	transform: scale(0.9);
+}
+
+.bg-fade-enter-active, .bg-fade-leave-active {
+	transition: opacity 0.3s !important;
+}
+.bg-fade-enter, .bg-fade-leave-to {
+	opacity: 0;
+}
+
+.mk-popup {
+	> .bg {
+		position: fixed;
+		top: 0;
+		left: 0;
+		z-index: 10000;
+		width: 100%;
+		height: 100%;
+		background: var(--modalBg)
+	}
+
+	> .content {
+		position: absolute;
+		z-index: 10001;
+		background: var(--panel);
+		border-radius: 4px;
+		box-shadow: 0 3px 12px rgba(27, 31, 35, 0.15);
+		overflow: hidden;
+		transform-origin: center top;
+
+		&.fixed {
+			position: fixed;
+		}
+	}
+}
+</style>
diff --git a/src/client/components/post-form-attaches.vue b/src/client/components/post-form-attaches.vue
new file mode 100644
index 0000000000000000000000000000000000000000..50ba9bfdcf4229c87c8d14c3c4e8e64f355b7afb
--- /dev/null
+++ b/src/client/components/post-form-attaches.vue
@@ -0,0 +1,158 @@
+<template>
+<div class="skeikyzd" v-show="files.length != 0">
+	<x-draggable class="files" :list="files" animation="150">
+		<div v-for="file in files" :key="file.id" @click="showFileMenu(file, $event)" @contextmenu.prevent="showFileMenu(file, $event)">
+			<x-file-thumbnail :data-id="file.id" class="thumbnail" :file="file" fit="cover"/>
+			<div class="sensitive" v-if="file.isSensitive">
+				<fa class="icon" :icon="faExclamationTriangle"/>
+			</div>
+		</div>
+	</x-draggable>
+	<p class="remain">{{ 4 - files.length }}/4</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import * as XDraggable from 'vuedraggable';
+import { faTimesCircle, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
+import { faExclamationTriangle, faICursor } from '@fortawesome/free-solid-svg-icons';
+import XFileThumbnail from './drive-file-thumbnail.vue'
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XDraggable,
+		XFileThumbnail
+	},
+
+	props: {
+		files: {
+			type: Array,
+			required: true
+		},
+		detachMediaFn: {
+			type: Function,
+			required: false
+		}
+	},
+
+	data() {
+		return {
+			faExclamationTriangle
+		};
+	},
+
+	methods: {
+		detachMedia(id) {
+			if (this.detachMediaFn) {
+				this.detachMediaFn(id);
+			} else if (this.$parent.detachMedia) {
+				this.$parent.detachMedia(id);
+			}
+		},
+		toggleSensitive(file) {
+			this.$root.api('drive/files/update', {
+				fileId: file.id,
+				isSensitive: !file.isSensitive
+			}).then(() => {
+				file.isSensitive = !file.isSensitive;
+				this.$parent.updateMedia(file);
+			});
+		},
+		async rename(file) {
+			const { canceled, result } = await this.$root.dialog({
+				title: this.$t('enterFileName'),
+				input: {
+					default: file.name
+				},
+				allowEmpty: false
+			});
+			if (canceled) return;
+			this.$root.api('drive/files/update', {
+				fileId: file.id,
+				name: result
+			}).then(() => {
+				file.name = result;
+				this.$parent.updateMedia(file);
+			});
+		},
+		showFileMenu(file, ev: MouseEvent) {
+			this.$root.menu({
+				items: [{
+					text: this.$t('renameFile'),
+					icon: faICursor,
+					action: () => { this.rename(file) }
+				}, {
+					text: file.isSensitive ? this.$t('unmarkAsSensitive') : this.$t('markAsSensitive'),
+					icon: file.isSensitive ? faEyeSlash : faEye,
+					action: () => { this.toggleSensitive(file) }
+				}, {
+					text: this.$t('attachCancel'),
+					icon: faTimesCircle,
+					action: () => { this.detachMedia(file.id) }
+				}],
+				source: ev.currentTarget || ev.target
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.skeikyzd {
+	padding: 4px;
+	position: relative;
+
+	> .files {
+		display: flex;
+		flex-wrap: wrap;
+
+		> div {
+			position: relative;
+			width: 64px;
+			height: 64px;
+			margin: 4px;
+			cursor: move;
+
+			&:hover > .remove {
+				display: block;
+			}
+
+			> .thumbnail {
+				width: 100%;
+				height: 100%;
+				z-index: 1;
+				color: var(--fg);
+			}
+
+			> .sensitive {
+				display: flex;
+				position: absolute;
+				width: 64px;
+				height: 64px;
+				top: 0;
+				left: 0;
+				z-index: 2;
+				background: rgba(17, 17, 17, .7);
+				color: #fff;
+
+				> .icon {
+					margin: auto;
+				}
+			}
+		}
+	}
+
+	> .remain {
+		display: block;
+		position: absolute;
+		top: 8px;
+		right: 8px;
+		margin: 0;
+		padding: 0;
+	}
+}
+</style>
diff --git a/src/client/components/post-form-dialog.vue b/src/client/components/post-form-dialog.vue
new file mode 100644
index 0000000000000000000000000000000000000000..fe70b88218390f0a4b70fdb7a0e5487f7673b77d
--- /dev/null
+++ b/src/client/components/post-form-dialog.vue
@@ -0,0 +1,157 @@
+<template>
+<div class="ulveipglmagnxfgvitaxyszerjwiqmwl">
+	<transition name="form-fade" appear>
+		<div class="bg" ref="bg" v-if="show" @click="close()"></div>
+	</transition>
+	<div class="main" ref="main" @click.self="close()" @keydown="onKeydown">
+		<transition name="form" appear
+			@after-leave="destroyDom"
+		>
+			<x-post-form ref="form"
+				v-if="show"
+				:reply="reply"
+				:renote="renote"
+				:mention="mention"
+				:specified="specified"
+				:initial-text="initialText"
+				:initial-note="initialNote"
+				:instant="instant"
+				@posted="onPosted"
+				@cancel="onCanceled"/>
+		</transition>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XPostForm from './post-form.vue';
+
+export default Vue.extend({
+	components: {
+		XPostForm
+	},
+
+	props: {
+		reply: {
+			type: Object,
+			required: false
+		},
+		renote: {
+			type: Object,
+			required: false
+		},
+		mention: {
+			type: Object,
+			required: false
+		},
+		specified: {
+			type: Object,
+			required: false
+		},
+		initialText: {
+			type: String,
+			required: false
+		},
+		initialNote: {
+			type: Object,
+			required: false
+		},
+		instant: {
+			type: Boolean,
+			required: false,
+			default: false
+		}
+	},
+
+	data() {
+		return {
+			show: true
+		};
+	},
+
+	methods: {
+		focus() {
+			this.$refs.form.focus();
+		},
+
+		close() {
+			this.show = false;
+			(this.$refs.bg as any).style.pointerEvents = 'none';
+			(this.$refs.main as any).style.pointerEvents = 'none';
+		},
+
+		onPosted() {
+			this.$emit('posted');
+			this.close();
+		},
+
+		onCanceled() {
+			this.$emit('cancel');
+			this.close();
+		},
+
+		onKeydown(e) {
+			if (e.which === 27) { // Esc
+				e.preventDefault();
+				e.stopPropagation();
+				this.close();
+			}
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.form-enter-active, .form-leave-active {
+	transition: opacity 0.3s, transform 0.3s !important;
+}
+.form-enter, .form-leave-to {
+	opacity: 0;
+	transform: scale(0.9);
+}
+
+.form-fade-enter-active, .form-fade-leave-active {
+	transition: opacity 0.3s !important;
+}
+.form-fade-enter, .form-fade-leave-to {
+	opacity: 0;
+}
+
+.ulveipglmagnxfgvitaxyszerjwiqmwl {
+	> .bg {
+		display: block;
+		position: fixed;
+		z-index: 10000;
+		top: 0;
+		left: 0;
+		width: 100%;
+		height: 100%;
+		background: rgba(#000, 0.7);
+	}
+
+	> .main {
+		display: block;
+		position: fixed;
+		z-index: 10000;
+		top: 32px;
+		left: 0;
+		right: 0;
+		height: calc(100% - 64px);
+		width: 500px;
+		max-width: calc(100% - 16px);
+		overflow: auto;
+		margin: 0 auto 0 auto;
+
+		@media (max-width: 550px) {
+			top: 16px;
+			height: calc(100% - 32px);
+		}
+
+		@media (max-width: 520px) {
+			top: 8px;
+			height: calc(100% - 16px);
+		}
+	}
+}
+</style>
diff --git a/src/client/app/common/scripts/post-form.ts b/src/client/components/post-form.vue
similarity index 54%
rename from src/client/app/common/scripts/post-form.ts
rename to src/client/components/post-form.vue
index 496782fd309e9bcdeb7b958040ea210e8cdbe179..762b82036bff0d1ee4d312669a7361d958cc48ab 100644
--- a/src/client/app/common/scripts/post-form.ts
+++ b/src/client/components/post-form.vue
@@ -1,21 +1,82 @@
+<template>
+<div class="gafaadew"
+	@dragover.stop="onDragover"
+	@dragenter="onDragenter"
+	@dragleave="onDragleave"
+	@drop.stop="onDrop"
+>
+	<header>
+		<button class="cancel _button" @click="cancel"><fa :icon="faTimes"/></button>
+		<div>
+			<span class="text-count" :class="{ over: trimmedLength(text) > 500 }">{{ 500 - trimmedLength(text) }}</span>
+			<button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}</button>
+		</div>
+	</header>
+	<div class="form">
+		<x-note-preview class="preview" v-if="reply" :note="reply"/>
+		<x-note-preview class="preview" v-if="renote" :note="renote"/>
+		<div class="with-quote" v-if="quoteId"><fa icon="quote-left"/> {{ $t('@.post-form.quote-attached') }}<button @click="quoteId = null"><fa icon="times"/></button></div>
+		<div v-if="visibility === 'specified'" class="to-specified">
+			<span style="margin-right: 8px;">{{ $t('recipient') }}</span>
+			<div class="visibleUsers">
+				<span v-for="u in visibleUsers">
+					<mk-acct :user="u"/>
+					<button class="_button" @click="removeVisibleUser(u)"><fa :icon="faTimes"/></button>
+				</span>
+				<button @click="addVisibleUser" class="_buttonPrimary"><fa :icon="faPlus" fixed-width/></button>
+			</div>
+		</div>
+		<input v-show="useCw" ref="cw" v-model="cw" :placeholder="$t('annotation')" v-autocomplete="{ model: 'cw' }">
+		<textarea v-model="text" ref="text" :disabled="posting" :placeholder="placeholder" v-autocomplete="{ model: 'text' }" @keydown="onKeydown" @paste="onPaste"></textarea>
+		<x-post-form-attaches class="attaches" :files="files"/>
+		<x-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/>
+		<x-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
+		<footer>
+			<button class="_button" @click="chooseFileFrom"><fa :icon="faPhotoVideo"/></button>
+			<button class="_button" @click="poll = !poll"><fa :icon="faChartPie"/></button>
+			<button class="_button" @click="useCw = !useCw"><fa :icon="faEyeSlash"/></button>
+			<button class="_button" @click="insertMention"><fa :icon="faAt"/></button>
+			<button class="_button" @click="insertEmoji"><fa :icon="faLaughSquint"/></button>
+			<button class="_button" @click="setVisibility" ref="visibilityButton">
+				<span v-if="visibility === 'public'"><fa :icon="faGlobe"/></span>
+				<span v-if="visibility === 'home'"><fa :icon="faHome"/></span>
+				<span v-if="visibility === 'followers'"><fa :icon="faUnlock"/></span>
+				<span v-if="visibility === 'specified'"><fa :icon="faEnvelope"/></span>
+			</button>
+		</footer>
+		<input ref="file" class="file _button" type="file" multiple="multiple" @change="onChangeFile"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faTimes, faUpload, faChartPie, faGlobe, faHome, faUnlock, faEnvelope, faPlus, faPhotoVideo, faCloud, faLink, faAt } from '@fortawesome/free-solid-svg-icons';
+import { faEyeSlash, faLaughSquint } from '@fortawesome/free-regular-svg-icons';
 import insertTextAtCursor from 'insert-text-at-cursor';
 import { length } from 'stringz';
 import { toASCII } from 'punycode';
-import MkVisibilityChooser from '../views/components/visibility-chooser.vue';
-import getFace from './get-face';
-import { parse } from '../../../../mfm/parse';
-import { host, url } from '../../config';
-import i18n from '../../i18n';
-import { erase, unique } from '../../../../prelude/array';
-import extractMentions from '../../../../misc/extract-mentions';
-import { formatTimeString } from '../../../../misc/format-time-string';
-
-export default (opts) => ({
-	i18n: i18n(),
+import i18n from '../i18n';
+import MkVisibilityChooser from './visibility-chooser.vue';
+import MkUserSelect from './user-select.vue';
+import XNotePreview from './note-preview.vue';
+import XEmojiPicker from './emoji-picker.vue';
+import { parse } from '../../mfm/parse';
+import { host, url } from '../config';
+import { erase, unique } from '../../prelude/array';
+import extractMentions from '../../misc/extract-mentions';
+import getAcct from '../../misc/acct/render';
+import { formatTimeString } from '../../misc/format-time-string';
+import { selectDriveFile } from '../scripts/select-drive-file';
+
+export default Vue.extend({
+	i18n,
 
 	components: {
-		XPostFormAttaches: () => import('../views/components/post-form-attaches.vue').then(m => m.default),
-		XPollEditor: () => import('../views/components/poll-editor.vue').then(m => m.default)
+		XNotePreview,
+		XUploader: () => import('./uploader.vue').then(m => m.default),
+		XPostFormAttaches: () => import('./post-form-attaches.vue').then(m => m.default),
+		XPollEditor: () => import('./poll-editor.vue').then(m => m.default)
 	},
 
 	props: {
@@ -31,6 +92,10 @@ export default (opts) => ({
 			type: Object,
 			required: false
 		},
+		specified: {
+			type: Object,
+			required: false
+		},
 		initialText: {
 			type: String,
 			required: false
@@ -58,15 +123,13 @@ export default (opts) => ({
 			pollExpiration: [],
 			useCw: false,
 			cw: null,
-			geo: null,
 			visibility: 'public',
 			visibleUsers: [],
-			localOnly: false,
 			autocomplete: null,
 			draghover: false,
 			quoteId: null,
 			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
-			maxNoteTextLength: 1000
+			faTimes, faUpload, faChartPie, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faCloud, faLink, faAt
 		};
 	},
 
@@ -81,44 +144,38 @@ export default (opts) => ({
 
 		placeholder(): string {
 			const xs = [
-				this.$t('@.note-placeholders.a'),
-				this.$t('@.note-placeholders.b'),
-				this.$t('@.note-placeholders.c'),
-				this.$t('@.note-placeholders.d'),
-				this.$t('@.note-placeholders.e'),
-				this.$t('@.note-placeholders.f')
+				this.$t('_postForm._placeholders.a'),
+				this.$t('_postForm._placeholders.b'),
+				this.$t('_postForm._placeholders.c'),
+				this.$t('_postForm._placeholders.d'),
+				this.$t('_postForm._placeholders.e'),
+				this.$t('_postForm._placeholders.f')
 			];
 			const x = xs[Math.floor(Math.random() * xs.length)];
-
+			
 			return this.renote
-				? opts.mobile ? this.$t('@.post-form.option-quote-placeholder') : this.$t('@.post-form.quote-placeholder')
+				? this.$t('_postForm.quotePlaceholder')
 				: this.reply
-					? this.$t('@.post-form.reply-placeholder')
+					? this.$t('_postForm.replyPlaceholder')
 					: x;
 		},
 
 		submitText(): string {
 			return this.renote
-				? this.$t('@.post-form.renote')
+				? this.$t('renote')
 				: this.reply
-					? this.$t('@.post-form.reply')
-					: this.$t('@.post-form.submit');
+					? this.$t('reply')
+					: this.$t('_postForm.post');
 		},
 
 		canPost(): boolean {
 			return !this.posting &&
 				(1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) &&
-				(length(this.text.trim()) <= this.maxNoteTextLength) &&
+				(length(this.text.trim()) <= 500) &&
 				(!this.poll || this.pollChoices.length >= 2);
 		}
 	},
 
-	created() {
-		this.$root.getMeta().then(meta => {
-			this.maxNoteTextLength = meta.maxNoteTextLength;
-		});
-	},
-
 	mounted() {
 		if (this.initialText) {
 			this.text = this.initialText;
@@ -153,10 +210,6 @@ export default (opts) => ({
 		// デフォルト公開範囲
 		this.applyVisibility(this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility);
 
-		if (this.reply && this.reply.localOnly) {
-			this.localOnly = true;
-		}
-
 		// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
 		if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
 			this.visibility = this.reply.visibility;
@@ -175,6 +228,11 @@ export default (opts) => ({
 			}
 		}
 
+		if (this.specified) {
+			this.visibility = 'specified';
+			this.visibleUsers.push(this.specified);
+		}
+
 		// keep cw when reply
 		if (this.$store.state.settings.keepCw && this.reply && this.reply.cw) {
 			this.useCw = true;
@@ -220,10 +278,7 @@ export default (opts) => ({
 						});
 					});
 				}
-				// hack 位置情報投稿が動くようになったら適用する
-				this.geo = null;
 				this.visibility = init.visibility;
-				this.localOnly = init.localOnly;
 				this.quoteId = init.renote ? init.renote.id : null;
 			}
 
@@ -250,26 +305,50 @@ export default (opts) => ({
 			(this.$refs.text as any).focus();
 		},
 
-		chooseFile() {
+		chooseFileFrom(ev) {
+			this.$root.menu({
+				items: [{
+					type: 'label',
+					text: this.$t('attachFile'),
+				}, {
+					text: this.$t('upload'),
+					icon: faUpload,
+					action: () => { this.chooseFileFromPc() }
+				}, {
+					text: this.$t('fromDrive'),
+					icon: faCloud,
+					action: () => { this.chooseFileFromDrive() }
+				}, {
+					text: this.$t('fromUrl'),
+					icon: faLink,
+					action: () => { this.chooseFileFromUrl() }
+				}],
+				source: ev.currentTarget || ev.target
+			});
+		},
+
+		chooseFileFromPc() {
 			(this.$refs.file as any).click();
 		},
 
 		chooseFileFromDrive() {
-			this.$chooseDriveFile({
-				multiple: true
-			}).then(files => {
-				for (const x of files) this.attachMedia(x);
+			selectDriveFile(this.$root, true).then(files => {
+				for (const file of files) {
+					this.attachMedia(file);
+				}
 			});
 		},
 
 		attachMedia(driveFile) {
 			this.files.push(driveFile);
-			this.$emit('change-attached-files', this.files);
 		},
 
 		detachMedia(id) {
 			this.files = this.files.filter(x => x.id != id);
-			this.$emit('change-attached-files', this.files);
+		},
+
+		updateMedia(file) {
+			Vue.set(this.files, this.files.findIndex(x => x.id === file.id), file);
 		},
 
 		onChangeFile() {
@@ -292,64 +371,23 @@ export default (opts) => ({
 			this.saveDraft();
 		},
 
-		setGeo() {
-			if (navigator.geolocation == null) {
-				this.$root.dialog({
-					type: 'warning',
-					text: this.$t('@.post-form.geolocation-alert')
-				});
-				return;
-			}
-
-			navigator.geolocation.getCurrentPosition(pos => {
-				this.geo = pos.coords;
-				this.$emit('geo-attached', this.geo);
-			}, err => {
-				this.$root.dialog({
-					type: 'error',
-					title: this.$t('@.post-form.error'),
-					text: err.message
-				});
-			}, {
-					enableHighAccuracy: true
-				});
-		},
-
-		removeGeo() {
-			this.geo = null;
-			this.$emit('geo-dettached');
-		},
-
 		setVisibility() {
 			const w = this.$root.new(MkVisibilityChooser, {
 				source: this.$refs.visibilityButton,
-				currentVisibility: this.localOnly ? `local-${this.visibility}` : this.visibility
+				currentVisibility: this.visibility
 			});
 			w.$once('chosen', v => {
 				this.applyVisibility(v);
 			});
-			this.$once('hook:beforeDestroy', () => {
-				w.close();
-			});
 		},
 
 		applyVisibility(v: string) {
-			const m = v.match(/^local-(.+)/);
-			if (m) {
-				this.localOnly = true;
-				this.visibility = m[1];
-			} else {
-				this.localOnly = false;
-				this.visibility = v;
-			}
+			this.visibility = v;
 		},
 
 		addVisibleUser() {
-			this.$root.dialog({
-				title: this.$t('@.post-form.enter-username'),
-				user: true
-			}).then(({ canceled, result: user }) => {
-				if (canceled) return;
+			const vm = this.$root.new(MkUserSelect, {});
+			vm.$once('selected', user => {
 				this.visibleUsers.push(user);
 			});
 		},
@@ -377,16 +415,7 @@ export default (opts) => ({
 					const lio = file.name.lastIndexOf('.');
 					const ext = lio >= 0 ? file.name.slice(lio) : '';
 					const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
-					const name = this.$store.state.settings.pasteDialog
-						? await this.$root.dialog({
-								title: this.$t('@.post-form.enter-file-name'),
-								input: {
-									default: formatted
-								},
-								allowEmpty: false
-							}).then(({ canceled, result }) => canceled ? false : result)
-						: formatted;
-					if (name) this.upload(file, name);
+					this.upload(file, formatted);
 				}
 			}
 
@@ -411,6 +440,7 @@ export default (opts) => ({
 		},
 
 		onDragover(e) {
+			if (!e.dataTransfer.items[0]) return;
 			const isFile = e.dataTransfer.items[0].kind == 'file';
 			const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
 			if (isFile || isDriveFile) {
@@ -449,22 +479,6 @@ export default (opts) => ({
 			//#endregion
 		},
 
-		async emoji() {
-			const Picker = await import('../../desktop/views/components/emoji-picker-dialog.vue').then(m => m.default);
-			const button = this.$refs.emoji;
-			const rect = button.getBoundingClientRect();
-			const vm = this.$root.new(Picker, {
-				x: button.offsetWidth + rect.left + window.pageXOffset,
-				y: rect.top + window.pageYOffset
-			});
-			vm.$once('chosen', emoji => {
-				insertTextAtCursor(this.$refs.text, emoji);
-			});
-			this.$once('hook:beforeDestroy', () => {
-				vm.close();
-			});
-		},
-
 		saveDraft() {
 			if (this.instant) return;
 
@@ -490,13 +504,8 @@ export default (opts) => ({
 			localStorage.setItem('drafts', JSON.stringify(data));
 		},
 
-		kao() {
-			this.text += getFace();
-		},
-
 		post() {
 			this.posting = true;
-			const viaMobile = opts.mobile && !this.$store.state.settings.disableViaMobile;
 			this.$root.api('notes/create', {
 				text: this.text == '' ? undefined : this.text,
 				fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
@@ -506,23 +515,12 @@ export default (opts) => ({
 				cw: this.useCw ? this.cw || '' : undefined,
 				visibility: this.visibility,
 				visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
-				localOnly: this.localOnly,
-				geo: this.geo ? {
-					coordinates: [this.geo.longitude, this.geo.latitude],
-					altitude: this.geo.altitude,
-					accuracy: this.geo.accuracy,
-					altitudeAccuracy: this.geo.altitudeAccuracy,
-					heading: isNaN(this.geo.heading) ? null : this.geo.heading,
-					speed: this.geo.speed,
-				} : null,
-				viaMobile: viaMobile
+				viaMobile: this.$root.isMobile
 			}).then(data => {
 				this.clear();
 				this.deleteDraft();
 				this.$emit('posted');
-				if (opts.onSuccess) opts.onSuccess(this);
 			}).catch(err => {
-				if (opts.onSuccess) opts.onFailure(this);
 			}).then(() => {
 				this.posting = false;
 			});
@@ -533,5 +531,217 @@ export default (opts) => ({
 				localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
 			}
 		},
+
+		cancel() {
+			this.$emit('cancel');
+		},
+
+		insertMention() {
+			const vm = this.$root.new(MkUserSelect, {});
+			vm.$once('selected', user => {
+				insertTextAtCursor(this.$refs.text, getAcct(user) + ' ');
+			});
+		},
+
+		insertEmoji(ev) {
+			const vm = this.$root.new(XEmojiPicker, {
+				source: ev.currentTarget || ev.target
+			}).$once('chosen', emoji => {
+				insertTextAtCursor(this.$refs.text, emoji);
+				vm.close();
+			});
+		}
 	}
 });
+</script>
+
+<style lang="scss" scoped>
+.gafaadew {
+	background: var(--panel);
+	border-radius: var(--radius);
+	box-shadow: 0 0 2px rgba(#000, 0.1);
+
+	> header {
+		z-index: 1000;
+		height: 66px;
+
+		@media (max-width: 500px) {
+			height: 50px;
+		}
+
+		> .cancel {
+			padding: 0;
+			font-size: 20px;
+			width: 64px;
+			line-height: 66px;
+
+			@media (max-width: 500px) {
+				width: 50px;
+				line-height: 50px;
+			}
+		}
+
+		> div {
+			position: absolute;
+			top: 0;
+			right: 0;
+
+			> .text-count {
+				line-height: 66px;
+
+				@media (max-width: 500px) {
+					line-height: 50px;
+				}
+			}
+
+			> .submit {
+				margin: 16px;
+				padding: 0 16px;
+				line-height: 34px;
+				vertical-align: bottom;
+				border-radius: 4px;
+
+				@media (max-width: 500px) {
+					margin: 8px;
+				}
+
+				&:disabled {
+					opacity: 0.7;
+				}
+			}
+		}
+	}
+
+	> .form {
+		max-width: 500px;
+		margin: 0 auto;
+
+		> .preview {
+			padding: 16px;
+		}
+
+		> .with-quote {
+			margin: 0 0 8px 0;
+			color: var(--accent);
+
+			> button {
+				padding: 4px 8px;
+				color: var(--accentAlpha04);
+
+				&:hover {
+					color: var(--accentAlpha06);
+				}
+
+				&:active {
+					color: var(--accentDarken30);
+				}
+			}
+		}
+
+		> .to-specified {
+			padding: 6px 24px;
+			margin-bottom: 8px;
+			overflow: auto;
+			white-space: nowrap;
+
+			@media (max-width: 500px) {
+				padding: 6px 16px;
+			}
+
+			> .visibleUsers {
+				display: inline;
+				top: -1px;
+				font-size: 14px;
+
+				> button {
+					padding: 4px;
+					border-radius: 8px;
+				}
+
+				> span {
+					margin-right: 14px;
+					padding: 8px 0 8px 8px;
+					border-radius: 8px;
+					background: var(--nwjktjjq);
+
+					> button {
+						padding: 4px 8px;
+					}
+				}
+			}
+		}
+
+		> input {
+			z-index: 1;
+		}
+
+		> input,
+		> textarea {
+			display: block;
+			box-sizing: border-box;
+			padding: 0 24px;
+			margin: 0;
+			width: 100%;
+			font-size: 16px;
+			border: none;
+			border-radius: 0;
+			background: transparent;
+			color: var(--fg);
+			font-family: initial;
+
+			@media (max-width: 500px) {
+				padding: 0 16px;
+			}
+
+			&:focus {
+				outline: none;
+			}
+
+			&:disabled {
+				opacity: 0.5;
+			}
+		}
+
+		> textarea {
+			max-width: 100%;
+			min-width: 100%;
+			min-height: 90px;
+
+			@media (max-width: 500px) {
+				min-height: 80px;
+			}
+		}
+
+		> .mk-uploader {
+			margin: 8px 0 0 0;
+			padding: 8px;
+		}
+
+		> .file {
+			display: none;
+		}
+
+		> footer {
+			padding: 0 16px 16px 16px;
+
+			@media (max-width: 500px) {
+				padding: 0 8px 8px 8px;
+			}
+
+			> * {
+				display: inline-block;
+				padding: 0;
+				margin: 0;
+				font-size: 16px;
+				width: 48px;
+				height: 48px;
+				border-radius: 6px;
+
+				&:hover {
+					background: var(--geavgsxy);
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/components/reaction-icon.vue b/src/client/components/reaction-icon.vue
new file mode 100644
index 0000000000000000000000000000000000000000..368ddc0efcb4c75d5ab52f65963266a715a1c313
--- /dev/null
+++ b/src/client/components/reaction-icon.vue
@@ -0,0 +1,32 @@
+<template>
+<mk-emoji :emoji="reaction.startsWith(':') ? null : reaction" :name="reaction.startsWith(':') ? reaction.substr(1, reaction.length - 2) : null" :is-reaction="true" :custom-emojis="customEmojis" :normal="true" :no-style="noStyle"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+export default Vue.extend({
+	i18n,
+	props: {
+		reaction: {
+			type: String,
+			required: true
+		},
+		noStyle: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+	data() {
+		return {
+			customEmojis: []
+		};
+	},
+	created() {
+		this.$root.getMeta().then(meta => {
+			if (meta && meta.emojis) this.customEmojis = meta.emojis;
+		});
+	},
+});
+</script>
diff --git a/src/client/components/reaction-picker.vue b/src/client/components/reaction-picker.vue
new file mode 100644
index 0000000000000000000000000000000000000000..00b964f07ca98e3725e5c0600345c8fbbc583ca5
--- /dev/null
+++ b/src/client/components/reaction-picker.vue
@@ -0,0 +1,229 @@
+<template>
+<x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }" v-hotkey.global="keymap">
+	<div class="rdfaahpb">
+		<transition-group
+			name="reaction-fade"
+			tag="div"
+			class="buttons"
+			ref="buttons"
+			:class="{ showFocus }"
+			:css="false"
+			@before-enter="beforeEnter"
+			@enter="enter"
+			mode="out-in"
+			appear
+		>
+			<button class="_button" v-for="(reaction, i) in rs" :key="reaction" @click="react(reaction)" :data-index="i" :tabindex="i + 1" :title="/^[a-z]+$/.test(reaction) ? $t('@.reactions.' + reaction) : reaction"><x-reaction-icon :reaction="reaction"/></button>
+		</transition-group>
+		<input class="text" v-model="text" :placeholder="$t('enterEmoji')" @keyup.enter="reactText" @input="tryReactText" v-autocomplete="{ model: 'text' }">
+	</div>
+</x-popup>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import { emojiRegex } from '../../misc/emoji-regex';
+import XReactionIcon from './reaction-icon.vue';
+import XPopup from './popup.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XPopup,
+		XReactionIcon,
+	},
+
+	props: {
+		source: {
+			required: true
+		},
+
+		reactions: {
+			required: false
+		},
+
+		showFocus: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+
+	data() {
+		return {
+			rs: this.reactions || this.$store.state.settings.reactions,
+			text: null,
+			focus: null
+		};
+	},
+
+	computed: {
+		keymap(): any {
+			return {
+				'esc': this.close,
+				'enter|space|plus': this.choose,
+				'up|k': this.focusUp,
+				'left|h|shift+tab': this.focusLeft,
+				'right|l|tab': this.focusRight,
+				'down|j': this.focusDown,
+				'1': () => this.react(this.rs[0]),
+				'2': () => this.react(this.rs[1]),
+				'3': () => this.react(this.rs[2]),
+				'4': () => this.react(this.rs[3]),
+				'5': () => this.react(this.rs[4]),
+				'6': () => this.react(this.rs[5]),
+				'7': () => this.react(this.rs[6]),
+				'8': () => this.react(this.rs[7]),
+				'9': () => this.react(this.rs[8]),
+				'0': () => this.react(this.rs[9]),
+			};
+		},
+	},
+
+	watch: {
+		focus(i) {
+			this.$refs.buttons.children[i].elm.focus();
+		}
+	},
+
+	mounted() {
+		this.focus = 0;
+	},
+
+	methods: {
+		close() {
+			this.$refs.popup.close();
+		},
+	
+		react(reaction) {
+			this.$emit('chosen', reaction);
+		},
+
+		reactText() {
+			if (!this.text) return;
+			this.react(this.text);
+		},
+
+		tryReactText() {
+			if (!this.text) return;
+			if (!this.text.match(emojiRegex)) return;
+			this.reactText();
+		},
+
+		focusUp() {
+			this.focus = this.focus == 0 ? 9 : this.focus < 5 ? (this.focus + 4) : (this.focus - 5);
+		},
+
+		focusDown() {
+			this.focus = this.focus == 9 ? 0 : this.focus >= 5 ? (this.focus - 4) : (this.focus + 5);
+		},
+
+		focusRight() {
+			this.focus = this.focus == 9 ? 0 : (this.focus + 1);
+		},
+
+		focusLeft() {
+			this.focus = this.focus == 0 ? 9 : (this.focus - 1);
+		},
+
+		choose() {
+			this.$refs.buttons.children[this.focus].elm.click();
+		},
+
+		beforeEnter(el) {
+			el.style.opacity = 0;
+			el.style.transform = 'scale(0.7)';
+		},
+
+		enter(el, done) {
+			el.style.transition = [getComputedStyle(el).transition, 'transform 1s cubic-bezier(0.23, 1, 0.32, 1)', 'opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1)'].filter(x => x != '').join(',');
+			setTimeout(() => {
+				el.style.opacity = 1;
+				el.style.transform = 'scale(1)';
+				setTimeout(done, 1000);
+			}, 0 * el.dataset.index)
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.rdfaahpb {
+	> .buttons {
+		padding: 6px 6px 0 6px;
+		width: 212px;
+		box-sizing: border-box;
+		text-align: center;
+
+		@media (max-width: 1025px) {
+			padding: 8px 8px 0 8px;
+			width: 256px;
+		}
+
+		&.showFocus {
+			> button:focus {
+				z-index: 1;
+
+				&:after {
+					content: "";
+					pointer-events: none;
+					position: absolute;
+					top: 0;
+					right: 0;
+					bottom: 0;
+					left: 0;
+					border: 2px solid var(--focus);
+					border-radius: 4px;
+				}
+			}
+		}
+
+		> button {
+			padding: 0;
+			width: 40px;
+			height: 40px;
+			font-size: 24px;
+			border-radius: 2px;
+
+			@media (max-width: 1025px) {
+				width: 48px;
+				height: 48px;
+				font-size: 26px;
+			}
+
+			> * {
+				height: 1em;
+			}
+
+			&:hover {
+				background: rgba(0, 0, 0, 0.05);
+			}
+
+			&:active {
+				background: var(--accent);
+				box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
+			}
+		}
+	}
+
+	> .text {
+		width: 208px;
+		padding: 8px;
+		margin: 0 0 6px 0;
+		box-sizing: border-box;
+		text-align: center;
+		font-size: 16px;
+		outline: none;
+		border: none;
+		background: transparent;
+		color: var(--fg);
+
+		@media (max-width: 1025px) {
+			width: 256px;
+			margin: 4px 0 8px 0;
+		}
+	}
+}
+</style>
diff --git a/src/client/components/reactions-viewer.details.vue b/src/client/components/reactions-viewer.details.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ea2523a11f495b4a7970915ddc5127a51db37a03
--- /dev/null
+++ b/src/client/components/reactions-viewer.details.vue
@@ -0,0 +1,117 @@
+<template>
+<transition name="zoom-in-top">
+	<div class="buebdbiu" ref="popover" v-if="show">
+		<template v-if="users.length <= 10">
+			<b v-for="u in users" :key="u.id" style="margin-right: 12px;">
+				<mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/>
+				<mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/>
+			</b>
+		</template>
+		<template v-if="10 < users.length">
+			<b v-for="u in users" :key="u.id" style="margin-right: 12px;">
+				<mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/>
+				<mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/>
+			</b>
+			<span slot="omitted">+{{ count - 10 }}</span>
+		</template>
+	</div>
+</transition>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+
+export default Vue.extend({
+	i18n,
+	props: {
+		reaction: {
+			type: String,
+			required: true,
+		},
+		users: {
+			type: Array,
+			required: true,
+		},
+		count: {
+			type: Number,
+			required: true,
+		},
+		source: {
+			required: true,
+		}
+	},
+	data() {
+		return {
+			show: false
+		};
+	},
+	mounted() {
+		this.show = true;
+
+		this.$nextTick(() => {
+			const popover = this.$refs.popover as any;
+
+			if (this.source == null) {
+				this.destroyDom();
+				return;
+			}
+			const rect = this.source.getBoundingClientRect();
+
+			const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+			const y = rect.top + window.pageYOffset + this.source.offsetHeight;
+			popover.style.left = (x - 28) + 'px';
+			popover.style.top = (y + 16) + 'px';
+		});
+	}
+	methods: {
+		close() {
+			this.show = false;
+			setTimeout(this.destroyDom, 300);
+		}
+	}
+})
+</script>
+
+<style lang="scss" scoped>
+.buebdbiu {
+	z-index: 10000;
+	display: block;
+	position: absolute;
+	max-width: 240px;
+	font-size: 0.8em;
+	padding: 6px 8px;
+	background: var(--panel);
+	text-align: center;
+	border-radius: 4px;
+	box-shadow: 0 2px 8px rgba(0,0,0,0.25);
+	pointer-events: none;
+	transform-origin: center -16px;
+
+	&:before {
+		content: "";
+		pointer-events: none;
+		display: block;
+		position: absolute;
+		top: -28px;
+		left: 12px;
+		border-top: solid 14px transparent;
+		border-right: solid 14px transparent;
+		border-bottom: solid 14px rgba(0,0,0,0.1);
+		border-left: solid 14px transparent;
+	}
+
+	&:after {
+		content: "";
+		pointer-events: none;
+		display: block;
+		position: absolute;
+		top: -27px;
+		left: 12px;
+		border-top: solid 14px transparent;
+		border-right: solid 14px transparent;
+		border-bottom: solid 14px var(--panel);
+		border-left: solid 14px transparent;
+	}
+}
+</style>
diff --git a/src/client/app/common/views/components/reactions-viewer.reaction.vue b/src/client/components/reactions-viewer.reaction.vue
similarity index 61%
rename from src/client/app/common/views/components/reactions-viewer.reaction.vue
rename to src/client/components/reactions-viewer.reaction.vue
index dade012c298969758b75ed8887edb92858b09a87..a878a283ff1aed4e739d0a9eb8d948aeb86c9e87 100644
--- a/src/client/app/common/views/components/reactions-viewer.reaction.vue
+++ b/src/client/components/reactions-viewer.reaction.vue
@@ -1,26 +1,27 @@
 <template>
 <span
-	class="reaction"
+	class="reaction _button"
 	:class="{ reacted: note.myReaction == reaction }"
 	@click="toggleReaction(reaction)"
 	v-if="count > 0"
-	v-particle="!isMe"
 	@mouseover="onMouseover"
 	@mouseleave="onMouseleave"
 	ref="reaction"
 >
-	<mk-reaction-icon :reaction="reaction" ref="icon"/>
+	<x-reaction-icon :reaction="reaction" ref="icon"/>
 	<span>{{ count }}</span>
 </span>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import Icon from './reaction-icon.vue';
-import anime from 'animejs';
 import XDetails from './reactions-viewer.details.vue';
+import XReactionIcon from './reaction-icon.vue';
 
 export default Vue.extend({
+	components: {
+		XReactionIcon
+	},
 	props: {
 		reaction: {
 			type: String,
@@ -126,83 +127,41 @@ export default Vue.extend({
 			}
 		},
 		anime() {
-			if (this.$store.state.device.reduceMotion) return;
 			if (document.hidden) return;
 
-			this.$nextTick(() => {
-				if (this.$refs.icon == null) return;
-
-				const rect = this.$refs.icon.$el.getBoundingClientRect();
-
-				const x = rect.left;
-				const y = rect.top;
-
-				const icon = new Icon({
-					parent: this,
-					propsData: {
-						reaction: this.reaction
-					}
-				}).$mount();
-
-				icon.$el.style.position = 'absolute';
-				icon.$el.style.zIndex = 100;
-				icon.$el.style.top = (y + window.scrollY) + 'px';
-				icon.$el.style.left = (x + window.scrollX) + 'px';
-				icon.$el.style.fontSize = window.getComputedStyle(this.$refs.icon.$el).fontSize;
-
-				document.body.appendChild(icon.$el);
-
-				anime({
-					targets: icon.$el,
-					opacity: [1, 0],
-					translateY: [0, -64],
-					duration: 1000,
-					easing: 'linear',
-					complete: () => {
-						icon.destroyDom();
-					}
-				});
-			});
+			// TODO
 		},
 	}
 });
 </script>
 
-<style lang="stylus" scoped>
-.reaction
-	display inline-block
-	height 32px
-	margin 2px
-	padding 0 6px
-	border-radius 4px
-	cursor pointer
-
-	&, *
-		-webkit-touch-callout none
-		-webkit-user-select none
-		-khtml-user-select none
-		-moz-user-select none
-		-ms-user-select none
-		user-select none
+<style lang="scss" scoped>
+.reaction {
+	display: inline-block;
+	height: 32px;
+	margin: 2px;
+	padding: 0 6px;
+	border-radius: 4px;
 
-	*
-		user-select none
-		pointer-events none
+	&.reacted {
+		background: var(--accent);
 
-	&.reacted
-		background var(--primary)
-
-		> span
-			color var(--primaryForeground)
+		> span {
+			color: #fff;
+		}
+	}
 
-	&:not(.reacted)
-		background var(--reactionViewerButtonBg)
+	&:not(.reacted) {
+		background: rgba(0, 0, 0, 0.05);
 
-		&:hover
-			background var(--reactionViewerButtonHoverBg)
+		&:hover {
+			background: rgba(0, 0, 0, 0.1);
+		}
+	}
 
-	> span
-		font-size 1.1em
-		line-height 32px
-		color var(--text)
+	> span {
+		font-size: 0.9em;
+		line-height: 32px;
+	}
+}
 </style>
diff --git a/src/client/app/common/views/components/reactions-viewer.vue b/src/client/components/reactions-viewer.vue
similarity index 77%
rename from src/client/app/common/views/components/reactions-viewer.vue
rename to src/client/components/reactions-viewer.vue
index 9701d2481a92b4a276bfc70424fc457a52b0075f..d089cf682c15d44cead582426c7bca60885d0ebf 100644
--- a/src/client/app/common/views/components/reactions-viewer.vue
+++ b/src/client/components/reactions-viewer.vue
@@ -31,17 +31,18 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.mk-reactions-viewer
-	margin: 4px -2px
+<style lang="scss" scoped>
+.mk-reactions-viewer {
+	margin: 4px -2px 0 -2px;
 
-	&:empty
-		display none
+	&:empty {
+		display: none;
+	}
 
-	&.isMe
-		> span
-			cursor default !important
-
-			&:hover
-				background var(--reactionViewerButtonBg) !important
+	&.isMe {
+		> span {
+			cursor: default !important;
+		}
+	}
+}
 </style>
diff --git a/src/client/components/renote-picker.vue b/src/client/components/renote-picker.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d8258d5f5df9210a3c454f1ab9f2598ea1c8bcda
--- /dev/null
+++ b/src/client/components/renote-picker.vue
@@ -0,0 +1,94 @@
+<template>
+<x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }" v-hotkey.global="keymap">
+	<div class="rdfaahpc">
+		<button class="_button" @click="renote()"><fa :icon="faRetweet"/></button>
+		<button class="_button" @click="quote()"><fa :icon="faQuoteRight"/></button>
+	</div>
+</x-popup>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faQuoteRight, faRetweet } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
+import XPopup from './popup.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XPopup,
+	},
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+
+		source: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			faQuoteRight, faRetweet
+		};
+	},
+
+	computed: {
+		keymap(): any {
+			return {
+				'esc': this.close,
+			};
+		}
+	},
+
+	methods: {
+		renote() {
+			(this as any).$root.api('notes/create', {
+				renoteId: this.note.id
+			}).then(() => {
+				this.$emit('closed');
+				this.destroyDom();
+			});
+		},
+
+		quote() {
+			this.$emit('closed');
+			this.destroyDom();
+			this.$root.post({
+				renote: this.note,
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.rdfaahpc {
+	padding: 4px;
+
+	> button {
+		padding: 0;
+		width: 40px;
+		height: 40px;
+		font-size: 16px;
+		border-radius: 2px;
+
+		> * {
+			height: 1em;
+		}
+
+		&:hover {
+			background: rgba(0, 0, 0, 0.05);
+		}
+
+		&:active {
+			background: var(--accent);
+			box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
+		}
+	}
+}
+</style>
diff --git a/src/client/components/sequential-entrance.vue b/src/client/components/sequential-entrance.vue
new file mode 100644
index 0000000000000000000000000000000000000000..70e486719ed24586147d5b9e718b6a5d2fa74241
--- /dev/null
+++ b/src/client/components/sequential-entrance.vue
@@ -0,0 +1,63 @@
+<template>
+<transition-group
+	name="staggered-fade"
+	tag="div"
+	:css="false"
+	@before-enter="beforeEnter"
+	@enter="enter"
+	@leave="leave"
+	mode="out-in"
+	appear
+>
+	<slot></slot>
+</transition-group>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: {
+		delay: {
+			type: Number,
+			required: false,
+			default: 40
+		},
+		direction: {
+			type: String,
+			required: false,
+			default: 'down'
+		}
+	},
+	methods: {
+		beforeEnter(el) {
+			el.style.opacity = 0;
+			el.style.transform = this.direction === 'down' ? 'translateY(-64px)' : 'translateY(64px)';
+		},
+		enter(el, done) {
+			el.style.transition = [getComputedStyle(el).transition, 'transform 0.7s cubic-bezier(0.23, 1, 0.32, 1)', 'opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1)'].filter(x => x != '').join(',');
+			setTimeout(() => {
+				el.style.opacity = 1;
+				el.style.transform = 'translateY(0px)';
+				setTimeout(done, 700);
+			}, this.delay * el.dataset.index)
+		},
+		leave(el, done) {
+			setTimeout(() => {
+				el.style.opacity = 0;
+				el.style.transform = this.direction === 'down' ? 'translateY(64px)' : 'translateY(-64px)';
+				setTimeout(done, 700);
+			}, this.delay * el.dataset.index)
+		},
+		focus() {
+			this.$slots.default[0].elm.focus();
+		}
+	}
+});
+</script>
+
+<style lang="scss">
+.staggered-fade-move {
+	transition: transform 0.7s !important;
+}
+</style>
diff --git a/src/client/components/signin-dialog.vue b/src/client/components/signin-dialog.vue
new file mode 100644
index 0000000000000000000000000000000000000000..dbc63c93bf52c4e805e8c7ffbb315fd2f70da88e
--- /dev/null
+++ b/src/client/components/signin-dialog.vue
@@ -0,0 +1,37 @@
+<template>
+<x-window ref="window" @closed="() => { $emit('closed'); destroyDom(); }">
+	<template #header>{{ $t('login') }}</template>
+	<x-signin :auto-set="autoSet" @login="onLogin"/>
+</x-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import XWindow from './window.vue';
+import XSignin from './signin.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XSignin,
+		XWindow,
+	},
+
+	props: {
+		autoSet: {
+			type: Boolean,
+			required: false,
+			default: false,
+		}
+	},
+
+	methods: {
+		onLogin(res) {
+			this.$emit('login', res);
+			this.$refs.window.close();
+		}
+	}
+});
+</script>
diff --git a/src/client/app/common/views/components/signin.vue b/src/client/components/signin.vue
similarity index 69%
rename from src/client/app/common/views/components/signin.vue
rename to src/client/components/signin.vue
index bb4a6605bd31a9c23e2202fb51b9db70717c211a..dc6fad1c5d6c23e645843874ab872aa0c9776706 100644
--- a/src/client/app/common/views/components/signin.vue
+++ b/src/client/components/signin.vue
@@ -1,17 +1,17 @@
 <template>
-<form class="mk-signin" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
+<form class="eppvobhk" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
 	<div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div>
 	<div class="normal-signin" v-if="!totpLogin">
-		<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange">
+		<mk-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange">
 			<span>{{ $t('username') }}</span>
 			<template #prefix>@</template>
 			<template #suffix>@{{ host }}</template>
-		</ui-input>
-		<ui-input v-model="password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required>
+		</mk-input>
+		<mk-input v-model="password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required>
 			<span>{{ $t('password') }}</span>
-			<template #prefix><fa icon="lock"/></template>
-		</ui-input>
-		<ui-button type="submit" :disabled="signing">{{ signing ? $t('signing-in') : $t('@.signin') }}</ui-button>
+			<template #prefix><fa :icon="faLock"/></template>
+		</mk-input>
+		<mk-button type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $t('loggingIn') : $t('login') }}</mk-button>
 		<p v-if="meta && meta.enableTwitterIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/twitter`"><fa :icon="['fab', 'twitter']"/> {{ $t('signin-with-twitter') }}</a></p>
 		<p v-if="meta && meta.enableGithubIntegration"  style="margin: 8px 0;"><a :href="`${apiUrl}/signin/github`"><fa :icon="['fab', 'github']"/> {{ $t('signin-with-github') }}</a></p>
 		<p v-if="meta && meta.enableDiscordIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/discord`"><fa :icon="['fab', 'discord']"/> {{ $t('signin-with-discord') /* TODO: Make these layouts better */ }}</a></p>
@@ -19,24 +19,24 @@
 	<div class="2fa-signin" v-if="totpLogin" :class="{ securityKeys: user && user.securityKeys }">
 		<div v-if="user && user.securityKeys" class="twofa-group tap-group">
 			<p>{{ $t('tap-key') }}</p>
-			<ui-button @click="queryKey" v-if="!queryingKey">
+			<mk-button @click="queryKey" v-if="!queryingKey">
 				{{ $t('@.error.retry') }}
-			</ui-button>
+			</mk-button>
 		</div>
 		<div class="or-hr" v-if="user && user.securityKeys">
 			<p class="or-msg">{{ $t('or') }}</p>
 		</div>
 		<div class="twofa-group totp-group">
-			<p style="margin-bottom:0;">{{ $t('enter-2fa-code') }}</p>
-			<ui-input v-model="password" type="password" :with-password-toggle="true" v-if="user && user.usePasswordLessLogin" required>
+			<p style="margin-bottom:0;">{{ $t('twoStepAuthentication') }}</p>
+			<mk-input v-model="password" type="password" :with-password-toggle="true" v-if="user && user.usePasswordLessLogin" required>
 				<span>{{ $t('password') }}</span>
-				<template #prefix><fa icon="lock"/></template>
-			</ui-input>
-			<ui-input v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
-				<span>{{ $t('@.2fa') }}</span>
-				<template #prefix><fa icon="gavel"/></template>
-			</ui-input>
-			<ui-button type="submit" :disabled="signing">{{ signing ? $t('signing-in') : $t('@.signin') }}</ui-button>
+				<template #prefix><fa :icon="faLock"/></template>
+			</mk-input>
+			<mk-input v-model="token" type="number" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
+				<span>{{ $t('token') }}</span>
+				<template #prefix><fa :icon="faGavel"/></template>
+			</mk-input>
+			<mk-button type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? $t('loggingIn') : $t('login') }}</mk-button>
 		</div>
 	</div>
 </form>
@@ -44,19 +44,32 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../../i18n';
-import { apiUrl, host } from '../../../config';
 import { toUnicode } from 'punycode';
-import { hexifyAB } from '../../scripts/2fa';
+import { faLock, faGavel } from '@fortawesome/free-solid-svg-icons';
+import MkButton from './ui/button.vue';
+import MkInput from './ui/input.vue';
+import i18n from '../i18n';
+import { apiUrl, host } from '../config';
+import { hexifyAB } from '../scripts/2fa';
 
 export default Vue.extend({
-	i18n: i18n('common/views/components/signin.vue'),
+	i18n,
+
+	components: {
+		MkButton,
+		MkInput,
+	},
 
 	props: {
 		withAvatar: {
 			type: Boolean,
 			required: false,
 			default: true
+		},
+		autoSet: {
+			type: Boolean,
+			required: false,
+			default: false,
 		}
 	},
 
@@ -74,6 +87,7 @@ export default Vue.extend({
 			credential: null,
 			challengeData: null,
 			queryingKey: false,
+			faLock, faGavel
 		};
 	},
 
@@ -81,6 +95,13 @@ export default Vue.extend({
 		this.$root.getMeta().then(meta => {
 			this.meta = meta;
 		});
+
+		if (this.autoSet) {
+			this.$once('login', res => {
+				localStorage.setItem('i', res.i);
+				location.reload();
+			});
+		}
 	},
 
 	methods: {
@@ -127,8 +148,7 @@ export default Vue.extend({
 					challengeId: this.challengeData.challengeId
 				});
 			}).then(res => {
-				localStorage.setItem('i', res.i);
-				location.reload();
+				this.$emit('login', res);
 			}).catch(err => {
 				if (err === null) return;
 				this.$root.dialog({
@@ -141,7 +161,6 @@ export default Vue.extend({
 
 		onSubmit() {
 			this.signing = true;
-
 			if (!this.totpLogin && this.user && this.user.twoFactorEnabled) {
 				if (window.PublicKeyCredential && this.user.securityKeys) {
 					this.$root.api('signin', {
@@ -171,12 +190,11 @@ export default Vue.extend({
 					password: this.password,
 					token: this.user && this.user.twoFactorEnabled ? this.token : undefined
 				}).then(res => {
-					localStorage.setItem('i', res.i);
-					location.reload();
+					this.$emit('login', res);
 				}).catch(() => {
 					this.$root.dialog({
 						type: 'error',
-						text: this.$t('login-failed')
+						text: this.$t('loginFailed')
 					});
 					this.signing = false;
 				});
@@ -186,63 +204,16 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.mk-signin
-	color #555
-
-	.or-hr,
-	.or-hr .or-msg,
-	.twofa-group,
-	.twofa-group p
-		color var(--text)
-
-	.tap-group > button
-		margin-bottom 1em
-
-	.securityKeys .or-hr
-		&
-			position relative
-
-		.or-msg
-			&:before
-				right 100%
-				margin-right 0.125em
-
-			&:after
-				left 100%
-				margin-left 0.125em
-
-			&:before, &:after
-				content ""
-				position absolute
-				top 50%
-				width 100%
-				height 2px
-				background #555
-
-			&
-				position relative
-				margin auto
-				left 0
-				right 0
-				top 0
-				bottom 0
-				font-size 1.5em
-				height 1.5em
-				width 3em
-				text-align center
-
-	&.signing
-		&, *
-			cursor wait !important
-
-	> .avatar
-		margin 0 auto 0 auto
-		width 64px
-		height 64px
-		background #ddd
-		background-position center
-		background-size cover
-		border-radius 100%
-
+<style lang="scss" scoped>
+.eppvobhk {
+	> .avatar {
+		margin: 0 auto 0 auto;
+		width: 64px;
+		height: 64px;
+		background: #ddd;
+		background-position: center;
+		background-size: cover;
+		border-radius: 100%;
+	}
+}
 </style>
diff --git a/src/client/components/signup-dialog.vue b/src/client/components/signup-dialog.vue
new file mode 100644
index 0000000000000000000000000000000000000000..76421d44ece5b30441ae062f6452a57d4434eed0
--- /dev/null
+++ b/src/client/components/signup-dialog.vue
@@ -0,0 +1,22 @@
+<template>
+<x-window @closed="() => { $emit('closed'); destroyDom(); }">
+	<template #header>{{ $t('signup') }}</template>
+	<x-signup/>
+</x-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import XWindow from './window.vue';
+import XSignup from './signup.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XSignup,
+		XWindow,
+	},
+});
+</script>
diff --git a/src/client/app/common/views/components/signup.vue b/src/client/components/signup.vue
similarity index 67%
rename from src/client/app/common/views/components/signup.vue
rename to src/client/components/signup.vue
index 893f6575fb179fce12caf5cd779bff3b61410081..c03a99def6191ea9745f51400a1326e0d6c894d7 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/components/signup.vue
@@ -1,62 +1,72 @@
 <template>
 <form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()">
 	<template v-if="meta">
-		<ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required styl="fill">
+		<mk-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required>
 			<span>{{ $t('invitation-code') }}</span>
 			<template #prefix><fa icon="id-card-alt"/></template>
 			<template #desc v-html="this.$t('invitation-info').replace('{}', 'mailto:' + meta.maintainerEmail)"></template>
-		</ui-input>
-		<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername" styl="fill">
+		</mk-input>
+		<mk-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername">
 			<span>{{ $t('username') }}</span>
 			<template #prefix>@</template>
 			<template #suffix>@{{ host }}</template>
 			<template #desc>
-				<span v-if="usernameState == 'wait'" style="color:#999"><fa icon="spinner" pulse fixed-width/> {{ $t('checking') }}</span>
-				<span v-if="usernameState == 'ok'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('available') }}</span>
-				<span v-if="usernameState == 'unavailable'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('unavailable') }}</span>
-				<span v-if="usernameState == 'error'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('error') }}</span>
-				<span v-if="usernameState == 'invalid-format'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('invalid-format') }}</span>
-				<span v-if="usernameState == 'min-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('too-short') }}</span>
-				<span v-if="usernameState == 'max-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('too-long') }}</span>
+				<span v-if="usernameState == 'wait'" style="color:#999"><fa :icon="faSpinner" pulse fixed-width/> {{ $t('checking') }}</span>
+				<span v-if="usernameState == 'ok'" style="color:#3CB7B5"><fa :icon="faCheck" fixed-width/> {{ $t('available') }}</span>
+				<span v-if="usernameState == 'unavailable'" style="color:#FF1161"><fa :icon="faExclamationTriangle" fixed-width/> {{ $t('unavailable') }}</span>
+				<span v-if="usernameState == 'error'" style="color:#FF1161"><fa :icon="faExclamationTriangle" fixed-width/> {{ $t('error') }}</span>
+				<span v-if="usernameState == 'invalid-format'" style="color:#FF1161"><fa :icon="faExclamationTriangle" fixed-width/> {{ $t('invalid-format') }}</span>
+				<span v-if="usernameState == 'min-range'" style="color:#FF1161"><fa :icon="faExclamationTriangle" fixed-width/> {{ $t('too-short') }}</span>
+				<span v-if="usernameState == 'max-range'" style="color:#FF1161"><fa :icon="faExclamationTriangle" fixed-width/> {{ $t('too-long') }}</span>
 			</template>
-		</ui-input>
-		<ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true" styl="fill">
+		</mk-input>
+		<mk-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword">
 			<span>{{ $t('password') }}</span>
-			<template #prefix><fa icon="lock"/></template>
+			<template #prefix><fa :icon="faLock"/></template>
 			<template #desc>
-				<p v-if="passwordStrength == 'low'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('weak-password') }}</p>
-				<p v-if="passwordStrength == 'medium'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('normal-password') }}</p>
-				<p v-if="passwordStrength == 'high'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('strong-password') }}</p>
+				<p v-if="passwordStrength == 'low'" style="color:#FF1161"><fa :icon="faExclamationTriangle" fixed-width/> {{ $t('weak-password') }}</p>
+				<p v-if="passwordStrength == 'medium'" style="color:#3CB7B5"><fa :icon="faCheck" fixed-width/> {{ $t('normal-password') }}</p>
+				<p v-if="passwordStrength == 'high'" style="color:#3CB7B5"><fa :icon="faCheck" fixed-width/> {{ $t('strong-password') }}</p>
 			</template>
-		</ui-input>
-		<ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype" styl="fill">
+		</mk-input>
+		<mk-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype">
 			<span>{{ $t('password') }} ({{ $t('retype') }})</span>
-			<template #prefix><fa icon="lock"/></template>
+			<template #prefix><fa :icon="faLock"/></template>
 			<template #desc>
-				<p v-if="passwordRetypeState == 'match'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('password-matched') }}</p>
-				<p v-if="passwordRetypeState == 'not-match'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('password-not-matched') }}</p>
+				<p v-if="passwordRetypeState == 'match'" style="color:#3CB7B5"><fa :icon="faCheck" fixed-width/> {{ $t('password-matched') }}</p>
+				<p v-if="passwordRetypeState == 'not-match'" style="color:#FF1161"><fa :icon="faExclamationTriangle" fixed-width/> {{ $t('password-not-matched') }}</p>
 			</template>
-		</ui-input>
-		<ui-switch v-model="ToSAgreement" v-if="meta.ToSUrl">
-			<i18n path="agree-to">
-				<a :href="meta.ToSUrl" target="_blank">{{ $t('tos') }}</a>
+		</mk-input>
+		<mk-switch v-model="ToSAgreement" v-if="meta.tosUrl">
+			<i18n path="agreeTo">
+				<a :href="meta.tosUrl" target="_blank">{{ $t('tos') }}</a>
 			</i18n>
-		</ui-switch>
+		</mk-switch>
 		<div v-if="meta.enableRecaptcha" class="g-recaptcha" :data-sitekey="meta.recaptchaSiteKey" style="margin: 16px 0;"></div>
-		<ui-button type="submit" :disabled=" submitting || !(meta.ToSUrl ? ToSAgreement : true) || passwordRetypeState == 'not-match'">{{ $t('create') }}</ui-button>
+		<mk-button type="submit" :disabled=" submitting || !(meta.tosUrl ? ToSAgreement : true) || passwordRetypeState == 'not-match'" primary>{{ $t('start') }}</mk-button>
 	</template>
 </form>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../../i18n';
+import { faLock, faExclamationTriangle, faSpinner, faCheck } from '@fortawesome/free-solid-svg-icons';
 const getPasswordStrength = require('syuilo-password-strength');
-import { host, url } from '../../../config';
 import { toUnicode } from 'punycode';
+import i18n from '../i18n';
+import { host, url } from '../config';
+import MkButton from './ui/button.vue';
+import MkInput from './ui/input.vue';
+import MkSwitch from './ui/switch.vue';
 
 export default Vue.extend({
-	i18n: i18n('common/views/components/signup.vue'),
+	i18n,
+
+	components: {
+		MkButton,
+		MkInput,
+		MkSwitch,
+	},
 
 	data() {
 		return {
@@ -71,7 +81,8 @@ export default Vue.extend({
 			passwordRetypeState: null,
 			meta: {},
 			submitting: false,
-			ToSAgreement: false
+			ToSAgreement: false,
+			faLock, faExclamationTriangle, faSpinner, faCheck
 		}
 	},
 
@@ -178,8 +189,3 @@ export default Vue.extend({
 	}
 });
 </script>
-
-<style lang="stylus" scoped>
-.mk-signup
-	min-width 302px
-</style>
diff --git a/src/client/components/sub-note-content.vue b/src/client/components/sub-note-content.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e60c19744231a071328a9babc5da59ae3517743e
--- /dev/null
+++ b/src/client/components/sub-note-content.vue
@@ -0,0 +1,65 @@
+<template>
+<div class="wrmlmaau">
+	<div class="body">
+		<span v-if="note.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
+		<span v-if="note.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span>
+		<router-link class="reply" v-if="note.replyId" :to="`/notes/${note.replyId}`"><fa :icon="faReply"/></router-link>
+		<mfm v-if="note.text" :text="note.text" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis"/>
+		<router-link class="rp" v-if="note.renoteId" :to="`/notes/${note.renoteId}`">RN: ...</router-link>
+	</div>
+	<details v-if="note.files.length > 0">
+		<summary>({{ $t('withNFiles', { n: note.files.length }) }})</summary>
+		<x-media-list :media-list="note.files"/>
+	</details>
+	<details v-if="note.poll">
+		<summary>{{ $t('poll') }}</summary>
+		<x-poll :note="note"/>
+	</details>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faReply } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
+import XPoll from './poll.vue';
+import XMediaList from './media-list.vue';
+
+export default Vue.extend({
+	i18n,
+	components: {
+		XPoll,
+		XMediaList,
+	},
+	props: {
+		note: {
+			type: Object,
+			required: true
+		}
+	},
+	data() {
+		return {
+			faReply
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.wrmlmaau {
+	overflow-wrap: break-word;
+
+	> .body {
+		> .reply {
+			margin-right: 6px;
+			color: var(--accent);
+		}
+
+		> .rp {
+			margin-left: 4px;
+			font-style: oblique;
+			color: var(--renote);
+		}
+	}
+}
+</style>
diff --git a/src/client/app/common/views/components/time.vue b/src/client/components/time.vue
similarity index 52%
rename from src/client/app/common/views/components/time.vue
rename to src/client/components/time.vue
index 8cfcc4cb4f33032b552cd855ed337544ffb1ab53..922067b4d5efd055e7cc037b8267429c19379d16 100644
--- a/src/client/app/common/views/components/time.vue
+++ b/src/client/components/time.vue
@@ -1,17 +1,17 @@
 <template>
 <time class="mk-time" :title="absolute">
-	<span v-if=" mode == 'relative' ">{{ relative }}</span>
-	<span v-if=" mode == 'absolute' ">{{ absolute }}</span>
-	<span v-if=" mode == 'detail' ">{{ absolute }} ({{ relative }})</span>
+	<span v-if="mode == 'relative'">{{ relative }}</span>
+	<span v-if="mode == 'absolute'">{{ absolute }}</span>
+	<span v-if="mode == 'detail'">{{ absolute }} ({{ relative }})</span>
 </time>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../../i18n';
+import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n: i18n(),
+	i18n,
 	props: {
 		time: {
 			type: [Date, String],
@@ -39,15 +39,15 @@ export default Vue.extend({
 			const time = this._time;
 			const ago = (this.now.getTime() - time.getTime()) / 1000/*ms*/;
 			return (
-				ago >= 31536000 ? this.$t('@.time.years_ago')  .replace('{}', (~~(ago / 31536000)).toString()) :
-				ago >= 2592000  ? this.$t('@.time.months_ago') .replace('{}', (~~(ago / 2592000)).toString()) :
-				ago >= 604800   ? this.$t('@.time.weeks_ago')  .replace('{}', (~~(ago / 604800)).toString()) :
-				ago >= 86400    ? this.$t('@.time.days_ago')   .replace('{}', (~~(ago / 86400)).toString()) :
-				ago >= 3600     ? this.$t('@.time.hours_ago')  .replace('{}', (~~(ago / 3600)).toString()) :
-				ago >= 60       ? this.$t('@.time.minutes_ago').replace('{}', (~~(ago / 60)).toString()) :
-				ago >= 10       ? this.$t('@.time.seconds_ago').replace('{}', (~~(ago % 60)).toString()) :
-				ago >= -1       ? this.$t('@.time.just_now') :
-				ago <  -1       ? this.$t('@.time.future') :
+				ago >= 31536000 ? this.$t('_ago.yearsAgo',   { n: (~~(ago / 31536000)).toString() }) :
+				ago >= 2592000  ? this.$t('_ago.monthsAgo',  { n: (~~(ago / 2592000)).toString() }) :
+				ago >= 604800   ? this.$t('_ago.weeksAgo',   { n: (~~(ago / 604800)).toString() }) :
+				ago >= 86400    ? this.$t('_ago.daysAgo',    { n: (~~(ago / 86400)).toString() }) :
+				ago >= 3600     ? this.$t('_ago.hoursAgo',   { n: (~~(ago / 3600)).toString() }) :
+				ago >= 60       ? this.$t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
+				ago >= 10       ? this.$t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
+				ago >= -1       ? this.$t('_ago.justNow') :
+				ago <  -1       ? this.$t('_ago.future') :
 				this.$t('@.time.unknown'));
 		}
 	},
diff --git a/src/client/components/timeline.vue b/src/client/components/timeline.vue
new file mode 100644
index 0000000000000000000000000000000000000000..f5edb185501fa3ce65d7ef0c4b0665612ffaca7b
--- /dev/null
+++ b/src/client/components/timeline.vue
@@ -0,0 +1,118 @@
+<template>
+<x-notes ref="tl" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XNotes from './notes.vue';
+
+export default Vue.extend({
+	components: {
+		XNotes
+	},
+
+	props: {
+		src: {
+			type: String,
+			required: true
+		},
+		list: {
+			required: false
+		},
+		antenna: {
+			required: false
+		}
+	},
+
+	data() {
+		return {
+			connection: null,
+			pagination: null,
+			baseQuery: {
+				includeMyRenotes: this.$store.state.settings.showMyRenotes,
+				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+				includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+			},
+			query: {},
+		};
+	},
+
+	created() {
+		this.$once('hook:beforeDestroy', () => {
+			this.connection.dispose();
+		});
+
+		const prepend = note => {
+			(this.$refs.tl as any).prepend(note);
+		};
+
+		const onUserAdded = () => {
+			(this.$refs.tl as any).reload();
+		};
+
+		const onUserRemoved = () => {
+			(this.$refs.tl as any).reload();
+		};
+
+		let endpoint;
+
+		if (this.src == 'antenna') {
+			endpoint = 'antennas/notes';
+			this.query = {
+				antennaId: this.antenna.id
+			};
+			this.connection = this.$root.stream.connectToChannel('antenna', {
+				antennaId: this.antenna.id
+			});
+			this.connection.on('note', prepend);
+		} else if (this.src == 'home') {
+			endpoint = 'notes/timeline';
+			const onChangeFollowing = () => {
+				this.fetch();
+			};
+			this.connection = this.$root.stream.useSharedConnection('homeTimeline');
+			this.connection.on('note', prepend);
+			this.connection.on('follow', onChangeFollowing);
+			this.connection.on('unfollow', onChangeFollowing);
+		} else if (this.src == 'local') {
+			endpoint = 'notes/local-timeline';
+			this.connection = this.$root.stream.useSharedConnection('localTimeline');
+			this.connection.on('note', prepend);
+		} else if (this.src == 'social') {
+			endpoint = 'notes/hybrid-timeline';
+			this.connection = this.$root.stream.useSharedConnection('hybridTimeline');
+			this.connection.on('note', prepend);
+		} else if (this.src == 'global') {
+			endpoint = 'notes/global-timeline';
+			this.connection = this.$root.stream.useSharedConnection('globalTimeline');
+			this.connection.on('note', prepend);
+		} else if (this.src == 'list') {
+			endpoint = 'notes/user-list-timeline';
+			this.query = {
+				listId: this.list.id
+			};
+			this.connection = this.$root.stream.connectToChannel('userList', {
+				listId: this.list.id
+			});
+			this.connection.on('note', prepend);
+			this.connection.on('userAdded', onUserAdded);
+			this.connection.on('userRemoved', onUserRemoved);
+		}
+
+		this.pagination = {
+			endpoint: endpoint,
+			limit: 10,
+			params: init => ({
+				untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
+				...this.baseQuery, ...this.query
+			})
+		};
+	},
+
+	methods: {
+		focus() {
+			this.$refs.tl.focus();
+		}
+	}
+});
+</script>
diff --git a/src/client/components/toast.vue b/src/client/components/toast.vue
new file mode 100644
index 0000000000000000000000000000000000000000..fefe91e3bd6e137050f9b7e147371864138198cd
--- /dev/null
+++ b/src/client/components/toast.vue
@@ -0,0 +1,76 @@
+<template>
+<div class="mk-toast">
+	<transition name="notification-slide" appear @after-leave="() => { destroyDom(); }">
+		<x-notification :notification="notification" class="notification" v-if="show"/>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XNotification from './notification.vue';
+
+export default Vue.extend({
+	components: {
+		XNotification
+	},
+	props: {
+		notification: {
+			type: Object,
+			required: true
+		}
+	},
+	data() {
+		return {
+			show: true
+		};
+	},
+	mounted() {
+		setTimeout(() => {
+			this.show = false;
+		}, 6000);
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.notification-slide-enter-active, .notification-slide-leave-active {
+	transition: opacity 0.3s, transform 0.3s !important;
+}
+.notification-slide-enter, .notification-slide-leave-to {
+	opacity: 0;
+	transform: translateX(-250px);
+}
+
+.mk-toast {
+	position: fixed;
+	z-index: 10000;
+	left: 0;
+	width: 250px;
+	top: 32px;
+	padding: 0 32px;
+	pointer-events: none;
+
+	@media (max-width: 700px) {
+		top: initial;
+		bottom: 112px;
+		padding: 0 16px;
+	}
+
+	@media (max-width: 500px) {
+		bottom: 92px;
+		padding: 0 8px;
+	}
+
+	> .notification {
+		height: 100%;
+		-webkit-backdrop-filter: blur(12px);
+		backdrop-filter: blur(12px);
+		background-color: var(--toastBg);
+		box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
+		border-radius: 8px;
+		color: var(--toastFg);
+		overflow: hidden;
+	}
+}
+</style>
diff --git a/src/client/components/ui/button.vue b/src/client/components/ui/button.vue
new file mode 100644
index 0000000000000000000000000000000000000000..4071faa1dde0e03372173ec335d2ad41a33e386d
--- /dev/null
+++ b/src/client/components/ui/button.vue
@@ -0,0 +1,204 @@
+<template>
+<component class="bghgjjyj _button"
+	:is="link ? 'a' : 'button'"
+	:class="{ inline, primary }"
+	:type="type"
+	@click="$emit('click', $event)"
+	@mousedown="onMousedown"
+>
+	<div ref="ripples" class="ripples"></div>
+	<div class="content">
+		<slot></slot>
+	</div>
+</component>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: {
+		type: {
+			type: String,
+			required: false
+		},
+		primary: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		inline: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		link: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		autofocus: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		wait: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+	mounted() {
+		if (this.autofocus) {
+			this.$nextTick(() => {
+				this.$el.focus();
+			});
+		}
+	},
+	methods: {
+		onMousedown(e: MouseEvent) {
+			function distance(p, q) {
+				return Math.hypot(p.x - q.x, p.y - q.y);
+			}
+
+			function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY) {
+				const origin = {x: circleCenterX, y: circleCenterY};
+				const dist1 = distance({x: 0, y: 0}, origin);
+				const dist2 = distance({x: boxW, y: 0}, origin);
+				const dist3 = distance({x: 0, y: boxH}, origin);
+				const dist4 = distance({x: boxW, y: boxH }, origin);
+				return Math.max(dist1, dist2, dist3, dist4) * 2;
+			}
+
+			const rect = e.target.getBoundingClientRect();
+
+			const ripple = document.createElement('div');
+			ripple.style.top = (e.clientY - rect.top - 1).toString() + 'px';
+			ripple.style.left = (e.clientX - rect.left - 1).toString() + 'px';
+
+			this.$refs.ripples.appendChild(ripple);
+
+			const circleCenterX = e.clientX - rect.left;
+			const circleCenterY = e.clientY - rect.top;
+
+			const scale = calcCircleScale(e.target.clientWidth, e.target.clientHeight, circleCenterX, circleCenterY);
+
+			setTimeout(() => {
+				ripple.style.transform = 'scale(' + (scale / 2) + ')';
+			}, 1);
+			setTimeout(() => {
+				ripple.style.transition = 'all 1s ease';
+				ripple.style.opacity = '0';
+			}, 1000);
+			setTimeout(() => {
+				if (this.$refs.ripples) this.$refs.ripples.removeChild(ripple);
+			}, 2000);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.bghgjjyj {
+	position: relative;
+	display: block;
+	min-width: 100px;
+	padding: 8px 14px;
+	text-align: center;
+	font-weight: normal;
+	font-size: 14px;
+	line-height: 24px;
+	box-shadow: none;
+	text-decoration: none;
+	background: var(--buttonBg);
+	border-radius: 6px;
+	overflow: hidden;
+
+	&:not(:disabled):hover {
+		background: var(--buttonHoverBg);
+	}
+
+	&:not(:disabled):active {
+		background: var(--buttonHoverBg);
+	}
+
+	&.primary {
+		color: #fff;
+		background: var(--accent);
+
+		&:not(:disabled):hover {
+			background: var(--jkhztclx);
+		}
+
+		&:not(:disabled):active {
+			background: var(--jkhztclx);
+		}
+	}
+
+	&:disabled {
+		opacity: 0.7;
+	}
+
+	&:focus {
+		&:after {
+			content: "";
+			pointer-events: none;
+			position: absolute;
+			top: -5px;
+			right: -5px;
+			bottom: -5px;
+			left: -5px;
+			border: 2px solid var(--accentAlpha03);
+			border-radius: 10px;
+		}
+	}
+
+	&.inline + .bghgjjyj {
+		margin-left: 12px;
+	}
+
+	&:not(.inline) + .bghgjjyj {
+		margin-top: 16px;
+	}
+
+	&.inline {
+		display: inline-block;
+		width: auto;
+		min-width: 100px;
+	}
+
+	&.primary {
+		font-weight: bold;
+	}
+
+	> .ripples {
+		position: absolute;
+		z-index: 0;
+		top: 0;
+		left: 0;
+		width: 100%;
+		height: 100%;
+		border-radius: 6px;
+		overflow: hidden;
+
+		::v-deep div {
+			position: absolute;
+			width: 2px;
+			height: 2px;
+			border-radius: 100%;
+			background: rgba(0, 0, 0, 0.1);
+			opacity: 1;
+			transform: scale(1);
+			transition: all 0.5s cubic-bezier(0,.5,0,1);
+		}
+	}
+
+	&.primary > .ripples ::v-deep div {
+		background: rgba(0, 0, 0, 0.15);
+	}
+
+	> .content {
+		position: relative;
+		z-index: 1;
+	}
+}
+</style>
diff --git a/src/client/components/ui/container.vue b/src/client/components/ui/container.vue
new file mode 100644
index 0000000000000000000000000000000000000000..19820a307d47c581bf6872783b136f4fec697a27
--- /dev/null
+++ b/src/client/components/ui/container.vue
@@ -0,0 +1,104 @@
+<template>
+<div class="ukygtjoj _panel" :class="{ naked, hideHeader: !showHeader }">
+	<header v-if="showHeader">
+		<div class="title"><slot name="header"></slot></div>
+		<slot name="func"></slot>
+		<button class="_button" v-if="bodyTogglable" @click="() => showBody = !showBody">
+			<template v-if="showBody"><fa :icon="faAngleUp"/></template>
+			<template v-else><fa :icon="faAngleDown"/></template>
+		</button>
+	</header>
+	<div v-show="showBody">
+		<slot></slot>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons';
+
+export default Vue.extend({
+	props: {
+		showHeader: {
+			type: Boolean,
+			required: false,
+			default: true
+		},
+		naked: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		bodyTogglable: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		expanded: {
+			type: Boolean,
+			required: false,
+			default: true
+		},
+	},
+	data() {
+		return {
+			showBody: this.expanded,
+			faAngleUp, faAngleDown
+		};
+	},
+	methods: {
+		toggleContent(show: boolean) {
+			if (!this.bodyTogglable) return;
+			this.showBody = show;
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ukygtjoj {
+	position: relative;
+	overflow: hidden;
+
+	& + .ukygtjoj {
+		margin-top: var(--margin);
+	}
+
+	&.naked {
+		background: transparent !important;
+		box-shadow: none !important;
+	}
+
+	> header {
+		position: relative;
+
+		> .title {
+			margin: 0;
+			padding: 12px 16px;
+
+			@media (max-width: 500px) {
+				padding: 8px 10px;
+			}
+
+			> [data-icon] {
+				margin-right: 6px;
+			}
+
+			&:empty {
+				display: none;
+			}
+		}
+
+		> button {
+			position: absolute;
+			z-index: 2;
+			top: 0;
+			right: 0;
+			padding: 0;
+			width: 42px;
+			height: 100%;
+		}
+	}
+}
+</style>
diff --git a/src/client/app/common/views/components/ui/hr.vue b/src/client/components/ui/hr.vue
similarity index 88%
rename from src/client/app/common/views/components/ui/hr.vue
rename to src/client/components/ui/hr.vue
index 38572cfcc329b1351770ee4d7e51589595dba3df..ae7f7dbf8e17fa1c8809f9bde796f6e107cfbbce 100644
--- a/src/client/app/common/views/components/ui/hr.vue
+++ b/src/client/components/ui/hr.vue
@@ -7,7 +7,7 @@ import Vue from 'vue';
 export default Vue.extend({});
 </script>
 
-<style lang="stylus" scoped>
+<style lang="scss" scoped>
 .evrzpitu
 	margin 16px 0
 	border-bottom solid var(--lineWidth) var(--faceDivider)
diff --git a/src/client/components/ui/info.vue b/src/client/components/ui/info.vue
new file mode 100644
index 0000000000000000000000000000000000000000..3e87fe261d8c3db3066006613b90d0abc6a46e66
--- /dev/null
+++ b/src/client/components/ui/info.vue
@@ -0,0 +1,55 @@
+<template>
+<div class="fpezltsf" :class="{ warn }">
+	<i v-if="warn"><fa :icon="faExclamationTriangle"/></i>
+	<i v-else><fa :icon="faInfoCircle"/></i>
+	<slot></slot>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faInfoCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
+
+export default Vue.extend({
+	props: {
+		warn: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+	data() {
+		return {
+			faInfoCircle, faExclamationTriangle
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.fpezltsf {
+	margin: 16px 0;
+	padding: 16px;
+	font-size: 90%;
+	background: var(--infoBg);
+	color: var(--infoFg);
+	border-radius: 5px;
+
+	&.warn {
+		background: var(--infoWarnBg);
+		color: var(--infoWarnFg);
+	}
+
+	&:first-child {
+		margin-top: 0;
+	}
+
+	&:last-child {
+		margin-bottom: 0;
+	}
+
+	> i {
+		margin-right: 4px;
+	}
+}
+</style>
diff --git a/src/client/components/ui/input.vue b/src/client/components/ui/input.vue
new file mode 100644
index 0000000000000000000000000000000000000000..69d842ef0f338149d53d24c3f5fe62ff4bd9cdc7
--- /dev/null
+++ b/src/client/components/ui/input.vue
@@ -0,0 +1,443 @@
+<template>
+<div class="juejbjww" :class="{ focused, filled, inline, disabled }">
+	<div class="icon" ref="icon"><slot name="icon"></slot></div>
+	<div class="input">
+		<span class="label" ref="label"><slot></slot></span>
+		<span class="title" ref="title">
+			<slot name="title"></slot>
+			<span class="warning" v-if="invalid"><fa :icon="faExclamationCircle"/>{{ $refs.input.validationMessage }}</span>
+		</span>
+		<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
+		<template v-if="type != 'file'">
+			<input v-if="debounce" ref="input"
+				v-debounce="500"
+				:type="type"
+				v-model.lazy="v"
+				:disabled="disabled"
+				:required="required"
+				:readonly="readonly"
+				:placeholder="placeholder"
+				:pattern="pattern"
+				:autocomplete="autocomplete"
+				:spellcheck="spellcheck"
+				@focus="focused = true"
+				@blur="focused = false"
+				@keydown="$emit('keydown', $event)"
+				@input="onInput"
+				:list="id"
+			>
+			<input v-else ref="input"
+				:type="type"
+				v-model="v"
+				:disabled="disabled"
+				:required="required"
+				:readonly="readonly"
+				:placeholder="placeholder"
+				:pattern="pattern"
+				:autocomplete="autocomplete"
+				:spellcheck="spellcheck"
+				@focus="focused = true"
+				@blur="focused = false"
+				@keydown="$emit('keydown', $event)"
+				@input="onInput"
+				:list="id"
+			>
+			<datalist :id="id" v-if="datalist">
+				<option v-for="data in datalist" :value="data"/>
+			</datalist>
+		</template>
+		<template v-else>
+			<input ref="input"
+				type="text"
+				:value="filePlaceholder"
+				readonly
+				@click="chooseFile"
+			>
+			<input ref="file"
+				type="file"
+				:value="value"
+				@change="onChangeFile"
+			>
+		</template>
+		<div class="suffix" ref="suffix"><slot name="suffix"></slot></div>
+	</div>
+	<button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $t('save') }}</button>
+	<div class="desc"><slot name="desc"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import debounce from 'v-debounce';
+import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
+
+export default Vue.extend({
+	directives: {
+		debounce
+	},
+	props: {
+		value: {
+			required: false
+		},
+		type: {
+			type: String,
+			required: false
+		},
+		required: {
+			type: Boolean,
+			required: false
+		},
+		readonly: {
+			type: Boolean,
+			required: false
+		},
+		disabled: {
+			type: Boolean,
+			required: false
+		},
+		pattern: {
+			type: String,
+			required: false
+		},
+		placeholder: {
+			type: String,
+			required: false
+		},
+		autofocus: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		autocomplete: {
+			required: false
+		},
+		spellcheck: {
+			required: false
+		},
+		debounce: {
+			required: false
+		},
+		datalist: {
+			type: Array,
+			required: false,
+		},
+		inline: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		save: {
+			type: Function,
+			required: false,
+		},
+	},
+	data() {
+		return {
+			v: this.value,
+			focused: false,
+			invalid: false,
+			changed: false,
+			id: Math.random().toString(),
+			faExclamationCircle
+		};
+	},
+	computed: {
+		filled(): boolean {
+			return this.v !== '' && this.v != null;
+		},
+		filePlaceholder(): string | null {
+			if (this.type != 'file') return null;
+			if (this.v == null) return null;
+
+			if (typeof this.v == 'string') return this.v;
+
+			if (Array.isArray(this.v)) {
+				return this.v.map(file => file.name).join(', ');
+			} else {
+				return this.v.name;
+			}
+		}
+	},
+	watch: {
+		value(v) {
+			this.v = v;
+		},
+		v(v) {
+			if (this.type === 'number') {
+				this.$emit('input', parseInt(v, 10));
+			} else {
+				this.$emit('input', v);
+			}
+
+			this.invalid = this.$refs.input.validity.badInput;
+		}
+	},
+	mounted() {
+		if (this.autofocus) {
+			this.$nextTick(() => {
+				this.$refs.input.focus();
+			});
+		}
+
+		this.$nextTick(() => {
+			// このコンポーネントが作成された時、非表示状態である場合がある
+			// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
+			const clock = setInterval(() => {
+				if (this.$refs.prefix) {
+					this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
+					if (this.$refs.prefix.offsetWidth) {
+						this.$refs.input.style.paddingLeft = this.$refs.prefix.offsetWidth + 'px';
+					}
+				}
+				if (this.$refs.suffix) {
+					if (this.$refs.suffix.offsetWidth) {
+						this.$refs.input.style.paddingRight = this.$refs.suffix.offsetWidth + 'px';
+					}
+				}
+			}, 100);
+
+			this.$once('hook:beforeDestroy', () => {
+				clearInterval(clock);
+			});
+		});
+
+		this.$on('keydown', (e: KeyboardEvent) => {
+			if (e.code == 'Enter') {
+				this.$emit('enter');
+			}
+		});
+	},
+	methods: {
+		focus() {
+			this.$refs.input.focus();
+		},
+		togglePassword() {
+			if (this.type == 'password') {
+				this.type = 'text'
+			} else {
+				this.type = 'password'
+			}
+		},
+		chooseFile() {
+			this.$refs.file.click();
+		},
+		onChangeFile() {
+			this.v = Array.from((this.$refs.file as any).files);
+			this.$emit('input', this.v);
+			this.$emit('change', this.v);
+		},
+		onInput(ev) {
+			this.changed = true;
+			this.$emit('change', ev);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.juejbjww {
+	position: relative;
+	margin: 32px 0;
+
+	> .icon {
+		position: absolute;
+		top: 0;
+		left: 0;
+		width: 24px;
+		text-align: center;
+		line-height: 32px;
+
+		&:not(:empty) + .input {
+			margin-left: 28px;
+		}
+	}
+
+	> .input {
+		position: relative;
+		
+		&:before {
+			content: '';
+			display: block;
+			position: absolute;
+			bottom: 0;
+			left: 0;
+			right: 0;
+			height: 1px;
+			background: var(--inputBorder);
+		}
+
+		&:after {
+			content: '';
+			display: block;
+			position: absolute;
+			bottom: 0;
+			left: 0;
+			right: 0;
+			height: 2px;
+			background: var(--accent);
+			opacity: 0;
+			transform: scaleX(0.12);
+			transition: border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+			will-change: border opacity transform;
+		}
+
+		> .label {
+			position: absolute;
+			z-index: 1;
+			top: 0;
+			left: 0;
+			pointer-events: none;
+			transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
+			transition-duration: 0.3s;
+			font-size: 16px;
+			line-height: 32px;
+			color: var(--inputLabel);
+			pointer-events: none;
+			//will-change transform
+			transform-origin: top left;
+			transform: scale(1);
+		}
+
+		> .title {
+			position: absolute;
+			z-index: 1;
+			top: -17px;
+			left: 0 !important;
+			pointer-events: none;
+			font-size: 16px;
+			line-height: 32px;
+			color: var(--inputLabel);
+			pointer-events: none;
+			//will-change transform
+			transform-origin: top left;
+			transform: scale(.75);
+			white-space: nowrap;
+			width: 133%;
+			overflow: hidden;
+			text-overflow: ellipsis;
+
+			> .warning {
+				margin-left: 0.5em;
+				color: var(--infoWarnFg);
+
+				> svg {
+					margin-right: 0.1em;
+				}
+			}
+		}
+
+		> input {
+			display: block;
+			width: 100%;
+			margin: 0;
+			padding: 0;
+			font: inherit;
+			font-weight: normal;
+			font-size: 16px;
+			line-height: 32px;
+			color: var(--inputText);
+			background: transparent;
+			border: none;
+			border-radius: 0;
+			outline: none;
+			box-shadow: none;
+			box-sizing: border-box;
+
+			&[type='file'] {
+				display: none;
+			}
+		}
+
+		> .prefix,
+		> .suffix {
+			display: block;
+			position: absolute;
+			z-index: 1;
+			top: 0;
+			font-size: 16px;
+			line-height: 32px;
+			color: var(--inputLabel);
+			pointer-events: none;
+
+			&:empty {
+				display: none;
+			}
+
+			> * {
+				display: inline-block;
+				min-width: 16px;
+				max-width: 150px;
+				overflow: hidden;
+				white-space: nowrap;
+				text-overflow: ellipsis;
+			}
+		}
+
+		> .prefix {
+			left: 0;
+			padding-right: 4px;
+		}
+
+		> .suffix {
+			right: 0;
+			padding-left: 4px;
+		}
+	}
+
+	> .save {
+		margin: 6px 0 0 0;
+		font-size: 13px;
+	}
+
+	> .desc {
+		margin: 6px 0 0 0;
+		font-size: 13px;
+		opacity: 0.7;
+
+		&:empty {
+			display: none;
+		}
+
+		* {
+			margin: 0;
+		}
+	}
+
+	&.focused {
+		> .input {
+			&:after {
+				opacity: 1;
+				transform: scaleX(1);
+			}
+
+			> .label {
+				color: var(--accent);
+			}
+		}
+	}
+
+	&.focused,
+	&.filled {
+		> .input {
+			> .label {
+				top: -17px;
+				left: 0 !important;
+				transform: scale(0.75);
+			}
+		}
+	}
+
+	&.inline {
+		display: inline-block;
+		margin: 0;
+	}
+
+	&.disabled {
+		opacity: 0.7;
+
+		&, * {
+			cursor: not-allowed !important;
+		}
+	}
+}
+</style>
diff --git a/src/client/components/ui/pagination.vue b/src/client/components/ui/pagination.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d953824e00af2f23defbd097346a6da255aff2da
--- /dev/null
+++ b/src/client/components/ui/pagination.vue
@@ -0,0 +1,59 @@
+<template>
+<sequential-entrance class="cxiknjgy" :class="{ autoMargin }">
+	<slot :items="items"></slot>
+	<div class="empty" v-if="empty" key="_empty_">
+		<slot name="empty"></slot>
+	</div>
+	<div class="more" v-if="more" key="_more_">
+		<mk-button :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore()">
+			<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
+			<template v-if="moreFetching"><fa :icon="faSpinner" pulse fixed-width/></template>
+		</mk-button>
+	</div>
+</sequential-entrance>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faSpinner } from '@fortawesome/free-solid-svg-icons';
+import MkButton from './button.vue';
+import paging from '../../scripts/paging';
+
+export default Vue.extend({
+	mixins: [
+		paging({}),
+	],
+
+	components: {
+		MkButton
+	},
+
+	props: {
+		pagination: {
+			required: true
+		},
+		autoMargin: {
+			required: false,
+			default: true
+		}
+	},
+
+	data() {
+		return {
+			faSpinner
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.cxiknjgy {
+	&.autoMargin > *:not(:last-child) {
+		margin-bottom: 16px;
+
+		@media (max-width: 500px) {
+			margin-bottom: 8px;
+		}
+	}
+}
+</style>
diff --git a/src/client/components/ui/radio.vue b/src/client/components/ui/radio.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7659d147e60a428302c72ec9312baa96986d1ca0
--- /dev/null
+++ b/src/client/components/ui/radio.vue
@@ -0,0 +1,119 @@
+<template>
+<div
+	class="novjtctn"
+	:class="{ disabled, checked }"
+	:aria-checked="checked"
+	:aria-disabled="disabled"
+	@click="toggle"
+>
+	<input type="radio"
+		:disabled="disabled"
+	>
+	<span class="button">
+		<span></span>
+	</span>
+	<span class="label"><slot></slot></span>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	model: {
+		prop: 'model',
+		event: 'change'
+	},
+	props: {
+		model: {
+			required: false
+		},
+		value: {
+			required: false
+		},
+		disabled: {
+			type: Boolean,
+			default: false
+		}
+	},
+	computed: {
+		checked(): boolean {
+			return this.model === this.value;
+		}
+	},
+	methods: {
+		toggle() {
+			this.$emit('change', this.value);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.novjtctn {
+	display: inline-block;
+	margin: 0 32px 0 0;
+	cursor: pointer;
+	transition: all 0.3s;
+
+	> * {
+		user-select: none;
+	}
+
+	&.disabled {
+		opacity: 0.6;
+		cursor: not-allowed;
+	}
+
+	&.checked {
+		> .button {
+			border-color: var(--radioActive);
+
+			&:after {
+				background-color: var(--radioActive);
+				transform: scale(1);
+				opacity: 1;
+			}
+		}
+	}
+
+	> input {
+		position: absolute;
+		width: 0;
+		height: 0;
+		opacity: 0;
+		margin: 0;
+	}
+
+	> .button {
+		position: absolute;
+		width: 20px;
+		height: 20px;
+		background: none;
+		border: solid 2px var(--inputLabel);
+		border-radius: 100%;
+		transition: inherit;
+
+		&:after {
+			content: '';
+			display: block;
+			position: absolute;
+			top: 3px;
+			right: 3px;
+			bottom: 3px;
+			left: 3px;
+			border-radius: 100%;
+			opacity: 0;
+			transform: scale(0);
+			transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
+		}
+	}
+
+	> .label {
+		margin-left: 28px;
+		display: block;
+		font-size: 16px;
+		line-height: 20px;
+		cursor: pointer;
+	}
+}
+</style>
diff --git a/src/client/components/ui/select.vue b/src/client/components/ui/select.vue
new file mode 100644
index 0000000000000000000000000000000000000000..8bad7c5d6552462a6e599884f8695fb825b669f1
--- /dev/null
+++ b/src/client/components/ui/select.vue
@@ -0,0 +1,220 @@
+<template>
+<div class="eiipwacr" :class="{ focused, disabled, filled, inline }">
+	<div class="icon" ref="icon"><slot name="icon"></slot></div>
+	<div class="input" @click="focus">
+		<span class="label" ref="label"><slot name="label"></slot></span>
+		<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
+		<select ref="input"
+			v-model="v"
+			:required="required"
+			:disabled="disabled"
+			@focus="focused = true"
+			@blur="focused = false"
+		>
+			<slot></slot>
+		</select>
+		<div class="suffix"><slot name="suffix"></slot></div>
+	</div>
+	<div class="text"><slot name="text"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: {
+		value: {
+			required: false
+		},
+		required: {
+			type: Boolean,
+			required: false
+		},
+		disabled: {
+			type: Boolean,
+			required: false
+		},
+		inline: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+	data() {
+		return {
+			focused: false
+		};
+	},
+	computed: {
+		v: {
+			get() {
+				return this.value;
+			},
+			set(v) {
+				this.$emit('input', v);
+			}
+		},
+		filled(): boolean {
+			return this.v != '' && this.v != null;
+		}
+	},
+	mounted() {
+		if (this.$refs.prefix) {
+			this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
+		}
+	},
+	methods: {
+		focus() {
+			this.$refs.input.focus();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.eiipwacr {
+	position: relative;
+	margin: 32px 0;
+
+	> .icon {
+		position: absolute;
+		top: 0;
+		left: 0;
+		width: 24px;
+		text-align: center;
+		line-height: 32px;
+
+		&:not(:empty) + .input {
+			margin-left: 28px;
+		}
+	}
+
+	> .input {
+		display: flex;
+
+		&:before {
+			content: '';
+			display: block;
+			position: absolute;
+			bottom: 0;
+			left: 0;
+			right: 0;
+			height: 1px;
+			background: var(--inputBorder);
+		}
+
+		&:after {
+			content: '';
+			display: block;
+			position: absolute;
+			bottom: 0;
+			left: 0;
+			right: 0;
+			height: 2px;
+			background: var(--accent);
+			opacity: 0;
+			transform: scaleX(0.12);
+			transition: border 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+			will-change: border opacity transform;
+		}
+
+		> .label {
+			position: absolute;
+			top: 0;
+			left: 0;
+			pointer-events: none;
+			transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
+			transition-duration: 0.3s;
+			font-size: 16px;
+			line-height: 32px;
+			pointer-events: none;
+			//will-change transform
+			transform-origin: top left;
+			transform: scale(1);
+		}
+
+		> select {
+			display: block;
+			flex: 1;
+			width: 100%;
+			padding: 0;
+			font: inherit;
+			font-weight: normal;
+			font-size: 16px;
+			height: 32px;
+			background: var(--panel);
+			border: none;
+			border-radius: 0;
+			outline: none;
+			box-shadow: none;
+			color: var(--fg);
+		}
+
+		> .prefix,
+		> .suffix {
+			display: block;
+			align-self: center;
+			justify-self: center;
+			font-size: 16px;
+			line-height: 32px;
+			color: rgba(#000, 0.54);
+			pointer-events: none;
+
+			&:empty {
+				display: none;
+			}
+
+			> * {
+				display: block;
+				min-width: 16px;
+			}
+		}
+
+		> .prefix {
+			padding-right: 4px;
+		}
+
+		> .suffix {
+			padding-left: 4px;
+		}
+	}
+
+	> .text {
+		margin: 6px 0;
+		font-size: 13px;
+
+		&:empty {
+			display: none;
+		}
+
+		* {
+			margin: 0;
+		}
+	}
+
+	&.focused {
+		> .input {
+			&:after {
+				opacity: 1;
+				transform: scaleX(1);
+			}
+
+			> .label {
+				color: var(--accent);
+			}
+		}
+	}
+
+	&.focused,
+	&.filled {
+		> .input {
+			> .label {
+				top: -17px;
+				left: 0 !important;
+				transform: scale(0.75);
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/components/ui/switch.vue b/src/client/components/ui/switch.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d4680ca2efadd85c02f7aa7af1c0ef3cfaaf0b43
--- /dev/null
+++ b/src/client/components/ui/switch.vue
@@ -0,0 +1,150 @@
+<template>
+<div
+	class="ziffeoms"
+	:class="{ disabled, checked }"
+	role="switch"
+	:aria-checked="checked"
+	:aria-disabled="disabled"
+	@click="toggle"
+>
+	<input
+		type="checkbox"
+		ref="input"
+		:disabled="disabled"
+		@keydown.enter="toggle"
+	>
+	<span class="button">
+		<span></span>
+	</span>
+	<span class="label">
+		<span :aria-hidden="!checked"><slot></slot></span>
+		<p :aria-hidden="!checked">
+			<slot name="desc"></slot>
+		</p>
+	</span>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	model: {
+		prop: 'value',
+		event: 'change'
+	},
+	props: {
+		value: {
+			type: Boolean,
+			default: false
+		},
+		disabled: {
+			type: Boolean,
+			default: false
+		}
+	},
+	computed: {
+		checked(): boolean {
+			return this.value;
+		}
+	},
+	methods: {
+		toggle() {
+			if (this.disabled) return;
+			this.$emit('change', !this.checked);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ziffeoms {
+	position: relative;
+	display: flex;
+	margin: 32px 0;
+	cursor: pointer;
+	transition: all 0.3s;
+
+	&:first-child {
+		margin-top: 0;
+	}
+
+	&:last-child {
+		margin-bottom: 0;
+	}
+
+	> * {
+		user-select: none;
+	}
+
+	&.disabled {
+		opacity: 0.6;
+		cursor: not-allowed;
+	}
+
+	&.checked {
+		> .button {
+			background-color: var(--xxubwiul);
+			border-color: var(--xxubwiul);
+
+			> * {
+				background-color: var(--accent);
+				transform: translateX(14px);
+			}
+		}
+	}
+
+	> input {
+		position: absolute;
+		width: 0;
+		height: 0;
+		opacity: 0;
+		margin: 0;
+	}
+
+	> .button {
+		position: relative;
+		display: inline-block;
+		flex-shrink: 0;
+		margin: 3px 0 0 0;
+		width: 34px;
+		height: 14px;
+		background: var(--nhzhphzx);
+		outline: none;
+		border-radius: 14px;
+		transition: inherit;
+
+		> * {
+			position: absolute;
+			top: -3px;
+			left: 0;
+			border-radius: 100%;
+			transition: background-color 0.3s, transform 0.3s;
+			width: 20px;
+			height: 20px;
+			background-color: #fff;
+			box-shadow: 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12);
+		}
+	}
+
+	> .label {
+		margin-left: 8px;
+		display: block;
+		font-size: 16px;
+		cursor: pointer;
+		transition: inherit;
+		color: var(--fg);
+
+		> span {
+			display: block;
+			line-height: 20px;
+			transition: inherit;
+		}
+
+		> p {
+			margin: 0;
+			opacity: 0.7;
+			font-size: 90%;
+		}
+	}
+}
+</style>
diff --git a/src/client/components/ui/textarea.vue b/src/client/components/ui/textarea.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7b42b78a7396608befc926844b62b1bf1241484e
--- /dev/null
+++ b/src/client/components/ui/textarea.vue
@@ -0,0 +1,218 @@
+<template>
+<div class="adhpbeos" :class="{ focused, filled, tall, pre }">
+	<div class="input">
+		<span class="label" ref="label"><slot></slot></span>
+		<textarea ref="input"
+			:value="value"
+			:required="required"
+			:readonly="readonly"
+			:pattern="pattern"
+			:autocomplete="autocomplete"
+			@input="onInput"
+			@focus="focused = true"
+			@blur="focused = false"
+		></textarea>
+	</div>
+	<button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $t('save') }}</button>
+	<div class="desc"><slot name="desc"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: {
+		value: {
+			required: false
+		},
+		required: {
+			type: Boolean,
+			required: false
+		},
+		readonly: {
+			type: Boolean,
+			required: false
+		},
+		pattern: {
+			type: String,
+			required: false
+		},
+		autocomplete: {
+			type: String,
+			required: false
+		},
+		tall: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		pre: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		save: {
+			type: Function,
+			required: false,
+		},
+	},
+	data() {
+		return {
+			focused: false,
+			changed: false,
+		}
+	},
+	computed: {
+		filled(): boolean {
+			return this.value != '' && this.value != null;
+		}
+	},
+	methods: {
+		focus() {
+			this.$refs.input.focus();
+		},
+		onInput(ev) {
+			this.changed = true;
+			this.$emit('input', ev.target.value);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.adhpbeos {
+	margin: 42px 0 32px 0;
+	position: relative;
+
+	&:last-child {
+		margin-bottom: 0;
+	}
+
+	> .input {
+		position: relative;
+	
+		&:before {
+			content: '';
+			display: block;
+			position: absolute;
+			top: 0;
+			bottom: 0;
+			left: 0;
+			right: 0;
+			background: none;
+			border: solid 1px var(--inputBorder);
+			border-radius: 3px;
+			pointer-events: none;
+		}
+
+		&:after {
+			content: '';
+			display: block;
+			position: absolute;
+			top: 0;
+			bottom: 0;
+			left: 0;
+			right: 0;
+			background: none;
+			border: solid 2px var(--accent);
+			border-radius: 3px;
+			opacity: 0;
+			transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+			pointer-events: none;
+		}
+
+		> .label {
+			position: absolute;
+			top: 6px;
+			left: 12px;
+			pointer-events: none;
+			transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
+			transition-duration: 0.3s;
+			font-size: 16px;
+			line-height: 32px;
+			pointer-events: none;
+			//will-change transform
+			transform-origin: top left;
+			transform: scale(1);
+		}
+
+		> textarea {
+			display: block;
+			width: 100%;
+			min-width: 100%;
+			max-width: 100%;
+			min-height: 130px;
+			padding: 12px;
+			box-sizing: border-box;
+			font: inherit;
+			font-weight: normal;
+			font-size: 16px;
+			background: transparent;
+			border: none;
+			border-radius: 0;
+			outline: none;
+			box-shadow: none;
+			color: var(--fg);
+		}
+	}
+
+	> .save {
+		margin: 6px 0 0 0;
+		font-size: 13px;
+	}
+
+	> .desc {
+		margin: 6px 0 0 0;
+		font-size: 13px;
+		opacity: 0.7;
+
+		&:empty {
+			display: none;
+		}
+
+		* {
+			margin: 0;
+		}
+	}
+
+	&.focused {
+		> .input {
+			&:after {
+				opacity: 1;
+			}
+
+			> .label {
+				color: var(--accent);
+			}
+		}
+	}
+
+	&.focused,
+	&.filled {
+		> .input {
+			> .label {
+				top: -24px;
+				left: 0 !important;
+				transform: scale(0.75);
+			}
+		}
+	}
+
+	&.tall {
+		> .input {
+			> textarea {
+				min-height: 200px;
+			}
+		}
+	}
+
+	&.pre {
+		> .input {
+			> textarea {
+				white-space: pre;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/components/uploader.vue b/src/client/components/uploader.vue
new file mode 100644
index 0000000000000000000000000000000000000000..14a4f845c18acae42bf76e2067b455384250f6f2
--- /dev/null
+++ b/src/client/components/uploader.vue
@@ -0,0 +1,242 @@
+<template>
+<div class="mk-uploader">
+	<ol v-if="uploads.length > 0">
+		<li v-for="ctx in uploads" :key="ctx.id">
+			<div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div>
+			<div class="top">
+				<p class="name"><fa icon="spinner" pulse/>{{ ctx.name }}</p>
+				<p class="status">
+					<span class="initing" v-if="ctx.progress == undefined">{{ $t('waiting') }}<mk-ellipsis/></span>
+					<span class="kb" v-if="ctx.progress != undefined">{{ String(Math.floor(ctx.progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span>
+					<span class="percentage" v-if="ctx.progress != undefined">{{ Math.floor((ctx.progress.value / ctx.progress.max) * 100) }}</span>
+				</p>
+			</div>
+			<progress v-if="ctx.progress != undefined && ctx.progress.value != ctx.progress.max" :value="ctx.progress.value" :max="ctx.progress.max"></progress>
+			<div class="progress initing" v-if="ctx.progress == undefined"></div>
+			<div class="progress waiting" v-if="ctx.progress != undefined && ctx.progress.value == ctx.progress.max"></div>
+		</li>
+	</ol>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import { apiUrl } from '../config';
+//import getMD5 from '../../scripts/get-md5';
+
+export default Vue.extend({
+	i18n,
+	data() {
+		return {
+			uploads: []
+		};
+	},
+	methods: {
+		checkExistence(fileData: ArrayBuffer): Promise<any> {
+			return new Promise((resolve, reject) => {
+				const data = new FormData();
+				data.append('md5', getMD5(fileData));
+
+				this.$root.api('drive/files/find-by-hash', {
+					md5: getMD5(fileData)
+				}).then(resp => {
+					resolve(resp.length > 0 ? resp[0] : null);
+				});
+			});
+		},
+
+		upload(file: File, folder: any, name?: string) {
+			if (folder && typeof folder == 'object') folder = folder.id;
+
+			const id = Math.random();
+
+			const reader = new FileReader();
+			reader.onload = (e: any) => {
+				const ctx = {
+					id: id,
+					name: name || file.name || 'untitled',
+					progress: undefined,
+					img: window.URL.createObjectURL(file)
+				};
+
+				this.uploads.push(ctx);
+				this.$emit('change', this.uploads);
+
+				const data = new FormData();
+				data.append('i', this.$store.state.i.token);
+				data.append('force', 'true');
+				data.append('file', file);
+
+				if (folder) data.append('folderId', folder);
+				if (name) data.append('name', name);
+
+				const xhr = new XMLHttpRequest();
+				xhr.open('POST', apiUrl + '/drive/files/create', true);
+				xhr.onload = (e: any) => {
+					const driveFile = JSON.parse(e.target.response);
+
+					this.$emit('uploaded', driveFile);
+
+					this.uploads = this.uploads.filter(x => x.id != id);
+					this.$emit('change', this.uploads);
+				};
+
+				xhr.upload.onprogress = e => {
+					if (e.lengthComputable) {
+						if (ctx.progress == undefined) ctx.progress = {};
+						ctx.progress.max = e.total;
+						ctx.progress.value = e.loaded;
+					}
+				};
+
+				xhr.send(data);
+			}
+			reader.readAsArrayBuffer(file);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-uploader {
+  overflow: auto;
+}
+.mk-uploader:empty {
+  display: none;
+}
+.mk-uploader > ol {
+  display: block;
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+.mk-uploader > ol > li {
+  display: grid;
+  margin: 8px 0 0 0;
+  padding: 0;
+  height: 36px;
+  width: 100%;
+  box-shadow: 0 -1px 0 var(--accentAlpha01);
+  border-top: solid 8px transparent;
+  grid-template-columns: 36px calc(100% - 44px);
+  grid-template-rows: 1fr 8px;
+  column-gap: 8px;
+  box-sizing: content-box;
+}
+.mk-uploader > ol > li:first-child {
+  margin: 0;
+  box-shadow: none;
+  border-top: none;
+}
+.mk-uploader > ol > li > .img {
+  display: block;
+  background-size: cover;
+  background-position: center center;
+  grid-column: 1/2;
+  grid-row: 1/3;
+}
+.mk-uploader > ol > li > .top {
+  display: flex;
+  grid-column: 2/3;
+  grid-row: 1/2;
+}
+.mk-uploader > ol > li > .top > .name {
+  display: block;
+  padding: 0 8px 0 0;
+  margin: 0;
+  font-size: 0.8em;
+  color: var(--accentAlpha07);
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  flex-shrink: 1;
+}
+.mk-uploader > ol > li > .top > .name > [data-icon] {
+  margin-right: 4px;
+}
+.mk-uploader > ol > li > .top > .status {
+  display: block;
+  margin: 0 0 0 auto;
+  padding: 0;
+  font-size: 0.8em;
+  flex-shrink: 0;
+}
+.mk-uploader > ol > li > .top > .status > .initing {
+  color: var(--accentAlpha05);
+}
+.mk-uploader > ol > li > .top > .status > .kb {
+  color: var(--accentAlpha05);
+}
+.mk-uploader > ol > li > .top > .status > .percentage {
+  display: inline-block;
+  width: 48px;
+  text-align: right;
+  color: var(--accentAlpha07);
+}
+.mk-uploader > ol > li > .top > .status > .percentage:after {
+  content: '%';
+}
+.mk-uploader > ol > li > progress {
+  display: block;
+  background: transparent;
+  border: none;
+  border-radius: 4px;
+  overflow: hidden;
+  grid-column: 2/3;
+  grid-row: 2/3;
+  z-index: 2;
+}
+.mk-uploader > ol > li > progress::-webkit-progress-value {
+  background: var(--accent);
+}
+.mk-uploader > ol > li > progress::-webkit-progress-bar {
+  background: var(--accentAlpha01);
+}
+.mk-uploader > ol > li > .progress {
+  display: block;
+  border: none;
+  border-radius: 4px;
+  background: linear-gradient(45deg, var(--accentLighten30) 25%, var(--accent) 25%, var(--accent) 50%, var(--accentLighten30) 50%, var(--accentLighten30) 75%, var(--accent) 75%, var(--accent));
+  background-size: 32px 32px;
+  animation: bg 1.5s linear infinite;
+  grid-column: 2/3;
+  grid-row: 2/3;
+  z-index: 1;
+}
+.mk-uploader > ol > li > .progress.initing {
+  opacity: 0.3;
+}
+@-moz-keyframes bg {
+  from {
+    background-position: 0 0;
+  }
+  to {
+    background-position: -64px 32px;
+  }
+}
+@-webkit-keyframes bg {
+  from {
+    background-position: 0 0;
+  }
+  to {
+    background-position: -64px 32px;
+  }
+}
+@-o-keyframes bg {
+  from {
+    background-position: 0 0;
+  }
+  to {
+    background-position: -64px 32px;
+  }
+}
+@keyframes bg {
+  from {
+    background-position: 0 0;
+  }
+  to {
+    background-position: -64px 32px;
+  }
+}
+</style>
diff --git a/src/client/components/url-preview.vue b/src/client/components/url-preview.vue
new file mode 100644
index 0000000000000000000000000000000000000000..f2ef1f1ba398f3b77c76464a9af9e577a471642b
--- /dev/null
+++ b/src/client/components/url-preview.vue
@@ -0,0 +1,331 @@
+<template>
+<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
+	<button class="disablePlayer" @click="playerEnabled = false" :title="$t('disable-player')"><fa icon="times"/></button>
+	<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen />
+</div>
+<div v-else-if="tweetUrl && detail" class="twitter">
+	<blockquote ref="tweet" class="twitter-tweet" :data-theme="$store.state.device.darkmode ? 'dark' : null">
+		<a :href="url"></a>
+	</blockquote>
+</div>
+<div v-else class="mk-url-preview" v-size="[{ max: 400 }, { max: 350 }]">
+	<transition name="zoom" mode="out-in">
+		<component :is="hasRoute ? 'router-link' : 'a'" :class="{ compact }" :[attr]="hasRoute ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url" v-if="!fetching">
+			<div class="thumbnail" v-if="thumbnail" :style="`background-image: url('${thumbnail}')`">
+				<button class="_button" v-if="!playerEnabled && player.url" @click.prevent="playerEnabled = true" :title="$t('enable-player')"><fa :icon="faPlayCircle"/></button>
+			</div>
+			<article>
+				<header>
+					<h1 :title="title">{{ title }}</h1>
+				</header>
+				<p v-if="description" :title="description">{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p>
+				<footer>
+					<img class="icon" v-if="icon" :src="icon"/>
+					<p :title="sitename">{{ sitename }}</p>
+				</footer>
+			</article>
+		</component>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faPlayCircle } from '@fortawesome/free-regular-svg-icons';
+import i18n from '../i18n';
+import { url as local, lang } from '../config';
+
+export default Vue.extend({
+	i18n,
+
+	props: {
+		url: {
+			type: String,
+			require: true
+		},
+
+		detail: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+
+		compact: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+
+	data() {
+		const isSelf = this.url.startsWith(local);
+		const hasRoute =
+			(this.url.substr(local.length) === '/') ||
+			this.url.substr(local.length).startsWith('/@') ||
+			this.url.substr(local.length).startsWith('/notes/') ||
+			this.url.substr(local.length).startsWith('/tags/');
+		return {
+			local,
+			fetching: true,
+			title: null,
+			description: null,
+			thumbnail: null,
+			icon: null,
+			sitename: null,
+			player: {
+				url: null,
+				width: null,
+				height: null
+			},
+			tweetUrl: null,
+			playerEnabled: false,
+			self: isSelf,
+			hasRoute: hasRoute,
+			attr: hasRoute ? 'to' : 'href',
+			target: hasRoute ? null : '_blank',
+			faPlayCircle
+		};
+	},
+
+	created() {
+		const requestUrl = new URL(this.url);
+
+		if (this.detail && requestUrl.hostname == 'twitter.com' && /^\/.+\/status(es)?\/\d+/.test(requestUrl.pathname)) {
+			this.tweetUrl = requestUrl;
+			const twttr = (window as any).twttr || {};
+			const loadTweet = () => twttr.widgets.load(this.$refs.tweet);
+
+			if (twttr.widgets) {
+				Vue.nextTick(loadTweet);
+			} else {
+				const wjsId = 'twitter-wjs';
+				if (!document.getElementById(wjsId)) {
+					const head = document.getElementsByTagName('head')[0];
+					const script = document.createElement('script');
+					script.setAttribute('id', wjsId);
+					script.setAttribute('src', 'https://platform.twitter.com/widgets.js');
+					head.appendChild(script);
+				}
+				twttr.ready = loadTweet;
+				(window as any).twttr = twttr;
+			}
+			return;
+		}
+
+		if (requestUrl.hostname === 'music.youtube.com') {
+			requestUrl.hostname = 'youtube.com';
+		}
+
+		const requestLang = (lang || 'ja-JP').replace('ja-KS', 'ja-JP');
+
+		requestUrl.hash = '';
+
+		fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => {
+			res.json().then(info => {
+				if (info.url == null) return;
+				this.title = info.title;
+				this.description = info.description;
+				this.thumbnail = info.thumbnail;
+				this.icon = info.icon;
+				this.sitename = info.sitename;
+				this.fetching = false;
+				this.player = info.player;
+			})
+		});
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.player {
+	position: relative;
+	width: 100%;
+
+	> button {
+		position: absolute;
+		top: -1.5em;
+		right: 0;
+		font-size: 1em;
+		width: 1.5em;
+		height: 1.5em;
+		padding: 0;
+		margin: 0;
+		color: var(--fg);
+		background: rgba(128, 128, 128, 0.2);
+		opacity: 0.7;
+
+		&:hover {
+			opacity: 0.9;
+		}
+	}
+
+	> iframe {
+		height: 100%;
+		left: 0;
+		position: absolute;
+		top: 0;
+		width: 100%;
+	}
+}
+
+.mk-url-preview {
+	&.max-width_400px {
+		> a {
+			font-size: 12px;
+
+			> .thumbnail {
+				height: 80px;
+			}
+
+			> article {
+				padding: 12px;
+			}
+		}
+	}
+
+	&.max-width_350px {
+		> a {
+			font-size: 10px;
+
+			> .thumbnail {
+				height: 70px;
+			}
+
+			> article {
+				padding: 8px;
+
+				> header {
+					margin-bottom: 4px;
+				}
+
+				> footer {
+					margin-top: 4px;
+
+					> img {
+						width: 12px;
+						height: 12px;
+					}
+				}
+			}
+
+			&.compact {
+				> .thumbnail {
+					position: absolute;
+					width: 56px;
+					height: 100%;
+				}
+
+				> article {
+					left: 56px;
+					width: calc(100% - 56px);
+					padding: 4px;
+
+					> header {
+						margin-bottom: 2px;
+					}
+
+					> footer {
+						margin-top: 2px;
+					}
+				}
+			}
+		}
+	}
+
+	> a {
+		position: relative;
+		display: block;
+		font-size: 14px;
+		box-shadow: 0 1px 4px var(--tyvedwbe);
+		border-radius: 4px;
+		overflow: hidden;
+
+		&:hover {
+			text-decoration: none;
+			border-color: rgba(0, 0, 0, 0.2);
+
+			> article > header > h1 {
+				text-decoration: underline;
+			}
+		}
+
+		> .thumbnail {
+			position: absolute;
+			width: 100px;
+			height: 100%;
+			background-position: center;
+			background-size: cover;
+			display: flex;
+			justify-content: center;
+			align-items: center;
+
+			> button {
+				font-size: 3.5em;
+				opacity: 0.7;
+
+				&:hover {
+					font-size: 4em;
+					opacity: 0.9;
+				}
+			}
+
+			& + article {
+				left: 100px;
+				width: calc(100% - 100px);
+			}
+		}
+
+		> article {
+			position: relative;
+			box-sizing: border-box;
+			padding: 16px;
+
+			> header {
+				margin-bottom: 8px;
+
+				> h1 {
+					margin: 0;
+					font-size: 1em;
+				}
+			}
+
+			> p {
+				margin: 0;
+				font-size: 0.8em;
+			}
+
+			> footer {
+				margin-top: 8px;
+				height: 16px;
+
+				> img {
+					display: inline-block;
+					width: 16px;
+					height: 16px;
+					margin-right: 4px;
+					vertical-align: top;
+				}
+
+				> p {
+					display: inline-block;
+					margin: 0;
+					color: var(--urlPreviewInfo);
+					font-size: 0.8em;
+					line-height: 16px;
+					vertical-align: top;
+				}
+			}
+		}
+
+		&.compact {
+			> article {
+				> header h1, p, footer {
+					overflow: hidden;
+					white-space: nowrap;
+					text-overflow: ellipsis;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/app/common/views/components/url.vue b/src/client/components/url.vue
similarity index 69%
rename from src/client/app/common/views/components/url.vue
rename to src/client/components/url.vue
index 3a304ad6e7658ab20f5be6147876243bed3b1edc..082e74400161b1bfa02a985527857260b5335514 100644
--- a/src/client/app/common/views/components/url.vue
+++ b/src/client/components/url.vue
@@ -11,14 +11,15 @@
 	<span class="pathname" v-if="pathname != ''">{{ self ? pathname.substr(1) : pathname }}</span>
 	<span class="query">{{ query }}</span>
 	<span class="hash">{{ hash }}</span>
-	<fa icon="external-link-square-alt" v-if="target === '_blank'"/>
+	<fa :icon="faExternalLinkSquareAlt" v-if="target === '_blank'" class="icon"/>
 </component>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import { faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
 import { toUnicode as decodePunycode } from 'punycode';
-import { url as local } from '../../../config';
+import { url as local } from '../config';
 
 export default Vue.extend({
 	props: ['url', 'rel'],
@@ -28,8 +29,7 @@ export default Vue.extend({
 			(this.url.substr(local.length) === '/') ||
 			this.url.substr(local.length).startsWith('/@') ||
 			this.url.substr(local.length).startsWith('/notes/') ||
-			this.url.substr(local.length).startsWith('/tags/') ||
-			this.url.substr(local.length).startsWith('/pages/'));
+			this.url.substr(local.length).startsWith('/tags/'));
 		return {
 			local,
 			schema: null,
@@ -41,7 +41,8 @@ export default Vue.extend({
 			self: isSelf,
 			hasRoute: hasRoute,
 			attr: hasRoute ? 'to' : 'href',
-			target: hasRoute ? null : '_blank'
+			target: hasRoute ? null : '_blank',
+			faExternalLinkSquareAlt
 		};
 	},
 	created() {
@@ -56,31 +57,39 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.mk-url
-	word-break break-all
+<style lang="scss" scoped>
+.mk-url {
+	word-break: break-all;
 
-	> [data-icon]
-		padding-left 2px
-		font-size .9em
-		font-weight 400
-		font-style normal
+	> .icon {
+		padding-left: 2px;
+		font-size: .9em;
+		font-weight: 400;
+		font-style: normal;
+	}
 
-	> .self
-		font-weight bold
+	> .self {
+		font-weight: bold;
+	}
 
-	> .schema
-		opacity 0.5
+	> .schema {
+		opacity: 0.5;
+	}
 
-	> .hostname
-		font-weight bold
+	> .hostname {
+		font-weight: bold;
+	}
 
-	> .pathname
-		opacity 0.8
+	> .pathname {
+		opacity: 0.8;
+	}
 
-	> .query
-		opacity 0.5
+	> .query {
+		opacity: 0.5;
+	}
 
-	> .hash
-		font-style italic
+	> .hash {
+		font-style: italic;
+	}
+}
 </style>
diff --git a/src/client/components/user-list.vue b/src/client/components/user-list.vue
new file mode 100644
index 0000000000000000000000000000000000000000..14a96f3c6f6f7f2f490d72b67e7fc9c92d86764f
--- /dev/null
+++ b/src/client/components/user-list.vue
@@ -0,0 +1,148 @@
+<template>
+<mk-container :body-togglable="true" :expanded="expanded">
+	<template #header><slot></slot></template>
+
+	<mk-error v-if="error" @retry="init()"/>
+
+	<div class="efvhhmdq">
+		<div class="no-users" v-if="empty">
+			<p>{{ $t('no-users') }}</p>
+		</div>
+		<div class="user" v-for="user in users" :key="user.id">
+			<mk-avatar class="avatar" :user="user"/>
+			<div class="body">
+				<div class="name">
+					<router-link class="name" :to="user | userPage" v-user-preview="user.id"><mk-user-name :user="user"/></router-link>
+					<span class="username"><mk-acct :user="user"/></span>
+				</div>
+				<div class="description">
+					<mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
+					<span v-else class="empty">{{ $t('noAccountDescription') }}</span>
+				</div>
+			</div>
+			<x-follow-button class="koudoku-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/>
+		</div>
+		<button class="more" :class="{ fetching: moreFetching }" v-if="more" @click="fetchMore()" :disabled="moreFetching">
+			<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>{{ moreFetching ? $t('@.loading') : $t('@.load-more') }}
+		</button>
+	</div>
+</mk-container>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import paging from '../scripts/paging';
+import MkContainer from './ui/container.vue';
+import XFollowButton from './follow-button.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		MkContainer,
+		XFollowButton,
+	},
+
+	mixins: [
+		paging({}),
+	],
+
+	props: {
+		pagination: {
+			required: true
+		},
+		extract: {
+			required: false
+		},
+		expanded: {
+			type: Boolean,
+			default: true
+		},
+	},
+
+	computed: {
+		users() {
+			return this.extract ? this.extract(this.items) : this.items;
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.efvhhmdq {
+	> .no-users {
+		text-align: center;
+	}
+
+	> .user {
+		position: relative;
+		display: flex;
+		padding: 16px;
+		border-bottom: solid 1px var(--divider);
+
+		&:last-child {
+			border-bottom: none;
+		}
+
+		> .avatar {
+			display: block;
+			flex-shrink: 0;
+			margin: 0 12px 0 0;
+			width: 42px;
+			height: 42px;
+			border-radius: 8px;
+		}
+
+		> .body {
+			flex: 1;
+
+			> .name {
+				font-weight: bold;
+						
+				> .name {
+					margin-right: 8px;
+				}
+
+				> .username {
+					opacity: 0.7;
+				}
+			}
+
+			> .description {
+				font-size: 90%;
+
+				> .empty {
+					opacity: 0.7;
+				}
+			}
+		}
+
+		> .koudoku-button {
+			flex-shrink: 0;
+		}
+	}
+
+	> .more {
+		display: block;
+		width: 100%;
+		padding: 16px;
+
+		&:hover {
+			background: rgba(#000, 0.025);
+		}
+
+		&:active {
+			background: rgba(#000, 0.05);
+		}
+
+		&.fetching {
+			cursor: wait;
+		}
+
+		> [data-icon] {
+			margin-right: 4px;
+		}
+	}
+}
+</style>
diff --git a/src/client/components/user-menu.vue b/src/client/components/user-menu.vue
new file mode 100644
index 0000000000000000000000000000000000000000..6e3280031c1d8e6f62f7608b06e89a19a72cf77b
--- /dev/null
+++ b/src/client/components/user-menu.vue
@@ -0,0 +1,188 @@
+<template>
+<x-menu :source="source" :items="items" @closed="$emit('closed')"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments } from '@fortawesome/free-solid-svg-icons';
+import { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons';
+import i18n from '../i18n';
+import XMenu from './menu.vue';
+import copyToClipboard from '../scripts/copy-to-clipboard';
+import { host } from '../config';
+import getAcct from '../../misc/acct/render';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XMenu
+	},
+
+	props: ['user', 'source'],
+
+	data() {
+		let menu = [{
+			icon: faAt,
+			text: this.$t('copyUsername'),
+			action: () => {
+				copyToClipboard(`@${this.user.username}@${this.user.host || host}`);
+			}
+		}, {
+			icon: faEnvelope,
+			text: this.$t('sendMessage'),
+			action: () => {
+				this.$root.post({ specified: this.user });
+			}
+		}, this.$store.state.i.id != this.user.id ? {
+			type: 'link',
+			to: `/my/messaging/${getAcct(this.user)}`,
+			icon: faComments,
+			text: this.$t('startMessaging'),
+		} : undefined, null, {
+			icon: faListUl,
+			text: this.$t('addToList'),
+			action: this.pushList
+		}] as any;
+
+		if (this.$store.getters.isSignedIn && this.$store.state.i.id != this.user.id) {
+			menu = menu.concat([null, {
+				icon: this.user.isMuted ? faEye : faEyeSlash,
+				text: this.user.isMuted ? this.$t('unmute') : this.$t('mute'),
+				action: this.toggleMute
+			}, {
+				icon: faBan,
+				text: this.user.isBlocking ? this.$t('unblock') : this.$t('block'),
+				action: this.toggleBlock
+			}]);
+
+			if (this.$store.state.i.isAdmin) {
+				menu = menu.concat([null, {
+					icon: faSnowflake,
+					text: this.user.isSuspended ? this.$t('unsuspend') : this.$t('suspend'),
+					action: this.toggleSuspend
+				}]);
+			}
+		}
+
+		if (this.$store.getters.isSignedIn && this.$store.state.i.id === this.user.id) {
+			menu = menu.concat([null, {
+				icon: faPencilAlt,
+				text: this.$t('editProfile'),
+				action: () => {
+					this.$router.push('/my/settings');
+				}
+			}]);
+		}
+
+		return {
+			items: menu
+		};
+	},
+
+	methods: {
+		async pushList() {
+			const t = this.$t('selectList'); // なぜか後で参照すると null になるので最初にメモリに確保しておく
+			const lists = await this.$root.api('users/lists/list');
+			if (lists.length === 0) {
+				this.$root.dialog({
+					type: 'error',
+					text: this.$t('youHaveNoLists')
+				});
+				return;
+			}
+			const { canceled, result: listId } = await this.$root.dialog({
+				type: null,
+				title: t,
+				select: {
+					items: lists.map(list => ({
+						value: list.id, text: list.name
+					}))
+				},
+				showCancelButton: true
+			});
+			if (canceled) return;
+			this.$root.api('users/lists/push', {
+				listId: listId,
+				userId: this.user.id
+			}).then(() => {
+				this.$root.dialog({
+					type: 'success',
+					iconOnly: true, autoClose: true
+				});
+			}).catch(e => {
+				this.$root.dialog({
+					type: 'error',
+					text: e
+				});
+			});
+		},
+
+		async toggleMute() {
+			this.$root.api(this.user.isMuted ? 'mute/delete' : 'mute/create', {
+				userId: this.user.id
+			}).then(() => {
+				this.user.isMuted = !this.user.isMuted;
+				this.$root.dialog({
+					type: 'success',
+					iconOnly: true, autoClose: true
+				});
+			}, e => {
+				this.$root.dialog({
+					type: 'error',
+					text: e
+				});
+			});
+		},
+
+		async toggleBlock() {
+			if (!await this.getConfirmed(this.user.isBlocking ? this.$t('unblockConfirm') : this.$t('blockConfirm'))) return;
+
+			this.$root.api(this.user.isBlocking ? 'blocking/delete' : 'blocking/create', {
+				userId: this.user.id
+			}).then(() => {
+				this.user.isBlocking = !this.user.isBlocking;
+				this.$root.dialog({
+					type: 'success',
+					iconOnly: true, autoClose: true
+				});
+			}, e => {
+				this.$root.dialog({
+					type: 'error',
+					text: e
+				});
+			});
+		},
+
+		async toggleSuspend() {
+			if (!await this.getConfirmed(this.$t(this.user.isSuspended ? 'unsuspendConfirm' : 'suspendConfirm'))) return;
+
+			this.$root.api(this.user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', {
+				userId: this.user.id
+			}).then(() => {
+				this.user.isSuspended = !this.user.isSuspended;
+				this.$root.dialog({
+					type: 'success',
+					iconOnly: true, autoClose: true
+				});
+			}, e => {
+				this.$root.dialog({
+					type: 'error',
+					text: e
+				});
+			});
+		},
+
+		async getConfirmed(text: string): Promise<Boolean> {
+			const confirm = await this.$root.dialog({
+				type: 'warning',
+				showCancelButton: true,
+				title: 'confirm',
+				text,
+			});
+
+			return !confirm.canceled;
+		},
+	}
+});
+</script>
diff --git a/src/client/components/user-moderate-dialog.vue b/src/client/components/user-moderate-dialog.vue
new file mode 100644
index 0000000000000000000000000000000000000000..894db5384efdce18ae6e763ab2c84f09ade0e3d5
--- /dev/null
+++ b/src/client/components/user-moderate-dialog.vue
@@ -0,0 +1,108 @@
+<template>
+<x-window @closed="() => { $emit('closed'); destroyDom(); }" :avatar="user">
+	<template #header><mk-user-name :user="user"/></template>
+	<div class="vrcsvlkm">
+		<mk-button @click="changePassword()">{{ $t('changePassword') }}</mk-button>
+		<mk-switch @change="toggleSilence()" v-model="silenced">{{ $t('silence') }}</mk-switch>
+		<mk-switch @change="toggleSuspend()" v-model="suspended">{{ $t('suspend') }}</mk-switch>
+	</div>
+</x-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import MkButton from './ui/button.vue';
+import MkSwitch from './ui/switch.vue';
+import XWindow from './window.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		MkButton,
+		MkSwitch,
+		XWindow,
+	},
+
+	props: {
+		user: {
+			type: Object,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			silenced: this.user.isSilenced,
+			suspended: this.user.isSuspended,
+		};
+	},
+
+	methods: {
+		async changePassword() {
+			const { canceled: canceled, result: newPassword } = await this.$root.dialog({
+				title: this.$t('newPassword'),
+				input: {
+					type: 'password'
+				}
+			});
+			if (canceled) return;
+
+			const dialog = this.$root.dialog({
+				type: 'waiting',
+				iconOnly: true
+			});
+			
+			this.$root.api('admin/change-password', {
+				userId: this.user.id,
+				newPassword
+			}).then(() => {
+				this.$root.dialog({
+					type: 'success',
+					iconOnly: true, autoClose: true
+				});
+			}).catch(e => {
+				this.$root.dialog({
+					type: 'error',
+					text: e
+				});
+			}).finally(() => {
+				dialog.close();
+			});
+		},
+
+		async toggleSilence() {
+			const confirm = await this.$root.dialog({
+				type: 'warning',
+				showCancelButton: true,
+				text: this.silenced ? this.$t('silenceConfirm') : this.$t('unsilenceConfirm'),
+			});
+			if (confirm.canceled) {
+				this.silenced = !this.silenced;
+			} else {
+				this.$root.api(this.silenced ? 'admin/silence-user' : 'admin/unsilence-user', { userId: this.user.id });
+			}
+		},
+
+		async toggleSuspend() {
+			const confirm = await this.$root.dialog({
+				type: 'warning',
+				showCancelButton: true,
+				text: this.suspended ? this.$t('suspendConfirm') : this.$t('unsuspendConfirm'),
+			});
+			if (confirm.canceled) {
+				this.suspended = !this.suspended;
+			} else {
+				this.$root.api(this.silenced ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: this.user.id });
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.vrcsvlkm {
+
+}
+</style>
diff --git a/src/client/app/common/views/components/user-name.vue b/src/client/components/user-name.vue
similarity index 100%
rename from src/client/app/common/views/components/user-name.vue
rename to src/client/components/user-name.vue
diff --git a/src/client/components/user-preview.vue b/src/client/components/user-preview.vue
new file mode 100644
index 0000000000000000000000000000000000000000..f20335d02bd36c7ca8a58084a889cae6a7da9013
--- /dev/null
+++ b/src/client/components/user-preview.vue
@@ -0,0 +1,181 @@
+<template>
+<transition name="popup" appear @after-leave="() => { $emit('closed'); destroyDom(); }">
+	<div v-if="show" class="fxxzrfni _panel" ref="content" :style="{ top: top + 'px', left: left + 'px' }" @mouseover="() => { $emit('mouseover'); }" @mouseleave="() => { $emit('mouseleave'); }">
+		<div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl})` : ''"></div>
+		<mk-avatar class="avatar" :user="u" :disable-preview="true"/>
+		<div class="title">
+			<router-link class="name" :to="u | userPage"><mk-user-name :user="u" :nowrap="false"/></router-link>
+			<p class="username"><mk-acct :user="u"/></p>
+		</div>
+		<div class="description">
+			<mfm v-if="u.description" :text="u.description" :author="u" :i="$store.state.i" :custom-emojis="u.emojis"/>
+		</div>
+		<div class="status">
+			<div>
+				<p>{{ $t('notes') }}</p><span>{{ u.notesCount }}</span>
+			</div>
+			<div>
+				<p>{{ $t('following') }}</p><span>{{ u.followingCount }}</span>
+			</div>
+			<div>
+				<p>{{ $t('followers') }}</p><span>{{ u.followersCount }}</span>
+			</div>
+		</div>
+		<x-follow-button class="koudoku-button" v-if="$store.getters.isSignedIn && u.id != $store.state.i.id" :user="u" mini/>
+	</div>
+</transition>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import parseAcct from '../../misc/acct/parse';
+import XFollowButton from './follow-button.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XFollowButton
+	},
+
+	props: {
+		user: {
+			type: [Object, String],
+			required: true
+		},
+		source: {
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			u: null,
+			show: false,
+			top: 0,
+			left: 0,
+		};
+	},
+
+	mounted() {
+		if (typeof this.user == 'object') {
+			this.u = this.user;
+			this.show = true;
+		} else {
+			const query = this.user.startsWith('@') ?
+				parseAcct(this.user.substr(1)) :
+				{ userId: this.user };
+
+			this.$root.api('users/show', query).then(user => {
+				this.u = user;
+				this.show = true;
+			});
+		}
+
+		const rect = this.source.getBoundingClientRect();
+		const x = ((rect.left + (this.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset;
+		const y = rect.top + this.source.offsetHeight + window.pageYOffset;
+
+		this.top = y;
+		this.left = x;
+	},
+
+	methods: {
+		close() {
+			this.show = false;
+			(this.$refs.content as any).style.pointerEvents = 'none';
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.popup-enter-active, .popup-leave-active {
+	transition: opacity 0.3s, transform 0.3s !important;
+}
+.popup-enter, .popup-leave-to {
+	opacity: 0;
+	transform: scale(0.9);
+}
+
+.fxxzrfni {
+	position: absolute;
+	z-index: 11000;
+	width: 300px;
+	overflow: hidden;
+
+	> .banner {
+		height: 84px;
+		background-color: rgba(0, 0, 0, 0.1);
+		background-size: cover;
+		background-position: center;
+	}
+
+	> .avatar {
+		display: block;
+		position: absolute;
+		top: 62px;
+		left: 13px;
+		z-index: 2;
+		width: 58px;
+		height: 58px;
+		border: solid 3px var(--face);
+		border-radius: 8px;
+	}
+
+	> .title {
+		display: block;
+		padding: 8px 0 8px 82px;
+
+		> .name {
+			display: inline-block;
+			margin: 0;
+			font-weight: bold;
+			line-height: 16px;
+			word-break: break-all;
+		}
+
+		> .username {
+			display: block;
+			margin: 0;
+			line-height: 16px;
+			font-size: 0.8em;
+			color: var(--text);
+			opacity: 0.7;
+		}
+	}
+
+	> .description {
+		padding: 0 16px;
+		font-size: 0.8em;
+		color: var(--text);
+	}
+
+	> .status {
+		padding: 8px 16px;
+
+		> div {
+			display: inline-block;
+			width: 33%;
+
+			> p {
+				margin: 0;
+				font-size: 0.7em;
+				color: var(--text);
+			}
+
+			> span {
+				font-size: 1em;
+				color: var(--accent);
+			}
+		}
+	}
+
+	> .koudoku-button {
+		position: absolute;
+		top: 8px;
+		right: 8px;
+	}
+}
+</style>
diff --git a/src/client/components/user-select.vue b/src/client/components/user-select.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a82626652d6207ba100782a27bbc4d1f9230eb28
--- /dev/null
+++ b/src/client/components/user-select.vue
@@ -0,0 +1,152 @@
+<template>
+<x-window ref="window" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="selected == null" @ok="ok()">
+	<template #header>{{ $t('selectUser') }}</template>
+	<div class="tbhwbxda">
+		<div class="inputs">
+			<mk-input v-model="username" class="input" @input="search" ref="username"><span>{{ $t('username') }}</span><template #prefix>@</template></mk-input>
+			<mk-input v-model="host" class="input" @input="search"><span>{{ $t('host') }}</span><template #prefix>@</template></mk-input>
+		</div>
+		<div class="users">
+			<div class="user" v-for="user in users" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()">
+				<mk-avatar :user="user" class="avatar" :disable-link="true"/>
+				<div class="body">
+					<mk-user-name :user="user" class="name"/>
+					<mk-acct :user="user" class="acct"/>
+				</div>
+			</div>
+		</div>
+	</div>
+</x-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import { faTimes, faCheck } from '@fortawesome/free-solid-svg-icons';
+import MkInput from './ui/input.vue';
+import XWindow from './window.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		MkInput,
+		XWindow,
+	},
+
+	props: {
+	},
+
+	data() {
+		return {
+			username: '',
+			host: '',
+			users: [],
+			selected: null,
+			faTimes, faCheck
+		};
+	},
+
+	mounted() {
+		this.focus();
+
+		this.$nextTick(() => {
+			this.focus();
+		});
+	},
+
+	methods: {
+		search() {
+			if (this.username == '' && this.host == '') {
+				this.users = [];
+				return;
+			}
+			this.$root.api('users/search-by-username-and-host', {
+				username: this.username,
+				host: this.host,
+				limit: 10,
+				detail: false
+			}).then(users => {
+				this.users = users;
+			});
+		},
+
+		focus() {
+			this.$refs.username.focus();
+		},
+
+		close() {
+			this.$refs.window.close();
+		},
+
+		ok() {
+			this.$emit('selected', this.selected);
+			this.close();
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.tbhwbxda {
+	display: flex;
+	flex-direction: column;
+	overflow: auto;
+	height: 100%;
+		
+	> .inputs {
+		margin-top: 16px;
+
+		> .input {
+			display: inline-block;
+			width: 50%;
+			margin: 0;
+		}
+	}
+
+	> .users {
+		flex: 1;
+		overflow: auto;
+
+		> .user {
+			display: flex;
+			align-items: center;
+			padding: 8px 16px;
+			font-size: 14px;
+
+			&:hover {
+				background: var(--bwqtlupy);
+			}
+
+			&.selected {
+				background: var(--accent);
+				color: #fff;
+			}
+
+			> * {
+				pointer-events: none;
+				user-select: none;
+			}
+
+			> .avatar {
+				width: 45px;
+				height: 45px;
+			}
+
+			> .body {
+				padding: 0 8px;
+				min-width: 0;
+
+				> .name {
+					display: block;
+					font-weight: bold;
+				}
+
+				> .acct {
+					opacity: 0.5;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/components/users-dialog.vue b/src/client/components/users-dialog.vue
new file mode 100644
index 0000000000000000000000000000000000000000..19310bc4e14f4e84bb41c459399b947725fb5b4a
--- /dev/null
+++ b/src/client/components/users-dialog.vue
@@ -0,0 +1,161 @@
+<template>
+<x-modal ref="modal" @closed="() => { $emit('closed'); destroyDom(); }">
+	<div class="mk-users-dialog">
+		<div class="header">
+			<span>{{ title }}</span>
+			<button class="_button" @click="close()"><fa :icon="faTimes"/></button>
+		</div>
+
+		<sequential-entrance class="users">
+			<router-link v-for="(item, i) in items" class="user" :key="item.id" :data-index="i" :to="extract ? extract(item) : item | userPage">
+				<mk-avatar :user="extract ? extract(item) : item" class="avatar" :disable-link="true"/>
+				<div class="body">
+					<mk-user-name :user="extract ? extract(item) : item" class="name"/>
+					<mk-acct :user="extract ? extract(item) : item" class="acct"/>
+				</div>
+			</router-link>
+		</sequential-entrance>
+
+		<button class="more _button" v-if="more" @click="fetchMore" :disabled="moreFetching">
+			<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
+			<template v-if="moreFetching"><fa :icon="faSpinner" pulse fixed-width/></template>
+		</button>
+
+		<p class="empty" v-if="empty">{{ $t('noUsers') }}</p>
+
+		<mk-error v-if="error" @retry="init()"/>
+	</div>
+</x-modal>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faTimes } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
+import paging from '../scripts/paging';
+import XModal from './modal.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XModal,
+	},
+
+	mixins: [
+		paging({}),
+	],
+
+	props: {
+		title: {
+			required: true
+		},
+		pagination: {
+			required: true
+		},
+		extract: {
+			required: false
+		}
+	},
+
+	data() {
+		return {
+			faTimes
+		};
+	},
+
+	methods: {
+		close() {
+			this.$refs.modal.close();
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-users-dialog {
+	width: 350px;
+	height: 350px;
+	background: var(--panel);
+	border-radius: var(--radius);
+	overflow: hidden;
+	display: flex;
+	flex-direction: column;
+
+	> .header {
+		display: flex;
+		flex-shrink: 0;
+
+		> button {
+			height: 58px;
+			width: 58px;
+
+			@media (max-width: 500px) {
+				height: 42px;
+				width: 42px;
+			}
+		}
+
+		> span {
+			flex: 1;
+			line-height: 58px;
+			padding-left: 32px;
+			font-weight: bold;
+
+			@media (max-width: 500px) {
+				line-height: 42px;
+				padding-left: 16px;
+			}
+		}
+	}
+
+	> .users {
+		flex: 1;
+		overflow: auto;
+
+		&:empty {
+			display: none;
+		}
+
+		> .user {
+			display: flex;
+			align-items: center;
+			font-size: 14px;
+			padding: 8px 32px;
+
+			@media (max-width: 500px) {
+				padding: 8px 16px;
+			}
+
+			> * {
+				pointer-events: none;
+				user-select: none;
+			}
+
+			> .avatar {
+				width: 45px;
+				height: 45px;
+			}
+
+			> .body {
+				padding: 0 8px;
+				overflow: hidden;
+
+				> .name {
+					display: block;
+					font-weight: bold;
+				}
+
+				> .acct {
+					opacity: 0.5;
+				}
+			}
+		}
+	}
+
+	> .empty {
+		text-align: center;
+		opacity: 0.5;
+	}
+}
+</style>
diff --git a/src/client/components/visibility-chooser.vue b/src/client/components/visibility-chooser.vue
new file mode 100644
index 0000000000000000000000000000000000000000..aa422b27dcac0fe2792ef59810515f9b72195baa
--- /dev/null
+++ b/src/client/components/visibility-chooser.vue
@@ -0,0 +1,127 @@
+<template>
+<x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }">
+	<sequential-entrance class="gqyayizv" :delay="30">
+		<button class="_button" @click="choose('public')" :class="{ active: v == 'public' }" data-index="0" key="0">
+			<div><fa :icon="faGlobe"/></div>
+			<div>
+				<span>{{ $t('_visibility.public') }}</span>
+				<span>{{ $t('_visibility.publicDescription') }}</span>
+			</div>
+		</button>
+		<button class="_button" @click="choose('home')" :class="{ active: v == 'home' }" data-index="1" key="1">
+			<div><fa :icon="faHome"/></div>
+			<div>
+				<span>{{ $t('_visibility.home') }}</span>
+				<span>{{ $t('_visibility.homeDescription') }}</span>
+			</div>
+		</button>
+		<button class="_button" @click="choose('followers')" :class="{ active: v == 'followers' }" data-index="2" key="2">
+			<div><fa :icon="faUnlock"/></div>
+			<div>
+				<span>{{ $t('_visibility.followers') }}</span>
+				<span>{{ $t('_visibility.followersDescription') }}</span>
+			</div>
+		</button>
+		<button class="_button" @click="choose('specified')" :class="{ active: v == 'specified' }" data-index="3" key="3">
+			<div><fa :icon="faEnvelope"/></div>
+			<div>
+				<span>{{ $t('_visibility.specified') }}</span>
+				<span>{{ $t('_visibility.specifiedDescription') }}</span>
+			</div>
+		</button>
+	</sequential-entrance>
+</x-popup>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faGlobe, faUnlock, faHome } from '@fortawesome/free-solid-svg-icons';
+import { faEnvelope } from '@fortawesome/free-regular-svg-icons';
+import i18n from '../i18n';
+import XPopup from './popup.vue';
+
+export default Vue.extend({
+	i18n,
+	components: {
+		XPopup
+	},
+	props: {
+		source: {
+			required: true
+		},
+		currentVisibility: {
+			type: String,
+			required: false
+		}
+	},
+	data() {
+		return {
+			v: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : (this.currentVisibility || this.$store.state.settings.defaultNoteVisibility),
+			faGlobe, faUnlock, faEnvelope, faHome
+		}
+	},
+	methods: {
+		choose(visibility) {
+			if (this.$store.state.settings.rememberNoteVisibility) {
+				this.$store.commit('device/setVisibility', visibility);
+			}
+			this.$emit('chosen', visibility);
+			this.destroyDom();
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.gqyayizv {
+	width: 240px;
+	padding: 8px 0;
+
+	> button {
+		display: flex;
+		padding: 8px 14px;
+		font-size: 12px;
+		text-align: left;
+		width: 100%;
+		box-sizing: border-box;
+
+		&:hover {
+			background: rgba(0, 0, 0, 0.05);
+		}
+
+		&:active {
+			background: rgba(0, 0, 0, 0.1);
+		}
+
+		&.active {
+			color: #fff;
+			background: var(--accent);
+		}
+
+		> *:first-child {
+			display: flex;
+			justify-content: center;
+			align-items: center;
+			margin-right: 10px;
+			width: 16px;
+			top: 0;
+			bottom: 0;
+			margin-top: auto;
+			margin-bottom: auto;
+		}
+
+		> *:last-child {
+			flex: 1 1 auto;
+
+			> span:first-child {
+				display: block;
+				font-weight: bold;
+			}
+
+			> span:last-child:not(:first-child) {
+				opacity: 0.6;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/components/window.vue b/src/client/components/window.vue
new file mode 100644
index 0000000000000000000000000000000000000000..bfdabee0595d5e55628d0af2bd1a82567ffe0522
--- /dev/null
+++ b/src/client/components/window.vue
@@ -0,0 +1,155 @@
+<template>
+<x-modal ref="modal" @closed="() => { $emit('closed'); destroyDom(); }">
+	<div class="ebkgoccj" :class="{ noPadding }" @keydown="onKeydown">
+		<div class="header">
+			<button class="_button" v-if="withOkButton" @click="close()"><fa :icon="faTimes"/></button>
+			<span class="title">
+				<mk-avatar :user="avatar" v-if="avatar" class="avatar"/>
+				<slot name="header"></slot>
+			</span>
+			<button class="_button" v-if="!withOkButton" @click="close()"><fa :icon="faTimes"/></button>
+			<button class="_button" v-if="withOkButton" @click="() => { $emit('ok'); close(); }" :disabled="okButtonDisabled"><fa :icon="faCheck"/></button>
+		</div>
+		<div class="body">
+			<slot></slot>
+		</div>
+	</div>
+</x-modal>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faTimes, faCheck } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
+import XModal from './modal.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XModal,
+	},
+
+	props: {
+		avatar: {
+			type: Object,
+			required: false
+		},
+		withOkButton: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		okButtonDisabled: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		noPadding: {
+			type: Boolean,
+			required: false,
+			default: false
+		}
+	},
+
+	data() {
+		return {
+			faTimes, faCheck
+		};
+	},
+
+	methods: {
+		close() {
+			this.$refs.modal.close();
+		},
+
+		onKeydown(e) {
+			if (e.which === 27) { // Esc
+				e.preventDefault();
+				e.stopPropagation();
+				this.close();
+			}
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ebkgoccj {
+	width: 400px;
+	height: 400px;
+	background: var(--panel);
+	border-radius: var(--radius);
+	overflow: hidden;
+	display: flex;
+	flex-direction: column;
+
+	@media (max-width: 500px) {
+		width: 350px;
+		height: 350px;
+	}
+
+	> .header {
+		$height: 58px;
+		$height-narrow: 42px;
+		display: flex;
+		flex-shrink: 0;
+
+		> button {
+			height: $height;
+			width: $height;
+
+			@media (max-width: 500px) {
+				height: $height-narrow;
+				width: $height-narrow;
+			}
+		}
+
+		> .title {
+			flex: 1;
+			line-height: $height;
+			padding-left: 32px;
+			font-weight: bold;
+			white-space: nowrap;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			pointer-events: none;
+
+			@media (max-width: 500px) {
+				line-height: $height-narrow;
+				padding-left: 16px;
+			}
+
+			> .avatar {
+				$size: 32px;
+				height: $size;
+				width: $size;
+				margin: (($height - $size) / 2) 8px (($height - $size) / 2) 0;
+
+				@media (max-width: 500px) {
+					$size: 24px;
+					height: $size;
+					width: $size;
+					margin: (($height-narrow - $size) / 2) 8px (($height-narrow - $size) / 2) 0;
+				}
+			}
+		}
+
+		> button + .title {
+			padding-left: 0;
+		}
+	}
+
+	> .body {
+		overflow: auto;
+	}
+
+	&:not(.noPadding) > .body {
+		padding: 0 32px 32px 32px;
+
+		@media (max-width: 500px) {
+			padding: 0 16px 16px 16px;
+		}
+	}
+}
+</style>
diff --git a/src/client/app/config.ts b/src/client/config.ts
similarity index 68%
rename from src/client/app/config.ts
rename to src/client/config.ts
index 55c0c6b3a53c0040c835896241249e20574f32b5..175a3f0b29202403ee5a55c0e593c82625ee631b 100644
--- a/src/client/app/config.ts
+++ b/src/client/config.ts
@@ -1,20 +1,18 @@
 declare const _LANGS_: string[];
-declare const _COPYRIGHT_: string;
 declare const _VERSION_: string;
-declare const _CODENAME_: string;
 declare const _ENV_: string;
 
 const address = new URL(location.href);
+const siteName = document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement;
 
 export const host = address.host;
 export const hostname = address.hostname;
 export const url = address.origin;
 export const apiUrl = url + '/api';
 export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming';
-export const lang = localStorage.getItem('lang') || window.lang; // windowは後方互換性のため
+export const lang = localStorage.getItem('lang');
 export const langs = _LANGS_;
 export const locale = JSON.parse(localStorage.getItem('locale'));
-export const copyright = _COPYRIGHT_;
 export const version = _VERSION_;
-export const codename = _CODENAME_;
 export const env = _ENV_;
+export const instanceName = siteName && siteName.content ? siteName.content : 'Misskey';
diff --git a/src/client/app/common/views/directives/autocomplete.ts b/src/client/directives/autocomplete.ts
similarity index 100%
rename from src/client/app/common/views/directives/autocomplete.ts
rename to src/client/directives/autocomplete.ts
diff --git a/src/client/app/desktop/views/directives/index.ts b/src/client/directives/index.ts
similarity index 51%
rename from src/client/app/desktop/views/directives/index.ts
rename to src/client/directives/index.ts
index 324e07596d90b0d6f6263488fbb6c1c17f3987cd..2e05b5202313f514d72f704aeced55af8b76fd6b 100644
--- a/src/client/app/desktop/views/directives/index.ts
+++ b/src/client/directives/index.ts
@@ -1,6 +1,10 @@
 import Vue from 'vue';
 
 import userPreview from './user-preview';
+import autocomplete from './autocomplete';
+import size from './size';
 
+Vue.directive('autocomplete', autocomplete);
 Vue.directive('userPreview', userPreview);
 Vue.directive('user-preview', userPreview);
+Vue.directive('size', size);
diff --git a/src/client/directives/size.ts b/src/client/directives/size.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c7d797e5aee1e7ea6d2ce3ae7d3a81b3d1bb2127
--- /dev/null
+++ b/src/client/directives/size.ts
@@ -0,0 +1,63 @@
+export default {
+	inserted(el, binding) {
+		const query = binding.value;
+
+		/*
+		const addClassRecursive = (el: Element, cls: string) => {
+			el.classList.add(cls);
+			if (el.children) {
+				for (const child of el.children) {
+					addClassRecursive(child, cls);
+				}
+			}
+		};
+
+		const removeClassRecursive = (el: Element, cls: string) => {
+			el.classList.remove(cls);
+			if (el.children) {
+				for (const child of el.children) {
+					removeClassRecursive(child, cls);
+				}
+			}
+		};*/
+
+		const addClass = (el: Element, cls: string) => {
+			el.classList.add(cls);
+		};
+
+		const removeClass = (el: Element, cls: string) => {
+			el.classList.remove(cls);
+		};
+
+		const calc = () => {
+			const width = el.clientWidth;
+			
+			for (const q of query) {
+				if (q.max) {
+					if (width <= q.max) {
+						addClass(el, 'max-width_' + q.max + 'px');
+					} else {
+						removeClass(el, 'max-width_' + q.max + 'px');
+					}
+				}
+				if (q.min) {
+					if (width >= q.min) {
+						addClass(el, 'min-width_' + q.min + 'px');
+					} else {
+						removeClass(el, 'min-width_' + q.min + 'px');
+					}
+				}
+			}
+		};
+
+		calc();
+
+		el._sizeResizeCb_ = calc;
+
+		window.addEventListener('resize', calc);
+	},
+
+	unbind(el, binding, vn) {
+		window.removeEventListener('resize', el._sizeResizeCb_);
+	}
+};
diff --git a/src/client/app/desktop/views/directives/user-preview.ts b/src/client/directives/user-preview.ts
similarity index 63%
rename from src/client/app/desktop/views/directives/user-preview.ts
rename to src/client/directives/user-preview.ts
index 8a4035881ad536ccaa408632a6c5507741d5e430..c3b4e7fce6b9d1d33bf2520da681bb43b31ea366 100644
--- a/src/client/app/desktop/views/directives/user-preview.ts
+++ b/src/client/directives/user-preview.ts
@@ -1,12 +1,8 @@
-/**
- * マウスオーバーするとユーザーがプレビューされる要素を設定します
- */
-
 import MkUserPreview from '../components/user-preview.vue';
 
 export default {
-	bind(el, binding, vn) {
-		const self = el._userPreviewDirective_ = {} as any;
+	bind(el: HTMLElement, binding, vn) {
+		const self = (el as any)._userPreviewDirective_ = {} as any;
 
 		self.user = binding.value;
 		self.tag = null;
@@ -26,28 +22,21 @@ export default {
 			self.tag = new MkUserPreview({
 				parent: vn.context,
 				propsData: {
-					user: self.user
+					user: self.user,
+					source: el
 				}
 			}).$mount();
 
-			const preview = self.tag.$el;
-			const rect = el.getBoundingClientRect();
-			const x = rect.left + el.offsetWidth + window.pageXOffset;
-			const y = rect.top + window.pageYOffset;
-
-			preview.style.top = y + 'px';
-			preview.style.left = x + 'px';
-
-			preview.addEventListener('mouseover', () => {
+			self.tag.$on('mouseover', () => {
 				clearTimeout(self.hideTimer);
 			});
 
-			preview.addEventListener('mouseleave', () => {
+			self.tag.$on('mouseleave', () => {
 				clearTimeout(self.showTimer);
 				self.hideTimer = setTimeout(self.close, 500);
 			});
 
-			document.body.appendChild(preview);
+			document.body.appendChild(self.tag.$el);
 		};
 
 		el.addEventListener('mouseover', () => {
diff --git a/src/client/app/common/views/filters/bytes.ts b/src/client/filters/bytes.ts
similarity index 86%
rename from src/client/app/common/views/filters/bytes.ts
rename to src/client/filters/bytes.ts
index 227ccae3a496140108a062589173963e4486a302..5b5d966cfd32257e530016f913b4c7bb75106db1 100644
--- a/src/client/app/common/views/filters/bytes.ts
+++ b/src/client/filters/bytes.ts
@@ -2,7 +2,7 @@ import Vue from 'vue';
 
 Vue.filter('bytes', (v, digits = 0) => {
 	if (v == null) return '?';
-	const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
+	const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
 	if (v == 0) return '0';
 	const isMinus = v < 0;
 	if (isMinus) v = -v;
diff --git a/src/client/filters/index.ts b/src/client/filters/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1759c19c2cfd0f476144da2130c743c31032fad8
--- /dev/null
+++ b/src/client/filters/index.ts
@@ -0,0 +1,4 @@
+require('./bytes');
+require('./number');
+require('./user');
+require('./note');
diff --git a/src/client/app/common/views/filters/note.ts b/src/client/filters/note.ts
similarity index 100%
rename from src/client/app/common/views/filters/note.ts
rename to src/client/filters/note.ts
diff --git a/src/client/app/common/views/filters/number.ts b/src/client/filters/number.ts
similarity index 100%
rename from src/client/app/common/views/filters/number.ts
rename to src/client/filters/number.ts
diff --git a/src/client/app/common/views/filters/user.ts b/src/client/filters/user.ts
similarity index 65%
rename from src/client/app/common/views/filters/user.ts
rename to src/client/filters/user.ts
index 9d4ae5c58b2eac0672094a048f563a83816a238b..e8f10c3db6e5a10bd2e526f2005425d5e5b567cf 100644
--- a/src/client/app/common/views/filters/user.ts
+++ b/src/client/filters/user.ts
@@ -1,7 +1,7 @@
 import Vue from 'vue';
-import getAcct from '../../../../../misc/acct/render';
-import getUserName from '../../../../../misc/get-user-name';
-import { url } from '../../../config';
+import getAcct from '../../misc/acct/render';
+import getUserName from '../../misc/get-user-name';
+import { url } from '../config';
 
 Vue.filter('acct', user => {
 	return getAcct(user);
diff --git a/src/client/i18n.ts b/src/client/i18n.ts
new file mode 100644
index 0000000000000000000000000000000000000000..05d319fbafd8a6045504080bf76a555ed3f74b6e
--- /dev/null
+++ b/src/client/i18n.ts
@@ -0,0 +1,12 @@
+import Vue from 'vue';
+import VueI18n from 'vue-i18n';
+import { lang, locale } from './config';
+
+Vue.use(VueI18n);
+
+export default new VueI18n({
+	locale: lang,
+	messages: {
+		[lang]: locale
+	}
+});
diff --git a/src/client/init.ts b/src/client/init.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3ea95aa96b6c27d093ea46444f6adead196405bd
--- /dev/null
+++ b/src/client/init.ts
@@ -0,0 +1,199 @@
+/**
+ * App entry point
+ */
+
+import Vue from 'vue';
+import Vuex from 'vuex';
+import VueMeta from 'vue-meta';
+import PortalVue from 'portal-vue';
+import VAnimateCss from 'v-animate-css';
+import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
+
+import i18n from './i18n';
+import VueHotkey from './scripts/hotkey';
+import App from './app.vue';
+import MiOS from './mios';
+import { version, langs, instanceName } from './config';
+import PostFormDialog from './components/post-form-dialog.vue';
+import Dialog from './components/dialog.vue';
+import Menu from './components/menu.vue';
+import { router } from './router';
+import { applyTheme, lightTheme } from './theme';
+
+Vue.use(Vuex);
+Vue.use(VueHotkey);
+Vue.use(VueMeta);
+Vue.use(PortalVue);
+Vue.use(VAnimateCss);
+Vue.component('fa', FontAwesomeIcon);
+
+require('./directives');
+require('./components');
+require('./widgets');
+require('./filters');
+
+Vue.mixin({
+	methods: {
+		destroyDom() {
+			this.$destroy();
+
+			if (this.$el.parentNode) {
+				this.$el.parentNode.removeChild(this.$el);
+			}
+		}
+	}
+});
+
+console.info(`Misskey v${version}`);
+
+// v11互換性のため
+if (localStorage.getItem('kyoppie') === 'yuppie') {
+	localStorage.clear();
+	location.reload(true);
+}
+
+if (localStorage.getItem('theme') == null) {
+	applyTheme(lightTheme);
+}
+
+//#region Detect the user language
+let lang = null;
+
+if (langs.map(x => x[0]).includes(navigator.language)) {
+	lang = navigator.language;
+} else {
+	lang = langs.map(x => x[0]).find(x => x.split('-')[0] == navigator.language);
+
+	if (lang == null) {
+		// Fallback
+		lang = 'en-US';
+	}
+}
+
+localStorage.setItem('lang', lang);
+//#endregion
+
+// Detect the user agent
+const ua = navigator.userAgent.toLowerCase();
+let isMobile = /mobile|iphone|ipad|android/.test(ua);
+
+// Get the <head> element
+const head = document.getElementsByTagName('head')[0];
+
+// If mobile, insert the viewport meta tag
+if (isMobile || window.innerWidth <= 1024) {
+	const viewport = document.getElementsByName("viewport").item(0);
+	viewport.setAttribute('content',
+		`${viewport.getAttribute('content')},minimum-scale=1,maximum-scale=1,user-scalable=no`);
+	head.appendChild(viewport);
+}
+
+//#region Fetch locale data
+const cachedLocale = localStorage.getItem('locale');
+
+if (cachedLocale == null) {
+	fetch(`/assets/locales/${lang}.${version}.json`)
+		.then(response => response.json()).then(locale => {
+			localStorage.setItem('locale', JSON.stringify(locale));
+			i18n.locale = lang;
+			i18n.setLocaleMessage(lang, locale);
+		});
+} else {
+	// TODO: 古い時だけ更新
+	setTimeout(() => {
+		fetch(`/assets/locales/${lang}.${version}.json`)
+			.then(response => response.json()).then(locale => {
+				localStorage.setItem('locale', JSON.stringify(locale));
+			});
+	}, 1000 * 5);
+}
+//#endregion
+
+//#region Set lang attr
+const html = document.documentElement;
+html.setAttribute('lang', lang);
+//#endregion
+
+// iOSでプライベートモードだとlocalStorageが使えないので既存のメソッドを上書きする
+try {
+	localStorage.setItem('foo', 'bar');
+} catch (e) {
+	Storage.prototype.setItem = () => { }; // noop
+}
+
+// http://qiita.com/junya/items/3ff380878f26ca447f85
+document.body.setAttribute('ontouchstart', '');
+
+// アプリ基底要素マウント
+document.body.innerHTML = '<div id="app"></div>';
+
+const os = new MiOS();
+
+os.init(async () => {
+	if (os.store.state.settings.wallpaper) document.documentElement.style.backgroundImage = `url(${os.store.state.settings.wallpaper})`;
+
+	if ('Notification' in window && os.store.getters.isSignedIn) {
+		// 許可を得ていなかったらリクエスト
+		if (Notification.permission === 'default') {
+			Notification.requestPermission();
+		}
+	}
+
+	const app = new Vue({
+		store: os.store,
+		metaInfo: {
+			title: null,
+			titleTemplate: title => title ? `${title} | ${instanceName}` : instanceName
+		},
+		data() {
+			return {
+				stream: os.stream,
+				isMobile: isMobile
+			};
+		},
+		methods: {
+			api: os.api,
+			getMeta: os.getMeta,
+			getMetaSync: os.getMetaSync,
+			signout: os.signout,
+			new(vm, props) {
+				const x = new vm({
+					parent: this,
+					propsData: props
+				}).$mount();
+				document.body.appendChild(x.$el);
+				return x;
+			},
+			dialog(opts) {
+				const vm = this.new(Dialog, opts);
+				const p: any = new Promise((res) => {
+					vm.$once('ok', result => res({ canceled: false, result }));
+					vm.$once('cancel', () => res({ canceled: true }));
+				});
+				p.close = () => {
+					vm.close();
+				};
+				return p;
+			},
+			menu(opts) {
+				const vm = this.new(Menu, opts);
+				const p: any = new Promise((res) => {
+					vm.$once('closed', () => res());
+				});
+				return p;
+			},
+			post(opts, cb) {
+				const vm = this.new(PostFormDialog, opts);
+				if (cb) vm.$once('closed', cb);
+				(vm as any).focus();
+			},
+		},
+		router: router,
+		render: createEl => createEl(App)
+	});
+
+	os.app = app;
+
+	// マウント
+	app.$mount('#app');
+});
diff --git a/src/client/app/mios.ts b/src/client/mios.ts
similarity index 72%
rename from src/client/app/mios.ts
rename to src/client/mios.ts
index 2c62f120ead81aedb5eca65f7dcb82541820ea45..282c51185f198a65ab4a4c85b30e69489ae530c1 100644
--- a/src/client/app/mios.ts
+++ b/src/client/mios.ts
@@ -1,14 +1,12 @@
 import autobind from 'autobind-decorator';
 import Vue from 'vue';
 import { EventEmitter } from 'eventemitter3';
-import { v4 as uuid } from 'uuid';
 
 import initStore from './store';
 import { apiUrl, version, locale } from './config';
-import Progress from './common/scripts/loading';
+import Progress from './scripts/loading';
 
-import Err from './common/views/components/connect-failed.vue';
-import Stream from './common/scripts/stream';
+import Stream from './scripts/stream';
 
 //#region api requests
 let spinner = null;
@@ -27,26 +25,10 @@ export default class MiOS extends EventEmitter {
 		chachedAt: Date;
 	};
 
-	public get instanceName() {
-		const siteName = document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement;
-		if (siteName && siteName.content) {
-			return siteName.content;
-		}
-
-		return 'Misskey';
-	}
-
 	private isMetaFetching = false;
 
 	public app: Vue;
 
-	/**
-	 * Whether is debug mode
-	 */
-	public get debug() {
-		return this.store ? this.store.state.device.debug : false;
-	}
-
 	public store: ReturnType<typeof initStore>;
 
 	/**
@@ -59,54 +41,6 @@ export default class MiOS extends EventEmitter {
 	 */
 	private swRegistration: ServiceWorkerRegistration = null;
 
-	/**
-	 * Whether should register ServiceWorker
-	 */
-	private shouldRegisterSw: boolean;
-
-	/**
-	 * ウィンドウシステム
-	 */
-	public windows = new WindowSystem();
-
-	/**
-	 * MiOSインスタンスを作成します
-	 * @param shouldRegisterSw ServiceWorkerを登録するかどうか
-	 */
-	constructor(shouldRegisterSw = false) {
-		super();
-
-		this.shouldRegisterSw = shouldRegisterSw;
-
-		if (this.debug) {
-			(window as any).os = this;
-		}
-	}
-
-	@autobind
-	public log(...args) {
-		if (!this.debug) return;
-		console.log.apply(null, args);
-	}
-
-	@autobind
-	public logInfo(...args) {
-		if (!this.debug) return;
-		console.info.apply(null, args);
-	}
-
-	@autobind
-	public logWarn(...args) {
-		if (!this.debug) return;
-		console.warn.apply(null, args);
-	}
-
-	@autobind
-	public logError(...args) {
-		if (!this.debug) return;
-		console.error.apply(null, args);
-	}
-
 	@autobind
 	public signout() {
 		this.store.dispatch('logout');
@@ -154,10 +88,7 @@ export default class MiOS extends EventEmitter {
 			// When failure
 			.catch(() => {
 				// Render the error screen
-				document.body.innerHTML = '<div id="err"></div>';
-				new Vue({
-					render: createEl => createEl(Err)
-				}).$mount('#err');
+				document.body.innerHTML = '<div id="err">Error</div>';
 
 				Progress.done();
 			});
@@ -177,11 +108,9 @@ export default class MiOS extends EventEmitter {
 			callback();
 
 			// Init service worker
-			if (this.shouldRegisterSw) {
-				this.getMeta().then(data => {
-					if (data.swPublickey) this.registerSw(data.swPublickey);
-				});
-			}
+			this.getMeta().then(data => {
+				if (data.swPublickey) this.registerSw(data.swPublickey);
+			});
 		};
 
 		// キャッシュがあったとき
@@ -199,9 +128,9 @@ export default class MiOS extends EventEmitter {
 				this.store.dispatch('mergeMe', freshData);
 			});
 		} else {
-			// Get token from cookie or localStorage
-			const i = (document.cookie.match(/i=(\w+)/) || [null, null])[1] || localStorage.getItem('i');
-
+			// Get token from localStorage
+			const i = localStorage.getItem('i');
+			
 			fetchme(i, me => {
 				if (me) {
 					this.store.dispatch('login', me);
@@ -240,18 +169,6 @@ export default class MiOS extends EventEmitter {
 				});
 			});
 
-			main.on('readAllMessagingMessages', () => {
-				this.store.dispatch('mergeMe', {
-					hasUnreadMessagingMessage: false
-				});
-			});
-
-			main.on('unreadMessagingMessage', () => {
-				this.store.dispatch('mergeMe', {
-					hasUnreadMessagingMessage: true
-				});
-			});
-
 			main.on('unreadMention', () => {
 				this.store.dispatch('mergeMe', {
 					hasUnreadMentions: true
@@ -276,6 +193,36 @@ export default class MiOS extends EventEmitter {
 				});
 			});
 
+			main.on('readAllMessagingMessages', () => {
+				this.store.dispatch('mergeMe', {
+					hasUnreadMessagingMessage: false
+				});
+			});
+
+			main.on('unreadMessagingMessage', () => {
+				this.store.dispatch('mergeMe', {
+					hasUnreadMessagingMessage: true
+				});
+			});
+
+			main.on('readAllAntennas', () => {
+				this.store.dispatch('mergeMe', {
+					hasUnreadAntenna: false
+				});
+			});
+
+			main.on('unreadAntenna', () => {
+				this.store.dispatch('mergeMe', {
+					hasUnreadAntenna: true
+				});
+			});
+
+			main.on('readAllAnnouncements', () => {
+				this.store.dispatch('mergeMe', {
+					hasUnreadAnnouncement: false
+				});
+			});
+
 			main.on('clientSettingUpdated', x => {
 				this.store.commit('settings/set', {
 					key: x.key,
@@ -309,8 +256,6 @@ export default class MiOS extends EventEmitter {
 
 		// When service worker activated
 		navigator.serviceWorker.ready.then(registration => {
-			this.log('[sw] ready: ', registration);
-
 			this.swRegistration = registration;
 
 			// Options of pushManager.subscribe
@@ -327,8 +272,6 @@ export default class MiOS extends EventEmitter {
 
 			// Subscribe push notification
 			this.swRegistration.pushManager.subscribe(opts).then(subscription => {
-				this.log('[sw] Subscribe OK:', subscription);
-
 				function encode(buffer: ArrayBuffer) {
 					return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
 				}
@@ -342,11 +285,8 @@ export default class MiOS extends EventEmitter {
 			})
 			// When subscribe failed
 			.catch(async (err: Error) => {
-				this.logError('[sw] Subscribe Error:', err);
-
 				// 通知が許可されていなかったとき
 				if (err.name == 'NotAllowedError') {
-					this.logError('[sw] Subscribe failed due to notification not allowed');
 					return;
 				}
 
@@ -362,69 +302,40 @@ export default class MiOS extends EventEmitter {
 		const sw = `/sw.${version}.js`;
 
 		// Register service worker
-		navigator.serviceWorker.register(sw).then(registration => {
-			// 登録成功
-			this.logInfo('[sw] Registration successful with scope: ', registration.scope);
-		}).catch(err => {
-			// 登録失敗 :(
-			this.logError('[sw] Registration failed: ', err);
-		});
+		navigator.serviceWorker.register(sw);
 	}
 
-	public requests = [];
-
 	/**
 	 * Misskey APIにリクエストします
 	 * @param endpoint エンドポイント名
 	 * @param data パラメータ
 	 */
 	@autobind
-	public api(endpoint: string, data: { [x: string]: any } = {}, silent = false): Promise<{ [x: string]: any }> {
-		if (!silent) {
-			if (++pending === 1) {
-				spinner = document.createElement('div');
-				spinner.setAttribute('id', 'wait');
-				document.body.appendChild(spinner);
-			}
+	public api(endpoint: string, data: { [x: string]: any } = {}, token?): Promise<{ [x: string]: any }> {
+		if (++pending === 1) {
+			spinner = document.createElement('div');
+			spinner.setAttribute('id', 'wait');
+			document.body.appendChild(spinner);
 		}
 
 		const onFinally = () => {
-			if (!silent) {
-				if (--pending === 0) spinner.parentNode.removeChild(spinner);
-			}
+			if (--pending === 0) spinner.parentNode.removeChild(spinner);
 		};
 
 		const promise = new Promise((resolve, reject) => {
 			// Append a credential
 			if (this.store.getters.isSignedIn) (data as any).i = this.store.state.i.token;
-
-			const req = {
-				id: uuid(),
-				date: new Date(),
-				name: endpoint,
-				data,
-				res: null,
-				status: null
-			};
-
-			if (this.debug) {
-				this.requests.push(req);
-			}
+			if (token) (data as any).i = token;
 
 			// Send request
 			fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
 				method: 'POST',
 				body: JSON.stringify(data),
-				credentials: endpoint === 'signin' ? 'include' : 'omit',
+				credentials: 'omit',
 				cache: 'no-cache'
 			}).then(async (res) => {
 				const body = res.status === 204 ? null : await res.json();
 
-				if (this.debug) {
-					req.status = res.status;
-					req.res = body;
-				}
-
 				if (res.status === 200) {
 					resolve(body);
 				} else if (res.status === 204) {
@@ -484,24 +395,6 @@ export default class MiOS extends EventEmitter {
 	}
 }
 
-class WindowSystem extends EventEmitter {
-	public windows = new Set();
-
-	public add(window) {
-		this.windows.add(window);
-		this.emit('added', window);
-	}
-
-	public remove(window) {
-		this.windows.delete(window);
-		this.emit('removed', window);
-	}
-
-	public getAll() {
-		return this.windows;
-	}
-}
-
 /**
  * Convert the URL safe base64 string to a Uint8Array
  * @param base64String base64 string
diff --git a/src/client/pages/about.vue b/src/client/pages/about.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e47856bb94ffdcccf93d029950c7dbae0fd28cbd
--- /dev/null
+++ b/src/client/pages/about.vue
@@ -0,0 +1,106 @@
+<template>
+<div class="mmnnbwxb">
+	<portal to="icon"><fa :icon="faInfoCircle"/></portal>
+	<portal to="title">{{ $t('about') }}</portal>
+
+	<section class="_section info" v-if="meta">
+		<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('instanceInfo') }}</div>
+		<div class="_content" v-if="meta.description">
+			<div>{{ meta.description }}</div>
+		</div>
+		<div class="_content table">
+			<div><b>{{ $t('administrator') }}</b><span>{{ meta.maintainerName }}</span></div>
+			<div><b></b><span>{{ meta.maintainerEmail }}</span></div>
+		</div>
+		<div class="_content table" v-if="stats">
+			<div><b>{{ $t('users') }}</b><span>{{ stats.originalUsersCount | number }}</span></div>
+			<div><b>{{ $t('notes') }}</b><span>{{ stats.originalNotesCount | number }}</span></div>
+		</div>
+		<div class="_content table">
+			<div><b>Misskey</b><span>v{{ version }}</span></div>
+		</div>
+	</section>
+
+	<section class="_section aboutMisskey">
+		<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('aboutMisskey') }}</div>
+		<div class="_content">
+			<div style="margin-bottom: 1em;">{{ $t('aboutMisskeyText') }}</div>
+			<div>{{ $t('misskeyMembers') }}</div>
+			<span class="members">
+				<a href="https://github.com/syuilo" target="_blank">@syuilo</a>
+				<a href="https://github.com/AyaMorisawa" target="_blank">@AyaMorisawa</a>
+				<a href="https://github.com/mei23" target="_blank">@mei23</a>
+				<a href="https://github.com/acid-chicken" target="_blank">@acid-chicken</a>
+				<a href="https://github.com/tamaina" target="_blank">@tamaina</a>
+				<a href="https://github.com/rinsuki" target="_blank">@rinsuki</a>
+			</span>
+			<div style="margin-top: 1em;">{{ $t('misskeySource') }}</div>
+			<a href="https://github.com/syuilo/misskey" target="_blank" style="color: var(--link);">https://github.com/syuilo/misskey</a>
+		</div>
+	</section>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
+import { version } from '../config';
+import i18n from '../i18n';
+
+export default Vue.extend({
+	i18n,
+
+	metaInfo() {
+		return {
+			title: this.$t('instance') as string
+		};
+	},
+
+	data() {
+		return {
+			version,
+			meta: null,
+			stats: null,
+			serverInfo: null,
+			faInfoCircle
+		}
+	},
+
+	created() {
+		this.$root.getMeta().then(meta => {
+			this.meta = meta;
+		});
+
+		this.$root.api('stats').then(res => {
+			this.stats = res;
+		});
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.mmnnbwxb {
+	> .info {
+		> .table {
+			> div {
+				display: flex;
+
+				> * {
+					flex: 1;
+				}
+			}
+		}
+	}
+
+	> .aboutMisskey {
+		> ._content {
+			> .members {
+				> a {
+					color: var(--link);
+					margin-right: 0.5em;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/announcements.vue b/src/client/pages/announcements.vue
new file mode 100644
index 0000000000000000000000000000000000000000..586bc0c03c8cf9a24b081dd7585c54f04b286348
--- /dev/null
+++ b/src/client/pages/announcements.vue
@@ -0,0 +1,73 @@
+<template>
+<div>
+	<portal to="icon"><fa :icon="faBroadcastTower"/></portal>
+	<portal to="title">{{ $t('announcements') }}</portal>
+
+	<mk-pagination :pagination="pagination" #default="{items}" class="ruryvtyk" ref="list">
+		<section class="_section announcement" v-for="(announcement, i) in items" :key="announcement.id" :data-index="i">
+			<div class="_title"><span v-if="$store.getters.isSignedIn && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
+			<div class="_content">
+				<mfm :text="announcement.text"/>
+				<img v-if="announcement.imageUrl" :src="announcement.imageUrl" alt=""/>
+			</div>
+			<div class="_footer" v-if="$store.getters.isSignedIn && !announcement.isRead">
+				<mk-button @click="read(announcement)" primary><fa :icon="faCheck"/> {{ $t('gotIt') }}</mk-button>
+			</div>
+		</section>
+	</mk-pagination>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faCheck, faBroadcastTower } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
+import MkPagination from '../components/ui/pagination.vue';
+import MkButton from '../components/ui/button.vue';
+
+export default Vue.extend({
+	i18n,
+
+	metaInfo() {
+		return {
+			title: this.$t('announcements') as string
+		};
+	},
+
+	components: {
+		MkPagination,
+		MkButton
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'announcements',
+				limit: 10,
+			},
+			faCheck, faBroadcastTower
+		};
+	},
+
+	methods: {
+		read(announcement) {
+			announcement.isRead = true;
+			this.$root.api('i/read-announcement', { announcementId: announcement.id });
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ruryvtyk {
+	> .announcement {
+		> ._content {
+			> img {
+				display: block;
+				max-height: 300px;
+				max-width: 100%;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/auth.form.vue b/src/client/pages/auth.form.vue
new file mode 100644
index 0000000000000000000000000000000000000000..80a792eb3601db728cb44a25c9669e57ed90f570
--- /dev/null
+++ b/src/client/pages/auth.form.vue
@@ -0,0 +1,63 @@
+<template>
+<section class="_section">
+	<div class="_title">{{ $t('_auth.shareAccess', { name: app.name }) }}</div>
+	<div class="_content">
+		<h2>{{ app.name }}</h2>
+		<p class="id">{{ app.id }}</p>
+		<p class="description">{{ app.description }}</p>
+	</div>
+	<div class="_content">
+		<h2>{{ $t('_auth.permissionAsk') }}</h2>
+		<ul>
+			<template v-for="p in app.permission">
+				<li :key="p">{{ $t(`_permissions.${p}`) }}</li>
+			</template>
+		</ul>
+	</div>
+	<div class="_footer">
+		<mk-button @click="cancel" inline>{{ $t('cancel') }}</mk-button>
+		<mk-button @click="accept" inline primary>{{ $t('accept') }}</mk-button>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import MkButton from '../components/ui/button.vue';
+
+export default Vue.extend({
+	i18n,
+	components: {
+		MkButton
+	},
+	props: ['session'],
+	computed: {
+		name(): string {
+			const el = document.createElement('div');
+			el.textContent = this.app.name
+			return el.innerHTML;
+		},
+		app(): any {
+			return this.session.app;
+		}
+	},
+	methods: {
+		cancel() {
+			this.$root.api('auth/deny', {
+				token: this.session.token
+			}).then(() => {
+				this.$emit('denied');
+			});
+		},
+
+		accept() {
+			this.$root.api('auth/accept', {
+				token: this.session.token
+			}).then(() => {
+				this.$emit('accepted');
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/auth.vue b/src/client/pages/auth.vue
new file mode 100644
index 0000000000000000000000000000000000000000..15ec81e01985fa8a60ffbbebb0414e71e1560008
--- /dev/null
+++ b/src/client/pages/auth.vue
@@ -0,0 +1,93 @@
+<template>
+<div class="_panel" v-if="$store.getters.isSignedIn && fetching">
+	<mk-loading/>
+</div>
+<div v-else-if="$store.getters.isSignedIn">
+	<x-form
+		class="form"
+		ref="form"
+		v-if="state == 'waiting'"
+		:session="session"
+		@denied="state = 'denied'"
+		@accepted="accepted"
+	/>
+	<div class="denied _panel" v-if="state == 'denied'">
+		<h1>{{ $t('denied') }}</h1>
+		<p>{{ $t('denied-paragraph') }}</p>
+	</div>
+	<div class="accepted _panel" v-if="state == 'accepted'">
+		<h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$t('allowed') }}</h1>
+		<p v-if="session.app.callbackUrl">{{ $t('callback-url') }}<mk-ellipsis/></p>
+		<p v-if="!session.app.callbackUrl">{{ $t('please-go-back') }}</p>
+	</div>
+	<div class="error _panel" v-if="state == 'fetch-session-error'">
+		<p>{{ $t('error') }}</p>
+	</div>
+</div>
+<div class="signin" v-else>
+	<h1>{{ $t('sign-in') }}</h1>
+	<mk-signin/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import XForm from './auth.form.vue';
+
+export default Vue.extend({
+	i18n,
+	components: {
+		XForm
+	},
+	data() {
+		return {
+			state: null,
+			session: null,
+			fetching: true
+		};
+	},
+	computed: {
+		token(): string {
+			return this.$route.params.token;
+		}
+	},
+	mounted() {
+		if (!this.$store.getters.isSignedIn) return;
+
+		// Fetch session
+		this.$root.api('auth/session/show', {
+			token: this.token
+		}).then(session => {
+			this.session = session;
+			this.fetching = false;
+
+			// 既に連携していた場合
+			if (this.session.app.isAuthorized) {
+				this.$root.api('auth/accept', {
+					token: this.session.token
+				}).then(() => {
+					this.accepted();
+				});
+			} else {
+				this.state = 'waiting';
+			}
+		}).catch(error => {
+			this.state = 'fetch-session-error';
+			this.fetching = false;
+		});
+	},
+	methods: {
+		accepted() {
+			this.state = 'accepted';
+			if (this.session.app.callbackUrl) {
+				location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/src/client/pages/drive.vue b/src/client/pages/drive.vue
new file mode 100644
index 0000000000000000000000000000000000000000..24a0d91ff6bb2c480022056f66e473dff091a6c8
--- /dev/null
+++ b/src/client/pages/drive.vue
@@ -0,0 +1,87 @@
+<template>
+<div>
+	<portal to="header">
+		<button @click="menu" class="_button _jmoebdiw_">
+			<fa :icon="faCloud" style="margin-right: 8px;"/>
+			<span v-if="folder">{{ $t('drive') }} ({{ folder.name }})</span>
+			<span v-else>{{ $t('drive') }}</span>
+			<fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/>
+		</button>
+	</portal>
+	<x-drive ref="drive" @cd="x => folder = x"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faCloud, faAngleDown, faAngleUp, faFolderPlus, faUpload, faLink, faICursor, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
+import XDrive from '../components/drive.vue';
+
+export default Vue.extend({
+	metaInfo() {
+		return {
+			title: this.$t('drive') as string
+		};
+	},
+
+	components: {
+		XDrive
+	},
+
+	data() {
+		return {
+			menuOpened: false,
+			folder: null,
+			faCloud, faAngleDown, faAngleUp
+		};
+	},
+
+	methods: {
+		menu(ev) {
+			this.menuOpened = true;
+			this.$root.menu({
+				items: [{
+					text: this.$t('addFile'),
+					type: 'label'
+				}, {
+					text: this.$t('upload'),
+					icon: faUpload,
+					action: () => { this.$refs.drive.selectLocalFile(); }
+				}, {
+					text: this.$t('fromUrl'),
+					icon: faLink,
+					action: () => { this.$refs.drive.urlUpload(); }
+				}, null, {
+					text: this.folder ? this.folder.name : this.$t('drive'),
+					type: 'label'
+				}, this.folder ? {
+					text: this.$t('renameFolder'),
+					icon: faICursor,
+					action: () => { this.$refs.drive.renameFolder(); }
+				} : undefined, this.folder ? {
+					text: this.$t('deleteFolder'),
+					icon: faTrashAlt,
+					action: () => { this.$refs.drive.deleteFolder(); }
+				} : undefined, {
+					text: this.$t('createFolder'),
+					icon: faFolderPlus,
+					action: () => { this.$refs.drive.createFolder(); }
+				}],
+				fixed: true,
+				noCenter: true,
+				source: ev.currentTarget || ev.target
+			}).then(() => {
+				this.menuOpened = false;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss">
+._jmoebdiw_ {
+	height: 100%;
+	padding: 0 16px;
+	font-weight: bold;
+}
+</style>
diff --git a/src/client/pages/explore.vue b/src/client/pages/explore.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ba2c3faa6cde543c8693ce402e3f5679582fa59b
--- /dev/null
+++ b/src/client/pages/explore.vue
@@ -0,0 +1,212 @@
+<template>
+<div>
+	<portal to="icon"><fa :icon="faHashtag"/></portal>
+	<portal to="title">{{ $t('explore') }}</portal>
+
+	<div class="localfedi7 _panel" v-if="meta && stats && tag == null" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }">
+		<header><span>{{ $t('explore', { host: meta.name || 'Misskey' }) }}</span></header>
+		<div><span>{{ $t('exploreUsersCount', { count: num(stats.originalUsersCount) }) }}</span></div>
+	</div>
+
+	<template v-if="tag == null">
+		<x-user-list :pagination="pinnedUsers" :expanded="false">
+			<fa :icon="faBookmark" fixed-width/>{{ $t('pinnedUsers') }}
+		</x-user-list>
+		<x-user-list :pagination="popularUsers" :expanded="false">
+			<fa :icon="faChartLine" fixed-width/>{{ $t('popularUsers') }}
+		</x-user-list>
+		<x-user-list :pagination="recentlyUpdatedUsers" :expanded="false">
+			<fa :icon="faCommentAlt" fixed-width/>{{ $t('recentlyUpdatedUsers') }}
+		</x-user-list>
+		<x-user-list :pagination="recentlyRegisteredUsers" :expanded="false">
+			<fa :icon="faPlus" fixed-width/>{{ $t('recentlyRegisteredUsers') }}
+		</x-user-list>
+	</template>
+
+	<div class="localfedi7 _panel" v-if="tag == null" :style="{ backgroundImage: `url(/assets/fedi.jpg)`, marginTop: 'var(--margin)' }">
+		<header><span>{{ $t('exploreFediverse') }}</span></header>
+	</div>
+
+	<mk-container :body-togglable="true" :expanded="false" ref="tags">
+		<template #header><fa :icon="faHashtag" fixed-width/>{{ $t('popularTags') }}</template>
+
+		<div class="vxjfqztj">
+			<router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</router-link>
+			<router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</router-link>
+		</div>
+	</mk-container>
+
+	<x-user-list v-if="tag != null" :pagination="tagUsers" :key="`${tag}`">
+		<fa :icon="faHashtag" fixed-width/>{{ tag }}
+	</x-user-list>
+	<template v-if="tag == null">
+		<x-user-list :pagination="popularUsersF" :expanded="false">
+			<fa :icon="faChartLine" fixed-width/>{{ $t('popularUsers') }}
+		</x-user-list>
+		<x-user-list :pagination="recentlyUpdatedUsersF" :expanded="false">
+			<fa :icon="faCommentAlt" fixed-width/>{{ $t('recentlyUpdatedUsers') }}
+		</x-user-list>
+		<x-user-list :pagination="recentlyRegisteredUsersF" :expanded="false">
+			<fa :icon="faRocket" fixed-width/>{{ $t('recentlyDiscoveredUsers') }}
+		</x-user-list>
+	</template>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faChartLine, faPlus, faHashtag, faRocket } from '@fortawesome/free-solid-svg-icons';
+import { faBookmark, faCommentAlt } from '@fortawesome/free-regular-svg-icons';
+import i18n from '../i18n';
+import XUserList from '../components/user-list.vue';
+import MkContainer from '../components/ui/container.vue';
+
+export default Vue.extend({
+	i18n,
+
+	metaInfo() {
+		return {
+			title: this.$t('explore') as string
+		};
+	},
+
+	components: {
+		XUserList,
+		MkContainer,
+	},
+
+	props: {
+		tag: {
+			type: String,
+			required: false
+		}
+	},
+
+	data() {
+		return {
+			pinnedUsers: { endpoint: 'pinned-users' },
+			popularUsers: { endpoint: 'users', limit: 10, noPaging: true, params: {
+				state: 'alive',
+				origin: 'local',
+				sort: '+follower',
+			} },
+			recentlyUpdatedUsers: { endpoint: 'users', limit: 10, noPaging: true, params: {
+				origin: 'local',
+				sort: '+updatedAt',
+			} },
+			recentlyRegisteredUsers: { endpoint: 'users', limit: 10, noPaging: true, params: {
+				origin: 'local',
+				state: 'alive',
+				sort: '+createdAt',
+			} },
+			popularUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: {
+				state: 'alive',
+				origin: 'remote',
+				sort: '+follower',
+			} },
+			recentlyUpdatedUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: {
+				origin: 'combined',
+				sort: '+updatedAt',
+			} },
+			recentlyRegisteredUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: {
+				origin: 'combined',
+				sort: '+createdAt',
+			} },
+			tagsLocal: [],
+			tagsRemote: [],
+			stats: null,
+			meta: null,
+			num: Vue.filter('number'),
+			faBookmark, faChartLine, faCommentAlt, faPlus, faHashtag, faRocket
+		};
+	},
+
+	computed: {
+		tagUsers(): any {
+			return {
+				endpoint: 'hashtags/users',
+				limit: 30,
+				params: {
+					tag: this.tag,
+					origin: 'combined',
+					sort: '+follower',
+				}
+			};
+		},
+	},
+
+	watch: {
+		tag() {
+			if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null);
+		}
+	},
+
+	created() {
+		this.$root.api('hashtags/list', {
+			sort: '+attachedLocalUsers',
+			attachedToLocalUserOnly: true,
+			limit: 30
+		}).then(tags => {
+			this.tagsLocal = tags;
+		});
+		this.$root.api('hashtags/list', {
+			sort: '+attachedRemoteUsers',
+			attachedToRemoteUserOnly: true,
+			limit: 30
+		}).then(tags => {
+			this.tagsRemote = tags;
+		});
+		this.$root.api('stats').then(stats => {
+			this.stats = stats;
+		});
+		this.$root.getMeta().then(meta => {
+			this.meta = meta;
+		});
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.localfedi7 {
+	color: #fff;
+	padding: 16px;
+	height: 80px;
+	background-position: 50%;
+	background-size: cover;
+	margin-bottom: var(--margin);
+
+	> * {
+		&:not(:last-child) {
+			margin-bottom: 8px;
+		}
+
+		> span {
+			display: inline-block;
+			padding: 6px 8px;
+			background: rgba(0, 0, 0, 0.7);
+		}
+	}
+
+	> header {
+		font-size: 20px;
+		font-weight: bold;
+	}
+
+	> div {
+		font-size: 14px;
+		opacity: 0.8;
+	}
+}
+
+.vxjfqztj {
+	padding: 16px;
+
+	> * {
+		margin-right: 16px;
+
+		&.local {
+			font-weight: bold;
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/favorites.vue b/src/client/pages/favorites.vue
new file mode 100644
index 0000000000000000000000000000000000000000..59bef2ca91d392a00f343d53af84fc1bed83e8a4
--- /dev/null
+++ b/src/client/pages/favorites.vue
@@ -0,0 +1,48 @@
+<template>
+<div>
+	<portal to="icon"><fa :icon="faStar"/></portal>
+	<portal to="title">{{ $t('favorites') }}</portal>
+	<x-notes :pagination="pagination" :detail="true" :extract="items => items.map(item => item.note)" @before="before()" @after="after()"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faStar } from '@fortawesome/free-solid-svg-icons';
+import Progress from '../scripts/loading';
+import XNotes from '../components/notes.vue';
+
+export default Vue.extend({
+	metaInfo() {
+		return {
+			title: this.$t('favorites') as string
+		};
+	},
+
+	components: {
+		XNotes
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'i/favorites',
+				limit: 10,
+				params: () => ({
+				})
+			},
+			faStar
+		};
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/featured.vue b/src/client/pages/featured.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e6293e9e836967b6be022436527e1c9a4931dbf6
--- /dev/null
+++ b/src/client/pages/featured.vue
@@ -0,0 +1,47 @@
+<template>
+<div>
+	<portal to="icon"><fa :icon="faFireAlt"/></portal>
+	<portal to="title">{{ $t('featured') }}</portal>
+	<x-notes ref="notes" :pagination="pagination" @before="before" @after="after"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faFireAlt } from '@fortawesome/free-solid-svg-icons';
+import Progress from '../scripts/loading';
+import XNotes from '../components/notes.vue';
+
+export default Vue.extend({
+	metaInfo() {
+		return {
+			title: this.$t('featured') as string
+		};
+	},
+
+	components: {
+		XNotes
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'notes/featured',
+				limit: 10,
+				offsetMode: true
+			},
+			faFireAlt
+		};
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/follow-requests.vue b/src/client/pages/follow-requests.vue
new file mode 100644
index 0000000000000000000000000000000000000000..c302088b971c217ed1b807f3e99873a32d5d0ec4
--- /dev/null
+++ b/src/client/pages/follow-requests.vue
@@ -0,0 +1,142 @@
+<template>
+<mk-pagination :pagination="pagination" #default="{items}" class="mk-follow-requests" ref="list">
+	<div class="user _panel" v-for="(req, i) in items" :key="req.id" :data-index="i">
+		<mk-avatar class="avatar" :user="req.follower"/>
+		<div class="body">
+			<div class="name">
+				<router-link class="name" :to="req.follower | userPage" v-user-preview="req.follower.id"><mk-user-name :user="req.follower"/></router-link>
+				<p class="acct">@{{ req.follower | acct }}</p>
+			</div>
+			<div class="description" v-if="req.follower.description" :title="req.follower.description">
+				<mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$store.state.i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
+			</div>
+			<div class="actions">
+				<button class="_button" @click="accept(req.follower)"><fa :icon="faCheck"/></button>
+				<button class="_button" @click="reject(req.follower)"><fa :icon="faTimes"/></button>
+			</div>
+		</div>
+	</div>
+</mk-pagination>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faCheck, faTimes } from '@fortawesome/free-solid-svg-icons';
+import MkPagination from '../components/ui/pagination.vue';
+
+export default Vue.extend({
+	metaInfo() {
+		return {
+			title: this.$t('followRequests') as string
+		};
+	},
+
+	components: {
+		MkPagination
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'following/requests/list',
+				limit: 10,
+			},
+			faCheck, faTimes
+		};
+	},
+
+	methods: {
+		accept(user) {
+			this.$root.api('following/requests/accept', { userId: user.id }).then(() => {
+				this.$refs.list.reload();
+			});
+		},
+		reject(user) {
+			this.$root.api('following/requests/reject', { userId: user.id }).then(() => {
+				this.$refs.list.reload();
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-follow-requests {
+	> .user {
+		display: flex;
+		padding: 16px;
+
+		> .avatar {
+			display: block;
+			flex-shrink: 0;
+			margin: 0 12px 0 0;
+			width: 42px;
+			height: 42px;
+			border-radius: 8px;
+		}
+
+		> .body {
+			display: flex;
+			width: calc(100% - 54px);
+			position: relative;
+
+			> .name {
+				width: 45%;
+
+				@media (max-width: 500px) {
+					width: 100%;
+				}
+
+				> .name,
+				> .acct {
+					display: block;
+					white-space: nowrap;
+					text-overflow: ellipsis;
+					overflow: hidden;
+					margin: 0;
+				}
+
+				> .name {
+					font-size: 16px;
+					line-height: 24px;
+				}
+
+				> .acct {
+					font-size: 15px;
+					line-height: 16px;
+					opacity: 0.7;
+				}
+			}
+
+			> .description {
+				width: 55%;
+				line-height: 42px;
+				white-space: nowrap;
+				overflow: hidden;
+				text-overflow: ellipsis;
+				opacity: 0.7;
+				font-size: 14px;
+				padding-right: 40px;
+				padding-left: 8px;
+				box-sizing: border-box;
+
+				@media (max-width: 500px) {
+					display: none;
+				}
+			}
+
+			> .actions {
+				position: absolute;
+				top: 0;
+				bottom: 0;
+				right: 0;
+				margin: auto 0;
+
+				> button {
+					padding: 12px;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/follow.vue b/src/client/pages/follow.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d765259737bb1b11868ef079e0fd0d3733af1751
--- /dev/null
+++ b/src/client/pages/follow.vue
@@ -0,0 +1,98 @@
+<template>
+<div class="mk-follow-page">
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+
+export default Vue.extend({
+	i18n,
+
+	created() {
+		const acct = new URL(location.href).searchParams.get('acct');
+		if (acct == null) return;
+
+		const dialog = this.$root.dialog({
+			type: 'waiting',
+			text: this.$t('fetchingAsApObject') + '...',
+			showOkButton: false,
+			showCancelButton: false,
+			cancelableByBgClick: false
+		});
+
+		if (acct.startsWith('https://')) {
+			this.$root.api('ap/show', {
+				uri: acct
+			}).then(res => {
+				if (res.type == 'User') {
+					this.follow(res.object);
+				} else {
+					this.$root.dialog({
+						type: 'error',
+						text: 'Not a user'
+					}).then(() => {
+						window.close();
+					});
+				}
+			}).catch(e => {
+				this.$root.dialog({
+					type: 'error',
+					text: e
+				}).then(() => {
+					window.close();
+				});
+			}).finally(() => {
+				dialog.close();
+			});
+		} else {
+			this.$root.api('users/show', parseAcct(acct)).then(user => {
+				this.follow(user);
+			}).catch(e => {
+				this.$root.dialog({
+					type: 'error',
+					text: e
+				}).then(() => {
+					window.close();
+				});
+			}).finally(() => {
+				dialog.close();
+			});
+		}
+	},
+
+	methods: {
+		async follow(user) {
+			const { canceled } = await this.$root.dialog({
+				type: 'question',
+				text: this.$t('followConfirm', { name: user.name || user.username }),
+				showCancelButton: true
+			});
+
+			if (canceled) {
+				window.close();
+				return;
+			}
+			
+			this.$root.api('following/create', {
+				userId: user.id
+			}).then(() => {
+				this.$root.dialog({
+					type: 'success',
+					iconOnly: true, autoClose: true
+				}).then(() => {
+					window.close();
+				});
+			}).catch(e => {
+				this.$root.dialog({
+					type: 'error',
+					text: e
+				}).then(() => {
+					window.close();
+				});
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/index.home.vue b/src/client/pages/index.home.vue
new file mode 100644
index 0000000000000000000000000000000000000000..0c3d2e7f8646c166562021eb1138fd45267d5af6
--- /dev/null
+++ b/src/client/pages/index.home.vue
@@ -0,0 +1,190 @@
+<template>
+<div class="mk-home" v-hotkey.global="keymap">
+	<portal to="header">
+		<button @click="choose" class="_button _kjvfvyph_">
+			<i><fa v-if="$store.state.i.hasUnreadAntenna" :icon="faCircle"/></i>
+			<fa v-if="src === 'home'" :icon="faHome"/>
+			<fa v-if="src === 'local'" :icon="faComments"/>
+			<fa v-if="src === 'social'" :icon="faShareAlt"/>
+			<fa v-if="src === 'global'" :icon="faGlobe"/>
+			<fa v-if="src === 'list'" :icon="faListUl"/>
+			<fa v-if="src === 'antenna'" :icon="faSatellite"/>
+			<span style="margin-left: 8px;">{{ src === 'list' ? list.name : src === 'antenna' ? antenna.name : $t('_timelines.' + src) }}</span>
+			<fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/>
+		</button>
+	</portal>
+	<x-timeline ref="tl" :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src" :src="src" :list="list" :antenna="antenna" @before="before()" @after="after()"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faListUl, faSatellite, faCircle } from '@fortawesome/free-solid-svg-icons';
+import { faComments } from '@fortawesome/free-regular-svg-icons';
+import Progress from '../scripts/loading';
+import XTimeline from '../components/timeline.vue';
+
+export default Vue.extend({
+	metaInfo() {
+		return {
+			title: this.$t('timeline') as string
+		};
+	},
+
+	components: {
+		XTimeline
+	},
+
+	data() {
+		return {
+			src: 'home',
+			list: null,
+			antenna: null,
+			menuOpened: false,
+			faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faComments, faListUl, faSatellite, faCircle
+		};
+	},
+
+	computed: {
+		keymap(): any {
+			return {
+				't': this.focus
+			};
+		}
+	},
+
+	watch: {
+		src() {
+			this.showNav = false;
+			this.saveSrc();
+		},
+		list(x) {
+			this.showNav = false;
+			this.saveSrc();
+			if (x != null) this.antenna = null;
+		},
+		antenna(x) {
+			this.showNav = false;
+			this.saveSrc();
+			if (x != null) this.list = null;
+		},
+	},
+
+	created() {
+		this.$root.getMeta().then((meta: Record<string, any>) => {
+			if (!(
+				this.enableGlobalTimeline = !meta.disableGlobalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin
+			) && this.src === 'global') this.src = 'local';
+			if (!(
+				this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin
+			) && ['local', 'social'].includes(this.src)) this.src = 'home';
+		});
+		if (this.$store.state.device.tl) {
+			this.src = this.$store.state.device.tl.src;
+			if (this.src === 'list') {
+				this.list = this.$store.state.device.tl.arg;
+			} else if (this.src === 'antenna') {
+				this.antenna = this.$store.state.device.tl.arg;
+			}
+		}
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		},
+
+		async choose(ev) {
+			this.menuOpened = true;
+			const [antennas, lists] = await Promise.all([
+				this.$root.api('antennas/list'),
+				this.$root.api('users/lists/list')
+			]);
+			const antennaItems = antennas.map(antenna => ({
+				text: antenna.name,
+				icon: faSatellite,
+				indicate: antenna.hasUnreadNote,
+				action: () => {
+					this.antenna = antenna;
+					this.setSrc('antenna');
+				}
+			}));
+			const listItems = lists.map(list => ({
+				text: list.name,
+				icon: faListUl,
+				action: () => {
+					this.list = list;
+					this.setSrc('list');
+				}
+			}));
+			this.$root.menu({
+				items: [{
+					text: this.$t('_timelines.home'),
+					icon: faHome,
+					action: () => { this.setSrc('home') }
+				}, {
+					text: this.$t('_timelines.local'),
+					icon: faComments,
+					action: () => { this.setSrc('local') }
+				}, {
+					text: this.$t('_timelines.social'),
+					icon: faShareAlt,
+					action: () => { this.setSrc('social') }
+				}, {
+					text: this.$t('_timelines.global'),
+					icon: faGlobe,
+					action: () => { this.setSrc('global') }
+				}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems],
+				fixed: true,
+				noCenter: true,
+				source: ev.currentTarget || ev.target
+			}).then(() => {
+				this.menuOpened = false;
+			});
+		},
+
+		setSrc(src) {
+			this.src = src;
+		},
+
+		saveSrc() {
+			this.$store.commit('device/setTl', {
+				src: this.src,
+				arg: this.src == 'list' ? this.list : this.antenna
+			});
+		},
+
+		focus() {
+			(this.$refs.tl as any).focus();
+		}
+	}
+});
+</script>
+
+<style lang="scss">
+@keyframes blink {
+	0% { opacity: 1; }
+	30% { opacity: 1; }
+	90% { opacity: 0; }
+}
+
+._kjvfvyph_ {
+	position: relative;
+	height: 100%;
+	padding: 0 16px;
+	font-weight: bold;
+
+	> i {
+		position: absolute;
+		top: 16px;
+		right: 8px;
+		color: var(--accent);
+		font-size: 12px;
+		animation: blink 1s infinite;
+	}
+}
+</style>
diff --git a/src/client/app/mobile/views/pages/index.vue b/src/client/pages/index.vue
similarity index 66%
rename from src/client/app/mobile/views/pages/index.vue
rename to src/client/pages/index.vue
index 5d11fc54231f1f1421b71deb37ab41e68db988a1..732d9b71cc0d568ad6157cfbf0baf89be91bfc53 100644
--- a/src/client/app/mobile/views/pages/index.vue
+++ b/src/client/pages/index.vue
@@ -4,13 +4,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import Home from './home.vue';
-import Welcome from './welcome.vue';
+import Home from './index.home.vue';
 
 export default Vue.extend({
 	components: {
 		Home,
-		Welcome
+		Welcome: () => import('./index.welcome.vue').then(m => m.default),
 	}
 });
 </script>
diff --git a/src/client/pages/index.welcome.entrance.vue b/src/client/pages/index.welcome.entrance.vue
new file mode 100644
index 0000000000000000000000000000000000000000..1b0cc7d034aeba0c84b46c1639da8db05b78f2b7
--- /dev/null
+++ b/src/client/pages/index.welcome.entrance.vue
@@ -0,0 +1,103 @@
+<template>
+<div class="rsqzvsbo">
+	<div class="_panel about">
+		<div class="banner" :style="{ backgroundImage: `url(${ banner })` }"></div>
+		<div class="body">
+			<h1 class="name" v-html="name || host"></h1>
+			<div class="desc" v-html="description || $t('introMisskey')"></div>
+			<mk-button @click="signup()" style="display: inline-block; margin-right: 16px;" primary>{{ $t('signup') }}</mk-button>
+			<mk-button @click="signin()" style="display: inline-block;">{{ $t('login') }}</mk-button>
+		</div>
+	</div>
+	<x-notes :pagination="featuredPagination"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { toUnicode } from 'punycode';
+import XSigninDialog from '../components/signin-dialog.vue';
+import XSignupDialog from '../components/signup-dialog.vue';
+import MkButton from '../components/ui/button.vue';
+import XNotes from '../components/notes.vue';
+import i18n from '../i18n';
+import { host } from '../config';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		MkButton,
+		XNotes,
+	},
+
+	data() {
+		return {
+			featuredPagination: {
+				endpoint: 'notes/featured',
+				limit: 10,
+				noPaging: true,
+			},
+			host: toUnicode(host),
+			meta: null,
+			name: null,
+			description: null,
+			banner: null,
+			announcements: [],
+		};
+	},
+
+	created() {
+		this.$root.getMeta().then(meta => {
+			this.meta = meta;
+			this.name = meta.name;
+			this.description = meta.description;
+			this.announcements = meta.announcements;
+			this.banner = meta.bannerUrl;
+		});
+
+		this.$root.api('stats').then(stats => {
+			this.stats = stats;
+		});
+	},
+
+	methods: {
+		signin() {
+			this.$root.new(XSigninDialog, {
+				autoSet: true
+			});
+		},
+
+		signup() {
+			this.$root.new(XSignupDialog);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.rsqzvsbo {
+	> .about {
+		overflow: hidden;
+		margin-bottom: var(--margin);
+
+		> .banner {
+			height: 170px;
+			background-size: cover;
+			background-position: center center;
+		}
+
+		> .body {
+			padding: 32px;
+
+			@media (max-width: 500px) {
+				padding: 16px;
+			}
+
+			> .name {
+				margin: 0 0 0.5em 0;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/index.welcome.setup.vue b/src/client/pages/index.welcome.setup.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a339ac0a28e8e4624ff763bc694a47eadfc1ba41
--- /dev/null
+++ b/src/client/pages/index.welcome.setup.vue
@@ -0,0 +1,102 @@
+<template>
+<form class="mk-setup" @submit.prevent="submit()">
+	<h1>Welcome to Misskey!</h1>
+	<div>
+		<p>{{ $t('intro') }}</p>
+		<mk-input v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" spellcheck="false" required>
+			<span>{{ $t('username') }}</span>
+			<template #prefix>@</template>
+			<template #suffix>@{{ host }}</template>
+		</mk-input>
+		<mk-input v-model="password" type="password">
+			<span>{{ $t('password') }}</span>
+			<template #prefix><fa :icon="faLock"/></template>
+		</mk-input>
+		<footer>
+			<mk-button primary type="submit" :disabled="submitting">{{ submitting ? $t('processing') : $t('done') }}<mk-ellipsis v-if="submitting"/></mk-button>
+		</footer>
+	</div>
+</form>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faLock } from '@fortawesome/free-solid-svg-icons';
+import MkButton from '../components/ui/button.vue';
+import MkInput from '../components/ui/input.vue';
+import { host } from '../config';
+import i18n from '../i18n';
+
+export default Vue.extend({
+	i18n,
+	
+	components: {
+		MkButton,
+		MkInput,
+	},
+
+	data() {
+		return {
+			username: '',
+			password: '',
+			submitting: false,
+			host,
+			faLock
+		}
+	},
+
+	methods: {
+		submit() {
+			if (this.submitting) return;
+			this.submitting = true;
+
+			this.$root.api('admin/accounts/create', {
+				username: this.username,
+				password: this.password,
+			}).then(res => {
+				localStorage.setItem('i', res.token);
+				location.href = '/';
+			}).catch(() => {
+				this.submitting = false;
+
+				this.$root.dialog({
+					type: 'error',
+					text: this.$t('some-error')
+				});
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-setup {
+	border-radius: var(--radius);
+	box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+	overflow: hidden;
+
+	> h1 {
+		margin: 0;
+		font-size: 1.5em;
+		text-align: center;
+		padding: 32px;
+		background: var(--accent);
+		color: #fff;
+	}
+
+	> div {
+		padding: 32px;
+		background: var(--panel);
+
+		> p {
+			margin-top: 0;
+		}
+
+		> footer {
+			> * {
+				margin: 0 auto;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/index.welcome.vue b/src/client/pages/index.welcome.vue
new file mode 100644
index 0000000000000000000000000000000000000000..213c3db22c238e592253a9ac87e40c276a88f698
--- /dev/null
+++ b/src/client/pages/index.welcome.vue
@@ -0,0 +1,34 @@
+<template>
+<div v-if="meta" class="mk-welcome">
+	<portal to="title">{{ instanceName }}</portal>
+	<x-setup v-if="meta.requireSetup"/>
+	<x-entrance v-else/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XSetup from './index.welcome.setup.vue';
+import XEntrance from './index.welcome.entrance.vue';
+import { getInstanceName } from '../scripts/get-instance-name';
+
+export default Vue.extend({
+	components: {
+		XSetup,
+		XEntrance,
+	},
+
+	data() {
+		return {
+			meta: null,
+			instanceName: getInstanceName(),
+		}
+	},
+
+	created() {
+		this.$root.getMeta().then(meta => {
+			this.meta = meta;
+		});
+	}
+});
+</script>
diff --git a/src/client/pages/instance/announcements.vue b/src/client/pages/instance/announcements.vue
new file mode 100644
index 0000000000000000000000000000000000000000..71cec64c7b13334e0d0c8fc1dbc46bc445a3bd15
--- /dev/null
+++ b/src/client/pages/instance/announcements.vue
@@ -0,0 +1,129 @@
+<template>
+<div class="ztgjmzrw">
+	<portal to="icon"><fa :icon="faBroadcastTower"/></portal>
+	<portal to="title">{{ $t('announcements') }}</portal>
+	<mk-button @click="add()" primary style="margin: 0 auto 16px auto;"><fa :icon="faPlus"/> {{ $t('add') }}</mk-button>
+	<section class="_section announcements">
+		<div class="_content announcement" v-for="announcement in announcements">
+			<mk-input v-model="announcement.title" style="margin-top: 8px;">
+				<span>{{ $t('title') }}</span>
+			</mk-input>
+			<mk-textarea v-model="announcement.text">
+				<span>{{ $t('text') }}</span>
+			</mk-textarea>
+			<mk-input v-model="announcement.imageUrl">
+				<span>{{ $t('imageUrl') }}</span>
+			</mk-input>
+			<p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p>
+			<div class="buttons">
+				<mk-button class="button" inline @click="save(announcement)" primary><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+				<mk-button class="button" inline @click="remove(announcement)"><fa :icon="faTrashAlt"/> {{ $t('remove') }}</mk-button>
+			</div>
+		</div>
+	</section>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faBroadcastTower, faPlus } from '@fortawesome/free-solid-svg-icons';
+import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
+import i18n from '../../i18n';
+import MkButton from '../../components/ui/button.vue';
+import MkInput from '../../components/ui/input.vue';
+import MkTextarea from '../../components/ui/textarea.vue';
+
+export default Vue.extend({
+	i18n,
+
+	metaInfo() {
+		return {
+			title: this.$t('announcements') as string
+		};
+	},
+
+	components: {
+		MkButton,
+		MkInput,
+		MkTextarea,
+	},
+
+	data() {
+		return {
+			announcements: [],
+			faBroadcastTower, faSave, faTrashAlt, faPlus
+		}
+	},
+
+	created() {
+		this.$root.api('admin/announcements/list').then(announcements => {
+			this.announcements = announcements;
+		});
+	},
+
+	methods: {
+		add() {
+			this.announcements.unshift({
+				id: null,
+				title: '',
+				text: '',
+				imageUrl: null
+			});
+		},
+
+		remove(announcement) {
+			this.$root.dialog({
+				type: 'warning',
+				text: this.$t('removeAreYouSure', { x: announcement.title }),
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+				this.announcements = this.announcements.filter(x => x != announcement);
+				this.$root.api('admin/announcements/delete', announcement);
+			});
+		},
+
+		save(announcement) {
+			if (announcement.id == null) {
+				this.$root.api('admin/announcements/create', announcement).then(() => {
+					this.$root.dialog({
+						type: 'success',
+						text: this.$t('saved')
+					});
+				}).catch(e => {
+					this.$root.dialog({
+						type: 'error',
+						text: e
+					});
+				});
+			} else {
+				this.$root.api('admin/announcements/update', announcement).then(() => {
+					this.$root.dialog({
+						type: 'success',
+						text: this.$t('saved')
+					});
+				}).catch(e => {
+					this.$root.dialog({
+						type: 'error',
+						text: e
+					});
+				});
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ztgjmzrw {
+	> .announcements {
+		> .announcement {
+			> .buttons {
+				> .button:first-child {
+					margin-right: 8px;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/instance/emojis.vue b/src/client/pages/instance/emojis.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7a69a7efe6f0f7db70d926de45e32aef53fc9683
--- /dev/null
+++ b/src/client/pages/instance/emojis.vue
@@ -0,0 +1,253 @@
+<template>
+<div class="mk-instance-emojis">
+	<portal to="icon"><fa :icon="faLaugh"/></portal>
+	<portal to="title">{{ $t('customEmojis') }}</portal>
+	<section class="_section local">
+		<div class="_title"><fa :icon="faLaugh"/> {{ $t('customEmojis') }}</div>
+		<div class="_content">
+			<input ref="file" type="file" style="display: none;" @change="onChangeFile"/>
+			<mk-pagination :pagination="pagination" class="emojis" ref="emojis">
+				<template #empty><span>{{ $t('noCustomEmojis') }}</span></template>
+				<template #default="{items}">
+					<div class="emoji" v-for="(emoji, i) in items" :key="emoji.id" :data-index="i" @click="selected = emoji" :class="{ selected: selected && (selected.id === emoji.id) }">
+						<img :src="emoji.url" class="img" :alt="emoji.name"/>
+						<div class="body">
+							<span class="name">{{ emoji.name }}</span>
+						</div>
+					</div>
+				</template>
+			</mk-pagination>
+		</div>
+		<div class="_footer">
+			<mk-button inline primary @click="add()"><fa :icon="faPlus"/> {{ $t('addEmoji') }}</mk-button>
+			<mk-button inline :disabled="selected == null" @click="del()"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</mk-button>
+		</div>
+	</section>
+	<section class="_section remote">
+		<div class="_title"><fa :icon="faLaugh"/> {{ $t('customEmojisOfRemote') }}</div>
+		<div class="_content">
+			<mk-input v-model="host" :debounce="true" style="margin-top: 0;"><span>{{ $t('host') }}</span></mk-input>
+			<mk-pagination :pagination="remotePagination" class="emojis" ref="remoteEmojis">
+				<template #empty><span>{{ $t('noCustomEmojis') }}</span></template>
+				<template #default="{items}">
+					<div class="emoji" v-for="(emoji, i) in items" :key="emoji.id" :data-index="i" @click="selectedRemote = emoji" :class="{ selected: selectedRemote && (selectedRemote.id === emoji.id) }">
+						<img :src="emoji.url" class="img" :alt="emoji.name"/>
+						<div class="body">
+							<span class="name">{{ emoji.name }}</span>
+							<span class="host">{{ emoji.host }}</span>
+						</div>
+					</div>
+				</template>
+			</mk-pagination>
+		</div>
+		<div class="_footer">
+			<mk-button inline primary :disabled="selectedRemote == null" @click="im()"><fa :icon="faPlus"/> {{ $t('import') }}</mk-button>
+		</div>
+	</section>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faPlus } from '@fortawesome/free-solid-svg-icons';
+import { faTrashAlt, faLaugh } from '@fortawesome/free-regular-svg-icons';
+import MkButton from '../../components/ui/button.vue';
+import MkInput from '../../components/ui/input.vue';
+import MkPagination from '../../components/ui/pagination.vue';
+import { apiUrl } from '../../config';
+
+export default Vue.extend({
+	metaInfo() {
+		return {
+			title: `${this.$t('customEmojis')} | ${this.$t('instance')}`
+		};
+	},
+
+	components: {
+		MkButton,
+		MkInput,
+		MkPagination,
+	},
+
+	data() {
+		return {
+			name: null,
+			selected: null,
+			selectedRemote: null,
+			host: '',
+			pagination: {
+				endpoint: 'admin/emoji/list',
+				limit: 10,
+			},
+			remotePagination: {
+				endpoint: 'admin/emoji/list-remote',
+				limit: 10,
+				params: () => ({
+					host: this.host ? this.host : null
+				})
+			},
+			faTrashAlt, faPlus, faLaugh
+		}
+	},
+
+	watch: {
+		host() {
+			this.$refs.remoteEmojis.reload();
+		}
+	},
+
+	methods: {
+		async add() {
+			const { canceled: canceled, result: name } = await this.$root.dialog({
+				title: this.$t('emojiName'),
+				input: true
+			});
+			if (canceled) return;
+
+			this.name = name;
+
+			(this.$refs.file as any).click();
+		},
+
+		onChangeFile() {
+			const [file] = Array.from((this.$refs.file as any).files);
+			if (file == null) return;
+			
+			const data = new FormData();
+			data.append('file', file);
+			data.append('name', this.name);
+			data.append('i', this.$store.state.i.token);
+
+			const dialog = this.$root.dialog({
+				type: 'waiting',
+				text: this.$t('uploading') + '...',
+				showOkButton: false,
+				showCancelButton: false,
+				cancelableByBgClick: false
+			});
+
+			fetch(apiUrl + '/admin/emoji/add', {
+				method: 'POST',
+				body: data
+			})
+			.then(response => response.json())
+			.then(f => {
+				this.$refs.emojis.reload();
+				this.$root.dialog({
+					type: 'success',
+					iconOnly: true, autoClose: true
+				});
+			})
+			.finally(() => {
+				dialog.close();
+			});
+		},
+
+		async del() {
+			const { canceled } = await this.$root.dialog({
+				type: 'warning',
+				text: this.$t('removeAreYouSure', { x: this.selected.name }),
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			this.$root.api('admin/emoji/remove', {
+				id: this.selected.id
+			}).then(() => {
+				this.$refs.emojis.reload();
+			});
+		},
+
+		im() {
+			this.$root.api('admin/emoji/copy', {
+				emojiId: this.selectedRemote.id,
+			}).then(() => {
+				this.$refs.emojis.reload();
+				this.$root.dialog({
+					type: 'success',
+					iconOnly: true, autoClose: true
+				});
+			}).catch(e => {
+				this.$root.dialog({
+					type: 'error',
+					text: e
+				});
+			});
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-instance-emojis {
+	> .local {
+		> ._content {
+			max-height: 300px;
+			overflow: auto;
+			
+			> .emojis {
+				> .emoji {
+					display: flex;
+					align-items: center;
+
+					&.selected {
+						background: var(--accent);
+						box-shadow: 0 0 0 8px var(--accent);
+						color: #fff;
+					}
+
+					> .img {
+						width: 50px;
+						height: 50px;
+					}
+
+					> .body {
+						padding: 8px;
+
+						> .name {
+							display: block;
+						}
+					}
+				}
+			}
+		}
+	}
+
+	> .remote {
+		> ._content {
+			max-height: 300px;
+			overflow: auto;
+			
+			> .emojis {
+				> .emoji {
+					display: flex;
+					align-items: center;
+
+					&.selected {
+						background: var(--accent);
+						box-shadow: 0 0 0 8px var(--accent);
+						color: #fff;
+					}
+
+					> .img {
+						width: 32px;
+						height: 32px;
+					}
+
+					> .body {
+						padding: 0 8px;
+
+						> .name {
+							display: block;
+						}
+
+						> .host {
+							opacity: 0.5;
+						}
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/instance/federation.instance.vue b/src/client/pages/instance/federation.instance.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a27556064a84dd6f6d36866043af078d2b0cd202
--- /dev/null
+++ b/src/client/pages/instance/federation.instance.vue
@@ -0,0 +1,576 @@
+<template>
+<x-window @closed="() => { $emit('closed'); destroyDom(); }" :no-padding="true">
+	<template #header>{{ instance.host }}</template>
+	<div class="mk-instance-info">
+		<div class="table info">
+			<div class="row">
+				<div class="cell">
+					<div class="label">{{ $t('software') }}</div>
+					<div class="data">{{ instance.softwareName || '?' }}</div>
+				</div>
+				<div class="cell">
+					<div class="label">{{ $t('version') }}</div>
+					<div class="data">{{ instance.softwareVersion || '?' }}</div>
+				</div>
+			</div>
+		</div>
+		<div class="table data">
+			<div class="row">
+				<div class="cell">
+					<div class="label"><fa :icon="faCrosshairs" fixed-width class="icon"/>{{ $t('registeredAt') }}</div>
+					<div class="data">{{ new Date(instance.caughtAt).toLocaleString() }} (<mk-time :time="instance.caughtAt"/>)</div>
+				</div>
+			</div>
+			<div class="row">
+				<div class="cell">
+					<div class="label"><fa :icon="faCloudDownloadAlt" fixed-width class="icon"/>{{ $t('following') }}</div>
+					<div class="data clickable" @click="showFollowing()">{{ instance.followingCount | number }}</div>
+				</div>
+				<div class="cell">
+					<div class="label"><fa :icon="faCloudUploadAlt" fixed-width class="icon"/>{{ $t('followers') }}</div>
+					<div class="data clickable" @click="showFollowers()">{{ instance.followersCount | number }}</div>
+				</div>
+			</div>
+			<div class="row">
+				<div class="cell">
+					<div class="label"><fa :icon="faUsers" fixed-width class="icon"/>{{ $t('users') }}</div>
+					<div class="data clickable" @click="showUsers()">{{ instance.usersCount | number }}</div>
+				</div>
+				<div class="cell">
+					<div class="label"><fa :icon="faPencilAlt" fixed-width class="icon"/>{{ $t('notes') }}</div>
+					<div class="data">{{ instance.notesCount | number }}</div>
+				</div>
+			</div>
+			<div class="row">
+				<div class="cell">
+					<div class="label"><fa :icon="faFileImage" fixed-width class="icon"/>{{ $t('files') }}</div>
+					<div class="data">{{ instance.driveFiles | number }}</div>
+				</div>
+				<div class="cell">
+					<div class="label"><fa :icon="faDatabase" fixed-width class="icon"/>{{ $t('storageUsage') }}</div>
+					<div class="data">{{ instance.driveUsage | bytes }}</div>
+				</div>
+			</div>
+			<div class="row">
+				<div class="cell">
+					<div class="label"><fa :icon="faLongArrowAltUp" fixed-width class="icon"/>{{ $t('latestRequestSentAt') }}</div>
+					<div class="data"><mk-time v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
+				</div>
+				<div class="cell">
+					<div class="label"><fa :icon="faTrafficLight" fixed-width class="icon"/>{{ $t('latestStatus') }}</div>
+					<div class="data">{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</div>
+				</div>
+			</div>
+			<div class="row">
+				<div class="cell">
+					<div class="label"><fa :icon="faLongArrowAltDown" fixed-width class="icon"/>{{ $t('latestRequestReceivedAt') }}</div>
+					<div class="data"><mk-time v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
+				</div>
+			</div>
+		</div>
+		<div class="chart">
+			<div class="header">
+				<span class="label">{{ $t('charts') }}</span>
+				<div class="selects">
+					<mk-select v-model="chartSrc" style="margin: 0; flex: 1;">
+						<option value="requests">{{ $t('_instanceCharts.requests') }}</option>
+						<option value="users">{{ $t('_instanceCharts.users') }}</option>
+						<option value="users-total">{{ $t('_instanceCharts.usersTotal') }}</option>
+						<option value="notes">{{ $t('_instanceCharts.notes') }}</option>
+						<option value="notes-total">{{ $t('_instanceCharts.notesTotal') }}</option>
+						<option value="ff">{{ $t('_instanceCharts.ff') }}</option>
+						<option value="ff-total">{{ $t('_instanceCharts.ffTotal') }}</option>
+						<option value="drive-usage">{{ $t('_instanceCharts.cacheSize') }}</option>
+						<option value="drive-usage-total">{{ $t('_instanceCharts.cacheSizeTotal') }}</option>
+						<option value="drive-files">{{ $t('_instanceCharts.files') }}</option>
+						<option value="drive-files-total">{{ $t('_instanceCharts.filesTotal') }}</option>
+					</mk-select>
+					<mk-select v-model="chartSpan" style="margin: 0;">
+						<option value="hour">{{ $t('perHour') }}</option>
+						<option value="day">{{ $t('perDay') }}</option>
+					</mk-select>
+				</div>
+			</div>
+			<div class="chart">
+				<canvas ref="chart"></canvas>
+			</div>
+		</div>
+		<div class="operations">
+			<span class="label">{{ $t('operations') }}</span>
+			<mk-switch v-model="isSuspended" class="switch">{{ $t('stopActivityDelivery') }}</mk-switch>
+			<mk-switch v-model="isBlocked" class="switch">{{ $t('blockThisInstance') }}</mk-switch>
+		</div>
+		<details class="metadata">
+			<summary class="label">{{ $t('metadata') }}</summary>
+			<pre><code>{{ JSON.stringify(instance.metadata, null, 2) }}</code></pre>
+		</details>
+	</div>
+</x-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Chart from 'chart.js';
+import i18n from '../../i18n';
+import { faTimes, faCrosshairs, faCloudDownloadAlt, faCloudUploadAlt, faUsers, faPencilAlt, faFileImage, faDatabase, faTrafficLight, faLongArrowAltUp, faLongArrowAltDown } from '@fortawesome/free-solid-svg-icons';
+import XWindow from '../../components/window.vue';
+import MkUsersDialog from '../../components/users-dialog.vue';
+import MkSelect from '../../components/ui/select.vue';
+import MkSwitch from '../../components/ui/switch.vue';
+
+const chartLimit = 90;
+const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
+const negate = arr => arr.map(x => -x);
+const alpha = hex => {
+	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
+	const r = parseInt(result[1], 16);
+	const g = parseInt(result[2], 16);
+	const b = parseInt(result[3], 16);
+	return `rgba(${r}, ${g}, ${b}, 0.1)`;
+};
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XWindow,
+		MkSelect,
+		MkSwitch,
+	},
+
+	props: {
+		instance: {
+			type: Object,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			meta: null,
+			isSuspended: false,
+			isBlocked: false,
+			now: null,
+			chart: null,
+			chartInstance: null,
+			chartSrc: 'requests',
+			chartSpan: 'hour',
+			faTimes, faCrosshairs, faCloudDownloadAlt, faCloudUploadAlt, faUsers, faPencilAlt, faFileImage, faDatabase, faTrafficLight, faLongArrowAltUp, faLongArrowAltDown
+		};
+	},
+
+	computed: {
+		data(): any {
+			if (this.chart == null) return null;
+			switch (this.chartSrc) {
+				case 'requests': return this.requestsChart();
+				case 'users': return this.usersChart(false);
+				case 'users-total': return this.usersChart(true);
+				case 'notes': return this.notesChart(false);
+				case 'notes-total': return this.notesChart(true);
+				case 'ff': return this.ffChart(false);
+				case 'ff-total': return this.ffChart(true);
+				case 'drive-usage': return this.driveUsageChart(false);
+				case 'drive-usage-total': return this.driveUsageChart(true);
+				case 'drive-files': return this.driveFilesChart(false);
+				case 'drive-files-total': return this.driveFilesChart(true);
+			}
+		},
+
+		stats(): any[] {
+			const stats =
+				this.chartSpan == 'day' ? this.chart.perDay :
+				this.chartSpan == 'hour' ? this.chart.perHour :
+				null;
+
+			return stats;
+		}
+	},
+
+	watch: {
+		isSuspended() {
+			this.$root.api('admin/federation/update-instance', {
+				host: this.instance.host,
+				isSuspended: this.isSuspended
+			});
+		},
+
+		isBlocked() {
+			this.$root.api('admin/update-meta', {
+				blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
+			});
+		},
+
+		chartSrc() {
+			this.renderChart();
+		},
+
+		chartSpan() {
+			this.renderChart();
+		}
+	},
+
+	async created() {
+		this.$root.getMeta().then(meta => {
+			this.meta = meta;
+			this.isSuspended = this.instance.isSuspended;
+			this.isBlocked = this.meta.blockedHosts.includes(this.instance.host);
+		});
+	
+		this.now = new Date();
+
+		const [perHour, perDay] = await Promise.all([
+			this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }),
+			this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }),
+		]);
+
+		const chart = {
+			perHour: perHour,
+			perDay: perDay
+		};
+
+		this.chart = chart;
+
+		this.renderChart();
+	},
+
+	methods: {
+		setSrc(src) {
+			this.chartSrc = src;
+		},
+
+		renderChart() {
+			if (this.chartInstance) {
+				this.chartInstance.destroy();
+			}
+
+			Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+			this.chartInstance = new Chart(this.$refs.chart, {
+				type: 'line',
+				data: {
+					labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
+					datasets: this.data.series.map(x => ({
+						label: x.name,
+						data: x.data.slice().reverse(),
+						pointRadius: 0,
+						lineTension: 0,
+						borderWidth: 2,
+						borderColor: x.color,
+						backgroundColor: alpha(x.color),
+					}))
+				},
+				options: {
+					aspectRatio: 2.5,
+					layout: {
+						padding: {
+							left: 16,
+							right: 16,
+							top: 16,
+							bottom: 0
+						}
+					},
+					legend: {
+						position: 'bottom',
+						labels: {
+							boxWidth: 16,
+						}
+					},
+					scales: {
+						xAxes: [{
+							gridLines: {
+								display: false
+							},
+							ticks: {
+								display: false
+							}
+						}],
+						yAxes: [{
+							position: 'right',
+							ticks: {
+								display: false
+							}
+						}]
+					},
+					tooltips: {
+						intersect: false,
+						mode: 'index',
+					}
+				}
+			});
+		},
+
+		getDate(ago: number) {
+			const y = this.now.getFullYear();
+			const m = this.now.getMonth();
+			const d = this.now.getDate();
+			const h = this.now.getHours();
+
+			return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
+		},
+
+		format(arr) {
+			return arr;
+		},
+
+		requestsChart(): any {
+			return {
+				series: [{
+					name: 'In',
+					color: '#008FFB',
+					data: this.format(this.stats.requests.received)
+				}, {
+					name: 'Out (succ)',
+					color: '#00E396',
+					data: this.format(this.stats.requests.succeeded)
+				}, {
+					name: 'Out (fail)',
+					color: '#FEB019',
+					data: this.format(this.stats.requests.failed)
+				}]
+			};
+		},
+
+		usersChart(total: boolean): any {
+			return {
+				series: [{
+					name: 'Users',
+					color: '#008FFB',
+					data: this.format(total
+						? this.stats.users.total
+						: sum(this.stats.users.inc, negate(this.stats.users.dec))
+					)
+				}]
+			};
+		},
+
+		notesChart(total: boolean): any {
+			return {
+				series: [{
+					name: 'Notes',
+					color: '#008FFB',
+					data: this.format(total
+						? this.stats.notes.total
+						: sum(this.stats.notes.inc, negate(this.stats.notes.dec))
+					)
+				}]
+			};
+		},
+
+		ffChart(total: boolean): any {
+			return {
+				series: [{
+					name: 'Following',
+					color: '#008FFB',
+					data: this.format(total
+						? this.stats.following.total
+						: sum(this.stats.following.inc, negate(this.stats.following.dec))
+					)
+				}, {
+					name: 'Followers',
+					color: '#00E396',
+					data: this.format(total
+						? this.stats.followers.total
+						: sum(this.stats.followers.inc, negate(this.stats.followers.dec))
+					)
+				}]
+			};
+		},
+
+		driveUsageChart(total: boolean): any {
+			return {
+				bytes: true,
+				series: [{
+					name: 'Drive usage',
+					color: '#008FFB',
+					data: this.format(total
+						? this.stats.drive.totalUsage
+						: sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage))
+					)
+				}]
+			};
+		},
+
+		driveFilesChart(total: boolean): any {
+			return {
+				series: [{
+					name: 'Drive files',
+					color: '#008FFB',
+					data: this.format(total
+						? this.stats.drive.totalFiles
+						: sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles))
+					)
+				}]
+			};
+		},
+
+		showFollowing() {
+			this.$root.new(MkUsersDialog, {
+				title: this.$t('instanceFollowing'),
+				pagination: {
+					endpoint: 'federation/following',
+					limit: 10,
+					params: {
+						host: this.instance.host
+					}
+				},
+				extract: item => item.follower
+			});
+		},
+
+		showFollowers() {
+			this.$root.new(MkUsersDialog, {
+				title: this.$t('instanceFollowers'),
+				pagination: {
+					endpoint: 'federation/followers',
+					limit: 10,
+					params: {
+						host: this.instance.host
+					}
+				},
+				extract: item => item.followee
+			});
+		},
+
+		showUsers() {
+			this.$root.new(MkUsersDialog, {
+				title: this.$t('instanceUsers'),
+				pagination: {
+					endpoint: 'federation/users',
+					limit: 10,
+					params: {
+						host: this.instance.host
+					}
+				}
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-instance-info {
+	overflow: auto;
+
+	> .table {
+		padding: 0 32px;
+
+		@media (max-width: 500px) {
+			padding: 0 16px;
+		}
+
+		> .row {
+			display: flex;
+
+			&:not(:last-child) {
+				margin-bottom: 8px;
+			}
+
+			> .cell {
+				flex: 1;
+
+				> .label {
+					font-size: 80%;
+					opacity: 0.7;
+
+					> .icon {
+						margin-right: 4px;
+						display: none;
+					}
+				}
+
+				> .data.clickable {
+					color: var(--accent);
+					cursor: pointer;
+				}
+			}
+		}
+	}
+
+	> .data {
+		margin-top: 16px;
+		padding-top: 16px;
+		border-top: solid 1px var(--divider);
+
+		@media (max-width: 500px) {
+			margin-top: 8px;
+			padding-top: 8px;
+		}
+	}
+
+	> .chart {
+		margin-top: 16px;
+		padding-top: 16px;
+		border-top: solid 1px var(--divider);
+
+		@media (max-width: 500px) {
+			margin-top: 8px;
+			padding-top: 8px;
+		}
+
+		> .header {
+			padding: 0 32px;
+
+			@media (max-width: 500px) {
+				padding: 0 16px;
+			}
+
+			> .label {
+				font-size: 80%;
+				opacity: 0.7;
+			}
+
+			> .selects {
+				display: flex;
+			}
+		}
+
+		> .chart {
+			padding: 0 16px;
+
+			@media (max-width: 500px) {
+				padding: 0;
+			}
+		}
+	}
+
+	> .operations {
+		padding: 16px 32px 16px 32px;
+		margin-top: 8px;
+		border-top: solid 1px var(--divider);
+
+		@media (max-width: 500px) {
+			padding: 8px 16px 8px 16px;
+			margin-top: 0;
+		}
+
+		> .label {
+			font-size: 80%;
+			opacity: 0.7;
+		}
+
+		> .switch {
+			margin: 16px 0;
+		}
+	}
+
+	> .metadata {
+		padding: 16px 32px 16px 32px;
+		border-top: solid 1px var(--divider);
+
+		@media (max-width: 500px) {
+			padding: 8px 16px 8px 16px;
+		}
+
+		> .label {
+			font-size: 80%;
+			opacity: 0.7;
+		}
+
+		> pre > code {
+			display: block;
+			max-height: 200px;
+			overflow: auto;
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/instance/federation.vue b/src/client/pages/instance/federation.vue
new file mode 100644
index 0000000000000000000000000000000000000000..224ff72a9f7cecdef027da39fc556e1463e534e3
--- /dev/null
+++ b/src/client/pages/instance/federation.vue
@@ -0,0 +1,165 @@
+<template>
+<div class="mk-federation">
+	<section class="_section instances">
+		<div class="_title"><fa :icon="faGlobe"/> {{ $t('instances') }}</div>
+		<div class="_content">
+			<div class="inputs" style="display: flex;">
+				<mk-input v-model="host" :debounce="true" style="margin: 0; flex: 1;"><span>{{ $t('host') }}</span></mk-input>
+				<mk-select v-model="state" style="margin: 0;">
+					<option value="all">{{ $t('all') }}</option>
+					<option value="federating">{{ $t('federating') }}</option>
+					<option value="subscribing">{{ $t('subscribing') }}</option>
+					<option value="publishing">{{ $t('publishing') }}</option>
+					<option value="suspended">{{ $t('suspended') }}</option>
+					<option value="blocked">{{ $t('blocked') }}</option>
+					<option value="notResponding">{{ $t('notResponding') }}</option>
+				</mk-select>
+			</div>
+		</div>
+		<div class="_content">
+			<mk-pagination :pagination="pagination" #default="{items}" class="instances" ref="instances" :key="host + state">
+				<div class="instance" v-for="(instance, i) in items" :key="instance.id" :data-index="i" @click="info(instance)">
+					<div class="host"><fa :icon="faCircle" class="indicator" :class="getStatus(instance)"/><b>{{ instance.host }}</b></div>
+					<div class="status">
+						<span class="sub" v-if="instance.followersCount > 0"><fa :icon="faCaretDown" class="icon"/>Sub</span>
+						<span class="sub" v-else><fa :icon="faCaretDown" class="icon"/>-</span>
+						<span class="pub" v-if="instance.followingCount > 0"><fa :icon="faCaretUp" class="icon"/>Pub</span>
+						<span class="pub" v-else><fa :icon="faCaretUp" class="icon"/>-</span>
+						<span class="lastCommunicatedAt"><fa :icon="faExchangeAlt" class="icon"/><mk-time :time="instance.lastCommunicatedAt"/></span>
+						<span class="latestStatus"><fa :icon="faTrafficLight" class="icon"/>{{ instance.latestStatus || '-' }}</span>
+					</div>
+				</div>
+			</mk-pagination>
+		</div>
+	</section>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faGlobe, faCircle, faExchangeAlt, faCaretDown, faCaretUp, faTrafficLight } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../../i18n';
+import MkButton from '../../components/ui/button.vue';
+import MkInput from '../../components/ui/input.vue';
+import MkSelect from '../../components/ui/select.vue';
+import MkPagination from '../../components/ui/pagination.vue';
+import MkInstanceInfo from './federation.instance.vue';
+
+export default Vue.extend({
+	i18n,
+
+	metaInfo() {
+		return {
+			title: this.$t('federation') as string
+		};
+	},
+
+	components: {
+		MkButton,
+		MkInput,
+		MkSelect,
+		MkPagination,
+	},
+
+	data() {
+		return {
+			host: '',
+			state: 'federating',
+			sort: '+pubSub',
+			pagination: {
+				endpoint: 'federation/instances',
+				limit: 10,
+				offsetMode: true,
+				params: () => ({
+					sort: this.sort,
+					host: this.host != '' ? this.host : null,
+					...(
+						this.state === 'federating' ? { federating: true } :
+						this.state === 'subscribing' ? { subscribing: true } :
+						this.state === 'publishing' ? { publishing: true } :
+						this.state === 'suspended' ? { suspended: true } :
+						this.state === 'blocked' ? { blocked: true } :
+						this.state === 'notResponding' ? { notResponding: true } :
+						{})
+				})
+			},
+			faGlobe, faCircle, faExchangeAlt, faCaretDown, faCaretUp, faTrafficLight
+		}
+	},
+
+	watch: {
+		host() {
+			this.$refs.instances.reload();
+		},
+		state() {
+			this.$refs.instances.reload();
+		}
+	},
+
+	methods: {
+		getStatus(instance) {
+			if (instance.isSuspended) return 'off';
+			if (instance.isNotResponding) return 'red';
+			return 'green';
+		},
+
+		info(instance) {
+			this.$root.new(MkInstanceInfo, {
+				instance: instance
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-federation {
+	> .instances {
+		> ._content {
+			> .instances {
+				> .instance {
+					cursor: pointer;
+
+					> .host {
+						> .indicator {
+							font-size: 70%;
+							vertical-align: baseline;
+							margin-right: 4px;
+
+							&.green {
+								color: #49c5ba;
+							}
+
+							&.yellow {
+								color: #c5a549;
+							}
+
+							&.red {
+								color: #c54949;
+							}
+
+							&.off {
+								color: rgba(0, 0, 0, 0.5);
+							}
+						}
+					}
+
+					> .status {
+						display: flex;
+						align-items: center;
+						font-size: 90%;
+
+						> span {
+							flex: 1;
+							
+							> .icon {
+								margin-right: 6px;
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/instance/files.vue b/src/client/pages/instance/files.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e7475e94c18207e1c4496e3a20f44637d8d0497f
--- /dev/null
+++ b/src/client/pages/instance/files.vue
@@ -0,0 +1,54 @@
+<template>
+<section class="_section">
+	<div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div>
+	<div class="_content">
+		<mk-button primary @click="clear()"><fa :icon="faTrashAlt"/> {{ $t('clearCachedFiles') }}</mk-button>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faCloud } from '@fortawesome/free-solid-svg-icons';
+import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
+import MkButton from '../../components/ui/button.vue';
+import MkPagination from '../../components/ui/pagination.vue';
+
+export default Vue.extend({
+	metaInfo() {
+		return {
+			title: `${this.$t('files')} | ${this.$t('instance')}`
+		};
+	},
+
+	components: {
+		MkButton,
+		MkPagination,
+	},
+
+	data() {
+		return {
+			faTrashAlt, faCloud
+		}
+	},
+
+	methods: {
+		clear() {
+			this.$root.dialog({
+				type: 'warning',
+				text: this.$t('clearCachedFilesConfirm'),
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+
+				this.$root.api('admin/drive/clean-remote-files', {}).then(() => {
+					this.$root.dialog({
+						type: 'success',
+						iconOnly: true, autoClose: true
+					});
+				});
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue
new file mode 100644
index 0000000000000000000000000000000000000000..5301fc7e01f6762b8242cecefff89e22cc72f911
--- /dev/null
+++ b/src/client/pages/instance/index.vue
@@ -0,0 +1,393 @@
+<template>
+<div v-if="meta" class="mk-instance-page">
+	<portal to="icon"><fa :icon="faServer"/></portal>
+	<portal to="title">{{ $t('instance') }}</portal>
+
+	<section class="_section info">
+		<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div>
+		<div class="_content">
+			<mk-input v-model="name" style="margin-top: 8px;">{{ $t('instanceName') }}</mk-input>
+			<mk-textarea v-model="description">{{ $t('instanceDescription') }}</mk-textarea>
+			<mk-input v-model="iconUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('iconUrl') }}</mk-input>
+			<mk-input v-model="bannerUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</mk-input>
+			<mk-input v-model="tosUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('tosUrl') }}</mk-input>
+			<mk-input v-model="maintainerName">{{ $t('maintainerName') }}</mk-input>
+			<mk-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</mk-input>
+		</div>
+		<div class="_footer">
+			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+		</div>
+	</section>
+
+	<section class="_section info">
+		<div class="_content">
+			<mk-switch v-model="enableLocalTimeline" @change="save()">{{ $t('enableLocalTimeline') }}</mk-switch>
+			<mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch>
+			<mk-info>{{ $t('disablingTimelinesInfo') }}</mk-info>
+		</div>
+	</section>
+
+	<section class="_section info">
+		<div class="_title"><fa :icon="faUser"/> {{ $t('registration') }}</div>
+		<div class="_content">
+			<mk-switch v-model="enableRegistration" @change="save()">{{ $t('enableRegistration') }}</mk-switch>
+			<mk-button v-if="!enableRegistration" @click="invite">{{ $t('invite') }}</mk-button>
+		</div>
+	</section>
+
+	<section class="_section">
+		<div class="_title"><fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div>
+		<div class="_content">
+			<mk-switch v-model="enableRecaptcha">{{ $t('enableRecaptcha') }}</mk-switch>
+			<template v-if="enableRecaptcha">
+				<mk-info>{{ $t('recaptcha-info') }}</mk-info>
+				<mk-info warn>{{ $t('recaptcha-info2') }}</mk-info>
+				<mk-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSiteKey') }}</mk-input>
+				<mk-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSecretKey') }}</mk-input>
+			</template>
+		</div>
+		<div class="_content" v-if="enableRecaptcha && recaptchaSiteKey">
+			<header>{{ $t('preview') }}</header>
+			<div ref="recaptcha" style="margin: 16px 0 0 0;" :key="recaptchaSiteKey"></div>
+		</div>
+		<div class="_footer">
+			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+		</div>
+	</section>
+
+	<section class="_section">
+		<div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
+		<div class="_content">
+			<mk-switch v-model="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworker-info') }}</template></mk-switch>
+			<template v-if="enableServiceWorker">
+				<mk-info>{{ $t('vapid-info') }}<br><code>npm i web-push -g<br>web-push generate-vapid-keys</code></mk-info>
+				<mk-horizon-group inputs class="fit-bottom">
+					<mk-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>{{ $t('vapid-publickey') }}</mk-input>
+					<mk-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>{{ $t('vapid-privatekey') }}</mk-input>
+				</mk-horizon-group>
+			</template>
+		</div>
+		<div class="_footer">
+			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+		</div>
+	</section>
+
+	<section class="_section">
+		<div class="_title"><fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div>
+		<div class="_content">
+			<mk-textarea v-model="pinnedUsers" style="margin-top: 0;">
+				<template #desc>{{ $t('pinnedUsersDescription') }} <button class="_textButton" @click="addPinUser">{{ $t('addUser') }}</button></template>
+			</mk-textarea>
+		</div>
+		<div class="_footer">
+			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+		</div>
+	</section>
+
+	<section class="_section">
+		<div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div>
+		<div class="_content">
+			<mk-switch v-model="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></mk-switch>
+			<mk-switch v-model="proxyRemoteFiles">{{ $t('proxyRemoteFiles') }}<template #desc>{{ $t('proxyRemoteFilesDescription') }}</template></mk-switch>
+			<mk-input v-model="localDriveCapacityMb" type="number">{{ $t('driveCapacityPerLocalAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
+			<mk-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" style="margin-bottom: 0;">{{ $t('driveCapacityPerRemoteAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
+		</div>
+		<div class="_footer">
+			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+		</div>
+	</section>
+
+	<section class="_section">
+		<div class="_title"><fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
+		<div class="_content">
+			<mk-input v-model="proxyAccount" style="margin: 0;"><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></mk-input>
+		</div>
+		<div class="_footer">
+			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+		</div>
+	</section>
+
+	<section class="_section">
+		<div class="_title"><fa :icon="faBan"/> {{ $t('blockedInstances') }}</div>
+		<div class="_content">
+			<mk-textarea v-model="blockedHosts" style="margin-top: 0;">
+				<template #desc>{{ $t('blockedInstancesDescription') }}</template>
+			</mk-textarea>
+		</div>
+		<div class="_footer">
+			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+		</div>
+	</section>
+
+	<section class="_section">
+		<div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
+		<div class="_content">
+			<header><fa :icon="faTwitter"/> {{ $t('twitter-integration-config') }}</header>
+			<mk-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</mk-switch>
+			<template v-if="enableTwitterIntegration">
+				<mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('twitter-integration-consumer-key') }}</mk-input>
+				<mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('twitter-integration-consumer-secret') }}</mk-input>
+				<mk-info>{{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }}</mk-info>
+			</template>
+		</div>
+		<div class="_content">
+			<header><fa :icon="faGithub"/> {{ $t('github-integration-config') }}</header>
+			<mk-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</mk-switch>
+			<template v-if="enableGithubIntegration">
+				<mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('github-integration-client-id') }}</mk-input>
+				<mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('github-integration-client-secret') }}</mk-input>
+				<mk-info>{{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }}</mk-info>
+			</template>
+		</div>
+		<div class="_content">
+			<header><fa :icon="faDiscord"/> {{ $t('discord-integration-config') }}</header>
+			<mk-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</mk-switch>
+			<template v-if="enableDiscordIntegration">
+				<mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('discord-integration-client-id') }}</mk-input>
+				<mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('discord-integration-client-secret') }}</mk-input>
+				<mk-info>{{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }}</mk-info>
+			</template>
+		</div>
+		<div class="_footer">
+			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+		</div>
+	</section>
+
+	<section class="_section info">
+		<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('instanceInfo') }}</div>
+		<div class="_content table" v-if="stats">
+			<div><b>{{ $t('users') }}</b><span>{{ stats.originalUsersCount | number }}</span></div>
+			<div><b>{{ $t('notes') }}</b><span>{{ stats.originalNotesCount | number }}</span></div>
+		</div>
+		<div class="_content table">
+			<div><b>Misskey</b><span>v{{ version }}</span></div>
+		</div>
+		<div class="_content table" v-if="serverInfo">
+			<div><b>Node.js</b><span>{{ serverInfo.node }}</span></div>
+			<div><b>PostgreSQL</b><span>v{{ serverInfo.psql }}</span></div>
+			<div><b>Redis</b><span>v{{ serverInfo.redis }}</span></div>
+		</div>
+	</section>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons';
+import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons';
+import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
+import MkButton from '../../components/ui/button.vue';
+import MkInput from '../../components/ui/input.vue';
+import MkTextarea from '../../components/ui/textarea.vue';
+import MkSwitch from '../../components/ui/switch.vue';
+import MkInfo from '../../components/ui/info.vue';
+import MkUserSelect from '../../components/user-select.vue';
+import { version } from '../../config';
+import i18n from '../../i18n';
+import getAcct from '../../../misc/acct/render';
+
+export default Vue.extend({
+	i18n,
+
+	metaInfo() {
+		return {
+			title: this.$t('instance') as string
+		};
+	},
+
+	components: {
+		MkButton,
+		MkInput,
+		MkTextarea,
+		MkSwitch,
+		MkInfo,
+	},
+
+	data() {
+		return {
+			version,
+			meta: null,
+			stats: null,
+			serverInfo: null,
+			proxyAccount: null,
+			cacheRemoteFiles: false,
+			proxyRemoteFiles: false,
+			localDriveCapacityMb: 0,
+			remoteDriveCapacityMb: 0,
+			blockedHosts: '',
+			pinnedUsers: '',
+			maintainerName: null,
+			maintainerEmail: null,
+			name: null,
+			description: null,
+			tosUrl: null,
+			bannerUrl: null,
+			iconUrl: null,
+			enableRegistration: false,
+			enableLocalTimeline: false,
+			enableGlobalTimeline: false,
+			enableRecaptcha: false,
+			recaptchaSiteKey: null,
+			recaptchaSecretKey: null,
+			enableServiceWorker: false,
+			swPublicKey: null,
+			swPrivateKey: null,
+			enableTwitterIntegration: false,
+			twitterConsumerKey: null,
+			twitterConsumerSecret: null,
+			enableGithubIntegration: false,
+			githubClientId: null,
+			githubClientSecret: null,
+			enableDiscordIntegration: false,
+			discordClientId: null,
+			discordClientSecret: null,
+			faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt
+		}
+	},
+
+	created() {
+		this.$root.getMeta().then(meta => {
+			this.meta = meta;
+			this.name = this.meta.name;
+			this.description = this.meta.description;
+			this.tosUrl = this.meta.tosUrl;
+			this.bannerUrl = this.meta.bannerUrl;
+			this.iconUrl = this.meta.iconUrl;
+			this.maintainerName = this.meta.maintainerName;
+			this.maintainerEmail = this.meta.maintainerEmail;
+			this.enableRegistration = !this.meta.disableRegistration;
+			this.enableLocalTimeline = !this.meta.disableLocalTimeline;
+			this.enableGlobalTimeline = !this.meta.disableGlobalTimeline;
+			this.enableRecaptcha = this.meta.enableRecaptcha;
+			this.recaptchaSiteKey = this.meta.recaptchaSiteKey;
+			this.recaptchaSecretKey = this.meta.recaptchaSecretKey;
+			this.proxyAccount = this.meta.proxyAccount;
+			this.cacheRemoteFiles = this.meta.cacheRemoteFiles;
+			this.proxyRemoteFiles = this.meta.proxyRemoteFiles;
+			this.localDriveCapacityMb = this.meta.driveCapacityPerLocalUserMb;
+			this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
+			this.blockedHosts = this.meta.blockedHosts.join('\n');
+			this.pinnedUsers = this.meta.pinnedUsers.join('\n');
+			this.enableServiceWorker = this.meta.enableServiceWorker;
+			this.swPublicKey = this.meta.swPublickey;
+			this.swPrivateKey = this.meta.swPrivateKey;
+			this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
+			this.twitterConsumerKey = this.meta.twitterConsumerKey;
+			this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
+			this.enableGithubIntegration = this.meta.enableGithubIntegration;
+			this.githubClientId = this.meta.githubClientId;
+			this.githubClientSecret = this.meta.githubClientSecret;
+			this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
+			this.discordClientId = this.meta.discordClientId;
+			this.discordClientSecret = this.meta.discordClientSecret;
+		});
+
+		this.$root.api('admin/server-info').then(res => {
+			this.serverInfo = res;
+		});
+
+		this.$root.api('stats').then(res => {
+			this.stats = res;
+		});
+	},
+
+	mounted() {
+		const renderRecaptchaPreview = () => {
+			if (!(window as any).grecaptcha) return;
+			if (!this.$refs.recaptcha) return;
+			if (!this.recaptchaSiteKey) return;
+			(window as any).grecaptcha.render(this.$refs.recaptcha, {
+				sitekey: this.recaptchaSiteKey
+			});
+		};
+		window.onRecaotchaLoad = () => {
+			renderRecaptchaPreview();
+		};
+		const head = document.getElementsByTagName('head')[0];
+		const script = document.createElement('script');
+		script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaotchaLoad');
+		head.appendChild(script);
+		this.$watch('enableRecaptcha', () => {
+			renderRecaptchaPreview();
+		});
+		this.$watch('recaptchaSiteKey', () => {
+			renderRecaptchaPreview();
+		});
+	},
+
+	methods: {
+		addPinUser() {
+			this.$root.new(MkUserSelect, {}).$once('selected', user => {
+				this.pinnedUsers = this.pinnedUsers.trim();
+				this.pinnedUsers += '\n@' + getAcct(user);
+				this.pinnedUsers = this.pinnedUsers.trim();
+			});
+		},
+
+		save(withDialog = false) {
+			this.$root.api('admin/update-meta', {
+				name: this.name,
+				description: this.description,
+				tosUrl: this.tosUrl,
+				bannerUrl: this.bannerUrl,
+				iconUrl: this.iconUrl,
+				maintainerName: this.maintainerName,
+				maintainerEmail: this.maintainerEmail,
+				disableRegistration: !this.enableRegistration,
+				disableLocalTimeline: !this.enableLocalTimeline,
+				disableGlobalTimeline: !this.enableGlobalTimeline,
+				enableRecaptcha: this.enableRecaptcha,
+				recaptchaSiteKey: this.recaptchaSiteKey,
+				recaptchaSecretKey: this.recaptchaSecretKey,
+				proxyAccount: this.proxyAccount,
+				cacheRemoteFiles: this.cacheRemoteFiles,
+				proxyRemoteFiles: this.proxyRemoteFiles,
+				localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
+				remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
+				blockedHosts: this.blockedHosts.split('\n') || [],
+				pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [],
+				enableServiceWorker: this.enableServiceWorker,
+				swPublicKey: this.swPublicKey,
+				swPrivateKey: this.swPrivateKey,
+				enableTwitterIntegration: this.enableTwitterIntegration,
+				twitterConsumerKey: this.twitterConsumerKey,
+				twitterConsumerSecret: this.twitterConsumerSecret,
+				enableGithubIntegration: this.enableGithubIntegration,
+				githubClientId: this.githubClientId,
+				githubClientSecret: this.githubClientSecret,
+				enableDiscordIntegration: this.enableDiscordIntegration,
+				discordClientId: this.discordClientId,
+				discordClientSecret: this.discordClientSecret,
+			}).then(() => {
+				if (withDialog) {
+					this.$root.dialog({
+						type: 'success',
+						iconOnly: true, autoClose: true
+					});
+				}
+			}).catch(e => {
+				this.$root.dialog({
+					type: 'error',
+					text: e
+				});
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-instance-page {
+	> .info {
+		> .table {
+			> div {
+				display: flex;
+
+				> * {
+					flex: 1;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/instance/monitor.vue b/src/client/pages/instance/monitor.vue
new file mode 100644
index 0000000000000000000000000000000000000000..3f3ce6d73a29ef40bab66c8f6b87f5b558478560
--- /dev/null
+++ b/src/client/pages/instance/monitor.vue
@@ -0,0 +1,381 @@
+<template>
+<div class="mk-instance-monitor">
+	<section class="_section">
+		<div class="_title"><fa :icon="faMicrochip"/> {{ $t('cpuAndMemory') }}</div>
+		<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
+			<canvas ref="cpumem"></canvas>
+		</div>
+		<div class="_content" v-if="serverInfo">
+			<div class="table">
+				<div class="row">
+					<div class="cell"><div class="label">CPU</div>{{ serverInfo.cpu.model }}</div>
+				</div>
+				<div class="row">
+					<div class="cell"><div class="label">MEM total</div>{{ serverInfo.mem.total | bytes }}</div>
+					<div class="cell"><div class="label">MEM used</div>{{ memUsage | bytes }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
+					<div class="cell"><div class="label">MEM free</div>{{ serverInfo.mem.total - memUsage | bytes }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
+				</div>
+			</div>
+		</div>
+	</section>
+	<section class="_section">
+		<div class="_title"><fa :icon="faHdd"/> {{ $t('disk') }}</div>
+		<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
+			<canvas ref="disk"></canvas>
+		</div>
+		<div class="_content" v-if="serverInfo">
+			<div class="table">
+				<div class="row">
+					<div class="cell"><div class="label">Disk total</div>{{ serverInfo.fs.total | bytes }}</div>
+					<div class="cell"><div class="label">Disk used</div>{{ serverInfo.fs.used | bytes }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
+					<div class="cell"><div class="label">Disk free</div>{{ serverInfo.fs.total - serverInfo.fs.used | bytes }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
+				</div>
+			</div>
+		</div>
+	</section>
+	<section class="_section">
+		<div class="_title"><fa :icon="faExchangeAlt"/> {{ $t('network') }}</div>
+		<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
+			<canvas ref="net"></canvas>
+		</div>
+		<div class="_content" v-if="serverInfo">
+			<div class="table">
+				<div class="row">
+					<div class="cell"><div class="label">Interface</div>{{ serverInfo.net.interface }}</div>
+				</div>
+			</div>
+		</div>
+	</section>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faTachometerAlt, faExchangeAlt, faMicrochip, faHdd } from '@fortawesome/free-solid-svg-icons';
+import Chart from 'chart.js';
+import i18n from '../../i18n';
+
+const alpha = (hex, a) => {
+	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
+	const r = parseInt(result[1], 16);
+	const g = parseInt(result[2], 16);
+	const b = parseInt(result[3], 16);
+	return `rgba(${r}, ${g}, ${b}, ${a})`;
+};
+
+export default Vue.extend({
+	i18n,
+
+	metaInfo() {
+		return {
+			title: `${this.$t('monitor')} | ${this.$t('instance')}`
+		};
+	},
+
+	components: {
+	},
+
+	data() {
+		return {
+			connection: null,
+			serverInfo: null,
+			memUsage: 0,
+			chartCpuMem: null,
+			chartNet: null,
+			faTachometerAlt, faExchangeAlt, faMicrochip, faHdd
+		}
+	},
+
+	mounted() {
+		Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+
+		this.chartCpuMem = new Chart(this.$refs.cpumem, {
+			type: 'line',
+			data: {
+				labels: [],
+				datasets: [{
+					label: 'CPU',
+					pointRadius: 0,
+					lineTension: 0,
+					borderWidth: 2,
+					borderColor: '#86b300',
+					backgroundColor: alpha('#86b300', 0.1),
+					data: []
+				}, {
+					label: 'MEM (active)',
+					pointRadius: 0,
+					lineTension: 0,
+					borderWidth: 2,
+					borderColor: '#935dbf',
+					backgroundColor: alpha('#935dbf', 0.02),
+					data: []
+				}, {
+					label: 'MEM (used)',
+					pointRadius: 0,
+					lineTension: 0,
+					borderWidth: 2,
+					borderColor: '#935dbf',
+					borderDash: [5, 5],
+					fill: false,
+					data: []
+				}]
+			},
+			options: {
+				aspectRatio: 3,
+				layout: {
+					padding: {
+						left: 0,
+						right: 0,
+						top: 8,
+						bottom: 0
+					}
+				},
+				legend: {
+					position: 'bottom',
+					labels: {
+						boxWidth: 16,
+					}
+				},
+				scales: {
+					xAxes: [{
+						gridLines: {
+							display: false
+						},
+						ticks: {
+							display: false
+						}
+					}],
+					yAxes: [{
+						position: 'right',
+						ticks: {
+							display: false,
+							max: 100
+						}
+					}]
+				},
+				tooltips: {
+					intersect: false,
+					mode: 'index',
+				}
+			}
+		});
+
+		this.chartNet = new Chart(this.$refs.net, {
+			type: 'line',
+			data: {
+				labels: [],
+				datasets: [{
+					label: 'In',
+					pointRadius: 0,
+					lineTension: 0,
+					borderWidth: 2,
+					borderColor: '#94a029',
+					backgroundColor: alpha('#94a029', 0.1),
+					data: []
+				}, {
+					label: 'Out',
+					pointRadius: 0,
+					lineTension: 0,
+					borderWidth: 2,
+					borderColor: '#ff9156',
+					backgroundColor: alpha('#ff9156', 0.1),
+					data: []
+				}]
+			},
+			options: {
+				aspectRatio: 3,
+				layout: {
+					padding: {
+						left: 0,
+						right: 0,
+						top: 8,
+						bottom: 0
+					}
+				},
+				legend: {
+					position: 'bottom',
+					labels: {
+						boxWidth: 16,
+					}
+				},
+				scales: {
+					xAxes: [{
+						gridLines: {
+							display: false
+						},
+						ticks: {
+							display: false
+						}
+					}],
+					yAxes: [{
+						position: 'right',
+						ticks: {
+							display: false,
+						}
+					}]
+				},
+				tooltips: {
+					intersect: false,
+					mode: 'index',
+				}
+			}
+		});
+
+		this.chartDisk = new Chart(this.$refs.disk, {
+			type: 'line',
+			data: {
+				labels: [],
+				datasets: [{
+					label: 'Read',
+					pointRadius: 0,
+					lineTension: 0,
+					borderWidth: 2,
+					borderColor: '#94a029',
+					backgroundColor: alpha('#94a029', 0.1),
+					data: []
+				}, {
+					label: 'Write',
+					pointRadius: 0,
+					lineTension: 0,
+					borderWidth: 2,
+					borderColor: '#ff9156',
+					backgroundColor: alpha('#ff9156', 0.1),
+					data: []
+				}]
+			},
+			options: {
+				aspectRatio: 3,
+				layout: {
+					padding: {
+						left: 0,
+						right: 0,
+						top: 8,
+						bottom: 0
+					}
+				},
+				legend: {
+					position: 'bottom',
+					labels: {
+						boxWidth: 16,
+					}
+				},
+				scales: {
+					xAxes: [{
+						gridLines: {
+							display: false
+						},
+						ticks: {
+							display: false
+						}
+					}],
+					yAxes: [{
+						position: 'right',
+						ticks: {
+							display: false,
+						}
+					}]
+				},
+				tooltips: {
+					intersect: false,
+					mode: 'index',
+				}
+			}
+		});
+
+		this.$root.api('admin/server-info', {}).then(res => {
+			this.serverInfo = res;
+
+			this.connection = this.$root.stream.useSharedConnection('serverStats');
+			this.connection.on('stats', this.onStats);
+			this.connection.on('statsLog', this.onStatsLog);
+			this.connection.send('requestLog', {
+				id: Math.random().toString().substr(2, 8),
+				length: 150
+			});
+		});
+	},
+
+	beforeDestroy() {
+		this.connection.off('stats', this.onStats);
+		this.connection.off('statsLog', this.onStatsLog);
+		this.connection.dispose();
+	},
+
+	methods: {
+		onStats(stats) {
+			const cpu = (stats.cpu * 100).toFixed(0);
+			const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
+			const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0);
+			this.memUsage = stats.mem.active;
+
+			this.chartCpuMem.data.labels.push('');
+			this.chartCpuMem.data.datasets[0].data.push(cpu);
+			this.chartCpuMem.data.datasets[1].data.push(memActive);
+			this.chartCpuMem.data.datasets[2].data.push(memUsed);
+			this.chartNet.data.labels.push('');
+			this.chartNet.data.datasets[0].data.push(stats.net.rx);
+			this.chartNet.data.datasets[1].data.push(stats.net.tx);
+			this.chartDisk.data.labels.push('');
+			this.chartDisk.data.datasets[0].data.push(stats.fs.r);
+			this.chartDisk.data.datasets[1].data.push(stats.fs.w);
+			if (this.chartCpuMem.data.datasets[0].data.length > 150) {
+				this.chartCpuMem.data.labels.shift();
+				this.chartCpuMem.data.datasets[0].data.shift();
+				this.chartCpuMem.data.datasets[1].data.shift();
+				this.chartCpuMem.data.datasets[2].data.shift();
+				this.chartNet.data.labels.shift();
+				this.chartNet.data.datasets[0].data.shift();
+				this.chartNet.data.datasets[1].data.shift();
+				this.chartDisk.data.labels.shift();
+				this.chartDisk.data.datasets[0].data.shift();
+				this.chartDisk.data.datasets[1].data.shift();
+			}
+			this.chartCpuMem.update();
+			this.chartNet.update();
+			this.chartDisk.update();
+		},
+
+		onStatsLog(statsLog) {
+			for (const stats of statsLog.reverse()) {
+				this.onStats(stats);
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-instance-monitor {
+	> section {
+		> ._content {
+			> .table {
+				> .row {
+					display: flex;
+
+					&:not(:last-child) {
+						margin-bottom: 16px;
+
+						@media (max-width: 500px) {
+							margin-bottom: 8px;
+						}
+					}
+
+					> .cell {
+						flex: 1;
+
+						> .label {
+							font-size: 80%;
+							opacity: 0.7;
+
+							> .icon {
+								margin-right: 4px;
+								display: none;
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/instance/queue.queue.vue b/src/client/pages/instance/queue.queue.vue
new file mode 100644
index 0000000000000000000000000000000000000000..cc542b176f06e9a9cdc1dcc4933334b3b31b4193
--- /dev/null
+++ b/src/client/pages/instance/queue.queue.vue
@@ -0,0 +1,204 @@
+<template>
+<section class="_section mk-queue-queue">
+	<div class="_title"><slot name="title"></slot></div>
+	<div class="_content status">
+		<div class="cell"><div class="label">Process</div>{{ activeSincePrevTick | number }}</div>
+		<div class="cell"><div class="label">Active</div>{{ active | number }}</div>
+		<div class="cell"><div class="label">Waiting</div>{{ waiting | number }}</div>
+		<div class="cell"><div class="label">Delayed</div>{{ delayed | number }}</div>
+	</div>
+	<div class="_content" style="margin-bottom: -8px;">
+		<canvas ref="chart"></canvas>
+	</div>
+	<div class="_content" style="max-height: 180px; overflow: auto;">
+		<sequential-entrance :delay="15" v-if="jobs.length > 0">
+			<div v-for="(job, i) in jobs" :key="job[0]" :data-index="i">
+				<span>{{ job[0] }}</span>
+				<span style="margin-left: 8px; opacity: 0.7;">({{ job[1] | number }} jobs)</span>
+			</div>
+		</sequential-entrance>
+		<span v-else style="opacity: 0.5;">{{ $t('noJobs') }}</span>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Chart from 'chart.js';
+import i18n from '../../i18n';
+
+const alpha = (hex, a) => {
+	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
+	const r = parseInt(result[1], 16);
+	const g = parseInt(result[2], 16);
+	const b = parseInt(result[3], 16);
+	return `rgba(${r}, ${g}, ${b}, ${a})`;
+};
+
+export default Vue.extend({
+	i18n,
+
+	props: {
+		domain: {
+			required: true
+		},
+		connection: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			chart: null,
+			jobs: [],
+			activeSincePrevTick: 0,
+			active: 0,
+			waiting: 0,
+			delayed: 0,
+		}
+	},
+
+	mounted() {
+		this.fetchJobs();
+
+		Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+
+		this.chart = new Chart(this.$refs.chart, {
+			type: 'line',
+			data: {
+				labels: [],
+				datasets: [{
+					label: 'Process',
+					pointRadius: 0,
+					lineTension: 0,
+					borderWidth: 2,
+					borderColor: '#00E396',
+					backgroundColor: alpha('#00E396', 0.1),
+					data: []
+				}, {
+					label: 'Active',
+					pointRadius: 0,
+					lineTension: 0,
+					borderWidth: 2,
+					borderColor: '#00BCD4',
+					backgroundColor: alpha('#00BCD4', 0.1),
+					data: []
+				}, {
+					label: 'Waiting',
+					pointRadius: 0,
+					lineTension: 0,
+					borderWidth: 2,
+					borderColor: '#FFB300',
+					backgroundColor: alpha('#FFB300', 0.1),
+					data: []
+				}, {
+					label: 'Delayed',
+					pointRadius: 0,
+					lineTension: 0,
+					borderWidth: 2,
+					borderColor: '#E53935',
+					borderDash: [5, 5],
+					fill: false,
+					data: []
+				}]
+			},
+			options: {
+				aspectRatio: 3,
+				layout: {
+					padding: {
+						left: 0,
+						right: 0,
+						top: 8,
+						bottom: 0
+					}
+				},
+				legend: {
+					position: 'bottom',
+					labels: {
+						boxWidth: 16,
+					}
+				},
+				scales: {
+					xAxes: [{
+						gridLines: {
+							display: false
+						},
+						ticks: {
+							display: false
+						}
+					}],
+					yAxes: [{
+						position: 'right',
+						ticks: {
+							display: false,
+						}
+					}]
+				},
+				tooltips: {
+					intersect: false,
+					mode: 'index',
+				}
+			}
+		});
+
+		this.connection.on('stats', this.onStats);
+		this.connection.on('statsLog', this.onStatsLog);
+	},
+
+	beforeDestroy() {
+		this.connection.off('stats', this.onStats);
+		this.connection.off('statsLog', this.onStatsLog);
+	},
+
+	methods: {
+		onStats(stats) {
+			this.activeSincePrevTick = stats[this.domain].activeSincePrevTick;
+			this.active = stats[this.domain].active;
+			this.waiting = stats[this.domain].waiting;
+			this.delayed = stats[this.domain].delayed;
+			this.chart.data.labels.push('');
+			this.chart.data.datasets[0].data.push(stats[this.domain].activeSincePrevTick);
+			this.chart.data.datasets[1].data.push(stats[this.domain].active);
+			this.chart.data.datasets[2].data.push(stats[this.domain].waiting);
+			this.chart.data.datasets[3].data.push(stats[this.domain].delayed);
+			if (this.chart.data.datasets[0].data.length > 200) {
+				this.chart.data.labels.shift();
+				this.chart.data.datasets[0].data.shift();
+				this.chart.data.datasets[1].data.shift();
+				this.chart.data.datasets[2].data.shift();
+				this.chart.data.datasets[3].data.shift();
+			}
+			this.chart.update();
+		},
+
+		onStatsLog(statsLog) {
+			for (const stats of statsLog.reverse()) {
+				this.onStats(stats);
+			}
+		},
+
+		fetchJobs() {
+			this.$root.api(this.domain === 'inbox' ? 'admin/queue/inbox-delayed' : this.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => {
+				this.jobs = jobs;
+			});
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-queue-queue {
+	> .status {
+		display: flex;
+
+		> .cell {
+			flex: 1;
+
+			> .label {
+				font-size: 80%;
+				opacity: 0.7;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/instance/queue.vue b/src/client/pages/instance/queue.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b7e633081f17cad97f2c499c4206a0c19c08c867
--- /dev/null
+++ b/src/client/pages/instance/queue.vue
@@ -0,0 +1,79 @@
+<template>
+<div>
+	<x-queue :connection="connection" domain="inbox">
+		<template #title><fa :icon="faExchangeAlt"/> In</template>
+	</x-queue>
+	<x-queue :connection="connection" domain="deliver">
+		<template #title><fa :icon="faExchangeAlt"/> Out</template>
+	</x-queue>
+	<section class="_section">
+		<div class="_content">
+			<mk-button @click="clear()"><fa :icon="faTrashAlt"/> {{ $t('clearQueue') }}</mk-button>
+		</div>
+	</section>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faExchangeAlt } from '@fortawesome/free-solid-svg-icons';
+import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
+import i18n from '../../i18n';
+import MkButton from '../../components/ui/button.vue';
+import XQueue from './queue.queue.vue';
+
+export default Vue.extend({
+	i18n,
+
+	metaInfo() {
+		return {
+			title: `${this.$t('jobQueue')} | ${this.$t('instance')}`
+		};
+	},
+
+	components: {
+		MkButton,
+		XQueue,
+	},
+
+	data() {
+		return {
+			connection: this.$root.stream.useSharedConnection('queueStats'),
+			faExchangeAlt, faTrashAlt
+		}
+	},
+
+	mounted() {
+		this.$nextTick(() => {
+			this.connection.send('requestLog', {
+				id: Math.random().toString().substr(2, 8),
+				length: 200
+			});
+		});
+	},
+
+	beforeDestroy() {
+		this.connection.dispose();
+	},
+
+	methods: {
+		clear() {
+			this.$root.dialog({
+				type: 'warning',
+				title: this.$t('clearQueueConfirmTitle'),
+				text: this.$t('clearQueueConfirmText'),
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+
+				this.$root.api('admin/queue/clear', {}).then(() => {
+					this.$root.dialog({
+						type: 'success',
+						iconOnly: true, autoClose: true
+					});
+				});
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/app/admin/views/dashboard.charts.vue b/src/client/pages/instance/stats.vue
similarity index 54%
rename from src/client/app/admin/views/dashboard.charts.vue
rename to src/client/pages/instance/stats.vue
index b2ac19efff17dc2081be19230d7c3634641849cc..595ad2cc3cbf65355597206f23e9a95bd9b1cbef 100644
--- a/src/client/app/admin/views/dashboard.charts.vue
+++ b/src/client/pages/instance/stats.vue
@@ -1,69 +1,89 @@
 <template>
-<div class="qvgidhudpqhjttdhxubzuyrhyzgslujw">
-	<header>
-		<b><fa :icon="['far', 'chart-bar']"/> {{ $t('title') }}:</b>
-		<select v-model="src">
-			<optgroup :label="$t('federation')">
-				<option value="federation-instances">{{ $t('charts.federation-instances') }}</option>
-				<option value="federation-instances-total">{{ $t('charts.federation-instances-total') }}</option>
-			</optgroup>
-			<optgroup :label="$t('users')">
-				<option value="users">{{ $t('charts.users') }}</option>
-				<option value="users-total">{{ $t('charts.users-total') }}</option>
-				<option value="active-users">{{ $t('charts.active-users') }}</option>
-			</optgroup>
-			<optgroup :label="$t('notes')">
-				<option value="notes">{{ $t('charts.notes') }}</option>
-				<option value="local-notes">{{ $t('charts.local-notes') }}</option>
-				<option value="remote-notes">{{ $t('charts.remote-notes') }}</option>
-				<option value="notes-total">{{ $t('charts.notes-total') }}</option>
-			</optgroup>
-			<optgroup :label="$t('drive')">
-				<option value="drive-files">{{ $t('charts.drive-files') }}</option>
-				<option value="drive-files-total">{{ $t('charts.drive-files-total') }}</option>
-				<option value="drive">{{ $t('charts.drive') }}</option>
-				<option value="drive-total">{{ $t('charts.drive-total') }}</option>
-			</optgroup>
-			<optgroup :label="$t('network')">
-				<option value="network-requests">{{ $t('charts.network-requests') }}</option>
-				<option value="network-time">{{ $t('charts.network-time') }}</option>
-				<option value="network-usage">{{ $t('charts.network-usage') }}</option>
-			</optgroup>
-		</select>
-		<div>
-			<span @click="span = 'day'" :class="{ active: span == 'day' }">{{ $t('per-day') }}</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">{{ $t('per-hour') }}</span>
+<div class="mk-instance-stats">
+	<section class="_section">
+		<div class="_title"><fa :icon="faChartBar"/> {{ $t('statistics') }}</div>
+		<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
+			<div class="selects" style="display: flex;">
+				<mk-select v-model="chartSrc" style="margin: 0; flex: 1;">
+					<optgroup :label="$t('federation')">
+						<option value="federation-instances">{{ $t('_charts.federationInstancesIncDec') }}</option>
+						<option value="federation-instances-total">{{ $t('_charts.federationInstancesTotal') }}</option>
+					</optgroup>
+					<optgroup :label="$t('users')">
+						<option value="users">{{ $t('_charts.usersIncDec') }}</option>
+						<option value="users-total">{{ $t('_charts.usersTotal') }}</option>
+						<option value="active-users">{{ $t('_charts.activeUsers') }}</option>
+					</optgroup>
+					<optgroup :label="$t('notes')">
+						<option value="notes">{{ $t('_charts.notesIncDec') }}</option>
+						<option value="local-notes">{{ $t('_charts.localNotesIncDec') }}</option>
+						<option value="remote-notes">{{ $t('_charts.remoteNotesIncDec') }}</option>
+						<option value="notes-total">{{ $t('_charts.notesTotal') }}</option>
+					</optgroup>
+					<optgroup :label="$t('drive')">
+						<option value="drive-files">{{ $t('_charts.filesIncDec') }}</option>
+						<option value="drive-files-total">{{ $t('_charts.filesTotal') }}</option>
+						<option value="drive">{{ $t('_charts.storageUsageIncDec') }}</option>
+						<option value="drive-total">{{ $t('_charts.storageUsageTotal') }}</option>
+					</optgroup>
+				</mk-select>
+				<mk-select v-model="chartSpan" style="margin: 0;">
+					<option value="hour">{{ $t('perHour') }}</option>
+					<option value="day">{{ $t('perDay') }}</option>
+				</mk-select>
+			</div>
+			<canvas ref="chart"></canvas>
 		</div>
-	</header>
-	<div ref="chart"></div>
+	</section>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import { faChartBar } from '@fortawesome/free-solid-svg-icons';
+import Chart from 'chart.js';
 import i18n from '../../i18n';
-import * as tinycolor from 'tinycolor2';
-import ApexCharts from 'apexcharts';
-
-const limit = 90;
+import MkSelect from '../../components/ui/select.vue';
 
+const chartLimit = 90;
 const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
 const negate = arr => arr.map(x => -x);
+const alpha = (hex, a) => {
+	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
+	const r = parseInt(result[1], 16);
+	const g = parseInt(result[2], 16);
+	const b = parseInt(result[3], 16);
+	return `rgba(${r}, ${g}, ${b}, ${a})`;
+};
 
 export default Vue.extend({
-	i18n: i18n('admin/views/charts.vue'),
+	i18n,
+
+	metaInfo() {
+		return {
+			title: `${this.$t('statistics')} | ${this.$t('instance')}`
+		};
+	},
+
+	components: {
+		MkSelect
+	},
+
 	data() {
 		return {
+			now: null,
 			chart: null,
-			src: 'notes',
-			span: 'hour',
-			chartInstance: null
-		};
+			chartInstance: null,
+			chartSrc: 'notes',
+			chartSpan: 'hour',
+			faChartBar
+		}
 	},
 
 	computed: {
 		data(): any {
 			if (this.chart == null) return null;
-			switch (this.src) {
+			switch (this.chartSrc) {
 				case 'federation-instances': return this.federationInstancesChart(false);
 				case 'federation-instances-total': return this.federationInstancesChart(true);
 				case 'users': return this.usersChart(false);
@@ -77,16 +97,13 @@ export default Vue.extend({
 				case 'drive-total': return this.driveTotalChart();
 				case 'drive-files': return this.driveFilesChart();
 				case 'drive-files-total': return this.driveFilesTotalChart();
-				case 'network-requests': return this.networkRequestsChart();
-				case 'network-time': return this.networkTimeChart();
-				case 'network-usage': return this.networkUsageChart();
 			}
 		},
 
 		stats(): any[] {
 			const stats =
-				this.span == 'day' ? this.chart.perDay :
-				this.span == 'hour' ? this.chart.perHour :
+				this.chartSpan == 'day' ? this.chart.perDay :
+				this.chartSpan == 'hour' ? this.chart.perHour :
 				null;
 
 			return stats;
@@ -94,32 +111,30 @@ export default Vue.extend({
 	},
 
 	watch: {
-		src() {
-			this.render();
+		chartSrc() {
+			this.renderChart();
 		},
 
-		span() {
-			this.render();
+		chartSpan() {
+			this.renderChart();
 		}
 	},
 
-	async mounted() {
+	async created() {
 		this.now = new Date();
 
 		const [perHour, perDay] = await Promise.all([Promise.all([
-			this.$root.api('charts/federation', { limit: limit, span: 'hour' }),
-			this.$root.api('charts/users', { limit: limit, span: 'hour' }),
-			this.$root.api('charts/active-users', { limit: limit, span: 'hour' }),
-			this.$root.api('charts/notes', { limit: limit, span: 'hour' }),
-			this.$root.api('charts/drive', { limit: limit, span: 'hour' }),
-			this.$root.api('charts/network', { limit: limit, span: 'hour' })
+			this.$root.api('charts/federation', { limit: chartLimit, span: 'hour' }),
+			this.$root.api('charts/users', { limit: chartLimit, span: 'hour' }),
+			this.$root.api('charts/active-users', { limit: chartLimit, span: 'hour' }),
+			this.$root.api('charts/notes', { limit: chartLimit, span: 'hour' }),
+			this.$root.api('charts/drive', { limit: chartLimit, span: 'hour' }),
 		]), Promise.all([
-			this.$root.api('charts/federation', { limit: limit, span: 'day' }),
-			this.$root.api('charts/users', { limit: limit, span: 'day' }),
-			this.$root.api('charts/active-users', { limit: limit, span: 'day' }),
-			this.$root.api('charts/notes', { limit: limit, span: 'day' }),
-			this.$root.api('charts/drive', { limit: limit, span: 'day' }),
-			this.$root.api('charts/network', { limit: limit, span: 'day' })
+			this.$root.api('charts/federation', { limit: chartLimit, span: 'day' }),
+			this.$root.api('charts/users', { limit: chartLimit, span: 'day' }),
+			this.$root.api('charts/active-users', { limit: chartLimit, span: 'day' }),
+			this.$root.api('charts/notes', { limit: chartLimit, span: 'day' }),
+			this.$root.api('charts/drive', { limit: chartLimit, span: 'day' }),
 		])]);
 
 		const chart = {
@@ -129,7 +144,6 @@ export default Vue.extend({
 				activeUsers: perHour[2],
 				notes: perHour[3],
 				drive: perHour[4],
-				network: perHour[5]
 			},
 			perDay: {
 				federation: perDay[0],
@@ -137,115 +151,94 @@ export default Vue.extend({
 				activeUsers: perDay[2],
 				notes: perDay[3],
 				drive: perDay[4],
-				network: perDay[5]
 			}
 		};
 
 		this.chart = chart;
 
-		this.render();
-	},
-
-	beforeDestroy() {
-		this.chartInstance.destroy();
+		this.renderChart();
 	},
 
 	methods: {
-		setSrc(src) {
-			this.src = src;
-		},
-
-		render() {
+		renderChart() {
 			if (this.chartInstance) {
 				this.chartInstance.destroy();
 			}
 
-			this.chartInstance = new ApexCharts(this.$refs.chart, {
-				chart: {
-					type: 'area',
-					height: 300,
-					animations: {
-						dynamicAnimation: {
-							enabled: false
-						}
-					},
-					toolbar: {
-						show: false
-					},
-					zoom: {
-						enabled: false
-					}
+			Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+			this.chartInstance = new Chart(this.$refs.chart, {
+				type: 'line',
+				data: {
+					labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
+					datasets: this.data.series.map(x => ({
+						label: x.name,
+						data: x.data.slice().reverse(),
+						pointRadius: 0,
+						lineTension: 0,
+						borderWidth: 2,
+						borderColor: x.color,
+						backgroundColor: alpha(x.color, 0.1),
+						hidden: !!x.hidden
+					}))
 				},
-				dataLabels: {
-					enabled: false
-				},
-				grid: {
-					clipMarkers: false,
-					borderColor: 'rgba(0, 0, 0, 0.1)',
-					xaxis: {
-						lines: {
-							show: true,
+				options: {
+					aspectRatio: 2.5,
+					layout: {
+						padding: {
+							left: 0,
+							right: 0,
+							top: 16,
+							bottom: 0
 						}
 					},
-				},
-				stroke: {
-					curve: 'straight',
-					width: 2
-				},
-				legend: {
-					labels: {
-						colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
-					},
-				},
-				xaxis: {
-					type: 'datetime',
-					labels: {
-						style: {
-							colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
+					legend: {
+						position: 'bottom',
+						labels: {
+							boxWidth: 16,
 						}
 					},
-					axisBorder: {
-						color: 'rgba(0, 0, 0, 0.1)'
+					scales: {
+						xAxes: [{
+							gridLines: {
+								display: false
+							},
+							ticks: {
+								display: false
+							}
+						}],
+						yAxes: [{
+							position: 'right',
+							ticks: {
+								display: false
+							}
+						}]
 					},
-					axisTicks: {
-						color: 'rgba(0, 0, 0, 0.1)'
-					},
-				},
-				yaxis: {
-					labels: {
-						formatter: this.data.bytes ? v => Vue.filter('bytes')(v, 0) : v => Vue.filter('number')(v),
-						style: {
-							color: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
-						}
+					tooltips: {
+						intersect: false,
+						mode: 'index',
 					}
-				},
-				series: this.data.series
+				}
 			});
-
-			this.chartInstance.render();
 		},
 
-		getDate(i: number) {
+		getDate(ago: number) {
 			const y = this.now.getFullYear();
 			const m = this.now.getMonth();
 			const d = this.now.getDate();
 			const h = this.now.getHours();
 
-			return (
-				this.span == 'day' ? new Date(y, m, d - i) :
-				this.span == 'hour' ? new Date(y, m, d, h - i) :
-				null
-			);
+			return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
 		},
 
 		format(arr) {
-			return arr.map((v, i) => ({ x: this.getDate(i).getTime(), y: v }));
+			return arr;
 		},
 
 		federationInstancesChart(total: boolean): any {
 			return {
 				series: [{
 					name: 'Instances',
+					color: '#008FFB',
 					data: this.format(total
 						? this.stats.federation.instance.total
 						: sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec))
@@ -259,6 +252,7 @@ export default Vue.extend({
 				series: [{
 					name: 'All',
 					type: 'line',
+					color: '#008FFB',
 					data: this.format(type == 'combined'
 						? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec))
 						: sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec))
@@ -266,6 +260,7 @@ export default Vue.extend({
 				}, {
 					name: 'Renotes',
 					type: 'area',
+					color: '#00E396',
 					data: this.format(type == 'combined'
 						? sum(this.stats.notes.local.diffs.renote, this.stats.notes.remote.diffs.renote)
 						: this.stats.notes[type].diffs.renote
@@ -273,6 +268,7 @@ export default Vue.extend({
 				}, {
 					name: 'Replies',
 					type: 'area',
+					color: '#FEB019',
 					data: this.format(type == 'combined'
 						? sum(this.stats.notes.local.diffs.reply, this.stats.notes.remote.diffs.reply)
 						: this.stats.notes[type].diffs.reply
@@ -280,6 +276,7 @@ export default Vue.extend({
 				}, {
 					name: 'Normal',
 					type: 'area',
+					color: '#FF4560',
 					data: this.format(type == 'combined'
 						? sum(this.stats.notes.local.diffs.normal, this.stats.notes.remote.diffs.normal)
 						: this.stats.notes[type].diffs.normal
@@ -293,14 +290,19 @@ export default Vue.extend({
 				series: [{
 					name: 'Combined',
 					type: 'line',
+					color: '#008FFB',
 					data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total))
 				}, {
 					name: 'Local',
 					type: 'area',
+					color: '#008FFB',
+					hidden: true,
 					data: this.format(this.stats.notes.local.total)
 				}, {
 					name: 'Remote',
 					type: 'area',
+					color: '#008FFB',
+					hidden: true,
 					data: this.format(this.stats.notes.remote.total)
 				}]
 			};
@@ -311,6 +313,7 @@ export default Vue.extend({
 				series: [{
 					name: 'Combined',
 					type: 'line',
+					color: '#008FFB',
 					data: this.format(total
 						? sum(this.stats.users.local.total, this.stats.users.remote.total)
 						: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec), this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
@@ -318,6 +321,8 @@ export default Vue.extend({
 				}, {
 					name: 'Local',
 					type: 'area',
+					color: '#008FFB',
+					hidden: true,
 					data: this.format(total
 						? this.stats.users.local.total
 						: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec))
@@ -325,6 +330,8 @@ export default Vue.extend({
 				}, {
 					name: 'Remote',
 					type: 'area',
+					color: '#008FFB',
+					hidden: true,
 					data: this.format(total
 						? this.stats.users.remote.total
 						: sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
@@ -338,14 +345,19 @@ export default Vue.extend({
 				series: [{
 					name: 'Combined',
 					type: 'line',
+					color: '#008FFB',
 					data: this.format(sum(this.stats.activeUsers.local.count, this.stats.activeUsers.remote.count))
 				}, {
 					name: 'Local',
 					type: 'area',
+					color: '#008FFB',
+					hidden: true,
 					data: this.format(this.stats.activeUsers.local.count)
 				}, {
 					name: 'Remote',
 					type: 'area',
+					color: '#008FFB',
+					hidden: true,
 					data: this.format(this.stats.activeUsers.remote.count)
 				}]
 			};
@@ -357,6 +369,7 @@ export default Vue.extend({
 				series: [{
 					name: 'All',
 					type: 'line',
+					color: '#008FFB',
 					data: this.format(
 						sum(
 							this.stats.drive.local.incSize,
@@ -368,18 +381,22 @@ export default Vue.extend({
 				}, {
 					name: 'Local +',
 					type: 'area',
+					color: '#008FFB',
 					data: this.format(this.stats.drive.local.incSize)
 				}, {
 					name: 'Local -',
 					type: 'area',
+					color: '#008FFB',
 					data: this.format(negate(this.stats.drive.local.decSize))
 				}, {
 					name: 'Remote +',
 					type: 'area',
+					color: '#008FFB',
 					data: this.format(this.stats.drive.remote.incSize)
 				}, {
 					name: 'Remote -',
 					type: 'area',
+					color: '#008FFB',
 					data: this.format(negate(this.stats.drive.remote.decSize))
 				}]
 			};
@@ -391,14 +408,19 @@ export default Vue.extend({
 				series: [{
 					name: 'Combined',
 					type: 'line',
+					color: '#008FFB',
 					data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize))
 				}, {
 					name: 'Local',
 					type: 'area',
+					color: '#008FFB',
+					hidden: true,
 					data: this.format(this.stats.drive.local.totalSize)
 				}, {
 					name: 'Remote',
 					type: 'area',
+					color: '#008FFB',
+					hidden: true,
 					data: this.format(this.stats.drive.remote.totalSize)
 				}]
 			};
@@ -409,6 +431,7 @@ export default Vue.extend({
 				series: [{
 					name: 'All',
 					type: 'line',
+					color: '#008FFB',
 					data: this.format(
 						sum(
 							this.stats.drive.local.incCount,
@@ -420,18 +443,22 @@ export default Vue.extend({
 				}, {
 					name: 'Local +',
 					type: 'area',
+					color: '#008FFB',
 					data: this.format(this.stats.drive.local.incCount)
 				}, {
 					name: 'Local -',
 					type: 'area',
+					color: '#008FFB',
 					data: this.format(negate(this.stats.drive.local.decCount))
 				}, {
 					name: 'Remote +',
 					type: 'area',
+					color: '#008FFB',
 					data: this.format(this.stats.drive.remote.incCount)
 				}, {
 					name: 'Remote -',
 					type: 'area',
+					color: '#008FFB',
 					data: this.format(negate(this.stats.drive.remote.decCount))
 				}]
 			};
@@ -442,86 +469,23 @@ export default Vue.extend({
 				series: [{
 					name: 'Combined',
 					type: 'line',
+					color: '#008FFB',
 					data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount))
 				}, {
 					name: 'Local',
 					type: 'area',
+					color: '#008FFB',
+					hidden: true,
 					data: this.format(this.stats.drive.local.totalCount)
 				}, {
 					name: 'Remote',
 					type: 'area',
+					color: '#008FFB',
+					hidden: true,
 					data: this.format(this.stats.drive.remote.totalCount)
 				}]
 			};
 		},
-
-		networkRequestsChart(): any {
-			return {
-				series: [{
-					name: 'Incoming',
-					data: this.format(this.stats.network.incomingRequests)
-				}]
-			};
-		},
-
-		networkTimeChart(): any {
-			const data = [];
-
-			for (let i = 0; i < limit; i++) {
-				data.push(this.stats.network.incomingRequests[i] != 0 ? (this.stats.network.totalTime[i] / this.stats.network.incomingRequests[i]) : 0);
-			}
-
-			return {
-				series: [{
-					name: 'Avg time',
-					data: this.format(data)
-				}]
-			};
-		},
-
-		networkUsageChart(): any {
-			return {
-				bytes: true,
-				series: [{
-					name: 'Incoming',
-					data: this.format(this.stats.network.incomingBytes)
-				}, {
-					name: 'Outgoing',
-					data: this.format(this.stats.network.outgoingBytes)
-				}]
-			};
-		},
 	}
 });
 </script>
-
-<style lang="stylus" scoped>
-.qvgidhudpqhjttdhxubzuyrhyzgslujw
-	display block
-	flex 1
-	padding 32px 24px
-	padding-bottom 0
-	box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
-	background var(--face)
-	border-radius 8px
-
-	> header
-		display flex
-		margin 0 8px
-		padding 0 0 8px 0
-		font-size 1em
-		color var(--adminDashboardCardFg)
-		border-bottom solid 1px var(--adminDashboardCardDivider)
-
-		> b
-			margin-right 8px
-
-		> *:last-child
-			margin-left auto
-
-			*
-				&:not(.active)
-					color var(--primary)
-					cursor pointer
-
-</style>
diff --git a/src/client/pages/instance/users.vue b/src/client/pages/instance/users.vue
new file mode 100644
index 0000000000000000000000000000000000000000..da59d8ce241ade3c7815a992cac134e265de38c4
--- /dev/null
+++ b/src/client/pages/instance/users.vue
@@ -0,0 +1,203 @@
+<template>
+<div class="mk-instance-users">
+	<portal to="icon"><fa :icon="faUsers"/></portal>
+	<portal to="title">{{ $t('users') }}</portal>
+
+	<section class="_section lookup">
+		<div class="_title"><fa :icon="faSearch"/> {{ $t('lookup') }}</div>
+		<div class="_content">
+			<mk-input class="target" v-model="target" type="text" @enter="showUser()" style="margin-top: 0;">
+				<span>{{ $t('usernameOrUserId') }}</span>
+			</mk-input>
+			<mk-button @click="showUser()" primary><fa :icon="faSearch"/> {{ $t('lookup') }}</mk-button>
+		</div>
+		<div class="_footer">
+			<mk-button inline primary @click="search()"><fa :icon="faSearch"/> {{ $t('search') }}</mk-button>
+		</div>
+	</section>
+
+	<section class="_section users">
+		<div class="_title"><fa :icon="faUsers"/> {{ $t('users') }}</div>
+		<div class="_content _list">
+			<mk-pagination :pagination="pagination" #default="{items}" class="users" ref="users" :auto-margin="false">
+				<button class="user _button _listItem" v-for="(user, i) in items" :key="user.id" :data-index="i" @click="show(user)">
+					<mk-avatar :user="user" class="avatar"/>
+					<div class="body">
+						<mk-user-name :user="user" class="name"/>
+						<mk-acct :user="user" class="acct"/>
+					</div>
+				</button>
+			</mk-pagination>
+		</div>
+		<div class="_footer">
+			<mk-button inline primary @click="addUser()"><fa :icon="faPlus"/> {{ $t('addUser') }}</mk-button>
+		</div>
+	</section>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faPlus, faUsers, faSearch } from '@fortawesome/free-solid-svg-icons';
+import parseAcct from '../../../misc/acct/parse';
+import MkButton from '../../components/ui/button.vue';
+import MkInput from '../../components/ui/input.vue';
+import MkPagination from '../../components/ui/pagination.vue';
+import MkUserModerateDialog from '../../components/user-moderate-dialog.vue';
+import MkUserSelect from '../../components/user-select.vue';
+
+export default Vue.extend({
+	metaInfo() {
+		return {
+			title: `${this.$t('users')} | ${this.$t('instance')}`
+		};
+	},
+
+	components: {
+		MkButton,
+		MkInput,
+		MkPagination,
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'admin/show-users',
+				limit: 10,
+				offsetMode: true
+			},
+			target: '',
+			faPlus, faUsers, faSearch
+		}
+	},
+
+	methods: {
+		/** テキストエリアのユーザーを解決する */
+		fetchUser() {
+			return new Promise((res) => {
+				const usernamePromise = this.$root.api('users/show', parseAcct(this.target));
+				const idPromise = this.$root.api('users/show', { userId: this.target });
+				let _notFound = false;
+				const notFound = () => {
+					if (_notFound) {
+						this.$root.dialog({
+							type: 'error',
+							text: this.$t('noSuchUser')
+						});
+					} else {
+						_notFound = true;
+					}
+				};
+				usernamePromise.then(res).catch(e => {
+					if (e.code === 'NO_SUCH_USER') {
+						notFound();
+					}
+				});
+				idPromise.then(res).catch(e => {
+					notFound();
+				});
+			});
+		},
+
+		/** テキストエリアから処理対象ユーザーを設定する */
+		async showUser() {
+			const user = await this.fetchUser();
+			this.$root.api('admin/show-user', { userId: user.id }).then(info => {
+				this.show(user, info);
+			});
+			this.target = '';
+		},
+
+		async addUser() {
+			const { canceled: canceled1, result: username } = await this.$root.dialog({
+				title: this.$t('username'),
+				input: true
+			});
+			if (canceled1) return;
+
+			const { canceled: canceled2, result: password } = await this.$root.dialog({
+				title: this.$t('password'),
+				input: { type: 'password' }
+			});
+			if (canceled2) return;
+
+			const dialog = this.$root.dialog({
+				type: 'waiting',
+				iconOnly: true
+			});
+
+			this.$root.api('admin/accounts/create', {
+				username: username,
+				password: password,
+			}).then(res => {
+				this.$refs.users.reload();
+				this.$root.dialog({
+					type: 'success',
+					iconOnly: true, autoClose: true
+				});
+			}).catch(e => {
+				this.$root.dialog({
+					type: 'error',
+					text: e.id
+				});
+			}).finally(() => {
+				dialog.close();
+			});
+		},
+
+		async show(user, info) {
+			if (info == null) info = await this.$root.api('admin/show-user', { userId: user.id });
+			this.$root.new(MkUserModerateDialog, {
+				user: { ...user, ...info }
+			});
+		},
+
+		search() {
+			this.$root.new(MkUserSelect, {}).$once('selected', user => {
+				this.$root.api('admin/show-user', { userId: user.id }).then(info => {
+					this.show(user, info);
+				});
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-instance-users {
+	> .users {
+		> ._content {
+			max-height: 300px;
+			overflow: auto;
+			
+			> .users {
+				> .user {
+					display: flex;
+					width: 100%;
+					box-sizing: border-box;
+					text-align: left;
+					align-items: center;
+
+					> .avatar {
+						width: 50px;
+						height: 50px;
+					}
+
+					> .body {
+						padding: 8px;
+
+						> .name {
+							display: block;
+							font-weight: bold;
+						}
+
+						> .acct {
+							opacity: 0.5;
+						}
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/mentions.vue b/src/client/pages/mentions.vue
new file mode 100644
index 0000000000000000000000000000000000000000..333af917349a4d02069ee7ceda0d711cb81b6c67
--- /dev/null
+++ b/src/client/pages/mentions.vue
@@ -0,0 +1,46 @@
+<template>
+<div>
+	<portal to="icon"><fa :icon="faAt"/></portal>
+	<portal to="title">{{ $t('mentions') }}</portal>
+	<x-notes :pagination="pagination" :detail="true" @before="before()" @after="after()"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faAt } from '@fortawesome/free-solid-svg-icons';
+import Progress from '../scripts/loading';
+import XNotes from '../components/notes.vue';
+
+export default Vue.extend({
+	metaInfo() {
+		return {
+			title: this.$t('mentions') as string
+		};
+	},
+
+	components: {
+		XNotes
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'notes/mentions',
+				limit: 10,
+			},
+			faAt
+		};
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/messages.vue b/src/client/pages/messages.vue
new file mode 100644
index 0000000000000000000000000000000000000000..1165004e97b920a34b4127417d28bb7b39aa096b
--- /dev/null
+++ b/src/client/pages/messages.vue
@@ -0,0 +1,49 @@
+<template>
+<div>
+	<portal to="icon"><fa :icon="faEnvelope"/></portal>
+	<portal to="title">{{ $t('directNotes') }}</portal>
+	<x-notes :pagination="pagination" :detail="true" @before="before()" @after="after()"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faEnvelope } from '@fortawesome/free-solid-svg-icons';
+import Progress from '../scripts/loading';
+import XNotes from '../components/notes.vue';
+
+export default Vue.extend({
+	metaInfo() {
+		return {
+			title: this.$t('directNotes') as string
+		};
+	},
+
+	components: {
+		XNotes
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'notes/mentions',
+				limit: 10,
+				params: () => ({
+					visibility: 'specified'
+				})
+			},
+			faEnvelope
+		};
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		}
+	}
+});
+</script>
diff --git a/src/client/app/common/views/components/messaging-room.form.vue b/src/client/pages/messaging-room.form.vue
similarity index 63%
rename from src/client/app/common/views/components/messaging-room.form.vue
rename to src/client/pages/messaging-room.form.vue
index bd63bab2c1c8074ca51556f559f0b3291d1b573d..4cdd2b1f325ccc2b598652318ed1802c4174f1ca 100644
--- a/src/client/app/common/views/components/messaging-room.form.vue
+++ b/src/client/pages/messaging-room.form.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-messaging-form"
+<div class="mk-messaging-form _panel"
 	@dragover.stop="onDragover"
 	@drop.stop="onDrop"
 >
@@ -12,15 +12,15 @@
 		v-autocomplete="{ model: 'text' }"
 	></textarea>
 	<div class="file" @click="file = null" v-if="file">{{ file.name }}</div>
-	<mk-uploader ref="uploader" @uploaded="onUploaded"/>
-	<button class="send" @click="send" :disabled="!canSend || sending" :title="$t('send')">
-		<template v-if="!sending"><fa icon="paper-plane"/></template><template v-if="sending"><fa icon="spinner .spin"/></template>
+	<x-uploader ref="uploader" @uploaded="onUploaded"/>
+	<button class="send _button" @click="send" :disabled="!canSend || sending" :title="$t('send')">
+		<template v-if="!sending"><fa :icon="faPaperPlane"/></template><template v-if="sending"><fa icon="spinner .spin"/></template>
 	</button>
-	<button class="attach-from-local" @click="chooseFile" :title="$t('attach-from-local')">
-		<fa icon="upload"/>
+	<button class="attach-from-local _button" @click="chooseFile" :title="$t('attach-from-local')">
+		<fa :icon="faUpload"/>
 	</button>
-	<button class="attach-from-drive" @click="chooseFileFromDrive" :title="$t('attach-from-drive')">
-		<fa :icon="['far', 'folder-open']"/>
+	<button class="attach-from-drive _button" @click="chooseFileFromDrive" :title="$t('attach-from-drive')">
+		<fa :icon="faCloud"/>
 	</button>
 	<input ref="file" type="file" @change="onChangeFile"/>
 </div>
@@ -28,12 +28,16 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../../i18n';
+import { faPaperPlane, faUpload, faCloud } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
 import * as autosize from 'autosize';
-import { formatTimeString } from '../../../../../misc/format-time-string';
+import { formatTimeString } from '../../misc/format-time-string';
 
 export default Vue.extend({
-	i18n: i18n('common/views/components/messaging-room.form.vue'),
+	i18n,
+	components: {
+		XUploader: () => import('../components/uploader.vue').then(m => m.default),
+	},
 	props: {
 		user: {
 			type: Object,
@@ -48,7 +52,8 @@ export default Vue.extend({
 		return {
 			text: null,
 			file: null,
-			sending: false
+			sending: false,
+			faPaperPlane, faUpload, faCloud
 		};
 	},
 	computed: {
@@ -226,110 +231,127 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.mk-messaging-form
-	> textarea
-		cursor auto
-		display block
-		width 100%
-		min-width 100%
-		max-width 100%
-		height 64px
-		margin 0
-		padding 8px
-		resize none
-		font-size 1em
-		color var(--inputText)
-		outline none
-		border none
-		border-top solid 1px var(--faceDivider)
-		border-radius 0
-		box-shadow none
-		background transparent
-
-	> .file
-		padding 8px
-		color #444
-		background #eee
-		cursor pointer
-
-	> .send
-		position absolute
-		bottom 0
-		right 0
-		margin 0
-		padding 10px 14px
-		font-size 1em
-		color #aaa
-		transition color 0.1s ease
-
-		&:hover
-			color var(--primary)
-
-		&:active
-			color var(--primaryDarken10)
-			transition color 0s ease
-
-	.files
-		display block
-		margin 0
-		padding 0 8px
-		list-style none
-
-		&:after
-			content ''
-			display block
-			clear both
-
-		> li
-			display block
-			float left
-			margin 4px
-			padding 0
-			width 64px
-			height 64px
-			background-color #eee
-			background-repeat no-repeat
-			background-position center center
-			background-size cover
-			cursor move
-
-			&:hover
-				> .remove
-					display block
-
-			> .remove
-				display none
-				position absolute
-				right -6px
-				top -6px
-				margin 0
-				padding 0
-				background transparent
-				outline none
-				border none
-				border-radius 0
-				box-shadow none
-				cursor pointer
-
-	.attach-from-local
-	.attach-from-drive
-		margin 0
-		padding 10px 14px
-		font-size 1em
-		font-weight normal
-		text-decoration none
-		color #aaa
-		transition color 0.1s ease
-
-		&:hover
-			color var(--primary)
-
-		&:active
-			color var(--primaryDarken10)
-			transition color 0s ease
-
-	input[type=file]
-		display none
+<style lang="scss" scoped>
+.mk-messaging-form {
+	position: relative;
+
+	> textarea {
+		cursor: auto;
+		display: block;
+		width: 100%;
+		min-width: 100%;
+		max-width: 100%;
+		height: 80px;
+		margin: 0;
+		padding: 16px 16px 0 16px;
+		resize: none;
+		font-size: 1em;
+		outline: none;
+		border: none;
+		border-radius: 0;
+		box-shadow: none;
+		background: transparent;
+		box-sizing: border-box;
+		color: var(--fg);
+	}
+
+	> .file {
+		padding: 8px;
+		color: #444;
+		background: #eee;
+		cursor: pointer;
+	}
 
+	> .send {
+		position: absolute;
+		bottom: 0;
+		right: 0;
+		margin: 0;
+		padding: 16px;
+		font-size: 1em;
+		color: #aaa;
+		transition: color 0.1s ease;
+
+		&:hover {
+			color: var(--accent);
+		}
+
+		&:active {
+			color: var(--accentDarken);
+			transition: color 0s ease;
+		}
+	}
+
+	.files {
+		display: block;
+		margin: 0;
+		padding: 0 8px;
+		list-style: none;
+
+		&:after {
+			content: '';
+			display: block;
+			clear: both;
+		}
+
+		> li {
+			display: block;
+			float: left;
+			margin: 4px;
+			padding: 0;
+			width: 64px;
+			height: 64px;
+			background-color: #eee;
+			background-repeat: no-repeat;
+			background-position: center center;
+			background-size: cover;
+			cursor: move;
+
+			&:hover {
+				> .remove {
+					display: block;
+				}
+			}
+
+			> .remove {
+				display: none;
+				position: absolute;
+				right: -6px;
+				top: -6px;
+				margin: 0;
+				padding: 0;
+				background: transparent;
+				outline: none;
+				border: none;
+				border-radius: 0;
+				box-shadow: none;
+				cursor: pointer;
+			}
+		}
+	}
+
+	.attach-from-local,
+	.attach-from-drive {
+		margin: 0;
+		padding: 16px;
+		font-size: 1em;
+		font-weight: normal;
+		text-decoration: none;
+		color: #aaa;
+		transition: color 0.1s ease;
+
+		&:hover {
+			color: var(--accent);
+		}
+
+		&:active {
+			color: var(--accentDarken);
+			transition: color 0s ease;
+		}
+	}
+
+	input[type=file] {
+		display: none;
+	}
+}
 </style>
diff --git a/src/client/pages/messaging-room.message.vue b/src/client/pages/messaging-room.message.vue
new file mode 100644
index 0000000000000000000000000000000000000000..392eb6acb08a6d41671d5a4f62a5830b45fb012e
--- /dev/null
+++ b/src/client/pages/messaging-room.message.vue
@@ -0,0 +1,336 @@
+<template>
+<div class="thvuemwp" :data-is-me="isMe">
+	<mk-avatar class="avatar" :user="message.user"/>
+	<div class="content">
+		<div class="balloon _panel" :data-no-text="message.text == null">
+			<button class="delete-button" v-if="isMe" :title="$t('@.delete')" @click="del">
+				<img src="/assets/desktop/remove.png" alt="Delete"/>
+			</button>
+			<div class="content" v-if="!message.isDeleted">
+				<mfm 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" rel="noopener" target="_blank" :title="message.file.name">
+						<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"
+							:style="{ backgroundColor: message.file.properties.avgColor || 'transparent' }"/>
+						<p v-else>{{ message.file.name }}</p>
+					</a>
+				</div>
+			</div>
+			<div class="content" v-else>
+				<p class="is-deleted">{{ $t('deleted') }}</p>
+			</div>
+		</div>
+		<div></div>
+		<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+		<footer>
+			<template v-if="isGroup">
+				<span class="read" v-if="message.reads.length > 0">{{ $t('messageRead') }} {{ message.reads.length }}</span>
+			</template>
+			<template v-else>
+				<span class="read" v-if="isMe && message.isRead">{{ $t('messageRead') }}</span>
+			</template>
+			<mk-time :time="message.createdAt"/>
+			<template v-if="message.is_edited"><fa icon="pencil-alt"/></template>
+		</footer>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import { parse } from '../../mfm/parse';
+import { unique } from '../../prelude/array';
+
+export default Vue.extend({
+	i18n,
+	props: {
+		message: {
+			required: true
+		},
+		isGroup: {
+			required: false
+		}
+	},
+	computed: {
+		isMe(): boolean {
+			return this.message.userId == this.$store.state.i.id;
+		},
+		urls(): string[] {
+			if (this.message.text) {
+				const ast = parse(this.message.text);
+				return unique(ast
+					.filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent))
+					.map(t => t.node.props.url));
+			} else {
+				return null;
+			}
+		}
+	},
+	methods: {
+		del() {
+			this.$root.api('messaging/messages/delete', {
+				messageId: this.message.id
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.thvuemwp {
+	$me-balloon-color: var(--accent);
+
+	position: relative;
+	background-color: transparent;
+	display: flex;
+
+	> .avatar {
+		display: block;
+		width: 54px;
+		height: 54px;
+		transition: all 0.1s ease;
+
+		@media (max-width: 400px) {
+			width: 48px;
+			height: 48px;
+		}
+	}
+
+	> .content {
+		min-width: 0;
+
+		> .balloon {
+			position: relative;
+			display: inline-flex;
+			align-items: center;
+			padding: 0;
+			min-height: 38px;
+			border-radius: 16px;
+			max-width: 100%;
+
+			&:before {
+				content: "";
+				pointer-events: none;
+				display: block;
+				position: absolute;
+				top: 12px;
+			}
+
+			& + * {
+				clear: both;
+			}
+
+			&:hover {
+				> .delete-button {
+					display: block;
+				}
+			}
+
+			> .delete-button {
+				display: none;
+				position: absolute;
+				z-index: 1;
+				top: -4px;
+				right: -4px;
+				margin: 0;
+				padding: 0;
+				cursor: pointer;
+				outline: none;
+				border: none;
+				border-radius: 0;
+				box-shadow: none;
+				background: transparent;
+
+				> img {
+					vertical-align: bottom;
+					width: 16px;
+					height: 16px;
+					cursor: pointer;
+				}
+			}
+
+			> .content {
+				max-width: 100%;
+
+				> .is-deleted {
+					display: block;
+					margin: 0;
+					padding: 0;
+					overflow: hidden;
+					overflow-wrap: break-word;
+					font-size: 1em;
+					color: rgba(#000, 0.5);
+				}
+
+				> .text {
+					display: block;
+					margin: 0;
+					padding: 12px 18px;
+					overflow: hidden;
+					overflow-wrap: break-word;
+					word-break: break-word;
+					font-size: 1em;
+					color: rgba(#000, 0.8);
+
+					@media (max-width: 500px) {
+						padding: 8px 16px;
+					}
+
+					@media (max-width: 400px) {
+						font-size: 0.9em;
+					}
+
+					& + .file {
+						> a {
+							border-radius: 0 0 16px 16px;
+						}
+					}
+				}
+
+				> .file {
+					> a {
+						display: block;
+						max-width: 100%;
+						border-radius: 16px;
+						overflow: hidden;
+						text-decoration: none;
+
+						&:hover {
+							text-decoration: none;
+
+							> p {
+								background: #ccc;
+							}
+						}
+
+						> * {
+							display: block;
+							margin: 0;
+							width: 100%;
+							max-height: 512px;
+							object-fit: contain;
+						}
+
+						> p {
+							padding: 30px;
+							text-align: center;
+							color: #555;
+							background: #ddd;
+						}
+					}
+				}
+			}
+		}
+
+		> .mk-url-preview {
+			margin: 8px 0;
+		}
+
+		> footer {
+			display: block;
+			margin: 2px 0 0 0;
+			font-size: 10px;
+			color: var(--messagingRoomMessageInfo);
+
+			> .read {
+				margin: 0 8px;
+			}
+
+			> [data-icon] {
+				margin-left: 4px;
+			}
+		}
+	}
+
+	&:not([data-is-me]) {
+
+		> .content {
+			padding-left: 16px;
+			padding-right: 32px;
+
+			> .balloon {
+				$color: var(--panel);
+				background: $color;
+
+				&[data-no-text] {
+					background: transparent;
+				}
+
+				&:not([data-no-text]):before {
+					left: -14px;
+					border-top: solid 8px transparent;
+					border-right: solid 8px $color;
+					border-bottom: solid 8px transparent;
+					border-left: solid 8px transparent;
+				}
+
+				> .content {
+					> .text {
+						color: var(--fg);
+					}
+				}
+			}
+
+			> footer {
+				text-align: left;
+			}
+		}
+	}
+
+	&[data-is-me] {
+		flex-direction: row-reverse;
+
+		> .content {
+			padding-right: 16px;
+			padding-left: 32px;
+			text-align: right;
+
+			> .balloon {
+				background: $me-balloon-color;
+				text-align: left;
+
+				&[data-no-text] {
+					background: transparent;
+				}
+
+				&:not([data-no-text]):before {
+					right: -14px;
+					left: auto;
+					border-top: solid 8px transparent;
+					border-right: solid 8px transparent;
+					border-bottom: solid 8px transparent;
+					border-left: solid 8px $me-balloon-color;
+				}
+
+				> .content {
+
+					> p.is-deleted {
+						color: rgba(#fff, 0.5);
+					}
+
+					> .text {
+						&, * {
+							color: #fff !important;
+						}
+					}
+				}
+			}
+
+			> footer {
+				text-align: right;
+
+				> .read {
+					user-select: none;
+				}
+			}
+		}
+	}
+
+	&[data-is-deleted] {
+		> .balloon {
+			opacity: 0.5;
+		}
+	}
+}
+</style>
diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/pages/messaging-room.vue
similarity index 53%
rename from src/client/app/common/views/components/messaging-room.vue
rename to src/client/pages/messaging-room.vue
index d5fa4143a093f50758a37ab699f1be16ad3a92f8..cba84b6de7d2b957788f40e4649f7e4cec46614c 100644
--- a/src/client/app/common/views/components/messaging-room.vue
+++ b/src/client/pages/messaging-room.vue
@@ -3,65 +3,60 @@
 	@dragover.prevent.stop="onDragover"
 	@drop.prevent.stop="onDrop"
 >
+	<template v-if="!fetching && user">
+		<portal to="title"><mk-user-name :user="user" :nowrap="false" class="name"/></portal>
+		<portal to="avatar"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal>
+	</template>
+	<template v-if="!fetching && group">
+		<portal to="title">{{ group.name }}</portal>
+	</template>
+
 	<div class="body">
-		<p class="init" v-if="init"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}</p>
-		<p class="empty" v-if="!init && messages.length == 0"><fa icon="info-circle"/>{{ user ? $t('not-talked-user') : $t('not-talked-group') }}</p>
-		<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages"><fa :icon="faFlag"/>{{ $t('no-history') }}</p>
-		<button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
+		<mk-loading v-if="fetching"/>
+		<p class="empty" v-if="!fetching && messages.length == 0"><fa icon="info-circle"/>{{ user ? $t('not-talked-user') : $t('not-talked-group') }}</p>
+		<p class="no-history" v-if="!fetching && messages.length > 0 && !existMoreMessages"><fa :icon="faFlag"/>{{ $t('noMoreHistory') }}</p>
+		<button class="more _button" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
 			<template v-if="fetchingMoreMessages"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreMessages ? $t('@.loading') : $t('@.load-more') }}
 		</button>
-		<template v-for="(message, i) in _messages">
-			<x-message :message="message" :key="message.id" :is-group="group != null"/>
-			<p class="date" v-if="i != messages.length - 1 && message._date != _messages[i + 1]._date">
-				<span>{{ _messages[i + 1]._datetext }}</span>
-			</p>
-		</template>
+		<x-list class="messages" :items="messages" v-slot="{ item: message, i }" direction="up">
+			<x-message :message="message" :is-group="group != null" :key="message.id" :data-index="messages.length - i"/>
+		</x-list>
 	</div>
 	<footer>
 		<transition name="fade">
 			<div class="new-message" v-show="showIndicator">
-				<button @click="onIndicatorClick"><i><fa :icon="faArrowCircleDown"/></i>{{ $t('new-message') }}</button>
+				<button class="_buttonPrimary" @click="onIndicatorClick"><i><fa :icon="faArrowCircleDown"/></i>{{ $t('new-message') }}</button>
 			</div>
 		</transition>
-		<x-form :user="user" :group="group" ref="form"/>
+		<x-form v-if="!fetching" :user="user" :group="group" ref="form"/>
 	</footer>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../../i18n';
+import { faArrowCircleDown, faFlag } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
+import XList from '../components/date-separated-list.vue';
 import XMessage from './messaging-room.message.vue';
 import XForm from './messaging-room.form.vue';
-import { url } from '../../../config';
-import { faArrowCircleDown, faFlag } from '@fortawesome/free-solid-svg-icons';
+import { url } from '../config';
+import parseAcct from '../../misc/acct/parse';
 
 export default Vue.extend({
-	i18n: i18n('common/views/components/messaging-room.vue'),
+	i18n,
 
 	components: {
 		XMessage,
-		XForm
-	},
-
-	props: {
-		user: {
-			type: Object,
-			requird: false,
-		},
-		group: {
-			type: Object,
-			requird: false,
-		},
-		isNaked: {
-			type: Boolean,
-			requird: false,
-		},
+		XForm,
+		XList,
 	},
 
 	data() {
 		return {
-			init: true,
+			fetching: true,
+			user: null,
+			group: null,
 			fetchingMoreMessages: false,
 			messages: [],
 			existMoreMessages: false,
@@ -73,58 +68,57 @@ export default Vue.extend({
 	},
 
 	computed: {
-		_messages(): any[] {
-			return (this.messages as any).map(message => {
-				const date = new Date(message.createdAt).getDate();
-				const month = new Date(message.createdAt).getMonth() + 1;
-				message._date = date;
-				message._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
-				return message;
-			});
-		},
-
 		form(): any {
 			return this.$refs.form;
 		}
 	},
 
-	mounted() {
-		this.connection = this.$root.stream.connectToChannel('messaging', {
-			otherparty: this.user ? this.user.id : undefined,
-			group: this.group ? this.group.id : undefined,
-		});
-
-		this.connection.on('message', this.onMessage);
-		this.connection.on('read', this.onRead);
-		this.connection.on('deleted', this.onDeleted);
-
-		if (this.isNaked) {
-			window.addEventListener('scroll', this.onScroll, { passive: true });
-		} else {
-			this.$el.addEventListener('scroll', this.onScroll, { passive: true });
-		}
-
-		document.addEventListener('visibilitychange', this.onVisibilitychange);
+	watch: {
+		$route: 'fetch'
+	},
 
-		this.fetchMessages().then(() => {
-			this.init = false;
-			this.scrollToBottom();
-		});
+	mounted() {
+		this.fetch();
 	},
 
 	beforeDestroy() {
 		this.connection.dispose();
 
-		if (this.isNaked) {
-			window.removeEventListener('scroll', this.onScroll);
-		} else {
-			this.$el.removeEventListener('scroll', this.onScroll);
-		}
+		window.removeEventListener('scroll', this.onScroll);
 
 		document.removeEventListener('visibilitychange', this.onVisibilitychange);
 	},
 
 	methods: {
+		async fetch() {
+			this.fetching = true;
+			if (this.$route.params.user) {
+				const user = await this.$root.api('users/show', parseAcct(this.$route.params.user));
+				this.user = user;
+			} else {
+				const group = await this.$root.api('users/groups/show', { groupId: this.$route.params.group });
+				this.group = group;
+			}
+
+			this.connection = this.$root.stream.connectToChannel('messaging', {
+				otherparty: this.user ? this.user.id : undefined,
+				group: this.group ? this.group.id : undefined,
+			});
+
+			this.connection.on('message', this.onMessage);
+			this.connection.on('read', this.onRead);
+			this.connection.on('deleted', this.onDeleted);
+
+			window.addEventListener('scroll', this.onScroll, { passive: true });
+
+			document.addEventListener('visibilitychange', this.onVisibilitychange);
+
+			this.fetchMessages().then(() => {
+				this.fetching = false;
+				this.scrollToBottom();
+			});
+		},
+
 		onDragover(e) {
 			const isFile = e.dataTransfer.items[0].kind == 'file';
 			const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
@@ -254,11 +248,7 @@ export default Vue.extend({
 		},
 
 		scrollToBottom() {
-			if (this.isNaked) {
-				window.scroll(0, document.body.offsetHeight);
-			} else {
-				this.$el.scrollTop = this.$el.scrollHeight;
-			}
+			window.scroll(0, document.body.offsetHeight);
 		},
 
 		onIndicatorClick() {
@@ -298,139 +288,108 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.mk-messaging-room
-	background var(--messagingRoomBg)
-
-	> .body
-		width 100%
-		max-width 600px
-		margin 0 auto
-		min-height calc(100% - 103px)
-
-		> .init,
-		> .empty
-			width 100%
-			margin 0
-			padding 16px 8px 8px 8px
-			text-align center
-			font-size 0.8em
-			color var(--messagingRoomInfo)
-			opacity 0.5
-
-			[data-icon]
-				margin-right 4px
-
-		> .no-history
-			display block
-			margin 0
-			padding 16px
-			text-align center
-			font-size 0.8em
-			color var(--messagingRoomInfo)
-			opacity 0.5
-
-			[data-icon]
-				margin-right 4px
-
-		> .more
-			display block
-			margin 16px auto
-			padding 0 12px
-			line-height 24px
-			color #fff
-			background rgba(#000, 0.3)
-			border-radius 12px
-
-			&:hover
-				background rgba(#000, 0.4)
-
-			&:active
-				background rgba(#000, 0.5)
-
-			&.fetching
-				cursor wait
-
-			> [data-icon]
-				margin-right 4px
-
-		> .message
-			// something
-
-		> .date
-			display block
-			margin 8px 0
-			text-align center
-
-			&:before
-				content ''
-				display block
-				position absolute
-				height 1px
-				width 90%
-				top 16px
-				left 0
-				right 0
-				margin 0 auto
-				background var(--messagingRoomDateDividerLine)
-
-			> span
-				display inline-block
-				margin 0
-				padding 0 16px
-				//font-weight bold
-				line-height 32px
-				color var(--messagingRoomDateDividerText)
-				background var(--messagingRoomBg)
-
-	> footer
-		position -webkit-sticky
-		position sticky
-		z-index 2
-		bottom 0
-		width 100%
-		max-width 600px
-		margin 0 auto
-		padding 0
-		background var(--messagingRoomBg)
-		background-clip content-box
-
-		> .new-message
-			position absolute
-			top -48px
-			width 100%
-			padding 8px 0
-			text-align center
-
-			> button
-				display inline-block
-				margin 0
-				padding 0 12px 0 30px
-				cursor pointer
-				line-height 32px
-				font-size 12px
-				color var(--primaryForeground)
-				background var(--primary)
-				border-radius 16px
-
-				&:hover
-					background var(--primaryLighten10)
-
-				&:active
-					background var(--primaryDarken10)
-
-				> i
-					position absolute
-					top 0
-					left 10px
-					line-height 32px
-					font-size 16px
-
-.fade-enter-active, .fade-leave-active
-	transition opacity 0.1s
-
-.fade-enter, .fade-leave-to
-	transition opacity 0.5s
-	opacity 0
+<style lang="scss" scoped>
+.mk-messaging-room {
+
+	> .body {
+		width: 100%;
+
+		> .empty {
+			width: 100%;
+			margin: 0;
+			padding: 16px 8px 8px 8px;
+			text-align: center;
+			font-size: 0.8em;
+			opacity: 0.5;
+
+			[data-icon] {
+				margin-right: 4px;
+			}
+		}
+
+		> .no-history {
+			display: block;
+			margin: 0;
+			padding: 16px;
+			text-align: center;
+			font-size: 0.8em;
+			color: var(--messagingRoomInfo);
+			opacity: 0.5;
+
+			[data-icon] {
+				margin-right: 4px;
+			}
+		}
+
+		> .more {
+			display: block;
+			margin: 16px auto;
+			padding: 0 12px;
+			line-height: 24px;
+			color: #fff;
+			background: rgba(#000, 0.3);
+			border-radius: 12px;
+
+			&:hover {
+				background: rgba(#000, 0.4);
+			}
+
+			&:active {
+				background: rgba(#000, 0.5);
+			}
+
+			&.fetching {
+				cursor: wait;
+			}
+
+			> [data-icon] {
+				margin-right: 4px;
+			}
+		}
+
+		> .messages {
+			> ::v-deep * {
+				margin-bottom: 16px;
+			}
+		}
+	}
+
+	> footer {
+		width: 100%;
+
+		> .new-message {
+			position: absolute;
+			top: -48px;
+			width: 100%;
+			padding: 8px 0;
+			text-align: center;
+
+			> button {
+				display: inline-block;
+				margin: 0;
+				padding: 0 12px 0 30px;
+				line-height: 32px;
+				font-size: 12px;
+				border-radius: 16px;
+
+				> i {
+					position: absolute;
+					top: 0;
+					left: 10px;
+					line-height: 32px;
+					font-size: 16px;
+				}
+			}
+		}
+	}
+}
+
+.fade-enter-active, .fade-leave-active {
+	transition: opacity 0.1s;
+}
 
+.fade-enter, .fade-leave-to {
+	transition: opacity 0.5s;
+	opacity: 0;
+}
 </style>
diff --git a/src/client/pages/messaging.vue b/src/client/pages/messaging.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b94e01cad99e7e7d439bb0a054c645f5356f11b6
--- /dev/null
+++ b/src/client/pages/messaging.vue
@@ -0,0 +1,328 @@
+<template>
+<div class="mk-messaging">
+	<portal to="icon"><fa :icon="faComments"/></portal>
+	<portal to="title">{{ $t('messaging') }}</portal>
+
+	<mk-button @click="start" primary class="start"><fa :icon="faPlus"/> {{ $t('startMessaging') }}</mk-button>
+
+	<sequential-entrance class="history" v-if="messages.length > 0" :delay="30">
+		<router-link v-for="(message, i) in messages"
+			class="message _panel"
+			:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
+			:data-is-me="isMe(message)"
+			:data-is-read="message.groupId ? message.reads.includes($store.state.i.id) : message.isRead"
+			:data-index="i"
+			:key="message.id"
+		>
+			<div>
+				<mk-avatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/>
+				<header v-if="message.groupId">
+					<span class="name">{{ message.group.name }}</span>
+					<mk-time :time="message.createdAt"/>
+				</header>
+				<header v-else>
+					<span class="name"><mk-user-name :user="isMe(message) ? message.recipient : message.user"/></span>
+					<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span>
+					<mk-time :time="message.createdAt"/>
+				</header>
+				<div class="body">
+					<p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p>
+				</div>
+			</div>
+		</router-link>
+	</sequential-entrance>
+	<p class="no-history" v-if="!fetching && messages.length == 0">{{ $t('no-history') }}</p>
+	<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faUser, faUsers, faComments, faPlus } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
+import getAcct from '../../misc/acct/render';
+import MkButton from '../components/ui/button.vue';
+import MkUserSelect from '../components/user-select.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		MkButton
+	},
+
+	data() {
+		return {
+			fetching: true,
+			moreFetching: false,
+			messages: [],
+			connection: null,
+			faUser, faUsers, faComments, faPlus
+		};
+	},
+
+	mounted() {
+		this.connection = this.$root.stream.useSharedConnection('messagingIndex');
+
+		this.connection.on('message', this.onMessage);
+		this.connection.on('read', this.onRead);
+
+		this.$root.api('messaging/history', { group: false }).then(userMessages => {
+			this.$root.api('messaging/history', { group: true }).then(groupMessages => {
+				const messages = userMessages.concat(groupMessages);
+				messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
+				this.messages = messages;
+				this.fetching = false;
+			});
+		});
+	},
+
+	beforeDestroy() {
+		this.connection.dispose();
+	},
+
+	methods: {
+		getAcct,
+
+		isMe(message) {
+			return message.userId == this.$store.state.i.id;
+		},
+
+		onMessage(message) {
+			if (message.recipientId) {
+				this.messages = this.messages.filter(m => !(
+					(m.recipientId == message.recipientId && m.userId == message.userId) ||
+					(m.recipientId == message.userId && m.userId == message.recipientId)));
+
+				this.messages.unshift(message);
+			} else if (message.groupId) {
+				this.messages = this.messages.filter(m => m.groupId !== message.groupId);
+				this.messages.unshift(message);
+			}
+		},
+
+		onRead(ids) {
+			for (const id of ids) {
+				const found = this.messages.find(m => m.id == id);
+				if (found) {
+					if (found.recipientId) {
+						found.isRead = true;
+					} else if (found.groupId) {
+						found.reads.push(this.$store.state.i.id);
+					}
+				}
+			}
+		},
+
+		start(ev) {
+			this.$root.menu({
+				items: [{
+					text: this.$t('withUser'),
+					action: () => { this.startUser() }
+				}, {
+					text: this.$t('withGroup'),
+					action: () => { this.startGroup() }
+				}],
+				noCenter: true,
+				source: ev.currentTarget || ev.target,
+			});
+		},
+
+		async startUser() {
+			this.$root.new(MkUserSelect, {}).$once('selected', user => {
+				this.$router.push(`/my/messaging/${getAcct(user)}`);
+			});
+		},
+
+		async startGroup() {
+			const groups1 = await this.$root.api('users/groups/owned');
+			const groups2 = await this.$root.api('users/groups/joined');
+			const { canceled, result: group } = await this.$root.dialog({
+				type: null,
+				title: this.$t('select-group'),
+				select: {
+					items: groups1.concat(groups2).map(group => ({
+						value: group, text: group.name
+					}))
+				},
+				showCancelButton: true
+			});
+			if (canceled) return;
+			this.navigateGroup(group);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-messaging {
+
+	> .start {
+		margin: 0 auto 16px auto;
+	}
+
+	> .history {
+		> .message {
+			display: block;
+			text-decoration: none;
+			margin-bottom: 16px;
+
+			@media (max-width: 500px) {
+				margin-bottom: 8px;
+			}
+
+			* {
+				pointer-events: none;
+				user-select: none;
+			}
+
+			&:hover {
+				.avatar {
+					filter: saturate(200%);
+				}
+			}
+
+			&:active {
+			}
+
+			&[data-is-read],
+			&[data-is-me] {
+				opacity: 0.8;
+			}
+
+			&:not([data-is-me]):not([data-is-read]) {
+				> div {
+					background-image: url("/assets/unread.svg");
+					background-repeat: no-repeat;
+					background-position: 0 center;
+				}
+			}
+
+			&:after {
+				content: "";
+				display: block;
+				clear: both;
+			}
+
+			> div {
+				padding: 20px 30px;
+
+				&:after {
+					content: "";
+					display: block;
+					clear: both;
+				}
+
+				> header {
+					display: flex;
+					align-items: center;
+					margin-bottom: 2px;
+					white-space: nowrap;
+					overflow: hidden;
+
+					> .name {
+						margin: 0;
+						padding: 0;
+						overflow: hidden;
+						text-overflow: ellipsis;
+						font-size: 1em;
+						font-weight: bold;
+						transition: all 0.1s ease;
+					}
+
+					> .username {
+						margin: 0 8px;
+					}
+
+					> .mk-time {
+						margin: 0 0 0 auto;
+					}
+				}
+
+				> .avatar {
+					float: left;
+					width: 54px;
+					height: 54px;
+					margin: 0 16px 0 0;
+					border-radius: 8px;
+					transition: all 0.1s ease;
+				}
+
+				> .body {
+
+					> .text {
+						display: block;
+						margin: 0 0 0 0;
+						padding: 0;
+						overflow: hidden;
+						overflow-wrap: break-word;
+						font-size: 1.1em;
+						color: var(--faceText);
+
+						.me {
+							opacity: 0.7;
+						}
+					}
+
+					> .image {
+						display: block;
+						max-width: 100%;
+						max-height: 512px;
+					}
+				}
+			}
+		}
+	}
+
+	> .no-history {
+		margin: 0;
+		padding: 2em 1em;
+		text-align: center;
+		color: #999;
+		font-weight: 500;
+	}
+
+	> .fetching {
+		margin: 0;
+		padding: 16px;
+		text-align: center;
+		color: var(--text);
+
+		> [data-icon] {
+			margin-right: 4px;
+		}
+	}
+
+	@media (max-width: 400px) {
+		> .search {
+			> .result {
+				> .users {
+					> li {
+						padding: 8px 16px;
+					}
+				}
+			}
+		}
+
+		> .history {
+			> .message {
+				&:not([data-is-me]):not([data-is-read]) {
+					> div {
+						background-image: none;
+						border-left: solid 4px #3aa2dc;
+					}
+				}
+
+				> div {
+					padding: 16px;
+					font-size: 14px;
+
+					> .avatar {
+						margin: 0 12px 0 0;
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/my-antennas/index.antenna.vue b/src/client/pages/my-antennas/index.antenna.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a4b140db1e26154aa7f1b3f3c7bae1a36ed3f253
--- /dev/null
+++ b/src/client/pages/my-antennas/index.antenna.vue
@@ -0,0 +1,174 @@
+<template>
+<div class="shaynizk _section">
+	<div class="_title" v-if="antenna.name">{{ antenna.name }}</div>
+	<div class="_content body">
+		<mk-input v-model="name" style="margin-top: 8px;">
+			<span>{{ $t('name') }}</span>
+		</mk-input>
+		<mk-select v-model="src">
+			<template #label>{{ $t('antennaSource') }}</template>
+			<option value="all">{{ $t('_antennaSources.all') }}</option>
+			<option value="home">{{ $t('_antennaSources.homeTimeline') }}</option>
+			<option value="users">{{ $t('_antennaSources.users') }}</option>
+			<option value="list">{{ $t('_antennaSources.userList') }}</option>
+		</mk-select>
+		<mk-select v-model="userListId" v-if="src === 'list'">
+			<template #label>{{ $t('userList') }}</template>
+			<option v-for="list in userLists" :value="list.id" :key="list.id">{{ list.name }}</option>
+		</mk-select>
+		<mk-textarea v-model="users" v-if="src === 'users'">
+			<span>{{ $t('users') }}</span>
+			<template #desc>{{ $t('antennaUsersDescription') }} <button class="_textButton" @click="addUser">{{ $t('addUser') }}</button></template>
+		</mk-textarea>
+		<mk-switch v-model="withReplies">{{ $t('withReplies') }}</mk-switch>
+		<mk-textarea v-model="keywords">
+			<span>{{ $t('antennaKeywords') }}</span>
+			<template #desc>{{ $t('antennaKeywordsDescription') }}</template>
+		</mk-textarea>
+		<mk-switch v-model="caseSensitive">{{ $t('caseSensitive') }}</mk-switch>
+		<mk-switch v-model="withFile">{{ $t('withFileAntenna') }}</mk-switch>
+		<mk-switch v-model="notify">{{ $t('notifyAntenna') }}</mk-switch>
+	</div>
+	<div class="_footer">
+		<mk-button inline @click="saveAntenna()" primary><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+		<mk-button inline @click="deleteAntenna()" v-if="antenna.id != null"><fa :icon="faTrash"/> {{ $t('delete') }}</mk-button>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faSave, faTrash } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../../i18n';
+import MkButton from '../../components/ui/button.vue';
+import MkInput from '../../components/ui/input.vue';
+import MkTextarea from '../../components/ui/textarea.vue';
+import MkSelect from '../../components/ui/select.vue';
+import MkSwitch from '../../components/ui/switch.vue';
+import MkUserSelect from '../../components/user-select.vue';
+import getAcct from '../../../misc/acct/render';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		MkButton, MkInput, MkTextarea, MkSelect, MkSwitch
+	},
+
+	props: {
+		antenna: {
+			type: Object,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			name: '',
+			src: '',
+			userListId: null,
+			users: '',
+			keywords: '',
+			caseSensitive: false,
+			withReplies: false,
+			withFile: false,
+			notify: false,
+			userLists: null,
+			faSave, faTrash
+		};
+	},
+
+	watch: {
+		async src() {
+			if (this.src === 'list' && this.userLists === null) {
+				this.userLists = await this.$root.api('users/lists/list');
+			}
+		}
+	},
+
+	created() {
+		this.name = this.antenna.name;
+		this.src = this.antenna.src;
+		this.userListId = this.antenna.userListId;
+		this.users = this.antenna.users.join('\n');
+		this.keywords = this.antenna.keywords.map(x => x.join(' ')).join('\n');
+		this.caseSensitive = this.antenna.caseSensitive;
+		this.withReplies = this.antenna.withReplies;
+		this.withFile = this.antenna.withFile;
+		this.notify = this.antenna.notify;
+	},
+
+	methods: {
+		async saveAntenna() {
+			if (this.antenna.id == null) {
+				await this.$root.api('antennas/create', {
+					name: this.name,
+					src: this.src,
+					userListId: this.userListId,
+					withReplies: this.withReplies,
+					withFile: this.withFile,
+					notify: this.notify,
+					caseSensitive: this.caseSensitive,
+					users: this.users.trim().split('\n').map(x => x.trim()),
+					keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' '))
+				});
+				this.$emit('created');
+			} else {
+				await this.$root.api('antennas/update', {
+					antennaId: this.antenna.id,
+					name: this.name,
+					src: this.src,
+					userListId: this.userListId,
+					withReplies: this.withReplies,
+					withFile: this.withFile,
+					notify: this.notify,
+					caseSensitive: this.caseSensitive,
+					users: this.users.trim().split('\n').map(x => x.trim()),
+					keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' '))
+				});
+			}
+
+			this.$root.dialog({
+				type: 'success',
+				iconOnly: true, autoClose: true
+			});
+		},
+
+		async deleteAntenna() {
+			const { canceled } = await this.$root.dialog({
+				type: 'warning',
+				text: this.$t('removeAreYouSure', { x: this.antenna.name }),
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			await this.$root.api('antennas/delete', {
+				antennaId: this.antenna.id,
+			});
+
+			this.$root.dialog({
+				type: 'success',
+				iconOnly: true, autoClose: true
+			});
+			this.$emit('deleted');
+		},
+
+		addUser() {
+			this.$root.new(MkUserSelect, {}).$once('selected', user => {
+				this.users = this.users.trim();
+				this.users += '\n@' + getAcct(user);
+				this.users = this.users.trim();
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.shaynizk {
+	> .body {
+		max-height: 250px;
+		overflow: auto;
+	}
+}
+</style>
diff --git a/src/client/pages/my-antennas/index.vue b/src/client/pages/my-antennas/index.vue
new file mode 100644
index 0000000000000000000000000000000000000000..3a9a11b5418f5568abaacfbcd782af3045ff6588
--- /dev/null
+++ b/src/client/pages/my-antennas/index.vue
@@ -0,0 +1,80 @@
+<template>
+<div class="ieepwinx">
+	<portal to="icon"><fa :icon="faSatellite"/></portal>
+	<portal to="title">{{ $t('manageAntennas') }}</portal>
+
+	<mk-button @click="create" primary class="add"><fa :icon="faPlus"/> {{ $t('createAntenna') }}</mk-button>
+
+	<x-antenna v-if="draft" :antenna="draft" @created="onAntennaCreated" style="margin-bottom: var(--margin);"/>
+
+	<mk-pagination :pagination="pagination" #default="{items}" class="antennas" ref="list">
+		<x-antenna v-for="(antenna, i) in items" :key="antenna.id" :data-index="i" :antenna="antenna" @created="onAntennaDeleted"/>
+	</mk-pagination>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faSatellite, faPlus } from '@fortawesome/free-solid-svg-icons';
+import MkPagination from '../../components/ui/pagination.vue';
+import MkButton from '../../components/ui/button.vue';
+import XAntenna from './index.antenna.vue';
+
+export default Vue.extend({
+	metaInfo() {
+		return {
+			title: this.$t('manageAntennas') as string,
+		};
+	},
+
+	components: {
+		MkPagination,
+		MkButton,
+		XAntenna,
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'antennas/list',
+				limit: 10,
+			},
+			draft: null,
+			faSatellite, faPlus
+		};
+	},
+
+	methods: {
+		create() {
+			this.draft = {
+				name: '',
+				src: 'all',
+				userListId: null,
+				users: [],
+				keywords: [],
+				withReplies: false,
+				caseSensitive: false,
+				withFile: false,
+				notify: false
+			};
+		},
+
+		onAntennaCreated() {
+			this.$refs.list.reload();
+			this.draft = null;
+		},
+
+		onAntennaDeleted() {
+			this.$refs.list.reload();
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ieepwinx {
+	> .add {
+		margin: 0 auto 16px auto;
+	}
+}
+</style>
diff --git a/src/client/pages/my-lists/index.vue b/src/client/pages/my-lists/index.vue
new file mode 100644
index 0000000000000000000000000000000000000000..6c4b46e85cd220f6c66e0571e70eb8051fb60554
--- /dev/null
+++ b/src/client/pages/my-lists/index.vue
@@ -0,0 +1,75 @@
+<template>
+<div class="qkcjvfiv">
+	<portal to="icon"><fa :icon="faListUl"/></portal>
+	<portal to="title">{{ $t('manageLists') }}</portal>
+
+	<mk-button @click="create" primary class="add"><fa :icon="faPlus"/> {{ $t('createList') }}</mk-button>
+
+	<mk-pagination :pagination="pagination" #default="{items}" class="lists" ref="list">
+		<div class="list _panel" v-for="(list, i) in items" :key="list.id" :data-index="i">
+			<router-link :to="`/lists/${ list.id }`">{{ list.name }}</router-link>
+		</div>
+	</mk-pagination>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faListUl, faPlus } from '@fortawesome/free-solid-svg-icons';
+import MkPagination from '../../components/ui/pagination.vue';
+import MkButton from '../../components/ui/button.vue';
+
+export default Vue.extend({
+	metaInfo() {
+		return {
+			title: this.$t('manageLists') as string,
+		};
+	},
+
+	components: {
+		MkPagination,
+		MkButton,
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'users/lists/list',
+				limit: 10,
+			},
+			faListUl, faPlus
+		};
+	},
+
+	methods: {
+		async create() {
+			const { canceled, result: name } = await this.$root.dialog({
+				title: this.$t('enterListName'),
+				input: true
+			});
+			if (canceled) return;
+			await this.$root.api('users/lists/create', { name: name });
+			this.$refs.list.reload();
+			this.$root.dialog({
+				type: 'success',
+				iconOnly: true, autoClose: true
+			});
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.qkcjvfiv {
+	> .add {
+		margin: 0 auto 16px auto;
+	}
+
+	> .lists {
+		> .list {
+			display: flex;
+			padding: 16px;
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/my-lists/list.vue b/src/client/pages/my-lists/list.vue
new file mode 100644
index 0000000000000000000000000000000000000000..8899b4c44d3b771b4e8c2fc19aa99f811ea46116
--- /dev/null
+++ b/src/client/pages/my-lists/list.vue
@@ -0,0 +1,163 @@
+<template>
+<div class="mk-list-page">
+	<transition name="zoom" mode="out-in">
+		<div v-if="list" :key="list.id" class="_section list">
+			<div class="_title">{{ list.name }}</div>
+			<div class="_content">
+				<div class="users">
+					<div class="user" v-for="(user, i) in users" :key="user.id" :data-index="i">
+						<mk-avatar :user="user" class="avatar"/>
+						<div class="body">
+							<mk-user-name :user="user" class="name"/>
+							<mk-acct :user="user" class="acct"/>
+						</div>
+						<div class="action">
+							<button class="_button" @click="removeUser(user)"><fa :icon="faTimes"/></button>
+						</div>
+					</div>
+				</div>
+			</div>
+			<div class="_footer">
+				<mk-button inline @click="renameList()">{{ $t('renameList') }}</mk-button>
+				<mk-button inline @click="deleteList()">{{ $t('deleteList') }}</mk-button>
+			</div>
+		</div>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faTimes } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../../i18n';
+import Progress from '../../scripts/loading';
+import MkButton from '../../components/ui/button.vue';
+
+export default Vue.extend({
+	i18n,
+
+	metaInfo() {
+		return {
+			title: this.list ? `${this.list.name} | ${this.$t('manageLists')}` : this.$t('manageLists')
+		};
+	},
+
+	components: {
+		MkButton
+	},
+
+	data() {
+		return {
+			list: null,
+			users: [],
+			faTimes
+		};
+	},
+
+	watch: {
+		$route: 'fetch'
+	},
+
+	created() {
+		this.fetch();
+	},
+
+	methods: {
+		fetch() {
+			Progress.start();
+			this.$root.api('users/lists/show', {
+				listId: this.$route.params.list
+			}).then(list => {
+				this.list = list;
+				this.$root.api('users/show', {
+					userIds: this.list.userIds
+				}).then(users => {
+					this.users = users;
+					Progress.done();
+				});
+			});
+		},
+
+		removeUser(user) {
+			this.$root.api('users/lists/pull', {
+				listId: this.list.id,
+				userId: user.id
+			}).then(() => {
+				this.users = this.users.filter(x => x.id !== user.id);
+			});
+		},
+
+		async renameList() {
+			const { canceled, result: name } = await this.$root.dialog({
+				title: this.$t('enterListName'),
+				input: {
+					default: this.list.name
+				}
+			});
+			if (canceled) return;
+
+			await this.$root.api('users/lists/update', {
+				listId: this.list.id,
+				name: name
+			});
+
+			this.list.name = name;
+		},
+
+		async deleteList() {
+			const { canceled } = await this.$root.dialog({
+				type: 'warning',
+				text: this.$t('deleteListConfirm', { list: this.list.name }),
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			await this.$root.api('users/lists/delete', {
+				listId: this.list.id
+			});
+			this.$root.dialog({
+				type: 'success',
+				iconOnly: true, autoClose: true
+			});
+			this.$router.push('/my/lists');
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-list-page {
+	> .list {
+		> ._content {
+			max-height: 400px;
+			overflow: auto;
+
+			> .users {
+				> .user {
+					display: flex;
+					align-items: center;
+
+					> .avatar {
+						width: 50px;
+						height: 50px;
+					}
+
+					> .body {
+						flex: 1;
+						padding: 8px;
+
+						> .name {
+							display: block;
+							font-weight: bold;
+						}
+
+						> .acct {
+							opacity: 0.5;
+						}
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/note.vue b/src/client/pages/note.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e7cdf19f81fa96918cb4b7046a47b18bf1f92487
--- /dev/null
+++ b/src/client/pages/note.vue
@@ -0,0 +1,55 @@
+<template>
+<div class="mk-note-page">
+	<transition name="zoom" mode="out-in">
+		<x-note v-if="note" :note="note" :key="note.id" :detail="true"/>
+		<div v-else-if="error">
+			<mk-error @retry="fetch()"/>
+		</div>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../i18n';
+import Progress from '../scripts/loading';
+import XNote from '../components/note.vue';
+
+export default Vue.extend({
+	i18n,
+	metaInfo() {
+		return {
+			title: this.$t('note') as string
+		};
+	},
+	components: {
+		XNote
+	},
+	data() {
+		return {
+			note: null,
+			error: null,
+		};
+	},
+	watch: {
+		$route: 'fetch'
+	},
+	created() {
+		this.fetch();
+	},
+	methods: {
+		fetch() {
+			Progress.start();
+			this.$root.api('notes/show', {
+				noteId: this.$route.params.note
+			}).then(note => {
+				this.note = note;
+			}).catch(e => {
+				this.error = e;
+			}).finally(() => {
+				Progress.done();
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/page-editor/els/page-editor.el.button.vue b/src/client/pages/page-editor/els/page-editor.el.button.vue
new file mode 100644
index 0000000000000000000000000000000000000000..8e74124b7956b694e643f5a1ad400deb23feb728
--- /dev/null
+++ b/src/client/pages/page-editor/els/page-editor.el.button.vue
@@ -0,0 +1,83 @@
+<template>
+<x-container @remove="() => $emit('remove')" :draggable="true">
+	<template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.button') }}</template>
+
+	<section class="xfhsjczc">
+		<mk-input v-model="value.text"><span>{{ $t('_pages.blocks._button.text') }}</span></mk-input>
+		<mk-switch v-model="value.primary"><span>{{ $t('_pages.blocks._button.colored') }}</span></mk-switch>
+		<mk-select v-model="value.action">
+			<template #label>{{ $t('_pages.blocks._button.action') }}</template>
+			<option value="dialog">{{ $t('_pages.blocks._button._action.dialog') }}</option>
+			<option value="resetRandom">{{ $t('_pages.blocks._button._action.resetRandom') }}</option>
+			<option value="pushEvent">{{ $t('_pages.blocks._button._action.pushEvent') }}</option>
+		</mk-select>
+		<template v-if="value.action === 'dialog'">
+			<mk-input v-model="value.content"><span>{{ $t('_pages.blocks._button._action._dialog.content') }}</span></mk-input>
+		</template>
+		<template v-else-if="value.action === 'pushEvent'">
+			<mk-input v-model="value.event"><span>{{ $t('_pages.blocks._button._action._pushEvent.event') }}</span></mk-input>
+			<mk-input v-model="value.message"><span>{{ $t('_pages.blocks._button._action._pushEvent.message') }}</span></mk-input>
+			<mk-select v-model="value.var">
+				<template #label>{{ $t('_pages.blocks._button._action._pushEvent.variable') }}</template>
+				<option :value="null">{{ $t('_pages.blocks._button._action._pushEvent.no-variable') }}</option>
+				<option v-for="v in aiScript.getVarsByType()" :value="v.name">{{ v.name }}</option>
+				<optgroup :label="$t('_pages.script.pageVariables')">
+					<option v-for="v in aiScript.getPageVarsByType()" :value="v">{{ v }}</option>
+				</optgroup>
+				<optgroup :label="$t('_pages.script.enviromentVariables')">
+					<option v-for="v in aiScript.getEnvVarsByType()" :value="v">{{ v }}</option>
+				</optgroup>
+			</mk-select>
+		</template>
+	</section>
+</x-container>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faBolt } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../../../i18n';
+import XContainer from '../page-editor.container.vue';
+import MkSelect from '../../../components/ui/select.vue';
+import MkInput from '../../../components/ui/input.vue';
+import MkSwitch from '../../../components/ui/switch.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XContainer, MkSelect, MkInput, MkSwitch
+	},
+
+	props: {
+		value: {
+			required: true
+		},
+		aiScript: {
+			required: true,
+		},
+	},
+
+	data() {
+		return {
+			faBolt
+		};
+	},
+
+	created() {
+		if (this.value.text == null) Vue.set(this.value, 'text', '');
+		if (this.value.action == null) Vue.set(this.value, 'action', 'dialog');
+		if (this.value.content == null) Vue.set(this.value, 'content', null);
+		if (this.value.event == null) Vue.set(this.value, 'event', null);
+		if (this.value.message == null) Vue.set(this.value, 'message', null);
+		if (this.value.primary == null) Vue.set(this.value, 'primary', false);
+		if (this.value.var == null) Vue.set(this.value, 'var', null);
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.xfhsjczc {
+	padding: 0 16px 0 16px;
+}
+</style>
diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.counter.vue b/src/client/pages/page-editor/els/page-editor.el.counter.vue
similarity index 50%
rename from src/client/app/common/views/pages/page-editor/els/page-editor.el.counter.vue
rename to src/client/pages/page-editor/els/page-editor.el.counter.vue
index 4fc2aac8fc2d2f7774077e647bd60d0eb18d7b1e..d9a4ddddee3e18d8beac15ffb84168d11c616071 100644
--- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.counter.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.counter.vue
@@ -1,11 +1,11 @@
 <template>
 <x-container @remove="() => $emit('remove')" :draggable="true">
-	<template #header><fa :icon="faBolt"/> {{ $t('blocks.counter') }}</template>
+	<template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.counter') }}</template>
 
 	<section style="padding: 0 16px 0 16px;">
-		<ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._counter.name') }}</span></ui-input>
-		<ui-input v-model="value.text"><span>{{ $t('blocks._counter.text') }}</span></ui-input>
-		<ui-input v-model="value.inc" type="number"><span>{{ $t('blocks._counter.inc') }}</span></ui-input>
+		<mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._counter.name') }}</span></mk-input>
+		<mk-input v-model="value.text"><span>{{ $t('_pages.blocks._counter.text') }}</span></mk-input>
+		<mk-input v-model="value.inc" type="number"><span>{{ $t('_pages.blocks._counter.inc') }}</span></mk-input>
 	</section>
 </x-container>
 </template>
@@ -13,14 +13,15 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../../../i18n';
+import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
+import MkInput from '../../../components/ui/input.vue';
 
 export default Vue.extend({
-	i18n: i18n('pages'),
+	i18n,
 
 	components: {
-		XContainer
+		XContainer, MkInput
 	},
 
 	props: {
diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.if.vue b/src/client/pages/page-editor/els/page-editor.el.if.vue
similarity index 76%
rename from src/client/app/common/views/pages/page-editor/els/page-editor.el.if.vue
rename to src/client/pages/page-editor/els/page-editor.el.if.vue
index a3743d89d691b110cb17a7ebdb4da9eb9a3668bc..3c545a7ddc36ade88dfc556c0c90a314ed7b7684 100644
--- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.if.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.if.vue
@@ -1,6 +1,6 @@
 <template>
 <x-container @remove="() => $emit('remove')" :draggable="true">
-	<template #header><fa :icon="faQuestion"/> {{ $t('blocks.if') }}</template>
+	<template #header><fa :icon="faQuestion"/> {{ $t('_pages.blocks.if') }}</template>
 	<template #func>
 		<button @click="add()">
 			<fa :icon="faPlus"/>
@@ -8,16 +8,16 @@
 	</template>
 
 	<section class="romcojzs">
-		<ui-select v-model="value.var">
-			<template #label>{{ $t('blocks._if.variable') }}</template>
+		<mk-select v-model="value.var">
+			<template #label>{{ $t('_pages.blocks._if.variable') }}</template>
 			<option v-for="v in aiScript.getVarsByType('boolean')" :value="v.name">{{ v.name }}</option>
-			<optgroup :label="$t('script.pageVariables')">
+			<optgroup :label="$t('_pages.script.pageVariables')">
 				<option v-for="v in aiScript.getPageVarsByType('boolean')" :value="v">{{ v }}</option>
 			</optgroup>
-			<optgroup :label="$t('script.enviromentVariables')">
+			<optgroup :label="$t('_pages.script.enviromentVariables')">
 				<option v-for="v in aiScript.getEnvVarsByType('boolean')" :value="v">{{ v }}</option>
 			</optgroup>
-		</ui-select>
+		</mk-select>
 
 		<x-blocks class="children" v-model="value.children" :ai-script="aiScript"/>
 	</section>
@@ -28,14 +28,15 @@
 import Vue from 'vue';
 import { v4 as uuid } from 'uuid';
 import { faPlus, faQuestion } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../../../i18n';
+import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
+import MkSelect from '../../../components/ui/select.vue';
 
 export default Vue.extend({
-	i18n: i18n('pages'),
+	i18n,
 
 	components: {
-		XContainer
+		XContainer, MkSelect
 	},
 
 	inject: ['getPageBlockList'],
@@ -83,8 +84,8 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.romcojzs
-	padding 0 16px 16px 16px
-
+<style lang="scss" scoped>
+.romcojzs {
+	padding: 0 16px 16px 16px;
+}
 </style>
diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.image.vue b/src/client/pages/page-editor/els/page-editor.el.image.vue
similarity index 66%
rename from src/client/app/common/views/pages/page-editor/els/page-editor.el.image.vue
rename to src/client/pages/page-editor/els/page-editor.el.image.vue
index e2e72b04c24ef6b3c915d37a4bdb8364dd91c5f3..e22701e5c0a1485c97295b2e7efc876ecbda1b39 100644
--- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.image.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.image.vue
@@ -1,6 +1,6 @@
 <template>
 <x-container @remove="() => $emit('remove')" :draggable="true">
-	<template #header><fa :icon="faImage"/> {{ $t('blocks.image') }}</template>
+	<template #header><fa :icon="faImage"/> {{ $t('_pages.blocks.image') }}</template>
 	<template #func>
 		<button @click="choose()">
 			<fa :icon="faFolderOpen"/>
@@ -8,7 +8,7 @@
 	</template>
 
 	<section class="oyyftmcf">
-		<x-file-thumbnail class="preview" v-if="file" :file="file" :detail="true" fit="contain" @click="choose()"/>
+		<mk-file-thumbnail class="preview" v-if="file" :file="file" :detail="true" fit="contain" @click="choose()"/>
 	</section>
 </x-container>
 </template>
@@ -17,15 +17,16 @@
 import Vue from 'vue';
 import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
 import { faImage, faFolderOpen } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../../../../../i18n';
+import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
-import XFileThumbnail from '../../../components/drive-file-thumbnail.vue';
+import MkFileThumbnail from '../../../components/drive-file-thumbnail.vue';
+import { selectDriveFile } from '../../../scripts/select-drive-file';
 
 export default Vue.extend({
-	i18n: i18n('pages'),
+	i18n,
 
 	components: {
-		XContainer, XFileThumbnail
+		XContainer, MkFileThumbnail
 	},
 
 	props: {
@@ -59,9 +60,7 @@ export default Vue.extend({
 
 	methods: {
 		async choose() {
-			this.$chooseDriveFile({
-				multiple: false
-			}).then(file => {
+			selectDriveFile(this.$root, false).then(file => {
 				this.file = file;
 				this.value.fileId = file.id;
 			});
@@ -70,9 +69,10 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.oyyftmcf
-	> .preview
-		height 150px
-
+<style lang="scss" scoped>
+.oyyftmcf {
+	> .preview {
+		height: 150px;
+	}
+}
 </style>
diff --git a/src/client/pages/page-editor/els/page-editor.el.number-input.vue b/src/client/pages/page-editor/els/page-editor.el.number-input.vue
new file mode 100644
index 0000000000000000000000000000000000000000..76dd254464aaf553dd11595c5194151ee533deed
--- /dev/null
+++ b/src/client/pages/page-editor/els/page-editor.el.number-input.vue
@@ -0,0 +1,43 @@
+<template>
+<x-container @remove="() => $emit('remove')" :draggable="true">
+	<template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.numberInput') }}</template>
+
+	<section style="padding: 0 16px 0 16px;">
+		<mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._numberInput.name') }}</span></mk-input>
+		<mk-input v-model="value.text"><span>{{ $t('_pages.blocks._numberInput.text') }}</span></mk-input>
+		<mk-input v-model="value.default" type="number"><span>{{ $t('_pages.blocks._numberInput.default') }}</span></mk-input>
+	</section>
+</x-container>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../../../i18n';
+import XContainer from '../page-editor.container.vue';
+import MkInput from '../../../components/ui/input.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XContainer, MkInput
+	},
+
+	props: {
+		value: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			faBolt, faMagic
+		};
+	},
+
+	created() {
+		if (this.value.name == null) Vue.set(this.value, 'name', '');
+	},
+});
+</script>
diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.post.vue b/src/client/pages/page-editor/els/page-editor.el.post.vue
similarity index 65%
rename from src/client/app/common/views/pages/page-editor/els/page-editor.el.post.vue
rename to src/client/pages/page-editor/els/page-editor.el.post.vue
index fc2f5f90320e1abd17b71040c2fa0f1f501c415a..10ec885d0f0c4b3bbc4a6f54065b02753c523eb5 100644
--- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.post.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.post.vue
@@ -1,9 +1,9 @@
 <template>
 <x-container @remove="() => $emit('remove')" :draggable="true">
-	<template #header><fa :icon="faPaperPlane"/> {{ $t('blocks.post') }}</template>
+	<template #header><fa :icon="faPaperPlane"/> {{ $t('_pages.blocks.post') }}</template>
 
 	<section style="padding: 0 16px 16px 16px;">
-		<ui-textarea v-model="value.text">{{ $t('blocks._post.text') }}</ui-textarea>
+		<mk-textarea v-model="value.text">{{ $t('_pages.blocks._post.text') }}</mk-textarea>
 	</section>
 </x-container>
 </template>
@@ -11,14 +11,15 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faPaperPlane } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../../../../../i18n';
+import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
+import MkTextarea from '../../../components/ui/textarea.vue';
 
 export default Vue.extend({
-	i18n: i18n('pages'),
+	i18n,
 
 	components: {
-		XContainer
+		XContainer, MkTextarea
 	},
 
 	props: {
diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.radio-button.vue b/src/client/pages/page-editor/els/page-editor.el.radio-button.vue
similarity index 53%
rename from src/client/app/common/views/pages/page-editor/els/page-editor.el.radio-button.vue
rename to src/client/pages/page-editor/els/page-editor.el.radio-button.vue
index 3401c46f47e43725369d7a844d0728576f9fc61c..8d404ec0df79c3a9e66f74220259eab225617ad6 100644
--- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.radio-button.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.radio-button.vue
@@ -1,12 +1,12 @@
 <template>
 <x-container @remove="() => $emit('remove')" :draggable="true">
-	<template #header><fa :icon="faBolt"/> {{ $t('blocks.radioButton') }}</template>
+	<template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.radioButton') }}</template>
 
 	<section style="padding: 0 16px 16px 16px;">
-		<ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._radioButton.name') }}</span></ui-input>
-		<ui-input v-model="value.title"><span>{{ $t('blocks._radioButton.title') }}</span></ui-input>
-		<ui-textarea v-model="values"><span>{{ $t('blocks._radioButton.values') }}</span></ui-textarea>
-		<ui-input v-model="value.default"><span>{{ $t('blocks._radioButton.default') }}</span></ui-input>
+		<mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._radioButton.name') }}</span></mk-input>
+		<mk-input v-model="value.title"><span>{{ $t('_pages.blocks._radioButton.title') }}</span></mk-input>
+		<mk-textarea v-model="values"><span>{{ $t('_pages.blocks._radioButton.values') }}</span></mk-textarea>
+		<mk-input v-model="value.default"><span>{{ $t('_pages.blocks._radioButton.default') }}</span></mk-input>
 	</section>
 </x-container>
 </template>
@@ -14,35 +14,32 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../../../i18n';
+import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
+import MkTextarea from '../../../components/ui/textarea.vue';
+import MkInput from '../../../components/ui/input.vue';
 
 export default Vue.extend({
-	i18n: i18n('pages'),
-
+	i18n,
 	components: {
-		XContainer
+		XContainer, MkTextarea, MkInput
 	},
-
 	props: {
 		value: {
 			required: true
 		},
 	},
-
 	data() {
 		return {
 			values: '',
 			faBolt, faMagic
 		};
 	},
-
 	watch: {
 		values() {
 			Vue.set(this.value, 'values', this.values.split('\n'));
 		}
 	},
-
 	created() {
 		if (this.value.name == null) Vue.set(this.value, 'name', '');
 		if (this.value.title == null) Vue.set(this.value, 'title', '');
diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.section.vue b/src/client/pages/page-editor/els/page-editor.el.section.vue
similarity index 93%
rename from src/client/app/common/views/pages/page-editor/els/page-editor.el.section.vue
rename to src/client/pages/page-editor/els/page-editor.el.section.vue
index 0f8f8509479e48d7fa97089a198bc92296532b1c..d405ee196566a5fbe857a33488f10b5207b66c18 100644
--- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.section.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.section.vue
@@ -21,11 +21,11 @@ import Vue from 'vue';
 import { v4 as uuid } from 'uuid';
 import { faPlus, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
 import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../../../../../i18n';
+import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
 
 export default Vue.extend({
-	i18n: i18n('pages'),
+	i18n,
 
 	components: {
 		XContainer
@@ -95,9 +95,10 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.ilrvjyvi
-	> .children
-		padding 16px
-
+<style lang="scss" scoped>
+.ilrvjyvi {
+	> .children {
+		padding: 16px;
+	}
+}
 </style>
diff --git a/src/client/pages/page-editor/els/page-editor.el.switch.vue b/src/client/pages/page-editor/els/page-editor.el.switch.vue
new file mode 100644
index 0000000000000000000000000000000000000000..8f169c3d23cee06e59dd5a7d9322a7cf1261f665
--- /dev/null
+++ b/src/client/pages/page-editor/els/page-editor.el.switch.vue
@@ -0,0 +1,50 @@
+<template>
+<x-container @remove="() => $emit('remove')" :draggable="true">
+	<template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.switch') }}</template>
+
+	<section class="kjuadyyj">
+		<mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._switch.name') }}</span></mk-input>
+		<mk-input v-model="value.text"><span>{{ $t('_pages.blocks._switch.text') }}</span></mk-input>
+		<mk-switch v-model="value.default"><span>{{ $t('_pages.blocks._switch.default') }}</span></mk-switch>
+	</section>
+</x-container>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../../../i18n';
+import XContainer from '../page-editor.container.vue';
+import MkSwitch from '../../../components/ui/switch.vue';
+import MkInput from '../../../components/ui/input.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XContainer, MkSwitch, MkInput
+	},
+
+	props: {
+		value: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			faBolt, faMagic
+		};
+	},
+
+	created() {
+		if (this.value.name == null) Vue.set(this.value, 'name', '');
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.kjuadyyj {
+	padding: 0 16px 16px 16px;
+}
+</style>
diff --git a/src/client/pages/page-editor/els/page-editor.el.text-input.vue b/src/client/pages/page-editor/els/page-editor.el.text-input.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7c9e3d6a0e34ad5fa9c30644a9ca826686f57c70
--- /dev/null
+++ b/src/client/pages/page-editor/els/page-editor.el.text-input.vue
@@ -0,0 +1,43 @@
+<template>
+<x-container @remove="() => $emit('remove')" :draggable="true">
+	<template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.textInput') }}</template>
+
+	<section style="padding: 0 16px 0 16px;">
+		<mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._textInput.name') }}</span></mk-input>
+		<mk-input v-model="value.text"><span>{{ $t('_pages.blocks._textInput.text') }}</span></mk-input>
+		<mk-input v-model="value.default" type="text"><span>{{ $t('_pages.blocks._textInput.default') }}</span></mk-input>
+	</section>
+</x-container>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../../../i18n';
+import XContainer from '../page-editor.container.vue';
+import MkInput from '../../../components/ui/input.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XContainer, MkInput
+	},
+
+	props: {
+		value: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			faBolt, faMagic
+		};
+	},
+
+	created() {
+		if (this.value.name == null) Vue.set(this.value, 'name', '');
+	},
+});
+</script>
diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.text.vue b/src/client/pages/page-editor/els/page-editor.el.text.vue
similarity index 57%
rename from src/client/app/common/views/pages/page-editor/els/page-editor.el.text.vue
rename to src/client/pages/page-editor/els/page-editor.el.text.vue
index c09f9cc1cfba4295287b7403612fdbd8f65834f6..00b6cd8a361fd29c48017a505e49c19b36beae83 100644
--- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.text.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.text.vue
@@ -1,6 +1,6 @@
 <template>
 <x-container @remove="() => $emit('remove')" :draggable="true">
-	<template #header><fa :icon="faAlignLeft"/> {{ $t('blocks.text') }}</template>
+	<template #header><fa :icon="faAlignLeft"/> {{ $t('_pages.blocks.text') }}</template>
 
 	<section class="ihymsbbe">
 		<textarea v-model="value.text"></textarea>
@@ -11,11 +11,11 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faAlignLeft } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../../../i18n';
+import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
 
 export default Vue.extend({
-	i18n: i18n('pages'),
+	i18n,
 
 	components: {
 		XContainer
@@ -39,20 +39,22 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.ihymsbbe
-	> textarea
-		display block
-		-webkit-appearance none
-		-moz-appearance none
-		appearance none
-		width 100%
-		min-width 100%
-		min-height 150px
-		border none
-		box-shadow none
-		padding 16px
-		background transparent
-		color var(--text)
-		font-size 14px
+<style lang="scss" scoped>
+.ihymsbbe {
+	> textarea {
+		display: block;
+		-webkit-appearance: none;
+		-moz-appearance: none;
+		appearance: none;
+		width: 100%;
+		min-width: 100%;
+		min-height: 150px;
+		border: none;
+		box-shadow: none;
+		padding: 16px;
+		background: transparent;
+		color: var(--fg);
+		font-size: 14px;
+	}
+}
 </style>
diff --git a/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue b/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue
new file mode 100644
index 0000000000000000000000000000000000000000..8081e706bcccbc375892a683b0b2367d0de2c74d
--- /dev/null
+++ b/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue
@@ -0,0 +1,44 @@
+<template>
+<x-container @remove="() => $emit('remove')" :draggable="true">
+	<template #header><fa :icon="faBolt"/> {{ $t('_pages.blocks.textareaInput') }}</template>
+
+	<section style="padding: 0 16px 16px 16px;">
+		<mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._textareaInput.name') }}</span></mk-input>
+		<mk-input v-model="value.text"><span>{{ $t('_pages.blocks._textareaInput.text') }}</span></mk-input>
+		<mk-textarea v-model="value.default"><span>{{ $t('_pages.blocks._textareaInput.default') }}</span></mk-textarea>
+	</section>
+</x-container>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../../../i18n';
+import XContainer from '../page-editor.container.vue';
+import MkTextarea from '../../../components/ui/textarea.vue';
+import MkInput from '../../../components/ui/input.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XContainer, MkTextarea, MkInput
+	},
+
+	props: {
+		value: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			faBolt, faMagic
+		};
+	},
+
+	created() {
+		if (this.value.name == null) Vue.set(this.value, 'name', '');
+	},
+});
+</script>
diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea.vue b/src/client/pages/page-editor/els/page-editor.el.textarea.vue
similarity index 57%
rename from src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea.vue
rename to src/client/pages/page-editor/els/page-editor.el.textarea.vue
index a0cc1966e814a5b880c8058a0b8e15d66018d34c..fd75849684c3cf56242b5073b76bd923311a0be2 100644
--- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.textarea.vue
@@ -1,6 +1,6 @@
 <template>
 <x-container @remove="() => $emit('remove')" :draggable="true">
-	<template #header><fa :icon="faAlignLeft"/> {{ $t('blocks.textarea') }}</template>
+	<template #header><fa :icon="faAlignLeft"/> {{ $t('_pages.blocks.textarea') }}</template>
 
 	<section class="ihymsbbe">
 		<textarea v-model="value.text"></textarea>
@@ -11,11 +11,11 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faAlignLeft } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../../../i18n';
+import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
 
 export default Vue.extend({
-	i18n: i18n('pages'),
+	i18n,
 
 	components: {
 		XContainer
@@ -39,20 +39,22 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.ihymsbbe
-	> textarea
-		display block
-		-webkit-appearance none
-		-moz-appearance none
-		appearance none
-		width 100%
-		min-width 100%
-		min-height 150px
-		border none
-		box-shadow none
-		padding 16px
-		background transparent
-		color var(--text)
-		font-size 14px
+<style lang="scss" scoped>
+.ihymsbbe {
+	> textarea {
+		display: block;
+		-webkit-appearance: none;
+		-moz-appearance: none;
+		appearance: none;
+		width: 100%;
+		min-width: 100%;
+		min-height: 150px;
+		border: none;
+		box-shadow: none;
+		padding: 16px;
+		background: transparent;
+		color: var(--fg);
+		font-size: 14px;
+	}
+}
 </style>
diff --git a/src/client/app/common/views/pages/page-editor/page-editor.blocks.vue b/src/client/pages/page-editor/page-editor.blocks.vue
similarity index 100%
rename from src/client/app/common/views/pages/page-editor/page-editor.blocks.vue
rename to src/client/pages/page-editor/page-editor.blocks.vue
diff --git a/src/client/pages/page-editor/page-editor.container.vue b/src/client/pages/page-editor/page-editor.container.vue
new file mode 100644
index 0000000000000000000000000000000000000000..5a4f096c7f57ccd0ed569ec0092fa72dc1efd5ba
--- /dev/null
+++ b/src/client/pages/page-editor/page-editor.container.vue
@@ -0,0 +1,152 @@
+<template>
+<div class="cpjygsrt" :class="{ error: error != null, warn: warn != null }">
+	<header>
+		<div class="title"><slot name="header"></slot></div>
+		<div class="buttons">
+			<slot name="func"></slot>
+			<button v-if="removable" @click="remove()" class="_button">
+				<fa :icon="faTrashAlt"/>
+			</button>
+			<button v-if="draggable" class="drag-handle _button">
+				<fa :icon="faBars"/>
+			</button>
+			<button @click="toggleContent(!showBody)" class="_button">
+				<template v-if="showBody"><fa :icon="faAngleUp"/></template>
+				<template v-else><fa :icon="faAngleDown"/></template>
+			</button>
+		</div>
+	</header>
+	<p v-show="showBody" class="error" v-if="error != null">{{ $t('_pages.script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p>
+	<p v-show="showBody" class="warn" v-if="warn != null">{{ $t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p>
+	<div v-show="showBody">
+		<slot></slot>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faBars, faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons';
+import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
+import i18n from '../../i18n';
+
+export default Vue.extend({
+	i18n,
+
+	props: {
+		expanded: {
+			type: Boolean,
+			default: true
+		},
+		removable: {
+			type: Boolean,
+			default: true
+		},
+		draggable: {
+			type: Boolean,
+			default: false
+		},
+		error: {
+			required: false,
+			default: null
+		},
+		warn: {
+			required: false,
+			default: null
+		}
+	},
+	data() {
+		return {
+			showBody: this.expanded,
+			faTrashAlt, faBars, faAngleUp, faAngleDown
+		};
+	},
+	methods: {
+		toggleContent(show: boolean) {
+			this.showBody = show;
+			this.$emit('toggle', show);
+		},
+		remove() {
+			this.$emit('remove');
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.cpjygsrt {
+	position: relative;
+	overflow: hidden;
+	background: var(--panel);
+	border: solid 2px var(--jvhmlskx);
+	border-radius: 6px;
+
+	&:hover {
+		border: solid 2px var(--yakfpmhl);
+	}
+
+	&.warn {
+		border: solid 2px #dec44c;
+	}
+
+	&.error {
+		border: solid 2px #f00;
+	}
+
+	& + .cpjygsrt {
+		margin-top: 16px;
+	}
+
+	> header {
+		> .title {
+			z-index: 1;
+			margin: 0;
+			padding: 0 16px;
+			line-height: 42px;
+			font-size: 0.9em;
+			font-weight: bold;
+			box-shadow: 0 1px rgba(#000, 0.07);
+
+			> [data-icon] {
+				margin-right: 6px;
+			}
+
+			&:empty {
+				display: none;
+			}
+		}
+
+		> .buttons {
+			position: absolute;
+			z-index: 2;
+			top: 0;
+			right: 0;
+
+			> button {
+				padding: 0;
+				width: 42px;
+				font-size: 0.9em;
+				line-height: 42px;
+			}
+
+			.drag-handle {
+				cursor: move;
+			}
+		}
+	}
+
+	> .warn {
+		color: #b19e49;
+		margin: 0;
+		padding: 16px 16px 0 16px;
+		font-size: 14px;
+	}
+
+	> .error {
+		color: #f00;
+		margin: 0;
+		padding: 16px 16px 0 16px;
+		font-size: 14px;
+	}
+}
+</style>
diff --git a/src/client/app/common/views/pages/page-editor/page-editor.script-block.vue b/src/client/pages/page-editor/page-editor.script-block.vue
similarity index 77%
rename from src/client/app/common/views/pages/page-editor/page-editor.script-block.vue
rename to src/client/pages/page-editor/page-editor.script-block.vue
index cf76cc003ee83dca44d7dd14357f8e1f9c8949c3..ae56803a39648c1b3f20fd7618a591a6110bffd1 100644
--- a/src/client/app/common/views/pages/page-editor/page-editor.script-block.vue
+++ b/src/client/pages/page-editor/page-editor.script-block.vue
@@ -8,7 +8,7 @@
 	</template>
 
 	<section v-if="value.type === null" class="pbglfege" @click="changeType()">
-		{{ $t('script.emptySlot') }}
+		{{ $t('_pages.script.emptySlot') }}
 	</section>
 	<section v-else-if="value.type === 'text'" class="tbwccoaw">
 		<input v-model="value.value"/>
@@ -17,7 +17,7 @@
 		<textarea v-model="value.value"></textarea>
 	</section>
 	<section v-else-if="value.type === 'textList'" class="tbwccoaw">
-		<textarea v-model="value.value" :placeholder="$t('script.blocks._textList.info')"></textarea>
+		<textarea v-model="value.value" :placeholder="$t('_pages.script.blocks._textList.info')"></textarea>
 	</section>
 	<section v-else-if="value.type === 'number'" class="tbwccoaw">
 		<input v-model="value.value" type="number"/>
@@ -25,46 +25,47 @@
 	<section v-else-if="value.type === 'ref'" class="hpdwcrvs">
 		<select v-model="value.value">
 			<option v-for="v in aiScript.getVarsByType(getExpectedType ? getExpectedType() : null).filter(x => x.name !== name)" :value="v.name">{{ v.name }}</option>
-			<optgroup :label="$t('script.argVariables')">
+			<optgroup :label="$t('_pages.script.argVariables')">
 				<option v-for="v in fnSlots" :value="v.name">{{ v.name }}</option>
 			</optgroup>
-			<optgroup :label="$t('script.pageVariables')">
+			<optgroup :label="$t('_pages.script.pageVariables')">
 				<option v-for="v in aiScript.getPageVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option>
 			</optgroup>
-			<optgroup :label="$t('script.enviromentVariables')">
+			<optgroup :label="$t('_pages.script.enviromentVariables')">
 				<option v-for="v in aiScript.getEnvVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option>
 			</optgroup>
 		</select>
 	</section>
 	<section v-else-if="value.type === 'fn'" class="" style="padding:0 16px 16px 16px;">
-		<ui-textarea v-model="slots">
-			<span>{{ $t('script.blocks._fn.slots') }}</span>
-			<template #desc>{{ $t('script.blocks._fn.slots-info') }}</template>
-		</ui-textarea>
-		<x-v v-if="value.value.expression" v-model="value.value.expression" :title="$t(`script.blocks._fn.arg1`)" :get-expected-type="() => null" :ai-script="aiScript" :fn-slots="value.value.slots" :name="name"/>
+		<mk-textarea v-model="slots">
+			<span>{{ $t('_pages.script.blocks._fn.slots') }}</span>
+			<template #desc>{{ $t('_pages.script.blocks._fn.slots-info') }}</template>
+		</mk-textarea>
+		<x-v v-if="value.value.expression" v-model="value.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :ai-script="aiScript" :fn-slots="value.value.slots" :name="name"/>
 	</section>
 	<section v-else-if="value.type.startsWith('fn:')" class="" style="padding:16px;">
 		<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="aiScript.getVarByName(value.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :ai-script="aiScript" :name="name" :key="i"/>
 	</section>
 	<section v-else class="" style="padding:16px;">
-		<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="$t(`script.blocks._${value.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :ai-script="aiScript" :name="name" :fn-slots="fnSlots" :key="i"/>
+		<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="$t(`_pages.script.blocks._${value.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :ai-script="aiScript" :name="name" :fn-slots="fnSlots" :key="i"/>
 	</section>
 </x-container>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../../../i18n';
-import XContainer from './page-editor.container.vue';
 import { faPencilAlt, faPlug } from '@fortawesome/free-solid-svg-icons';
-import { isLiteralBlock, funcDefs, blockDefs } from '../../../../../../misc/aiscript/index';
 import { v4 as uuid } from 'uuid';
+import i18n from '../../i18n';
+import XContainer from './page-editor.container.vue';
+import MkTextarea from '../../components/ui/textarea.vue';
+import { isLiteralBlock, funcDefs, blockDefs } from '../../scripts/aiscript/index';
 
 export default Vue.extend({
-	i18n: i18n('pages'),
+	i18n,
 
 	components: {
-		XContainer
+		XContainer, MkTextarea
 	},
 
 	inject: ['getScriptBlockList'],
@@ -117,7 +118,7 @@ export default Vue.extend({
 		typeText(): any {
 			if (this.value.type === null) return null;
 			if (this.value.type.startsWith('fn:')) return this.value.type.split(':')[1];
-			return this.$t(`script.blocks.${this.value.type}`);
+			return this.$t(`_pages.script.blocks.${this.value.type}`);
 		},
 	},
 
@@ -228,44 +229,50 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.turmquns
-	opacity 0.7
-
-.pbglfege
-	opacity 0.5
-	padding 16px
-	text-align center
-	cursor pointer
-	color var(--text)
+<style lang="scss" scoped>
+.turmquns {
+	opacity: 0.7;
+}
 
-.tbwccoaw
-	> input
-	> textarea
-		display block
-		-webkit-appearance none
-		-moz-appearance none
-		appearance none
-		width 100%
-		max-width 100%
-		min-width 100%
-		border none
-		box-shadow none
-		padding 16px
-		font-size 16px
-		background transparent
-		color var(--text)
+.pbglfege {
+	opacity: 0.5;
+	padding: 16px;
+	text-align: center;
+	cursor: pointer;
+	color: var(--fg);
+}
 
-	> textarea
-		min-height 100px
+.tbwccoaw {
+	> input,
+	> textarea {
+		display: block;
+		-webkit-appearance: none;
+		-moz-appearance: none;
+		appearance: none;
+		width: 100%;
+		max-width: 100%;
+		min-width: 100%;
+		border: none;
+		box-shadow: none;
+		padding: 16px;
+		font-size: 16px;
+		background: transparent;
+		color: var(--fg);
+	}
 
-.hpdwcrvs
-	padding 16px
+	> textarea {
+		min-height: 100px;
+	}
+}
 
-	> select
-		display block
-		padding 4px
-		font-size 16px
-		width 100%
+.hpdwcrvs {
+	padding: 16px;
 
+	> select {
+		display: block;
+		padding: 4px;
+		font-size: 16px;
+		width: 100%;
+	}
+}
 </style>
diff --git a/src/client/app/common/views/pages/page-editor/page-editor.vue b/src/client/pages/page-editor/page-editor.vue
similarity index 66%
rename from src/client/app/common/views/pages/page-editor/page-editor.vue
rename to src/client/pages/page-editor/page-editor.vue
index cbe65ad6f03b5fdc2a359f73882d61d4da834dcf..a5a4588f132b6e05873432da3cb448e255d6a828 100644
--- a/src/client/app/common/views/pages/page-editor/page-editor.vue
+++ b/src/client/pages/page-editor/page-editor.vue
@@ -1,58 +1,58 @@
 <template>
 <div>
-	<div class="gwbmwxkm" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
+	<div class="gwbmwxkm _panel">
 		<header>
 			<div class="title"><fa :icon="faStickyNote"/> {{ readonly ? $t('read-page') : pageId ? $t('edit-page') : $t('new-page') }}</div>
 			<div class="buttons">
-				<button @click="del()" v-if="!readonly"><fa :icon="faTrashAlt"/></button>
-				<button @click="() => showOptions = !showOptions"><fa :icon="faCog"/></button>
-				<button @click="save()" v-if="!readonly"><fa :icon="faSave"/></button>
+				<button class="_button" @click="del()" v-if="!readonly"><fa :icon="faTrashAlt"/></button>
+				<button class="_button" @click="() => showOptions = !showOptions"><fa :icon="faCog"/></button>
+				<button class="_button" @click="save()" v-if="!readonly"><fa :icon="faSave"/></button>
 			</div>
 		</header>
 
 		<section>
 			<router-link class="view" v-if="pageId" :to="`/@${ author.username }/pages/${ currentName }`"><fa :icon="faExternalLinkSquareAlt"/> {{ $t('view-page') }}</router-link>
 
-			<ui-input v-model="title">
+			<mk-input v-model="title">
 				<span>{{ $t('title') }}</span>
-			</ui-input>
+			</mk-input>
 
 			<template v-if="showOptions">
-				<ui-input v-model="summary">
+				<mk-input v-model="summary">
 					<span>{{ $t('summary') }}</span>
-				</ui-input>
+				</mk-input>
 
-				<ui-input v-model="name">
+				<mk-input v-model="name">
 					<template #prefix>{{ url }}/@{{ author.username }}/pages/</template>
 					<span>{{ $t('url') }}</span>
-				</ui-input>
+				</mk-input>
 
-				<ui-switch v-model="alignCenter">{{ $t('align-center') }}</ui-switch>
+				<mk-switch v-model="alignCenter">{{ $t('align-center') }}</mk-switch>
 
-				<ui-select v-model="font">
+				<mk-select v-model="font">
 					<template #label>{{ $t('font') }}</template>
 					<option value="serif">{{ $t('fontSerif') }}</option>
 					<option value="sans-serif">{{ $t('fontSansSerif') }}</option>
-				</ui-select>
+				</mk-select>
 
-				<ui-switch v-model="hideTitleWhenPinned">{{ $t('hide-title-when-pinned') }}</ui-switch>
+				<mk-switch v-model="hideTitleWhenPinned">{{ $t('hide-title-when-pinned') }}</mk-switch>
 
 				<div class="eyeCatch">
-					<ui-button v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage()"><fa :icon="faPlus"/> {{ $t('set-eye-catching-image') }}</ui-button>
+					<mk-button v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage()"><fa :icon="faPlus"/> {{ $t('set-eye-catching-image') }}</mk-button>
 					<div v-else-if="eyeCatchingImage">
 						<img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name"/>
-						<ui-button @click="removeEyeCatchingImage()" v-if="!readonly"><fa :icon="faTrashAlt"/> {{ $t('remove-eye-catching-image') }}</ui-button>
+						<mk-button @click="removeEyeCatchingImage()" v-if="!readonly"><fa :icon="faTrashAlt"/> {{ $t('remove-eye-catching-image') }}</mk-button>
 					</div>
 				</div>
 			</template>
 
 			<x-blocks class="content" v-model="content" :ai-script="aiScript"/>
 
-			<ui-button @click="add()" v-if="!readonly"><fa :icon="faPlus"/></ui-button>
+			<mk-button @click="add()" v-if="!readonly"><fa :icon="faPlus"/></mk-button>
 		</section>
 	</div>
 
-	<ui-container :body-togglable="true">
+	<mk-container :body-togglable="true">
 		<template #header><fa :icon="faMagic"/> {{ $t('variables') }}</template>
 		<div class="qmuvgica">
 			<x-draggable tag="div" class="variables" v-show="variables.length > 0" :list="variables" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5">
@@ -69,25 +69,25 @@
 				/>
 			</x-draggable>
 
-			<ui-button @click="addVariable()" class="add" v-if="!readonly"><fa :icon="faPlus"/></ui-button>
+			<mk-button @click="addVariable()" class="add" v-if="!readonly"><fa :icon="faPlus"/></mk-button>
 
-			<ui-info><span v-html="$t('variables-info')"></span><a @click="() => moreDetails = true" style="display:block;">{{ $t('more-details') }}</a></ui-info>
+			<x-info><span v-html="$t('variables-info')"></span><a @click="() => moreDetails = true" style="display:block;">{{ $t('more-details') }}</a></x-info>
 
 			<template v-if="moreDetails">
-				<ui-info><span v-html="$t('variables-info2')"></span></ui-info>
-				<ui-info><span v-html="$t('variables-info3')"></span></ui-info>
-				<ui-info><span v-html="$t('variables-info4')"></span></ui-info>
+				<x-info><span v-html="$t('variables-info2')"></span></x-info>
+				<x-info><span v-html="$t('variables-info3')"></span></x-info>
+				<x-info><span v-html="$t('variables-info4')"></span></x-info>
 			</template>
 		</div>
-	</ui-container>
+	</mk-container>
 
-	<ui-container :body-togglable="true" :expanded="false">
+	<mk-container :body-togglable="true" :expanded="false">
 		<template #header><fa :icon="faCode"/> {{ $t('inspector') }}</template>
 		<div style="padding:0 32px 32px 32px;">
-			<ui-textarea :value="JSON.stringify(content, null, 2)" readonly tall>{{ $t('content') }}</ui-textarea>
-			<ui-textarea :value="JSON.stringify(variables, null, 2)" readonly tall>{{ $t('variables') }}</ui-textarea>
+			<mk-textarea :value="JSON.stringify(content, null, 2)" readonly tall>{{ $t('content') }}</mk-textarea>
+			<mk-textarea :value="JSON.stringify(variables, null, 2)" readonly tall>{{ $t('variables') }}</mk-textarea>
 		</div>
-	</ui-container>
+	</mk-container>
 </div>
 </template>
 
@@ -96,20 +96,26 @@ import Vue from 'vue';
 import * as XDraggable from 'vuedraggable';
 import { faICursor, faPlus, faMagic, faCog, faCode, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
 import { faSave, faStickyNote, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../../../../i18n';
+import { v4 as uuid } from 'uuid';
+import i18n from '../../i18n';
 import XVariable from './page-editor.script-block.vue';
 import XBlocks from './page-editor.blocks.vue';
-import { v4 as uuid } from 'uuid';
-import { blockDefs } from '../../../../../../misc/aiscript/index';
-import { ASTypeChecker } from '../../../../../../misc/aiscript/type-checker';
-import { url } from '../../../../config';
-import { collectPageVars } from '../../../scripts/collect-page-vars';
+import MkTextarea from '../../components/ui/textarea.vue';
+import MkContainer from '../../components/ui/container.vue';
+import MkButton from '../../components/ui/button.vue';
+import MkSelect from '../../components/ui/select.vue';
+import MkSwitch from '../../components/ui/switch.vue';
+import MkInput from '../../components/ui/input.vue';
+import { blockDefs } from '../../scripts/aiscript/index';
+import { ASTypeChecker } from '../../scripts/aiscript/type-checker';
+import { url } from '../../config';
+import { collectPageVars } from '../../scripts/collect-page-vars';
 
 export default Vue.extend({
-	i18n: i18n('pages'),
+	i18n,
 
 	components: {
-		XDraggable, XVariable, XBlocks
+		XDraggable, XVariable, XBlocks, MkTextarea, MkContainer, MkButton, MkSelect, MkSwitch, MkInput
 	},
 
 	props: {
@@ -268,7 +274,7 @@ export default Vue.extend({
 						type: 'success',
 						text: this.$t('page-created')
 					});
-					this.$router.push(`/i/pages/edit/${this.pageId}`);
+					this.$router.push(`/my/pages/edit/${this.pageId}`);
 				}).catch(onError);
 			}
 		},
@@ -287,7 +293,7 @@ export default Vue.extend({
 						type: 'success',
 						text: this.$t('page-deleted')
 					});
-					this.$router.push(`/i/pages`);
+					this.$router.push(`/my/pages`);
 				});
 			});
 		},
@@ -344,27 +350,27 @@ export default Vue.extend({
 			return [{
 				label: this.$t('content-blocks'),
 				items: [
-					{ value: 'section', text: this.$t('blocks.section') },
-					{ value: 'text', text: this.$t('blocks.text') },
-					{ value: 'image', text: this.$t('blocks.image') },
-					{ value: 'textarea', text: this.$t('blocks.textarea') },
+					{ value: 'section', text: this.$t('_pages.blocks.section') },
+					{ value: 'text', text: this.$t('_pages.blocks.text') },
+					{ value: 'image', text: this.$t('_pages.blocks.image') },
+					{ value: 'textarea', text: this.$t('_pages.blocks.textarea') },
 				]
 			}, {
 				label: this.$t('input-blocks'),
 				items: [
-					{ value: 'button', text: this.$t('blocks.button') },
-					{ value: 'radioButton', text: this.$t('blocks.radioButton') },
-					{ value: 'textInput', text: this.$t('blocks.textInput') },
-					{ value: 'textareaInput', text: this.$t('blocks.textareaInput') },
-					{ value: 'numberInput', text: this.$t('blocks.numberInput') },
-					{ value: 'switch', text: this.$t('blocks.switch') },
-					{ value: 'counter', text: this.$t('blocks.counter') }
+					{ value: 'button', text: this.$t('_pages.blocks.button') },
+					{ value: 'radioButton', text: this.$t('_pages.blocks.radioButton') },
+					{ value: 'textInput', text: this.$t('_pages.blocks.textInput') },
+					{ value: 'textareaInput', text: this.$t('_pages.blocks.textareaInput') },
+					{ value: 'numberInput', text: this.$t('_pages.blocks.numberInput') },
+					{ value: 'switch', text: this.$t('_pages.blocks.switch') },
+					{ value: 'counter', text: this.$t('_pages.blocks.counter') }
 				]
 			}, {
 				label: this.$t('special-blocks'),
 				items: [
-					{ value: 'if', text: this.$t('blocks.if') },
-					{ value: 'post', text: this.$t('blocks.post') }
+					{ value: 'if', text: this.$t('_pages.blocks.if') },
+					{ value: 'post', text: this.$t('_pages.blocks.post') }
 				]
 			}];
 		},
@@ -379,7 +385,7 @@ export default Vue.extend({
 				if (category) {
 					category.items.push({
 						value: block.type,
-						text: this.$t(`script.blocks.${block.type}`)
+						text: this.$t(`_pages.script.blocks.${block.type}`)
 					});
 				} else {
 					list.push({
@@ -387,7 +393,7 @@ export default Vue.extend({
 						label: this.$t(`script.categories.${block.category}`),
 						items: [{
 							value: block.type,
-							text: this.$t(`script.blocks.${block.type}`)
+							text: this.$t(`_pages.script.blocks.${block.type}`)
 						}]
 					});
 				}
@@ -422,87 +428,89 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.gwbmwxkm
-	overflow hidden
-	background var(--face)
-	margin-bottom 16px
-
-	&.round
-		border-radius 6px
-
-	&.shadow
-		box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
-
-	> header
-		background var(--faceHeader)
-
-		> .title
-			z-index 1
-			margin 0
-			padding 0 16px
-			line-height 42px
-			font-size 0.9em
-			font-weight bold
-			color var(--faceHeaderText)
-			box-shadow 0 var(--lineWidth) rgba(#000, 0.07)
-
-			> [data-icon]
-				margin-right 6px
-
-			&:empty
-				display none
-
-		> .buttons
-			position absolute
-			z-index 2
-			top 0
-			right 0
-
-			> button
-				padding 0
-				width 42px
-				font-size 0.9em
-				line-height 42px
-				color var(--faceTextButton)
-
-				&:hover
-					color var(--faceTextButtonHover)
+<style lang="scss" scoped>
+.gwbmwxkm {
+	margin-bottom: var(--margin);
+
+	> header {
+		background: var(--faceHeader);
+
+		> .title {
+			z-index: 1;
+			margin: 0;
+			padding: 0 16px;
+			line-height: 42px;
+			font-size: 0.9em;
+			font-weight: bold;
+			color: var(--faceHeaderText);
+			box-shadow: 0 var(--lineWidth) rgba(#000, 0.07);
+
+			> [data-icon] {
+				margin-right: 6px;
+			}
 
-				&:active
-					color var(--faceTextButtonActive)
+			&:empty {
+				display: none;
+			}
+		}
 
-	> section
-		padding 0 32px 32px 32px
+		> .buttons {
+			position: absolute;
+			z-index: 2;
+			top: 0;
+			right: 0;
+
+			> button {
+				padding: 0;
+				width: 42px;
+				font-size: 0.9em;
+				line-height: 42px;
+			}
+		}
+	}
 
-		@media (max-width 500px)
-			padding 0 16px 16px 16px
+	> section {
+		padding: 0 32px 32px 32px;
 
-		> .view
-			display inline-block
-			margin 16px 0 0 0
-			font-size 14px
+		@media (max-width: 500px) {
+			padding: 0 16px 16px 16px;
+		}
 
-		> .content
-			margin-bottom 16px
+		> .view {
+			display: inline-block;
+			margin: 16px 0 0 0;
+			font-size: 14px;
+		}
 
-		> .eyeCatch
-			margin-bottom 16px
+		> .content {
+			margin-bottom: 16px;
+		}
 
-			> div
-				> img
-					max-width 100%
+		> .eyeCatch {
+			margin-bottom: 16px;
 
-.qmuvgica
-	padding 32px
+			> div {
+				> img {
+					max-width: 100%;
+				}
+			}
+		}
+	}
+}
 
-	@media (max-width 500px)
-		padding 16px
+.qmuvgica {
+	padding: 32px;
 
-	> .variables
-		margin-bottom 16px
+	@media (max-width: 500px) {
+		padding: 16px;
+	}
 
-	> .add
-		margin-bottom 16px
+	> .variables {
+		margin-bottom: 16px;
+	}
 
+	> .add {
+		margin-bottom: 16px;
+	}
+}
 </style>
diff --git a/src/client/app/common/views/pages/page.vue b/src/client/pages/page.vue
similarity index 66%
rename from src/client/app/common/views/pages/page.vue
rename to src/client/pages/page.vue
index d1c4c2be43a0dfb34b56c370ab1430b4750a6005..72c51017315e054d11fe376364a7e870afdeec19 100644
--- a/src/client/app/common/views/pages/page.vue
+++ b/src/client/pages/page.vue
@@ -1,10 +1,14 @@
 <template>
-<x-page v-if="page" :page="page" :key="page.id" :show-footer="true"/>
+<div class="xcukqgmh _panel">
+	<portal to="avatar" v-if="page"><mk-avatar class="avatar" :user="page.user" :disable-preview="true"/></portal>
+	<portal to="title" v-if="page">{{ page.title || page.name }}</portal>
+
+	<x-page v-if="page" :page="page" :key="page.id" :show-footer="true"/>
+</div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
 import XPage from '../components/page/page.vue';
 
 export default Vue.extend({
@@ -52,12 +56,14 @@ export default Vue.extend({
 				username: this.username,
 			}).then(page => {
 				this.page = page;
-				this.$emit('init', {
-					title: this.page.title,
-					icon: faStickyNote
-				});
 			});
 		},
 	}
 });
 </script>
+
+<style lang="scss" scoped>
+.xcukqgmh {
+
+}
+</style>
diff --git a/src/client/pages/pages.vue b/src/client/pages/pages.vue
new file mode 100644
index 0000000000000000000000000000000000000000..bee7d30a6178922a44c3aed04f44ef107c3c0f0d
--- /dev/null
+++ b/src/client/pages/pages.vue
@@ -0,0 +1,78 @@
+<template>
+<div>
+	<mk-container :body-togglable="true">
+		<template #header><fa :icon="faEdit" fixed-width/>{{ $t('my-pages') }}</template>
+		<div class="rknalgpo my">
+			<mk-button class="new" @click="create()"><fa :icon="faPlus"/></mk-button>
+			<mk-pagination :pagination="myPagesPagination" #default="{items}">
+				<mk-page-preview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
+			</mk-pagination>
+		</div>
+	</mk-container>
+
+	<mk-container :body-togglable="true">
+		<template #header><fa :icon="faHeart" fixed-width/>{{ $t('liked-pages') }}</template>
+		<div class="rknalgpo">
+			<mk-pagination :pagination="likedPagesPagination" #default="{items}">
+				<mk-page-preview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/>
+			</mk-pagination>
+		</div>
+	</mk-container>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faPlus, faEdit } from '@fortawesome/free-solid-svg-icons';
+import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons';
+import i18n from '../i18n';
+import MkPagePreview from '../components/page-preview.vue';
+import MkPagination from '../components/ui/pagination.vue';
+import MkButton from '../components/ui/button.vue';
+import MkContainer from '../components/ui/container.vue';
+
+export default Vue.extend({
+	i18n,
+	components: {
+		MkPagePreview, MkPagination, MkButton, MkContainer
+	},
+	data() {
+		return {
+			myPagesPagination: {
+				endpoint: 'i/pages',
+				limit: 5,
+			},
+			likedPagesPagination: {
+				endpoint: 'i/page-likes',
+				limit: 5,
+			},
+			faStickyNote, faPlus, faEdit, faHeart
+		};
+	},
+	methods: {
+		create() {
+			this.$router.push(`/my/pages/new`);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.rknalgpo {
+	padding: 16px;
+
+	&.my .ckltabjg:first-child {
+		margin-top: 16px;
+	}
+
+	.ckltabjg:not(:last-child) {
+		margin-bottom: 8px;
+	}
+
+	@media (min-width: 500px) {
+		.ckltabjg:not(:last-child) {
+			margin-bottom: 16px;
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/search.vue b/src/client/pages/search.vue
new file mode 100644
index 0000000000000000000000000000000000000000..c3e87c0d0c4322b3094d55e96d3bd73b35c0c9bb
--- /dev/null
+++ b/src/client/pages/search.vue
@@ -0,0 +1,55 @@
+<template>
+<div>
+	<portal to="icon"><fa :icon="faSearch"/></portal>
+	<portal to="title">{{ $t('searchWith', { q: $route.query.q }) }}</portal>
+	<x-notes ref="notes" :pagination="pagination" @before="before" @after="after"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faSearch } from '@fortawesome/free-solid-svg-icons';
+import Progress from '../scripts/loading';
+import XNotes from '../components/notes.vue';
+
+export default Vue.extend({
+	metaInfo() {
+		return {
+			title: this.$t('searchWith', { q: this.$route.query.q }) as string
+		};
+	},
+
+	components: {
+		XNotes
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'notes/search',
+				limit: 10,
+				params: () => ({
+					query: this.$route.query.q,
+				})
+			},
+			faSearch
+		};
+	},
+
+	watch: {
+		$route() {
+			(this.$refs.notes as any).reload();
+		}
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/settings/2fa.vue b/src/client/pages/settings/2fa.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7163f2ece47c951aa600a0ac5cbe4f171b5bd7f2
--- /dev/null
+++ b/src/client/pages/settings/2fa.vue
@@ -0,0 +1,264 @@
+<template>
+<section class="_section">
+	<div class="_title"><fa :icon="faLock"/> {{ $t('twoStepAuthentication') }}</div>
+	<div class="_content">
+		<p v-if="!data && !$store.state.i.twoFactorEnabled"><mk-button @click="register">{{ $t('_2fa.registerDevice') }}</mk-button></p>
+		<template v-if="$store.state.i.twoFactorEnabled">
+			<h2 class="heading">{{ $t('totp-header') }}</h2>
+			<p>{{ $t('already-registered') }}</p>
+			<mk-button @click="unregister">{{ $t('unregister') }}</mk-button>
+
+			<template v-if="supportsCredentials">
+				<hr class="totp-method-sep">
+
+				<h2 class="heading">{{ $t('security-key-header') }}</h2>
+				<p>{{ $t('security-key') }}</p>
+				<div class="key-list">
+					<div class="key" v-for="key in $store.state.i.securityKeysList">
+						<h3>
+							{{ key.name }}
+						</h3>
+						<div class="last-used">
+							{{ $t('last-used') }}
+							<mk-time :time="key.lastUsed"/>
+						</div>
+						<mk-button @click="unregisterKey(key)">
+							{{ $t('unregister') }}
+						</mk-button>
+					</div>
+				</div>
+
+				<mk-switch v-model="usePasswordLessLogin" @change="updatePasswordLessLogin" v-if="$store.state.i.securityKeysList.length > 0">
+					{{ $t('use-password-less-login') }}
+				</mk-switch>
+
+				<mk-info warn v-if="registration && registration.error">{{ $t('something-went-wrong') }} {{ registration.error }}</mk-info>
+				<mk-button v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('register') }}</mk-button>
+
+				<ol v-if="registration && !registration.error">
+					<li v-if="registration.stage >= 0">
+						{{ $t('activate-key') }}
+						<fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 0" />
+					</li>
+					<li v-if="registration.stage >= 1">
+						<mk-form :disabled="registration.stage != 1 || registration.saving">
+							<mk-input v-model="keyName" :max="30">
+								<span>{{ $t('security-key-name') }}</span>
+							</mk-input>
+							<mk-button @click="registerKey" :disabled="this.keyName.length == 0">
+								{{ $t('register-security-key') }}
+							</mk-button>
+							<fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 1" />
+						</mk-form>
+					</li>
+				</ol>
+			</template>
+		</template>
+		<div v-if="data && !$store.state.i.twoFactorEnabled">
+			<ol style="margin: 0; padding: 0 0 0 1em;">
+				<li>
+					<i18n path="_2fa.step1" tag="span">
+						<a href="https://authy.com/" rel="noopener" target="_blank" place="a" style="color: var(--link);">Authy</a>
+						<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" place="b" style="color: var(--link);">Google Authenticator</a>
+					</i18n>
+				</li>
+				<li>{{ $t('_2fa.step2') }}<br><img :src="data.qr"></li>
+				<li>{{ $t('_2fa.step3') }}<br>
+					<mk-input v-model="token" type="number" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false">{{ $t('token') }}</mk-input>
+					<mk-button primary @click="submit">{{ $t('done') }}</mk-button>
+				</li>
+			</ol>
+			<mk-info>{{ $t('_2fa.step4') }}</mk-info>
+		</div>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faLock } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../../i18n';
+import { hostname } from '../../config';
+import { hexifyAB } from '../../scripts/2fa';
+import MkButton from '../../components/ui/button.vue';
+import MkInfo from '../../components/ui/info.vue';
+import MkInput from '../../components/ui/input.vue';
+
+function stringifyAB(buffer) {
+	return String.fromCharCode.apply(null, new Uint8Array(buffer));
+}
+
+export default Vue.extend({
+	i18n,
+	components: {
+		MkButton, MkInfo, MkInput
+	},
+	data() {
+		return {
+			data: null,
+			supportsCredentials: !!navigator.credentials,
+			usePasswordLessLogin: this.$store.state.i.usePasswordLessLogin,
+			registration: null,
+			keyName: '',
+			token: null,
+			faLock
+		};
+	},
+	methods: {
+		register() {
+			this.$root.dialog({
+				title: this.$t('password'),
+				input: {
+					type: 'password'
+				}
+			}).then(({ canceled, result: password }) => {
+				if (canceled) return;
+				this.$root.api('i/2fa/register', {
+					password: password
+				}).then(data => {
+					this.data = data;
+				});
+			});
+		},
+
+		unregister() {
+			this.$root.dialog({
+				title: this.$t('password'),
+				input: {
+					type: 'password'
+				}
+			}).then(({ canceled, result: password }) => {
+				if (canceled) return;
+				this.$root.api('i/2fa/unregister', {
+					password: password
+				}).then(() => {
+					this.usePasswordLessLogin = false;
+					this.updatePasswordLessLogin();
+				}).then(() => {
+					this.$root.dialog({
+						type: 'success',
+						iconOnly: true, autoClose: true
+					});
+					this.$store.state.i.twoFactorEnabled = false;
+				});
+			});
+		},
+
+		submit() {
+			this.$root.api('i/2fa/done', {
+				token: this.token
+			}).then(() => {
+				this.$root.dialog({
+					type: 'success',
+					iconOnly: true, autoClose: true
+				});
+				this.$store.state.i.twoFactorEnabled = true;
+			}).catch(e => {
+				this.$root.dialog({
+					type: 'error',
+					iconOnly: true, autoClose: true
+				});
+			});
+		},
+
+		registerKey() {
+			this.registration.saving = true;
+			this.$root.api('i/2fa/key-done', {
+				password: this.registration.password,
+				name: this.keyName,
+				challengeId: this.registration.challengeId,
+				// we convert each 16 bits to a string to serialise
+				clientDataJSON: stringifyAB(this.registration.credential.response.clientDataJSON),
+				attestationObject: hexifyAB(this.registration.credential.response.attestationObject)
+			}).then(key => {
+				this.registration = null;
+				key.lastUsed = new Date();
+				this.$root.dialog({
+					type: 'success',
+					iconOnly: true, autoClose: true
+				});
+			})
+		},
+
+		unregisterKey(key) {
+			this.$root.dialog({
+				title: this.$t('password'),
+				input: {
+					type: 'password'
+				}
+			}).then(({ canceled, result: password }) => {
+				if (canceled) return;
+				return this.$root.api('i/2fa/remove-key', {
+					password,
+					credentialId: key.id
+				}).then(() => {
+					this.usePasswordLessLogin = false;
+					this.updatePasswordLessLogin();
+				}).then(() => {
+					this.$root.dialog({
+						type: 'success',
+						iconOnly: true, autoClose: true
+					});
+				});
+			});
+		},
+
+		addSecurityKey() {
+			this.$root.dialog({
+				title: this.$t('password'),
+				input: {
+					type: 'password'
+				}
+			}).then(({ canceled, result: password }) => {
+				if (canceled) return;
+				this.$root.api('i/2fa/register-key', {
+					password
+				}).then(registration => {
+					this.registration = {
+						password,
+						challengeId: registration.challengeId,
+						stage: 0,
+						publicKeyOptions: {
+							challenge: Buffer.from(
+								registration.challenge
+									.replace(/\-/g, "+")
+									.replace(/_/g, "/"),
+								'base64'
+							),
+							rp: {
+								id: hostname,
+								name: 'Misskey'
+							},
+							user: {
+								id: Uint8Array.from(this.$store.state.i.id, c => c.charCodeAt(0)),
+								name: this.$store.state.i.username,
+								displayName: this.$store.state.i.name,
+							},
+							pubKeyCredParams: [{alg: -7, type: 'public-key'}],
+							timeout: 60000,
+							attestation: 'direct'
+						},
+						saving: true
+					};
+					return navigator.credentials.create({
+						publicKey: this.registration.publicKeyOptions
+					});
+				}).then(credential => {
+					this.registration.credential = credential;
+					this.registration.saving = false;
+					this.registration.stage = 1;
+				}).catch(err => {
+					console.warn('Error while registering?', err);
+					this.registration.error = err.message;
+					this.registration.stage = -1;
+				});
+			});
+		},
+		updatePasswordLessLogin() {
+			this.$root.api('i/2fa/password-less', {
+				value: !!this.usePasswordLessLogin
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/settings/drive.vue b/src/client/pages/settings/drive.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d0c18a07e5ff51d8be513b21f03aba1ad6181aa6
--- /dev/null
+++ b/src/client/pages/settings/drive.vue
@@ -0,0 +1,212 @@
+<template>
+<section class="mk-settings-page-drive _section">
+	<div class="_title"><fa :icon="faCloud"/> {{ $t('drive') }}</div>
+	<div class="_content">
+		<mk-pagination :pagination="drivePagination" #default="{items}" class="drive" ref="drive">
+			<div class="file" v-for="(file, i) in items" :key="file.id" :data-index="i" @click="selected = file" :class="{ selected: selected && (selected.id === file.id) }">
+				<x-file-thumbnail class="thumbnail" :file="file" fit="cover"/>
+				<div class="body">
+					<p class="name">
+						<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
+						<span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
+					</p>
+					<footer>
+						<span class="type"><x-file-type-icon :type="file.type" class="icon"/>{{ file.type }}</span>
+						<span class="separator"></span>
+						<span class="data-size">{{ file.size | bytes }}</span>
+						<span class="separator"></span>
+						<span class="created-at"><fa :icon="faClock"/><mk-time :time="file.createdAt"/></span>
+						<template v-if="file.isSensitive">
+							<span class="separator"></span>
+							<span class="nsfw"><fa :icon="faEyeSlash"/> {{ $t('nsfw') }}</span>
+						</template>
+					</footer>
+				</div>
+			</div>
+		</mk-pagination>
+	</div>
+	<div class="_footer">
+		<mk-button primary inline :disabled="selected == null" @click="download()"><fa :icon="faDownload"/> {{ $t('download') }}</mk-button>
+		<mk-button inline :disabled="selected == null" @click="del()"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</mk-button>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faCloud, faDownload } from '@fortawesome/free-solid-svg-icons';
+import { faClock, faEyeSlash, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
+import XFileTypeIcon from '../../components/file-type-icon.vue';
+import XFileThumbnail from '../../components/drive-file-thumbnail.vue';
+import MkButton from '../../components/ui/button.vue';
+import MkPagination from '../../components/ui/pagination.vue';
+import i18n from '../../i18n';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		XFileTypeIcon,
+		XFileThumbnail,
+		MkPagination,
+		MkButton,
+	},
+
+	data() {
+		return {
+			selected: null,
+			connection: null,
+			drivePagination: {
+				endpoint: 'drive/files',
+				limit: 10,
+			},
+			faCloud, faClock, faEyeSlash, faDownload, faTrashAlt
+		}
+	},
+
+	created() {
+		this.connection = this.$root.stream.useSharedConnection('drive');
+
+		this.connection.on('fileCreated', this.onStreamDriveFileCreated);
+		this.connection.on('fileUpdated', this.onStreamDriveFileUpdated);
+		this.connection.on('fileDeleted', this.onStreamDriveFileDeleted);
+	},
+
+	beforeDestroy() {
+		this.connection.dispose();
+	},
+
+	methods: {
+		onStreamDriveFileCreated(file) {
+			this.$refs.drive.prepend(file);
+		},
+
+		onStreamDriveFileUpdated(file) {
+			// TODO
+		},
+
+		onStreamDriveFileDeleted(fileId) {
+			this.$refs.drive.remove(x => x.id === fileId);
+		},
+
+		download() {
+			window.open(this.selected.url, '_blank');
+		},
+
+		async del() {
+			const { canceled } = await this.$root.dialog({
+				type: 'warning',
+				text: this.$t('driveFileDeleteConfirm', { name: this.selected.name }),
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			this.$root.api('drive/files/delete', {
+				fileId: this.selected.id
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-settings-page-drive {
+	> ._content {
+		max-height: 350px;
+		overflow: auto;
+		
+		> .drive {
+			> .file {
+				display: grid;
+				margin: 0 auto;
+				grid-template-columns: 64px 1fr;
+				grid-column-gap: 10px;
+				cursor: pointer;
+
+				&.selected {
+					background: var(--accent);
+					box-shadow: 0 0 0 8px var(--accent);
+					color: #fff;
+				}
+
+				&:not(:last-child) {
+					margin-bottom: 16px;
+				}
+
+				> .thumbnail {
+					width: 64px;
+					height: 64px;
+				}
+
+				> .body {
+					display: block;
+					word-break: break-all;
+					padding-top: 4px;
+
+					> .name {
+						display: block;
+						margin: 0;
+						padding: 0;
+						font-size: 0.9em;
+						font-weight: bold;
+						word-break: break-word;
+
+						> .ext {
+							opacity: 0.5;
+						}
+					}
+
+					> .tags {
+						display: block;
+						margin: 4px 0 0 0;
+						padding: 0;
+						list-style: none;
+						font-size: 0.5em;
+
+						> .tag {
+							display: inline-block;
+							margin: 0 5px 0 0;
+							padding: 1px 5px;
+							border-radius: 2px;
+						}
+					}
+
+					> footer {
+						display: block;
+						margin: 4px 0 0 0;
+						font-size: 0.7em;
+
+						> .separator {
+							padding: 0 4px;
+						}
+
+						> .type {
+							opacity: 0.7;
+
+							> .icon {
+								margin-right: 4px;
+							}
+						}
+
+						> .data-size {
+							opacity: 0.7;
+						}
+
+						> .created-at {
+							opacity: 0.7;
+
+							> [data-icon] {
+								margin-right: 2px;
+							}
+						}
+
+						> .nsfw {
+							color: #bf4633;
+						}
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/settings/general.vue b/src/client/pages/settings/general.vue
new file mode 100644
index 0000000000000000000000000000000000000000..6b63da742c02783cbb6452835cafd6b80f650c6c
--- /dev/null
+++ b/src/client/pages/settings/general.vue
@@ -0,0 +1,108 @@
+<template>
+<section class="mk-settings-page-general _section">
+	<div class="_title"><fa :icon="faCog"/> {{ $t('general') }}</div>
+	<div class="_content">
+		<mk-input type="file" @change="onWallpaperChange" style="margin-top: 0;">
+			<span>{{ $t('wallpaper') }}</span>
+			<template #icon><fa :icon="faImage"/></template>
+			<template #desc v-if="wallpaperUploading">{{ $t('uploading') }}<mk-ellipsis/></template>
+		</mk-input>
+		<mk-button primary :disabled="$store.state.settings.wallpaper == null" @click="delWallpaper()">{{ $t('removeWallpaper') }}</mk-button>
+	</div>
+	<div class="_content">
+		<mk-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch">
+			{{ $t('auto-watch') }}<template #desc>{{ $t('auto-watch-desc') }}</template>
+		</mk-switch>
+	</div>
+	<div class="_content">
+		<mk-button @click="readAllNotifications">{{ $t('mark-as-read-all-notifications') }}</mk-button>
+		<mk-button @click="readAllUnreadNotes">{{ $t('mark-as-read-all-unread-notes') }}</mk-button>
+		<mk-button @click="readAllMessagingMessages">{{ $t('mark-as-read-all-talk-messages') }}</mk-button>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faImage, faCog } from '@fortawesome/free-solid-svg-icons';
+import MkInput from '../../components/ui/input.vue';
+import MkButton from '../../components/ui/button.vue';
+import MkSwitch from '../../components/ui/switch.vue';
+import i18n from '../../i18n';
+import { apiUrl } from '../../config';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		MkInput,
+		MkButton,
+		MkSwitch,
+	},
+	
+	data() {
+		return {
+			wallpaperUploading: false,
+			faImage, faCog
+		}
+	},
+
+	computed: {
+		wallpaper: {
+			get() { return this.$store.state.settings.wallpaper; },
+			set(value) { this.$store.dispatch('settings/set', { key: 'wallpaper', value }); }
+		},
+	},
+
+	methods: {
+		onWallpaperChange([file]) {
+			this.wallpaperUploading = true;
+
+			const data = new FormData();
+			data.append('file', file);
+			data.append('i', this.$store.state.i.token);
+
+			fetch(apiUrl + '/drive/files/create', {
+				method: 'POST',
+				body: data
+			})
+			.then(response => response.json())
+			.then(f => {
+				this.wallpaper = f.url;
+				this.wallpaperUploading = false;
+				document.documentElement.style.backgroundImage = `url(${this.$store.state.settings.wallpaper})`;
+			})
+			.catch(e => {
+				this.wallpaperUploading = false;
+				this.$root.dialog({
+					type: 'error',
+					text: e
+				});
+			});
+		},
+
+		delWallpaper() {
+			this.wallpaper = null;
+			document.documentElement.style.backgroundImage = 'none';
+		},
+
+		onChangeAutoWatch(v) {
+			this.$root.api('i/update', {
+				autoWatch: v
+			});
+		},
+
+		readAllUnreadNotes() {
+			this.$root.api('i/read_all_unread_notes');
+		},
+
+		readAllMessagingMessages() {
+			this.$root.api('i/read_all_messaging_messages');
+		},
+
+		readAllNotifications() {
+			this.$root.api('notifications/mark_all_as_read');
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/settings/import-export.vue b/src/client/pages/settings/import-export.vue
new file mode 100644
index 0000000000000000000000000000000000000000..5714aabbf641e02deba414b6cd8439e7d50fef84
--- /dev/null
+++ b/src/client/pages/settings/import-export.vue
@@ -0,0 +1,121 @@
+<template>
+<section class="mk-settings-page-import-export _section">
+	<div class="_title"><fa :icon="faBoxes"/> {{ $t('importAndExport') }}</div>
+	<div class="_content">
+		<input ref="file" type="file" style="display: none;" @change="onChangeFile"/>
+		<mk-select v-model="exportTarget" style="margin-top: 0;">
+			<option value="notes">{{ $t('_exportOrImport.allNotes') }}</option>
+			<option value="following">{{ $t('_exportOrImport.followingList') }}</option>
+			<option value="user-lists">{{ $t('_exportOrImport.userLists') }}</option>
+			<option value="mute">{{ $t('_exportOrImport.muteList') }}</option>
+			<option value="blocking">{{ $t('_exportOrImport.blockingList') }}</option>
+		</mk-select>
+		<mk-button inline @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</mk-button>
+		<mk-button inline @click="doImport()" :disabled="!['following', 'user-lists'].includes(exportTarget)"><fa :icon="faUpload"/> {{ $t('import') }}</mk-button>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faDownload, faUpload, faBoxes } from '@fortawesome/free-solid-svg-icons';
+import MkButton from '../../components/ui/button.vue';
+import MkSelect from '../../components/ui/select.vue';
+import i18n from '../../i18n';
+import { apiUrl } from '../../config';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		MkButton,
+		MkSelect,
+	},
+
+	data() {
+		return {
+			exportTarget: 'notes',
+			faDownload, faUpload, faBoxes
+		}
+	},
+
+	methods: {
+		doExport() {
+			this.$root.api(
+				this.exportTarget == 'notes' ? 'i/export-notes' :
+				this.exportTarget == 'following' ? 'i/export-following' :
+				this.exportTarget == 'blocking' ? 'i/export-blocking' :
+				this.exportTarget == 'user-lists' ? 'i/export-user-lists' :
+				null, {})
+			.then(() => {
+				this.$root.dialog({
+					type: 'info',
+					text: this.$t('exportRequested')
+				});
+			}).catch((e: any) => {
+				this.$root.dialog({
+					type: 'error',
+					text: e.message
+				});
+			});
+		},
+
+		doImport() {
+			(this.$refs.file as any).click();
+		},
+
+		onChangeFile() {
+			const [file] = Array.from((this.$refs.file as any).files);
+			
+			const data = new FormData();
+			data.append('file', file);
+			data.append('i', this.$store.state.i.token);
+
+			const dialog = this.$root.dialog({
+				type: 'waiting',
+				text: this.$t('uploading') + '...',
+				showOkButton: false,
+				showCancelButton: false,
+				cancelableByBgClick: false
+			});
+
+			fetch(apiUrl + '/drive/files/create', {
+				method: 'POST',
+				body: data
+			})
+			.then(response => response.json())
+			.then(f => {
+				this.reqImport(f);
+			})
+			.catch(e => {
+				this.$root.dialog({
+					type: 'error',
+					text: e
+				});
+			})
+			.finally(() => {
+				dialog.close();
+			});
+		},
+
+		reqImport(file) {
+			this.$root.api(
+				this.exportTarget == 'following' ? 'i/import-following' :
+				this.exportTarget == 'user-lists' ? 'i/import-user-lists' :
+				null, {
+					fileId: file.id
+			}).then(() => {
+				this.$root.dialog({
+					type: 'info',
+					text: this.$t('importRequested')
+				});
+			}).catch((e: any) => {
+				this.$root.dialog({
+					type: 'error',
+					text: e.message
+				});
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue
new file mode 100644
index 0000000000000000000000000000000000000000..1a00c65760c94b7540c59983797544f90785b700
--- /dev/null
+++ b/src/client/pages/settings/index.vue
@@ -0,0 +1,94 @@
+<template>
+<div class="mk-settings-page">
+	<portal to="icon"><fa :icon="faCog"/></portal>
+	<portal to="title">{{ $t('settings') }}</portal>
+
+	<x-profile-setting/>
+	<x-privacy-setting/>
+	<x-reaction-setting/>
+	<x-theme/>
+	<x-import-export/>
+	<x-drive/>
+	<x-general/>
+	<x-mute-block/>
+	<x-security/>
+	<x-2fa/>
+	<x-integration/>
+
+	<mk-button @click="cacheClear()" primary class="cacheClear">{{ $t('cacheClear') }}</mk-button>
+	<mk-button @click="$root.signout()" primary class="logout">{{ $t('logout') }}</mk-button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faCog } from '@fortawesome/free-solid-svg-icons';
+import XProfileSetting from './profile.vue';
+import XPrivacySetting from './privacy.vue';
+import XImportExport from './import-export.vue';
+import XDrive from './drive.vue';
+import XGeneral from './general.vue';
+import XReactionSetting from './reaction.vue';
+import XMuteBlock from './mute-block.vue';
+import XSecurity from './security.vue';
+import XTheme from './theme.vue';
+import X2fa from './2fa.vue';
+import XIntegration from './integration.vue';
+import MkButton from '../../components/ui/button.vue';
+
+export default Vue.extend({
+	metaInfo() {
+		return {
+			title: this.$t('settings') as string
+		};
+	},
+
+	components: {
+		XProfileSetting,
+		XPrivacySetting,
+		XImportExport,
+		XDrive,
+		XGeneral,
+		XReactionSetting,
+		XMuteBlock,
+		XSecurity,
+		XTheme,
+		X2fa,
+		XIntegration,
+		MkButton,
+	},
+
+	data() {
+		return {
+			faCog
+		}
+	},
+
+	methods: {
+		cacheClear() {
+			// Clear cache (service worker)
+			try {
+				navigator.serviceWorker.controller.postMessage('clear');
+
+				navigator.serviceWorker.getRegistrations().then(registrations => {
+					for (const registration of registrations) registration.unregister();
+				});
+			} catch (e) {
+				console.error(e);
+			}
+
+			// Force reload
+			location.reload(true);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-settings-page {
+	> .logout,
+	> .cacheClear {
+		margin: 8px auto;
+	}
+}
+</style>
diff --git a/src/client/pages/settings/integration.vue b/src/client/pages/settings/integration.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b156e130274a357c531ae0d989cb9744e0d34c5a
--- /dev/null
+++ b/src/client/pages/settings/integration.vue
@@ -0,0 +1,122 @@
+<template>
+<section class="_section" v-if="enableTwitterIntegration || enableDiscordIntegration || enableGithubIntegration">
+	<div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
+	<div class="_content" v-if="enableTwitterIntegration">
+		<header><fa :icon="faTwitter"/> Twitter</header>
+		<p v-if="$store.state.i.twitter">{{ $t('connectedTo') }}: <a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
+		<mk-button v-if="$store.state.i.twitter" @click="disconnectTwitter">{{ $t('disconnectSerice') }}</mk-button>
+		<mk-button v-else @click="connectTwitter">{{ $t('connectSerice') }}</mk-button>
+	</div>
+
+	<div class="_content" v-if="enableDiscordIntegration">
+		<header><fa :icon="faDiscord"/> Discord</header>
+		<p v-if="$store.state.i.discord">{{ $t('connectedTo') }}: <a :href="`https://discordapp.com/users/${$store.state.i.discord.id}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.discord.username }}#{{ $store.state.i.discord.discriminator }}</a></p>
+		<mk-button v-if="$store.state.i.discord" @click="disconnectDiscord">{{ $t('disconnectSerice') }}</mk-button>
+		<mk-button v-else @click="connectDiscord">{{ $t('connectSerice') }}</mk-button>
+	</div>
+
+	<div class="_content" v-if="enableGithubIntegration">
+		<header><fa :icon="faGithub"/> GitHub</header>
+		<p v-if="$store.state.i.github">{{ $t('connectedTo') }}: <a :href="`https://github.com/${$store.state.i.github.login}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.github.login }}</a></p>
+		<mk-button v-if="$store.state.i.github" @click="disconnectGithub">{{ $t('disconnectSerice') }}</mk-button>
+		<mk-button v-else @click="connectGithub">{{ $t('connectSerice') }}</mk-button>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faShareAlt } from '@fortawesome/free-solid-svg-icons';
+import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
+import i18n from '../../i18n';
+import { apiUrl } from '../../config';
+import MkButton from '../../components/ui/button.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		MkButton
+	},
+
+	data() {
+		return {
+			apiUrl,
+			twitterForm: null,
+			discordForm: null,
+			githubForm: null,
+			enableTwitterIntegration: false,
+			enableDiscordIntegration: false,
+			enableGithubIntegration: false,
+			faShareAlt, faTwitter, faDiscord, faGithub
+		};
+	},
+
+	created() {
+		this.$root.getMeta().then(meta => {
+			this.enableTwitterIntegration = meta.enableTwitterIntegration;
+			this.enableDiscordIntegration = meta.enableDiscordIntegration;
+			this.enableGithubIntegration = meta.enableGithubIntegration;
+		});
+	},
+
+	mounted() {
+		if (!document.cookie.match(/i=(\w+)/)) {
+			document.cookie = `i=${this.$store.state.i.token}; path=/;` +
+			` domain=${document.location.hostname}; max-age=31536000;` +
+			(document.location.protocol.startsWith('https') ? ' secure' : '');
+		}
+		this.$watch('$store.state.i', () => {
+			if (this.$store.state.i.twitter) {
+				if (this.twitterForm) this.twitterForm.close();
+			}
+			if (this.$store.state.i.discord) {
+				if (this.discordForm) this.discordForm.close();
+			}
+			if (this.$store.state.i.github) {
+				if (this.githubForm) this.githubForm.close();
+			}
+		}, {
+			deep: true
+		});
+	},
+
+	methods: {
+		connectTwitter() {
+			this.twitterForm = window.open(apiUrl + '/connect/twitter',
+				'twitter_connect_window',
+				'height=570, width=520');
+		},
+
+		disconnectTwitter() {
+			window.open(apiUrl + '/disconnect/twitter',
+				'twitter_disconnect_window',
+				'height=570, width=520');
+		},
+
+		connectDiscord() {
+			this.discordForm = window.open(apiUrl + '/connect/discord',
+				'discord_connect_window',
+				'height=570, width=520');
+		},
+
+		disconnectDiscord() {
+			window.open(apiUrl + '/disconnect/discord',
+				'discord_disconnect_window',
+				'height=570, width=520');
+		},
+
+		connectGithub() {
+			this.githubForm = window.open(apiUrl + '/connect/github',
+				'github_connect_window',
+				'height=570, width=520');
+		},
+
+		disconnectGithub() {
+			window.open(apiUrl + '/disconnect/github',
+				'github_disconnect_window',
+				'height=570, width=520');
+		},
+	}
+});
+</script>
diff --git a/src/client/pages/settings/mute-block.vue b/src/client/pages/settings/mute-block.vue
new file mode 100644
index 0000000000000000000000000000000000000000..109b33d4f55b38bc630c6a5c815265ec7876499f
--- /dev/null
+++ b/src/client/pages/settings/mute-block.vue
@@ -0,0 +1,76 @@
+<template>
+<section class="mk-settings-page-mute-block _section">
+	<div class="_title"><fa :icon="faBan"/> {{ $t('muteAndBlock') }}</div>
+	<div class="_content">
+		<span>{{ $t('mutedUsers') }}</span>
+		<mk-pagination :pagination="mutingPagination" class="muting">
+			<template #empty><span>{{ $t('noUsers') }}</span></template>
+			<template #default="{items}">
+				<div class="user" v-for="(mute, i) in items" :key="mute.id" :data-index="i">
+					<router-link class="name" :to="mute.mutee | userPage">
+						<mk-acct :user="mute.mutee"/>
+					</router-link>
+				</div>
+			</template>
+		</mk-pagination>
+	</div>
+	<div class="_content">
+		<span>{{ $t('blockedUsers') }}</span>
+		<mk-pagination :pagination="blockingPagination" class="blocking">
+			<template #empty><span>{{ $t('noUsers') }}</span></template>
+			<template #default="{items}">
+				<div class="user" v-for="(block, i) in items" :key="block.id" :data-index="i">
+					<router-link class="name" :to="block.blockee | userPage">
+						<mk-acct :user="block.blockee"/>
+					</router-link>
+				</div>
+			</template>
+		</mk-pagination>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faBan } from '@fortawesome/free-solid-svg-icons';
+import MkPagination from '../../components/ui/pagination.vue';
+import i18n from '../../i18n';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		MkPagination,
+	},
+
+	data() {
+		return {
+			mutingPagination: {
+				endpoint: 'mute/list',
+				limit: 10,
+			},
+			blockingPagination: {
+				endpoint: 'blocking/list',
+				limit: 10,
+			},
+			faBan
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-settings-page-mute-block {
+	> ._content {
+		max-height: 350px;
+		overflow: auto;
+
+		> .muting,
+		> .blocking {
+			> .empty {
+				opacity: 0.5 !important;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/settings/privacy.vue b/src/client/pages/settings/privacy.vue
new file mode 100644
index 0000000000000000000000000000000000000000..0fc67d5b7db5b4f72ac45b8884c4bc3a61065c5a
--- /dev/null
+++ b/src/client/pages/settings/privacy.vue
@@ -0,0 +1,69 @@
+<template>
+<section class="mk-settings-page-privacy _section">
+	<div class="_title"><fa :icon="faLock"/> {{ $t('privacy') }}</div>
+	<div class="_content">
+		<mk-switch v-model="isLocked" @change="save()">{{ $t('makeFollowManuallyApprove') }}</mk-switch>
+		<mk-switch v-model="autoAcceptFollowed" :disabled="!isLocked" @change="save()">{{ $t('autoAcceptFollowed') }}</mk-switch>
+	</div>
+	<div class="_content">
+		<mk-select v-model="defaultNoteVisibility" style="margin-top: 8px;">
+			<template #label>{{ $t('defaultNoteVisibility') }}</template>
+			<option value="public">{{ $t('_visibility.public') }}</option>
+			<option value="followers">{{ $t('_visibility.followers') }}</option>
+			<option value="specified">{{ $t('_visibility.specified') }}</option>
+		</mk-select>
+		<mk-switch v-model="rememberNoteVisibility" @change="save()">{{ $t('rememberNoteVisibility') }}</mk-switch>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faLock } from '@fortawesome/free-solid-svg-icons';
+import MkSelect from '../../components/ui/select.vue';
+import MkSwitch from '../../components/ui/switch.vue';
+import i18n from '../../i18n';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		MkSelect,
+		MkSwitch,
+	},
+	
+	data() {
+		return {
+			isLocked: false,
+			autoAcceptFollowed: false,
+			faLock
+		}
+	},
+
+	computed: {
+		defaultNoteVisibility: {
+			get() { return this.$store.state.settings.defaultNoteVisibility; },
+			set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); }
+		},
+
+		rememberNoteVisibility: {
+			get() { return this.$store.state.settings.rememberNoteVisibility; },
+			set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); }
+		},
+	},
+
+	created() {
+		this.isLocked = this.$store.state.i.isLocked;
+		this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed;
+	},
+
+	methods: {
+		save() {
+			this.$root.api('i/update', {
+				isLocked: !!this.isLocked,
+				autoAcceptFollowed: !!this.autoAcceptFollowed,
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/settings/profile.vue b/src/client/pages/settings/profile.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e6219c2d56f4e25c8c0eaeeb3fd17f55a650637e
--- /dev/null
+++ b/src/client/pages/settings/profile.vue
@@ -0,0 +1,246 @@
+<template>
+<section class="mk-settings-page-profile _section">
+	<div class="_title"><fa :icon="faUser"/> {{ $t('profile') }}<small style="display: block; font-weight: normal; opacity: 0.6;">@{{ $store.state.i.username }}@{{ host }}</small></div>
+	<div class="_content">
+		<mk-input v-model="name" :max="30">
+			<span>{{ $t('_profile.name') }}</span>
+		</mk-input>
+
+		<mk-textarea v-model="description" :max="500">
+			<span>{{ $t('_profile.description') }}</span>
+			<template #desc>{{ $t('_profile.youCanIncludeHashtags') }}</template>
+		</mk-textarea>
+
+		<mk-input v-model="location">
+			<span>{{ $t('location') }}</span>
+			<template #prefix><fa :icon="faMapMarkerAlt"/></template>
+		</mk-input>
+
+		<mk-input v-model="birthday" type="date">
+			<template #title>{{ $t('birthday') }}</template>
+			<template #prefix><fa :icon="faBirthdayCake"/></template>
+		</mk-input>
+
+		<mk-input type="file" @change="onAvatarChange">
+			<span>{{ $t('avatar') }}</span>
+			<template #icon><fa :icon="faImage"/></template>
+			<template #desc v-if="avatarUploading">{{ $t('uploading') }}<mk-ellipsis/></template>
+		</mk-input>
+
+		<mk-input type="file" @change="onBannerChange">
+			<span>{{ $t('banner') }}</span>
+			<template #icon><fa :icon="faImage"/></template>
+			<template #desc v-if="bannerUploading">{{ $t('uploading') }}<mk-ellipsis/></template>
+		</mk-input>
+
+		<details class="fields">
+			<summary>{{ $t('_profile.metadata') }}</summary>
+			<div class="row">
+				<mk-input v-model="fieldName0">{{ $t('_profile.metadataLabel') }}</mk-input>
+				<mk-input v-model="fieldValue0">{{ $t('_profile.metadataContent') }}</mk-input>
+			</div>
+			<div class="row">
+				<mk-input v-model="fieldName1">{{ $t('_profile.metadataLabel') }}</mk-input>
+				<mk-input v-model="fieldValue1">{{ $t('_profile.metadataContent') }}</mk-input>
+			</div>
+			<div class="row">
+				<mk-input v-model="fieldName2">{{ $t('_profile.metadataLabel') }}</mk-input>
+				<mk-input v-model="fieldValue2">{{ $t('_profile.metadataContent') }}</mk-input>
+			</div>
+			<div class="row">
+				<mk-input v-model="fieldName3">{{ $t('_profile.metadataLabel') }}</mk-input>
+				<mk-input v-model="fieldValue3">{{ $t('_profile.metadataContent') }}</mk-input>
+			</div>
+		</details>
+
+		<mk-switch v-model="isBot">{{ $t('flagAsBot') }}</mk-switch>
+		<mk-switch v-model="isCat">{{ $t('flagAsCat') }}</mk-switch>
+	</div>
+	<div class="_footer">
+		<mk-button @click="save(true)" primary><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faUnlockAlt, faCogs, faImage, faUser, faMapMarkerAlt, faBirthdayCake } from '@fortawesome/free-solid-svg-icons';
+import { faSave } from '@fortawesome/free-regular-svg-icons';
+import MkButton from '../../components/ui/button.vue';
+import MkInput from '../../components/ui/input.vue';
+import MkTextarea from '../../components/ui/textarea.vue';
+import MkSwitch from '../../components/ui/switch.vue';
+import i18n from '../../i18n';
+import { apiUrl, host } from '../../config';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		MkButton,
+		MkInput,
+		MkTextarea,
+		MkSwitch,
+	},
+	
+	data() {
+		return {
+			host,
+			name: null,
+			description: null,
+			birthday: null,
+			location: null,
+			fieldName0: null,
+			fieldValue0: null,
+			fieldName1: null,
+			fieldValue1: null,
+			fieldName2: null,
+			fieldValue2: null,
+			fieldName3: null,
+			fieldValue3: null,
+			avatarId: null,
+			bannerId: null,
+			isBot: false,
+			isCat: false,
+			saving: false,
+			avatarUploading: false,
+			bannerUploading: false,
+			faSave, faUnlockAlt, faCogs, faImage, faUser, faMapMarkerAlt, faBirthdayCake
+		}
+	},
+
+	created() {
+		this.name = this.$store.state.i.name;
+		this.description = this.$store.state.i.description;
+		this.location = this.$store.state.i.location;
+		this.birthday = this.$store.state.i.birthday;
+		this.avatarId = this.$store.state.i.avatarId;
+		this.bannerId = this.$store.state.i.bannerId;
+		this.isBot = this.$store.state.i.isBot;
+		this.isCat = this.$store.state.i.isCat;
+
+		this.fieldName0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].name : null;
+		this.fieldValue0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].value : null;
+		this.fieldName1 = this.$store.state.i.fields[1] ? this.$store.state.i.fields[1].name : null;
+		this.fieldValue1 = this.$store.state.i.fields[1] ? this.$store.state.i.fields[1].value : null;
+		this.fieldName2 = this.$store.state.i.fields[2] ? this.$store.state.i.fields[2].name : null;
+		this.fieldValue2 = this.$store.state.i.fields[2] ? this.$store.state.i.fields[2].value : null;
+		this.fieldName3 = this.$store.state.i.fields[3] ? this.$store.state.i.fields[3].name : null;
+		this.fieldValue3 = this.$store.state.i.fields[3] ? this.$store.state.i.fields[3].value : null;
+	},
+
+	methods: {
+		onAvatarChange([file]) {
+			this.avatarUploading = true;
+
+			const data = new FormData();
+			data.append('file', file);
+			data.append('i', this.$store.state.i.token);
+
+			fetch(apiUrl + '/drive/files/create', {
+				method: 'POST',
+				body: data
+			})
+			.then(response => response.json())
+			.then(f => {
+				this.avatarId = f.id;
+				this.avatarUploading = false;
+			})
+			.catch(e => {
+				this.avatarUploading = false;
+					this.$root.dialog({
+					type: 'error',
+					text: e
+				});
+			});
+		},
+
+		onBannerChange([file]) {
+			this.bannerUploading = true;
+
+			const data = new FormData();
+			data.append('file', file);
+			data.append('i', this.$store.state.i.token);
+
+			fetch(apiUrl + '/drive/files/create', {
+				method: 'POST',
+				body: data
+			})
+			.then(response => response.json())
+			.then(f => {
+				this.bannerId = f.id;
+				this.bannerUploading = false;
+			})
+			.catch(e => {
+				this.bannerUploading = false;
+				this.$root.dialog({
+					type: 'error',
+					text: e
+				});
+			});
+		},
+
+		save(notify) {
+			const fields = [
+				{ name: this.fieldName0, value: this.fieldValue0 },
+				{ name: this.fieldName1, value: this.fieldValue1 },
+				{ name: this.fieldName2, value: this.fieldValue2 },
+				{ name: this.fieldName3, value: this.fieldValue3 },
+			];
+
+			this.saving = true;
+
+			this.$root.api('i/update', {
+				name: this.name || null,
+				description: this.description || null,
+				location: this.location || null,
+				birthday: this.birthday || null,
+				avatarId: this.avatarId || undefined,
+				bannerId: this.bannerId || undefined,
+				fields,
+				isBot: !!this.isBot,
+				isCat: !!this.isCat,
+			}).then(i => {
+				this.saving = false;
+				this.$store.state.i.avatarId = i.avatarId;
+				this.$store.state.i.avatarUrl = i.avatarUrl;
+				this.$store.state.i.bannerId = i.bannerId;
+				this.$store.state.i.bannerUrl = i.bannerUrl;
+
+				if (notify) {
+					this.$root.dialog({
+						type: 'success',
+						iconOnly: true, autoClose: true
+					});
+				}
+			}).catch(err => {
+				this.saving = false;
+				this.$root.dialog({
+					type: 'error',
+					text: err.id
+				});
+			});
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-settings-page-profile {
+	> ._content {
+		> *:first-child {
+			margin-top: 0;
+		}
+
+		> .fields {
+			> .row {
+				> * {
+					display: inline-block;
+					width: 50%;
+					margin-bottom: 0;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/settings/reaction.vue b/src/client/pages/settings/reaction.vue
new file mode 100644
index 0000000000000000000000000000000000000000..310237b5fdd26e3cb010ed8f3fd164421bf2faec
--- /dev/null
+++ b/src/client/pages/settings/reaction.vue
@@ -0,0 +1,62 @@
+<template>
+<section class="mk-settings-page-reaction _section">
+	<div class="_title"><fa :icon="faLaugh"/> {{ $t('reaction') }}</div>
+	<div class="_content">
+		<mk-textarea v-model="reactions" style="margin-top: 16px;">{{ $t('reaction') }}<template #desc>{{ $t('reactionSettingDescription') }}</template></mk-textarea>
+	</div>
+	<div class="_footer">
+		<mk-button @click="save()" primary inline :disabled="!changed"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+		<mk-button inline @click="preview"><fa :icon="faEye"/> {{ $t('preview') }}</mk-button>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faLaugh, faSave, faEye } from '@fortawesome/free-regular-svg-icons';
+import MkTextarea from '../../components/ui/textarea.vue';
+import MkButton from '../../components/ui/button.vue';
+import MkReactionPicker from '../../components/reaction-picker.vue';
+import i18n from '../../i18n';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		MkTextarea,
+		MkButton,
+	},
+	
+	data() {
+		return {
+			reactions: this.$store.state.settings.reactions.join('\n'),
+			changed: false,
+			faLaugh, faSave, faEye
+		}
+	},
+
+	watch: {
+		reactions() {
+			this.changed = true;
+		}
+	},
+
+	methods: {
+		save() {
+			this.$store.dispatch('settings/set', { key: 'reactions', value: this.reactions.trim().split('\n') });
+			this.changed = false;
+		},
+
+		preview(ev) {
+			const picker = this.$root.new(MkReactionPicker, {
+				source: ev.currentTarget || ev.target,
+				reactions: this.reactions.trim().split('\n'),
+				showFocus: false,
+			});
+			picker.$once('chosen', reaction => {
+				picker.close();
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/settings/security.vue b/src/client/pages/settings/security.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ecf9c01dd5b70d34548f0aad56dfaa29a6c1e81e
--- /dev/null
+++ b/src/client/pages/settings/security.vue
@@ -0,0 +1,87 @@
+<template>
+<section class="_section">
+	<div class="_title"><fa :icon="faLock"/> {{ $t('password') }}</div>
+	<div class="_content">
+		<mk-button primary @click="change()">{{ $t('changePassword') }}</mk-button>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faLock } from '@fortawesome/free-solid-svg-icons';
+import MkButton from '../../components/ui/button.vue';
+import i18n from '../../i18n';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		MkButton,
+	},
+	
+	data() {
+		return {
+			faLock
+		}
+	},
+
+	methods: {
+		async change() {
+			const { canceled: canceled1, result: currentPassword } = await this.$root.dialog({
+				title: this.$t('currentPassword'),
+				input: {
+					type: 'password'
+				}
+			});
+			if (canceled1) return;
+
+			const { canceled: canceled2, result: newPassword } = await this.$root.dialog({
+				title: this.$t('newPassword'),
+				input: {
+					type: 'password'
+				}
+			});
+			if (canceled2) return;
+
+			const { canceled: canceled3, result: newPassword2 } = await this.$root.dialog({
+				title: this.$t('newPasswordRetype'),
+				input: {
+					type: 'password'
+				}
+			});
+			if (canceled3) return;
+
+			if (newPassword !== newPassword2) {
+				this.$root.dialog({
+					type: 'error',
+					text: this.$t('retypedNotMatch')
+				});
+				return;
+			}
+
+			const dialog = this.$root.dialog({
+				type: 'waiting',
+				iconOnly: true
+			});
+			
+			this.$root.api('i/change-password', {
+				currentPassword,
+				newPassword
+			}).then(() => {
+				this.$root.dialog({
+					type: 'success',
+					iconOnly: true, autoClose: true
+				});
+			}).catch(e => {
+				this.$root.dialog({
+					type: 'error',
+					text: e
+				});
+			}).finally(() => {
+				dialog.close();
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/settings/theme.vue b/src/client/pages/settings/theme.vue
new file mode 100644
index 0000000000000000000000000000000000000000..71628ab2b9c88adc4137ac19a51b919a4c305f98
--- /dev/null
+++ b/src/client/pages/settings/theme.vue
@@ -0,0 +1,76 @@
+<template>
+<section class="mk-settings-page-theme _section">
+	<div class="_title"><fa :icon="faPalette"/> {{ $t('theme') }}</div>
+	<div class="_content">
+		<mk-select v-model="theme" :placeholder="$t('theme')">
+			<template #label>{{ $t('theme') }}</template>
+			<optgroup :label="$t('lightThemes')">
+				<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+			</optgroup>
+			<optgroup :label="$t('darkThemes')">
+				<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+			</optgroup>
+		</mk-select>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faPalette } from '@fortawesome/free-solid-svg-icons';
+import MkInput from '../../components/ui/input.vue';
+import MkButton from '../../components/ui/button.vue';
+import MkSelect from '../../components/ui/select.vue';
+import i18n from '../../i18n';
+import { Theme, builtinThemes, applyTheme } from '../../theme';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		MkInput,
+		MkButton,
+		MkSelect,
+	},
+	
+	data() {
+		return {
+			wallpaperUploading: false,
+			faPalette
+		}
+	},
+
+	computed: {
+		themes(): Theme[] {
+			return builtinThemes.concat(this.$store.state.device.themes);
+		},
+
+		installedThemes(): Theme[] {
+			return this.$store.state.device.themes;
+		},
+	
+		darkThemes(): Theme[] {
+			return this.themes.filter(t => t.base == 'dark' || t.kind == 'dark');
+		},
+
+		lightThemes(): Theme[] {
+			return this.themes.filter(t => t.base == 'light' || t.kind == 'light');
+		},
+		
+		theme: {
+			get() { return this.$store.state.device.theme; },
+			set(value) { this.$store.commit('device/set', { key: 'theme', value }); }
+		},
+	},
+
+	watch: {
+		theme() {
+			applyTheme(this.themes.find(x => x.id === this.theme));
+		}
+	},
+
+	methods: {
+
+	}
+});
+</script>
diff --git a/src/client/pages/tag.vue b/src/client/pages/tag.vue
new file mode 100644
index 0000000000000000000000000000000000000000..f53f3c5ca174c1da93139015cbc4f36618be5ec6
--- /dev/null
+++ b/src/client/pages/tag.vue
@@ -0,0 +1,49 @@
+<template>
+<x-notes ref="notes" :pagination="pagination" @before="before" @after="after"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../scripts/loading';
+import XNotes from '../components/notes.vue';
+
+export default Vue.extend({
+	metaInfo() {
+		return {
+			title: '#' + this.$route.params.tag
+		};
+	},
+
+	components: {
+		XNotes
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'notes/search-by-tag',
+				limit: 10,
+				params: () => ({
+					tag: this.$route.params.tag,
+				})
+			}
+		};
+	},
+
+	watch: {
+		$route() {
+			(this.$refs.notes as any).reload();
+		}
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/user/follow-list.vue b/src/client/pages/user/follow-list.vue
new file mode 100644
index 0000000000000000000000000000000000000000..faaee3b107391c23a15fc5b4e5881f29db1c5486
--- /dev/null
+++ b/src/client/pages/user/follow-list.vue
@@ -0,0 +1,140 @@
+<template>
+<mk-pagination :pagination="pagination" #default="{items}" class="mk-following-or-followers" ref="list">
+	<div class="user _panel" v-for="(user, i) in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" :data-index="i">
+		<mk-avatar class="avatar" :user="user"/>
+		<div class="body">
+			<div class="name">
+				<router-link class="name" :to="user | userPage" v-user-preview="user.id"><mk-user-name :user="user"/></router-link>
+				<p class="acct">@{{ user | acct }}</p>
+			</div>
+			<div class="description" v-if="user.description" :title="user.description">
+				<mfm :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :plain="true" :nowrap="true"/>
+			</div>
+			<x-follow-button class="koudoku-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/>
+		</div>
+	</div>
+</mk-pagination>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import parseAcct from '../../../misc/acct/parse';
+import i18n from '../../i18n';
+import XFollowButton from '../../components/follow-button.vue';
+import MkPagination from '../../components/ui/pagination.vue';
+
+export default Vue.extend({
+	i18n,
+
+	components: {
+		MkPagination,
+		XFollowButton,
+	},
+
+	props: {
+		type: {
+			type: String,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: () => this.type === 'following' ? 'users/following' : 'users/followers',
+				limit: 20,
+				params: {
+					...parseAcct(this.$route.params.user),
+				}
+			},
+		};
+	},
+
+	watch: {
+		type() {
+			this.$refs.list.reload();
+		},
+
+		'$route'() {
+			this.$refs.list.reload();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-following-or-followers {
+	> .user {
+		display: flex;
+		padding: 16px;
+
+		> .avatar {
+			display: block;
+			flex-shrink: 0;
+			margin: 0 12px 0 0;
+			width: 42px;
+			height: 42px;
+			border-radius: 8px;
+		}
+
+		> .body {
+			display: flex;
+			width: calc(100% - 54px);
+			position: relative;
+
+			> .name {
+				width: 45%;
+
+				@media (max-width: 500px) {
+					width: 100%;
+				}
+
+				> .name,
+				> .acct {
+					display: block;
+					white-space: nowrap;
+					text-overflow: ellipsis;
+					overflow: hidden;
+					margin: 0;
+				}
+
+				> .name {
+					font-size: 16px;
+					line-height: 24px;
+				}
+
+				> .acct {
+					font-size: 15px;
+					line-height: 16px;
+					opacity: 0.7;
+				}
+			}
+
+			> .description {
+				width: 55%;
+				line-height: 42px;
+				white-space: nowrap;
+				overflow: hidden;
+				text-overflow: ellipsis;
+				opacity: 0.7;
+				font-size: 14px;
+				padding-right: 40px;
+				padding-left: 8px;
+				box-sizing: border-box;
+
+				@media (max-width: 500px) {
+					display: none;
+				}
+			}
+
+			> .koudoku-button {
+				position: absolute;
+				top: 0;
+				bottom: 0;
+				right: 0;
+				margin: auto 0;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/app/common/views/components/activity.vue b/src/client/pages/user/index.activity.vue
similarity index 97%
rename from src/client/app/common/views/components/activity.vue
rename to src/client/pages/user/index.activity.vue
index a958616943f220c956e28aa37a2b813a3a804e05..29dcca066448cf099473bb6939bef73d4ede3d5a 100644
--- a/src/client/app/common/views/components/activity.vue
+++ b/src/client/pages/user/index.activity.vue
@@ -17,7 +17,7 @@ export default Vue.extend({
 		limit: {
 			type: Number,
 			required: false,
-			default: 21
+			default: 40
 		}
 	},
 	data() {
@@ -69,7 +69,7 @@ export default Vue.extend({
 				},
 				plotOptions: {
 					bar: {
-						columnWidth: '80%'
+						columnWidth: '40%'
 					}
 				},
 				dataLabels: {
diff --git a/src/client/app/mobile/views/pages/user/home.photos.vue b/src/client/pages/user/index.photos.vue
similarity index 59%
rename from src/client/app/mobile/views/pages/user/home.photos.vue
rename to src/client/pages/user/index.photos.vue
index b5547c916f834a4d8bb0c15d4155353cb2c1a2b1..cd29254f483ca5a793fd365e624bb4151799c4db 100644
--- a/src/client/app/mobile/views/pages/user/home.photos.vue
+++ b/src/client/pages/user/index.photos.vue
@@ -1,6 +1,6 @@
 <template>
-<div class="root photos">
-	<p class="initializing" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
+<div class="ujigsodd">
+	<mk-loading v-if="fetching"/>
 	<div class="stream" v-if="!fetching && images.length > 0">
 		<a v-for="(image, i) in images" :key="i"
 			class="img"
@@ -14,11 +14,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../../../i18n';
-import { getStaticImageUrl } from '../../../../common/scripts/get-static-image-url';
+import i18n from '../../i18n';
+import { getStaticImageUrl } from '../../scripts/get-static-image-url';
 
 export default Vue.extend({
-	i18n: i18n('mobile/views/pages/user/home.photos.vue'),
+	i18n,
 	props: ['user'],
 	data() {
 		return {
@@ -63,37 +63,36 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
-.root.photos
+<style lang="scss" scoped>
+.ujigsodd {
 
-	> .stream
-		display -webkit-flex
-		display -moz-flex
-		display -ms-flex
-		display flex
-		justify-content center
-		flex-wrap wrap
-		padding 8px
+	> .stream {
+		display: flex;
+		justify-content: center;
+		flex-wrap: wrap;
+		padding: 8px;
 
-		> .img
-			flex 1 1 33%
-			width 33%
-			height 90px
-			background-position center center
-			background-size cover
-			background-clip content-box
-			border solid 2px transparent
-			border-radius 4px
+		> .img {
+			flex: 1 1 33%;
+			width: 33%;
+			height: 90px;
+			box-sizing: border-box;
+			background-position: center center;
+			background-size: cover;
+			background-clip: content-box;
+			border: solid 2px transparent;
+			border-radius: 4px;
+		}
+	}
 
-	> .initializing
-	> .empty
-		margin 0
-		padding 16px
-		text-align center
-		color var(--text)
-
-		> i
-			margin-right 4px
+	> .empty {
+		margin: 0;
+		padding: 16px;
+		text-align: center;
 
+		> i {
+			margin-right: 4px;
+		}
+	}
+}
 </style>
-
diff --git a/src/client/pages/user/index.timeline.vue b/src/client/pages/user/index.timeline.vue
new file mode 100644
index 0000000000000000000000000000000000000000..1878a9b1f38c1d2a04e5d480bf5d6e7a345633e2
--- /dev/null
+++ b/src/client/pages/user/index.timeline.vue
@@ -0,0 +1,79 @@
+<template>
+<div class="kjeftjfm">
+	<div class="with">
+		<button class="_button" @click="with_ = null" :class="{ active: with_ === null }">{{ $t('notes') }}</button>
+		<button class="_button" @click="with_ = 'replies'" :class="{ active: with_ === 'replies' }">{{ $t('notesAndReplies') }}</button>
+		<button class="_button" @click="with_ = 'files'" :class="{ active: with_ === 'files' }">{{ $t('withFiles') }}</button>
+	</div>
+	<x-notes ref="timeline" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XNotes from '../../components/notes.vue';
+
+export default Vue.extend({
+	components: {
+		XNotes
+	},
+
+	props: {
+		user: {
+			type: Object,
+			required: true,
+		},
+	},
+
+	watch: {
+		user() {
+			this.$refs.timeline.reload();
+		},
+
+		with_() {
+			this.$refs.timeline.reload();
+		},
+	},
+
+	data() {
+		return {
+			date: null,
+			with_: null,
+			pagination: {
+				endpoint: 'users/notes',
+				limit: 10,
+				params: init => ({
+					userId: this.user.id,
+					includeReplies: this.with_ === 'replies',
+					withFiles: this.with_ === 'files',
+					untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
+				})
+			}
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.kjeftjfm {
+	> .with {
+		display: flex;
+		margin-bottom: var(--margin);
+
+		@media (max-width: 500px) {
+			font-size: 80%;
+		}
+
+		> button {
+			flex: 1;
+			padding: 11px 8px 8px 8px;
+			border-bottom: solid 3px transparent;
+
+			&.active {
+				color: var(--accent);
+				border-bottom-color: var(--accent);
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7bf45621b38022f2805342d5b5785caa73657e8f
--- /dev/null
+++ b/src/client/pages/user/index.vue
@@ -0,0 +1,476 @@
+<template>
+<div class="mk-user-page" v-if="user">
+	<portal to="title" v-if="user"><mk-user-name :user="user" :nowrap="false" class="name"/></portal>
+	<portal to="avatar" v-if="user"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal>
+	
+	<div class="remote-caution _panel" v-if="user.host != null"><fa :icon="faExclamationTriangle" style="margin-right: 8px;"/>{{ $t('remoteUserCaution') }}<a :href="user.url" rel="nofollow noopener" target="_blank">{{ $t('showOnRemote') }}</a></div>
+	<transition name="zoom" mode="out-in" appear>
+		<div class="profile _panel" :key="user.id">
+			<div class="banner-container" :style="style">
+				<div class="banner" ref="banner" :style="style"></div>
+				<div class="fade"></div>
+				<div class="title">
+					<mk-user-name class="name" :user="user" :nowrap="true"/>
+					<div class="bottom">
+						<span class="username"><mk-acct :user="user" :detail="true" /></span>
+						<span v-if="user.isAdmin" :title="$t('isAdmin')"><fa :icon="faBookmark"/></span>
+						<span v-if="user.isLocked" :title="$t('isLocked')"><fa :icon="faLock"/></span>
+						<span v-if="user.isBot" :title="$t('isBot')"><fa :icon="faRobot"/></span>
+					</div>
+				</div>
+				<span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('followsYou') }}</span>
+				<div class="actions" v-if="$store.getters.isSignedIn">
+					<button @click="menu" class="menu _button" ref="menu"><fa :icon="faEllipsisH"/></button>
+					<x-follow-button v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" class="koudoku"/>
+				</div>
+			</div>
+			<mk-avatar class="avatar" :user="user" :disable-preview="true"/>
+			<div class="title">
+				<mk-user-name :user="user" :nowrap="false" class="name"/>
+				<div class="bottom">
+					<span class="username"><mk-acct :user="user" :detail="true" /></span>
+					<span v-if="user.isAdmin" :title="$t('isAdmin')"><fa :icon="faBookmark"/></span>
+					<span v-if="user.isLocked" :title="$t('isLocked')"><fa :icon="faLock"/></span>
+					<span v-if="user.isBot" :title="$t('isBot')"><fa :icon="faRobot"/></span>
+				</div>
+			</div>
+			<div class="description">
+				<mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
+				<p v-else class="empty">{{ $t('noAccountDescription') }}</p>
+			</div>
+			<div class="fields system">
+				<dl class="field" v-if="user.location">
+					<dt class="name"><fa :icon="faMapMarker" fixed-width/> {{ $t('location') }}</dt>
+					<dd class="value">{{ user.location }}</dd>
+				</dl>
+				<dl class="field" v-if="user.birthday">
+					<dt class="name"><fa :icon="faBirthdayCake" fixed-width/> {{ $t('birthday') }}</dt>
+					<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
+				</dl>
+				<dl class="field">
+					<dt class="name"><fa :icon="faCalendarAlt" fixed-width/> {{ $t('registeredDate') }}</dt>
+					<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<mk-time :time="user.createdAt"/>)</dd>
+				</dl>
+			</div>
+			<div class="fields" v-if="user.fields.length > 0">
+				<dl class="field" v-for="(field, i) in user.fields" :key="i">
+					<dt class="name">
+						<mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
+					</dt>
+					<dd class="value">
+						<mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :colored="false"/>
+					</dd>
+				</dl>
+			</div>
+			<div class="status" v-if="user.host === null">
+				<router-link :to="user | userPage()" :class="{ active: $route.name === 'user' }">
+					<b>{{ user.notesCount | number }}</b>
+					<span>{{ $t('notes') }}</span>
+				</router-link>
+				<router-link :to="user | userPage('following')" :class="{ active: $route.name === 'userFollowing' }">
+					<b>{{ user.followingCount | number }}</b>
+					<span>{{ $t('following') }}</span>
+				</router-link>
+				<router-link :to="user | userPage('followers')" :class="{ active: $route.name === 'userFollowers' }">
+					<b>{{ user.followersCount | number }}</b>
+					<span>{{ $t('followers') }}</span>
+				</router-link>
+			</div>
+		</div>
+	</transition>
+	<router-view :user="user"></router-view>
+	<template v-if="$route.name == 'user'">
+		<sequential-entrance class="pins">
+			<x-note v-for="(note, i) in user.pinnedNotes" class="note" :note="note" :key="note.id" :data-index="i" :detail="true" :pinned="true"/>
+		</sequential-entrance>
+		<mk-container :body-togglable="true" class="content">
+			<template #header><fa :icon="faImage"/>{{ $t('images') }}</template>
+			<div>
+				<x-photos :user="user" :key="user.id"/>
+			</div>
+		</mk-container>
+		<mk-container :body-togglable="true" class="content">
+			<template #header><fa :icon="faChartBar"/>{{ $t('activity') }}</template>
+			<div style="padding:8px;">
+				<x-activity :user="user" :key="user.id"/>
+			</div>
+		</mk-container>
+		<x-user-timeline :user="user"/>
+	</template>
+</div>
+<div v-else-if="error">
+	<mk-error @retry="fetch()"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faEllipsisH, faRobot, faLock, faBookmark, faExclamationTriangle, faChartBar, faImage, faBirthdayCake, faMapMarker } from '@fortawesome/free-solid-svg-icons';
+import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons';
+import * as age from 's-age';
+import XUserTimeline from './index.timeline.vue';
+import XUserMenu from '../../components/user-menu.vue';
+import XNote from '../../components/note.vue';
+import XFollowButton from '../../components/follow-button.vue';
+import MkContainer from '../../components/ui/container.vue';
+import Progress from '../../scripts/loading';
+import parseAcct from '../../../misc/acct/parse';
+
+export default Vue.extend({
+	components: {
+		XUserTimeline,
+		XNote,
+		XFollowButton,
+		MkContainer,
+		XPhotos: () => import('./index.photos.vue').then(m => m.default),
+		XActivity: () => import('./index.activity.vue').then(m => m.default),
+	},
+
+	metaInfo() {
+		return {
+			title: (this.user ? '@' + Vue.filter('acct')(this.user).replace('@', ' | ') : null) as string
+		};
+	},
+
+	data() {
+		return {
+			user: null,
+			error: null,
+			parallaxAnimationId: null,
+			faEllipsisH, faRobot, faLock, faBookmark, faExclamationTriangle, faChartBar, faImage, faBirthdayCake, faMapMarker, faCalendarAlt
+		};
+	},
+
+	computed: {
+		style(): any {
+			if (this.user.bannerUrl == null) return {};
+			return {
+				backgroundImage: `url(${ this.user.bannerUrl })`
+			};
+		},
+
+		age(): number {
+			return age(this.user.birthday);
+		}
+	},
+
+	watch: {
+		$route: 'fetch'
+	},
+
+	created() {
+		this.fetch();
+	},
+
+	mounted() {
+		window.requestAnimationFrame(this.parallaxLoop);
+		window.addEventListener('scroll', this.parallax, { passive: true });
+		document.addEventListener('touchmove', this.parallax, { passive: true });
+		this.$once('hook:beforeDestroy', () => {
+			window.cancelAnimationFrame(this.parallaxAnimationId);
+			window.removeEventListener('scroll', this.parallax);
+			document.removeEventListener('touchmove', this.parallax);
+		});
+	},
+
+	methods: {
+		fetch() {
+			Progress.start();
+			this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
+				this.user = user;
+			}).catch(e => {
+				this.error = e;
+			}).finally(() => {
+				Progress.done();
+			});
+		},
+
+		menu() {
+			this.$root.new(XUserMenu, {
+				source: this.$refs.menu,
+				user: this.user
+			});
+		},
+
+		parallaxLoop() {
+			this.parallaxAnimationId = window.requestAnimationFrame(this.parallaxLoop);
+			this.parallax();
+		},
+
+		parallax() {
+			const banner = this.$refs.banner as any;
+			if (banner == null) return;
+
+			const top = window.scrollY;
+
+			if (top < 0) return;
+
+			const z = 1.75; // 奥行き(小さいほど奥)
+			const pos = -(top / z);
+			banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-user-page {
+	> .remote-caution {
+		font-size: 0.8em;
+		padding: 16px;
+		margin-bottom: var(--margin);
+
+		> a {
+			margin-left: 4px;
+			color: var(--accent);
+		}
+	}
+
+	> .profile {
+		position: relative;
+		margin-bottom: var(--margin);
+		overflow: hidden;
+
+		> .banner-container {
+			position: relative;
+			height: 250px;
+			overflow: hidden;
+			background-size: cover;
+			background-position: center;
+
+			@media (max-width: 500px) {
+				height: 140px;
+			}
+
+			> .banner {
+				height: 100%;
+				background-color: #4c5e6d;
+				background-size: cover;
+				background-position: center;
+				box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
+			}
+
+			> .fade {
+				position: absolute;
+				bottom: 0;
+				left: 0;
+				width: 100%;
+				height: 78px;
+				background: linear-gradient(transparent, rgba(#000, 0.7));
+
+				@media (max-width: 500px) {
+					display: none;
+				}
+			}
+
+			> .followed {
+				position: absolute;
+				top: 12px;
+				left: 12px;
+				padding: 4px 6px;
+				color: #fff;
+				background: rgba(0, 0, 0, 0.7);
+				font-size: 12px;
+			}
+
+			> .actions {
+				position: absolute;
+				top: 12px;
+				right: 12px;
+				-webkit-backdrop-filter: blur(8px);
+				backdrop-filter: blur(8px);
+				background: rgba(0, 0, 0, 0.2);
+				padding: 8px;
+				border-radius: 24px;
+		
+				> .menu {
+					vertical-align: bottom;
+					height: 31px;
+					width: 31px;
+					color: #fff;
+					text-shadow: 0 0 8px #000;
+					font-size: 16px;
+				}
+
+				> .koudoku {
+					margin-left: 4px;
+					vertical-align: bottom;
+				}
+			}
+
+			> .title {
+				position: absolute;
+				bottom: 0;
+				left: 0;
+				width: 100%;
+				padding: 0 0 8px 154px;
+				box-sizing: border-box;
+				color: #fff;
+
+				@media (max-width: 500px) {
+					display: none;
+				}
+
+				> .name {
+					display: block;
+					margin: 0;
+					line-height: 32px;
+					font-weight: bold;
+					font-size: 1.8em;
+					text-shadow: 0 0 8px #000;
+				}
+
+				> .bottom {
+					> * {
+						display: inline-block;
+						margin-right: 16px;
+						line-height: 20px;
+						opacity: 0.8;
+
+						&.username {
+							font-weight: bold;
+						}
+					}
+				}
+			}
+		}
+
+		> .title {
+			display: none;
+			text-align: center;
+			padding: 50px 8px 16px 8px;
+			font-weight: bold;
+			border-bottom: solid 1px var(--divider);
+
+			@media (max-width: 500px) {
+				display: block;
+			}
+
+			> .bottom {
+				> * {
+					display: inline-block;
+					margin-right: 8px;
+					opacity: 0.8;
+				}
+			}
+		}
+
+		> .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);
+
+			@media (max-width: 500px) {
+				top: 90px;
+				left: 0;
+				right: 0;
+				width: 92px;
+				height: 92px;
+				margin: auto;
+			}
+		}
+
+		> .description {
+			padding: 24px 24px 24px 154px;
+			font-size: 15px;
+
+			@media (max-width: 500px) {
+				padding: 16px;
+				text-align: center;
+			}
+
+			> .empty {
+				margin: 0;
+				opacity: 0.5;
+			}
+		}
+
+		> .fields {
+			padding: 24px;
+			font-size: 14px;
+			border-top: solid 1px var(--divider);
+
+			@media (max-width: 500px) {
+				padding: 16px;
+			}
+		
+			> .field {
+				display: flex;
+				padding: 0;
+				margin: 0;
+				align-items: center;
+
+				&:not(:last-child) {
+					margin-bottom: 8px;
+				}
+
+				> .name {
+					width: 30%;
+					overflow: hidden;
+					white-space: nowrap;
+					text-overflow: ellipsis;
+					font-weight: bold;
+					text-align: center;
+				}
+
+				> .value {
+					width: 70%;
+					overflow: hidden;
+					white-space: nowrap;
+					text-overflow: ellipsis;
+				}
+			}
+
+			&.system > .field > .name {
+			}
+		}
+
+		> .status {
+			display: flex;
+			padding: 24px;
+			border-top: solid 1px var(--divider);
+
+			@media (max-width: 500px) {
+				padding: 16px;
+			}
+
+			> a {
+				flex: 1;
+				text-align: center;
+
+				&.active {
+					color: var(--accent);
+				}
+
+				&:hover {
+					text-decoration: none;
+				}
+
+				> b {
+					display: block;
+					line-height: 16px;
+				}
+
+				> span {
+					font-size: 70%;
+				}
+			}
+		}
+	}
+
+	> .pins {
+		> .note {
+			margin-bottom: var(--margin);
+		}
+	}
+
+	> .content {
+		margin-bottom: var(--margin);
+	}
+}
+</style>
diff --git a/src/client/router.ts b/src/client/router.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7eb12c8e44f11103684fe8241c9e756bb163b960
--- /dev/null
+++ b/src/client/router.ts
@@ -0,0 +1,53 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import MkIndex from './pages/index.vue';
+
+Vue.use(VueRouter);
+
+export const router = new VueRouter({
+	mode: 'history',
+	routes: [
+		{ path: '/', name: 'index', component: MkIndex },
+		{ path: '/@:user', name: 'user', component: () => import('./pages/user/index.vue').then(m => m.default), children: [
+			{ path: 'following', name: 'userFollowing', component: () => import('./pages/user/follow-list.vue').then(m => m.default), props: { type: 'following' } },
+			{ path: 'followers', name: 'userFollowers', component: () => import('./pages/user/follow-list.vue').then(m => m.default), props: { type: 'followers' } },
+		]},
+		{ path: '/@:user/pages/:page', component: () => import('./pages/page.vue').then(m => m.default), props: route => ({ pageName: route.params.page, username: route.params.user }) },
+		{ path: '/@:user/pages/:pageName/view-source', component: () => import('./pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
+		{ path: '/announcements', component: () => import('./pages/announcements.vue').then(m => m.default) },
+		{ path: '/about', component: () => import('./pages/about.vue').then(m => m.default) },
+		{ path: '/featured', component: () => import('./pages/featured.vue').then(m => m.default) },
+		{ path: '/explore', component: () => import('./pages/explore.vue').then(m => m.default) },
+		{ path: '/explore/tags/:tag', props: true, component: () => import('./pages/explore.vue').then(m => m.default) },
+		{ path: '/search', component: () => import('./pages/search.vue').then(m => m.default) },
+		{ path: '/my/favorites', component: () => import('./pages/favorites.vue').then(m => m.default) },
+		{ path: '/my/messages', component: () => import('./pages/messages.vue').then(m => m.default) },
+		{ path: '/my/mentions', component: () => import('./pages/mentions.vue').then(m => m.default) },
+		{ path: '/my/messaging', name: 'messaging', component: () => import('./pages/messaging.vue').then(m => m.default) },
+		{ path: '/my/messaging/:user', component: () => import('./pages/messaging-room.vue').then(m => m.default) },
+		{ path: '/my/drive', name: 'drive', component: () => import('./pages/drive.vue').then(m => m.default) },
+		{ path: '/my/drive/folder/:folder', component: () => import('./pages/drive.vue').then(m => m.default) },
+		{ path: '/my/pages', name: 'pages', component: () => import('./pages/pages.vue').then(m => m.default) },
+		{ path: '/my/pages/new', component: () => import('./pages/page-editor/page-editor.vue').then(m => m.default) },
+		{ path: '/my/pages/edit/:pageId', component: () => import('./pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initPageId: route.params.pageId }) },
+		{ path: '/my/settings', component: () => import('./pages/settings/index.vue').then(m => m.default) },
+		{ path: '/my/follow-requests', component: () => import('./pages/follow-requests.vue').then(m => m.default) },
+		{ path: '/my/lists', component: () => import('./pages/my-lists/index.vue').then(m => m.default) },
+		{ path: '/my/lists/:list', component: () => import('./pages/my-lists/list.vue').then(m => m.default) },
+		{ path: '/my/antennas', component: () => import('./pages/my-antennas/index.vue').then(m => m.default) },
+		{ path: '/instance', component: () => import('./pages/instance/index.vue').then(m => m.default) },
+		{ path: '/instance/emojis', component: () => import('./pages/instance/emojis.vue').then(m => m.default) },
+		{ path: '/instance/users', component: () => import('./pages/instance/users.vue').then(m => m.default) },
+		{ path: '/instance/files', component: () => import('./pages/instance/files.vue').then(m => m.default) },
+		{ path: '/instance/monitor', component: () => import('./pages/instance/monitor.vue').then(m => m.default) },
+		{ path: '/instance/queue', component: () => import('./pages/instance/queue.vue').then(m => m.default) },
+		{ path: '/instance/stats', component: () => import('./pages/instance/stats.vue').then(m => m.default) },
+		{ path: '/instance/federation', component: () => import('./pages/instance/federation.vue').then(m => m.default) },
+		{ path: '/instance/announcements', component: () => import('./pages/instance/announcements.vue').then(m => m.default) },
+		{ path: '/notes/:note', name: 'note', component: () => import('./pages/note.vue').then(m => m.default) },
+		{ path: '/tags/:tag', component: () => import('./pages/tag.vue').then(m => m.default) },
+		{ path: '/auth/:token', component: () => import('./pages/auth.vue').then(m => m.default) },
+		{ path: '/authorize-follow', component: () => import('./pages/follow.vue').then(m => m.default) },
+		/*{ path: '*', component: MkNotFound }*/
+	]
+});
diff --git a/src/client/app/common/scripts/2fa.ts b/src/client/scripts/2fa.ts
similarity index 64%
rename from src/client/app/common/scripts/2fa.ts
rename to src/client/scripts/2fa.ts
index f638cce156e2bcf7f5b01e13b09abfc8936588f5..e431361aac5005b3b44f0f43180f6dff0c6cc490 100644
--- a/src/client/app/common/scripts/2fa.ts
+++ b/src/client/scripts/2fa.ts
@@ -1,5 +1,5 @@
 export function hexifyAB(buffer) {
 	return Array.from(new Uint8Array(buffer))
-		.map(item => item.toString(16).padStart(2, 0))
+		.map(item => item.toString(16).padStart(2, '0'))
 		.join('');
 }
diff --git a/src/misc/aiscript/evaluator.ts b/src/client/scripts/aiscript/evaluator.ts
similarity index 99%
rename from src/misc/aiscript/evaluator.ts
rename to src/client/scripts/aiscript/evaluator.ts
index c53cef75b607935b7dc137f46b71cf5040601fd5..cc1adf4499fc0dd0e49ac0e8c37c3da1588dd86f 100644
--- a/src/misc/aiscript/evaluator.ts
+++ b/src/client/scripts/aiscript/evaluator.ts
@@ -1,6 +1,7 @@
 import autobind from 'autobind-decorator';
 import * as seedrandom from 'seedrandom';
 import { Variable, PageVar, envVarsDef, funcDefs, Block, isFnBlock } from '.';
+import { version } from '../../config';
 
 type Fn = {
 	slots: string[];
@@ -16,7 +17,7 @@ export class ASEvaluator {
 	private envVars: Record<keyof typeof envVarsDef, any>;
 
 	private opts: {
-		randomSeed: string; user?: any; visitor?: any; page?: any; url?: string; version: string;
+		randomSeed: string; user?: any; visitor?: any; page?: any; url?: string;
 	};
 
 	constructor(variables: Variable[], pageVars: PageVar[], opts: ASEvaluator['opts']) {
@@ -28,7 +29,7 @@ export class ASEvaluator {
 
 		this.envVars = {
 			AI: 'kawaii',
-			VERSION: opts.version,
+			VERSION: version,
 			URL: opts.page ? `${opts.url}/@${opts.page.user.username}/pages/${opts.page.name}` : '',
 			LOGIN: opts.visitor != null,
 			NAME: opts.visitor ? opts.visitor.name || opts.visitor.username : '',
diff --git a/src/misc/aiscript/index.ts b/src/client/scripts/aiscript/index.ts
similarity index 100%
rename from src/misc/aiscript/index.ts
rename to src/client/scripts/aiscript/index.ts
diff --git a/src/misc/aiscript/type-checker.ts b/src/client/scripts/aiscript/type-checker.ts
similarity index 100%
rename from src/misc/aiscript/type-checker.ts
rename to src/client/scripts/aiscript/type-checker.ts
diff --git a/src/client/app/common/scripts/collect-page-vars.ts b/src/client/scripts/collect-page-vars.ts
similarity index 100%
rename from src/client/app/common/scripts/collect-page-vars.ts
rename to src/client/scripts/collect-page-vars.ts
diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/scripts/compose-notification.ts
similarity index 60%
rename from src/client/app/common/scripts/compose-notification.ts
rename to src/client/scripts/compose-notification.ts
index ec854f2f4d7463ae49b8cf98b8473cc5d44cebcf..bf3255250676b94415f14ccf4b14dd45dbc63a29 100644
--- a/src/client/app/common/scripts/compose-notification.ts
+++ b/src/client/scripts/compose-notification.ts
@@ -1,6 +1,5 @@
-import getNoteSummary from '../../../../misc/get-note-summary';
-import getReactionEmoji from '../../../../misc/get-reaction-emoji';
-import getUserName from '../../../../misc/get-user-name';
+import getNoteSummary from '../../misc/get-note-summary';
+import getUserName from '../../misc/get-user-name';
 
 type Notification = {
 	title: string;
@@ -20,20 +19,6 @@ export default function(type, data): Notification {
 				icon: data.url
 			};
 
-		case 'unreadMessagingMessage':
-			return {
-				title: `New message from ${getUserName(data.user)}`,
-				body: data.text, // TODO: getMessagingMessageSummary(data),
-				icon: data.user.avatarUrl
-			};
-
-		case 'reversiInvited':
-			return {
-				title: 'Play reversi with me',
-				body: `You got reversi invitation from ${getUserName(data.parent)}`,
-				icon: data.parent.avatarUrl
-			};
-
 		case 'notification':
 			switch (data.type) {
 				case 'mention':
@@ -59,7 +44,7 @@ export default function(type, data): Notification {
 
 				case 'reaction':
 					return {
-						title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`,
+						title: `${getUserName(data.user)}: ${data.reaction}:`,
 						body: getNoteSummary(data.note),
 						icon: data.user.avatarUrl
 					};
diff --git a/src/client/app/common/scripts/contains.ts b/src/client/scripts/contains.ts
similarity index 55%
rename from src/client/app/common/scripts/contains.ts
rename to src/client/scripts/contains.ts
index a5071b3f250a093e6d6493a20a1ba3b1a5d066ef..770bda63bb051ffb0c843d8873bf5818a6fb67f1 100644
--- a/src/client/app/common/scripts/contains.ts
+++ b/src/client/scripts/contains.ts
@@ -1,4 +1,5 @@
-export default (parent, child) => {
+export default (parent, child, checkSame = true) => {
+	if (checkSame && parent === child) return true;
 	let node = child.parentNode;
 	while (node) {
 		if (node == parent) return true;
diff --git a/src/client/app/common/scripts/copy-to-clipboard.ts b/src/client/scripts/copy-to-clipboard.ts
similarity index 100%
rename from src/client/app/common/scripts/copy-to-clipboard.ts
rename to src/client/scripts/copy-to-clipboard.ts
diff --git a/src/client/app/common/scripts/gen-search-query.ts b/src/client/scripts/gen-search-query.ts
similarity index 86%
rename from src/client/app/common/scripts/gen-search-query.ts
rename to src/client/scripts/gen-search-query.ts
index fc26cb7f787f22bd194515b9bae7f6b0662cf101..2520da75df7fc501955eb24b2e6ea74f92b54dc0 100644
--- a/src/client/app/common/scripts/gen-search-query.ts
+++ b/src/client/scripts/gen-search-query.ts
@@ -1,5 +1,5 @@
-import parseAcct from '../../../../misc/acct/parse';
-import { host as localHost } from '../../config';
+import parseAcct from '../../misc/acct/parse';
+import { host as localHost } from '../config';
 
 export async function genSearchQuery(v: any, q: string) {
 	let host: string;
diff --git a/src/client/scripts/get-instance-name.ts b/src/client/scripts/get-instance-name.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b12a3a4c67efee7de59736d1ede940785f81fbfd
--- /dev/null
+++ b/src/client/scripts/get-instance-name.ts
@@ -0,0 +1,8 @@
+export function getInstanceName() {
+	const siteName = document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement;
+	if (siteName && siteName.content) {
+		return siteName.content;
+	}
+
+	return 'Misskey';
+}
diff --git a/src/client/app/common/scripts/get-md5.ts b/src/client/scripts/get-md5.ts
similarity index 100%
rename from src/client/app/common/scripts/get-md5.ts
rename to src/client/scripts/get-md5.ts
diff --git a/src/client/app/common/scripts/get-static-image-url.ts b/src/client/scripts/get-static-image-url.ts
similarity index 75%
rename from src/client/app/common/scripts/get-static-image-url.ts
rename to src/client/scripts/get-static-image-url.ts
index 7460ca38f2f8066b1e7a469f18a4c63b5257c0dd..eff76af25637a6e1f6bf049d731dd1b5d3b02201 100644
--- a/src/client/app/common/scripts/get-static-image-url.ts
+++ b/src/client/scripts/get-static-image-url.ts
@@ -1,5 +1,5 @@
-import { url as instanceUrl } from '../../config';
-import * as url from '../../../../prelude/url';
+import { url as instanceUrl } from '../config';
+import * as url from '../../prelude/url';
 
 export function getStaticImageUrl(baseUrl: string): string {
 	const u = new URL(baseUrl);
diff --git a/src/client/app/common/hotkey.ts b/src/client/scripts/hotkey.ts
similarity index 98%
rename from src/client/app/common/hotkey.ts
rename to src/client/scripts/hotkey.ts
index a53d3f479e32ff0ae63e125279fd47bd5a175517..ec627ab15b68299d91e0500be035f68835a8d542 100644
--- a/src/client/app/common/hotkey.ts
+++ b/src/client/scripts/hotkey.ts
@@ -1,5 +1,5 @@
 import keyCode from './keycode';
-import { concat } from '../../../prelude/array';
+import { concat } from '../../prelude/array';
 
 type pattern = {
 	which: string[];
diff --git a/src/client/app/common/keycode.ts b/src/client/scripts/keycode.ts
similarity index 100%
rename from src/client/app/common/keycode.ts
rename to src/client/scripts/keycode.ts
diff --git a/src/client/app/common/scripts/loading.ts b/src/client/scripts/loading.ts
similarity index 100%
rename from src/client/app/common/scripts/loading.ts
rename to src/client/scripts/loading.ts
diff --git a/src/client/app/common/scripts/paging.ts b/src/client/scripts/paging.ts
similarity index 52%
rename from src/client/app/common/scripts/paging.ts
rename to src/client/scripts/paging.ts
index b4f2ec1ae1d86ca9f4241ca38c8f81c177b6bf78..b24d705f1589cb51d351d09f4a6eba5bd4150520 100644
--- a/src/client/app/common/scripts/paging.ts
+++ b/src/client/scripts/paging.ts
@@ -4,7 +4,6 @@ export default (opts) => ({
 	data() {
 		return {
 			items: [],
-			queue: [],
 			offset: 0,
 			fetching: true,
 			moreFetching: false,
@@ -20,14 +19,10 @@ export default (opts) => ({
 
 		error(): boolean {
 			return !this.fetching && !this.inited;
-		}
+		},
 	},
 
 	watch: {
-		queue(x) {
-			if (opts.onQueueChanged) opts.onQueueChanged(this, x);
-		},
-
 		pagination() {
 			this.init();
 		}
@@ -38,51 +33,31 @@ export default (opts) => ({
 		this.init();
 	},
 
-	mounted() {
-		if (opts.captureWindowScroll) {
-			this.isScrollTop = () => {
-				return window.scrollY <= 8;
-			};
-
-			window.addEventListener('scroll', this.onScroll, { passive: true });
-		} else if (opts.isContainer) {
-			this.isScrollTop = () => {
-				return this.$el.scrollTop <= 8;
-			};
-
-			this.$el.addEventListener('scroll', this.onScroll, { passive: true });
-		}
-	},
-
-	beforeDestroy() {
-		if (opts.captureWindowScroll) {
-			window.removeEventListener('scroll', this.onScroll);
-		} else if (opts.isContainer) {
-			this.$el.removeEventListener('scroll', this.onScroll);
-		}
-	},
-
 	methods: {
+		isScrollTop() {
+			return window.scrollY <= 8;
+		},
+
 		updateItem(i, item) {
 			Vue.set((this as any).items, i, item);
 		},
 
 		reload() {
-			this.queue = [];
 			this.items = [];
 			this.init();
 		},
 
 		async init() {
 			this.fetching = true;
-			if (opts.beforeInit) opts.beforeInit(this);
+			if (opts.before) opts.before(this);
 			let params = typeof this.pagination.params === 'function' ? this.pagination.params(true) : this.pagination.params;
 			if (params && params.then) params = await params;
-			await this.$root.api(this.pagination.endpoint, {
-				limit: (this.pagination.limit || 10) + 1,
+			const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
+			await this.$root.api(endpoint, {
+				limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
 				...params
 			}).then(x => {
-				if (x.length == (this.pagination.limit || 10) + 1) {
+				if (!this.pagination.noPaging && (x.length === (this.pagination.limit || 10) + 1)) {
 					x.pop();
 					this.items = x;
 					this.more = true;
@@ -93,10 +68,10 @@ export default (opts) => ({
 				this.offset = x.length;
 				this.inited = true;
 				this.fetching = false;
-				if (opts.onInited) opts.onInited(this);
+				if (opts.after) opts.after(this, null);
 			}, e => {
 				this.fetching = false;
-				if (opts.onInited) opts.onInited(this);
+				if (opts.after) opts.after(this, e);
 			});
 		},
 
@@ -105,16 +80,17 @@ export default (opts) => ({
 			this.moreFetching = true;
 			let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params;
 			if (params && params.then) params = await params;
-			await this.$root.api(this.pagination.endpoint, {
+			const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
+			await this.$root.api(endpoint, {
 				limit: (this.pagination.limit || 10) + 1,
-				...(this.pagination.endpoint === 'notes/search' ? {
+				...(this.pagination.offsetMode ? {
 					offset: this.offset,
 				} : {
 					untilId: this.items[this.items.length - 1].id,
 				}),
 				...params
 			}).then(x => {
-				if (x.length == (this.pagination.limit || 10) + 1) {
+				if (x.length === (this.pagination.limit || 10) + 1) {
 					x.pop();
 					this.items = this.items.concat(x);
 					this.more = true;
@@ -135,17 +111,15 @@ export default (opts) => ({
 				if (cancel) return;
 			}
 
-			if (this.isScrollTop == null || this.isScrollTop()) {
-				// Prepend the item
-				this.items.unshift(item);
+			// Prepend the item
+			this.items.unshift(item);
 
+			if (this.isScrollTop()) {
 				// オーバーフローしたら古い投稿は捨てる
 				if (this.items.length >= opts.displayLimit) {
 					this.items = this.items.slice(0, opts.displayLimit);
 					this.more = true;
 				}
-			} else {
-				this.queue.push(item);
 			}
 		},
 
@@ -153,37 +127,8 @@ export default (opts) => ({
 			this.items.push(item);
 		},
 
-		releaseQueue() {
-			for (const n of this.queue) {
-				this.prepend(n, true);
-			}
-			this.queue = [];
-		},
-
-		onScroll() {
-			if (this.isScrollTop()) {
-				this.onTop();
-			}
-
-			if (this.$store.state.settings.fetchOnScroll) {
-				// 親要素が display none だったら弾く
-				// https://github.com/syuilo/misskey/issues/1569
-				// http://d.hatena.ne.jp/favril/20091105/1257403319
-				if (this.$el.offsetHeight == 0) return;
-
-				const bottomPosition = opts.isContainer ? this.$el.scrollHeight : document.body.offsetHeight;
-
-				const currentBottomPosition = opts.isContainer ? this.$el.scrollTop + this.$el.clientHeight : window.scrollY + window.innerHeight;
-				if (currentBottomPosition > (bottomPosition - 8)) this.onBottom();
-			}
-		},
-
-		onTop() {
-			this.releaseQueue();
+		remove(find) {
+			this.items = this.items.filter(x => !find(x));
 		},
-
-		onBottom() {
-			this.fetchMore();
-		}
 	}
 });
diff --git a/src/client/app/common/scripts/please-login.ts b/src/client/scripts/please-login.ts
similarity index 100%
rename from src/client/app/common/scripts/please-login.ts
rename to src/client/scripts/please-login.ts
diff --git a/src/client/app/common/scripts/search.ts b/src/client/scripts/search.ts
similarity index 95%
rename from src/client/app/common/scripts/search.ts
rename to src/client/scripts/search.ts
index 2897ed63181ccdd66010a29a3a51dd6b64acaa41..02dd39b035cd1ec696ee50ed594ad7add53e677a 100644
--- a/src/client/app/common/scripts/search.ts
+++ b/src/client/scripts/search.ts
@@ -28,7 +28,7 @@ export async function search(v: any, q: string) {
 		v.$root.$emit('warp', date);
 		v.$root.dialog({
 			icon: faHistory,
-			splash: true,
+			iconOnly: true, autoClose: true
 		});
 		return;
 	}
@@ -36,7 +36,7 @@ export async function search(v: any, q: string) {
 	if (q.startsWith('https://')) {
 		const dialog = v.$root.dialog({
 			type: 'waiting',
-			text: v.$t('@.fetching-as-ap-object'),
+			text: v.$t('fetchingAsApObject') + '...',
 			showOkButton: false,
 			showCancelButton: false,
 			cancelableByBgClick: false
diff --git a/src/client/scripts/select-drive-file.ts b/src/client/scripts/select-drive-file.ts
new file mode 100644
index 0000000000000000000000000000000000000000..004ddf2fd953fd0826217c3bc8707516755d3ed5
--- /dev/null
+++ b/src/client/scripts/select-drive-file.ts
@@ -0,0 +1,12 @@
+import DriveWindow from '../components/drive-window.vue';
+
+export function selectDriveFile($root: any, multiple) {
+	return new Promise((res, rej) => {
+		const w = $root.new(DriveWindow, {
+			multiple
+		});
+		w.$once('selected', files => {
+			res(multiple ? files : files[0]);
+		});
+	});
+}
diff --git a/src/client/app/common/scripts/stream.ts b/src/client/scripts/stream.ts
similarity index 98%
rename from src/client/app/common/scripts/stream.ts
rename to src/client/scripts/stream.ts
index a1b4223b55fbaf69e966040841571a4484cbdf37..7f0e1280b6b7eebca240de6be8d457b9d7985053 100644
--- a/src/client/app/common/scripts/stream.ts
+++ b/src/client/scripts/stream.ts
@@ -1,8 +1,8 @@
 import autobind from 'autobind-decorator';
 import { EventEmitter } from 'eventemitter3';
 import ReconnectingWebsocket from 'reconnecting-websocket';
-import { wsUrl } from '../../config';
-import MiOS from '../../mios';
+import { wsUrl } from '../config';
+import MiOS from '../mios';
 
 /**
  * Misskey stream connection
diff --git a/src/client/store.ts b/src/client/store.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c9f61b811275dd31ad075977433af705a3d51482
--- /dev/null
+++ b/src/client/store.ts
@@ -0,0 +1,193 @@
+import Vuex from 'vuex';
+import createPersistedState from 'vuex-persistedstate';
+import * as nestedProperty from 'nested-property';
+
+import MiOS from './mios';
+
+const defaultSettings = {
+	keepCw: false,
+	showFullAcct: false,
+	rememberNoteVisibility: false,
+	defaultNoteVisibility: 'public',
+	uploadFolder: null,
+	pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
+	wallpaper: null,
+	memo: null,
+	reactions: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
+	widgets: []
+};
+
+const defaultDeviceSettings = {
+	lang: null,
+	loadRawImages: false,
+	alwaysShowNsfw: false,
+	useOsDefaultEmojis: false,
+	accounts: [],
+	recentEmojis: [],
+	themes: [],
+	theme: 'light',
+};
+
+export default (os: MiOS) => new Vuex.Store({
+	plugins: [createPersistedState({
+		paths: ['i', 'device', 'settings']
+	})],
+
+	state: {
+		i: null,
+	},
+
+	getters: {
+		isSignedIn: state => state.i != null,
+	},
+
+	mutations: {
+		updateI(state, x) {
+			state.i = x;
+		},
+
+		updateIKeyValue(state, x) {
+			state.i[x.key] = x.value;
+		},
+	},
+
+	actions: {
+		login(ctx, i) {
+			ctx.commit('updateI', i);
+			ctx.dispatch('settings/merge', i.clientData);
+			ctx.dispatch('addAcount', { id: i.id, i: localStorage.getItem('i') });
+		},
+
+		addAcount(ctx, info) {
+			if (!ctx.state.device.accounts.some(x => x.id === info.id)) {
+				ctx.commit('device/set', {
+					key: 'accounts',
+					value: ctx.state.device.accounts.concat([{ id: info.id, token: info.i }])
+				});
+			}
+		},
+
+		logout(ctx) {
+			ctx.commit('updateI', null);
+			localStorage.removeItem('i');
+		},
+
+		switchAccount(ctx, i) {
+			ctx.commit('updateI', i);
+			ctx.commit('settings/init', i.clientData);
+			localStorage.setItem('i', i.token);
+		},
+
+		mergeMe(ctx, me) {
+			for (const [key, value] of Object.entries(me)) {
+				ctx.commit('updateIKeyValue', { key, value });
+			}
+
+			if (me.clientData) {
+				ctx.dispatch('settings/merge', me.clientData);
+			}
+		},
+	},
+
+	modules: {
+		device: {
+			namespaced: true,
+
+			state: defaultDeviceSettings,
+
+			mutations: {
+				set(state, x: { key: string; value: any }) {
+					state[x.key] = x.value;
+				},
+
+				setTl(state, x) {
+					state.tl = {
+						src: x.src,
+						arg: x.arg
+					};
+				},
+
+				setVisibility(state, visibility) {
+					state.visibility = visibility;
+				},
+			}
+		},
+
+		settings: {
+			namespaced: true,
+
+			state: defaultSettings,
+
+			mutations: {
+				set(state, x: { key: string; value: any }) {
+					nestedProperty.set(state, x.key, x.value);
+				},
+
+				init(state, x) {
+					for (const [key, value] of Object.entries(defaultSettings)) {
+						if (x[key]) {
+							state[key] = x[key];
+						} else {
+							state[key] = value;
+						}
+					}
+				},
+			},
+
+			actions: {
+				merge(ctx, settings) {
+					if (settings == null) return;
+					for (const [key, value] of Object.entries(settings)) {
+						ctx.commit('set', { key, value });
+					}
+				},
+
+				set(ctx, x) {
+					ctx.commit('set', x);
+
+					if (ctx.rootGetters.isSignedIn) {
+						os.api('i/update-client-setting', {
+							name: x.key,
+							value: x.value
+						});
+					}
+				},
+
+				setWidgets(ctx, widgets) {
+					ctx.state.widgets = widgets;
+					ctx.dispatch('updateWidgets');
+				},
+
+				addWidget(ctx, widget) {
+					ctx.state.widgets.unshift(widget);
+					ctx.dispatch('updateWidgets');
+				},
+
+				removeWidget(ctx, widget) {
+					ctx.state.widgets = ctx.state.widgets.filter(w => w.id != widget.id);
+					ctx.dispatch('updateWidgets');
+				},
+
+				updateWidget(ctx, x) {
+					const w = ctx.state.widgets.find(w => w.id == x.id);
+					if (w) {
+						w.data = x.data;
+						ctx.dispatch('updateWidgets');
+					}
+				},
+
+				updateWidgets(ctx) {
+					const widgets = ctx.state.widgets;
+					ctx.commit('set', {
+						key: 'widgets',
+						value: widgets
+					});
+					os.api('i/update-client-setting', {
+						name: 'widgets',
+						value: widgets
+					});
+				},
+			}
+		}
+	}
+});
diff --git a/src/client/style.scss b/src/client/style.scss
new file mode 100644
index 0000000000000000000000000000000000000000..5f90a7aa144df253a2944c4817ea424a3a206248
--- /dev/null
+++ b/src/client/style.scss
@@ -0,0 +1,341 @@
+@charset "utf-8";
+
+:root {
+	--radius: 8px;
+	--marginFull: 16px;
+	--marginHalf: 8px;
+
+	--margin: var(--marginFull);
+
+	@media (max-width: 500px) {
+		--margin: var(--marginHalf);
+	}
+}
+
+* {
+	tap-highlight-color: transparent;
+	-webkit-tap-highlight-color: transparent;
+}
+
+html {
+	background-color: var(--bg);
+	background-attachment: fixed;
+	background-size: cover;
+	background-position: center;
+	color: var(--fg);
+	overflow: auto;
+	overflow-y: scroll;
+
+	&, * {
+		scrollbar-color: var(--scrollbarHandle) var(--panel);
+
+		&:hover {
+			scrollbar-color: var(--scrollbarHandleHover) var(--panel);
+		}
+
+		&:active {
+			scrollbar-color: var(--accent) var(--panel);
+		}
+
+		&::-webkit-scrollbar {
+			width: 6px;
+			height: 6px;
+		}
+
+		&::-webkit-scrollbar-track {
+			background: var(--panel);
+		}
+
+		&::-webkit-scrollbar-thumb {
+			background: var(--scrollbarHandle);
+
+			&:hover {
+				background: var(--scrollbarHandleHover);
+			}
+
+			&:active {
+				background: var(--accent);
+			}
+		}
+	}
+}
+
+html.changing-theme {
+	&, * {
+		transition: background 1s ease !important;
+	}
+}
+
+body {
+	overflow-wrap: break-word;
+}
+
+#ini {
+	position: fixed;
+	z-index: 1;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	cursor: wait;
+
+	> svg {
+		position: absolute;
+		top: 0;
+		right: 0;
+		bottom: 0;
+		left: 0;
+		margin: auto;
+		width: 64px;
+		height: 64px;
+		animation: ini 0.6s infinite linear;
+	}
+}
+
+html, body {
+	margin: 0;
+	padding: 0;
+	scroll-behavior: smooth;
+	text-size-adjust: 100%;
+	font-family: Roboto, HelveticaNeue, Arial, sans-serif;
+}
+
+a {
+	text-decoration: none;
+	cursor: pointer;
+	color: inherit;
+
+	&:hover {
+		text-decoration: underline;
+	}
+
+	* {
+		cursor: pointer;
+	}
+}
+
+#nprogress {
+	pointer-events: none;
+	position: absolute;
+	z-index: 10000;
+
+	.bar {
+		background: var(--accent);
+		position: fixed;
+		z-index: 10001;
+		top: 0;
+		left: 0;
+		width: 100%;
+		height: 2px;
+	}
+
+	.peg {
+		display: block;
+		position: absolute;
+		right: 0;
+		width: 100px;
+		height: 100%;
+		box-shadow: 0 0 10px var(--accent), 0 0 5px var(--accent);
+		opacity: 1;
+		transform: rotate(3deg) translate(0px, -4px);
+	}
+}
+
+#wait {
+	display: block;
+	position: fixed;
+	z-index: 10000;
+	top: 15px;
+	right: 15px;
+
+	&:before {
+		content: "";
+		display: block;
+		width: 18px;
+		height: 18px;
+		box-sizing: border-box;
+		border: solid 2px transparent;
+		border-top-color: var(--accent);
+		border-left-color: var(--accent);
+		border-radius: 50%;
+		animation: progress-spinner 400ms linear infinite;
+	}
+}
+
+._button {
+	appearance: none;
+	padding: 0;
+	background: none;
+	border: none;
+	cursor: pointer;
+	color: var(--fg);
+	touch-action: manipulation;
+	font-size: 1em;
+
+	&, * {
+		user-select: none;
+		-webkit-user-select: none;
+		-webkit-touch-callout: none;
+	}
+
+	* {
+		pointer-events: none;
+	}
+
+	&:focus {
+		outline: none;
+	}
+
+	&:disabled {
+		opacity: 0.5;
+		cursor: default;
+	}
+}
+
+._buttonPrimary {
+	@extend ._button;
+	color: #fff;
+	background: var(--accent);
+
+	&:not(:disabled):hover {
+		background: var(--jkhztclx);
+	}
+
+	&:not(:disabled):active {
+		background: var(--zbqjwygh);
+	}
+}
+
+._textButton {
+	@extend ._button;
+	color: var(--accent);
+
+	&:not(:disabled):hover {
+		text-decoration: underline;
+	}
+}
+
+._shadow {
+	box-shadow: 0 8px 32px var(--shadow);
+
+	@media (max-width: 700px) {
+		box-shadow: 0 4px 16px var(--shadow);
+	}
+
+	@media (max-width: 500px) {
+		box-shadow: 0 2px 8px var(--shadow);
+	}
+}
+
+._panel {
+	@extend ._shadow;
+	position: relative;
+	background: var(--panel);
+	border-radius: var(--radius);
+}
+
+._section {
+	@extend ._panel;
+
+	& + ._section {
+		margin-top: var(--margin);
+	}
+
+	> ._title {
+		margin: 0;
+		padding: 22px 32px;
+		font-size: 1.1em;
+		border-bottom: solid 1px var(--divider);
+		font-weight: bold;
+
+		@media (max-width: 500px) {
+			padding: 16px;
+			font-size: 1em;
+		}
+	}
+
+	> ._content {
+		padding: 32px;
+
+		@media (max-width: 500px) {
+			padding: 16px;
+		}
+
+		& + ._content {
+			border-top: solid 1px var(--divider);
+		}
+
+		&._list {
+			padding: 16px;
+
+			@media (max-width: 500px) {
+				padding: 8px;
+			}
+
+			._listItem {
+				padding: 8px 16px;
+				border-radius: var(--radius);
+
+				@media (max-width: 500px) {
+					padding: 8px;
+				}
+
+				&:hover {
+					background: var(--listItemHoverBg);
+				}
+
+				> * {
+					pointer-events: none;
+				}
+			}
+		}
+	}
+
+	> ._footer {
+		border-top: solid 1px var(--divider);
+		padding: 24px 32px;
+
+		@media (max-width: 500px) {
+			padding: 16px;
+		}
+	}
+}
+
+.zoom-enter-active, .zoom-leave-active {
+	transition: opacity 0.5s, transform 0.5s !important;
+}
+.zoom-enter, .zoom-leave-to {
+	opacity: 0;
+	transform: scale(0.9);
+}
+
+.zoom-in-top-enter-active,
+.zoom-in-top-leave-active {
+	opacity: 1;
+	transform: scaleY(1);
+	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+	transform-origin: center top;
+}
+.zoom-in-top-enter,
+.zoom-in-top-leave-active {
+	opacity: 0;
+	transform: scaleY(0);
+}
+
+@keyframes progress-spinner {
+	0% {
+		transform: rotate(0deg);
+	}
+	100% {
+		transform: rotate(360deg);
+	}
+}
+
+@keyframes ini {
+	from {
+		transform: rotate(0deg);
+	}
+	to {
+		transform: rotate(360deg);
+	}
+}
diff --git a/src/client/style.styl b/src/client/style.styl
deleted file mode 100644
index 9638ea53051636d927c7c87b868899008b529bac..0000000000000000000000000000000000000000
--- a/src/client/style.styl
+++ /dev/null
@@ -1,43 +0,0 @@
-@charset "utf-8"
-
-/*
-	::selection
-		background var(--primary)
-		color #fff
-*/
-
-*
-	position relative
-	box-sizing border-box
-	background-clip padding-box !important
-	tap-highlight-color transparent
-	-webkit-tap-highlight-color transparent
-
-html, body
-	margin 0
-	padding 0
-	scroll-behavior smooth
-	text-size-adjust 100%
-	font-family Roboto, HelveticaNeue, Arial, sans-serif
-
-html.changing-theme
-	&, *
-		transition background 1s ease !important
-
-a
-	text-decoration none
-	color var(--link)
-	cursor pointer
-
-	&:hover
-		text-decoration underline
-
-	*
-		cursor pointer
-
-@css {
-	a {
-		tap-highlight-color: var(--linkTapHighlight) !important;
-		-webkit-tap-highlight-color: var(--linkTapHighlight) !important;
-	}
-}
diff --git a/src/client/app/sw.js b/src/client/sw.js
similarity index 87%
rename from src/client/app/sw.js
rename to src/client/sw.js
index d080130e3d385c5a576a0f3c385c8a2919c7f239..a84647b6567dd18fb29cd572940bd1a17b5c8ff0 100644
--- a/src/client/app/sw.js
+++ b/src/client/sw.js
@@ -2,7 +2,7 @@
  * Service Worker
  */
 
-import composeNotification from './common/scripts/compose-notification';
+import composeNotification from './scripts/compose-notification';
 
 // eslint-disable-next-line no-undef
 const version = _VERSION_;
@@ -18,10 +18,7 @@ self.addEventListener('install', ev => {
 		caches.open(cacheName)
 			.then(cache => {
 				return cache.addAll([
-					"/",
-					`/assets/desktop.${version}.js`,
-					`/assets/mobile.${version}.js`,
-					"/assets/error.jpg"
+					'/assets/error.jpg'
 				]);
 			})
 			.then(() => self.skipWaiting())
@@ -48,7 +45,7 @@ self.addEventListener('fetch', ev => {
 				return response || fetch(ev.request);
 			})
 			.catch(() => {
-				return caches.match("/");
+				return caches.match('/');
 			})
 	);
 });
diff --git a/src/client/theme.ts b/src/client/theme.ts
new file mode 100644
index 0000000000000000000000000000000000000000..644ab2c989e464b232fb4de8119a935a1d690ee5
--- /dev/null
+++ b/src/client/theme.ts
@@ -0,0 +1,100 @@
+import * as tinycolor from 'tinycolor2';
+
+export type Theme = {
+	id: string;
+	name: string;
+	author: string;
+	desc?: string;
+	base?: 'dark' | 'light';
+	props: { [key: string]: string };
+};
+
+export const lightTheme: Theme = require('./themes/light.json5');
+export const darkTheme: Theme = require('./themes/dark.json5');
+
+export const builtinThemes = [
+	lightTheme,
+	darkTheme,
+	require('./themes/lavender.json5'),
+	require('./themes/halloween.json5'),
+	require('./themes/garden.json5'),
+	require('./themes/mauve.json5'),
+	require('./themes/elegant.json5'),
+	require('./themes/rainy.json5'),
+	require('./themes/urban.json5'),
+	require('./themes/cafe.json5'),
+];
+
+let timeout = null;
+
+export function applyTheme(theme: Theme, persist = true) {
+	if (timeout) clearTimeout(timeout);
+
+	document.documentElement.classList.add('changing-theme');
+
+	timeout = setTimeout(() => {
+		document.documentElement.classList.remove('changing-theme');
+	}, 1000);
+
+	// Deep copy
+	const _theme = JSON.parse(JSON.stringify(theme));
+
+	if (_theme.base) {
+		const base = [lightTheme, darkTheme].find(x => x.id == _theme.base);
+		_theme.props = Object.assign({}, base.props, _theme.props);
+	}
+
+	const props = compile(_theme);
+
+	for (const tag of document.head.children) {
+		if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
+			tag.setAttribute('content', props['accent']);
+			break;
+		}
+	}
+
+	for (const [k, v] of Object.entries(props)) {
+		document.documentElement.style.setProperty(`--${k}`, v.toString());
+	}
+
+	if (persist) {
+		localStorage.setItem('theme', JSON.stringify(props));
+	}
+}
+
+function compile(theme: Theme): { [key: string]: string } {
+	function getColor(code: string): tinycolor.Instance {
+		// ref
+		if (code[0] == '@') {
+			return getColor(theme.props[code.substr(1)]);
+		}
+
+		// func
+		if (code[0] == ':') {
+			const parts = code.split('<');
+			const func = parts.shift().substr(1);
+			const arg = parseFloat(parts.shift());
+			const color = getColor(parts.join('<'));
+
+			switch (func) {
+				case 'darken': return color.darken(arg);
+				case 'lighten': return color.lighten(arg);
+				case 'alpha': return color.setAlpha(arg);
+			}
+		}
+
+		return tinycolor(code);
+	}
+
+	const props = {};
+
+	for (const [k, v] of Object.entries(theme.props)) {
+		props[k] = genValue(getColor(v));
+	}
+
+	return props;
+}
+
+function genValue(c: tinycolor.Instance): string {
+	return c.toRgbString();
+}
diff --git a/src/client/themes/cafe.json5 b/src/client/themes/cafe.json5
index 084f69299ce617e511d73d2da7d7fb5962c3ce96..b86ea3f6ec454f4aedc7b67209a66b2895c88d85 100644
--- a/src/client/themes/cafe.json5
+++ b/src/client/themes/cafe.json5
@@ -6,16 +6,15 @@
 
 	base: 'light',
 
-	vars: {
-		primary: 'rgb(234, 154, 82)',
-		secondary: 'rgb(238, 236, 232)',
-		text: 'rgb(149, 143, 139)',
-	},
-
 	props: {
-		renoteGradient: '#ffe1c7',
-		renoteText: '$primary',
-		quoteBorder: '$primary',
-		mfmMention: '#56907b',
+		accent: 'rgb(234, 154, 82)',
+		bg: '#DDD9D1',
+		fg: 'rgb(149, 143, 139)',
+		panel: '#EEECE8',
+		renote: '@accent',
+		link: '@accent',
+		mention: '@accent',
+		hashtag: '@accent',
+		inputBorder: 'rgba(0, 0, 0, 0.1)',
 	},
 }
diff --git a/src/client/themes/dark.json5 b/src/client/themes/dark.json5
index 0665d59901367d4c682daa99e9f86f12aa288f02..3bb56c8ae364fdf748ec3cddba6cf8202ff509c3 100644
--- a/src/client/themes/dark.json5
+++ b/src/client/themes/dark.json5
@@ -6,237 +6,59 @@
 	desc: 'Default dark theme',
 	kind: 'dark',
 
-	vars: {
-		primary: '#fb4e4e',
-		secondary: '#282C37',
-		text: '#d6dae0',
-	},
-
 	props: {
-		primary: '$primary',
-		primaryForeground: '#fff',
-		secondary: '$secondary',
-		bg: ':darken<8<$secondary',
-		text: '$text',
-		textHighlighted: ':lighten<7<$text',
-
-		scrollbarTrack: ':darken<5<$secondary',
-		scrollbarHandle: ':lighten<5<$secondary',
-		scrollbarHandleHover: ':lighten<10<$secondary',
-
-		link: '$primary',
-		linkTapHighlight: ':alpha<0.7<@link',
-
-		notificationIndicator: '$primary',
-
-		switchActive: '$primary',
-		switchActiveTrack: ':alpha<0.4<@switchActive',
-		radioActive: '$primary',
-
-		face: '$secondary',
-		faceText: '#fff',
-		faceHeader: ':lighten<5<$secondary',
-		faceHeaderText: '$text',
-		faceDivider: 'rgba(0, 0, 0, 0.3)',
-		faceTextButton: '$text',
-		faceTextButtonHover: ':lighten<10<$text',
-		faceTextButtonActive: ':darken<10<$text',
-		faceClearButtonHover: 'rgba(0, 0, 0, 0.1)',
-		faceClearButtonActive: 'rgba(0, 0, 0, 0.2)',
-		popupBg: ':lighten<5<$secondary',
-		popupFg: '$text',
-
-		subNoteBg: 'rgba(0, 0, 0, 0.18)',
-		subNoteText: ':alpha<0.7<$text',
-		renoteGradient: '#314027',
-		renoteText: '#9dbb00',
-		quoteBorder: '#4e945e',
-		noteText: '#fff',
-		noteHeaderName: '#fff',
-		noteHeaderBadgeFg: '#758188',
-		noteHeaderBadgeBg: 'rgba(0, 0, 0, 0.25)',
-		noteHeaderAdminFg: '#f15f71',
-		noteHeaderAdminBg: '#5d282e',
-		noteHeaderAcct: ':alpha<0.65<$text',
-		noteHeaderInfo: ':alpha<0.5<$text',
-
-		noteActions: ':alpha<0.45<$text',
-		noteActionsHover: ':alpha<0.6<$text',
-		noteActionsReplyHover: '#0af',
-		noteActionsRenoteHover: '#8d0',
-		noteActionsReactionHover: '#fa0',
-		noteActionsHighlighted: ':alpha<0.7<$text',
-
-		noteAttachedFile: 'rgba(255, 255, 255, 0.1)',
-
-		modalBackdrop: 'rgba(0, 0, 0, 0.5)',
-
-		dateDividerBg: ':darken<2<$secondary',
-		dateDividerFg: ':alpha<0.7<$text',
-
-		switchTrack: 'rgba(255, 255, 255, 0.15)',
-		radioBorder: 'rgba(255, 255, 255, 0.6)',
-		inputBorder: 'rgba(255, 255, 255, 0.7)',
-		inputLabel: 'rgba(255, 255, 255, 0.7)',
-		inputText: '#fff',
-
-		buttonBg: 'rgba(255, 255, 255, 0.05)',
-		buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
-		buttonActiveBg: 'rgba(255, 255, 255, 0.15)',
-
-		autocompleteItemHoverBg: 'rgba(255, 255, 255, 0.1)',
-		autocompleteItemText: 'rgba(255, 255, 255, 0.8)',
-		autocompleteItemTextSub: 'rgba(255, 255, 255, 0.3)',
-
-		cwButtonBg: '#687390',
-		cwButtonFg: '#393f4f',
-		cwButtonHoverBg: '#707b97',
-
-		reactionPickerButtonHoverBg: 'rgba(255, 255, 255, 0.18)',
-
-		reactionViewerButtonBg: 'rgba(255, 255, 255, 0.1)',
-		reactionViewerButtonHoverBg: 'rgba(255, 255, 255, 0.2)',
-
-		pollEditorInputBg: 'rgba(0, 0, 0, 0.25)',
-
-		pollChoiceText: '#fff',
-		pollChoiceBorder: 'rgba(255, 255, 255, 0.1)',
-
-		urlPreviewBorder: 'rgba(0, 0, 0, 0.4)',
-		urlPreviewBorderHover: 'rgba(255, 255, 255, 0.2)',
-		urlPreviewTitle: '$text',
-		urlPreviewText: ':alpha<0.7<$text',
-		urlPreviewInfo: ':alpha<0.8<$text',
-
-		calendarWeek: '#43d5dc',
-		calendarSaturdayOrSunday: '#ff6679',
-		calendarDay: '$text',
-
-		materBg: 'rgba(0, 0, 0, 0.3)',
-
-		chartCaption: ':alpha<0.6<$text',
-
-		announcementsBg: '#253a50',
-		announcementsTitle: '#539eff',
-		announcementsText: '#fff',
-
-		googleSearchBg: 'rgba(0, 0, 0, 0.2)',
-		googleSearchFg: '#dee4e8',
-		googleSearchBorder: 'rgba(255, 255, 255, 0.2)',
-		googleSearchHoverBorder: 'rgba(255, 255, 255, 0.3)',
-		googleSearchHoverButton: 'rgba(255, 255, 255, 0.1)',
-
-		mfmTitleBg: 'rgba(0, 0, 0, 0.2)',
-		mfmQuote: ':alpha<0.7<$text',
-		mfmQuoteLine: ':alpha<0.6<$text',
-		mfmUrl: '$primary',
-		mfmLink: '@mfmUrl',
-		mfmMention: '$primary',
-		mfmMentionForeground: '@primaryForeground',
-		mfmHashtag: '$primary',
-
-		suspendedInfoBg: '#611d1d',
-		suspendedInfoFg: '#ffb4b4',
-		remoteInfoBg: '#42321c',
-		remoteInfoFg: '#ffbd3e',
-
+		accent: '#86b300',
+		accentDarken: ':darken<10<@accent',
+		accentLighten: ':lighten<10<@accent',
+		focus: ':alpha<0.3<@accent',
+		bg: '#000',
+		fg: '#c7d1d8',
+		panel: '#111213',
+		shadow: 'rgba(0, 0, 0, 0.1)',
+		header: 'rgba(20, 20, 20, 0.75)',
+		navBg: '@panel',
+		navFg: '@fg',
+		navActive: '@accent',
+		navIndicator: '@accent',
+		link: '#44a4c1',
+		hashtag: '#ff9156',
+		mention: '@accent',
+		renote: '#229e82',
+		modalBg: 'rgba(0, 0, 0, 0.5)',
+		divider: 'rgba(255, 255, 255, 0.1)',
+		scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
+		scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
+		dateLabelBg: 'rgba(255, 255, 255, 0.08)',
+		dateLabelFg: '#fff',
 		infoBg: '#253142',
 		infoFg: '#fff',
 		infoWarnBg: '#42321c',
 		infoWarnFg: '#ffbd3e',
-
-		messagingRoomBg: '@bg',
-		messagingRoomInfo: '#fff',
-		messagingRoomDateDividerLine: 'rgba(255, 255, 255, 0.1)',
-		messagingRoomDateDividerText: 'rgba(255, 255, 255, 0.3)',
-		messagingRoomMessageInfo: 'rgba(255, 255, 255, 0.4)',
-		messagingRoomMessageBg: '$secondary',
-		messagingRoomMessageFg: '#fff',
-
-		driveFileIcon: '$text',
-
-		formButtonBorder: 'rgba(255, 255, 255, 0.1)',
-		formButtonHoverBg: ':alpha<0.2<$primary',
-		formButtonHoverBorder: ':alpha<0.5<$primary',
-		formButtonActiveBg: ':alpha<0.12<$primary',
-
-		desktopHeaderBg: ':lighten<5<$secondary',
-		desktopHeaderFg: '$text',
-		desktopHeaderHoverFg: '#fff',
-		desktopHeaderSearchBg: 'rgba(0, 0, 0, 0.1)',
-		desktopHeaderSearchHoverBg: 'rgba(255, 255, 255, 0.04)',
-		desktopHeaderSearchFg: '#fff',
-		desktopNotificationBg: ':alpha<0.9<$secondary',
-		desktopNotificationFg: ':alpha<0.7<$text',
-		desktopNotificationShadow: 'rgba(0, 0, 0, 0.4)',
-		desktopPostFormBg: '@face',
-		desktopPostFormTextareaBg: 'rgba(0, 0, 0, 0.25)',
-		desktopPostFormTextareaFg: '#fff',
-		desktopPostFormTransparentButtonFg: '$primary',
-		desktopPostFormTransparentButtonActiveGradientStart: ':darken<8<$secondary',
-		desktopPostFormTransparentButtonActiveGradientEnd: ':darken<3<$secondary',
-		desktopRenoteFormFooter: ':lighten<5<$secondary',
-		desktopTimelineHeaderShadow: 'rgba(0, 0, 0, 0.15)',
-		desktopTimelineSrc: '@faceTextButton',
-		desktopTimelineSrcHover: '@faceTextButtonHover',
-		desktopWindowTitle: '@faceHeaderText',
-		desktopWindowShadow: 'rgba(0, 0, 0, 0.5)',
-		desktopDriveBg: '@bg',
-		desktopDriveFolderBg: ':alpha<0.2<$primary',
-		desktopDriveFolderHoverBg: ':alpha<0.3<$primary',
-		desktopDriveFolderActiveBg: ':alpha<0.3<:darken<10<$primary',
-		desktopDriveFolderFg: '#fff',
-		desktopSettingsNavItem: ':alpha<0.8<$text',
-		desktopSettingsNavItemHover: ':lighten<10<$text',
-
-		deckAcrylicColumnBg: 'rgba(0, 0, 0, 0.25)',
-		deckColumnBg: ':darken<3<@face',
-
-		mobileHeaderBg: ':lighten<5<$secondary',
-		mobileHeaderFg: '$text',
-		mobileNavBackdrop: 'rgba(0, 0, 0, 0.7)',
-		mobilePostFormDivider: 'rgba(0, 0, 0, 0.2)',
-		mobilePostFormTextareaBg: 'rgba(0, 0, 0, 0.3)',
-		mobilePostFormButton: '$text',
-		mobileDriveNavBg: ':alpha<0.75<$secondary',
-		mobileHomeTlItemHover: 'rgba(255, 255, 255, 0.1)',
-		mobileUserPageName: '#fff',
-		mobileUserPageAcct: '$text',
-		mobileUserPageDescription: '$text',
-		mobileUserPageFollowedBg: 'rgba(0, 0, 0, 0.3)',
-		mobileUserPageFollowedFg: '$text',
-		mobileUserPageStatusHighlight: '#fff',
-		mobileUserPageHeaderShadow: 'rgba(0, 0, 0, 0.3)',
-		mobileAnnouncement: 'rgba(30, 129, 216, 0.2)',
-		mobileAnnouncementFg: '#fff',
-		mobileSignedInAsBg: '#273c34',
-		mobileSignedInAsFg: '#49ab63',
-		mobileSignoutBg: '#652222',
-		mobileSignoutFg: '#ff5f56',
-
-		reversiBannerGradientStart: '#45730e',
-		reversiBannerGradientEnd: '#464300',
-		reversiDescBg: 'rgba(255, 255, 255, 0.1)',
-		reversiListItemShadow: 'rgba(0, 0, 0, 0.7)',
-		reversiMapSelectBorder: 'rgba(255, 255, 255, 0.1)',
-		reversiMapSelectHoverBorder: 'rgba(255, 255, 255, 0.2)',
-		reversiRoomFormShadow: 'rgba(0, 0, 0, 0.7)',
-		reversiRoomFooterBg: ':alpha<0.9<$secondary',
-		reversiGameHeaderLine: ':alpha<0.5<$secondary',
-		reversiGameEmptyCell: ':lighten<2<$secondary',
-		reversiGameEmptyCellMyTurn: ':lighten<5<$secondary',
-		reversiGameEmptyCellCanPut: ':lighten<4<$secondary',
-
-		adminDashboardHeaderFg: ':alpha<0.9<$text',
-		adminDashboardHeaderBorder: 'rgba(0, 0, 0, 0.3)',
-		adminDashboardCardBg: '$secondary',
-		adminDashboardCardFg: '$text',
-		adminDashboardCardDivider: 'rgba(0, 0, 0, 0.3)',
-
-		pageBlockBorder: 'rgba(255, 255, 255, 0.1)',
-		pageBlockBorderHover: 'rgba(255, 255, 255, 0.15)',
-
-		groupUserListOwnerFg: '#f15f71',
-		groupUserListOwnerBg: '#5d282e'
+		cwBg: '#687390',
+		cwFg: '#393f4f',
+		cwHoverBg: '#707b97',
+		toastBg: 'rgba(0, 0, 0, 0.5)',
+		toastFg: '#c7d1d8',
+		buttonBg: 'rgba(255, 255, 255, 0.05)',
+		buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
+		inputBorder: '#959da2',
+		listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
+		driveFolderBg: ':alpha<0.3<@accent',
+		bonzsgfz: ':alpha<0<@bg',
+		pcncwizz: ':darken<2<@panel',
+		vocsgcxy: 'rgba(0, 0, 0, 0.5)',
+		yrnqrguo: 'rgba(255, 255, 255, 0.05)',
+		mkykhqkw: ':lighten<3<@fg',
+		nwjktjjq: 'rgba(255, 255, 255, 0.1)',
+		geavgsxy: 'rgba(255, 255, 255, 0.05)',
+		nhzhphzx: 'rgba(255, 255, 255, 0.15)',
+		tyvedwbe: 'rgba(0, 0, 0, 0.5)',
+		bwqtlupy: 'rgba(255, 255, 255, 0.05)',
+		jkhztclx: ':lighten<5<@accent',
+		zbqjwygh: ':darken<5<@accent',
+		xxubwiul: ':alpha<0.4<@accent',
+		aupeazdm: 'rgba(0, 0, 0, 0.3)',
+		jvhmlskx: 'rgba(255, 255, 255, 0.1)',
+		yakfpmhl: 'rgba(255, 255, 255, 0.15)',
 	},
 }
diff --git a/src/client/themes/elegant.json5 b/src/client/themes/elegant.json5
new file mode 100644
index 0000000000000000000000000000000000000000..52ae4cbfa0ab5f68c373b8e2e6b289033612b8c2
--- /dev/null
+++ b/src/client/themes/elegant.json5
@@ -0,0 +1,18 @@
+{
+	id: 'de465368-9dd1-4bba-b1b9-69f312fcf6e7',
+
+	name: 'Elegant',
+	author: 'syuilo',
+	desc: 'Inspired by meimei\'s theme',
+
+	base: 'dark',
+
+	props: {
+		accent: 'rgb(187, 146, 45)',
+		panel: 'rgb(76, 33, 33)',
+		bg: 'rgb(53, 21, 21)',
+		fg: 'rgb(216, 210, 199)',
+		header: 'rgba(76, 45, 33, 0.75)',
+		renote: '@accent',
+	},
+}
diff --git a/src/client/themes/future.json5 b/src/client/themes/future.json5
deleted file mode 100644
index c89b90fae71ae16b0dddbafa9ddfcef8cf5637f3..0000000000000000000000000000000000000000
--- a/src/client/themes/future.json5
+++ /dev/null
@@ -1,39 +0,0 @@
-{
-	id: 'bb5a8287-a072-4b0a-8ae5-ea2a0d33f4f2',
-
-	name: 'Future',
-	author: 'syuilo',
-	desc: 'Sci-fi flavored',
-
-	base: 'dark',
-
-	vars: {
-		c0: '#0e0e0e',
-		c1: 'rgb(255, 105, 78)',
-		c2: 'rgb(99, 197, 210)',
-		c4: 'rgb(253, 254, 214)',
-		c3: 'rgb(204, 254, 253)',
-		primary: '$c1',
-		secondary: '#191919',
-		text: '$c3',
-	},
-
-	props: {
-		bg: '$c0',
-		noteText: '$c4',
-		noteHeaderAcct: ':alpha<0.65<$c4',
-		noteHeaderInfo: ':alpha<0.5<$c4',
-		subNoteText: ':alpha<0.7<$c4',
-		renoteGradient: '$secondary',
-		renoteText: '$c2',
-		quoteBorder: '$c2',
-		mfmHashtag: '$c1',
-		mfmUrl: '$c2',
-		mfmLink: '$c2',
-		mfmMention: '$c1',
-		mfmMentionForeground: '#fff',
-		notificationIndicator: '$c2',
-		link: '$c2',
-		desktopHeaderBg: '$secondary',
-	},
-}
diff --git a/src/client/themes/garden.json5 b/src/client/themes/garden.json5
new file mode 100644
index 0000000000000000000000000000000000000000..ae12fb3e78f6e39adf04c1b8397ab68bb85c8973
--- /dev/null
+++ b/src/client/themes/garden.json5
@@ -0,0 +1,17 @@
+{
+	id: '45b13782-9143-4dd8-ac0c-4a872321fc63',
+
+	name: 'Garden',
+	author: 'syuilo',
+
+	base: 'light',
+
+	props: {
+		accent: 'rgb(147, 206, 188)',
+		bg: 'rgb(253, 245, 242)',
+		fg: 'rgb(161, 147, 139)',
+		renote: '@accent',
+		hashtag: 'rgb(226, 152, 48)',
+		link: '#aac12c',
+	},
+}
diff --git a/src/client/themes/gray.json5 b/src/client/themes/gray.json5
deleted file mode 100644
index 59494f278af10592ac227658d921c4c01a8e591a..0000000000000000000000000000000000000000
--- a/src/client/themes/gray.json5
+++ /dev/null
@@ -1,21 +0,0 @@
-{
-	id: '56ff14eb-1e6d-4c0c-9e84-71eb156234e5',
-
-	name: 'Gray',
-	author: 'syuilo',
-
-	base: 'light',
-
-	vars: {
-		primary: '#C03233',
-		secondary: 'rgb(213, 213, 213)',
-		text: 'rgb(102, 102, 102)',
-	},
-
-	props: {
-		renoteGradient: '#bdbdbd',
-		renoteText: '$primary',
-		quoteBorder: '$primary',
-		desktopPostFormBg: '#ececec',
-	},
-}
diff --git a/src/client/themes/gruvbox-dark.json5 b/src/client/themes/gruvbox-dark.json5
deleted file mode 100644
index 2d03153190a634495436e0bb2183f37dc189fd0e..0000000000000000000000000000000000000000
--- a/src/client/themes/gruvbox-dark.json5
+++ /dev/null
@@ -1,29 +0,0 @@
-{
-	id: '0c6e70e2-a1ec-4053-9b1a-b6082fe016cb',
-
-	name: 'Gruvbox Dark',
-	author: 'syuilo',
-
-	base: 'dark',
-
-	vars: {
-		primary: 'rgb(215, 153, 33)',
-		secondary: 'rgb(40, 40, 40)',
-		text: 'rgb(235, 219, 178)',
-	},
-
-	props: {
-		renoteGradient: '#58581e',
-		renoteText: 'rgb(169, 174, 36)',
-		quoteBorder: 'rgb(169, 174, 36)',
-		mfmMention: 'rgb(177, 98, 134)',
-		mfmMentionForeground: '#fff',
-		mfmUrl: 'rgb(69, 133, 136)',
-		mfmLink: 'rgb(104, 157, 106)',
-		mfmHashtag: 'rgb(251, 73, 52)',
-		notificationIndicator: 'rgb(184, 187, 38)',
-		switchActive: 'rgb(254, 128, 25)',
-		radioActive: 'rgb(131, 165, 152)',
-		link: 'rgb(104, 157, 106)',
-	},
-}
diff --git a/src/client/themes/halloween.json5 b/src/client/themes/halloween.json5
index 608105903a69a03c68070d1d7d8b402cc8f74f9f..1394c793edcb56398ecc3a50ad91a0dbbe3b53a9 100644
--- a/src/client/themes/halloween.json5
+++ b/src/client/themes/halloween.json5
@@ -7,15 +7,11 @@
 
 	base: 'dark',
 
-	vars: {
-		primary: '#d67036',
-		secondary: '#1f1d30',
-		text: '#b1bee3',
-	},
-
 	props: {
-		renoteGradient: '#5d2d1a',
-		renoteText: '#ff6c00',
-		quoteBorder: '#c3631c',
+		accent: '#d67036',
+		panel: '#1f1d30',
+		bg: '#0f0e17',
+		fg: '#b1bee3',
+		renote: '@accent',
 	},
 }
diff --git a/src/client/themes/japanese-sushi-set.json5 b/src/client/themes/japanese-sushi-set.json5
deleted file mode 100644
index 94edecca52eb15e9d74c90b3742c1d9271ac7c2d..0000000000000000000000000000000000000000
--- a/src/client/themes/japanese-sushi-set.json5
+++ /dev/null
@@ -1,20 +0,0 @@
-{
-	id: '2b0a0654-cdb4-4c9a-8244-736b647d3c2a',
-
-	name: 'Japanese Sushi Set',
-	author: 'Noizenecio',
-
-	base: 'dark',
-
-	vars: {
-		primary: 'rgb(234, 136, 50)',
-		secondary: 'rgb(34, 36, 42)',
-		text: 'rgb(221, 209, 203)',
-	},
-
-	props: {
-		renoteGradient: '#6d3d14',
-		renoteText: '$primary',
-		quoteBorder: '$primary',
-	},
-}
diff --git a/src/client/themes/lavender.json5 b/src/client/themes/lavender.json5
index e3078ad516af1fe23bdbd65ff6c95579152509f0..4eb4a54749c494bc854d1240a9f23af432a46a0d 100644
--- a/src/client/themes/lavender.json5
+++ b/src/client/themes/lavender.json5
@@ -2,19 +2,18 @@
 	id: 'e9c8c01d-9c15-48d0-9b5c-3d00843b5b36',
 
 	name: 'Lavender',
-	author: 'sokuyuku',
+	author: 'syuilo',
 
 	base: 'light',
 
-	vars: {
-		primary: 'rgb(206, 147, 191)',
-		secondary: 'rgb(253, 242, 243)',
-		text: 'rgb(161, 139, 146)',
-	},
-
 	props: {
-		renoteGradient: '#f7e4ec',
-		renoteText: '$primary',
-		quoteBorder: '$primary',
+		accent: 'rgb(206, 147, 191)',
+		bg: 'rgb(253, 242, 243)',
+		fg: 'rgb(161, 139, 146)',
+		renote: '@accent',
+		link: '@accent',
+		mention: '@accent',
+		hashtag: '@accent',
+		dateLabelBg: 'rgb(204, 186, 188)',
 	},
 }
diff --git a/src/client/themes/light.json5 b/src/client/themes/light.json5
index cbe456ca5d667430d2f25f859cecdc35f945da7e..075933f3508e39ec130ef97ee040c34b8149a1ea 100644
--- a/src/client/themes/light.json5
+++ b/src/client/themes/light.json5
@@ -6,237 +6,59 @@
 	desc: 'Default light theme',
 	kind: 'light',
 
-	vars: {
-		primary: '#f18570',
-		secondary: '#fff',
-		text: '#666',
-	},
-
 	props: {
-		primary: '$primary',
-		primaryForeground: '#fff',
-		secondary: '$secondary',
-		bg: ':darken<8<$secondary',
-		text: '$text',
-		textHighlighted: ':darken<7<$text',
-
-		scrollbarTrack: '#fff',
-		scrollbarHandle: '#00000033',
-		scrollbarHandleHover: '#00000066',
-
-		link: '$primary',
-		linkTapHighlight: ':alpha<0.7<@link',
-
-		notificationIndicator: '$primary',
-
-		switchActive: '$primary',
-		switchActiveTrack: ':alpha<0.4<@switchActive',
-		radioActive: '$primary',
-
-		face: '$secondary',
-		faceText: '$text',
-		faceHeader: ':lighten<5<$secondary',
-		faceHeaderText: '$text',
-		faceDivider: 'rgba(0, 0, 0, 0.082)',
-		faceTextButton: ':alpha<0.7<$text',
-		faceTextButtonHover: ':alpha<0.7<:darken<7<$text',
-		faceTextButtonActive: ':alpha<0.7<:darken<10<$text',
-		faceClearButtonHover: 'rgba(0, 0, 0, 0.025)',
-		faceClearButtonActive: 'rgba(0, 0, 0, 0.05)',
-		popupBg: ':lighten<5<$secondary',
-		popupFg: '$text',
-
-		subNoteBg: 'rgba(0, 0, 0, 0.01)',
-		subNoteText: ':alpha<0.7<$text',
-		renoteGradient: '#edfde2',
-		renoteText: '#9dbb00',
-		quoteBorder: '#c0dac6',
-		noteText: '$text',
-		noteHeaderName: ':darken<2<$text',
-		noteHeaderBadgeFg: '#aaa',
-		noteHeaderBadgeBg: 'rgba(0, 0, 0, 0.05)',
-		noteHeaderAdminFg: '#f15f71',
-		noteHeaderAdminBg: '#ffdfdf',
-		noteHeaderAcct: ':alpha<0.7<@noteHeaderName',
-		noteHeaderInfo: ':alpha<0.7<@noteHeaderName',
-
-		noteActions: ':alpha<0.3<$text',
-		noteActionsHover: ':alpha<0.9<$text',
-		noteActionsReplyHover: '#0af',
-		noteActionsRenoteHover: '#8d0',
-		noteActionsReactionHover: '#fa0',
-		noteActionsHighlighted: '#888',
-
-		noteAttachedFile: 'rgba(0, 0, 0, 0.05)',
-
-		modalBackdrop: 'rgba(0, 0, 0, 0.1)',
-
-		dateDividerBg: ':darken<2<$secondary',
-		dateDividerFg: ':alpha<0.7<$text',
-
-		switchTrack: 'rgba(0, 0, 0, 0.25)',
-		radioBorder: 'rgba(0, 0, 0, 0.4)',
-		inputBorder: 'rgba(0, 0, 0, 0.42)',
-		inputLabel: 'rgba(0, 0, 0, 0.54)',
-		inputText: '#000',
-
-		buttonBg: 'rgba(0, 0, 0, 0.05)',
-		buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
-		buttonActiveBg: 'rgba(0, 0, 0, 0.15)',
-
-		autocompleteItemHoverBg: 'rgba(0, 0, 0, 0.1)',
-		autocompleteItemText: 'rgba(0, 0, 0, 0.8)',
-		autocompleteItemTextSub: 'rgba(0, 0, 0, 0.3)',
-
-		cwButtonBg: '#b1b9c1',
-		cwButtonFg: '#fff',
-		cwButtonHoverBg: '#bbc4ce',
-
-		reactionPickerButtonHoverBg: '#eee',
-
-		reactionViewerButtonBg: 'rgba(0, 0, 0, 0.05)',
-		reactionViewerButtonHoverBg: 'rgba(0, 0, 0, 0.1)',
-
-		pollEditorInputBg: '#fff',
-
-		pollChoiceText: '#000',
-		pollChoiceBorder: 'rgba(0, 0, 0, 0.1)',
-
-		urlPreviewBorder: 'rgba(0, 0, 0, 0.1)',
-		urlPreviewBorderHover: 'rgba(0, 0, 0, 0.2)',
-		urlPreviewTitle: '$text',
-		urlPreviewText: ':alpha<0.7<$text',
-		urlPreviewInfo: ':alpha<0.8<$text',
-
-		calendarWeek: '#19a2a9',
-		calendarSaturdayOrSunday: '#ef95a0',
-		calendarDay: '$text',
-
-		materBg: 'rgba(0, 0, 0, 0.1)',
-
-		chartCaption: ':alpha<0.6<$text',
-
-		announcementsBg: '#f3f9ff',
-		announcementsTitle: '#4078c0',
-		announcementsText: '#57616f',
-
-		googleSearchBg: '#fff',
-		googleSearchFg: '#55595c',
-		googleSearchBorder: 'rgba(0, 0, 0, 0.2)',
-		googleSearchHoverBorder: 'rgba(0, 0, 0, 0.3)',
-		googleSearchHoverButton: 'rgba(0, 0, 0, 0.05)',
-
-		mfmTitleBg: 'rgba(0, 0, 0, 0.07)',
-		mfmQuote: ':alpha<0.6<$text',
-		mfmQuoteLine: ':alpha<0.5<$text',
-		mfmUrl: '$primary',
-		mfmLink: '@mfmUrl',
-		mfmMention: '$primary',
-		mfmMentionForeground: '@primaryForeground',
-		mfmHashtag: '$primary',
-
-		suspendedInfoBg: '#ffdbdb',
-		suspendedInfoFg: '#570808',
-		remoteInfoBg: '#fff0db',
-		remoteInfoFg: '#573c08',
-
+		accent: '#86b300',
+		accentDarken: ':darken<10<@accent',
+		accentLighten: ':lighten<10<@accent',
+		focus: ':alpha<0.3<@accent',
+		bg: '#fafafa',
+		fg: '#5c6a73',
+		panel: '#fff',
+		shadow: 'rgba(0, 0, 0, 0.1)',
+		header: 'rgba(255, 255, 255, 0.75)',
+		navBg: '@panel',
+		navFg: '@fg',
+		navActive: '@accent',
+		navIndicator: '@accent',
+		link: '#44a4c1',
+		hashtag: '#ff9156',
+		mention: '@accent',
+		renote: '#229e82',
+		modalBg: 'rgba(0, 0, 0, 0.3)',
+		divider: 'rgba(0, 0, 0, 0.1)',
+		scrollbarHandle: 'rgba(0, 0, 0, 0.2)',
+		scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)',
+		dateLabelBg: 'rgba(0, 0, 0, 0.5)',
+		dateLabelFg: '#fff',
 		infoBg: '#e5f5ff',
 		infoFg: '#72818a',
 		infoWarnBg: '#fff0db',
 		infoWarnFg: '#573c08',
-
-		messagingRoomBg: '#fff',
-		messagingRoomInfo: '#000',
-		messagingRoomDateDividerLine: 'rgba(0, 0, 0, 0.1)',
-		messagingRoomDateDividerText: 'rgba(0, 0, 0, 0.3)',
-		messagingRoomMessageInfo: 'rgba(0, 0, 0, 0.4)',
-		messagingRoomMessageBg: '#eee',
-		messagingRoomMessageFg: '#333',
-
-		driveFileIcon: '$text',
-
-		formButtonBorder: 'rgba(0, 0, 0, 0.1)',
-		formButtonHoverBg: ':alpha<0.12<$primary',
-		formButtonHoverBorder: ':alpha<0.3<$primary',
-		formButtonActiveBg: ':alpha<0.12<$primary',
-
-		desktopHeaderBg: ':lighten<5<$secondary',
-		desktopHeaderFg: '$text',
-		desktopHeaderHoverFg: ':darken<7<$text',
-		desktopHeaderSearchBg: 'rgba(0, 0, 0, 0.05)',
-		desktopHeaderSearchHoverBg: 'rgba(0, 0, 0, 0.08)',
-		desktopHeaderSearchFg: '#000',
-		desktopNotificationBg: ':alpha<0.9<$secondary',
-		desktopNotificationFg: ':alpha<0.7<$text',
-		desktopNotificationShadow: 'rgba(0, 0, 0, 0.2)',
-		desktopPostFormBg: ':lighten<33<$primary',
-		desktopPostFormTextareaBg: '#fff',
-		desktopPostFormTextareaFg: '#333',
-		desktopPostFormTransparentButtonFg: ':alpha<0.5<$primary',
-		desktopPostFormTransparentButtonActiveGradientStart: ':lighten<30<$primary',
-		desktopPostFormTransparentButtonActiveGradientEnd: ':lighten<33<$primary',
-		desktopRenoteFormFooter: ':lighten<33<$primary',
-		desktopTimelineHeaderShadow: 'rgba(0, 0, 0, 0.08)',
-		desktopTimelineSrc: '$text',
-		desktopTimelineSrcHover: ':darken<7<$text',
-		desktopWindowTitle: '$text',
-		desktopWindowShadow: 'rgba(0, 0, 0, 0.2)',
-		desktopDriveBg: '#fff',
-		desktopDriveFolderBg: ':lighten<31<$primary',
-		desktopDriveFolderHoverBg: ':lighten<27<$primary',
-		desktopDriveFolderActiveBg: ':lighten<25<$primary',
-		desktopDriveFolderFg: ':darken<10<$primary',
-		desktopSettingsNavItem: ':alpha<0.8<$text',
-		desktopSettingsNavItemHover: ':darken<10<$text',
-
-		deckAcrylicColumnBg: 'rgba(0, 0, 0, 0.1)',
-		deckColumnBg: ':darken<4<@face',
-
-		mobileHeaderBg: ':lighten<5<$secondary',
-		mobileHeaderFg: '$text',
-		mobileNavBackdrop: 'rgba(0, 0, 0, 0.2)',
-		mobilePostFormDivider: 'rgba(0, 0, 0, 0.1)',
-		mobilePostFormTextareaBg: '#fff',
-		mobilePostFormButton: '$text',
-		mobileDriveNavBg: ':alpha<0.75<$secondary',
-		mobileHomeTlItemHover: 'rgba(0, 0, 0, 0.05)',
-		mobileUserPageName: '#757c82',
-		mobileUserPageAcct: '#969ea5',
-		mobileUserPageDescription: '#757c82',
-		mobileUserPageFollowedBg: '#a7bec7',
-		mobileUserPageFollowedFg: '#fff',
-		mobileUserPageStatusHighlight: '#787e86',
-		mobileUserPageHeaderShadow: 'rgba(0, 0, 0, 0.07)',
-		mobileAnnouncement: 'rgba(155, 196, 232, 0.2)',
-		mobileAnnouncementFg: '#3f4967',
-		mobileSignedInAsBg: '#fcfff5',
-		mobileSignedInAsFg: '#2c662d',
-		mobileSignoutBg: '#fff6f5',
-		mobileSignoutFg: '#cc2727',
-
-		reversiBannerGradientStart: '#8bca3e',
-		reversiBannerGradientEnd: '#d6cf31',
-		reversiDescBg: 'rgba(0, 0, 0, 0.1)',
-		reversiListItemShadow: 'rgba(0, 0, 0, 0.15)',
-		reversiMapSelectBorder: 'rgba(0, 0, 0, 0.1)',
-		reversiMapSelectHoverBorder: 'rgba(0, 0, 0, 0.2)',
-		reversiRoomFormShadow: 'rgba(0, 0, 0, 0.1)',
-		reversiRoomFooterBg: ':alpha<0.9<$secondary',
-		reversiGameHeaderLine: '#c4cdd4',
-		reversiGameEmptyCell: 'rgba(0, 0, 0, 0.06)',
-		reversiGameEmptyCellMyTurn: 'rgba(0, 0, 0, 0.12)',
-		reversiGameEmptyCellCanPut: 'rgba(0, 0, 0, 0.09)',
-
-		adminDashboardHeaderFg: ':alpha<0.9<$text',
-		adminDashboardHeaderBorder: 'rgba(0, 0, 0, 0.1)',
-		adminDashboardCardBg: '$secondary',
-		adminDashboardCardFg: '$text',
-		adminDashboardCardDivider: 'rgba(0, 0, 0, 0.082)',
-
-		pageBlockBorder: 'rgba(0, 0, 0, 0.1)',
-		pageBlockBorderHover: 'rgba(0, 0, 0, 0.15)',
-
-		groupUserListOwnerFg: '#f15f71',
-		groupUserListOwnerBg: '#ffdfdf'
+		cwBg: '#b1b9c1',
+		cwFg: '#fff',
+		cwHoverBg: '#bbc4ce',
+		toastBg: 'rgba(255, 255, 255, 0.5)',
+		toastFg: '#0c0c0c',
+		buttonBg: 'rgba(0, 0, 0, 0.05)',
+		buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
+		inputBorder: '#dae0e4',
+		listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
+		driveFolderBg: ':alpha<0.3<@accent',
+		bonzsgfz: ':alpha<0<@bg',
+		pcncwizz: ':darken<2<@panel',
+		vocsgcxy: 'rgba(255, 255, 255, 0.5)',
+		yrnqrguo: 'rgba(0, 0, 0, 0.05)',
+		mkykhqkw: ':darken<3<@fg',
+		nwjktjjq: 'rgba(0, 0, 0, 0.1)',
+		geavgsxy: 'rgba(0, 0, 0, 0.05)',
+		nhzhphzx: 'rgba(0, 0, 0, 0.25)',
+		tyvedwbe: 'rgba(0, 0, 0, 0.1)',
+		bwqtlupy: 'rgba(0, 0, 0, 0.05)',
+		jkhztclx: ':lighten<5<@accent',
+		zbqjwygh: ':darken<5<@accent',
+		xxubwiul: ':alpha<0.4<@accent',
+		aupeazdm: 'rgba(0, 0, 0, 0.1)',
+		jvhmlskx: 'rgba(0, 0, 0, 0.1)',
+		yakfpmhl: 'rgba(0, 0, 0, 0.15)',
 	},
 }
diff --git a/src/client/themes/mauve.json5 b/src/client/themes/mauve.json5
index b2ec28b44558d66430fdee6ff54806979ec819ad..47304c922f26598c50691deee65559437c801593 100644
--- a/src/client/themes/mauve.json5
+++ b/src/client/themes/mauve.json5
@@ -1,20 +1,18 @@
 {
-	id: '252b2caf-86c2-4c3f-a73f-e1fc1cfa5298',
+	id: '6846bcbe-afbe-487c-bece-77e8cfc4ab8a',
 
 	name: 'Mauve',
-	author: 'とわこ',
+	author: 'syuilo',
 
 	base: 'dark',
 
-	vars: {
-		primary: 'rgb(133, 88, 150)',
-		secondary: 'rgb(54, 43, 59)',
-		text: 'rgb(229, 223, 231)',
-	},
-
 	props: {
-		renoteGradient: '#54415d',
-		renoteText: '$primary',
-		quoteBorder: '$primary',
+		accent: 'rgb(133, 88, 150)',
+		panel: 'rgb(54, 43, 59)',
+		bg: '#201A23',
+		fg: 'rgb(229, 223, 231)',
+		shadow: 'rgba(0, 0, 0, 0.2)',
+		header: 'rgba(87, 70, 97, 0.5)',
+		renote: '@accent',
 	},
 }
diff --git a/src/client/themes/monokai.json5 b/src/client/themes/monokai.json5
deleted file mode 100644
index 1ecd68730e6198c9987b192b5788fff66e541026..0000000000000000000000000000000000000000
--- a/src/client/themes/monokai.json5
+++ /dev/null
@@ -1,29 +0,0 @@
-{
-	id: 'fef11dc4-6b17-436e-b374-73282c44ddc0',
-
-	name: 'Monokai',
-	author: 'syuilo',
-
-	base: 'dark',
-
-	vars: {
-		primary: '#f92672',
-		secondary: '#272822',
-		text: '#f8f8f2',
-	},
-
-	props: {
-		renoteGradient: '#3f500f',
-		renoteText: '#a6e22e',
-		quoteBorder: '#a6e22e',
-		mfmMention: '#ae81ff',
-		mfmMentionForeground: '#fff',
-		mfmUrl: '#66d9ef',
-		mfmLink: '#e6db74',
-		mfmHashtag: '#fd971f',
-		notificationIndicator: '#66d9ef',
-		switchActive: 'rgb(166, 226, 46)',
-		radioActive: '#fd971f',
-		link: '#e6db74',
-	},
-}
diff --git a/src/client/themes/rainy.json5 b/src/client/themes/rainy.json5
index 26ff3a6c86eaf69a99927526c68ca482306b83c8..0ad6338295dd8ddb7da4d53b84f0e591a35028f7 100644
--- a/src/client/themes/rainy.json5
+++ b/src/client/themes/rainy.json5
@@ -1,21 +1,15 @@
 {
-	id: '2058b33e-5127-4e63-ae67-a900f3a11723',
+	id: '2d7d1479-acb8-4e2e-85bb-565a2d8e6966',
 
 	name: 'Rainy',
 	author: 'syuilo',
-	desc: 'It\'s a rainy day.',
 
 	base: 'light',
 
-	vars: {
-		primary: 'rgb(100, 184, 193)',
-		secondary: 'rgb(228, 234, 234)',
-		text: 'rgb(85, 94, 92)',
-	},
-
 	props: {
-		renoteGradient: '#bcd0d0',
-		renoteText: '$primary',
-		quoteBorder: '$primary',
+		accent: 'rgb(147, 199, 206)',
+		bg: 'rgb(220, 229, 232)',
+		fg: 'rgb(139, 153, 161)',
+		renote: '@accent',
 	},
 }
diff --git a/src/client/themes/tweet-deck.json5 b/src/client/themes/tweet-deck.json5
deleted file mode 100644
index aac9e3d009f21dbff054238198d4a95edaebd7f9..0000000000000000000000000000000000000000
--- a/src/client/themes/tweet-deck.json5
+++ /dev/null
@@ -1,44 +0,0 @@
-{
-	name: 'Tweet Deck',
-	id: '06f82fb4-0dad-4d70-8a3f-56cae91e1163',
-	author: 'simirall',
-	desc: 'Tweet like a pro.',
-	base: 'dark',
-	vars: {
-		primary: '#1da1f2',
-		secondary: '#15202b',
-		text: '#fdfdfd',
-	},
-	props: {
-		bg: '#10171e',
-		faceHeader: '$secondary',
-		faceTextButton: '$primary',
-		renoteGradient: '$secondary',
-		renoteText: '#17bf63',
-		quoteBorder: '#38444d',
-		noteHeaderAdminFg: '$primary',
-		noteHeaderAdminBg: '$secondary',
-		noteActionsReplyHover: '$primary',
-		noteActionsRenoteHover: '#17bf63',
-		noteActionsReactionHover: '#e0245e',
-		calendarWeek: '$primary',
-		calendarSaturdayOrSunday: '#e0245e',
-		announcementsBg: '$secondary',
-		announcementsTitle: '$primary',
-		suspendedInfoBg: '$secondary',
-		suspendedInfoFg: '$primary',
-		remoteInfoBg: '$secondary',
-		remoteInfoFg: '$primary',
-		desktopHeaderBg: '#1c2938',
-		desktopHeaderFg: '#a9adae',
-		desktopHeaderHoverFg: '#fff',
-		desktopPostFormTransparentButtonFg: '#a9adae',
-		desktopTimelineSrc: '$primary',
-		desktopTimelineSrcHover: '#fff',
-		deckAcrylicColumnBg: 'rgba(0, 0, 0, 0.0)',
-		reversiBannerGradientStart: '$primary',
-		reversiBannerGradientEnd: '$primary',
-		reversiGameEmptyCellMyTurn: ':lighten<5<$primary',
-		reversiGameEmptyCellCanPut: ':lighten<4<$primary',
-	},
-}
diff --git a/src/client/themes/urban.json5 b/src/client/themes/urban.json5
new file mode 100644
index 0000000000000000000000000000000000000000..342d3b9cabd1a81a68a6eed9370c3944e774117a
--- /dev/null
+++ b/src/client/themes/urban.json5
@@ -0,0 +1,18 @@
+{
+	id: 'b9392635-8c3d-4397-aaf7-796e49781899',
+
+	name: 'Urban',
+	author: 'syuilo',
+
+	base: 'dark',
+
+	props: {
+		accent: 'rgb(212, 104, 48)',
+		panel: 'rgb(38, 44, 53)',
+		bg: 'rgb(26, 29, 33)',
+		fg: 'rgb(199, 209, 216)',
+		shadow: 'rgba(0, 0, 0, 0.2)',
+		header: 'rgba(51, 64, 72, 0.75)',
+		renote: '@accent',
+	},
+}
diff --git a/src/client/themes/vivid.json5 b/src/client/themes/vivid.json5
deleted file mode 100644
index 27bf742f3f30a4c15879b9cf37bd65f40b9642d0..0000000000000000000000000000000000000000
--- a/src/client/themes/vivid.json5
+++ /dev/null
@@ -1,23 +0,0 @@
-{
-	id: '2d066d6e-bd39-4f23-bd48-686d5c1c6ae8',
-
-	name: 'Vivid',
-	author: 'syuilo',
-
-	base: 'light',
-
-	vars: {
-		primary: 'rgb(255, 153, 64)',
-		secondary: 'rgb(255, 255, 255)',
-		text: 'rgb(108, 118, 128)',
-	},
-
-	props: {
-		bg: 'rgb(250, 250, 250)',
-		mfmMention: '#f07171',
-		mfmMentionForeground: '#fff',
-		mfmUrl: '#86b300',
-		mfmLink: '#399ee6',
-		mfmHashtag: '#fa8d3e'
-	},
-}
diff --git a/src/client/app/tsconfig.json b/src/client/tsconfig.json
similarity index 100%
rename from src/client/app/tsconfig.json
rename to src/client/tsconfig.json
diff --git a/src/client/app/v.d.ts b/src/client/v.d.ts
similarity index 100%
rename from src/client/app/v.d.ts
rename to src/client/v.d.ts
diff --git a/src/client/widgets/calendar.vue b/src/client/widgets/calendar.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ae9dbfecefee6100f55efc80ea9134dadd13701f
--- /dev/null
+++ b/src/client/widgets/calendar.vue
@@ -0,0 +1,206 @@
+<template>
+<div class="mkw-calendar" :class="{ _panel: props.design === 0 }">
+	<div class="calendar" :data-is-holiday="isHoliday">
+		<p class="month-and-year">
+			<span class="year">{{ $t('yearX', { year }) }}</span>
+			<span class="month">{{ $t('monthX', { month }) }}</span>
+		</p>
+		<p class="day">{{ $t('dayX', { day }) }}</p>
+		<p class="week-day">{{ weekDay }}</p>
+	</div>
+	<div class="info">
+		<div>
+			<p>{{ $t('today') }}: <b>{{ dayP.toFixed(1) }}%</b></p>
+			<div class="meter">
+				<div class="val" :style="{ width: `${dayP}%` }"></div>
+			</div>
+		</div>
+		<div>
+			<p>{{ $t('thisMonth') }}: <b>{{ monthP.toFixed(1) }}%</b></p>
+			<div class="meter">
+				<div class="val" :style="{ width: `${monthP}%` }"></div>
+			</div>
+		</div>
+		<div>
+			<p>{{ $t('thisYear') }}: <b>{{ yearP.toFixed(1) }}%</b></p>
+			<div class="meter">
+				<div class="val" :style="{ width: `${yearP}%` }"></div>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import define from './define';
+import i18n from '../i18n';
+
+export default define({
+	name: 'calendar',
+	props: () => ({
+		design: 0
+	})
+}).extend({
+	i18n,
+	data() {
+		return {
+			now: new Date(),
+			year: null,
+			month: null,
+			day: null,
+			weekDay: null,
+			yearP: null,
+			dayP: null,
+			monthP: null,
+			isHoliday: null,
+			clock: null
+		};
+	},
+	created() {
+		this.tick();
+		this.clock = setInterval(this.tick, 1000);
+	},
+	beforeDestroy() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		func() {
+			if (this.props.design == 2) {
+				this.props.design = 0;
+			} else {
+				this.props.design++;
+			}
+			this.save();
+		},
+		tick() {
+			const now = new Date();
+			const nd = now.getDate();
+			const nm = now.getMonth();
+			const ny = now.getFullYear();
+
+			this.year = ny;
+			this.month = nm + 1;
+			this.day = nd;
+			this.weekDay = [
+				this.$t('_weekday.sunday'),
+				this.$t('_weekday.monday'),
+				this.$t('_weekday.tuesday'),
+				this.$t('_weekday.wednesday'),
+				this.$t('_weekday.thursday'),
+				this.$t('_weekday.friday'),
+				this.$t('_weekday.saturday')
+			][now.getDay()];
+
+			const dayNumer   = now.getTime() - new Date(ny, nm, nd).getTime();
+			const dayDenom   = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/;
+			const monthNumer = now.getTime() - new Date(ny, nm, 1).getTime();
+			const monthDenom = new Date(ny, nm + 1, 1).getTime() - new Date(ny, nm, 1).getTime();
+			const yearNumer  = now.getTime() - new Date(ny, 0, 1).getTime();
+			const yearDenom  = new Date(ny + 1, 0, 1).getTime() - new Date(ny, 0, 1).getTime();
+
+			this.dayP   = dayNumer   / dayDenom   * 100;
+			this.monthP = monthNumer / monthDenom * 100;
+			this.yearP  = yearNumer  / yearDenom  * 100;
+
+			this.isHoliday = now.getDay() == 0 || now.getDay() == 6;
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mkw-calendar {
+	padding: 16px 0;
+
+	&:after {
+		content: "";
+		display: block;
+		clear: both;
+	}
+
+	> .calendar {
+		float: left;
+		width: 60%;
+		text-align: center;
+
+		&[data-is-holiday] {
+			> .day {
+				color: #ef95a0;
+			}
+		}
+
+		> p {
+			margin: 0;
+			line-height: 18px;
+			font-size: 14px;
+
+			> span {
+				margin: 0 4px;
+			}
+		}
+
+		> .day {
+			margin: 10px 0;
+			line-height: 32px;
+			font-size: 28px;
+		}
+	}
+
+	> .info {
+		display: block;
+		float: left;
+		width: 40%;
+		padding: 0 16px 0 0;
+		box-sizing: border-box;
+
+		> div {
+			margin-bottom: 8px;
+
+			&:last-child {
+				margin-bottom: 4px;
+			}
+
+			> p {
+				margin: 0 0 2px 0;
+				font-size: 12px;
+				line-height: 18px;
+				opacity: 0.8;
+
+				> b {
+					margin-left: 2px;
+				}
+			}
+
+			> .meter {
+				width: 100%;
+				overflow: hidden;
+				background: var(--aupeazdm);
+				border-radius: 8px;
+
+				> .val {
+					height: 4px;
+					transition: width .3s cubic-bezier(0.23, 1, 0.32, 1);
+				}
+			}
+
+			&:nth-child(1) {
+				> .meter > .val {
+					background: #f7796c;
+				}
+			}
+
+			&:nth-child(2) {
+				> .meter > .val {
+					background: #a1de41;
+				}
+			}
+
+			&:nth-child(3) {
+				> .meter > .val {
+					background: #41ddde;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/app/common/define-widget.ts b/src/client/widgets/define.ts
similarity index 75%
rename from src/client/app/common/define-widget.ts
rename to src/client/widgets/define.ts
index ba4deafe3ae0fea26c1abe6b36d2c84ba5d2a158..768446c128196e0ae4164f39280cec27e6a333c4 100644
--- a/src/client/app/common/define-widget.ts
+++ b/src/client/widgets/define.ts
@@ -9,14 +9,6 @@ export default function <T extends object>(data: {
 			widget: {
 				type: Object
 			},
-			column: {
-				type: Object,
-				default: null
-			},
-			platform: {
-				type: String,
-				required: true
-			},
 			isCustomizeMode: {
 				type: Boolean,
 				default: false
@@ -59,11 +51,7 @@ export default function <T extends object>(data: {
 			},
 
 			save() {
-				if (this.platform == 'deck') {
-					this.$store.commit('updateDeckColumn', this.column);
-				} else {
-					this.$store.commit('updateWidget', this.widget);
-				}
+				this.$store.dispatch('settings/updateWidget', this.widget);
 			}
 		}
 	});
diff --git a/src/client/widgets/index.ts b/src/client/widgets/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4743be0763b2b5d9f2fd8b66afb68be3684469fa
--- /dev/null
+++ b/src/client/widgets/index.ts
@@ -0,0 +1,8 @@
+import Vue from 'vue';
+
+Vue.component('mkw-memo', () => import('./memo.vue').then(m => m.default));
+Vue.component('mkw-notifications', () => import('./notifications.vue').then(m => m.default));
+Vue.component('mkw-timeline', () => import('./timeline.vue').then(m => m.default));
+Vue.component('mkw-calendar', () => import('./calendar.vue').then(m => m.default));
+Vue.component('mkw-rss', () => import('./rss.vue').then(m => m.default));
+Vue.component('mkw-trends', () => import('./trends.vue').then(m => m.default));
diff --git a/src/client/widgets/memo.vue b/src/client/widgets/memo.vue
new file mode 100644
index 0000000000000000000000000000000000000000..974c13eb0d38a992669ed39a9838e8c3824338e3
--- /dev/null
+++ b/src/client/widgets/memo.vue
@@ -0,0 +1,120 @@
+<template>
+<div>
+	<mk-container :show-header="!props.compact">
+		<template #header><fa :icon="faStickyNote"/>{{ $t('_widgets.memo') }}</template>
+
+		<div class="otgbylcu">
+			<textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea>
+			<button @click="saveMemo" :disabled="!changed">{{ $t('save') }}</button>
+		</div>
+	</mk-container>
+</div>
+</template>
+
+<script lang="ts">
+import { faStickyNote } from '@fortawesome/free-solid-svg-icons';
+import MkContainer from '../components/ui/container.vue';
+import define from './define';
+import i18n from '../i18n';
+
+export default define({
+	name: 'memo',
+	props: () => ({
+		compact: false
+	})
+}).extend({
+	i18n,
+	
+	components: {
+		MkContainer
+	},
+
+	data() {
+		return {
+			text: null,
+			changed: false,
+			timeoutId: null,
+			faStickyNote
+		};
+	},
+
+	created() {
+		this.text = this.$store.state.settings.memo;
+
+		this.$watch('$store.state.settings.memo', text => {
+			this.text = text;
+		});
+	},
+
+	methods: {
+		func() {
+			this.props.compact = !this.props.compact;
+			this.save();
+		},
+
+		onChange() {
+			this.changed = true;
+			clearTimeout(this.timeoutId);
+			this.timeoutId = setTimeout(this.saveMemo, 1000);
+		},
+
+		saveMemo() {
+			this.$store.dispatch('settings/set', {
+				key: 'memo',
+				value: this.text
+			});
+			this.changed = false;
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.otgbylcu {
+	padding-bottom: 28px + 16px;
+
+	> textarea {
+		display: block;
+		width: 100%;
+		max-width: 100%;
+		min-width: 100%;
+		padding: 16px;
+		color: var(--inputText);
+		background: var(--face);
+		border: none;
+		border-bottom: solid var(--lineWidth) var(--faceDivider);
+		border-radius: 0;
+	}
+
+	> button {
+		display: block;
+		position: absolute;
+		bottom: 8px;
+		right: 8px;
+		margin: 0;
+		padding: 0 10px;
+		height: 28px;
+		color: #fff;
+		background: var(--accent) !important;
+		outline: none;
+		border: none;
+		border-radius: 4px;
+		transition: background 0.1s ease;
+		cursor: pointer;
+
+		&:hover {
+			background: var(--accentLighten10) !important;
+		}
+
+		&:active {
+			background: var(--accentDarken) !important;
+			transition: background 0s ease;
+		}
+
+		&:disabled {
+			opacity: 0.7;
+			cursor: default;
+		}
+	}
+}
+</style>
diff --git a/src/client/widgets/notifications.vue b/src/client/widgets/notifications.vue
new file mode 100644
index 0000000000000000000000000000000000000000..bc9b3a65a0c02820f378655475715417ef61dca2
--- /dev/null
+++ b/src/client/widgets/notifications.vue
@@ -0,0 +1,46 @@
+<template>
+<div class="mkw-notifications">
+	<mk-container :show-header="!props.compact">
+		<template #header><fa :icon="faBell"/>{{ $t('notifications') }}</template>
+
+		<div style="height: 300px; overflow: auto; background: var(--bg);">
+			<x-notifications/>
+		</div>
+	</mk-container>
+</div>
+</template>
+
+<script lang="ts">
+import { faBell } from '@fortawesome/free-solid-svg-icons';
+import MkContainer from '../components/ui/container.vue';
+import XNotifications from '../components/notifications.vue';
+import define from './define';
+import i18n from '../i18n';
+
+export default define({
+	name: 'notifications',
+	props: () => ({
+		compact: false
+	})
+}).extend({
+	i18n,
+	
+	components: {
+		MkContainer,
+		XNotifications,
+	},
+
+	data() {
+		return {
+			faBell
+		};
+	},
+
+	methods: {
+		func() {
+			this.props.compact = !this.props.compact;
+			this.save();
+		},
+	}
+});
+</script>
diff --git a/src/client/widgets/rss.vue b/src/client/widgets/rss.vue
new file mode 100644
index 0000000000000000000000000000000000000000..61c1e23b6e1b32b9506685144470e3b47220c7c6
--- /dev/null
+++ b/src/client/widgets/rss.vue
@@ -0,0 +1,101 @@
+<template>
+<div>
+	<mk-container :show-header="!props.compact">
+		<template #header><fa :icon="faRssSquare"/>RSS</template>
+		<template #func><button class="_button" @click="setting"><fa :icon="faCog"/></button></template>
+
+		<div class="ekmkgxbj">
+			<mk-loading v-if="fetching"/>
+			<div class="feed" v-else>
+				<a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
+			</div>
+		</div>
+	</mk-container>
+</div>
+</template>
+
+<script lang="ts">
+import { faRssSquare, faCog } from '@fortawesome/free-solid-svg-icons';
+import MkContainer from '../components/ui/container.vue';
+import define from './define';
+import i18n from '../i18n';
+
+export default define({
+	name: 'rss',
+	props: () => ({
+		compact: false,
+		url: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews'
+	})
+}).extend({
+	i18n,
+	components: {
+		MkContainer
+	},
+	data() {
+		return {
+			items: [],
+			fetching: true,
+			clock: null,
+			faRssSquare, faCog
+		};
+	},
+	mounted() {
+		this.fetch();
+		this.clock = setInterval(this.fetch, 60000);
+	},
+	beforeDestroy() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		func() {
+			this.props.compact = !this.props.compact;
+			this.save();
+		},
+		fetch() {
+			fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, {
+			}).then(res => {
+				res.json().then(feed => {
+					this.items = feed.items;
+					this.fetching = false;
+				});
+			});
+		},
+		setting() {
+			this.$root.dialog({
+				title: 'URL',
+				input: {
+					type: 'url',
+					default: this.props.url
+				}
+			}).then(({ canceled, result: url }) => {
+				if (canceled) return;
+				this.props.url = url;
+				this.save();
+				this.fetch();
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ekmkgxbj {
+	> .feed {
+		padding: 0;
+		font-size: 0.9em;
+
+		> a {
+			display: block;
+			padding: 8px 16px;
+			color: var(--text);
+			white-space: nowrap;
+			text-overflow: ellipsis;
+			overflow: hidden;
+
+			&:nth-child(even) {
+				background: rgba(#000, 0.05);
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/widgets/timeline.vue b/src/client/widgets/timeline.vue
new file mode 100644
index 0000000000000000000000000000000000000000..5a22a0c1a574184d2767edaf9bfd75551e96ad25
--- /dev/null
+++ b/src/client/widgets/timeline.vue
@@ -0,0 +1,113 @@
+<template>
+<div class="mkw-timeline">
+	<mk-container :show-header="!props.compact">
+		<template #header>
+			<button @click="choose" class="_button">
+				<fa v-if="props.src === 'home'" :icon="faHome"/>
+				<fa v-if="props.src === 'local'" :icon="faComments"/>
+				<fa v-if="props.src === 'social'" :icon="faShareAlt"/>
+				<fa v-if="props.src === 'global'" :icon="faGlobe"/>
+				<fa v-if="props.src === 'list'" :icon="faListUl"/>
+				<fa v-if="props.src === 'antenna'" :icon="faSatellite"/>
+				<span style="margin-left: 8px;">{{ props.src === 'list' ? props.list.name : props.src === 'antenna' ? props.antenna.name : $t('_timelines.' + props.src) }}</span>
+				<fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/>
+			</button>
+		</template>
+
+		<div style="height: 300px; padding: 8px; overflow: auto; background: var(--bg);">
+			<x-timeline :key="props.src === 'list' ? `list:${props.list.id}` : props.src === 'antenna' ? `antenna:${props.antenna.id}` : props.src" :src="props.src" :list="props.list" :antenna="props.antenna"/>
+		</div>
+	</mk-container>
+</div>
+</template>
+
+<script lang="ts">
+import { faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faListUl, faSatellite } from '@fortawesome/free-solid-svg-icons';
+import { faComments } from '@fortawesome/free-regular-svg-icons';
+import MkContainer from '../components/ui/container.vue';
+import XTimeline from '../components/timeline.vue';
+import define from './define';
+import i18n from '../i18n';
+
+export default define({
+	name: 'timeline',
+	props: () => ({
+		src: 'home',
+		list: null,
+		compact: false
+	})
+}).extend({
+	i18n,
+	
+	components: {
+		MkContainer,
+		XTimeline,
+	},
+
+	data() {
+		return {
+			menuOpened: false,
+			faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faComments, faListUl, faSatellite
+		};
+	},
+
+	methods: {
+		func() {
+			this.props.compact = !this.props.compact;
+			this.save();
+		},
+
+		async choose(ev) {
+			this.menuOpened = true;
+			const [antennas, lists] = await Promise.all([
+				this.$root.api('antennas/list'),
+				this.$root.api('users/lists/list')
+			]);
+			const antennaItems = antennas.map(antenna => ({
+				text: antenna.name,
+				icon: faSatellite,
+				action: () => {
+					this.props.antenna = antenna;
+					this.setSrc('antenna');
+				}
+			}));
+			const listItems = lists.map(list => ({
+				text: list.name,
+				icon: faListUl,
+				action: () => {
+					this.props.list = list;
+					this.setSrc('list');
+				}
+			}));
+			this.$root.menu({
+				items: [{
+					text: this.$t('_timelines.home'),
+					icon: faHome,
+					action: () => { this.setSrc('home') }
+				}, {
+					text: this.$t('_timelines.local'),
+					icon: faComments,
+					action: () => { this.setSrc('local') }
+				}, {
+					text: this.$t('_timelines.social'),
+					icon: faShareAlt,
+					action: () => { this.setSrc('social') }
+				}, {
+					text: this.$t('_timelines.global'),
+					icon: faGlobe,
+					action: () => { this.setSrc('global') }
+				}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems],
+				noCenter: true,
+				source: ev.currentTarget || ev.target
+			}).then(() => {
+				this.menuOpened = false;
+			});
+		},
+
+		setSrc(src) {
+			this.props.src = src;
+			this.save();
+		},
+	}
+});
+</script>
diff --git a/src/client/app/common/views/components/trends.chart.vue b/src/client/widgets/trends.chart.vue
similarity index 100%
rename from src/client/app/common/views/components/trends.chart.vue
rename to src/client/widgets/trends.chart.vue
diff --git a/src/client/widgets/trends.vue b/src/client/widgets/trends.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7e887f0f224a06d91281d1e92c855f112e854237
--- /dev/null
+++ b/src/client/widgets/trends.vue
@@ -0,0 +1,124 @@
+<template>
+<div>
+	<mk-container :show-header="!props.compact">
+		<template #header><fa :icon="faHashtag"/>{{ $t('_widgets.trends') }}</template>
+
+		<div class="wbrkwala">
+			<transition-group tag="div" name="chart">
+				<div v-for="stat in stats" :key="stat.tag">
+					<div class="tag">
+						<router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
+						<p>{{ $t('count').replace('{}', stat.usersCount) }}</p>
+					</div>
+					<x-chart class="chart" :src="stat.chart"/>
+				</div>
+			</transition-group>
+		</div>
+	</mk-container>
+</div>
+</template>
+
+<script lang="ts">
+import { faHashtag } from '@fortawesome/free-solid-svg-icons';
+import MkContainer from '../components/ui/container.vue';
+import define from './define';
+import i18n from '../i18n';
+import XChart from './trends.chart.vue';
+
+export default define({
+	name: 'hashtags',
+	props: () => ({
+		compact: false
+	})
+}).extend({
+	i18n,
+	components: {
+		MkContainer, XChart
+	},
+	data() {
+		return {
+			stats: [],
+			fetching: true,
+			faHashtag
+		};
+	},
+	mounted() {
+		this.fetch();
+		this.clock = setInterval(this.fetch, 1000 * 60);
+	},
+	beforeDestroy() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		func() {
+			this.props.compact = !this.props.compact;
+			this.save();
+		},
+		fetch() {
+			this.$root.api('hashtags/trend').then(stats => {
+				this.stats = stats;
+				this.fetching = false;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.wbrkwala {
+	> .fetching,
+	> .empty {
+		margin: 0;
+		padding: 16px;
+		text-align: center;
+		color: var(--text);
+		opacity: 0.7;
+
+		> [data-icon] {
+			margin-right: 4px;
+		}
+	}
+
+	> div {
+		.chart-move {
+			transition: transform 1s ease;
+		}
+
+		> div {
+			display: flex;
+			align-items: center;
+			padding: 14px 16px;
+
+			&:not(:last-child) {
+				border-bottom: solid 1px var(--divider);
+			}
+
+			> .tag {
+				flex: 1;
+				overflow: hidden;
+				font-size: 14px;
+				color: var(--fg);
+
+				> a {
+					display: block;
+					width: 100%;
+					white-space: nowrap;
+					overflow: hidden;
+					text-overflow: ellipsis;
+					color: inherit;
+				}
+
+				> p {
+					margin: 0;
+					font-size: 75%;
+					opacity: 0.7;
+				}
+			}
+
+			> .chart {
+				height: 30px;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/config/load.ts b/src/config/load.ts
index eea9ed8561575afc488899a9b4bdd076c40fa989..3c07494e92682478adc600dcf1262dc20e14c18b 100644
--- a/src/config/load.ts
+++ b/src/config/load.ts
@@ -41,8 +41,6 @@ export default function load() {
 	mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
 	mixin.userAgent = `Misskey/${meta.version} (${config.url})`;
 
-	if (config.autoAdmin == null) config.autoAdmin = false;
-
 	if (!config.redis.prefix) config.redis.prefix = mixin.host;
 
 	return Object.assign(config, mixin);
diff --git a/src/config/types.ts b/src/config/types.ts
index aeb2c1233334bdbdab16f660c7b2e7d459d912c0..78ae0251333c6180e277bd050c30e518fd49dcbd 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -32,8 +32,6 @@ export type Source = {
 		ssl?: boolean;
 	};
 
-	autoAdmin?: boolean;
-
 	proxy?: string;
 	proxySmtp?: string;
 
diff --git a/src/daemons/notes-stats-child.ts b/src/daemons/notes-stats-child.ts
deleted file mode 100644
index b60f5badfd12323d8e4d4eac9257620523612e27..0000000000000000000000000000000000000000
--- a/src/daemons/notes-stats-child.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { MoreThanOrEqual, getRepository } from 'typeorm';
-import { Note } from '../models/entities/note';
-import { initDb } from '../db/postgre';
-
-const interval = 5000;
-
-initDb().then(() => {
-	const Notes = getRepository(Note);
-
-	async function tick() {
-		const [all, local] = await Promise.all([Notes.count({
-			createdAt: MoreThanOrEqual(new Date(Date.now() - interval))
-		}), Notes.count({
-			createdAt: MoreThanOrEqual(new Date(Date.now() - interval)),
-			userHost: null
-		})]);
-
-		const stats = {
-			all, local
-		};
-
-		process.send!(stats);
-	}
-
-	tick();
-
-	setInterval(tick, interval);
-});
diff --git a/src/daemons/notes-stats.ts b/src/daemons/notes-stats.ts
deleted file mode 100644
index bddb54cfa56aeb93c4ca54a8c37d3359993079ce..0000000000000000000000000000000000000000
--- a/src/daemons/notes-stats.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import * as childProcess from 'child_process';
-import * as Deque from 'double-ended-queue';
-import Xev from 'xev';
-
-const ev = new Xev();
-
-export default function() {
-	const log = new Deque<any>();
-
-	const p = childProcess.fork(__dirname + '/notes-stats-child.js');
-
-	p.on('message', stats => {
-		ev.emit('notesStats', stats);
-		log.push(stats);
-		if (log.length > 100) log.shift();
-	});
-
-	ev.on('requestNotesStatsLog', id => {
-		ev.emit(`notesStatsLog:${id}`, log.toArray());
-	});
-
-	process.on('exit', code => {
-		process.kill(p.pid);
-	});
-
-}
diff --git a/src/daemons/queue-stats.ts b/src/daemons/queue-stats.ts
index e560354c74ccc8774e5e5d0898d7edefc6705530..288e855ae973aa8f2771aedb71025bbb07403417 100644
--- a/src/daemons/queue-stats.ts
+++ b/src/daemons/queue-stats.ts
@@ -1,25 +1,22 @@
-import * as Deque from 'double-ended-queue';
 import Xev from 'xev';
-import { deliverQueue, inboxQueue, dbQueue, objectStorageQueue } from '../queue';
+import { deliverQueue, inboxQueue } from '../queue';
 
 const ev = new Xev();
 
-const interval = 3000;
+const interval = 10000;
 
 /**
  * Report queue stats regularly
  */
 export default function() {
-	const log = new Deque<any>();
+	const log = [] as any[];
 
 	ev.on('requestQueueStatsLog', x => {
-		ev.emit(`queueStatsLog:${x.id}`, log.toArray().slice(0, x.length || 50));
+		ev.emit(`queueStatsLog:${x.id}`, log.slice(0, x.length || 50));
 	});
 
 	let activeDeliverJobs = 0;
 	let activeInboxJobs = 0;
-	let activeDbJobs = 0;
-	let activeObjectStorageJobs = 0;
 
 	deliverQueue.on('global:active', () => {
 		activeDeliverJobs++;
@@ -29,19 +26,9 @@ export default function() {
 		activeInboxJobs++;
 	});
 
-	dbQueue.on('global:active', () => {
-		activeDbJobs++;
-	});
-
-	objectStorageQueue.on('global:active', () => {
-		activeObjectStorageJobs++;
-	});
-
 	async function tick() {
 		const deliverJobCounts = await deliverQueue.getJobCounts();
 		const inboxJobCounts = await inboxQueue.getJobCounts();
-		const dbJobCounts = await dbQueue.getJobCounts();
-		const objectStorageJobCounts = await objectStorageQueue.getJobCounts();
 
 		const stats = {
 			deliver: {
@@ -56,18 +43,6 @@ export default function() {
 				waiting: inboxJobCounts.waiting,
 				delayed: inboxJobCounts.delayed
 			},
-			db: {
-				activeSincePrevTick: activeDbJobs,
-				active: dbJobCounts.active,
-				waiting: dbJobCounts.waiting,
-				delayed: dbJobCounts.delayed
-			},
-			objectStorage: {
-				activeSincePrevTick: activeObjectStorageJobs,
-				active: objectStorageJobCounts.active,
-				waiting: objectStorageJobCounts.waiting,
-				delayed: objectStorageJobCounts.delayed
-			},
 		};
 
 		ev.emit('queueStats', stats);
@@ -77,8 +52,6 @@ export default function() {
 
 		activeDeliverJobs = 0;
 		activeInboxJobs = 0;
-		activeDbJobs = 0;
-		activeObjectStorageJobs = 0;
 	}
 
 	tick();
diff --git a/src/daemons/server-stats.ts b/src/daemons/server-stats.ts
index ee62c32d7ebd3859d1281113db1e5573c8763d90..88df421ba007033cf0f9157896873c67f3748176 100644
--- a/src/daemons/server-stats.ts
+++ b/src/daemons/server-stats.ts
@@ -1,39 +1,41 @@
-import * as os from 'os';
-import * as sysUtils from 'systeminformation';
-import * as diskusage from 'diskusage';
-import * as Deque from 'double-ended-queue';
+import * as si from 'systeminformation';
 import Xev from 'xev';
 import * as osUtils from 'os-utils';
 
 const ev = new Xev();
 
-const interval = 1000;
+const interval = 2000;
 
 /**
  * Report server stats regularly
  */
 export default function() {
-	const log = new Deque<any>();
+	const log = [] as any[];
 
 	ev.on('requestServerStatsLog', x => {
-		ev.emit(`serverStatsLog:${x.id}`, log.toArray().slice(0, x.length || 50));
+		ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50));
 	});
 
 	async function tick() {
 		const cpu = await cpuUsage();
-		const usedmem = await usedMem();
-		const totalmem = await totalMem();
-		const disk = await diskusage.check(os.platform() == 'win32' ? 'c:' : '/');
+		const memStats = await mem();
+		const netStats = await net();
+		const fsStats = await fs();
 
 		const stats = {
-			cpu_usage: cpu,
+			cpu: cpu,
 			mem: {
-				total: totalmem,
-				used: usedmem
+				used: memStats.used,
+				active: memStats.active,
 			},
-			disk,
-			os_uptime: os.uptime(),
-			process_uptime: process.uptime()
+			net: {
+				rx: Math.max(0, netStats.rx_sec),
+				tx: Math.max(0, netStats.tx_sec),
+			},
+			fs: {
+				r: Math.max(0, fsStats.rIO_sec),
+				w: Math.max(0, fsStats.wIO_sec),
+			}
 		};
 		ev.emit('serverStats', stats);
 		log.unshift(stats);
@@ -54,14 +56,21 @@ function cpuUsage() {
 	});
 }
 
-// MEMORY(excl buffer + cache) STAT
-async function usedMem() {
-	const data = await sysUtils.mem();
-	return data.active;
+// MEMORY STAT
+async function mem() {
+	const data = await si.mem();
+	return data;
+}
+
+// NETWORK STAT
+async function net() {
+	const iface = await si.networkInterfaceDefault();
+	const data = await si.networkStats(iface);
+	return data[0];
 }
 
-// TOTAL MEMORY STAT
-async function totalMem() {
-	const data = await sysUtils.mem();
-	return data.total;
+// FS STAT
+async function fs() {
+	const data = await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 }));
+	return data;
 }
diff --git a/src/db/postgre.ts b/src/db/postgre.ts
index 0947a5983b6d01cb9dbba0a5d133b3bb28120f47..3e12db3a072ae3acfe025a31a12bba683eeadada 100644
--- a/src/db/postgre.ts
+++ b/src/db/postgre.ts
@@ -49,6 +49,12 @@ import { Page } from '../models/entities/page';
 import { PageLike } from '../models/entities/page-like';
 import { ModerationLog } from '../models/entities/moderation-log';
 import { UsedUsername } from '../models/entities/used-username';
+import { Announcement } from '../models/entities/announcement';
+import { AnnouncementRead } from '../models/entities/announcement-read';
+import { Clip } from '../models/entities/clip';
+import { ClipNote } from '../models/entities/clip-note';
+import { Antenna } from '../models/entities/antenna';
+import { AntennaNote } from '../models/entities/antenna-note';
 
 const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
 
@@ -85,6 +91,8 @@ class MyCustomLogger implements Logger {
 }
 
 export const entities = [
+	Announcement,
+	AnnouncementRead,
 	Meta,
 	Instance,
 	App,
@@ -128,6 +136,10 @@ export const entities = [
 	MessagingMessage,
 	Signin,
 	ModerationLog,
+	Clip,
+	ClipNote,
+	Antenna,
+	AntennaNote,
 	ReversiGame,
 	ReversiMatching,
 	...charts as any
diff --git a/src/docs/style.styl b/src/docs/style.styl
index 96d14c2b981337eb9d1f85b0eb9b9011acae575d..6b63cab7d7a56e401caee09c2311af55244247fe 100644
--- a/src/docs/style.styl
+++ b/src/docs/style.styl
@@ -2,7 +2,7 @@
 @import "./ui"
 
 html
-	--primary #fb4e4e
+	--accent #fb4e4e
 	--link #fb4e4e
 	--linkTapHighlight #fb4e4eb3
 
diff --git a/src/misc/check-hit-antenna.ts b/src/misc/check-hit-antenna.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b527d34354f83fba6bc0499fb348be523f8bf73c
--- /dev/null
+++ b/src/misc/check-hit-antenna.ts
@@ -0,0 +1,53 @@
+import { Antenna } from '../models/entities/antenna';
+import { Note } from '../models/entities/note';
+import { User } from '../models/entities/user';
+import { UserListJoinings } from '../models';
+import parseAcct from './acct/parse';
+import { getFullApAccount } from './convert-host';
+
+export async function checkHitAntenna(antenna: Antenna, note: Note, noteUser: User, followers: User['id'][]): Promise<boolean> {
+	if (note.visibility === 'specified') return false;
+
+	if (note.visibility === 'followers') {
+		if (!followers.includes(antenna.userId)) return false;
+	}
+
+	if (!antenna.withReplies && note.replyId != null) return false;
+
+	if (antenna.src === 'home') {
+		if (!followers.includes(antenna.userId)) return false;
+	} else if (antenna.src === 'list') {
+		const listUsers = (await UserListJoinings.find({
+			userListId: antenna.userListId!
+		})).map(x => x.userId);
+
+		if (!listUsers.includes(note.userId)) return false;
+	} else if (antenna.src === 'users') {
+		const accts = antenna.users.map(x => {
+			const { username, host } = parseAcct(x);
+			return getFullApAccount(username, host).toLowerCase();
+		});
+		if (!accts.includes(getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
+	}
+
+	if (antenna.keywords.length > 0) {
+		if (note.text == null) return false;
+
+		const matched = antenna.keywords.some(keywords =>
+			keywords.every(keyword =>
+				antenna.caseSensitive
+					? note.text!.includes(keyword)
+					: note.text!.toLowerCase().includes(keyword.toLowerCase())
+			));
+		
+		if (!matched) return false;
+	}
+
+	if (antenna.withFile) {
+		if (note.fileIds.length === 0) return false;
+	}
+
+	// TODO: eval expression
+
+	return true;
+}
diff --git a/src/misc/reaction-lib.ts b/src/misc/reaction-lib.ts
index ced90ce78f5f90db389efbf5880876690fda8c55..eba57ea30388f2fa04addf2fe5587875e779e487 100644
--- a/src/misc/reaction-lib.ts
+++ b/src/misc/reaction-lib.ts
@@ -2,31 +2,29 @@ import { emojiRegex } from './emoji-regex';
 import { fetchMeta } from './fetch-meta';
 import { Emojis } from '../models';
 
-const basic10: Record<string, string> = {
-	'👍': 'like',
-	'❤': 'love',	// ここに記述する場合は異体字セレクタを入れない
-	'😆': 'laugh',
-	'🤔': 'hmm',
-	'😮': 'surprise',
-	'🎉': 'congrats',
-	'💢': 'angry',
-	'😥': 'confused',
-	'😇': 'rip',
-	'🍮': 'pudding',
+const legacy10: Record<string, string> = {
+	'like':     '👍',
+	'love':     '❤', // ここに記述する場合は異体字セレクタを入れない
+	'laugh':    '😆',
+	'hmm':      '🤔',
+	'surprise': '😮',
+	'congrats': '🎉',
+	'angry':    '💢',
+	'confused': '😥',
+	'rip':      '😇',
+	'pudding':  '🍮',
 };
 
 export async function getFallbackReaction(): Promise<string> {
 	const meta = await fetchMeta();
-	return  meta.useStarForReactionFallback ? 'star' : 'like';
+	return meta.useStarForReactionFallback ? '⭐' : '👍';
 }
 
-export async function toDbReaction(reaction?: string | null, enableEmoji = true): Promise<string> {
+export async function toDbReaction(reaction?: string | null): Promise<string> {
 	if (reaction == null) return await getFallbackReaction();
 
-	// 既存の文字列リアクションはそのまま
-	if (Object.values(basic10).includes(reaction)) return reaction;
-
-	if (!enableEmoji) return await getFallbackReaction();
+	// 文字列タイプのリアクションを絵文字に変換
+	if (Object.keys(legacy10).includes(reaction)) return legacy10[reaction];
 
 	// Unicode絵文字
 	const match = emojiRegex.exec(reaction);
@@ -34,17 +32,8 @@ export async function toDbReaction(reaction?: string | null, enableEmoji = true)
 		// 合字を含む1つの絵文字
 		const unicode = match[0];
 
-		// 異体字セレクタ除去後の絵文字
-		const normalized = unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
-
-		// Unicodeプリンは寿司化不能とするため文字列化しない
-		if (normalized === '🍮') return normalized;
-
-		// プリン以外の既存のリアクションは文字列化する
-		if (basic10[normalized]) return basic10[normalized];
-
-		// それ以外はUnicodeのまま
-		return normalized;
+		// 異体字セレクタ除去
+		return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
 	}
 
 	const custom = reaction.match(/^:([\w+-]+):$/);
@@ -59,3 +48,8 @@ export async function toDbReaction(reaction?: string | null, enableEmoji = true)
 
 	return await getFallbackReaction();
 }
+
+export function convertLegacyReaction(reaction: string): string {
+	if (Object.keys(legacy10).includes(reaction)) return legacy10[reaction];
+	return reaction;
+}
diff --git a/src/models/entities/announcement-read.ts b/src/models/entities/announcement-read.ts
new file mode 100644
index 0000000000000000000000000000000000000000..892beb826f8a39eaaee3ca5b117f6584f6cb335a
--- /dev/null
+++ b/src/models/entities/announcement-read.ts
@@ -0,0 +1,36 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Announcement } from './announcement';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'announcementId'], { unique: true })
+export class AnnouncementRead {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the AnnouncementRead.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column(id())
+	public announcementId: Announcement['id'];
+
+	@ManyToOne(type => Announcement, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public announcement: Announcement | null;
+}
diff --git a/src/models/entities/announcement.ts b/src/models/entities/announcement.ts
new file mode 100644
index 0000000000000000000000000000000000000000..06d379c2295f5786b9202735ae5bf31467e71c38
--- /dev/null
+++ b/src/models/entities/announcement.ts
@@ -0,0 +1,43 @@
+import { Entity, Index, Column, PrimaryColumn } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class Announcement {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Announcement.'
+	})
+	public createdAt: Date;
+
+	@Column('timestamp with time zone', {
+		comment: 'The updated date of the Announcement.',
+		nullable: true
+	})
+	public updatedAt: Date | null;
+
+	@Column('varchar', {
+		length: 8192, nullable: false
+	})
+	public text: string;
+
+	@Column('varchar', {
+		length: 256, nullable: false
+	})
+	public title: string;
+
+	@Column('varchar', {
+		length: 1024, nullable: true
+	})
+	public imageUrl: string | null;
+
+	constructor(data: Partial<Announcement>) {
+		if (data == null) return;
+
+		for (const [k, v] of Object.entries(data)) {
+			(this as any)[k] = v;
+		}
+	}
+}
diff --git a/src/models/entities/antenna-note.ts b/src/models/entities/antenna-note.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9b911524efc6cbf67c5c8e607101384761094c6d
--- /dev/null
+++ b/src/models/entities/antenna-note.ts
@@ -0,0 +1,43 @@
+import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
+import { Note } from './note';
+import { Antenna } from './antenna';
+import { id } from '../id';
+
+@Entity()
+@Index(['noteId', 'antennaId'], { unique: true })
+export class AntennaNote {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The note ID.'
+	})
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The antenna ID.'
+	})
+	public antennaId: Antenna['id'];
+
+	@ManyToOne(type => Antenna, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public antenna: Antenna | null;
+
+	@Index()
+	@Column('boolean', {
+		default: false
+	})
+	public read: boolean;
+}
diff --git a/src/models/entities/antenna.ts b/src/models/entities/antenna.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e9971c6c07e1dbf29b523e16c74aae6e5282210f
--- /dev/null
+++ b/src/models/entities/antenna.ts
@@ -0,0 +1,81 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+import { UserList } from './user-list';
+
+@Entity()
+export class Antenna {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Antenna.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The owner ID.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('varchar', {
+		length: 128,
+		comment: 'The name of the Antenna.'
+	})
+	public name: string;
+
+	@Column('enum', { enum: ['home', 'all', 'users', 'list'] })
+	public src: 'home' | 'all' | 'users' | 'list';
+
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public userListId: UserList['id'] | null;
+
+	@ManyToOne(type => UserList, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public userList: UserList | null;
+
+	@Column('varchar', {
+		length: 1024, array: true,
+		default: '{}'
+	})
+	public users: string[];
+
+	@Column('jsonb', {
+		default: []
+	})
+	public keywords: string[][];
+
+	@Column('boolean', {
+		default: false
+	})
+	public caseSensitive: boolean;
+
+	@Column('boolean', {
+		default: false
+	})
+	public withReplies: boolean;
+
+	@Column('boolean')
+	public withFile: boolean;
+
+	@Column('varchar', {
+		length: 2048, nullable: true,
+	})
+	public expression: string | null;
+
+	@Column('boolean')
+	public notify: boolean;
+}
diff --git a/src/models/entities/clip-note.ts b/src/models/entities/clip-note.ts
new file mode 100644
index 0000000000000000000000000000000000000000..19e4750fc6f42908bd52b0a35ae96424fcaa0f91
--- /dev/null
+++ b/src/models/entities/clip-note.ts
@@ -0,0 +1,37 @@
+import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
+import { Note } from './note';
+import { Clip } from './clip';
+import { id } from '../id';
+
+@Entity()
+@Index(['noteId', 'clipId'], { unique: true })
+export class ClipNote {
+	@PrimaryColumn(id())
+	public id: string;
+	
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The note ID.'
+	})
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The clip ID.'
+	})
+	public clipId: Clip['id'];
+
+	@ManyToOne(type => Clip, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public clip: Clip | null;
+}
diff --git a/src/models/entities/clip.ts b/src/models/entities/clip.ts
new file mode 100644
index 0000000000000000000000000000000000000000..37d21f73b11ed4e00c4bb78e9f9f26d12fe11f93
--- /dev/null
+++ b/src/models/entities/clip.ts
@@ -0,0 +1,38 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class Clip {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Clip.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The owner ID.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('varchar', {
+		length: 128,
+		comment: 'The name of the Clip.'
+	})
+	public name: string;
+
+	@Column('boolean', {
+		default: false
+	})
+	public isPublic: boolean;
+}
diff --git a/src/models/entities/meta.ts b/src/models/entities/meta.ts
index e5b189ef836f0bb289242a6d5fba10182838b2d1..4063c811391022aabbfd26bd85f4a38ea1bf4404 100644
--- a/src/models/entities/meta.ts
+++ b/src/models/entities/meta.ts
@@ -34,11 +34,6 @@ export class Meta {
 	})
 	public maintainerEmail: string | null;
 
-	@Column('jsonb', {
-		default: [],
-	})
-	public announcements: Record<string, any>[];
-
 	@Column('boolean', {
 		default: false,
 	})
@@ -54,11 +49,6 @@ export class Meta {
 	})
 	public disableGlobalTimeline: boolean;
 
-	@Column('boolean', {
-		default: true,
-	})
-	public enableEmojiReaction: boolean;
-
 	@Column('boolean', {
 		default: false,
 	})
diff --git a/src/models/entities/note.ts b/src/models/entities/note.ts
index d67faae829cff901e2127f5ebd7ede43598e501b..c77c7bd8d6484fd7069034aa04e71519d8695721 100644
--- a/src/models/entities/note.ts
+++ b/src/models/entities/note.ts
@@ -58,18 +58,6 @@ export class Note {
 	})
 	public cw: string | null;
 
-	@Column({
-		...id(),
-		nullable: true
-	})
-	public appId: App['id'] | null;
-
-	@ManyToOne(type => App, {
-		onDelete: 'SET NULL'
-	})
-	@JoinColumn()
-	public app: App | null;
-
 	@Index()
 	@Column({
 		...id(),
@@ -177,11 +165,6 @@ export class Note {
 	})
 	public hasPoll: boolean;
 
-	@Column('jsonb', {
-		nullable: true, default: null
-	})
-	public geo: any | null;
-
 	//#region Denormalized fields
 	@Index()
 	@Column('varchar', {
diff --git a/src/models/entities/notification.ts b/src/models/entities/notification.ts
index 627a57bececeedd972c6de0c9405ca435ce386e6..e359640e828b14203d1f17a4297030d82c91d58d 100644
--- a/src/models/entities/notification.ts
+++ b/src/models/entities/notification.ts
@@ -2,6 +2,7 @@ import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typ
 import { User } from './user';
 import { id } from '../id';
 import { Note } from './note';
+import { FollowRequest } from './follow-request';
 
 @Entity()
 export class Notification {
@@ -54,12 +55,14 @@ export class Notification {
 	 * quote - (自分または自分がWatchしている)投稿が引用Renoteされた
 	 * reaction - (自分または自分がWatchしている)投稿にリアクションされた
 	 * pollVote - (自分または自分がWatchしている)投稿の投票に投票された
+	 * receiveFollowRequest - フォローリクエストされた
+	 * followRequestAccepted - 自分の送ったフォローリクエストが承認された
 	 */
-	@Column('varchar', {
-		length: 32,
+	@Column('enum', {
+		enum: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted'],
 		comment: 'The type of the Notification.'
 	})
-	public type: string;
+	public type: 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollVote' | 'receiveFollowRequest' | 'followRequestAccepted';
 
 	/**
 	 * 通知が読まれたかどうか
@@ -82,6 +85,18 @@ export class Notification {
 	@JoinColumn()
 	public note: Note | null;
 
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public followRequestId: FollowRequest['id'] | null;
+
+	@ManyToOne(type => FollowRequest, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public followRequest: FollowRequest | null;
+
 	@Column('varchar', {
 		length: 128, nullable: true
 	})
diff --git a/src/models/index.ts b/src/models/index.ts
index fc40ebfb230895c89a56b5d88ff52e27dabdb3a9..15a5c5470dca4ab3d674683abe47c58b63c691e2 100644
--- a/src/models/index.ts
+++ b/src/models/index.ts
@@ -1,4 +1,6 @@
 import { getRepository, getCustomRepository } from 'typeorm';
+import { Announcement } from './entities/announcement';
+import { AnnouncementRead } from './entities/announcement-read';
 import { Instance } from './entities/instance';
 import { Emoji } from './entities/emoji';
 import { Poll } from './entities/poll';
@@ -44,7 +46,13 @@ import { PageRepository } from './repositories/page';
 import { PageLikeRepository } from './repositories/page-like';
 import { ModerationLogRepository } from './repositories/moderation-logs';
 import { UsedUsername } from './entities/used-username';
+import { ClipRepository } from './repositories/clip';
+import { ClipNote } from './entities/clip-note';
+import { AntennaRepository } from './repositories/antenna';
+import { AntennaNote } from './entities/antenna-note';
 
+export const Announcements = getRepository(Announcement);
+export const AnnouncementReads = getRepository(AnnouncementRead);
 export const Apps = getCustomRepository(AppRepository);
 export const Notes = getCustomRepository(NoteRepository);
 export const NoteFavorites = getCustomRepository(NoteFavoriteRepository);
@@ -90,3 +98,7 @@ export const Logs = getRepository(Log);
 export const Pages = getCustomRepository(PageRepository);
 export const PageLikes = getCustomRepository(PageLikeRepository);
 export const ModerationLogs = getCustomRepository(ModerationLogRepository);
+export const Clips = getCustomRepository(ClipRepository);
+export const ClipNotes = getRepository(ClipNote);
+export const Antennas = getCustomRepository(AntennaRepository);
+export const AntennaNotes = getRepository(AntennaNote);
diff --git a/src/models/repositories/antenna.ts b/src/models/repositories/antenna.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c47a7ea35c13298770a1ecba0b323fcfecbc1af7
--- /dev/null
+++ b/src/models/repositories/antenna.ts
@@ -0,0 +1,58 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Antenna } from '../entities/antenna';
+import { ensure } from '../../prelude/ensure';
+import { SchemaType } from '../../misc/schema';
+import { AntennaNotes } from '..';
+
+export type PackedAntenna = SchemaType<typeof packedAntennaSchema>;
+
+@EntityRepository(Antenna)
+export class AntennaRepository extends Repository<Antenna> {
+	public async pack(
+		src: Antenna['id'] | Antenna,
+	): Promise<PackedAntenna> {
+		const antenna = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+		const hasUnreadNote = (await AntennaNotes.findOne({ antennaId: antenna.id, read: false })) != null;
+
+		return {
+			id: antenna.id,
+			createdAt: antenna.createdAt.toISOString(),
+			name: antenna.name,
+			keywords: antenna.keywords,
+			src: antenna.src,
+			userListId: antenna.userListId,
+			users: antenna.users,
+			caseSensitive: antenna.caseSensitive,
+			notify: antenna.notify,
+			withReplies: antenna.withReplies,
+			withFile: antenna.withFile,
+			hasUnreadNote
+		};
+	}
+}
+
+export const packedAntennaSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			description: 'The unique identifier for this Antenna.',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+			description: 'The date that the Antenna was created.'
+		},
+		name: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			description: 'The name of the Antenna.'
+		},
+	},
+};
diff --git a/src/models/repositories/clip.ts b/src/models/repositories/clip.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9644ceec7e7267c1c0f98da92691b73392dabe5d
--- /dev/null
+++ b/src/models/repositories/clip.ts
@@ -0,0 +1,46 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Clip } from '../entities/clip';
+import { ensure } from '../../prelude/ensure';
+import { SchemaType } from '../../misc/schema';
+
+export type PackedClip = SchemaType<typeof packedClipSchema>;
+
+@EntityRepository(Clip)
+export class ClipRepository extends Repository<Clip> {
+	public async pack(
+		src: Clip['id'] | Clip,
+	): Promise<PackedClip> {
+		const clip = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+		return {
+			id: clip.id,
+			createdAt: clip.createdAt.toISOString(),
+			name: clip.name,
+		};
+	}
+}
+
+export const packedClipSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			description: 'The unique identifier for this Clip.',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+			description: 'The date that the Clip was created.'
+		},
+		name: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			description: 'The name of the Clip.'
+		},
+	},
+};
diff --git a/src/models/repositories/note-reaction.ts b/src/models/repositories/note-reaction.ts
index 79b4989a20851d20dd3fbdd183a0e44cfc9e7265..3439f3c8cb8334857c7bac9d06a2580476a86d1e 100644
--- a/src/models/repositories/note-reaction.ts
+++ b/src/models/repositories/note-reaction.ts
@@ -3,6 +3,7 @@ import { NoteReaction } from '../entities/note-reaction';
 import { Users } from '..';
 import { ensure } from '../../prelude/ensure';
 import { SchemaType } from '../../misc/schema';
+import { convertLegacyReaction } from '../../misc/reaction-lib';
 
 export type PackedNoteReaction = SchemaType<typeof packedNoteReactionSchema>;
 
@@ -18,7 +19,7 @@ export class NoteReactionRepository extends Repository<NoteReaction> {
 			id: reaction.id,
 			createdAt: reaction.createdAt.toISOString(),
 			user: await Users.pack(reaction.userId, me),
-			type: reaction.reaction,
+			type: convertLegacyReaction(reaction.reaction),
 		};
 	}
 }
diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts
index 9c01bf84d5161c988913cbb0e745e97a2ea75425..73eb2f05383f171161f0319ea7ea6cb1a495438a 100644
--- a/src/models/repositories/note.ts
+++ b/src/models/repositories/note.ts
@@ -7,6 +7,7 @@ import { Emojis, Users, Apps, PollVotes, DriveFiles, NoteReactions, Followings,
 import { ensure } from '../../prelude/ensure';
 import { SchemaType } from '../../misc/schema';
 import { awaitAll } from '../../prelude/await-all';
+import { convertLegacyReaction } from '../../misc/reaction-lib';
 
 export type PackedNote = SchemaType<typeof packedNoteSchema>;
 
@@ -71,7 +72,6 @@ export class NoteRepository extends Repository<Note> {
 			packedNote.text = null;
 			packedNote.poll = undefined;
 			packedNote.cw = null;
-			packedNote.geo = undefined;
 			packedNote.isHidden = true;
 		}
 	}
@@ -163,7 +163,7 @@ export class NoteRepository extends Repository<Note> {
 			});
 
 			if (reaction) {
-				return reaction.reaction;
+				return convertLegacyReaction(reaction.reaction);
 			}
 
 			return undefined;
@@ -178,7 +178,6 @@ export class NoteRepository extends Repository<Note> {
 		const packed = await awaitAll({
 			id: note.id,
 			createdAt: note.createdAt.toISOString(),
-			app: note.appId ? Apps.pack(note.appId) : undefined,
 			userId: note.userId,
 			user: Users.pack(note.user || note.userId, meId),
 			text: text,
@@ -189,7 +188,7 @@ export class NoteRepository extends Repository<Note> {
 			viaMobile: note.viaMobile || undefined,
 			renoteCount: note.renoteCount,
 			repliesCount: note.repliesCount,
-			reactions: note.reactions,
+			reactions: note.reactions, // v12 TODO: convert legacy reaction
 			tags: note.tags.length > 0 ? note.tags : undefined,
 			emojis: populateEmojis(note.emojis, host, Object.keys(note.reactions)),
 			fileIds: note.fileIds,
@@ -356,9 +355,6 @@ export const packedNoteSchema = {
 			type: 'object' as const,
 			optional: true as const, nullable: true as const,
 		},
-		geo: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-		},
+
 	},
 };
diff --git a/src/models/repositories/notification.ts b/src/models/repositories/notification.ts
index 4a1036816096d11c355cf147bd3191193e062c71..6407c19d4c559336ad42ee5e6e49c1fda7acf786 100644
--- a/src/models/repositories/notification.ts
+++ b/src/models/repositories/notification.ts
@@ -70,7 +70,7 @@ export const packedNotificationSchema = {
 		type: {
 			type: 'string' as const,
 			optional: false as const, nullable: false as const,
-			enum: ['follow', 'receiveFollowRequest', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote'],
+			enum: ['follow', 'followRequestAccepted', 'receiveFollowRequest', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote'],
 			description: 'The type of the notification.'
 		},
 		userId: {
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
index 5c8cbb3d17e78e500f55d9fff1227f2a3609bdd7..c86fd6e1fc17adea9f08dd69024bc4002d182991 100644
--- a/src/models/repositories/user.ts
+++ b/src/models/repositories/user.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
 import { EntityRepository, Repository, In, Not } from 'typeorm';
 import { User, ILocalUser, IRemoteUser } from '../entities/user';
-import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages } from '..';
+import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes } from '..';
 import { ensure } from '../../prelude/ensure';
 import config from '../../config';
 import { SchemaType } from '../../misc/schema';
@@ -84,6 +84,47 @@ export class UserRepository extends Repository<User> {
 		return withUser || withGroups.some(x => x);
 	}
 
+	public async getHasUnreadAnnouncement(userId: User['id']): Promise<boolean> {
+		const reads = await AnnouncementReads.find({
+			userId: userId
+		});
+
+		const count = await Announcements.count(reads.length > 0 ? {
+			id: Not(In(reads.map(read => read.announcementId)))
+		} : {});
+
+		return count > 0;
+	}
+
+	public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
+		const antennas = await Antennas.find({ userId });
+		
+		const unread = antennas.length > 0 ? await AntennaNotes.findOne({
+			antennaId: In(antennas.map(x => x.id)),
+			read: false
+		}) : null;
+
+		return unread != null;
+	}
+
+	public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
+		const mute = await Mutings.find({
+			muterId: userId
+		});
+		const mutedUserIds = mute.map(m => m.muteeId);
+	
+		const count = await Notifications.count({
+			where: {
+				notifieeId: userId,
+				...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}),
+				isRead: false
+			},
+			take: 1
+		});
+
+		return count > 0;
+	}
+
 	public async pack(
 		src: User['id'] | User,
 		me?: User['id'] | User | null | undefined,
@@ -193,14 +234,10 @@ export class UserRepository extends Repository<User> {
 				alwaysMarkNsfw: profile!.alwaysMarkNsfw,
 				carefulBot: profile!.carefulBot,
 				autoAcceptFollowed: profile!.autoAcceptFollowed,
+				hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
+				hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
 				hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
-				hasUnreadNotification: Notifications.count({
-					where: {
-						notifieeId: user.id,
-						isRead: false
-					},
-					take: 1
-				}).then(count => count > 0),
+				hasUnreadNotification: this.getHasUnreadNotification(user.id),
 				pendingReceivedFollowRequestsCount: FollowRequests.count({
 					followeeId: user.id
 				}),
diff --git a/src/queue/processors/db/export-notes.ts b/src/queue/processors/db/export-notes.ts
index 94a4302e052728faafdb0995247b5c0f9b92f52b..0fd8c02c4ad6731d546b877742de2bc80a796b39 100644
--- a/src/queue/processors/db/export-notes.ts
+++ b/src/queue/processors/db/export-notes.ts
@@ -128,8 +128,6 @@ function serialize(note: Note, poll: Poll | null = null): any {
 		viaMobile: note.viaMobile,
 		visibility: note.visibility,
 		visibleUserIds: note.visibleUserIds,
-		appId: note.appId,
-		geo: note.geo,
 		localOnly: note.localOnly
 	};
 }
diff --git a/src/queue/processors/inbox.ts b/src/queue/processors/inbox.ts
index 1a583ec865eead8ad880a80c80b95427edd54745..74108f354b7bdf6c4b542b26068cb275c2c3088c 100644
--- a/src/queue/processors/inbox.ts
+++ b/src/queue/processors/inbox.ts
@@ -3,7 +3,6 @@ import * as httpSignature from 'http-signature';
 import { IRemoteUser } from '../../models/entities/user';
 import perform from '../../remote/activitypub/perform';
 import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person';
-import { publishApLogStream } from '../../services/stream';
 import Logger from '../../services/logger';
 import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc';
 import { Instances, Users, UserPublickeys } from '../../models';
@@ -89,15 +88,6 @@ export default async (job: Bull.Job): Promise<void> => {
 		return;
 	}
 
-	//#region Log
-	publishApLogStream({
-		direction: 'in',
-		activity: activity.type,
-		host: user.host,
-		actor: user.username
-	});
-	//#endregion
-
 	// Update stats
 	registerOrFetchInstanceDoc(user.host).then(i => {
 		Instances.update(i.id, {
diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts
index 10cd68f6adf7d476088b92e8298228abd9fda358..d5c83208b6c404da60f4adb18fc7552683705fde 100644
--- a/src/remote/activitypub/models/note.ts
+++ b/src/remote/activitypub/models/note.ts
@@ -267,7 +267,6 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
 		text,
 		viaMobile: false,
 		localOnly: false,
-		geo: undefined,
 		visibility,
 		visibleUsers,
 		apMentions,
diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts
index 869fabd032388a6eb2346d57ca9be095d5c37755..dc777a3c5d5fd554b6661c7c652b08cb04616f41 100644
--- a/src/remote/activitypub/request.ts
+++ b/src/remote/activitypub/request.ts
@@ -5,7 +5,6 @@ import * as cache from 'lookup-dns-cache';
 
 import config from '../../config';
 import { ILocalUser } from '../../models/entities/user';
-import { publishApLogStream } from '../../services/stream';
 import { UserKeypairs } from '../../models';
 import { ensure } from '../../prelude/ensure';
 import * as httpsProxyAgent from 'https-proxy-agent';
@@ -69,13 +68,4 @@ export default async (user: ILocalUser, url: string, object: any) => {
 
 		req.end(data);
 	});
-
-	//#region Log
-	publishApLogStream({
-		direction: 'out',
-		activity: object.type,
-		host: null,
-		actor: user.username
-	});
-	//#endregion
 };
diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts
index c8d43ba286266cfe3ee7e2c1d34a60f7989b89f7..f686446c5c3a99ba67b431fb801b0f14c4ce5130 100644
--- a/src/server/api/common/read-notification.ts
+++ b/src/server/api/common/read-notification.ts
@@ -1,8 +1,8 @@
 import { publishMainStream } from '../../../services/stream';
 import { User } from '../../../models/entities/user';
 import { Notification } from '../../../models/entities/notification';
-import { Mutings, Notifications } from '../../../models';
-import { In, Not } from 'typeorm';
+import { Notifications, Users } from '../../../models';
+import { In } from 'typeorm';
 
 /**
  * Mark notifications as read
@@ -11,11 +11,6 @@ export async function readNotification(
 	userId: User['id'],
 	notificationIds: Notification['id'][]
 ) {
-	const mute = await Mutings.find({
-		muterId: userId
-	});
-	const mutedUserIds = mute.map(m => m.muteeId);
-
 	// Update documents
 	await Notifications.update({
 		id: In(notificationIds),
@@ -24,14 +19,7 @@ export async function readNotification(
 		isRead: true
 	});
 
-	// Calc count of my unread notifications
-	const count = await Notifications.count({
-		notifieeId: userId,
-		...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}),
-		isRead: false
-	});
-
-	if (count === 0) {
+	if (!await Users.getHasUnreadNotification(userId)) {
 		// 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行
 		publishMainStream(userId, 'readAllNotifications');
 	}
diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts
index 66f89182d290417dba72f8151e0c496c984ad1fd..aa2786f8fcf312ee0eb0d18ee46010055515b144 100644
--- a/src/server/api/common/signin.ts
+++ b/src/server/api/common/signin.ts
@@ -24,7 +24,10 @@ export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) {
 
 		ctx.redirect(config.url);
 	} else {
-		ctx.body = { i: user.token };
+		ctx.body = {
+			id: user.id,
+			i: user.token
+		};
 		ctx.status = 200;
 	}
 
diff --git a/src/server/api/common/signup.ts b/src/server/api/common/signup.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f0eb27e5e4546dac6f98f87ab18ed0fbd4d408a9
--- /dev/null
+++ b/src/server/api/common/signup.ts
@@ -0,0 +1,104 @@
+import * as bcrypt from 'bcryptjs';
+import { generateKeyPair } from 'crypto';
+import generateUserToken from './generate-native-user-token';
+import { User } from '../../../models/entities/user';
+import { Users, UsedUsernames } from '../../../models';
+import { UserProfile } from '../../../models/entities/user-profile';
+import { getConnection } from 'typeorm';
+import { genId } from '../../../misc/gen-id';
+import { toPunyNullable } from '../../../misc/convert-host';
+import { UserKeypair } from '../../../models/entities/user-keypair';
+import { usersChart } from '../../../services/chart';
+import { UsedUsername } from '../../../models/entities/used-username';
+
+export async function signup(username: User['username'], password: UserProfile['password'], host: string | null = null) {
+	// Validate username
+	if (!Users.validateLocalUsername.ok(username)) {
+		throw new Error('INVALID_USERNAME');
+	}
+
+	// Validate password
+	if (!Users.validatePassword.ok(password)) {
+		throw new Error('INVALID_PASSWORD');
+	}
+
+	const usersCount = await Users.count({});
+
+	// Generate hash of password
+	const salt = await bcrypt.genSalt(8);
+	const hash = await bcrypt.hash(password, salt);
+
+	// Generate secret
+	const secret = generateUserToken();
+
+	// Check username duplication
+	if (await Users.findOne({ usernameLower: username.toLowerCase(), host: null })) {
+		throw new Error('DUPLICATED_USERNAME');
+	}
+
+	// Check deleted username duplication
+	if (await UsedUsernames.findOne({ username: username.toLowerCase() })) {
+		throw new Error('USED_USERNAME');
+	}
+
+	const keyPair = await new Promise<string[]>((res, rej) =>
+		generateKeyPair('rsa', {
+			modulusLength: 4096,
+			publicKeyEncoding: {
+				type: 'spki',
+				format: 'pem'
+			},
+			privateKeyEncoding: {
+				type: 'pkcs8',
+				format: 'pem',
+				cipher: undefined,
+				passphrase: undefined
+			}
+		} as any, (err, publicKey, privateKey) =>
+			err ? rej(err) : res([publicKey, privateKey])
+		));
+
+	let account!: User;
+
+	// Start transaction
+	await getConnection().transaction(async transactionalEntityManager => {
+		const exist = await transactionalEntityManager.findOne(User, {
+			usernameLower: username.toLowerCase(),
+			host: null
+		});
+
+		if (exist) throw new Error(' the username is already used');
+
+		account = await transactionalEntityManager.save(new User({
+			id: genId(),
+			createdAt: new Date(),
+			username: username,
+			usernameLower: username.toLowerCase(),
+			host: toPunyNullable(host),
+			token: secret,
+			isAdmin: usersCount === 0,
+		}));
+
+		await transactionalEntityManager.save(new UserKeypair({
+			publicKey: keyPair[0],
+			privateKey: keyPair[1],
+			userId: account.id
+		}));
+
+		await transactionalEntityManager.save(new UserProfile({
+			userId: account.id,
+			autoAcceptFollowed: true,
+			autoWatch: false,
+			password: hash,
+		}));
+
+		await transactionalEntityManager.save(new UsedUsername({
+			createdAt: new Date(),
+			username: username.toLowerCase(),
+		}));
+	});
+
+	usersChart.update(account, true);
+
+	return { account, secret };
+}
diff --git a/src/server/api/endpoints/admin/accounts/create.ts b/src/server/api/endpoints/admin/accounts/create.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ac80b579b76127258817725c59aaa9bb55771dd1
--- /dev/null
+++ b/src/server/api/endpoints/admin/accounts/create.ts
@@ -0,0 +1,33 @@
+import define from '../../../define';
+import { Users } from '../../../../../models';
+import { signup } from '../../../common/signup';
+
+export const meta = {
+	tags: ['admin'],
+
+	params: {
+		username: {
+			validator: Users.validateLocalUsername,
+		},
+
+		password: {
+			validator: Users.validatePassword,
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const noUsers = (await Users.count({})) === 0;
+	if (!noUsers && me == null) throw new Error('access denied');
+
+	const { account, secret } = await signup(ps.username, ps.password);
+
+	const res = await Users.pack(account, account, {
+		detail: true,
+		includeSecrets: true
+	});
+
+	(res as any).token = secret;
+
+	return res;
+});
diff --git a/src/server/api/endpoints/admin/announcements/create.ts b/src/server/api/endpoints/admin/announcements/create.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c1d48a7d3857c7c7f00981b23f14b06af52a9da9
--- /dev/null
+++ b/src/server/api/endpoints/admin/announcements/create.ts
@@ -0,0 +1,36 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { Announcements } from '../../../../../models';
+import { genId } from '../../../../../misc/gen-id';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+
+	params: {
+		title: {
+			validator: $.str.min(1)
+		},
+		text: {
+			validator: $.str.min(1)
+		},
+		imageUrl: {
+			validator: $.nullable.str.min(1)
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	const announcement = await Announcements.save({
+		id: genId(),
+		createdAt: new Date(),
+		updatedAt: null,
+		title: ps.title,
+		text: ps.text,
+		imageUrl: ps.imageUrl,
+	});
+
+	return announcement;
+});
diff --git a/src/server/api/endpoints/admin/announcements/delete.ts b/src/server/api/endpoints/admin/announcements/delete.ts
new file mode 100644
index 0000000000000000000000000000000000000000..284b4bf54925a1ff6d3e08df57b6c05c9898bb6a
--- /dev/null
+++ b/src/server/api/endpoints/admin/announcements/delete.ts
@@ -0,0 +1,34 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ID } from '../../../../../misc/cafy-id';
+import { Announcements } from '../../../../../models';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+
+	params: {
+		id: {
+			validator: $.type(ID)
+		}
+	},
+
+	errors: {
+		noSuchAnnouncement: {
+			message: 'No such announcement.',
+			code: 'NO_SUCH_ANNOUNCEMENT',
+			id: 'ecad8040-a276-4e85-bda9-015a708d291e'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const announcement = await Announcements.findOne(ps.id);
+
+	if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);
+
+	await Announcements.delete(announcement.id);
+});
diff --git a/src/server/api/endpoints/admin/announcements/list.ts b/src/server/api/endpoints/admin/announcements/list.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f4e622144e5fa5366c21e8d98d5a3145910fb824
--- /dev/null
+++ b/src/server/api/endpoints/admin/announcements/list.ts
@@ -0,0 +1,41 @@
+import $ from 'cafy';
+import { ID } from '../../../../../misc/cafy-id';
+import define from '../../../define';
+import { Announcements, AnnouncementReads } from '../../../../../models';
+import { makePaginationQuery } from '../../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	}
+};
+
+export default define(meta, async (ps) => {
+	const query = makePaginationQuery(Announcements.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
+
+	const announcements = await query.take(ps.limit!).getMany();
+
+	for (const announcement of announcements) {
+		(announcement as any).reads = await AnnouncementReads.count({
+			announcementId: announcement.id
+		});
+	}
+
+	return announcements;
+});
diff --git a/src/server/api/endpoints/admin/announcements/update.ts b/src/server/api/endpoints/admin/announcements/update.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b65c3a4f93041ef2cd9de697a35eac0a6aeafd89
--- /dev/null
+++ b/src/server/api/endpoints/admin/announcements/update.ts
@@ -0,0 +1,48 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ID } from '../../../../../misc/cafy-id';
+import { Announcements } from '../../../../../models';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+
+	params: {
+		id: {
+			validator: $.type(ID)
+		},
+		title: {
+			validator: $.str.min(1)
+		},
+		text: {
+			validator: $.str.min(1)
+		},
+		imageUrl: {
+			validator: $.nullable.str.min(1)
+		}
+	},
+
+	errors: {
+		noSuchAnnouncement: {
+			message: 'No such announcement.',
+			code: 'NO_SUCH_ANNOUNCEMENT',
+			id: 'd3aae5a7-6372-4cb4-b61c-f511ffc2d7cc'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const announcement = await Announcements.findOne(ps.id);
+
+	if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);
+
+	await Announcements.update(announcement.id, {
+		updatedAt: new Date(),
+		title: ps.title,
+		text: ps.text,
+		imageUrl: ps.imageUrl,
+	});
+});
diff --git a/src/server/api/endpoints/admin/emoji/list-remote.ts b/src/server/api/endpoints/admin/emoji/list-remote.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0a3e74c33322f009e4d10c228ab1e825042a6801
--- /dev/null
+++ b/src/server/api/endpoints/admin/emoji/list-remote.ts
@@ -0,0 +1,62 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { Emojis } from '../../../../../models';
+import { toPuny } from '../../../../../misc/convert-host';
+import { makePaginationQuery } from '../../../common/make-pagination-query';
+import { ID } from '../../../../../misc/cafy-id';
+
+export const meta = {
+	desc: {
+		'ja-JP': 'カスタム絵文字を取得します。'
+	},
+
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+
+	params: {
+		host: {
+			validator: $.optional.nullable.str,
+			default: null as any
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	const q = makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId);
+
+	if (ps.host == null) {
+		q.andWhere(`emoji.host IS NOT NULL`);
+	} else {
+		q.andWhere(`emoji.host = :host`, { host: toPuny(ps.host) });
+	}
+
+	const emojis = await q
+		.orderBy('emoji.category', 'ASC')
+		.orderBy('emoji.name', 'ASC')
+		.take(ps.limit!)
+		.getMany();
+
+	return emojis.map(e => ({
+		id: e.id,
+		name: e.name,
+		category: e.category,
+		aliases: e.aliases,
+		host: e.host,
+		url: e.url
+	}));
+});
diff --git a/src/server/api/endpoints/admin/emoji/list.ts b/src/server/api/endpoints/admin/emoji/list.ts
index d2a5e7df0d61fa1b487177dea01e417cf1703d7b..d525a659c01775375dfeaabdcab3f189681e8359 100644
--- a/src/server/api/endpoints/admin/emoji/list.ts
+++ b/src/server/api/endpoints/admin/emoji/list.ts
@@ -1,7 +1,8 @@
 import $ from 'cafy';
 import define from '../../../define';
 import { Emojis } from '../../../../../models';
-import { toPunyNullable } from '../../../../../misc/convert-host';
+import { makePaginationQuery } from '../../../common/make-pagination-query';
+import { ID } from '../../../../../misc/cafy-id';
 
 export const meta = {
 	desc: {
@@ -14,23 +15,28 @@ export const meta = {
 	requireModerator: true,
 
 	params: {
-		host: {
-			validator: $.optional.nullable.str,
-			default: null as any
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
 		}
 	}
 };
 
 export default define(meta, async (ps) => {
-	const emojis = await Emojis.find({
-		where: {
-			host: toPunyNullable(ps.host)
-		},
-		order: {
-			category: 'ASC',
-			name: 'ASC'
-		}
-	});
+	const emojis = await makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId)
+		.andWhere(`emoji.host IS NULL`)
+		.orderBy('emoji.category', 'ASC')
+		.orderBy('emoji.name', 'ASC')
+		.take(ps.limit!)
+		.getMany();
 
 	return emojis.map(e => ({
 		id: e.id,
diff --git a/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/src/server/api/endpoints/admin/queue/deliver-delayed.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d33837c09925add93d5f5c88c2dec1bf014a8d96
--- /dev/null
+++ b/src/server/api/endpoints/admin/queue/deliver-delayed.ts
@@ -0,0 +1,31 @@
+import define from '../../../define';
+import { deliverQueue } from '../../../../../queue';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+
+	params: {
+	}
+};
+
+export default define(meta, async (ps) => {
+	const jobs = await deliverQueue.getJobs(['delayed']);
+
+	const res = [] as [string, number][];
+
+	for (const job of jobs) {
+		const host = new URL(job.data.to).host;
+		if (res.find(x => x[0] === host)) {
+			res.find(x => x[0] === host)![1]++;
+		} else {
+			res.push([host, 1]);
+		}
+	}
+
+	res.sort((a, b) => b[1] - a[1]);
+
+	return res;
+});
diff --git a/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/src/server/api/endpoints/admin/queue/inbox-delayed.ts
new file mode 100644
index 0000000000000000000000000000000000000000..643e22f10da014017d36a57a112c1ffbf19ca715
--- /dev/null
+++ b/src/server/api/endpoints/admin/queue/inbox-delayed.ts
@@ -0,0 +1,31 @@
+import define from '../../../define';
+import { inboxQueue } from '../../../../../queue';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+
+	params: {
+	}
+};
+
+export default define(meta, async (ps) => {
+	const jobs = await inboxQueue.getJobs(['delayed']);
+
+	const res = [] as [string, number][];
+
+	for (const job of jobs) {
+		const host = new URL(job.data.signature.keyId).host;
+		if (res.find(x => x[0] === host)) {
+			res.find(x => x[0] === host)![1]++;
+		} else {
+			res.push([host, 1]);
+		}
+	}
+
+	res.sort((a, b) => b[1] - a[1]);
+
+	return res;
+});
diff --git a/src/server/api/endpoints/admin/server-info.ts b/src/server/api/endpoints/admin/server-info.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f51040a2c88f4c408aedcc1895ca22754b07813d
--- /dev/null
+++ b/src/server/api/endpoints/admin/server-info.ts
@@ -0,0 +1,45 @@
+import * as os from 'os';
+import * as si from 'systeminformation';
+import { getConnection } from 'typeorm';
+import define from '../../define';
+import redis from '../../../../db/redis';
+
+export const meta = {
+	requireCredential: false,
+
+	desc: {
+	},
+
+	tags: ['meta'],
+
+	params: {
+	},
+};
+
+export default define(meta, async () => {
+	const memStats = await si.mem();
+	const fsStats = await si.fsSize();
+	const netInterface = await si.networkInterfaceDefault();
+
+	return {
+		machine: os.hostname(),
+		os: os.platform(),
+		node: process.version,
+		psql: await getConnection().query('SHOW server_version').then(x => x[0].server_version),
+		redis: redis.server_info.redis_version,
+		cpu: {
+			model: os.cpus()[0].model,
+			cores: os.cpus().length
+		},
+		mem: {
+			total: memStats.total
+		},
+		fs: {
+			total: fsStats[0].size,
+			used: fsStats[0].used,
+		},
+		net: {
+			interface: netInterface
+		}
+	};
+});
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts
index bc37228d0aa0bb33b497e25c51fd66d2d3a5d5c1..65650f12952fb49c329fb92f38172f3a666d595f 100644
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ b/src/server/api/endpoints/admin/update-meta.ts
@@ -13,16 +13,9 @@ export const meta = {
 	tags: ['admin'],
 
 	requireCredential: true,
-	requireModerator: true,
+	requireAdmin: true,
 
 	params: {
-		announcements: {
-			validator: $.optional.nullable.arr($.obj()),
-			desc: {
-				'ja-JP': 'お知らせ'
-			}
-		},
-
 		disableRegistration: {
 			validator: $.optional.nullable.bool,
 			desc: {
@@ -44,13 +37,6 @@ export const meta = {
 			}
 		},
 
-		enableEmojiReaction: {
-			validator: $.optional.nullable.bool,
-			desc: {
-				'ja-JP': '絵文字リアクションを有効にするか否か'
-			}
-		},
-
 		useStarForReactionFallback: {
 			validator: $.optional.nullable.bool,
 			desc: {
@@ -347,7 +333,7 @@ export const meta = {
 			}
 		},
 
-		ToSUrl: {
+		tosUrl: {
 			validator: $.optional.nullable.str,
 			desc: {
 				'ja-JP': '利用規約のURL'
@@ -413,10 +399,6 @@ export const meta = {
 export default define(meta, async (ps, me) => {
 	const set = {} as Partial<Meta>;
 
-	if (ps.announcements) {
-		set.announcements = ps.announcements;
-	}
-
 	if (typeof ps.disableRegistration === 'boolean') {
 		set.disableRegistration = ps.disableRegistration;
 	}
@@ -429,10 +411,6 @@ export default define(meta, async (ps, me) => {
 		set.disableGlobalTimeline = ps.disableGlobalTimeline;
 	}
 
-	if (typeof ps.enableEmojiReaction === 'boolean') {
-		set.enableEmojiReaction = ps.enableEmojiReaction;
-	}
-
 	if (typeof ps.useStarForReactionFallback === 'boolean') {
 		set.useStarForReactionFallback = ps.useStarForReactionFallback;
 	}
@@ -601,8 +579,8 @@ export default define(meta, async (ps, me) => {
 		set.swPrivateKey = ps.swPrivateKey;
 	}
 
-	if (ps.ToSUrl !== undefined) {
-		set.ToSUrl = ps.ToSUrl;
+	if (ps.tosUrl !== undefined) {
+		set.ToSUrl = ps.tosUrl;
 	}
 
 	if (ps.repositoryUrl !== undefined) {
diff --git a/src/server/api/endpoints/announcements.ts b/src/server/api/endpoints/announcements.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c6050d609201dc7231db5924118559ea5c3ba2e8
--- /dev/null
+++ b/src/server/api/endpoints/announcements.ts
@@ -0,0 +1,42 @@
+import $ from 'cafy';
+import { ID } from '../../../misc/cafy-id';
+import define from '../define';
+import { Announcements, AnnouncementReads } from '../../../models';
+import { makePaginationQuery } from '../common/make-pagination-query';
+
+export const meta = {
+	requireCredential: false,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(Announcements.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
+
+	const announcements = await query.take(ps.limit!).getMany();
+
+	if (user) {
+		const reads = (await AnnouncementReads.find({
+			userId: user.id
+		})).map(x => x.announcementId);
+
+		for (const announcement of announcements) {
+			(announcement as any).isRead = reads.includes(announcement.id);
+		}
+	}
+
+	return announcements;
+});
diff --git a/src/server/api/endpoints/antennas/create.ts b/src/server/api/endpoints/antennas/create.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0e00eda1a455d12c683a99d7263337f83f195c3c
--- /dev/null
+++ b/src/server/api/endpoints/antennas/create.ts
@@ -0,0 +1,92 @@
+import $ from 'cafy';
+import define from '../../define';
+import { genId } from '../../../../misc/gen-id';
+import { Antennas, UserLists } from '../../../../models';
+import { ID } from '../../../../misc/cafy-id';
+import { ApiError } from '../../error';
+
+export const meta = {
+	tags: ['antennas'],
+
+	requireCredential: true,
+
+	kind: 'write:account',
+
+	params: {
+		name: {
+			validator: $.str.range(1, 100)
+		},
+
+		src: {
+			validator: $.str.or(['home', 'all', 'users', 'list'])
+		},
+
+		userListId: {
+			validator: $.nullable.optional.type(ID),
+		},
+
+		keywords: {
+			validator: $.arr($.arr($.str))
+		},
+
+		users: {
+			validator: $.arr($.str)
+		},
+
+		caseSensitive: {
+			validator: $.bool
+		},
+
+		withReplies: {
+			validator: $.bool
+		},
+
+		withFile: {
+			validator: $.bool
+		},
+
+		notify: {
+			validator: $.bool
+		}
+	},
+
+	errors: {
+		noSuchUserList: {
+			message: 'No such user list.',
+			code: 'NO_SUCH_USER_LIST',
+			id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	let userList;
+
+	if (ps.src === 'list') {
+		userList = await UserLists.findOne({
+			id: ps.userListId,
+			userId: user.id,
+		});
+	
+		if (userList == null) {
+			throw new ApiError(meta.errors.noSuchUserList);
+		}
+	}
+
+	const antenna = await Antennas.save({
+		id: genId(),
+		createdAt: new Date(),
+		userId: user.id,
+		name: ps.name,
+		src: ps.src,
+		userListId: userList ? userList.id : null,
+		keywords: ps.keywords,
+		users: ps.users,
+		caseSensitive: ps.caseSensitive,
+		withReplies: ps.withReplies,
+		withFile: ps.withFile,
+		notify: ps.notify,
+	});
+
+	return await Antennas.pack(antenna);
+});
diff --git a/src/server/api/endpoints/antennas/delete.ts b/src/server/api/endpoints/antennas/delete.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6bf9165aedfe5803c1904911523f8c1818c87f2c
--- /dev/null
+++ b/src/server/api/endpoints/antennas/delete.ts
@@ -0,0 +1,40 @@
+import $ from 'cafy';
+import { ID } from '../../../../misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Antennas } from '../../../../models';
+
+export const meta = {
+	tags: ['antennas'],
+
+	requireCredential: true,
+
+	kind: 'write:account',
+
+	params: {
+		antennaId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchAntenna: {
+			message: 'No such antenna.',
+			code: 'NO_SUCH_ANTENNA',
+			id: 'b34dcf9d-348f-44bb-99d0-6c9314cfe2df'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const antenna = await Antennas.findOne({
+		id: ps.antennaId,
+		userId: user.id
+	});
+
+	if (antenna == null) {
+		throw new ApiError(meta.errors.noSuchAntenna);
+	}
+
+	await Antennas.delete(antenna.id);
+});
diff --git a/src/server/api/endpoints/antennas/list.ts b/src/server/api/endpoints/antennas/list.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3f9deff32f9dbeb2b71f4e84c5f7372e65e04846
--- /dev/null
+++ b/src/server/api/endpoints/antennas/list.ts
@@ -0,0 +1,18 @@
+import define from '../../define';
+import { Antennas } from '../../../../models';
+
+export const meta = {
+	tags: ['antennas', 'account'],
+
+	requireCredential: true,
+
+	kind: 'read:account',
+};
+
+export default define(meta, async (ps, me) => {
+	const antennas = await Antennas.find({
+		userId: me.id,
+	});
+
+	return await Promise.all(antennas.map(x => Antennas.pack(x)));
+});
diff --git a/src/server/api/endpoints/antennas/notes.ts b/src/server/api/endpoints/antennas/notes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b4c8e7e698b4a8939287b532eb61700b16e6955b
--- /dev/null
+++ b/src/server/api/endpoints/antennas/notes.ts
@@ -0,0 +1,72 @@
+import $ from 'cafy';
+import { ID } from '../../../../misc/cafy-id';
+import define from '../../define';
+import { Antennas, Notes, AntennaNotes } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { ApiError } from '../../error';
+
+export const meta = {
+	tags: ['account', 'notes', 'antennas'],
+
+	requireCredential: true,
+
+	kind: 'read:account',
+
+	params: {
+		antennaId: {
+			validator: $.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchAntenna: {
+			message: 'No such antenna.',
+			code: 'NO_SUCH_ANTENNA',
+			id: '850926e0-fd3b-49b6-b69a-b28a5dbd82fe'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const antenna = await Antennas.findOne({
+		id: ps.antennaId,
+		userId: user.id
+	});
+
+	if (antenna == null) {
+		throw new ApiError(meta.errors.noSuchAntenna);
+	}
+
+	const antennaQuery = AntennaNotes.createQueryBuilder('joining')
+		.select('joining.noteId')
+		.where('joining.antennaId = :antennaId', { antennaId: antenna.id });
+
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.andWhere(`note.id IN (${ antennaQuery.getQuery() })`)
+		.leftJoinAndSelect('note.user', 'user')
+		.setParameters(antennaQuery.getParameters());
+
+	generateVisibilityQuery(query, user);
+	generateMuteQuery(query, user);
+
+	const notes = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Notes.packMany(notes, user);
+});
diff --git a/src/server/api/endpoints/antennas/show.ts b/src/server/api/endpoints/antennas/show.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dd87de1dce2b5fc9a1bb35c5ebe557c4941e01ce
--- /dev/null
+++ b/src/server/api/endpoints/antennas/show.ts
@@ -0,0 +1,41 @@
+import $ from 'cafy';
+import { ID } from '../../../../misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Antennas } from '../../../../models';
+
+export const meta = {
+	tags: ['antennas', 'account'],
+
+	requireCredential: true,
+
+	kind: 'read:account',
+
+	params: {
+		antennaId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchAntenna: {
+			message: 'No such antenna.',
+			code: 'NO_SUCH_ANTENNA',
+			id: 'c06569fb-b025-4f23-b22d-1fcd20d2816b'
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	// Fetch the antenna
+	const antenna = await Antennas.findOne({
+		id: ps.antennaId,
+		userId: me.id,
+	});
+
+	if (antenna == null) {
+		throw new ApiError(meta.errors.noSuchAntenna);
+	}
+
+	return await Antennas.pack(antenna);
+});
diff --git a/src/server/api/endpoints/antennas/update.ts b/src/server/api/endpoints/antennas/update.ts
new file mode 100644
index 0000000000000000000000000000000000000000..28875d0f08492a31799f6f24ea707a2f968d062a
--- /dev/null
+++ b/src/server/api/endpoints/antennas/update.ts
@@ -0,0 +1,108 @@
+import $ from 'cafy';
+import { ID } from '../../../../misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Antennas, UserLists } from '../../../../models';
+
+export const meta = {
+	tags: ['antennas'],
+
+	requireCredential: true,
+
+	kind: 'write:account',
+
+	params: {
+		antennaId: {
+			validator: $.type(ID),
+		},
+
+		name: {
+			validator: $.str.range(1, 100)
+		},
+
+		src: {
+			validator: $.str.or(['home', 'all', 'users', 'list'])
+		},
+
+		userListId: {
+			validator: $.nullable.optional.type(ID),
+		},
+
+		keywords: {
+			validator: $.arr($.arr($.str))
+		},
+
+		users: {
+			validator: $.arr($.str)
+		},
+
+		caseSensitive: {
+			validator: $.bool
+		},
+
+		withReplies: {
+			validator: $.bool
+		},
+
+		withFile: {
+			validator: $.bool
+		},
+
+		notify: {
+			validator: $.bool
+		}
+	},
+
+	errors: {
+		noSuchAntenna: {
+			message: 'No such antenna.',
+			code: 'NO_SUCH_ANTENNA',
+			id: '10c673ac-8852-48eb-aa1f-f5b67f069290'
+		},
+
+		noSuchUserList: {
+			message: 'No such user list.',
+			code: 'NO_SUCH_USER_LIST',
+			id: '1c6b35c9-943e-48c2-81e4-2844989407f7'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Fetch the antenna
+	const antenna = await Antennas.findOne({
+		id: ps.antennaId,
+		userId: user.id
+	});
+
+	if (antenna == null) {
+		throw new ApiError(meta.errors.noSuchAntenna);
+	}
+
+	let userList;
+
+	if (ps.src === 'list') {
+		userList = await UserLists.findOne({
+			id: ps.userListId,
+			userId: user.id,
+		});
+	
+		if (userList == null) {
+			throw new ApiError(meta.errors.noSuchUserList);
+		}
+	}
+
+	await Antennas.update(antenna.id, {
+		name: ps.name,
+		src: ps.src,
+		userListId: userList ? userList.id : null,
+		keywords: ps.keywords,
+		users: ps.users,
+		caseSensitive: ps.caseSensitive,
+		withReplies: ps.withReplies,
+		withFile: ps.withFile,
+		notify: ps.notify,
+	});
+
+	return await Antennas.pack(antenna.id);
+});
diff --git a/src/server/api/endpoints/clips/create.ts b/src/server/api/endpoints/clips/create.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a6761c5533e38c9736f0516178d6694858e61964
--- /dev/null
+++ b/src/server/api/endpoints/clips/create.ts
@@ -0,0 +1,29 @@
+import $ from 'cafy';
+import define from '../../define';
+import { genId } from '../../../../misc/gen-id';
+import { Clips } from '../../../../models';
+
+export const meta = {
+	tags: ['clips'],
+
+	requireCredential: true,
+
+	kind: 'write:account',
+
+	params: {
+		name: {
+			validator: $.str.range(1, 100)
+		}
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const clip = await Clips.save({
+		id: genId(),
+		createdAt: new Date(),
+		userId: user.id,
+		name: ps.name,
+	});
+
+	return await Clips.pack(clip);
+});
diff --git a/src/server/api/endpoints/clips/delete.ts b/src/server/api/endpoints/clips/delete.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7e185e4652da40822c4b06fa67174b75d0f6f4ac
--- /dev/null
+++ b/src/server/api/endpoints/clips/delete.ts
@@ -0,0 +1,40 @@
+import $ from 'cafy';
+import { ID } from '../../../../misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Clips } from '../../../../models';
+
+export const meta = {
+	tags: ['clips'],
+
+	requireCredential: true,
+
+	kind: 'write:account',
+
+	params: {
+		clipId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchClip: {
+			message: 'No such clip.',
+			code: 'NO_SUCH_CLIP',
+			id: '70ca08ba-6865-4630-b6fb-8494759aa754'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const clip = await Clips.findOne({
+		id: ps.clipId,
+		userId: user.id
+	});
+
+	if (clip == null) {
+		throw new ApiError(meta.errors.noSuchClip);
+	}
+
+	await Clips.delete(clip.id);
+});
diff --git a/src/server/api/endpoints/clips/list.ts b/src/server/api/endpoints/clips/list.ts
new file mode 100644
index 0000000000000000000000000000000000000000..aa16a18d42cc630eaf44521e96819bfbc66dc659
--- /dev/null
+++ b/src/server/api/endpoints/clips/list.ts
@@ -0,0 +1,18 @@
+import define from '../../define';
+import { Clips } from '../../../../models';
+
+export const meta = {
+	tags: ['clips', 'account'],
+
+	requireCredential: true,
+
+	kind: 'read:account',
+};
+
+export default define(meta, async (ps, me) => {
+	const clips = await Clips.find({
+		userId: me.id,
+	});
+
+	return await Promise.all(clips.map(x => Clips.pack(x)));
+});
diff --git a/src/server/api/endpoints/clips/notes.ts b/src/server/api/endpoints/clips/notes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4e76a4d1f364c0d4b560ba3d5521bf602e60a69a
--- /dev/null
+++ b/src/server/api/endpoints/clips/notes.ts
@@ -0,0 +1,67 @@
+import $ from 'cafy';
+import { ID } from '../../../../misc/cafy-id';
+import define from '../../define';
+import { Clips, Notes } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+
+export const meta = {
+	tags: ['account', 'notes', 'clips'],
+
+	requireCredential: true,
+
+	kind: 'read:account',
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchClip: {
+			message: 'No such list.',
+			code: 'NO_SUCH_CLIP',
+			id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const clip = await Clips.findOne({
+		id: ps.clipId,
+		userId: user.id
+	});
+
+	if (clip == null) {
+		throw new ApiError(meta.errors.noSuchClip);
+	}
+
+	const clipQuery = ClipNotes.createQueryBuilder('joining')
+		.select('joining.noteId')
+		.where('joining.clipId = :clipId', { clipId: clip.id });
+
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.andWhere(`note.id IN (${ clipQuery.getQuery() })`)
+		.leftJoinAndSelect('note.user', 'user')
+		.setParameters(clipQuery.getParameters());
+
+	generateVisibilityQuery(query, user);
+	generateMuteQuery(query, user);
+
+	const notes = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Notes.packMany(notes, user);
+});
diff --git a/src/server/api/endpoints/clips/show.ts b/src/server/api/endpoints/clips/show.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0766b3e929fabd37bc4747f835536816739f3e66
--- /dev/null
+++ b/src/server/api/endpoints/clips/show.ts
@@ -0,0 +1,41 @@
+import $ from 'cafy';
+import { ID } from '../../../../misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Clips } from '../../../../models';
+
+export const meta = {
+	tags: ['clips', 'account'],
+
+	requireCredential: true,
+
+	kind: 'read:account',
+
+	params: {
+		clipId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchClip: {
+			message: 'No such clip.',
+			code: 'NO_SUCH_CLIP',
+			id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20'
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	// Fetch the clip
+	const clip = await Clips.findOne({
+		id: ps.clipId,
+		userId: me.id,
+	});
+
+	if (clip == null) {
+		throw new ApiError(meta.errors.noSuchClip);
+	}
+
+	return await Clips.pack(clip);
+});
diff --git a/src/server/api/endpoints/clips/update.ts b/src/server/api/endpoints/clips/update.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d1c31eb8e69172bf270675178d73d5c5b3d3efb7
--- /dev/null
+++ b/src/server/api/endpoints/clips/update.ts
@@ -0,0 +1,49 @@
+import $ from 'cafy';
+import { ID } from '../../../../misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Clips } from '../../../../models';
+
+export const meta = {
+	tags: ['clips'],
+
+	requireCredential: true,
+
+	kind: 'write:account',
+
+	params: {
+		clipId: {
+			validator: $.type(ID),
+		},
+
+		name: {
+			validator: $.str.range(1, 100),
+		}
+	},
+
+	errors: {
+		noSuchClip: {
+			message: 'No such clip.',
+			code: 'NO_SUCH_CLIP',
+			id: 'b4d92d70-b216-46fa-9a3f-a8c811699257'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Fetch the clip
+	const clip = await Clips.findOne({
+		id: ps.clipId,
+		userId: user.id
+	});
+
+	if (clip == null) {
+		throw new ApiError(meta.errors.noSuchClip);
+	}
+
+	await Clips.update(clip.id, {
+		name: ps.name
+	});
+
+	return await Clips.pack(clip.id);
+});
diff --git a/src/server/api/endpoints/federation/followers.ts b/src/server/api/endpoints/federation/followers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d885daf70e006e9c2b8d858794bae5ba94390a81
--- /dev/null
+++ b/src/server/api/endpoints/federation/followers.ts
@@ -0,0 +1,51 @@
+import $ from 'cafy';
+import { ID } from '../../../../misc/cafy-id';
+import define from '../../define';
+import { Followings } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: false,
+
+	params: {
+		host: {
+			validator: $.str
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Following',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
+		.andWhere(`following.followeeHost = :host`, { host: ps.host });
+
+	const followings = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Followings.packMany(followings, me, { populateFollowee: true });
+});
diff --git a/src/server/api/endpoints/federation/following.ts b/src/server/api/endpoints/federation/following.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1f7981731826e0380cbe3a94dd805f5bcf92e0eb
--- /dev/null
+++ b/src/server/api/endpoints/federation/following.ts
@@ -0,0 +1,51 @@
+import $ from 'cafy';
+import { ID } from '../../../../misc/cafy-id';
+import define from '../../define';
+import { Followings } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: false,
+
+	params: {
+		host: {
+			validator: $.str
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Following',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
+		.andWhere(`following.followerHost = :host`, { host: ps.host });
+
+	const followings = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Followings.packMany(followings, me, { populateFollowee: true });
+});
diff --git a/src/server/api/endpoints/federation/instances.ts b/src/server/api/endpoints/federation/instances.ts
index bc0eb9a1d700a5c5e00a906759965e53d0805046..002cfd4335e78d2309d74e15d583eedd7f7e9394 100644
--- a/src/server/api/endpoints/federation/instances.ts
+++ b/src/server/api/endpoints/federation/instances.ts
@@ -9,6 +9,10 @@ export const meta = {
 	requireCredential: false,
 
 	params: {
+		host: {
+			validator: $.optional.nullable.str,
+		},
+
 		blocked: {
 			validator: $.optional.nullable.bool,
 		},
@@ -17,7 +21,19 @@ export const meta = {
 			validator: $.optional.nullable.bool,
 		},
 
-		markedAsClosed: {
+		suspended: {
+			validator: $.optional.nullable.bool,
+		},
+
+		federating: {
+			validator: $.optional.nullable.bool,
+		},
+
+		subscribing: {
+			validator: $.optional.nullable.bool,
+		},
+
+		publishing: {
 			validator: $.optional.nullable.bool,
 		},
 
@@ -41,6 +57,8 @@ export default define(meta, async (ps, me) => {
 	const query = Instances.createQueryBuilder('instance');
 
 	switch (ps.sort) {
+		case '+pubSub': query.orderBy('instance.followingCount', 'DESC').orderBy('instance.followersCount', 'DESC'); break;
+		case '-pubSub': query.orderBy('instance.followingCount', 'ASC').orderBy('instance.followersCount', 'ASC'); break;
 		case '+notes': query.orderBy('instance.notesCount', 'DESC'); break;
 		case '-notes': query.orderBy('instance.notesCount', 'ASC'); break;
 		case '+users': query.orderBy('instance.usersCount', 'DESC'); break;
@@ -78,14 +96,42 @@ export default define(meta, async (ps, me) => {
 		}
 	}
 
-	if (typeof ps.markedAsClosed === 'boolean') {
-		if (ps.markedAsClosed) {
-			query.andWhere('instance.isMarkedAsClosed = TRUE');
+	if (typeof ps.suspended === 'boolean') {
+		if (ps.suspended) {
+			query.andWhere('instance.isSuspended = TRUE');
 		} else {
-			query.andWhere('instance.isMarkedAsClosed = FALSE');
+			query.andWhere('instance.isSuspended = FALSE');
 		}
 	}
 
+	if (typeof ps.federating === 'boolean') {
+		if (ps.federating) {
+			query.andWhere('((instance.followingCount > 0) OR (instance.followersCount > 0))');
+		} else {
+			query.andWhere('((instance.followingCount = 0) AND (instance.followersCount = 0))');
+		}
+	}
+
+	if (typeof ps.subscribing === 'boolean') {
+		if (ps.subscribing) {
+			query.andWhere('instance.followersCount > 0');
+		} else {
+			query.andWhere('instance.followersCount = 0');
+		}
+	}
+
+	if (typeof ps.publishing === 'boolean') {
+		if (ps.publishing) {
+			query.andWhere('instance.followingCount > 0');
+		} else {
+			query.andWhere('instance.followingCount = 0');
+		}
+	}
+
+	if (ps.host) {
+		query.andWhere('instance.host like :host', { host: '%' + ps.host.toLowerCase() + '%' })
+	}
+
 	const instances = await query.take(ps.limit!).skip(ps.offset).getMany();
 
 	return instances;
diff --git a/src/server/api/endpoints/federation/users.ts b/src/server/api/endpoints/federation/users.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f69bbf949c9a572ac495533fb33e1cf461bb8f55
--- /dev/null
+++ b/src/server/api/endpoints/federation/users.ts
@@ -0,0 +1,51 @@
+import $ from 'cafy';
+import { ID } from '../../../../misc/cafy-id';
+import define from '../../define';
+import { Users } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: false,
+
+	params: {
+		host: {
+			validator: $.str
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = makePaginationQuery(Users.createQueryBuilder('user'), ps.sinceId, ps.untilId)
+		.andWhere(`user.host = :host`, { host: ps.host });
+
+	const users = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Users.packMany(users, me, { detail: true });
+});
diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts
index cd00501a2ed6c8adabad8916924d50bf4a1605d2..f624550d493fb89aab2ae48ac20e776ff1ee9738 100644
--- a/src/server/api/endpoints/i/notifications.ts
+++ b/src/server/api/endpoints/i/notifications.ts
@@ -42,12 +42,12 @@ export const meta = {
 		},
 
 		includeTypes: {
-			validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'])),
+			validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted'])),
 			default: [] as string[]
 		},
 
 		excludeTypes: {
-			validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'])),
+			validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted'])),
 			default: [] as string[]
 		}
 	},
diff --git a/src/server/api/endpoints/i/read-announcement.ts b/src/server/api/endpoints/i/read-announcement.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c5fbe7d576da76dddc73a35d1afa8e6ced942e4d
--- /dev/null
+++ b/src/server/api/endpoints/i/read-announcement.ts
@@ -0,0 +1,60 @@
+import $ from 'cafy';
+import { ID } from '../../../../misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { genId } from '../../../../misc/gen-id';
+import { AnnouncementReads, Announcements, Users } from '../../../../models';
+import { publishMainStream } from '../../../../services/stream';
+
+export const meta = {
+	tags: ['account'],
+
+	requireCredential: true,
+
+	kind: 'write:account',
+
+	params: {
+		announcementId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchAnnouncement: {
+			message: 'No such announcement.',
+			code: 'NO_SUCH_ANNOUNCEMENT',
+			id: '184663db-df88-4bc2-8b52-fb85f0681939'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Check if announcement exists
+	const announcement = await Announcements.findOne(ps.announcementId);
+
+	if (announcement == null) {
+		throw new ApiError(meta.errors.noSuchAnnouncement);
+	}
+
+	// Check if already read
+	const read = await AnnouncementReads.findOne({
+		announcementId: ps.announcementId,
+		userId: user.id
+	});
+
+	if (read != null) {
+		return;
+	}
+
+	// Create read
+	await AnnouncementReads.save({
+		id: genId(),
+		createdAt: new Date(),
+		announcementId: ps.announcementId,
+		userId: user.id,
+	});
+
+	if (!await Users.getHasUnreadAnnouncement(user.id)) {
+		publishMainStream(user.id, 'readAllAnnouncements');
+	}
+});
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index b71c35946e51f923d24f993da8d73d9f5834c1ab..2c605a6f0ba85d739b4be746f9533b5d85ee316a 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -1,11 +1,8 @@
 import $ from 'cafy';
-import * as os from 'os';
 import config from '../../../config';
 import define from '../define';
 import { fetchMeta } from '../../../misc/fetch-meta';
-import { Emojis } from '../../../models';
-import { getConnection } from 'typeorm';
-import redis from '../../../db/redis';
+import { Emojis, Users } from '../../../models';
 import { DB_MAX_NOTE_TEXT_LENGTH } from '../../../misc/hard-limits';
 
 export const meta = {
@@ -83,11 +80,6 @@ export const meta = {
 				optional: false as const, nullable: false as const,
 				description: 'Whether disabled GTL.',
 			},
-			enableEmojiReaction: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
-				description: 'Whether enabled emoji reaction.',
-			},
 		}
 	}
 };
@@ -119,27 +111,15 @@ export default define(meta, async (ps, me) => {
 		uri: config.url,
 		description: instance.description,
 		langs: instance.langs,
-		ToSUrl: instance.ToSUrl,
+		tosUrl: instance.ToSUrl,
 		repositoryUrl: instance.repositoryUrl,
 		feedbackUrl: instance.feedbackUrl,
 
 		secure: config.https != null,
-		machine: os.hostname(),
-		os: os.platform(),
-		node: process.version,
-		psql: await getConnection().query('SHOW server_version').then(x => x[0].server_version),
-		redis: redis.server_info.redis_version,
-
-		cpu: {
-			model: os.cpus()[0].model,
-			cores: os.cpus().length
-		},
 
-		announcements: instance.announcements || [],
 		disableRegistration: instance.disableRegistration,
 		disableLocalTimeline: instance.disableLocalTimeline,
 		disableGlobalTimeline: instance.disableGlobalTimeline,
-		enableEmojiReaction: instance.enableEmojiReaction,
 		driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
 		driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
 		cacheRemoteFiles: instance.cacheRemoteFiles,
@@ -159,6 +139,7 @@ export default define(meta, async (ps, me) => {
 			category: e.category,
 			url: e.url,
 		})),
+		requireSetup: (await Users.count({})) === 0,
 		enableEmail: instance.enableEmail,
 
 		enableTwitterIntegration: instance.enableTwitterIntegration,
@@ -183,7 +164,7 @@ export default define(meta, async (ps, me) => {
 		};
 	}
 
-	if (me && (me.isAdmin || me.isModerator)) {
+	if (me && me.isAdmin) {
 		response.useStarForReactionFallback = instance.useStarForReactionFallback;
 		response.pinnedUsers = instance.pinnedUsers;
 		response.hiddenTags = instance.hiddenTags;
diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts
index 810ad51b6718f3489b5d408a00352f1b879f8abe..73db73ed97d40d9fa76d3f98a618760f40b589e9 100644
--- a/src/server/api/endpoints/notes/create.ts
+++ b/src/server/api/endpoints/notes/create.ts
@@ -113,23 +113,6 @@ export const meta = {
 			}
 		},
 
-		geo: {
-			validator: $.optional.nullable.obj({
-				coordinates: $.arr().length(2)
-					.item(0, $.num.range(-180, 180))
-					.item(1, $.num.range(-90, 90)),
-				altitude: $.nullable.num,
-				accuracy: $.nullable.num,
-				altitudeAccuracy: $.nullable.num,
-				heading: $.nullable.num.range(0, 360),
-				speed: $.nullable.num
-			}).strict(),
-			desc: {
-				'ja-JP': '位置情報'
-			},
-			ref: 'geo'
-		},
-
 		fileIds: {
 			validator: $.optional.arr($.type(ID)).unique().range(1, 4),
 			desc: {
@@ -308,7 +291,6 @@ export default define(meta, async (ps, user, app) => {
 		apMentions: ps.noExtractMentions ? [] : undefined,
 		apHashtags: ps.noExtractHashtags ? [] : undefined,
 		apEmojis: ps.noExtractEmojis ? [] : undefined,
-		geo: ps.geo
 	});
 
 	return {
diff --git a/src/server/api/endpoints/notes/featured.ts b/src/server/api/endpoints/notes/featured.ts
index 0a1d8668b0c4d98edb5c9a2660a72811b42fd8d3..a499afabf0e3e5875eb28efb97970b3e28352ce4 100644
--- a/src/server/api/endpoints/notes/featured.ts
+++ b/src/server/api/endpoints/notes/featured.ts
@@ -15,12 +15,17 @@ export const meta = {
 
 	params: {
 		limit: {
-			validator: $.optional.num.range(1, 30),
+			validator: $.optional.num.range(1, 100),
 			default: 10,
 			desc: {
 				'ja-JP': '最大数'
 			}
-		}
+		},
+
+		offset: {
+			validator: $.optional.num.min(0),
+			default: 0
+		},
 	},
 
 	res: {
@@ -35,6 +40,7 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
+	const max = 30;
 	const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで
 
 	const query = Notes.createQueryBuilder('note')
@@ -46,7 +52,14 @@ export default define(meta, async (ps, user) => {
 
 	if (user) generateMuteQuery(query, user);
 
-	const notes = await query.orderBy('note.score', 'DESC').take(ps.limit!).getMany();
+	let notes = await query
+		.orderBy('note.score', 'DESC')
+		.take(max)
+		.getMany();
+
+	notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
+
+	notes = notes.slice(ps.offset, ps.offset + ps.limit);
 
 	return await Notes.packMany(notes, user);
 });
diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts
index 5557b469e408dffaccfef9f09ac79344d6bed3b1..efc08d0d4a403b9071509ec4caea84ce37e0c0f5 100644
--- a/src/server/api/endpoints/notes/search.ts
+++ b/src/server/api/endpoints/notes/search.ts
@@ -1,11 +1,13 @@
 import $ from 'cafy';
 import es from '../../../../db/elasticsearch';
 import define from '../../define';
-import { ApiError } from '../../error';
 import { Notes } from '../../../../models';
 import { In } from 'typeorm';
 import { ID } from '../../../../misc/cafy-id';
 import config from '../../../../config';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMuteQuery } from '../../common/generate-mute-query';
 
 export const meta = {
 	desc: {
@@ -22,16 +24,19 @@ export const meta = {
 			validator: $.str
 		},
 
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
 		limit: {
 			validator: $.optional.num.range(1, 100),
 			default: 10
 		},
 
-		offset: {
-			validator: $.optional.num.min(0),
-			default: 0
-		},
-
 		host: {
 			validator: $.optional.nullable.str,
 			default: undefined
@@ -54,74 +59,80 @@ export const meta = {
 	},
 
 	errors: {
-		searchingNotAvailable: {
-			message: 'Searching not available.',
-			code: 'SEARCHING_NOT_AVAILABLE',
-			id: '7ee9c119-16a1-479f-a6fd-6fab00ed946f'
-		}
 	}
 };
 
 export default define(meta, async (ps, me) => {
-	if (es == null) throw new ApiError(meta.errors.searchingNotAvailable);
+	if (es == null) {
+		const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+			.andWhere('note.text ILIKE :q', { q: `%${ps.query}%` })
+			.leftJoinAndSelect('note.user', 'user');
 
-	const userQuery = ps.userId != null ? [{
-		term: {
-			userId: ps.userId
-		}
-	}] : [];
-
-	const hostQuery = ps.userId == null ?
-		ps.host === null ? [{
-			bool: {
-				must_not: {
-					exists: {
-						field: 'userHost'
-					}
-				}
-			}
-		}] : ps.host !== undefined ? [{
+		generateVisibilityQuery(query, me);
+		if (me) generateMuteQuery(query, me);
+
+		const notes = await query.take(ps.limit!).getMany();
+
+		return await Notes.packMany(notes, me);
+	} else {
+		const userQuery = ps.userId != null ? [{
 			term: {
-				userHost: ps.host
+				userId: ps.userId
 			}
-		}] : []
-	: [];
-
-	const result = await es.search({
-		index: config.elasticsearch.index || 'misskey_note',
-		body: {
-			size: ps.limit!,
-			from: ps.offset,
-			query: {
+		}] : [];
+
+		const hostQuery = ps.userId == null ?
+			ps.host === null ? [{
 				bool: {
-					must: [{
-						simple_query_string: {
-							fields: ['text'],
-							query: ps.query.toLowerCase(),
-							default_operator: 'and'
-						},
-					}, ...hostQuery, ...userQuery]
+					must_not: {
+						exists: {
+							field: 'userHost'
+						}
+					}
 				}
-			},
-			sort: [{
-				_doc: 'desc'
-			}]
-		}
-	});
+			}] : ps.host !== undefined ? [{
+				term: {
+					userHost: ps.host
+				}
+			}] : []
+		: [];
+
+		const result = await es.search({
+			index: config.elasticsearch.index || 'misskey_note',
+			body: {
+				size: ps.limit!,
+				from: ps.offset,
+				query: {
+					bool: {
+						must: [{
+							simple_query_string: {
+								fields: ['text'],
+								query: ps.query.toLowerCase(),
+								default_operator: 'and'
+							},
+						}, ...hostQuery, ...userQuery]
+					}
+				},
+				sort: [{
+					_doc: 'desc'
+				}]
+			}
+		});
 
-	const hits = result.body.hits.hits.map((hit: any) => hit._id);
+		const hits = result.body.hits.hits.map((hit: any) => hit._id);
 
-	if (hits.length === 0) return [];
+		if (hits.length === 0) return [];
 
-	// Fetch found notes
-	const notes = await Notes.find({
-		where: {
-			id: In(hits)
-		},
-		order: {
-			id: -1
-		}
-	});
+		// Fetch found notes
+		const notes = await Notes.find({
+			where: {
+				id: In(hits)
+			},
+			order: {
+				id: -1
+			}
+		});
 
-	return await Notes.packMany(notes, me);
+		return await Notes.packMany(notes, me);
+	}
 });
diff --git a/src/server/api/endpoints/users/search-by-username-and-host.ts b/src/server/api/endpoints/users/search-by-username-and-host.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8544731dfd2777d24931d8a7367ca816e6988d49
--- /dev/null
+++ b/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -0,0 +1,101 @@
+import $ from 'cafy';
+import define from '../../define';
+import { Users } from '../../../../models';
+import { User } from '../../../../models/entities/user';
+
+export const meta = {
+	desc: {
+		'ja-JP': 'ユーザーを検索します。'
+	},
+
+	tags: ['users'],
+
+	requireCredential: false,
+
+	params: {
+		username: {
+			validator: $.optional.nullable.str,
+			desc: {
+				'ja-JP': 'クエリ'
+			}
+		},
+
+		host: {
+			validator: $.optional.nullable.str,
+			desc: {
+				'ja-JP': 'クエリ'
+			}
+		},
+
+		offset: {
+			validator: $.optional.num.min(0),
+			default: 0,
+			desc: {
+				'ja-JP': 'オフセット'
+			}
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10,
+			desc: {
+				'ja-JP': '取得する数'
+			}
+		},
+
+		detail: {
+			validator: $.optional.bool,
+			default: true,
+			desc: {
+				'ja-JP': '詳細なユーザー情報を含めるか否か'
+			}
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	if (ps.host) {
+		const q = Users.createQueryBuilder('user')
+			.where('user.isSuspended = FALSE')
+			.andWhere('user.host LIKE :host', { host: ps.host.toLowerCase() + '%' });
+
+		if (ps.username) {
+			q.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' })
+		}
+
+		const users = await q.take(ps.limit!).skip(ps.offset).getMany();
+
+		return await Users.packMany(users, me, { detail: ps.detail });
+	} else {
+		let users = await Users.createQueryBuilder('user')
+			.where('user.host IS NULL')
+			.andWhere('user.isSuspended = FALSE')
+			.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' })
+			.take(ps.limit!)
+			.skip(ps.offset)
+			.getMany();
+
+		if (users.length < ps.limit!) {
+			const otherUsers = await Users.createQueryBuilder('user')
+				.where('user.host IS NOT NULL')
+				.andWhere('user.isSuspended = FALSE')
+				.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' })
+				.take(ps.limit! - users.length)
+				.getMany();
+
+			users = users.concat(otherUsers);
+		}
+
+		return await Users.packMany(users, me, { detail: ps.detail });
+	}
+});
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index af1aefda84c68b9ae07914254dc8bde31a8fcdb4..79ee74389c312ef63316f7f56198342245929321 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -1,19 +1,8 @@
 import * as Koa from 'koa';
-import * as bcrypt from 'bcryptjs';
-import { generateKeyPair } from 'crypto';
-import generateUserToken from '../common/generate-native-user-token';
-import config from '../../../config';
 import { fetchMeta } from '../../../misc/fetch-meta';
 import * as recaptcha from 'recaptcha-promise';
-import { Users, Signins, RegistrationTickets, UsedUsernames } from '../../../models';
-import { genId } from '../../../misc/gen-id';
-import { usersChart } from '../../../services/chart';
-import { User } from '../../../models/entities/user';
-import { UserKeypair } from '../../../models/entities/user-keypair';
-import { toPunyNullable } from '../../../misc/convert-host';
-import { UserProfile } from '../../../models/entities/user-profile';
-import { getConnection } from 'typeorm';
-import { UsedUsername } from '../../../models/entities/used-username';
+import { Users, RegistrationTickets } from '../../../models';
+import { signup } from '../common/signup';
 
 export default async (ctx: Koa.Context) => {
 	const body = ctx.request.body;
@@ -31,7 +20,6 @@ export default async (ctx: Koa.Context) => {
 
 		if (!success) {
 			ctx.throw(400, 'recaptcha-failed');
-			return;
 		}
 	}
 
@@ -58,114 +46,18 @@ export default async (ctx: Koa.Context) => {
 		RegistrationTickets.delete(ticket.id);
 	}
 
-	// Validate username
-	if (!Users.validateLocalUsername.ok(username)) {
-		ctx.status = 400;
-		return;
-	}
-
-	// Validate password
-	if (!Users.validatePassword.ok(password)) {
-		ctx.status = 400;
-		return;
-	}
-
-	const usersCount = await Users.count({});
-
-	// Generate hash of password
-	const salt = await bcrypt.genSalt(8);
-	const hash = await bcrypt.hash(password, salt);
-
-	// Generate secret
-	const secret = generateUserToken();
-
-	// Check username duplication
-	if (await Users.findOne({ usernameLower: username.toLowerCase(), host: null })) {
-		ctx.status = 400;
-		return;
-	}
-
-	// Check deleted username duplication
-	if (await UsedUsernames.findOne({ username: username.toLowerCase() })) {
-		ctx.status = 400;
-		return;
-	}
-
-	const keyPair = await new Promise<string[]>((res, rej) =>
-		generateKeyPair('rsa', {
-			modulusLength: 4096,
-			publicKeyEncoding: {
-				type: 'spki',
-				format: 'pem'
-			},
-			privateKeyEncoding: {
-				type: 'pkcs8',
-				format: 'pem',
-				cipher: undefined,
-				passphrase: undefined
-			}
-		} as any, (err, publicKey, privateKey) =>
-			err ? rej(err) : res([publicKey, privateKey])
-		));
-
-	let account!: User;
+	try {
+		const { account, secret } = await signup(username, password, host);
 
-	// Start transaction
-	await getConnection().transaction(async transactionalEntityManager => {
-		const exist = await transactionalEntityManager.findOne(User, {
-			usernameLower: username.toLowerCase(),
-			host: null
+		const res = await Users.pack(account, account, {
+			detail: true,
+			includeSecrets: true
 		});
 
-		if (exist) throw new Error(' the username is already used');
-
-		account = await transactionalEntityManager.save(new User({
-			id: genId(),
-			createdAt: new Date(),
-			username: username,
-			usernameLower: username.toLowerCase(),
-			host: toPunyNullable(host),
-			token: secret,
-			isAdmin: config.autoAdmin && usersCount === 0,
-		}));
-
-		await transactionalEntityManager.save(new UserKeypair({
-			publicKey: keyPair[0],
-			privateKey: keyPair[1],
-			userId: account.id
-		}));
-
-		await transactionalEntityManager.save(new UserProfile({
-			userId: account.id,
-			autoAcceptFollowed: true,
-			autoWatch: false,
-			password: hash,
-		}));
-
-		await transactionalEntityManager.save(new UsedUsername({
-			createdAt: new Date(),
-			username: username.toLowerCase(),
-		}));
-	});
+		(res as any).token = secret;
 
-	usersChart.update(account, true);
-
-	// Append signin history
-	await Signins.save({
-		id: genId(),
-		createdAt: new Date(),
-		userId: account.id,
-		ip: ctx.ip,
-		headers: ctx.headers,
-		success: true
-	});
-
-	const res = await Users.pack(account, account, {
-		detail: true,
-		includeSecrets: true
-	});
-
-	(res as any).token = secret;
-
-	ctx.body = res;
+		ctx.body = res;
+	} catch (e) {
+		ctx.throw(400, e);
+	}
 };
diff --git a/src/server/api/stream/channels/antenna.ts b/src/server/api/stream/channels/antenna.ts
new file mode 100644
index 0000000000000000000000000000000000000000..714edb502d7cceca04250a7109c9bfdc36d66338
--- /dev/null
+++ b/src/server/api/stream/channels/antenna.ts
@@ -0,0 +1,41 @@
+import autobind from 'autobind-decorator';
+import Channel from '../channel';
+import { Notes } from '../../../../models';
+import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
+
+export default class extends Channel {
+	public readonly chName = 'antenna';
+	public static shouldShare = false;
+	public static requireCredential = false;
+	private antennaId: string;
+
+	@autobind
+	public async init(params: any) {
+		this.antennaId = params.antennaId as string;
+
+		// Subscribe stream
+		this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent);
+	}
+
+	@autobind
+	private async onEvent(data: any) {
+		const { type, body } = data;
+
+		if (type === 'note') {
+			const note = await Notes.pack(body.id, this.user, { detail: true });
+
+			// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+			if (shouldMuteThisNote(note, this.muting)) return;
+
+			this.send('note', note);
+		} else {
+			this.send(type, body);
+		}
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off(`antennaStream:${this.antennaId}`, this.onEvent);
+	}
+}
diff --git a/src/server/api/stream/channels/ap-log.ts b/src/server/api/stream/channels/ap-log.ts
deleted file mode 100644
index 867fd3670b9e27398a47346b80fa84043868dc92..0000000000000000000000000000000000000000
--- a/src/server/api/stream/channels/ap-log.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import autobind from 'autobind-decorator';
-import Channel from '../channel';
-
-export default class extends Channel {
-	public readonly chName = 'apLog';
-	public static shouldShare = true;
-	public static requireCredential = false;
-
-	@autobind
-	public async init(params: any) {
-		// Subscribe events
-		this.subscriber.on('apLog', this.onLog);
-	}
-
-	@autobind
-	private async onLog(log: any) {
-		this.send('log', log);
-	}
-
-	@autobind
-	public dispose() {
-		// Unsubscribe events
-		this.subscriber.off('apLog', this.onLog);
-	}
-}
diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts
index b9feb702580de3f7a2909514c3f1b8ba3dbb9863..e32f4111c29668e9f1e32b9e44993408edf85140 100644
--- a/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/src/server/api/stream/channels/hybrid-timeline.ts
@@ -50,7 +50,7 @@ export default class extends Channel {
 					detail: true
 				});
 			}
-	}
+		}
 
 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 		if (shouldMuteThisNote(note, this.muting)) return;
diff --git a/src/server/api/stream/channels/index.ts b/src/server/api/stream/channels/index.ts
index 4527fb1e46dd0ecfb456b892a193e4a9096aad83..6efad078c6712c2e1177618fd40f148558d36c02 100644
--- a/src/server/api/stream/channels/index.ts
+++ b/src/server/api/stream/channels/index.ts
@@ -3,15 +3,14 @@ import homeTimeline from './home-timeline';
 import localTimeline from './local-timeline';
 import hybridTimeline from './hybrid-timeline';
 import globalTimeline from './global-timeline';
-import notesStats from './notes-stats';
 import serverStats from './server-stats';
 import queueStats from './queue-stats';
 import userList from './user-list';
+import antenna from './antenna';
 import messaging from './messaging';
 import messagingIndex from './messaging-index';
 import drive from './drive';
 import hashtag from './hashtag';
-import apLog from './ap-log';
 import admin from './admin';
 import gamesReversi from './games/reversi';
 import gamesReversiGame from './games/reversi-game';
@@ -22,15 +21,14 @@ export default {
 	localTimeline,
 	hybridTimeline,
 	globalTimeline,
-	notesStats,
 	serverStats,
 	queueStats,
 	userList,
+	antenna,
 	messaging,
 	messagingIndex,
 	drive,
 	hashtag,
-	apLog,
 	admin,
 	gamesReversi,
 	gamesReversiGame
diff --git a/src/server/api/stream/channels/notes-stats.ts b/src/server/api/stream/channels/notes-stats.ts
deleted file mode 100644
index 0c6b84d6cf7c4d51c6b166a3f698e3c56dbabc53..0000000000000000000000000000000000000000
--- a/src/server/api/stream/channels/notes-stats.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import autobind from 'autobind-decorator';
-import Xev from 'xev';
-import Channel from '../channel';
-
-const ev = new Xev();
-
-export default class extends Channel {
-	public readonly chName = 'notesStats';
-	public static shouldShare = true;
-	public static requireCredential = false;
-
-	@autobind
-	public async init(params: any) {
-		ev.addListener('notesStats', this.onStats);
-	}
-
-	@autobind
-	private onStats(stats: any) {
-		this.send('stats', stats);
-	}
-
-	@autobind
-	public onMessage(type: string, body: any) {
-		switch (type) {
-			case 'requestLog':
-				ev.once(`notesStatsLog:${body.id}`, statsLog => {
-					this.send('statsLog', statsLog);
-				});
-				ev.emit('requestNotesStatsLog', body.id);
-				break;
-		}
-	}
-
-	@autobind
-	public dispose() {
-		ev.removeListener('notesStats', this.onStats);
-	}
-}
diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts
index f73f3229d5081ab8d292c76d431659de027ac7d3..6ec644a024cd9aaec0db879b59964fc8b5f57236 100644
--- a/src/server/api/stream/index.ts
+++ b/src/server/api/stream/index.ts
@@ -9,6 +9,7 @@ import { EventEmitter } from 'events';
 import { User } from '../../../models/entities/user';
 import { App } from '../../../models/entities/app';
 import { Users, Followings, Mutings } from '../../../models';
+import { ApiError } from '../error';
 
 /**
  * Main stream connection
@@ -83,8 +84,16 @@ export default class Connection {
 		// 呼び出し
 		call(endpoint, user, this.app, payload.data).then(res => {
 			this.sendMessageToWs(`api:${payload.id}`, { res });
-		}).catch(e => {
-			this.sendMessageToWs(`api:${payload.id}`, { e });
+		}).catch((e: ApiError) => {
+			this.sendMessageToWs(`api:${payload.id}`, {
+				error: {
+					message: e.message,
+					code: e.code,
+					id: e.id,
+					kind: e.kind,
+					...(e.info ? { info: e.info } : {})
+				}
+			});
 		});
 	}
 
@@ -111,7 +120,7 @@ export default class Connection {
 			this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
 		}
 
-		if (payload.read && this.user) {
+		if (this.user) {
 			readNote(this.user.id, payload.id);
 		}
 	}
diff --git a/src/server/nodeinfo.ts b/src/server/nodeinfo.ts
index 211fa2a73ec21c085c98dc28fed35c5317b0a70e..2ff924e68d0d018374f1ff3226302da23423ab3e 100644
--- a/src/server/nodeinfo.ts
+++ b/src/server/nodeinfo.ts
@@ -59,10 +59,9 @@ const nodeinfo2 = async () => {
 				email: meta.maintainerEmail
 			},
 			langs: meta.langs,
-			ToSUrl: meta.ToSUrl,
+			tosUrl: meta.ToSUrl,
 			repositoryUrl: meta.repositoryUrl,
 			feedbackUrl: meta.feedbackUrl,
-			announcements: meta.announcements,
 			disableRegistration: meta.disableRegistration,
 			disableLocalTimeline: meta.disableLocalTimeline,
 			disableGlobalTimeline: meta.disableGlobalTimeline,
diff --git a/src/server/web/docs.ts b/src/server/web/docs.ts
index 558e8114661ff424a19d549858caadb1a7ee5281..c6c5fd1e2fcf8f36921f9c1ccc3e70efd1f0ec0e 100644
--- a/src/server/web/docs.ts
+++ b/src/server/web/docs.ts
@@ -12,7 +12,6 @@ import * as send from 'koa-send';
 import * as glob from 'glob';
 import config from '../../config';
 import { licenseHtml } from '../../misc/license';
-import { copyright } from '../../const.json';
 import * as locales from '../../../locales';
 import * as nestedProperty from 'nested-property';
 
@@ -48,7 +47,7 @@ async function genVars(lang: string): Promise<{ [key: string]: any }> {
 
 	vars['config'] = config;
 
-	vars['copyright'] = copyright;
+	vars['copyright'] = '(c) Misskey';
 
 	vars['license'] = licenseHtml;
 
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index 06c7274f5a5f0f422817390ebfb034072c00aaf7..57bcb855a1bbe94f8177d12b0a4ce5fbe244dc75 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -31,6 +31,7 @@ const app = new Koa();
 app.use(views(__dirname + '/views', {
 	extension: 'pug',
 	options: {
+		version: config.version,
 		config
 	}
 }));
diff --git a/src/server/web/views/base.pug b/src/server/web/views/base.pug
index 97c7a87e1bd347b4cfb3c8f321e4fc547af72e4f..43b82a5f05a6a2aac6bab1cdcb7c816e0a125550 100644
--- a/src/server/web/views/base.pug
+++ b/src/server/web/views/base.pug
@@ -10,7 +10,7 @@ html
 		meta(charset='utf-8')
 		meta(name='application-name' content='Misskey')
 		meta(name='referrer' content='origin')
-		meta(name='theme-color' content='#105779')
+		meta(name='theme-color' content='#86b300')
 		meta(property='og:site_name' content= instanceName || 'Misskey')
 		meta(name='viewport' content='width=device-width, initial-scale=1')
 		link(rel='icon' href= icon || '/favicon.ico')
@@ -30,12 +30,23 @@ html
 			meta(property='og:image' content=img)
 
 		style
-			include ./../../../../built/client/assets/init.css
-		script
-			include ./../../../../built/client/assets/boot.js
-
-		script
-			include ./../../../../built/client/assets/safe.js
+			include ./../../../../built/client/assets/style.css
+		script(src=`/assets/app.${version}.js` async defer)
+		script.
+			const theme = localStorage.getItem('theme');
+			if (theme) {
+				for (const [k, v] of Object.entries(JSON.parse(theme))) {
+					document.documentElement.style.setProperty(`--${k}`, v.toString());
+					if (k === 'accent') {
+						for (const tag of document.head.children) {
+							if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
+								tag.setAttribute('content', v);
+								break;
+							}
+						}
+					}
+				}
+			}
 
 	body
 		noscript: p
diff --git a/src/services/add-note-to-antenna.ts b/src/services/add-note-to-antenna.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0055639c0b07d9852df4927d7a64c867bc5642f6
--- /dev/null
+++ b/src/services/add-note-to-antenna.ts
@@ -0,0 +1,54 @@
+import { Antenna } from '../models/entities/antenna';
+import { Note } from '../models/entities/note';
+import { AntennaNotes, Mutings, Notes } from '../models';
+import { genId } from '../misc/gen-id';
+import shouldMuteThisNote from '../misc/should-mute-this-note';
+import { ensure } from '../prelude/ensure';
+import { publishAntennaStream, publishMainStream } from './stream';
+import { User } from '../models/entities/user';
+
+export async function addNoteToAntenna(antenna: Antenna, note: Note, noteUser: User) {
+	// 通知しない設定になっているか、自分自身の投稿なら既読にする
+	const read = !antenna.notify || (antenna.userId === noteUser.id);
+
+	AntennaNotes.save({
+		id: genId(),
+		antennaId: antenna.id,
+		noteId: note.id,
+		read: read,
+	});
+
+	publishAntennaStream(antenna.id, 'note', note);
+
+	if (!read) {
+		const mutings = await Mutings.find({
+			where: {
+				muterId: antenna.userId
+			},
+			select: ['muteeId']
+		});
+
+		const _note: Note = {
+			...note
+		};
+
+		if (note.replyId != null) {
+			_note.reply = await Notes.findOne(note.replyId).then(ensure);
+		}
+		if (note.renoteId != null) {
+			_note.renote = await Notes.findOne(note.renoteId).then(ensure);
+		}
+		
+		if (shouldMuteThisNote(_note, mutings.map(x => x.muteeId))) {
+			return;
+		}
+
+		// 2秒経っても既読にならなかったら通知
+		setTimeout(async () => {
+			const unread = await AntennaNotes.findOne({ antennaId: antenna.id, read: false });
+			if (unread) {
+				publishMainStream(antenna.userId, 'unreadAntenna', antenna);
+			}
+		}, 2000);
+	}
+}
diff --git a/src/services/create-notification.ts b/src/services/create-notification.ts
index 5bff8adfd4dbdc499fb4e439b845ed13bdd8f583..f9cf04dc69ee967bea45131808345ca3992558c6 100644
--- a/src/services/create-notification.ts
+++ b/src/services/create-notification.ts
@@ -5,6 +5,7 @@ import { genId } from '../misc/gen-id';
 import { User } from '../models/entities/user';
 import { Note } from '../models/entities/note';
 import { Notification } from '../models/entities/notification';
+import { FollowRequest } from '../models/entities/follow-request';
 
 export async function createNotification(
 	notifieeId: User['id'],
@@ -14,6 +15,7 @@ export async function createNotification(
 		noteId?: Note['id'];
 		reaction?: string;
 		choice?: number;
+		followRequestId?: FollowRequest['id'];
 	}
 ) {
 	if (notifieeId === notifierId) {
@@ -33,6 +35,7 @@ export async function createNotification(
 		if (content.noteId) data.noteId = content.noteId;
 		if (content.reaction) data.reaction = content.reaction;
 		if (content.choice) data.choice = content.choice;
+		if (content.followRequestId) data.followRequestId = content.followRequestId;
 	}
 
 	// Create notification
diff --git a/src/services/following/create.ts b/src/services/following/create.ts
index b69dfe42b93fcd0a4dae0bb2a01c5c0eb0b02b1c..ce352ffbc1d51f51bc74a4be7225858fde1caf71 100644
--- a/src/services/following/create.ts
+++ b/src/services/following/create.ts
@@ -45,11 +45,21 @@ export async function insertFollowingDoc(followee: User, follower: User) {
 		}
 	});
 
-	await FollowRequests.delete({
+	const req = await FollowRequests.findOne({
 		followeeId: followee.id,
 		followerId: follower.id
 	});
 
+	if (req) {
+		await FollowRequests.delete({
+			followeeId: followee.id,
+			followerId: follower.id
+		});
+
+		// 通知を作成
+		createNotification(follower.id, followee.id, 'followRequestAccepted');
+	}
+
 	if (alreadyFollowed) return;
 
 	//#region Increment counts
diff --git a/src/services/following/requests/create.ts b/src/services/following/requests/create.ts
index 32e79d136d72819d9f9e7bdd3ed40a314d28908d..1c6bac0fbe90b2d33043145be39595a14c0cddc5 100644
--- a/src/services/following/requests/create.ts
+++ b/src/services/following/requests/create.ts
@@ -25,7 +25,7 @@ export default async function(follower: User, followee: User, requestId?: string
 	if (blocking != null) throw new Error('blocking');
 	if (blocked != null) throw new Error('blocked');
 
-	await FollowRequests.save({
+	const followRequest = await FollowRequests.save({
 		id: genId(),
 		createdAt: new Date(),
 		followerId: follower.id,
@@ -50,7 +50,9 @@ export default async function(follower: User, followee: User, requestId?: string
 		}).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
 
 		// 通知を作成
-		createNotification(followee.id, follower.id, 'receiveFollowRequest');
+		createNotification(followee.id, follower.id, 'receiveFollowRequest', {
+			followRequestId: followRequest.id
+		});
 	}
 
 	if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 289a3393b090389a07ef699c79eb2151e1414dfd..e6433ac04d9d0965ed73b55039e62790d1da914e 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -17,7 +17,7 @@ import extractMentions from '../../misc/extract-mentions';
 import extractEmojis from '../../misc/extract-emojis';
 import extractHashtags from '../../misc/extract-hashtags';
 import { Note, IMentionedRemoteUsers } from '../../models/entities/note';
-import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles } from '../../models';
+import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings } from '../../models';
 import { DriveFile } from '../../models/entities/drive-file';
 import { App } from '../../models/entities/app';
 import { Not, getConnection, In } from 'typeorm';
@@ -28,6 +28,8 @@ import { Poll, IPoll } from '../../models/entities/poll';
 import { createNotification } from '../create-notification';
 import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error';
 import { ensure } from '../../prelude/ensure';
+import { checkHitAntenna } from '../../misc/check-hit-antenna';
+import { addNoteToAntenna } from '../add-note-to-antenna';
 
 type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
 
@@ -90,7 +92,6 @@ type Option = {
 	reply?: Note | null;
 	renote?: Note | null;
 	files?: DriveFile[] | null;
-	geo?: any | null;
 	poll?: IPoll | null;
 	viaMobile?: boolean | null;
 	localOnly?: boolean | null;
@@ -207,6 +208,23 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
 	// Increment notes count (user)
 	incNotesCountOfUser(user);
 
+	// Antenna
+	Antennas.find().then(async antennas => {
+		const followings = await Followings.createQueryBuilder('following')
+			.andWhere(`following.followeeId = :userId`, { userId: note.userId })
+			.getMany();
+
+		const followers = followings.map(f => f.followerId);
+		
+		for (const antenna of antennas) {
+			checkHitAntenna(antenna, note, user, followers).then(hit => {
+				if (hit) {
+					addNoteToAntenna(antenna, note, user);
+				}
+			});
+		}
+	});
+
 	if (data.reply) {
 		saveReply(data.reply, note);
 	}
@@ -361,8 +379,6 @@ async function insertNote(user: User, data: Option, tags: string[], emojis: stri
 		userId: user.id,
 		viaMobile: data.viaMobile!,
 		localOnly: data.localOnly!,
-		geo: data.geo || null,
-		appId: data.app ? data.app.id : null,
 		visibility: data.visibility as any,
 		visibleUserIds: data.visibility == 'specified'
 			? data.visibleUsers
diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts
index 436347774b4bcf3bfa187925bb991ddea76e8641..a09fbd9c2fc3a8c99c921a65eb895475e71b9510 100644
--- a/src/services/note/reaction/create.ts
+++ b/src/services/note/reaction/create.ts
@@ -5,7 +5,6 @@ import { deliver } from '../../../queue';
 import { renderActivity } from '../../../remote/activitypub/renderer';
 import { IdentifiableError } from '../../../misc/identifiable-error';
 import { toDbReaction } from '../../../misc/reaction-lib';
-import { fetchMeta } from '../../../misc/fetch-meta';
 import { User } from '../../../models/entities/user';
 import { Note } from '../../../models/entities/note';
 import { NoteReactions, Users, NoteWatchings, Notes, UserProfiles } from '../../../models';
@@ -22,8 +21,7 @@ export default async (user: User, note: Note, reaction?: string) => {
 		throw new IdentifiableError('2d8e7297-1873-4c00-8404-792c68d7bef0', 'cannot react to my note');
 	}
 
-	const meta = await fetchMeta();
-	reaction = await toDbReaction(reaction, meta.enableEmojiReaction);
+	reaction = await toDbReaction(reaction);
 
 	// Create reaction
 	await NoteReactions.save({
diff --git a/src/services/note/read.ts b/src/services/note/read.ts
index c05a58534abbfc9d884330f6e842f9b584f77e03..1cbe0e311b340806712c3a981a3a34f3158ca7f1 100644
--- a/src/services/note/read.ts
+++ b/src/services/note/read.ts
@@ -1,7 +1,7 @@
 import { publishMainStream } from '../stream';
 import { Note } from '../../models/entities/note';
 import { User } from '../../models/entities/user';
-import { NoteUnreads } from '../../models';
+import { NoteUnreads, Antennas, AntennaNotes, Users } from '../../models';
 
 /**
  * Mark a note as read
@@ -17,27 +17,54 @@ export default (
 	});
 
 	// v11 TODO: https://github.com/typeorm/typeorm/issues/2415
-	//if (res.affected == 0) {
+	//if (res.affected === 0) {
 	//	return;
 	//}
 
-	const count1 = await NoteUnreads.count({
-		userId: userId,
-		isSpecified: false
-	});
-
-	const count2 = await NoteUnreads.count({
-		userId: userId,
-		isSpecified: true
-	});
+	const [count1, count2] = await Promise.all([
+		NoteUnreads.count({
+			userId: userId,
+			isSpecified: false
+		}),
+		NoteUnreads.count({
+			userId: userId,
+			isSpecified: true
+		})
+	]);
 
-	if (count1 == 0) {
+	if (count1 === 0) {
 		// 全て既読になったイベントを発行
 		publishMainStream(userId, 'readAllUnreadMentions');
 	}
 
-	if (count2 == 0) {
+	if (count2 === 0) {
 		// 全て既読になったイベントを発行
 		publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
 	}
+
+	const antennas = await Antennas.find({ userId });
+
+	await Promise.all(antennas.map(async antenna => {
+		await AntennaNotes.update({
+			antennaId: antenna.id,
+			noteId: noteId
+		}, {
+			read: true
+		});
+
+		const count = await AntennaNotes.count({
+			antennaId: antenna.id,
+			read: false
+		});
+
+		if (count === 0) {
+			publishMainStream(userId, 'readAntenna', antenna);
+		}
+	}));
+
+	Users.getHasUnreadAntenna(userId).then(unread => {
+		if (!unread) {
+			publishMainStream(userId, 'readAllAntennas');
+		}
+	})
 });
diff --git a/src/services/stream.ts b/src/services/stream.ts
index 32b990ced76a948c119e84c74b98ab6c856845ac..269aed56b968586bef32cda9b89a2df81eff50d2 100644
--- a/src/services/stream.ts
+++ b/src/services/stream.ts
@@ -5,6 +5,7 @@ import { UserList } from '../models/entities/user-list';
 import { ReversiGame } from '../models/entities/games/reversi/game';
 import { UserGroup } from '../models/entities/user-group';
 import config from '../config';
+import { Antenna } from '../models/entities/antenna';
 
 class Publisher {
 	private publish = (channel: string, type: string | null, value?: any): void => {
@@ -37,6 +38,10 @@ class Publisher {
 		this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
+	public publishAntennaStream = (antennaId: Antenna['id'], type: string, value?: any): void => {
+		this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
 	public publishMessagingStream = (userId: User['id'], otherpartyId: User['id'], type: string, value?: any): void => {
 		this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
 	}
@@ -61,10 +66,6 @@ class Publisher {
 		this.publish('notesStream', null, note);
 	}
 
-	public publishApLogStream = (log: any): void => {
-		this.publish('apLog', null, log);
-	}
-
 	public publishAdminStream = (userId: User['id'], type: string, value?: any): void => {
 		this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
@@ -79,10 +80,10 @@ export const publishDriveStream = publisher.publishDriveStream;
 export const publishNoteStream = publisher.publishNoteStream;
 export const publishNotesStream = publisher.publishNotesStream;
 export const publishUserListStream = publisher.publishUserListStream;
+export const publishAntennaStream = publisher.publishAntennaStream;
 export const publishMessagingStream = publisher.publishMessagingStream;
 export const publishGroupMessagingStream = publisher.publishGroupMessagingStream;
 export const publishMessagingIndexStream = publisher.publishMessagingIndexStream;
 export const publishReversiStream = publisher.publishReversiStream;
 export const publishReversiGameStream = publisher.publishReversiGameStream;
-export const publishApLogStream = publisher.publishApLogStream;
 export const publishAdminStream = publisher.publishAdminStream;
diff --git a/webpack.config.ts b/webpack.config.ts
index 195773f33bcc6cae48d19988834f0ce6f9719a06..bec2093d57158d381ceb22fd9b672bb08d1052c1 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -6,7 +6,6 @@ import * as fs from 'fs';
 import * as webpack from 'webpack';
 import * as chalk from 'chalk';
 const { VueLoaderPlugin } = require('vue-loader');
-const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
 const ProgressBarPlugin = require('progress-bar-webpack-plugin');
 const TerserPlugin = require('terser-webpack-plugin');
 
@@ -20,13 +19,9 @@ class WebpackOnBuildPlugin {
 }
 
 const isProduction = process.env.NODE_ENV == 'production';
-const useHardSource = process.env.MISSKEY_USE_HARD_SOURCE;
-
-const constants = require('./src/const.json');
 
 const locales = require('./locales');
 const meta = require('./package.json');
-const codename = meta.codename;
 
 const postcss = {
 	loader: 'postcss-loader',
@@ -41,12 +36,8 @@ const postcss = {
 
 module.exports = {
 	entry: {
-		desktop: './src/client/app/desktop/script.ts',
-		mobile: './src/client/app/mobile/script.ts',
-		dev: './src/client/app/dev/script.ts',
-		auth: './src/client/app/auth/script.ts',
-		admin: './src/client/app/admin/script.ts',
-		sw: './src/client/app/sw.js'
+		app: './src/client/init.ts',
+		sw: './src/client/sw.js'
 	},
 	module: {
 		rules: [{
@@ -64,7 +55,7 @@ module.exports = {
 				loader: 'vue-svg-inline-loader'
 			}]
 		}, {
-			test: /\.styl(us)?$/,
+			test: /\.scss?$/,
 			exclude: /node_modules/,
 			oneOf: [{
 				resourceQuery: /module/,
@@ -76,7 +67,13 @@ module.exports = {
 						modules: true
 					}
 				}, postcss, {
-					loader: 'stylus-loader'
+					loader: 'sass-loader',
+					options: {
+						implementation: require('sass'),
+						sassOptions: {
+							fiber: require('fibers')
+						}
+					}
 				}]
 			}, {
 				use: [{
@@ -84,7 +81,13 @@ module.exports = {
 				}, {
 					loader: 'css-loader'
 				}, postcss, {
-					loader: 'stylus-loader'
+					loader: 'sass-loader',
+					options: {
+						implementation: require('sass'),
+						sassOptions: {
+							fiber: require('fibers')
+						}
+					}
 				}]
 			}]
 		}, {
@@ -107,42 +110,32 @@ module.exports = {
 				loader: 'ts-loader',
 				options: {
 					happyPackMode: true,
-					configFile: __dirname + '/src/client/app/tsconfig.json',
+					transpileOnly: true,
+					configFile: __dirname + '/src/client/tsconfig.json',
 					appendTsSuffixTo: [/\.vue$/]
 				}
 			}]
 		}]
 	},
 	plugins: [
-		...(useHardSource ? [new HardSourceWebpackPlugin()] : []),
 		new ProgressBarPlugin({
-			format: chalk`  {cyan.bold yes we can} {bold [}:bar{bold ]} {green.bold :percent} {gray (:current/:total)} :elapseds`,
+			format: chalk`  {cyan.bold Yes we can} {bold [}:bar{bold ]} {green.bold :percent} {gray :elapseds}`,
 			clear: false
 		}),
 		new webpack.DefinePlugin({
-			_COPYRIGHT_: JSON.stringify(constants.copyright),
 			_VERSION_: JSON.stringify(meta.version),
-			_CODENAME_: JSON.stringify(codename),
 			_LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]: [string, any]) => [k, v && v.meta && v.meta.lang])),
 			_ENV_: JSON.stringify(process.env.NODE_ENV)
 		}),
-		new webpack.DefinePlugin({
-			'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development')
-		}),
+		new VueLoaderPlugin(),
 		new WebpackOnBuildPlugin((stats: any) => {
 			fs.writeFileSync('./built/meta.json', JSON.stringify({ version: meta.version }), 'utf-8');
-
-			fs.mkdirSync('./built/client/assets/locales', { recursive: true });
-
-			for (const [lang, locale] of Object.entries(locales))
-				fs.writeFileSync(`./built/client/assets/locales/${lang}.json`, JSON.stringify(locale), 'utf-8');
 		}),
-		new VueLoaderPlugin(),
-		new webpack.optimize.ModuleConcatenationPlugin()
 	],
 	output: {
 		path: __dirname + '/built/client/assets',
 		filename: `[name].${meta.version}.js`,
+		chunkFilename: '[hash:5].[id].js',
 		publicPath: `/assets/`
 	},
 	resolve: {
@@ -156,6 +149,9 @@ module.exports = {
 	resolveLoader: {
 		modules: ['node_modules']
 	},
+	externals: {
+		moment: 'moment'
+	},
 	optimization: {
 		minimizer: [new TerserPlugin()]
 	},
diff --git a/yarn.lock b/yarn.lock
index 764d48cbeca017da8e69aa6ddca128f4058676cf..cc96c1b9179e4fb854782e8f575aa030637d5aa1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3,33 +3,33 @@
 
 
 "@babel/code-frame@^7.0.0":
-  version "7.5.5"
-  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d"
-  integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
+  integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==
   dependencies:
-    "@babel/highlight" "^7.0.0"
+    "@babel/highlight" "^7.8.3"
 
-"@babel/highlight@^7.0.0":
-  version "7.5.0"
-  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540"
-  integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==
+"@babel/highlight@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.3.tgz#28f173d04223eaaa59bc1d439a3836e6d1265797"
+  integrity sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==
   dependencies:
     chalk "^2.0.0"
     esutils "^2.0.2"
     js-tokens "^4.0.0"
 
 "@babel/polyfill@^7.7.0":
-  version "7.7.0"
-  resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.7.0.tgz#e1066e251e17606ec7908b05617f9b7f8180d8f3"
-  integrity sha512-/TS23MVvo34dFmf8mwCisCbWGrfhbiWZSwBo6HkADTBhUa2Q/jWltyY/tpofz/b6/RIhqaqQcquptCirqIhOaQ==
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.8.3.tgz#2333fc2144a542a7c07da39502ceeeb3abe4debd"
+  integrity sha512-0QEgn2zkCzqGIkSWWAEmvxD7e00Nm9asTtQvi7HdlYvMhjy/J38V/1Y9ode0zEJeIuxAI0uftiAzqc7nVeWUGg==
   dependencies:
     core-js "^2.6.5"
     regenerator-runtime "^0.13.2"
 
 "@babel/runtime@^7.7.4":
-  version "7.7.7"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.7.tgz#194769ca8d6d7790ec23605af9ee3e42a0aa79cf"
-  integrity sha512-uCnC2JEVAu8AKB5do1WRIsvrdJ0flYx/A/9f/6chdacnEZ7LmavjdsDXr5ksYBegxtuTPR5Va9/+13QF/kFkCA==
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.3.tgz#0811944f73a6c926bb2ad35e918dcc1bfab279f1"
+  integrity sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w==
   dependencies:
     regenerator-runtime "^0.13.2"
 
@@ -142,10 +142,10 @@
   dependencies:
     type-detect "4.0.8"
 
-"@tokenizer/token@^0.1.0":
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.1.0.tgz#9ee92540c56ca02e997d65dd14645cf9438c3dcf"
-  integrity sha512-fXk7a5R+aE8bfDRbfT+xRG2evSatjbljGGSUflfQmqw555My8II/EWly2GmcHaqXF5HCMitBEfSNhCRZCrLGGg==
+"@tokenizer/token@^0.1.0", "@tokenizer/token@^0.1.1":
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.1.1.tgz#f0d92c12f87079ddfd1b29f614758b9696bc29e3"
+  integrity sha512-XO6INPbZCxdprl+9qa/AAbFFOMzzwqYxpjPgLICrMD6C2FCw6qfJOPcBk6JqqPLSaZ/Qx87qn4rpPmPMwaAK6w==
 
 "@types/accepts@*":
   version "1.3.5"
@@ -204,9 +204,9 @@
     "@types/node" "*"
 
 "@types/cheerio@^0.22.10":
-  version "0.22.13"
-  resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.13.tgz#5eecda091a24514185dcba99eda77e62bf6523e6"
-  integrity sha512-OZd7dCUOUkiTorf97vJKwZnSja/DmHfuBAroe1kREZZTCf/tlFecwHhsOos3uVHxeKGZDwzolIrCUApClkdLuA==
+  version "0.22.15"
+  resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.15.tgz#69040ffa92c309beeeeb7e92db66ac3f80700c0b"
+  integrity sha512-UGiiVtJK5niCqMKYmLEFz1Wl/3L5zF/u78lu8CwoUywWXRr9LDimeYuOzXVLXBMO758fcTdFtgjvqlztMH90MA==
   dependencies:
     "@types/node" "*"
 
@@ -216,9 +216,9 @@
   integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
 
 "@types/connect@*":
-  version "3.4.32"
-  resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28"
-  integrity sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==
+  version "3.4.33"
+  resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546"
+  integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==
   dependencies:
     "@types/node" "*"
 
@@ -252,18 +252,23 @@
   resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
   integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
 
+"@types/expect@^1.20.4":
+  version "1.20.4"
+  resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5"
+  integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==
+
 "@types/express-serve-static-core@*":
-  version "4.16.9"
-  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.9.tgz#69e00643b0819b024bdede95ced3ff239bb54558"
-  integrity sha512-GqpaVWR0DM8FnRUJYKlWgyARoBUAVfRIeVDZQKOttLFp5SmhhF9YFIYeTPwMd/AXfxlP7xVO2dj1fGu0Q+krKQ==
+  version "4.17.1"
+  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.1.tgz#82be64a77211b205641e0209096fd3afb62481d3"
+  integrity sha512-9e7jj549ZI+RxY21Cl0t8uBnWyb22HzILupyHZjYEVK//5TT/1bZodU+yUbLnPdoYViBBnNWbxp4zYjGV0zUGw==
   dependencies:
     "@types/node" "*"
     "@types/range-parser" "*"
 
 "@types/express@*":
-  version "4.17.1"
-  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.1.tgz#4cf7849ae3b47125a567dfee18bfca4254b88c5c"
-  integrity sha512-VfH/XCP0QbQk5B5puLqTLEeFgR8lfCJHZJKkInZ9mkYd+u8byX0kztXEQxEk4wZXJs8HI+7km2ALXjn4YKcX9w==
+  version "4.17.2"
+  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.2.tgz#a0fb7a23d8855bac31bc01d5a58cadd9b2173e6c"
+  integrity sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA==
   dependencies:
     "@types/body-parser" "*"
     "@types/express-serve-static-core" "*"
@@ -335,9 +340,9 @@
   integrity sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ==
 
 "@types/ioredis@*":
-  version "4.0.18"
-  resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.0.18.tgz#6c48f43cb03992960d724ab9e549d581d12e3a76"
-  integrity sha512-iDIRGPGP4LwoeiKNxQcI38ZA5T8SC+MbGCiiNFJ+LNy9tdegj6f9PAZ7se4tiWJhUHbf25kEJt7k3YfmYjWKZg==
+  version "4.14.4"
+  resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.14.4.tgz#4e46ab8f995f860535518bc9fa206c36f325aa69"
+  integrity sha512-6eM+TBd6YE8E+4DruPYKBYvMKSx+eRdBcJLlaxqZsViR5UQWu+VEkkltet5Z2ZFhRqHMnDf38xDvI1GESCD2Ig==
   dependencies:
     "@types/node" "*"
 
@@ -361,9 +366,9 @@
     parse5 "^4.0.0"
 
 "@types/json-schema@^7.0.3":
-  version "7.0.3"
-  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
-  integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==
+  version "7.0.4"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
+  integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==
 
 "@types/katex@0.11.0":
   version "0.11.0"
@@ -371,9 +376,9 @@
   integrity sha512-27BfE8zASRLYfSBNMk5/+KIjr2CBBrH0i5lhsO04fca4TGirIIMay73v3zNkzqmsaeIa/Mi5kejWDcxPLAmkvA==
 
 "@types/keygrip@*":
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.1.tgz#ff540462d2fb4d0a88441ceaf27d287b01c3d878"
-  integrity sha1-/1QEYtL7TQqIRBzq8n0oewHD2Hg=
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72"
+  integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==
 
 "@types/koa-bodyparser@4.3.0":
   version "4.3.0"
@@ -383,9 +388,9 @@
     "@types/koa" "*"
 
 "@types/koa-compose@*":
-  version "3.2.4"
-  resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.4.tgz#76a461634a59c3e13449831708bb9b355fb1548e"
-  integrity sha512-ioou0rxkuWL+yBQYsHUQAzRTfVxAg8Y2VfMftU+Y3RA03/MzuFL0x/M2sXXj3PkfnENbHsjeHR1aMdezLYpTeA==
+  version "3.2.5"
+  resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d"
+  integrity sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==
   dependencies:
     "@types/koa" "*"
 
@@ -439,19 +444,7 @@
   dependencies:
     "@types/koa" "*"
 
-"@types/koa@*":
-  version "2.0.50"
-  resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.0.50.tgz#c295cbbd59f4898e7a8794c0f2e42e60da83aab2"
-  integrity sha512-TcgOD2lh0EISSadAk1DOBYw7kNoY9XdeB3vEMOKiDDaTMYm+V54nyPsU7Ulb/htb5OBIR79RgTeCWntCcophLw==
-  dependencies:
-    "@types/accepts" "*"
-    "@types/cookies" "*"
-    "@types/http-assert" "*"
-    "@types/keygrip" "*"
-    "@types/koa-compose" "*"
-    "@types/node" "*"
-
-"@types/koa@2.11.0":
+"@types/koa@*", "@types/koa@2.11.0":
   version "2.11.0"
   resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.11.0.tgz#394a3e9ec94f796003a6c8374b4dbc2778746f20"
   integrity sha512-Hgx/1/rVlJvqYBrdeCsS7PDiR2qbxlMt1RnmNWD4Uxi5FF9nwkYqIldo7urjc+dfNpk+2NRGcnAYd4L5xEhCcQ==
@@ -505,9 +498,9 @@
   integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==
 
 "@types/node@*":
-  version "12.7.12"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.12.tgz#7c6c571cc2f3f3ac4a59a5f2bd48f5bdbc8653cc"
-  integrity sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ==
+  version "13.1.8"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-13.1.8.tgz#1d590429fe8187a02707720ecf38a6fe46ce294b"
+  integrity sha512-6XzyyNM9EKQW4HKuzbo/CkOIjn/evtCmsU+MUM1xDfJ+3/rNjBttM1NgN7AOQvN6tP1Sl1D1PIKMreTArnxM9A==
 
 "@types/node@13.1.4":
   version "13.1.4"
@@ -610,17 +603,7 @@
   dependencies:
     "@types/node" "*"
 
-"@types/request@*":
-  version "2.48.3"
-  resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.3.tgz#970b8ed2317568c390361d29c555a95e74bd6135"
-  integrity sha512-3Wo2jNYwqgXcIz/rrq18AdOZUQB8cQ34CXZo+LUwPJNpvRAL86+Kc2wwI8mqpz9Cr1V+enIox5v+WZhy/p3h8w==
-  dependencies:
-    "@types/caseless" "*"
-    "@types/node" "*"
-    "@types/tough-cookie" "*"
-    form-data "^2.5.0"
-
-"@types/request@2.48.4":
+"@types/request@*", "@types/request@2.48.4":
   version "2.48.4"
   resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.4.tgz#df3d43d7b9ed3550feaa1286c6eabf0738e6cf7e"
   integrity sha512-W1t1MTKYR8PxICH+A4HgEIPuAC3sbljoEVfyZbeFJJDbr30guDspJri2XOaM2E+Un7ZjrihaDi7cf6fPa2tbgw==
@@ -683,9 +666,9 @@
     systeminformation "*"
 
 "@types/tapable@*":
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370"
-  integrity sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ==
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.5.tgz#9adbc12950582aa65ead76bffdf39fe0c27a3c02"
+  integrity sha512-/gG2M/Imw7cQFp8PGvz/SwocNrmKFjFsm5Pb8HdbHkZ1K8pmuPzOX4VeVoiEecFCVf4CsN1r3/BRvx+6sNqwtQ==
 
 "@types/tinycolor2@1.4.2":
   version "1.4.2"
@@ -698,9 +681,9 @@
   integrity sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA==
 
 "@types/tough-cookie@*":
-  version "2.3.5"
-  resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.5.tgz#9da44ed75571999b65c37b60c9b2b88db54c585d"
-  integrity sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==
+  version "2.3.6"
+  resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.6.tgz#c880579e087d7a0db13777ff8af689f4ffc7b0d5"
+  integrity sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ==
 
 "@types/uglify-js@*":
   version "3.0.4"
@@ -738,10 +721,11 @@
     "@types/vinyl" "*"
 
 "@types/vinyl@*":
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.3.tgz#80a6ce362ab5b32a0c98e860748a31bce9bff0de"
-  integrity sha512-hrT6xg16CWSmndZqOTJ6BGIn2abKyTw0B58bI+7ioUoj3Sma6u8ftZ1DTI2yCaJamOVGLOnQWiPH3a74+EaqTA==
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.4.tgz#9a7a8071c8d14d3a95d41ebe7135babe4ad5995a"
+  integrity sha512-2o6a2ixaVI2EbwBPg1QYLGQoHK56p/8X/sGfKbFC8N6sY9lfjsMf/GprtkQkSya0D4uRiutRZ2BWj7k3JvLsAQ==
   dependencies:
+    "@types/expect" "^1.20.4"
     "@types/node" "*"
 
 "@types/web-push@3.3.0":
@@ -752,9 +736,9 @@
     "@types/node" "*"
 
 "@types/webpack-sources@*":
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.5.tgz#be47c10f783d3d6efe1471ff7f042611bd464a92"
-  integrity sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w==
+  version "0.1.6"
+  resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.6.tgz#3d21dfc2ec0ad0c77758e79362426a9ba7d7cbcb"
+  integrity sha512-FtAWR7wR5ocJ9+nP137DV81tveD/ZgB1sadnJ/axUGM3BUVfRPx8oQNMtv3JNfTeHx3VP7cXiyfR/jmtEsVHsQ==
   dependencies:
     "@types/node" "*"
     "@types/source-list-map" "*"
@@ -769,9 +753,9 @@
     "@types/webpack" "*"
 
 "@types/webpack@*":
-  version "4.39.3"
-  resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.39.3.tgz#1d55f8fce117a325368bf7612950552ee4ed4467"
-  integrity sha512-afGNNuTfKk1YfHrQ+IwF0QhDkSSMIMMt8BRRErTKaGVvWTMABDjT22/4kJ4bRoSzir9LVgxuuceyZ4Z5I82Cyg==
+  version "4.41.2"
+  resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.2.tgz#c6faf0111de27afdffe1158dac559e447c273516"
+  integrity sha512-DNMQOfEvwzWRRyp6Wy9QVCgJ3gkelZsuBE2KUD318dg95s9DKGiT5CszmmV58hq8jk89I9NClre48AEy1MWAJA==
   dependencies:
     "@types/anymatch" "*"
     "@types/node" "*"
@@ -839,16 +823,16 @@
     tsutils "^3.17.1"
 
 "@vue/component-compiler-utils@^3.1.0":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.1.0.tgz#64cd394925f5af1f9c3228c66e954536f5311857"
-  integrity sha512-OJ7swvl8LtKtX5aYP8jHhO6fQBIRIGkU6rvWzK+CGJiNOnvg16nzcBkd9qMZzW8trI2AsqAKx263nv7kb5rhZw==
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.1.1.tgz#d4ef8f80292674044ad6211e336a302e4d2a6575"
+  integrity sha512-+lN3nsfJJDGMNz7fCpcoYIORrXo0K3OTsdr8jCM7FuqdI4+70TY6gxY6viJ2Xi1clqyPg7LpeOWwjF31vSMmUw==
   dependencies:
     consolidate "^0.15.1"
     hash-sum "^1.0.2"
     lru-cache "^4.1.2"
     merge-source-map "^1.1.0"
     postcss "^7.0.14"
-    postcss-selector-parser "^5.0.0"
+    postcss-selector-parser "^6.0.2"
     prettier "^1.18.2"
     source-map "~0.6.1"
     vue-template-es2015-compiler "^1.9.0"
@@ -1010,9 +994,9 @@
   integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
 
 abab@^2.0.0:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.2.tgz#a2fba1b122c69a85caa02d10f9270c7219709a9d"
-  integrity sha512-2scffjvioEmNz0OyDSLGWDfKCVwaKc6l9Pm9kOIREU13ClXZvHpg/nRL5xyjSSSLhOnXqft2HpsAzNEEA8cFFg==
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a"
+  integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==
 
 abbrev@1:
   version "1.1.1"
@@ -1027,26 +1011,6 @@ accepts@^1.3.5:
     mime-types "~2.1.24"
     negotiator "0.6.2"
 
-accord@^0.26.3:
-  version "0.26.4"
-  resolved "https://registry.yarnpkg.com/accord/-/accord-0.26.4.tgz#fc4c8d3ebab406a07cb28819b859651c44a92e80"
-  integrity sha1-/EyNPrq0BqB8sogZuFllHESpLoA=
-  dependencies:
-    convert-source-map "^1.2.0"
-    glob "^7.0.5"
-    indx "^0.2.3"
-    lodash.clone "^4.3.2"
-    lodash.defaults "^4.0.1"
-    lodash.flatten "^4.2.0"
-    lodash.merge "^4.4.0"
-    lodash.partialright "^4.1.4"
-    lodash.pick "^4.2.1"
-    lodash.uniq "^4.3.0"
-    resolve "^1.1.7"
-    semver "^5.3.0"
-    uglify-js "^2.7.0"
-    when "^3.7.7"
-
 acorn-globals@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf"
@@ -1088,9 +1052,9 @@ acorn@^4.0.4, acorn@~4.0.2:
   integrity sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=
 
 acorn@^6.0.1, acorn@^6.2.1:
-  version "6.3.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e"
-  integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==
+  version "6.4.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.0.tgz#b659d2ffbafa24baf5db1cdbb2c94a983ecd2784"
+  integrity sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==
 
 acorn@^7.1.0:
   version "7.1.0"
@@ -1137,11 +1101,11 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1:
   integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==
 
 ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5:
-  version "6.10.2"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52"
-  integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==
+  version "6.11.0"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.11.0.tgz#c3607cbc8ae392d8a5a536f25b21f8e5f3f87fe9"
+  integrity sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==
   dependencies:
-    fast-deep-equal "^2.0.1"
+    fast-deep-equal "^3.1.1"
     fast-json-stable-stringify "^2.0.0"
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
@@ -1182,13 +1146,6 @@ ansi-colors@^3.0.5:
   resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
   integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==
 
-ansi-cyan@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/ansi-cyan/-/ansi-cyan-0.1.1.tgz#538ae528af8982f28ae30d86f2f17456d2609873"
-  integrity sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=
-  dependencies:
-    ansi-wrap "0.1.0"
-
 ansi-escapes@^4.2.1:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.0.tgz#a4ce2b33d6b214b7950d8595c212f12ac9cc569d"
@@ -1203,13 +1160,6 @@ ansi-gray@^0.1.1:
   dependencies:
     ansi-wrap "0.1.0"
 
-ansi-red@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/ansi-red/-/ansi-red-0.1.1.tgz#8c638f9d1080800a353c9c28c8a81ca4705d946c"
-  integrity sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=
-  dependencies:
-    ansi-wrap "0.1.0"
-
 ansi-regex@^2.0.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
@@ -1243,9 +1193,9 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1:
     color-convert "^1.9.0"
 
 ansi-styles@^4.0.0, ansi-styles@^4.1.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.0.tgz#5681f0dcf7ae5880a7841d8831c4724ed9cc0172"
-  integrity sha512-7kFQgnEaMdRtwf6uSfUnVr9gSGC7faurn+J/Mv90/W+iTtN0405/nLdopfMWwchyxhbGYl6TC4Sccn9TUkGAgg==
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
+  integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
   dependencies:
     "@types/color-name" "^1.1.1"
     color-convert "^2.0.1"
@@ -1324,9 +1274,9 @@ are-we-there-yet@~1.1.2:
     readable-stream "^2.0.6"
 
 arg@^4.1.0:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.1.tgz#485f8e7c390ce4c5f78257dbea80d4be11feda4c"
-  integrity sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw==
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.2.tgz#e70c90579e02c63d80e3ad4e31d8bfdb8bd50064"
+  integrity sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg==
 
 argparse@^1.0.7:
   version "1.0.10"
@@ -1335,14 +1285,6 @@ argparse@^1.0.7:
   dependencies:
     sprintf-js "~1.0.2"
 
-arr-diff@^1.0.1:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-1.1.0.tgz#687c32758163588fef7de7b36fabe495eb1a399a"
-  integrity sha1-aHwydYFjWI/vfeezb6vklesaOZo=
-  dependencies:
-    arr-flatten "^1.0.1"
-    array-slice "^0.2.3"
-
 arr-diff@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@@ -1367,11 +1309,6 @@ arr-map@^2.0.0, arr-map@^2.0.2:
   dependencies:
     make-iterator "^1.0.0"
 
-arr-union@^2.0.1:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-2.1.0.tgz#20f9eab5ec70f5c7d215b1077b1c39161d292c7d"
-  integrity sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=
-
 arr-union@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
@@ -1402,11 +1339,6 @@ array-last@^1.1.1:
   dependencies:
     is-number "^4.0.0"
 
-array-slice@^0.2.3:
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5"
-  integrity sha1-3Tz7gO15c6dRF82sabC5nshhhvU=
-
 array-slice@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4"
@@ -1441,13 +1373,14 @@ asn1.js@^4.0.0:
     minimalistic-assert "^1.0.0"
 
 asn1.js@^5.0.0:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.2.0.tgz#292c0357f26a47802ac9727e8772c09c7fc9bd85"
-  integrity sha512-Q7hnYGGNYbcmGrCPulXfkEw7oW7qjWeM4ZTALmgpuIcZLxyqqKYWxCZg2UBm8bklrnB4m2mGyJPWfoktdORD8A==
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.3.0.tgz#439099fe9174e09cff5a54a9dda70260517e8689"
+  integrity sha512-WHnQJFcOrIWT1RLOkFFBQkFVvyt9BPOOrH+Dp152Zk4R993rSzXUGPmkybIcUFhHE2d/iHH+nCaOWVCDbO8fgA==
   dependencies:
     bn.js "^4.0.0"
     inherits "^2.0.1"
     minimalistic-assert "^1.0.0"
+    safer-buffer "^2.1.0"
 
 asn1@~0.2.3:
   version "0.2.4"
@@ -1499,11 +1432,6 @@ async-each@^1.0.1:
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
   integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
 
-async-limiter@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
-  integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
-
 async-settle@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/async-settle/-/async-settle-1.0.0.tgz#1d0a914bb02575bec8a8f3a74e5080f72b2c0c6b"
@@ -1535,7 +1463,7 @@ asynckit@^0.4.0:
   resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
   integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
 
-atob@^2.1.1:
+atob@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
   integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
@@ -1578,9 +1506,9 @@ aws-sign2@~0.7.0:
   integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
 
 aws4@^1.8.0:
-  version "1.8.0"
-  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
-  integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
+  integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
 
 babel-runtime@^6.26.0:
   version "6.26.0"
@@ -1681,9 +1609,16 @@ binary-extensions@^2.0.0:
   integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
 
 binaryextensions@2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.2.tgz#c83c3d74233ba7674e4f313cb2a2b70f54e94b7c"
-  integrity sha512-xVNN69YGDghOqCCtA6FI7avYrr02mTJjOgB0/f1VPD3pJC8QEvjTKWc4epDx8AqxxA75NI0QpVM2gPJXUbE4Tg==
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.2.0.tgz#e7c6ba82d4f5f5758c26078fe8eea28881233311"
+  integrity sha512-bHhs98rj/7i/RZpCSJ3uk55pLXOItjIrh2sRQZSM6OoktScX+LxJzvlU+FELp9j3TdcddTmmYArLSGptCTwjuw==
+
+bindings@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
+  integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
+  dependencies:
+    file-uri-to-path "1.0.0"
 
 bl@^3.0.0:
   version "3.0.0"
@@ -1693,9 +1628,9 @@ bl@^3.0.0:
     readable-stream "^3.0.1"
 
 bluebird@^3.1.1, bluebird@^3.4.1, bluebird@^3.5.5:
-  version "3.7.0"
-  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.0.tgz#56a6a886e03f6ae577cffedeb524f8f2450293cf"
-  integrity sha512-aBQ1FxIa7kSWCcmKHlcHFlT2jt6J/l4FzC7KcPELkOJOsPOb/bccdhmIrKDfXhwFrmc7vDoDrrepFvGqjyXGJg==
+  version "3.7.2"
+  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
+  integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
 
 bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
   version "4.11.8"
@@ -1718,16 +1653,11 @@ bootstrap-vue@2.1.0:
     portal-vue "^2.1.6"
     vue-functional-data-merge "^3.1.0"
 
-bootstrap@4.4.1:
+bootstrap@4.4.1, "bootstrap@>=4.3.1 <5.0.0":
   version "4.4.1"
   resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.4.1.tgz#8582960eea0c5cd2bede84d8b0baf3789c3e8b01"
   integrity sha512-tbx5cHubwE6e2ZG7nqM3g/FZ5PQEDMWmMGNrCUBVRPHXTJaH7CBDdsLeu3eCh3B1tzAxTnAbtmrzvWEvT2NNEA==
 
-"bootstrap@>=4.3.1 <5.0.0":
-  version "4.3.1"
-  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.3.1.tgz#280ca8f610504d99d7b6b4bfc4b68cec601704ac"
-  integrity sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==
-
 brace-expansion@^1.1.7:
   version "1.1.11"
   resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -1834,13 +1764,13 @@ browserify-zlib@^0.2.0:
     pako "~1.0.5"
 
 browserslist@^4.0.0:
-  version "4.8.2"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.2.tgz#b45720ad5fbc8713b7253c20766f701c9a694289"
-  integrity sha512-+M4oeaTplPm/f1pXDw84YohEv7B1i/2Aisei8s4s6k3QsoSHa7i5sz8u/cGQkkatCPxMASKxPualR4wwYgVboA==
+  version "4.8.3"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.3.tgz#65802fcd77177c878e015f0e3189f2c4f627ba44"
+  integrity sha512-iU43cMMknxG1ClEZ2MDKeonKE1CCrFVkQK2AqO2YWFmvIrx4JWrvQ4w4hQez6EpVI8rHTtqh/ruHHDHSOKxvUg==
   dependencies:
-    caniuse-lite "^1.0.30001015"
+    caniuse-lite "^1.0.30001017"
     electron-to-chromium "^1.3.322"
-    node-releases "^1.1.42"
+    node-releases "^1.1.44"
 
 buffer-alloc-unsafe@^1.1.0:
   version "1.1.0"
@@ -1885,7 +1815,7 @@ buffer-xor@^1.0.3:
   resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
   integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
 
-buffer@4.9.1, buffer@^4.3.0:
+buffer@4.9.1:
   version "4.9.1"
   resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
   integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=
@@ -1894,6 +1824,15 @@ buffer@4.9.1, buffer@^4.3.0:
     ieee754 "^1.1.4"
     isarray "^1.0.0"
 
+buffer@^4.3.0:
+  version "4.9.2"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
+  integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
+  dependencies:
+    base64-js "^1.0.2"
+    ieee754 "^1.1.4"
+    isarray "^1.0.0"
+
 buffer@^5.1.0, buffer@^5.4.3:
   version "5.4.3"
   resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115"
@@ -2071,10 +2010,10 @@ caniuse-api@^3.0.0:
     lodash.memoize "^4.1.2"
     lodash.uniq "^4.5.0"
 
-caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001015:
-  version "1.0.30001015"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001015.tgz#15a7ddf66aba786a71d99626bc8f2b91c6f0f5f0"
-  integrity sha512-/xL2AbW/XWHNu1gnIrO8UitBGoFthcsDgU9VLK1/dpsoxbaD5LscHozKze05R6WLsBvLhqv78dAPozMFQBYLbQ==
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001017:
+  version "1.0.30001021"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001021.tgz#e75ed1ef6dbadd580ac7e7720bb16f07b083f254"
+  integrity sha512-wuMhT7/hwkgd8gldgp2jcrUjOU9RXJ4XxGumQeOsUr91l3WwmM68Cpa/ymCnWEDqakwFXhuDQbaKNHXBPgeE9g==
 
 caseless@~0.12.0:
   version "0.12.0"
@@ -2149,6 +2088,29 @@ chardet@^0.7.0:
   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
   integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
 
+chart.js@2.9.3:
+  version "2.9.3"
+  resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.3.tgz#ae3884114dafd381bc600f5b35a189138aac1ef7"
+  integrity sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==
+  dependencies:
+    chartjs-color "^2.1.0"
+    moment "^2.10.2"
+
+chartjs-color-string@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
+  integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
+  dependencies:
+    color-name "^1.0.0"
+
+chartjs-color@^2.1.0:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
+  integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
+  dependencies:
+    chartjs-color-string "^0.6.0"
+    color-convert "^1.9.3"
+
 check-error@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
@@ -2215,6 +2177,21 @@ chokidar@3.3.0:
   optionalDependencies:
     fsevents "~2.1.1"
 
+"chokidar@>=2.0.0 <4.0.0":
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450"
+  integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==
+  dependencies:
+    anymatch "~3.1.1"
+    braces "~3.0.2"
+    glob-parent "~5.1.0"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.3.0"
+  optionalDependencies:
+    fsevents "~2.1.2"
+
 chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.1.2:
   version "2.1.8"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -2288,7 +2265,7 @@ cli-cursor@^3.1.0:
   dependencies:
     restore-cursor "^3.1.0"
 
-cli-highlight@2.1.4:
+cli-highlight@2.1.4, cli-highlight@^2.0.0:
   version "2.1.4"
   resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.4.tgz#098cb642cf17f42adc1c1145e07f960ec4d7522b"
   integrity sha512-s7Zofobm20qriqDoU9sXptQx0t2R9PEgac92mENNm7xaEe1hn71IIMsXMK+6encA6WRCWWxIGQbipr3q998tlQ==
@@ -2300,17 +2277,6 @@ cli-highlight@2.1.4:
     parse5-htmlparser2-tree-adapter "^5.1.1"
     yargs "^15.0.0"
 
-cli-highlight@^2.0.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.1.tgz#2180223d51618b112f4509cf96e4a6c750b07e97"
-  integrity sha512-0y0VlNmdD99GXZHYnvrQcmHxP8Bi6T00qucGgBgGv4kJ0RyDthNnnFPupHV7PYv/OXSVk+azFbOeaW6+vGmx9A==
-  dependencies:
-    chalk "^2.3.0"
-    highlight.js "^9.6.0"
-    mz "^2.4.0"
-    parse5 "^4.0.0"
-    yargs "^13.0.0"
-
 cli-width@^2.0.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
@@ -2366,6 +2332,15 @@ clone-buffer@^1.0.0:
   resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
   integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg=
 
+clone-deep@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
+  integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
+  dependencies:
+    is-plain-object "^2.0.4"
+    kind-of "^6.0.2"
+    shallow-clone "^3.0.0"
+
 clone-stats@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680"
@@ -2446,7 +2421,7 @@ collection-visit@^1.0.0:
     map-visit "^1.0.0"
     object-visit "^1.0.0"
 
-color-convert@^1.9.0, color-convert@^1.9.1:
+color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3:
   version "1.9.3"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
   integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@@ -2503,17 +2478,12 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
   dependencies:
     delayed-stream "~1.0.0"
 
-commander@2.20.0:
-  version "2.20.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
-  integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
-
 commander@4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83"
   integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw==
 
-commander@^2.12.1, commander@^2.19.0, commander@^2.20.0:
+commander@^2.12.1, commander@^2.19.0, commander@^2.20.0, commander@~2.20.3:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@@ -2529,11 +2499,11 @@ component-emitter@^1.2.1:
   integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
 
 compressible@^2.0.0:
-  version "2.0.17"
-  resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.17.tgz#6e8c108a16ad58384a977f3a482ca20bff2f38c1"
-  integrity sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==
+  version "2.0.18"
+  resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
+  integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
   dependencies:
-    mime-db ">= 1.40.0 < 2"
+    mime-db ">= 1.43.0 < 2"
 
 concat-map@0.0.1:
   version "0.0.1"
@@ -2568,16 +2538,14 @@ config-chain@^1.1.12:
     proto-list "~1.2.1"
 
 consola@^2.10.1:
-  version "2.10.1"
-  resolved "https://registry.yarnpkg.com/consola/-/consola-2.10.1.tgz#4693edba714677c878d520e4c7e4f69306b4b927"
-  integrity sha512-4sxpH6SGFYLADfUip4vuY65f/gEogrzJoniVhNUYkJHtng0l8ZjnDCqxxrSVRHOHwKxsy8Vm5ONZh1wOR3/l/w==
+  version "2.11.3"
+  resolved "https://registry.yarnpkg.com/consola/-/consola-2.11.3.tgz#f7315836224c143ac5094b47fd4c816c2cd1560e"
+  integrity sha512-aoW0YIIAmeftGR8GSpw6CGQluNdkWMWh3yEFjH/hmynTYnMtibXszii3lxCXmk8YxJtI3FAK5aTiquA5VH68Gw==
 
 console-browserify@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
-  integrity sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=
-  dependencies:
-    date-now "^0.1.4"
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
+  integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
 
 console-control-strings@^1.0.0, console-control-strings@~1.1.0:
   version "1.1.0"
@@ -2618,10 +2586,10 @@ content-type@^1.0.4:
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
   integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
 
-convert-source-map@1.X, convert-source-map@^1.2.0, convert-source-map@^1.5.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20"
-  integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==
+convert-source-map@1.X, convert-source-map@^1.5.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
+  integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
   dependencies:
     safe-buffer "~5.1.1"
 
@@ -2672,9 +2640,9 @@ copy-to@^2.0.1:
   integrity sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=
 
 core-js@^2.4.0, core-js@^2.6.10, core-js@^2.6.5:
-  version "2.6.10"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.10.tgz#8a5b8391f8cc7013da703411ce5b585706300d7f"
-  integrity sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA==
+  version "2.6.11"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
+  integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
 
 core-util-is@1.0.2, core-util-is@~1.0.0:
   version "1.0.2"
@@ -2818,25 +2786,18 @@ css-loader@3.4.1:
     postcss-value-parser "^4.0.2"
     schema-utils "^2.6.0"
 
-css-parse@~2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-2.0.0.tgz#a468ee667c16d81ccf05c58c38d2a97c780dbfd4"
-  integrity sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=
-  dependencies:
-    css "^2.0.0"
-
 css-select-base-adapter@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7"
   integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==
 
 css-select@^2.0.0:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.0.2.tgz#ab4386cec9e1f668855564b17c3733b43b2a5ede"
-  integrity sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef"
+  integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==
   dependencies:
     boolbase "^1.0.0"
-    css-what "^2.1.2"
+    css-what "^3.2.1"
     domutils "^1.7.0"
     nth-check "^1.0.2"
 
@@ -2863,12 +2824,17 @@ css-unit-converter@^1.1.1:
   resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996"
   integrity sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=
 
-css-what@2.1, css-what@^2.1.2:
+css-what@2.1:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
   integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
 
-css@2.X, css@^2.0.0, css@^2.2.1:
+css-what@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1"
+  integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==
+
+css@2.X, css@^2.2.1:
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929"
   integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==
@@ -3014,11 +2980,6 @@ data-urls@^1.1.0:
     whatwg-mimetype "^2.2.0"
     whatwg-url "^7.0.0"
 
-date-now@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
-  integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=
-
 dateformat@3.0.3:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
@@ -3052,7 +3013,7 @@ debug@3.1.0, debug@~3.1.0:
   dependencies:
     ms "2.0.0"
 
-debug@3.2.6, debug@3.X, debug@^3.1.0, debug@^3.2.6:
+debug@3.2.6, debug@3.X, debug@^3.1.0:
   version "3.2.6"
   resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
   integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
@@ -3127,7 +3088,7 @@ default-resolution@^2.0.0:
   resolved "https://registry.yarnpkg.com/default-resolution/-/default-resolution-2.0.0.tgz#bcb82baa72ad79b426a76732f1a81ad6df26d684"
   integrity sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=
 
-define-properties@^1.1.1, define-properties@^1.1.2, define-properties@^1.1.3:
+define-properties@^1.1.2, define-properties@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
   integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
@@ -3187,9 +3148,9 @@ depd@~2.0.0:
   integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
 
 des.js@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
-  integrity sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
+  integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==
   dependencies:
     inherits "^2.0.1"
     minimalistic-assert "^1.0.0"
@@ -3209,7 +3170,7 @@ detect-indent@^5.0.0:
   resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d"
   integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50=
 
-detect-libc@^1.0.2, detect-libc@^1.0.3:
+detect-libc@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
   integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
@@ -3233,9 +3194,9 @@ diff@3.5.0:
   integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
 
 diff@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff"
-  integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
+  integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
 
 diffie-hellman@^5.0.0:
   version "5.0.3"
@@ -3272,9 +3233,9 @@ doctypes@^1.1.0:
   integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=
 
 dom-serializer@0:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.1.tgz#13650c850daffea35d8b626a4cfc4d3a17643fdb"
-  integrity sha512-sK3ujri04WyjwQXVoK4PU3y8ula1stq10GJZpqHIUgoGZdsGzAGu65BnU3d08aTVSvO7mGPZUc0wTEDL+qGE0Q==
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
+  integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
   dependencies:
     domelementtype "^2.0.1"
     entities "^2.0.0"
@@ -3403,14 +3364,14 @@ ee-first@1.1.1:
   integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
 
 electron-to-chromium@^1.3.322:
-  version "1.3.322"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.322.tgz#a6f7e1c79025c2b05838e8e344f6e89eb83213a8"
-  integrity sha512-Tc8JQEfGQ1MzfSzI/bTlSr7btJv/FFO7Yh6tanqVmIWOuNCu6/D1MilIEgLtmWqIrsv+o4IjpLAhgMBr/ncNAA==
+  version "1.3.337"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.337.tgz#b2c093cdb66121a946d333b454adcdc5666ceaed"
+  integrity sha512-uJ+wLjslYQ/2rAusDg+6FlK8DLhHWTLCe7gkofBehTifW7KCkPVTn5rhKSCncWYNq34Iy/o4OfswuEkAO2RBaw==
 
 elliptic@^6.0.0:
-  version "6.5.1"
-  resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.1.tgz#c380f5f909bf1b9b4428d028cd18d3b0efd6b52b"
-  integrity sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg==
+  version "6.5.2"
+  resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762"
+  integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==
   dependencies:
     bn.js "^4.4.0"
     brorand "^1.0.1"
@@ -3494,39 +3455,40 @@ error-inject@^1.0.0:
   resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37"
   integrity sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=
 
-es-abstract@^1.12.0, es-abstract@^1.13.0, es-abstract@^1.5.1:
-  version "1.15.0"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.15.0.tgz#8884928ec7e40a79e3c9bc812d37d10c8b24cc57"
-  integrity sha512-bhkEqWJ2t2lMeaJDuk7okMkJWI/yqgH/EoGwpcvv0XW9RWQsRspI4wt6xuyuvMvvQE3gg/D9HXppgk21w78GyQ==
+es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2:
+  version "1.17.2"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.2.tgz#965b10af56597b631da15872c17a405e86c1fd46"
+  integrity sha512-YoKuru3Lyoy7yVTBSH2j7UxTqe/je3dWAruC0sHvZX1GNd5zX8SSLvQqEgO9b3Ex8IW+goFI9arEEsFIbulhOw==
   dependencies:
-    es-to-primitive "^1.2.0"
+    es-to-primitive "^1.2.1"
     function-bind "^1.1.1"
     has "^1.0.3"
-    has-symbols "^1.0.0"
-    is-callable "^1.1.4"
-    is-regex "^1.0.4"
-    object-inspect "^1.6.0"
+    has-symbols "^1.0.1"
+    is-callable "^1.1.5"
+    is-regex "^1.0.5"
+    object-inspect "^1.7.0"
     object-keys "^1.1.1"
-    string.prototype.trimleft "^2.1.0"
-    string.prototype.trimright "^2.1.0"
+    object.assign "^4.1.0"
+    string.prototype.trimleft "^2.1.1"
+    string.prototype.trimright "^2.1.1"
 
-es-to-primitive@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377"
-  integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==
+es-to-primitive@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+  integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
   dependencies:
     is-callable "^1.1.4"
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
 
-es5-ext@^0.10.35, es5-ext@^0.10.45, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.51, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46:
-  version "0.10.51"
-  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.51.tgz#ed2d7d9d48a12df86e0299287e93a09ff478842f"
-  integrity sha512-oRpWzM2WcLHVKpnrcyB7OW8j/s67Ba04JCm0WnNv3RiABSvs7mrQlutB8DBv793gKcp0XENR8Il8WxGTlZ73gQ==
+es5-ext@^0.10.35, es5-ext@^0.10.45, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46:
+  version "0.10.53"
+  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1"
+  integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==
   dependencies:
     es6-iterator "~2.0.3"
-    es6-symbol "~3.1.1"
-    next-tick "^1.0.0"
+    es6-symbol "~3.1.3"
+    next-tick "~1.0.0"
 
 es6-iterator@^2.0.1, es6-iterator@^2.0.3, es6-iterator@~2.0.3:
   version "2.0.3"
@@ -3549,13 +3511,13 @@ es6-promisify@^5.0.0:
   dependencies:
     es6-promise "^4.0.3"
 
-es6-symbol@^3.1.1, es6-symbol@~3.1.1:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.2.tgz#859fdd34f32e905ff06d752e7171ddd4444a7ed1"
-  integrity sha512-/ZypxQsArlv+KHpGvng52/Iz8by3EQPxhmbuz8yFG89N/caTFBSbcXONDw0aMjy827gQg26XAjP4uXFvnfINmQ==
+es6-symbol@^3.1.1, es6-symbol@~3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
+  integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
   dependencies:
     d "^1.0.1"
-    es5-ext "^0.10.51"
+    ext "^1.1.2"
 
 es6-weak-map@^2.0.1, es6-weak-map@^2.0.2:
   version "2.0.3"
@@ -3583,11 +3545,11 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
 escodegen@^1.11.1:
-  version "1.12.0"
-  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.12.0.tgz#f763daf840af172bb3a2b6dd7219c0e17f7ff541"
-  integrity sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg==
+  version "1.13.0"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.13.0.tgz#c7adf9bd3f3cc675bb752f202f79a720189cab29"
+  integrity sha512-eYk2dCkxR07DsHA/X2hRBj0CFAZeri/LyDMc0C8JT1Hqi6JnVpMhJ7XFITbb0+yZS3lVkaPL2oCkZ3AVmeVbMw==
   dependencies:
-    esprima "^3.1.3"
+    esprima "^4.0.1"
     estraverse "^4.2.0"
     esutils "^2.0.2"
     optionator "^0.8.1"
@@ -3682,12 +3644,7 @@ espree@^6.1.2:
     acorn-jsx "^5.1.0"
     eslint-visitor-keys "^1.1.0"
 
-esprima@^3.1.3:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
-  integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=
-
-esprima@^4.0.0:
+esprima@^4.0.0, esprima@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
   integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
@@ -3735,9 +3692,9 @@ events@1.1.1:
   integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=
 
 events@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88"
-  integrity sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59"
+  integrity sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==
 
 evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
   version "1.0.3"
@@ -3805,12 +3762,12 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
   dependencies:
     homedir-polyfill "^1.0.1"
 
-extend-shallow@^1.1.2:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-1.1.4.tgz#19d6bf94dfc09d76ba711f39b872d21ff4dd9071"
-  integrity sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=
+ext@^1.1.2:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
+  integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==
   dependencies:
-    kind-of "^1.1.0"
+    type "^2.0.0"
 
 extend-shallow@^2.0.1:
   version "2.0.1"
@@ -3875,17 +3832,17 @@ fancy-log@1.3.3, fancy-log@^1.3.2:
     parse-node-version "^1.0.0"
     time-stamp "^1.0.0"
 
-fast-deep-equal@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
-  integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
+fast-deep-equal@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
+  integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
 
 fast-json-stable-stringify@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
-  integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I=
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+  integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
 
-fast-levenshtein@~2.0.4, fast-levenshtein@~2.0.6:
+fast-levenshtein@~2.0.6:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
   integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
@@ -3897,6 +3854,13 @@ feed@4.1.0:
   dependencies:
     xml-js "^1.6.11"
 
+fibers@4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/fibers/-/fibers-4.0.2.tgz#d04f9ccd0aba179588202202faeb4fed65d497f5"
+  integrity sha512-FhICi1K4WZh9D6NC18fh2ODF3EWy1z0gzIdV9P7+s2pRjfRBnCkMDJ6x3bV1DkVymKH8HGrQa/FNOBjYvnJ/tQ==
+  dependencies:
+    detect-libc "^1.0.3"
+
 figgy-pudding@^3.5.1:
   version "3.5.1"
   resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
@@ -3931,6 +3895,11 @@ file-type@13.0.1:
     token-types "^2.0.0"
     typedarray-to-buffer "^3.1.5"
 
+file-uri-to-path@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
+  integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
+
 fill-range@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
@@ -4127,13 +4096,6 @@ fs-constants@^1.0.0:
   resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
   integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
 
-fs-minipass@^1.2.5:
-  version "1.2.7"
-  resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7"
-  integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==
-  dependencies:
-    minipass "^2.6.0"
-
 fs-minipass@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.0.0.tgz#a6415edab02fae4b9e9230bc87ee2e4472003cd1"
@@ -4165,14 +4127,14 @@ fs.realpath@^1.0.0:
   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 
 fsevents@^1.2.7:
-  version "1.2.9"
-  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f"
-  integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==
+  version "1.2.11"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.11.tgz#67bf57f4758f02ede88fb2a1712fef4d15358be3"
+  integrity sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw==
   dependencies:
+    bindings "^1.5.0"
     nan "^2.12.1"
-    node-pre-gyp "^0.12.0"
 
-fsevents@~2.1.1:
+fsevents@~2.1.1, fsevents@~2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805"
   integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==
@@ -4224,11 +4186,9 @@ get-paths@0.0.7:
     pify "^4.0.1"
 
 get-port@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.0.0.tgz#aa22b6b86fd926dd7884de3e23332c9f70c031a6"
-  integrity sha512-imzMU0FjsZqNa6BqOjbbW6w5BivHIuQKopjpPqcnx0AVHJQKCxK1O+Ab3OrVXhrekqfVMjwA9ZYu062R+KcIsQ==
-  dependencies:
-    type-fest "^0.3.0"
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
+  integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==
 
 get-stream@^4.0.0:
   version "4.1.0"
@@ -4316,19 +4276,7 @@ glob@7.1.3:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
-  version "7.1.4"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
-  integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==
-  dependencies:
-    fs.realpath "^1.0.0"
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "^3.0.4"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
-glob@^7.1.6:
+glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
   version "7.1.6"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
   integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -4398,9 +4346,9 @@ good-listener@^1.2.2:
     delegate "^3.1.2"
 
 graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.2:
-  version "4.2.2"
-  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02"
-  integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
+  integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
 
 growl@1.10.5:
   version "1.10.5"
@@ -4441,6 +4389,20 @@ gulp-cli@^2.2.0:
     v8flags "^3.0.1"
     yargs "^7.1.0"
 
+gulp-dart-sass@0.9.1:
+  version "0.9.1"
+  resolved "https://registry.yarnpkg.com/gulp-dart-sass/-/gulp-dart-sass-0.9.1.tgz#6243d12744759e17568bf06183b4aa1a16a74432"
+  integrity sha512-6gmKmzDwSzjrN1nIC7K0URpX3fG5jMXxbpBLF42GFhnuM5+9Is7D9W55K+9A0MnFeqKyZGIzof4a9GDpB8wJgQ==
+  dependencies:
+    chalk "^2.3.0"
+    lodash.clonedeep "^4.3.2"
+    plugin-error "^1.0.1"
+    replace-ext "^1.0.0"
+    sass "^1.10.3"
+    strip-ansi "^4.0.0"
+    through2 "^2.0.0"
+    vinyl-sourcemaps-apply "^0.2.0"
+
 gulp-mocha@7.0.2:
   version "7.0.2"
   resolved "https://registry.yarnpkg.com/gulp-mocha/-/gulp-mocha-7.0.2.tgz#c7e13d133b3fde96d777e877f90b46225255e408"
@@ -4484,19 +4446,6 @@ gulp-sourcemaps@2.6.5:
     strip-bom-string "1.X"
     through2 "2.X"
 
-gulp-stylus@2.7.0:
-  version "2.7.0"
-  resolved "https://registry.yarnpkg.com/gulp-stylus/-/gulp-stylus-2.7.0.tgz#f3e932626004927b75ea27ff5c1d3b0ba0b7cbb1"
-  integrity sha512-LlneLeHcaRBaEqxwo5YCirpsfkR7uleQ4pHXW8IE2ZeA6M3jpgI90+zQ6SptMTSWr1RSQW3WYFZVA3P0coUojw==
-  dependencies:
-    accord "^0.26.3"
-    lodash.assign "^3.2.0"
-    plugin-error "^0.1.2"
-    replace-ext "0.0.1"
-    stylus "^0.54.0"
-    through2 "^2.0.0"
-    vinyl-sourcemaps-apply "^0.2.0"
-
 gulp-terser@1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/gulp-terser/-/gulp-terser-1.2.0.tgz#41df2a1d0257d011ba8b05efb2568432ecd0495b"
@@ -4548,6 +4497,17 @@ gulplog@^1.0.0:
   dependencies:
     glogg "^1.0.0"
 
+handlebars@^4.5.3:
+  version "4.7.2"
+  resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.2.tgz#01127b3840156a0927058779482031afe0e730d7"
+  integrity sha512-4PwqDL2laXtTWZghzzCtunQUTLbo31pcCJrd/B/9JP8XbhVzpS5ZXuKqlOzsd1rtcaLo4KqAn8nl8mkknS4MHw==
+  dependencies:
+    neo-async "^2.6.0"
+    optimist "^0.6.1"
+    source-map "^0.6.1"
+  optionalDependencies:
+    uglify-js "^3.1.4"
+
 har-schema@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
@@ -4597,10 +4557,10 @@ has-flag@^4.0.0:
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
   integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
 
-has-symbols@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
-  integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=
+has-symbols@^1.0.0, has-symbols@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
+  integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
 
 has-unicode@^2.0.0:
   version "2.0.1"
@@ -4638,7 +4598,7 @@ has-values@^1.0.0:
     is-number "^3.0.0"
     kind-of "^4.0.0"
 
-has@^1.0.0, has@^1.0.1, has@^1.0.3:
+has@^1.0.0, has@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
   integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
@@ -4677,14 +4637,16 @@ hex-color-regex@^1.1.0:
   integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
 
 highlight.js@^9.6.0:
-  version "9.15.10"
-  resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.10.tgz#7b18ed75c90348c045eef9ed08ca1319a2219ad2"
-  integrity sha512-RoV7OkQm0T3os3Dd2VHLNMoaoDVx77Wygln3n9l5YV172XonWG6rgQD3XnF/BuFFZw9A0TJgmMSO8FEWQgvcXw==
+  version "9.17.1"
+  resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.17.1.tgz#14a4eded23fd314b05886758bb906e39dd627f9a"
+  integrity sha512-TA2/doAur5Ol8+iM3Ov7qy3jYcr/QiJ2eDTdRF4dfbjG7AaaB99J5G+zSl11ljbl6cIcahgPY6SKb3sC3EJ0fw==
+  dependencies:
+    handlebars "^4.5.3"
 
 highlightjs@^9.8.0:
-  version "9.12.0"
-  resolved "https://registry.yarnpkg.com/highlightjs/-/highlightjs-9.12.0.tgz#9b84eb42a7aa8488eb69ac79fec44cf495bf72a1"
-  integrity sha512-eAhWMtDZaOZIQdxIP4UEB1vNp/CVXQPdMSihTSuaExhFIRC0BVpXbtP3mTP1hDoGOyh7nbB3cuC3sOPhG5wGDA==
+  version "9.16.2"
+  resolved "https://registry.yarnpkg.com/highlightjs/-/highlightjs-9.16.2.tgz#07ea6cc7c93340fc440734fb7abf28558f1f0fe1"
+  integrity sha512-FK1vmMj8BbEipEy8DLIvp71t5UsC7n2D6En/UfM/91PCwmOpj6f2iu0Y0coRC62KSRHHC+dquM2xMULV/X7NFg==
 
 hmac-drbg@^1.0.0:
   version "1.0.1"
@@ -4834,9 +4796,9 @@ https-proxy-agent@4.0.0:
     debug "4"
 
 https-proxy-agent@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz#0106efa5d63d6d6f3ab87c999fa4877a3fd1ff97"
-  integrity sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ==
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81"
+  integrity sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==
   dependencies:
     agent-base "^4.3.0"
     debug "^3.1.0"
@@ -4853,7 +4815,7 @@ humanize-number@0.0.2:
   resolved "https://registry.yarnpkg.com/humanize-number/-/humanize-number-0.0.2.tgz#11c0af6a471643633588588048f1799541489c18"
   integrity sha1-EcCvakcWQ2M1iFiASPF5lUFInBg=
 
-iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4:
+iconv-lite@0.4.24, iconv-lite@^0.4.24:
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
   integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@@ -4877,13 +4839,6 @@ iferr@^0.1.5:
   resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
   integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
 
-ignore-walk@^3.0.1:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37"
-  integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==
-  dependencies:
-    minimatch "^3.0.4"
-
 ignore@^4.0.6:
   version "4.0.6"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
@@ -4910,9 +4865,9 @@ import-fresh@^2.0.0:
     resolve-from "^3.0.0"
 
 import-fresh@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.1.0.tgz#6d33fa1dcef6df930fae003446f33415af905118"
-  integrity sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
+  integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==
   dependencies:
     parent-module "^1.0.0"
     resolve-from "^4.0.0"
@@ -4947,11 +4902,6 @@ indexes-of@^1.0.1:
   resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
   integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
 
-indx@^0.2.3:
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/indx/-/indx-0.2.3.tgz#15dcf56ee9cf65c0234c513c27fbd580e70fbc50"
-  integrity sha1-Fdz1bunPZcAjTFE8J/vVgOcPvFA=
-
 infer-owner@^1.0.3, infer-owner@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
@@ -4991,9 +4941,9 @@ ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
   integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
 
 inquirer@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.0.tgz#9e2b032dde77da1db5db804758b8fea3a970519a"
-  integrity sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ==
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.3.tgz#f9b4cd2dff58b9f73e8d43759436ace15bed4567"
+  integrity sha512-+OiOVeVydu4hnCGLCSX+wedovR/Yzskv9BFqUNNKq9uU2qg7LCcCo3R86S2E7WLo0y/x2pnEZfZe1CoYnORUAw==
   dependencies:
     ansi-escapes "^4.2.1"
     chalk "^2.4.2"
@@ -5004,7 +4954,7 @@ inquirer@^7.0.0:
     lodash "^4.17.15"
     mute-stream "0.0.8"
     run-async "^2.2.0"
-    rxjs "^6.4.0"
+    rxjs "^6.5.3"
     string-width "^4.1.0"
     strip-ansi "^5.1.0"
     through "^2.3.6"
@@ -5118,10 +5068,10 @@ is-buffer@~2.0.3:
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623"
   integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==
 
-is-callable@^1.1.4:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
-  integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==
+is-callable@^1.1.4, is-callable@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
+  integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==
 
 is-color-stop@^1.0.0:
   version "1.1.0"
@@ -5150,9 +5100,9 @@ is-data-descriptor@^1.0.0:
     kind-of "^6.0.0"
 
 is-date-object@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
-  integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
+  integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
 
 is-descriptor@^0.1.0:
   version "0.1.6"
@@ -5239,11 +5189,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
     is-extglob "^2.1.1"
 
 is-nan@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.2.1.tgz#9faf65b6fb6db24b7f5c0628475ea71f988401e2"
-  integrity sha1-n69ltvttskt/XAYoR16nH5iEAeI=
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.0.tgz#85d1f5482f7051c2019f5673ccebdb06f3b0db03"
+  integrity sha512-z7bbREymOqt2CCaZVly8aC4ML3Xhfi0ekuOnjO2L8vKdl+CttdVoGZQhd4adMFAsxQ5VeRVwORs4tU8RH+HFtQ==
   dependencies:
-    define-properties "^1.1.1"
+    define-properties "^1.1.3"
 
 is-negated-glob@^1.0.0:
   version "1.0.0"
@@ -5296,12 +5246,12 @@ is-promise@^2.0.0, is-promise@^2.1, is-promise@^2.1.0:
   resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
   integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=
 
-is-regex@^1.0.3, is-regex@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
-  integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=
+is-regex@^1.0.3, is-regex@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae"
+  integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==
   dependencies:
-    has "^1.0.1"
+    has "^1.0.3"
 
 is-relative@^1.0.0:
   version "1.0.0"
@@ -5345,11 +5295,11 @@ is-svg@^3.0.0:
     html-comment-regex "^1.1.0"
 
 is-symbol@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38"
-  integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
+  integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
   dependencies:
-    has-symbols "^1.0.0"
+    has-symbols "^1.0.1"
 
 is-typedarray@^1.0.0, is-typedarray@~1.0.0:
   version "1.0.0"
@@ -5453,9 +5403,9 @@ jpeg-js@^0.3.3:
   integrity sha512-MUj2XlMB8kpe+8DJUGH/3UJm4XpI8XEgZQ+CiHDeyrGoKPdW/8FJv6ku+3UiYm5Fz3CWaL+iXmD8Q4Ap6aC1Jw==
 
 js-beautify@^1.6.12:
-  version "1.10.2"
-  resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.10.2.tgz#88c9099cd6559402b124cfab18754936f8a7b178"
-  integrity sha512-ZtBYyNUYJIsBWERnQP0rPN9KjkrDfJcMjuVGcvXOUJrD1zmOGwhRwQ4msG+HJ+Ni/FA7+sRQEMYVzdTQDvnzvQ==
+  version "1.10.3"
+  resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.10.3.tgz#c73fa10cf69d3dfa52d8ed624f23c64c0a6a94c1"
+  integrity sha512-wfk/IAWobz1TfApSdivH5PJ0miIHgDoYb1ugSqHcODPmaYu46rYe5FVuIEkhjg8IQiv6rDNPyhsqbsohI/C2vQ==
   dependencies:
     config-chain "^1.1.12"
     editorconfig "^0.15.3"
@@ -5487,9 +5437,9 @@ jsbn@~0.1.0:
   integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
 
 jschardet@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.1.0.tgz#0491e1e00ec091490d4ca3dc8e30926b38de596b"
-  integrity sha512-Fuk25QlebgHnvCQvAm258+y2D8yVs1VFIFwiqXbvxG4n9vo5YZsBPZuMxnc7Gb9ElAeN8QfRDvgkkIhW4DPoPA==
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.1.1.tgz#af6f8fd0b3b0f5d46a8fd9614a4fce490575c184"
+  integrity sha512-pA5qG9Zwm8CBpGlK/lo2GE9jPxwqRgMV7Lzc/1iaPccw6v4Rhj8Zg2BTyrdmHmxlJojnbLupLeRnaPLsq03x6Q==
 
 jsdom@15.2.1:
   version "15.2.1"
@@ -5635,11 +5585,6 @@ keygrip@~1.1.0:
   dependencies:
     tsscmp "1.0.6"
 
-kind-of@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-1.1.0.tgz#140a3d2d41a36d2efcfa9377b62c24f8495a5c44"
-  integrity sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=
-
 kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@@ -5660,9 +5605,9 @@ kind-of@^5.0.0, kind-of@^5.0.2:
   integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
 
 kind-of@^6.0.0, kind-of@^6.0.2:
-  version "6.0.2"
-  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
-  integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+  integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
 
 koa-bodyparser@4.2.1:
   version "4.2.1"
@@ -5937,52 +5882,6 @@ locate-path@^5.0.0:
   dependencies:
     p-locate "^4.1.0"
 
-lodash._baseassign@^3.0.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e"
-  integrity sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=
-  dependencies:
-    lodash._basecopy "^3.0.0"
-    lodash.keys "^3.0.0"
-
-lodash._basecopy@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36"
-  integrity sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=
-
-lodash._bindcallback@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
-  integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4=
-
-lodash._createassigner@^3.0.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11"
-  integrity sha1-g4pbri/aymOsIt7o4Z+k5taXCxE=
-  dependencies:
-    lodash._bindcallback "^3.0.0"
-    lodash._isiterateecall "^3.0.0"
-    lodash.restparam "^3.0.0"
-
-lodash._getnative@^3.0.0:
-  version "3.9.1"
-  resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
-  integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=
-
-lodash._isiterateecall@^3.0.0:
-  version "3.0.9"
-  resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c"
-  integrity sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=
-
-lodash.assign@^3.2.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa"
-  integrity sha1-POnwI0tLIiPilrj6CsH+6OvKZPo=
-  dependencies:
-    lodash._baseassign "^3.0.0"
-    lodash._createassigner "^3.0.0"
-    lodash.keys "^3.0.0"
-
 lodash.assignin@^4.0.9:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2"
@@ -5993,12 +5892,7 @@ lodash.bind@^4.1.4:
   resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35"
   integrity sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=
 
-lodash.clone@^4.3.2:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6"
-  integrity sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=
-
-lodash.clonedeep@^4.5.0:
+lodash.clonedeep@^4.3.2:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
   integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
@@ -6023,16 +5917,6 @@ lodash.foreach@^4.3.0:
   resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
   integrity sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=
 
-lodash.isarguments@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
-  integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=
-
-lodash.isarray@^3.0.0:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
-  integrity sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=
-
 lodash.isfinite@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3"
@@ -6043,15 +5927,6 @@ lodash.isregexp@3.0.5:
   resolved "https://registry.yarnpkg.com/lodash.isregexp/-/lodash.isregexp-3.0.5.tgz#e0f596242f2fa228a840086b6c8ad82e4b71fd2d"
   integrity sha1-4PWWJC8voiioQAhrbIrYLktx/S0=
 
-lodash.keys@^3.0.0:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
-  integrity sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=
-  dependencies:
-    lodash._getnative "^3.0.0"
-    lodash.isarguments "^3.0.0"
-    lodash.isarray "^3.0.0"
-
 lodash.map@^4.4.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3"
@@ -6067,11 +5942,6 @@ lodash.merge@^4.4.0:
   resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
   integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
 
-lodash.partialright@^4.1.4:
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/lodash.partialright/-/lodash.partialright-4.2.1.tgz#0130d80e83363264d40074f329b8a3e7a8a1cc4b"
-  integrity sha1-ATDYDoM2MmTUAHTzKbij56ihzEs=
-
 lodash.pick@^4.2.1:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
@@ -6087,11 +5957,6 @@ lodash.reject@^4.4.0:
   resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415"
   integrity sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=
 
-lodash.restparam@^3.0.0:
-  version "3.6.1"
-  resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
-  integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=
-
 lodash.some@^4.4.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"
@@ -6112,7 +5977,7 @@ lodash.unescape@4.0.1:
   resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c"
   integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=
 
-lodash.uniq@^4.3.0, lodash.uniq@^4.5.0:
+lodash.uniq@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
   integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
@@ -6365,22 +6230,17 @@ miller-rabin@^4.0.0:
     bn.js "^4.0.0"
     brorand "^1.0.1"
 
-mime-db@1.40.0:
-  version "1.40.0"
-  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32"
-  integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==
-
-"mime-db@>= 1.40.0 < 2":
-  version "1.42.0"
-  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac"
-  integrity sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==
+mime-db@1.43.0, "mime-db@>= 1.43.0 < 2":
+  version "1.43.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58"
+  integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==
 
 mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.19, mime-types@~2.1.24:
-  version "2.1.24"
-  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81"
-  integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==
+  version "2.1.26"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06"
+  integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==
   dependencies:
-    mime-db "1.40.0"
+    mime-db "1.43.0"
 
 mime@^2.4.4:
   version "2.4.4"
@@ -6424,6 +6284,11 @@ minimist@^1.2.0:
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
   integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
 
+minimist@~0.0.1:
+  version "0.0.10"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
+  integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=
+
 minipass-collect@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
@@ -6445,28 +6310,13 @@ minipass-pipeline@^1.2.2:
   dependencies:
     minipass "^3.0.0"
 
-minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0:
-  version "2.9.0"
-  resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
-  integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==
-  dependencies:
-    safe-buffer "^5.1.2"
-    yallist "^3.0.0"
-
-minipass@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.0.1.tgz#b4fec73bd61e8a40f0b374ddd04260ade2c8ec20"
-  integrity sha512-2y5okJ4uBsjoD2vAbLKL9EUQPPkC0YMIp+2mZOXG3nBba++pdfJWRxx2Ewirc0pwAJYu4XtWg2EkVo1nRXuO/w==
+minipass@^3.0.0, minipass@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.1.tgz#7607ce778472a185ad6d89082aa2070f79cedcd5"
+  integrity sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==
   dependencies:
     yallist "^4.0.0"
 
-minizlib@^1.2.1:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
-  integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==
-  dependencies:
-    minipass "^2.9.0"
-
 minizlib@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.0.tgz#fd52c645301ef09a63a2c209697c294c6ce02cf3"
@@ -6499,7 +6349,7 @@ mixin-deep@^1.2.0:
     for-in "^1.0.2"
     is-extendable "^1.0.1"
 
-mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1, mkdirp@~0.5.x:
+mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
   integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
@@ -6537,9 +6387,9 @@ mocha@7.0.0:
     yargs-unparser "1.6.0"
 
 mocha@^6.2.0:
-  version "6.2.1"
-  resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.1.tgz#da941c99437da9bac412097859ff99543969f94c"
-  integrity sha512-VCcWkLHwk79NYQc8cxhkmI8IigTIhsCwZ6RTxQsqK6go4UvEhzJkYuHm8B2YtlSxcYq2fY+ucr4JBwoD6ci80A==
+  version "6.2.2"
+  resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.2.tgz#5d8987e28940caf8957a7d7664b910dc5b2fea20"
+  integrity sha512-FgDS9Re79yU1xz5d+C4rv1G7QagNGHZ+iXF81hO8zY35YZZcLEsJVfFolfsqKFWunATEvNzMK0r/CwWd/szO9A==
   dependencies:
     ansi-colors "3.2.3"
     browser-stdout "1.3.1"
@@ -6573,13 +6423,13 @@ moji@0.5.1:
     object-assign "^3.0.0"
 
 moment-timezone@^0.5.25:
-  version "0.5.26"
-  resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.26.tgz#c0267ca09ae84631aa3dc33f65bedbe6e8e0d772"
-  integrity sha512-sFP4cgEKTCymBBKgoxZjYzlSovC20Y6J7y3nanDc5RoBIXKlZhoYwBoZGe3flwU6A372AcRwScH8KiwV6zjy1g==
+  version "0.5.27"
+  resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.27.tgz#73adec8139b6fe30452e78f210f27b1f346b8877"
+  integrity sha512-EIKQs7h5sAsjhPCqN6ggx6cEbs94GK050254TIJySD1bzoM5JTYDwAU1IoVOeTOL6Gm27kYJ51/uuvq1kIlrbw==
   dependencies:
     moment ">= 2.9.0"
 
-"moment@>= 2.9.0", moment@^2.22.2:
+"moment@>= 2.9.0", moment@^2.10.2, moment@^2.22.2:
   version "2.24.0"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
   integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
@@ -6676,21 +6526,12 @@ natural-compare@^1.4.0:
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
   integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
 
-needle@^2.2.1:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c"
-  integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==
-  dependencies:
-    debug "^3.2.6"
-    iconv-lite "^0.4.4"
-    sax "^1.2.4"
-
 negotiator@0.6.2:
   version "0.6.2"
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
   integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
 
-neo-async@^2.5.0, neo-async@^2.6.1:
+neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1:
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
   integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
@@ -6705,7 +6546,7 @@ next-line@^1.1.0:
   resolved "https://registry.yarnpkg.com/next-line/-/next-line-1.1.0.tgz#fcae57853052b6a9bae8208e40dd7d3c2d304603"
   integrity sha1-/K5XhTBStqm66CCOQN19PC0wRgM=
 
-next-tick@1, next-tick@^1.0.0:
+next-tick@1, next-tick@^1.0.0, next-tick@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
   integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
@@ -6723,9 +6564,9 @@ no-case@^2.2.0:
     lower-case "^1.1.1"
 
 node-abi@^2.7.0:
-  version "2.11.0"
-  resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.11.0.tgz#b7dce18815057544a049be5ae75cd1fdc2e9ea59"
-  integrity sha512-kuy/aEg75u40v378WRllQ4ZexaXJiCvB68D2scDXclp/I4cRq6togpbOoKhmN07tns9Zldu51NNERo0wehfX9g==
+  version "2.13.0"
+  resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.13.0.tgz#e2f2ec444d0aca3ea1b3874b6de41d1665828f63"
+  integrity sha512-9HrZGFVTR5SOu3PZAnAY2hLO36aW1wmA+FDsVkr85BTST32TLCA1H/AEcatVRAsWLyXS3bqUDYCAjq5/QGuSTA==
   dependencies:
     semver "^5.4.1"
 
@@ -6784,26 +6625,10 @@ node-object-hash@^1.2.0:
   resolved "https://registry.yarnpkg.com/node-object-hash/-/node-object-hash-1.4.2.tgz#385833d85b229902b75826224f6077be969a9e94"
   integrity sha512-UdS4swXs85fCGWWf6t6DMGgpN/vnlKeSGEQ7hJcrs7PBFoxoKLmibc3QRb7fwiYsjdL7PX8iI/TMSlZ90dgHhQ==
 
-node-pre-gyp@^0.12.0:
-  version "0.12.0"
-  resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149"
-  integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==
-  dependencies:
-    detect-libc "^1.0.2"
-    mkdirp "^0.5.1"
-    needle "^2.2.1"
-    nopt "^4.0.1"
-    npm-packlist "^1.1.6"
-    npmlog "^4.0.2"
-    rc "^1.2.7"
-    rimraf "^2.6.1"
-    semver "^5.3.0"
-    tar "^4"
-
-node-releases@^1.1.42:
-  version "1.1.42"
-  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.42.tgz#a999f6a62f8746981f6da90627a8d2fc090bbad7"
-  integrity sha512-OQ/ESmUqGawI2PRX+XIRao44qWYBBfN54ImQYdWVTQqUckuejOg76ysSqDBK8NG3zwySRVnX36JwDQ6x+9GxzA==
+node-releases@^1.1.44:
+  version "1.1.46"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.46.tgz#6b262afef1bdc9a950a96df2e77e0d2290f484bf"
+  integrity sha512-YOjdx+Uoh9FbRO7yVYbnbt1puRWPQMemR3SutLeyv2XfxKs1ihpe0OLAUwBPEP2ImNH/PZC7SEiC6j32dwRZ7g==
   dependencies:
     semver "^6.3.0"
 
@@ -6822,7 +6647,7 @@ noop-logger@^0.1.1:
   resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2"
   integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=
 
-nopt@^4.0.1, nopt@~4.0.1:
+nopt@~4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
   integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=
@@ -6864,19 +6689,6 @@ now-and-later@^2.0.0:
   dependencies:
     once "^1.3.2"
 
-npm-bundled@^1.0.1:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd"
-  integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==
-
-npm-packlist@^1.1.6:
-  version "1.4.6"
-  resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.6.tgz#53ba3ed11f8523079f1457376dd379ee4ea42ff4"
-  integrity sha512-u65uQdb+qwtGvEJh/DgQgW1Xg7sqeNbmxYyrvlNznaVTjV3E5P6F/EFjM+BVHXl7JJlsdG8A64M0XI8FI/IOlg==
-  dependencies:
-    ignore-walk "^3.0.1"
-    npm-bundled "^1.0.1"
-
 npm-run-path@^2.0.0:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@@ -6891,7 +6703,7 @@ npm-run-path@^3.0.0:
   dependencies:
     path-key "^3.0.0"
 
-npmlog@^4.0.1, npmlog@^4.0.2, npmlog@^4.1.2:
+npmlog@^4.0.1, npmlog@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
   integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@@ -6957,10 +6769,10 @@ object-copy@^0.1.0:
     define-property "^0.2.5"
     kind-of "^3.0.3"
 
-object-inspect@^1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b"
-  integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==
+object-inspect@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
+  integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
 
 object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
   version "1.1.1"
@@ -6974,7 +6786,7 @@ object-visit@^1.0.0:
   dependencies:
     isobject "^3.0.0"
 
-object.assign@4.1.0, object.assign@^4.0.1, object.assign@^4.0.4:
+object.assign@4.1.0, object.assign@^4.0.1, object.assign@^4.0.4, object.assign@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
   integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
@@ -6994,13 +6806,13 @@ object.defaults@^1.0.0, object.defaults@^1.1.0:
     for-own "^1.0.0"
     isobject "^3.0.0"
 
-object.getownpropertydescriptors@^2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
-  integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=
+object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
+  integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==
   dependencies:
-    define-properties "^1.1.2"
-    es-abstract "^1.5.1"
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
 
 object.map@^1.0.0:
   version "1.0.1"
@@ -7026,12 +6838,12 @@ object.reduce@^1.0.0:
     make-iterator "^1.0.0"
 
 object.values@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9"
-  integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
+  integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
   dependencies:
     define-properties "^1.1.3"
-    es-abstract "^1.12.0"
+    es-abstract "^1.17.0-next.1"
     function-bind "^1.1.1"
     has "^1.0.3"
 
@@ -7066,19 +6878,15 @@ opentype.js@^0.4.3:
   resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-0.4.11.tgz#281a2390639cc15931c955d8d63c14a7c7772b41"
   integrity sha1-KBojkGOcwVkxyVXY1jwUp8d3K0E=
 
-optionator@^0.8.1:
-  version "0.8.2"
-  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
-  integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=
+optimist@^0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
+  integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY=
   dependencies:
-    deep-is "~0.1.3"
-    fast-levenshtein "~2.0.4"
-    levn "~0.3.0"
-    prelude-ls "~1.1.2"
-    type-check "~0.3.2"
-    wordwrap "~1.0.0"
+    minimist "~0.0.1"
+    wordwrap "~0.0.2"
 
-optionator@^0.8.3:
+optionator@^0.8.1, optionator@^0.8.3:
   version "0.8.3"
   resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
   integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
@@ -7167,9 +6975,9 @@ p-is-promise@^3.0.0:
   integrity sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==
 
 p-limit@^2.0.0, p-limit@^2.2.0:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.1.tgz#aa07a788cc3151c939b5131f63570f0dd2009537"
-  integrity sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e"
+  integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==
   dependencies:
     p-try "^2.0.0"
 
@@ -7370,9 +7178,9 @@ path-key@^2.0.0, path-key@^2.0.1:
   integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
 
 path-key@^3.0.0, path-key@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.0.tgz#99a10d870a803bdd5ee6f0470e58dfcd2f9a54d3"
-  integrity sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg==
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+  integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
 
 path-parse@^1.0.6:
   version "1.0.6"
@@ -7485,16 +7293,11 @@ pgpass@1.x:
   dependencies:
     split "^1.0.0"
 
-picomatch@^2.0.4:
+picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.0.7:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a"
   integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==
 
-picomatch@^2.0.5:
-  version "2.0.7"
-  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6"
-  integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==
-
 pify@^2.0.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -7546,17 +7349,6 @@ plugin-error@1.0.1, plugin-error@^1.0.1:
     arr-union "^3.1.0"
     extend-shallow "^3.0.2"
 
-plugin-error@^0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-0.1.2.tgz#3b9bb3335ccf00f425e07437e19276967da47ace"
-  integrity sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=
-  dependencies:
-    ansi-cyan "^0.1.1"
-    ansi-red "^0.1.1"
-    arr-diff "^1.0.1"
-    arr-union "^2.0.1"
-    extend-shallow "^1.1.2"
-
 pn@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
@@ -7572,10 +7364,10 @@ popper.js@^1.16.0:
   resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.0.tgz#2e1816bcbbaa518ea6c2e15a466f4cb9c6e2fbb3"
   integrity sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw==
 
-portal-vue@^2.1.6:
-  version "2.1.6"
-  resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-2.1.6.tgz#a7d4790b14a79af7fd159a60ec88c30cddc6c639"
-  integrity sha512-lvCF85D4e8whd0nN32D8FqKwwkk7nYUI3Ku8UAEx4Z1reomu75dv5evRUTZNaj1EalxxWNXiNl0EHRq36fG8WA==
+portal-vue@2.1.7, portal-vue@^2.1.6:
+  version "2.1.7"
+  resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-2.1.7.tgz#ea08069b25b640ca08a5b86f67c612f15f4e4ad4"
+  integrity sha512-+yCno2oB3xA7irTt0EU5Ezw22L2J51uKAacE/6hMPMoO/mx3h4rXFkkBkT4GFsMDv/vEe8TNKC3ujJJ0PTwb6g==
 
 portscanner@2.2.0:
   version "2.2.0"
@@ -7879,7 +7671,7 @@ postcss-selector-parser@^3.0.0:
     indexes-of "^1.0.1"
     uniq "^1.0.1"
 
-postcss-selector-parser@^5.0.0, postcss-selector-parser@^5.0.0-rc.4:
+postcss-selector-parser@^5.0.0-rc.4:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c"
   integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==
@@ -7926,28 +7718,10 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2:
   resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz#482282c09a42706d1fc9a069b73f44ec08391dc9"
   integrity sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==
 
-postcss@^7.0.0, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.5, postcss@^7.0.6:
-  version "7.0.18"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.18.tgz#4b9cda95ae6c069c67a4d933029eddd4838ac233"
-  integrity sha512-/7g1QXXgegpF+9GJj4iN7ChGF40sYuGYJ8WZu8DZWnmhQ/G36hfdk3q9LBJmoK+lZ+yzZ5KYpOoxq7LF1BxE8g==
-  dependencies:
-    chalk "^2.4.2"
-    source-map "^0.6.1"
-    supports-color "^6.1.0"
-
-postcss@^7.0.1:
-  version "7.0.23"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.23.tgz#9f9759fad661b15964f3cfc3140f66f1e05eadc1"
-  integrity sha512-hOlMf3ouRIFXD+j2VJecwssTwbvsPGJVMzupptg+85WA+i7MwyrydmQAgY3R+m0Bc0exunhbJmijy8u8+vufuQ==
-  dependencies:
-    chalk "^2.4.2"
-    source-map "^0.6.1"
-    supports-color "^6.1.0"
-
-postcss@^7.0.23:
-  version "7.0.25"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.25.tgz#dd2a2a753d50b13bed7a2009b4a18ac14d9db21e"
-  integrity sha512-NXXVvWq9icrm/TgQC0O6YVFi4StfJz46M1iNd/h6B26Nvh/HKI+q4YZtFN/EjcInZliEscO/WL10BXnc1E5nwg==
+postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.23, postcss@^7.0.5, postcss@^7.0.6:
+  version "7.0.26"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.26.tgz#5ed615cfcab35ba9bbb82414a4fa88ea10429587"
+  integrity sha512-IY4oRjpXWYshuTDFxMVkJDtWIk2LhsTlu8bZnbEJA4+bYT16Lvpo8Qv6EvDumhYRgzjZl489pmsY3qVgJQ08nA==
   dependencies:
     chalk "^2.4.2"
     source-map "^0.6.1"
@@ -8096,12 +7870,12 @@ promise-sequential@1.1.1:
   integrity sha1-956JUO+G56eoW/MgRSZDWS9tL7I=
 
 promise.prototype.finally@^3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-3.1.1.tgz#cb279d3a5020ca6403b3d92357f8e22d50ed92aa"
-  integrity sha512-gnt8tThx0heJoI3Ms8a/JdkYBVhYP/wv+T7yQimR+kdOEJL21xTFbiJhMRqnSPcr54UVvMbsscDk2w+ivyaLPw==
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-3.1.2.tgz#b8af89160c9c673cefe3b4c4435b53cfd0287067"
+  integrity sha512-A2HuJWl2opDH0EafgdjwEw7HysI8ff/n4lW4QEVBCUXFk9QeGecBWv0Deph0UmLe3tTNYegz8MOjsVuE6SMoJA==
   dependencies:
     define-properties "^1.1.3"
-    es-abstract "^1.13.0"
+    es-abstract "^1.17.0-next.0"
     function-bind "^1.1.1"
 
 promise@^7.0.1:
@@ -8127,9 +7901,9 @@ pseudomap@^1.0.2:
   integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
 
 psl@^1.1.24, psl@^1.1.28:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2"
-  integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c"
+  integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==
 
 public-encrypt@^4.0.0:
   version "4.0.3"
@@ -8321,9 +8095,9 @@ qrcode@1.4.4:
     yargs "^13.2.4"
 
 qs@^6.4.0, qs@^6.5.2:
-  version "6.9.0"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.0.tgz#d1297e2a049c53119cb49cca366adbbacc80b409"
-  integrity sha512-27RP4UotQORTpmNQDX8BHPukOnBP3p1uUJY5UnDhaJB+rMt9iMsok724XL+UHU23bEFOHRMQ2ZhI99qOWUMGFA==
+  version "6.9.1"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.1.tgz#20082c65cb78223635ab1a9eaca8875a29bf8ec9"
+  integrity sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA==
 
 qs@~6.5.2:
   version "6.5.2"
@@ -8415,9 +8189,9 @@ read-pkg@^1.0.0:
     path-type "^1.0.0"
 
 "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
-  version "2.3.6"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
-  integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
+  version "2.3.7"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
+  integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
   dependencies:
     core-util-is "~1.0.0"
     inherits "~2.0.3"
@@ -8438,9 +8212,9 @@ readable-stream@1.1.x:
     string_decoder "~0.10.x"
 
 "readable-stream@2 || 3", readable-stream@^3.0.1, readable-stream@^3.1.1:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc"
-  integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.5.0.tgz#465d70e6d1087f6162d079cd0b5db7fbebfd1606"
+  integrity sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA==
   dependencies:
     inherits "^2.0.3"
     string_decoder "^1.1.1"
@@ -8467,6 +8241,13 @@ readdirp@~3.2.0:
   dependencies:
     picomatch "^2.0.4"
 
+readdirp@~3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17"
+  integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==
+  dependencies:
+    picomatch "^2.0.7"
+
 recaptcha-promise@0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/recaptcha-promise/-/recaptcha-promise-0.1.3.tgz#7d3d66d045a53674054ebdfa1684e0609ef5d912"
@@ -8595,11 +8376,6 @@ repeat-string@^1.5.2, repeat-string@^1.6.1:
   resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
   integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
 
-replace-ext@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924"
-  integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=
-
 replace-ext@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
@@ -8637,7 +8413,7 @@ request-promise-core@1.1.3:
   dependencies:
     lodash "^4.17.15"
 
-request-promise-native@1.0.7, request-promise-native@^1.0.7:
+request-promise-native@1.0.7:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59"
   integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==
@@ -8646,7 +8422,7 @@ request-promise-native@1.0.7, request-promise-native@^1.0.7:
     stealthy-require "^1.1.1"
     tough-cookie "^2.3.3"
 
-request-promise-native@1.0.8:
+request-promise-native@1.0.8, request-promise-native@^1.0.7:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36"
   integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==
@@ -8760,9 +8536,9 @@ resolve-url@^0.2.1:
   integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
 
 resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0:
-  version "1.12.0"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6"
-  integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==
+  version "1.14.2"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.14.2.tgz#dbf31d0fa98b1f29aa5169783b9c290cb865fea2"
+  integrity sha512-EjlOBLBO1kxsUxsKjLt7TAECyKW6fOh1VRkykQkKGzcBbjjPIxBqGh0jf7GJ3k/f5mxMqW3htMD3WdTUVtW8HQ==
   dependencies:
     path-parse "^1.0.6"
 
@@ -8810,7 +8586,7 @@ rimraf@3.0.0:
   dependencies:
     glob "^7.1.3"
 
-rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@^2.7.1:
+rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@^2.7.1:
   version "2.7.1"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
   integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
@@ -8857,10 +8633,10 @@ run-queue@^1.0.0, run-queue@^1.0.3:
   dependencies:
     aproba "^1.1.1"
 
-rxjs@^6.4.0:
-  version "6.5.3"
-  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a"
-  integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==
+rxjs@^6.5.3:
+  version "6.5.4"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c"
+  integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==
   dependencies:
     tslib "^1.9.0"
 
@@ -8886,11 +8662,29 @@ safe-regex@^1.1.0:
   dependencies:
     ret "~0.1.10"
 
-"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@^2.1.2, safer-buffer@~2.1.0:
+"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
+sass-loader@8.0.1:
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.1.tgz#46cc2bc90de9ed0a121d023800a32049215fb57c"
+  integrity sha512-ANR2JHuoxzCI+OPDA0hJBv1Y16A2021hucu0S3DOGgpukKzq9W+4vX9jhIqs4qibT5E7RIRsHMMrN0kdF5nUig==
+  dependencies:
+    clone-deep "^4.0.1"
+    loader-utils "^1.2.3"
+    neo-async "^2.6.1"
+    schema-utils "^2.6.1"
+    semver "^7.1.1"
+
+sass@1.25.0, sass@^1.10.3:
+  version "1.25.0"
+  resolved "https://registry.yarnpkg.com/sass/-/sass-1.25.0.tgz#f8bd7dfbb39d6b0305e27704a8ebe637820693f3"
+  integrity sha512-uQMjye0Y70SEDGO56n0j91tauqS9E1BmpKHtiYNQScXDHeaE9uHwNEqQNFf4Bes/3DHMNinB6u79JsG10XWNyw==
+  dependencies:
+    chokidar ">=2.0.0 <4.0.0"
+
 sax@1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
@@ -8917,26 +8711,10 @@ schema-utils@^1.0.0:
     ajv-errors "^1.0.0"
     ajv-keywords "^3.1.0"
 
-schema-utils@^2.0.1:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.4.1.tgz#e89ade5d056dc8bcaca377574bb4a9c4e1b8be56"
-  integrity sha512-RqYLpkPZX5Oc3fw/kHHHyP56fg5Y+XBpIpV8nCg0znIALfq3OH+Ea9Hfeac9BAMwG5IICltiZ0vxFvJQONfA5w==
-  dependencies:
-    ajv "^6.10.2"
-    ajv-keywords "^3.4.1"
-
-schema-utils@^2.5.0:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.5.0.tgz#8f254f618d402cc80257486213c8970edfd7c22f"
-  integrity sha512-32ISrwW2scPXHUSusP8qMg5dLUawKkyV+/qIEV9JdXKx+rsM6mi8vZY8khg2M69Qom16rtroWXD3Ybtiws38gQ==
-  dependencies:
-    ajv "^6.10.2"
-    ajv-keywords "^3.4.1"
-
-schema-utils@^2.6.0, schema-utils@^2.6.1:
-  version "2.6.1"
-  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.1.tgz#eb78f0b945c7bcfa2082b3565e8db3548011dc4f"
-  integrity sha512-0WXHDs1VDJyo+Zqs9TKLKyD/h7yDpHUhEFsM2CzkICFdoX1av+GBq/J2xRTFfsQO5kBfhZzANf2VcIm84jqDbg==
+schema-utils@^2.0.1, schema-utils@^2.5.0, schema-utils@^2.6.0, schema-utils@^2.6.1:
+  version "2.6.4"
+  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.4.tgz#a27efbf6e4e78689d91872ee3ccfa57d7bdd0f53"
+  integrity sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==
   dependencies:
     ajv "^6.10.2"
     ajv-keywords "^3.4.1"
@@ -8983,6 +8761,11 @@ semver@^6.0.0, semver@^6.1.2, semver@^6.3.0:
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
   integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
 
+semver@^7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.1.tgz#29104598a197d6cbe4733eeecbe968f7b43a9667"
+  integrity sha512-WfuG+fl6eh3eZ2qAf6goB7nhiCd7NPXhmyFxigB/TOkQyeLP8w8GsVehvtGNtnNmyboz4TgeK40B1Kbql/8c5A==
+
 serialize-javascript@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61"
@@ -9026,6 +8809,13 @@ sha.js@^2.4.0, sha.js@^2.4.8:
     inherits "^2.0.1"
     safe-buffer "^5.0.1"
 
+shallow-clone@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
+  integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
+  dependencies:
+    kind-of "^6.0.2"
+
 sharp@0.23.4:
   version "0.23.4"
   resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.23.4.tgz#ca36067cb6ff7067fa6c77b01651cb9a890f8eb3"
@@ -9162,9 +8952,9 @@ sort-keys@^2.0.0:
     is-plain-obj "^1.0.0"
 
 sortablejs@^1.10.1:
-  version "1.10.1"
-  resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.1.tgz#3d52b00f871be00f00f84d99a60d120bf3dfe52c"
-  integrity sha512-N6r7GrVmO8RW1rn0cTdvK3JR0BcqecAJ0PmYMCL3ZuqTH3pY+9QyqkmJSkkLyyDvd+AJnwaxTP22Ybr/83V9hQ==
+  version "1.10.2"
+  resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.2.tgz#6e40364d913f98b85a14f6678f92b5c1221f5290"
+  integrity sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==
 
 source-list-map@^2.0.0:
   version "2.0.1"
@@ -9172,20 +8962,20 @@ source-list-map@^2.0.0:
   integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
 
 source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
-  version "0.5.2"
-  resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259"
-  integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
+  integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
   dependencies:
-    atob "^2.1.1"
+    atob "^2.1.2"
     decode-uri-component "^0.2.0"
     resolve-url "^0.2.1"
     source-map-url "^0.4.0"
     urix "^0.1.0"
 
 source-map-support@^0.5.6, source-map-support@~0.5.12:
-  version "0.5.13"
-  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932"
-  integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==
+  version "0.5.16"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042"
+  integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==
   dependencies:
     buffer-from "^1.0.0"
     source-map "^0.6.0"
@@ -9295,12 +9085,12 @@ ssri@^6.0.1:
     figgy-pudding "^3.5.1"
 
 ssri@^7.0.0:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/ssri/-/ssri-7.0.1.tgz#b0cab7bbb11ac9ea07f003453e2011f8cbed9f34"
-  integrity sha512-FfndBvkXL9AHyGLNzU3r9AvYIBBZ7gm+m+kd0p8cT3/v4OliMAyipZAhLVEv1Zi/k4QFq9CstRGVd9pW/zcHFQ==
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/ssri/-/ssri-7.1.0.tgz#92c241bf6de82365b5c7fb4bd76e975522e1294d"
+  integrity sha512-77/WrDZUWocK0mvA5NTRQyveUf+wsrIc6vyrxpS8tVvYBcX215QbafrJR3KtkpskIzoFLqqNuuYQvxaMjXJ/0g==
   dependencies:
     figgy-pudding "^3.5.1"
-    minipass "^3.0.0"
+    minipass "^3.1.1"
 
 stable@^0.1.8:
   version "0.1.8"
@@ -9375,9 +9165,9 @@ stream-parser@~0.3.1:
     debug "2"
 
 stream-shift@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
-  integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
+  integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
 
 streamsearch@0.1.2:
   version "0.1.2"
@@ -9419,18 +9209,18 @@ string-width@^4.1.0, string-width@^4.2.0:
     is-fullwidth-code-point "^3.0.0"
     strip-ansi "^6.0.0"
 
-string.prototype.trimleft@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634"
-  integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==
+string.prototype.trimleft@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74"
+  integrity sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==
   dependencies:
     define-properties "^1.1.3"
     function-bind "^1.1.1"
 
-string.prototype.trimright@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58"
-  integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==
+string.prototype.trimright@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9"
+  integrity sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==
   dependencies:
     define-properties "^1.1.3"
     function-bind "^1.1.1"
@@ -9522,11 +9312,11 @@ strip-json-comments@^3.0.1:
   integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==
 
 strtok3@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-5.0.1.tgz#f89e09fa024dff82e39c160faf8d8706c24e7d21"
-  integrity sha512-AWliiIjyb87onqO8pM+1Hozm+PPcR4YYIWbFUT5OKQ+tOMwgdT8HwJd/IS8v3/gKdAtE5aE2p3FhcWqryuZPLQ==
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-5.0.2.tgz#bb81f1f56742e16f1a30ccce5dc3d9498aa5475a"
+  integrity sha512-EFeVpFC5qDsqPEJSrIYyS/ueFBknGhgSK9cW+YAJF/cgJG/KSjoK7X6rK5xnpcLe7y1LVkVFCXWbAb+ClNKzKQ==
   dependencies:
-    "@tokenizer/token" "^0.1.0"
+    "@tokenizer/token" "^0.1.1"
     debug "^4.1.1"
     peek-readable "^3.1.0"
 
@@ -9547,29 +9337,6 @@ stylehacks@^4.0.0:
     postcss "^7.0.0"
     postcss-selector-parser "^3.0.0"
 
-stylus-loader@3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/stylus-loader/-/stylus-loader-3.0.2.tgz#27a706420b05a38e038e7cacb153578d450513c6"
-  integrity sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA==
-  dependencies:
-    loader-utils "^1.0.2"
-    lodash.clonedeep "^4.5.0"
-    when "~3.6.x"
-
-stylus@0.54.7, stylus@^0.54.0:
-  version "0.54.7"
-  resolved "https://registry.yarnpkg.com/stylus/-/stylus-0.54.7.tgz#c6ce4793965ee538bcebe50f31537bfc04d88cd2"
-  integrity sha512-Yw3WMTzVwevT6ZTrLCYNHAFmanMxdylelL3hkWNgPMeTCpMwpV3nXjpOHuBXtFv7aiO2xRuQS6OoAdgkNcSNug==
-  dependencies:
-    css-parse "~2.0.0"
-    debug "~3.1.0"
-    glob "^7.1.3"
-    mkdirp "~0.5.x"
-    safer-buffer "^2.1.2"
-    sax "~1.2.4"
-    semver "^6.0.0"
-    source-map "^0.7.3"
-
 summaly@2.3.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/summaly/-/summaly-2.3.1.tgz#028ef4d206a5da06ae5c723fc60719d38368d6f2"
@@ -9706,9 +9473,9 @@ syslog-pro@1.0.0:
     moment "^2.22.2"
 
 systeminformation@*:
-  version "4.15.3"
-  resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.15.3.tgz#639cdc224b5c3811f1a0bfba33f869bbc6fb930f"
-  integrity sha512-Fx2ARGHtLl2/xLeNoTR8/doXSxUXuAzIN+dyCK9O43j/UETLBt77yTEbTxmYsVD47PYjX1iQTdcY41CZckY+zg==
+  version "4.19.1"
+  resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.19.1.tgz#2f43204152feaf55d8692b61b855f16287817115"
+  integrity sha512-6Jb+0aepgRb2B4vFgxKKcrr8QexhERkPX1z0EzIyCKDMQjILt+ZbNtFxrGjFwa67tn9Wbqs1L5t/CjCUwB1FcA==
 
 systeminformation@4.17.3:
   version "4.17.3"
@@ -9756,19 +9523,6 @@ tar-stream@^2.0.0:
     inherits "^2.0.3"
     readable-stream "^3.1.1"
 
-tar@^4:
-  version "4.4.13"
-  resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
-  integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
-  dependencies:
-    chownr "^1.1.1"
-    fs-minipass "^1.2.5"
-    minipass "^2.8.6"
-    minizlib "^1.2.1"
-    mkdirp "^0.5.0"
-    safe-buffer "^5.1.2"
-    yallist "^3.0.3"
-
 tar@^5.0.5:
   version "5.0.5"
   resolved "https://registry.yarnpkg.com/tar/-/tar-5.0.5.tgz#03fcdb7105bc8ea3ce6c86642b9c942495b04f93"
@@ -9810,28 +9564,10 @@ terser-webpack-plugin@^1.4.3:
     webpack-sources "^1.4.0"
     worker-farm "^1.7.0"
 
-terser@^4.0.0:
-  version "4.4.2"
-  resolved "https://registry.yarnpkg.com/terser/-/terser-4.4.2.tgz#448fffad0245f4c8a277ce89788b458bfd7706e8"
-  integrity sha512-Uufrsvhj9O1ikwgITGsZ5EZS6qPokUOkCegS7fYOdGTv+OA90vndUbU6PEjr5ePqHfNUbGyMO7xyIZv2MhsALQ==
-  dependencies:
-    commander "^2.20.0"
-    source-map "~0.6.1"
-    source-map-support "~0.5.12"
-
-terser@^4.1.2:
-  version "4.3.8"
-  resolved "https://registry.yarnpkg.com/terser/-/terser-4.3.8.tgz#707f05f3f4c1c70c840e626addfdb1c158a17136"
-  integrity sha512-otmIRlRVmLChAWsnSFNO0Bfk6YySuBp6G9qrHiJwlLDd4mxe2ta4sjI7TzIR+W1nBMjilzrMcPOz9pSusgx3hQ==
-  dependencies:
-    commander "^2.20.0"
-    source-map "~0.6.1"
-    source-map-support "~0.5.12"
-
-terser@^4.4.3:
-  version "4.4.3"
-  resolved "https://registry.yarnpkg.com/terser/-/terser-4.4.3.tgz#401abc52b88869cf904412503b1eb7da093ae2f0"
-  integrity sha512-0ikKraVtRDKGzHrzkCv5rUNDzqlhmhowOBqC0XqUHFpW+vJ45+20/IFBcebwKfiS2Z9fJin6Eo+F1zLZsxi8RA==
+terser@^4.0.0, terser@^4.1.2, terser@^4.4.3:
+  version "4.6.3"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.3.tgz#e33aa42461ced5238d352d2df2a67f21921f8d87"
+  integrity sha512-Lw+ieAXmY69d09IIc/yqeBqXpEQIpDGZqT34ui1QWXIUpR2RjbqEkT8X7Lgex19hslSqcWM5iMN2kM11eMsESQ==
   dependencies:
     commander "^2.20.0"
     source-map "~0.6.1"
@@ -9848,9 +9584,9 @@ textarea-caret@3.1.0:
   integrity sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==
 
 textextensions@2:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.5.0.tgz#e21d3831dafa37513dd80666dff541414e314293"
-  integrity sha512-1IkVr355eHcomgK7fgj1Xsokturx6L5S2JRT5WcRdA6v5shk9sxWuO/w/VbpQexwkXJMQIa/j1dBi3oo7+HhcA==
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.6.0.tgz#d7e4ab13fe54e32e08873be40d51b74229b00fc4"
+  integrity sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ==
 
 thenify-all@^1.0.0:
   version "1.6.0"
@@ -10161,11 +9897,6 @@ type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5:
   resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
   integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
 
-type-fest@^0.3.0:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1"
-  integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==
-
 type-fest@^0.8.1:
   version "0.8.1"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
@@ -10189,6 +9920,11 @@ type@^1.0.1:
   resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
   integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
 
+type@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3"
+  integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==
+
 typedarray-to-buffer@^3.1.5:
   version "3.1.5"
   resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
@@ -10226,7 +9962,7 @@ typescript@3.7.4:
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.4.tgz#1743a5ec5fef6a1fa9f3e4708e33c81c73876c19"
   integrity sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw==
 
-uglify-js@^2.6.1, uglify-js@^2.7.0:
+uglify-js@^2.6.1:
   version "2.8.29"
   resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
   integrity sha1-KcVzMUgFe7Th913zW3qcty5qWd0=
@@ -10236,12 +9972,12 @@ uglify-js@^2.6.1, uglify-js@^2.7.0:
   optionalDependencies:
     uglify-to-browserify "~1.0.0"
 
-uglify-js@^3.5.1:
-  version "3.6.1"
-  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.1.tgz#ae7688c50e1bdcf2f70a0e162410003cf9798311"
-  integrity sha512-+dSJLJpXBb6oMHP+Yvw8hUgElz4gLTh82XuX68QiJVTXaE5ibl6buzhNkQdYhBlIhozWOC9ge16wyRmjG4TwVQ==
+uglify-js@^3.1.4, uglify-js@^3.5.1:
+  version "3.7.5"
+  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.5.tgz#278c7c24927ac5a32d3336fc68fd4ae1177a486a"
+  integrity sha512-GFZ3EXRptKGvb/C1Sq6nO1iI7AGcjyqmIyOw0DrD0675e+NNbGO72xmMM2iEBdFbxaTLo70NbjM/Wy54uZIlsg==
   dependencies:
-    commander "2.20.0"
+    commander "~2.20.3"
     source-map "~0.6.1"
 
 uglify-to-browserify@~1.0.0:
@@ -10412,12 +10148,14 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1:
   integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
 
 util.promisify@^1.0.0, util.promisify@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"
-  integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee"
+  integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==
   dependencies:
-    define-properties "^1.1.2"
-    object.getownpropertydescriptors "^2.0.3"
+    define-properties "^1.1.3"
+    es-abstract "^1.17.2"
+    has-symbols "^1.0.1"
+    object.getownpropertydescriptors "^2.1.0"
 
 util@0.10.3:
   version "0.10.3"
@@ -10438,11 +10176,16 @@ uuid@3.3.2:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
   integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
 
-uuid@3.3.3, uuid@^3.3.2, uuid@^3.3.3:
+uuid@3.3.3:
   version "3.3.3"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866"
   integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==
 
+uuid@^3.3.2, uuid@^3.3.3:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
+  integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
+
 v-animate-css@0.0.3:
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/v-animate-css/-/v-animate-css-0.0.3.tgz#fa3f41e5995404f0e5a741cdb2c576ce01f9df16"
@@ -10565,9 +10308,9 @@ vinyl@^2.0.0, vinyl@^2.1.0:
     replace-ext "^1.0.0"
 
 vm-browserify@^1.0.1:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019"
-  integrity sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
+  integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
 
 void-elements@^2.0.1:
   version "2.0.1"
@@ -10625,11 +10368,6 @@ vue-i18n@8.15.3:
   resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-8.15.3.tgz#9f947802d9b734fcb92e2ce724da654f2f9fc0f4"
   integrity sha512-PVNgo6yhOmacZVFjSapZ314oewwLyXHjJwAqjnaPN1GJAJd/dvsrShGzSiJuCX4Hc36G4epJvNXUwO8y7wEKew==
 
-vue-js-modal@1.3.31:
-  version "1.3.31"
-  resolved "https://registry.yarnpkg.com/vue-js-modal/-/vue-js-modal-1.3.31.tgz#fdece823d4f2816c8b1075c1fd8f667df11f5a42"
-  integrity sha512-gwt2904sWbMUuUcHwKQ510IEs4G7S3bqVWLYeTOc2eEyWMmmnT9UmojDsXIexFnPVM7cZTua37z3Jm/h0i0y8Q==
-
 vue-json-pretty@1.6.3:
   version "1.6.3"
   resolved "https://registry.yarnpkg.com/vue-json-pretty/-/vue-json-pretty-1.6.3.tgz#c7f378f3c9f68977047de28197735bc2cf81b15b"
@@ -10653,6 +10391,13 @@ vue-marquee-text-component@1.1.1:
   dependencies:
     vue "^2.5.17"
 
+vue-meta@2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/vue-meta/-/vue-meta-2.3.1.tgz#32a1c2634f49433f30e7e7a028aa5e5743f84f6a"
+  integrity sha512-hnZvDNvLh+PefJLfYkZhG6cSBNKikgQyiEK8lI/P2qscM1DC/qHHOfdACPQ/VDnlaWU9VlcobCTNyVtssTR4XQ==
+  dependencies:
+    deepmerge "^4.0.0"
+
 vue-prism-component@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/vue-prism-component/-/vue-prism-component-1.1.1.tgz#df0e375f7f9b367b069b2d54e6ed86facde96030"
@@ -10700,16 +10445,11 @@ vue-template-es2015-compiler@^1.9.0:
   resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
   integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
 
-vue@2.6.11:
+vue@2.6.11, vue@^2.5.13, vue@^2.5.17:
   version "2.6.11"
   resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5"
   integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==
 
-vue@^2.5.13, vue@^2.5.16, vue@^2.5.17:
-  version "2.6.10"
-  resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"
-  integrity sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==
-
 vuedraggable@2.23.2:
   version "2.23.2"
   resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-2.23.2.tgz#0d95d7fdf4f02f56755a26b3c9dca5c7ca9cfa72"
@@ -10717,13 +10457,6 @@ vuedraggable@2.23.2:
   dependencies:
     sortablejs "^1.10.1"
 
-vuewordcloud@18.7.11:
-  version "18.7.11"
-  resolved "https://registry.yarnpkg.com/vuewordcloud/-/vuewordcloud-18.7.11.tgz#285ae2b6d93ac6f1da6e60141227f68a64b30f24"
-  integrity sha1-KFrittk6xvHabmAUEif2imSzDyQ=
-  dependencies:
-    vue "^2.5.16"
-
 vuex-persistedstate@2.7.0:
   version "2.7.0"
   resolved "https://registry.yarnpkg.com/vuex-persistedstate/-/vuex-persistedstate-2.7.0.tgz#f60aae4e1163bf293696a625526dbffaa42e429e"
@@ -10857,24 +10590,14 @@ whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0:
   integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
 
 whatwg-url@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd"
-  integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
+  integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
   dependencies:
     lodash.sortby "^4.7.0"
     tr46 "^1.0.1"
     webidl-conversions "^4.0.2"
 
-when@^3.7.7:
-  version "3.7.8"
-  resolved "https://registry.yarnpkg.com/when/-/when-3.7.8.tgz#c7130b6a7ea04693e842cdc9e7a1f2aa39a39f82"
-  integrity sha1-xxMLan6gRpPoQs3J56Hyqjmjn4I=
-
-when@~3.6.x:
-  version "3.6.4"
-  resolved "https://registry.yarnpkg.com/when/-/when-3.6.4.tgz#473b517ec159e2b85005497a13983f095412e34e"
-  integrity sha1-RztRfsFZ4rhQBUl6E5g/CVQS404=
-
 which-module@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
@@ -10898,9 +10621,9 @@ which@1.3.1, which@^1.1.1, which@^1.2.14, which@^1.2.9, which@^1.3.1:
     isexe "^2.0.0"
 
 which@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/which/-/which-2.0.1.tgz#f1cf94d07a8e571b6ff006aeb91d0300c47ef0a4"
-  integrity sha512-N7GBZOTswtB9lkQBZA4+zAXrjEIWAUOB93AvzUiudRzRxhUdLURQ7D/gAIMY1gatT/LTbmbcv8SiYazy3eYB7w==
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+  integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
   dependencies:
     isexe "^2.0.0"
 
@@ -10934,10 +10657,10 @@ wordwrap@0.0.2:
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
   integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=
 
-wordwrap@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
-  integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
+wordwrap@~0.0.2:
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+  integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc=
 
 worker-farm@^1.7.0:
   version "1.7.0"
@@ -11005,18 +10728,11 @@ write@1.0.3:
   dependencies:
     mkdirp "^0.5.1"
 
-ws@7.2.1:
+ws@7.2.1, ws@^7.0.0:
   version "7.2.1"
   resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e"
   integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==
 
-ws@^7.0.0:
-  version "7.1.2"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.1.2.tgz#c672d1629de8bb27a9699eb599be47aeeedd8f73"
-  integrity sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg==
-  dependencies:
-    async-limiter "^1.0.0"
-
 xev@2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/xev/-/xev-2.0.1.tgz#24484173a22115bc8a990ef5d4d5129695b827a7"
@@ -11043,12 +10759,11 @@ xml2js@0.4.19:
     xmlbuilder "~9.0.1"
 
 xml2js@^0.4.17:
-  version "0.4.22"
-  resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.22.tgz#4fa2d846ec803237de86f30aa9b5f70b6600de02"
-  integrity sha512-MWTbxAQqclRSTnehWWe5nMKzI3VmJ8ltiJEco8akcC6j3miOhjjfzKum5sId+CWhfxdOs/1xauYr8/ZDBtQiRw==
+  version "0.4.23"
+  resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
+  integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==
   dependencies:
     sax ">=0.6.0"
-    util.promisify "~1.0.0"
     xmlbuilder "~11.0.0"
 
 xmlbuilder@~11.0.0:
@@ -11091,7 +10806,7 @@ yallist@^2.1.2:
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
   integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
 
-yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3:
+yallist@^3.0.2:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
   integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
@@ -11167,7 +10882,7 @@ yargs@13.2.4:
     y18n "^4.0.0"
     yargs-parser "^13.1.0"
 
-yargs@13.3.0, yargs@^13.0.0, yargs@^13.2.1, yargs@^13.2.4, yargs@^13.3.0:
+yargs@13.3.0, yargs@^13.2.1, yargs@^13.2.4, yargs@^13.3.0:
   version "13.3.0"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83"
   integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==
@@ -11201,9 +10916,9 @@ yargs@^14.2:
     yargs-parser "^15.0.0"
 
 yargs@^15.0.0:
-  version "15.0.2"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.0.2.tgz#4248bf218ef050385c4f7e14ebdf425653d13bd3"
-  integrity sha512-GH/X/hYt+x5hOat4LMnCqMd8r5Cv78heOMIJn1hr7QPPBqfeC6p89Y78+WB9yGDvfpCvgasfmWLzNzEioOUD9Q==
+  version "15.1.0"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.1.0.tgz#e111381f5830e863a89550bd4b136bb6a5f37219"
+  integrity sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg==
   dependencies:
     cliui "^6.0.0"
     decamelize "^1.2.0"