diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 6a684eaa48..3e9585f96d 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -17,14 +17,14 @@ jobs:
 
     services:
       postgres:
-        image: postgres:12.2-alpine
+        image: postgres:13
         ports:
           - 54312:5432
         env:
           POSTGRES_DB: test-misskey
           POSTGRES_HOST_AUTH_METHOD: trust
       redis:
-        image: redis:4.0-alpine
+        image: redis:6
         ports:
           - 56312:6379
 
@@ -51,19 +51,21 @@ jobs:
     runs-on: ubuntu-latest
 
     strategy:
+      fail-fast: false
       matrix:
         node-version: [16.x]
+        browser: [chrome]
 
     services:
       postgres:
-        image: postgres:12.2-alpine
+        image: postgres:13
         ports:
           - 54312:5432
         env:
           POSTGRES_DB: test-misskey
           POSTGRES_HOST_AUTH_METHOD: trust
       redis:
-        image: redis:4.0-alpine
+        image: redis:6
         ports:
           - 56312:6379
 
@@ -71,6 +73,12 @@ jobs:
     - uses: actions/checkout@v2
       with:
         submodules: true
+    # https://github.com/cypress-io/cypress-docker-images/issues/150
+    #- name: Install mplayer for FireFox
+    #  run: sudo apt install mplayer -y
+    #  if: ${{ matrix.browser == 'firefox' }}
+    #- uses: browser-actions/setup-firefox@latest
+    #  if: ${{ matrix.browser == 'firefox' }}
     - name: Use Node.js ${{ matrix.node-version }}
       uses: actions/setup-node@v1
       with:
@@ -87,5 +95,24 @@ jobs:
       run: cp .github/misskey/test.yml .config
     - name: Build
       run: yarn build
-    - name: Test
-      run: yarn e2e
+    # https://github.com/cypress-io/cypress/issues/4351#issuecomment-559489091
+    - name: ALSA Env
+      run: echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc
+    - name: Cypress run
+      uses: cypress-io/github-action@v2
+      with:
+        install: false
+        start: npm run start:test
+        wait-on: 'http://localhost:61812'
+        headless: false
+        browser: ${{ matrix.browser }}
+    - uses: actions/upload-artifact@v2
+      if: failure()
+      with:
+        name: ${{ matrix.browser }}-cypress-screenshots
+        path: cypress/screenshots
+    - uses: actions/upload-artifact@v2
+      if: always()
+      with:
+        name: ${{ matrix.browser }}-cypress-videos
+        path: cypress/videos
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f4cd8249c4..1611a7e47b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,8 +12,16 @@
 ### Changes
 - Room機能が削除されました
   - 後日別リポジトリとして復活予定です
+- リバーシ機能が削除されました
+  - 後日別リポジトリとして復活予定です
+- Chat UIが削除されました
+- ノートに添付できるファイルの数が16に増えました
+- カスタム絵文字にSVGを指定した場合、PNGに変換されて表示されるようになりました
 
 ### Improvements
+- カスタム絵文字一括編集機能
+- カスタム絵文字一括インポート
+- 投稿フォームで一時的に投稿するアカウントを切り替えられるように
 
 ### Bugfixes
 
diff --git a/cypress/integration/basic.js b/cypress/integration/basic.js
index a754f41b98..aca44ef15d 100644
--- a/cypress/integration/basic.js
+++ b/cypress/integration/basic.js
@@ -41,8 +41,6 @@ describe('After setup instance', () => {
 			username: 'admin',
 			password: 'pass',
 		}).its('body').as('admin');
-
-		cy.get('@admin');
 	});
 
 	afterEach(() => {
@@ -82,15 +80,11 @@ describe('After user signup', () => {
 			password: 'pass',
 		}).its('body').as('admin');
 
-		cy.get('@admin').then(() => {
-			// ユーザー作成
-			cy.request('POST', '/api/signup', {
-				username: 'alice',
-				password: 'alice1234',
-			}).its('body').as('alice');
-		});
-
-		cy.get('@alice');
+		// ユーザー作成
+		cy.request('POST', '/api/signup', {
+			username: 'alice',
+			password: 'alice1234',
+		}).its('body').as('alice');
 	});
 
 	afterEach(() => {
@@ -145,27 +139,21 @@ describe('After user singed in', () => {
 			password: 'pass',
 		}).its('body').as('admin');
 
-		cy.get('@admin').then(() => {
-			// ユーザー作成
-			cy.request('POST', '/api/signup', {
-				username: 'alice',
-				password: 'alice1234',
-			}).its('body').as('alice');
-		});
+		// ユーザー作成
+		cy.request('POST', '/api/signup', {
+			username: 'alice',
+			password: 'alice1234',
+		}).its('body').as('alice');
 
-		cy.get('@alice').then(() => {
-			cy.visit('/');
+		cy.visit('/');
 
-			cy.intercept('POST', '/api/signin').as('signin');
+		cy.intercept('POST', '/api/signin').as('signin');
 
-			cy.get('[data-cy-signin]').click();
-			cy.get('[data-cy-signin-username] input').type('alice');
-			cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
+		cy.get('[data-cy-signin]').click();
+		cy.get('[data-cy-signin-username] input').type('alice');
+		cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
 
-			cy.wait('@signin').as('signedIn');
-		});
-
-		cy.get('@signedIn');
+		cy.wait('@signin').as('signedIn');
 	});
 
 	afterEach(() => {
diff --git a/cypress/support/index.js b/cypress/support/index.js
index a9ac34476d..9185be344c 100644
--- a/cypress/support/index.js
+++ b/cypress/support/index.js
@@ -20,7 +20,13 @@ import './commands'
 // require('./commands')
 
 Cypress.on('uncaught:exception', (err, runnable) => {
-  if (err.message.includes('ResizeObserver loop limit exceeded')) {
-    return false
-  }
+	if ([
+		// Chrome
+		'ResizeObserver loop limit exceeded',
+
+		// Firefox
+		'ResizeObserver loop completed with undelivered notifications',
+	].some(msg => err.message.includes(msg))) {
+		return false;
+	}
 });
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 5ba09c4a81..b3279d78b8 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -242,7 +242,6 @@ uploadFromUrlDescription: "アップロードしたいファイルのURL"
 uploadFromUrlRequested: "アップロードをリクエストしました"
 uploadFromUrlMayTakeTime: "アップロードが完了するまで時間がかかる場合があります。"
 explore: "みつける"
-games: "Misskey Games"
 messageRead: "既読"
 noMoreHistory: "これより過去の履歴はありません"
 startMessaging: "チャットを開始"
@@ -620,8 +619,11 @@ reportAbuse: "通報"
 reportAbuseOf: "{name}を通報する"
 fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。"
 abuseReported: "内容が送信されました。ご報告ありがとうございました。"
+reporter: "通報者"
 reporteeOrigin: "通報先"
 reporterOrigin: "通報元"
+forwardReport: "リモートインスタンスに通報を転送する"
+forwardReportIsAnonymous: "リモートインスタンスからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。"
 send: "送信"
 abuseMarkAsResolved: "対応済みにする"
 openInNewTab: "新しいタブで開く"
@@ -669,7 +671,6 @@ emailVerified: "メールアドレスが確認されました"
 noteFavoritesCount: "お気に入りノートの数"
 pageLikesCount: "Pageにいいねした数"
 pageLikedCount: "Pageにいいねされた数"
-reversiCount: "リバーシの対局数"
 contact: "連絡先"
 useSystemFont: "システムのデフォルトのフォントを使う"
 clips: "クリップ"
@@ -745,6 +746,7 @@ notRecommended: "非推奨"
 botProtection: "Botプロテクション"
 instanceBlocking: "インスタンスブロック"
 selectAccount: "アカウントを選択"
+switchAccount: "アカウントを切り替え"
 enabled: "有効"
 disabled: "無効"
 quickAction: "クイックアクション"
@@ -957,40 +959,6 @@ _mfm:
   rotate: "回転"
   rotateDescription: "指定した角度で回転させます。"
 
-_reversi:
-  reversi: "リバーシ"
-  gameSettings: "対局の設定"
-  chooseBoard: "ボードを選択"
-  blackOrWhite: "先行/後攻"
-  blackIs: "{name}が黒(先行)"
-  rules: "ルール"
-  botSettings: "Botのオプション"
-  thisGameIsStartedSoon: "対局は数秒後に開始されます"
-  waitingForOther: "相手の準備が完了するのを待っています"
-  waitingForMe: "あなたの準備が完了するのを待っています"
-  waitingBoth: "準備してください"
-  ready: "準備完了"
-  cancelReady: "準備を再開"
-  opponentTurn: "相手のターンです"
-  myTurn: "あなたのターンです"
-  turnOf: "{name}のターンです"
-  pastTurnOf: "{name}のターン"
-  surrender: "投了"
-  surrendered: "投了により"
-  drawn: "引き分け"
-  won: "{name}の勝ち"
-  black: "黒"
-  white: "白"
-  total: "合計"
-  turnCount: "{count}ターン目"
-  myGames: "自分の対局"
-  allGames: "みんなの対局"
-  ended: "終了"
-  playing: "対局中"
-  isLlotheo: "石の少ない方が勝ち(ロセオ)"
-  loopedMap: "ループマップ"
-  canPutEverywhere: "どこでも置けるモード"
-
 _instanceTicker:
   none: "表示しない"
   remote: "リモートユーザーに表示"
@@ -1118,8 +1086,6 @@ _sfx:
   chatBg: "チャット(バックグラウンド)"
   antenna: "アンテナ受信"
   channel: "チャンネル通知"
-  reversiPutBlack: "リバーシ: 黒が打ったとき"
-  reversiPutWhite: "リバーシ: 白が打ったとき"
 
 _ago:
   unknown: "謎"
diff --git a/package.json b/package.json
index c42b71582b..5a4ea315ba 100644
--- a/package.json
+++ b/package.json
@@ -42,12 +42,12 @@
 		"js-yaml": "4.1.0"
 	},
 	"devDependencies": {
-		"@redocly/openapi-core": "1.0.0-beta.54",
-		"@types/fluent-ffmpeg": "2.1.17",
-		"@typescript-eslint/parser": "5.4.0",
+		"@redocly/openapi-core": "1.0.0-beta.79",
+		"@types/fluent-ffmpeg": "2.1.20",
+		"@typescript-eslint/parser": "5.10.0",
 		"cross-env": "7.0.3",
-		"cypress": "9.1.0",
+		"cypress": "9.3.1",
 		"start-server-and-test": "1.14.0",
-		"typescript": "4.5.2"
+		"typescript": "4.5.5"
 	}
 }
diff --git a/packages/backend/migration/1637320813000-forwarded-report.js b/packages/backend/migration/1637320813000-forwarded-report.js
new file mode 100644
index 0000000000..4056f7b5f4
--- /dev/null
+++ b/packages/backend/migration/1637320813000-forwarded-report.js
@@ -0,0 +1,13 @@
+const { QueryRunner } = require('typeorm');
+
+module.exports = class forwardedReport1637320813000 {
+	name = 'forwardedReport1637320813000';
+
+	async up(queryRunner) {
+		await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "forwarded" boolean NOT NULL DEFAULT false`);
+	}
+
+	async down(queryRunner) {
+		await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "forwarded"`);
+	}
+};
diff --git a/packages/backend/migration/1642611822809-emoji-url.js b/packages/backend/migration/1642611822809-emoji-url.js
new file mode 100644
index 0000000000..f229c403f4
--- /dev/null
+++ b/packages/backend/migration/1642611822809-emoji-url.js
@@ -0,0 +1,15 @@
+const { MigrationInterface, QueryRunner } = require("typeorm");
+
+module.exports = class emojiUrl1642611822809 {
+		name = 'emojiUrl1642611822809'
+
+		async up(queryRunner) {
+			await queryRunner.query(`ALTER TABLE "emoji" RENAME COLUMN "url" TO "originalUrl"`);
+			await queryRunner.query(`ALTER TABLE "emoji" ADD "publicUrl" character varying(512) NOT NULL DEFAULT ''`);
+		}
+
+		async down(queryRunner) {
+			await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "publicUrl"`);
+			await queryRunner.query(`ALTER TABLE "emoji" RENAME COLUMN "originalUrl" TO "url"`);
+		}
+}
diff --git a/packages/backend/migration/1642613870898-drive-file-webpublic-type.js b/packages/backend/migration/1642613870898-drive-file-webpublic-type.js
new file mode 100644
index 0000000000..e10c2ac2d2
--- /dev/null
+++ b/packages/backend/migration/1642613870898-drive-file-webpublic-type.js
@@ -0,0 +1,13 @@
+const { MigrationInterface, QueryRunner } = require("typeorm");
+
+module.exports = class driveFileWebpublicType1642613870898 {
+    name = 'driveFileWebpublicType1642613870898'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "drive_file" ADD "webpublicType" character varying(128)`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "webpublicType"`);
+    }
+}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 65da382e2d..3d3a901f34 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -22,85 +22,78 @@
 		"@sinonjs/fake-timers": "7.1.2",
 		"@syuilo/aiscript": "0.11.1",
 		"@types/bcryptjs": "2.4.2",
-		"@types/bull": "3.15.5",
+		"@types/bull": "3.15.7",
 		"@types/cbor": "6.0.0",
 		"@types/dateformat": "3.0.1",
-		"@types/escape-regexp": "0.0.0",
+		"@types/escape-regexp": "0.0.1",
 		"@types/glob": "7.2.0",
 		"@types/is-url": "1.2.30",
-		"@types/js-yaml": "4.0.4",
-		"@types/jsdom": "16.2.13",
+		"@types/js-yaml": "4.0.5",
+		"@types/jsdom": "16.2.14",
 		"@types/jsonld": "1.5.6",
 		"@types/koa": "2.13.4",
-		"@types/koa-bodyparser": "4.3.3",
+		"@types/koa-bodyparser": "4.3.5",
 		"@types/koa-cors": "0.0.2",
 		"@types/koa-favicon": "2.0.21",
 		"@types/koa-logger": "3.1.2",
 		"@types/koa-mount": "4.0.1",
 		"@types/koa-send": "4.1.3",
 		"@types/koa-views": "7.0.0",
-		"@types/koa__cors": "3.0.3",
+		"@types/koa__cors": "3.1.1",
 		"@types/koa__multer": "2.0.4",
-		"@types/koa__router": "8.0.8",
+		"@types/koa__router": "8.0.11",
 		"@types/mocha": "8.2.3",
-		"@types/node": "16.11.7",
-		"@types/node-fetch": "2.5.12",
+		"@types/node": "17.0.10",
+		"@types/node-fetch": "3.0.3",
 		"@types/nodemailer": "6.4.4",
 		"@types/oauth": "0.9.1",
 		"@types/parse5": "6.0.3",
 		"@types/portscanner": "2.1.1",
-		"@types/pug": "2.0.5",
+		"@types/pug": "2.0.6",
 		"@types/punycode": "2.1.0",
-		"@types/qrcode": "1.4.1",
+		"@types/qrcode": "1.4.2",
 		"@types/random-seed": "0.3.3",
-		"@types/ratelimiter": "3.4.2",
-		"@types/redis": "2.8.32",
+		"@types/ratelimiter": "3.4.3",
+		"@types/redis": "4.0.11",
 		"@types/rename": "1.0.4",
 		"@types/request-stats": "3.0.0",
-		"@types/sanitize-html": "2.5.0",
+		"@types/sanitize-html": "2.6.2",
 		"@types/seedrandom": "2.4.28",
-		"@types/sharp": "0.29.3",
+		"@types/sharp": "0.29.5",
 		"@types/sinonjs__fake-timers": "6.0.4",
-		"@types/speakeasy": "2.0.6",
+		"@types/speakeasy": "2.0.7",
 		"@types/throttle-debounce": "2.1.0",
 		"@types/tinycolor2": "1.4.3",
-		"@types/tmp": "0.2.2",
-		"@types/uuid": "8.3.1",
+		"@types/tmp": "0.2.3",
+		"@types/uuid": "8.3.4",
 		"@types/web-push": "3.3.2",
 		"@types/webpack": "5.28.0",
 		"@types/webpack-stream": "3.2.12",
 		"@types/websocket": "1.0.4",
-		"@types/ws": "8.2.0",
-		"@typescript-eslint/eslint-plugin": "5.3.1",
-		"@typescript-eslint/parser": "5.1.0",
+		"@types/ws": "8.2.2",
+		"@typescript-eslint/eslint-plugin": "5.10.0",
+		"@typescript-eslint/parser": "5.10.0",
 		"abort-controller": "3.0.0",
 		"archiver": "5.3.0",
 		"autobind-decorator": "2.4.0",
 		"autwh": "0.1.0",
-		"aws-sdk": "2.1013.0",
+		"aws-sdk": "2.1061.0",
 		"bcryptjs": "2.4.3",
 		"blurhash": "1.1.4",
-		"broadcast-channel": "4.5.0",
-		"bull": "4.1.0",
+		"broadcast-channel": "4.9.0",
+		"bull": "4.2.1",
 		"cacheable-lookup": "6.0.4",
 		"cafy": "15.2.1",
 		"cbor": "8.1.0",
 		"chalk": "4.1.2",
-		"chart.js": "3.6.0",
-		"chartjs-adapter-date-fns": "2.0.0",
-		"chartjs-plugin-zoom": "1.1.1",
 		"cli-highlight": "2.1.11",
-		"content-disposition": "0.5.3",
+		"content-disposition": "0.5.4",
 		"crc-32": "1.2.0",
-		"css-loader": "6.5.1",
-		"cssnano": "5.0.10",
-		"date-fns": "2.25.0",
 		"dateformat": "4.5.1",
-		"deep-email-validator": "0.1.18",
+		"deep-email-validator": "0.1.21",
 		"escape-regexp": "0.0.1",
-		"eslint": "8.2.0",
-		"eslint-plugin-import": "2.25.3",
-		"eslint-plugin-vue": "8.0.3",
+		"eslint": "8.7.0",
+		"eslint-plugin-import": "2.25.4",
 		"eventemitter3": "4.0.7",
 		"feed": "4.2.2",
 		"file-type": "16.5.3",
@@ -108,9 +101,9 @@
 		"glob": "7.2.0",
 		"got": "11.8.2",
 		"hpagent": "0.1.2",
-		"http-signature": "1.3.5",
+		"http-signature": "1.3.6",
 		"ip-cidr": "3.0.4",
-		"is-svg": "4.3.1",
+		"is-svg": "4.3.2",
 		"js-yaml": "4.1.0",
 		"jsdom": "16.7.0",
 		"json5": "2.2.0",
@@ -127,30 +120,29 @@
 		"koa-slow": "2.1.0",
 		"koa-views": "7.0.2",
 		"langmap": "0.0.16",
-		"mfm-js": "0.20.0",
+		"mfm-js": "0.21.0",
 		"mime-types": "2.1.34",
-		"misskey-js": "0.0.12",
+		"misskey-js": "0.0.13",
 		"mocha": "8.4.0",
 		"ms": "3.0.0-canary.1",
-		"multer": "1.4.3",
+		"multer": "1.4.4",
 		"nested-property": "4.0.0",
 		"node-fetch": "2.6.1",
-		"nodemailer": "6.7.0",
+		"nodemailer": "6.7.2",
 		"os-utils": "0.0.14",
 		"parse5": "6.0.1",
 		"pg": "8.7.1",
 		"portscanner": "2.2.0",
-		"prismjs": "1.25.0",
 		"private-ip": "2.3.3",
-		"probe-image-size": "7.2.1",
+		"probe-image-size": "7.2.2",
 		"promise-limit": "2.7.0",
 		"pug": "3.0.2",
 		"punycode": "2.1.1",
-		"pureimage": "0.3.5",
-		"qrcode": "1.4.4",
+		"pureimage": "0.3.8",
+		"qrcode": "1.5.0",
 		"random-seed": "0.3.0",
 		"ratelimiter": "3.4.1",
-		"re2": "1.16.0",
+		"re2": "1.17.3",
 		"redis": "3.1.2",
 		"redis-lock": "0.1.4",
 		"reflect-metadata": "0.1.13",
@@ -159,9 +151,9 @@
 		"require-all": "3.0.0",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.2",
-		"sanitize-html": "2.5.3",
+		"sanitize-html": "2.6.1",
 		"seedrandom": "3.0.5",
-		"sharp": "0.29.2",
+		"sharp": "0.29.3",
 		"speakeasy": "2.0.0",
 		"strict-event-emitter-types": "2.0.0",
 		"stringz": "2.1.0",
@@ -175,20 +167,21 @@
 		"ts-loader": "9.2.6",
 		"ts-node": "10.4.0",
 		"tsc-alias": "1.4.1",
-		"tsconfig-paths": "3.11.0",
+		"tsconfig-paths": "3.12.0",
 		"twemoji-parser": "13.1.0",
-		"typeorm": "0.2.39",
-		"typescript": "4.4.4",
+		"typeorm": "0.2.41",
+		"typescript": "4.5.5",
 		"ulid": "2.3.0",
+		"unzipper": "0.10.11",
 		"uuid": "8.3.2",
 		"web-push": "3.4.5",
 		"websocket": "1.0.34",
-		"ws": "8.2.3",
+		"ws": "8.4.2",
 		"xev": "2.0.1"
 	},
 	"devDependencies": {
-		"@redocly/openapi-core": "1.0.0-beta.54",
-		"@types/fluent-ffmpeg": "2.1.17",
+		"@redocly/openapi-core": "1.0.0-beta.79",
+		"@types/fluent-ffmpeg": "2.1.20",
 		"cross-env": "7.0.3",
 		"execa": "6.0.0"
 	}
diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts
index b5f228d919..69336c2a46 100644
--- a/packages/backend/src/db/postgre.ts
+++ b/packages/backend/src/db/postgre.ts
@@ -40,8 +40,6 @@ import { Signin } from '@/models/entities/signin';
 import { AuthSession } from '@/models/entities/auth-session';
 import { FollowRequest } from '@/models/entities/follow-request';
 import { Emoji } from '@/models/entities/emoji';
-import { ReversiGame } from '@/models/entities/games/reversi/game';
-import { ReversiMatching } from '@/models/entities/games/reversi/matching';
 import { UserNotePining } from '@/models/entities/user-note-pining';
 import { Poll } from '@/models/entities/poll';
 import { UserKeypair } from '@/models/entities/user-keypair';
@@ -166,8 +164,6 @@ export const entities = [
 	AntennaNote,
 	PromoNote,
 	PromoRead,
-	ReversiGame,
-	ReversiMatching,
 	Relay,
 	MutedNote,
 	Channel,
@@ -224,7 +220,9 @@ export async function resetDb() {
 		WHERE nspname NOT IN ('pg_catalog', 'information_schema')
 			AND C.relkind = 'r'
 			AND nspname !~ '^pg_toast';`);
-		await Promise.all(tables.map(t => t.table).map(x => conn.query(`DELETE FROM "${x}" CASCADE`)));
+		for (const table of tables) {
+			await conn.query(`DELETE FROM "${table.table}" CASCADE`);
+		}
 	};
 
 	for (let i = 1; i <= 3; i++) {
diff --git a/packages/backend/src/games/reversi/core.ts b/packages/backend/src/games/reversi/core.ts
deleted file mode 100644
index 0cf7714543..0000000000
--- a/packages/backend/src/games/reversi/core.ts
+++ /dev/null
@@ -1,263 +0,0 @@
-import { count, concat } from '@/prelude/array';
-
-// MISSKEY REVERSI ENGINE
-
-/**
- * true ... 黒
- * false ... 白
- */
-export type Color = boolean;
-const BLACK = true;
-const WHITE = false;
-
-export type MapPixel = 'null' | 'empty';
-
-export type Options = {
-	isLlotheo: boolean;
-	canPutEverywhere: boolean;
-	loopedBoard: boolean;
-};
-
-export type Undo = {
-	/**
-	 * 色
-	 */
-	color: Color;
-
-	/**
-	 * どこに打ったか
-	 */
-	pos: number;
-
-	/**
-	 * 反転した石の位置の配列
-	 */
-	effects: number[];
-
-	/**
-	 * ターン
-	 */
-	turn: Color | null;
-};
-
-/**
- * リバーシエンジン
- */
-export default class Reversi {
-	public map: MapPixel[];
-	public mapWidth: number;
-	public mapHeight: number;
-	public board: (Color | null | undefined)[];
-	public turn: Color | null = BLACK;
-	public opts: Options;
-
-	public prevPos = -1;
-	public prevColor: Color | null = null;
-
-	private logs: Undo[] = [];
-
-	/**
-	 * ゲームを初期化します
-	 */
-	constructor(map: string[], opts: Options) {
-		//#region binds
-		this.put = this.put.bind(this);
-		//#endregion
-
-		//#region Options
-		this.opts = opts;
-		if (this.opts.isLlotheo == null) this.opts.isLlotheo = false;
-		if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false;
-		if (this.opts.loopedBoard == null) this.opts.loopedBoard = false;
-		//#endregion
-
-		//#region Parse map data
-		this.mapWidth = map[0].length;
-		this.mapHeight = map.length;
-		const mapData = map.join('');
-
-		this.board = mapData.split('').map(d => d === '-' ? null : d === 'b' ? BLACK : d === 'w' ? WHITE : undefined);
-
-		this.map = mapData.split('').map(d => d === '-' || d === 'b' || d === 'w' ? 'empty' : 'null');
-		//#endregion
-
-		// ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある
-		if (!this.canPutSomewhere(BLACK)) this.turn = this.canPutSomewhere(WHITE) ? WHITE : null;
-	}
-
-	/**
-	 * 黒石の数
-	 */
-	public get blackCount() {
-		return count(BLACK, this.board);
-	}
-
-	/**
-	 * 白石の数
-	 */
-	public get whiteCount() {
-		return count(WHITE, this.board);
-	}
-
-	public transformPosToXy(pos: number): number[] {
-		const x = pos % this.mapWidth;
-		const y = Math.floor(pos / this.mapWidth);
-		return [x, y];
-	}
-
-	public transformXyToPos(x: number, y: number): number {
-		return x + (y * this.mapWidth);
-	}
-
-	/**
-	 * 指定のマスに石を打ちます
-	 * @param color 石の色
-	 * @param pos 位置
-	 */
-	public put(color: Color, pos: number) {
-		this.prevPos = pos;
-		this.prevColor = color;
-
-		this.board[pos] = color;
-
-		// 反転させられる石を取得
-		const effects = this.effects(color, pos);
-
-		// 反転させる
-		for (const pos of effects) {
-			this.board[pos] = color;
-		}
-
-		const turn = this.turn;
-
-		this.logs.push({
-			color,
-			pos,
-			effects,
-			turn,
-		});
-
-		this.calcTurn();
-	}
-
-	private calcTurn() {
-		// ターン計算
-		this.turn =
-			this.canPutSomewhere(!this.prevColor) ? !this.prevColor :
-			this.canPutSomewhere(this.prevColor!) ? this.prevColor :
-			null;
-	}
-
-	public undo() {
-		const undo = this.logs.pop()!;
-		this.prevColor = undo.color;
-		this.prevPos = undo.pos;
-		this.board[undo.pos] = null;
-		for (const pos of undo.effects) {
-			const color = this.board[pos];
-			this.board[pos] = !color;
-		}
-		this.turn = undo.turn;
-	}
-
-	/**
-	 * 指定した位置のマップデータのマスを取得します
-	 * @param pos 位置
-	 */
-	public mapDataGet(pos: number): MapPixel {
-		const [x, y] = this.transformPosToXy(pos);
-		return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? 'null' : this.map[pos];
-	}
-
-	/**
-	 * 打つことができる場所を取得します
-	 */
-	public puttablePlaces(color: Color): number[] {
-		return Array.from(this.board.keys()).filter(i => this.canPut(color, i));
-	}
-
-	/**
-	 * 打つことができる場所があるかどうかを取得します
-	 */
-	public canPutSomewhere(color: Color): boolean {
-		return this.puttablePlaces(color).length > 0;
-	}
-
-	/**
-	 * 指定のマスに石を打つことができるかどうかを取得します
-	 * @param color 自分の色
-	 * @param pos 位置
-	 */
-	public canPut(color: Color, pos: number): boolean {
-		return (
-			this.board[pos] !== null ? false : // 既に石が置いてある場所には打てない
-			this.opts.canPutEverywhere ? this.mapDataGet(pos) == 'empty' : // 挟んでなくても置けるモード
-			this.effects(color, pos).length !== 0); // 相手の石を1つでも反転させられるか
-	}
-
-	/**
-	 * 指定のマスに石を置いた時の、反転させられる石を取得します
-	 * @param color 自分の色
-	 * @param initPos 位置
-	 */
-	public effects(color: Color, initPos: number): number[] {
-		const enemyColor = !color;
-
-		const diffVectors: [number, number][] = [
-			[  0,  -1], // 上
-			[ +1,  -1], // 右上
-			[ +1,   0], // 右
-			[ +1,  +1], // 右下
-			[  0,  +1], // 下
-			[ -1,  +1], // 左下
-			[ -1,   0], // 左
-			[ -1,  -1],  // 左上
-		];
-
-		const effectsInLine = ([dx, dy]: [number, number]): number[] => {
-			const nextPos = (x: number, y: number): [number, number] => [x + dx, y + dy];
-
-			const found: number[] = []; // 挟めるかもしれない相手の石を入れておく配列
-			let [x, y] = this.transformPosToXy(initPos);
-			while (true) {
-				[x, y] = nextPos(x, y);
-
-				// 座標が指し示す位置がボード外に出たとき
-				if (this.opts.loopedBoard && this.transformXyToPos(
-					(x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth),
-					(y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) === initPos) {
-						// 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ)
-					return found;
-				} else if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight) {
-					return []; // 挟めないことが確定 (盤面外に到達)
-				}
-
-				const pos = this.transformXyToPos(x, y);
-				if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達)
-				const stone = this.board[pos];
-				if (stone === null) return []; // 挟めないことが確定 (石が置かれていないマスに到達)
-				if (stone === enemyColor) found.push(pos); // 挟めるかもしれない (相手の石を発見)
-				if (stone === color) return found; // 挟めることが確定 (対となる自分の石を発見)
-			}
-		};
-
-		return concat(diffVectors.map(effectsInLine));
-	}
-
-	/**
-	 * ゲームが終了したか否か
-	 */
-	public get isEnded(): boolean {
-		return this.turn === null;
-	}
-
-	/**
-	 * ゲームの勝者 (null = 引き分け)
-	 */
-	public get winner(): Color | null {
-		return this.isEnded ?
-			this.blackCount == this.whiteCount ? null :
-			this.opts.isLlotheo === this.blackCount > this.whiteCount ? WHITE : BLACK :
-			undefined as never;
-	}
-}
diff --git a/packages/backend/src/games/reversi/maps.ts b/packages/backend/src/games/reversi/maps.ts
deleted file mode 100644
index 8442c6d741..0000000000
--- a/packages/backend/src/games/reversi/maps.ts
+++ /dev/null
@@ -1,896 +0,0 @@
-/**
- * 組み込みマップ定義
- *
- * データ値:
- * (スペース) ... マス無し
- * - ... マス
- * b ... 初期配置される黒石
- * w ... 初期配置される白石
- */
-
-export type Map = {
-	name?: string;
-	category?: string;
-	author?: string;
-	data: string[];
-};
-
-export const fourfour: Map = {
-	name: '4x4',
-	category: '4x4',
-	data: [
-		'----',
-		'-wb-',
-		'-bw-',
-		'----',
-	],
-};
-
-export const sixsix: Map = {
-	name: '6x6',
-	category: '6x6',
-	data: [
-		'------',
-		'------',
-		'--wb--',
-		'--bw--',
-		'------',
-		'------',
-	],
-};
-
-export const roundedSixsix: Map = {
-	name: '6x6 rounded',
-	category: '6x6',
-	author: 'syuilo',
-	data: [
-		' ---- ',
-		'------',
-		'--wb--',
-		'--bw--',
-		'------',
-		' ---- ',
-	],
-};
-
-export const roundedSixsix2: Map = {
-	name: '6x6 rounded 2',
-	category: '6x6',
-	author: 'syuilo',
-	data: [
-		'  --  ',
-		' ---- ',
-		'--wb--',
-		'--bw--',
-		' ---- ',
-		'  --  ',
-	],
-};
-
-export const eighteight: Map = {
-	name: '8x8',
-	category: '8x8',
-	data: [
-		'--------',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'--------',
-	],
-};
-
-export const eighteightH1: Map = {
-	name: '8x8 handicap 1',
-	category: '8x8',
-	data: [
-		'b-------',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'--------',
-	],
-};
-
-export const eighteightH2: Map = {
-	name: '8x8 handicap 2',
-	category: '8x8',
-	data: [
-		'b-------',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'-------b',
-	],
-};
-
-export const eighteightH3: Map = {
-	name: '8x8 handicap 3',
-	category: '8x8',
-	data: [
-		'b------b',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'-------b',
-	],
-};
-
-export const eighteightH4: Map = {
-	name: '8x8 handicap 4',
-	category: '8x8',
-	data: [
-		'b------b',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'b------b',
-	],
-};
-
-export const eighteightH28: Map = {
-	name: '8x8 handicap 28',
-	category: '8x8',
-	data: [
-		'bbbbbbbb',
-		'b------b',
-		'b------b',
-		'b--wb--b',
-		'b--bw--b',
-		'b------b',
-		'b------b',
-		'bbbbbbbb',
-	],
-};
-
-export const roundedEighteight: Map = {
-	name: '8x8 rounded',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		' ------ ',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		' ------ ',
-	],
-};
-
-export const roundedEighteight2: Map = {
-	name: '8x8 rounded 2',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'  ----  ',
-		' ------ ',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		' ------ ',
-		'  ----  ',
-	],
-};
-
-export const roundedEighteight3: Map = {
-	name: '8x8 rounded 3',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'   --   ',
-		'  ----  ',
-		' ------ ',
-		'---wb---',
-		'---bw---',
-		' ------ ',
-		'  ----  ',
-		'   --   ',
-	],
-};
-
-export const eighteightWithNotch: Map = {
-	name: '8x8 with notch',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'---  ---',
-		'--------',
-		'--------',
-		' --wb-- ',
-		' --bw-- ',
-		'--------',
-		'--------',
-		'---  ---',
-	],
-};
-
-export const eighteightWithSomeHoles: Map = {
-	name: '8x8 with some holes',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'--- ----',
-		'----- --',
-		'-- -----',
-		'---wb---',
-		'---bw- -',
-		' -------',
-		'--- ----',
-		'--------',
-	],
-};
-
-export const circle: Map = {
-	name: 'Circle',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'   --   ',
-		' ------ ',
-		' ------ ',
-		'---wb---',
-		'---bw---',
-		' ------ ',
-		' ------ ',
-		'   --   ',
-	],
-};
-
-export const smile: Map = {
-	name: 'Smile',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		' ------ ',
-		'--------',
-		'-- -- --',
-		'---wb---',
-		'-- bw --',
-		'---  ---',
-		'--------',
-		' ------ ',
-	],
-};
-
-export const window: Map = {
-	name: 'Window',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'--------',
-		'-  --  -',
-		'-  --  -',
-		'---wb---',
-		'---bw---',
-		'-  --  -',
-		'-  --  -',
-		'--------',
-	],
-};
-
-export const reserved: Map = {
-	name: 'Reserved',
-	category: '8x8',
-	author: 'Aya',
-	data: [
-		'w------b',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'b------w',
-	],
-};
-
-export const x: Map = {
-	name: 'X',
-	category: '8x8',
-	author: 'Aya',
-	data: [
-		'w------b',
-		'-w----b-',
-		'--w--b--',
-		'---wb---',
-		'---bw---',
-		'--b--w--',
-		'-b----w-',
-		'b------w',
-	],
-};
-
-export const parallel: Map = {
-	name: 'Parallel',
-	category: '8x8',
-	author: 'Aya',
-	data: [
-		'--------',
-		'--------',
-		'--------',
-		'---bb---',
-		'---ww---',
-		'--------',
-		'--------',
-		'--------',
-	],
-};
-
-export const lackOfBlack: Map = {
-	name: 'Lack of Black',
-	category: '8x8',
-	data: [
-		'--------',
-		'--------',
-		'--------',
-		'---w----',
-		'---bw---',
-		'--------',
-		'--------',
-		'--------',
-	],
-};
-
-export const squareParty: Map = {
-	name: 'Square Party',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'--------',
-		'-wwwbbb-',
-		'-w-wb-b-',
-		'-wwwbbb-',
-		'-bbbwww-',
-		'-b-bw-w-',
-		'-bbbwww-',
-		'--------',
-	],
-};
-
-export const minesweeper: Map = {
-	name: 'Minesweeper',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'b-b--w-w',
-		'-w-wb-b-',
-		'w-b--w-b',
-		'-b-wb-w-',
-		'-w-bw-b-',
-		'b-w--b-w',
-		'-b-bw-w-',
-		'w-w--b-b',
-	],
-};
-
-export const tenthtenth: Map = {
-	name: '10x10',
-	category: '10x10',
-	data: [
-		'----------',
-		'----------',
-		'----------',
-		'----------',
-		'----wb----',
-		'----bw----',
-		'----------',
-		'----------',
-		'----------',
-		'----------',
-	],
-};
-
-export const hole: Map = {
-	name: 'The Hole',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'----------',
-		'----------',
-		'--wb--wb--',
-		'--bw--bw--',
-		'----  ----',
-		'----  ----',
-		'--wb--wb--',
-		'--bw--bw--',
-		'----------',
-		'----------',
-	],
-};
-
-export const grid: Map = {
-	name: 'Grid',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'----------',
-		'- - -- - -',
-		'----------',
-		'- - -- - -',
-		'----wb----',
-		'----bw----',
-		'- - -- - -',
-		'----------',
-		'- - -- - -',
-		'----------',
-	],
-};
-
-export const cross: Map = {
-	name: 'Cross',
-	category: '10x10',
-	author: 'Aya',
-	data: [
-		'   ----   ',
-		'   ----   ',
-		'   ----   ',
-		'----------',
-		'----wb----',
-		'----bw----',
-		'----------',
-		'   ----   ',
-		'   ----   ',
-		'   ----   ',
-	],
-};
-
-export const charX: Map = {
-	name: 'Char X',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'---    ---',
-		'----  ----',
-		'----------',
-		' -------- ',
-		'  --wb--  ',
-		'  --bw--  ',
-		' -------- ',
-		'----------',
-		'----  ----',
-		'---    ---',
-	],
-};
-
-export const charY: Map = {
-	name: 'Char Y',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'---    ---',
-		'----  ----',
-		'----------',
-		' -------- ',
-		'  --wb--  ',
-		'  --bw--  ',
-		'  ------  ',
-		'  ------  ',
-		'  ------  ',
-		'  ------  ',
-	],
-};
-
-export const walls: Map = {
-	name: 'Walls',
-	category: '10x10',
-	author: 'Aya',
-	data: [
-		' bbbbbbbb ',
-		'w--------w',
-		'w--------w',
-		'w--------w',
-		'w---wb---w',
-		'w---bw---w',
-		'w--------w',
-		'w--------w',
-		'w--------w',
-		' bbbbbbbb ',
-	],
-};
-
-export const cpu: Map = {
-	name: 'CPU',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		' b b  b b ',
-		'w--------w',
-		' -------- ',
-		'w--------w',
-		' ---wb--- ',
-		' ---bw--- ',
-		'w--------w',
-		' -------- ',
-		'w--------w',
-		' b b  b b ',
-	],
-};
-
-export const checker: Map = {
-	name: 'Checker',
-	category: '10x10',
-	author: 'Aya',
-	data: [
-		'----------',
-		'----------',
-		'----------',
-		'---wbwb---',
-		'---bwbw---',
-		'---wbwb---',
-		'---bwbw---',
-		'----------',
-		'----------',
-		'----------',
-	],
-};
-
-export const japaneseCurry: Map = {
-	name: 'Japanese curry',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'w-b-b-b-b-',
-		'-w-b-b-b-b',
-		'w-w-b-b-b-',
-		'-w-w-b-b-b',
-		'w-w-wwb-b-',
-		'-w-wbb-b-b',
-		'w-w-w-b-b-',
-		'-w-w-w-b-b',
-		'w-w-w-w-b-',
-		'-w-w-w-w-b',
-	],
-};
-
-export const mosaic: Map = {
-	name: 'Mosaic',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'- - - - - ',
-		' - - - - -',
-		'- - - - - ',
-		' - w w - -',
-		'- - b b - ',
-		' - w w - -',
-		'- - b b - ',
-		' - - - - -',
-		'- - - - - ',
-		' - - - - -',
-	],
-};
-
-export const arena: Map = {
-	name: 'Arena',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'- - -- - -',
-		' - -  - - ',
-		'- ------ -',
-		' -------- ',
-		'- --wb-- -',
-		'- --bw-- -',
-		' -------- ',
-		'- ------ -',
-		' - -  - - ',
-		'- - -- - -',
-	],
-};
-
-export const reactor: Map = {
-	name: 'Reactor',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'-w------b-',
-		'b- -  - -w',
-		'- --wb-- -',
-		'---b  w---',
-		'- b wb w -',
-		'- w bw b -',
-		'---w  b---',
-		'- --bw-- -',
-		'w- -  - -b',
-		'-b------w-',
-	],
-};
-
-export const sixeight: Map = {
-	name: '6x8',
-	category: 'Special',
-	data: [
-		'------',
-		'------',
-		'------',
-		'--wb--',
-		'--bw--',
-		'------',
-		'------',
-		'------',
-	],
-};
-
-export const spark: Map = {
-	name: 'Spark',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		' -      - ',
-		'----------',
-		' -------- ',
-		' -------- ',
-		' ---wb--- ',
-		' ---bw--- ',
-		' -------- ',
-		' -------- ',
-		'----------',
-		' -      - ',
-	],
-};
-
-export const islands: Map = {
-	name: 'Islands',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		'--------  ',
-		'---wb---  ',
-		'---bw---  ',
-		'--------  ',
-		'  -    -  ',
-		'  -    -  ',
-		'  --------',
-		'  --------',
-		'  --------',
-		'  --------',
-	],
-};
-
-export const galaxy: Map = {
-	name: 'Galaxy',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		'   ------   ',
-		'  --www---  ',
-		' ------w--- ',
-		'---bbb--w---',
-		'--b---b-w-b-',
-		'-b--wwb-w-b-',
-		'-b-w-bww--b-',
-		'-b-w-b---b--',
-		'---w--bbb---',
-		' ---w------ ',
-		'  ---www--  ',
-		'   ------   ',
-	],
-};
-
-export const triangle: Map = {
-	name: 'Triangle',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		'    --    ',
-		'    --    ',
-		'   ----   ',
-		'   ----   ',
-		'  --wb--  ',
-		'  --bw--  ',
-		' -------- ',
-		' -------- ',
-		'----------',
-		'----------',
-	],
-};
-
-export const iphonex: Map = {
-	name: 'iPhone X',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		' --  -- ',
-		'--------',
-		'--------',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'--------',
-		'--------',
-		' ------ ',
-	],
-};
-
-export const dealWithIt: Map = {
-	name: 'Deal with it!',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		'------------',
-		'--w-b-------',
-		' --b-w------',
-		'  --w-b---- ',
-		'   -------  ',
-	],
-};
-
-export const experiment: Map = {
-	name: 'Let\'s experiment',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		' ------------ ',
-		'------wb------',
-		'------bw------',
-		'--------------',
-		'    -    -    ',
-		'------  ------',
-		'bbbbbb  wwwwww',
-		'bbbbbb  wwwwww',
-		'bbbbbb  wwwwww',
-		'bbbbbb  wwwwww',
-		'wwwwww  bbbbbb',
-	],
-};
-
-export const bigBoard: Map = {
-	name: 'Big board',
-	category: 'Special',
-	data: [
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'-------wb-------',
-		'-------bw-------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-	],
-};
-
-export const twoBoard: Map = {
-	name: 'Two board',
-	category: 'Special',
-	author: 'Aya',
-	data: [
-		'-------- --------',
-		'-------- --------',
-		'-------- --------',
-		'---wb--- ---wb---',
-		'---bw--- ---bw---',
-		'-------- --------',
-		'-------- --------',
-		'-------- --------',
-	],
-};
-
-export const test1: Map = {
-	name: 'Test1',
-	category: 'Test',
-	data: [
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-	],
-};
-
-export const test2: Map = {
-	name: 'Test2',
-	category: 'Test',
-	data: [
-		'------',
-		'------',
-		'-b--w-',
-		'-w--b-',
-		'-w--b-',
-	],
-};
-
-export const test3: Map = {
-	name: 'Test3',
-	category: 'Test',
-	data: [
-		'-w-',
-		'--w',
-		'w--',
-		'-w-',
-		'--w',
-		'w--',
-		'-w-',
-		'--w',
-		'w--',
-		'-w-',
-		'---',
-		'b--',
-	],
-};
-
-export const test4: Map = {
-	name: 'Test4',
-	category: 'Test',
-	data: [
-		'-w--b-',
-		'-w--b-',
-		'------',
-		'-w--b-',
-		'-w--b-',
-	],
-};
-
-// 検証用: この盤面で藍(lv3)が黒で始めると何故か(?)A1に打ってしまう
-export const test6: Map = {
-	name: 'Test6',
-	category: 'Test',
-	data: [
-		'--wwwww-',
-		'wwwwwwww',
-		'wbbbwbwb',
-		'wbbbbwbb',
-		'wbwbbwbb',
-		'wwbwbbbb',
-		'--wbbbbb',
-		'-wwwww--',
-	],
-};
-
-// 検証用: この盤面で藍(lv3)が黒で始めると何故か(?)G7に打ってしまう
-export const test7: Map = {
-	name: 'Test7',
-	category: 'Test',
-	data: [
-		'b--w----',
-		'b-wwww--',
-		'bwbwwwbb',
-		'wbwwwwb-',
-		'wwwwwww-',
-		'-wwbbwwb',
-		'--wwww--',
-		'--wwww--',
-	],
-};
-
-// 検証用: この盤面で藍(lv5)が黒で始めると何故か(?)A1に打ってしまう
-export const test8: Map = {
-	name: 'Test8',
-	category: 'Test',
-	data: [
-		'--------',
-		'-----w--',
-		'w--www--',
-		'wwwwww--',
-		'bbbbwww-',
-		'wwwwww--',
-		'--www---',
-		'--ww----',
-	],
-};
diff --git a/packages/backend/src/games/reversi/package.json b/packages/backend/src/games/reversi/package.json
deleted file mode 100644
index a4415ad141..0000000000
--- a/packages/backend/src/games/reversi/package.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-  "name": "misskey-reversi",
-  "version": "0.0.5",
-  "description": "Misskey reversi engine",
-  "keywords": [
-    "misskey"
-  ],
-  "author": "syuilo <i@syuilo.com>",
-  "license": "MIT",
-  "repository": "https://github.com/misskey-dev/misskey.git",
-  "bugs": "https://github.com/misskey-dev/misskey/issues",
-  "main": "./built/core.js",
-  "types": "./built/core.d.ts",
-  "scripts": {
-    "build": "tsc"
-  },
-  "dependencies": {}
-}
diff --git a/packages/backend/src/games/reversi/tsconfig.json b/packages/backend/src/games/reversi/tsconfig.json
deleted file mode 100644
index 851fb6b7e4..0000000000
--- a/packages/backend/src/games/reversi/tsconfig.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
-	"compilerOptions": {
-		"noEmitOnError": false,
-		"noImplicitAny": false,
-		"noImplicitReturns": true,
-		"noFallthroughCasesInSwitch": true,
-		"experimentalDecorators": true,
-		"declaration": true,
-		"sourceMap": false,
-		"target": "es2017",
-		"module": "commonjs",
-		"removeComments": false,
-		"noLib": false,
-		"outDir": "./built",
-		"rootDir": "./"
-	},
-	"compileOnSave": false,
-	"include": [
-		"./core.ts"
-	]
-}
diff --git a/packages/backend/src/misc/gen-avatar.ts b/packages/backend/src/misc/gen-identicon.ts
similarity index 90%
rename from packages/backend/src/misc/gen-avatar.ts
rename to packages/backend/src/misc/gen-identicon.ts
index 8838ec8d15..5cedd7afaf 100644
--- a/packages/backend/src/misc/gen-avatar.ts
+++ b/packages/backend/src/misc/gen-identicon.ts
@@ -1,5 +1,6 @@
 /**
- * Random avatar generator
+ * Identicon generator
+ * https://en.wikipedia.org/wiki/Identicon
  */
 
 import * as p from 'pureimage';
@@ -34,9 +35,9 @@ const cellSize = actualSize / n;
 const sideN = Math.floor(n / 2);
 
 /**
- * Generate buffer of random avatar by seed
+ * Generate buffer of an identicon by seed
  */
-export function genAvatar(seed: string, stream: WriteStream): Promise<void> {
+export function genIdenticon(seed: string, stream: WriteStream): Promise<void> {
 	const rand = gen.create(seed);
 	const canvas = p.make(size, size);
 	const ctx = canvas.getContext('2d');
diff --git a/packages/backend/src/misc/populate-emojis.ts b/packages/backend/src/misc/populate-emojis.ts
index b021ec46eb..26c05e5fa6 100644
--- a/packages/backend/src/misc/populate-emojis.ts
+++ b/packages/backend/src/misc/populate-emojis.ts
@@ -62,7 +62,8 @@ export async function populateEmoji(emojiName: string, noteUserHost: string | nu
 	if (emoji == null) return null;
 
 	const isLocal = emoji.host == null;
-	const url = isLocal ? emoji.url : `${config.url}/proxy/image.png?${query({ url: emoji.url })}`;
+	const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため
+	const url = isLocal ? emojiUrl : `${config.url}/proxy/image.png?${query({ url: emojiUrl })}`;
 
 	return {
 		name: emojiName,
@@ -116,7 +117,7 @@ export async function prefetchEmojis(emojis: { name: string; host: string | null
 	}
 	const _emojis = emojisQuery.length > 0 ? await Emojis.find({
 		where: emojisQuery,
-		select: ['name', 'host', 'url'],
+		select: ['name', 'host', 'originalUrl', 'publicUrl'],
 	}) : [];
 	for (const emoji of _emojis) {
 		cache.set(`${emoji.name} ${emoji.host}`, emoji);
diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts
index 4131875ef7..2dae954af9 100644
--- a/packages/backend/src/misc/schema.ts
+++ b/packages/backend/src/misc/schema.ts
@@ -1,32 +1,44 @@
-import { SimpleObj, SimpleSchema } from './simple-schema';
-import { packedUserSchema } from '@/models/repositories/user';
-import { packedNoteSchema } from '@/models/repositories/note';
-import { packedUserListSchema } from '@/models/repositories/user-list';
-import { packedAppSchema } from '@/models/repositories/app';
-import { packedMessagingMessageSchema } from '@/models/repositories/messaging-message';
-import { packedNotificationSchema } from '@/models/repositories/notification';
-import { packedDriveFileSchema } from '@/models/repositories/drive-file';
-import { packedDriveFolderSchema } from '@/models/repositories/drive-folder';
-import { packedFollowingSchema } from '@/models/repositories/following';
-import { packedMutingSchema } from '@/models/repositories/muting';
-import { packedBlockingSchema } from '@/models/repositories/blocking';
-import { packedNoteReactionSchema } from '@/models/repositories/note-reaction';
-import { packedHashtagSchema } from '@/models/repositories/hashtag';
-import { packedPageSchema } from '@/models/repositories/page';
-import { packedUserGroupSchema } from '@/models/repositories/user-group';
-import { packedNoteFavoriteSchema } from '@/models/repositories/note-favorite';
-import { packedChannelSchema } from '@/models/repositories/channel';
-import { packedAntennaSchema } from '@/models/repositories/antenna';
-import { packedClipSchema } from '@/models/repositories/clip';
-import { packedFederationInstanceSchema } from '@/models/repositories/federation-instance';
-import { packedQueueCountSchema } from '@/models/repositories/queue';
-import { packedGalleryPostSchema } from '@/models/repositories/gallery-post';
-import { packedEmojiSchema } from '@/models/repositories/emoji';
-import { packedReversiGameSchema } from '@/models/repositories/games/reversi/game';
-import { packedReversiMatchingSchema } from '@/models/repositories/games/reversi/matching';
+import {
+	packedUserLiteSchema,
+	packedUserDetailedNotMeOnlySchema,
+	packedMeDetailedOnlySchema,
+	packedUserDetailedNotMeSchema,
+	packedMeDetailedSchema,
+	packedUserDetailedSchema,
+	packedUserSchema,
+} from '@/models/schema/user';
+import { packedNoteSchema } from '@/models/schema/note';
+import { packedUserListSchema } from '@/models/schema/user-list';
+import { packedAppSchema } from '@/models/schema/app';
+import { packedMessagingMessageSchema } from '@/models/schema/messaging-message';
+import { packedNotificationSchema } from '@/models/schema/notification';
+import { packedDriveFileSchema } from '@/models/schema/drive-file';
+import { packedDriveFolderSchema } from '@/models/schema/drive-folder';
+import { packedFollowingSchema } from '@/models/schema/following';
+import { packedMutingSchema } from '@/models/schema/muting';
+import { packedBlockingSchema } from '@/models/schema/blocking';
+import { packedNoteReactionSchema } from '@/models/schema/note-reaction';
+import { packedHashtagSchema } from '@/models/schema/hashtag';
+import { packedPageSchema } from '@/models/schema/page';
+import { packedUserGroupSchema } from '@/models/schema/user-group';
+import { packedNoteFavoriteSchema } from '@/models/schema/note-favorite';
+import { packedChannelSchema } from '@/models/schema/channel';
+import { packedAntennaSchema } from '@/models/schema/antenna';
+import { packedClipSchema } from '@/models/schema/clip';
+import { packedFederationInstanceSchema } from '@/models/schema/federation-instance';
+import { packedQueueCountSchema } from '@/models/schema/queue';
+import { packedGalleryPostSchema } from '@/models/schema/gallery-post';
+import { packedEmojiSchema } from '@/models/schema/emoji';
 
 export const refs = {
+	UserLite: packedUserLiteSchema,
+	UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema,
+	MeDetailedOnly: packedMeDetailedOnlySchema,
+	UserDetailedNotMe: packedUserDetailedNotMeSchema,
+	MeDetailed: packedMeDetailedSchema,
+	UserDetailed: packedUserDetailedSchema,
 	User: packedUserSchema,
+
 	UserList: packedUserListSchema,
 	UserGroup: packedUserGroupSchema,
 	App: packedAppSchema,
@@ -49,16 +61,52 @@ export const refs = {
 	FederationInstance: packedFederationInstanceSchema,
 	GalleryPost: packedGalleryPostSchema,
 	Emoji: packedEmojiSchema,
-	ReversiGame: packedReversiGameSchema,
-	ReversiMatching: packedReversiMatchingSchema,
 };
 
-export type Packed<x extends keyof typeof refs> = ObjType<(typeof refs[x])['properties']>;
+// Packed = SchemaTypeDef<typeof refs[x]>; とすると展開されてマウスホバー時に型表示が使い物にならなくなる
+// ObjType<r['properties']>を指定すると(なぜか)展開されずにPacked<'Hoge'>と表示される
+type PackedDef<r extends { properties?: Obj; oneOf?: ReadonlyArray<MinimumSchema>; allOf?: ReadonlyArray<MinimumSchema> }> =
+	r['allOf'] extends ReadonlyArray<MinimumSchema> ? UnionToIntersection<UnionSchemaType<r['allOf']>> :
+	r['oneOf'] extends ReadonlyArray<MinimumSchema> ? UnionSchemaType<r['oneOf']> :
+	r['properties'] extends Obj ? ObjType<r['properties']> :
+	never;
+export type Packed<x extends keyof typeof refs> = PackedDef<typeof refs[x]>;
 
-export interface Schema extends SimpleSchema {
-	items?: Schema;
-	properties?: Obj;
-	ref?: keyof typeof refs;
+type TypeStringef = 'boolean' | 'number' | 'string' | 'array' | 'object' | 'any';
+type StringDefToType<T extends TypeStringef> =
+	T extends 'boolean' ? boolean :
+	T extends 'number' ? number :
+	T extends 'string' ? string | Date :
+	T extends 'array' ? ReadonlyArray<any> :
+	T extends 'object' ? Record<string, any> :
+	any;
+
+// https://swagger.io/specification/?sbsearch=optional#schema-object
+type OfSchema = {
+	readonly anyOf?: ReadonlyArray<MinimumSchema>;
+	readonly oneOf?: ReadonlyArray<MinimumSchema>;
+	readonly allOf?: ReadonlyArray<MinimumSchema>;
+}
+
+export interface MinimumSchema extends OfSchema {
+	readonly type?: TypeStringef;
+	readonly nullable?: boolean;
+	readonly optional?: boolean;
+	readonly items?: MinimumSchema;
+	readonly properties?: Obj;
+	readonly description?: string;
+	readonly example?: any;
+	readonly format?: string;
+	readonly ref?: keyof typeof refs;
+	readonly enum?: ReadonlyArray<string>;
+	readonly default?: (this['type'] extends TypeStringef ? StringDefToType<this['type']> : any) | null;
+	readonly maxLength?: number;
+	readonly minLength?: number;
+}
+
+export interface Schema extends MinimumSchema {
+	readonly nullable: boolean;
+	readonly optional: boolean;
 }
 
 type NonUndefinedPropertyNames<T extends Obj> = {
@@ -69,22 +117,13 @@ type UndefinedPropertyNames<T extends Obj> = {
 	[K in keyof T]: T[K]['optional'] extends true ? K : never
 }[keyof T];
 
-type OnlyRequired<T extends Obj> = Pick<T, NonUndefinedPropertyNames<T>>;
-type OnlyOptional<T extends Obj> = Pick<T, UndefinedPropertyNames<T>>;
-
-export interface Obj extends SimpleObj { [key: string]: Schema; }
+export interface Obj { [key: string]: Schema; }
 
 export type ObjType<s extends Obj> =
-	{ [P in keyof OnlyOptional<s>]?: SchemaType<s[P]> } &
-	{ [P in keyof OnlyRequired<s>]: SchemaType<s[P]> };
+	{ -readonly [P in UndefinedPropertyNames<s>]?: SchemaType<s[P]> } &
+	{ -readonly [P in NonUndefinedPropertyNames<s>]: SchemaType<s[P]> };
 
-// https://qiita.com/hrsh7th@github/items/84e8968c3601009cdcf2
-type MyType<T extends Schema> = {
-	0: any;
-	1: SchemaType<T>;
-}[T extends Schema ? 1 : 0];
-
-type NullOrUndefined<p extends Schema, T> =
+type NullOrUndefined<p extends MinimumSchema, T> =
 	p['nullable'] extends true
 		?	p['optional'] extends true
 			? (T | null | undefined)
@@ -93,15 +132,41 @@ type NullOrUndefined<p extends Schema, T> =
 			? (T | undefined)
 			: T;
 
-export type SchemaType<p extends Schema> =
-	p['type'] extends 'number' ? NullOrUndefined<p, number> :
-	p['type'] extends 'string' ? NullOrUndefined<p, string> :
-	p['type'] extends 'boolean' ? NullOrUndefined<p, boolean> :
-	p['type'] extends 'array' ? NullOrUndefined<p, MyType<NonNullable<p['items']>>[]> :
-	p['type'] extends 'object' ? (
-		p['ref'] extends keyof typeof refs
-			? NullOrUndefined<p, Packed<p['ref']>>
-			: NullOrUndefined<p, ObjType<NonNullable<p['properties']>>>
+// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
+type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
+
+// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552
+// 単純にSchemaTypeDef<X>で判定するだけではダメ
+type UnionSchemaType<a extends readonly any[], X extends MinimumSchema = a[number]> = X extends any ? SchemaType<X> : never;
+type ArrayUnion<T> = T extends any ? Array<T> : never; 
+
+export type SchemaTypeDef<p extends MinimumSchema> =
+	p['type'] extends 'number' ? number :
+	p['type'] extends 'string' ? (
+		p['enum'] extends readonly string[] ?
+			p['enum'][number] :
+			p['format'] extends 'date-time' ? string : // Dateにする??
+			string
 	) :
-	p['type'] extends 'any' ? NullOrUndefined<p, any> :
+	p['type'] extends 'boolean' ? boolean :
+	p['type'] extends 'object' ? (
+		p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
+		p['properties'] extends NonNullable<Obj> ? ObjType<p['properties']> :
+		p['anyOf'] extends ReadonlyArray<MinimumSchema> ? UnionSchemaType<p['anyOf']> & Partial<UnionToIntersection<UnionSchemaType<p['anyOf']>>> :
+		p['allOf'] extends ReadonlyArray<MinimumSchema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
+		any
+	) :
+	p['type'] extends 'array' ? (
+		p['items'] extends OfSchema ? (
+			p['items']['anyOf'] extends ReadonlyArray<MinimumSchema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] :
+			p['items']['oneOf'] extends ReadonlyArray<MinimumSchema> ? ArrayUnion<UnionSchemaType<NonNullable<p['items']['oneOf']>>> :
+			p['items']['allOf'] extends ReadonlyArray<MinimumSchema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
+			never
+		) :
+		p['items'] extends NonNullable<MinimumSchema> ? SchemaTypeDef<p['items']>[] :
+		any[]
+	) :
+	p['oneOf'] extends ReadonlyArray<MinimumSchema> ? UnionSchemaType<p['oneOf']> :
 	any;
+
+export type SchemaType<p extends MinimumSchema> = NullOrUndefined<p, SchemaTypeDef<p>>;
diff --git a/packages/backend/src/misc/simple-schema.ts b/packages/backend/src/misc/simple-schema.ts
deleted file mode 100644
index abbb348e24..0000000000
--- a/packages/backend/src/misc/simple-schema.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-export interface SimpleSchema {
-	type: 'boolean' | 'number' | 'string' | 'array' | 'object' | 'any';
-	nullable: boolean;
-	optional: boolean;
-	items?: SimpleSchema;
-	properties?: SimpleObj;
-	description?: string;
-	example?: any;
-	format?: string;
-	ref?: string;
-	enum?: string[];
-	default?: boolean | null;
-}
-
-export interface SimpleObj { [key: string]: SimpleSchema; }
diff --git a/packages/backend/src/models/entities/abuse-user-report.ts b/packages/backend/src/models/entities/abuse-user-report.ts
index 019d613f76..27c1e47fd8 100644
--- a/packages/backend/src/models/entities/abuse-user-report.ts
+++ b/packages/backend/src/models/entities/abuse-user-report.ts
@@ -51,6 +51,11 @@ export class AbuseUserReport {
 	})
 	public resolved: boolean;
 
+	@Column('boolean', {
+		default: false
+	})
+	public forwarded: boolean;
+
 	@Column('varchar', {
 		length: 2048,
 	})
diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts
index 0af52d7cc0..cec86880f5 100644
--- a/packages/backend/src/models/entities/drive-file.ts
+++ b/packages/backend/src/models/entities/drive-file.ts
@@ -101,6 +101,11 @@ export class DriveFile {
 	})
 	public webpublicUrl: string | null;
 
+	@Column('varchar', {
+		length: 128, nullable: true,
+	})
+	public webpublicType: string | null;
+
 	@Index({ unique: true })
 	@Column('varchar', {
 		length: 256, nullable: true,
diff --git a/packages/backend/src/models/entities/emoji.ts b/packages/backend/src/models/entities/emoji.ts
index 1146908a88..2e9c11d21c 100644
--- a/packages/backend/src/models/entities/emoji.ts
+++ b/packages/backend/src/models/entities/emoji.ts
@@ -32,13 +32,19 @@ export class Emoji {
 	@Column('varchar', {
 		length: 512,
 	})
-	public url: string;
+	public originalUrl: string;
+
+	@Column('varchar', {
+		length: 512,
+	})
+	public publicUrl: string;
 
 	@Column('varchar', {
 		length: 512, nullable: true,
 	})
 	public uri: string | null;
 
+	// publicUrlの方のtypeが入る
 	@Column('varchar', {
 		length: 64, nullable: true,
 	})
diff --git a/packages/backend/src/models/entities/games/reversi/game.ts b/packages/backend/src/models/entities/games/reversi/game.ts
deleted file mode 100644
index fe9b8a5ba5..0000000000
--- a/packages/backend/src/models/entities/games/reversi/game.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from '../../user';
-import { id } from '../../../id';
-
-@Entity()
-export class ReversiGame {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the ReversiGame.',
-	})
-	public createdAt: Date;
-
-	@Column('timestamp with time zone', {
-		nullable: true,
-		comment: 'The started date of the ReversiGame.',
-	})
-	public startedAt: Date | null;
-
-	@Column(id())
-	public user1Id: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE',
-	})
-	@JoinColumn()
-	public user1: User | null;
-
-	@Column(id())
-	public user2Id: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE',
-	})
-	@JoinColumn()
-	public user2: User | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public user1Accepted: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public user2Accepted: boolean;
-
-	/**
-	 * どちらのプレイヤーが先行(黒)か
-	 * 1 ... user1
-	 * 2 ... user2
-	 */
-	@Column('integer', {
-		nullable: true,
-	})
-	public black: number | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public isStarted: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public isEnded: boolean;
-
-	@Column({
-		...id(),
-		nullable: true,
-	})
-	public winnerId: User['id'] | null;
-
-	@Column({
-		...id(),
-		nullable: true,
-	})
-	public surrendered: User['id'] | null;
-
-	@Column('jsonb', {
-		default: [],
-	})
-	public logs: {
-		at: Date;
-		color: boolean;
-		pos: number;
-	}[];
-
-	@Column('varchar', {
-		array: true, length: 64,
-	})
-	public map: string[];
-
-	@Column('varchar', {
-		length: 32,
-	})
-	public bw: string;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public isLlotheo: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public canPutEverywhere: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public loopedBoard: boolean;
-
-	@Column('jsonb', {
-		nullable: true, default: null,
-	})
-	public form1: any | null;
-
-	@Column('jsonb', {
-		nullable: true, default: null,
-	})
-	public form2: any | null;
-
-	/**
-	 * ログのposを文字列としてすべて連結したもののCRC32値
-	 */
-	@Column('varchar', {
-		length: 32, nullable: true,
-	})
-	public crc32: string | null;
-}
diff --git a/packages/backend/src/models/entities/games/reversi/matching.ts b/packages/backend/src/models/entities/games/reversi/matching.ts
deleted file mode 100644
index 70bb555e2f..0000000000
--- a/packages/backend/src/models/entities/games/reversi/matching.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from '../../user';
-import { id } from '../../../id';
-
-@Entity()
-export class ReversiMatching {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the ReversiMatching.',
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column(id())
-	public parentId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE',
-	})
-	@JoinColumn()
-	public parent: User | null;
-
-	@Index()
-	@Column(id())
-	public childId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE',
-	})
-	@JoinColumn()
-	public child: User | null;
-}
diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts
index 7154cca550..67da347395 100644
--- a/packages/backend/src/models/index.ts
+++ b/packages/backend/src/models/index.ts
@@ -18,7 +18,6 @@ import { AccessToken } from './entities/access-token';
 import { UserNotePining } from './entities/user-note-pining';
 import { SigninRepository } from './repositories/signin';
 import { MessagingMessageRepository } from './repositories/messaging-message';
-import { ReversiGameRepository } from './repositories/games/reversi/game';
 import { UserListRepository } from './repositories/user-list';
 import { UserListJoining } from './entities/user-list-joining';
 import { UserGroupRepository } from './repositories/user-group';
@@ -30,7 +29,6 @@ import { BlockingRepository } from './repositories/blocking';
 import { NoteReactionRepository } from './repositories/note-reaction';
 import { NotificationRepository } from './repositories/notification';
 import { NoteFavoriteRepository } from './repositories/note-favorite';
-import { ReversiMatchingRepository } from './repositories/games/reversi/matching';
 import { UserPublickey } from './entities/user-publickey';
 import { UserKeypair } from './entities/user-keypair';
 import { AppRepository } from './repositories/app';
@@ -107,8 +105,6 @@ export const AuthSessions = getCustomRepository(AuthSessionRepository);
 export const AccessTokens = getRepository(AccessToken);
 export const Signins = getCustomRepository(SigninRepository);
 export const MessagingMessages = getCustomRepository(MessagingMessageRepository);
-export const ReversiGames = getCustomRepository(ReversiGameRepository);
-export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
 export const Pages = getCustomRepository(PageRepository);
 export const PageLikes = getCustomRepository(PageLikeRepository);
 export const GalleryPosts = getCustomRepository(GalleryPostRepository);
diff --git a/packages/backend/src/models/repositories/abuse-user-report.ts b/packages/backend/src/models/repositories/abuse-user-report.ts
index 5e267b3c2b..943b65eb64 100644
--- a/packages/backend/src/models/repositories/abuse-user-report.ts
+++ b/packages/backend/src/models/repositories/abuse-user-report.ts
@@ -27,6 +27,7 @@ export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
 			assignee: report.assigneeId ? Users.pack(report.assignee || report.assigneeId, null, {
 				detail: true,
 			}) : null,
+			forwarded: report.forwarded,
 		});
 	}
 
diff --git a/packages/backend/src/models/repositories/antenna.ts b/packages/backend/src/models/repositories/antenna.ts
index 548f44f1b7..3bf0645a7f 100644
--- a/packages/backend/src/models/repositories/antenna.ts
+++ b/packages/backend/src/models/repositories/antenna.ts
@@ -31,94 +31,3 @@ export class AntennaRepository extends Repository<Antenna> {
 		};
 	}
 }
-
-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',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		keywords: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-			items: {
-				type: 'array' as const,
-				optional: false as const, nullable: false as const,
-				items: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-				},
-			},
-		},
-		excludeKeywords: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-			items: {
-				type: 'array' as const,
-				optional: false as const, nullable: false as const,
-				items: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-				},
-			},
-		},
-		src: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			enum: ['home', 'all', 'users', 'list', 'group'],
-		},
-		userListId: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id',
-		},
-		userGroupId: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id',
-		},
-		users: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			},
-		},
-		caseSensitive: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-			default: false,
-		},
-		notify: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		withReplies: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-			default: false,
-		},
-		withFile: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		hasUnreadNote: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-			default: false,
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/app.ts b/packages/backend/src/models/repositories/app.ts
index bec0765ac2..6bac4d9598 100644
--- a/packages/backend/src/models/repositories/app.ts
+++ b/packages/backend/src/models/repositories/app.ts
@@ -38,38 +38,3 @@ export class AppRepository extends Repository<App> {
 		};
 	}
 }
-
-export const packedAppSchema = {
-	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,
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		callbackUrl: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		permission: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			},
-		},
-		secret: {
-			type: 'string' as const,
-			optional: true as const, nullable: false as const,
-		},
-		isAuthorized: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const,
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/blocking.ts b/packages/backend/src/models/repositories/blocking.ts
index a6895eabf4..c20b02f501 100644
--- a/packages/backend/src/models/repositories/blocking.ts
+++ b/packages/backend/src/models/repositories/blocking.ts
@@ -30,31 +30,3 @@ export class BlockingRepository extends Repository<Blocking> {
 		return Promise.all(blockings.map(x => this.pack(x, me)));
 	}
 }
-
-export const packedBlockingSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		blockeeId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		blockee: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User' as const,
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/channel.ts b/packages/backend/src/models/repositories/channel.ts
index 0a6b02f495..b3afb823ab 100644
--- a/packages/backend/src/models/repositories/channel.ts
+++ b/packages/backend/src/models/repositories/channel.ts
@@ -40,56 +40,3 @@ export class ChannelRepository extends Repository<Channel> {
 		};
 	}
 }
-
-export const packedChannelSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		lastNotedAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'date-time',
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		description: {
-			type: 'string' as const,
-			nullable: true as const, optional: false as const,
-		},
-		bannerUrl: {
-			type: 'string' as const,
-			format: 'url',
-			nullable: true as const, optional: false as const,
-		},
-		notesCount: {
-			type: 'number' as const,
-			nullable: false as const, optional: false as const,
-		},
-		usersCount: {
-			type: 'number' as const,
-			nullable: false as const, optional: false as const,
-		},
-		isFollowing: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const,
-		},
-		userId: {
-			type: 'string' as const,
-			nullable: true as const, optional: false as const,
-			format: 'id',
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/clip.ts b/packages/backend/src/models/repositories/clip.ts
index 7892811d48..6f9ceeb50a 100644
--- a/packages/backend/src/models/repositories/clip.ts
+++ b/packages/backend/src/models/repositories/clip.ts
@@ -29,42 +29,3 @@ export class ClipRepository extends Repository<Clip> {
 	}
 }
 
-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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		userId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		user: {
-			type: 'object' as const,
-			ref: 'User' as const,
-			optional: false as const, nullable: false as const,
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		description: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		isPublic: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts
index 79b890aa6e..44db9a0a58 100644
--- a/packages/backend/src/models/repositories/drive-file.ts
+++ b/packages/backend/src/models/repositories/drive-file.ts
@@ -3,7 +3,7 @@ import { DriveFile } from '@/models/entities/drive-file';
 import { Users, DriveFolders } from '../index';
 import { User } from '@/models/entities/user';
 import { toPuny } from '@/misc/convert-host';
-import { awaitAll } from '@/prelude/await-all';
+import { awaitAll, Promiseable } from '@/prelude/await-all';
 import { Packed } from '@/misc/schema';
 import config from '@/config/index';
 import { query, appendQuery } from '@/prelude/url';
@@ -126,7 +126,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
 
 		const meta = await fetchMeta();
 
-		return await awaitAll({
+		return await awaitAll<Packed<'DriveFile'>>({
 			id: file.id,
 			createdAt: file.createdAt.toISOString(),
 			name: file.name,
@@ -156,112 +156,3 @@ export class DriveFileRepository extends Repository<DriveFile> {
 		return items.filter(x => x != null);
 	}
 }
-
-export const packedDriveFileSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			example: 'lenna.jpg',
-		},
-		type: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			example: 'image/jpeg',
-		},
-		md5: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'md5',
-			example: '15eca7fba0480996e2245f5185bf39f2',
-		},
-		size: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-			example: 51469,
-		},
-		isSensitive: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		blurhash: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		properties: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				width: {
-					type: 'number' as const,
-					optional: true as const, nullable: false as const,
-					example: 1280,
-				},
-				height: {
-					type: 'number' as const,
-					optional: true as const, nullable: false as const,
-					example: 720,
-				},
-				orientation: {
-					type: 'number' as const,
-					optional: true as const, nullable: false as const,
-					example: 8,
-				},
-				avgColor: {
-					type: 'string' as const,
-					optional: true as const, nullable: false as const,
-					example: 'rgb(40,65,87)',
-				},
-			},
-		},
-		url: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'url',
-		},
-		thumbnailUrl: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'url',
-		},
-		comment: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		folderId: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		folder: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			ref: 'DriveFolder' as const,
-		},
-		userId: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		user: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			ref: 'User' as const,
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/drive-folder.ts b/packages/backend/src/models/repositories/drive-folder.ts
index 4ee4a68e08..b2e6cee9b8 100644
--- a/packages/backend/src/models/repositories/drive-folder.ts
+++ b/packages/backend/src/models/repositories/drive-folder.ts
@@ -48,44 +48,3 @@ export class DriveFolderRepository extends Repository<DriveFolder> {
 		});
 	}
 }
-
-export const packedDriveFolderSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		foldersCount: {
-			type: 'number' as const,
-			optional: true as const, nullable: false as const,
-		},
-		filesCount: {
-			type: 'number' as const,
-			optional: true as const, nullable: false as const,
-		},
-		parentId: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		parent: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			ref: 'DriveFolder' as const,
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/emoji.ts b/packages/backend/src/models/repositories/emoji.ts
index b7529595a9..b9dc6ed0ac 100644
--- a/packages/backend/src/models/repositories/emoji.ts
+++ b/packages/backend/src/models/repositories/emoji.ts
@@ -15,7 +15,8 @@ export class EmojiRepository extends Repository<Emoji> {
 			name: emoji.name,
 			category: emoji.category,
 			host: emoji.host,
-			url: emoji.url,
+			// || emoji.originalUrl してるのは後方互換性のため
+			url: emoji.publicUrl || emoji.originalUrl,
 		};
 	}
 
@@ -25,41 +26,3 @@ export class EmojiRepository extends Repository<Emoji> {
 		return Promise.all(emojis.map(x => this.pack(x)));
 	}
 }
-
-export const packedEmojiSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		aliases: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id',
-			},
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		category: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		host: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		url: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/federation-instance.ts b/packages/backend/src/models/repositories/federation-instance.ts
index 90dbbaab1c..426fd5bfc3 100644
--- a/packages/backend/src/models/repositories/federation-instance.ts
+++ b/packages/backend/src/models/repositories/federation-instance.ts
@@ -1,106 +1,2 @@
 import config from '@/config/index';
 
-export const packedFederationInstanceSchema = {
-	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',
-		},
-		caughtAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		host: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			example: 'misskey.example.com',
-		},
-		usersCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		notesCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		followingCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		followersCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		driveUsage: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		driveFiles: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		latestRequestSentAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'date-time',
-		},
-		lastCommunicatedAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		isNotResponding: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		isSuspended: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		softwareName: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			example: 'misskey',
-		},
-		softwareVersion: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			example: config.version,
-		},
-		openRegistrations: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: true as const,
-			example: true,
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		description: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		maintainerName: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		maintainerEmail: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		iconUrl: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'url',
-		},
-		infoUpdatedAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'date-time',
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/following.ts b/packages/backend/src/models/repositories/following.ts
index 1dfaaf908a..9d20f442df 100644
--- a/packages/backend/src/models/repositories/following.ts
+++ b/packages/backend/src/models/repositories/following.ts
@@ -84,41 +84,3 @@ export class FollowingRepository extends Repository<Following> {
 		return Promise.all(followings.map(x => this.pack(x, me, opts)));
 	}
 }
-
-export const packedFollowingSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		followeeId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		followee: {
-			type: 'object' as const,
-			optional: true as const, nullable: false as const,
-			ref: 'User' as const,
-		},
-		followerId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		follower: {
-			type: 'object' as const,
-			optional: true as const, nullable: false as const,
-			ref: 'User' as const,
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/gallery-post.ts b/packages/backend/src/models/repositories/gallery-post.ts
index 6d37e3120e..e9233bb91e 100644
--- a/packages/backend/src/models/repositories/gallery-post.ts
+++ b/packages/backend/src/models/repositories/gallery-post.ts
@@ -38,74 +38,3 @@ export class GalleryPostRepository extends Repository<GalleryPost> {
 		return Promise.all(posts.map(x => this.pack(x, me)));
 	}
 }
-
-export const packedGalleryPostSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		updatedAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		title: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		description: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		userId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		user: {
-			type: 'object' as const,
-			ref: 'User' as const,
-			optional: false as const, nullable: false as const,
-		},
-		fileIds: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id',
-			},
-		},
-		files: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				ref: 'DriveFile' as const,
-			},
-		},
-		tags: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			},
-		},
-		isSensitive: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/games/reversi/game.ts b/packages/backend/src/models/repositories/games/reversi/game.ts
deleted file mode 100644
index a9e0496760..0000000000
--- a/packages/backend/src/models/repositories/games/reversi/game.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-import { User } from '@/models/entities/user';
-import { EntityRepository, Repository } from 'typeorm';
-import { Users } from '../../../index';
-import { ReversiGame } from '@/models/entities/games/reversi/game';
-import { Packed } from '@/misc/schema';
-
-@EntityRepository(ReversiGame)
-export class ReversiGameRepository extends Repository<ReversiGame> {
-	public async pack(
-		src: ReversiGame['id'] | ReversiGame,
-		me?: { id: User['id'] } | null | undefined,
-		options?: {
-			detail?: boolean
-		}
-	): Promise<Packed<'ReversiGame'>> {
-		const opts = Object.assign({
-			detail: true,
-		}, options);
-
-		const game = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return {
-			id: game.id,
-			createdAt: game.createdAt.toISOString(),
-			startedAt: game.startedAt && game.startedAt.toISOString(),
-			isStarted: game.isStarted,
-			isEnded: game.isEnded,
-			form1: game.form1,
-			form2: game.form2,
-			user1Accepted: game.user1Accepted,
-			user2Accepted: game.user2Accepted,
-			user1Id: game.user1Id,
-			user2Id: game.user2Id,
-			user1: await Users.pack(game.user1Id, me),
-			user2: await Users.pack(game.user2Id, me),
-			winnerId: game.winnerId,
-			winner: game.winnerId ? await Users.pack(game.winnerId, me) : null,
-			surrendered: game.surrendered,
-			black: game.black,
-			bw: game.bw,
-			isLlotheo: game.isLlotheo,
-			canPutEverywhere: game.canPutEverywhere,
-			loopedBoard: game.loopedBoard,
-			...(opts.detail ? {
-				logs: game.logs.map(log => ({
-					at: log.at.toISOString(),
-					color: log.color,
-					pos: log.pos,
-				})),
-				map: game.map,
-			} : {}),
-		};
-	}
-}
-
-export const packedReversiGameSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		startedAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'date-time',
-		},
-		isStarted: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		isEnded: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		form1: {
-			type: 'any' as const,
-			optional: false as const, nullable: true as const,
-		},
-		form2: {
-			type: 'any' as const,
-			optional: false as const, nullable: true as const,
-		},
-		user1Accepted: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		user2Accepted: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		user1Id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		user2Id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		user1: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User' as const,
-		},
-		user2: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User' as const,
-		},
-		winnerId: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		winner: {
-			type: 'object' as const,
-			optional: false as const, nullable: true as const,
-			ref: 'User' as const,
-		},
-		surrendered: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		black: {
-			type: 'number' as const,
-			optional: false as const, nullable: true as const,
-		},
-		bw: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		isLlotheo: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		canPutEverywhere: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		loopedBoard: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		logs: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'object' as const,
-				optional: true as const, nullable: false as const,
-				properties: {
-					at: {
-						type: 'string' as const,
-						optional: false as const, nullable: false as const,
-						format: 'date-time',
-					},
-					color: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
-					},
-					pos: {
-						type: 'number' as const,
-						optional: false as const, nullable: false as const,
-					},
-				},
-			},
-		},
-		map: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			},
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/games/reversi/matching.ts b/packages/backend/src/models/repositories/games/reversi/matching.ts
deleted file mode 100644
index b55f598068..0000000000
--- a/packages/backend/src/models/repositories/games/reversi/matching.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { ReversiMatching } from '@/models/entities/games/reversi/matching';
-import { Users } from '../../../index';
-import { awaitAll } from '@/prelude/await-all';
-import { User } from '@/models/entities/user';
-import { Packed } from '@/misc/schema';
-
-@EntityRepository(ReversiMatching)
-export class ReversiMatchingRepository extends Repository<ReversiMatching> {
-	public async pack(
-		src: ReversiMatching['id'] | ReversiMatching,
-		me: { id: User['id'] }
-	): Promise<Packed<'ReversiMatching'>> {
-		const matching = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return await awaitAll({
-			id: matching.id,
-			createdAt: matching.createdAt.toISOString(),
-			parentId: matching.parentId,
-			parent: Users.pack(matching.parentId, me, {
-				detail: true,
-			}),
-			childId: matching.childId,
-			child: Users.pack(matching.childId, me, {
-				detail: true,
-			}),
-		});
-	}
-}
-
-export const packedReversiMatchingSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		parentId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		parent: {
-			type: 'object' as const,
-			optional: false as const, nullable: true as const,
-			ref: 'User' as const,
-		},
-		childId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		child: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User' as const,
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/hashtag.ts b/packages/backend/src/models/repositories/hashtag.ts
index 6e513c7ebb..c4b8d50c4e 100644
--- a/packages/backend/src/models/repositories/hashtag.ts
+++ b/packages/backend/src/models/repositories/hashtag.ts
@@ -24,39 +24,3 @@ export class HashtagRepository extends Repository<Hashtag> {
 		return Promise.all(hashtags.map(x => this.pack(x)));
 	}
 }
-
-export const packedHashtagSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		tag: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			example: 'misskey',
-		},
-		mentionedUsersCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		mentionedLocalUsersCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		mentionedRemoteUsersCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		attachedUsersCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		attachedLocalUsersCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		attachedRemoteUsersCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/messaging-message.ts b/packages/backend/src/models/repositories/messaging-message.ts
index 1b2dd3a246..0a342430b9 100644
--- a/packages/backend/src/models/repositories/messaging-message.ts
+++ b/packages/backend/src/models/repositories/messaging-message.ts
@@ -42,78 +42,3 @@ export class MessagingMessageRepository extends Repository<MessagingMessage> {
 		};
 	}
 }
-
-export const packedMessagingMessageSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		userId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		user: {
-			type: 'object' as const,
-			ref: 'User' as const,
-			optional: true as const, nullable: false as const,
-		},
-		text: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		fileId: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-			format: 'id',
-		},
-		file: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			ref: 'DriveFile' as const,
-		},
-		recipientId: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id',
-		},
-		recipient: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			ref: 'User' as const,
-		},
-		groupId: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id',
-		},
-		group: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			ref: 'UserGroup' as const,
-		},
-		isRead: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const,
-		},
-		reads: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id',
-			},
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/muting.ts b/packages/backend/src/models/repositories/muting.ts
index b82d1f0daa..bdbe9b47da 100644
--- a/packages/backend/src/models/repositories/muting.ts
+++ b/packages/backend/src/models/repositories/muting.ts
@@ -30,31 +30,3 @@ export class MutingRepository extends Repository<Muting> {
 		return Promise.all(mutings.map(x => this.pack(x, me)));
 	}
 }
-
-export const packedMutingSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		muteeId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		mutee: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User' as const,
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/note-favorite.ts b/packages/backend/src/models/repositories/note-favorite.ts
index 47586a9116..c5de55c0c0 100644
--- a/packages/backend/src/models/repositories/note-favorite.ts
+++ b/packages/backend/src/models/repositories/note-favorite.ts
@@ -26,31 +26,3 @@ export class NoteFavoriteRepository extends Repository<NoteFavorite> {
 		return Promise.all(favorites.map(x => this.pack(x, me)));
 	}
 }
-
-export const packedNoteFavoriteSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		note: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note' as const,
-		},
-		noteId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/note-reaction.ts b/packages/backend/src/models/repositories/note-reaction.ts
index dfb25cbea1..097574effa 100644
--- a/packages/backend/src/models/repositories/note-reaction.ts
+++ b/packages/backend/src/models/repositories/note-reaction.ts
@@ -31,30 +31,3 @@ export class NoteReactionRepository extends Repository<NoteReaction> {
 		};
 	}
 }
-
-export const packedNoteReactionSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		user: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User' as const,
-		},
-		type: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts
index 96dfad70e9..9a7fef4977 100644
--- a/packages/backend/src/models/repositories/note.ts
+++ b/packages/backend/src/models/repositories/note.ts
@@ -218,7 +218,7 @@ export class NoteRepository extends Repository<Note> {
 
 		const reactionEmojiNames = Object.keys(note.reactions).filter(x => x?.startsWith(':')).map(x => decodeReaction(x).reaction).map(x => x.replace(/:/g, ''));
 
-		const packed = await awaitAll({
+		const packed: Packed<'Note'> = await awaitAll({
 			id: note.id,
 			createdAt: note.createdAt.toISOString(),
 			userId: note.userId,
@@ -320,188 +320,3 @@ export class NoteRepository extends Repository<Note> {
 		})));
 	}
 }
-
-export const packedNoteSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		text: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		cw: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-		},
-		userId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		user: {
-			type: 'object' as const,
-			ref: 'User' as const,
-			optional: false as const, nullable: false as const,
-		},
-		replyId: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		renoteId: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		reply: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			ref: 'Note' as const,
-		},
-		renote: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			ref: 'Note' as const,
-		},
-		isHidden: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const,
-		},
-		visibility: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		mentions: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id',
-			},
-		},
-		visibleUserIds: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id',
-			},
-		},
-		fileIds: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id',
-			},
-		},
-		files: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				ref: 'DriveFile' as const,
-			},
-		},
-		tags: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			},
-		},
-		poll: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-		},
-		channelId: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		channel: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			items: {
-				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,
-					},
-					name: {
-						type: 'string' as const,
-						optional: false as const, nullable: true as const,
-					},
-				},
-			},
-		},
-		localOnly: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const,
-		},
-		emojis: {
-			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,
-				properties: {
-					name: {
-						type: 'string' as const,
-						optional: false as const, nullable: false as const,
-					},
-					url: {
-						type: 'string' as const,
-						optional: false as const, nullable: true as const,
-					},
-				},
-			},
-		},
-		reactions: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-		},
-		renoteCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		repliesCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		uri: {
-			type: 'string' as const,
-			optional: true as const, nullable: false as const,
-		},
-		url: {
-			type: 'string' as const,
-			optional: true as const, nullable: false as const,
-		},
-
-		myReaction: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/notification.ts b/packages/backend/src/models/repositories/notification.ts
index 47d569ed21..5e42798898 100644
--- a/packages/backend/src/models/repositories/notification.ts
+++ b/packages/backend/src/models/repositories/notification.ts
@@ -107,69 +107,3 @@ export class NotificationRepository extends Repository<Notification> {
 		})));
 	}
 }
-
-export const packedNotificationSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		isRead: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		type: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			enum: [...notificationTypes],
-		},
-		user: {
-			type: 'object' as const,
-			ref: 'User' as const,
-			optional: true as const, nullable: true as const,
-		},
-		userId: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-			format: 'id',
-		},
-		note: {
-			type: 'object' as const,
-			ref: 'Note' as const,
-			optional: true as const, nullable: true as const,
-		},
-		reaction: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-		},
-		choice: {
-			type: 'number' as const,
-			optional: true as const, nullable: true as const,
-		},
-		invitation: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-		},
-		body: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-		},
-		header: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-		},
-		icon: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/page.ts b/packages/backend/src/models/repositories/page.ts
index 46b453cad9..ec76c2e418 100644
--- a/packages/backend/src/models/repositories/page.ts
+++ b/packages/backend/src/models/repositories/page.ts
@@ -87,56 +87,3 @@ export class PageRepository extends Repository<Page> {
 		return Promise.all(pages.map(x => this.pack(x, me)));
 	}
 }
-
-export const packedPageSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		updatedAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		title: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		summary: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		content: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-		},
-		variables: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-		},
-		userId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		user: {
-			type: 'object' as const,
-			ref: 'User' as const,
-			optional: false as const, nullable: false as const,
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/queue.ts b/packages/backend/src/models/repositories/queue.ts
deleted file mode 100644
index 521c634390..0000000000
--- a/packages/backend/src/models/repositories/queue.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-export const packedQueueCountSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		waiting: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		active: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		completed: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		failed: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		delayed: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		paused: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/user-group.ts b/packages/backend/src/models/repositories/user-group.ts
index 02a0348885..3ed37ca0ed 100644
--- a/packages/backend/src/models/repositories/user-group.ts
+++ b/packages/backend/src/models/repositories/user-group.ts
@@ -23,39 +23,3 @@ export class UserGroupRepository extends Repository<UserGroup> {
 		};
 	}
 }
-
-export const packedUserGroupSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		ownerId: {
-			type: 'string' as const,
-			nullable: false as const, optional: false as const,
-			format: 'id',
-		},
-		userIds: {
-			type: 'array' as const,
-			nullable: false as const, optional: true as const,
-			items: {
-				type: 'string' as const,
-				nullable: false as const, optional: false as const,
-				format: 'id',
-			},
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/user-list.ts b/packages/backend/src/models/repositories/user-list.ts
index 792a17cb49..a2bffe8357 100644
--- a/packages/backend/src/models/repositories/user-list.ts
+++ b/packages/backend/src/models/repositories/user-list.ts
@@ -22,34 +22,3 @@ export class UserListRepository extends Repository<UserList> {
 		};
 	}
 }
-
-export const packedUserListSchema = {
-	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',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		userIds: {
-			type: 'array' as const,
-			nullable: false as const, optional: true as const,
-			items: {
-				type: 'string' as const,
-				nullable: false as const, optional: false as const,
-				format: 'id',
-			},
-		},
-	},
-};
diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts
index 3dc7c67ec2..2b8398832d 100644
--- a/packages/backend/src/models/repositories/user.ts
+++ b/packages/backend/src/models/repositories/user.ts
@@ -4,11 +4,19 @@ import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
 import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances } from '../index';
 import config from '@/config/index';
 import { Packed } from '@/misc/schema';
-import { awaitAll } from '@/prelude/await-all';
+import { awaitAll, Promiseable } from '@/prelude/await-all';
 import { populateEmojis } from '@/misc/populate-emojis';
 import { getAntennas } from '@/misc/antenna-cache';
 import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const';
 
+type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
+type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> =
+	Detailed extends true ? 
+		ExpectsMe extends true ? Packed<'MeDetailed'> :
+		ExpectsMe extends false ? Packed<'UserDetailedNotMe'> :
+		Packed<'UserDetailed'> :
+	Packed<'UserLite'>;
+
 @EntityRepository(User)
 export class UserRepository extends Repository<User> {
 	public async getRelation(me: User['id'], target: User['id']) {
@@ -144,7 +152,7 @@ export class UserRepository extends Repository<User> {
 		return count > 0;
 	}
 
-	public getOnlineStatus(user: User): string {
+	public getOnlineStatus(user: User): 'unknown' | 'online' | 'active' | 'offline' {
 		if (user.hideOnlineStatus) return 'unknown';
 		if (user.lastActiveDate == null) return 'unknown';
 		const elapsed = Date.now() - user.lastActiveDate.getTime();
@@ -159,18 +167,18 @@ export class UserRepository extends Repository<User> {
 		if (user.avatarUrl) {
 			return user.avatarUrl;
 		} else {
-			return `${config.url}/random-avatar/${user.id}`;
+			return `${config.url}/identicon/${user.id}`;
 		}
 	}
 
-	public async pack(
+	public async pack<ExpectsMe extends boolean | null = null, D extends boolean = false>(
 		src: User['id'] | User,
 		me?: { id: User['id'] } | null | undefined,
 		options?: {
-			detail?: boolean,
+			detail?: D,
 			includeSecrets?: boolean,
 		}
-	): Promise<Packed<'User'>> {
+	): Promise<IsMeAndIsUserDetailed<ExpectsMe, D>> {
 		const opts = Object.assign({
 			detail: false,
 			includeSecrets: false,
@@ -178,8 +186,9 @@ export class UserRepository extends Repository<User> {
 
 		const user = typeof src === 'object' ? src : await this.findOneOrFail(src);
 		const meId = me ? me.id : null;
+		const isMe = meId === user.id;
 
-		const relation = meId && (meId !== user.id) && opts.detail ? await this.getRelation(meId, user.id) : null;
+		const relation = meId && !isMe && opts.detail ? await this.getRelation(meId, user.id) : null;
 		const pins = opts.detail ? await UserNotePinings.createQueryBuilder('pin')
 			.where('pin.userId = :userId', { userId: user.id })
 			.innerJoinAndSelect('pin.note', 'note')
@@ -188,12 +197,12 @@ export class UserRepository extends Repository<User> {
 		const profile = opts.detail ? await UserProfiles.findOneOrFail(user.id) : null;
 
 		const followingCount = profile == null ? null :
-			(profile.ffVisibility === 'public') || (meId === user.id) ? user.followingCount :
+			(profile.ffVisibility === 'public') || isMe ? user.followingCount :
 			(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
 			null;
 
 		const followersCount = profile == null ? null :
-			(profile.ffVisibility === 'public') || (meId === user.id) ? user.followersCount :
+			(profile.ffVisibility === 'public') || isMe ? user.followersCount :
 			(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
 			null;
 
@@ -227,12 +236,11 @@ export class UserRepository extends Repository<User> {
 				uri: user.uri,
 				createdAt: user.createdAt.toISOString(),
 				updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
-				lastFetchedAt: user.lastFetchedAt?.toISOString(),
+				lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
 				bannerUrl: user.bannerUrl,
 				bannerBlurhash: user.bannerBlurhash,
 				bannerColor: null, // 後方互換性のため
 				isLocked: user.isLocked,
-				isModerator: user.isModerator || falsy,
 				isSilenced: user.isSilenced || falsy,
 				isSuspended: user.isSuspended || falsy,
 				description: profile!.description,
@@ -260,7 +268,7 @@ export class UserRepository extends Repository<User> {
 					: false,
 			} : {}),
 
-			...(opts.detail && meId === user.id ? {
+			...(opts.detail && isMe ? {
 				avatarId: user.avatarId,
 				bannerId: user.bannerId,
 				injectFeaturedNote: profile!.injectFeaturedNote,
@@ -315,19 +323,19 @@ export class UserRepository extends Repository<User> {
 				isBlocked: relation.isBlocked,
 				isMuted: relation.isMuted,
 			} : {}),
-		};
+		} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
 
 		return await awaitAll(packed);
 	}
 
-	public packMany(
+	public packMany<D extends boolean = false>(
 		users: (User['id'] | User)[],
 		me?: { id: User['id'] } | null | undefined,
 		options?: {
-			detail?: boolean,
+			detail?: D,
 			includeSecrets?: boolean,
 		}
-	) {
+	): Promise<IsUserDetailed<D>[]> {
 		return Promise.all(users.map(u => this.pack(u, me, options)));
 	}
 
@@ -352,313 +360,3 @@ export class UserRepository extends Repository<User> {
 	public validateBirthday = $.str.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/);
 	//#endregion
 }
-
-export const packedUserSchema = {
-	type: 'object' as const,
-	nullable: false as const, optional: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			nullable: false as const, optional: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		name: {
-			type: 'string' as const,
-			nullable: true as const, optional: false as const,
-			example: '藍',
-		},
-		username: {
-			type: 'string' as const,
-			nullable: false as const, optional: false as const,
-			example: 'ai',
-		},
-		host: {
-			type: 'string' as const,
-			nullable: true as const, optional: false as const,
-			example: 'misskey.example.com',
-		},
-		avatarUrl: {
-			type: 'string' as const,
-			format: 'url',
-			nullable: true as const, optional: false as const,
-		},
-		avatarBlurhash: {
-			type: 'any' as const,
-			nullable: true as const, optional: false as const,
-		},
-		avatarColor: {
-			type: 'any' as const,
-			nullable: true as const, optional: false as const,
-			default: null,
-		},
-		isAdmin: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-			default: false,
-		},
-		isModerator: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-			default: false,
-		},
-		isBot: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		isCat: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		emojis: {
-			type: 'array' as const,
-			nullable: false as const, optional: false as const,
-			items: {
-				type: 'object' as const,
-				nullable: false as const, optional: false as const,
-				properties: {
-					name: {
-						type: 'string' as const,
-						nullable: false as const, optional: false as const,
-					},
-					url: {
-						type: 'string' as const,
-						nullable: false as const, optional: false as const,
-						format: 'url',
-					},
-				},
-			},
-		},
-		url: {
-			type: 'string' as const,
-			format: 'url',
-			nullable: true as const, optional: true as const,
-		},
-		createdAt: {
-			type: 'string' as const,
-			nullable: false as const, optional: true as const,
-			format: 'date-time',
-		},
-		updatedAt: {
-			type: 'string' as const,
-			nullable: true as const, optional: true as const,
-			format: 'date-time',
-		},
-		bannerUrl: {
-			type: 'string' as const,
-			format: 'url',
-			nullable: true as const, optional: true as const,
-		},
-		bannerBlurhash: {
-			type: 'any' as const,
-			nullable: true as const, optional: true as const,
-		},
-		bannerColor: {
-			type: 'any' as const,
-			nullable: true as const, optional: true as const,
-			default: null,
-		},
-		isLocked: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		isSuspended: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-			example: false,
-		},
-		description: {
-			type: 'string' as const,
-			nullable: true as const, optional: true as const,
-			example: 'Hi masters, I am Ai!',
-		},
-		location: {
-			type: 'string' as const,
-			nullable: true as const, optional: true as const,
-		},
-		birthday: {
-			type: 'string' as const,
-			nullable: true as const, optional: true as const,
-			example: '2018-03-12',
-		},
-		fields: {
-			type: 'array' as const,
-			nullable: false as const, optional: true as const,
-			items: {
-				type: 'object' as const,
-				nullable: false as const, optional: false as const,
-				properties: {
-					name: {
-						type: 'string' as const,
-						nullable: false as const, optional: false as const,
-					},
-					value: {
-						type: 'string' as const,
-						nullable: false as const, optional: false as const,
-					},
-				},
-				maxLength: 4,
-			},
-		},
-		followersCount: {
-			type: 'number' as const,
-			nullable: false as const, optional: true as const,
-		},
-		followingCount: {
-			type: 'number' as const,
-			nullable: false as const, optional: true as const,
-		},
-		notesCount: {
-			type: 'number' as const,
-			nullable: false as const, optional: true as const,
-		},
-		pinnedNoteIds: {
-			type: 'array' as const,
-			nullable: false as const, optional: true as const,
-			items: {
-				type: 'string' as const,
-				nullable: false as const, optional: false as const,
-				format: 'id',
-			},
-		},
-		pinnedNotes: {
-			type: 'array' as const,
-			nullable: false as const, optional: true as const,
-			items: {
-				type: 'object' as const,
-				nullable: false as const, optional: false as const,
-				ref: 'Note' as const,
-			},
-		},
-		pinnedPageId: {
-			type: 'string' as const,
-			nullable: true as const, optional: true as const,
-		},
-		pinnedPage: {
-			type: 'object' as const,
-			nullable: true as const, optional: true as const,
-			ref: 'Page' as const,
-		},
-		twoFactorEnabled: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-			default: false,
-		},
-		usePasswordLessLogin: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-			default: false,
-		},
-		securityKeys: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-			default: false,
-		},
-		avatarId: {
-			type: 'string' as const,
-			nullable: true as const, optional: true as const,
-			format: 'id',
-		},
-		bannerId: {
-			type: 'string' as const,
-			nullable: true as const, optional: true as const,
-			format: 'id',
-		},
-		autoWatch: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		injectFeaturedNote: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		alwaysMarkNsfw: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		carefulBot: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		autoAcceptFollowed: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		hasUnreadSpecifiedNotes: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		hasUnreadMentions: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		hasUnreadAnnouncement: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		hasUnreadAntenna: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		hasUnreadChannel: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		hasUnreadMessagingMessage: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		hasUnreadNotification: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		hasPendingReceivedFollowRequest: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		integrations: {
-			type: 'object' as const,
-			nullable: false as const, optional: true as const,
-		},
-		mutedWords: {
-			type: 'array' as const,
-			nullable: false as const, optional: true as const,
-		},
-		mutedInstances: {
-			type: 'array' as const,
-			nullable: false as const, optional: true as const,
-		},
-		mutingNotificationTypes: {
-			type: 'array' as const,
-			nullable: false as const, optional: true as const,
-		},
-		isFollowing: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const,
-		},
-		hasPendingFollowRequestFromYou: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const,
-		},
-		hasPendingFollowRequestToYou: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const,
-		},
-		isFollowed: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const,
-		},
-		isBlocking: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const,
-		},
-		isBlocked: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const,
-		},
-		isMuted: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const,
-		},
-	},
-};
diff --git a/packages/backend/src/models/schema/antenna.ts b/packages/backend/src/models/schema/antenna.ts
new file mode 100644
index 0000000000..9cf522802c
--- /dev/null
+++ b/packages/backend/src/models/schema/antenna.ts
@@ -0,0 +1,89 @@
+export const packedAntennaSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+		},
+		createdAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		name: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		keywords: {
+			type: 'array',
+			optional: false, nullable: false,
+			items: {
+				type: 'array',
+				optional: false, nullable: false,
+				items: {
+					type: 'string',
+					optional: false, nullable: false,
+				},
+			},
+		},
+		excludeKeywords: {
+			type: 'array',
+			optional: false, nullable: false,
+			items: {
+				type: 'array',
+				optional: false, nullable: false,
+				items: {
+					type: 'string',
+					optional: false, nullable: false,
+				},
+			},
+		},
+		src: {
+			type: 'string',
+			optional: false, nullable: false,
+			enum: ['home', 'all', 'users', 'list', 'group'],
+		},
+		userListId: {
+			type: 'string',
+			optional: false, nullable: true,
+			format: 'id',
+		},
+		userGroupId: {
+			type: 'string',
+			optional: false, nullable: true,
+			format: 'id',
+		},
+		users: {
+			type: 'array',
+			optional: false, nullable: false,
+			items: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+		},
+		caseSensitive: {
+			type: 'boolean',
+			optional: false, nullable: false,
+			default: false,
+		},
+		notify: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		withReplies: {
+			type: 'boolean',
+			optional: false, nullable: false,
+			default: false,
+		},
+		withFile: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		hasUnreadNote: {
+			type: 'boolean',
+			optional: false, nullable: false,
+			default: false,
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/app.ts b/packages/backend/src/models/schema/app.ts
new file mode 100644
index 0000000000..c80dc81c33
--- /dev/null
+++ b/packages/backend/src/models/schema/app.ts
@@ -0,0 +1,33 @@
+export const packedAppSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		name: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		callbackUrl: {
+			type: 'string',
+			optional: false, nullable: true,
+		},
+		permission: {
+			type: 'array',
+			optional: false, nullable: false,
+			items: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+		},
+		secret: {
+			type: 'string',
+			optional: true, nullable: false,
+		},
+		isAuthorized: {
+			type: 'boolean',
+			optional: true, nullable: false,
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/blocking.ts b/packages/backend/src/models/schema/blocking.ts
new file mode 100644
index 0000000000..5532322420
--- /dev/null
+++ b/packages/backend/src/models/schema/blocking.ts
@@ -0,0 +1,26 @@
+export const packedBlockingSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		blockeeId: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+		},
+		blockee: {
+			type: 'object',
+			optional: false, nullable: false,
+			ref: 'UserDetailed',
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/channel.ts b/packages/backend/src/models/schema/channel.ts
new file mode 100644
index 0000000000..7f4f2a48b8
--- /dev/null
+++ b/packages/backend/src/models/schema/channel.ts
@@ -0,0 +1,51 @@
+export const packedChannelSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		lastNotedAt: {
+			type: 'string',
+			optional: false, nullable: true,
+			format: 'date-time',
+		},
+		name: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		description: {
+			type: 'string',
+			nullable: true, optional: false,
+		},
+		bannerUrl: {
+			type: 'string',
+			format: 'url',
+			nullable: true, optional: false,
+		},
+		notesCount: {
+			type: 'number',
+			nullable: false, optional: false,
+		},
+		usersCount: {
+			type: 'number',
+			nullable: false, optional: false,
+		},
+		isFollowing: {
+			type: 'boolean',
+			optional: true, nullable: false,
+		},
+		userId: {
+			type: 'string',
+			nullable: true, optional: false,
+			format: 'id',
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/clip.ts b/packages/backend/src/models/schema/clip.ts
new file mode 100644
index 0000000000..f0ee2ce0c4
--- /dev/null
+++ b/packages/backend/src/models/schema/clip.ts
@@ -0,0 +1,38 @@
+export const packedClipSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		userId: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+		},
+		user: {
+			type: 'object',
+			ref: 'UserLite',
+			optional: false, nullable: false,
+		},
+		name: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		description: {
+			type: 'string',
+			optional: false, nullable: true,
+		},
+		isPublic: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/drive-file.ts b/packages/backend/src/models/schema/drive-file.ts
new file mode 100644
index 0000000000..4359076612
--- /dev/null
+++ b/packages/backend/src/models/schema/drive-file.ts
@@ -0,0 +1,107 @@
+export const packedDriveFileSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		name: {
+			type: 'string',
+			optional: false, nullable: false,
+			example: 'lenna.jpg',
+		},
+		type: {
+			type: 'string',
+			optional: false, nullable: false,
+			example: 'image/jpeg',
+		},
+		md5: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'md5',
+			example: '15eca7fba0480996e2245f5185bf39f2',
+		},
+		size: {
+			type: 'number',
+			optional: false, nullable: false,
+			example: 51469,
+		},
+		isSensitive: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		blurhash: {
+			type: 'string',
+			optional: false, nullable: true,
+		},
+		properties: {
+			type: 'object',
+			optional: false, nullable: false,
+			properties: {
+				width: {
+					type: 'number',
+					optional: true, nullable: false,
+					example: 1280,
+				},
+				height: {
+					type: 'number',
+					optional: true, nullable: false,
+					example: 720,
+				},
+				orientation: {
+					type: 'number',
+					optional: true, nullable: false,
+					example: 8,
+				},
+				avgColor: {
+					type: 'string',
+					optional: true, nullable: false,
+					example: 'rgb(40,65,87)',
+				},
+			},
+		},
+		url: {
+			type: 'string',
+			optional: false, nullable: true,
+			format: 'url',
+		},
+		thumbnailUrl: {
+			type: 'string',
+			optional: false, nullable: true,
+			format: 'url',
+		},
+		comment: {
+			type: 'string',
+			optional: false, nullable: true,
+		},
+		folderId: {
+			type: 'string',
+			optional: false, nullable: true,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		folder: {
+			type: 'object',
+			optional: true, nullable: true,
+			ref: 'DriveFolder',
+		},
+		userId: {
+			type: 'string',
+			optional: false, nullable: true,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		user: {
+			type: 'object',
+			optional: true, nullable: true,
+			ref: 'UserLite',
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/drive-folder.ts b/packages/backend/src/models/schema/drive-folder.ts
new file mode 100644
index 0000000000..88cb8ab4a2
--- /dev/null
+++ b/packages/backend/src/models/schema/drive-folder.ts
@@ -0,0 +1,39 @@
+export const packedDriveFolderSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		name: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		foldersCount: {
+			type: 'number',
+			optional: true, nullable: false,
+		},
+		filesCount: {
+			type: 'number',
+			optional: true, nullable: false,
+		},
+		parentId: {
+			type: 'string',
+			optional: false, nullable: true,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		parent: {
+			type: 'object',
+			optional: true, nullable: true,
+			ref: 'DriveFolder',
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/emoji.ts b/packages/backend/src/models/schema/emoji.ts
new file mode 100644
index 0000000000..5f9af88db4
--- /dev/null
+++ b/packages/backend/src/models/schema/emoji.ts
@@ -0,0 +1,36 @@
+export const packedEmojiSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		aliases: {
+			type: 'array',
+			optional: false, nullable: false,
+			items: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+		},
+		name: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		category: {
+			type: 'string',
+			optional: false, nullable: true,
+		},
+		host: {
+			type: 'string',
+			optional: false, nullable: true,
+		},
+		url: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/federation-instance.ts b/packages/backend/src/models/schema/federation-instance.ts
new file mode 100644
index 0000000000..eef2f9e24f
--- /dev/null
+++ b/packages/backend/src/models/schema/federation-instance.ts
@@ -0,0 +1,105 @@
+import config from "@/config";
+
+export const packedFederationInstanceSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+		},
+		caughtAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		host: {
+			type: 'string',
+			optional: false, nullable: false,
+			example: 'misskey.example.com',
+		},
+		usersCount: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+		notesCount: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+		followingCount: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+		followersCount: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+		driveUsage: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+		driveFiles: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+		latestRequestSentAt: {
+			type: 'string',
+			optional: false, nullable: true,
+			format: 'date-time',
+		},
+		lastCommunicatedAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		isNotResponding: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		isSuspended: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		softwareName: {
+			type: 'string',
+			optional: false, nullable: true,
+			example: 'misskey',
+		},
+		softwareVersion: {
+			type: 'string',
+			optional: false, nullable: true,
+			example: config.version,
+		},
+		openRegistrations: {
+			type: 'boolean',
+			optional: false, nullable: true,
+			example: true,
+		},
+		name: {
+			type: 'string',
+			optional: false, nullable: true,
+		},
+		description: {
+			type: 'string',
+			optional: false, nullable: true,
+		},
+		maintainerName: {
+			type: 'string',
+			optional: false, nullable: true,
+		},
+		maintainerEmail: {
+			type: 'string',
+			optional: false, nullable: true,
+		},
+		iconUrl: {
+			type: 'string',
+			optional: false, nullable: true,
+			format: 'url',
+		},
+		infoUpdatedAt: {
+			type: 'string',
+			optional: false, nullable: true,
+			format: 'date-time',
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/following.ts b/packages/backend/src/models/schema/following.ts
new file mode 100644
index 0000000000..2bcffbfc4d
--- /dev/null
+++ b/packages/backend/src/models/schema/following.ts
@@ -0,0 +1,36 @@
+export const packedFollowingSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		followeeId: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+		},
+		followee: {
+			type: 'object',
+			optional: true, nullable: false,
+			ref: 'UserDetailed',
+		},
+		followerId: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+		},
+		follower: {
+			type: 'object',
+			optional: true, nullable: false,
+			ref: 'UserDetailed',
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/gallery-post.ts b/packages/backend/src/models/schema/gallery-post.ts
new file mode 100644
index 0000000000..fc503d4a64
--- /dev/null
+++ b/packages/backend/src/models/schema/gallery-post.ts
@@ -0,0 +1,69 @@
+export const packedGalleryPostSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		updatedAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		title: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		description: {
+			type: 'string',
+			optional: false, nullable: true,
+		},
+		userId: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+		},
+		user: {
+			type: 'object',
+			ref: 'UserLite',
+			optional: false, nullable: false,
+		},
+		fileIds: {
+			type: 'array',
+			optional: true, nullable: false,
+			items: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+		},
+		files: {
+			type: 'array',
+			optional: true, nullable: false,
+			items: {
+				type: 'object',
+				optional: false, nullable: false,
+				ref: 'DriveFile',
+			},
+		},
+		tags: {
+			type: 'array',
+			optional: true, nullable: false,
+			items: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+		},
+		isSensitive: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/hashtag.ts b/packages/backend/src/models/schema/hashtag.ts
new file mode 100644
index 0000000000..98f8827640
--- /dev/null
+++ b/packages/backend/src/models/schema/hashtag.ts
@@ -0,0 +1,34 @@
+export const packedHashtagSchema = {
+	type: 'object',
+	properties: {
+		tag: {
+			type: 'string',
+			optional: false, nullable: false,
+			example: 'misskey',
+		},
+		mentionedUsersCount: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+		mentionedLocalUsersCount: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+		mentionedRemoteUsersCount: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+		attachedUsersCount: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+		attachedLocalUsersCount: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+		attachedRemoteUsersCount: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/messaging-message.ts b/packages/backend/src/models/schema/messaging-message.ts
new file mode 100644
index 0000000000..b1ffa45955
--- /dev/null
+++ b/packages/backend/src/models/schema/messaging-message.ts
@@ -0,0 +1,73 @@
+export const packedMessagingMessageSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		userId: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+		},
+		user: {
+			type: 'object',
+			ref: 'UserLite',
+			optional: true, nullable: false,
+		},
+		text: {
+			type: 'string',
+			optional: false, nullable: true,
+		},
+		fileId: {
+			type: 'string',
+			optional: true, nullable: true,
+			format: 'id',
+		},
+		file: {
+			type: 'object',
+			optional: true, nullable: true,
+			ref: 'DriveFile',
+		},
+		recipientId: {
+			type: 'string',
+			optional: false, nullable: true,
+			format: 'id',
+		},
+		recipient: {
+			type: 'object',
+			optional: true, nullable: true,
+			ref: 'UserLite',
+		},
+		groupId: {
+			type: 'string',
+			optional: false, nullable: true,
+			format: 'id',
+		},
+		group: {
+			type: 'object',
+			optional: true, nullable: true,
+			ref: 'UserGroup',
+		},
+		isRead: {
+			type: 'boolean',
+			optional: true, nullable: false,
+		},
+		reads: {
+			type: 'array',
+			optional: true, nullable: false,
+			items: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/muting.ts b/packages/backend/src/models/schema/muting.ts
new file mode 100644
index 0000000000..d75a4fbfed
--- /dev/null
+++ b/packages/backend/src/models/schema/muting.ts
@@ -0,0 +1,26 @@
+export const packedMutingSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		muteeId: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+		},
+		mutee: {
+			type: 'object',
+			optional: false, nullable: false,
+			ref: 'UserDetailed',
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/note-favorite.ts b/packages/backend/src/models/schema/note-favorite.ts
new file mode 100644
index 0000000000..d133f7367d
--- /dev/null
+++ b/packages/backend/src/models/schema/note-favorite.ts
@@ -0,0 +1,26 @@
+export const packedNoteFavoriteSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		note: {
+			type: 'object',
+			optional: false, nullable: false,
+			ref: 'Note',
+		},
+		noteId: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/note-reaction.ts b/packages/backend/src/models/schema/note-reaction.ts
new file mode 100644
index 0000000000..0d8fc5449b
--- /dev/null
+++ b/packages/backend/src/models/schema/note-reaction.ts
@@ -0,0 +1,25 @@
+export const packedNoteReactionSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		user: {
+			type: 'object',
+			optional: false, nullable: false,
+			ref: 'UserLite',
+		},
+		type: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/note.ts b/packages/backend/src/models/schema/note.ts
new file mode 100644
index 0000000000..cdf4b9a544
--- /dev/null
+++ b/packages/backend/src/models/schema/note.ts
@@ -0,0 +1,183 @@
+export const packedNoteSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		text: {
+			type: 'string',
+			optional: false, nullable: true,
+		},
+		cw: {
+			type: 'string',
+			optional: true, nullable: true,
+		},
+		userId: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+		},
+		user: {
+			type: 'object',
+			ref: 'UserLite',
+			optional: false, nullable: false,
+		},
+		replyId: {
+			type: 'string',
+			optional: true, nullable: true,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		renoteId: {
+			type: 'string',
+			optional: true, nullable: true,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		reply: {
+			type: 'object',
+			optional: true, nullable: true,
+			ref: 'Note',
+		},
+		renote: {
+			type: 'object',
+			optional: true, nullable: true,
+			ref: 'Note',
+		},
+		isHidden: {
+			type: 'boolean',
+			optional: true, nullable: false,
+		},
+		visibility: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		mentions: {
+			type: 'array',
+			optional: true, nullable: false,
+			items: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+		},
+		visibleUserIds: {
+			type: 'array',
+			optional: true, nullable: false,
+			items: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+		},
+		fileIds: {
+			type: 'array',
+			optional: true, nullable: false,
+			items: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+		},
+		files: {
+			type: 'array',
+			optional: true, nullable: false,
+			items: {
+				type: 'object',
+				optional: false, nullable: false,
+				ref: 'DriveFile',
+			},
+		},
+		tags: {
+			type: 'array',
+			optional: true, nullable: false,
+			items: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+		},
+		poll: {
+			type: 'object',
+			optional: true, nullable: true,
+		},
+		channelId: {
+			type: 'string',
+			optional: true, nullable: true,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		channel: {
+			type: 'object',
+			optional: true, nullable: true,
+			items: {
+				type: 'object',
+				optional: false, nullable: false,
+				properties: {
+					id: {
+						type: 'string',
+						optional: false, nullable: false,
+					},
+					name: {
+						type: 'string',
+						optional: false, nullable: true,
+					},
+				},
+			},
+		},
+		localOnly: {
+			type: 'boolean',
+			optional: true, nullable: false,
+		},
+		emojis: {
+			type: 'array',
+			optional: false, nullable: false,
+			items: {
+				type: 'object',
+				optional: false, nullable: false,
+				properties: {
+					name: {
+						type: 'string',
+						optional: false, nullable: false,
+					},
+					url: {
+						type: 'string',
+						optional: false, nullable: true,
+					},
+				},
+			},
+		},
+		reactions: {
+			type: 'object',
+			optional: false, nullable: false,
+		},
+		renoteCount: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+		repliesCount: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+		uri: {
+			type: 'string',
+			optional: true, nullable: false,
+		},
+		url: {
+			type: 'string',
+			optional: true, nullable: false,
+		},
+
+		myReaction: {
+			type: 'object',
+			optional: true, nullable: true,
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/notification.ts b/packages/backend/src/models/schema/notification.ts
new file mode 100644
index 0000000000..f3c293c480
--- /dev/null
+++ b/packages/backend/src/models/schema/notification.ts
@@ -0,0 +1,66 @@
+import { notificationTypes } from "@/types";
+
+export const packedNotificationSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		isRead: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		type: {
+			type: 'string',
+			optional: false, nullable: false,
+			enum: [...notificationTypes],
+		},
+		user: {
+			type: 'object',
+			ref: 'UserLite',
+			optional: true, nullable: true,
+		},
+		userId: {
+			type: 'string',
+			optional: true, nullable: true,
+			format: 'id',
+		},
+		note: {
+			type: 'object',
+			ref: 'Note',
+			optional: true, nullable: true,
+		},
+		reaction: {
+			type: 'string',
+			optional: true, nullable: true,
+		},
+		choice: {
+			type: 'number',
+			optional: true, nullable: true,
+		},
+		invitation: {
+			type: 'object',
+			optional: true, nullable: true,
+		},
+		body: {
+			type: 'string',
+			optional: true, nullable: true,
+		},
+		header: {
+			type: 'string',
+			optional: true, nullable: true,
+		},
+		icon: {
+			type: 'string',
+			optional: true, nullable: true,
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/page.ts b/packages/backend/src/models/schema/page.ts
new file mode 100644
index 0000000000..55ba3ce7f7
--- /dev/null
+++ b/packages/backend/src/models/schema/page.ts
@@ -0,0 +1,51 @@
+export const packedPageSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		updatedAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		title: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		name: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		summary: {
+			type: 'string',
+			optional: false, nullable: true,
+		},
+		content: {
+			type: 'array',
+			optional: false, nullable: false,
+		},
+		variables: {
+			type: 'array',
+			optional: false, nullable: false,
+		},
+		userId: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+		},
+		user: {
+			type: 'object',
+			ref: 'UserLite',
+			optional: false, nullable: false,
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/queue.ts b/packages/backend/src/models/schema/queue.ts
new file mode 100644
index 0000000000..7ceeda26af
--- /dev/null
+++ b/packages/backend/src/models/schema/queue.ts
@@ -0,0 +1,25 @@
+export const packedQueueCountSchema = {
+	type: 'object',
+	properties: {
+		waiting: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+		active: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+		completed: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+		failed: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+		delayed: {
+			type: 'number',
+			optional: false, nullable: false,
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/user-group.ts b/packages/backend/src/models/schema/user-group.ts
new file mode 100644
index 0000000000..a73bf82bb8
--- /dev/null
+++ b/packages/backend/src/models/schema/user-group.ts
@@ -0,0 +1,34 @@
+export const packedUserGroupSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		name: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		ownerId: {
+			type: 'string',
+			nullable: false, optional: false,
+			format: 'id',
+		},
+		userIds: {
+			type: 'array',
+			nullable: false, optional: true,
+			items: {
+				type: 'string',
+				nullable: false, optional: false,
+				format: 'id',
+			},
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/user-list.ts b/packages/backend/src/models/schema/user-list.ts
new file mode 100644
index 0000000000..3ba5dc4a8a
--- /dev/null
+++ b/packages/backend/src/models/schema/user-list.ts
@@ -0,0 +1,29 @@
+export const packedUserListSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string',
+			optional: false, nullable: false,
+			format: 'date-time',
+		},
+		name: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		userIds: {
+			type: 'array',
+			nullable: false, optional: true,
+			items: {
+				type: 'string',
+				nullable: false, optional: false,
+				format: 'id',
+			},
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/user.ts b/packages/backend/src/models/schema/user.ts
new file mode 100644
index 0000000000..616bedc0dc
--- /dev/null
+++ b/packages/backend/src/models/schema/user.ts
@@ -0,0 +1,467 @@
+export const packedUserLiteSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			nullable: false, optional: false,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		name: {
+			type: 'string',
+			nullable: true, optional: false,
+			example: '藍',
+		},
+		username: {
+			type: 'string',
+			nullable: false, optional: false,
+			example: 'ai',
+		},
+		host: {
+			type: 'string',
+			nullable: true, optional: false,
+			example: 'misskey.example.com',
+		},
+		avatarUrl: {
+			type: 'string',
+			format: 'url',
+			nullable: true, optional: false,
+		},
+		avatarBlurhash: {
+			type: 'any',
+			nullable: true, optional: false,
+		},
+		avatarColor: {
+			type: 'any',
+			nullable: true, optional: false,
+			default: null,
+		},
+		isAdmin: {
+			type: 'boolean',
+			nullable: false, optional: true,
+			default: false,
+		},
+		isModerator: {
+			type: 'boolean',
+			nullable: false, optional: true,
+			default: false,
+		},
+		isBot: {
+			type: 'boolean',
+			nullable: false, optional: true,
+		},
+		isCat: {
+			type: 'boolean',
+			nullable: false, optional: true,
+		},
+		emojis: {
+			type: 'array',
+			nullable: false, optional: false,
+			items: {
+				type: 'object',
+				nullable: false, optional: false,
+				properties: {
+					name: {
+						type: 'string',
+						nullable: false, optional: false,
+					},
+					url: {
+						type: 'string',
+						nullable: false, optional: false,
+						format: 'url',
+					},
+				},
+			},
+		},
+		onlineStatus: {
+			type: 'string',
+			format: 'url',
+			nullable: true, optional: false,
+			enum: ['unknown', 'online', 'active', 'offline'],
+		},
+	},
+} as const;
+
+export const packedUserDetailedNotMeOnlySchema = {
+	type: 'object',
+	properties: {
+		url: {
+			type: 'string',
+			format: 'url',
+			nullable: true, optional: false,
+		},
+		uri: {
+			type: 'string',
+			format: 'uri',
+			nullable: true, optional: false,
+		},
+		createdAt: {
+			type: 'string',
+			nullable: false, optional: false,
+			format: 'date-time',
+		},
+		updatedAt: {
+			type: 'string',
+			nullable: true, optional: false,
+			format: 'date-time',
+		},
+		lastFetchedAt: {
+			type: 'string',
+			nullable: true, optional: false,
+			format: 'date-time',
+		},
+		bannerUrl: {
+			type: 'string',
+			format: 'url',
+			nullable: true, optional: false,
+		},
+		bannerBlurhash: {
+			type: 'any',
+			nullable: true, optional: false,
+		},
+		bannerColor: {
+			type: 'any',
+			nullable: true, optional: false,
+			default: null,
+		},
+		isLocked: {
+			type: 'boolean',
+			nullable: false, optional: false,
+		},
+		isSilenced: {
+			type: 'boolean',
+			nullable: false, optional: false,
+		},
+		isSuspended: {
+			type: 'boolean',
+			nullable: false, optional: false,
+			example: false,
+		},
+		description: {
+			type: 'string',
+			nullable: true, optional: false,
+			example: 'Hi masters, I am Ai!',
+		},
+		location: {
+			type: 'string',
+			nullable: true, optional: false,
+		},
+		birthday: {
+			type: 'string',
+			nullable: true, optional: false,
+			example: '2018-03-12',
+		},
+		lang: {
+			type: 'string',
+			nullable: true, optional: false,
+			example: 'ja-JP',
+		},
+		fields: {
+			type: 'array',
+			nullable: false, optional: false,
+			items: {
+					type: 'object',
+					nullable: false, optional: false,
+					properties: {
+						name: {
+							type: 'string',
+							nullable: false, optional: false,
+						},
+						value: {
+							type: 'string',
+							nullable: false, optional: false,
+						},
+					},
+					maxLength: 4,
+			},
+		},
+		followersCount: {
+			type: 'number',
+			nullable: false, optional: false,
+		},
+		followingCount: {
+			type: 'number',
+			nullable: false, optional: false,
+		},
+		notesCount: {
+			type: 'number',
+			nullable: false, optional: false,
+		},
+		pinnedNoteIds: {
+			type: 'array',
+			nullable: false, optional: false,
+			items: {
+				type: 'string',
+				nullable: false, optional: false,
+				format: 'id',
+			},
+		},
+		pinnedNotes: {
+			type: 'array',
+			nullable: false, optional: false,
+			items: {
+				type: 'object',
+				nullable: false, optional: false,
+				ref: 'Note',
+			},
+		},
+		pinnedPageId: {
+			type: 'string',
+			nullable: true, optional: false,
+		},
+		pinnedPage: {
+			type: 'object',
+			nullable: true, optional: false,
+			ref: 'Page',
+		},
+		publicReactions: {
+			type: 'boolean',
+			nullable: false, optional: false,
+		},
+		twoFactorEnabled: {
+			type: 'boolean',
+			nullable: false, optional: false,
+			default: false,
+		},
+		usePasswordLessLogin: {
+			type: 'boolean',
+			nullable: false, optional: false,
+			default: false,
+		},
+		securityKeys: {
+			type: 'boolean',
+			nullable: false, optional: false,
+			default: false,
+		},
+		//#region relations
+		isFollowing: {
+			type: 'boolean',
+			nullable: false, optional: true,
+		},
+		isFollowed: {
+			type: 'boolean',
+			nullable: false, optional: true,
+		},
+		hasPendingFollowRequestFromYou: {
+			type: 'boolean',
+			nullable: false, optional: true,
+		},
+		hasPendingFollowRequestToYou: {
+			type: 'boolean',
+			nullable: false, optional: true,
+		},
+		isBlocking: {
+			type: 'boolean',
+			nullable: false, optional: true,
+		},
+		isBlocked: {
+			type: 'boolean',
+			nullable: false, optional: true,
+		},
+		isMuted: {
+			type: 'boolean',
+			nullable: false, optional: true,
+		},
+		//#endregion
+	},
+} as const;
+
+export const packedMeDetailedOnlySchema = {
+	type: 'object',
+	properties: {
+		avatarId: {
+			type: 'string',
+			nullable: true, optional: false,
+			format: 'id',
+		},
+		bannerId: {
+			type: 'string',
+			nullable: true, optional: false,
+			format: 'id',
+		},
+		injectFeaturedNote: {
+			type: 'boolean',
+			nullable: true, optional: false,
+		},
+		receiveAnnouncementEmail: {
+			type: 'boolean',
+			nullable: true, optional: false,
+		},
+		alwaysMarkNsfw: {
+			type: 'boolean',
+			nullable: true, optional: false,
+		},
+		carefulBot: {
+			type: 'boolean',
+			nullable: true, optional: false,
+		},
+		autoAcceptFollowed: {
+			type: 'boolean',
+			nullable: true, optional: false,
+		},
+		noCrawle: {
+			type: 'boolean',
+			nullable: true, optional: false,
+		},
+		isExplorable: {
+			type: 'boolean',
+			nullable: false, optional: false,
+		},
+		isDeleted: {
+			type: 'boolean',
+			nullable: false, optional: false,
+		},
+		hideOnlineStatus: {
+			type: 'boolean',
+			nullable: false, optional: false,
+		},
+		hasUnreadSpecifiedNotes: {
+			type: 'boolean',
+			nullable: false, optional: false,
+		},
+		hasUnreadMentions: {
+			type: 'boolean',
+			nullable: false, optional: false,
+		},
+		hasUnreadAnnouncement: {
+			type: 'boolean',
+			nullable: false, optional: false,
+		},
+		hasUnreadAntenna: {
+			type: 'boolean',
+			nullable: false, optional: false,
+		},
+		hasUnreadChannel: {
+			type: 'boolean',
+			nullable: false, optional: false,
+		},
+		hasUnreadMessagingMessage: {
+			type: 'boolean',
+			nullable: false, optional: false,
+		},
+		hasUnreadNotification: {
+			type: 'boolean',
+			nullable: false, optional: false,
+		},
+		hasPendingReceivedFollowRequest: {
+			type: 'boolean',
+			nullable: false, optional: false,
+		},
+		integrations: {
+			type: 'object',
+			nullable: true, optional: false,
+		},
+		mutedWords: {
+			type: 'array',
+			nullable: false, optional: false,
+			items: {
+				type: 'array',
+				nullable: false, optional: false,
+				items: {
+					type: 'string',
+					nullable: false, optional: false,
+				},
+			},
+		},
+		mutedInstances: {
+			type: 'array',
+			nullable: true, optional: false,
+			items: {
+				type: 'string',
+				nullable: false, optional: false,
+			},
+		},
+		mutingNotificationTypes: {
+			type: 'array',
+			nullable: true, optional: false,
+			items: {
+				type: 'string',
+				nullable: false, optional: false,
+			},
+		},
+		emailNotificationTypes: {
+			type: 'array',
+			nullable: true, optional: false,
+			items: {
+				type: 'string',
+				nullable: false, optional: false,
+			},
+		},
+		//#region secrets
+		email: {
+			type: 'string',
+			nullable: true, optional: true,
+		},
+		emailVerified: {
+			type: 'boolean',
+			nullable: true, optional: true,
+		},
+		securityKeysList: {
+			type: 'array',
+			nullable: false, optional: true,
+			items: {
+				type: 'object',
+				nullable: false, optional: false,
+			},
+		},
+		//#endregion
+	},
+} as const;
+
+export const packedUserDetailedNotMeSchema = {
+	type: 'object',
+	allOf: [
+		{
+			type: 'object',
+			ref: 'UserLite',
+		},
+		{
+			type: 'object',
+			ref: 'UserDetailedNotMeOnly',
+		},
+	],
+} as const;
+
+export const packedMeDetailedSchema = {
+	type: 'object',
+	allOf: [
+		{
+			type: 'object',
+			ref: 'UserLite',
+		},
+		{
+			type: 'object',
+			ref: 'UserDetailedNotMeOnly',
+		},
+		{
+			type: 'object',
+			ref: 'MeDetailedOnly',
+		},
+	],
+} as const;
+
+export const packedUserDetailedSchema = {
+	oneOf: [
+		{
+			type: 'object',
+			ref: 'UserDetailedNotMe',
+		},
+		{
+			type: 'object',
+			ref: 'MeDetailed',
+		},
+	],
+} as const;
+
+export const packedUserSchema = {
+	oneOf: [
+		{
+			type: 'object',
+			ref: 'UserLite',
+		},
+		{
+			type: 'object',
+			ref: 'UserDetailed',
+		},
+	],
+} as const;
diff --git a/packages/backend/src/prelude/await-all.ts b/packages/backend/src/prelude/await-all.ts
index 24795f3ae5..b955c3a5d8 100644
--- a/packages/backend/src/prelude/await-all.ts
+++ b/packages/backend/src/prelude/await-all.ts
@@ -1,13 +1,11 @@
-type Await<T> = T extends Promise<infer U> ? U : T;
-
-type AwaitAll<T> = {
-	[P in keyof T]: Await<T[P]>;
+export type Promiseable<T> = {
+	[K in keyof T]: Promise<T[K]> | T[K];
 };
 
-export async function awaitAll<T>(obj: T): Promise<AwaitAll<T>> {
-	const target = {} as any;
-	const keys = Object.keys(obj);
-	const values = Object.values(obj);
+export async function awaitAll<T>(obj: Promiseable<T>): Promise<T> {
+	const target = {} as T;
+	const keys = Object.keys(obj) as unknown as (keyof T)[];
+	const values = Object.values(obj) as any[];
 
 	const resolvedValues = await Promise.all(values.map(value =>
 		(!value || !value.constructor || value.constructor.name !== 'Object')
diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts
index 2fbc1b1c01..f9994c3b59 100644
--- a/packages/backend/src/queue/index.ts
+++ b/packages/backend/src/queue/index.ts
@@ -213,6 +213,16 @@ export function createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']
 	});
 }
 
+export function createImportCustomEmojisJob(user: ThinUser, fileId: DriveFile['id']) {
+	return dbQueue.add('importCustomEmojis', {
+		user: user,
+		fileId: fileId,
+	}, {
+		removeOnComplete: true,
+		removeOnFail: true,
+	});
+}
+
 export function createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) {
 	return dbQueue.add('deleteAccount', {
 		user: user,
diff --git a/packages/backend/src/queue/processors/db/export-blocking.ts b/packages/backend/src/queue/processors/db/export-blocking.ts
index 8c886d3b4e..af5c7eba15 100644
--- a/packages/backend/src/queue/processors/db/export-blocking.ts
+++ b/packages/backend/src/queue/processors/db/export-blocking.ts
@@ -3,7 +3,7 @@ import * as tmp from 'tmp';
 import * as fs from 'fs';
 
 import { queueLogger } from '../../logger';
-import addFile from '@/services/drive/add-file';
+import { addFile } from '@/services/drive/add-file';
 import * as dateFormat from 'dateformat';
 import { getFullApAccount } from '@/misc/convert-host';
 import { Users, Blockings } from '@/models/index';
diff --git a/packages/backend/src/queue/processors/db/export-custom-emojis.ts b/packages/backend/src/queue/processors/db/export-custom-emojis.ts
index 3930b9d6d4..0c06b12c9a 100644
--- a/packages/backend/src/queue/processors/db/export-custom-emojis.ts
+++ b/packages/backend/src/queue/processors/db/export-custom-emojis.ts
@@ -6,11 +6,12 @@ import { ulid } from 'ulid';
 const mime = require('mime-types');
 const archiver = require('archiver');
 import { queueLogger } from '../../logger';
-import addFile from '@/services/drive/add-file';
+import { addFile } from '@/services/drive/add-file';
 import * as dateFormat from 'dateformat';
 import { Users, Emojis } from '@/models/index';
 import {  } from '@/queue/types';
 import { downloadUrl } from '@/misc/download-url';
+import config from '@/config/index';
 
 const logger = queueLogger.createSubLogger('export-custom-emojis');
 
@@ -52,7 +53,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
 		});
 	};
 
-	await writeMeta(`{"metaVersion":1,"emojis":[`);
+	await writeMeta(`{"metaVersion":2,"host":"${config.host}","exportedAt":"${new Date().toString()}","emojis":[`);
 
 	const customEmojis = await Emojis.find({
 		where: {
@@ -64,21 +65,25 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
 	});
 
 	for (const emoji of customEmojis) {
-		const exportId = ulid().toLowerCase();
 		const ext = mime.extension(emoji.type);
-		const emojiPath = path + '/' + exportId + (ext ? '.' + ext : '');
+		const fileName = emoji.name + (ext ? '.' + ext : '');
+		const emojiPath = path + '/' + fileName;
 		fs.writeFileSync(emojiPath, '', 'binary');
 		let downloaded = false;
 
 		try {
-			await downloadUrl(emoji.url, emojiPath);
+			await downloadUrl(emoji.originalUrl, emojiPath);
 			downloaded = true;
 		} catch (e) { // TODO: 何度か再試行
 			logger.error(e);
 		}
 
+		if (!downloaded) {
+			fs.unlinkSync(emojiPath);
+		}
+
 		const content = JSON.stringify({
-			id: exportId,
+			fileName: fileName,
 			downloaded: downloaded,
 			emoji: emoji,
 		});
diff --git a/packages/backend/src/queue/processors/db/export-following.ts b/packages/backend/src/queue/processors/db/export-following.ts
index fbb9e25247..0c088dc371 100644
--- a/packages/backend/src/queue/processors/db/export-following.ts
+++ b/packages/backend/src/queue/processors/db/export-following.ts
@@ -3,7 +3,7 @@ import * as tmp from 'tmp';
 import * as fs from 'fs';
 
 import { queueLogger } from '../../logger';
-import addFile from '@/services/drive/add-file';
+import { addFile } from '@/services/drive/add-file';
 import * as dateFormat from 'dateformat';
 import { getFullApAccount } from '@/misc/convert-host';
 import { Users, Followings, Mutings } from '@/models/index';
diff --git a/packages/backend/src/queue/processors/db/export-mute.ts b/packages/backend/src/queue/processors/db/export-mute.ts
index 0b1fd24fe0..f5928b875d 100644
--- a/packages/backend/src/queue/processors/db/export-mute.ts
+++ b/packages/backend/src/queue/processors/db/export-mute.ts
@@ -3,7 +3,7 @@ import * as tmp from 'tmp';
 import * as fs from 'fs';
 
 import { queueLogger } from '../../logger';
-import addFile from '@/services/drive/add-file';
+import { addFile } from '@/services/drive/add-file';
 import * as dateFormat from 'dateformat';
 import { getFullApAccount } from '@/misc/convert-host';
 import { Users, Mutings } from '@/models/index';
diff --git a/packages/backend/src/queue/processors/db/export-notes.ts b/packages/backend/src/queue/processors/db/export-notes.ts
index e64e763513..df7675dec7 100644
--- a/packages/backend/src/queue/processors/db/export-notes.ts
+++ b/packages/backend/src/queue/processors/db/export-notes.ts
@@ -3,7 +3,7 @@ import * as tmp from 'tmp';
 import * as fs from 'fs';
 
 import { queueLogger } from '../../logger';
-import addFile from '@/services/drive/add-file';
+import { addFile } from '@/services/drive/add-file';
 import * as dateFormat from 'dateformat';
 import { Users, Notes, Polls } from '@/models/index';
 import { MoreThan } from 'typeorm';
diff --git a/packages/backend/src/queue/processors/db/export-user-lists.ts b/packages/backend/src/queue/processors/db/export-user-lists.ts
index 44a8f9f671..b9b6cb0de6 100644
--- a/packages/backend/src/queue/processors/db/export-user-lists.ts
+++ b/packages/backend/src/queue/processors/db/export-user-lists.ts
@@ -3,7 +3,7 @@ import * as tmp from 'tmp';
 import * as fs from 'fs';
 
 import { queueLogger } from '../../logger';
-import addFile from '@/services/drive/add-file';
+import { addFile } from '@/services/drive/add-file';
 import * as dateFormat from 'dateformat';
 import { getFullApAccount } from '@/misc/convert-host';
 import { Users, UserLists, UserListJoinings } from '@/models/index';
diff --git a/packages/backend/src/queue/processors/db/import-custom-emojis.ts b/packages/backend/src/queue/processors/db/import-custom-emojis.ts
new file mode 100644
index 0000000000..d2b0eb269a
--- /dev/null
+++ b/packages/backend/src/queue/processors/db/import-custom-emojis.ts
@@ -0,0 +1,85 @@
+import * as Bull from 'bull';
+import * as tmp from 'tmp';
+import * as fs from 'fs';
+const unzipper = require('unzipper');
+import { getConnection } from 'typeorm';
+
+import { queueLogger } from '../../logger';
+import { downloadUrl } from '@/misc/download-url';
+import { DriveFiles, Emojis } from '@/models/index';
+import { DbUserImportJobData } from '@/queue/types';
+import { addFile } from '@/services/drive/add-file';
+import { genId } from '@/misc/gen-id';
+
+const logger = queueLogger.createSubLogger('import-custom-emojis');
+
+// TODO: 名前衝突時の動作を選べるようにする
+export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
+	logger.info(`Importing custom emojis ...`);
+
+	const file = await DriveFiles.findOne({
+		id: job.data.fileId,
+	});
+	if (file == null) {
+		done();
+		return;
+	}
+
+	// Create temp dir
+	const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => {
+		tmp.dir((e, path, cleanup) => {
+			if (e) return rej(e);
+			res([path, cleanup]);
+		});
+	});
+
+	logger.info(`Temp dir is ${path}`);
+
+	const destPath = path + '/emojis.zip';
+
+	try {
+		fs.writeFileSync(destPath, '', 'binary');
+		await downloadUrl(file.url, destPath);
+	} catch (e) { // TODO: 何度か再試行
+		logger.error(e);
+		throw e;
+	}
+
+	const outputPath = path + '/emojis';
+	const unzipStream = fs.createReadStream(destPath);
+	const extractor = unzipper.Extract({ path: outputPath });
+	extractor.on('close', async () => {
+		const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8');
+		const meta = JSON.parse(metaRaw);
+
+		for (const record of meta.emojis) {
+			if (!record.downloaded) continue;
+			const emojiInfo = record.emoji;
+			const emojiPath = outputPath + '/' + record.fileName;
+			await Emojis.delete({
+				name: emojiInfo.name,
+			});
+			const driveFile = await addFile(null, emojiPath, record.fileName, null, null, true);
+			const emoji = await Emojis.insert({
+				id: genId(),
+				updatedAt: new Date(),
+				name: emojiInfo.name,
+				category: emojiInfo.category,
+				host: null,
+				aliases: emojiInfo.aliases,
+				originalUrl: driveFile.url,
+				publicUrl: driveFile.webpublicUrl ?? driveFile.url,
+				type: driveFile.webpublicType ?? driveFile.type,
+			}).then(x => Emojis.findOneOrFail(x.identifiers[0]));
+		}
+
+		await getConnection().queryResultCache!.remove(['meta_emojis']);
+
+		cleanup();
+	
+		logger.succ('Imported');
+		done();
+	});
+	unzipStream.pipe(extractor);
+	logger.succ(`Unzipping to ${outputPath}`);
+}
diff --git a/packages/backend/src/queue/processors/db/index.ts b/packages/backend/src/queue/processors/db/index.ts
index 1542f401ef..5fffa378f5 100644
--- a/packages/backend/src/queue/processors/db/index.ts
+++ b/packages/backend/src/queue/processors/db/index.ts
@@ -12,6 +12,7 @@ import { importUserLists } from './import-user-lists';
 import { deleteAccount } from './delete-account';
 import { importMuting } from './import-muting';
 import { importBlocking } from './import-blocking';
+import { importCustomEmojis } from './import-custom-emojis';
 
 const jobs = {
 	deleteDriveFiles,
@@ -25,6 +26,7 @@ const jobs = {
 	importMuting,
 	importBlocking,
 	importUserLists,
+	importCustomEmojis,
 	deleteAccount,
 } as Record<string, Bull.ProcessCallbackFunction<DbJobData> | Bull.ProcessPromiseFunction<DbJobData>>;
 
diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts
index a0fdf7f239..6847925a51 100644
--- a/packages/backend/src/remote/activitypub/models/note.ts
+++ b/packages/backend/src/remote/activitypub/models/note.ts
@@ -320,14 +320,15 @@ export async function extractEmojis(tags: IObject | IObject[], host: string): Pr
 			if ((tag.updated != null && exists.updatedAt == null)
 				|| (tag.id != null && exists.uri == null)
 				|| (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)
-				|| (tag.icon!.url !== exists.url)
+				|| (tag.icon!.url !== exists.originalUrl)
 			) {
 				await Emojis.update({
 					host,
 					name,
 				}, {
 					uri: tag.id,
-					url: tag.icon!.url,
+					originalUrl: tag.icon!.url,
+					publicUrl: tag.icon!.url,
 					updatedAt: new Date(),
 				});
 
@@ -347,7 +348,8 @@ export async function extractEmojis(tags: IObject | IObject[], host: string): Pr
 			host,
 			name,
 			uri: tag.id,
-			url: tag.icon!.url,
+			originalUrl: tag.icon!.url,
+			publicUrl: tag.icon!.url,
 			updatedAt: new Date(),
 			aliases: [],
 		} as Partial<Emoji>).then(x => Emojis.findOneOrFail(x.identifiers[0]));
diff --git a/packages/backend/src/remote/activitypub/renderer/emoji.ts b/packages/backend/src/remote/activitypub/renderer/emoji.ts
index 9d08c8ba81..e7ae7d959a 100644
--- a/packages/backend/src/remote/activitypub/renderer/emoji.ts
+++ b/packages/backend/src/remote/activitypub/renderer/emoji.ts
@@ -9,6 +9,6 @@ export default (emoji: Emoji) => ({
 	icon: {
 		type: 'Image',
 		mediaType: emoji.type || 'image/png',
-		url: emoji.url,
+		url: emoji.publicUrl || emoji.originalUrl, // || emoji.originalUrl してるのは後方互換性のため
 	},
 });
diff --git a/packages/backend/src/remote/activitypub/renderer/flag.ts b/packages/backend/src/remote/activitypub/renderer/flag.ts
new file mode 100644
index 0000000000..60ac496509
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/flag.ts
@@ -0,0 +1,15 @@
+import config from '@/config/index';
+import { IObject, IActivity } from '@/remote/activitypub/type';
+import { ILocalUser, IRemoteUser } from '@/models/entities/user';
+import { getInstanceActor } from '@/services/instance-actor';
+
+// to anonymise reporters, the reporting actor must be a system user
+// object has to be a uri or array of uris
+export const renderFlag = (user: ILocalUser, object: [string], content: string): IActivity => {
+	return {
+		type: 'Flag',
+		actor: `${config.url}/users/${user.id}`,
+		content,
+		object,
+	};
+};
diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts
index 36aadb532b..399ee65bde 100644
--- a/packages/backend/src/server/api/call.ts
+++ b/packages/backend/src/server/api/call.ts
@@ -1,5 +1,5 @@
 import { performance } from 'perf_hooks';
-import limiter from './limiter';
+import { limiter } from './limiter';
 import { User } from '@/models/entities/user';
 import endpoints from './endpoints';
 import { ApiError } from './error';
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index a61b3f564c..bb4e972b8e 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -3,7 +3,7 @@ import { dirname } from 'path';
 import { Context } from 'cafy';
 import * as path from 'path';
 import * as glob from 'glob';
-import { SimpleSchema } from '@/misc/simple-schema';
+import { Schema } from '@/misc/schema';
 
 //const _filename = fileURLToPath(import.meta.url);
 const _filename = __filename;
@@ -18,87 +18,87 @@ export type Param = {
 };
 
 export interface IEndpointMeta {
-	stability?: string; //'deprecated' | 'experimental' | 'stable';
+	readonly stability?: 'deprecated' | 'experimental' | 'stable';
 
-	tags?: string[];
+	readonly tags?: ReadonlyArray<string>;
 
-	params?: {
-		[key: string]: Param;
+	readonly params?: {
+		readonly [key: string]: Param;
 	};
 
-	errors?: {
-		[key: string]: {
-			message: string;
-			code: string;
-			id: string;
+	readonly errors?: {
+		readonly [key: string]: {
+			readonly message: string;
+			readonly code: string;
+			readonly id: string;
 		};
 	};
 
-	res?: SimpleSchema;
+	readonly res?: Schema;
 
 	/**
 	 * このエンドポイントにリクエストするのにユーザー情報が必須か否か
 	 * 省略した場合は false として解釈されます。
 	 */
-	requireCredential?: boolean;
+	readonly requireCredential?: boolean;
 
 	/**
 	 * 管理者のみ使えるエンドポイントか否か
 	 */
-	requireAdmin?: boolean;
+	readonly requireAdmin?: boolean;
 
 	/**
 	 * 管理者またはモデレーターのみ使えるエンドポイントか否か
 	 */
-	requireModerator?: boolean;
+	readonly requireModerator?: boolean;
 
 	/**
 	 * エンドポイントのリミテーションに関するやつ
 	 * 省略した場合はリミテーションは無いものとして解釈されます。
 	 * また、withCredential が false の場合はリミテーションを行うことはできません。
 	 */
-	limit?: {
+	readonly limit?: {
 
 		/**
 		 * 複数のエンドポイントでリミットを共有したい場合に指定するキー
 		 */
-		key?: string;
+		readonly key?: string;
 
 		/**
 		 * リミットを適用する期間(ms)
 		 * このプロパティを設定する場合、max プロパティも設定する必要があります。
 		 */
-		duration?: number;
+		readonly duration?: number;
 
 		/**
 		 * durationで指定した期間内にいくつまでリクエストできるのか
 		 * このプロパティを設定する場合、duration プロパティも設定する必要があります。
 		 */
-		max?: number;
+		readonly max?: number;
 
 		/**
 		 * 最低でもどれくらいの間隔を開けてリクエストしなければならないか(ms)
 		 */
-		minInterval?: number;
+		readonly minInterval?: number;
 	};
 
 	/**
 	 * ファイルの添付を必要とするか否か
 	 * 省略した場合は false として解釈されます。
 	 */
-	requireFile?: boolean;
+	readonly requireFile?: boolean;
 
 	/**
 	 * サードパーティアプリからはリクエストすることができないか否か
 	 * 省略した場合は false として解釈されます。
 	 */
-	secure?: boolean;
+	readonly secure?: boolean;
 
 	/**
 	 * エンドポイントの種類
 	 * パーミッションの実現に利用されます。
 	 */
-	kind?: string;
+	readonly kind?: string;
 }
 
 export interface IEndpoint {
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
index 5d4cc2c044..ed7b146d03 100644
--- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -46,69 +46,74 @@ export const meta = {
 			]),
 			default: 'combined',
 		},
+
+		forwarded: {
+			validator: $.optional.bool,
+			default: false,
+		},
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			properties: {
 				id: {
-					type: 'string' as const,
-					nullable: false as const, optional: false as const,
+					type: 'string',
+					nullable: false, optional: false,
 					format: 'id',
 					example: 'xxxxxxxxxx',
 				},
 				createdAt: {
-					type: 'string' as const,
-					nullable: false as const, optional: false as const,
+					type: 'string',
+					nullable: false, optional: false,
 					format: 'date-time',
 				},
 				comment: {
-					type: 'string' as const,
-					nullable: false as const, optional: false as const,
+					type: 'string',
+					nullable: false, optional: false,
 				},
 				resolved: {
-					type: 'boolean' as const,
-					nullable: false as const, optional: false as const,
+					type: 'boolean',
+					nullable: false, optional: false,
 					example: false,
 				},
 				reporterId: {
-					type: 'string' as const,
-					nullable: false as const, optional: false as const,
+					type: 'string',
+					nullable: false, optional: false,
 					format: 'id',
 				},
 				targetUserId: {
-					type: 'string' as const,
-					nullable: false as const, optional: false as const,
+					type: 'string',
+					nullable: false, optional: false,
 					format: 'id',
 				},
 				assigneeId: {
-					type: 'string' as const,
-					nullable: true as const, optional: false as const,
+					type: 'string',
+					nullable: true, optional: false,
 					format: 'id',
 				},
 				reporter: {
-					type: 'object' as const,
-					nullable: false as const, optional: false as const,
+					type: 'object',
+					nullable: false, optional: false,
 					ref: 'User',
 				},
 				targetUser: {
-					type: 'object' as const,
-					nullable: false as const, optional: false as const,
+					type: 'object',
+					nullable: false, optional: false,
 					ref: 'User',
 				},
 				assignee: {
-					type: 'object' as const,
-					nullable: true as const, optional: true as const,
+					type: 'object',
+					nullable: true, optional: true,
 					ref: 'User',
 				},
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
index edfac244f5..20f1232959 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -16,17 +16,17 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'User',
 		properties: {
 			token: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, _me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts
index 3ed6ac2f33..1701c1e3a7 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts
@@ -9,7 +9,7 @@ import { ID } from '@/misc/cafy-id';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -17,7 +17,7 @@ export const meta = {
 			validator: $.type(ID),
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts
index 3388ef2726..00ad2012fe 100644
--- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts
@@ -6,7 +6,7 @@ import { genId } from '@/misc/gen-id';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -32,7 +32,7 @@ export const meta = {
 			validator: $.str.min(1),
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts
index ab0458af6e..c0124e2484 100644
--- a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts
+++ b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts
@@ -7,7 +7,7 @@ import { ApiError } from '../../../error';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -23,7 +23,7 @@ export const meta = {
 			id: 'ccac9863-3a03-416e-b899-8a64041118b1',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/list.ts b/packages/backend/src/server/api/endpoints/admin/ad/list.ts
index 04597ec7b5..7a83637f3b 100644
--- a/packages/backend/src/server/api/endpoints/admin/ad/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/ad/list.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../../common/make-pagination-query';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -24,7 +24,7 @@ export const meta = {
 			validator: $.optional.type(ID),
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts
index 7fab745ef7..c2b09ab9cf 100644
--- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts
@@ -7,7 +7,7 @@ import { ApiError } from '../../../error';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -44,7 +44,7 @@ export const meta = {
 			id: 'b7aa1727-1354-47bc-a182-3a9c3973d300',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
index 7d169d6caf..24c4caa37d 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
@@ -6,7 +6,7 @@ import { genId } from '@/misc/gen-id';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -22,40 +22,40 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			id: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				format: 'id',
 				example: 'xxxxxxxxxx',
 			},
 			createdAt: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				format: 'date-time',
 			},
 			updatedAt: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 				format: 'date-time',
 			},
 			title: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 			},
 			text: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 			},
 			imageUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts
index 81dd8cfc59..5548f99006 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts
@@ -7,7 +7,7 @@ import { ApiError } from '../../../error';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -23,7 +23,7 @@ export const meta = {
 			id: 'ecad8040-a276-4e85-bda9-015a708d291e',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
index f6ad6d36f4..e5cc53ccdd 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../../common/make-pagination-query';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -26,48 +26,48 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			properties: {
 				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 					format: 'id',
 					example: 'xxxxxxxxxx',
 				},
 				createdAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 					format: 'date-time',
 				},
 				updatedAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
+					type: 'string',
+					optional: false, nullable: true,
 					format: 'date-time',
 				},
 				text: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 				},
 				title: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 				},
 				imageUrl: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
+					type: 'string',
+					optional: false, nullable: true,
 				},
 				reads: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
+					type: 'number',
+					optional: false, nullable: false,
 				},
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts
index 6fb4b571ad..f66293bb18 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts
@@ -7,7 +7,7 @@ import { ApiError } from '../../../error';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -32,7 +32,7 @@ export const meta = {
 			id: 'd3aae5a7-6372-4cb4-b61c-f511ffc2d7cc',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
index 82cbe7b194..249e63a0f8 100644
--- a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
@@ -7,7 +7,7 @@ import { ID } from '@/misc/cafy-id';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -15,7 +15,7 @@ export const meta = {
 			validator: $.type(ID),
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/delete-logs.ts b/packages/backend/src/server/api/endpoints/admin/delete-logs.ts
deleted file mode 100644
index 197ad01cb3..0000000000
--- a/packages/backend/src/server/api/endpoints/admin/delete-logs.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import define from '../../define';
-import { Logs } from '@/models/index';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-};
-
-// eslint-disable-next-line import/no-default-export
-export default define(meta, async (ps) => {
-	await Logs.clear();	// TRUNCATE
-});
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts
index 518535fdde..acabbfef5c 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts
@@ -4,9 +4,9 @@ import { createCleanRemoteFilesJob } from '@/queue/index';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts
index a523c5b398..452e7069a8 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts
@@ -6,9 +6,9 @@ import { DriveFiles } from '@/models/index';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/files.ts b/packages/backend/src/server/api/endpoints/admin/drive/files.ts
index b90ad90440..264f549867 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/files.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/files.ts
@@ -7,7 +7,7 @@ import { ID } from '@/misc/cafy-id';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 	requireModerator: true,
 
 	params: {
@@ -44,15 +44,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'DriveFile',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
index bc0857588c..5d9a1f2703 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
@@ -7,7 +7,7 @@ import { DriveFiles } from '@/models/index';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -29,137 +29,137 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			id: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				format: 'id',
 				example: 'xxxxxxxxxx',
 			},
 			createdAt: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				format: 'date-time',
 			},
 			userId: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 				format: 'id',
 				example: 'xxxxxxxxxx',
 			},
 			userHost: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 			},
 			md5: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				format: 'md5',
 				example: '15eca7fba0480996e2245f5185bf39f2',
 			},
 			name: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				example: 'lenna.jpg',
 			},
 			type: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				example: 'image/jpeg',
 			},
 			size: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
+				type: 'number',
+				optional: false, nullable: false,
 				example: 51469,
 			},
 			comment: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 			},
 			blurhash: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 			},
 			properties: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
+				type: 'object',
+				optional: false, nullable: false,
 				properties: {
 					width: {
-						type: 'number' as const,
-						optional: false as const, nullable: false as const,
+						type: 'number',
+						optional: false, nullable: false,
 						example: 1280,
 					},
 					height: {
-						type: 'number' as const,
-						optional: false as const, nullable: false as const,
+						type: 'number',
+						optional: false, nullable: false,
 						example: 720,
 					},
 					avgColor: {
-						type: 'string' as const,
-						optional: true as const, nullable: false as const,
+						type: 'string',
+						optional: true, nullable: false,
 						example: 'rgb(40,65,87)',
 					},
 				},
 			},
 			storedInternal: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: true as const,
+				type: 'boolean',
+				optional: false, nullable: true,
 				example: true,
 			},
 			url: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 				format: 'url',
 			},
 			thumbnailUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 				format: 'url',
 			},
 			webpublicUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 				format: 'url',
 			},
 			accessKey: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 			},
 			thumbnailAccessKey: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 			},
 			webpublicAccessKey: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 			},
 			uri: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 			},
 			src: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 			},
 			folderId: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 				format: 'id',
 				example: 'xxxxxxxxxx',
 			},
 			isSensitive: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			isLink: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts
new file mode 100644
index 0000000000..f0fd73c276
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts
@@ -0,0 +1,39 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ID } from '@/misc/cafy-id';
+import { Emojis } from '@/models/index';
+import { getConnection, In } from 'typeorm';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+
+	params: {
+		ids: {
+			validator: $.arr($.type(ID)),
+		},
+
+		aliases: {
+			validator: $.arr($.str),
+		},
+	},
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+export default define(meta, async (ps) => {
+	const emojis = await Emojis.find({
+		id: In(ps.ids),
+	});
+
+	for (const emoji of emojis) {
+		await Emojis.update(emoji.id, {
+			updatedAt: new Date(),
+			aliases: [...new Set(emoji.aliases.concat(ps.aliases))],
+		});
+	}
+
+	await getConnection().queryResultCache!.remove(['meta_emojis']);
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
index f7a0fdb875..1dfeae262f 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
@@ -12,7 +12,7 @@ import { publishBroadcastStream } from '@/services/stream';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -28,7 +28,7 @@ export const meta = {
 			id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
@@ -45,8 +45,9 @@ export default define(meta, async (ps, me) => {
 		category: null,
 		host: null,
 		aliases: [],
-		url: file.url,
-		type: file.type,
+		originalUrl: file.url,
+		publicUrl: file.webpublicUrl ?? file.url,
+		type: file.webpublicType ?? file.type,
 	}).then(x => Emojis.findOneOrFail(x.identifiers[0]));
 
 	await getConnection().queryResultCache!.remove(['meta_emojis']);
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
index 6dc7d1bd97..4f53739793 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
@@ -12,7 +12,7 @@ import { publishBroadcastStream } from '@/services/stream';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -30,17 +30,17 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			id: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				format: 'id',
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
@@ -54,7 +54,7 @@ export default define(meta, async (ps, me) => {
 
 	try {
 		// Create file
-		driveFile = await uploadFromUrl(emoji.url, null, null, null, false, true);
+		driveFile = await uploadFromUrl(emoji.originalUrl, null, null, null, false, true);
 	} catch (e) {
 		throw new ApiError();
 	}
@@ -65,9 +65,9 @@ export default define(meta, async (ps, me) => {
 		name: emoji.name,
 		host: null,
 		aliases: [],
-		url: driveFile.url,
-		type: driveFile.type,
-		fileId: driveFile.id,
+		originalUrl: driveFile.url,
+		publicUrl: driveFile.webpublicUrl ?? driveFile.url,
+		type: driveFile.webpublicType ?? driveFile.type,
 	}).then(x => Emojis.findOneOrFail(x.identifiers[0]));
 
 	await getConnection().queryResultCache!.remove(['meta_emojis']);
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts
new file mode 100644
index 0000000000..797a5de672
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts
@@ -0,0 +1,37 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ID } from '@/misc/cafy-id';
+import { Emojis } from '@/models/index';
+import { getConnection, In } from 'typeorm';
+import { insertModerationLog } from '@/services/insert-moderation-log';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+
+	params: {
+		ids: {
+			validator: $.arr($.type(ID)),
+		},
+	},
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+export default define(meta, async (ps, me) => {
+	const emojis = await Emojis.find({
+		id: In(ps.ids),
+	});
+
+	for (const emoji of emojis) {
+		await Emojis.delete(emoji.id);
+	
+		await getConnection().queryResultCache!.remove(['meta_emojis']);
+	
+		insertModerationLog(me, 'deleteEmoji', {
+			emoji: emoji,
+		});
+	}
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts
similarity index 91%
rename from packages/backend/src/server/api/endpoints/admin/emoji/remove.ts
rename to packages/backend/src/server/api/endpoints/admin/emoji/delete.ts
index 440c1008c7..1580439024 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/remove.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts
@@ -9,7 +9,7 @@ import { ApiError } from '../../../error';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -25,7 +25,7 @@ export const meta = {
 			id: 'be83669b-773a-44b7-b1f8-e5e5170ac3c2',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
@@ -37,7 +37,7 @@ export default define(meta, async (ps, me) => {
 
 	await getConnection().queryResultCache!.remove(['meta_emojis']);
 
-	insertModerationLog(me, 'removeEmoji', {
+	insertModerationLog(me, 'deleteEmoji', {
 		emoji: emoji,
 	});
 });
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts
new file mode 100644
index 0000000000..8856a38f24
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts
@@ -0,0 +1,21 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { createImportCustomEmojisJob } from '@/queue/index';
+import ms from 'ms';
+import { ID } from '@/misc/cafy-id';
+
+export const meta = {
+	secure: true,
+	requireCredential: true,
+	requireModerator: true,
+	params: {
+		fileId: {
+			validator: $.type(ID),
+		},
+	},
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+export default define(meta, async (ps, user) => {
+	createImportCustomEmojisJob(user, ps.fileId);
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
index 42b6cb1fcf..6e502547f5 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
@@ -8,7 +8,7 @@ import { ID } from '@/misc/cafy-id';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -37,45 +37,45 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			properties: {
 				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 					format: 'id',
 				},
 				aliases: {
-					type: 'array' as const,
-					optional: false as const, nullable: false as const,
+					type: 'array',
+					optional: false, nullable: false,
 					items: {
-						type: 'string' as const,
-						optional: false as const, nullable: false as const,
+						type: 'string',
+						optional: false, nullable: false,
 					},
 				},
 				name: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 				},
 				category: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
+					type: 'string',
+					optional: false, nullable: true,
 				},
 				host: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
+					type: 'string',
+					optional: false, nullable: true,
 				},
 				url: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 				},
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
index 5026af914a..76ef190f94 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
@@ -8,7 +8,7 @@ import { Emoji } from '@/models/entities/emoji';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -32,45 +32,45 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			properties: {
 				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 					format: 'id',
 				},
 				aliases: {
-					type: 'array' as const,
-					optional: false as const, nullable: false as const,
+					type: 'array',
+					optional: false, nullable: false,
 					items: {
-						type: 'string' as const,
-						optional: false as const, nullable: false as const,
+						type: 'string',
+						optional: false, nullable: false,
 					},
 				},
 				name: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 				},
 				category: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
+					type: 'string',
+					optional: false, nullable: true,
 				},
 				host: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
+					type: 'string',
+					optional: false, nullable: true,
 				},
 				url: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 				},
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts
new file mode 100644
index 0000000000..c49f84b7fb
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts
@@ -0,0 +1,39 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ID } from '@/misc/cafy-id';
+import { Emojis } from '@/models/index';
+import { getConnection, In } from 'typeorm';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+
+	params: {
+		ids: {
+			validator: $.arr($.type(ID)),
+		},
+
+		aliases: {
+			validator: $.arr($.str),
+		},
+	},
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+export default define(meta, async (ps) => {
+	const emojis = await Emojis.find({
+		id: In(ps.ids),
+	});
+
+	for (const emoji of emojis) {
+		await Emojis.update(emoji.id, {
+			updatedAt: new Date(),
+			aliases: emoji.aliases.filter(x => !ps.aliases.includes(x)),
+		});
+	}
+
+	await getConnection().queryResultCache!.remove(['meta_emojis']);
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts
new file mode 100644
index 0000000000..06197820f0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts
@@ -0,0 +1,35 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ID } from '@/misc/cafy-id';
+import { Emojis } from '@/models/index';
+import { getConnection, In } from 'typeorm';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+
+	params: {
+		ids: {
+			validator: $.arr($.type(ID)),
+		},
+
+		aliases: {
+			validator: $.arr($.str),
+		},
+	},
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+export default define(meta, async (ps) => {
+	await Emojis.update({
+		id: In(ps.ids),
+	}, {
+		updatedAt: new Date(),
+		aliases: ps.aliases,
+	});
+
+	await getConnection().queryResultCache!.remove(['meta_emojis']);
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts
new file mode 100644
index 0000000000..f0645f111b
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts
@@ -0,0 +1,35 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ID } from '@/misc/cafy-id';
+import { Emojis } from '@/models/index';
+import { getConnection, In } from 'typeorm';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+
+	params: {
+		ids: {
+			validator: $.arr($.type(ID)),
+		},
+
+		category: {
+			validator: $.optional.nullable.str,
+		},
+	},
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+export default define(meta, async (ps) => {
+	await Emojis.update({
+		id: In(ps.ids),
+	}, {
+		updatedAt: new Date(),
+		category: ps.category,
+	});
+
+	await getConnection().queryResultCache!.remove(['meta_emojis']);
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
index 391887257a..54a2cf9517 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -8,7 +8,7 @@ import { ApiError } from '../../../error';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -36,7 +36,7 @@ export const meta = {
 			id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts
index 9046196838..db023c6f0b 100644
--- a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts
+++ b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts
@@ -6,7 +6,7 @@ import { DriveFiles } from '@/models/index';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -14,7 +14,7 @@ export const meta = {
 			validator: $.str,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts
index d9e3900a29..b68252ef2e 100644
--- a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts
+++ b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts
@@ -7,7 +7,7 @@ import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -15,7 +15,7 @@ export const meta = {
 			validator: $.str,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
index 485bbe7d58..4de8ad1336 100644
--- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
+++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
@@ -6,7 +6,7 @@ import { Followings, Users } from '@/models/index';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -14,7 +14,7 @@ export const meta = {
 			validator: $.str,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
index 3ddccecc6a..6ac2f1f467 100644
--- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
+++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
@@ -6,7 +6,7 @@ import { toPuny } from '@/misc/convert-host';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -18,7 +18,7 @@ export const meta = {
 			validator: $.bool,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts
index 877e67aa79..9a2bccec77 100644
--- a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts
+++ b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts
@@ -2,14 +2,14 @@ import define from '../../define';
 import { getConnection } from 'typeorm';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	tags: ['admin'],
 
 	params: {
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async () => {
diff --git a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts
index 5d4ea9c444..1c5f250676 100644
--- a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts
+++ b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts
@@ -2,7 +2,7 @@ import define from '../../define';
 import { getConnection } from 'typeorm';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	tags: ['admin'],
@@ -11,8 +11,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		example: {
 			migrations: {
 				count: 66,
@@ -20,7 +20,7 @@ export const meta = {
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async () => {
diff --git a/packages/backend/src/server/api/endpoints/admin/invite.ts b/packages/backend/src/server/api/endpoints/admin/invite.ts
index 1c8c51434b..3428709c04 100644
--- a/packages/backend/src/server/api/endpoints/admin/invite.ts
+++ b/packages/backend/src/server/api/endpoints/admin/invite.ts
@@ -6,25 +6,25 @@ import { genId } from '@/misc/gen-id';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			code: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				example: '2ERUA5VR',
 				maxLength: 8,
 				minLength: 8,
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async () => {
diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts b/packages/backend/src/server/api/endpoints/admin/moderators/add.ts
index ea795895f2..0308cf2761 100644
--- a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/moderators/add.ts
@@ -6,7 +6,7 @@ import { Users } from '@/models/index';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireAdmin: true,
 
 	params: {
@@ -14,7 +14,7 @@ export const meta = {
 			validator: $.type(ID),
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts b/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts
index 25f237d81f..bdb976e9ec 100644
--- a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts
+++ b/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts
@@ -6,7 +6,7 @@ import { Users } from '@/models/index';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireAdmin: true,
 
 	params: {
@@ -14,7 +14,7 @@ export const meta = {
 			validator: $.type(ID),
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts
index 1bd54ba890..f2735ac9f8 100644
--- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts
@@ -8,7 +8,7 @@ import { PromoNotes } from '@/models/index';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -34,7 +34,7 @@ export const meta = {
 			id: 'ae427aa2-7a41-484f-a18c-2c1104051604',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts
index 8a91168ece..3c8e7a27a2 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts
@@ -5,11 +5,11 @@ import { insertModerationLog } from '@/services/insert-moderation-log';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
index 2c867463e3..4760e2c310 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
@@ -5,25 +5,25 @@ import define from '../../../define';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
+			type: 'array',
+			optional: false, nullable: false,
 			items: {
 				anyOf: [
 					{
-						type: 'string' as const,
+						type: 'string',
 					},
 					{
-						type: 'number' as const,
+						type: 'number',
 					},
 				],
 			},
@@ -33,7 +33,7 @@ export const meta = {
 			12,
 		]],
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
index 974e680121..a95aabc506 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
@@ -5,25 +5,25 @@ import { inboxQueue } from '@/queue/queues';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
+			type: 'array',
+			optional: false, nullable: false,
 			items: {
 				anyOf: [
 					{
-						type: 'string' as const,
+						type: 'string',
 					},
 					{
-						type: 'number' as const,
+						type: 'number',
 					},
 				],
 			},
@@ -33,7 +33,7 @@ export const meta = {
 			12,
 		]],
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts b/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts
index 70649e0675..df0b4a8f13 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts
@@ -5,7 +5,7 @@ import define from '../../../define';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -24,37 +24,37 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			properties: {
 				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 					format: 'id',
 				},
 				data: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
+					type: 'object',
+					optional: false, nullable: false,
 				},
 				attempts: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
+					type: 'number',
+					optional: false, nullable: false,
 				},
 				maxAttempts: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
+					type: 'number',
+					optional: false, nullable: false,
 				},
 				timestamp: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
+					type: 'number',
+					optional: false, nullable: false,
 				},
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
index 5de871a60a..dab0be5dbc 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
@@ -4,30 +4,34 @@ import define from '../../../define';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			deliver: {
+				optional: false, nullable: false,
 				ref: 'QueueCount',
 			},
 			inbox: {
+				optional: false, nullable: false,
 				ref: 'QueueCount',
 			},
 			db: {
+				optional: false, nullable: false,
 				ref: 'QueueCount',
 			},
 			objectStorage: {
+				optional: false, nullable: false,
 				ref: 'QueueCount',
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts
index c4a0f13028..65890a00f7 100644
--- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts
@@ -7,8 +7,8 @@ import { ApiError } from '../../../error';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
-	requireModerator: true as const,
+	requireCredential: true,
+	requireModerator: true,
 
 	params: {
 		inbox: {
@@ -25,22 +25,22 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			id: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				format: 'id',
 			},
 			inbox: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				format: 'url',
 			},
 			status: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				default: 'requesting',
 				enum: [
 					'requesting',
@@ -50,7 +50,7 @@ export const meta = {
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/relays/list.ts b/packages/backend/src/server/api/endpoints/admin/relays/list.ts
index 1e8afd783d..bdddf13374 100644
--- a/packages/backend/src/server/api/endpoints/admin/relays/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/relays/list.ts
@@ -4,32 +4,32 @@ import { listRelay } from '@/services/relay';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
-	requireModerator: true as const,
+	requireCredential: true,
+	requireModerator: true,
 
 	params: {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			properties: {
 				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 					format: 'id',
 				},
 				inbox: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 					format: 'url',
 				},
 				status: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 					default: 'requesting',
 					enum: [
 						'requesting',
@@ -40,7 +40,7 @@ export const meta = {
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts
index 293de2b91b..4b04e620c1 100644
--- a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts
+++ b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts
@@ -5,15 +5,15 @@ import { removeRelay } from '@/services/relay';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
-	requireModerator: true as const,
+	requireCredential: true,
+	requireModerator: true,
 
 	params: {
 		inbox: {
 			validator: $.str,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts
index 227bcbab91..b6cf1ee2d0 100644
--- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts
+++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts
@@ -8,7 +8,7 @@ import { Users, UserProfiles } from '@/models/index';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -18,18 +18,18 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			password: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				minLength: 8,
 				maxLength: 8,
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
index a189c3e99b..b00457f092 100644
--- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
+++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
@@ -1,20 +1,30 @@
 import $ from 'cafy';
 import { ID } from '@/misc/cafy-id';
 import define from '../../define';
-import { AbuseUserReports } from '@/models/index';
+import { AbuseUserReports, Users } from '@/models/index';
+import { getInstanceActor } from '@/services/instance-actor';
+import { deliver } from '@/queue/index';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import { renderFlag } from '@/remote/activitypub/renderer/flag';
 
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
 		reportId: {
 			validator: $.type(ID),
 		},
+
+		forward: {
+			validator: $.optional.boolean,
+			required: false,
+			default: false,
+		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
@@ -24,8 +34,16 @@ export default define(meta, async (ps, me) => {
 		throw new Error('report not found');
 	}
 
+	if (ps.forward && report.targetUserHost != null) {
+		const actor = await getInstanceActor();
+		const targetUser = await Users.findOne(report.targetUserId);
+
+		deliver(actor, renderActivity(renderFlag(actor, [targetUser.uri], report.comment)), targetUser.inbox);
+	}
+
 	await AbuseUserReports.update(report.id, {
 		resolved: true,
 		assigneeId: me.id,
+		forwarded: ps.forward && report.targetUserHost != null,
 	});
 });
diff --git a/packages/backend/src/server/api/endpoints/admin/resync-chart.ts b/packages/backend/src/server/api/endpoints/admin/resync-chart.ts
index 49d41cd8c2..d80d2b0426 100644
--- a/packages/backend/src/server/api/endpoints/admin/resync-chart.ts
+++ b/packages/backend/src/server/api/endpoints/admin/resync-chart.ts
@@ -5,9 +5,9 @@ import { insertModerationLog } from '@/services/insert-moderation-log';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/send-email.ts b/packages/backend/src/server/api/endpoints/admin/send-email.ts
index 74ec98f75d..c2972c35fa 100644
--- a/packages/backend/src/server/api/endpoints/admin/send-email.ts
+++ b/packages/backend/src/server/api/endpoints/admin/send-email.ts
@@ -5,7 +5,7 @@ import { sendEmail } from '@/services/send-email';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -19,7 +19,7 @@ export const meta = {
 			validator: $.str,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/server-info.ts b/packages/backend/src/server/api/endpoints/admin/server-info.ts
index 9346c5dd2c..cd282e364c 100644
--- a/packages/backend/src/server/api/endpoints/admin/server-info.ts
+++ b/packages/backend/src/server/api/endpoints/admin/server-info.ts
@@ -5,7 +5,7 @@ import define from '../../define';
 import { redisClient } from '../../../../db/redis';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	tags: ['admin', 'meta'],
@@ -14,81 +14,81 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			machine: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 			},
 			os: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				example: 'linux',
 			},
 			node: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 			},
 			psql: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 			},
 			cpu: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
+				type: 'object',
+				optional: false, nullable: false,
 				properties: {
 					model: {
-						type: 'string' as const,
-						optional: false as const, nullable: false as const,
+						type: 'string',
+						optional: false, nullable: false,
 					},
 					cores: {
-						type: 'number' as const,
-						optional: false as const, nullable: false as const,
+						type: 'number',
+						optional: false, nullable: false,
 					},
 				},
 			},
 			mem: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
+				type: 'object',
+				optional: false, nullable: false,
 				properties: {
 					total: {
-						type: 'number' as const,
-						optional: false as const, nullable: false as const,
+						type: 'number',
+						optional: false, nullable: false,
 						format: 'bytes',
 					},
 				},
 			},
 			fs: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
+				type: 'object',
+				optional: false, nullable: false,
 				properties: {
 					total: {
-						type: 'number' as const,
-						optional: false as const, nullable: false as const,
+						type: 'number',
+						optional: false, nullable: false,
 						format: 'bytes',
 					},
 					used: {
-						type: 'number' as const,
-						optional: false as const, nullable: false as const,
+						type: 'number',
+						optional: false, nullable: false,
 						format: 'bytes',
 					},
 				},
 			},
 			net: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
+				type: 'object',
+				optional: false, nullable: false,
 				properties: {
 					interface: {
-						type: 'string' as const,
-						optional: false as const, nullable: false as const,
+						type: 'string',
+						optional: false, nullable: false,
 						example: 'eth0',
 					},
 				},
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async () => {
diff --git a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts
index 0d3759a84b..84e2b84bb5 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -26,44 +26,44 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			properties: {
 				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 					format: 'id',
 				},
 				createdAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 					format: 'date-time',
 				},
 				type: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 				},
 				info: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
+					type: 'object',
+					optional: false, nullable: false,
 				},
 				userId: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 					format: 'id',
 				},
 				user: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User',
+					type: 'object',
+					optional: false, nullable: false,
+					ref: 'UserDetailed',
 				},
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts
index 7ac922ca7e..c2a6a294b5 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts
@@ -6,7 +6,7 @@ import { Users } from '@/models/index';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -16,148 +16,148 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		nullable: false as const, optional: false as const,
+		type: 'object',
+		nullable: false, optional: false,
 		properties: {
 			id: {
-				type: 'string' as const,
-				nullable: false as const, optional: false as const,
+				type: 'string',
+				nullable: false, optional: false,
 				format: 'id',
 			},
 			createdAt: {
-				type: 'string' as const,
-				nullable: false as const, optional: false as const,
+				type: 'string',
+				nullable: false, optional: false,
 				format: 'date-time',
 			},
 			updatedAt: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const,
+				type: 'string',
+				nullable: true, optional: false,
 				format: 'date-time',
 			},
 			lastFetchedAt: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const,
+				type: 'string',
+				nullable: true, optional: false,
 			},
 			username: {
-				type: 'string' as const,
-				nullable: false as const, optional: false as const,
+				type: 'string',
+				nullable: false, optional: false,
 			},
 			name: {
-				type: 'string' as const,
-				nullable: false as const, optional: false as const,
+				type: 'string',
+				nullable: true, optional: false,
 			},
 			folowersCount: {
-				type: 'number' as const,
-				nullable: false as const, optional: false as const,
+				type: 'number',
+				nullable: false, optional: true,
 			},
 			followingCount: {
-				type: 'number' as const,
-				nullable: false as const, optional: false as const,
+				type: 'number',
+				nullable: false, optional: false,
 			},
 			notesCount: {
-				type: 'number' as const,
-				nullable: false as const, optional: false as const,
+				type: 'number',
+				nullable: false, optional: false,
 			},
 			avatarId: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const,
+				type: 'string',
+				nullable: true, optional: false,
 			},
 			bannerId: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const,
+				type: 'string',
+				nullable: true, optional: false,
 			},
 			tags: {
-				type: 'array' as const,
-				nullable: false as const, optional: false as const,
+				type: 'array',
+				nullable: false, optional: false,
 				items: {
-					type: 'string' as const,
-					nullable: false as const, optional: false as const,
+					type: 'string',
+					nullable: false, optional: false,
 				},
 			},
 			avatarUrl: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const,
+				type: 'string',
+				nullable: true, optional: false,
 				format: 'url',
 			},
 			bannerUrl: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const,
+				type: 'string',
+				nullable: true, optional: false,
 				format: 'url',
 			},
 			avatarBlurhash: {
-				type: 'any' as const,
-				nullable: true as const, optional: false as const,
+				type: 'any',
+				nullable: true, optional: false,
 				default: null,
 			},
 			bannerBlurhash: {
-				type: 'any' as const,
-				nullable: true as const, optional: false as const,
+				type: 'any',
+				nullable: true, optional: false,
 				default: null,
 			},
 			isSuspended: {
-				type: 'boolean' as const,
-				nullable: false as const, optional: false as const,
+				type: 'boolean',
+				nullable: false, optional: false,
 			},
 			isSilenced: {
-				type: 'boolean' as const,
-				nullable: false as const, optional: false as const,
+				type: 'boolean',
+				nullable: false, optional: false,
 			},
 			isLocked: {
-				type: 'boolean' as const,
-				nullable: false as const, optional: false as const,
+				type: 'boolean',
+				nullable: false, optional: false,
 			},
 			isBot: {
-				type: 'boolean' as const,
-				nullable: false as const, optional: false as const,
+				type: 'boolean',
+				nullable: false, optional: false,
 			},
 			isCat: {
-				type: 'boolean' as const,
-				nullable: false as const, optional: false as const,
+				type: 'boolean',
+				nullable: false, optional: false,
 			},
 			isAdmin: {
-				type: 'boolean' as const,
-				nullable: false as const, optional: false as const,
+				type: 'boolean',
+				nullable: false, optional: false,
 			},
 			isModerator: {
-				type: 'boolean' as const,
-				nullable: false as const, optional: false as const,
+				type: 'boolean',
+				nullable: false, optional: false,
 			},
 			emojis: {
-				type: 'array' as const,
-				nullable: false as const, optional: false as const,
+				type: 'array',
+				nullable: false, optional: false,
 				items: {
-					type: 'string' as const,
-					nullable: false as const, optional: false as const,
+					type: 'string',
+					nullable: false, optional: false,
 				},
 			},
 			host: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const,
+				type: 'string',
+				nullable: true, optional: false,
 			},
 			inbox: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const,
+				type: 'string',
+				nullable: true, optional: false,
 			},
 			sharedInbox: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const,
+				type: 'string',
+				nullable: true, optional: false,
 			},
 			featured: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const,
+				type: 'string',
+				nullable: true, optional: false,
 			},
 			uri: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const,
+				type: 'string',
+				nullable: true, optional: false,
 			},
 			token: {
-				type: 'string' as const,
-				nullable: false as const, optional: false as const,
+				type: 'string',
+				nullable: true, optional: false,
 				default: '<MASKED>',
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts
index 507183e876..d3dde99b72 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-users.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts
@@ -5,7 +5,7 @@ import { Users } from '@/models/index';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -64,15 +64,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		nullable: false as const, optional: false as const,
+		type: 'array',
+		nullable: false, optional: false,
 		items: {
-			type: 'object' as const,
-			nullable: false as const, optional: false as const,
-			ref: 'User',
+			type: 'object',
+			nullable: false, optional: false,
+			ref: 'UserDetailed',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts
index b4a1ddcc0f..872bd2a6ac 100644
--- a/packages/backend/src/server/api/endpoints/admin/silence-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/silence-user.ts
@@ -7,7 +7,7 @@ import { insertModerationLog } from '@/services/insert-moderation-log';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -15,7 +15,7 @@ export const meta = {
 			validator: $.type(ID),
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts
index 95771e0c32..2bb1875fc0 100644
--- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts
@@ -11,7 +11,7 @@ import { publishUserEvent } from '@/services/stream';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -19,7 +19,7 @@ export const meta = {
 			validator: $.type(ID),
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
index e04960a4ec..a4c6ff2ade 100644
--- a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
@@ -7,7 +7,7 @@ import { insertModerationLog } from '@/services/insert-moderation-log';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -15,7 +15,7 @@ export const meta = {
 			validator: $.type(ID),
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts
index 0a6ceadab9..5ab56d51c7 100644
--- a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts
@@ -8,7 +8,7 @@ import { doPostUnsuspend } from '@/services/unsuspend-user';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -16,7 +16,7 @@ export const meta = {
 			validator: $.type(ID),
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 0d5455cd33..aa2d1222f7 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -9,7 +9,7 @@ import { ID } from '@/misc/cafy-id';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireAdmin: true,
 
 	params: {
@@ -297,7 +297,7 @@ export const meta = {
 			validator: $.optional.bool,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/admin/vacuum.ts b/packages/backend/src/server/api/endpoints/admin/vacuum.ts
index 798a51acc5..4229ef0d29 100644
--- a/packages/backend/src/server/api/endpoints/admin/vacuum.ts
+++ b/packages/backend/src/server/api/endpoints/admin/vacuum.ts
@@ -6,7 +6,7 @@ import { insertModerationLog } from '@/services/insert-moderation-log';
 export const meta = {
 	tags: ['admin'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 	requireModerator: true,
 
 	params: {
@@ -17,7 +17,7 @@ export const meta = {
 			validator: $.bool,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts
index 122d04f17c..0bd29607d6 100644
--- a/packages/backend/src/server/api/endpoints/announcements.ts
+++ b/packages/backend/src/server/api/endpoints/announcements.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../common/make-pagination-query';
 export const meta = {
 	tags: ['meta'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		limit: {
@@ -30,48 +30,48 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			properties: {
 				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 					format: 'id',
 					example: 'xxxxxxxxxx',
 				},
 				createdAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 					format: 'date-time',
 				},
 				updatedAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
+					type: 'string',
+					optional: false, nullable: true,
 					format: 'date-time',
 				},
 				text: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 				},
 				title: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 				},
 				imageUrl: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
+					type: 'string',
+					optional: false, nullable: true,
 				},
 				isRead: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
+					type: 'boolean',
+					optional: true, nullable: false,
 				},
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts
index b3276bd9cd..2092d177ba 100644
--- a/packages/backend/src/server/api/endpoints/antennas/create.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/create.ts
@@ -9,7 +9,7 @@ import { publishInternalEvent } from '@/services/stream';
 export const meta = {
 	tags: ['antennas'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -74,11 +74,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'Antenna',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/antennas/delete.ts b/packages/backend/src/server/api/endpoints/antennas/delete.ts
index c6da545400..b2793fc70d 100644
--- a/packages/backend/src/server/api/endpoints/antennas/delete.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/delete.ts
@@ -8,7 +8,7 @@ import { publishInternalEvent } from '@/services/stream';
 export const meta = {
 	tags: ['antennas'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -25,7 +25,7 @@ export const meta = {
 			id: 'b34dcf9d-348f-44bb-99d0-6c9314cfe2df',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/antennas/list.ts b/packages/backend/src/server/api/endpoints/antennas/list.ts
index 1f24e0fb97..bb58912612 100644
--- a/packages/backend/src/server/api/endpoints/antennas/list.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/list.ts
@@ -4,20 +4,20 @@ import { Antennas } from '@/models/index';
 export const meta = {
 	tags: ['antennas', 'account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:account',
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Antenna',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts
index 09500a38dc..eb7de901c5 100644
--- a/packages/backend/src/server/api/endpoints/antennas/notes.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts
@@ -12,7 +12,7 @@ import { generateBlockedUserQuery } from '../../common/generate-block-query';
 export const meta = {
 	tags: ['antennas', 'account', 'notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:account',
 
@@ -52,15 +52,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/antennas/show.ts b/packages/backend/src/server/api/endpoints/antennas/show.ts
index a02c44ac2b..a37d37d31c 100644
--- a/packages/backend/src/server/api/endpoints/antennas/show.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/show.ts
@@ -7,7 +7,7 @@ import { Antennas } from '@/models/index';
 export const meta = {
 	tags: ['antennas', 'account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:account',
 
@@ -26,11 +26,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'Antenna',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts
index 968f761a35..900f725505 100644
--- a/packages/backend/src/server/api/endpoints/antennas/update.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/update.ts
@@ -8,7 +8,7 @@ import { publishInternalEvent } from '@/services/stream';
 export const meta = {
 	tags: ['antennas'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -83,11 +83,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'Antenna',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/ap/get.ts b/packages/backend/src/server/api/endpoints/ap/get.ts
index 0acce9bdbc..ff8c677b91 100644
--- a/packages/backend/src/server/api/endpoints/ap/get.ts
+++ b/packages/backend/src/server/api/endpoints/ap/get.ts
@@ -7,7 +7,7 @@ import ms from 'ms';
 export const meta = {
 	tags: ['federation'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	limit: {
 		duration: ms('1hour'),
@@ -24,10 +24,10 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts
index e4e13117e6..7d17d8edce 100644
--- a/packages/backend/src/server/api/endpoints/ap/show.ts
+++ b/packages/backend/src/server/api/endpoints/ap/show.ts
@@ -12,11 +12,12 @@ import { User } from '@/models/entities/user';
 import { fetchMeta } from '@/misc/fetch-meta';
 import { isActor, isPost, getApId } from '@/remote/activitypub/type';
 import ms from 'ms';
+import { SchemaType } from '@/misc/schema';
 
 export const meta = {
 	tags: ['federation'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	limit: {
 		duration: ms('1hour'),
@@ -38,21 +39,41 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			type: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				enum: ['User', 'Note'],
+		optional: false, nullable: false,
+		oneOf: [
+			{
+				type: 'object',
+				properties: {
+					type: {
+						type: 'string',
+						optional: false, nullable: false,
+						enum: ['User'],
+					},
+					object: {
+						type: 'object',
+						optional: false, nullable: false,
+						ref: 'UserDetailedNotMe',
+					}
+				}
 			},
-			object: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-			},
-		},
+			{
+				type: 'object',
+				properties: {
+					type: {
+						type: 'string',
+						optional: false, nullable: false,
+						enum: ['Note'],
+					},
+					object: {
+						type: 'object',
+						optional: false, nullable: false,
+						ref: 'Note',
+					}
+				}
+			}
+		],
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
@@ -67,7 +88,7 @@ export default define(meta, async (ps) => {
 /***
  * URIからUserかNoteを解決する
  */
-async function fetchAny(uri: string) {
+async function fetchAny(uri: string): Promise<SchemaType<typeof meta['res']> | null> {
 	// URIがこのサーバーを指しているなら、ローカルユーザーIDとしてDBからフェッチ
 	if (uri.startsWith(config.url + '/')) {
 		const parts = uri.split('/');
@@ -96,8 +117,8 @@ async function fetchAny(uri: string) {
 	}
 
 	// ブロックしてたら中断
-	const meta = await fetchMeta();
-	if (meta.blockedHosts.includes(extractDbHost(uri))) return null;
+	const fetchedMeta = await fetchMeta();
+	if (fetchedMeta.blockedHosts.includes(extractDbHost(uri))) return null;
 
 	// URI(AP Object id)としてDB検索
 	{
@@ -172,7 +193,7 @@ async function fetchAny(uri: string) {
 	return null;
 }
 
-async function mergePack(user: User | null | undefined, note: Note | null | undefined) {
+async function mergePack(user: User | null | undefined, note: Note | null | undefined): Promise<SchemaType<typeof meta.res> | null> {
 	if (user != null) {
 		return {
 			type: 'User',
diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts
index 88e5d866c2..838cbf606e 100644
--- a/packages/backend/src/server/api/endpoints/app/create.ts
+++ b/packages/backend/src/server/api/endpoints/app/create.ts
@@ -8,7 +8,7 @@ import { secureRndstr } from '@/misc/secure-rndstr';
 export const meta = {
 	tags: ['app'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		name: {
@@ -31,11 +31,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'App',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/app/show.ts b/packages/backend/src/server/api/endpoints/app/show.ts
index 701a23b223..9f4777b383 100644
--- a/packages/backend/src/server/api/endpoints/app/show.ts
+++ b/packages/backend/src/server/api/endpoints/app/show.ts
@@ -13,12 +13,6 @@ export const meta = {
 		},
 	},
 
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'App',
-	},
-
 	errors: {
 		noSuchApp: {
 			message: 'No such app.',
@@ -28,11 +22,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'App',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user, token) => {
diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts
index 6b2b0bc706..f028135ca5 100644
--- a/packages/backend/src/server/api/endpoints/auth/accept.ts
+++ b/packages/backend/src/server/api/endpoints/auth/accept.ts
@@ -9,7 +9,7 @@ import { secureRndstr } from '@/misc/secure-rndstr';
 export const meta = {
 	tags: ['auth'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -26,7 +26,7 @@ export const meta = {
 			id: '9c72d8de-391a-43c1-9d06-08d29efde8df',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/auth/session/generate.ts b/packages/backend/src/server/api/endpoints/auth/session/generate.ts
index b9e5e84f6e..98987eba5b 100644
--- a/packages/backend/src/server/api/endpoints/auth/session/generate.ts
+++ b/packages/backend/src/server/api/endpoints/auth/session/generate.ts
@@ -9,7 +9,7 @@ import { genId } from '@/misc/gen-id';
 export const meta = {
 	tags: ['auth'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		appSecret: {
@@ -18,16 +18,16 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			token: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 			},
 			url: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				format: 'url',
 			},
 		},
@@ -40,7 +40,7 @@ export const meta = {
 			id: '92f93e63-428e-4f2f-a5a4-39e1407fe998',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/auth/session/show.ts b/packages/backend/src/server/api/endpoints/auth/session/show.ts
index 3a32b5e548..ae0d016cea 100644
--- a/packages/backend/src/server/api/endpoints/auth/session/show.ts
+++ b/packages/backend/src/server/api/endpoints/auth/session/show.ts
@@ -6,7 +6,7 @@ import { AuthSessions } from '@/models/index';
 export const meta = {
 	tags: ['auth'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		token: {
@@ -23,26 +23,26 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			id: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				format: 'id',
 			},
 			app: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
+				type: 'object',
+				optional: false, nullable: false,
 				ref: 'App',
 			},
 			token: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts
index 131cdf3dfa..fe0211ebe3 100644
--- a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts
+++ b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts
@@ -6,7 +6,7 @@ import { Apps, AuthSessions, AccessTokens, Users } from '@/models/index';
 export const meta = {
 	tags: ['auth'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		appSecret: {
@@ -19,18 +19,18 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			accessToken: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 			},
 
 			user: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				ref: 'User',
+				type: 'object',
+				optional: false, nullable: false,
+				ref: 'UserDetailedNotMe',
 			},
 		},
 	},
@@ -54,7 +54,7 @@ export const meta = {
 			id: '8c8a4145-02cc-4cca-8e66-29ba60445a8e',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts
index f718bcd205..6d555ff569 100644
--- a/packages/backend/src/server/api/endpoints/blocking/create.ts
+++ b/packages/backend/src/server/api/endpoints/blocking/create.ts
@@ -15,7 +15,7 @@ export const meta = {
 		max: 100,
 	},
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:blocks',
 
@@ -46,11 +46,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User',
+		type: 'object',
+		optional: false, nullable: false,
+		ref: 'UserDetailedNotMe',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts
index e563117410..942cddaedf 100644
--- a/packages/backend/src/server/api/endpoints/blocking/delete.ts
+++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts
@@ -15,7 +15,7 @@ export const meta = {
 		max: 100,
 	},
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:blocks',
 
@@ -46,11 +46,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User',
+		type: 'object',
+		optional: false, nullable: false,
+		ref: 'UserDetailedNotMe',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/blocking/list.ts b/packages/backend/src/server/api/endpoints/blocking/list.ts
index f1f4b16999..9a4f662140 100644
--- a/packages/backend/src/server/api/endpoints/blocking/list.ts
+++ b/packages/backend/src/server/api/endpoints/blocking/list.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
 export const meta = {
 	tags: ['account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:blocks',
 
@@ -27,15 +27,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Blocking',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts
index 9c88231ddd..48be728d99 100644
--- a/packages/backend/src/server/api/endpoints/channels/create.ts
+++ b/packages/backend/src/server/api/endpoints/channels/create.ts
@@ -9,7 +9,7 @@ import { ID } from '@/misc/cafy-id';
 export const meta = {
 	tags: ['channels'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:channels',
 
@@ -28,8 +28,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'Channel',
 	},
 
@@ -40,7 +40,7 @@ export const meta = {
 			id: 'cd1e9f3e-5a12-4ab4-96f6-5d0a2cc32050',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/channels/featured.ts b/packages/backend/src/server/api/endpoints/channels/featured.ts
index cec14cda11..ceadde907c 100644
--- a/packages/backend/src/server/api/endpoints/channels/featured.ts
+++ b/packages/backend/src/server/api/endpoints/channels/featured.ts
@@ -4,18 +4,18 @@ import { Channels } from '@/models/index';
 export const meta = {
 	tags: ['channels'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Channel',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/channels/follow.ts b/packages/backend/src/server/api/endpoints/channels/follow.ts
index 3f4904f71d..bf580eea60 100644
--- a/packages/backend/src/server/api/endpoints/channels/follow.ts
+++ b/packages/backend/src/server/api/endpoints/channels/follow.ts
@@ -9,7 +9,7 @@ import { publishUserEvent } from '@/services/stream';
 export const meta = {
 	tags: ['channels'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:channels',
 
@@ -26,7 +26,7 @@ export const meta = {
 			id: 'c0031718-d573-4e85-928e-10039f1fbb68',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/channels/followed.ts b/packages/backend/src/server/api/endpoints/channels/followed.ts
index 82ae1a3afd..9e4c942af2 100644
--- a/packages/backend/src/server/api/endpoints/channels/followed.ts
+++ b/packages/backend/src/server/api/endpoints/channels/followed.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
 export const meta = {
 	tags: ['channels', 'account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:channels',
 
@@ -27,15 +27,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Channel',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/channels/owned.ts b/packages/backend/src/server/api/endpoints/channels/owned.ts
index 128ea41158..5473636a85 100644
--- a/packages/backend/src/server/api/endpoints/channels/owned.ts
+++ b/packages/backend/src/server/api/endpoints/channels/owned.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
 export const meta = {
 	tags: ['channels', 'account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:channels',
 
@@ -27,15 +27,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Channel',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/channels/show.ts b/packages/backend/src/server/api/endpoints/channels/show.ts
index 28cb927647..598a87ec4e 100644
--- a/packages/backend/src/server/api/endpoints/channels/show.ts
+++ b/packages/backend/src/server/api/endpoints/channels/show.ts
@@ -7,7 +7,7 @@ import { Channels } from '@/models/index';
 export const meta = {
 	tags: ['channels'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		channelId: {
@@ -16,8 +16,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'Channel',
 	},
 
@@ -28,7 +28,7 @@ export const meta = {
 			id: '6f6c314b-7486-4897-8966-c04a66a02923',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
index ec60e5f236..927ce7c741 100644
--- a/packages/backend/src/server/api/endpoints/channels/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts
@@ -9,7 +9,7 @@ import { activeUsersChart } from '@/services/chart/index';
 export const meta = {
 	tags: ['notes', 'channels'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		channelId: {
@@ -39,11 +39,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
@@ -55,7 +55,7 @@ export const meta = {
 			id: '4d0eeeba-a02c-4c3c-9966-ef60d38d2e7f',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/channels/unfollow.ts b/packages/backend/src/server/api/endpoints/channels/unfollow.ts
index 1472aa0457..ada0cb29fd 100644
--- a/packages/backend/src/server/api/endpoints/channels/unfollow.ts
+++ b/packages/backend/src/server/api/endpoints/channels/unfollow.ts
@@ -8,7 +8,7 @@ import { publishUserEvent } from '@/services/stream';
 export const meta = {
 	tags: ['channels'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:channels',
 
@@ -25,7 +25,7 @@ export const meta = {
 			id: '19959ee9-0153-4c51-bbd9-a98c49dc59d6',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts
index fee79df2fa..1f7108a1cb 100644
--- a/packages/backend/src/server/api/endpoints/channels/update.ts
+++ b/packages/backend/src/server/api/endpoints/channels/update.ts
@@ -7,7 +7,7 @@ import { Channels, DriveFiles } from '@/models/index';
 export const meta = {
 	tags: ['channels'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:channels',
 
@@ -30,8 +30,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'Channel',
 	},
 
@@ -54,7 +54,7 @@ export const meta = {
 			id: 'e86c14a4-0da2-4032-8df3-e737a04c7f3b',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/charts/active-users.ts b/packages/backend/src/server/api/endpoints/charts/active-users.ts
index ac77f8f1da..f7eadc7089 100644
--- a/packages/backend/src/server/api/endpoints/charts/active-users.ts
+++ b/packages/backend/src/server/api/endpoints/charts/active-users.ts
@@ -23,7 +23,7 @@ export const meta = {
 	},
 
 	res: convertLog(activeUsersChart.schema),
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/charts/drive.ts b/packages/backend/src/server/api/endpoints/charts/drive.ts
index 7f15467e7d..364279da95 100644
--- a/packages/backend/src/server/api/endpoints/charts/drive.ts
+++ b/packages/backend/src/server/api/endpoints/charts/drive.ts
@@ -23,7 +23,7 @@ export const meta = {
 	},
 
 	res: convertLog(driveChart.schema),
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/charts/federation.ts b/packages/backend/src/server/api/endpoints/charts/federation.ts
index 677c28c1e0..6feb82b6d9 100644
--- a/packages/backend/src/server/api/endpoints/charts/federation.ts
+++ b/packages/backend/src/server/api/endpoints/charts/federation.ts
@@ -23,7 +23,7 @@ export const meta = {
 	},
 
 	res: convertLog(federationChart.schema),
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/charts/hashtag.ts b/packages/backend/src/server/api/endpoints/charts/hashtag.ts
index aa41bcf90e..99dc77998e 100644
--- a/packages/backend/src/server/api/endpoints/charts/hashtag.ts
+++ b/packages/backend/src/server/api/endpoints/charts/hashtag.ts
@@ -27,7 +27,7 @@ export const meta = {
 	},
 
 	res: convertLog(hashtagChart.schema),
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/charts/instance.ts b/packages/backend/src/server/api/endpoints/charts/instance.ts
index 2ac415464d..23e6fbf2b0 100644
--- a/packages/backend/src/server/api/endpoints/charts/instance.ts
+++ b/packages/backend/src/server/api/endpoints/charts/instance.ts
@@ -27,7 +27,7 @@ export const meta = {
 	},
 
 	res: convertLog(instanceChart.schema),
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/charts/network.ts b/packages/backend/src/server/api/endpoints/charts/network.ts
index 4056becad2..c5a39bbd76 100644
--- a/packages/backend/src/server/api/endpoints/charts/network.ts
+++ b/packages/backend/src/server/api/endpoints/charts/network.ts
@@ -23,7 +23,7 @@ export const meta = {
 	},
 
 	res: convertLog(networkChart.schema),
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/charts/notes.ts b/packages/backend/src/server/api/endpoints/charts/notes.ts
index 00d6e0aa64..dcbd80c3e9 100644
--- a/packages/backend/src/server/api/endpoints/charts/notes.ts
+++ b/packages/backend/src/server/api/endpoints/charts/notes.ts
@@ -23,7 +23,7 @@ export const meta = {
 	},
 
 	res: convertLog(notesChart.schema),
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/charts/user/drive.ts b/packages/backend/src/server/api/endpoints/charts/user/drive.ts
index 462fb5879b..94787b4a57 100644
--- a/packages/backend/src/server/api/endpoints/charts/user/drive.ts
+++ b/packages/backend/src/server/api/endpoints/charts/user/drive.ts
@@ -28,7 +28,7 @@ export const meta = {
 	},
 
 	res: convertLog(perUserDriveChart.schema),
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/charts/user/following.ts b/packages/backend/src/server/api/endpoints/charts/user/following.ts
index 3aee237d8f..effe0c54b9 100644
--- a/packages/backend/src/server/api/endpoints/charts/user/following.ts
+++ b/packages/backend/src/server/api/endpoints/charts/user/following.ts
@@ -28,7 +28,7 @@ export const meta = {
 	},
 
 	res: convertLog(perUserFollowingChart.schema),
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/charts/user/notes.ts b/packages/backend/src/server/api/endpoints/charts/user/notes.ts
index 3536e7b779..df68a5fe52 100644
--- a/packages/backend/src/server/api/endpoints/charts/user/notes.ts
+++ b/packages/backend/src/server/api/endpoints/charts/user/notes.ts
@@ -28,7 +28,7 @@ export const meta = {
 	},
 
 	res: convertLog(perUserNotesChart.schema),
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts
index 9c5515f0b4..dcd067305f 100644
--- a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts
@@ -28,7 +28,7 @@ export const meta = {
 	},
 
 	res: convertLog(perUserReactionsChart.schema),
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/charts/users.ts b/packages/backend/src/server/api/endpoints/charts/users.ts
index d8bbb8300c..d32e14ad61 100644
--- a/packages/backend/src/server/api/endpoints/charts/users.ts
+++ b/packages/backend/src/server/api/endpoints/charts/users.ts
@@ -23,7 +23,7 @@ export const meta = {
 	},
 
 	res: convertLog(usersChart.schema),
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts
index 992ba0edd3..4a740b6cfe 100644
--- a/packages/backend/src/server/api/endpoints/clips/add-note.ts
+++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts
@@ -9,7 +9,7 @@ import { getNote } from '../../common/getters';
 export const meta = {
 	tags: ['account', 'notes', 'clips'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -42,7 +42,7 @@ export const meta = {
 			id: '734806c4-542c-463a-9311-15c512803965',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts
index e9900247a1..852e66c9e4 100644
--- a/packages/backend/src/server/api/endpoints/clips/create.ts
+++ b/packages/backend/src/server/api/endpoints/clips/create.ts
@@ -6,7 +6,7 @@ import { Clips } from '@/models/index';
 export const meta = {
 	tags: ['clips'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -25,11 +25,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'Clip',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/clips/delete.ts b/packages/backend/src/server/api/endpoints/clips/delete.ts
index b7d16322dd..85c64a115d 100644
--- a/packages/backend/src/server/api/endpoints/clips/delete.ts
+++ b/packages/backend/src/server/api/endpoints/clips/delete.ts
@@ -7,7 +7,7 @@ import { Clips } from '@/models/index';
 export const meta = {
 	tags: ['clips'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -24,7 +24,7 @@ export const meta = {
 			id: '70ca08ba-6865-4630-b6fb-8494759aa754',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/clips/list.ts b/packages/backend/src/server/api/endpoints/clips/list.ts
index 8388438bd1..d88897d164 100644
--- a/packages/backend/src/server/api/endpoints/clips/list.ts
+++ b/packages/backend/src/server/api/endpoints/clips/list.ts
@@ -4,20 +4,20 @@ import { Clips } from '@/models/index';
 export const meta = {
 	tags: ['clips', 'account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:account',
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Clip',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts
index c8871cd3fa..eeb20631c1 100644
--- a/packages/backend/src/server/api/endpoints/clips/notes.ts
+++ b/packages/backend/src/server/api/endpoints/clips/notes.ts
@@ -11,7 +11,7 @@ import { generateBlockedUserQuery } from '../../common/generate-block-query';
 export const meta = {
 	tags: ['account', 'notes', 'clips'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	kind: 'read:account',
 
@@ -43,15 +43,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/clips/show.ts b/packages/backend/src/server/api/endpoints/clips/show.ts
index ce65abd65d..0a45672019 100644
--- a/packages/backend/src/server/api/endpoints/clips/show.ts
+++ b/packages/backend/src/server/api/endpoints/clips/show.ts
@@ -7,7 +7,7 @@ import { Clips } from '@/models/index';
 export const meta = {
 	tags: ['clips', 'account'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	kind: 'read:account',
 
@@ -26,11 +26,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'Clip',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts
index 44e8fe33e8..795483d5b2 100644
--- a/packages/backend/src/server/api/endpoints/clips/update.ts
+++ b/packages/backend/src/server/api/endpoints/clips/update.ts
@@ -7,7 +7,7 @@ import { Clips } from '@/models/index';
 export const meta = {
 	tags: ['clips'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -38,11 +38,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'Clip',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts
index 35ac98bdf2..d9ab9883ca 100644
--- a/packages/backend/src/server/api/endpoints/drive.ts
+++ b/packages/backend/src/server/api/endpoints/drive.ts
@@ -5,25 +5,25 @@ import { DriveFiles } from '@/models/index';
 export const meta = {
 	tags: ['drive', 'account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:drive',
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			capacity: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
+				type: 'number',
+				optional: false, nullable: false,
 			},
 			usage: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
+				type: 'number',
+				optional: false, nullable: false,
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts
index 7a577bce64..a5c0a626a1 100644
--- a/packages/backend/src/server/api/endpoints/drive/files.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
 export const meta = {
 	tags: ['drive'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:drive',
 
@@ -36,15 +36,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'DriveFile',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts
index a02ac3eef0..835dde8058 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts
@@ -7,7 +7,7 @@ import { DriveFiles, Notes } from '@/models/index';
 export const meta = {
 	tags: ['drive', 'notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:drive',
 
@@ -18,11 +18,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
@@ -34,7 +34,7 @@ export const meta = {
 			id: 'c118ece3-2e4b-4296-99d1-51756e32d232',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts
index 14517ab4b5..a45d357ee8 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts
@@ -5,7 +5,7 @@ import { DriveFiles } from '@/models/index';
 export const meta = {
 	tags: ['drive'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:drive',
 
@@ -16,10 +16,10 @@ export const meta = {
 	},
 
 	res: {
-		type: 'boolean' as const,
-		optional: false as const, nullable: false as const,
+		type: 'boolean',
+		optional: false, nullable: false,
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts
index 640b62c6ec..3799181540 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/create.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts
@@ -1,7 +1,7 @@
 import ms from 'ms';
 import $ from 'cafy';
 import { ID } from '@/misc/cafy-id';
-import create from '@/services/drive/add-file';
+import { addFile } from '@/services/drive/add-file';
 import define from '../../../define';
 import { apiLogger } from '../../../logger';
 import { ApiError } from '../../../error';
@@ -10,7 +10,7 @@ import { DriveFiles } from '@/models/index';
 export const meta = {
 	tags: ['drive'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	limit: {
 		duration: ms('1hour'),
@@ -46,8 +46,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'DriveFile',
 	},
 
@@ -58,7 +58,7 @@ export const meta = {
 			id: 'f449b209-0c60-4e51-84d5-29486263bfd4',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user, _, file, cleanup) => {
@@ -79,7 +79,7 @@ export default define(meta, async (ps, user, _, file, cleanup) => {
 
 	try {
 		// Create file
-		const driveFile = await create(user, file.path, name, null, ps.folderId, ps.force, false, null, null, ps.isSensitive);
+		const driveFile = await addFile(user, file.path, name, null, ps.folderId, ps.force, false, null, null, ps.isSensitive);
 		return await DriveFiles.pack(driveFile, { self: true });
 	} catch (e) {
 		apiLogger.error(e);
diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts
index 2e32e6879b..308beb58a4 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts
@@ -9,7 +9,7 @@ import { DriveFiles } from '@/models/index';
 export const meta = {
 	tags: ['drive'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:drive',
 
@@ -32,7 +32,7 @@ export const meta = {
 			id: '5eb8d909-2540-4970-90b8-dd6f86088121',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts
index 5617769a9e..dc74dcb7e6 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts
@@ -5,7 +5,7 @@ import { DriveFiles } from '@/models/index';
 export const meta = {
 	tags: ['drive'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:drive',
 
@@ -16,15 +16,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'DriveFile',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/find.ts b/packages/backend/src/server/api/endpoints/drive/files/find.ts
index 415ab79612..2244df13cd 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/find.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/find.ts
@@ -4,7 +4,7 @@ import define from '../../../define';
 import { DriveFiles } from '@/models/index';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	tags: ['drive'],
 
@@ -22,15 +22,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'DriveFile',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts
index 4a8830e9cb..18b17c4653 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/show.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts
@@ -8,7 +8,7 @@ import { DriveFiles } from '@/models/index';
 export const meta = {
 	tags: ['drive'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:drive',
 
@@ -23,8 +23,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'DriveFile',
 	},
 
@@ -47,7 +47,7 @@ export const meta = {
 			id: '89674805-722c-440c-8d88-5641830dc3e4',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts
index 329e959c53..b7ca80e83c 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/update.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts
@@ -9,7 +9,7 @@ import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
 export const meta = {
 	tags: ['drive'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:drive',
 
@@ -60,11 +60,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'DriveFile',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
index 7e3ffd881f..6ab1ca137d 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
@@ -15,7 +15,7 @@ export const meta = {
 		max: 60,
 	},
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:drive',
 
@@ -50,7 +50,7 @@ export const meta = {
 			default: false,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/drive/folders.ts b/packages/backend/src/server/api/endpoints/drive/folders.ts
index 85938e7b54..8f8d1d2c0a 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
 export const meta = {
 	tags: ['drive'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:drive',
 
@@ -32,15 +32,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'DriveFolder',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/create.ts b/packages/backend/src/server/api/endpoints/drive/folders/create.ts
index 401b591525..38ed17e0e5 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders/create.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders/create.ts
@@ -9,7 +9,7 @@ import { genId } from '@/misc/gen-id';
 export const meta = {
 	tags: ['drive'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:drive',
 
@@ -37,7 +37,7 @@ export const meta = {
 		optional: false as const, nullable: false as const,
 		ref: 'DriveFolder',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts
index 2360a3abf8..13716fccea 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts
@@ -8,7 +8,7 @@ import { DriveFolders, DriveFiles } from '@/models/index';
 export const meta = {
 	tags: ['drive'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:drive',
 
@@ -31,7 +31,7 @@ export const meta = {
 			id: 'b0fc8a17-963c-405d-bfbc-859a487295e1',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/find.ts b/packages/backend/src/server/api/endpoints/drive/folders/find.ts
index 47e45a23fc..911f51d78b 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders/find.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders/find.ts
@@ -6,7 +6,7 @@ import { DriveFolders } from '@/models/index';
 export const meta = {
 	tags: ['drive'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:drive',
 
@@ -22,15 +22,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'DriveFolder',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/show.ts b/packages/backend/src/server/api/endpoints/drive/folders/show.ts
index d6eac59fe0..58a6dd3c06 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders/show.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders/show.ts
@@ -7,7 +7,7 @@ import { DriveFolders } from '@/models/index';
 export const meta = {
 	tags: ['drive'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:drive',
 
@@ -18,8 +18,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'DriveFolder',
 	},
 
@@ -30,7 +30,7 @@ export const meta = {
 			id: 'd74ab9eb-bb09-4bba-bf24-fb58f761e1e9',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
index 5ae5424fb2..5b0cccd1c6 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
@@ -8,7 +8,7 @@ import { DriveFolders } from '@/models/index';
 export const meta = {
 	tags: ['drive'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:drive',
 
@@ -47,11 +47,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'DriveFolder',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/drive/stream.ts b/packages/backend/src/server/api/endpoints/drive/stream.ts
index 675e3225f5..9ba7804946 100644
--- a/packages/backend/src/server/api/endpoints/drive/stream.ts
+++ b/packages/backend/src/server/api/endpoints/drive/stream.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
 export const meta = {
 	tags: ['drive'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:drive',
 
@@ -31,15 +31,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'DriveFile',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/email-address/available.ts b/packages/backend/src/server/api/endpoints/email-address/available.ts
index 5e8f1706f3..19f9b7ccdc 100644
--- a/packages/backend/src/server/api/endpoints/email-address/available.ts
+++ b/packages/backend/src/server/api/endpoints/email-address/available.ts
@@ -5,7 +5,7 @@ import { validateEmailForAccount } from '@/services/validate-email-for-account';
 export const meta = {
 	tags: ['users'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		emailAddress: {
@@ -14,20 +14,20 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			available: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			reason: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts
index 597911da82..42fd468838 100644
--- a/packages/backend/src/server/api/endpoints/endpoint.ts
+++ b/packages/backend/src/server/api/endpoints/endpoint.ts
@@ -3,7 +3,7 @@ import define from '../define';
 import endpoints from '../endpoints';
 
 export const meta = {
-	requireCredential: false as const,
+	requireCredential: false,
 
 	tags: ['meta'],
 
@@ -12,7 +12,7 @@ export const meta = {
 			validator: $.str,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/endpoints.ts b/packages/backend/src/server/api/endpoints/endpoints.ts
index 4e304dbeb1..ebb78de337 100644
--- a/packages/backend/src/server/api/endpoints/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints/endpoints.ts
@@ -2,7 +2,7 @@ import define from '../define';
 import endpoints from '../endpoints';
 
 export const meta = {
-	requireCredential: false as const,
+	requireCredential: false,
 
 	tags: ['meta'],
 
@@ -10,11 +10,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
+			type: 'string',
+			optional: false, nullable: false,
 		},
 		example: [
 			'admin/abuse-user-reports',
@@ -23,7 +23,7 @@ export const meta = {
 			'...',
 		],
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async () => {
diff --git a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts
index 8b7e8a0493..24c9f56aa6 100644
--- a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts
+++ b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts
@@ -5,12 +5,12 @@ import ms from 'ms';
 
 export const meta = {
 	secure: true,
-	requireCredential: true as const,
+	requireCredential: true,
 	limit: {
 		duration: ms('1hour'),
 		max: 1,
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/federation/followers.ts b/packages/backend/src/server/api/endpoints/federation/followers.ts
index a44b0aecf6..c0a85f166c 100644
--- a/packages/backend/src/server/api/endpoints/federation/followers.ts
+++ b/packages/backend/src/server/api/endpoints/federation/followers.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
 export const meta = {
 	tags: ['federation'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		host: {
@@ -29,15 +29,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Following',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/federation/following.ts b/packages/backend/src/server/api/endpoints/federation/following.ts
index c10e25864e..147f0aedb2 100644
--- a/packages/backend/src/server/api/endpoints/federation/following.ts
+++ b/packages/backend/src/server/api/endpoints/federation/following.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
 export const meta = {
 	tags: ['federation'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		host: {
@@ -29,15 +29,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Following',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts
index e73d0e5ada..11df7ed6b6 100644
--- a/packages/backend/src/server/api/endpoints/federation/instances.ts
+++ b/packages/backend/src/server/api/endpoints/federation/instances.ts
@@ -7,7 +7,7 @@ import { fetchMeta } from '@/misc/fetch-meta';
 export const meta = {
 	tags: ['federation'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		host: {
@@ -54,15 +54,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'FederationInstance',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/federation/show-instance.ts b/packages/backend/src/server/api/endpoints/federation/show-instance.ts
index 15b4f31518..6f13b28cae 100644
--- a/packages/backend/src/server/api/endpoints/federation/show-instance.ts
+++ b/packages/backend/src/server/api/endpoints/federation/show-instance.ts
@@ -6,7 +6,7 @@ import { toPuny } from '@/misc/convert-host';
 export const meta = {
 	tags: ['federation'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		host: {
@@ -15,11 +15,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: true, nullable: false,
 		ref: 'FederationInstance',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts
index 371859345f..092f805bc2 100644
--- a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts
+++ b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts
@@ -7,14 +7,14 @@ import { updatePerson } from '@/remote/activitypub/models/person';
 export const meta = {
 	tags: ['federation'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	params: {
 		userId: {
 			validator: $.type(ID),
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/federation/users.ts b/packages/backend/src/server/api/endpoints/federation/users.ts
index 8372169d67..9a8f749936 100644
--- a/packages/backend/src/server/api/endpoints/federation/users.ts
+++ b/packages/backend/src/server/api/endpoints/federation/users.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
 export const meta = {
 	tags: ['federation'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		host: {
@@ -29,15 +29,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User',
+			type: 'object',
+			optional: false, nullable: false,
+			ref: 'UserDetailedNotMe',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts
index 951cf8fa20..96aede4550 100644
--- a/packages/backend/src/server/api/endpoints/following/create.ts
+++ b/packages/backend/src/server/api/endpoints/following/create.ts
@@ -15,7 +15,7 @@ export const meta = {
 		max: 100,
 	},
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:following',
 
@@ -58,11 +58,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User',
+		type: 'object',
+		optional: false, nullable: false,
+		ref: 'UserLite',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/following/delete.ts b/packages/backend/src/server/api/endpoints/following/delete.ts
index de43fa4387..4cd0c49452 100644
--- a/packages/backend/src/server/api/endpoints/following/delete.ts
+++ b/packages/backend/src/server/api/endpoints/following/delete.ts
@@ -15,7 +15,7 @@ export const meta = {
 		max: 100,
 	},
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:following',
 
@@ -46,11 +46,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User',
+		type: 'object',
+		optional: false, nullable: false,
+		ref: 'UserLite',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts
index 388ddda797..92e887e00b 100644
--- a/packages/backend/src/server/api/endpoints/following/invalidate.ts
+++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts
@@ -15,7 +15,7 @@ export const meta = {
 		max: 100,
 	},
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:following',
 
@@ -46,11 +46,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User',
+		type: 'object',
+		optional: false, nullable: false,
+		ref: 'UserLite',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/following/requests/accept.ts b/packages/backend/src/server/api/endpoints/following/requests/accept.ts
index 29f0ace2ab..7e7c056f55 100644
--- a/packages/backend/src/server/api/endpoints/following/requests/accept.ts
+++ b/packages/backend/src/server/api/endpoints/following/requests/accept.ts
@@ -8,7 +8,7 @@ import { getUser } from '../../../common/getters';
 export const meta = {
 	tags: ['following', 'account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:following',
 
@@ -30,7 +30,7 @@ export const meta = {
 			id: 'bcde4f8b-0913-4614-8881-614e522fb041',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts
index d5281c468b..19ed02c152 100644
--- a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts
+++ b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts
@@ -9,7 +9,7 @@ import { Users } from '@/models/index';
 export const meta = {
 	tags: ['following', 'account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:following',
 
@@ -34,11 +34,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User',
+		type: 'object',
+		optional: false, nullable: false,
+		ref: 'UserLite',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/following/requests/list.ts b/packages/backend/src/server/api/endpoints/following/requests/list.ts
index bfd793faf9..ec0c76502c 100644
--- a/packages/backend/src/server/api/endpoints/following/requests/list.ts
+++ b/packages/backend/src/server/api/endpoints/following/requests/list.ts
@@ -4,36 +4,36 @@ import { FollowRequests } from '@/models/index';
 export const meta = {
 	tags: ['following', 'account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:following',
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			properties: {
 				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 					format: 'id',
 				},
 				follower: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User',
+					type: 'object',
+					optional: false, nullable: false,
+					ref: 'UserLite',
 				},
 				followee: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User',
+					type: 'object',
+					optional: false, nullable: false,
+					ref: 'UserLite',
 				},
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/following/requests/reject.ts b/packages/backend/src/server/api/endpoints/following/requests/reject.ts
index 77a14af1f7..a5ce1e7c77 100644
--- a/packages/backend/src/server/api/endpoints/following/requests/reject.ts
+++ b/packages/backend/src/server/api/endpoints/following/requests/reject.ts
@@ -8,7 +8,7 @@ import { getUser } from '../../../common/getters';
 export const meta = {
 	tags: ['following', 'account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:following',
 
@@ -25,7 +25,7 @@ export const meta = {
 			id: 'abc2ffa6-25b2-4380-ba99-321ff3a94555',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/gallery/featured.ts b/packages/backend/src/server/api/endpoints/gallery/featured.ts
index a6ca956924..ff7c16889f 100644
--- a/packages/backend/src/server/api/endpoints/gallery/featured.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/featured.ts
@@ -4,18 +4,18 @@ import { GalleryPosts } from '@/models/index';
 export const meta = {
 	tags: ['gallery'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'GalleryPost',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/gallery/popular.ts b/packages/backend/src/server/api/endpoints/gallery/popular.ts
index efd1f76052..2c3368a19d 100644
--- a/packages/backend/src/server/api/endpoints/gallery/popular.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/popular.ts
@@ -4,18 +4,18 @@ import { GalleryPosts } from '@/models/index';
 export const meta = {
 	tags: ['gallery'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'GalleryPost',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts.ts b/packages/backend/src/server/api/endpoints/gallery/posts.ts
index 1b1368b06f..9d2601c7e9 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts.ts
@@ -23,15 +23,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'GalleryPost',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts
index 1a75ba2cb2..e9d5df1ab6 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts
@@ -11,7 +11,7 @@ import { DriveFile } from '@/models/entities/drive-file';
 export const meta = {
 	tags: ['gallery'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:gallery',
 
@@ -40,15 +40,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'GalleryPost',
 	},
 
 	errors: {
 
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
index dbf7b2f9c0..2a13b9ed58 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
@@ -7,7 +7,7 @@ import { ID } from '@/misc/cafy-id';
 export const meta = {
 	tags: ['gallery'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:gallery',
 
@@ -24,7 +24,7 @@ export const meta = {
 			id: 'ae52f367-4bd7-4ecd-afc6-5672fff427f5',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts
index 2f98f41fa4..0fb408fa5f 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts
@@ -8,7 +8,7 @@ import { genId } from '@/misc/gen-id';
 export const meta = {
 	tags: ['gallery'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:gallery-likes',
 
@@ -37,7 +37,7 @@ export const meta = {
 			id: '40e9ed56-a59c-473a-bf3f-f289c54fb5a7',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts
index 7044f948fb..4325d2ad37 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts
@@ -7,7 +7,7 @@ import { GalleryPosts } from '@/models/index';
 export const meta = {
 	tags: ['gallery'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		postId: {
@@ -24,11 +24,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'GalleryPost',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts
index 8cf4f34251..9cca09bddc 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts
@@ -7,7 +7,7 @@ import { GalleryPosts, GalleryLikes } from '@/models/index';
 export const meta = {
 	tags: ['gallery'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:gallery-likes',
 
@@ -30,7 +30,7 @@ export const meta = {
 			id: 'e3e8e06e-be37-41f7-a5b4-87a8250288f0',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts
index 67028125d4..c35e1bbf58 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts
@@ -10,7 +10,7 @@ import { DriveFile } from '@/models/entities/drive-file';
 export const meta = {
 	tags: ['gallery'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:gallery',
 
@@ -43,15 +43,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'GalleryPost',
 	},
 
 	errors: {
 
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/games/reversi/games.ts b/packages/backend/src/server/api/endpoints/games/reversi/games.ts
deleted file mode 100644
index 8b0e812ca9..0000000000
--- a/packages/backend/src/server/api/endpoints/games/reversi/games.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ReversiGames } from '@/models/index';
-import { makePaginationQuery } from '../../../common/make-pagination-query';
-import { Brackets } from 'typeorm';
-
-export const meta = {
-	tags: ['games'],
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10,
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		my: {
-			validator: $.optional.bool,
-			default: false,
-		},
-	},
-
-	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,
-			properties: {
-				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id',
-				},
-				createdAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'date-time',
-				},
-				startedAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'date-time',
-				},
-				isStarted: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-				},
-				isEnded: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-				},
-				form1: {
-					type: 'any' as const,
-					optional: false as const, nullable: true as const,
-				},
-				form2: {
-					type: 'any' as const,
-					optional: false as const, nullable: true as const,
-				},
-				user1Accepted: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-					default: false,
-				},
-				user2Accepted: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-					default: false,
-				},
-				user1Id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id',
-				},
-				user2Id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id',
-				},
-				user1: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User',
-				},
-				user2: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User',
-				},
-				winnerId: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
-					format: 'id',
-				},
-				winner: {
-					type: 'object' as const,
-					optional: false as const, nullable: true as const,
-					ref: 'User',
-				},
-				surrendered: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
-					format: 'id',
-				},
-				black: {
-					type: 'number' as const,
-					optional: false as const, nullable: true as const,
-				},
-				bw: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-				},
-				isLlotheo: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-				},
-				canPutEverywhere: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-				},
-				loopedBoard: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-				},
-			},
-		},
-	},
-};
-
-// eslint-disable-next-line import/no-default-export
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(ReversiGames.createQueryBuilder('game'), ps.sinceId, ps.untilId)
-		.andWhere('game.isStarted = TRUE');
-
-	if (ps.my && user) {
-		query.andWhere(new Brackets(qb => { qb
-			.where('game.user1Id = :userId', { userId: user.id })
-			.orWhere('game.user2Id = :userId', { userId: user.id });
-		}));
-	}
-
-	// Fetch games
-	const games = await query.take(ps.limit!).getMany();
-
-	return await Promise.all(games.map((g) => ReversiGames.pack(g, user, {
-		detail: false,
-	})));
-});
diff --git a/packages/backend/src/server/api/endpoints/games/reversi/games/show.ts b/packages/backend/src/server/api/endpoints/games/reversi/games/show.ts
deleted file mode 100644
index 020e9e6fc2..0000000000
--- a/packages/backend/src/server/api/endpoints/games/reversi/games/show.ts
+++ /dev/null
@@ -1,169 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import Reversi from '../../../../../../games/reversi/core';
-import define from '../../../../define';
-import { ApiError } from '../../../../error';
-import { ReversiGames } from '@/models/index';
-
-export const meta = {
-	tags: ['games'],
-
-	params: {
-		gameId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchGame: {
-			message: 'No such game.',
-			code: 'NO_SUCH_GAME',
-			id: 'f13a03db-fae1-46c9-87f3-43c8165419e1',
-		},
-	},
-
-	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,
-			properties: {
-				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id',
-				},
-				createdAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'date-time',
-				},
-				startedAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'date-time',
-				},
-				isStarted: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-				},
-				isEnded: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-				},
-				form1: {
-					type: 'any' as const,
-					optional: false as const, nullable: true as const,
-				},
-				form2: {
-					type: 'any' as const,
-					optional: false as const, nullable: true as const,
-				},
-				user1Accepted: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-					default: false,
-				},
-				user2Accepted: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-					default: false,
-				},
-				user1Id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id',
-				},
-				user2Id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id',
-				},
-				user1: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User',
-				},
-				user2: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User',
-				},
-				winnerId: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
-					format: 'id',
-				},
-				winner: {
-					type: 'object' as const,
-					optional: false as const, nullable: true as const,
-					ref: 'User',
-				},
-				surrendered: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
-					format: 'id',
-				},
-				black: {
-					type: 'number' as const,
-					optional: false as const, nullable: true as const,
-				},
-				bw: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-				},
-				isLlotheo: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-				},
-				canPutEverywhere: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-				},
-				loopedBoard: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-				},
-				board: {
-					type: 'array' as const,
-					optional: false as const, nullable: false as const,
-					items: {
-						type: 'any' as const,
-						optional: false as const, nullable: false as const,
-					},
-				},
-				turn: {
-					type: 'any' as const,
-					optional: false as const, nullable: false as const,
-				},
-			},
-		},
-	},
-};
-
-// eslint-disable-next-line import/no-default-export
-export default define(meta, async (ps, user) => {
-	const game = await ReversiGames.findOne(ps.gameId);
-
-	if (game == null) {
-		throw new ApiError(meta.errors.noSuchGame);
-	}
-
-	const o = new Reversi(game.map, {
-		isLlotheo: game.isLlotheo,
-		canPutEverywhere: game.canPutEverywhere,
-		loopedBoard: game.loopedBoard,
-	});
-
-	for (const log of game.logs) {
-		o.put(log.color, log.pos);
-	}
-
-	const packed = await ReversiGames.pack(game, user);
-
-	return Object.assign({
-		board: o.board,
-		turn: o.turn,
-	}, packed);
-});
diff --git a/packages/backend/src/server/api/endpoints/games/reversi/games/surrender.ts b/packages/backend/src/server/api/endpoints/games/reversi/games/surrender.ts
deleted file mode 100644
index a0eb4705ba..0000000000
--- a/packages/backend/src/server/api/endpoints/games/reversi/games/surrender.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import { publishReversiGameStream } from '@/services/stream';
-import define from '../../../../define';
-import { ApiError } from '../../../../error';
-import { ReversiGames } from '@/models/index';
-
-export const meta = {
-	tags: ['games'],
-
-	requireCredential: true as const,
-
-	params: {
-		gameId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchGame: {
-			message: 'No such game.',
-			code: 'NO_SUCH_GAME',
-			id: 'ace0b11f-e0a6-4076-a30d-e8284c81b2df',
-		},
-
-		alreadyEnded: {
-			message: 'That game has already ended.',
-			code: 'ALREADY_ENDED',
-			id: '6c2ad4a6-cbf1-4a5b-b187-b772826cfc6d',
-		},
-
-		accessDenied: {
-			message: 'Access denied.',
-			code: 'ACCESS_DENIED',
-			id: '6e04164b-a992-4c93-8489-2123069973e1',
-		},
-	},
-};
-
-// eslint-disable-next-line import/no-default-export
-export default define(meta, async (ps, user) => {
-	const game = await ReversiGames.findOne(ps.gameId);
-
-	if (game == null) {
-		throw new ApiError(meta.errors.noSuchGame);
-	}
-
-	if (game.isEnded) {
-		throw new ApiError(meta.errors.alreadyEnded);
-	}
-
-	if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) {
-		throw new ApiError(meta.errors.accessDenied);
-	}
-
-	const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id;
-
-	await ReversiGames.update(game.id, {
-		surrendered: user.id,
-		isEnded: true,
-		winnerId: winnerId,
-	});
-
-	publishReversiGameStream(game.id, 'ended', {
-		winnerId: winnerId,
-		game: await ReversiGames.pack(game.id, user),
-	});
-});
diff --git a/packages/backend/src/server/api/endpoints/games/reversi/invitations.ts b/packages/backend/src/server/api/endpoints/games/reversi/invitations.ts
deleted file mode 100644
index 0285a2f63b..0000000000
--- a/packages/backend/src/server/api/endpoints/games/reversi/invitations.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import define from '../../../define';
-import { ReversiMatchings } from '@/models/index';
-
-export const meta = {
-	tags: ['games'],
-
-	requireCredential: true as const,
-
-	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,
-			properties: {
-				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id',
-				},
-				createdAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'date-time',
-				},
-				parentId: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id',
-				},
-				parent: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User',
-				},
-				childId: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id',
-				},
-				child: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User',
-				},
-			},
-		},
-	},
-};
-
-// eslint-disable-next-line import/no-default-export
-export default define(meta, async (ps, user) => {
-	// Find session
-	const invitations = await ReversiMatchings.find({
-		childId: user.id,
-	});
-
-	return await Promise.all(invitations.map((i) => ReversiMatchings.pack(i, user)));
-});
diff --git a/packages/backend/src/server/api/endpoints/games/reversi/match.ts b/packages/backend/src/server/api/endpoints/games/reversi/match.ts
deleted file mode 100644
index b1d958306d..0000000000
--- a/packages/backend/src/server/api/endpoints/games/reversi/match.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import { publishMainStream, publishReversiStream } from '@/services/stream';
-import { eighteight } from '../../../../../games/reversi/maps';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { getUser } from '../../../common/getters';
-import { genId } from '@/misc/gen-id';
-import { ReversiMatchings, ReversiGames } from '@/models/index';
-import { ReversiGame } from '@/models/entities/games/reversi/game';
-import { ReversiMatching } from '@/models/entities/games/reversi/matching';
-
-export const meta = {
-	tags: ['games'],
-
-	requireCredential: true as const,
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '0b4f0559-b484-4e31-9581-3f73cee89b28',
-		},
-
-		isYourself: {
-			message: 'Target user is yourself.',
-			code: 'TARGET_IS_YOURSELF',
-			id: '96fd7bd6-d2bc-426c-a865-d055dcd2828e',
-		},
-	},
-};
-
-// eslint-disable-next-line import/no-default-export
-export default define(meta, async (ps, user) => {
-	// Myself
-	if (ps.userId === user.id) {
-		throw new ApiError(meta.errors.isYourself);
-	}
-
-	// Find session
-	const exist = await ReversiMatchings.findOne({
-		parentId: ps.userId,
-		childId: user.id,
-	});
-
-	if (exist) {
-		// Destroy session
-		ReversiMatchings.delete(exist.id);
-
-		// Create game
-		const game = await ReversiGames.save({
-			id: genId(),
-			createdAt: new Date(),
-			user1Id: exist.parentId,
-			user2Id: user.id,
-			user1Accepted: false,
-			user2Accepted: false,
-			isStarted: false,
-			isEnded: false,
-			logs: [],
-			map: eighteight.data,
-			bw: 'random',
-			isLlotheo: false,
-		} as Partial<ReversiGame>);
-
-		publishReversiStream(exist.parentId, 'matched', await ReversiGames.pack(game, { id: exist.parentId }));
-
-		const other = await ReversiMatchings.count({
-			childId: user.id,
-		});
-
-		if (other == 0) {
-			publishMainStream(user.id, 'reversiNoInvites');
-		}
-
-		return await ReversiGames.pack(game, user);
-	} else {
-		// Fetch child
-		const child = await getUser(ps.userId).catch(e => {
-			if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-			throw e;
-		});
-
-		// 以前のセッションはすべて削除しておく
-		await ReversiMatchings.delete({
-			parentId: user.id,
-		});
-
-		// セッションを作成
-		const matching = await ReversiMatchings.save({
-			id: genId(),
-			createdAt: new Date(),
-			parentId: user.id,
-			childId: child.id,
-		} as ReversiMatching);
-
-		const packed = await ReversiMatchings.pack(matching, child);
-		publishReversiStream(child.id, 'invited', packed);
-		publishMainStream(child.id, 'reversiInvited', packed);
-
-		return;
-	}
-});
diff --git a/packages/backend/src/server/api/endpoints/games/reversi/match/cancel.ts b/packages/backend/src/server/api/endpoints/games/reversi/match/cancel.ts
deleted file mode 100644
index 3ef753bcd0..0000000000
--- a/packages/backend/src/server/api/endpoints/games/reversi/match/cancel.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import define from '../../../../define';
-import { ReversiMatchings } from '@/models/index';
-
-export const meta = {
-	tags: ['games'],
-
-	requireCredential: true as const,
-};
-
-// eslint-disable-next-line import/no-default-export
-export default define(meta, async (ps, user) => {
-	await ReversiMatchings.delete({
-		parentId: user.id,
-	});
-});
diff --git a/packages/backend/src/server/api/endpoints/get-online-users-count.ts b/packages/backend/src/server/api/endpoints/get-online-users-count.ts
index b429eacef3..5b13d5a3b8 100644
--- a/packages/backend/src/server/api/endpoints/get-online-users-count.ts
+++ b/packages/backend/src/server/api/endpoints/get-online-users-count.ts
@@ -6,11 +6,11 @@ import define from '../define';
 export const meta = {
 	tags: ['meta'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async () => {
diff --git a/packages/backend/src/server/api/endpoints/hashtags/list.ts b/packages/backend/src/server/api/endpoints/hashtags/list.ts
index 3900fdcc4a..9fa9b3edc6 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/list.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/list.ts
@@ -5,7 +5,7 @@ import { Hashtags } from '@/models/index';
 export const meta = {
 	tags: ['hashtags'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		limit: {
@@ -47,15 +47,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Hashtag',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts
index 8642bb39ff..0d646c64f5 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/search.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts
@@ -5,7 +5,7 @@ import { Hashtags } from '@/models/index';
 export const meta = {
 	tags: ['hashtags'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		limit: {
@@ -24,14 +24,14 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
+			type: 'string',
+			optional: false, nullable: false,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/hashtags/show.ts b/packages/backend/src/server/api/endpoints/hashtags/show.ts
index 454b98e5a4..242cef99d4 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/show.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/show.ts
@@ -7,7 +7,7 @@ import { normalizeForSearch } from '@/misc/normalize-for-search';
 export const meta = {
 	tags: ['hashtags'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		tag: {
@@ -16,8 +16,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'Hashtag',
 	},
 
@@ -28,7 +28,7 @@ export const meta = {
 			id: '110ee688-193e-4a3a-9ecf-c167b2e6981e',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts
index e02666a888..be964ad639 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts
@@ -23,35 +23,35 @@ const max = 5;
 export const meta = {
 	tags: ['hashtags'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			properties: {
 				tag: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 				},
 				chart: {
-					type: 'array' as const,
-					optional: false as const, nullable: false as const,
+					type: 'array',
+					optional: false, nullable: false,
 					items: {
-						type: 'number' as const,
-						optional: false as const, nullable: false as const,
+						type: 'number',
+						optional: false, nullable: false,
 					},
 				},
 				usersCount: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
+					type: 'number',
+					optional: false, nullable: false,
 				},
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async () => {
diff --git a/packages/backend/src/server/api/endpoints/hashtags/users.ts b/packages/backend/src/server/api/endpoints/hashtags/users.ts
index 6bbe87a0d5..2158dc4349 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/users.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/users.ts
@@ -4,7 +4,7 @@ import { Users } from '@/models/index';
 import { normalizeForSearch } from '@/misc/normalize-for-search';
 
 export const meta = {
-	requireCredential: false as const,
+	requireCredential: false,
 
 	tags: ['hashtags', 'users'],
 
@@ -48,15 +48,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User',
+			type: 'object',
+			optional: false, nullable: false,
+			ref: 'UserDetailed',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts
index e5a2c9d2f7..d69c118cfc 100644
--- a/packages/backend/src/server/api/endpoints/i.ts
+++ b/packages/backend/src/server/api/endpoints/i.ts
@@ -4,23 +4,23 @@ import { Users } from '@/models/index';
 export const meta = {
 	tags: ['account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	params: {},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User',
+		type: 'object',
+		optional: false, nullable: false,
+		ref: 'MeDetailed',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user, token) => {
 	const isSecure = token == null;
 
 	// ここで渡ってきている user はキャッシュされていて古い可能性もあるので id だけ渡す
-	return await Users.pack(user.id, user, {
+	return await Users.pack<true, true>(user.id, user, {
 		detail: true,
 		includeSecrets: isSecure,
 	});
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
index f6c5ac33aa..4853908693 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
@@ -4,7 +4,7 @@ import define from '../../../define';
 import { UserProfiles } from '@/models/index';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -13,7 +13,7 @@ export const meta = {
 			validator: $.str,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index cd1f16e548..271632d362 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -16,7 +16,7 @@ import { publishMainStream } from '@/services/stream';
 const cborDecodeFirst = promisify(cbor.decodeFirst) as any;
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -37,7 +37,7 @@ export const meta = {
 			validator: $.str,
 		},
 	},
-};
+} as const;
 
 const rpIdHashReal = hash(Buffer.from(config.hostname, 'utf-8'));
 
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
index 9eecbb3dc8..854848a434 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
@@ -3,7 +3,7 @@ import define from '../../../define';
 import { UserProfiles } from '@/models/index';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -12,7 +12,7 @@ export const meta = {
 			validator: $.boolean,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
index 19294d15a6..b6b0fd50b4 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
@@ -10,7 +10,7 @@ import { hash } from '../../../2fa';
 const randomBytes = promisify(crypto.randomBytes);
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -19,7 +19,7 @@ export const meta = {
 			validator: $.str,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
index 0fe824a6ef..c5cfb9dfad 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
@@ -7,7 +7,7 @@ import define from '../../../define';
 import { UserProfiles } from '@/models/index';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -16,7 +16,7 @@ export const meta = {
 			validator: $.str,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
index b9a6354238..03e1d0434d 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
@@ -5,7 +5,7 @@ import { UserProfiles, UserSecurityKeys, Users } from '@/models/index';
 import { publishMainStream } from '@/services/stream';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -17,7 +17,7 @@ export const meta = {
 			validator: $.str,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
index 0e7014d3f5..a19ad6810d 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
@@ -4,7 +4,7 @@ import define from '../../../define';
 import { UserProfiles } from '@/models/index';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -13,7 +13,7 @@ export const meta = {
 			validator: $.str,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts
index 6c71a071f2..63999b0981 100644
--- a/packages/backend/src/server/api/endpoints/i/apps.ts
+++ b/packages/backend/src/server/api/endpoints/i/apps.ts
@@ -3,7 +3,7 @@ import define from '../../define';
 import { AccessTokens } from '@/models/index';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -17,7 +17,7 @@ export const meta = {
 			]),
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts
index 127a272c44..52122b851b 100644
--- a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts
+++ b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts
@@ -3,7 +3,7 @@ import define from '../../define';
 import { AccessTokens, Apps } from '@/models/index';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -23,7 +23,7 @@ export const meta = {
 			default: 'desc',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts
index 5ba7a2a87e..7b6c137737 100644
--- a/packages/backend/src/server/api/endpoints/i/change-password.ts
+++ b/packages/backend/src/server/api/endpoints/i/change-password.ts
@@ -4,7 +4,7 @@ import define from '../../define';
 import { UserProfiles } from '@/models/index';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -17,7 +17,7 @@ export const meta = {
 			validator: $.str,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts
index bfbf2e5e5c..e1eee949fc 100644
--- a/packages/backend/src/server/api/endpoints/i/delete-account.ts
+++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts
@@ -7,7 +7,7 @@ import { publishUserEvent } from '@/services/stream';
 import { createDeleteAccountJob } from '@/queue';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -16,7 +16,7 @@ export const meta = {
 			validator: $.str,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/export-blocking.ts b/packages/backend/src/server/api/endpoints/i/export-blocking.ts
index 4f2143475f..44d8a1cb38 100644
--- a/packages/backend/src/server/api/endpoints/i/export-blocking.ts
+++ b/packages/backend/src/server/api/endpoints/i/export-blocking.ts
@@ -4,12 +4,12 @@ import ms from 'ms';
 
 export const meta = {
 	secure: true,
-	requireCredential: true as const,
+	requireCredential: true,
 	limit: {
 		duration: ms('1hour'),
 		max: 1,
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/export-following.ts b/packages/backend/src/server/api/endpoints/i/export-following.ts
index 4b2f4c86a9..5d1617d57b 100644
--- a/packages/backend/src/server/api/endpoints/i/export-following.ts
+++ b/packages/backend/src/server/api/endpoints/i/export-following.ts
@@ -5,7 +5,7 @@ import ms from 'ms';
 
 export const meta = {
 	secure: true,
-	requireCredential: true as const,
+	requireCredential: true,
 	limit: {
 		duration: ms('1hour'),
 		max: 1,
@@ -20,7 +20,7 @@ export const meta = {
 			default: false,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/export-mute.ts b/packages/backend/src/server/api/endpoints/i/export-mute.ts
index 1f655c5118..27ce8f0b29 100644
--- a/packages/backend/src/server/api/endpoints/i/export-mute.ts
+++ b/packages/backend/src/server/api/endpoints/i/export-mute.ts
@@ -4,12 +4,12 @@ import ms from 'ms';
 
 export const meta = {
 	secure: true,
-	requireCredential: true as const,
+	requireCredential: true,
 	limit: {
 		duration: ms('1hour'),
 		max: 1,
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/export-notes.ts b/packages/backend/src/server/api/endpoints/i/export-notes.ts
index aa3d7955fa..25b1849e80 100644
--- a/packages/backend/src/server/api/endpoints/i/export-notes.ts
+++ b/packages/backend/src/server/api/endpoints/i/export-notes.ts
@@ -4,12 +4,12 @@ import ms from 'ms';
 
 export const meta = {
 	secure: true,
-	requireCredential: true as const,
+	requireCredential: true,
 	limit: {
 		duration: ms('1day'),
 		max: 1,
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts b/packages/backend/src/server/api/endpoints/i/export-user-lists.ts
index e135ba1031..d28b699c5a 100644
--- a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts
+++ b/packages/backend/src/server/api/endpoints/i/export-user-lists.ts
@@ -4,12 +4,12 @@ import ms from 'ms';
 
 export const meta = {
 	secure: true,
-	requireCredential: true as const,
+	requireCredential: true,
 	limit: {
 		duration: ms('1min'),
 		max: 1,
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/favorites.ts b/packages/backend/src/server/api/endpoints/i/favorites.ts
index 2ae5aa87a7..92c767876b 100644
--- a/packages/backend/src/server/api/endpoints/i/favorites.ts
+++ b/packages/backend/src/server/api/endpoints/i/favorites.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
 export const meta = {
 	tags: ['account', 'notes', 'favorites'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:favorites',
 
@@ -27,15 +27,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'NoteFavorite',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts
index ebcb459c68..f1c5763593 100644
--- a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts
+++ b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../../common/make-pagination-query';
 export const meta = {
 	tags: ['account', 'gallery'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:gallery-likes',
 
@@ -27,22 +27,22 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			id: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				format: 'id',
 			},
 			page: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
+				type: 'object',
+				optional: false, nullable: false,
 				ref: 'GalleryPost',
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts
index bb77484dab..d46d42f633 100644
--- a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts
+++ b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../../common/make-pagination-query';
 export const meta = {
 	tags: ['account', 'gallery'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:gallery',
 
@@ -27,15 +27,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'GalleryPost',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts
index 93c8321166..4e1a4d3db9 100644
--- a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts
+++ b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts
@@ -4,7 +4,7 @@ import { MutedNotes } from '@/models/index';
 export const meta = {
 	tags: ['account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:account',
 
@@ -12,16 +12,16 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			count: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
+				type: 'number',
+				optional: false, nullable: false,
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
index 6b5100c21e..acc5797420 100644
--- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
@@ -8,7 +8,7 @@ import { DriveFiles } from '@/models/index';
 
 export const meta = {
 	secure: true,
-	requireCredential: true as const,
+	requireCredential: true,
 
 	limit: {
 		duration: ms('1hour'),
@@ -46,7 +46,7 @@ export const meta = {
 			id: '6f3a4dcc-f060-a707-4950-806fbdbe60d6',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts
index a0ab45b0a4..35006746fb 100644
--- a/packages/backend/src/server/api/endpoints/i/import-following.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-following.ts
@@ -8,7 +8,7 @@ import { DriveFiles } from '@/models/index';
 
 export const meta = {
 	secure: true,
-	requireCredential: true as const,
+	requireCredential: true,
 	limit: {
 		duration: ms('1hour'),
 		max: 1,
@@ -45,7 +45,7 @@ export const meta = {
 			id: '31a1b42c-06f7-42ae-8a38-a661c5c9f691',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts
index b5878f4f5d..7bbb2e008e 100644
--- a/packages/backend/src/server/api/endpoints/i/import-muting.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts
@@ -8,7 +8,7 @@ import { DriveFiles } from '@/models/index';
 
 export const meta = {
 	secure: true,
-	requireCredential: true as const,
+	requireCredential: true,
 
 	limit: {
 		duration: ms('1hour'),
@@ -46,7 +46,7 @@ export const meta = {
 			id: 'd2f12af1-e7b4-feac-86a3-519548f2728e',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
index 563ecf38e6..759d41b6cd 100644
--- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
@@ -8,7 +8,7 @@ import { DriveFiles } from '@/models/index';
 
 export const meta = {
 	secure: true,
-	requireCredential: true as const,
+	requireCredential: true,
 	limit: {
 		duration: ms('1hour'),
 		max: 1,
@@ -45,7 +45,7 @@ export const meta = {
 			id: '99efe367-ce6e-4d44-93f8-5fae7b040356',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts
index f02e94f328..59efd32bb2 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications.ts
@@ -12,7 +12,7 @@ import { Brackets } from 'typeorm';
 export const meta = {
 	tags: ['account', 'notifications'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:notifications',
 
@@ -55,15 +55,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Notification',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/page-likes.ts b/packages/backend/src/server/api/endpoints/i/page-likes.ts
index 7b8f4864fa..59239c7446 100644
--- a/packages/backend/src/server/api/endpoints/i/page-likes.ts
+++ b/packages/backend/src/server/api/endpoints/i/page-likes.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
 export const meta = {
 	tags: ['account', 'pages'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:page-likes',
 
@@ -27,22 +27,22 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			id: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				format: 'id',
 			},
 			page: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
+				type: 'object',
+				optional: false, nullable: false,
 				ref: 'Page',
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/pages.ts b/packages/backend/src/server/api/endpoints/i/pages.ts
index 78de9e2bfb..bef775d063 100644
--- a/packages/backend/src/server/api/endpoints/i/pages.ts
+++ b/packages/backend/src/server/api/endpoints/i/pages.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
 export const meta = {
 	tags: ['account', 'pages'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:pages',
 
@@ -27,15 +27,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Page',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/pin.ts b/packages/backend/src/server/api/endpoints/i/pin.ts
index 9fc73908ae..a940d1b99b 100644
--- a/packages/backend/src/server/api/endpoints/i/pin.ts
+++ b/packages/backend/src/server/api/endpoints/i/pin.ts
@@ -8,7 +8,7 @@ import { Users } from '@/models/index';
 export const meta = {
 	tags: ['account', 'notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -39,11 +39,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User',
+		type: 'object',
+		optional: false, nullable: false,
+		ref: 'MeDetailed',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
@@ -54,7 +54,7 @@ export default define(meta, async (ps, user) => {
 		throw e;
 	});
 
-	return await Users.pack(user.id, user, {
+	return await Users.pack<true, true>(user.id, user, {
 		detail: true,
 	});
 });
diff --git a/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts b/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts
index 26c1abced4..4e4fb3840f 100644
--- a/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts
+++ b/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts
@@ -5,13 +5,13 @@ import { MessagingMessages, UserGroupJoinings } from '@/models/index';
 export const meta = {
 	tags: ['account', 'messaging'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
 	params: {
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts
index 44c32a9ee1..99f17ddfc9 100644
--- a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts
+++ b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts
@@ -5,13 +5,13 @@ import { NoteUnreads } from '@/models/index';
 export const meta = {
 	tags: ['account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
 	params: {
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/read-announcement.ts b/packages/backend/src/server/api/endpoints/i/read-announcement.ts
index 5814c5c3c1..e9bb66264b 100644
--- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts
+++ b/packages/backend/src/server/api/endpoints/i/read-announcement.ts
@@ -9,7 +9,7 @@ import { publishMainStream } from '@/services/stream';
 export const meta = {
 	tags: ['account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -26,7 +26,7 @@ export const meta = {
 			id: '184663db-df88-4bc2-8b52-fb85f0681939',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
index 13dbd7bd5f..a20719363b 100644
--- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
+++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
@@ -6,7 +6,7 @@ import define from '../../define';
 import { Users, UserProfiles } from '@/models/index';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -15,7 +15,7 @@ export const meta = {
 			validator: $.str,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts
index 0f36d3f507..2941b441e2 100644
--- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts
+++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts
@@ -3,7 +3,7 @@ import define from '../../../define';
 import { RegistryItems } from '@/models/index';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -13,7 +13,7 @@ export const meta = {
 			default: [],
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts
index 36d8452074..51371353c9 100644
--- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts
+++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts
@@ -4,7 +4,7 @@ import { RegistryItems } from '@/models/index';
 import { ApiError } from '../../../error';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -26,7 +26,7 @@ export const meta = {
 			id: '97a1e8e7-c0f7-47d2-957a-92e61256e01a',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts
index e1b80035b6..ac617defb0 100644
--- a/packages/backend/src/server/api/endpoints/i/registry/get.ts
+++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts
@@ -4,7 +4,7 @@ import { RegistryItems } from '@/models/index';
 import { ApiError } from '../../../error';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -26,7 +26,7 @@ export const meta = {
 			id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts
index 53ff7b1369..0445922188 100644
--- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts
+++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts
@@ -3,7 +3,7 @@ import define from '../../../define';
 import { RegistryItems } from '@/models/index';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -13,7 +13,7 @@ export const meta = {
 			default: [],
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys.ts b/packages/backend/src/server/api/endpoints/i/registry/keys.ts
index 40ae094d55..a3c9d0e5ee 100644
--- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts
+++ b/packages/backend/src/server/api/endpoints/i/registry/keys.ts
@@ -3,7 +3,7 @@ import define from '../../../define';
 import { RegistryItems } from '@/models/index';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -13,7 +13,7 @@ export const meta = {
 			default: [],
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/registry/remove.ts b/packages/backend/src/server/api/endpoints/i/registry/remove.ts
index bea9be73e4..08185f224b 100644
--- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts
+++ b/packages/backend/src/server/api/endpoints/i/registry/remove.ts
@@ -4,7 +4,7 @@ import { RegistryItems } from '@/models/index';
 import { ApiError } from '../../../error';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -26,7 +26,7 @@ export const meta = {
 			id: '1fac4e8a-a6cd-4e39-a4a5-3a7e11f1b019',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts
index c7596f48f1..9de68ac6e8 100644
--- a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts
+++ b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts
@@ -2,13 +2,13 @@ import define from '../../../define';
 import { RegistryItems } from '@/models/index';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
 	params: {
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/registry/set.ts b/packages/backend/src/server/api/endpoints/i/registry/set.ts
index c373b1a215..27884046b4 100644
--- a/packages/backend/src/server/api/endpoints/i/registry/set.ts
+++ b/packages/backend/src/server/api/endpoints/i/registry/set.ts
@@ -5,7 +5,7 @@ import { RegistryItems } from '@/models/index';
 import { genId } from '@/misc/gen-id';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -23,7 +23,7 @@ export const meta = {
 			default: [],
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts
index acef3a58ed..51721c5b58 100644
--- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts
+++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts
@@ -5,7 +5,7 @@ import { ID } from '@/misc/cafy-id';
 import { publishUserEvent } from '@/services/stream';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -14,7 +14,7 @@ export const meta = {
 			validator: $.type(ID),
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/signin-history.ts b/packages/backend/src/server/api/endpoints/i/signin-history.ts
index deb1315477..796e2ec309 100644
--- a/packages/backend/src/server/api/endpoints/i/signin-history.ts
+++ b/packages/backend/src/server/api/endpoints/i/signin-history.ts
@@ -5,7 +5,7 @@ import { Signins } from '@/models/index';
 import { makePaginationQuery } from '../../common/make-pagination-query';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -23,7 +23,7 @@ export const meta = {
 			validator: $.optional.type(ID),
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/unpin.ts b/packages/backend/src/server/api/endpoints/i/unpin.ts
index 8182254ac1..9c82b74960 100644
--- a/packages/backend/src/server/api/endpoints/i/unpin.ts
+++ b/packages/backend/src/server/api/endpoints/i/unpin.ts
@@ -8,7 +8,7 @@ import { Users } from '@/models/index';
 export const meta = {
 	tags: ['account', 'notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -27,11 +27,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User',
+		type: 'object',
+		optional: false, nullable: false,
+		ref: 'MeDetailed',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
@@ -40,7 +40,7 @@ export default define(meta, async (ps, user) => {
 		throw e;
 	});
 
-	return await Users.pack(user.id, user, {
+	return await Users.pack<true, true>(user.id, user, {
 		detail: true,
 	});
 });
diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts
index 19bf802482..b4479aa50d 100644
--- a/packages/backend/src/server/api/endpoints/i/update-email.ts
+++ b/packages/backend/src/server/api/endpoints/i/update-email.ts
@@ -11,7 +11,7 @@ import { ApiError } from '../../error';
 import { validateEmailForAccount } from '@/services/validate-email-for-account';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -43,7 +43,7 @@ export const meta = {
 			id: 'a2defefb-f220-8849-0af6-17f816099323',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 3c6050efde..6b7e53aa1f 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -19,7 +19,7 @@ import { normalizeForSearch } from '@/misc/normalize-for-search';
 export const meta = {
 	tags: ['account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -162,11 +162,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User',
+		type: 'object',
+		optional: false, nullable: false,
+		ref: 'MeDetailed',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, _user, token) => {
@@ -279,7 +279,7 @@ export default define(meta, async (ps, _user, token) => {
 	if (Object.keys(updates).length > 0) await Users.update(user.id, updates);
 	if (Object.keys(profileUpdates).length > 0) await UserProfiles.update(user.id, profileUpdates);
 
-	const iObj = await Users.pack(user.id, user, {
+	const iObj = await Users.pack<true, true>(user.id, user, {
 		detail: true,
 		includeSecrets: isSecure,
 	});
diff --git a/packages/backend/src/server/api/endpoints/i/user-group-invites.ts b/packages/backend/src/server/api/endpoints/i/user-group-invites.ts
index cbe3f64a20..76a3131e6d 100644
--- a/packages/backend/src/server/api/endpoints/i/user-group-invites.ts
+++ b/packages/backend/src/server/api/endpoints/i/user-group-invites.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
 export const meta = {
 	tags: ['account', 'groups'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:user-groups',
 
@@ -27,26 +27,26 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			properties: {
 				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 					format: 'id',
 				},
 				group: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
+					type: 'object',
+					optional: false, nullable: false,
 					ref: 'UserGroup',
 				},
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/messaging/history.ts b/packages/backend/src/server/api/endpoints/messaging/history.ts
index ca5b1d06a3..5ac49cf96b 100644
--- a/packages/backend/src/server/api/endpoints/messaging/history.ts
+++ b/packages/backend/src/server/api/endpoints/messaging/history.ts
@@ -7,7 +7,7 @@ import { Brackets } from 'typeorm';
 export const meta = {
 	tags: ['messaging'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:messaging',
 
@@ -24,15 +24,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'MessagingMessage',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/messaging/messages.ts b/packages/backend/src/server/api/endpoints/messaging/messages.ts
index 9332695fdc..7dbddd80e2 100644
--- a/packages/backend/src/server/api/endpoints/messaging/messages.ts
+++ b/packages/backend/src/server/api/endpoints/messaging/messages.ts
@@ -11,7 +11,7 @@ import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivit
 export const meta = {
 	tags: ['messaging'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:messaging',
 
@@ -44,11 +44,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'MessagingMessage',
 		},
 	},
@@ -72,7 +72,7 @@ export const meta = {
 			id: 'a053a8dd-a491-4718-8f87-50775aad9284',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts
index 6092746676..5ec16f5e5a 100644
--- a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts
+++ b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts
@@ -11,7 +11,7 @@ import { createMessage } from '@/services/messages/create';
 export const meta = {
 	tags: ['messaging'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:messaging',
 
@@ -34,8 +34,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'MessagingMessage',
 	},
 
@@ -82,7 +82,7 @@ export const meta = {
 			id: 'c15a5199-7422-4968-941a-2a462c478f7d',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts b/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts
index 7362f705ef..2975419cef 100644
--- a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts
+++ b/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts
@@ -9,7 +9,7 @@ import { deleteMessage } from '@/services/messages/delete';
 export const meta = {
 	tags: ['messaging'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:messaging',
 
@@ -32,7 +32,7 @@ export const meta = {
 			id: '54b5b326-7925-42cf-8019-130fda8b56af',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/read.ts b/packages/backend/src/server/api/endpoints/messaging/messages/read.ts
index c698f39792..42c3f49f6f 100644
--- a/packages/backend/src/server/api/endpoints/messaging/messages/read.ts
+++ b/packages/backend/src/server/api/endpoints/messaging/messages/read.ts
@@ -8,7 +8,7 @@ import { readUserMessagingMessage, readGroupMessagingMessage } from '../../../co
 export const meta = {
 	tags: ['messaging'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:messaging',
 
@@ -25,7 +25,7 @@ export const meta = {
 			id: '86d56a2f-a9c3-4afb-b13c-3e9bfef9aa14',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts
index 6b07011fd8..693a7a04ec 100644
--- a/packages/backend/src/server/api/endpoints/meta.ts
+++ b/packages/backend/src/server/api/endpoints/meta.ts
@@ -9,7 +9,7 @@ import { MoreThan } from 'typeorm';
 export const meta = {
 	tags: ['meta'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		detail: {
@@ -19,434 +19,434 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			maintainerName: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 			},
 			maintainerEmail: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 			},
 			version: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				example: config.version,
 			},
 			name: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 			},
 			uri: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				format: 'url',
 				example: 'https://misskey.example.com',
 			},
 			description: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 			},
 			langs: {
-				type: 'array' as const,
-				optional: false as const, nullable: false as const,
+				type: 'array',
+				optional: false, nullable: false,
 				items: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 				},
 			},
 			tosUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 			},
 			repositoryUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				default: 'https://github.com/misskey-dev/misskey',
 			},
 			feedbackUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				default: 'https://github.com/misskey-dev/misskey/issues/new',
 			},
 			secure: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 				default: false,
 			},
 			disableRegistration: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			disableLocalTimeline: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			disableGlobalTimeline: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			driveCapacityPerLocalUserMb: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
+				type: 'number',
+				optional: false, nullable: false,
 			},
 			driveCapacityPerRemoteUserMb: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
+				type: 'number',
+				optional: false, nullable: false,
 			},
 			cacheRemoteFiles: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			proxyRemoteFiles: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			emailRequiredForSignup: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			enableHcaptcha: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			hcaptchaSiteKey: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 			},
 			enableRecaptcha: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			recaptchaSiteKey: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 			},
 			swPublickey: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 			},
 			mascotImageUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				default: '/assets/ai.png',
 			},
 			bannerUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 			},
 			errorImageUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				default: 'https://xn--931a.moe/aiart/yubitun.png',
 			},
 			iconUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 			},
 			maxNoteTextLength: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
+				type: 'number',
+				optional: false, nullable: false,
 				default: 500,
 			},
 			emojis: {
-				type: 'array' as const,
-				optional: false as const, nullable: false as const,
+				type: 'array',
+				optional: false, nullable: false,
 				items: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
+					type: 'object',
+					optional: false, nullable: false,
 					properties: {
 						id: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const,
+							type: 'string',
+							optional: false, nullable: false,
 							format: 'id',
 						},
 						aliases: {
-							type: 'array' as const,
-							optional: false as const, nullable: false as const,
+							type: 'array',
+							optional: false, nullable: false,
 							items: {
-								type: 'string' as const,
-								optional: false as const, nullable: false as const,
+								type: 'string',
+								optional: false, nullable: false,
 							},
 						},
 						category: {
-							type: 'string' as const,
-							optional: false as const, nullable: true as const,
+							type: 'string',
+							optional: false, nullable: true,
 						},
 						host: {
-							type: 'string' as const,
-							optional: false as const, nullable: true as const,
+							type: 'string',
+							optional: false, nullable: true,
 						},
 						url: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const,
+							type: 'string',
+							optional: false, nullable: false,
 							format: 'url',
 						},
 					},
 				},
 			},
 			ads: {
-				type: 'array' as const,
-				optional: false as const, nullable: false as const,
+				type: 'array',
+				optional: false, nullable: false,
 				items: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
+					type: 'object',
+					optional: false, nullable: false,
 					properties: {
 						place: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const,
+							type: 'string',
+							optional: false, nullable: false,
 						},
 						url: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const,
+							type: 'string',
+							optional: false, nullable: false,
 							format: 'url',
 						},
 						imageUrl: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const,
+							type: 'string',
+							optional: false, nullable: false,
 							format: 'url',
 						},
 					},
 				},
 			},
 			requireSetup: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 				example: false,
 			},
 			enableEmail: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			enableTwitterIntegration: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			enableGithubIntegration: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			enableDiscordIntegration: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			enableServiceWorker: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			translatorAvailable: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			proxyAccountName: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
+				type: 'string',
+				optional: false, nullable: true,
 			},
 			features: {
-				type: 'object' as const,
-				optional: true as const, nullable: false as const,
+				type: 'object',
+				optional: true, nullable: false,
 				properties: {
 					registration: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 					localTimeLine: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 					globalTimeLine: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 					elasticsearch: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 					hcaptcha: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 					recaptcha: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 					objectStorage: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 					twitter: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 					github: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 					discord: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 					serviceWorker: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 					miauth: {
-						type: 'boolean' as const,
-						optional: true as const, nullable: false as const,
+						type: 'boolean',
+						optional: true, nullable: false,
 						default: true,
 					},
 				},
 			},
 			userStarForReactionFallback: {
-				type: 'boolean' as const,
-				optional: true as const, nullable: false as const,
+				type: 'boolean',
+				optional: true, nullable: false,
 			},
 			pinnedUsers: {
-				type: 'array' as const,
-				optional: true as const, nullable: false as const,
+				type: 'array',
+				optional: true, nullable: false,
 				items: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 				},
 			},
 			hiddenTags: {
-				type: 'array' as const,
-				optional: true as const, nullable: false as const,
+				type: 'array',
+				optional: true, nullable: false,
 				items: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 				},
 			},
 			blockedHosts: {
-				type: 'array' as const,
-				optional: true as const, nullable: false as const,
+				type: 'array',
+				optional: true, nullable: false,
 				items: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 				},
 			},
 			hcaptchaSecretKey: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			recaptchaSecretKey: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			proxyAccountId: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 				format: 'id',
 			},
 			twitterConsumerKey: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			twitterConsumerSecret: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			githubClientId: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			githubClientSecret: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			discordClientId: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			discordClientSecret: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			summaryProxy: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			email: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			smtpSecure: {
-				type: 'boolean' as const,
-				optional: true as const, nullable: false as const,
+				type: 'boolean',
+				optional: true, nullable: false,
 			},
 			smtpHost: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			smtpPort: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			smtpUser: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			smtpPass: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			swPrivateKey: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			useObjectStorage: {
-				type: 'boolean' as const,
-				optional: true as const, nullable: false as const,
+				type: 'boolean',
+				optional: true, nullable: false,
 			},
 			objectStorageBaseUrl: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			objectStorageBucket: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			objectStoragePrefix: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			objectStorageEndpoint: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			objectStorageRegion: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			objectStoragePort: {
-				type: 'number' as const,
-				optional: true as const, nullable: true as const,
+				type: 'number',
+				optional: true, nullable: true,
 			},
 			objectStorageAccessKey: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			objectStorageSecretKey: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
+				type: 'string',
+				optional: true, nullable: true,
 			},
 			objectStorageUseSSL: {
-				type: 'boolean' as const,
-				optional: true as const, nullable: false as const,
+				type: 'boolean',
+				optional: true, nullable: false,
 			},
 			objectStorageUseProxy: {
-				type: 'boolean' as const,
-				optional: true as const, nullable: false as const,
+				type: 'boolean',
+				optional: true, nullable: false,
 			},
 			objectStorageSetPublicRead: {
-				type: 'boolean' as const,
-				optional: true as const, nullable: false as const,
+				type: 'boolean',
+				optional: true, nullable: false,
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts
index cc739a51e3..158c8877e9 100644
--- a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts
+++ b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts
@@ -7,7 +7,7 @@ import { secureRndstr } from '@/misc/secure-rndstr';
 export const meta = {
 	tags: ['auth'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	secure: true,
 
@@ -34,16 +34,16 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			token: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts
index 000e48ddea..6ba5a453c3 100644
--- a/packages/backend/src/server/api/endpoints/mute/create.ts
+++ b/packages/backend/src/server/api/endpoints/mute/create.ts
@@ -11,7 +11,7 @@ import { publishUserEvent } from '@/services/stream';
 export const meta = {
 	tags: ['account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:mutes',
 
@@ -40,7 +40,7 @@ export const meta = {
 			id: '7e7359cb-160c-4956-b08f-4d1c653cd007',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/mute/delete.ts b/packages/backend/src/server/api/endpoints/mute/delete.ts
index c204add1bd..21948dc3d1 100644
--- a/packages/backend/src/server/api/endpoints/mute/delete.ts
+++ b/packages/backend/src/server/api/endpoints/mute/delete.ts
@@ -9,7 +9,7 @@ import { publishUserEvent } from '@/services/stream';
 export const meta = {
 	tags: ['account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:mutes',
 
@@ -38,7 +38,7 @@ export const meta = {
 			id: '5467d020-daa9-4553-81e1-135c0c35a96d',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/mute/list.ts b/packages/backend/src/server/api/endpoints/mute/list.ts
index a736161b9c..4c6a81b63c 100644
--- a/packages/backend/src/server/api/endpoints/mute/list.ts
+++ b/packages/backend/src/server/api/endpoints/mute/list.ts
@@ -7,7 +7,7 @@ import { Mutings } from '@/models/index';
 export const meta = {
 	tags: ['account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:mutes',
 
@@ -27,15 +27,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Muting',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/my/apps.ts b/packages/backend/src/server/api/endpoints/my/apps.ts
index d9443d38d9..42bd5c5f75 100644
--- a/packages/backend/src/server/api/endpoints/my/apps.ts
+++ b/packages/backend/src/server/api/endpoints/my/apps.ts
@@ -5,7 +5,7 @@ import { Apps } from '@/models/index';
 export const meta = {
 	tags: ['account', 'app'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	params: {
 		limit: {
@@ -20,54 +20,54 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			properties: {
 				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 				},
 				name: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 				},
 				callbackUrl: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
+					type: 'string',
+					optional: false, nullable: false,
 				},
 				permission: {
-					type: 'array' as const,
-					optional: false as const, nullable: false as const,
+					type: 'array',
+					optional: false, nullable: false,
 					items: {
-						type: 'string' as const,
-						optional: false as const, nullable: false as const,
+						type: 'string',
+						optional: false, nullable: false,
 					},
 				},
 				secret: {
-					type: 'string' as const,
-					optional: true as const, nullable: false as const,
+					type: 'string',
+					optional: true, nullable: false,
 				},
 				isAuthorized: {
-					type: 'object' as const,
-					optional: true as const, nullable: false as const,
+					type: 'object',
+					optional: true, nullable: false,
 					properties: {
 						appId: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const,
+							type: 'string',
+							optional: false, nullable: false,
 						},
 						userId: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const,
+							type: 'string',
+							optional: false, nullable: false,
 						},
 					},
 				},
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts
index c853996a48..9edc6cb11c 100644
--- a/packages/backend/src/server/api/endpoints/notes.ts
+++ b/packages/backend/src/server/api/endpoints/notes.ts
@@ -43,15 +43,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts
index d6062f4ebb..088ef65e96 100644
--- a/packages/backend/src/server/api/endpoints/notes/children.ts
+++ b/packages/backend/src/server/api/endpoints/notes/children.ts
@@ -12,7 +12,7 @@ import { generateMutedInstanceQuery } from '../../common/generate-muted-instance
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		noteId: {
@@ -34,15 +34,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts
index 67bac3670b..b89c6db4a8 100644
--- a/packages/backend/src/server/api/endpoints/notes/clips.ts
+++ b/packages/backend/src/server/api/endpoints/notes/clips.ts
@@ -9,7 +9,7 @@ import { In } from 'typeorm';
 export const meta = {
 	tags: ['clips', 'notes'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		noteId: {
@@ -18,12 +18,12 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
+			type: 'object',
+			optional: false, nullable: false,
+			ref: 'Clip',
 		},
 	},
 
@@ -34,7 +34,7 @@ export const meta = {
 			id: '47db1a1c-b0af-458d-8fb4-986e4efafe1e',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts
index a465b08d3a..4bd89c32e7 100644
--- a/packages/backend/src/server/api/endpoints/notes/conversation.ts
+++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts
@@ -9,7 +9,7 @@ import { Notes } from '@/models/index';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		noteId: {
@@ -28,11 +28,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
@@ -44,7 +44,7 @@ export const meta = {
 			id: 'e1035875-9551-45ec-afa8-1ded1fcb53c8',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index ec05e5ea94..4efa76b248 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -25,7 +25,7 @@ setInterval(() => {
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	limit: {
 		duration: ms('1hour'),
@@ -48,7 +48,7 @@ export const meta = {
 			validator: $.optional.nullable.str.pipe(text =>
 				text.trim() != ''
 					&& length(text.trim()) <= maxNoteTextLength
-					&& Array.from(text.trim()).length <= DB_MAX_NOTE_TEXT_LENGTH	// DB limit
+					&& Array.from(text.trim()).length <= DB_MAX_NOTE_TEXT_LENGTH,	// DB limit
 			),
 			default: null,
 		},
@@ -78,11 +78,11 @@ export const meta = {
 		},
 
 		fileIds: {
-			validator: $.optional.arr($.type(ID)).unique().range(1, 4),
+			validator: $.optional.arr($.type(ID)).unique().range(1, 16),
 		},
 
 		mediaIds: {
-			validator: $.optional.arr($.type(ID)).unique().range(1, 4),
+			validator: $.optional.arr($.type(ID)).unique().range(1, 16),
 			deprecated: true,
 		},
 
@@ -113,12 +113,12 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			createdNote: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
+				type: 'object',
+				optional: false, nullable: false,
 				ref: 'Note',
 			},
 		},
@@ -173,7 +173,7 @@ export const meta = {
 			id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts
index 0f6f99c7c1..9e080d9e99 100644
--- a/packages/backend/src/server/api/endpoints/notes/delete.ts
+++ b/packages/backend/src/server/api/endpoints/notes/delete.ts
@@ -10,7 +10,7 @@ import { Users } from '@/models/index';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:notes',
 
@@ -39,7 +39,7 @@ export const meta = {
 			id: 'fe8d7103-0ea8-4ec3-814d-f8b401dc69e9',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts
index d5d39c3040..78da6a3b00 100644
--- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts
@@ -9,7 +9,7 @@ import { genId } from '@/misc/gen-id';
 export const meta = {
 	tags: ['notes', 'favorites'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:favorites',
 
@@ -32,7 +32,7 @@ export const meta = {
 			id: 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
index 62f32633b3..3f3d50f0d5 100644
--- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
+++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
@@ -8,7 +8,7 @@ import { NoteFavorites } from '@/models/index';
 export const meta = {
 	tags: ['notes', 'favorites'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:favorites',
 
@@ -31,7 +31,7 @@ export const meta = {
 			id: 'b625fc69-635e-45e9-86f4-dbefbef35af5',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts
index 8f45b541dc..5a47fb9e08 100644
--- a/packages/backend/src/server/api/endpoints/notes/featured.ts
+++ b/packages/backend/src/server/api/endpoints/notes/featured.ts
@@ -7,7 +7,7 @@ import { generateBlockedUserQuery } from '../../common/generate-block-query';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		limit: {
@@ -22,15 +22,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
index 14831a127c..cac8b7d8a9 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -43,11 +43,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
@@ -59,7 +59,7 @@ export const meta = {
 			id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6b',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
index bac2437056..9683df4611 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -18,7 +18,7 @@ import { generateBlockedUserQuery } from '../../common/generate-block-query';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	params: {
 		limit: {
@@ -63,11 +63,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
@@ -79,7 +79,7 @@ export const meta = {
 			id: '620763f4-f621-4533-ab33-0577a1a3c342',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
index 334e754d0d..7776644124 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -54,11 +54,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
@@ -70,7 +70,7 @@ export const meta = {
 			id: '45a6eb02-7695-4393-b023-dd3be9aaaefd',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts
index 8ae90233a9..81b3844365 100644
--- a/packages/backend/src/server/api/endpoints/notes/mentions.ts
+++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts
@@ -13,7 +13,7 @@ import { generateMutedNoteThreadQuery } from '../../common/generate-muted-note-t
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	params: {
 		following: {
@@ -40,15 +40,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
index 7562c6d7f8..79b558e65e 100644
--- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
+++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
@@ -6,7 +6,7 @@ import { Brackets, In } from 'typeorm';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	params: {
 		limit: {
@@ -21,15 +21,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts
index a08709e329..77387cacb2 100644
--- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts
+++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts
@@ -17,7 +17,7 @@ import { genId } from '@/misc/gen-id';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:votes',
 
@@ -68,7 +68,7 @@ export const meta = {
 			id: '85a5377e-b1e9-4617-b0b9-5bea73331e49',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts
index d29893e203..5205a78171 100644
--- a/packages/backend/src/server/api/endpoints/notes/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts
@@ -10,7 +10,7 @@ import { NoteReaction } from '@/models/entities/note-reaction';
 export const meta = {
 	tags: ['notes', 'reactions'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		noteId: {
@@ -41,11 +41,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'NoteReaction',
 		},
 	},
@@ -57,7 +57,7 @@ export const meta = {
 			id: '263fff3d-d0e1-4af4-bea7-8408059b451a',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
index 28403d821f..1b42781ceb 100644
--- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
@@ -8,7 +8,7 @@ import { ApiError } from '../../../error';
 export const meta = {
 	tags: ['reactions', 'notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:reactions',
 
@@ -41,7 +41,7 @@ export const meta = {
 			id: '20ef5475-9f38-4e4c-bd33-de6d979498ec',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts
index 2b1ce1e21e..1d686b5971 100644
--- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts
+++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts
@@ -9,7 +9,7 @@ import { ApiError } from '../../../error';
 export const meta = {
 	tags: ['reactions', 'notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:reactions',
 
@@ -38,7 +38,7 @@ export const meta = {
 			id: '92f4426d-4196-4125-aa5b-02943e2ec8fc',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts
index abfd928db1..f71d23146a 100644
--- a/packages/backend/src/server/api/endpoints/notes/renotes.ts
+++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts
@@ -12,7 +12,7 @@ import { generateBlockedUserQuery } from '../../common/generate-block-query';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		noteId: {
@@ -34,11 +34,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
@@ -50,7 +50,7 @@ export const meta = {
 			id: '12908022-2e21-46cd-ba6a-3edaf6093f46',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts
index 12efe94be1..62c56534e1 100644
--- a/packages/backend/src/server/api/endpoints/notes/replies.ts
+++ b/packages/backend/src/server/api/endpoints/notes/replies.ts
@@ -10,7 +10,7 @@ import { generateBlockedUserQuery } from '../../common/generate-block-query';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		noteId: {
@@ -32,15 +32,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
index 85daf7e48e..87eaffe2f1 100644
--- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
@@ -56,15 +56,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts
index 5ff3258954..e75212b14b 100644
--- a/packages/backend/src/server/api/endpoints/notes/search.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search.ts
@@ -13,7 +13,7 @@ import { generateBlockedUserQuery } from '../../common/generate-block-query';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		query: {
@@ -50,18 +50,18 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
 
 	errors: {
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts
index b645f86793..feb94be1a1 100644
--- a/packages/backend/src/server/api/endpoints/notes/show.ts
+++ b/packages/backend/src/server/api/endpoints/notes/show.ts
@@ -8,7 +8,7 @@ import { Notes } from '@/models/index';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		noteId: {
@@ -17,8 +17,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'Note',
 	},
 
@@ -29,7 +29,7 @@ export const meta = {
 			id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts
index 5bbe3ec86d..c3e9090bbf 100644
--- a/packages/backend/src/server/api/endpoints/notes/state.ts
+++ b/packages/backend/src/server/api/endpoints/notes/state.ts
@@ -6,7 +6,7 @@ import { NoteFavorites, Notes, NoteThreadMutings, NoteWatchings } from '@/models
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	params: {
 		noteId: {
@@ -15,24 +15,24 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			isFavorited: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			isWatching: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 			isMutedThread: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts
index 0bd55f8988..a8b50d90f6 100644
--- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts
@@ -10,7 +10,7 @@ import readNote from '@/services/note/read';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -27,7 +27,7 @@ export const meta = {
 			id: '5ff67ada-ed3b-2e71-8e87-a1a421e177d2',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts
index 756f1b9fd4..f76b526ce1 100644
--- a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts
+++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts
@@ -8,7 +8,7 @@ import { NoteThreadMutings } from '@/models';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -25,7 +25,7 @@ export const meta = {
 			id: 'bddd57ac-ceb3-b29d-4334-86ea5fae481a',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index 4acfed35e9..8be2861aec 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -16,7 +16,7 @@ import { generateBlockedUserQuery } from '../../common/generate-block-query';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	params: {
 		limit: {
@@ -61,15 +61,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts
index 98a45ace18..ed069cb75a 100644
--- a/packages/backend/src/server/api/endpoints/notes/translate.ts
+++ b/packages/backend/src/server/api/endpoints/notes/translate.ts
@@ -13,7 +13,7 @@ import { Notes } from '@/models';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		noteId: {
@@ -25,8 +25,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 	},
 
 	errors: {
@@ -36,7 +36,7 @@ export const meta = {
 			id: 'bea9b03f-36e0-49c5-a4db-627a029f8971',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts
index 2c1e1c7390..8db543d328 100644
--- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts
+++ b/packages/backend/src/server/api/endpoints/notes/unrenote.ts
@@ -10,7 +10,7 @@ import { Notes, Users } from '@/models/index';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:notes',
 
@@ -33,7 +33,7 @@ export const meta = {
 			id: 'efd4a259-2442-496b-8dd7-b255aa1a160f',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
index c3b273937c..89de73fb9d 100644
--- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -11,7 +11,7 @@ import { Brackets } from 'typeorm';
 export const meta = {
 	tags: ['notes', 'lists'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	params: {
 		listId: {
@@ -60,11 +60,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
@@ -76,7 +76,7 @@ export const meta = {
 			id: '8fb1fbd5-e476-4c37-9fb0-43d55b63a2ff',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/watching/create.ts b/packages/backend/src/server/api/endpoints/notes/watching/create.ts
index d55ae20023..6433c6bc2a 100644
--- a/packages/backend/src/server/api/endpoints/notes/watching/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/watching/create.ts
@@ -8,7 +8,7 @@ import { ApiError } from '../../../error';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -25,7 +25,7 @@ export const meta = {
 			id: 'ea0e37a6-90a3-4f58-ba6b-c328ca206fc7',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts b/packages/backend/src/server/api/endpoints/notes/watching/delete.ts
index 522ae6360a..3e9faa2b23 100644
--- a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts
+++ b/packages/backend/src/server/api/endpoints/notes/watching/delete.ts
@@ -8,7 +8,7 @@ import { ApiError } from '../../../error';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -25,7 +25,7 @@ export const meta = {
 			id: '09b3695c-f72c-4731-a428-7cff825fc82e',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notifications/create.ts b/packages/backend/src/server/api/endpoints/notifications/create.ts
index f78c6e1219..bd8a7ba1b7 100644
--- a/packages/backend/src/server/api/endpoints/notifications/create.ts
+++ b/packages/backend/src/server/api/endpoints/notifications/create.ts
@@ -5,7 +5,7 @@ import { createNotification } from '@/services/create-notification';
 export const meta = {
 	tags: ['notifications'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:notifications',
 
@@ -25,7 +25,7 @@ export const meta = {
 
 	errors: {
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user, token) => {
diff --git a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts
index f14a91a3dc..4cec38a95d 100644
--- a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts
+++ b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts
@@ -5,10 +5,10 @@ import { Notifications } from '@/models/index';
 export const meta = {
 	tags: ['notifications', 'account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:notifications',
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/notifications/read.ts b/packages/backend/src/server/api/endpoints/notifications/read.ts
index 8cb5cdbedd..95bac775f3 100644
--- a/packages/backend/src/server/api/endpoints/notifications/read.ts
+++ b/packages/backend/src/server/api/endpoints/notifications/read.ts
@@ -12,7 +12,7 @@ export const meta = {
 
 	tags: ['notifications', 'account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:notifications',
 
@@ -33,7 +33,7 @@ export const meta = {
 			}
 		}
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/page-push.ts b/packages/backend/src/server/api/endpoints/page-push.ts
index f5da35e783..61c0160f83 100644
--- a/packages/backend/src/server/api/endpoints/page-push.ts
+++ b/packages/backend/src/server/api/endpoints/page-push.ts
@@ -6,7 +6,7 @@ import { Users, Pages } from '@/models/index';
 import { ApiError } from '../error';
 
 export const meta = {
-	requireCredential: true as const,
+	requireCredential: true,
 	secure: true,
 
 	params: {
@@ -30,7 +30,7 @@ export const meta = {
 			id: '4a13ad31-6729-46b4-b9af-e86b265c2e74',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts
index dcf916909d..7ee50fbdfa 100644
--- a/packages/backend/src/server/api/endpoints/pages/create.ts
+++ b/packages/backend/src/server/api/endpoints/pages/create.ts
@@ -10,7 +10,7 @@ import { ApiError } from '../../error';
 export const meta = {
 	tags: ['pages'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:pages',
 
@@ -65,8 +65,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'Page',
 	},
 
@@ -82,7 +82,7 @@ export const meta = {
 			id: '4650348e-301c-499a-83c9-6aa988c66bc1',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts
index b3a37446d2..aeda823e52 100644
--- a/packages/backend/src/server/api/endpoints/pages/delete.ts
+++ b/packages/backend/src/server/api/endpoints/pages/delete.ts
@@ -7,7 +7,7 @@ import { ID } from '@/misc/cafy-id';
 export const meta = {
 	tags: ['pages'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:pages',
 
@@ -30,7 +30,7 @@ export const meta = {
 			id: '8b741b3e-2c22-44b3-a15f-29949aa1601e',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/pages/featured.ts b/packages/backend/src/server/api/endpoints/pages/featured.ts
index b32964c49d..7f0d58b350 100644
--- a/packages/backend/src/server/api/endpoints/pages/featured.ts
+++ b/packages/backend/src/server/api/endpoints/pages/featured.ts
@@ -4,18 +4,18 @@ import { Pages } from '@/models/index';
 export const meta = {
 	tags: ['pages'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Page',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts
index deff8cc021..c479f637a9 100644
--- a/packages/backend/src/server/api/endpoints/pages/like.ts
+++ b/packages/backend/src/server/api/endpoints/pages/like.ts
@@ -8,7 +8,7 @@ import { genId } from '@/misc/gen-id';
 export const meta = {
 	tags: ['pages'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:page-likes',
 
@@ -37,7 +37,7 @@ export const meta = {
 			id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts
index 4a98ec0736..5cda5386d5 100644
--- a/packages/backend/src/server/api/endpoints/pages/show.ts
+++ b/packages/backend/src/server/api/endpoints/pages/show.ts
@@ -8,7 +8,7 @@ import { Page } from '@/models/entities/page';
 export const meta = {
 	tags: ['pages'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		pageId: {
@@ -25,8 +25,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'Page',
 	},
 
@@ -37,7 +37,7 @@ export const meta = {
 			id: '222120c0-3ead-4528-811b-b96f233388d7',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/pages/unlike.ts b/packages/backend/src/server/api/endpoints/pages/unlike.ts
index f6e74dcf1a..cca5e5b5a9 100644
--- a/packages/backend/src/server/api/endpoints/pages/unlike.ts
+++ b/packages/backend/src/server/api/endpoints/pages/unlike.ts
@@ -7,7 +7,7 @@ import { Pages, PageLikes } from '@/models/index';
 export const meta = {
 	tags: ['pages'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:page-likes',
 
@@ -30,7 +30,7 @@ export const meta = {
 			id: 'f5e586b0-ce93-4050-b0e3-7f31af5259ee',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts
index 06c63706ae..991085ee09 100644
--- a/packages/backend/src/server/api/endpoints/pages/update.ts
+++ b/packages/backend/src/server/api/endpoints/pages/update.ts
@@ -9,7 +9,7 @@ import { Not } from 'typeorm';
 export const meta = {
 	tags: ['pages'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:pages',
 
@@ -88,7 +88,7 @@ export const meta = {
 			id: '2298a392-d4a1-44c5-9ebb-ac1aeaa5a9ab',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/ping.ts b/packages/backend/src/server/api/endpoints/ping.ts
index 2130ce54e0..3eab70ae2e 100644
--- a/packages/backend/src/server/api/endpoints/ping.ts
+++ b/packages/backend/src/server/api/endpoints/ping.ts
@@ -1,7 +1,7 @@
 import define from '../define';
 
 export const meta = {
-	requireCredential: false as const,
+	requireCredential: false,
 
 	tags: ['meta'],
 
@@ -9,16 +9,16 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			pong: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
+				type: 'number',
+				optional: false, nullable: false,
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async () => {
diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts
index 548871a02c..ff0e22555f 100644
--- a/packages/backend/src/server/api/endpoints/pinned-users.ts
+++ b/packages/backend/src/server/api/endpoints/pinned-users.ts
@@ -7,21 +7,21 @@ import { User } from '@/models/entities/user';
 export const meta = {
 	tags: ['users'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User',
+			type: 'object',
+			optional: false, nullable: false,
+			ref: 'UserDetailed',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts
index 271d887899..8d8c60d755 100644
--- a/packages/backend/src/server/api/endpoints/promo/read.ts
+++ b/packages/backend/src/server/api/endpoints/promo/read.ts
@@ -9,7 +9,7 @@ import { genId } from '@/misc/gen-id';
 export const meta = {
 	tags: ['notes'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	params: {
 		noteId: {
@@ -24,7 +24,7 @@ export const meta = {
 			id: 'd785b897-fcd3-4fe9-8fc3-b85c26e6c932',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/request-reset-password.ts b/packages/backend/src/server/api/endpoints/request-reset-password.ts
index 2a29e58914..af1aeb4311 100644
--- a/packages/backend/src/server/api/endpoints/request-reset-password.ts
+++ b/packages/backend/src/server/api/endpoints/request-reset-password.ts
@@ -11,7 +11,7 @@ import { genId } from '@/misc/gen-id';
 import { IsNull } from 'typeorm';
 
 export const meta = {
-	requireCredential: false as const,
+	requireCredential: false,
 
 	limit: {
 		duration: ms('1hour'),
@@ -31,7 +31,7 @@ export const meta = {
 	errors: {
 
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts
index caae137d76..e99dc9db15 100644
--- a/packages/backend/src/server/api/endpoints/reset-db.ts
+++ b/packages/backend/src/server/api/endpoints/reset-db.ts
@@ -4,7 +4,7 @@ import { ApiError } from '../error';
 import { resetDb } from '@/db/postgre';
 
 export const meta = {
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 	},
@@ -12,7 +12,7 @@ export const meta = {
 	errors: {
 
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts
index 3cf89f3758..a7366584b1 100644
--- a/packages/backend/src/server/api/endpoints/reset-password.ts
+++ b/packages/backend/src/server/api/endpoints/reset-password.ts
@@ -6,7 +6,7 @@ import { Users, UserProfiles, PasswordResetRequests } from '@/models/index';
 import { ApiError } from '../error';
 
 export const meta = {
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		token: {
@@ -21,7 +21,7 @@ export const meta = {
 	errors: {
 
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts
index 2613416ee3..1ad2c54ab5 100644
--- a/packages/backend/src/server/api/endpoints/server-info.ts
+++ b/packages/backend/src/server/api/endpoints/server-info.ts
@@ -3,7 +3,7 @@ import * as si from 'systeminformation';
 import define from '../define';
 
 export const meta = {
-	requireCredential: false as const,
+	requireCredential: false,
 
 	desc: {
 	},
@@ -12,7 +12,7 @@ export const meta = {
 
 	params: {
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async () => {
diff --git a/packages/backend/src/server/api/endpoints/stats.ts b/packages/backend/src/server/api/endpoints/stats.ts
index 75f7a94985..9879ef2adf 100644
--- a/packages/backend/src/server/api/endpoints/stats.ts
+++ b/packages/backend/src/server/api/endpoints/stats.ts
@@ -3,7 +3,7 @@ import { NoteReactions, Notes, Users } from '@/models/index';
 import { federationChart, driveChart } from '@/services/chart/index';
 
 export const meta = {
-	requireCredential: false as const,
+	requireCredential: false,
 
 	tags: ['meta'],
 
@@ -11,40 +11,40 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			notesCount: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
+				type: 'number',
+				optional: false, nullable: false,
 			},
 			originalNotesCount: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
+				type: 'number',
+				optional: false, nullable: false,
 			},
 			usersCount: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
+				type: 'number',
+				optional: false, nullable: false,
 			},
 			originalUsersCount: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
+				type: 'number',
+				optional: false, nullable: false,
 			},
 			instances: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
+				type: 'number',
+				optional: false, nullable: false,
 			},
 			driveUsageLocal: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
+				type: 'number',
+				optional: false, nullable: false,
 			},
 			driveUsageRemote: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
+				type: 'number',
+				optional: false, nullable: false,
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async () => {
diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts
index 8657a0b77c..ae3e9ce77a 100644
--- a/packages/backend/src/server/api/endpoints/sw/register.ts
+++ b/packages/backend/src/server/api/endpoints/sw/register.ts
@@ -7,7 +7,7 @@ import { SwSubscriptions } from '@/models/index';
 export const meta = {
 	tags: ['account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	params: {
 		endpoint: {
@@ -24,21 +24,21 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			state: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 				enum: ['already-subscribed', 'subscribed'],
 			},
 			key: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
+				type: 'string',
+				optional: false, nullable: false,
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/sw/unregister.ts b/packages/backend/src/server/api/endpoints/sw/unregister.ts
index 67a2ed0a96..6f569e9417 100644
--- a/packages/backend/src/server/api/endpoints/sw/unregister.ts
+++ b/packages/backend/src/server/api/endpoints/sw/unregister.ts
@@ -5,14 +5,14 @@ import { SwSubscriptions } from '../../../../models';
 export const meta = {
 	tags: ['account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	params: {
 		endpoint: {
 			validator: $.str,
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts
index ede03eeb10..74120fc406 100644
--- a/packages/backend/src/server/api/endpoints/username/available.ts
+++ b/packages/backend/src/server/api/endpoints/username/available.ts
@@ -5,7 +5,7 @@ import { Users, UsedUsernames } from '@/models/index';
 export const meta = {
 	tags: ['users'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		username: {
@@ -14,16 +14,16 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		properties: {
 			available: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
+				type: 'boolean',
+				optional: false, nullable: false,
 			},
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps) => {
diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts
index fdcf59f777..6b11ec0f01 100644
--- a/packages/backend/src/server/api/endpoints/users.ts
+++ b/packages/backend/src/server/api/endpoints/users.ts
@@ -7,7 +7,7 @@ import { generateBlockQueryForUsers } from '../common/generate-block-query';
 export const meta = {
 	tags: ['users'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		limit: {
@@ -53,15 +53,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User',
+			type: 'object',
+			optional: false, nullable: false,
+			ref: 'UserDetailed',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts
index 91da853e6e..d4152fbf50 100644
--- a/packages/backend/src/server/api/endpoints/users/clips.ts
+++ b/packages/backend/src/server/api/endpoints/users/clips.ts
@@ -25,7 +25,7 @@ export const meta = {
 			validator: $.optional.type(ID),
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts
index d9c0ed7623..6214ab40ba 100644
--- a/packages/backend/src/server/api/endpoints/users/followers.ts
+++ b/packages/backend/src/server/api/endpoints/users/followers.ts
@@ -9,7 +9,7 @@ import { toPunyNullable } from '@/misc/convert-host';
 export const meta = {
 	tags: ['users'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		userId: {
@@ -39,11 +39,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Following',
 		},
 	},
@@ -61,7 +61,7 @@ export const meta = {
 			id: '3c6a84db-d619-26af-ca14-06232a21df8a',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts
index a77fedd68e..76112eab25 100644
--- a/packages/backend/src/server/api/endpoints/users/following.ts
+++ b/packages/backend/src/server/api/endpoints/users/following.ts
@@ -9,7 +9,7 @@ import { toPunyNullable } from '@/misc/convert-host';
 export const meta = {
 	tags: ['users'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		userId: {
@@ -39,11 +39,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Following',
 		},
 	},
@@ -61,7 +61,7 @@ export const meta = {
 			id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts
index bffa4a502e..c5f08b4c94 100644
--- a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts
+++ b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts
@@ -25,7 +25,7 @@ export const meta = {
 			validator: $.optional.type(ID),
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts
index 5852da1849..d886d3355a 100644
--- a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts
+++ b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts
@@ -10,7 +10,7 @@ import { Notes, Users } from '@/models/index';
 export const meta = {
 	tags: ['users'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		userId: {
@@ -24,12 +24,22 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User',
+			type: 'object',
+			optional: false, nullable: false,
+			properties: {
+				user: {
+					type: 'object',
+					optional: false, nullable: false,
+					ref: 'UserDetailed',
+				},
+				weight: {
+					type: 'number',
+					optional: false, nullable: false,
+				},
+			},
 		},
 	},
 
@@ -40,7 +50,7 @@ export const meta = {
 			id: 'e6965129-7b2a-40a4-bae2-cd84cd434822',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/groups/create.ts b/packages/backend/src/server/api/endpoints/users/groups/create.ts
index bbe041122e..25e29de01c 100644
--- a/packages/backend/src/server/api/endpoints/users/groups/create.ts
+++ b/packages/backend/src/server/api/endpoints/users/groups/create.ts
@@ -8,7 +8,7 @@ import { UserGroupJoining } from '@/models/entities/user-group-joining';
 export const meta = {
 	tags: ['groups'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:user-groups',
 
@@ -19,11 +19,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'UserGroup',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/users/groups/delete.ts b/packages/backend/src/server/api/endpoints/users/groups/delete.ts
index 5a38428ca4..f30ab78ca0 100644
--- a/packages/backend/src/server/api/endpoints/users/groups/delete.ts
+++ b/packages/backend/src/server/api/endpoints/users/groups/delete.ts
@@ -7,7 +7,7 @@ import { UserGroups } from '@/models/index';
 export const meta = {
 	tags: ['groups'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:user-groups',
 
@@ -24,7 +24,7 @@ export const meta = {
 			id: '63dbd64c-cd77-413f-8e08-61781e210b38',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts
index 45b6c39dc3..7061db538b 100644
--- a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts
+++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts
@@ -9,7 +9,7 @@ import { UserGroupJoining } from '@/models/entities/user-group-joining';
 export const meta = {
 	tags: ['groups', 'users'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:user-groups',
 
@@ -26,7 +26,7 @@ export const meta = {
 			id: '98c11eca-c890-4f42-9806-c8c8303ebb5e',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts
index 03ce90aa3d..f5ca3dec8b 100644
--- a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts
+++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts
@@ -7,7 +7,7 @@ import { UserGroupInvitations } from '@/models/index';
 export const meta = {
 	tags: ['groups', 'users'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:user-groups',
 
@@ -24,7 +24,7 @@ export const meta = {
 			id: 'ad7471d4-2cd9-44b4-ac68-e7136b4ce656',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/users/groups/invite.ts b/packages/backend/src/server/api/endpoints/users/groups/invite.ts
index 68ce0f1adc..3b7a4edb81 100644
--- a/packages/backend/src/server/api/endpoints/users/groups/invite.ts
+++ b/packages/backend/src/server/api/endpoints/users/groups/invite.ts
@@ -11,7 +11,7 @@ import { createNotification } from '@/services/create-notification';
 export const meta = {
 	tags: ['groups', 'users'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:user-groups',
 
@@ -50,7 +50,7 @@ export const meta = {
 			id: 'ee0f58b4-b529-4d13-b761-b9a3e69f97e6',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/groups/joined.ts b/packages/backend/src/server/api/endpoints/users/groups/joined.ts
index ad18a2f121..ab48b1910d 100644
--- a/packages/backend/src/server/api/endpoints/users/groups/joined.ts
+++ b/packages/backend/src/server/api/endpoints/users/groups/joined.ts
@@ -5,20 +5,20 @@ import { Not, In } from 'typeorm';
 export const meta = {
 	tags: ['groups', 'account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:user-groups',
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'UserGroup',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/groups/leave.ts b/packages/backend/src/server/api/endpoints/users/groups/leave.ts
index af259d28ef..d2fcdab301 100644
--- a/packages/backend/src/server/api/endpoints/users/groups/leave.ts
+++ b/packages/backend/src/server/api/endpoints/users/groups/leave.ts
@@ -7,7 +7,7 @@ import { UserGroups, UserGroupJoinings } from '@/models/index';
 export const meta = {
 	tags: ['groups', 'users'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:user-groups',
 
@@ -30,7 +30,7 @@ export const meta = {
 			id: 'b6d6e0c2-ef8a-9bb8-653d-79f4a3107c69',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/groups/owned.ts b/packages/backend/src/server/api/endpoints/users/groups/owned.ts
index d11d04b84d..6193a71019 100644
--- a/packages/backend/src/server/api/endpoints/users/groups/owned.ts
+++ b/packages/backend/src/server/api/endpoints/users/groups/owned.ts
@@ -4,20 +4,20 @@ import { UserGroups } from '@/models/index';
 export const meta = {
 	tags: ['groups', 'account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:user-groups',
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'UserGroup',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/groups/pull.ts b/packages/backend/src/server/api/endpoints/users/groups/pull.ts
index e4c5d1f9ee..785bea140d 100644
--- a/packages/backend/src/server/api/endpoints/users/groups/pull.ts
+++ b/packages/backend/src/server/api/endpoints/users/groups/pull.ts
@@ -8,7 +8,7 @@ import { UserGroups, UserGroupJoinings } from '@/models/index';
 export const meta = {
 	tags: ['groups', 'users'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:user-groups',
 
@@ -41,7 +41,7 @@ export const meta = {
 			id: '1546eed5-4414-4dea-81c1-b0aec4f6d2af',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/groups/show.ts b/packages/backend/src/server/api/endpoints/users/groups/show.ts
index 55b86d0e53..eb26eac2a8 100644
--- a/packages/backend/src/server/api/endpoints/users/groups/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/groups/show.ts
@@ -7,7 +7,7 @@ import { UserGroups, UserGroupJoinings } from '@/models/index';
 export const meta = {
 	tags: ['groups', 'account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:user-groups',
 
@@ -18,8 +18,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'UserGroup',
 	},
 
@@ -30,7 +30,7 @@ export const meta = {
 			id: 'ea04751e-9b7e-487b-a509-330fb6bd6b9b',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts
index 6795f1dd21..4b1c8fbbdb 100644
--- a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts
+++ b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts
@@ -8,7 +8,7 @@ import { UserGroups, UserGroupJoinings } from '@/models/index';
 export const meta = {
 	tags: ['groups', 'users'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:user-groups',
 
@@ -23,8 +23,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'UserGroup',
 	},
 
@@ -47,7 +47,7 @@ export const meta = {
 			id: 'd31bebee-196d-42c2-9a3e-9474d4be6cc4',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/groups/update.ts b/packages/backend/src/server/api/endpoints/users/groups/update.ts
index 0d188af738..6caf903555 100644
--- a/packages/backend/src/server/api/endpoints/users/groups/update.ts
+++ b/packages/backend/src/server/api/endpoints/users/groups/update.ts
@@ -7,7 +7,7 @@ import { UserGroups } from '@/models/index';
 export const meta = {
 	tags: ['groups'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:user-groups',
 
@@ -22,8 +22,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'UserGroup',
 	},
 
@@ -34,7 +34,7 @@ export const meta = {
 			id: '9081cda3-7a9e-4fac-a6ce-908d70f282f6',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts
index 4c89d1b7eb..945b511628 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/create.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts
@@ -7,7 +7,7 @@ import { UserList } from '@/models/entities/user-list';
 export const meta = {
 	tags: ['lists'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -18,11 +18,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'UserList',
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/users/lists/delete.ts b/packages/backend/src/server/api/endpoints/users/lists/delete.ts
index 0f2495dc29..3183d2a09c 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/delete.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/delete.ts
@@ -7,7 +7,7 @@ import { UserLists } from '@/models/index';
 export const meta = {
 	tags: ['lists'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -24,7 +24,7 @@ export const meta = {
 			id: '78436795-db79-42f5-b1e2-55ea2cf19166',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts
index 4cf7db69a3..ae66b0aacc 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/list.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts
@@ -4,20 +4,20 @@ import { UserLists } from '@/models/index';
 export const meta = {
 	tags: ['lists', 'account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:account',
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'UserList',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts
index 980d90c614..4c74aefa8a 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts
@@ -9,7 +9,7 @@ import { UserLists, UserListJoinings, Users } from '@/models/index';
 export const meta = {
 	tags: ['lists', 'users'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -36,7 +36,7 @@ export const meta = {
 			id: '588e7f72-c744-4a61-b180-d354e912bda2',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts
index a0ca314f36..8b50c475b0 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/push.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts
@@ -9,7 +9,7 @@ import { UserLists, UserListJoinings, Blockings } from '@/models/index';
 export const meta = {
 	tags: ['lists', 'users'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -48,7 +48,7 @@ export const meta = {
 			id: '990232c5-3f9d-4d83-9f3f-ef27b6332a4b',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts
index 5822c06971..06555c1a88 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts
@@ -7,7 +7,7 @@ import { UserLists } from '@/models/index';
 export const meta = {
 	tags: ['lists', 'account'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:account',
 
@@ -18,8 +18,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'UserList',
 	},
 
@@ -30,7 +30,7 @@ export const meta = {
 			id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts
index 50fcf6ff54..02b0d5fe18 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/update.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts
@@ -7,7 +7,7 @@ import { UserLists } from '@/models/index';
 export const meta = {
 	tags: ['lists'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'write:account',
 
@@ -22,8 +22,8 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
+		type: 'object',
+		optional: false, nullable: false,
 		ref: 'UserList',
 	},
 
@@ -34,7 +34,7 @@ export const meta = {
 			id: '796666fe-3dff-4d39-becb-8a5932c1d5b7',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts
index 2069eb60ab..99158fb0ae 100644
--- a/packages/backend/src/server/api/endpoints/users/notes.ts
+++ b/packages/backend/src/server/api/endpoints/users/notes.ts
@@ -66,11 +66,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'Note',
 		},
 	},
@@ -82,7 +82,7 @@ export const meta = {
 			id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts
index 9fb985dc12..6e003dd1af 100644
--- a/packages/backend/src/server/api/endpoints/users/pages.ts
+++ b/packages/backend/src/server/api/endpoints/users/pages.ts
@@ -25,7 +25,7 @@ export const meta = {
 			validator: $.optional.type(ID),
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts
index 6f68aca184..312d4dbf23 100644
--- a/packages/backend/src/server/api/endpoints/users/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/users/reactions.ts
@@ -9,7 +9,7 @@ import { ApiError } from '../../error';
 export const meta = {
 	tags: ['users', 'reactions'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		userId: {
@@ -39,11 +39,11 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'NoteReaction',
 		},
 	},
@@ -55,7 +55,7 @@ export const meta = {
 			id: '673a7dd2-6924-1093-e0c0-e68456ceae5c',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts
index 5e640330c8..9ea39eb2dd 100644
--- a/packages/backend/src/server/api/endpoints/users/recommendation.ts
+++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts
@@ -8,7 +8,7 @@ import { generateBlockedUserQuery, generateBlockQueryForUsers } from '../../comm
 export const meta = {
 	tags: ['users'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	kind: 'read:account',
 
@@ -25,15 +25,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User',
+			type: 'object',
+			optional: false, nullable: false,
+			ref: 'UserDetailed',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts
index 629c1d0c1d..7e319ca105 100644
--- a/packages/backend/src/server/api/endpoints/users/relation.ts
+++ b/packages/backend/src/server/api/endpoints/users/relation.ts
@@ -6,7 +6,7 @@ import { Users } from '@/models/index';
 export const meta = {
 	tags: ['users'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	params: {
 		userId: {
@@ -15,92 +15,91 @@ export const meta = {
 	},
 
 	res: {
+		optional: false, nullable: false,
 		oneOf: [
 			{
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
+				type: 'object',
 				properties: {
 					id: {
-						type: 'string' as const,
-						optional: false as const, nullable: false as const,
+						type: 'string',
+						optional: false, nullable: false,
 						format: 'id',
 					},
 					isFollowing: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 					hasPendingFollowRequestFromYou: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 					hasPendingFollowRequestToYou: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 					isFollowed: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 					isBlocking: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 					isBlocked: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 					isMuted: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
+						type: 'boolean',
+						optional: false, nullable: false,
 					},
 				},
 			},
 			{
-				type: 'array' as const,
-				optional: false as const, nullable: false as const,
+				type: 'array',
 				items: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
+					type: 'object',
+					optional: false, nullable: false,
 					properties: {
 						id: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const,
+							type: 'string',
+							optional: false, nullable: false,
 							format: 'id',
 						},
 						isFollowing: {
-							type: 'boolean' as const,
-							optional: false as const, nullable: false as const,
+							type: 'boolean',
+							optional: false, nullable: false,
 						},
 						hasPendingFollowRequestFromYou: {
-							type: 'boolean' as const,
-							optional: false as const, nullable: false as const,
+							type: 'boolean',
+							optional: false, nullable: false,
 						},
 						hasPendingFollowRequestToYou: {
-							type: 'boolean' as const,
-							optional: false as const, nullable: false as const,
+							type: 'boolean',
+							optional: false, nullable: false,
 						},
 						isFollowed: {
-							type: 'boolean' as const,
-							optional: false as const, nullable: false as const,
+							type: 'boolean',
+							optional: false, nullable: false,
 						},
 						isBlocking: {
-							type: 'boolean' as const,
-							optional: false as const, nullable: false as const,
+							type: 'boolean',
+							optional: false, nullable: false,
 						},
 						isBlocked: {
-							type: 'boolean' as const,
-							optional: false as const, nullable: false as const,
+							type: 'boolean',
+							optional: false, nullable: false,
 						},
 						isMuted: {
-							type: 'boolean' as const,
-							optional: false as const, nullable: false as const,
+							type: 'boolean',
+							optional: false, nullable: false,
 						},
 					},
 				},
 			},
 		],
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
index bd80710fec..ed2aa7bb26 100644
--- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts
+++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
@@ -13,7 +13,7 @@ import { fetchMeta } from '@/misc/fetch-meta';
 export const meta = {
 	tags: ['users'],
 
-	requireCredential: true as const,
+	requireCredential: true,
 
 	params: {
 		userId: {
@@ -44,7 +44,7 @@ export const meta = {
 			id: '35e166f5-05fb-4f87-a2d5-adb42676d48f',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
index b9cf332f47..d67625e624 100644
--- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
+++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -8,7 +8,7 @@ import { User } from '@/models/entities/user';
 export const meta = {
 	tags: ['users'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		username: {
@@ -31,15 +31,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'User',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
@@ -112,6 +112,6 @@ export default define(meta, async (ps, me) => {
 				.getMany();
 		}
 
-		return await Users.packMany(users, me, { detail: ps.detail });
+		return await Users.packMany(users, me, { detail: !!ps.detail });
 	}
 });
diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts
index 35f1988fce..26f818afcc 100644
--- a/packages/backend/src/server/api/endpoints/users/search.ts
+++ b/packages/backend/src/server/api/endpoints/users/search.ts
@@ -7,7 +7,7 @@ import { Brackets } from 'typeorm';
 export const meta = {
 	tags: ['users'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		query: {
@@ -36,15 +36,15 @@ export const meta = {
 	},
 
 	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
+		type: 'array',
+		optional: false, nullable: false,
 		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
+			type: 'object',
+			optional: false, nullable: false,
 			ref: 'User',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts
index e8b2a781f7..92910e9ed8 100644
--- a/packages/backend/src/server/api/endpoints/users/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/show.ts
@@ -11,7 +11,7 @@ import { User } from '@/models/entities/user';
 export const meta = {
 	tags: ['users'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		userId: {
@@ -32,9 +32,20 @@ export const meta = {
 	},
 
 	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User',
+		optional: false, nullable: false,
+		oneOf: [
+			{
+				type: 'object',
+				ref: 'UserDetailed',
+			},
+			{
+				type: 'array',
+				items: {
+					type: 'object',
+					ref: 'UserDetailed',
+				}
+			},
+		]
 	},
 
 	errors: {
@@ -42,7 +53,7 @@ export const meta = {
 			message: 'Failed to resolve remote user.',
 			code: 'FAILED_TO_RESOLVE_REMOTE_USER',
 			id: 'ef7b9be4-9cba-4e6f-ab41-90ed171c7d3c',
-			kind: 'server' as const,
+			kind: 'server',
 		},
 
 		noSuchUser: {
@@ -51,7 +62,7 @@ export const meta = {
 			id: '4362f8dc-731f-4ad8-a694-be5a88922a24',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts
index bd62e7fb21..381e433479 100644
--- a/packages/backend/src/server/api/endpoints/users/stats.ts
+++ b/packages/backend/src/server/api/endpoints/users/stats.ts
@@ -2,12 +2,12 @@ import $ from 'cafy';
 import define from '../../define';
 import { ApiError } from '../../error';
 import { ID } from '@/misc/cafy-id';
-import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, ReversiGames, Users } from '@/models/index';
+import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, Users } from '@/models/index';
 
 export const meta = {
 	tags: ['users'],
 
-	requireCredential: false as const,
+	requireCredential: false,
 
 	params: {
 		userId: {
@@ -22,7 +22,7 @@ export const meta = {
 			id: '9e638e45-3b25-4ef7-8f95-07e8498f1819',
 		},
 	},
-};
+} as const;
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, me) => {
@@ -50,7 +50,6 @@ export default define(meta, async (ps, me) => {
 		pageLikedCount,
 		driveFilesCount,
 		driveUsage,
-		reversiCount,
 	] = await Promise.all([
 		Notes.createQueryBuilder('note')
 			.where('note.userId = :userId', { userId: user.id })
@@ -113,10 +112,6 @@ export default define(meta, async (ps, me) => {
 			.where('file.userId = :userId', { userId: user.id })
 			.getCount(),
 		DriveFiles.calcDriveUsageOf(user),
-		ReversiGames.createQueryBuilder('game')
-			.where('game.user1Id = :userId', { userId: user.id })
-			.orWhere('game.user2Id = :userId', { userId: user.id })
-			.getCount(),
 	]);
 
 	return {
@@ -140,6 +135,5 @@ export default define(meta, async (ps, me) => {
 		pageLikedCount,
 		driveFilesCount,
 		driveUsage,
-		reversiCount,
 	};
 });
diff --git a/packages/backend/src/server/api/limiter.ts b/packages/backend/src/server/api/limiter.ts
index 5f617771e0..4721f6263a 100644
--- a/packages/backend/src/server/api/limiter.ts
+++ b/packages/backend/src/server/api/limiter.ts
@@ -7,8 +7,8 @@ import Logger from '@/services/logger';
 
 const logger = new Logger('limiter');
 
-export default (endpoint: IEndpoint, user: User) => new Promise<void>((ok, reject) => {
-	const limitation = endpoint.meta.limit!;
+export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user: User) => new Promise<void>((ok, reject) => {
+	const limitation = endpoint.meta.limit;
 
 	const key = Object.prototype.hasOwnProperty.call(limitation, 'key')
 		? limitation.key
@@ -30,7 +30,7 @@ export default (endpoint: IEndpoint, user: User) => new Promise<void>((ok, rejec
 	}
 
 	// Short-term limit
-	function min() {
+	function min(): void {
 		const minIntervalLimiter = new Limiter({
 			id: `${user.id}:${key}:min`,
 			duration: limitation.minInterval,
@@ -58,7 +58,7 @@ export default (endpoint: IEndpoint, user: User) => new Promise<void>((ok, rejec
 	}
 
 	// Long term limit
-	function max() {
+	function max(): void {
 		const limiter = new Limiter({
 			id: `${user.id}:${key}`,
 			duration: limitation.duration,
diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts
index 1c521f212f..1efef8d26d 100644
--- a/packages/backend/src/server/api/openapi/gen-spec.ts
+++ b/packages/backend/src/server/api/openapi/gen-spec.ts
@@ -118,7 +118,7 @@ export function genOpenapiSpec(lang = 'ja-JP') {
 			description: desc,
 			externalDocs: {
 				description: 'Source code',
-				url: `https://github.com/misskey-dev/misskey/blob/develop/src/server/api/endpoints/${endpoint.name}.ts`,
+				url: `https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/${endpoint.name}.ts`,
 			},
 			...(endpoint.meta.tags ? {
 				tags: [endpoint.meta.tags[0]],
diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts
index 723b3e884a..eb42667fd5 100644
--- a/packages/backend/src/server/api/openapi/schemas.ts
+++ b/packages/backend/src/server/api/openapi/schemas.ts
@@ -1,6 +1,6 @@
-import { refs, Schema } from '@/misc/schema';
+import { refs, MinimumSchema } from '@/misc/schema';
 
-export function convertSchemaToOpenApiSchema(schema: Schema) {
+export function convertSchemaToOpenApiSchema(schema: MinimumSchema) {
 	const res: any = schema;
 
 	if (schema.type === 'object' && schema.properties) {
@@ -15,6 +15,10 @@ export function convertSchemaToOpenApiSchema(schema: Schema) {
 		res.items = convertSchemaToOpenApiSchema(schema.items);
 	}
 
+	if (schema.anyOf) res.anyOf = schema.anyOf.map(convertSchemaToOpenApiSchema);
+	if (schema.oneOf) res.oneOf = schema.oneOf.map(convertSchemaToOpenApiSchema);
+	if (schema.allOf) res.allOf = schema.allOf.map(convertSchemaToOpenApiSchema);
+
 	if (schema.ref) {
 		res.$ref = `#/components/schemas/${schema.ref}`;
 	}
diff --git a/packages/backend/src/server/api/stream/channels/games/reversi-game.ts b/packages/backend/src/server/api/stream/channels/games/reversi-game.ts
deleted file mode 100644
index 314db48b5e..0000000000
--- a/packages/backend/src/server/api/stream/channels/games/reversi-game.ts
+++ /dev/null
@@ -1,372 +0,0 @@
-import autobind from 'autobind-decorator';
-import * as CRC32 from 'crc-32';
-import { publishReversiGameStream } from '@/services/stream';
-import Reversi from '../../../../../games/reversi/core';
-import * as maps from '../../../../../games/reversi/maps';
-import Channel from '../../channel';
-import { ReversiGame } from '@/models/entities/games/reversi/game';
-import { ReversiGames, Users } from '@/models/index';
-import { User } from '@/models/entities/user';
-
-export default class extends Channel {
-	public readonly chName = 'gamesReversiGame';
-	public static shouldShare = false;
-	public static requireCredential = false;
-
-	private gameId: ReversiGame['id'] | null = null;
-	private watchers: Record<User['id'], Date> = {};
-	private emitWatchersIntervalId: ReturnType<typeof setInterval>;
-
-	@autobind
-	public async init(params: any) {
-		this.gameId = params.gameId;
-
-		// Subscribe game stream
-		this.subscriber.on(`reversiGameStream:${this.gameId}`, this.onEvent);
-		this.emitWatchersIntervalId = setInterval(this.emitWatchers, 5000);
-
-		const game = await ReversiGames.findOne(this.gameId!);
-		if (game == null) throw new Error('game not found');
-
-		// 観戦者イベント
-		this.watch(game);
-	}
-
-	@autobind
-	private onEvent(data: any) {
-		if (data.type === 'watching') {
-			const id = data.body;
-			this.watchers[id] = new Date();
-		} else {
-			this.send(data);
-		}
-	}
-
-	@autobind
-	private async emitWatchers() {
-		const now = new Date();
-
-		// Remove not watching users
-		for (const [userId, date] of Object.entries(this.watchers)) {
-			if (now.getTime() - date.getTime() > 5000) delete this.watchers[userId];
-		}
-
-		const users = await Users.packMany(Object.keys(this.watchers), null, { detail: false });
-
-		this.send({
-			type: 'watchers',
-			body: users,
-		});
-	}
-
-	@autobind
-	public dispose() {
-		// Unsubscribe events
-		this.subscriber.off(`reversiGameStream:${this.gameId}`, this.onEvent);
-		clearInterval(this.emitWatchersIntervalId);
-	}
-
-	@autobind
-	public onMessage(type: string, body: any) {
-		switch (type) {
-			case 'accept': this.accept(true); break;
-			case 'cancelAccept': this.accept(false); break;
-			case 'updateSettings': this.updateSettings(body.key, body.value); break;
-			case 'initForm': this.initForm(body); break;
-			case 'updateForm': this.updateForm(body.id, body.value); break;
-			case 'message': this.message(body); break;
-			case 'set': this.set(body.pos); break;
-			case 'check': this.check(body.crc32); break;
-		}
-	}
-
-	@autobind
-	private async updateSettings(key: string, value: any) {
-		if (this.user == null) return;
-
-		const game = await ReversiGames.findOne(this.gameId!);
-		if (game == null) throw new Error('game not found');
-
-		if (game.isStarted) return;
-		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
-		if ((game.user1Id === this.user.id) && game.user1Accepted) return;
-		if ((game.user2Id === this.user.id) && game.user2Accepted) return;
-
-		if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard'].includes(key)) return;
-
-		await ReversiGames.update(this.gameId!, {
-			[key]: value,
-		});
-
-		publishReversiGameStream(this.gameId!, 'updateSettings', {
-			key: key,
-			value: value,
-		});
-	}
-
-	@autobind
-	private async initForm(form: any) {
-		if (this.user == null) return;
-
-		const game = await ReversiGames.findOne(this.gameId!);
-		if (game == null) throw new Error('game not found');
-
-		if (game.isStarted) return;
-		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
-
-		const set = game.user1Id === this.user.id ? {
-			form1: form,
-		} : {
-			form2: form,
-		};
-
-		await ReversiGames.update(this.gameId!, set);
-
-		publishReversiGameStream(this.gameId!, 'initForm', {
-			userId: this.user.id,
-			form,
-		});
-	}
-
-	@autobind
-	private async updateForm(id: string, value: any) {
-		if (this.user == null) return;
-
-		const game = await ReversiGames.findOne(this.gameId!);
-		if (game == null) throw new Error('game not found');
-
-		if (game.isStarted) return;
-		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
-
-		const form = game.user1Id === this.user.id ? game.form2 : game.form1;
-
-		const item = form.find((i: any) => i.id == id);
-
-		if (item == null) return;
-
-		item.value = value;
-
-		const set = game.user1Id === this.user.id ? {
-			form2: form,
-		} : {
-				form1: form,
-			};
-
-		await ReversiGames.update(this.gameId!, set);
-
-		publishReversiGameStream(this.gameId!, 'updateForm', {
-			userId: this.user.id,
-			id,
-			value,
-		});
-	}
-
-	@autobind
-	private async message(message: any) {
-		if (this.user == null) return;
-
-		message.id = Math.random();
-		publishReversiGameStream(this.gameId!, 'message', {
-			userId: this.user.id,
-			message,
-		});
-	}
-
-	@autobind
-	private async accept(accept: boolean) {
-		if (this.user == null) return;
-
-		const game = await ReversiGames.findOne(this.gameId!);
-		if (game == null) throw new Error('game not found');
-
-		if (game.isStarted) return;
-
-		let bothAccepted = false;
-
-		if (game.user1Id === this.user.id) {
-			await ReversiGames.update(this.gameId!, {
-				user1Accepted: accept,
-			});
-
-			publishReversiGameStream(this.gameId!, 'changeAccepts', {
-				user1: accept,
-				user2: game.user2Accepted,
-			});
-
-			if (accept && game.user2Accepted) bothAccepted = true;
-		} else if (game.user2Id === this.user.id) {
-			await ReversiGames.update(this.gameId!, {
-				user2Accepted: accept,
-			});
-
-			publishReversiGameStream(this.gameId!, 'changeAccepts', {
-				user1: game.user1Accepted,
-				user2: accept,
-			});
-
-			if (accept && game.user1Accepted) bothAccepted = true;
-		} else {
-			return;
-		}
-
-		if (bothAccepted) {
-			// 3秒後、まだacceptされていたらゲーム開始
-			setTimeout(async () => {
-				const freshGame = await ReversiGames.findOne(this.gameId!);
-				if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return;
-				if (!freshGame.user1Accepted || !freshGame.user2Accepted) return;
-
-				let bw: number;
-				if (freshGame.bw == 'random') {
-					bw = Math.random() > 0.5 ? 1 : 2;
-				} else {
-					bw = parseInt(freshGame.bw, 10);
-				}
-
-				function getRandomMap() {
-					const mapCount = Object.entries(maps).length;
-					const rnd = Math.floor(Math.random() * mapCount);
-					return Object.values(maps)[rnd].data;
-				}
-
-				const map = freshGame.map != null ? freshGame.map : getRandomMap();
-
-				await ReversiGames.update(this.gameId!, {
-					startedAt: new Date(),
-					isStarted: true,
-					black: bw,
-					map: map,
-				});
-
-				//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
-				const o = new Reversi(map, {
-					isLlotheo: freshGame.isLlotheo,
-					canPutEverywhere: freshGame.canPutEverywhere,
-					loopedBoard: freshGame.loopedBoard,
-				});
-
-				if (o.isEnded) {
-					let winner;
-					if (o.winner === true) {
-						winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id;
-					} else if (o.winner === false) {
-						winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id;
-					} else {
-						winner = null;
-					}
-
-					await ReversiGames.update(this.gameId!, {
-						isEnded: true,
-						winnerId: winner,
-					});
-
-					publishReversiGameStream(this.gameId!, 'ended', {
-						winnerId: winner,
-						game: await ReversiGames.pack(this.gameId!, this.user),
-					});
-				}
-				//#endregion
-
-				publishReversiGameStream(this.gameId!, 'started',
-					await ReversiGames.pack(this.gameId!, this.user));
-			}, 3000);
-		}
-	}
-
-	// 石を打つ
-	@autobind
-	private async set(pos: number) {
-		if (this.user == null) return;
-
-		const game = await ReversiGames.findOne(this.gameId!);
-		if (game == null) throw new Error('game not found');
-
-		if (!game.isStarted) return;
-		if (game.isEnded) return;
-		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
-
-		const myColor =
-			((game.user1Id === this.user.id) && game.black == 1) || ((game.user2Id === this.user.id) && game.black == 2)
-				? true
-				: false;
-
-		const o = new Reversi(game.map, {
-			isLlotheo: game.isLlotheo,
-			canPutEverywhere: game.canPutEverywhere,
-			loopedBoard: game.loopedBoard,
-		});
-
-		// 盤面の状態を再生
-		for (const log of game.logs) {
-			o.put(log.color, log.pos);
-		}
-
-		if (o.turn !== myColor) return;
-
-		if (!o.canPut(myColor, pos)) return;
-		o.put(myColor, pos);
-
-		let winner;
-		if (o.isEnded) {
-			if (o.winner === true) {
-				winner = game.black == 1 ? game.user1Id : game.user2Id;
-			} else if (o.winner === false) {
-				winner = game.black == 1 ? game.user2Id : game.user1Id;
-			} else {
-				winner = null;
-			}
-		}
-
-		const log = {
-			at: new Date(),
-			color: myColor,
-			pos,
-		};
-
-		const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString();
-
-		game.logs.push(log);
-
-		await ReversiGames.update(this.gameId!, {
-			crc32,
-			isEnded: o.isEnded,
-			winnerId: winner,
-			logs: game.logs,
-		});
-
-		publishReversiGameStream(this.gameId!, 'set', Object.assign(log, {
-			next: o.turn,
-		}));
-
-		if (o.isEnded) {
-			publishReversiGameStream(this.gameId!, 'ended', {
-				winnerId: winner,
-				game: await ReversiGames.pack(this.gameId!, this.user),
-			});
-		}
-	}
-
-	@autobind
-	private async check(crc32: string | number) {
-		const game = await ReversiGames.findOne(this.gameId!);
-		if (game == null) throw new Error('game not found');
-
-		if (!game.isStarted) return;
-
-		if (crc32.toString() !== game.crc32) {
-			this.send('rescue', await ReversiGames.pack(game, this.user));
-		}
-
-		// ついでに観戦者イベントを発行
-		this.watch(game);
-	}
-
-	@autobind
-	private watch(game: ReversiGame) {
-		if (this.user != null) {
-			if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) {
-				publishReversiGameStream(this.gameId!, 'watching', this.user.id);
-			}
-		}
-	}
-}
diff --git a/packages/backend/src/server/api/stream/channels/games/reversi.ts b/packages/backend/src/server/api/stream/channels/games/reversi.ts
deleted file mode 100644
index 121560ff87..0000000000
--- a/packages/backend/src/server/api/stream/channels/games/reversi.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import autobind from 'autobind-decorator';
-import { publishMainStream } from '@/services/stream';
-import Channel from '../../channel';
-import { ReversiMatchings } from '@/models/index';
-
-export default class extends Channel {
-	public readonly chName = 'gamesReversi';
-	public static shouldShare = true;
-	public static requireCredential = true;
-
-	@autobind
-	public async init(params: any) {
-		// Subscribe reversi stream
-		this.subscriber.on(`reversiStream:${this.user!.id}`, data => {
-			this.send(data);
-		});
-	}
-
-	@autobind
-	public async onMessage(type: string, body: any) {
-		switch (type) {
-			case 'ping': {
-				if (body.id == null) return;
-				const matching = await ReversiMatchings.findOne({
-					parentId: this.user!.id,
-					childId: body.id,
-				});
-				if (matching == null) return;
-				publishMainStream(matching.childId, 'reversiInvited', await ReversiMatchings.pack(matching, { id: matching.childId }));
-				break;
-			}
-		}
-	}
-}
diff --git a/packages/backend/src/server/api/stream/channels/index.ts b/packages/backend/src/server/api/stream/channels/index.ts
index 89d93f2da3..f3826c4cf7 100644
--- a/packages/backend/src/server/api/stream/channels/index.ts
+++ b/packages/backend/src/server/api/stream/channels/index.ts
@@ -13,8 +13,6 @@ import drive from './drive';
 import hashtag from './hashtag';
 import channel from './channel';
 import admin from './admin';
-import gamesReversi from './games/reversi';
-import gamesReversiGame from './games/reversi-game';
 
 export default {
 	main,
@@ -32,6 +30,4 @@ export default {
 	hashtag,
 	channel,
 	admin,
-	gamesReversi,
-	gamesReversiGame,
 };
diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts
index f4302f64a0..e70c26f5e5 100644
--- a/packages/backend/src/server/api/stream/types.ts
+++ b/packages/backend/src/server/api/stream/types.ts
@@ -11,7 +11,6 @@ import { Emoji } from '@/models/entities/emoji';
 import { UserList } from '@/models/entities/user-list';
 import { MessagingMessage } from '@/models/entities/messaging-message';
 import { UserGroup } from '@/models/entities/user-group';
-import { ReversiGame } from '@/models/entities/games/reversi/game';
 import { AbuseUserReport } from '@/models/entities/abuse-user-report';
 import { Signin } from '@/models/entities/signin';
 import { Page } from '@/models/entities/page';
@@ -37,7 +36,7 @@ export interface UserStreamTypes {
 	updateUserProfile: UserProfile;
 	mute: User;
 	unmute: User;
-	follow: Packed<'User'>;
+	follow: Packed<'UserDetailedNotMe'>;
 	unfollow: Packed<'User'>;
 	userAdded: Packed<'User'>;
 }
@@ -47,7 +46,7 @@ export interface MainStreamTypes {
 	mention: Packed<'Note'>;
 	reply: Packed<'Note'>;
 	renote: Packed<'Note'>;
-	follow: Packed<'User'>;
+	follow: Packed<'UserDetailedNotMe'>;
 	followed: Packed<'User'>;
 	unfollow: Packed<'User'>;
 	meUpdated: Packed<'User'>;
@@ -77,8 +76,6 @@ export interface MainStreamTypes {
 	readAllChannels: undefined;
 	unreadChannel: Note['id'];
 	myTokenRegenerated: undefined;
-	reversiNoInvites: undefined;
-	reversiInvited: Packed<'ReversiMatching'>;
 	signin: Signin;
 	registryUpdated: {
 		scope?: string[];
@@ -158,47 +155,6 @@ export interface MessagingIndexStreamTypes {
 	message: Packed<'MessagingMessage'>;
 }
 
-export interface ReversiStreamTypes {
-	matched: Packed<'ReversiGame'>;
-	invited: Packed<'ReversiMatching'>;
-}
-
-export interface ReversiGameStreamTypes {
-	started: Packed<'ReversiGame'>;
-	ended: {
-		winnerId?: User['id'] | null,
-		game: Packed<'ReversiGame'>;
-	};
-	updateSettings: {
-		key: string;
-		value: FIXME;
-	};
-	initForm: {
-		userId: User['id'];
-		form: FIXME;
-	};
-	updateForm: {
-		userId: User['id'];
-		id: string;
-		value: FIXME;
-	};
-	message: {
-		userId: User['id'];
-		message: FIXME;
-	};
-	changeAccepts: {
-		user1: boolean;
-		user2: boolean;
-	};
-	set: {
-		at: Date;
-		color: boolean;
-		pos: number;
-		next: boolean;
-	};
-	watching: User['id'];
-}
-
 export interface AdminStreamTypes {
 	newAbuseUserReport: {
 		id: AbuseUserReport['id'];
@@ -268,14 +224,6 @@ export type StreamMessages = {
 		name: `messagingIndexStream:${User['id']}`;
 		payload: EventUnionFromDictionary<MessagingIndexStreamTypes>;
 	};
-	reversi: {
-		name: `reversiStream:${User['id']}`;
-		payload: EventUnionFromDictionary<ReversiStreamTypes>;
-	};
-	reversiGame: {
-		name: `reversiGameStream:${ReversiGame['id']}`;
-		payload: EventUnionFromDictionary<ReversiGameStreamTypes>;
-	};
 	admin: {
 		name: `adminStream:${User['id']}`;
 		payload: EventUnionFromDictionary<AdminStreamTypes>;
diff --git a/packages/backend/src/server/file/send-drive-file.ts b/packages/backend/src/server/file/send-drive-file.ts
index 7bfc36e25a..f3c6c518fa 100644
--- a/packages/backend/src/server/file/send-drive-file.ts
+++ b/packages/backend/src/server/file/send-drive-file.ts
@@ -11,7 +11,7 @@ import { DriveFiles } from '@/models/index';
 import { InternalStorage } from '@/services/drive/internal-storage';
 import { downloadUrl } from '@/misc/download-url';
 import { detectType } from '@/misc/get-file-info';
-import { convertToJpeg, convertToPngOrJpeg } from '@/services/drive/image-processor';
+import { convertToJpeg, convertToPng, convertToPngOrJpeg } from '@/services/drive/image-processor';
 import { GenerateVideoThumbnail } from '@/services/drive/generate-video-thumbnail';
 import { StatusError } from '@/misc/fetch';
 import { FILE_TYPE_BROWSERSAFE } from '@/const';
@@ -67,13 +67,19 @@ export default async function(ctx: Koa.Context) {
 					if (isThumbnail) {
 						if (['image/jpeg', 'image/webp'].includes(mime)) {
 							return await convertToJpeg(path, 498, 280);
-						} else if (['image/png'].includes(mime)) {
+						} else if (['image/png', 'image/svg+xml'].includes(mime)) {
 							return await convertToPngOrJpeg(path, 498, 280);
 						} else if (mime.startsWith('video/')) {
 							return await GenerateVideoThumbnail(path);
 						}
 					}
 
+					if (isWebpublic) {
+						if (['image/svg+xml'].includes(mime)) {
+							return await convertToPng(path, 2048, 2048);
+						}
+					}
+
 					return {
 						data: fs.readFileSync(path),
 						ext,
diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts
index 85fe21accb..764306c7d8 100644
--- a/packages/backend/src/server/index.ts
+++ b/packages/backend/src/server/index.ts
@@ -23,7 +23,7 @@ import Logger from '@/services/logger';
 import { envOption } from '../env';
 import { UserProfiles, Users } from '@/models/index';
 import { networkChart } from '@/services/chart/index';
-import { genAvatar } from '@/misc/gen-avatar';
+import { genIdenticon } from '@/misc/gen-identicon';
 import { createTemp } from '@/misc/create-temp';
 import { publishMainStream } from '@/services/stream';
 import * as Acct from 'misskey-js/built/acct';
@@ -84,9 +84,9 @@ router.get('/avatar/@:acct', async ctx => {
 	}
 });
 
-router.get('/random-avatar/:x', async ctx => {
+router.get('/identicon/:x', async ctx => {
 	const [temp] = await createTemp();
-	await genAvatar(ctx.params.x, fs.createWriteStream(temp));
+	await genIdenticon(ctx.params.x, fs.createWriteStream(temp));
 	ctx.set('Content-Type', 'image/png');
 	ctx.body = fs.createReadStream(temp);
 });
diff --git a/packages/backend/src/server/proxy/proxy-media.ts b/packages/backend/src/server/proxy/proxy-media.ts
index aba08bb805..c234b70c55 100644
--- a/packages/backend/src/server/proxy/proxy-media.ts
+++ b/packages/backend/src/server/proxy/proxy-media.ts
@@ -19,15 +19,16 @@ export async function proxyMedia(ctx: Koa.Context) {
 
 		const { mime, ext } = await detectType(path);
 
-		if (!mime.startsWith('image/')) throw 403;
-		if (!FILE_TYPE_BROWSERSAFE.includes(mime)) throw 403;
-
 		let image: IImage;
 
-		if ('static' in ctx.query && ['image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp'].includes(mime)) {
+		if ('static' in ctx.query && ['image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/svg+xml'].includes(mime)) {
 			image = await convertToPng(path, 498, 280);
-		} else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng'].includes(mime)) {
+		} else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/svg+xml'].includes(mime)) {
 			image = await convertToJpeg(path, 200, 200);
+		}	else if (['image/svg+xml'].includes(mime)) {
+			image = await convertToPng(path, 2048, 2048);
+		} else if (!mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(mime)) {
+			throw new StatusError('Rejected type', 403, 'Rejected type');
 		} else {
 			image = {
 				data: fs.readFileSync(path),
diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts
index 5623f7db83..e95a115aec 100644
--- a/packages/backend/src/server/web/index.ts
+++ b/packages/backend/src/server/web/index.ts
@@ -390,9 +390,6 @@ router.get('/cli', async ctx => {
 const override = (source: string, target: string, depth: number = 0) =>
 	[, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/');
 
-router.get('/othello', async ctx => ctx.redirect(override(ctx.URL.pathname, 'games/reversi', 1)));
-router.get('/reversi', async ctx => ctx.redirect(override(ctx.URL.pathname, 'games')));
-
 router.get('/flush', async ctx => {
 	await ctx.render('flush');
 });
diff --git a/packages/backend/src/services/chart/core.ts b/packages/backend/src/services/chart/core.ts
index f97fa521d3..e406449f4f 100644
--- a/packages/backend/src/services/chart/core.ts
+++ b/packages/backend/src/services/chart/core.ts
@@ -7,7 +7,7 @@
 import * as nestedProperty from 'nested-property';
 import autobind from 'autobind-decorator';
 import Logger from '../logger';
-import { SimpleSchema } from '@/misc/simple-schema';
+import { Schema } from '@/misc/schema';
 import { EntitySchema, getRepository, Repository, LessThan, Between } from 'typeorm';
 import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/prelude/time';
 import { getChartInsertLock } from '@/misc/app-lock';
@@ -57,7 +57,7 @@ export default abstract class Chart<T extends Record<string, any>> {
 		diff: DeepPartial<T>;
 		group: string | null;
 	}[] = [];
-	public schema: SimpleSchema;
+	public schema: Schema;
 	protected repositoryForHour: Repository<Log>;
 	protected repositoryForDay: Repository<Log>;
 
@@ -71,7 +71,7 @@ export default abstract class Chart<T extends Record<string, any>> {
 	protected abstract fetchActual(group: string | null): Promise<DeepPartial<T>>;
 
 	@autobind
-	private static convertSchemaToFlatColumnDefinitions(schema: SimpleSchema) {
+	private static convertSchemaToFlatColumnDefinitions(schema: Schema) {
 		const columns = {} as Record<string, unknown>;
 		const flatColumns = (x: Obj, path?: string) => {
 			for (const [k, v] of Object.entries(x)) {
@@ -183,7 +183,7 @@ export default abstract class Chart<T extends Record<string, any>> {
 	}
 
 	@autobind
-	public static schemaToEntity(name: string, schema: SimpleSchema, grouped = false): {
+	public static schemaToEntity(name: string, schema: Schema, grouped = false): {
 		hour: EntitySchema,
 		day: EntitySchema,
 	} {
@@ -233,7 +233,7 @@ export default abstract class Chart<T extends Record<string, any>> {
 		};
 	}
 
-	constructor(name: string, schema: SimpleSchema, grouped = false) {
+	constructor(name: string, schema: Schema, grouped = false) {
 		this.name = name;
 		this.schema = schema;
 
@@ -573,8 +573,8 @@ export default abstract class Chart<T extends Record<string, any>> {
 	}
 }
 
-export function convertLog(logSchema: SimpleSchema): SimpleSchema {
-	const v: SimpleSchema = JSON.parse(JSON.stringify(logSchema)); // copy
+export function convertLog(logSchema: Schema): Schema {
+	const v: Schema = JSON.parse(JSON.stringify(logSchema)); // copy
 	if (v.type === 'number') {
 		v.type = 'array';
 		v.items = {
diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts
index 38793412fa..9de4465eb9 100644
--- a/packages/backend/src/services/drive/add-file.ts
+++ b/packages/backend/src/services/drive/add-file.ts
@@ -101,13 +101,14 @@ async function save(file: DriveFile, path: string, name: string, type: string, h
 		file.accessKey = key;
 		file.thumbnailAccessKey = thumbnailKey;
 		file.webpublicAccessKey = webpublicKey;
+		file.webpublicType = alts.webpublic?.type ?? null;
 		file.name = name;
 		file.type = type;
 		file.md5 = hash;
 		file.size = size;
 		file.storedInternal = false;
 
-		return await DriveFiles.save(file);
+		return await DriveFiles.insert(file).then(x => DriveFiles.findOneOrFail(x.identifiers[0]));
 	} else { // use internal storage
 		const accessKey = uuid();
 		const thumbnailAccessKey = 'thumbnail-' + uuid();
@@ -135,12 +136,13 @@ async function save(file: DriveFile, path: string, name: string, type: string, h
 		file.accessKey = accessKey;
 		file.thumbnailAccessKey = thumbnailAccessKey;
 		file.webpublicAccessKey = webpublicAccessKey;
+		file.webpublicType = alts.webpublic?.type ?? null;
 		file.name = name;
 		file.type = type;
 		file.md5 = hash;
 		file.size = size;
 
-		return await DriveFiles.save(file);
+		return await DriveFiles.insert(file).then(x => DriveFiles.findOneOrFail(x.identifiers[0]));
 	}
 }
 
@@ -310,7 +312,7 @@ async function deleteOldFile(user: IRemoteUser) {
  * @param sensitive Mark file as sensitive
  * @return Created drive file
  */
-export default async function(
+export async function addFile(
 	user: { id: User['id']; host: User['host'] } | null,
 	path: string,
 	name: string | null = null,
@@ -436,7 +438,7 @@ export default async function(
 			file.type = info.type.mime;
 			file.storedInternal = false;
 
-			file = await DriveFiles.save(file);
+			file = await DriveFiles.insert(file).then(x => DriveFiles.findOneOrFail(x.identifiers[0]));
 		} catch (e) {
 			// duplicate key error (when already registered)
 			if (isDuplicateKeyValueError(e)) {
diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts
index 28f42bc344..a723c3e9ac 100644
--- a/packages/backend/src/services/drive/upload-from-url.ts
+++ b/packages/backend/src/services/drive/upload-from-url.ts
@@ -1,5 +1,5 @@
 import { URL } from 'url';
-import create from './add-file';
+import { addFile } from './add-file';
 import { User } from '@/models/entities/user';
 import { driveLogger } from './logger';
 import { createTemp } from '@/misc/create-temp';
@@ -41,7 +41,7 @@ export default async (
 	let error;
 
 	try {
-		driveFile = await create(user, path, name, comment, folderId, force, link, url, uri, sensitive);
+		driveFile = await addFile(user, path, name, comment, folderId, force, link, url, uri, sensitive);
 		logger.succ(`Got: ${driveFile.id}`);
 	} catch (e) {
 		error = e;
diff --git a/packages/backend/src/services/following/create.ts b/packages/backend/src/services/following/create.ts
index 45bd226588..bc5ac275b5 100644
--- a/packages/backend/src/services/following/create.ts
+++ b/packages/backend/src/services/following/create.ts
@@ -14,6 +14,7 @@ import { instanceChart, perUserFollowingChart } from '@/services/chart/index';
 import { genId } from '@/misc/gen-id';
 import { createNotification } from '../create-notification';
 import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error';
+import { Packed } from '@/misc/schema';
 
 const logger = new Logger('following/create');
 
@@ -89,8 +90,8 @@ export async function insertFollowingDoc(followee: { id: User['id']; host: User[
 		Users.pack(followee.id, follower, {
 			detail: true,
 		}).then(packed => {
-			publishUserEvent(follower.id, 'follow', packed);
-			publishMainStream(follower.id, 'follow', packed);
+			publishUserEvent(follower.id, 'follow', packed as Packed<"UserDetailedNotMe">);
+			publishMainStream(follower.id, 'follow', packed as Packed<"UserDetailedNotMe">);
 		});
 	}
 
diff --git a/packages/backend/src/services/note/reaction/create.ts b/packages/backend/src/services/note/reaction/create.ts
index e0baa6a096..47f46419dd 100644
--- a/packages/backend/src/services/note/reaction/create.ts
+++ b/packages/backend/src/services/note/reaction/create.ts
@@ -81,19 +81,15 @@ export default async (user: { id: User['id']; host: User['host']; }, note: Note,
 			name: decodedReaction.name,
 			host: decodedReaction.host,
 		},
-		select: ['name', 'host', 'url'],
+		select: ['name', 'host', 'originalUrl', 'publicUrl'],
 	});
 
-	if (emoji) {
-		emoji = {
-			name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`,
-			url: emoji.url,
-		} as any;
-	}
-
 	publishNoteStream(note.id, 'reacted', {
 		reaction: decodedReaction.reaction,
-		emoji: emoji,
+		emoji: emoji != null ? {
+			name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`,
+			url: emoji.publicUrl || emoji.originalUrl, // || emoji.originalUrl してるのは後方互換性のため
+		} : null,
 		userId: user.id,
 	});
 
diff --git a/packages/backend/src/services/stream.ts b/packages/backend/src/services/stream.ts
index 7d11ff3f45..c0cefe9af4 100644
--- a/packages/backend/src/services/stream.ts
+++ b/packages/backend/src/services/stream.ts
@@ -2,7 +2,6 @@ import { redisClient } from '../db/redis';
 import { User } from '@/models/entities/user';
 import { Note } from '@/models/entities/note';
 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/index';
 import { Antenna } from '@/models/entities/antenna';
@@ -20,8 +19,6 @@ import {
 	MessagingIndexStreamTypes,
 	MessagingStreamTypes,
 	NoteStreamTypes,
-	ReversiGameStreamTypes,
-	ReversiStreamTypes,
 	UserListStreamTypes,
 	UserStreamTypes,
 } from '@/server/api/stream/types';
@@ -90,14 +87,6 @@ class Publisher {
 		this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	};
 
-	public publishReversiStream = <K extends keyof ReversiStreamTypes>(userId: User['id'], type: K, value?: ReversiStreamTypes[K]): void => {
-		this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value);
-	};
-
-	public publishReversiGameStream = <K extends keyof ReversiGameStreamTypes>(gameId: ReversiGame['id'], type: K, value?: ReversiGameStreamTypes[K]): void => {
-		this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
-	};
-
 	public publishNotesStream = (note: Packed<'Note'>): void => {
 		this.publish('notesStream', null, note);
 	};
@@ -124,6 +113,4 @@ 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 publishAdminStream = publisher.publishAdminStream;
diff --git a/packages/backend/src/tools/add-emoji.ts b/packages/backend/src/tools/add-emoji.ts
deleted file mode 100644
index a3f4b54c7e..0000000000
--- a/packages/backend/src/tools/add-emoji.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { initDb } from '@/db/postgre';
-import { genId } from '@/misc/gen-id';
-
-async function main(name: string, url: string, alias?: string): Promise<any> {
-	await initDb();
-	const { Emojis } = await import('@/models/index');
-
-	const aliases = alias != null ? [ alias ] : [];
-
-	await Emojis.save({
-		id: genId(),
-		host: null,
-		name,
-		url,
-		aliases,
-		updatedAt: new Date(),
-	});
-}
-
-const args = process.argv.slice(2);
-const name = args[0];
-const url = args[1];
-
-if (!name) throw new Error('require name');
-if (!url) throw new Error('require url');
-
-main(name, url).then(() => {
-	console.log('success');
-	process.exit(0);
-}).catch(e => {
-	console.warn(e);
-	process.exit(1);
-});
diff --git a/packages/backend/test/docker-compose.yml b/packages/backend/test/docker-compose.yml
index c045e7c6c4..5f95bec4c0 100644
--- a/packages/backend/test/docker-compose.yml
+++ b/packages/backend/test/docker-compose.yml
@@ -2,12 +2,12 @@ version: "3"
 
 services:
   redistest:
-    image: redis:4.0-alpine
+    image: redis:6
     ports:
       - "127.0.0.1:56312:6379"
 
   dbtest:
-    image: postgres:12.2-alpine
+    image: postgres:13
     ports:
       - "127.0.0.1:54312:5432"
     environment:
diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock
index 16144b6d57..99e2e2306e 100644
--- a/packages/backend/yarn.lock
+++ b/packages/backend/yarn.lock
@@ -82,14 +82,14 @@
     pump "^3.0.0"
     secure-json-parse "^2.1.0"
 
-"@eslint/eslintrc@^1.0.4":
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.4.tgz#dfe0ff7ba270848d10c5add0715e04964c034b31"
-  integrity sha512-h8Vx6MdxwWI2WM8/zREHMoqdgLNXEL4QX3MWSVMdyNJGvXVOs+6lp+m2hc3FnuMHDc4poxFNI20vCk0OmI4G0Q==
+"@eslint/eslintrc@^1.0.5":
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.5.tgz#33f1b838dbf1f923bfa517e008362b78ddbbf318"
+  integrity sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==
   dependencies:
     ajv "^6.12.4"
     debug "^4.3.2"
-    espree "^9.0.0"
+    espree "^9.2.0"
     globals "^13.9.0"
     ignore "^4.0.6"
     import-fresh "^3.2.1"
@@ -97,19 +97,24 @@
     minimatch "^3.0.4"
     strip-json-comments "^3.1.1"
 
-"@humanwhocodes/config-array@^0.6.0":
-  version "0.6.0"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.6.0.tgz#b5621fdb3b32309d2d16575456cbc277fa8f021a"
-  integrity sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==
+"@gar/promisify@^1.0.1":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210"
+  integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw==
+
+"@humanwhocodes/config-array@^0.9.2":
+  version "0.9.2"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.2.tgz#68be55c737023009dfc5fe245d51181bb6476914"
+  integrity sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA==
   dependencies:
-    "@humanwhocodes/object-schema" "^1.2.0"
+    "@humanwhocodes/object-schema" "^1.2.1"
     debug "^4.1.1"
     minimatch "^3.0.4"
 
-"@humanwhocodes/object-schema@^1.2.0":
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
-  integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
+"@humanwhocodes/object-schema@^1.2.1":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
+  integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
 
 "@koa/cors@3.1.0":
   version "3.1.0"
@@ -134,6 +139,36 @@
     methods "^1.1.2"
     path-to-regexp "^6.1.0"
 
+"@node-redis/bloom@^1.0.0":
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/@node-redis/bloom/-/bloom-1.0.1.tgz#144474a0b7dc4a4b91badea2cfa9538ce0a1854e"
+  integrity sha512-mXEBvEIgF4tUzdIN89LiYsbi6//EdpFA7L8M+DHCvePXg+bfHWi+ct5VI6nHUFQE5+ohm/9wmgihCH3HSkeKsw==
+
+"@node-redis/client@^1.0.2":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@node-redis/client/-/client-1.0.2.tgz#7f09fb739675728fbc6e73536f7cd1be99bf7b8f"
+  integrity sha512-C+gkx68pmTnxfV+y4pzasvCH3s4UGHNOAUNhdJxGI27aMdnXNDZct7ffDHBL7bAZSGv9FSwCP5PeYvEIEKGbiA==
+  dependencies:
+    cluster-key-slot "1.1.0"
+    generic-pool "3.8.2"
+    redis-parser "3.0.0"
+    yallist "4.0.0"
+
+"@node-redis/json@^1.0.2":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@node-redis/json/-/json-1.0.2.tgz#8ad2d0f026698dc1a4238cc3d1eb099a3bee5ab8"
+  integrity sha512-qVRgn8WfG46QQ08CghSbY4VhHFgaTY71WjpwRBGEuqGPfWwfRcIf3OqSpR7Q/45X+v3xd8mvYjywqh0wqJ8T+g==
+
+"@node-redis/search@^1.0.2":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@node-redis/search/-/search-1.0.2.tgz#8cfc91006ea787df801d41410283e1f59027f818"
+  integrity sha512-gWhEeji+kTAvzZeguUNJdMSZNH2c5dv3Bci8Nn2f7VGuf6IvvwuZDSBOuOlirLVgayVuWzAG7EhwaZWK1VDnWQ==
+
+"@node-redis/time-series@^1.0.1":
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/@node-redis/time-series/-/time-series-1.0.1.tgz#703149f8fa4f6fff377c61a0873911e7c1ba5cc3"
+  integrity sha512-+nTn6EewVj3GlUXPuD3dgheWqo219jTxlo6R+pg24OeVvFHx9aFGGiyOgj3vBPhWUdRZ0xMcujXV5ki4fbLyMw==
+
 "@nodelib/fs.scandir@2.1.3":
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"
@@ -155,6 +190,14 @@
     "@nodelib/fs.scandir" "2.1.3"
     fastq "^1.6.0"
 
+"@npmcli/fs@^1.0.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.0.tgz#bec1d1b89c170d40e1b73ad6c943b0b75e7d2951"
+  integrity sha512-VhP1qZLXcrXRIaPoqb4YA55JQxLNF3jNR4T55IdOJa3+IFJKNYHtPvtXx8slmeMavj37vCzCfrqQM1vWLsYKLA==
+  dependencies:
+    "@gar/promisify" "^1.0.1"
+    semver "^7.3.5"
+
 "@npmcli/move-file@^1.0.1":
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674"
@@ -163,29 +206,30 @@
     mkdirp "^1.0.4"
     rimraf "^3.0.2"
 
-"@redocly/ajv@^8.6.2":
-  version "8.6.2"
-  resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.6.2.tgz#8c4e485e72f7864f91fae40093bed548ec2619b2"
-  integrity sha512-tU8fQs0D76ZKhJ2cWtnfQthWqiZgGBx0gH0+5D8JvaBEBaqA8foPPBt3Nonwr3ygyv5xrw2IzKWgIY86BlGs+w==
+"@redocly/ajv@^8.6.4":
+  version "8.6.4"
+  resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.6.4.tgz#94053e7a9d4146d1a4feacd3813892873f229a85"
+  integrity sha512-y9qNj0//tZtWB2jfXNK3BX18BSBp9zNR7KE7lMysVHwbZtY392OJCjm6Rb/h4UHH2r1AqjNEHFD6bRn+DqU9Mw==
   dependencies:
     fast-deep-equal "^3.1.1"
     json-schema-traverse "^1.0.0"
     require-from-string "^2.0.2"
     uri-js "^4.2.2"
 
-"@redocly/openapi-core@1.0.0-beta.54":
-  version "1.0.0-beta.54"
-  resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.0.0-beta.54.tgz#42575a849c4dd54b9d0c6413fb8ca547e087cd11"
-  integrity sha512-uYs0N1Trjkh7u8IMIuCU2VxCXhMyGWSZUkP/WNdTR1OgBUtvNdF9C32zoQV+hyCIH4gVu42ROHkjisy333ZX+w==
+"@redocly/openapi-core@1.0.0-beta.79":
+  version "1.0.0-beta.79"
+  resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.0.0-beta.79.tgz#7512b3507ab99dc78226f9069669c5302abb0969"
+  integrity sha512-do79vGt3iiHsaVG9LKY8dH+d1E7TLHr+3T+CQ1lqagtWVjYOxqGaoxAT8tRD7R1W0z8BmS4e2poNON6c1sxP5g==
   dependencies:
-    "@redocly/ajv" "^8.6.2"
+    "@redocly/ajv" "^8.6.4"
     "@types/node" "^14.11.8"
     colorette "^1.2.0"
     js-levenshtein "^1.1.6"
-    js-yaml "^3.14.1"
+    js-yaml "^4.1.0"
     lodash.isequal "^4.5.0"
     minimatch "^3.0.4"
     node-fetch "^2.6.1"
+    pluralize "^8.0.0"
     yaml-ast-parser "0.0.43"
 
 "@sindresorhus/is@^3.0.0":
@@ -245,11 +289,6 @@
   resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
   integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
 
-"@trysound/sax@0.2.0":
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
-  integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
-
 "@tsconfig/node10@^1.0.7":
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.7.tgz#1eb1de36c73478a2479cc661ef5af1c16d86d606"
@@ -295,12 +334,13 @@
     "@types/connect" "*"
     "@types/node" "*"
 
-"@types/bull@3.15.5":
-  version "3.15.5"
-  resolved "https://registry.yarnpkg.com/@types/bull/-/bull-3.15.5.tgz#a4459c127c5b10fb847531579a2cd5db35751366"
-  integrity sha512-XgJQWJ03jyKMfdoL8IAIoHIo7JkkL74kcxuujTONkSJswm0giIJ9kuVgDNHS0OvD+OiPNcFmbBl0H3scj2+A8A==
+"@types/bull@3.15.7":
+  version "3.15.7"
+  resolved "https://registry.yarnpkg.com/@types/bull/-/bull-3.15.7.tgz#a9d7fb332cc02dc021d0eb234b9604b356e9e6de"
+  integrity sha512-7NC7XN5NoS0A+leJ/dR69ZfKaegOlCZaii/xGgKnCyh1UYisRncibImb7VMwrc3OdJcbDJt6+4om70TeNl3J7g==
   dependencies:
     "@types/ioredis" "*"
+    "@types/redis" "^2.8.0"
 
 "@types/cacheable-request@^6.0.1":
   version "6.0.1"
@@ -356,10 +396,10 @@
   resolved "https://registry.yarnpkg.com/@types/disposable-email-domains/-/disposable-email-domains-1.0.2.tgz#0280f6b38fa7f14e54b056a434135ecd254483b1"
   integrity sha512-SDKwyYTjk3y5aZBxxc38yRecpJPjsqn57STz1bNxYYlv4k11bBe7QB8w4llXDTmQXKT1mFvgGmJv+8Zdu3YmJw==
 
-"@types/escape-regexp@0.0.0":
-  version "0.0.0"
-  resolved "https://registry.yarnpkg.com/@types/escape-regexp/-/escape-regexp-0.0.0.tgz#bff0225f9ef30d0dbdbe0e2a24283ee5342990c3"
-  integrity sha512-HTansGo4tJ7K7W9I9LBdQqnHtPB/Y7tlS+EMrkboaAQLsRPhRpHaqAHe01K1HVXM5e1u1IplRd8EBh+pJrp7Dg==
+"@types/escape-regexp@0.0.1":
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/@types/escape-regexp/-/escape-regexp-0.0.1.tgz#f1a977ccdf2ef059e9862bd3af5e92cbbe723e0e"
+  integrity sha512-ogj/ZTIdeFkiuxDwawYuZSIgC6suFGgBeZPr6Xs5lHEcvIXTjXGtH+/n8f1XhZhespaUwJ5LIGRICPji972FLw==
 
 "@types/eslint-scope@^3.7.0":
   version "3.7.0"
@@ -400,10 +440,10 @@
     "@types/qs" "*"
     "@types/serve-static" "*"
 
-"@types/fluent-ffmpeg@2.1.17":
-  version "2.1.17"
-  resolved "https://registry.yarnpkg.com/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.17.tgz#6958dda400fe1b33c21f3683db76905cb210d053"
-  integrity sha512-/bdvjKw/mtBHlJ2370d04nt4CsWqU5MrwS/NtO96V01jxitJ4+iq8OFNcqc5CegeV3TQOK3uueK02kvRK+zjUg==
+"@types/fluent-ffmpeg@2.1.20":
+  version "2.1.20"
+  resolved "https://registry.yarnpkg.com/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.20.tgz#3b5f42fc8263761d58284fa46ee6759a64ce54ac"
+  integrity sha512-B+OvhCdJ3LgEq2PhvWNOiB/EfwnXLElfMCgc4Z1K5zXgSfo9I6uGKwR/lqmNPFQuebNnes7re3gqkV77SyypLg==
   dependencies:
     "@types/node" "*"
 
@@ -442,15 +482,15 @@
   resolved "https://registry.yarnpkg.com/@types/is-url/-/is-url-1.2.30.tgz#85567e8bee4fee69202bc3448f9fb34b0d56c50a"
   integrity sha512-AnlNFwjzC8XLda5VjRl4ItSd8qp8pSNowvsut0WwQyBWHpOxjxRJm8iO6uETWqEyLdYdb9/1j+Qd9gQ4l5I4fw==
 
-"@types/js-yaml@4.0.4":
-  version "4.0.4"
-  resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.4.tgz#cc38781257612581a1a0eb25f1709d2b06812fce"
-  integrity sha512-AuHubXUmg0AzkXH0Mx6sIxeY/1C110mm/EkE/gB1sTRz3h2dao2W/63q42SlVST+lICxz5Oki2hzYA6+KnnieQ==
+"@types/js-yaml@4.0.5":
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138"
+  integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==
 
-"@types/jsdom@16.2.13":
-  version "16.2.13"
-  resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-16.2.13.tgz#126c8b7441b159d6234610a48de77b6066f1823f"
-  integrity sha512-8JQCjdeAidptSsOcRWk2iTm9wCcwn9l+kRG6k5bzUacrnm1ezV4forq0kWjUih/tumAeoG+OspOvQEbbRucBTw==
+"@types/jsdom@16.2.14":
+  version "16.2.14"
+  resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-16.2.14.tgz#26fe9da6a8870715b154bb84cd3b2e53433d8720"
+  integrity sha512-6BAy1xXEmMuHeAJ4Fv4yXKwBDTGTOseExKE3OaHiNycdHdZw59KfYzrt0DkDluvwmik1HRt6QS7bImxUmpSy+w==
   dependencies:
     "@types/node" "*"
     "@types/parse5" "*"
@@ -493,10 +533,10 @@
   dependencies:
     "@types/node" "*"
 
-"@types/koa-bodyparser@4.3.3":
-  version "4.3.3"
-  resolved "https://registry.yarnpkg.com/@types/koa-bodyparser/-/koa-bodyparser-4.3.3.tgz#9c7d4295576bc863d550002f732f1c57dd88cc58"
-  integrity sha512-/ileIpXsy1fFEzgZhZ07eZH8rAVL7jwuk/kaoVEfauO6s80g2LIDIJKEyDbuAL9S/BWflKzEC0PHD6aXkmaSbw==
+"@types/koa-bodyparser@4.3.5":
+  version "4.3.5"
+  resolved "https://registry.yarnpkg.com/@types/koa-bodyparser/-/koa-bodyparser-4.3.5.tgz#0c5fa44d7150202ffc16b89bd730ce1b6c7bc250"
+  integrity sha512-NRqqoTtt7cfdDk/KNo+EwCIKRuzPAu/wsaZ7tgIvSIBtNfxuZHYueaLoWdxX3ZftWavQv07NE46TcpyoZGqpgQ==
   dependencies:
     "@types/koa" "*"
 
@@ -577,10 +617,10 @@
     "@types/koa-compose" "*"
     "@types/node" "*"
 
-"@types/koa__cors@3.0.3":
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/@types/koa__cors/-/koa__cors-3.0.3.tgz#49d75813b443ba3d4da28ea6cf6244b7e99a3b23"
-  integrity sha512-74Xb4hJOPGKlrQ4PRBk1A/p0gfLpgbnpT0o67OMVbwyeMXvlBN+ZCRztAAmkKZs+8hKbgMutUlZVbA52Hr/0IA==
+"@types/koa__cors@3.1.1":
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/@types/koa__cors/-/koa__cors-3.1.1.tgz#198b5abbc425a672ae57c311b420bc270e65bdef"
+  integrity sha512-O7MBkCocnLrpEvkMrYAp17arUDS+KuS5bXMG/Z4aPSbrO7vrYB6YrqcsTD3Dp2OnAL3j4WME2k/x2kOcyzwNUw==
   dependencies:
     "@types/koa" "*"
 
@@ -591,10 +631,10 @@
   dependencies:
     "@types/koa" "*"
 
-"@types/koa__router@8.0.8":
-  version "8.0.8"
-  resolved "https://registry.yarnpkg.com/@types/koa__router/-/koa__router-8.0.8.tgz#b1e0e9a512498777d3366bbdf0e853df27ec831c"
-  integrity sha512-9pGCaDtzCsj4HJ8HmGuqzk8+s57sPj4njWd08GG5o92n5Xp9io2snc40CPpXFhoKcZ8OKhuu6ht4gNou9e1C2w==
+"@types/koa__router@8.0.11":
+  version "8.0.11"
+  resolved "https://registry.yarnpkg.com/@types/koa__router/-/koa__router-8.0.11.tgz#d7b37e6db934fc072ea1baa2ab92bc8ac4564f3e"
+  integrity sha512-WXgKWpBsbS14kzmzD9LeFapOIa678h7zvUHxDwXwSx4ETKXhXLVUAToX6jZ/U7EihM7qwyD9W/BZvB0MRu7MTQ==
   dependencies:
     "@types/koa" "*"
 
@@ -613,23 +653,22 @@
   resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323"
   integrity sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw==
 
-"@types/node-fetch@2.5.12":
-  version "2.5.12"
-  resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.12.tgz#8a6f779b1d4e60b7a57fb6fd48d84fb545b9cc66"
-  integrity sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==
+"@types/node-fetch@3.0.3":
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-3.0.3.tgz#9d969c9a748e841554a40ee435d26e53fa3ee899"
+  integrity sha512-HhggYPH5N+AQe/OmN6fmhKmRRt2XuNJow+R3pQwJxOOF9GuwM7O2mheyGeIrs5MOIeNjDEdgdoyHBOrFeJBR3g==
   dependencies:
-    "@types/node" "*"
-    form-data "^3.0.0"
+    node-fetch "*"
 
 "@types/node@*":
   version "16.6.2"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.2.tgz#331b7b9f8621c638284787c5559423822fdffc50"
   integrity sha512-LSw8TZt12ZudbpHc6EkIyDM3nHVWKYrAvGy6EAJfNfjusbwnThqjqxUKKRwuV3iWYeW/LYMzNgaq3MaLffQ2xA==
 
-"@types/node@16.11.7":
-  version "16.11.7"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.7.tgz#36820945061326978c42a01e56b61cd223dfdc42"
-  integrity sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==
+"@types/node@17.0.10":
+  version "17.0.10"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.10.tgz#616f16e9d3a2a3d618136b1be244315d95bd7cab"
+  integrity sha512-S/3xB4KzyFxYGCppyDt68yzBU9ysL88lSdIah4D6cptdcltc4NCPCAMc0+PCpg/lLIyC7IPvj2Z52OJWeIUkog==
 
 "@types/node@^14.11.8":
   version "14.17.9"
@@ -665,20 +704,20 @@
   resolved "https://registry.yarnpkg.com/@types/portscanner/-/portscanner-2.1.1.tgz#89d5094e16f3d941f20f3889dfa5d3a164b3dd3b"
   integrity sha512-1NsVIbgBKvrqxwtMN0V6CLji1ERwKSI/RWz0J3y++CzSwYNGBStCfpIFgxV3ZwxsDR5PoZqoUWhwraDm+Ztn0Q==
 
-"@types/pug@2.0.5":
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.5.tgz#69bc700934dd473c7ab97270bd2dbacefe562231"
-  integrity sha512-LOnASQoeNZMkzexRuyqcBBDZ6rS+rQxUMkmj5A0PkhhiSZivLIuz6Hxyr1mkGoEZEkk66faROmpMi4fFkrKsBA==
+"@types/pug@2.0.6":
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.6.tgz#f830323c88172e66826d0bde413498b61054b5a6"
+  integrity sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==
 
 "@types/punycode@2.1.0":
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/@types/punycode/-/punycode-2.1.0.tgz#89e4f3d09b3f92e87a80505af19be7e0c31d4e83"
   integrity sha512-PG5aLpW6PJOeV2fHRslP4IOMWn+G+Uq8CfnyJ+PDS8ndCbU+soO+fB3NKCKo0p/Jh2Y4aPaiQZsrOXFdzpcA6g==
 
-"@types/qrcode@1.4.1":
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.4.1.tgz#0689f400c3a95d2db040c99c99834faa09ee9dc1"
-  integrity sha512-vxMyr7JM7tYPxu8vUE83NiosWX5DZieCyYeJRoOIg0pAkyofCBzknJ2ycUZkPGDFis2RS8GN/BeJLnRnAPxeCA==
+"@types/qrcode@1.4.2":
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.4.2.tgz#7d7142d6fa9921f195db342ed08b539181546c74"
+  integrity sha512-7uNT9L4WQTNJejHTSTdaJhfBSCN73xtXaHFyBJ8TSwiLhe4PRuTue7Iph0s2nG9R/ifUaSnGhLUOZavlBEqDWQ==
   dependencies:
     "@types/node" "*"
 
@@ -697,10 +736,12 @@
   resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
   integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
 
-"@types/ratelimiter@3.4.2":
-  version "3.4.2"
-  resolved "https://registry.yarnpkg.com/@types/ratelimiter/-/ratelimiter-3.4.2.tgz#adf1a6d0cbe72d42207efc510a9170602e23456c"
-  integrity sha512-iz+yyY+ViphaM8ZwrX1mUQzelIeC59zyaaLKTJ0YVOOCkCpIYpaysiIM4z5Xv9HdXYqIb80S+DhH7J22A0rW2w==
+"@types/ratelimiter@3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@types/ratelimiter/-/ratelimiter-3.4.3.tgz#2159c234b9d75bcc2be39379f05c6af0a5e4a3b7"
+  integrity sha512-B/IRdHGcttRsDeDJ4+VFjzRA1mzqTxsYlg2X8GLQtTgRUMhQQc+bL8zFmuHhZkK4oA+Ldb4K1NogspNDxevWBA==
+  dependencies:
+    "@types/redis" "^2.8.0"
 
 "@types/readable-stream@^2.3.9":
   version "2.3.9"
@@ -710,7 +751,14 @@
     "@types/node" "*"
     safe-buffer "*"
 
-"@types/redis@2.8.32":
+"@types/redis@4.0.11":
+  version "4.0.11"
+  resolved "https://registry.yarnpkg.com/@types/redis/-/redis-4.0.11.tgz#0bb4c11ac9900a21ad40d2a6768ec6aaf651c0e1"
+  integrity sha512-bI+gth8La8Wg/QCR1+V1fhrL9+LZUSWfcqpOj2Kc80ZQ4ffbdL173vQd5wovmoV9i071FU9oP2g6etLuEwb6Rg==
+  dependencies:
+    redis "*"
+
+"@types/redis@^2.8.0":
   version "2.8.32"
   resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.32.tgz#1d3430219afbee10f8cfa389dad2571a05ecfb11"
   integrity sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==
@@ -736,10 +784,10 @@
   dependencies:
     "@types/node" "*"
 
-"@types/sanitize-html@2.5.0":
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.5.0.tgz#bfef58fbcf2674b20ffcc23c3506faa68c3a13e3"
-  integrity sha512-PeFIEZsO9m1+ACJlXUaimgrR+5DEDiIXhz7Hso307jmq5Yz0lb5kDp8LiTr5dMMMliC/jNNx/qds7Zoxa4zexw==
+"@types/sanitize-html@2.6.2":
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.6.2.tgz#9c47960841b9def1e4c9dfebaaab010a3f6e97b9"
+  integrity sha512-7Lu2zMQnmHHQGKXVvCOhSziQMpa+R2hMHFefzbYoYMHeaXR0uXqNeOc3JeQQQ8/6Xa2Br/P1IQTLzV09xxAiUQ==
   dependencies:
     htmlparser2 "^6.0.0"
 
@@ -756,10 +804,10 @@
     "@types/express-serve-static-core" "*"
     "@types/mime" "*"
 
-"@types/sharp@0.29.3":
-  version "0.29.3"
-  resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.29.3.tgz#54ceb81b68bb99a0e62db2b52f4fb0bea84878ac"
-  integrity sha512-83Xp05eK2hvfNnmKLr2Fz0C2A0jrr2TnSLqKRbkLTYuAu+Erj6mKQLoEMGafE73Om8p3q3ryZxtHFM/7hy4Adg==
+"@types/sharp@0.29.5":
+  version "0.29.5"
+  resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.29.5.tgz#9c7032d30d138ad16dde6326beaff2af757b91b3"
+  integrity sha512-3TC+S3H5RwnJmLYMHrcdfNjz/CaApKmujjY9b6PU/pE6n0qfooi99YqXGWoW8frU9EWYj/XTI35Pzxa+ThAZ5Q==
   dependencies:
     "@types/node" "*"
 
@@ -773,10 +821,10 @@
   resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
   integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==
 
-"@types/speakeasy@2.0.6":
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/@types/speakeasy/-/speakeasy-2.0.6.tgz#12540f7b64d08180393ae2c5c8c280866a85da61"
-  integrity sha512-2wIXZp5yJUddhsSZarYCZIakCvzwQgTVdtT29DYVdFzc0cHttanaQx9THRhtjY4kDqVaF2jhyFOEofozOioFdQ==
+"@types/speakeasy@2.0.7":
+  version "2.0.7"
+  resolved "https://registry.yarnpkg.com/@types/speakeasy/-/speakeasy-2.0.7.tgz#cb087c501b3eef744a1ae620c19812dd1c3b2f3f"
+  integrity sha512-JEcOhN2SQCoX86ZfiZEe8px84sVJtivBXMZfOVyARTYEj0hrwwbj1nF0FwEL3nJSoEV6uTbcdLllMKBgAYHWCQ==
   dependencies:
     "@types/node" "*"
 
@@ -795,10 +843,10 @@
   resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.3.tgz#ed4a0901f954b126e6a914b4839c77462d56e706"
   integrity sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==
 
-"@types/tmp@0.2.2":
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.2.tgz#424537a3b91828cb26aaf697f21ae3cd1b69f7e7"
-  integrity sha512-MhSa0yylXtVMsyT8qFpHA1DLHj4DvQGH5ntxrhHSh8PxUVNi35Wk+P5hVgqbO2qZqOotqr9jaoPRL+iRjWYm/A==
+"@types/tmp@0.2.3":
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.3.tgz#908bfb113419fd6a42273674c00994d40902c165"
+  integrity sha512-dDZH/tXzwjutnuk4UacGgFRwV+JSLaXL1ikvidfJprkb7L9Nx1njcRHHmi3Dsvt7pgqqTEeucQuOrWHPFgzVHA==
 
 "@types/tough-cookie@*":
   version "4.0.0"
@@ -812,10 +860,10 @@
   dependencies:
     source-map "^0.6.1"
 
-"@types/uuid@8.3.1":
-  version "8.3.1"
-  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f"
-  integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==
+"@types/uuid@8.3.4":
+  version "8.3.4"
+  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
+  integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
 
 "@types/web-push@3.3.2":
   version "3.3.2"
@@ -869,10 +917,10 @@
   dependencies:
     "@types/node" "*"
 
-"@types/ws@8.2.0":
-  version "8.2.0"
-  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.0.tgz#75faefbe2328f3b833cb8dc640658328990d04f3"
-  integrity sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==
+"@types/ws@8.2.2":
+  version "8.2.2"
+  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.2.tgz#7c5be4decb19500ae6b3d563043cd407bf366c21"
+  integrity sha512-NOn5eIcgWLOo6qW8AcuLZ7G8PycXu0xTxxkS6Q18VWFxgPUSOwV0pBj2a/4viNZVu25i7RIB7GttdkAIUUXOOg==
   dependencies:
     "@types/node" "*"
 
@@ -881,13 +929,14 @@
   resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.2.tgz#808c9fa7e4517274ed555fa158f2de4b4f468e71"
   integrity sha512-HrCIVMLjE1MOozVoD86622S7aunluLb2PJdPfb3nYiEtohm8mIB/vyv0Fd37AdeMFrTUQXEunw78YloMA3Qilg==
 
-"@typescript-eslint/eslint-plugin@5.3.1":
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.3.1.tgz#d8ff412f10f54f6364e7fd7c1e70eb6767f434c3"
-  integrity sha512-cFImaoIr5Ojj358xI/SDhjog57OK2NqlpxwdcgyxDA3bJlZcJq5CPzUXtpD7CxI2Hm6ATU7w5fQnnkVnmwpHqw==
+"@typescript-eslint/eslint-plugin@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.10.0.tgz#e90afea96dff8620892ad216b0e4ccdf8ee32d3a"
+  integrity sha512-XXVKnMsq2fuu9K2KsIxPUGqb6xAImz8MEChClbXmE3VbveFtBUU5bzM6IPVWqzyADIgdkS2Ws/6Xo7W2TeZWjQ==
   dependencies:
-    "@typescript-eslint/experimental-utils" "5.3.1"
-    "@typescript-eslint/scope-manager" "5.3.1"
+    "@typescript-eslint/scope-manager" "5.10.0"
+    "@typescript-eslint/type-utils" "5.10.0"
+    "@typescript-eslint/utils" "5.10.0"
     debug "^4.3.2"
     functional-red-black-tree "^1.0.1"
     ignore "^5.1.8"
@@ -895,94 +944,69 @@
     semver "^7.3.5"
     tsutils "^3.21.0"
 
-"@typescript-eslint/experimental-utils@5.3.1":
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.3.1.tgz#bbd8f9b67b4d5fdcb9d2f90297d8fcda22561e05"
-  integrity sha512-RgFn5asjZ5daUhbK5Sp0peq0SSMytqcrkNfU4pnDma2D8P3ElZ6JbYjY8IMSFfZAJ0f3x3tnO3vXHweYg0g59w==
+"@typescript-eslint/parser@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.10.0.tgz#8f59e036f5f1cffc178cacbd5ccdd02aeb96c91c"
+  integrity sha512-pJB2CCeHWtwOAeIxv8CHVGJhI5FNyJAIpx5Pt72YkK3QfEzt6qAlXZuyaBmyfOdM62qU0rbxJzNToPTVeJGrQw==
+  dependencies:
+    "@typescript-eslint/scope-manager" "5.10.0"
+    "@typescript-eslint/types" "5.10.0"
+    "@typescript-eslint/typescript-estree" "5.10.0"
+    debug "^4.3.2"
+
+"@typescript-eslint/scope-manager@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.10.0.tgz#bb5d872e8b9e36203908595507fbc4d3105329cb"
+  integrity sha512-tgNgUgb4MhqK6DoKn3RBhyZ9aJga7EQrw+2/OiDk5hKf3pTVZWyqBi7ukP+Z0iEEDMF5FDa64LqODzlfE4O/Dg==
+  dependencies:
+    "@typescript-eslint/types" "5.10.0"
+    "@typescript-eslint/visitor-keys" "5.10.0"
+
+"@typescript-eslint/type-utils@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.10.0.tgz#8524b9479c19c478347a7df216827e749e4a51e5"
+  integrity sha512-TzlyTmufJO5V886N+hTJBGIfnjQDQ32rJYxPaeiyWKdjsv2Ld5l8cbS7pxim4DeNs62fKzRSt8Q14Evs4JnZyQ==
+  dependencies:
+    "@typescript-eslint/utils" "5.10.0"
+    debug "^4.3.2"
+    tsutils "^3.21.0"
+
+"@typescript-eslint/types@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.10.0.tgz#beb3cb345076f5b088afe996d57bcd1dfddaa75c"
+  integrity sha512-wUljCgkqHsMZbw60IbOqT/puLfyqqD5PquGiBo1u1IS3PLxdi3RDGlyf032IJyh+eQoGhz9kzhtZa+VC4eWTlQ==
+
+"@typescript-eslint/typescript-estree@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.0.tgz#4be24a3dea0f930bb1397c46187d0efdd955a224"
+  integrity sha512-x+7e5IqfwLwsxTdliHRtlIYkgdtYXzE0CkFeV6ytAqq431ZyxCFzNMNR5sr3WOlIG/ihVZr9K/y71VHTF/DUQA==
+  dependencies:
+    "@typescript-eslint/types" "5.10.0"
+    "@typescript-eslint/visitor-keys" "5.10.0"
+    debug "^4.3.2"
+    globby "^11.0.4"
+    is-glob "^4.0.3"
+    semver "^7.3.5"
+    tsutils "^3.21.0"
+
+"@typescript-eslint/utils@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.10.0.tgz#c3d152a85da77c400e37281355561c72fb1b5a65"
+  integrity sha512-IGYwlt1CVcFoE2ueW4/ioEwybR60RAdGeiJX/iDAw0t5w0wK3S7QncDwpmsM70nKgGTuVchEWB8lwZwHqPAWRg==
   dependencies:
     "@types/json-schema" "^7.0.9"
-    "@typescript-eslint/scope-manager" "5.3.1"
-    "@typescript-eslint/types" "5.3.1"
-    "@typescript-eslint/typescript-estree" "5.3.1"
+    "@typescript-eslint/scope-manager" "5.10.0"
+    "@typescript-eslint/types" "5.10.0"
+    "@typescript-eslint/typescript-estree" "5.10.0"
     eslint-scope "^5.1.1"
     eslint-utils "^3.0.0"
 
-"@typescript-eslint/parser@5.1.0":
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.1.0.tgz#6c7f837d210d2bc0a811e7ea742af414f4e00908"
-  integrity sha512-vx1P+mhCtYw3+bRHmbalq/VKP2Y3gnzNgxGxfEWc6OFpuEL7iQdAeq11Ke3Rhy8NjgB+AHsIWEwni3e+Y7djKA==
+"@typescript-eslint/visitor-keys@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.0.tgz#770215497ad67cd15a572b52089991d5dfe06281"
+  integrity sha512-GMxj0K1uyrFLPKASLmZzCuSddmjZVbVj3Ouy5QVuIGKZopxvOr24JsS7gruz6C3GExE01mublZ3mIBOaon9zuQ==
   dependencies:
-    "@typescript-eslint/scope-manager" "5.1.0"
-    "@typescript-eslint/types" "5.1.0"
-    "@typescript-eslint/typescript-estree" "5.1.0"
-    debug "^4.3.2"
-
-"@typescript-eslint/scope-manager@5.1.0":
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.1.0.tgz#6f1f26ad66a8f71bbb33b635e74fec43f76b44df"
-  integrity sha512-yYlyVjvn5lvwCL37i4hPsa1s0ORsjkauhTqbb8MnpvUs7xykmcjGqwlNZ2Q5QpoqkJ1odlM2bqHqJwa28qV6Tw==
-  dependencies:
-    "@typescript-eslint/types" "5.1.0"
-    "@typescript-eslint/visitor-keys" "5.1.0"
-
-"@typescript-eslint/scope-manager@5.3.1":
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.3.1.tgz#3cfbfbcf5488fb2a9a6fbbe97963ee1e8d419269"
-  integrity sha512-XksFVBgAq0Y9H40BDbuPOTUIp7dn4u8oOuhcgGq7EoDP50eqcafkMVGrypyVGvDYHzjhdUCUwuwVUK4JhkMAMg==
-  dependencies:
-    "@typescript-eslint/types" "5.3.1"
-    "@typescript-eslint/visitor-keys" "5.3.1"
-
-"@typescript-eslint/types@5.1.0":
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.1.0.tgz#a8a75ddfc611660de6be17d3ad950302385607a9"
-  integrity sha512-sEwNINVxcB4ZgC6Fe6rUyMlvsB2jvVdgxjZEjQUQVlaSPMNamDOwO6/TB98kFt4sYYfNhdhTPBEQqNQZjMMswA==
-
-"@typescript-eslint/types@5.3.1":
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.3.1.tgz#afaa715b69ebfcfde3af8b0403bf27527912f9b7"
-  integrity sha512-bG7HeBLolxKHtdHG54Uac750eXuQQPpdJfCYuw4ZI3bZ7+GgKClMWM8jExBtp7NSP4m8PmLRM8+lhzkYnSmSxQ==
-
-"@typescript-eslint/typescript-estree@5.1.0":
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.1.0.tgz#132aea34372df09decda961cb42457433aa6e83d"
-  integrity sha512-SSz+l9YrIIsW4s0ZqaEfnjl156XQ4VRmJsbA0ZE1XkXrD3cRpzuZSVCyqeCMR3EBjF27IisWakbBDGhGNIOvfQ==
-  dependencies:
-    "@typescript-eslint/types" "5.1.0"
-    "@typescript-eslint/visitor-keys" "5.1.0"
-    debug "^4.3.2"
-    globby "^11.0.4"
-    is-glob "^4.0.3"
-    semver "^7.3.5"
-    tsutils "^3.21.0"
-
-"@typescript-eslint/typescript-estree@5.3.1":
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.3.1.tgz#50cc4bfb93dc31bc75e08ae52e29fcb786d606ec"
-  integrity sha512-PwFbh/PKDVo/Wct6N3w+E4rLZxUDgsoII/GrWM2A62ETOzJd4M6s0Mu7w4CWsZraTbaC5UQI+dLeyOIFF1PquQ==
-  dependencies:
-    "@typescript-eslint/types" "5.3.1"
-    "@typescript-eslint/visitor-keys" "5.3.1"
-    debug "^4.3.2"
-    globby "^11.0.4"
-    is-glob "^4.0.3"
-    semver "^7.3.5"
-    tsutils "^3.21.0"
-
-"@typescript-eslint/visitor-keys@5.1.0":
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.1.0.tgz#e01a01b27eb173092705ae983aa1451bd1842630"
-  integrity sha512-uqNXepKBg81JVwjuqAxYrXa1Ql/YDzM+8g/pS+TCPxba0wZttl8m5DkrasbfnmJGHs4lQ2jTbcZ5azGhI7kK+w==
-  dependencies:
-    "@typescript-eslint/types" "5.1.0"
-    eslint-visitor-keys "^3.0.0"
-
-"@typescript-eslint/visitor-keys@5.3.1":
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.3.1.tgz#c2860ff22939352db4f3806f34b21d8ad00588ba"
-  integrity sha512-3cHUzUuVTuNHx0Gjjt5pEHa87+lzyqOiHXy/Gz+SJOCW1mpw9xQHIIEwnKn+Thph1mgWyZ90nboOcSuZr/jTTQ==
-  dependencies:
-    "@typescript-eslint/types" "5.3.1"
+    "@typescript-eslint/types" "5.10.0"
     eslint-visitor-keys "^3.0.0"
 
 "@ungap/promise-all-settled@1.1.2":
@@ -1189,12 +1213,12 @@ acorn@^8.4.1:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c"
   integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==
 
-acorn@^8.5.0:
-  version "8.5.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2"
-  integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==
+acorn@^8.7.0:
+  version "8.7.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
+  integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
 
-agent-base@6:
+agent-base@6, agent-base@^6.0.2:
   version "6.0.2"
   resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
   integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
@@ -1233,12 +1257,7 @@ ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5:
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
-alphanum-sort@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
-  integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
-
-ansi-colors@4.1.1, ansi-colors@^4.1.1:
+ansi-colors@4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
   integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
@@ -1253,11 +1272,6 @@ ansi-regex@^3.0.0:
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
   integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
 
-ansi-regex@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
-  integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
-
 ansi-regex@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
@@ -1268,7 +1282,7 @@ ansi-regex@^5.0.1:
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
   integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
 
-ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+ansi-styles@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
   integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
@@ -1311,6 +1325,11 @@ aproba@^1.0.3:
   resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
   integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
 
+"aproba@^1.0.3 || ^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
+  integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
+
 archiver-utils@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2"
@@ -1340,6 +1359,14 @@ archiver@5.3.0:
     tar-stream "^2.2.0"
     zip-stream "^4.1.0"
 
+are-we-there-yet@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c"
+  integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==
+  dependencies:
+    delegates "^1.0.0"
+    readable-stream "^3.6.0"
+
 are-we-there-yet@~1.1.2:
   version "1.1.5"
   resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
@@ -1353,13 +1380,6 @@ arg@^4.1.0:
   resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
   integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
 
-argparse@^1.0.7:
-  version "1.0.10"
-  resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
-  integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
-  dependencies:
-    sprintf-js "~1.0.2"
-
 argparse@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
@@ -1456,27 +1476,27 @@ autwh@0.1.0:
   dependencies:
     oauth "0.9.15"
 
-aws-sdk@2.1013.0:
-  version "2.1013.0"
-  resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1013.0.tgz#85babc473b0bc90cc1160eb48baf616ddb86e346"
-  integrity sha512-TXxkp/meAdofpC15goFpNuur7fvh/mcMRfHJoP1jYzTtD0wcoB4FK16GLcny0uDYgkQgZuiO9QYv3Rq5bhGCqQ==
+aws-sdk@2.1061.0:
+  version "2.1061.0"
+  resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1061.0.tgz#79c75e6856e5a59e0857d0d066a8ff5ff5e0d752"
+  integrity sha512-T29yV+EPo4Fis9hAArxAXS/u6utKnlBq3DEu85LTSIA8i6e6Xg7e9u7Rveo8DmrlVrf7EGCNThaeF9WERHnwLg==
   dependencies:
     buffer "4.9.2"
     events "1.1.1"
     ieee754 "1.1.13"
-    jmespath "0.15.0"
+    jmespath "0.16.0"
     querystring "0.2.0"
     sax "1.2.1"
     url "0.10.3"
     uuid "3.3.2"
     xml2js "0.4.19"
 
-axios@^0.19.2:
-  version "0.19.2"
-  resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
-  integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
+axios@^0.24.0:
+  version "0.24.0"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6"
+  integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==
   dependencies:
-    follow-redirects "1.5.10"
+    follow-redirects "^1.14.4"
 
 babel-walk@3.0.0-canary-5:
   version "3.0.0-canary-5"
@@ -1522,6 +1542,11 @@ big-integer@^1.6.16:
   resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e"
   integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==
 
+big-integer@^1.6.17:
+  version "1.6.51"
+  resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686"
+  integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==
+
 big.js@^5.2.2:
   version "5.2.2"
   resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
@@ -1532,6 +1557,14 @@ binary-extensions@^2.0.0:
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
   integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
 
+binary@~0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79"
+  integrity sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=
+  dependencies:
+    buffers "~0.1.1"
+    chainsaw "~0.1.0"
+
 bl@^4.0.1, bl@^4.0.3:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489"
@@ -1546,6 +1579,11 @@ bluebird@^3.7.2:
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
   integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
 
+bluebird@~3.4.1:
+  version "3.4.7"
+  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
+  integrity sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=
+
 blurhash@1.1.4:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.4.tgz#a7010ceb3019cd2c9809b17c910ebf6175d29244"
@@ -1556,7 +1594,7 @@ bn.js@^4.0.0:
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
   integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
 
-boolbase@^1.0.0, boolbase@~1.0.0:
+boolbase@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
   integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
@@ -1576,16 +1614,17 @@ braces@^3.0.1, braces@~3.0.2:
   dependencies:
     fill-range "^7.0.1"
 
-broadcast-channel@4.5.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-4.5.0.tgz#d4717c493e219908fcb7f2f9078fe0baf95b77c1"
-  integrity sha512-jp+VPlQ1HyR0CM3uIYUrdpXupBvhTMFRkjR6mEmt5W4HaGDPFEzrO2Jqvi2PZ6zCC4zwLeco7CC5EUJPrVH8Tw==
+broadcast-channel@4.9.0:
+  version "4.9.0"
+  resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-4.9.0.tgz#8af337d4ea19aeb6b819ec2eb3dda942b28c724c"
+  integrity sha512-xWzFb3wrOZGJF2kOSs2D3KvHXdLDMVb+WypEIoNvwblcHgUBydVy65pDJ9RS4WN9Kyvs0UVQuCCzfKme0G6Qjw==
   dependencies:
     "@babel/runtime" "^7.16.0"
     detect-node "^2.1.0"
     microseconds "0.2.0"
     nano-time "1.0.0"
     oblivious-set "1.0.0"
+    p-queue "6.6.2"
     rimraf "3.0.2"
     unload "2.3.1"
 
@@ -1599,7 +1638,7 @@ browser-stdout@1.3.1:
   resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
   integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
 
-browserslist@^4.0.0, browserslist@^4.14.5:
+browserslist@^4.14.5:
   version "4.16.3"
   resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717"
   integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==
@@ -1610,48 +1649,6 @@ browserslist@^4.0.0, browserslist@^4.14.5:
     escalade "^3.1.1"
     node-releases "^1.1.70"
 
-browserslist@^4.16.0:
-  version "4.16.4"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.4.tgz#7ebf913487f40caf4637b892b268069951c35d58"
-  integrity sha512-d7rCxYV8I9kj41RH8UKYnvDYCRENUlHRgyXy/Rhr/1BaeLGfiCptEdFE8MIrvGfWbBFNjVYx76SQWvNX1j+/cQ==
-  dependencies:
-    caniuse-lite "^1.0.30001208"
-    colorette "^1.2.2"
-    electron-to-chromium "^1.3.712"
-    escalade "^3.1.1"
-    node-releases "^1.1.71"
-
-browserslist@^4.16.6:
-  version "4.16.6"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2"
-  integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==
-  dependencies:
-    caniuse-lite "^1.0.30001219"
-    colorette "^1.2.2"
-    electron-to-chromium "^1.3.723"
-    escalade "^3.1.1"
-    node-releases "^1.1.71"
-
-bs-logger@0.x:
-  version "0.2.6"
-  resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8"
-  integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==
-  dependencies:
-    fast-json-stable-stringify "2.x"
-
-buffer-alloc-unsafe@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
-  integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
-
-buffer-alloc@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
-  integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
-  dependencies:
-    buffer-alloc-unsafe "^1.1.0"
-    buffer-fill "^1.0.0"
-
 buffer-crc32@^0.2.1, buffer-crc32@^0.2.13:
   version "0.2.13"
   resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
@@ -1662,21 +1659,16 @@ buffer-equal-constant-time@1.0.1:
   resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
   integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
 
-buffer-fill@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
-  integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
-
-buffer-from@1.x:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
-  integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
-
-buffer-from@^1.0.0, buffer-from@^1.1.1:
+buffer-from@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
   integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
 
+buffer-indexof-polyfill@~1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c"
+  integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==
+
 buffer-writer@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04"
@@ -1691,7 +1683,7 @@ buffer@4.9.2:
     ieee754 "^1.1.4"
     isarray "^1.0.0"
 
-buffer@^5.4.3, buffer@^5.5.0:
+buffer@^5.5.0:
   version "5.6.0"
   resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
   integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==
@@ -1707,6 +1699,11 @@ buffer@^6.0.3:
     base64-js "^1.3.1"
     ieee754 "^1.2.1"
 
+buffers@~0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb"
+  integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s=
+
 bufferutil@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.1.tgz#3a177e8e5819a1243fe16b63a199951a7ad8d4a7"
@@ -1714,10 +1711,10 @@ bufferutil@^4.0.1:
   dependencies:
     node-gyp-build "~3.7.0"
 
-bull@4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/bull/-/bull-4.1.0.tgz#ff8f628694e7dbbdf89b6a6860a1f33e990527fd"
-  integrity sha512-rQcLuAmzZIv1dHJO/yKrWu497xcTxMpYeTEBfpStrJFZ1UZpBSGHSx+defbtFVeGEeY8Pn0aMGRvRtldUBVUyQ==
+bull@4.2.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/bull/-/bull-4.2.1.tgz#c5a7e1496c7903274ce90192e4e5cb18f6c866c0"
+  integrity sha512-YkCQZMOub++siHw3SbYYXZ5xGEn6Tt3BPoCVq/irPNCxUqUYzta8yDlXyyAsfMKMVj0M7PcnynUabfMf9PFpOA==
   dependencies:
     cron-parser "^2.13.0"
     debuglog "^1.0.0"
@@ -1725,9 +1722,7 @@ bull@4.1.0:
     ioredis "^4.27.0"
     lodash "^4.17.21"
     p-timeout "^3.2.0"
-    promise.prototype.finally "^3.1.2"
     semver "^7.3.2"
-    util.promisify "^1.0.1"
     uuid "^8.3.0"
 
 busboy@^0.2.11:
@@ -1743,11 +1738,12 @@ bytes@3.1.0, bytes@^3.1.0:
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
   integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
 
-cacache@^15.0.5:
-  version "15.1.0"
-  resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.1.0.tgz#164c2f857ee606e4cc793c63018fefd0ea5eba7b"
-  integrity sha512-mfx0C+mCfWjD1PnwQ9yaOrwG1ou9FkKnx0SvzUHWdFt7r7GaRtzT+9M8HAvLu62zIHtnpQ/1m93nWNDCckJGXQ==
+cacache@^15.2.0:
+  version "15.3.0"
+  resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb"
+  integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==
   dependencies:
+    "@npmcli/fs" "^1.0.0"
     "@npmcli/move-file" "^1.0.1"
     chownr "^2.0.0"
     fs-minipass "^2.0.0"
@@ -1825,36 +1821,11 @@ camelcase@^6.0.0:
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
   integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
 
-caniuse-api@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
-  integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
-  dependencies:
-    browserslist "^4.0.0"
-    caniuse-lite "^1.0.0"
-    lodash.memoize "^4.1.2"
-    lodash.uniq "^4.5.0"
-
-caniuse-lite@^1.0.0:
-  version "1.0.30001048"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001048.tgz#4bb4f1bc2eb304e5e1154da80b93dee3f1cf447e"
-  integrity sha512-g1iSHKVxornw0K8LG9LLdf+Fxnv7T1Z+mMsf0/YYLclQX4Cd522Ap0Lrw6NFqHgezit78dtyWxzlV2Xfc7vgRg==
-
 caniuse-lite@^1.0.30001181:
   version "1.0.30001191"
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001191.tgz#bacb432b6701f690c8c5f7c680166b9a9f0843d9"
   integrity sha512-xJJqzyd+7GCJXkcoBiQ1GuxEiOBCLQ0aVW9HMekifZsAVGdj5eJ4mFB9fEhSHipq9IOk/QXFJUiIr9lZT+EsGw==
 
-caniuse-lite@^1.0.30001208:
-  version "1.0.30001208"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz#a999014a35cebd4f98c405930a057a0d75352eb9"
-  integrity sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA==
-
-caniuse-lite@^1.0.30001219:
-  version "1.0.30001230"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz#8135c57459854b2240b57a4a6786044bdc5a9f71"
-  integrity sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ==
-
 canonicalize@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-1.0.1.tgz#657b4f3fa38a6ecb97a9e5b7b26d7a19cc6e0da9"
@@ -1875,6 +1846,13 @@ cbor@8.1.0:
   dependencies:
     nofilter "^3.1.0"
 
+chainsaw@~0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98"
+  integrity sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=
+  dependencies:
+    traverse ">=0.3.0 <0.4"
+
 chalk@4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72"
@@ -1920,23 +1898,6 @@ character-parser@^2.2.0:
   dependencies:
     is-regex "^1.0.3"
 
-chart.js@3.6.0:
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.6.0.tgz#a87fce8431d4e7c5523d721f487f53aada1e42fe"
-  integrity sha512-iOzzDKePL+bj+ccIsVAgWQehCXv8xOKGbaU2fO/myivH736zcx535PGJzQGanvcSGVOqX6yuLZsN3ygcQ35UgQ==
-
-chartjs-adapter-date-fns@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz#5e53b2f660b993698f936f509c86dddf9ed44c6b"
-  integrity sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==
-
-chartjs-plugin-zoom@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/chartjs-plugin-zoom/-/chartjs-plugin-zoom-1.1.1.tgz#8a28923a17fcb5eb57a0dc94c5113bf402677647"
-  integrity sha512-1q54WOzK7FtAjkbemQeqvmFUV0btNYIQny2HbQ6Awq9wUtCz7Zmj6vIgp3C1DYMQwN0nqgpC3vnApqiwI7cSdQ==
-  dependencies:
-    hammerjs "^2.0.8"
-
 cheerio@0.22.0:
   version "0.22.0"
   resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e"
@@ -2008,14 +1969,14 @@ cli-highlight@2.1.11, cli-highlight@^2.1.11:
     parse5-htmlparser2-tree-adapter "^6.0.0"
     yargs "^16.0.0"
 
-cliui@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
-  integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
+cliui@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
+  integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
   dependencies:
-    string-width "^3.1.0"
-    strip-ansi "^5.2.0"
-    wrap-ansi "^5.1.0"
+    string-width "^4.2.0"
+    strip-ansi "^6.0.0"
+    wrap-ansi "^6.2.0"
 
 cliui@^7.0.2:
   version "7.0.4"
@@ -2033,7 +1994,7 @@ clone-response@^1.0.2:
   dependencies:
     mimic-response "^1.0.0"
 
-cluster-key-slot@^1.1.0:
+cluster-key-slot@1.1.0, cluster-key-slot@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
   integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==
@@ -2100,6 +2061,11 @@ color-string@^1.6.0:
     color-name "^1.0.0"
     simple-swizzle "^0.2.2"
 
+color-support@^1.1.2:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
+  integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
+
 color@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/color/-/color-4.0.1.tgz#21df44cd10245a91b1ccf5ba031609b0e10e7d67"
@@ -2108,12 +2074,7 @@ color@^4.0.1:
     color-convert "^2.0.1"
     color-string "^1.6.0"
 
-colord@^2.9.1:
-  version "2.9.1"
-  resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.1.tgz#c961ea0efeb57c9f0f4834458f26cb9cc4a3f90e"
-  integrity sha512-4LBMSt09vR0uLnPVkOUBnmxgoaeN4ewRbx801wY/bXcltXfpR/G46OdWn96XpYmCWuYvO46aBZP4NgX8HpNAcw==
-
-colorette@^1.2.0, colorette@^1.2.1, colorette@^1.2.2:
+colorette@^1.2.0, colorette@^1.2.1:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
   integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
@@ -2130,11 +2091,6 @@ commander@^2.19.0, commander@^2.20.0:
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
 
-commander@^7.2.0:
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
-  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
-
 commander@^8.2.0:
   version "8.3.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
@@ -2182,7 +2138,7 @@ config-chain@^1.1.12:
     ini "^1.3.4"
     proto-list "~1.2.1"
 
-console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
   integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
@@ -2202,7 +2158,14 @@ constantinople@^4.0.1:
     "@babel/parser" "^7.6.0"
     "@babel/types" "^7.6.1"
 
-content-disposition@0.5.3, content-disposition@~0.5.2:
+content-disposition@0.5.4:
+  version "0.5.4"
+  resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
+  integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
+  dependencies:
+    safe-buffer "5.2.1"
+
+content-disposition@~0.5.2:
   version "0.5.3"
   resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
   integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
@@ -2277,43 +2240,6 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
-css-color-names@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-1.0.1.tgz#6ff7ee81a823ad46e020fa2fd6ab40a887e2ba67"
-  integrity sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA==
-
-css-declaration-sorter@^6.0.3:
-  version "6.0.3"
-  resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.0.3.tgz#9dfd8ea0df4cc7846827876fafb52314890c21a9"
-  integrity sha512-52P95mvW1SMzuRZegvpluT6yEv0FqQusydKQPZsNN5Q7hh8EwQvN8E2nwuJ16BBvNN6LcoIZXu/Bk58DAhrrxw==
-  dependencies:
-    timsort "^0.3.0"
-
-css-loader@6.5.1:
-  version "6.5.1"
-  resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.5.1.tgz#0c43d4fbe0d97f699c91e9818cb585759091d1b1"
-  integrity sha512-gEy2w9AnJNnD9Kuo4XAP9VflW/ujKoS9c/syO+uWMlm5igc7LysKzPXaDoR2vroROkSwsTS2tGr1yGGEbZOYZQ==
-  dependencies:
-    icss-utils "^5.1.0"
-    postcss "^8.2.15"
-    postcss-modules-extract-imports "^3.0.0"
-    postcss-modules-local-by-default "^4.0.0"
-    postcss-modules-scope "^3.0.0"
-    postcss-modules-values "^4.0.0"
-    postcss-value-parser "^4.1.0"
-    semver "^7.3.5"
-
-css-select@^4.1.3:
-  version "4.1.3"
-  resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067"
-  integrity sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==
-  dependencies:
-    boolbase "^1.0.0"
-    css-what "^5.0.0"
-    domhandler "^4.2.0"
-    domutils "^2.6.0"
-    nth-check "^2.0.0"
-
 css-select@~1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
@@ -2324,86 +2250,11 @@ css-select@~1.2.0:
     domutils "1.5.1"
     nth-check "~1.0.1"
 
-css-tree@^1.1.2, css-tree@^1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
-  integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
-  dependencies:
-    mdn-data "2.0.14"
-    source-map "^0.6.1"
-
 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-what@^5.0.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe"
-  integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==
-
-cssesc@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
-  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
-
-cssnano-preset-default@^5.1.6:
-  version "5.1.6"
-  resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.1.6.tgz#1bdb83be6a6b1fee6dc5e9ec2e61286bcadcc7a6"
-  integrity sha512-X2nDeNGBXc0486oHjT2vSj+TdeyVsxRvJUxaOH50hOM6vSDLkKd0+59YXpSZRInJ4sNtBOykS4KsPfhdrU/35w==
-  dependencies:
-    css-declaration-sorter "^6.0.3"
-    cssnano-utils "^2.0.1"
-    postcss-calc "^8.0.0"
-    postcss-colormin "^5.2.1"
-    postcss-convert-values "^5.0.2"
-    postcss-discard-comments "^5.0.1"
-    postcss-discard-duplicates "^5.0.1"
-    postcss-discard-empty "^5.0.1"
-    postcss-discard-overridden "^5.0.1"
-    postcss-merge-longhand "^5.0.3"
-    postcss-merge-rules "^5.0.2"
-    postcss-minify-font-values "^5.0.1"
-    postcss-minify-gradients "^5.0.3"
-    postcss-minify-params "^5.0.1"
-    postcss-minify-selectors "^5.1.0"
-    postcss-normalize-charset "^5.0.1"
-    postcss-normalize-display-values "^5.0.1"
-    postcss-normalize-positions "^5.0.1"
-    postcss-normalize-repeat-style "^5.0.1"
-    postcss-normalize-string "^5.0.1"
-    postcss-normalize-timing-functions "^5.0.1"
-    postcss-normalize-unicode "^5.0.1"
-    postcss-normalize-url "^5.0.2"
-    postcss-normalize-whitespace "^5.0.1"
-    postcss-ordered-values "^5.0.2"
-    postcss-reduce-initial "^5.0.1"
-    postcss-reduce-transforms "^5.0.1"
-    postcss-svgo "^5.0.3"
-    postcss-unique-selectors "^5.0.1"
-
-cssnano-utils@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-2.0.1.tgz#8660aa2b37ed869d2e2f22918196a9a8b6498ce2"
-  integrity sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==
-
-cssnano@5.0.10:
-  version "5.0.10"
-  resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.0.10.tgz#92207eb7c9c6dc08d318050726f9fad0adf7220b"
-  integrity sha512-YfNhVJJ04imffOpbPbXP2zjIoByf0m8E2c/s/HnvSvjXgzXMfgopVjAEGvxYOjkOpWuRQDg/OZFjO7WW94Ri8w==
-  dependencies:
-    cssnano-preset-default "^5.1.6"
-    is-resolvable "^1.1.0"
-    lilconfig "^2.0.3"
-    yaml "^1.10.2"
-
-csso@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
-  integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
-  dependencies:
-    css-tree "^1.1.2"
-
 cssom@^0.4.4:
   version "0.4.4"
   resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
@@ -2441,6 +2292,11 @@ data-uri-to-buffer@^3.0.1:
   resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636"
   integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==
 
+data-uri-to-buffer@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b"
+  integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==
+
 data-urls@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
@@ -2450,11 +2306,6 @@ data-urls@^2.0.0:
     whatwg-mimetype "^2.3.0"
     whatwg-url "^8.0.0"
 
-date-fns@2.25.0:
-  version "2.25.0"
-  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.25.0.tgz#8c5c8f1d958be3809a9a03f4b742eba894fc5680"
-  integrity sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w==
-
 dateformat@4.5.1:
   version "4.5.1"
   resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.5.1.tgz#c20e7a9ca77d147906b6dc2261a8be0a5bd2173c"
@@ -2481,13 +2332,6 @@ debug@4.3.3:
   dependencies:
     ms "2.1.2"
 
-debug@=3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
-  integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
-  dependencies:
-    ms "2.0.0"
-
 debug@^3.2.6:
   version "3.2.6"
   resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
@@ -2529,13 +2373,6 @@ decimal.js@^10.2.1:
   resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3"
   integrity sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==
 
-decompress-response@^4.2.0:
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986"
-  integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==
-  dependencies:
-    mimic-response "^2.0.0"
-
 decompress-response@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
@@ -2543,17 +2380,15 @@ decompress-response@^6.0.0:
   dependencies:
     mimic-response "^3.1.0"
 
-deep-email-validator@0.1.18:
-  version "0.1.18"
-  resolved "https://registry.yarnpkg.com/deep-email-validator/-/deep-email-validator-0.1.18.tgz#a072a93f28e11863cc6b9ca3ae964e0e45b3ece8"
-  integrity sha512-eo2WEUidQvppg6Qdek8iwOqmXvaxRJ2D2VJKbIOwUgLZNFveDDdJMBsFc+yq0S+lILEUcmzrJRrCWbyoe7QUzQ==
+deep-email-validator@0.1.21:
+  version "0.1.21"
+  resolved "https://registry.yarnpkg.com/deep-email-validator/-/deep-email-validator-0.1.21.tgz#5d0120fe1aeae83ab7cb39378a40a381b681219f"
+  integrity sha512-DBAmMzbr+MAubXQ+TS9tZuPwLcdKscb8YzKZiwoLqF3NmaeEgXvSSHhZ0EXOFeKFE2FNWC4mNXCyiQ/JdFXUwg==
   dependencies:
     "@types/disposable-email-domains" "^1.0.1"
-    axios "^0.19.2"
-    disposable-email-domains "^1.0.53"
-    lodash "^4.17.15"
+    axios "^0.24.0"
+    disposable-email-domains "^1.0.59"
     mailcheck "^1.1.1"
-    ts-jest "^25.2.1"
 
 deep-equal@~1.0.1:
   version "1.0.1"
@@ -2580,7 +2415,7 @@ defer-to-connect@^2.0.0:
   resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.0.tgz#83d6b199db041593ac84d781b5222308ccf4c2c1"
   integrity sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg==
 
-define-properties@^1.1.2, define-properties@^1.1.3:
+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==
@@ -2667,10 +2502,10 @@ dir-glob@^3.0.1:
   dependencies:
     path-type "^4.0.0"
 
-disposable-email-domains@^1.0.53:
-  version "1.0.58"
-  resolved "https://registry.yarnpkg.com/disposable-email-domains/-/disposable-email-domains-1.0.58.tgz#ac9c879c02c4f0898bfb6c0c80b959c0b0b7bc51"
-  integrity sha512-frnNCPqTjk6t/sosPoco6EIFHbP9SazHQkeltJNfZeUyNgewaVf+kFjEfVkVDVd436Vln43YElJPb8JozhBs7Q==
+disposable-email-domains@^1.0.59:
+  version "1.0.59"
+  resolved "https://registry.yarnpkg.com/disposable-email-domains/-/disposable-email-domains-1.0.59.tgz#8b3670667dcef9d0d21b224de283d56d468913c2"
+  integrity sha512-45NbOP1Oboaddf0pD5mGnT+1msEifY6VUcR9Msq4zBHk2EeGv9PxiwuoynIfdGID1BSFR3U3egPfMbERkqXxUQ==
 
 doctrine@^2.1.0:
   version "2.1.0"
@@ -2775,7 +2610,7 @@ domutils@^1.5.1:
     dom-serializer "0"
     domelementtype "1"
 
-domutils@^2.5.2, domutils@^2.6.0:
+domutils@^2.5.2:
   version "2.8.0"
   resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
   integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
@@ -2789,6 +2624,13 @@ dotenv@^8.2.0:
   resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
   integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
 
+duplexer2@~0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
+  integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=
+  dependencies:
+    readable-stream "^2.0.2"
+
 ecc-jsbn@~0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@@ -2824,21 +2666,6 @@ electron-to-chromium@^1.3.649:
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.672.tgz#3a6e335016dab4bc584d5292adc4f98f54541f6a"
   integrity sha512-gFQe7HBb0lbOMqK2GAS5/1F+B0IMdYiAgB9OT/w1F4M7lgJK2aNOMNOM622aEax+nS1cTMytkiT0uMOkbtFmHw==
 
-electron-to-chromium@^1.3.712:
-  version "1.3.717"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.717.tgz#78d4c857070755fb58ab64bcc173db1d51cbc25f"
-  integrity sha512-OfzVPIqD1MkJ7fX+yTl2nKyOE4FReeVfMCzzxQS+Kp43hZYwHwThlGP+EGIZRXJsxCM7dqo8Y65NOX/HP12iXQ==
-
-electron-to-chromium@^1.3.723:
-  version "1.3.742"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.742.tgz#7223215acbbd3a5284962ebcb6df85d88b95f200"
-  integrity sha512-ihL14knI9FikJmH2XUIDdZFWJxvr14rPSdOhJ7PpS27xbz8qmaRwCwyg/bmFwjWKmWK9QyamiCZVCvXm5CH//Q==
-
-emoji-regex@^7.0.1:
-  version "7.0.3"
-  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
-  integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
-
 emoji-regex@^8.0.0:
   version "8.0.0"
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
@@ -2849,6 +2676,11 @@ emojis-list@^3.0.0:
   resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
   integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
 
+encode-utf8@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda"
+  integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==
+
 encodeurl@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
@@ -2884,13 +2716,6 @@ enhanced-resolve@^5.7.0:
     graceful-fs "^4.2.4"
     tapable "^2.2.0"
 
-enquirer@^2.3.5:
-  version "2.3.6"
-  resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
-  integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
-  dependencies:
-    ansi-colors "^4.1.1"
-
 entities@^1.1.1, entities@~1.1.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
@@ -2911,23 +2736,6 @@ err-code@^2.0.2:
   resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
   integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==
 
-es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
-  version "1.17.5"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9"
-  integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==
-  dependencies:
-    es-to-primitive "^1.2.1"
-    function-bind "^1.1.1"
-    has "^1.0.3"
-    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"
-    object.assign "^4.1.0"
-    string.prototype.trimleft "^2.1.1"
-    string.prototype.trimright "^2.1.1"
-
 es-abstract@^1.19.0, es-abstract@^1.19.1:
   version "1.19.1"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3"
@@ -3039,43 +2847,32 @@ eslint-import-resolver-node@^0.3.6:
     debug "^3.2.7"
     resolve "^1.20.0"
 
-eslint-module-utils@^2.7.1:
-  version "2.7.1"
-  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.1.tgz#b435001c9f8dd4ab7f6d0efcae4b9696d4c24b7c"
-  integrity sha512-fjoetBXQZq2tSTWZ9yWVl2KuFrTZZH3V+9iD1V1RfpDgxzJR+mPd/KZmMiA8gbPqdBzpNiEHOuT7IYEWxrH0zQ==
+eslint-module-utils@^2.7.2:
+  version "2.7.2"
+  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.2.tgz#1d0aa455dcf41052339b63cada8ab5fd57577129"
+  integrity sha512-zquepFnWCY2ISMFwD/DqzaM++H+7PDzOpUvotJWm/y1BAFt5R4oeULgdrTejKqLkz7MA/tgstsUMNYc7wNdTrg==
   dependencies:
     debug "^3.2.7"
     find-up "^2.1.0"
-    pkg-dir "^2.0.0"
 
-eslint-plugin-import@2.25.3:
-  version "2.25.3"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.3.tgz#a554b5f66e08fb4f6dc99221866e57cfff824766"
-  integrity sha512-RzAVbby+72IB3iOEL8clzPLzL3wpDrlwjsTBAQXgyp5SeTqqY+0bFubwuo+y/HLhNZcXV4XqTBO4LGsfyHIDXg==
+eslint-plugin-import@2.25.4:
+  version "2.25.4"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz#322f3f916a4e9e991ac7af32032c25ce313209f1"
+  integrity sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==
   dependencies:
     array-includes "^3.1.4"
     array.prototype.flat "^1.2.5"
     debug "^2.6.9"
     doctrine "^2.1.0"
     eslint-import-resolver-node "^0.3.6"
-    eslint-module-utils "^2.7.1"
+    eslint-module-utils "^2.7.2"
     has "^1.0.3"
     is-core-module "^2.8.0"
     is-glob "^4.0.3"
     minimatch "^3.0.4"
     object.values "^1.1.5"
     resolve "^1.20.0"
-    tsconfig-paths "^3.11.0"
-
-eslint-plugin-vue@8.0.3:
-  version "8.0.3"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-8.0.3.tgz#791cc4543940319e612ea61a1d779e8c87cf749a"
-  integrity sha512-Rlhhy5ltzde0sRwSkqHuNePTXLMMaJ5+qsQubM4RYloYsQ8cXlnJT5MDaCzSirkGADipOHtmQXIbbPFAzUrADg==
-  dependencies:
-    eslint-utils "^3.0.0"
-    natural-compare "^1.4.0"
-    semver "^7.3.5"
-    vue-eslint-parser "^8.0.1"
+    tsconfig-paths "^3.12.0"
 
 eslint-scope@^5.1.1:
   version "5.1.1"
@@ -3085,10 +2882,10 @@ eslint-scope@^5.1.1:
     esrecurse "^4.3.0"
     estraverse "^4.1.1"
 
-eslint-scope@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-6.0.0.tgz#9cf45b13c5ac8f3d4c50f46a5121f61b3e318978"
-  integrity sha512-uRDL9MWmQCkaFus8RF5K9/L/2fn+80yoW3jkD53l4shjCh26fCtvJGasxjUqP5OT87SYTxCVA3BwTUzuELx9kA==
+eslint-scope@^7.1.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.0.tgz#c1f6ea30ac583031f203d65c73e723b01298f153"
+  integrity sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==
   dependencies:
     esrecurse "^4.3.0"
     estraverse "^5.2.0"
@@ -3110,24 +2907,28 @@ eslint-visitor-keys@^3.0.0:
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.0.0.tgz#e32e99c6cdc2eb063f204eda5db67bfe58bb4186"
   integrity sha512-mJOZa35trBTb3IyRmo8xmKBZlxf+N7OnUl4+ZhJHs/r+0770Wh/LEACE2pqMGMe27G/4y8P2bYGk4J70IC5k1Q==
 
-eslint@8.2.0:
-  version "8.2.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.2.0.tgz#44d3fb506d0f866a506d97a0fc0e90ee6d06a815"
-  integrity sha512-erw7XmM+CLxTOickrimJ1SiF55jiNlVSp2qqm0NuBWPtHYQCegD5ZMaW0c3i5ytPqL+SSLaCxdvQXFPLJn+ABw==
+eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz#6fbb166a6798ee5991358bc2daa1ba76cc1254a1"
+  integrity sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==
+
+eslint@8.7.0:
+  version "8.7.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.7.0.tgz#22e036842ee5b7cf87b03fe237731675b4d3633c"
+  integrity sha512-ifHYzkBGrzS2iDU7KjhCAVMGCvF6M3Xfs8X8b37cgrUlDt6bWRTpRh6T/gtSXv1HJ/BUGgmjvNvOEGu85Iif7w==
   dependencies:
-    "@eslint/eslintrc" "^1.0.4"
-    "@humanwhocodes/config-array" "^0.6.0"
+    "@eslint/eslintrc" "^1.0.5"
+    "@humanwhocodes/config-array" "^0.9.2"
     ajv "^6.10.0"
     chalk "^4.0.0"
     cross-spawn "^7.0.2"
     debug "^4.3.2"
     doctrine "^3.0.0"
-    enquirer "^2.3.5"
     escape-string-regexp "^4.0.0"
-    eslint-scope "^6.0.0"
+    eslint-scope "^7.1.0"
     eslint-utils "^3.0.0"
-    eslint-visitor-keys "^3.0.0"
-    espree "^9.0.0"
+    eslint-visitor-keys "^3.2.0"
+    espree "^9.3.0"
     esquery "^1.4.0"
     esutils "^2.0.2"
     fast-deep-equal "^3.1.3"
@@ -3135,7 +2936,7 @@ eslint@8.2.0:
     functional-red-black-tree "^1.0.1"
     glob-parent "^6.0.1"
     globals "^13.6.0"
-    ignore "^4.0.6"
+    ignore "^5.2.0"
     import-fresh "^3.0.0"
     imurmurhash "^0.1.4"
     is-glob "^4.0.0"
@@ -3146,9 +2947,7 @@ eslint@8.2.0:
     minimatch "^3.0.4"
     natural-compare "^1.4.0"
     optionator "^0.9.1"
-    progress "^2.0.0"
     regexpp "^3.2.0"
-    semver "^7.2.1"
     strip-ansi "^6.0.1"
     strip-json-comments "^3.1.0"
     text-table "^0.2.0"
@@ -3159,16 +2958,16 @@ esm@^3.2.22:
   resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
   integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
 
-espree@^9.0.0:
-  version "9.0.0"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-9.0.0.tgz#e90a2965698228502e771c7a58489b1a9d107090"
-  integrity sha512-r5EQJcYZ2oaGbeR0jR0fFVijGOcwai07/690YRXLINuhmVeRY4UKSAsQPe/0BNuDgwP7Ophoc1PRsr2E3tkbdQ==
+espree@^9.2.0, espree@^9.3.0:
+  version "9.3.0"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.0.tgz#c1240d79183b72aaee6ccfa5a90bc9111df085a8"
+  integrity sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ==
   dependencies:
-    acorn "^8.5.0"
+    acorn "^8.7.0"
     acorn-jsx "^5.3.1"
-    eslint-visitor-keys "^3.0.0"
+    eslint-visitor-keys "^3.1.0"
 
-esprima@^4.0.0, esprima@^4.0.1:
+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==
@@ -3212,7 +3011,7 @@ event-target-shim@^5.0.0:
   resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
   integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
 
-eventemitter3@4.0.7, eventemitter3@^4.0.7:
+eventemitter3@4.0.7, eventemitter3@^4.0.4, eventemitter3@^4.0.7:
   version "4.0.7"
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
   integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
@@ -3305,7 +3104,7 @@ fast-glob@^3.1.1:
     micromatch "^4.0.2"
     picomatch "^2.2.1"
 
-fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
+fast-json-stable-stringify@^2.0.0:
   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==
@@ -3339,6 +3138,14 @@ fetch-blob@^2.1.1:
   resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-2.1.2.tgz#a7805db1361bd44c1ef62bb57fb5fe8ea173ef3c"
   integrity sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==
 
+fetch-blob@^3.1.2, fetch-blob@^3.1.4:
+  version "3.1.4"
+  resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.1.4.tgz#e8c6567f80ad7fc22fd302e7dcb72bafde9c1717"
+  integrity sha512-Eq5Xv5+VlSrYWEqKrusxY1C3Hm/hjeAsCGVG3ft7pZahlUAChpGZT/Ms1WmSLnEAisEXszjzu/s+ce6HZB2VHA==
+  dependencies:
+    node-domexception "^1.0.0"
+    web-streams-polyfill "^3.0.3"
+
 file-entry-cache@^6.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@@ -3385,12 +3192,13 @@ find-up@^2.1.0:
   dependencies:
     locate-path "^2.0.0"
 
-find-up@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
-  integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+find-up@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+  integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
   dependencies:
-    locate-path "^3.0.0"
+    locate-path "^5.0.0"
+    path-exists "^4.0.0"
 
 findup-sync@^4.0.0:
   version "4.0.0"
@@ -3428,12 +3236,10 @@ fluent-ffmpeg@2.1.2:
     async ">=0.2.9"
     which "^1.1.1"
 
-follow-redirects@1.5.10:
-  version "1.5.10"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
-  integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
-  dependencies:
-    debug "=3.1.0"
+follow-redirects@^1.14.4:
+  version "1.14.7"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
+  integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==
 
 form-data@^3.0.0:
   version "3.0.0"
@@ -3444,6 +3250,13 @@ form-data@^3.0.0:
     combined-stream "^1.0.8"
     mime-types "^2.1.12"
 
+formdata-polyfill@^4.0.10:
+  version "4.0.10"
+  resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
+  integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
+  dependencies:
+    fetch-blob "^3.1.2"
+
 fresh@~0.5.2:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
@@ -3480,6 +3293,16 @@ fsevents@~2.1.2:
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
   integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
 
+fstream@^1.0.12:
+  version "1.0.12"
+  resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045"
+  integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==
+  dependencies:
+    graceful-fs "^4.1.2"
+    inherits "~2.0.0"
+    mkdirp ">=0.5 0"
+    rimraf "2"
+
 function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -3490,6 +3313,21 @@ functional-red-black-tree@^1.0.1:
   resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
   integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
 
+gauge@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.0.tgz#afba07aa0374a93c6219603b1fb83eaa2264d8f8"
+  integrity sha512-F8sU45yQpjQjxKkm1UOAhf0U/O0aFt//Fl7hsrNVto+patMHjs7dPI9mFOGUKbhrgKm0S3EjW3scMFuQmWSROw==
+  dependencies:
+    ansi-regex "^5.0.1"
+    aproba "^1.0.3 || ^2.0.0"
+    color-support "^1.1.2"
+    console-control-strings "^1.0.0"
+    has-unicode "^2.0.1"
+    signal-exit "^3.0.0"
+    string-width "^4.2.3"
+    strip-ansi "^6.0.1"
+    wide-align "^1.1.2"
+
 gauge@~2.7.3:
   version "2.7.4"
   resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@@ -3504,6 +3342,11 @@ gauge@~2.7.3:
     strip-ansi "^3.0.1"
     wide-align "^1.1.0"
 
+generic-pool@3.8.2:
+  version "3.8.2"
+  resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.8.2.tgz#aab4f280adb522fdfbdc5e5b64d718d3683f04e9"
+  integrity sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==
+
 get-caller-file@^2.0.1, get-caller-file@^2.0.5:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
@@ -3690,7 +3533,7 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.4:
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
   integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
 
-graceful-fs@^4.2.0:
+graceful-fs@^4.2.0, graceful-fs@^4.2.2:
   version "4.2.8"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
   integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
@@ -3705,11 +3548,6 @@ growl@1.10.5:
   resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
   integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==
 
-hammerjs@^2.0.8:
-  version "2.0.8"
-  resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.8.tgz#04ef77862cff2bb79d30f7692095930222bf60f1"
-  integrity sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=
-
 has-bigints@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
@@ -3725,7 +3563,7 @@ 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, has-symbols@^1.0.1:
+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==
@@ -3742,7 +3580,7 @@ has-tostringtag@^1.0.0:
   dependencies:
     has-symbols "^1.0.2"
 
-has-unicode@^2.0.0:
+has-unicode@^2.0.0, has-unicode@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
   integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
@@ -3865,13 +3703,13 @@ http-proxy-agent@^4.0.1:
     agent-base "6"
     debug "4"
 
-http-signature@1.3.5:
-  version "1.3.5"
-  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.5.tgz#9f19496ffbf3227298d7b5f156e0e1a948678683"
-  integrity sha512-NwoTQYSJoFt34jSBbwzDHDofoA61NGXzu6wXh95o1Ry62EnmKjXb/nR/RknLeZ3G/uGwrlKNY2z7uPt+Cdl7Tw==
+http-signature@1.3.6:
+  version "1.3.6"
+  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9"
+  integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==
   dependencies:
     assert-plus "^1.0.0"
-    jsprim "^1.2.2"
+    jsprim "^2.0.2"
     sshpk "^1.14.1"
 
 http2-wrapper@^1.0.0-beta.5.0:
@@ -3936,11 +3774,6 @@ iconv-lite@^0.6.2:
   dependencies:
     safer-buffer ">= 2.1.2 < 3.0.0"
 
-icss-utils@^5.0.0, icss-utils@^5.1.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
-  integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
-
 ieee754@1.1.13, ieee754@^1.1.4:
   version "1.1.13"
   resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
@@ -3966,6 +3799,11 @@ ignore@^5.1.8:
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.9.tgz#9ec1a5cbe8e1446ec60d4420060d43aa6e7382fb"
   integrity sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==
 
+ignore@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
+  integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
+
 import-fresh@^3.0.0, import-fresh@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
@@ -3984,11 +3822,6 @@ indent-string@^4.0.0:
   resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
   integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
 
-indexes-of@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
-  integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
-
 infer-owner@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
@@ -4007,7 +3840,7 @@ inflight@^1.0.4:
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
+inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -4022,10 +3855,10 @@ ini@^1.3.4, ini@~1.3.0:
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
   integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==
 
-install-artifact-from-github@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.2.0.tgz#adcbd123c16a4337ec44ea76d0ebf253cc16b074"
-  integrity sha512-3OxCPcY55XlVM3kkfIpeCgmoSKnMsz2A3Dbhsq0RXpIknKQmrX1YiznCeW9cD2ItFmDxziA3w6Eg8d80AoL3oA==
+install-artifact-from-github@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.3.0.tgz#cab6ff821976b8a35b0c079da19a727c90381a40"
+  integrity sha512-iT8v1GwOAX0pPXifF/5ihnMhHOCo3OeK7z3TQa4CtSNCIg8k0UxqBEk9jRwz8OP68hHXvJ2gxRa89KYHtBkqGA==
 
 internal-slot@^1.0.3:
   version "1.0.3"
@@ -4083,11 +3916,6 @@ ipaddr.js@^2.0.1:
   resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0"
   integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==
 
-is-absolute-url@^3.0.3:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698"
-  integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==
-
 is-arrayish@^0.3.1:
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
@@ -4120,7 +3948,7 @@ is-buffer@^1.1.5:
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
   integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
 
-is-callable@^1.1.4, is-callable@^1.1.5:
+is-callable@^1.1.4:
   version "1.1.5"
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
   integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==
@@ -4266,7 +4094,7 @@ is-promise@^2.0.0:
   resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
   integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
 
-is-regex@^1.0.3, is-regex@^1.0.5:
+is-regex@^1.0.3:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae"
   integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==
@@ -4281,11 +4109,6 @@ is-regex@^1.1.4:
     call-bind "^1.0.2"
     has-tostringtag "^1.0.0"
 
-is-resolvable@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
-  integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
-
 is-shared-array-buffer@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6"
@@ -4303,10 +4126,10 @@ is-string@^1.0.5, is-string@^1.0.7:
   dependencies:
     has-tostringtag "^1.0.0"
 
-is-svg@4.3.1:
-  version "4.3.1"
-  resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-4.3.1.tgz#8c63ec8c67c8c7f0a8de0a71c8c7d58eccf4406b"
-  integrity sha512-h2CGs+yPUyvkgTJQS9cJzo9lYK06WgRiXUqBBHtglSzVKAuH4/oWsqk7LGfbSa1hGk9QcZ0SyQtVggvBA8LZXA==
+is-svg@4.3.2:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-4.3.2.tgz#a119e9932e1af53f6be1969d1790d6cc5fd947d3"
+  integrity sha512-mM90duy00JGMyjqIVHu9gNTjywdZV+8qNasX8cm/EEYZ53PHDgajvbBwNVvty5dwSAxLUD3p3bdo+7sR/UMrpw==
   dependencies:
     fast-xml-parser "^3.19.0"
 
@@ -4356,11 +4179,6 @@ isarray@^1.0.0, isarray@~1.0.0:
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
   integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
 
-isarray@^2.0.1:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
-  integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
-
 isexe@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -4375,10 +4193,10 @@ jest-worker@^26.6.2:
     merge-stream "^2.0.0"
     supports-color "^7.0.0"
 
-jmespath@0.15.0:
-  version "0.15.0"
-  resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
-  integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=
+jmespath@0.16.0:
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076"
+  integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==
 
 jpeg-js@^0.4.1:
   version "0.4.1"
@@ -4420,14 +4238,6 @@ js-yaml@4.1.0, js-yaml@^4.0.0, js-yaml@^4.1.0:
   dependencies:
     argparse "^2.0.1"
 
-js-yaml@^3.14.1:
-  version "3.14.1"
-  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
-  integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
-  dependencies:
-    argparse "^1.0.7"
-    esprima "^4.0.0"
-
 jsbn@1.1.0, jsbn@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
@@ -4496,10 +4306,10 @@ json-schema-traverse@^1.0.0:
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
   integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
 
-json-schema@0.2.3:
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
-  integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+json-schema@0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
+  integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
 
 json-stable-stringify-without-jsonify@^1.0.1:
   version "1.0.1"
@@ -4520,7 +4330,7 @@ json5-loader@4.0.1:
     loader-utils "^2.0.0"
     schema-utils "^3.0.0"
 
-json5@2.2.0, json5@2.x:
+json5@2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
   integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
@@ -4567,14 +4377,14 @@ jsonld@5.2.0:
     lru-cache "^6.0.0"
     rdf-canonize "^3.0.0"
 
-jsprim@^1.2.2:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
-  integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+jsprim@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d"
+  integrity sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==
   dependencies:
     assert-plus "1.0.0"
     extsprintf "1.3.0"
-    json-schema "0.2.3"
+    json-schema "0.4.0"
     verror "1.10.0"
 
 jsrsasign@8.0.20:
@@ -4795,10 +4605,10 @@ levn@~0.3.0:
     prelude-ls "~1.1.2"
     type-check "~0.3.2"
 
-lilconfig@^2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.3.tgz#68f3005e921dafbd2a2afb48379986aa6d2579fd"
-  integrity sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==
+listenercount@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937"
+  integrity sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=
 
 loader-runner@^4.2.0:
   version "4.2.0"
@@ -4822,13 +4632,12 @@ locate-path@^2.0.0:
     p-locate "^2.0.0"
     path-exists "^3.0.0"
 
-locate-path@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
-  integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+locate-path@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+  integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
   dependencies:
-    p-locate "^3.0.0"
-    path-exists "^3.0.0"
+    p-locate "^4.1.0"
 
 locate-path@^6.0.0:
   version "6.0.0"
@@ -4897,11 +4706,6 @@ lodash.map@^4.4.0:
   resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3"
   integrity sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=
 
-lodash.memoize@4.x, lodash.memoize@^4.1.2:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
-  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
-
 lodash.merge@^4.4.0, lodash.merge@^4.6.2:
   version "4.6.2"
   resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
@@ -4937,12 +4741,7 @@ lodash.union@^4.6.0:
   resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88"
   integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=
 
-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=
-
-lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0:
+lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -4979,18 +4778,18 @@ mailcheck@^1.1.1:
   resolved "https://registry.yarnpkg.com/mailcheck/-/mailcheck-1.1.1.tgz#d87cf6ba0b64ba512199dbf93f1489f479591e34"
   integrity sha1-2Hz2ugtkulEhmdv5PxSJ9HlZHjQ=
 
-make-error@1.x, make-error@^1.1.1:
+make-error@^1.1.1:
   version "1.3.6"
   resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
   integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
 
-make-fetch-happen@^8.0.14:
-  version "8.0.14"
-  resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz#aaba73ae0ab5586ad8eaa68bd83332669393e222"
-  integrity sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ==
+make-fetch-happen@^9.1.0:
+  version "9.1.0"
+  resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968"
+  integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==
   dependencies:
     agentkeepalive "^4.1.3"
-    cacache "^15.0.5"
+    cacache "^15.2.0"
     http-cache-semantics "^4.1.0"
     http-proxy-agent "^4.0.1"
     https-proxy-agent "^5.0.0"
@@ -5001,15 +4800,11 @@ make-fetch-happen@^8.0.14:
     minipass-fetch "^1.3.2"
     minipass-flush "^1.0.5"
     minipass-pipeline "^1.2.4"
+    negotiator "^0.6.2"
     promise-retry "^2.0.1"
-    socks-proxy-agent "^5.0.0"
+    socks-proxy-agent "^6.0.0"
     ssri "^8.0.0"
 
-mdn-data@2.0.14:
-  version "2.0.14"
-  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
-  integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
-
 media-typer@0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -5035,21 +4830,13 @@ methods@^1.1.2:
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
   integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
 
-mfm-js@0.20.0:
-  version "0.20.0"
-  resolved "https://registry.yarnpkg.com/mfm-js/-/mfm-js-0.20.0.tgz#3afdcd7959461fd825aa8af9b9e8a57cdbddc290"
-  integrity sha512-1+3tV3nWUKQNh/ztX3wXu5iLBtdsg6q3wUhl+XyOhc2H3sQdG+sih/w2c0nR9TIawjN+Z1/pvgGzxMJHfmKQmA==
+mfm-js@0.21.0:
+  version "0.21.0"
+  resolved "https://registry.yarnpkg.com/mfm-js/-/mfm-js-0.21.0.tgz#954cc6e7071700b0b1872c78a90bada10be7f772"
+  integrity sha512-nyQXaipa7rmAw9ER9uYigMvGcdCwhSv93abZBwccnSnPOc1W3S/WW0+sN28g3YSmlHDCA0i2q9aAFc9EgOi5KA==
   dependencies:
     twemoji-parser "13.1.x"
 
-micromatch@4.x:
-  version "4.0.4"
-  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
-  integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
-  dependencies:
-    braces "^3.0.1"
-    picomatch "^2.2.3"
-
 micromatch@^4.0.0, micromatch@^4.0.2:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
@@ -5097,11 +4884,6 @@ mimic-response@^1.0.0:
   resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
   integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
 
-mimic-response@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43"
-  integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==
-
 mimic-response@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
@@ -5185,10 +4967,10 @@ minizlib@^2.0.0, minizlib@^2.1.1:
     minipass "^3.0.0"
     yallist "^4.0.0"
 
-misskey-js@0.0.12:
-  version "0.0.12"
-  resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.12.tgz#c8fc2fc823c69b0b7d1cb153a5a29afc33f0ff4d"
-  integrity sha512-Aq74/XskxaFN5CeCLeKPp5UP/xTFHvVnOV677G/zoSIShJRTeLsN5YnzwFpOVI2KN21JQ/ExesKDLoWlvQHtNA==
+misskey-js@0.0.13:
+  version "0.0.13"
+  resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.13.tgz#03a4e469186e28752d599dc4093519eb64647970"
+  integrity sha512-kBdJdfe281gtykzzsrN3IAxWUQIimzPiJGyKWf863ggWJlWYVPmP9hTFlX2z8oPOaypgVBPEPHyw/jNUdc2DbQ==
   dependencies:
     autobind-decorator "^2.4.0"
     eventemitter3 "^4.0.7"
@@ -5204,7 +4986,7 @@ mkdirp-classic@^0.5.3:
   resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
   integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
 
-mkdirp@0.x, mkdirp@^0.5.4:
+"mkdirp@>=0.5 0", mkdirp@^0.5.4:
   version "0.5.5"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
   integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
@@ -5279,10 +5061,10 @@ ms@3.0.0-canary.1:
   resolved "https://registry.yarnpkg.com/ms/-/ms-3.0.0-canary.1.tgz#c7b34fbce381492fd0b345d1cf56e14d67b77b80"
   integrity sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g==
 
-multer@1.4.3:
-  version "1.4.3"
-  resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.3.tgz#4db352d6992e028ac0eacf7be45c6efd0264297b"
-  integrity sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==
+multer@1.4.4:
+  version "1.4.4"
+  resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4.tgz#e2bc6cac0df57a8832b858d7418ccaa8ebaf7d8c"
+  integrity sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==
   dependencies:
     append-field "^1.0.0"
     busboy "^0.2.11"
@@ -5307,10 +5089,10 @@ mz@^2.4.0, mz@^2.7.0:
     object-assign "^4.0.1"
     thenify-all "^1.0.0"
 
-nan@^2.14.2:
-  version "2.14.2"
-  resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
-  integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
+nan@^2.15.0:
+  version "2.15.0"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
+  integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
 
 nano-time@1.0.0:
   version "1.0.0"
@@ -5324,11 +5106,6 @@ nanoid@3.1.20:
   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
   integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
 
-nanoid@^3.1.23:
-  version "3.1.23"
-  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
-  integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
-
 nanoid@^3.1.30:
   version "3.1.30"
   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
@@ -5353,7 +5130,7 @@ needle@^2.5.2:
     iconv-lite "^0.4.4"
     sax "^1.2.4"
 
-negotiator@0.6.2:
+negotiator@0.6.2, 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==
@@ -5383,18 +5160,32 @@ next-tick@~1.0.0:
   resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
   integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
 
-node-abi@^2.21.0:
-  version "2.21.0"
-  resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.21.0.tgz#c2dc9ebad6f4f53d6ea9b531e7b8faad81041d48"
-  integrity sha512-smhrivuPqEM3H5LmnY3KU6HfYv0u4QklgAxfFyRNujKUzbUcYZ+Jc2EhukB9SRcD2VpqhxM7n/MIcp1Ua1/JMg==
+node-abi@^3.3.0:
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.5.0.tgz#26e8b7b251c3260a5ac5ba5aef3b4345a0229248"
+  integrity sha512-LtHvNIBgOy5mO8mPEUtkCW/YCRWYEKshIvqhe1GHHyXEHEB5mgICyYnAcl4qan3uFeRROErKGzatFHPf6kDxWw==
   dependencies:
-    semver "^5.4.1"
+    semver "^7.3.5"
 
 node-addon-api@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.2.0.tgz#117cbb5a959dff0992e1c586ae0393573e4d2a87"
   integrity sha512-eazsqzwG2lskuzBqCGPi7Ac2UgOoMz8JVOXVhTvvPDYhthvNpefx8jWD8Np7Gv+2Sz0FlPWZk0nJV0z598Wn8Q==
 
+node-domexception@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
+  integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
+
+node-fetch@*:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.0.tgz#59390db4e489184fa35d4b74caf5510e8dfbaf3b"
+  integrity sha512-8xeimMwMItMw8hRrOl3C9/xzU49HV/yE6ORew/l+dxWimO5A4Ra8ld2rerlJvc/O7et5Z1zrWsPX43v1QBjCxw==
+  dependencies:
+    data-uri-to-buffer "^4.0.0"
+    fetch-blob "^3.1.4"
+    formdata-polyfill "^4.0.10"
+
 node-fetch@2.6.1, node-fetch@^2.6.1:
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
@@ -5413,31 +5204,31 @@ node-gyp-build@~3.7.0:
   resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-3.7.0.tgz#daa77a4f547b9aed3e2aac779eaf151afd60ec8d"
   integrity sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==
 
-node-gyp@^8.0.0:
-  version "8.0.0"
-  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.0.0.tgz#225af2b06b8419ae81f924bf25ae4c167f6378a5"
-  integrity sha512-Jod6NxyWtcwrpAQe0O/aXOpC5QfncotgtG73dg65z6VW/C6g/G4jiajXQUBIJ8pk/VfM6mBYE9BN/HvudTunUQ==
+node-gyp@^8.4.1:
+  version "8.4.1"
+  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937"
+  integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==
   dependencies:
     env-paths "^2.2.0"
     glob "^7.1.4"
     graceful-fs "^4.2.6"
-    make-fetch-happen "^8.0.14"
+    make-fetch-happen "^9.1.0"
     nopt "^5.0.0"
-    npmlog "^4.1.2"
+    npmlog "^6.0.0"
     rimraf "^3.0.2"
     semver "^7.3.5"
-    tar "^6.1.0"
+    tar "^6.1.2"
     which "^2.0.2"
 
-node-releases@^1.1.70, node-releases@^1.1.71:
+node-releases@^1.1.70:
   version "1.1.71"
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb"
   integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==
 
-nodemailer@6.7.0:
-  version "6.7.0"
-  resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.0.tgz#86614722c4e0c33d1b5b02aecb90d6d629932b0d"
-  integrity sha512-AtiTVUFHLiiDnMQ43zi0YgkzHOEWUkhDgPlBXrsDzJiJvB29Alo4OKxHQ0ugF3gRqRQIneCLtZU3yiUo7pItZw==
+nodemailer@6.7.2:
+  version "6.7.2"
+  resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.2.tgz#44b2ad5f7ed71b7067f7a21c4fedabaec62b85e0"
+  integrity sha512-Dz7zVwlef4k5R71fdmxwR8Q39fiboGbu3xgswkzGwczUfjp873rVxt1O46+Fh0j1ORnAC6L9+heI8uUpO6DT7Q==
 
 nofilter@^2.0.3:
   version "2.0.3"
@@ -5476,11 +5267,6 @@ normalize-url@^4.1.0:
   resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129"
   integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==
 
-normalize-url@^6.0.1:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
-  integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
-
 npm-run-path@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.0.1.tgz#748dd68ed7de377bb1f7132c7dafe657be5ab400"
@@ -5488,7 +5274,7 @@ npm-run-path@^5.0.1:
   dependencies:
     path-key "^4.0.0"
 
-npmlog@^4.0.1, npmlog@^4.1.2:
+npmlog@^4.0.1:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
   integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@@ -5498,12 +5284,15 @@ npmlog@^4.0.1, npmlog@^4.1.2:
     gauge "~2.7.3"
     set-blocking "~2.0.0"
 
-nth-check@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125"
-  integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==
+npmlog@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.0.tgz#ba9ef39413c3d936ea91553db7be49c34ad0520c"
+  integrity sha512-03ppFRGlsyUaQFbGC2C8QWJN/C/K7PsfyD9aQdhVKAQIH4sQBc8WASqFBP7O+Ut4d2oo5LoeoboB3cGdBZSp6Q==
   dependencies:
-    boolbase "^1.0.0"
+    are-we-there-yet "^2.0.0"
+    console-control-strings "^1.1.0"
+    gauge "^4.0.0"
+    set-blocking "^2.0.0"
 
 nth-check@~1.0.1:
   version "1.0.2"
@@ -5537,26 +5326,11 @@ object-inspect@^1.11.0, object-inspect@^1.9.0:
   resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1"
   integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==
 
-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:
+object-keys@^1.0.12, object-keys@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
   integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
 
-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==
-  dependencies:
-    define-properties "^1.1.2"
-    function-bind "^1.1.1"
-    has-symbols "^1.0.0"
-    object-keys "^1.0.11"
-
 object.assign@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
@@ -5567,14 +5341,6 @@ object.assign@^4.1.2:
     has-symbols "^1.0.1"
     object-keys "^1.1.1"
 
-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.3"
-    es-abstract "^1.17.0-next.1"
-
 object.values@^1.1.5:
   version "1.1.5"
   resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac"
@@ -5684,7 +5450,7 @@ p-limit@^1.1.0:
   dependencies:
     p-try "^1.0.0"
 
-p-limit@^2.0.0:
+p-limit@^2.2.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
   integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
@@ -5712,12 +5478,12 @@ p-locate@^2.0.0:
   dependencies:
     p-limit "^1.1.0"
 
-p-locate@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
-  integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+p-locate@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+  integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
   dependencies:
-    p-limit "^2.0.0"
+    p-limit "^2.2.0"
 
 p-locate@^5.0.0:
   version "5.0.0"
@@ -5738,6 +5504,14 @@ p-map@^4.0.0:
   dependencies:
     aggregate-error "^3.0.0"
 
+p-queue@6.6.2:
+  version "6.6.2"
+  resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426"
+  integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==
+  dependencies:
+    eventemitter3 "^4.0.4"
+    p-timeout "^3.2.0"
+
 p-timeout@^3.2.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
@@ -5910,28 +5684,26 @@ picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.0.7, picomatch@^2.2.1:
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
   integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
 
-picomatch@^2.2.3:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
-  integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
-
 pify@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
   integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
 
-pkg-dir@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
-  integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=
-  dependencies:
-    find-up "^2.1.0"
+pluralize@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
+  integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
 
-pngjs@^3.3.0, pngjs@^3.3.1:
+pngjs@^3.3.1:
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
   integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
 
+pngjs@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
+  integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
+
 portscanner@2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/portscanner/-/portscanner-2.2.0.tgz#6059189b3efa0965c9d96a56b958eb9508411cf1"
@@ -5940,274 +5712,6 @@ portscanner@2.2.0:
     async "^2.6.0"
     is-number-like "^1.0.3"
 
-postcss-calc@^8.0.0:
-  version "8.0.0"
-  resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.0.0.tgz#a05b87aacd132740a5db09462a3612453e5df90a"
-  integrity sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g==
-  dependencies:
-    postcss-selector-parser "^6.0.2"
-    postcss-value-parser "^4.0.2"
-
-postcss-colormin@^5.2.1:
-  version "5.2.1"
-  resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.2.1.tgz#6e444a806fd3c578827dbad022762df19334414d"
-  integrity sha512-VVwMrEYLcHYePUYV99Ymuoi7WhKrMGy/V9/kTS0DkCoJYmmjdOMneyhzYUxcNgteKDVbrewOkSM7Wje/MFwxzA==
-  dependencies:
-    browserslist "^4.16.6"
-    caniuse-api "^3.0.0"
-    colord "^2.9.1"
-    postcss-value-parser "^4.1.0"
-
-postcss-convert-values@^5.0.2:
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.0.2.tgz#879b849dc3677c7d6bc94b6a2c1a3f0808798059"
-  integrity sha512-KQ04E2yadmfa1LqXm7UIDwW1ftxU/QWZmz6NKnHnUvJ3LEYbbcX6i329f/ig+WnEByHegulocXrECaZGLpL8Zg==
-  dependencies:
-    postcss-value-parser "^4.1.0"
-
-postcss-discard-comments@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz#9eae4b747cf760d31f2447c27f0619d5718901fe"
-  integrity sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==
-
-postcss-discard-duplicates@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz#68f7cc6458fe6bab2e46c9f55ae52869f680e66d"
-  integrity sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==
-
-postcss-discard-empty@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz#ee136c39e27d5d2ed4da0ee5ed02bc8a9f8bf6d8"
-  integrity sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==
-
-postcss-discard-overridden@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz#454b41f707300b98109a75005ca4ab0ff2743ac6"
-  integrity sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==
-
-postcss-merge-longhand@^5.0.3:
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.0.3.tgz#42194a5ffbaa5513edbf606ef79c44958564658b"
-  integrity sha512-kmB+1TjMTj/bPw6MCDUiqSA5e/x4fvLffiAdthra3a0m2/IjTrWsTmD3FdSskzUjEwkj5ZHBDEbv5dOcqD7CMQ==
-  dependencies:
-    css-color-names "^1.0.1"
-    postcss-value-parser "^4.1.0"
-    stylehacks "^5.0.1"
-
-postcss-merge-rules@^5.0.2:
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.0.2.tgz#d6e4d65018badbdb7dcc789c4f39b941305d410a"
-  integrity sha512-5K+Md7S3GwBewfB4rjDeol6V/RZ8S+v4B66Zk2gChRqLTCC8yjnHQ601omj9TKftS19OPGqZ/XzoqpzNQQLwbg==
-  dependencies:
-    browserslist "^4.16.6"
-    caniuse-api "^3.0.0"
-    cssnano-utils "^2.0.1"
-    postcss-selector-parser "^6.0.5"
-    vendors "^1.0.3"
-
-postcss-minify-font-values@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz#a90cefbfdaa075bd3dbaa1b33588bb4dc268addf"
-  integrity sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA==
-  dependencies:
-    postcss-value-parser "^4.1.0"
-
-postcss-minify-gradients@^5.0.3:
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.0.3.tgz#f970a11cc71e08e9095e78ec3a6b34b91c19550e"
-  integrity sha512-Z91Ol22nB6XJW+5oe31+YxRsYooxOdFKcbOqY/V8Fxse1Y3vqlNRpi1cxCqoACZTQEhl+xvt4hsbWiV5R+XI9Q==
-  dependencies:
-    colord "^2.9.1"
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
-
-postcss-minify-params@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz#371153ba164b9d8562842fdcd929c98abd9e5b6c"
-  integrity sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw==
-  dependencies:
-    alphanum-sort "^1.0.2"
-    browserslist "^4.16.0"
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
-    uniqs "^2.0.0"
-
-postcss-minify-selectors@^5.1.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz#4385c845d3979ff160291774523ffa54eafd5a54"
-  integrity sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og==
-  dependencies:
-    alphanum-sort "^1.0.2"
-    postcss-selector-parser "^6.0.5"
-
-postcss-modules-extract-imports@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
-  integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
-
-postcss-modules-local-by-default@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c"
-  integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==
-  dependencies:
-    icss-utils "^5.0.0"
-    postcss-selector-parser "^6.0.2"
-    postcss-value-parser "^4.1.0"
-
-postcss-modules-scope@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06"
-  integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==
-  dependencies:
-    postcss-selector-parser "^6.0.4"
-
-postcss-modules-values@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c"
-  integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==
-  dependencies:
-    icss-utils "^5.0.0"
-
-postcss-normalize-charset@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz#121559d1bebc55ac8d24af37f67bd4da9efd91d0"
-  integrity sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==
-
-postcss-normalize-display-values@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz#62650b965981a955dffee83363453db82f6ad1fd"
-  integrity sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ==
-  dependencies:
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
-
-postcss-normalize-positions@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz#868f6af1795fdfa86fbbe960dceb47e5f9492fe5"
-  integrity sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg==
-  dependencies:
-    postcss-value-parser "^4.1.0"
-
-postcss-normalize-repeat-style@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz#cbc0de1383b57f5bb61ddd6a84653b5e8665b2b5"
-  integrity sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w==
-  dependencies:
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
-
-postcss-normalize-string@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz#d9eafaa4df78c7a3b973ae346ef0e47c554985b0"
-  integrity sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA==
-  dependencies:
-    postcss-value-parser "^4.1.0"
-
-postcss-normalize-timing-functions@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz#8ee41103b9130429c6cbba736932b75c5e2cb08c"
-  integrity sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q==
-  dependencies:
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
-
-postcss-normalize-unicode@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz#82d672d648a411814aa5bf3ae565379ccd9f5e37"
-  integrity sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA==
-  dependencies:
-    browserslist "^4.16.0"
-    postcss-value-parser "^4.1.0"
-
-postcss-normalize-url@^5.0.2:
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.0.2.tgz#ddcdfb7cede1270740cf3e4dfc6008bd96abc763"
-  integrity sha512-k4jLTPUxREQ5bpajFQZpx8bCF2UrlqOTzP9kEqcEnOfwsRshWs2+oAFIHfDQB8GO2PaUaSE0NlTAYtbluZTlHQ==
-  dependencies:
-    is-absolute-url "^3.0.3"
-    normalize-url "^6.0.1"
-    postcss-value-parser "^4.1.0"
-
-postcss-normalize-whitespace@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz#b0b40b5bcac83585ff07ead2daf2dcfbeeef8e9a"
-  integrity sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA==
-  dependencies:
-    postcss-value-parser "^4.1.0"
-
-postcss-ordered-values@^5.0.2:
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.0.2.tgz#1f351426977be00e0f765b3164ad753dac8ed044"
-  integrity sha512-8AFYDSOYWebJYLyJi3fyjl6CqMEG/UVworjiyK1r573I56kb3e879sCJLGvR3merj+fAdPpVplXKQZv+ey6CgQ==
-  dependencies:
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
-
-postcss-reduce-initial@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz#9d6369865b0f6f6f6b165a0ef5dc1a4856c7e946"
-  integrity sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw==
-  dependencies:
-    browserslist "^4.16.0"
-    caniuse-api "^3.0.0"
-
-postcss-reduce-transforms@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz#93c12f6a159474aa711d5269923e2383cedcf640"
-  integrity sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA==
-  dependencies:
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
-
-postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
-  version "6.0.4"
-  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3"
-  integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==
-  dependencies:
-    cssesc "^3.0.0"
-    indexes-of "^1.0.1"
-    uniq "^1.0.1"
-    util-deprecate "^1.0.2"
-
-postcss-selector-parser@^6.0.5:
-  version "6.0.6"
-  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea"
-  integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==
-  dependencies:
-    cssesc "^3.0.0"
-    util-deprecate "^1.0.2"
-
-postcss-svgo@^5.0.3:
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.0.3.tgz#d945185756e5dfaae07f9edb0d3cae7ff79f9b30"
-  integrity sha512-41XZUA1wNDAZrQ3XgWREL/M2zSw8LJPvb5ZWivljBsUQAGoEKMYm6okHsTjJxKYI4M75RQEH4KYlEM52VwdXVA==
-  dependencies:
-    postcss-value-parser "^4.1.0"
-    svgo "^2.7.0"
-
-postcss-unique-selectors@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz#3be5c1d7363352eff838bd62b0b07a0abad43bfc"
-  integrity sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w==
-  dependencies:
-    alphanum-sort "^1.0.2"
-    postcss-selector-parser "^6.0.5"
-    uniqs "^2.0.0"
-
-postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
-  integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
-
-postcss@^8.2.15:
-  version "8.3.0"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.0.tgz#b1a713f6172ca427e3f05ef1303de8b65683325f"
-  integrity sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==
-  dependencies:
-    colorette "^1.2.2"
-    nanoid "^3.1.23"
-    source-map-js "^0.6.2"
-
 postcss@^8.3.11:
   version "8.3.11"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.11.tgz#c3beca7ea811cd5e1c4a3ec6d2e7599ef1f8f858"
@@ -6239,10 +5743,10 @@ postgres-interval@^1.1.0:
   dependencies:
     xtend "^4.0.0"
 
-prebuild-install@^6.1.4:
-  version "6.1.4"
-  resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.4.tgz#ae3c0142ad611d58570b89af4986088a4937e00f"
-  integrity sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==
+prebuild-install@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.0.0.tgz#3c5ce3902f1cb9d6de5ae94ca53575e4af0c1574"
+  integrity sha512-IvSenf33K7JcgddNz2D5w521EgO+4aMMjFt73Uk9FRzQ7P+QZPKrp7qPsDydsSwjGt3T5xRNnM1bj1zMTD5fTA==
   dependencies:
     detect-libc "^1.0.3"
     expand-template "^2.0.3"
@@ -6250,11 +5754,11 @@ prebuild-install@^6.1.4:
     minimist "^1.2.3"
     mkdirp-classic "^0.5.3"
     napi-build-utils "^1.0.1"
-    node-abi "^2.21.0"
+    node-abi "^3.3.0"
     npmlog "^4.0.1"
     pump "^3.0.0"
     rc "^1.2.7"
-    simple-get "^3.0.3"
+    simple-get "^4.0.0"
     tar-fs "^2.0.0"
     tunnel-agent "^0.6.0"
 
@@ -6282,11 +5786,6 @@ printj@~1.1.0:
   resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
   integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==
 
-prismjs@1.25.0:
-  version "1.25.0"
-  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756"
-  integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==
-
 private-ip@2.3.3:
   version "2.3.3"
   resolved "https://registry.yarnpkg.com/private-ip/-/private-ip-2.3.3.tgz#1e80ff8443e5ac78f555631aec3ea6ff027fa6aa"
@@ -6297,10 +5796,10 @@ private-ip@2.3.3:
     is-ip "^3.1.0"
     netmask "^2.0.2"
 
-probe-image-size@7.2.1:
-  version "7.2.1"
-  resolved "https://registry.yarnpkg.com/probe-image-size/-/probe-image-size-7.2.1.tgz#df0c924e67e247bc94f8fcb0fad7f0081061fc44"
-  integrity sha512-d+6L3NvQBCNt4peRDoEfA7r9bPm6/qy18FnLKwg4NWBC5JrJm0pMLRg1kF4XNsPe1bUdt3WIMonPJzQWN2HXjQ==
+probe-image-size@7.2.2:
+  version "7.2.2"
+  resolved "https://registry.yarnpkg.com/probe-image-size/-/probe-image-size-7.2.2.tgz#e5851b9be7864f21e3bac5e6e4fac9da9055b412"
+  integrity sha512-QUm+w1S9WTsT5GZB830u0BHExrUmF0J4fyRm5kbLUMEP3fl9UVYXc3xOBVqZNnH9tnvVEJO8vDk3PMtsLqjxug==
   dependencies:
     lodash.merge "^4.6.2"
     needle "^2.5.2"
@@ -6311,11 +5810,6 @@ process-nextick-args@~2.0.0:
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
   integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
 
-progress@^2.0.0:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
-  integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
-
 promise-inflight@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
@@ -6334,15 +5828,6 @@ promise-retry@^2.0.1:
     err-code "^2.0.2"
     retry "^0.12.0"
 
-promise.prototype.finally@^3.1.2:
-  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.17.0-next.0"
-    function-bind "^1.1.1"
-
 promise@^7.0.1:
   version "7.3.1"
   resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
@@ -6486,10 +5971,10 @@ punycode@2.1.1, punycode@^2.1.0, punycode@^2.1.1:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
-pureimage@0.3.5:
-  version "0.3.5"
-  resolved "https://registry.yarnpkg.com/pureimage/-/pureimage-0.3.5.tgz#cd5e91f7b6409fcf4880297aaa3e7fc0afc24d5e"
-  integrity sha512-+CFUEpoX6GemlKlHihI7Ii4IqKqF5KZjd682sAxwzbc4t4zU4Gwhxd4W3UMZW94nJzf0n4nA9zJrwTR4jZB4TA==
+pureimage@0.3.8:
+  version "0.3.8"
+  resolved "https://registry.yarnpkg.com/pureimage/-/pureimage-0.3.8.tgz#b9c2a127f3182ab94fb4520e83f4fbcbdd9b38f1"
+  integrity sha512-+CuR0HM0VmBfKKQTM56myBonDZAhZkS6ymJ8W5oYYDXG7y7X34B/dEH3UesbJI497Vc2OkA+g8T1/Xj/FTyQ8A==
   dependencies:
     jpeg-js "^0.4.1"
     opentype.js "^0.4.3"
@@ -6500,18 +5985,15 @@ q@1.4.1:
   resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e"
   integrity sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=
 
-qrcode@1.4.4:
-  version "1.4.4"
-  resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.4.4.tgz#f0c43568a7e7510a55efc3b88d9602f71963ea83"
-  integrity sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==
+qrcode@1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.0.tgz#95abb8a91fdafd86f8190f2836abbfc500c72d1b"
+  integrity sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ==
   dependencies:
-    buffer "^5.4.3"
-    buffer-alloc "^1.2.0"
-    buffer-from "^1.1.1"
     dijkstrajs "^1.0.1"
-    isarray "^2.0.1"
-    pngjs "^3.3.0"
-    yargs "^13.2.4"
+    encode-utf8 "^1.0.3"
+    pngjs "^5.0.0"
+    yargs "^15.3.1"
 
 qs@^6.4.0, qs@^6.5.2:
   version "6.9.3"
@@ -6579,14 +6061,14 @@ rdf-canonize@^3.0.0:
   dependencies:
     setimmediate "^1.0.5"
 
-re2@1.16.0:
-  version "1.16.0"
-  resolved "https://registry.yarnpkg.com/re2/-/re2-1.16.0.tgz#f311eb4865b1296123800ea8e013cec8dab25590"
-  integrity sha512-eizTZL2ZO0ZseLqfD4t3Qd0M3b3Nr0MBWpX81EbPMIud/1d/CSfUIx2GQK8fWiAeHoSekO5EOeFib2udTZLwYw==
+re2@1.17.3:
+  version "1.17.3"
+  resolved "https://registry.yarnpkg.com/re2/-/re2-1.17.3.tgz#8cceb48f52c45b860b1f67cee8a44726f7d05e9a"
+  integrity sha512-Dp5iWVR8W3C7Nm9DziMY4BleMPRb/pe6kvfbzLv80dVYaXRc9jRnwwNqU0oE/taRm0qYR1+Qrtzk9rPjS9ecaQ==
   dependencies:
-    install-artifact-from-github "^1.2.0"
-    nan "^2.14.2"
-    node-gyp "^8.0.0"
+    install-artifact-from-github "^1.3.0"
+    nan "^2.15.0"
+    node-gyp "^8.4.1"
 
 readable-stream@1.1.x:
   version "1.1.14"
@@ -6598,7 +6080,7 @@ readable-stream@1.1.x:
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.2.2:
+readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@~2.3.6:
   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==
@@ -6662,13 +6144,24 @@ redis-lock@0.1.4:
   resolved "https://registry.yarnpkg.com/redis-lock/-/redis-lock-0.1.4.tgz#e83590bee22b5f01cdb65bfbd88d988045356272"
   integrity sha512-7/+zu86XVQfJVx1nHTzux5reglDiyUCDwmW7TSlvVezfhH2YLc/Rc8NE0ejQG+8/0lwKzm29/u/4+ogKeLosiA==
 
-redis-parser@^3.0.0:
+redis-parser@3.0.0, redis-parser@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
   integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=
   dependencies:
     redis-errors "^1.0.0"
 
+redis@*:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/redis/-/redis-4.0.2.tgz#096cf716842731a24f34c7c3a996c143e2b133bb"
+  integrity sha512-Ip1DJ/lwuvtJz9AZ6pl1Bv33fWzk5d3iQpGzsXpi04ErkT4fq0pfGOm4k/p9DHmPGieEIOWvJ9xmIeQMooLybg==
+  dependencies:
+    "@node-redis/bloom" "^1.0.0"
+    "@node-redis/client" "^1.0.2"
+    "@node-redis/json" "^1.0.2"
+    "@node-redis/search" "^1.0.2"
+    "@node-redis/time-series" "^1.0.1"
+
 redis@3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/redis/-/redis-3.1.2.tgz#766851117e80653d23e0ed536254677ab647638c"
@@ -6780,6 +6273,13 @@ reusify@^1.0.4:
   resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
   integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
 
+rimraf@2:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
+  integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
+  dependencies:
+    glob "^7.1.3"
+
 rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@@ -6805,7 +6305,7 @@ s-age@1.1.2:
   resolved "https://registry.yarnpkg.com/s-age/-/s-age-1.1.2.tgz#c0cf15233ccc93f41de92ea42c36d957977d1ea2"
   integrity sha512-aSN2TlF39WLoZA/6cgYSJZhKt63kJ4EaadejPWjWY9/h4rksIqvfWY3gfd+3uAegSM1IXsA9aWeEhJtkxkFQtA==
 
-safe-buffer@*:
+safe-buffer@*, safe-buffer@5.2.1:
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@@ -6825,10 +6325,10 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
-sanitize-html@2.5.3:
-  version "2.5.3"
-  resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.5.3.tgz#91aa3dc760b072cdf92f9c6973747569b1ba1cd8"
-  integrity sha512-DGATXd1fs/Rm287/i5FBKVYSBBUL0iAaztOA1/RFhEs4yqo39/X52i/q/CwsfCUG5cilmXSBmnQmyWfnKhBlOg==
+sanitize-html@2.6.1:
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.6.1.tgz#5d37c08e189c61c0631560a889b10d9d155d000e"
+  integrity sha512-DzjSz3H5qDntD7s1TcWCSoRPmNR8UmA+y+xZQOvWgjATe2Br9ZW73+vD3Pj6Snrg0RuEuJdXgrKvnYuiuixRkA==
   dependencies:
     deepmerge "^4.2.2"
     escape-string-regexp "^4.0.0"
@@ -6878,17 +6378,12 @@ seedrandom@3.0.5:
   resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7"
   integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==
 
-semver@6.x:
-  version "6.3.0"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
-  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
-
-semver@^5.4.1, semver@^5.6.0:
+semver@^5.6.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
 
-semver@^7.2.1, semver@^7.3.2, semver@^7.3.4:
+semver@^7.3.2, semver@^7.3.4:
   version "7.3.4"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
   integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
@@ -6914,7 +6409,7 @@ set-blocking@^2.0.0, set-blocking@~2.0.0:
   resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
   integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
 
-setimmediate@^1.0.5:
+setimmediate@^1.0.5, setimmediate@~1.0.4:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
   integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
@@ -6937,17 +6432,17 @@ sha.js@^2.4.11:
     inherits "^2.0.1"
     safe-buffer "^5.0.1"
 
-sharp@0.29.2:
-  version "0.29.2"
-  resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.29.2.tgz#e8c003cd9cb321585b32dbda6eed3baa7d6f2308"
-  integrity sha512-XWRdiYLIJ3tDUejRyG24KERnJzMfIoyiJBntd2S6/uj3NEeNgRFRLgiBlvPxMa8aml14dKKD98yHinSNKp1xzQ==
+sharp@0.29.3:
+  version "0.29.3"
+  resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.29.3.tgz#0da183d626094c974516a48fab9b3e4ba92eb5c2"
+  integrity sha512-fKWUuOw77E4nhpyzCCJR1ayrttHoFHBT2U/kR/qEMRhvPEcluG4BKj324+SCO1e84+knXHwhJ1HHJGnUt4ElGA==
   dependencies:
     color "^4.0.1"
     detect-libc "^1.0.3"
     node-addon-api "^4.2.0"
-    prebuild-install "^6.1.4"
+    prebuild-install "^7.0.0"
     semver "^7.3.5"
-    simple-get "^3.1.0"
+    simple-get "^4.0.0"
     tar-fs "^2.1.1"
     tunnel-agent "^0.6.0"
 
@@ -6992,12 +6487,12 @@ simple-concat@^1.0.0:
   resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6"
   integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=
 
-simple-get@^3.0.3, simple-get@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3"
-  integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==
+simple-get@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.0.tgz#73fa628278d21de83dadd5512d2cc1f4872bd675"
+  integrity sha512-ZalZGexYr3TA0SwySsr5HlgOOinS4Jsa8YB2GJ6lUNAazyAu4KG/VmzMTwAt2YVXzzVj8QmefmAonZIK2BSGcQ==
   dependencies:
-    decompress-response "^4.2.0"
+    decompress-response "^6.0.0"
     once "^1.3.1"
     simple-concat "^1.0.0"
 
@@ -7018,16 +6513,16 @@ smart-buffer@^4.1.0:
   resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba"
   integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==
 
-socks-proxy-agent@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-5.0.0.tgz#7c0f364e7b1cf4a7a437e71253bed72e9004be60"
-  integrity sha512-lEpa1zsWCChxiynk+lCycKuC502RxDWLKJZoIhnxrWNjLSDGYRFflHA1/228VkRcnv9TIb8w98derGbpKxJRgA==
+socks-proxy-agent@^6.0.0:
+  version "6.1.1"
+  resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz#e664e8f1aaf4e1fb3df945f09e3d94f911137f87"
+  integrity sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==
   dependencies:
-    agent-base "6"
-    debug "4"
-    socks "^2.3.3"
+    agent-base "^6.0.2"
+    debug "^4.3.1"
+    socks "^2.6.1"
 
-socks@^2.3.3:
+socks@^2.6.1:
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e"
   integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==
@@ -7082,11 +6577,6 @@ sprintf-js@1.1.2:
   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
   integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==
 
-sprintf-js@~1.0.2:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
-  integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
-
 sshpk@^1.14.1:
   version "1.16.1"
   resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
@@ -7109,11 +6599,6 @@ ssri@^8.0.0, ssri@^8.0.1:
   dependencies:
     minipass "^3.1.1"
 
-stable@^0.1.8:
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
-  integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
-
 standard-as-callback@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
@@ -7158,14 +6643,14 @@ string-width@^1.0.1:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^4.0.0"
 
-string-width@^3.0.0, string-width@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
-  integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
+"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
   dependencies:
-    emoji-regex "^7.0.1"
-    is-fullwidth-code-point "^2.0.0"
-    strip-ansi "^5.1.0"
+    emoji-regex "^8.0.0"
+    is-fullwidth-code-point "^3.0.0"
+    strip-ansi "^6.0.1"
 
 string-width@^4.1.0, string-width@^4.2.0:
   version "4.2.0"
@@ -7176,14 +6661,6 @@ string-width@^4.1.0, string-width@^4.2.0:
     is-fullwidth-code-point "^3.0.0"
     strip-ansi "^6.0.0"
 
-string.prototype.trimend@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
-  integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.5"
-
 string.prototype.trimend@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
@@ -7192,32 +6669,6 @@ string.prototype.trimend@^1.0.4:
     call-bind "^1.0.2"
     define-properties "^1.1.3"
 
-string.prototype.trimleft@^2.1.1:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz#4408aa2e5d6ddd0c9a80739b087fbc067c03b3cc"
-  integrity sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.5"
-    string.prototype.trimstart "^1.0.0"
-
-string.prototype.trimright@^2.1.1:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz#c76f1cef30f21bbad8afeb8db1511496cfb0f2a3"
-  integrity sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.5"
-    string.prototype.trimend "^1.0.0"
-
-string.prototype.trimstart@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
-  integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.5"
-
 string.prototype.trimstart@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed"
@@ -7266,13 +6717,6 @@ strip-ansi@^4.0.0:
   dependencies:
     ansi-regex "^3.0.0"
 
-strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
-  integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
-  dependencies:
-    ansi-regex "^4.1.0"
-
 strip-ansi@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
@@ -7320,14 +6764,6 @@ style-loader@3.3.1:
   resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575"
   integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==
 
-stylehacks@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.0.1.tgz#323ec554198520986806388c7fdaebc38d2c06fb"
-  integrity sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA==
-  dependencies:
-    browserslist "^4.16.0"
-    postcss-selector-parser "^6.0.4"
-
 summaly@2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/summaly/-/summaly-2.5.0.tgz#ec5af6e84857efcb6c844d896e83569e64a923ea"
@@ -7365,19 +6801,6 @@ supports-color@^7.0.0, supports-color@^7.1.0:
   dependencies:
     has-flag "^4.0.0"
 
-svgo@^2.7.0:
-  version "2.8.0"
-  resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24"
-  integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==
-  dependencies:
-    "@trysound/sax" "0.2.0"
-    commander "^7.2.0"
-    css-select "^4.1.3"
-    css-tree "^1.1.3"
-    csso "^4.2.0"
-    picocolors "^1.0.0"
-    stable "^0.1.8"
-
 symbol-tree@^3.2.4:
   version "3.2.4"
   resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@@ -7454,10 +6877,10 @@ tar@^6.0.2:
     mkdirp "^1.0.3"
     yallist "^4.0.0"
 
-tar@^6.1.0:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83"
-  integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==
+tar@^6.1.2:
+  version "6.1.11"
+  resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
+  integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==
   dependencies:
     chownr "^2.0.0"
     fs-minipass "^2.0.0"
@@ -7516,11 +6939,6 @@ through@2:
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
   integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
 
-timsort@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
-  integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
-
 tinycolor2@1.4.2:
   version "1.4.2"
   resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
@@ -7584,21 +7002,10 @@ trace-redirect@1.0.6:
   resolved "https://registry.yarnpkg.com/trace-redirect/-/trace-redirect-1.0.6.tgz#ac629b5bf8247d30dde5a35fe9811b811075b504"
   integrity sha512-UUfa1DjjU5flcjMdaFIiIEGDTyu2y/IiMjOX4uGXa7meKBS4vD4f2Uy/tken9Qkd4Jsm4sRsfZcIIPqrRVF3Mg==
 
-ts-jest@^25.2.1:
-  version "25.5.1"
-  resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.5.1.tgz#2913afd08f28385d54f2f4e828be4d261f4337c7"
-  integrity sha512-kHEUlZMK8fn8vkxDjwbHlxXRB9dHYpyzqKIGDNxbzs+Rz+ssNDSDNusEK8Fk/sDd4xE6iKoQLfFkFVaskmTJyw==
-  dependencies:
-    bs-logger "0.x"
-    buffer-from "1.x"
-    fast-json-stable-stringify "2.x"
-    json5 "2.x"
-    lodash.memoize "4.x"
-    make-error "1.x"
-    micromatch "4.x"
-    mkdirp "0.x"
-    semver "6.x"
-    yargs-parser "18.x"
+"traverse@>=0.3.0 <0.4":
+  version "0.3.9"
+  resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
+  integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=
 
 ts-loader@9.2.6:
   version "9.2.6"
@@ -7640,10 +7047,10 @@ tsc-alias@1.4.1:
     mylas "^2.1.4"
     normalize-path "^3.0.0"
 
-tsconfig-paths@3.11.0, tsconfig-paths@^3.11.0:
-  version "3.11.0"
-  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36"
-  integrity sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==
+tsconfig-paths@3.12.0, tsconfig-paths@^3.12.0:
+  version "3.12.0"
+  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz#19769aca6ee8f6a1a341e38c8fa45dd9fb18899b"
+  integrity sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==
   dependencies:
     "@types/json5" "^0.0.29"
     json5 "^1.0.1"
@@ -7743,10 +7150,10 @@ typedarray@^0.0.6:
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
-typeorm@0.2.39:
-  version "0.2.39"
-  resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.2.39.tgz#4d22fc68d114b2ca88a8d7b064f31af15e836ade"
-  integrity sha512-yQdvDpmmmn8wp1We25V76KIBPYR/lDbymNbGC++Uq8mSRhpHIPnlg26VAT4CF6Ypqx72zn8eqr+/72uSo7HdJQ==
+typeorm@0.2.41:
+  version "0.2.41"
+  resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.2.41.tgz#88758101ac158dc0a0a903d70eaacea2974281cc"
+  integrity sha512-/d8CLJJxKPgsnrZWiMyPI0rz2MFZnBQrnQ5XP3Vu3mswv2WPexb58QM6BEtmRmlTMYN5KFWUz8SKluze+wS9xw==
   dependencies:
     "@sqltools/formatter" "^1.2.2"
     app-root-path "^3.0.0"
@@ -7765,10 +7172,10 @@ typeorm@0.2.39:
     yargs "^17.0.1"
     zen-observable-ts "^1.0.0"
 
-typescript@4.4.4:
-  version "4.4.4"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
-  integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
+typescript@4.5.5:
+  version "4.5.5"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3"
+  integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==
 
 ulid@2.3.0:
   version "2.3.0"
@@ -7785,16 +7192,6 @@ unbox-primitive@^1.0.1:
     has-symbols "^1.0.2"
     which-boxed-primitive "^1.0.2"
 
-uniq@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
-  integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
-
-uniqs@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
-  integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI=
-
 unique-filename@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
@@ -7827,6 +7224,22 @@ unpipe@1.0.0:
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
   integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
 
+unzipper@0.10.11:
+  version "0.10.11"
+  resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.11.tgz#0b4991446472cbdb92ee7403909f26c2419c782e"
+  integrity sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==
+  dependencies:
+    big-integer "^1.6.17"
+    binary "~0.3.0"
+    bluebird "~3.4.1"
+    buffer-indexof-polyfill "~1.0.0"
+    duplexer2 "~0.1.4"
+    fstream "^1.0.12"
+    graceful-fs "^4.2.2"
+    listenercount "~1.0.1"
+    readable-stream "~2.3.6"
+    setimmediate "~1.0.4"
+
 uri-js@^4.2.2:
   version "4.2.2"
   resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
@@ -7854,21 +7267,11 @@ utf-8-validate@^5.0.2:
   dependencies:
     node-gyp-build "~3.7.0"
 
-util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
 
-util.promisify@^1.0.1:
-  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.3"
-    es-abstract "^1.17.2"
-    has-symbols "^1.0.1"
-    object.getownpropertydescriptors "^2.1.0"
-
 uuid@3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
@@ -7894,11 +7297,6 @@ vary@^1.1.2:
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
   integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
 
-vendors@^1.0.3:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
-  integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
-
 verror@1.10.0:
   version "1.10.0"
   resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
@@ -7913,19 +7311,6 @@ void-elements@^3.1.0:
   resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
   integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=
 
-vue-eslint-parser@^8.0.1:
-  version "8.0.1"
-  resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-8.0.1.tgz#25e08b20a414551531f3e19f999902e1ecf45f13"
-  integrity sha512-lhWjDXJhe3UZw2uu3ztX51SJAPGPey1Tff2RK3TyZURwbuI4vximQLzz4nQfCv8CZq4xx7uIiogHMMoSJPr33A==
-  dependencies:
-    debug "^4.3.2"
-    eslint-scope "^6.0.0"
-    eslint-visitor-keys "^3.0.0"
-    espree "^9.0.0"
-    esquery "^1.4.0"
-    lodash "^4.17.21"
-    semver "^7.3.5"
-
 w3c-hr-time@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
@@ -7960,6 +7345,11 @@ web-push@3.4.5:
     minimist "^1.2.5"
     urlsafe-base64 "^1.0.0"
 
+web-streams-polyfill@^3.0.3:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965"
+  integrity sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==
+
 webidl-conversions@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
@@ -8086,6 +7476,13 @@ wide-align@1.1.3, wide-align@^1.1.0:
   dependencies:
     string-width "^1.0.2 || 2"
 
+wide-align@^1.1.2:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
+  integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==
+  dependencies:
+    string-width "^1.0.2 || 2 || 3 || 4"
+
 with@^7.0.0:
   version "7.0.2"
   resolved "https://registry.yarnpkg.com/with/-/with-7.0.2.tgz#ccee3ad542d25538a7a7a80aad212b9828495bac"
@@ -8106,14 +7503,14 @@ workerpool@6.1.0:
   resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.0.tgz#a8e038b4c94569596852de7a8ea4228eefdeb37b"
   integrity sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==
 
-wrap-ansi@^5.1.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
-  integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
+wrap-ansi@^6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
+  integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
   dependencies:
-    ansi-styles "^3.2.0"
-    string-width "^3.0.0"
-    strip-ansi "^5.0.0"
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
 
 wrap-ansi@^7.0.0:
   version "7.0.0"
@@ -8129,10 +7526,10 @@ wrappy@1:
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
-ws@8.2.3:
-  version "8.2.3"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
-  integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
+ws@8.4.2:
+  version "8.4.2"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.2.tgz#18e749868d8439f2268368829042894b6907aa0b"
+  integrity sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA==
 
 ws@^7.4.6:
   version "7.5.3"
@@ -8207,43 +7604,30 @@ yaeti@^0.0.6:
   resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577"
   integrity sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=
 
+yallist@4.0.0, yallist@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
 yallist@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
   integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
 
-yallist@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
-  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
-
 yaml-ast-parser@0.0.43:
   version "0.0.43"
   resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb"
   integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==
 
-yaml@^1.10.2:
-  version "1.10.2"
-  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
-  integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
-
-yargs-parser@18.x:
-  version "18.1.3"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
-  integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
-  dependencies:
-    camelcase "^5.0.0"
-    decamelize "^1.2.0"
-
 yargs-parser@20.2.4, yargs-parser@^20.2.2:
   version "20.2.4"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
   integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
 
-yargs-parser@^13.1.2:
-  version "13.1.2"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
-  integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
+yargs-parser@^18.1.2:
+  version "18.1.3"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
+  integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
   dependencies:
     camelcase "^5.0.0"
     decamelize "^1.2.0"
@@ -8271,21 +7655,22 @@ yargs@16.2.0, yargs@^16.0.0:
     y18n "^5.0.5"
     yargs-parser "^20.2.2"
 
-yargs@^13.2.4:
-  version "13.3.2"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
-  integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
+yargs@^15.3.1:
+  version "15.4.1"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
+  integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
   dependencies:
-    cliui "^5.0.0"
-    find-up "^3.0.0"
+    cliui "^6.0.0"
+    decamelize "^1.2.0"
+    find-up "^4.1.0"
     get-caller-file "^2.0.1"
     require-directory "^2.1.1"
     require-main-filename "^2.0.0"
     set-blocking "^2.0.0"
-    string-width "^3.0.0"
+    string-width "^4.2.0"
     which-module "^2.0.0"
     y18n "^4.0.0"
-    yargs-parser "^13.1.2"
+    yargs-parser "^18.1.2"
 
 yargs@^17.0.1:
   version "17.1.1"
diff --git a/packages/client/.eslintrc.js b/packages/client/.eslintrc.js
index 8e4ff6e455..e0113019ac 100644
--- a/packages/client/.eslintrc.js
+++ b/packages/client/.eslintrc.js
@@ -14,6 +14,10 @@ module.exports = {
 		"plugin:vue/vue3-recommended"
 	],
 	rules: {
+		// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
+		// data の禁止理由: 抽象的すぎるため
+		// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
+		"id-denylist": ["error", "window", "data", "e"],
 		"vue/attributes-order": ["error", {
 			"alphabetical": false
 		}],
diff --git a/packages/client/package.json b/packages/client/package.json
index 167ab2e816..6c4cf764d0 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -13,7 +13,7 @@
 		"@discordapp/twemoji": "13.1.0",
 		"@syuilo/aiscript": "0.11.1",
 		"@types/dateformat": "3.0.1",
-		"@types/escape-regexp": "0.0.0",
+		"@types/escape-regexp": "0.0.1",
 		"@types/glob": "7.2.0",
 		"@types/gulp": "4.0.9",
 		"@types/gulp-rename": "2.0.1",
@@ -21,7 +21,6 @@
 		"@types/katex": "0.11.1",
 		"@types/matter-js": "0.17.6",
 		"@types/mocha": "8.2.3",
-		"@types/node": "16.11.12",
 		"@types/oauth": "0.9.1",
 		"@types/parse5": "6.0.3",
 		"@types/punycode": "2.1.0",
@@ -32,14 +31,14 @@
 		"@types/throttle-debounce": "2.1.0",
 		"@types/tinycolor2": "1.4.3",
 		"@types/tmp": "0.2.3",
-		"@types/uuid": "8.3.3",
+		"@types/uuid": "8.3.4",
 		"@types/web-push": "3.3.2",
 		"@types/webpack": "5.28.0",
 		"@types/webpack-stream": "3.2.12",
 		"@types/websocket": "1.0.4",
 		"@types/ws": "8.2.2",
-		"@typescript-eslint/parser": "5.8.1",
-		"@vue/compiler-sfc": "3.2.26",
+		"@typescript-eslint/parser": "5.10.0",
+		"@vue/compiler-sfc": "3.2.28",
 		"abort-controller": "3.0.0",
 		"autobind-decorator": "2.4.0",
 		"autosize": "5.0.1",
@@ -49,37 +48,37 @@
 		"chart.js": "3.7.0",
 		"chartjs-adapter-date-fns": "2.0.0",
 		"chartjs-plugin-zoom": "1.2.0",
-		"compare-versions": "4.1.2",
+		"compare-versions": "4.1.3",
 		"content-disposition": "0.5.4",
 		"crc-32": "1.2.0",
 		"css-loader": "6.5.1",
-		"cssnano": "5.0.14",
+		"cssnano": "5.0.15",
 		"date-fns": "2.28.0",
 		"escape-regexp": "0.0.1",
-		"eslint": "8.6.0",
-		"eslint-plugin-vue": "8.2.0",
+		"eslint": "8.7.0",
+		"eslint-plugin-vue": "8.3.0",
 		"eventemitter3": "4.0.7",
 		"feed": "4.2.2",
 		"glob": "7.2.0",
-		"idb-keyval": "6.0.3",
+		"idb-keyval": "6.1.0",
 		"insert-text-at-cursor": "0.3.0",
 		"ip-cidr": "3.0.4",
 		"json5": "2.2.0",
 		"json5-loader": "4.0.1",
-		"katex": "0.15.1",
+		"katex": "0.15.2",
 		"langmap": "0.0.16",
 		"matter-js": "0.18.0",
-		"mfm-js": "0.20.0",
-		"misskey-js": "0.0.12",
+		"mfm-js": "0.21.0",
+		"misskey-js": "0.0.13",
 		"mocha": "8.4.0",
 		"ms": "2.1.3",
 		"nested-property": "4.0.0",
 		"parse5": "6.0.1",
-		"photoswipe": "git://github.com/dimsemenov/photoswipe#v5-beta",
+		"photoswipe": "git+https://github.com/dimsemenov/photoswipe#v5-beta",
 		"portscanner": "2.2.0",
 		"postcss": "8.4.5",
 		"postcss-loader": "6.2.1",
-		"prismjs": "1.25.0",
+		"prismjs": "1.26.0",
 		"private-ip": "2.3.3",
 		"promise-limit": "2.7.0",
 		"pug": "3.0.2",
@@ -91,7 +90,7 @@
 		"request-stats": "3.0.0",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.2",
-		"sass": "1.45.2",
+		"sass": "1.49.0",
 		"sass-loader": "12.4.0",
 		"seedrandom": "3.0.5",
 		"strict-event-emitter-types": "2.0.0",
@@ -99,7 +98,7 @@
 		"style-loader": "3.3.1",
 		"syuilo-password-strength": "0.0.1",
 		"textarea-caret": "3.1.0",
-		"three": "0.117.1",
+		"three": "0.136.0",
 		"throttle-debounce": "3.0.1",
 		"tinycolor2": "1.4.2",
 		"tmp": "0.2.1",
@@ -108,11 +107,11 @@
 		"tsc-alias": "1.5.0",
 		"tsconfig-paths": "3.12.0",
 		"twemoji-parser": "13.1.0",
-		"typescript": "4.5.4",
+		"typescript": "4.5.5",
 		"uuid": "8.3.2",
 		"v-debounce": "0.1.2",
 		"vanilla-tilt": "1.7.2",
-		"vue": "3.2.26",
+		"vue": "3.2.28",
 		"vue-loader": "17.0.0",
 		"vue-prism-editor": "2.0.0-alpha.2",
 		"vue-router": "4.0.5",
@@ -120,18 +119,18 @@
 		"vue-svg-loader": "0.17.0-beta.2",
 		"vuedraggable": "4.0.1",
 		"web-push": "3.4.5",
-		"webpack": "5.65.0",
+		"webpack": "5.66.0",
 		"webpack-cli": "4.9.1",
 		"websocket": "1.0.34",
-		"ws": "8.4.0"
+		"ws": "8.4.2"
 	},
 	"devDependencies": {
-		"@redocly/openapi-core": "1.0.0-beta.54",
-		"@types/fluent-ffmpeg": "2.1.17",
-		"@typescript-eslint/eslint-plugin": "5.8.1",
+		"@redocly/openapi-core": "1.0.0-beta.79",
+		"@types/fluent-ffmpeg": "2.1.20",
+		"@typescript-eslint/eslint-plugin": "5.10.0",
 		"cross-env": "7.0.3",
-		"cypress": "9.2.0",
-		"eslint-plugin-import": "2.25.3",
+		"cypress": "9.3.1",
+		"eslint-plugin-import": "2.25.4",
 		"start-server-and-test": "1.14.0"
 	}
 }
diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts
index 4c83b78c91..5a935e1dc7 100644
--- a/packages/client/src/account.ts
+++ b/packages/client/src/account.ts
@@ -16,6 +16,8 @@ const data = localStorage.getItem('account');
 // TODO: 外部からはreadonlyに
 export const $i = data ? reactive(JSON.parse(data) as Account) : null;
 
+export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator);
+
 export async function signout() {
 	waiting();
 	localStorage.removeItem('account');
@@ -127,7 +129,12 @@ export async function login(token: Account['token'], redirect?: string) {
 	unisonReload();
 }
 
-export async function openAccountMenu(ev: MouseEvent) {
+export async function openAccountMenu(opts: {
+	includeCurrentAccount?: boolean;
+	withExtraOperation: boolean;
+	active?: misskey.entities.UserDetailed['id'];
+	onChoose?: (account: misskey.entities.UserDetailed) => void;
+}, ev: MouseEvent) {
 	function showSigninDialog() {
 		popup(import('@/components/signin-dialog.vue'), {}, {
 			done: res => {
@@ -146,7 +153,7 @@ export async function openAccountMenu(ev: MouseEvent) {
 		}, 'closed');
 	}
 
-	async function switchAccount(account: any) {
+	async function switchAccount(account: misskey.entities.UserDetailed) {
 		const storedAccounts = await getAccounts();
 		const token = storedAccounts.find(x => x.id === account.id).token;
 		switchAccountWithToken(token);
@@ -159,48 +166,58 @@ export async function openAccountMenu(ev: MouseEvent) {
 	const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
 	const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) });
 
+	function createItem(account: misskey.entities.UserDetailed) {
+		return {
+			type: 'user',
+			user: account,
+			active: opts.active != null ? opts.active === account.id : false,
+			action: () => {
+				if (opts.onChoose) {
+					opts.onChoose(account);
+				} else {
+					switchAccount(account);
+				}
+			},
+		};
+	}
+
 	const accountItemPromises = storedAccounts.map(a => new Promise(res => {
 		accountsPromise.then(accounts => {
 			const account = accounts.find(x => x.id === a.id);
 			if (account == null) return res(null);
-			res({
-				type: 'user',
-				user: account,
-				action: () => { switchAccount(account); }
-			});
+			res(createItem(account));
 		});
 	}));
 
-	popupMenu([...[{
-		type: 'link',
-		text: i18n.locale.profile,
-		to: `/@${ $i.username }`,
-		avatar: $i,
-	}, null, ...accountItemPromises, {
-		icon: 'fas fa-plus',
-		text: i18n.locale.addAccount,
-		action: () => {
-			popupMenu([{
-				text: i18n.locale.existingAccount,
-				action: () => { showSigninDialog(); },
-			}, {
-				text: i18n.locale.createAccount,
-				action: () => { createAccount(); },
-			}], ev.currentTarget || ev.target);
-		},
-	}, {
-		type: 'link',
-		icon: 'fas fa-users',
-		text: i18n.locale.manageAccounts,
-		to: `/settings/accounts`,
-	}]], ev.currentTarget || ev.target, {
-		align: 'left'
-	});
-}
-
-// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
-declare module '@vue/runtime-core' {
-	interface ComponentCustomProperties {
-		$i: typeof $i;
+	if (opts.withExtraOperation) {
+		popupMenu([...[{
+			type: 'link',
+			text: i18n.locale.profile,
+			to: `/@${ $i.username }`,
+			avatar: $i,
+		}, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
+			icon: 'fas fa-plus',
+			text: i18n.locale.addAccount,
+			action: () => {
+				popupMenu([{
+					text: i18n.locale.existingAccount,
+					action: () => { showSigninDialog(); },
+				}, {
+					text: i18n.locale.createAccount,
+					action: () => { createAccount(); },
+				}], ev.currentTarget || ev.target);
+			},
+		}, {
+			type: 'link',
+			icon: 'fas fa-users',
+			text: i18n.locale.manageAccounts,
+			to: `/settings/accounts`,
+		}]], ev.currentTarget || ev.target, {
+			align: 'left'
+		});
+	} else {
+		popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget || ev.target, {
+			align: 'left'
+		});
 	}
 }
diff --git a/packages/client/src/components/note.sub.vue b/packages/client/src/components/MkNoteSub.vue
similarity index 66%
rename from packages/client/src/components/note.sub.vue
rename to packages/client/src/components/MkNoteSub.vue
index de4218e535..30c27e6235 100644
--- a/packages/client/src/components/note.sub.vue
+++ b/packages/client/src/components/MkNoteSub.vue
@@ -10,13 +10,13 @@
 					<XCwButton v-model="showContent" :note="note"/>
 				</p>
 				<div v-show="note.cw == null || showContent" class="content">
-					<XSubNote-content class="text" :note="note"/>
+					<MkNoteSubNoteContent class="text" :note="note"/>
 				</div>
 			</div>
 		</div>
 	</div>
 	<template v-if="depth < 5">
-		<XSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :depth="depth + 1"/>
+		<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :depth="depth + 1"/>
 	</template>
 	<div v-else class="more">
 		<MkA class="text _link" :to="notePage(note)">{{ $ts.continueThread }} <i class="fas fa-angle-double-right"></i></MkA>
@@ -24,63 +24,36 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
 import { notePage } from '@/filters/note';
 import XNoteHeader from './note-header.vue';
-import XSubNoteContent from './sub-note-content.vue';
+import MkNoteSubNoteContent from './sub-note-content.vue';
 import XCwButton from './cw-button.vue';
 import * as os from '@/os';
 
-export default defineComponent({
-	name: 'XSub',
+const props = withDefaults(defineProps<{
+	note: misskey.entities.Note;
+	detail?: boolean;
 
-	components: {
-		XNoteHeader,
-		XSubNoteContent,
-		XCwButton,
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		detail: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		// how many notes are in between this one and the note being viewed in detail
-		depth: {
-			type: Number,
-			required: false,
-			default: 1
-		},
-	},
-
-	data() {
-		return {
-			showContent: false,
-			replies: [],
-		};
-	},
-
-	created() {
-		if (this.detail) {
-			os.api('notes/children', {
-				noteId: this.note.id,
-				limit: 5
-			}).then(replies => {
-				this.replies = replies;
-			});
-		}
-	},
-
-	methods: {
-		notePage,
-	}
+	// how many notes are in between this one and the note being viewed in detail
+	depth?: number;
+}>(), {
+	depth: 1,
 });
+
+let showContent = $ref(false);
+let replies: misskey.entities.Note[] = $ref([]);
+
+if (props.detail) {
+	os.api('notes/children', {
+		noteId: props.note.id,
+		limit: 5
+	}).then(res => {
+		replies = res;
+	});
+}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/abuse-report-window.vue b/packages/client/src/components/abuse-report-window.vue
index 6b07639f6d..cd04f62bca 100644
--- a/packages/client/src/components/abuse-report-window.vue
+++ b/packages/client/src/components/abuse-report-window.vue
@@ -1,8 +1,8 @@
 <template>
-<XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="$emit('closed')">
+<XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
 	<template #header>
 		<i class="fas fa-exclamation-circle" style="margin-right: 0.5em;"></i>
-		<I18n :src="$ts.reportAbuseOf" tag="span">
+		<I18n :src="i18n.locale.reportAbuseOf" tag="span">
 			<template #name>
 				<b><MkAcct :user="user"/></b>
 			</template>
@@ -11,65 +11,51 @@
 	<div class="dpvffvvy _monolithic_">
 		<div class="_section">
 			<MkTextarea v-model="comment">
-				<template #label>{{ $ts.details }}</template>
-				<template #caption>{{ $ts.fillAbuseReportDescription }}</template>
+				<template #label>{{ i18n.locale.details }}</template>
+				<template #caption>{{ i18n.locale.fillAbuseReportDescription }}</template>
 			</MkTextarea>
 		</div>
 		<div class="_section">
-			<MkButton primary full :disabled="comment.length === 0" @click="send">{{ $ts.send }}</MkButton>
+			<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.locale.send }}</MkButton>
 		</div>
 	</div>
 </XWindow>
 </template>
 
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
+<script setup lang="ts">
+import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
 import XWindow from '@/components/ui/window.vue';
 import MkTextarea from '@/components/form/textarea.vue';
 import MkButton from '@/components/ui/button.vue';
 import * as os from '@/os';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		XWindow,
-		MkTextarea,
-		MkButton,
-	},
+const props = defineProps<{
+	user: Misskey.entities.User;
+	initialComment?: string;
+}>();
 
-	props: {
-		user: {
-			type: Object,
-			required: true,
-		},
-		initialComment: {
-			type: String,
-			required: false,
-		},
-	},
+const emit = defineEmits<{
+	(e: 'closed'): void;
+}>();
 
-	emits: ['closed'],
+const window = ref<InstanceType<typeof XWindow>>();
+const comment = ref(props.initialComment || '');
 
-	data() {
-		return {
-			comment: this.initialComment || '',
-		};
-	},
-
-	methods: {
-		send() {
-			os.apiWithDialog('users/report-abuse', {
-				userId: this.user.id,
-				comment: this.comment,
-			}, undefined, res => {
-				os.alert({
-					type: 'success',
-					text: this.$ts.abuseReported
-				});
-				this.$refs.window.close();
-			});
-		}
-	},
-});
+function send() {
+	os.apiWithDialog('users/report-abuse', {
+		userId: props.user.id,
+		comment: comment.value,
+	}, undefined).then(res => {
+		os.alert({
+			type: 'success',
+			text: i18n.locale.abuseReported
+		});
+		window.value?.close();
+		emit('closed');
+	});
+}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/abuse-report.vue b/packages/client/src/components/abuse-report.vue
new file mode 100644
index 0000000000..b67cef209b
--- /dev/null
+++ b/packages/client/src/components/abuse-report.vue
@@ -0,0 +1,102 @@
+<template>
+<div class="bcekxzvu _card _gap">
+	<div class="_content target">
+		<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>
+		<MkA class="info" :to="userPage(report.targetUser)" v-user-preview="report.targetUserId">
+			<MkUserName class="name" :user="report.targetUser"/>
+			<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
+		</MkA>
+	</div>
+	<div class="_content">
+		<div>
+			<Mfm :text="report.comment"/>
+		</div>
+		<hr/>
+		<div>{{ $ts.reporter }}: <MkAcct :user="report.reporter"/></div>
+		<div v-if="report.assignee">
+			{{ $ts.moderator }}:
+			<MkAcct :user="report.assignee"/>
+		</div>
+		<div><MkTime :time="report.createdAt"/></div>
+	</div>
+	<div class="_footer">
+		<MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved">
+			{{ $ts.forwardReport }}
+			<template #caption>{{ $ts.forwardReportIsAnonymous }}</template>
+		</MkSwitch>
+		<MkButton v-if="!report.resolved" primary @click="resolve">{{ $ts.abuseMarkAsResolved }}</MkButton>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+import MkButton from '@/components/ui/button.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import { acct, userPage } from '@/filters/user';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkSwitch,
+	},
+
+	emits: ['resolved'],
+
+	props: {
+		report: {
+			type: Object,
+			required: true,
+		}
+	}
+
+	data() {
+		return {
+			forward: this.report.forwarded,
+		};
+	}
+
+	methods: {
+		acct,
+		userPage,
+
+		resolve() {
+			os.apiWithDialog('admin/resolve-abuse-user-report', {
+				forward: this.forward,
+				reportId: this.report.id,
+			}).then(() => {
+				this.$emit('resolved', this.report.id);
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.bcekxzvu {
+	> .target {
+		display: flex;
+		width: 100%;
+		box-sizing: border-box;
+		text-align: left;
+		align-items: center;
+
+		> .avatar {
+			width: 42px;
+			height: 42px;
+		}
+
+		> .info {
+			margin-left: 0.3em;
+			padding: 0 8px;
+			flex: 1;
+
+			> .name {
+				font-weight: bold;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/analog-clock.vue b/packages/client/src/components/analog-clock.vue
index 450488b198..59b8e97304 100644
--- a/packages/client/src/components/analog-clock.vue
+++ b/packages/client/src/components/analog-clock.vue
@@ -40,106 +40,64 @@
 </svg>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
 import * as tinycolor from 'tinycolor2';
 
-export default defineComponent({
-	props: {
-		thickness: {
-			type: Number,
-			default: 0.1
-		}
-	},
+withDefaults(defineProps<{
+	thickness: number;
+}>(), {
+	thickness: 0.1,
+});
 
-	data() {
-		return {
-			now: new Date(),
-			enabled: true,
+const now = ref(new Date());
+const enabled = ref(true);
+const graduationsPadding = ref(0.5);
+const handsPadding = ref(1);
+const handsTailLength = ref(0.7);
+const hHandLengthRatio = ref(0.75);
+const mHandLengthRatio = ref(1);
+const sHandLengthRatio = ref(1);
+const computedStyle = getComputedStyle(document.documentElement);
 
-			graduationsPadding: 0.5,
-			handsPadding: 1,
-			handsTailLength: 0.7,
-			hHandLengthRatio: 0.75,
-			mHandLengthRatio: 1,
-			sHandLengthRatio: 1,
-
-			computedStyle: getComputedStyle(document.documentElement)
-		};
-	},
-
-	computed: {
-		dark(): boolean {
-			return tinycolor(this.computedStyle.getPropertyValue('--bg')).isDark();
-		},
-
-		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 tinycolor(this.computedStyle.getPropertyValue('--fg')).toHexString();
-		},
-		hHandColor(): string {
-			return tinycolor(this.computedStyle.getPropertyValue('--accent')).toHexString();
-		},
-
-		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 / 60) / 60) / 6;
-		},
-		mAngle(): number {
-			return Math.PI * (this.m + this.s / 60) / 30;
-		},
-		sAngle(): number {
-			return Math.PI * this.s / 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();
-				setTimeout(update, 1000);
-			}
-		};
-		update();
-	},
-
-	beforeUnmount() {
-		this.enabled = false;
-	},
-
-	methods: {
-		tick() {
-			this.now = new Date();
-		}
+const dark = computed(() => tinycolor(computedStyle.getPropertyValue('--bg')).isDark());
+const majorGraduationColor = computed(() => dark.value ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)');
+const minorGraduationColor = computed(() => dark.value ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)');
+const sHandColor = computed(() => dark.value ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)');
+const mHandColor = computed(() => tinycolor(computedStyle.getPropertyValue('--fg')).toHexString());
+const hHandColor = computed(() => tinycolor(computedStyle.getPropertyValue('--accent')).toHexString());
+const s = computed(() => now.value.getSeconds());
+const m = computed(() => now.value.getMinutes());
+const h = computed(() => now.value.getHours());
+const hAngle = computed(() => Math.PI * (h.value % 12 + (m.value + s.value / 60) / 60) / 6);
+const mAngle = computed(() => Math.PI * (m.value + s.value / 60) / 30);
+const sAngle = computed(() => Math.PI * s.value / 30);
+const graduations = computed(() => {
+	const angles: number[] = [];
+	for (let i = 0; i < 60; i++) {
+		const angle = Math.PI * i / 30;
+		angles.push(angle);
 	}
+
+	return angles;
+});
+
+function tick() {
+	now.value = new Date();
+}
+
+onMounted(() => {
+	const update = () => {
+		if (enabled.value) {
+			tick();
+			window.setTimeout(update, 1000);
+		}
+	};
+	update();
+});
+
+onBeforeUnmount(() => {
+	enabled.value = false;
 });
 </script>
 
diff --git a/packages/client/src/components/autocomplete.vue b/packages/client/src/components/autocomplete.vue
index 30be2ac741..7ba83b7cb1 100644
--- a/packages/client/src/components/autocomplete.vue
+++ b/packages/client/src/components/autocomplete.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="swhvrteh _popup _shadow" :style="{ zIndex }" @contextmenu.prevent="() => {}">
+<div ref="rootEl" class="swhvrteh _popup _shadow" :style="{ zIndex }" @contextmenu.prevent="() => {}">
 	<ol v-if="type === 'user'" ref="suggests" class="users">
 		<li v-for="user in users" tabindex="-1" class="user" @click="complete(type, user)" @keydown="onKeydown">
 			<img class="avatar" :src="user.avatarUrl"/>
@@ -8,7 +8,7 @@
 			</span>
 			<span class="username">@{{ acct(user) }}</span>
 		</li>
-		<li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ $ts.selectUser }}</li>
+		<li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ i18n.locale.selectUser }}</li>
 	</ol>
 	<ol v-else-if="hashtags.length > 0" ref="suggests" class="hashtags">
 		<li v-for="hashtag in hashtags" tabindex="-1" @click="complete(type, hashtag)" @keydown="onKeydown">
@@ -17,8 +17,8 @@
 	</ol>
 	<ol v-else-if="emojis.length > 0" ref="suggests" class="emojis">
 		<li v-for="emoji in emojis" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown">
-			<span v-if="emoji.isCustomEmoji" class="emoji"><img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
-			<span v-else-if="!$store.state.useOsNativeEmojis" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span>
+			<span v-if="emoji.isCustomEmoji" class="emoji"><img :src="defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
+			<span v-else-if="!defaultStore.state.useOsNativeEmojis" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span>
 			<span v-else class="emoji">{{ emoji.emoji }}</span>
 			<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
 			<span v-if="emoji.aliasOf" class="alias">({{ emoji.aliasOf }})</span>
@@ -33,15 +33,17 @@
 </template>
 
 <script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import { emojilist } from '@/scripts/emojilist';
+import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
 import contains from '@/scripts/contains';
-import { twemojiSvgBase } from '@/scripts/twemoji-base';
 import { getStaticImageUrl } from '@/scripts/get-static-image-url';
 import { acct } from '@/filters/user';
 import * as os from '@/os';
-import { instance } from '@/instance';
 import { MFM_TAGS } from '@/scripts/mfm-tags';
+import { defaultStore } from '@/store';
+import { emojilist } from '@/scripts/emojilist';
+import { instance } from '@/instance';
+import { twemojiSvgBase } from '@/scripts/twemoji-base';
+import { i18n } from '@/i18n';
 
 type EmojiDef = {
 	emoji: string;
@@ -54,16 +56,14 @@ type EmojiDef = {
 const lib = emojilist.filter(x => x.category !== 'flags');
 
 const char2file = (char: string) => {
-	let codes = Array.from(char).map(x => x.codePointAt(0).toString(16));
+	let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16));
 	if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f');
-	codes = codes.filter(x => x && x.length);
-	return codes.join('-');
+	return codes.filter(x => x && x.length).join('-');
 };
 
 const emjdb: EmojiDef[] = lib.map(x => ({
 	emoji: x.char,
 	name: x.name,
-	aliasOf: null,
 	url: `${twemojiSvgBase}/${char2file(x.char)}.svg`
 }));
 
@@ -112,291 +112,270 @@ emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
 const emojiDb = markRaw(emojiDefinitions.concat(emjdb));
 //#endregion
 
-export default defineComponent({
-	props: {
-		type: {
-			type: String,
-			required: true,
-		},
+export default {
+	emojiDb,
+	emojiDefinitions,
+	emojilist,
+	customEmojis,
+};
+</script>
 
-		q: {
-			type: String,
-			required: false,
-		},
+<script lang="ts" setup>
+const props = defineProps<{
+	type: string;
+	q: string | null;
+	textarea: HTMLTextAreaElement;
+	close: () => void;
+	x: number;
+	y: number;
+}>();
 
-		textarea: {
-			type: HTMLTextAreaElement,
-			required: true,
-		},
+const emit = defineEmits<{
+	(e: 'done', v: { type: string; value: any }): void;
+	(e: 'closed'): void;
+}>();
 
-		close: {
-			type: Function,
-			required: true,
-		},
+const suggests = ref<Element>();
+const rootEl = ref<HTMLDivElement>();
 
-		x: {
-			type: Number,
-			required: true,
-		},
+const fetching = ref(true);
+const users = ref<any[]>([]);
+const hashtags = ref<any[]>([]);
+const emojis = ref<(EmojiDef)[]>([]);
+const items = ref<Element[] | HTMLCollection>([]);
+const mfmTags = ref<string[]>([]);
+const select = ref(-1);
+const zIndex = os.claimZIndex('high');
 
-		y: {
-			type: Number,
-			required: true,
-		},
-	},
+function complete(type: string, value: any) {
+	emit('done', { type, value });
+	emit('closed');
+	if (type === 'emoji') {
+		let recents = defaultStore.state.recentlyUsedEmojis;
+		recents = recents.filter((e: any) => e !== value);
+		recents.unshift(value);
+		defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
+	}
+}
 
-	emits: ['done', 'closed'],
+function setPosition() {
+	if (!rootEl.value) return;
+	if (props.x + rootEl.value.offsetWidth > window.innerWidth) {
+		rootEl.value.style.left = (window.innerWidth - rootEl.value.offsetWidth) + 'px';
+	} else {
+		rootEl.value.style.left = `${props.x}px`;
+	}
+	if (props.y + rootEl.value.offsetHeight > window.innerHeight) {
+		rootEl.value.style.top = (props.y - rootEl.value.offsetHeight) + 'px';
+		rootEl.value.style.marginTop = '0';
+	} else {
+		rootEl.value.style.top = props.y + 'px';
+		rootEl.value.style.marginTop = 'calc(1em + 8px)';
+	}
+}
 
-	data() {
-		return {
-			getStaticImageUrl,
-			fetching: true,
-			users: [],
-			hashtags: [],
-			emojis: [],
-			items: [],
-			mfmTags: [],
-			select: -1,
-			zIndex: os.claimZIndex('high'),
+function exec() {
+	select.value = -1;
+	if (suggests.value) {
+		for (const el of Array.from(items.value)) {
+			el.removeAttribute('data-selected');
 		}
-	},
-
-	updated() {
-		this.setPosition();
-		this.items = (this.$refs.suggests as Element | undefined)?.children || [];
-	},
-
-	mounted() {
-		this.setPosition();
-
-		this.textarea.addEventListener('keydown', this.onKeydown);
-
-		for (const el of Array.from(document.querySelectorAll('body *'))) {
-			el.addEventListener('mousedown', this.onMousedown);
+	}
+	if (props.type === 'user') {
+		if (!props.q) {
+			users.value = [];
+			fetching.value = false;
+			return;
 		}
 
-		this.$nextTick(() => {
-			this.exec();
+		const cacheKey = `autocomplete:user:${props.q}`;
+		const cache = sessionStorage.getItem(cacheKey);
 
-			this.$watch('q', () => {
-				this.$nextTick(() => {
-					this.exec();
+		if (cache) {
+			const users = JSON.parse(cache);
+			users.value = users;
+			fetching.value = false;
+		} else {
+			os.api('users/search-by-username-and-host', {
+				username: props.q,
+				limit: 10,
+				detail: false
+			}).then(searchedUsers => {
+				users.value = searchedUsers as any[];
+				fetching.value = false;
+				// キャッシュ
+				sessionStorage.setItem(cacheKey, JSON.stringify(searchedUsers));
+			});
+		}
+	} else if (props.type === 'hashtag') {
+		if (!props.q || props.q == '') {
+			hashtags.value = JSON.parse(localStorage.getItem('hashtags') || '[]');
+			fetching.value = false;
+		} else {
+			const cacheKey = `autocomplete:hashtag:${props.q}`;
+			const cache = sessionStorage.getItem(cacheKey);
+			if (cache) {
+				const hashtags = JSON.parse(cache);
+				hashtags.value = hashtags;
+				fetching.value = false;
+			} else {
+				os.api('hashtags/search', {
+					query: props.q,
+					limit: 30
+				}).then(searchedHashtags => {
+					hashtags.value = searchedHashtags as any[];
+					fetching.value = false;
+					// キャッシュ
+					sessionStorage.setItem(cacheKey, JSON.stringify(searchedHashtags));
 				});
+			}
+		}
+	} else if (props.type === 'emoji') {
+		if (!props.q || props.q == '') {
+			// 最近使った絵文字をサジェスト
+			emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji == emoji)).filter(x => x) as EmojiDef[];
+			return;
+		}
+
+		const matched: EmojiDef[] = [];
+		const max = 30;
+
+		emojiDb.some(x => {
+			if (x.name.startsWith(props.q || '') && !x.aliasOf && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
+			return matched.length == max;
+		});
+
+		if (matched.length < max) {
+			emojiDb.some(x => {
+				if (x.name.startsWith(props.q || '') && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
+				return matched.length == max;
+			});
+		}
+
+		if (matched.length < max) {
+			emojiDb.some(x => {
+				if (x.name.includes(props.q || '') && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
+				return matched.length == max;
+			});
+		}
+
+		emojis.value = matched;
+	} else if (props.type === 'mfmTag') {
+		if (!props.q || props.q == '') {
+			mfmTags.value = MFM_TAGS;
+			return;
+		}
+
+		mfmTags.value = MFM_TAGS.filter(tag => tag.startsWith(props.q || ''));
+	}
+}
+
+function onMousedown(e: Event) {
+	if (!contains(rootEl.value, e.target) && (rootEl.value != e.target)) props.close();
+}
+
+function onKeydown(e: KeyboardEvent) {
+	const cancel = () => {
+		e.preventDefault();
+		e.stopPropagation();
+	};
+
+	switch (e.key) {
+		case 'Enter':
+			if (select.value !== -1) {
+				cancel();
+				(items.value[select.value] as any).click();
+			} else {
+				props.close();
+			}
+			break;
+
+		case 'Escape':
+			cancel();
+			props.close();
+			break;
+
+		case 'ArrowUp':
+			if (select.value !== -1) {
+				cancel();
+				selectPrev();
+			} else {
+				props.close();
+			}
+			break;
+
+		case 'Tab':
+		case 'ArrowDown':
+			cancel();
+			selectNext();
+			break;
+
+		default:
+			e.stopPropagation();
+			props.textarea.focus();
+	}
+}
+
+function selectNext() {
+	if (++select.value >= items.value.length) select.value = 0;
+	if (items.value.length === 0) select.value = -1;
+	applySelect();
+}
+
+function selectPrev() {
+	if (--select.value < 0) select.value = items.value.length - 1;
+	applySelect();
+}
+
+function applySelect() {
+	for (const el of Array.from(items.value)) {
+		el.removeAttribute('data-selected');
+	}
+
+	if (select.value !== -1) {
+		items.value[select.value].setAttribute('data-selected', 'true');
+		(items.value[select.value] as any).focus();
+	}
+}
+
+function chooseUser() {
+	props.close();
+	os.selectUser().then(user => {
+		complete('user', user);
+		props.textarea.focus();
+	});
+}
+
+onUpdated(() => {
+	setPosition();
+	items.value = suggests.value?.children || [];
+});
+
+onMounted(() => {
+	setPosition();
+
+	props.textarea.addEventListener('keydown', onKeydown);
+
+	for (const el of Array.from(document.querySelectorAll('body *'))) {
+		el.addEventListener('mousedown', onMousedown);
+	}
+
+	nextTick(() => {
+		exec();
+
+		watch(() => props.q, () => {
+			nextTick(() => {
+				exec();
 			});
 		});
-	},
+	});
+});
 
-	beforeUnmount() {
-		this.textarea.removeEventListener('keydown', this.onKeydown);
+onBeforeUnmount(() => {
+	props.textarea.removeEventListener('keydown', onKeydown);
 
-		for (const el of Array.from(document.querySelectorAll('body *'))) {
-			el.removeEventListener('mousedown', this.onMousedown);
-		}
-	},
-
-	methods: {
-		complete(type, value) {
-			this.$emit('done', { type, value });
-			this.$emit('closed');
-
-			if (type === 'emoji') {
-				let recents = this.$store.state.recentlyUsedEmojis;
-				recents = recents.filter((e: any) => e !== value);
-				recents.unshift(value);
-				this.$store.set('recentlyUsedEmojis', recents.splice(0, 32));
-			}
-		},
-
-		setPosition() {
-			if (this.x + this.$el.offsetWidth > window.innerWidth) {
-				this.$el.style.left = (window.innerWidth - this.$el.offsetWidth) + 'px';
-			} else {
-				this.$el.style.left = this.x + 'px';
-			}
-
-			if (this.y + this.$el.offsetHeight > window.innerHeight) {
-				this.$el.style.top = (this.y - this.$el.offsetHeight) + 'px';
-				this.$el.style.marginTop = '0';
-			} else {
-				this.$el.style.top = this.y + 'px';
-				this.$el.style.marginTop = 'calc(1em + 8px)';
-			}
-		},
-
-		exec() {
-			this.select = -1;
-			if (this.$refs.suggests) {
-				for (const el of Array.from(this.items)) {
-					el.removeAttribute('data-selected');
-				}
-			}
-
-			if (this.type === 'user') {
-				if (this.q == null) {
-					this.users = [];
-					this.fetching = false;
-					return;
-				}
-
-				const cacheKey = `autocomplete:user:${this.q}`;
-				const cache = sessionStorage.getItem(cacheKey);
-				if (cache) {
-					const users = JSON.parse(cache);
-					this.users = users;
-					this.fetching = false;
-				} else {
-					os.api('users/search-by-username-and-host', {
-						username: this.q,
-						limit: 10,
-						detail: false
-					}).then(users => {
-						this.users = users;
-						this.fetching = false;
-
-						// キャッシュ
-						sessionStorage.setItem(cacheKey, JSON.stringify(users));
-					});
-				}
-			} else if (this.type === 'hashtag') {
-				if (this.q == null || this.q == '') {
-					this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
-					this.fetching = false;
-				} else {
-					const cacheKey = `autocomplete:hashtag:${this.q}`;
-					const cache = sessionStorage.getItem(cacheKey);
-					if (cache) {
-						const hashtags = JSON.parse(cache);
-						this.hashtags = hashtags;
-						this.fetching = false;
-					} else {
-						os.api('hashtags/search', {
-							query: this.q,
-							limit: 30
-						}).then(hashtags => {
-							this.hashtags = hashtags;
-							this.fetching = false;
-
-							// キャッシュ
-							sessionStorage.setItem(cacheKey, JSON.stringify(hashtags));
-						});
-					}
-				}
-			} else if (this.type === 'emoji') {
-				if (this.q == null || this.q == '') {
-					// 最近使った絵文字をサジェスト
-					this.emojis = this.$store.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji == emoji)).filter(x => x != null);
-					return;
-				}
-
-				const matched = [];
-				const max = 30;
-
-				emojiDb.some(x => {
-					if (x.name.startsWith(this.q) && !x.aliasOf && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
-					return matched.length == max;
-				});
-				if (matched.length < max) {
-					emojiDb.some(x => {
-						if (x.name.startsWith(this.q) && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
-						return matched.length == max;
-					});
-				}
-				if (matched.length < max) {
-					emojiDb.some(x => {
-						if (x.name.includes(this.q) && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
-						return matched.length == max;
-					});
-				}
-
-				this.emojis = matched;
-			} else if (this.type === 'mfmTag') {
-				if (this.q == null || this.q == '') {
-					this.mfmTags = MFM_TAGS;
-					return;
-				}
-
-				this.mfmTags = MFM_TAGS.filter(tag => tag.startsWith(this.q));
-			}
-		},
-
-		onMousedown(e) {
-			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
-		},
-
-		onKeydown(e) {
-			const cancel = () => {
-				e.preventDefault();
-				e.stopPropagation();
-			};
-
-			switch (e.which) {
-				case 10: // [ENTER]
-				case 13: // [ENTER]
-					if (this.select !== -1) {
-						cancel();
-						(this.items[this.select] as any).click();
-					} else {
-						this.close();
-					}
-					break;
-
-				case 27: // [ESC]
-					cancel();
-					this.close();
-					break;
-
-				case 38: // [↑]
-					if (this.select !== -1) {
-						cancel();
-						this.selectPrev();
-					} else {
-						this.close();
-					}
-					break;
-
-				case 9: // [TAB]
-				case 40: // [↓]
-					cancel();
-					this.selectNext();
-					break;
-
-				default:
-					e.stopPropagation();
-					this.textarea.focus();
-			}
-		},
-
-		selectNext() {
-			if (++this.select >= this.items.length) this.select = 0;
-			if (this.items.length === 0) this.select = -1;
-			this.applySelect();
-		},
-
-		selectPrev() {
-			if (--this.select < 0) this.select = this.items.length - 1;
-			this.applySelect();
-		},
-
-		applySelect() {
-			for (const el of Array.from(this.items)) {
-				el.removeAttribute('data-selected');
-			}
-
-			if (this.select !== -1) {
-				this.items[this.select].setAttribute('data-selected', 'true');
-				(this.items[this.select] as any).focus();
-			}
-		},
-
-		chooseUser() {
-			this.close();
-			os.selectUser().then(user => {
-				this.complete('user', user);
-				this.textarea.focus();
-			});
-		},
-
-		acct
+	for (const el of Array.from(document.querySelectorAll('body *'))) {
+		el.removeEventListener('mousedown', onMousedown);
 	}
 });
 </script>
diff --git a/packages/client/src/components/captcha.vue b/packages/client/src/components/captcha.vue
index baa922506e..770804cf44 100644
--- a/packages/client/src/components/captcha.vue
+++ b/packages/client/src/components/captcha.vue
@@ -1,12 +1,14 @@
 <template>
 <div>
-	<span v-if="!available">{{ $ts.waiting }}<MkEllipsis/></span>
-	<div ref="captcha"></div>
+	<span v-if="!available">{{ i18n.locale.waiting }}<MkEllipsis/></span>
+	<div ref="captchaEl"></div>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, PropType } from 'vue';
+<script lang="ts" setup>
+import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';
+import { defaultStore } from '@/store';
+import { i18n } from '@/i18n';
 
 type Captcha = {
 	render(container: string | Node, options: {
@@ -14,7 +16,7 @@ type Captcha = {
 	}): string;
 	remove(id: string): void;
 	execute(id: string): void;
-	reset(id: string): void;
+	reset(id?: string): void;
 	getResponse(id: string): string;
 };
 
@@ -29,95 +31,85 @@ declare global {
 	}
 }
 
-export default defineComponent({
-	props: {
-		provider: {
-			type: String as PropType<CaptchaProvider>,
-			required: true,
-		},
-		sitekey: {
-			type: String,
-			required: true,
-		},
-		modelValue: {
-			type: String,
-		},
-	},
+const props = defineProps<{
+	provider: CaptchaProvider;
+	sitekey: string;
+	modelValue?: string | null;
+}>();
 
-	data() {
-		return {
-			available: false,
-		};
-	},
+const emit = defineEmits<{
+	(e: 'update:modelValue', v: string | null): void;
+}>();
 
-	computed: {
-		variable(): string {
-			switch (this.provider) {
-				case 'hcaptcha': return 'hcaptcha';
-				case 'recaptcha': return 'grecaptcha';
-			}
-		},
-		loaded(): boolean {
-			return !!window[this.variable];
-		},
-		src(): string {
-			const endpoint = ({
-				hcaptcha: 'https://hcaptcha.com/1',
-				recaptcha: 'https://www.recaptcha.net/recaptcha',
-			} as Record<CaptchaProvider, string>)[this.provider];
+const available = ref(false);
 
-			return `${typeof endpoint === 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`;
-		},
-		captcha(): Captcha {
-			return window[this.variable] || {} as unknown as Captcha;
-		},
-	},
+const captchaEl = ref<HTMLDivElement | undefined>();
 
-	created() {
-		if (this.loaded) {
-			this.available = true;
-		} else {
-			(document.getElementById(this.provider) || document.head.appendChild(Object.assign(document.createElement('script'), {
-				async: true,
-				id: this.provider,
-				src: this.src,
-			})))
-				.addEventListener('load', () => this.available = true);
-		}
-	},
-
-	mounted() {
-		if (this.available) {
-			this.requestRender();
-		} else {
-			this.$watch('available', this.requestRender);
-		}
-	},
-
-	beforeUnmount() {
-		this.reset();
-	},
-
-	methods: {
-		reset() {
-			if (this.captcha?.reset) this.captcha.reset();
-		},
-		requestRender() {
-			if (this.captcha.render && this.$refs.captcha instanceof Element) {
-				this.captcha.render(this.$refs.captcha, {
-					sitekey: this.sitekey,
-					theme: this.$store.state.darkMode ? 'dark' : 'light',
-					callback: this.callback,
-					'expired-callback': this.callback,
-					'error-callback': this.callback,
-				});
-			} else {
-				setTimeout(this.requestRender.bind(this), 1);
-			}
-		},
-		callback(response?: string) {
-			this.$emit('update:modelValue', typeof response == 'string' ? response : null);
-		},
-	},
+const variable = computed(() => {
+	switch (props.provider) {
+		case 'hcaptcha': return 'hcaptcha';
+		case 'recaptcha': return 'grecaptcha';
+	}
 });
+
+const loaded = computed(() => !!window[variable.value]);
+
+const src = computed(() => {
+	switch (props.provider) {
+		case 'hcaptcha': return 'https://js.hcaptcha.com/1/api.js?render=explicit&recaptchacompat=off';
+		case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit';
+	}
+});
+
+const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
+
+if (loaded.value) {
+	available.value = true;
+} else {
+	(document.getElementById(props.provider) || document.head.appendChild(Object.assign(document.createElement('script'), {
+		async: true,
+		id: props.provider,
+		src: src.value,
+	})))
+		.addEventListener('load', () => available.value = true);
+}
+
+function reset() {
+	if (captcha.value?.reset) captcha.value.reset();
+}
+
+function requestRender() {
+	if (captcha.value.render && captchaEl.value instanceof Element) {
+		captcha.value.render(captchaEl.value, {
+			sitekey: props.sitekey,
+			theme: defaultStore.state.darkMode ? 'dark' : 'light',
+			callback: callback,
+			'expired-callback': callback,
+			'error-callback': callback,
+		});
+	} else {
+		window.setTimeout(requestRender, 1);
+	}
+}
+
+function callback(response?: string) {
+	emit('update:modelValue', typeof response == 'string' ? response : null);
+}
+
+onMounted(() => {
+	if (available.value) {
+		requestRender();
+	} else {
+		watch(available, requestRender);
+	}
+});
+
+onBeforeUnmount(() => {
+	reset();
+});
+
+defineExpose({
+	reset,
+});
+
 </script>
diff --git a/packages/client/src/components/channel-follow-button.vue b/packages/client/src/components/channel-follow-button.vue
index abde7c8148..0ad5384cd5 100644
--- a/packages/client/src/components/channel-follow-button.vue
+++ b/packages/client/src/components/channel-follow-button.vue
@@ -6,66 +6,54 @@
 >
 	<template v-if="!wait">
 		<template v-if="isFollowing">
-			<span v-if="full">{{ $ts.unfollow }}</span><i class="fas fa-minus"></i>
+			<span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i>
 		</template>
 		<template v-else>
-			<span v-if="full">{{ $ts.follow }}</span><i class="fas fa-plus"></i>
+			<span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i>
 		</template>
 	</template>
 	<template v-else>
-		<span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
+		<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
 	</template>
 </button>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
 import * as os from '@/os';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	props: {
-		channel: {
-			type: Object,
-			required: true
-		},
-		full: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-	},
-
-	data() {
-		return {
-			isFollowing: this.channel.isFollowing,
-			wait: false,
-		};
-	},
-
-	methods: {
-		async onClick() {
-			this.wait = true;
-
-			try {
-				if (this.isFollowing) {
-					await os.api('channels/unfollow', {
-						channelId: this.channel.id
-					});
-					this.isFollowing = false;
-				} else {
-					await os.api('channels/follow', {
-						channelId: this.channel.id
-					});
-					this.isFollowing = true;
-				}
-			} catch (e) {
-				console.error(e);
-			} finally {
-				this.wait = false;
-			}
-		}
-	}
+const props = withDefaults(defineProps<{
+	channel: Record<string, any>;
+	full?: boolean;
+}>(), {
+	full: false,
 });
+
+const isFollowing = ref<boolean>(props.channel.isFollowing);
+const wait = ref(false);
+
+async function onClick() {
+	wait.value = true;
+
+	try {
+		if (isFollowing.value) {
+			await os.api('channels/unfollow', {
+				channelId: props.channel.id
+			});
+			isFollowing.value = false;
+		} else {
+			await os.api('channels/follow', {
+				channelId: props.channel.id
+			});
+			isFollowing.value = true;
+		}
+	} catch (e) {
+		console.error(e);
+	} finally {
+		wait.value = false;
+	}
+}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/channel-preview.vue b/packages/client/src/components/channel-preview.vue
index f2b6de97fd..8d135a192f 100644
--- a/packages/client/src/components/channel-preview.vue
+++ b/packages/client/src/components/channel-preview.vue
@@ -6,7 +6,7 @@
 		<div class="status">
 			<div>
 				<i class="fas fa-users fa-fw"></i>
-				<I18n :src="$ts._channel.usersCount" tag="span" style="margin-left: 4px;">
+				<I18n :src="i18n.locale._channel.usersCount" tag="span" style="margin-left: 4px;">
 					<template #n>
 						<b>{{ channel.usersCount }}</b>
 					</template>
@@ -14,7 +14,7 @@
 			</div>
 			<div>
 				<i class="fas fa-pencil-alt fa-fw"></i>
-				<I18n :src="$ts._channel.notesCount" tag="span" style="margin-left: 4px;">
+				<I18n :src="i18n.locale._channel.notesCount" tag="span" style="margin-left: 4px;">
 					<template #n>
 						<b>{{ channel.notesCount }}</b>
 					</template>
@@ -27,37 +27,26 @@
 	</article>
 	<footer>
 		<span v-if="channel.lastNotedAt">
-			{{ $ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
+			{{ i18n.locale.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
 		</span>
 	</footer>
 </MkA>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	props: {
-		channel: {
-			type: Object,
-			required: true
-		},
-	},
+const props = defineProps<{
+	channel: Record<string, any>;
+}>();
 
-	data() {
-		return {
-		};
-	},
-
-	computed: {
-		bannerStyle() {
-			if (this.channel.bannerUrl) {
-				return { backgroundImage: `url(${this.channel.bannerUrl})` };
-			} else {
-				return { backgroundColor: '#4c5e6d' };
-			}
-		}
-	},
+const bannerStyle = computed(() => {
+	if (props.channel.bannerUrl) {
+		return { backgroundImage: `url(${props.channel.bannerUrl})` };
+	} else {
+		return { backgroundColor: '#4c5e6d' };
+	}
 });
 </script>
 
diff --git a/packages/client/src/components/code-core.vue b/packages/client/src/components/code-core.vue
index b58484c2ac..45a38afe04 100644
--- a/packages/client/src/components/code-core.vue
+++ b/packages/client/src/components/code-core.vue
@@ -3,33 +3,17 @@
 <pre v-else :class="`language-${prismLang}`"><code :class="`language-${prismLang}`" v-html="html"></code></pre>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
 import 'prismjs';
 import 'prismjs/themes/prism-okaidia.css';
 
-export default defineComponent({
-	props: {
-		code: {
-			type: String,
-			required: true
-		},
-		lang: {
-			type: String,
-			required: false
-		},
-		inline: {
-			type: Boolean,
-			required: false
-		}
-	},
-	computed: {
-		prismLang() {
-			return Prism.languages[this.lang] ? this.lang : 'js';
-		},
-		html() {
-			return Prism.highlight(this.code, Prism.languages[this.prismLang], this.prismLang);
-		}
-	}
-});
+const props = defineProps<{
+	code: string;
+	lang?: string;
+	inline?: boolean;
+}>();
+
+const prismLang = computed(() => Prism.languages[props.lang] ? props.lang : 'js');
+const html = computed(() => Prism.highlight(props.code, Prism.languages[prismLang.value], prismLang.value));
 </script>
diff --git a/packages/client/src/components/code.vue b/packages/client/src/components/code.vue
index f5d6c5673a..d6478fd2f8 100644
--- a/packages/client/src/components/code.vue
+++ b/packages/client/src/components/code.vue
@@ -2,26 +2,14 @@
 <XCode :code="code" :lang="lang" :inline="inline"/>
 </template>
 
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
+<script lang="ts" setup>
+import { defineAsyncComponent } from 'vue';
 
-export default defineComponent({
-	components: {
-		XCode: defineAsyncComponent(() => import('./code-core.vue'))
-	},
-	props: {
-		code: {
-			type: String,
-			required: true
-		},
-		lang: {
-			type: String,
-			required: false
-		},
-		inline: {
-			type: Boolean,
-			required: false
-		}
-	}
-});
+defineProps<{
+	code: string;
+	lang?: string;
+	inline?: boolean;
+}>();
+
+const XCode = defineAsyncComponent(() => import('./code-core.vue'));
 </script>
diff --git a/packages/client/src/components/cw-button.vue b/packages/client/src/components/cw-button.vue
index b0a9860de2..ccfd11462a 100644
--- a/packages/client/src/components/cw-button.vue
+++ b/packages/client/src/components/cw-button.vue
@@ -1,6 +1,6 @@
 <template>
 <button class="nrvgflfu _button" @click="toggle">
-	<b>{{ modelValue ? $ts._cw.hide : $ts._cw.show }}</b>
+	<b>{{ modelValue ? i18n.locale._cw.hide : i18n.locale._cw.show }}</b>
 	<span v-if="!modelValue">{{ label }}</span>
 </button>
 </template>
diff --git a/packages/client/src/components/date-separated-list.vue b/packages/client/src/components/date-separated-list.vue
index aa84c6f60d..c85a0a6ffc 100644
--- a/packages/client/src/components/date-separated-list.vue
+++ b/packages/client/src/components/date-separated-list.vue
@@ -1,6 +1,8 @@
 <script lang="ts">
 import { defineComponent, h, PropType, TransitionGroup } from 'vue';
 import MkAd from '@/components/global/ad.vue';
+import { i18n } from '@/i18n';
+import { defaultStore } from '@/store';
 
 export default defineComponent({
 	props: {
@@ -30,29 +32,29 @@ export default defineComponent({
 		},
 	},
 
-	methods: {
-		getDateText(time: string) {
+	setup(props, { slots, expose }) {
+		function getDateText(time: string) {
 			const date = new Date(time).getDate();
 			const month = new Date(time).getMonth() + 1;
-			return this.$t('monthAndDay', {
+			return i18n.t('monthAndDay', {
 				month: month.toString(),
 				day: date.toString()
 			});
 		}
-	},
 
-	render() {
-		if (this.items.length === 0) return;
+		if (props.items.length === 0) return;
 
-		const renderChildren = () => this.items.map((item, i) => {
-			const el = this.$slots.default({
+		const renderChildren = () => props.items.map((item, i) => {
+			if (!slots || !slots.default) return;
+
+			const el = slots.default({
 				item: item
 			})[0];
 			if (el.key == null && item.id) el.key = item.id;
 
 			if (
-				i != this.items.length - 1 &&
-				new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate()
+				i != props.items.length - 1 &&
+				new Date(item.createdAt).getDate() != new Date(props.items[i + 1].createdAt).getDate()
 			) {
 				const separator = h('div', {
 					class: 'separator',
@@ -64,10 +66,10 @@ export default defineComponent({
 						h('i', {
 							class: 'fas fa-angle-up icon',
 						}),
-						this.getDateText(item.createdAt)
+						getDateText(item.createdAt)
 					]),
 					h('span', [
-						this.getDateText(this.items[i + 1].createdAt),
+						getDateText(props.items[i + 1].createdAt),
 						h('i', {
 							class: 'fas fa-angle-down icon',
 						})
@@ -76,7 +78,7 @@ export default defineComponent({
 
 				return [el, separator];
 			} else {
-				if (this.ad && item._shouldInsertAd_) {
+				if (props.ad && item._shouldInsertAd_) {
 					return [h(MkAd, {
 						class: 'a', // advertiseの意(ブロッカー対策)
 						key: item.id + ':ad',
@@ -88,18 +90,19 @@ export default defineComponent({
 			}
 		});
 
-		return h(this.$store.state.animation ? TransitionGroup : 'div', this.$store.state.animation ? {
-			class: 'sqadhkmv' + (this.noGap ? ' noGap' : ''),
-			name: 'list',
-			tag: 'div',
-			'data-direction': this.direction,
-			'data-reversed': this.reversed ? 'true' : 'false',
-		} : {
-			class: 'sqadhkmv' + (this.noGap ? ' noGap' : ''),
-		}, {
-			default: renderChildren
-		});
-	},
+		return () => h(
+			defaultStore.state.animation ? TransitionGroup : 'div',
+			defaultStore.state.animation ? {
+					class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''),
+					name: 'list',
+					tag: 'div',
+					'data-direction': props.direction,
+					'data-reversed': props.reversed ? 'true' : 'false',
+				} : {
+					class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''),
+				},
+			{ default: renderChildren });
+	}
 });
 </script>
 
diff --git a/packages/client/src/components/dialog.vue b/packages/client/src/components/dialog.vue
index c2fa1b02b8..b6b649cde9 100644
--- a/packages/client/src/components/dialog.vue
+++ b/packages/client/src/components/dialog.vue
@@ -1,5 +1,5 @@
 <template>
-<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="$emit('closed')">
+<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="emit('closed')">
 	<div class="mk-dialog">
 		<div v-if="icon" class="icon">
 			<i :class="icon"></i>
@@ -14,7 +14,7 @@
 		</div>
 		<header v-if="title"><Mfm :text="title"/></header>
 		<div v-if="text" class="body"><Mfm :text="text"/></div>
-		<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown">
+		<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" @keydown="onInputKeydown">
 			<template v-if="input.type === 'password'" #prefix><i class="fas fa-lock"></i></template>
 		</MkInput>
 		<MkSelect v-if="select" v-model="selectedValue" autofocus>
@@ -28,8 +28,8 @@
 			</template>
 		</MkSelect>
 		<div v-if="(showOkButton || showCancelButton) && !actions" class="buttons">
-			<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? $ts.ok : $ts.gotIt }}</MkButton>
-			<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ $ts.cancel }}</MkButton>
+			<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.locale.ok : i18n.locale.gotIt }}</MkButton>
+			<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.locale.cancel }}</MkButton>
 		</div>
 		<div v-if="actions" class="buttons">
 			<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); close(); }">{{ action.text }}</MkButton>
@@ -38,118 +38,108 @@
 </MkModal>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onBeforeUnmount, onMounted, ref } from 'vue';
 import MkModal from '@/components/ui/modal.vue';
 import MkButton from '@/components/ui/button.vue';
 import MkInput from '@/components/form/input.vue';
 import MkSelect from '@/components/form/select.vue';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		MkModal,
-		MkButton,
-		MkInput,
-		MkSelect,
-	},
+type Input = {
+	type: HTMLInputElement['type'];
+	placeholder?: string | null;
+	default: any | null;
+};
 
-	props: {
-		type: {
-			type: String,
-			required: false,
-			default: 'info'
-		},
-		title: {
-			type: String,
-			required: false
-		},
-		text: {
-			type: String,
-			required: false
-		},
-		input: {
-			required: false
-		},
-		select: {
-			required: false
-		},
-		icon: {
-			required: false
-		},
-		actions: {
-			required: false
-		},
-		showOkButton: {
-			type: Boolean,
-			default: true
-		},
-		showCancelButton: {
-			type: Boolean,
-			default: false
-		},
-		cancelableByBgClick: {
-			type: Boolean,
-			default: true
-		},
-	},
+type Select = {
+	items: {
+		value: string;
+		text: string;
+	}[];
+	groupedItems: {
+		label: string;
+		items: {
+			value: string;
+			text: string;
+		}[];
+	}[];
+	default: string | null;
+};
 
-	emits: ['done', 'closed'],
+const props = withDefaults(defineProps<{
+	type?: 'success' | 'error' | 'warning' | 'info' | 'question' | 'waiting';
+	title: string;
+	text?: string;
+	input?: Input;
+	select?: Select;
+	icon?: string;
+	actions?: {
+		text: string;
+		primary?: boolean,
+		callback: (...args: any[]) => void;
+	}[];
+	showOkButton?: boolean;
+	showCancelButton?: boolean;
+	cancelableByBgClick?: boolean;
+}>(), {
+	type: 'info',
+	showOkButton: true,
+	showCancelButton: false,
+	cancelableByBgClick: true,
+});
 
-	data() {
-		return {
-			inputValue: this.input && this.input.default ? this.input.default : 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,
-		};
-	},
+const emit = defineEmits<{
+	(e: 'done', v: { canceled: boolean; result: any }): void;
+	(e: 'closed'): void;
+}>();
 
-	mounted() {
-		document.addEventListener('keydown', this.onKeydown);
-	},
+const modal = ref<InstanceType<typeof MkModal>>();
 
-	beforeUnmount() {
-		document.removeEventListener('keydown', this.onKeydown);
-	},
+const inputValue = ref(props.input?.default || null);
+const selectedValue = ref(props.select?.default || null);
 
-	methods: {
-		done(canceled, result?) {
-			this.$emit('done', { canceled, result });
-			this.$refs.modal.close();
-		},
+function done(canceled: boolean, result?) {
+	emit('done', { canceled, result });
+	modal.value?.close();
+}
 
-		async ok() {
-			if (!this.showOkButton) return;
+async function ok() {
+	if (!props.showOkButton) return;
 
-			const result =
-				this.input ? this.inputValue :
-				this.select ? this.selectedValue :
-				true;
-			this.done(false, result);
-		},
+	const result =
+		props.input ? inputValue.value :
+		props.select ? selectedValue.value :
+		true;
+	done(false, result);
+}
 
-		cancel() {
-			this.done(true);
-		},
+function cancel() {
+	done(true);
+}
+/*
+function onBgClick() {
+	if (props.cancelableByBgClick) cancel();
+}
+*/
+function onKeydown(e: KeyboardEvent) {
+	if (e.key === 'Escape') cancel();
+}
 
-		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();
-			}
-		}
+function onInputKeydown(e: KeyboardEvent) {
+	if (e.key === 'Enter') {
+		e.preventDefault();
+		e.stopPropagation();
+		ok();
 	}
+}
+
+onMounted(() => {
+	document.addEventListener('keydown', onKeydown);
+});
+
+onBeforeUnmount(() => {
+	document.removeEventListener('keydown', onKeydown);
 });
 </script>
 
diff --git a/packages/client/src/components/drive-file-thumbnail.vue b/packages/client/src/components/drive-file-thumbnail.vue
index e94b6b8bcb..81b80e7e8e 100644
--- a/packages/client/src/components/drive-file-thumbnail.vue
+++ b/packages/client/src/components/drive-file-thumbnail.vue
@@ -14,71 +14,42 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as Misskey from 'misskey-js';
 import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
-import { ColdDeviceStorage } from '@/store';
 
-export default defineComponent({
-	components: {
-		ImgWithBlurhash
-	},
-	props: {
-		file: {
-			type: Object,
-			required: true
-		},
-		fit: {
-			type: String,
-			required: false,
-			default: 'cover'
-		},
-	},
-	data() {
-		return {
-			isContextmenuShowing: false,
-			isDragging: false,
+const props = defineProps<{
+	file: Misskey.entities.DriveFile;
+	fit: string;
+}>();
 
-		};
-	},
-	computed: {
-		is(): 'image' | 'video' | 'midi' | 'audio' | 'csv' | 'pdf' | 'textfile' | 'archive' | 'unknown' {
-			if (this.file.type.startsWith('image/')) return 'image';
-			if (this.file.type.startsWith('video/')) return 'video';
-			if (this.file.type === 'audio/midi') return 'midi';
-			if (this.file.type.startsWith('audio/')) return 'audio';
-			if (this.file.type.endsWith('/csv')) return 'csv';
-			if (this.file.type.endsWith('/pdf')) return 'pdf';
-			if (this.file.type.startsWith('text/')) return 'textfile';
-			if ([
-					"application/zip",
-					"application/x-cpio",
-					"application/x-bzip",
-					"application/x-bzip2",
-					"application/java-archive",
-					"application/x-rar-compressed",
-					"application/x-tar",
-					"application/gzip",
-					"application/x-7z-compressed"
-				].some(e => e === this.file.type)) return 'archive';
-			return 'unknown';
-		},
-		isThumbnailAvailable(): boolean {
-			return this.file.thumbnailUrl
-				? (this.is === 'image' || this.is === 'video')
-				: false;
-		},
-	},
-	mounted() {
-		const audioTag = this.$refs.volumectrl as HTMLAudioElement;
-		if (audioTag) audioTag.volume = ColdDeviceStorage.get('mediaVolume');
-	},
-	methods: {
-		volumechange() {
-			const audioTag = this.$refs.volumectrl as HTMLAudioElement;
-			ColdDeviceStorage.set('mediaVolume', audioTag.volume);
-		}
-	}
+const is = computed(() => {
+	if (props.file.type.startsWith('image/')) return 'image';
+	if (props.file.type.startsWith('video/')) return 'video';
+	if (props.file.type === 'audio/midi') return 'midi';
+	if (props.file.type.startsWith('audio/')) return 'audio';
+	if (props.file.type.endsWith('/csv')) return 'csv';
+	if (props.file.type.endsWith('/pdf')) return 'pdf';
+	if (props.file.type.startsWith('text/')) return 'textfile';
+	if ([
+			"application/zip",
+			"application/x-cpio",
+			"application/x-bzip",
+			"application/x-bzip2",
+			"application/java-archive",
+			"application/x-rar-compressed",
+			"application/x-tar",
+			"application/gzip",
+			"application/x-7z-compressed"
+		].some(e => e === props.file.type)) return 'archive';
+	return 'unknown';
+});
+
+const isThumbnailAvailable = computed(() => {
+	return props.file.thumbnailUrl
+		? (is.value === 'image' as const || is.value === 'video')
+		: false;
 });
 </script>
 
diff --git a/packages/client/src/components/drive-select-dialog.vue b/packages/client/src/components/drive-select-dialog.vue
index 75537dfe3e..6d84511277 100644
--- a/packages/client/src/components/drive-select-dialog.vue
+++ b/packages/client/src/components/drive-select-dialog.vue
@@ -7,64 +7,51 @@
 	@click="cancel()"
 	@close="cancel()"
 	@ok="ok()"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 >
 	<template #header>
-		{{ multiple ? ((type === 'file') ? $ts.selectFiles : $ts.selectFolders) : ((type === 'file') ? $ts.selectFile : $ts.selectFolder) }}
+		{{ multiple ? ((type === 'file') ? i18n.locale.selectFiles : i18n.locale.selectFolders) : ((type === 'file') ? i18n.locale.selectFile : i18n.locale.selectFolder) }}
 		<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
 	</template>
 	<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
 </XModalWindow>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
 import XDrive from './drive.vue';
 import XModalWindow from '@/components/ui/modal-window.vue';
 import number from '@/filters/number';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		XDrive,
-		XModalWindow,
-	},
-
-	props: {
-		type: {
-			type: String,
-			required: false,
-			default: 'file'
-		},
-		multiple: {
-			type: Boolean,
-			default: false
-		}
-	},
-
-	emits: ['done', 'closed'],
-
-	data() {
-		return {
-			selected: []
-		};
-	},
-
-	methods: {
-		ok() {
-			this.$emit('done', this.selected);
-			this.$refs.dialog.close();
-		},
-
-		cancel() {
-			this.$emit('done');
-			this.$refs.dialog.close();
-		},
-
-		onChangeSelection(xs) {
-			this.selected = xs;
-		},
-
-		number
-	}
+withDefaults(defineProps<{
+	type?: 'file' | 'folder';
+	multiple: boolean;
+}>(), {
+	type: 'file',
 });
+
+const emit = defineEmits<{
+	(e: 'done', r?: Misskey.entities.DriveFile[]): void;
+	(e: 'closed'): void;
+}>();
+
+const dialog = ref<InstanceType<typeof XModalWindow>>();
+
+const selected = ref<Misskey.entities.DriveFile[]>([]);
+
+function ok() {
+	emit('done', selected.value);
+	dialog.value?.close();
+}
+
+function cancel() {
+	emit('done');
+	dialog.value?.close();
+}
+
+function onChangeSelection(files: Misskey.entities.DriveFile[]) {
+	selected.value = files;
+}
 </script>
diff --git a/packages/client/src/components/drive-window.vue b/packages/client/src/components/drive-window.vue
index 43f07ebe76..8b60bf7794 100644
--- a/packages/client/src/components/drive-window.vue
+++ b/packages/client/src/components/drive-window.vue
@@ -3,42 +3,27 @@
 	:initial-width="800"
 	:initial-height="500"
 	:can-resize="true"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 >
 	<template #header>
-		{{ $ts.drive }}
+		{{ i18n.locale.drive }}
 	</template>
 	<XDrive :initial-folder="initialFolder"/>
 </XWindow>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import {  } from 'vue';
+import * as Misskey from 'misskey-js';
 import XDrive from './drive.vue';
 import XWindow from '@/components/ui/window.vue';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		XDrive,
-		XWindow,
-	},
+defineProps<{
+	initialFolder?: Misskey.entities.DriveFolder;
+}>();
 
-	props: {
-		initialFolder: {
-			type: Object,
-			required: false
-		},
-	},
-
-	emits: ['closed'],
-
-	data() {
-		return {
-		};
-	},
-
-	methods: {
-
-	}
-});
+const emit = defineEmits<{
+	(e: 'closed'): void;
+}>();
 </script>
diff --git a/packages/client/src/components/drive.file.vue b/packages/client/src/components/drive.file.vue
index 511647229e..fd6a813838 100644
--- a/packages/client/src/components/drive.file.vue
+++ b/packages/client/src/components/drive.file.vue
@@ -8,17 +8,17 @@
 	@dragstart="onDragstart"
 	@dragend="onDragend"
 >
-	<div v-if="$i.avatarId == file.id" class="label">
+	<div v-if="$i?.avatarId == file.id" class="label">
 		<img src="/client-assets/label.svg"/>
-		<p>{{ $ts.avatar }}</p>
+		<p>{{ i18n.locale.avatar }}</p>
 	</div>
-	<div v-if="$i.bannerId == file.id" class="label">
+	<div v-if="$i?.bannerId == file.id" class="label">
 		<img src="/client-assets/label.svg"/>
-		<p>{{ $ts.banner }}</p>
+		<p>{{ i18n.locale.banner }}</p>
 	</div>
 	<div v-if="file.isSensitive" class="label red">
 		<img src="/client-assets/label-red.svg"/>
-		<p>{{ $ts.nsfw }}</p>
+		<p>{{ i18n.locale.nsfw }}</p>
 	</div>
 
 	<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
@@ -30,179 +30,155 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+import * as Misskey from 'misskey-js';
 import copyToClipboard from '@/scripts/copy-to-clipboard';
 import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
 import bytes from '@/filters/bytes';
 import * as os from '@/os';
+import { i18n } from '@/i18n';
+import { $i } from '@/account';
 
-export default defineComponent({
-	components: {
-		MkDriveFileThumbnail
-	},
-
-	props: {
-		file: {
-			type: Object,
-			required: true,
-		},
-		isSelected: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		selectMode: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
-
-	emits: ['chosen'],
-
-	data() {
-		return {
-			isDragging: false
-		};
-	},
-
-	computed: {
-		// TODO: parentへの参照を無くす
-		browser(): any {
-			return this.$parent;
-		},
-		title(): string {
-			return `${this.file.name}\n${this.file.type} ${bytes(this.file.size)}`;
-		}
-	},
-
-	methods: {
-		getMenu() {
-			return [{
-				text: this.$ts.rename,
-				icon: 'fas fa-i-cursor',
-				action: this.rename
-			}, {
-				text: this.file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
-				icon: this.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
-				action: this.toggleSensitive
-			}, {
-				text: this.$ts.describeFile,
-				icon: 'fas fa-i-cursor',
-				action: this.describe
-			}, null, {
-				text: this.$ts.copyUrl,
-				icon: 'fas fa-link',
-				action: this.copyUrl
-			}, {
-				type: 'a',
-				href: this.file.url,
-				target: '_blank',
-				text: this.$ts.download,
-				icon: 'fas fa-download',
-				download: this.file.name
-			}, null, {
-				text: this.$ts.delete,
-				icon: 'fas fa-trash-alt',
-				danger: true,
-				action: this.deleteFile
-			}];
-		},
-
-		onClick(ev) {
-			if (this.selectMode) {
-				this.$emit('chosen', this.file);
-			} else {
-				os.popupMenu(this.getMenu(), ev.currentTarget || ev.target);
-			}
-		},
-
-		onContextmenu(e) {
-			os.contextMenu(this.getMenu(), e);
-		},
-
-		onDragstart(e) {
-			e.dataTransfer.effectAllowed = 'move';
-			e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(this.file));
-			this.isDragging = true;
-
-			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
-			// (=あなたの子供が、ドラッグを開始しましたよ)
-			this.browser.isDragSource = true;
-		},
-
-		onDragend(e) {
-			this.isDragging = false;
-			this.browser.isDragSource = false;
-		},
-
-		rename() {
-			os.inputText({
-				title: this.$ts.renameFile,
-				placeholder: this.$ts.inputNewFileName,
-				default: this.file.name,
-				allowEmpty: false
-			}).then(({ canceled, result: name }) => {
-				if (canceled) return;
-				os.api('drive/files/update', {
-					fileId: this.file.id,
-					name: name
-				});
-			});
-		},
-
-		describe() {
-			os.popup(import('@/components/media-caption.vue'), {
-				title: this.$ts.describeFile,
-				input: {
-					placeholder: this.$ts.inputNewDescription,
-					default: this.file.comment !== null ? this.file.comment : '',
-				},
-				image: this.file
-			}, {
-				done: result => {
-					if (!result || result.canceled) return;
-					let comment = result.result;
-					os.api('drive/files/update', {
-						fileId: this.file.id,
-						comment: comment.length == 0 ? null : comment
-					});
-				}
-			}, 'closed');
-		},
-
-		toggleSensitive() {
-			os.api('drive/files/update', {
-				fileId: this.file.id,
-				isSensitive: !this.file.isSensitive
-			});
-		},
-
-		copyUrl() {
-			copyToClipboard(this.file.url);
-			os.success();
-		},
-
-		addApp() {
-			alert('not implemented yet');
-		},
-
-		async deleteFile() {
-			const { canceled } = await os.confirm({
-				type: 'warning',
-				text: this.$t('driveFileDeleteConfirm', { name: this.file.name }),
-			});
-			if (canceled) return;
-
-			os.api('drive/files/delete', {
-				fileId: this.file.id
-			});
-		},
-
-		bytes
-	}
+const props = withDefaults(defineProps<{
+	file: Misskey.entities.DriveFile;
+	isSelected?: boolean;
+	selectMode?: boolean;
+}>(), {
+	isSelected: false,
+	selectMode: false,
 });
+
+const emit = defineEmits<{
+	(e: 'chosen', r: Misskey.entities.DriveFile): void;
+	(e: 'dragstart'): void;
+	(e: 'dragend'): void;
+}>();
+
+const isDragging = ref(false);
+
+const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`);
+
+function getMenu() {
+	return [{
+		text: i18n.locale.rename,
+		icon: 'fas fa-i-cursor',
+		action: rename
+	}, {
+		text: props.file.isSensitive ? i18n.locale.unmarkAsSensitive : i18n.locale.markAsSensitive,
+		icon: props.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
+		action: toggleSensitive
+	}, {
+		text: i18n.locale.describeFile,
+		icon: 'fas fa-i-cursor',
+		action: describe
+	}, null, {
+		text: i18n.locale.copyUrl,
+		icon: 'fas fa-link',
+		action: copyUrl
+	}, {
+		type: 'a',
+		href: props.file.url,
+		target: '_blank',
+		text: i18n.locale.download,
+		icon: 'fas fa-download',
+		download: props.file.name
+	}, null, {
+		text: i18n.locale.delete,
+		icon: 'fas fa-trash-alt',
+		danger: true,
+		action: deleteFile
+	}];
+}
+
+function onClick(ev: MouseEvent) {
+	if (props.selectMode) {
+		emit('chosen', props.file);
+	} else {
+		os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined);
+	}
+}
+
+function onContextmenu(e: MouseEvent) {
+	os.contextMenu(getMenu(), e);
+}
+
+function onDragstart(e: DragEvent) {
+	if (e.dataTransfer) {
+		e.dataTransfer.effectAllowed = 'move';
+		e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file));
+	}
+	isDragging.value = true;
+
+	emit('dragstart');
+}
+
+function onDragend() {
+	isDragging.value = false;
+	emit('dragend');
+}
+
+function rename() {
+	os.inputText({
+		title: i18n.locale.renameFile,
+		placeholder: i18n.locale.inputNewFileName,
+		default: props.file.name,
+	}).then(({ canceled, result: name }) => {
+		if (canceled) return;
+		os.api('drive/files/update', {
+			fileId: props.file.id,
+			name: name
+		});
+	});
+}
+
+function describe() {
+	os.popup(import('@/components/media-caption.vue'), {
+		title: i18n.locale.describeFile,
+		input: {
+			placeholder: i18n.locale.inputNewDescription,
+			default: props.file.comment !== null ? props.file.comment : '',
+		},
+		image: props.file
+	}, {
+		done: result => {
+			if (!result || result.canceled) return;
+			let comment = result.result;
+			os.api('drive/files/update', {
+				fileId: props.file.id,
+				comment: comment.length == 0 ? null : comment
+			});
+		}
+	}, 'closed');
+}
+
+function toggleSensitive() {
+	os.api('drive/files/update', {
+		fileId: props.file.id,
+		isSensitive: !props.file.isSensitive
+	});
+}
+
+function copyUrl() {
+	copyToClipboard(props.file.url);
+	os.success();
+}
+/*
+function addApp() {
+	alert('not implemented yet');
+}
+*/
+async function deleteFile() {
+	const { canceled } = await os.confirm({
+		type: 'warning',
+		text: i18n.t('driveFileDeleteConfirm', { name: props.file.name }),
+	});
+
+	if (canceled) return;
+	os.api('drive/files/delete', {
+		fileId: props.file.id
+	});
+}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/drive.folder.vue b/packages/client/src/components/drive.folder.vue
index aaba736cf8..20a6343cfe 100644
--- a/packages/client/src/components/drive.folder.vue
+++ b/packages/client/src/components/drive.folder.vue
@@ -19,243 +19,233 @@
 		<template v-if="!hover"><i class="fas fa-folder fa-fw"></i></template>
 		{{ folder.name }}
 	</p>
-	<p v-if="$store.state.uploadFolder == folder.id" class="upload">
-		{{ $ts.uploadFolder }}
+	<p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
+		{{ i18n.locale.uploadFolder }}
 	</p>
 	<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+import * as Misskey from 'misskey-js';
 import * as os from '@/os';
+import { i18n } from '@/i18n';
+import { defaultStore } from '@/store';
 
-export default defineComponent({
-	props: {
-		folder: {
-			type: Object,
-			required: true,
-		},
-		isSelected: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		selectMode: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
-
-	emits: ['chosen'],
-
-	data() {
-		return {
-			hover: false,
-			draghover: false,
-			isDragging: false,
-		};
-	},
-
-	computed: {
-		browser(): any {
-			return this.$parent;
-		},
-		title(): string {
-			return this.folder.name;
-		}
-	},
-
-	methods: {
-		checkboxClicked(e) {
-			this.$emit('chosen', this.folder);
-		},
-
-		onClick() {
-			this.browser.move(this.folder);
-		},
-
-		onMouseover() {
-			this.hover = true;
-		},
-
-		onMouseout() {
-			this.hover = false
-		},
-
-		onDragover(e) {
-			// 自分自身がドラッグされている場合
-			if (this.isDragging) {
-				// 自分自身にはドロップさせない
-				e.dataTransfer.dropEffect = 'none';
-				return;
-			}
-
-			const isFile = e.dataTransfer.items[0].kind == 'file';
-			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
-			const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
-
-			if (isFile || isDriveFile || isDriveFolder) {
-				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
-			} else {
-				e.dataTransfer.dropEffect = 'none';
-			}
-		},
-
-		onDragenter() {
-			if (!this.isDragging) this.draghover = true;
-		},
-
-		onDragleave() {
-			this.draghover = false;
-		},
-
-		onDrop(e) {
-			this.draghover = false;
-
-			// ファイルだったら
-			if (e.dataTransfer.files.length > 0) {
-				for (const file of Array.from(e.dataTransfer.files)) {
-					this.browser.upload(file, this.folder);
-				}
-				return;
-			}
-
-			//#region ドライブのファイル
-			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
-			if (driveFile != null && driveFile != '') {
-				const file = JSON.parse(driveFile);
-				this.browser.removeFile(file.id);
-				os.api('drive/files/update', {
-					fileId: file.id,
-					folderId: this.folder.id
-				});
-			}
-			//#endregion
-
-			//#region ドライブのフォルダ
-			const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
-			if (driveFolder != null && driveFolder != '') {
-				const folder = JSON.parse(driveFolder);
-
-				// 移動先が自分自身ならreject
-				if (folder.id == this.folder.id) return;
-
-				this.browser.removeFolder(folder.id);
-				os.api('drive/folders/update', {
-					folderId: folder.id,
-					parentId: this.folder.id
-				}).then(() => {
-					// noop
-				}).catch(err => {
-					switch (err) {
-						case 'detected-circular-definition':
-							os.alert({
-								title: this.$ts.unableToProcess,
-								text: this.$ts.circularReferenceFolder
-							});
-							break;
-						default:
-							os.alert({
-								type: 'error',
-								text: this.$ts.somethingHappened
-							});
-					}
-				});
-			}
-			//#endregion
-		},
-
-		onDragstart(e) {
-			e.dataTransfer.effectAllowed = 'move';
-			e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(this.folder));
-			this.isDragging = true;
-
-			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
-			// (=あなたの子供が、ドラッグを開始しましたよ)
-			this.browser.isDragSource = true;
-		},
-
-		onDragend() {
-			this.isDragging = false;
-			this.browser.isDragSource = false;
-		},
-
-		go() {
-			this.browser.move(this.folder.id);
-		},
-
-		newWindow() {
-			this.browser.newWindow(this.folder);
-		},
-
-		rename() {
-			os.inputText({
-				title: this.$ts.renameFolder,
-				placeholder: this.$ts.inputNewFolderName,
-				default: this.folder.name
-			}).then(({ canceled, result: name }) => {
-				if (canceled) return;
-				os.api('drive/folders/update', {
-					folderId: this.folder.id,
-					name: name
-				});
-			});
-		},
-
-		deleteFolder() {
-			os.api('drive/folders/delete', {
-				folderId: this.folder.id
-			}).then(() => {
-				if (this.$store.state.uploadFolder === this.folder.id) {
-					this.$store.set('uploadFolder', null);
-				}
-			}).catch(err => {
-				switch(err.id) {
-					case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
-						os.alert({
-							type: 'error',
-							title: this.$ts.unableToDelete,
-							text: this.$ts.hasChildFilesOrFolders
-						});
-						break;
-					default:
-						os.alert({
-							type: 'error',
-							text: this.$ts.unableToDelete
-						});
-				}
-			});
-		},
-
-		setAsUploadFolder() {
-			this.$store.set('uploadFolder', this.folder.id);
-		},
-
-		onContextmenu(e) {
-			os.contextMenu([{
-				text: this.$ts.openInWindow,
-				icon: 'fas fa-window-restore',
-				action: () => {
-					os.popup(import('./drive-window.vue'), {
-						initialFolder: this.folder
-					}, {
-					}, 'closed');
-				}
-			}, null, {
-				text: this.$ts.rename,
-				icon: 'fas fa-i-cursor',
-				action: this.rename
-			}, null, {
-				text: this.$ts.delete,
-				icon: 'fas fa-trash-alt',
-				danger: true,
-				action: this.deleteFolder
-			}], e);
-		},
-	}
+const props = withDefaults(defineProps<{
+	folder: Misskey.entities.DriveFolder;
+	isSelected?: boolean;
+	selectMode?: boolean;
+}>(), {
+	isSelected: false,
+	selectMode: false,
 });
+
+const emit = defineEmits<{
+	(ev: 'chosen', v: Misskey.entities.DriveFolder): void;
+	(ev: 'move', v: Misskey.entities.DriveFolder): void;
+	(ev: 'upload', file: File, folder: Misskey.entities.DriveFolder);
+	(ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
+	(ev: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
+	(ev: 'dragstart'): void;
+	(ev: 'dragend'): void;
+}>();
+
+const hover = ref(false);
+const draghover = ref(false);
+const isDragging = ref(false);
+
+const title = computed(() => props.folder.name);
+
+function checkboxClicked() {
+	emit('chosen', props.folder);
+}
+
+function onClick() {
+	emit('move', props.folder);
+}
+
+function onMouseover() {
+	hover.value = true;
+}
+
+function onMouseout() {
+	hover.value = false
+}
+
+function onDragover(ev: DragEvent) {
+	if (!ev.dataTransfer) return;
+
+	// 自分自身がドラッグされている場合
+	if (isDragging.value) {
+		// 自分自身にはドロップさせない
+		ev.dataTransfer.dropEffect = 'none';
+		return;
+	}
+
+	const isFile = ev.dataTransfer.items[0].kind == 'file';
+	const isDriveFile = ev.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+	const isDriveFolder = ev.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
+
+	if (isFile || isDriveFile || isDriveFolder) {
+		ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+	} else {
+		ev.dataTransfer.dropEffect = 'none';
+	}
+}
+
+function onDragenter() {
+	if (!isDragging.value) draghover.value = true;
+}
+
+function onDragleave() {
+	draghover.value = false;
+}
+
+function onDrop(ev: DragEvent) {
+	draghover.value = false;
+
+	if (!ev.dataTransfer) return;
+
+	// ファイルだったら
+	if (ev.dataTransfer.files.length > 0) {
+		for (const file of Array.from(ev.dataTransfer.files)) {
+			emit('upload', file, props.folder);
+		}
+		return;
+	}
+
+	//#region ドライブのファイル
+	const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+	if (driveFile != null && driveFile != '') {
+		const file = JSON.parse(driveFile);
+		emit('removeFile', file.id);
+		os.api('drive/files/update', {
+			fileId: file.id,
+			folderId: props.folder.id
+		});
+	}
+	//#endregion
+
+	//#region ドライブのフォルダ
+	const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
+	if (driveFolder != null && driveFolder != '') {
+		const folder = JSON.parse(driveFolder);
+
+		// 移動先が自分自身ならreject
+		if (folder.id == props.folder.id) return;
+
+		emit('removeFolder', folder.id);
+		os.api('drive/folders/update', {
+			folderId: folder.id,
+			parentId: props.folder.id
+		}).then(() => {
+			// noop
+		}).catch(err => {
+			switch (err) {
+				case 'detected-circular-definition':
+					os.alert({
+						title: i18n.locale.unableToProcess,
+						text: i18n.locale.circularReferenceFolder
+					});
+					break;
+				default:
+					os.alert({
+						type: 'error',
+						text: i18n.locale.somethingHappened
+					});
+			}
+		});
+	}
+	//#endregion
+}
+
+function onDragstart(ev: DragEvent) {
+	if (!ev.dataTransfer) return;
+
+	ev.dataTransfer.effectAllowed = 'move';
+	ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(props.folder));
+	isDragging.value = true;
+
+	// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
+	// (=あなたの子供が、ドラッグを開始しましたよ)
+	emit('dragstart');
+}
+
+function onDragend() {
+	isDragging.value = false;
+	emit('dragend');
+}
+
+function go() {
+	emit('move', props.folder.id);
+}
+
+function rename() {
+	os.inputText({
+		title: i18n.locale.renameFolder,
+		placeholder: i18n.locale.inputNewFolderName,
+		default: props.folder.name
+	}).then(({ canceled, result: name }) => {
+		if (canceled) return;
+		os.api('drive/folders/update', {
+			folderId: props.folder.id,
+			name: name
+		});
+	});
+}
+
+function deleteFolder() {
+	os.api('drive/folders/delete', {
+		folderId: props.folder.id
+	}).then(() => {
+		if (defaultStore.state.uploadFolder === props.folder.id) {
+			defaultStore.set('uploadFolder', null);
+		}
+	}).catch(err => {
+		switch(err.id) {
+			case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
+				os.alert({
+					type: 'error',
+					title: i18n.locale.unableToDelete,
+					text: i18n.locale.hasChildFilesOrFolders
+				});
+				break;
+			default:
+				os.alert({
+					type: 'error',
+					text: i18n.locale.unableToDelete
+				});
+		}
+	});
+}
+
+function setAsUploadFolder() {
+	defaultStore.set('uploadFolder', props.folder.id);
+}
+
+function onContextmenu(ev: MouseEvent) {
+	os.contextMenu([{
+		text: i18n.locale.openInWindow,
+		icon: 'fas fa-window-restore',
+		action: () => {
+			os.popup(import('./drive-window.vue'), {
+				initialFolder: props.folder
+			}, {
+			}, 'closed');
+		}
+	}, null, {
+		text: i18n.locale.rename,
+		icon: 'fas fa-i-cursor',
+		action: rename,
+	}, null, {
+		text: i18n.locale.delete,
+		icon: 'fas fa-trash-alt',
+		danger: true,
+		action: deleteFolder,
+	}], ev);
+}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/drive.nav-folder.vue b/packages/client/src/components/drive.nav-folder.vue
index 4f0e6ce0e9..7c35c5d3da 100644
--- a/packages/client/src/components/drive.nav-folder.vue
+++ b/packages/client/src/components/drive.nav-folder.vue
@@ -8,114 +8,111 @@
 	@drop.stop="onDrop"
 >
 	<i v-if="folder == null" class="fas fa-cloud"></i>
-	<span>{{ folder == null ? $ts.drive : folder.name }}</span>
+	<span>{{ folder == null ? i18n.locale.drive : folder.name }}</span>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
 import * as os from '@/os';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	props: {
-		folder: {
-			type: Object,
-			required: false,
-		}
-	},
+const props = defineProps<{
+	folder?: Misskey.entities.DriveFolder;
+	parentFolder: Misskey.entities.DriveFolder | null;
+}>();
 
-	data() {
-		return {
-			hover: false,
-			draghover: false,
-		};
-	},
+const emit = defineEmits<{
+	(e: 'move', v?: Misskey.entities.DriveFolder): void;
+	(e: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void;
+	(e: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
+	(e: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
+}>();
 
-	computed: {
-		browser(): any {
-			return this.$parent;
-		}
-	},
+const hover = ref(false);
+const draghover = ref(false);
 
-	methods: {
-		onClick() {
-			this.browser.move(this.folder);
-		},
+function onClick() {
+	emit('move', props.folder);
+}
 
-		onMouseover() {
-			this.hover = true;
-		},
+function onMouseover() {
+	hover.value = true;
+}
 
-		onMouseout() {
-			this.hover = false;
-		},
+function onMouseout() {
+	hover.value = false;
+}
 
-		onDragover(e) {
-			// このフォルダがルートかつカレントディレクトリならドロップ禁止
-			if (this.folder == null && this.browser.folder == null) {
-				e.dataTransfer.dropEffect = 'none';
-			}
+function onDragover(e: DragEvent) {
+	if (!e.dataTransfer) return;
 
-			const isFile = e.dataTransfer.items[0].kind == 'file';
-			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
-			const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
-
-			if (isFile || isDriveFile || isDriveFolder) {
-				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
-			} else {
-				e.dataTransfer.dropEffect = 'none';
-			}
-
-			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;
-
-			// ファイルだったら
-			if (e.dataTransfer.files.length > 0) {
-				for (const file of Array.from(e.dataTransfer.files)) {
-					this.browser.upload(file, this.folder);
-				}
-				return;
-			}
-
-			//#region ドライブのファイル
-			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
-			if (driveFile != null && driveFile != '') {
-				const file = JSON.parse(driveFile);
-				this.browser.removeFile(file.id);
-				os.api('drive/files/update', {
-					fileId: file.id,
-					folderId: this.folder ? this.folder.id : null
-				});
-			}
-			//#endregion
-
-			//#region ドライブのフォルダ
-			const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
-			if (driveFolder != null && driveFolder != '') {
-				const folder = JSON.parse(driveFolder);
-				// 移動先が自分自身ならreject
-				if (this.folder && folder.id == this.folder.id) return;
-				this.browser.removeFolder(folder.id);
-				os.api('drive/folders/update', {
-					folderId: folder.id,
-					parentId: this.folder ? this.folder.id : null
-				});
-			}
-			//#endregion
-		}
+	// このフォルダがルートかつカレントディレクトリならドロップ禁止
+	if (props.folder == null && props.parentFolder == null) {
+		e.dataTransfer.dropEffect = 'none';
 	}
-});
+
+	const isFile = e.dataTransfer.items[0].kind == 'file';
+	const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+	const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
+
+	if (isFile || isDriveFile || isDriveFolder) {
+		e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+	} else {
+		e.dataTransfer.dropEffect = 'none';
+	}
+
+	return false;
+}
+
+function onDragenter() {
+	if (props.folder || props.parentFolder) draghover.value = true;
+}
+
+function onDragleave() {
+	if (props.folder || props.parentFolder) draghover.value = false;
+}
+
+function onDrop(e: DragEvent) {
+	draghover.value = false;
+
+	if (!e.dataTransfer) return;
+
+	// ファイルだったら
+	if (e.dataTransfer.files.length > 0) {
+		for (const file of Array.from(e.dataTransfer.files)) {
+			emit('upload', file, props.folder);
+		}
+		return;
+	}
+
+	//#region ドライブのファイル
+	const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+	if (driveFile != null && driveFile != '') {
+		const file = JSON.parse(driveFile);
+		emit('removeFile', file.id);
+		os.api('drive/files/update', {
+			fileId: file.id,
+			folderId: props.folder ? props.folder.id : null
+		});
+	}
+	//#endregion
+
+	//#region ドライブのフォルダ
+	const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
+	if (driveFolder != null && driveFolder != '') {
+		const folder = JSON.parse(driveFolder);
+		// 移動先が自分自身ならreject
+		if (props.folder && folder.id == props.folder.id) return;
+		emit('removeFolder', folder.id);
+		os.api('drive/folders/update', {
+			folderId: folder.id,
+			parentId: props.folder ? props.folder.id : null
+		});
+	}
+	//#endregion
+}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue
index f8d3d810b7..e27b0a5fbb 100644
--- a/packages/client/src/components/drive.vue
+++ b/packages/client/src/components/drive.vue
@@ -2,10 +2,24 @@
 <div class="yfudmmck">
 	<nav>
 		<div class="path" @contextmenu.prevent.stop="() => {}">
-			<XNavFolder :class="{ current: folder == null }"/>
+			<XNavFolder
+				:class="{ current: folder == null }"
+				:parent-folder="folder"
+				@move="move"
+				@upload="upload"
+				@removeFile="removeFile"
+				@removeFolder="removeFolder"
+			/>
 			<template v-for="f in hierarchyFolders">
 				<span class="separator"><i class="fas fa-angle-right"></i></span>
-				<XNavFolder :folder="f"/>
+				<XNavFolder
+					:folder="f"
+					:parent-folder="folder"
+					@move="move"
+					@upload="upload"
+					@removeFile="removeFile"
+					@removeFolder="removeFolder"
+				/>
 			</template>
 			<span v-if="folder != null" class="separator"><i class="fas fa-angle-right"></i></span>
 			<span v-if="folder != null" class="folder current">{{ folder.name }}</span>
@@ -22,616 +36,600 @@
 	>
 		<div ref="contents" class="contents">
 			<div v-show="folders.length > 0" ref="foldersContainer" class="folders">
-				<XFolder v-for="(f, i) in folders" :key="f.id" v-anim="i" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder"/>
+				<XFolder
+					v-for="(f, i) in folders"
+					:key="f.id"
+					v-anim="i"
+					class="folder"
+					:folder="f"
+					:select-mode="select === 'folder'"
+					:is-selected="selectedFolders.some(x => x.id === f.id)"
+					@chosen="chooseFolder"
+					@move="move"
+					@upload="upload"
+					@removeFile="removeFile"
+					@removeFolder="removeFolder"
+					@dragstart="isDragSource = true"
+					@dragend="isDragSource = false"
+				/>
 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
 				<div v-for="(n, i) in 16" :key="i" class="padding"></div>
-				<MkButton v-if="moreFolders" ref="moreFolders">{{ $ts.loadMore }}</MkButton>
+				<MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.locale.loadMore }}</MkButton>
 			</div>
 			<div v-show="files.length > 0" ref="filesContainer" class="files">
-				<XFile v-for="(file, i) in files" :key="file.id" v-anim="i" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile"/>
+				<XFile
+					v-for="(file, i) in files"
+					:key="file.id"
+					v-anim="i"
+					class="file"
+					:file="file"
+					:select-mode="select === 'file'"
+					:is-selected="selectedFiles.some(x => x.id === file.id)"
+					@chosen="chooseFile"
+					@dragstart="isDragSource = true"
+					@dragend="isDragSource = false"
+				/>
 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
 				<div v-for="(n, i) in 16" :key="i" class="padding"></div>
-				<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ $ts.loadMore }}</MkButton>
+				<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.locale.loadMore }}</MkButton>
 			</div>
 			<div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty">
-				<p v-if="draghover">{{ $t('empty-draghover') }}</p>
-				<p v-if="!draghover && folder == null"><strong>{{ $ts.emptyDrive }}</strong><br/>{{ $t('empty-drive-description') }}</p>
-				<p v-if="!draghover && folder != null">{{ $ts.emptyFolder }}</p>
+				<p v-if="draghover">{{ i18n.t('empty-draghover') }}</p>
+				<p v-if="!draghover && folder == null"><strong>{{ i18n.locale.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p>
+				<p v-if="!draghover && folder != null">{{ i18n.locale.emptyFolder }}</p>
 			</div>
 		</div>
 		<MkLoading v-if="fetching"/>
 	</div>
 	<div v-if="draghover" class="dropzone"></div>
-	<input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/>
+	<input ref="fileInput" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { markRaw, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue';
+import * as Misskey from 'misskey-js';
 import XNavFolder from './drive.nav-folder.vue';
 import XFolder from './drive.folder.vue';
 import XFile from './drive.file.vue';
 import MkButton from './ui/button.vue';
 import * as os from '@/os';
 import { stream } from '@/stream';
-
-export default defineComponent({
-	components: {
-		XNavFolder,
-		XFolder,
-		XFile,
-		MkButton,
-	},
-
-	props: {
-		initialFolder: {
-			type: Object,
-			required: false
-		},
-		type: {
-			type: String,
-			required: false,
-			default: undefined
-		},
-		multiple: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		select: {
-			type: String,
-			required: false,
-			default: null
-		}
-	},
-
-	emits: ['selected', 'change-selection', 'move-root', 'cd', 'open-folder'],
-
-	data() {
-		return {
-			/**
-			 * 現在の階層(フォルダ)
-			 * * null でルートを表す
-			 */
-			folder: null,
-
-			files: [],
-			folders: [],
-			moreFiles: false,
-			moreFolders: false,
-			hierarchyFolders: [],
-			selectedFiles: [],
-			selectedFolders: [],
-			uploadings: os.uploads,
-			connection: null,
-
-			/**
-			 * ドロップされようとしているか
-			 */
-			draghover: false,
-
-			/**
-			 * 自信の所有するアイテムがドラッグをスタートさせたか
-			 * (自分自身の階層にドロップできないようにするためのフラグ)
-			 */
-			isDragSource: false,
-
-			fetching: true,
-
-			ilFilesObserver: new IntersectionObserver(
-				(entries) => entries.some((entry) => entry.isIntersecting)
-				&& !this.fetching && this.moreFiles &&
-					this.fetchMoreFiles()
-			),
-			moreFilesElement: null as Element,
-
-		};
-	},
-
-	watch: {
-		folder() {
-			this.$emit('cd', this.folder);
-		}
-	},
-
-	mounted() {
-		if (this.$store.state.enableInfiniteScroll && this.$refs.loadMoreFiles) {
-			this.$nextTick(() => {
-				this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
-			});
-		}
-
-		this.connection = markRaw(stream.useChannel('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);
-		this.connection.on('folderDeleted', this.onStreamDriveFolderDeleted);
-
-		if (this.initialFolder) {
-			this.move(this.initialFolder);
-		} else {
-			this.fetch();
-		}
-	},
-
-	activated() {
-		if (this.$store.state.enableInfiniteScroll) {
-			this.$nextTick(() => {
-				this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
-			});
-		}
-	},
-
-	beforeUnmount() {
-		this.connection.dispose();
-		this.ilFilesObserver.disconnect();
-	},
-
-	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);
-			}
-		},
-
-		onStreamDriveFolderDeleted(folderId) {
-			this.removeFolder(folderId);
-		},
-
-		onDragover(e): any {
-			// ドラッグ元が自分自身の所有するアイテムだったら
-			if (this.isDragSource) {
-				// 自分自身にはドロップさせない
-				e.dataTransfer.dropEffect = 'none';
-				return;
-			}
-
-			const isFile = e.dataTransfer.items[0].kind == 'file';
-			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
-			const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
-
-			if (isFile || isDriveFile || isDriveFolder) {
-				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
-			} else {
-				e.dataTransfer.dropEffect = 'none';
-			}
-
-			return false;
-		},
-
-		onDragenter(e) {
-			if (!this.isDragSource) this.draghover = true;
-		},
-
-		onDragleave(e) {
-			this.draghover = false;
-		},
-
-		onDrop(e): any {
-			this.draghover = false;
-
-			// ドロップされてきたものがファイルだったら
-			if (e.dataTransfer.files.length > 0) {
-				for (const file of Array.from(e.dataTransfer.files)) {
-					this.upload(file, this.folder);
-				}
-				return;
-			}
-
-			//#region ドライブのファイル
-			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
-			if (driveFile != null && driveFile != '') {
-				const file = JSON.parse(driveFile);
-				if (this.files.some(f => f.id == file.id)) return;
-				this.removeFile(file.id);
-				os.api('drive/files/update', {
-					fileId: file.id,
-					folderId: this.folder ? this.folder.id : null
-				});
-			}
-			//#endregion
-
-			//#region ドライブのフォルダ
-			const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
-			if (driveFolder != null && driveFolder != '') {
-				const folder = JSON.parse(driveFolder);
-
-				// 移動先が自分自身ならreject
-				if (this.folder && folder.id == this.folder.id) return false;
-				if (this.folders.some(f => f.id == folder.id)) return false;
-				this.removeFolder(folder.id);
-				os.api('drive/folders/update', {
-					folderId: folder.id,
-					parentId: this.folder ? this.folder.id : null
-				}).then(() => {
-					// noop
-				}).catch(err => {
-					switch (err) {
-						case 'detected-circular-definition':
-							os.alert({
-								title: this.$ts.unableToProcess,
-								text: this.$ts.circularReferenceFolder
-							});
-							break;
-						default:
-							os.alert({
-								type: 'error',
-								text: this.$ts.somethingHappened
-							});
-					}
-				});
-			}
-			//#endregion
-		},
-
-		selectLocalFile() {
-			(this.$refs.fileInput as any).click();
-		},
-
-		urlUpload() {
-			os.inputText({
-				title: this.$ts.uploadFromUrl,
-				type: 'url',
-				placeholder: this.$ts.uploadFromUrlDescription
-			}).then(({ canceled, result: url }) => {
-				if (canceled) return;
-				os.api('drive/files/upload-from-url', {
-					url: url,
-					folderId: this.folder ? this.folder.id : undefined
-				});
-
-				os.alert({
-					title: this.$ts.uploadFromUrlRequested,
-					text: this.$ts.uploadFromUrlMayTakeTime
-				});
-			});
-		},
-
-		createFolder() {
-			os.inputText({
-				title: this.$ts.createFolder,
-				placeholder: this.$ts.folderName
-			}).then(({ canceled, result: name }) => {
-				if (canceled) return;
-				os.api('drive/folders/create', {
-					name: name,
-					parentId: this.folder ? this.folder.id : undefined
-				}).then(folder => {
-					this.addFolder(folder, true);
-				});
-			});
-		},
-
-		renameFolder(folder) {
-			os.inputText({
-				title: this.$ts.renameFolder,
-				placeholder: this.$ts.inputNewFolderName,
-				default: folder.name
-			}).then(({ canceled, result: name }) => {
-				if (canceled) return;
-				os.api('drive/folders/update', {
-					folderId: folder.id,
-					name: name
-				}).then(folder => {
-					// FIXME: 画面を更新するために自分自身に移動
-					this.move(folder);
-				});
-			});
-		},
-
-		deleteFolder(folder) {
-			os.api('drive/folders/delete', {
-				folderId: folder.id
-			}).then(() => {
-				// 削除時に親フォルダに移動
-				this.move(folder.parentId);
-			}).catch(err => {
-				switch(err.id) {
-					case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
-						os.alert({
-							type: 'error',
-							title: this.$ts.unableToDelete,
-							text: this.$ts.hasChildFilesOrFolders
-						});
-						break;
-					default:
-						os.alert({
-							type: 'error',
-							text: this.$ts.unableToDelete
-						});
-					}
-			});
-		},
-
-		onChangeFileInput() {
-			for (const file of Array.from((this.$refs.fileInput as any).files)) {
-				this.upload(file, this.folder);
-			}
-		},
-
-		upload(file, folder) {
-			if (folder && typeof folder == 'object') folder = folder.id;
-			os.upload(file, folder).then(res => {
-				this.addFile(res, true);
-			});
-		},
-
-		chooseFile(file) {
-			const isAlreadySelected = this.selectedFiles.some(f => f.id == file.id);
-			if (this.multiple) {
-				if (isAlreadySelected) {
-					this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id);
-				} else {
-					this.selectedFiles.push(file);
-				}
-				this.$emit('change-selection', this.selectedFiles);
-			} else {
-				if (isAlreadySelected) {
-					this.$emit('selected', file);
-				} else {
-					this.selectedFiles = [file];
-					this.$emit('change-selection', [file]);
-				}
-			}
-		},
-
-		chooseFolder(folder) {
-			const isAlreadySelected = this.selectedFolders.some(f => f.id == folder.id);
-			if (this.multiple) {
-				if (isAlreadySelected) {
-					this.selectedFolders = this.selectedFolders.filter(f => f.id != folder.id);
-				} else {
-					this.selectedFolders.push(folder);
-				}
-				this.$emit('change-selection', this.selectedFolders);
-			} else {
-				if (isAlreadySelected) {
-					this.$emit('selected', folder);
-				} else {
-					this.selectedFolders = [folder];
-					this.$emit('change-selection', [folder]);
-				}
-			}
-		},
-
-		move(target) {
-			if (target == null) {
-				this.goRoot();
-				return;
-			} else if (typeof target == 'object') {
-				target = target.id;
-			}
-
-			this.fetching = true;
-
-			os.api('drive/folders/show', {
-				folderId: target
-			}).then(folder => {
-				this.folder = folder;
-				this.hierarchyFolders = [];
-
-				const dive = folder => {
-					this.hierarchyFolders.unshift(folder);
-					if (folder.parent) dive(folder.parent);
-				};
-
-				if (folder.parent) dive(folder.parent);
-
-				this.$emit('open-folder', folder);
-				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)) {
-				const exist = this.folders.map(f => f.id).indexOf(folder.id);
-				this.folders[exist] = folder;
-				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);
-				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() {
-			// 既にrootにいるなら何もしない
-			if (this.folder == null) return;
-
-			this.folder = null;
-			this.hierarchyFolders = [];
-			this.$emit('move-root');
-			this.fetch();
-		},
-
-		fetch() {
-			this.folders = [];
-			this.files = [];
-			this.moreFolders = false;
-			this.moreFiles = false;
-			this.fetching = true;
-
-			let fetchedFolders = null;
-			let fetchedFiles = null;
-
-			const foldersMax = 30;
-			const filesMax = 30;
-
-			// フォルダ一覧取得
-			os.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();
-			});
-
-			// ファイル一覧取得
-			os.api('drive/files', {
-				folderId: this.folder ? this.folder.id : null,
-				type: this.type,
-				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;
-				} else {
-					flag = true;
-				}
-			};
-		},
-
-		fetchMoreFiles() {
-			this.fetching = true;
-
-			const max = 30;
-
-			// ファイル一覧取得
-			os.api('drive/files', {
-				folderId: this.folder ? this.folder.id : null,
-				type: this.type,
-				untilId: this.files[this.files.length - 1].id,
-				limit: max + 1
-			}).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;
-			});
-		},
-
-		getMenu() {
-			return [{
-				text: this.$ts.addFile,
-				type: 'label'
-			}, {
-				text: this.$ts.upload,
-				icon: 'fas fa-upload',
-				action: () => { this.selectLocalFile(); }
-			}, {
-				text: this.$ts.fromUrl,
-				icon: 'fas fa-link',
-				action: () => { this.urlUpload(); }
-			}, null, {
-				text: this.folder ? this.folder.name : this.$ts.drive,
-				type: 'label'
-			}, this.folder ? {
-				text: this.$ts.renameFolder,
-				icon: 'fas fa-i-cursor',
-				action: () => { this.renameFolder(this.folder); }
-			} : undefined, this.folder ? {
-				text: this.$ts.deleteFolder,
-				icon: 'fas fa-trash-alt',
-				action: () => { this.deleteFolder(this.folder); }
-			} : undefined, {
-				text: this.$ts.createFolder,
-				icon: 'fas fa-folder-plus',
-				action: () => { this.createFolder(); }
-			}];
-		},
-
-		showMenu(ev) {
-			os.popupMenu(this.getMenu(), ev.currentTarget || ev.target);
-		},
-
-		onContextmenu(ev) {
-			os.contextMenu(this.getMenu(), ev);
-		},
+import { defaultStore } from '@/store';
+import { i18n } from '@/i18n';
+
+const props = withDefaults(defineProps<{
+	initialFolder?: Misskey.entities.DriveFolder;
+	type?: string;
+	multiple?: boolean;
+	select?: 'file' | 'folder' | null;
+}>(), {
+	multiple: false,
+	select: null,
+});
+
+const emit = defineEmits<{
+	(e: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void;
+	(e: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
+	(e: 'move-root'): void;
+	(e: 'cd', v: Misskey.entities.DriveFolder | null): void;
+	(e: 'open-folder', v: Misskey.entities.DriveFolder): void;
+}>();
+
+const loadMoreFiles = ref<InstanceType<typeof MkButton>>();
+const fileInput = ref<HTMLInputElement>();
+
+const folder = ref<Misskey.entities.DriveFolder | null>(null);
+const files = ref<Misskey.entities.DriveFile[]>([]);
+const folders = ref<Misskey.entities.DriveFolder[]>([]);
+const moreFiles = ref(false);
+const moreFolders = ref(false);
+const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]);
+const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
+const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
+const uploadings = os.uploads;
+const connection = stream.useChannel('drive');
+
+// ドロップされようとしているか
+const draghover = ref(false);
+
+// 自身の所有するアイテムがドラッグをスタートさせたか
+// (自分自身の階層にドロップできないようにするためのフラグ)
+const isDragSource = ref(false);
+
+const fetching = ref(true);
+
+const ilFilesObserver = new IntersectionObserver(
+	(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles()
+)
+
+watch(folder, () => emit('cd', folder.value));
+
+function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
+	addFile(file, true);
+}
+
+function onStreamDriveFileUpdated(file: Misskey.entities.DriveFile) {
+	const current = folder.value ? folder.value.id : null;
+	if (current != file.folderId) {
+		removeFile(file);
+	} else {
+		addFile(file, true);
 	}
+}
+
+function onStreamDriveFileDeleted(fileId: string) {
+	removeFile(fileId);
+}
+
+function onStreamDriveFolderCreated(createdFolder: Misskey.entities.DriveFolder) {
+	addFolder(createdFolder, true);
+}
+
+function onStreamDriveFolderUpdated(updatedFolder: Misskey.entities.DriveFolder) {
+	const current = folder.value ? folder.value.id : null;
+	if (current != updatedFolder.parentId) {
+		removeFolder(updatedFolder);
+	} else {
+		addFolder(updatedFolder, true);
+	}
+}
+
+function onStreamDriveFolderDeleted(folderId: string) {
+	removeFolder(folderId);
+}
+
+function onDragover(e: DragEvent): any {
+	if (!e.dataTransfer) return;
+
+	// ドラッグ元が自分自身の所有するアイテムだったら
+	if (isDragSource.value) {
+		// 自分自身にはドロップさせない
+		e.dataTransfer.dropEffect = 'none';
+		return;
+	}
+
+	const isFile = e.dataTransfer.items[0].kind == 'file';
+	const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+	const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
+	if (isFile || isDriveFile || isDriveFolder) {
+		e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+	} else {
+		e.dataTransfer.dropEffect = 'none';
+	}
+
+	return false;
+}
+
+function onDragenter() {
+	if (!isDragSource.value) draghover.value = true;
+}
+
+function onDragleave() {
+	draghover.value = false;
+}
+
+function onDrop(e: DragEvent): any {
+	draghover.value = false;
+
+	if (!e.dataTransfer) return;
+
+	// ドロップされてきたものがファイルだったら
+	if (e.dataTransfer.files.length > 0) {
+		for (const file of Array.from(e.dataTransfer.files)) {
+			upload(file, folder.value);
+		}
+		return;
+	}
+
+	//#region ドライブのファイル
+	const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+	if (driveFile != null && driveFile != '') {
+		const file = JSON.parse(driveFile);
+		if (files.value.some(f => f.id == file.id)) return;
+		removeFile(file.id);
+		os.api('drive/files/update', {
+			fileId: file.id,
+			folderId: folder.value ? folder.value.id : null
+		});
+	}
+	//#endregion
+
+	//#region ドライブのフォルダ
+	const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
+	if (driveFolder != null && driveFolder != '') {
+		const droppedFolder = JSON.parse(driveFolder);
+
+		// 移動先が自分自身ならreject
+		if (folder.value && droppedFolder.id == folder.value.id) return false;
+		if (folders.value.some(f => f.id == droppedFolder.id)) return false;
+		removeFolder(droppedFolder.id);
+		os.api('drive/folders/update', {
+			folderId: droppedFolder.id,
+			parentId: folder.value ? folder.value.id : null
+		}).then(() => {
+			// noop
+		}).catch(err => {
+			switch (err) {
+				case 'detected-circular-definition':
+					os.alert({
+						title: i18n.locale.unableToProcess,
+						text: i18n.locale.circularReferenceFolder
+					});
+					break;
+				default:
+					os.alert({
+						type: 'error',
+						text: i18n.locale.somethingHappened
+					});
+			}
+		});
+	}
+	//#endregion
+}
+
+function selectLocalFile() {
+	fileInput.value?.click();
+}
+
+function urlUpload() {
+	os.inputText({
+		title: i18n.locale.uploadFromUrl,
+		type: 'url',
+		placeholder: i18n.locale.uploadFromUrlDescription
+	}).then(({ canceled, result: url }) => {
+		if (canceled || !url) return;
+		os.api('drive/files/upload-from-url', {
+			url: url,
+			folderId: folder.value ? folder.value.id : undefined
+		});
+
+		os.alert({
+			title: i18n.locale.uploadFromUrlRequested,
+			text: i18n.locale.uploadFromUrlMayTakeTime
+		});
+	});
+}
+
+function createFolder() {
+	os.inputText({
+		title: i18n.locale.createFolder,
+		placeholder: i18n.locale.folderName
+	}).then(({ canceled, result: name }) => {
+		if (canceled) return;
+		os.api('drive/folders/create', {
+			name: name,
+			parentId: folder.value ? folder.value.id : undefined
+		}).then(createdFolder => {
+			addFolder(createdFolder, true);
+		});
+	});
+}
+
+function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
+	os.inputText({
+		title: i18n.locale.renameFolder,
+		placeholder: i18n.locale.inputNewFolderName,
+		default: folderToRename.name
+	}).then(({ canceled, result: name }) => {
+		if (canceled) return;
+		os.api('drive/folders/update', {
+			folderId: folderToRename.id,
+			name: name
+		}).then(updatedFolder => {
+			// FIXME: 画面を更新するために自分自身に移動
+			move(updatedFolder);
+		});
+	});
+}
+
+function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
+	os.api('drive/folders/delete', {
+		folderId: folderToDelete.id
+	}).then(() => {
+		// 削除時に親フォルダに移動
+		move(folderToDelete.parentId);
+	}).catch(err => {
+		switch(err.id) {
+			case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
+				os.alert({
+					type: 'error',
+					title: i18n.locale.unableToDelete,
+					text: i18n.locale.hasChildFilesOrFolders
+				});
+				break;
+			default:
+				os.alert({
+					type: 'error',
+					text: i18n.locale.unableToDelete
+				});
+			}
+	});
+}
+
+function onChangeFileInput() {
+	if (!fileInput.value?.files) return;
+	for (const file of Array.from(fileInput.value.files)) {
+		upload(file, folder.value);
+	}
+}
+
+function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) {
+	os.upload(file, (folderToUpload && typeof folderToUpload == 'object') ? folderToUpload.id : null).then(res => {
+		addFile(res, true);
+	});
+}
+
+function chooseFile(file: Misskey.entities.DriveFile) {
+	const isAlreadySelected = selectedFiles.value.some(f => f.id == file.id);
+	if (props.multiple) {
+		if (isAlreadySelected) {
+			selectedFiles.value = selectedFiles.value.filter(f => f.id != file.id);
+		} else {
+			selectedFiles.value.push(file);
+		}
+		emit('change-selection', selectedFiles.value);
+	} else {
+		if (isAlreadySelected) {
+			emit('selected', file);
+		} else {
+			selectedFiles.value = [file];
+			emit('change-selection', [file]);
+		}
+	}
+}
+
+function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
+	const isAlreadySelected = selectedFolders.value.some(f => f.id == folderToChoose.id);
+	if (props.multiple) {
+		if (isAlreadySelected) {
+			selectedFolders.value = selectedFolders.value.filter(f => f.id != folderToChoose.id);
+		} else {
+			selectedFolders.value.push(folderToChoose);
+		}
+		emit('change-selection', selectedFolders.value);
+	} else {
+		if (isAlreadySelected) {
+			emit('selected', folderToChoose);
+		} else {
+			selectedFolders.value = [folderToChoose];
+			emit('change-selection', [folderToChoose]);
+		}
+	}
+}
+
+function move(target?: Misskey.entities.DriveFolder) {
+	if (!target) {
+		goRoot();
+		return;
+	} else if (typeof target == 'object') {
+		target = target.id;
+	}
+
+	fetching.value = true;
+
+	os.api('drive/folders/show', {
+		folderId: target
+	}).then(folderToMove => {
+		folder.value = folderToMove;
+		hierarchyFolders.value = [];
+
+		const dive = folderToDive => {
+			hierarchyFolders.value.unshift(folderToDive);
+			if (folderToDive.parent) dive(folderToDive.parent);
+		};
+
+		if (folderToMove.parent) dive(folderToMove.parent);
+
+		emit('open-folder', folderToMove);
+		fetch();
+	});
+}
+
+function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) {
+	const current = folder.value ? folder.value.id : null;
+	if (current != folderToAdd.parentId) return;
+
+	if (folders.value.some(f => f.id == folderToAdd.id)) {
+		const exist = folders.value.map(f => f.id).indexOf(folderToAdd.id);
+		folders.value[exist] = folderToAdd;
+		return;
+	}
+
+	if (unshift) {
+		folders.value.unshift(folderToAdd);
+	} else {
+		folders.value.push(folderToAdd);
+	}
+}
+
+function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) {
+	const current = folder.value ? folder.value.id : null;
+	if (current != fileToAdd.folderId) return;
+
+	if (files.value.some(f => f.id == fileToAdd.id)) {
+		const exist = files.value.map(f => f.id).indexOf(fileToAdd.id);
+		files.value[exist] = fileToAdd;
+		return;
+	}
+
+	if (unshift) {
+		files.value.unshift(fileToAdd);
+	} else {
+		files.value.push(fileToAdd);
+	}
+}
+
+function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) {
+	const folderIdToRemove = typeof folderToRemove === 'object' ? folderToRemove.id : folderToRemove;
+	folders.value = folders.value.filter(f => f.id != folderIdToRemove);
+}
+
+function removeFile(file: Misskey.entities.DriveFile | string) {
+	const fileId = typeof file === 'object' ? file.id : file;
+	files.value = files.value.filter(f => f.id != fileId);
+}
+
+function appendFile(file: Misskey.entities.DriveFile) {
+	addFile(file);
+}
+
+function appendFolder(folderToAppend: Misskey.entities.DriveFolder) {
+	addFolder(folderToAppend);
+}
+/*
+function prependFile(file: Misskey.entities.DriveFile) {
+	addFile(file, true);
+}
+
+function prependFolder(folderToPrepend: Misskey.entities.DriveFolder) {
+	addFolder(folderToPrepend, true);
+}
+*/
+function goRoot() {
+	// 既にrootにいるなら何もしない
+	if (folder.value == null) return;
+
+	folder.value = null;
+	hierarchyFolders.value = [];
+	emit('move-root');
+	fetch();
+}
+
+async function fetch() {
+	folders.value = [];
+	files.value = [];
+	moreFolders.value = false;
+	moreFiles.value = false;
+	fetching.value = true;
+
+	const foldersMax = 30;
+	const filesMax = 30;
+
+	const foldersPromise = os.api('drive/folders', {
+		folderId: folder.value ? folder.value.id : null,
+		limit: foldersMax + 1
+	}).then(fetchedFolders => {
+		if (fetchedFolders.length == foldersMax + 1) {
+			moreFolders.value = true;
+			fetchedFolders.pop();
+		}
+		return fetchedFolders;
+	});
+
+	const filesPromise = os.api('drive/files', {
+		folderId: folder.value ? folder.value.id : null,
+		type: props.type,
+		limit: filesMax + 1
+	}).then(fetchedFiles => {
+		if (fetchedFiles.length == filesMax + 1) {
+			moreFiles.value = true;
+			fetchedFiles.pop();
+		}
+		return fetchedFiles;
+	});
+
+	const [fetchedFolders, fetchedFiles] = await Promise.all([foldersPromise, filesPromise]);
+
+	for (const x of fetchedFolders) appendFolder(x);
+	for (const x of fetchedFiles) appendFile(x);
+
+	fetching.value = false;
+}
+
+function fetchMoreFiles() {
+	fetching.value = true;
+
+	const max = 30;
+
+	// ファイル一覧取得
+	os.api('drive/files', {
+		folderId: folder.value ? folder.value.id : null,
+		type: props.type,
+		untilId: files.value[files.value.length - 1].id,
+		limit: max + 1
+	}).then(files => {
+		if (files.length == max + 1) {
+			moreFiles.value = true;
+			files.pop();
+		} else {
+			moreFiles.value = false;
+		}
+		for (const x of files) appendFile(x);
+		fetching.value = false;
+	});
+}
+
+function getMenu() {
+	return [{
+		text: i18n.locale.addFile,
+		type: 'label'
+	}, {
+		text: i18n.locale.upload,
+		icon: 'fas fa-upload',
+		action: () => { selectLocalFile(); }
+	}, {
+		text: i18n.locale.fromUrl,
+		icon: 'fas fa-link',
+		action: () => { urlUpload(); }
+	}, null, {
+		text: folder.value ? folder.value.name : i18n.locale.drive,
+		type: 'label'
+	}, folder.value ? {
+		text: i18n.locale.renameFolder,
+		icon: 'fas fa-i-cursor',
+		action: () => { renameFolder(folder.value); }
+	} : undefined, folder.value ? {
+		text: i18n.locale.deleteFolder,
+		icon: 'fas fa-trash-alt',
+		action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }
+	} : undefined, {
+		text: i18n.locale.createFolder,
+		icon: 'fas fa-folder-plus',
+		action: () => { createFolder(); }
+	}];
+}
+
+function showMenu(ev: MouseEvent) {
+	os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined);
+}
+
+function onContextmenu(ev: MouseEvent) {
+	os.contextMenu(getMenu(), ev);
+}
+
+onMounted(() => {
+	if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) {
+		nextTick(() => {
+			ilFilesObserver.observe(loadMoreFiles.value?.$el)
+		});
+	}
+
+	connection.on('fileCreated', onStreamDriveFileCreated);
+	connection.on('fileUpdated', onStreamDriveFileUpdated);
+	connection.on('fileDeleted', onStreamDriveFileDeleted);
+	connection.on('folderCreated', onStreamDriveFolderCreated);
+	connection.on('folderUpdated', onStreamDriveFolderUpdated);
+	connection.on('folderDeleted', onStreamDriveFolderDeleted);
+
+	if (props.initialFolder) {
+		move(props.initialFolder);
+	} else {
+		fetch();
+	}
+});
+
+onActivated(() => {
+	if (defaultStore.state.enableInfiniteScroll) {
+		nextTick(() => {
+			ilFilesObserver.observe(loadMoreFiles.value?.$el)
+		});
+	}
+});
+
+onBeforeUnmount(() => {
+	connection.dispose();
+	ilFilesObserver.disconnect();
 });
 </script>
 
diff --git a/packages/client/src/components/emoji-picker-dialog.vue b/packages/client/src/components/emoji-picker-dialog.vue
index 51c634dd8e..f06a24636c 100644
--- a/packages/client/src/components/emoji-picker-dialog.vue
+++ b/packages/client/src/components/emoji-picker-dialog.vue
@@ -1,58 +1,65 @@
 <template>
-<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'middle'" :prefer-type="asReactionPicker && $store.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'" :transparent-bg="true" :manual-showing="manualShowing" :src="src" @click="$refs.modal.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')">
-	<MkEmojiPicker ref="picker" class="ryghynhb _popup _shadow" :class="{ drawer: type === 'drawer' }" :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" :as-drawer="type === 'drawer'" :max-height="maxHeight" @chosen="chosen"/>
+<MkModal
+	ref="modal"
+	v-slot="{ type, maxHeight }"
+	:z-priority="'middle'"
+	:prefer-type="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
+	:transparent-bg="true"
+	:manual-showing="manualShowing"
+	:src="src"
+	@click="modal?.close()"
+	@opening="opening"
+	@close="emit('close')"
+	@closed="emit('closed')"
+>
+	<MkEmojiPicker
+		ref="picker"
+		class="ryghynhb _popup _shadow"
+		:class="{ drawer: type === 'drawer' }"
+		:show-pinned="showPinned"
+		:as-reaction-picker="asReactionPicker"
+		:as-drawer="type === 'drawer'"
+		:max-height="maxHeight"
+		@chosen="chosen"
+	/>
 </MkModal>
 </template>
 
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
 import MkModal from '@/components/ui/modal.vue';
 import MkEmojiPicker from '@/components/emoji-picker.vue';
+import { defaultStore } from '@/store';
 
-export default defineComponent({
-	components: {
-		MkModal,
-		MkEmojiPicker,
-	},
-
-	props: {
-		manualShowing: {
-			type: Boolean,
-			required: false,
-			default: null,
-		},
-		src: {
-			required: false
-		},
-		showPinned: {
-			required: false,
-			default: true
-		},
-		asReactionPicker: {
-			required: false
-		},
-	},
-
-	emits: ['done', 'close', 'closed'],
-
-	data() {
-		return {
-
-		};
-	},
-
-	methods: {
-		chosen(emoji: any) {
-			this.$emit('done', emoji);
-			this.$refs.modal.close();
-		},
-
-		opening() {
-			this.$refs.picker.reset();
-			this.$refs.picker.focus();
-		}
-	}
+withDefaults(defineProps<{
+	manualShowing?: boolean;
+	src?: HTMLElement;
+	showPinned?: boolean;
+	asReactionPicker?: boolean;
+}>(), {
+	manualShowing: false,
+	showPinned: true,
+	asReactionPicker: false,
 });
+
+const emit = defineEmits<{
+	(e: 'done', v: any): void;
+	(e: 'close'): void;
+	(e: 'closed'): void;
+}>();
+
+const modal = ref<InstanceType<typeof MkModal>>();
+const picker = ref<InstanceType<typeof MkEmojiPicker>>();
+
+function chosen(emoji: any) {
+	emit('done', emoji);
+	modal.value?.close();
+}
+
+function opening() {
+	picker.value?.reset();
+	picker.value?.focus();
+}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/emoji-picker-window.vue b/packages/client/src/components/emoji-picker-window.vue
index 0ffa0c1187..4d27fb48ba 100644
--- a/packages/client/src/components/emoji-picker-window.vue
+++ b/packages/client/src/components/emoji-picker-window.vue
@@ -5,50 +5,33 @@
 	:can-resize="false"
 	:mini="true"
 	:front="true"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 >
 	<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
 </MkWindow>
 </template>
 
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import MkWindow from '@/components/ui/window.vue';
 import MkEmojiPicker from '@/components/emoji-picker.vue';
 
-export default defineComponent({
-	components: {
-		MkWindow,
-		MkEmojiPicker,
-	},
-
-	props: {
-		src: {
-			required: false
-		},
-		showPinned: {
-			required: false,
-			default: true
-		},
-		asReactionPicker: {
-			required: false
-		},
-	},
-
-	emits: ['chosen', 'closed'],
-
-	data() {
-		return {
-
-		};
-	},
-
-	methods: {
-		chosen(emoji: any) {
-			this.$emit('chosen', emoji);
-		},
-	}
+withDefaults(defineProps<{
+	src?: HTMLElement;
+	showPinned?: boolean;
+	asReactionPicker?: boolean;
+}>(), {
+	showPinned: true,
 });
+
+const emit = defineEmits<{
+	(e: 'chosen', v: any): void;
+	(e: 'closed'): void;
+}>();
+
+function chosen(emoji: any) {
+	emit('chosen', emoji);
+}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/emoji-picker.section.vue b/packages/client/src/components/emoji-picker.section.vue
index 08c4f6813d..1026e894d1 100644
--- a/packages/client/src/components/emoji-picker.section.vue
+++ b/packages/client/src/components/emoji-picker.section.vue
@@ -7,7 +7,7 @@
 		<button v-for="emoji in emojis"
 			:key="emoji"
 			class="_button"
-			@click="chosen(emoji, $event)"
+			@click="emit('chosen', emoji, $event)"
 		>
 			<MkEmoji :emoji="emoji" :normal="true"/>
 		</button>
@@ -15,35 +15,19 @@
 </section>
 </template>
 
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import { getStaticImageUrl } from '@/scripts/get-static-image-url';
+<script lang="ts" setup>
+import { ref } from 'vue';
 
-export default defineComponent({
-	props: {
-		emojis: {
-			required: true,
-		},
-		initialShown: {
-			required: false
-		}
-	},
+const props = defineProps<{
+	emojis: string[];
+	initialShown?: boolean;
+}>();
 
-	emits: ['chosen'],
+const emit = defineEmits<{
+	(e: 'chosen', v: string, ev: MouseEvent): void;
+}>();
 
-	data() {
-		return {
-			getStaticImageUrl,
-			shown: this.initialShown,
-		};
-	},
-
-	methods: {
-		chosen(emoji: any, ev) {
-			this.$parent.chosen(emoji, ev);
-		},
-	}
-});
+const shown = ref(!!props.initialShown);
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/emoji-picker.vue b/packages/client/src/components/emoji-picker.vue
index a8eed1ca21..96670fa58c 100644
--- a/packages/client/src/components/emoji-picker.vue
+++ b/packages/client/src/components/emoji-picker.vue
@@ -1,18 +1,18 @@
 <template>
-<div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : null }">
-	<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="$ts.search" @paste.stop="paste" @keyup.enter="done()">
+<div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
+	<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.locale.search" @paste.stop="paste" @keyup.enter="done()">
 	<div ref="emojis" class="emojis">
 		<section class="result">
 			<div v-if="searchResultCustom.length > 0">
 				<button v-for="emoji in searchResultCustom"
-					:key="emoji"
+					:key="emoji.id"
 					class="_button"
 					:title="emoji.name"
 					tabindex="0"
 					@click="chosen(emoji, $event)"
 				>
-					<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>
-					<img v-else :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
+					<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
+					<img :src="disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
 				</button>
 			</div>
 			<div v-if="searchResultUnicode.length > 0">
@@ -43,9 +43,9 @@
 			</section>
 
 			<section>
-				<header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ $ts.recentUsed }}</header>
+				<header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.locale.recentUsed }}</header>
 				<div>
-					<button v-for="emoji in $store.state.recentlyUsedEmojis"
+					<button v-for="emoji in recentlyUsedEmojis"
 						:key="emoji"
 						class="_button"
 						@click="chosen(emoji, $event)"
@@ -56,12 +56,12 @@
 			</section>
 		</div>
 		<div>
-			<header class="_acrylic">{{ $ts.customEmojis }}</header>
-			<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')">{{ category || $ts.other }}</XSection>
+			<header class="_acrylic">{{ i18n.locale.customEmojis }}</header>
+			<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.locale.other }}</XSection>
 		</div>
 		<div>
-			<header class="_acrylic">{{ $ts.emoji }}</header>
-			<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)">{{ category }}</XSection>
+			<header class="_acrylic">{{ i18n.locale.emoji }}</header>
+			<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
 		</div>
 	</div>
 	<div class="tabs">
@@ -73,277 +73,272 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import { emojilist } from '@/scripts/emojilist';
+<script lang="ts" setup>
+import { ref, computed, watch, onMounted } from 'vue';
+import * as Misskey from 'misskey-js';
+import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist';
 import { getStaticImageUrl } from '@/scripts/get-static-image-url';
 import Ripple from '@/components/ripple.vue';
 import * as os from '@/os';
 import { isTouchUsing } from '@/scripts/touch';
 import { isMobile } from '@/scripts/is-mobile';
-import { emojiCategories } from '@/instance';
+import { emojiCategories, instance } from '@/instance';
 import XSection from './emoji-picker.section.vue';
+import { i18n } from '@/i18n';
+import { defaultStore } from '@/store';
 
-export default defineComponent({
-	components: {
-		XSection
-	},
+const props = withDefaults(defineProps<{
+	showPinned?: boolean;
+	asReactionPicker?: boolean;
+	maxHeight?: number;
+	asDrawer?: boolean;
+}>(), {
+	showPinned: true,
+});
 
-	props: {
-		showPinned: {
-			required: false,
-			default: true,
-		},
-		asReactionPicker: {
-			required: false,
-		},
-		maxHeight: {
-			type: Number,
-			required: false,
-		},
-		asDrawer: {
-			type: Boolean,
-			required: false
-		},
-	},
+const emit = defineEmits<{
+	(e: 'chosen', v: string): void;
+}>();
 
-	emits: ['chosen'],
+const search = ref<HTMLInputElement>();
+const emojis = ref<HTMLDivElement>();
 
-	data() {
-		return {
-			emojilist: markRaw(emojilist),
-			getStaticImageUrl,
-			pinned: this.$store.reactiveState.reactions,
-			width: this.asReactionPicker ? this.$store.state.reactionPickerWidth : 3,
-			height: this.asReactionPicker ? this.$store.state.reactionPickerHeight : 2,
-			big: this.asReactionPicker ? isTouchUsing : false,
-			customEmojiCategories: emojiCategories,
-			customEmojis: this.$instance.emojis,
-			q: null,
-			searchResultCustom: [],
-			searchResultUnicode: [],
-			tab: 'index',
-			categories: ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'],
-		};
-	},
+const {
+	reactions: pinned,
+	reactionPickerWidth,
+	reactionPickerHeight,
+	disableShowingAnimatedImages,
+	recentlyUsedEmojis,
+} = defaultStore.reactiveState;
 
-	watch: {
-		q() {
-			this.$refs.emojis.scrollTop = 0;
+const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3);
+const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2);
+const big = props.asReactionPicker ? isTouchUsing : false;
+const customEmojiCategories = emojiCategories;
+const customEmojis = instance.emojis;
+const q = ref<string | null>(null);
+const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
+const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
+const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
 
-			if (this.q == null || this.q === '') {
-				this.searchResultCustom = [];
-				this.searchResultUnicode = [];
-				return;
-			}
+watch(q, () => {
+	if (emojis.value) emojis.value.scrollTop = 0;
 
-			const q = this.q.replace(/:/g, '');
-
-			const searchCustom = () => {
-				const max = 8;
-				const emojis = this.customEmojis;
-				const matches = new Set();
-
-				const exactMatch = emojis.find(e => e.name === q);
-				if (exactMatch) matches.add(exactMatch);
-
-				if (q.includes(' ')) { // AND検索
-					const keywords = q.split(' ');
-
-					// 名前にキーワードが含まれている
-					for (const emoji of emojis) {
-						if (keywords.every(keyword => emoji.name.includes(keyword))) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-					if (matches.size >= max) return matches;
-
-					// 名前またはエイリアスにキーワードが含まれている
-					for (const emoji of emojis) {
-						if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-				} else {
-					for (const emoji of emojis) {
-						if (emoji.name.startsWith(q)) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-					if (matches.size >= max) return matches;
-
-					for (const emoji of emojis) {
-						if (emoji.aliases.some(alias => alias.startsWith(q))) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-					if (matches.size >= max) return matches;
-
-					for (const emoji of emojis) {
-						if (emoji.name.includes(q)) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-					if (matches.size >= max) return matches;
-
-					for (const emoji of emojis) {
-						if (emoji.aliases.some(alias => alias.includes(q))) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-				}
-
-				return matches;
-			};
-
-			const searchUnicode = () => {
-				const max = 8;
-				const emojis = this.emojilist;
-				const matches = new Set();
-
-				const exactMatch = emojis.find(e => e.name === q);
-				if (exactMatch) matches.add(exactMatch);
-
-				if (q.includes(' ')) { // AND検索
-					const keywords = q.split(' ');
-
-					// 名前にキーワードが含まれている
-					for (const emoji of emojis) {
-						if (keywords.every(keyword => emoji.name.includes(keyword))) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-					if (matches.size >= max) return matches;
-
-					// 名前またはエイリアスにキーワードが含まれている
-					for (const emoji of emojis) {
-						if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-				} else {
-					for (const emoji of emojis) {
-						if (emoji.name.startsWith(q)) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-					if (matches.size >= max) return matches;
-
-					for (const emoji of emojis) {
-						if (emoji.keywords.some(keyword => keyword.startsWith(q))) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-					if (matches.size >= max) return matches;
-
-					for (const emoji of emojis) {
-						if (emoji.name.includes(q)) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-					if (matches.size >= max) return matches;
-
-					for (const emoji of emojis) {
-						if (emoji.keywords.some(keyword => keyword.includes(q))) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-				}
-
-				return matches;
-			};
-
-			this.searchResultCustom = Array.from(searchCustom());
-			this.searchResultUnicode = Array.from(searchUnicode());
-		}
-	},
-
-	mounted() {
-		this.focus();
-	},
-
-	methods: {
-		focus() {
-			if (!isMobile && !isTouchUsing) {
-				this.$refs.search.focus({
-					preventScroll: true
-				});
-			}
-		},
-
-		reset() {
-			this.$refs.emojis.scrollTop = 0;
-			this.q = '';
-		},
-
-		getKey(emoji: any) {
-			return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
-		},
-
-		chosen(emoji: any, ev) {
-			if (ev) {
-				const el = ev.currentTarget || ev.target;
-				const rect = el.getBoundingClientRect();
-				const x = rect.left + (el.offsetWidth / 2);
-				const y = rect.top + (el.offsetHeight / 2);
-				os.popup(Ripple, { x, y }, {}, 'end');
-			}
-
-			const key = this.getKey(emoji);
-			this.$emit('chosen', key);
-
-			// 最近使った絵文字更新
-			if (!this.pinned.includes(key)) {
-				let recents = this.$store.state.recentlyUsedEmojis;
-				recents = recents.filter((e: any) => e !== key);
-				recents.unshift(key);
-				this.$store.set('recentlyUsedEmojis', recents.splice(0, 32));
-			}
-		},
-
-		paste(event) {
-			const paste = (event.clipboardData || window.clipboardData).getData('text');
-			if (this.done(paste)) {
-				event.preventDefault();
-			}
-		},
-
-		done(query) {
-			if (query == null) query = this.q;
-			if (query == null) return;
-			const q = query.replace(/:/g, '');
-			const exactMatchCustom = this.customEmojis.find(e => e.name === q);
-			if (exactMatchCustom) {
-				this.chosen(exactMatchCustom);
-				return true;
-			}
-			const exactMatchUnicode = this.emojilist.find(e => e.char === q || e.name === q);
-			if (exactMatchUnicode) {
-				this.chosen(exactMatchUnicode);
-				return true;
-			}
-			if (this.searchResultCustom.length > 0) {
-				this.chosen(this.searchResultCustom[0]);
-				return true;
-			}
-			if (this.searchResultUnicode.length > 0) {
-				this.chosen(this.searchResultUnicode[0]);
-				return true;
-			}
-		},
+	if (q.value == null || q.value === '') {
+		searchResultCustom.value = [];
+		searchResultUnicode.value = [];
+		return;
 	}
+
+	const newQ = q.value.replace(/:/g, '');
+
+	const searchCustom = () => {
+		const max = 8;
+		const emojis = customEmojis;
+		const matches = new Set<Misskey.entities.CustomEmoji>();
+
+		const exactMatch = emojis.find(e => e.name === newQ);
+		if (exactMatch) matches.add(exactMatch);
+
+		if (newQ.includes(' ')) { // AND検索
+			const keywords = newQ.split(' ');
+
+			// 名前にキーワードが含まれている
+			for (const emoji of emojis) {
+				if (keywords.every(keyword => emoji.name.includes(keyword))) {
+					matches.add(emoji);
+					if (matches.size >= max) break;
+				}
+			}
+			if (matches.size >= max) return matches;
+
+			// 名前またはエイリアスにキーワードが含まれている
+			for (const emoji of emojis) {
+				if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) {
+					matches.add(emoji);
+					if (matches.size >= max) break;
+				}
+			}
+		} else {
+			for (const emoji of emojis) {
+				if (emoji.name.startsWith(newQ)) {
+					matches.add(emoji);
+					if (matches.size >= max) break;
+				}
+			}
+			if (matches.size >= max) return matches;
+
+			for (const emoji of emojis) {
+				if (emoji.aliases.some(alias => alias.startsWith(newQ))) {
+					matches.add(emoji);
+					if (matches.size >= max) break;
+				}
+			}
+			if (matches.size >= max) return matches;
+
+			for (const emoji of emojis) {
+				if (emoji.name.includes(newQ)) {
+					matches.add(emoji);
+					if (matches.size >= max) break;
+				}
+			}
+			if (matches.size >= max) return matches;
+
+			for (const emoji of emojis) {
+				if (emoji.aliases.some(alias => alias.includes(newQ))) {
+					matches.add(emoji);
+					if (matches.size >= max) break;
+				}
+			}
+		}
+
+		return matches;
+	};
+
+	const searchUnicode = () => {
+		const max = 8;
+		const emojis = emojilist;
+		const matches = new Set<UnicodeEmojiDef>();
+
+		const exactMatch = emojis.find(e => e.name === newQ);
+		if (exactMatch) matches.add(exactMatch);
+
+		if (newQ.includes(' ')) { // AND検索
+			const keywords = newQ.split(' ');
+
+			// 名前にキーワードが含まれている
+			for (const emoji of emojis) {
+				if (keywords.every(keyword => emoji.name.includes(keyword))) {
+					matches.add(emoji);
+					if (matches.size >= max) break;
+				}
+			}
+			if (matches.size >= max) return matches;
+
+			// 名前またはエイリアスにキーワードが含まれている
+			for (const emoji of emojis) {
+				if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) {
+					matches.add(emoji);
+					if (matches.size >= max) break;
+				}
+			}
+		} else {
+			for (const emoji of emojis) {
+				if (emoji.name.startsWith(newQ)) {
+					matches.add(emoji);
+					if (matches.size >= max) break;
+				}
+			}
+			if (matches.size >= max) return matches;
+
+			for (const emoji of emojis) {
+				if (emoji.keywords.some(keyword => keyword.startsWith(newQ))) {
+					matches.add(emoji);
+					if (matches.size >= max) break;
+				}
+			}
+			if (matches.size >= max) return matches;
+
+			for (const emoji of emojis) {
+				if (emoji.name.includes(newQ)) {
+					matches.add(emoji);
+					if (matches.size >= max) break;
+				}
+			}
+			if (matches.size >= max) return matches;
+
+			for (const emoji of emojis) {
+				if (emoji.keywords.some(keyword => keyword.includes(newQ))) {
+					matches.add(emoji);
+					if (matches.size >= max) break;
+				}
+			}
+		}
+
+		return matches;
+	};
+
+	searchResultCustom.value = Array.from(searchCustom());
+	searchResultUnicode.value = Array.from(searchUnicode());
+});
+
+function focus() {
+	if (!isMobile && !isTouchUsing) {
+		search.value?.focus({
+			preventScroll: true
+		});
+	}
+}
+
+function reset() {
+	if (emojis.value) emojis.value.scrollTop = 0;
+	q.value = '';
+}
+
+function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): string {
+	return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
+}
+
+function chosen(emoji: any, ev?: MouseEvent) {
+	const el = ev && (ev.currentTarget || ev.target) as HTMLElement | null | undefined;
+	if (el) {
+		const rect = el.getBoundingClientRect();
+		const x = rect.left + (el.offsetWidth / 2);
+		const y = rect.top + (el.offsetHeight / 2);
+		os.popup(Ripple, { x, y }, {}, 'end');
+	}
+
+	const key = getKey(emoji);
+	emit('chosen', key);
+
+	// 最近使った絵文字更新
+	if (!pinned.value.includes(key)) {
+		let recents = defaultStore.state.recentlyUsedEmojis;
+		recents = recents.filter((e: any) => e !== key);
+		recents.unshift(key);
+		defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
+	}
+}
+
+function paste(event: ClipboardEvent) {
+	const paste = (event.clipboardData || window.clipboardData).getData('text');
+	if (done(paste)) {
+		event.preventDefault();
+	}
+}
+
+function done(query?: any): boolean | void {
+	if (query == null) query = q.value;
+	if (query == null || typeof query !== 'string') return;
+
+	const q2 = query.replace(/:/g, '');
+	const exactMatchCustom = customEmojis.find(e => e.name === q2);
+	if (exactMatchCustom) {
+		chosen(exactMatchCustom);
+		return true;
+	}
+	const exactMatchUnicode = emojilist.find(e => e.char === q2 || e.name === q2);
+	if (exactMatchUnicode) {
+		chosen(exactMatchUnicode);
+		return true;
+	}
+	if (searchResultCustom.value.length > 0) {
+		chosen(searchResultCustom.value[0]);
+		return true;
+	}
+	if (searchResultUnicode.value.length > 0) {
+		chosen(searchResultUnicode.value[0]);
+		return true;
+	}
+}
+
+onMounted(() => {
+	focus();
+});
+
+defineExpose({
+	focus,
+	reset,
 });
 </script>
 
diff --git a/packages/client/src/components/featured-photos.vue b/packages/client/src/components/featured-photos.vue
index af5892c98e..e58b5d2849 100644
--- a/packages/client/src/components/featured-photos.vue
+++ b/packages/client/src/components/featured-photos.vue
@@ -2,25 +2,15 @@
 <div v-if="meta" class="xfbouadm" :style="{ backgroundImage: `url(${ meta.backgroundImageUrl })` }"></div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
 import * as os from '@/os';
 
-export default defineComponent({
-	components: {
-	},
+const meta = ref<Misskey.entities.DetailedInstanceMetadata>();
 
-	data() {
-		return {
-			meta: null,
-		};
-	},
-
-	created() {
-		os.api('meta', { detail: true }).then(meta => {
-			this.meta = meta;
-		});
-	},
+os.api('meta', { detail: true }).then(gotMeta => {
+	meta.value = gotMeta;
 });
 </script>
 
diff --git a/packages/client/src/components/follow-button.vue b/packages/client/src/components/follow-button.vue
index b16b22f26f..345edb6441 100644
--- a/packages/client/src/components/follow-button.vue
+++ b/packages/client/src/components/follow-button.vue
@@ -6,129 +6,110 @@
 >
 	<template v-if="!wait">
 		<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
-			<span v-if="full">{{ $ts.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
+			<span v-if="full">{{ i18n.locale.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
 		</template>
 		<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合。 -->
-			<span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
+			<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
 		</template>
 		<template v-else-if="isFollowing">
-			<span v-if="full">{{ $ts.unfollow }}</span><i class="fas fa-minus"></i>
+			<span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i>
 		</template>
 		<template v-else-if="!isFollowing && user.isLocked">
-			<span v-if="full">{{ $ts.followRequest }}</span><i class="fas fa-plus"></i>
+			<span v-if="full">{{ i18n.locale.followRequest }}</span><i class="fas fa-plus"></i>
 		</template>
 		<template v-else-if="!isFollowing && !user.isLocked">
-			<span v-if="full">{{ $ts.follow }}</span><i class="fas fa-plus"></i>
+			<span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i>
 		</template>
 	</template>
 	<template v-else>
-		<span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
+		<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
 	</template>
 </button>
 </template>
 
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { onBeforeUnmount, onMounted, ref } from 'vue';
+import * as Misskey from 'misskey-js';
 import * as os from '@/os';
 import { stream } from '@/stream';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-		full: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		large: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-	},
+const props = withDefaults(defineProps<{
+	user: Misskey.entities.UserDetailed,
+	full?: boolean,
+	large?: boolean,
+}>(), {
+	full: false,
+	large: false,
+});
 
-	data() {
-		return {
-			isFollowing: this.user.isFollowing,
-			hasPendingFollowRequestFromYou: this.user.hasPendingFollowRequestFromYou,
-			wait: false,
-			connection: null,
-		};
-	},
+const isFollowing = ref(props.user.isFollowing);
+const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFromYou);
+const wait = ref(false);
+const connection = stream.useChannel('main');
 
-	created() {
-		// 渡されたユーザー情報が不完全な場合
-		if (this.user.isFollowing == null) {
-			os.api('users/show', {
-				userId: this.user.id
-			}).then(u => {
-				this.isFollowing = u.isFollowing;
-				this.hasPendingFollowRequestFromYou = u.hasPendingFollowRequestFromYou;
-			});
-		}
-	},
+if (props.user.isFollowing == null) {
+	os.api('users/show', {
+		userId: props.user.id
+	}).then(u => {
+		isFollowing.value = u.isFollowing;
+		hasPendingFollowRequestFromYou.value = u.hasPendingFollowRequestFromYou;
+	});
+}
 
-	mounted() {
-		this.connection = markRaw(stream.useChannel('main'));
-
-		this.connection.on('follow', this.onFollowChange);
-		this.connection.on('unfollow', this.onFollowChange);
-	},
-
-	beforeUnmount() {
-		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 os.confirm({
-						type: 'warning',
-						text: this.$t('unfollowConfirm', { name: this.user.name || this.user.username }),
-					});
-
-					if (canceled) return;
-
-					await os.api('following/delete', {
-						userId: this.user.id
-					});
-				} else {
-					if (this.hasPendingFollowRequestFromYou) {
-						await os.api('following/requests/cancel', {
-							userId: this.user.id
-						});
-					} else if (this.user.isLocked) {
-						await os.api('following/create', {
-							userId: this.user.id
-						});
-						this.hasPendingFollowRequestFromYou = true;
-					} else {
-						await os.api('following/create', {
-							userId: this.user.id
-						});
-						this.hasPendingFollowRequestFromYou = true;
-					}
-				}
-			} catch (e) {
-				console.error(e);
-			} finally {
-				this.wait = false;
-			}
-		}
+function onFollowChange(user: Misskey.entities.UserDetailed) {
+	if (user.id == props.user.id) {
+		isFollowing.value = user.isFollowing;
+		hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
 	}
+}
+
+async function onClick() {
+	wait.value = true;
+
+	try {
+		if (isFollowing.value) {
+			const { canceled } = await os.confirm({
+				type: 'warning',
+				text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
+			});
+
+			if (canceled) return;
+
+			await os.api('following/delete', {
+				userId: props.user.id
+			});
+		} else {
+			if (hasPendingFollowRequestFromYou.value) {
+				await os.api('following/requests/cancel', {
+					userId: props.user.id
+				});
+			} else if (props.user.isLocked) {
+				await os.api('following/create', {
+					userId: props.user.id
+				});
+				hasPendingFollowRequestFromYou.value = true;
+			} else {
+				await os.api('following/create', {
+					userId: props.user.id
+				});
+				hasPendingFollowRequestFromYou.value = true;
+			}
+		}
+	} catch (e) {
+		console.error(e);
+	} finally {
+		wait.value = false;
+	}
+}
+
+onMounted(() => {
+	connection.on('follow', onFollowChange);
+	connection.on('unfollow', onFollowChange);
+});
+
+onBeforeUnmount(() => {
+	connection.dispose();
 });
 </script>
 
diff --git a/packages/client/src/components/forgot-password.vue b/packages/client/src/components/forgot-password.vue
index b03a6133b4..c74e1ac75e 100644
--- a/packages/client/src/components/forgot-password.vue
+++ b/packages/client/src/components/forgot-password.vue
@@ -2,72 +2,64 @@
 <XModalWindow ref="dialog"
 	:width="370"
 	:height="400"
-	@close="$refs.dialog.close()"
-	@closed="$emit('closed')"
+	@close="dialog.close()"
+	@closed="emit('closed')"
 >
-	<template #header>{{ $ts.forgotPassword }}</template>
+	<template #header>{{ i18n.locale.forgotPassword }}</template>
 
-	<form v-if="$instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
+	<form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
 		<div class="main _formRoot">
 			<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
-				<template #label>{{ $ts.username }}</template>
+				<template #label>{{ i18n.locale.username }}</template>
 				<template #prefix>@</template>
 			</MkInput>
 
 			<MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required>
-				<template #label>{{ $ts.emailAddress }}</template>
-				<template #caption>{{ $ts._forgotPassword.enterEmail }}</template>
+				<template #label>{{ i18n.locale.emailAddress }}</template>
+				<template #caption>{{ i18n.locale._forgotPassword.enterEmail }}</template>
 			</MkInput>
 
-			<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ $ts.send }}</MkButton>
+			<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.locale.send }}</MkButton>
 		</div>
 		<div class="sub">
-			<MkA to="/about" class="_link">{{ $ts._forgotPassword.ifNoEmail }}</MkA>
+			<MkA to="/about" class="_link">{{ i18n.locale._forgotPassword.ifNoEmail }}</MkA>
 		</div>
 	</form>
-	<div v-else>
-		{{ $ts._forgotPassword.contactAdmin }}
+	<div v-else class="bafecedb">
+		{{ i18n.locale._forgotPassword.contactAdmin }}
 	</div>
 </XModalWindow>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import XModalWindow from '@/components/ui/modal-window.vue';
 import MkButton from '@/components/ui/button.vue';
 import MkInput from '@/components/form/input.vue';
 import * as os from '@/os';
+import { instance } from '@/instance';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		XModalWindow,
-		MkButton,
-		MkInput,
-	},
+const emit = defineEmits<{
+	(e: 'done'): void;
+	(e: 'closed'): void;
+}>();
 
-	emits: ['done', 'closed'],
+let dialog: InstanceType<typeof XModalWindow> = $ref();
 
-	data() {
-		return {
-			username: '',
-			email: '',
-			processing: false,
-		};
-	},
+let username = $ref('');
+let email = $ref('');
+let processing = $ref(false);
 
-	methods: {
-		async onSubmit() {
-			this.processing = true;
-			await os.apiWithDialog('request-reset-password', {
-				username: this.username,
-				email: this.email,
-			});
-
-			this.$emit('done');
-			this.$refs.dialog.close();
-		}
-	}
-});
+async function onSubmit() {
+	processing = true;
+	await os.apiWithDialog('request-reset-password', {
+		username,
+		email,
+	});
+	emit('done');
+	dialog.close();
+}
 </script>
 
 <style lang="scss" scoped>
@@ -81,4 +73,8 @@ export default defineComponent({
 		padding: 24px;
 	}
 }
+
+.bafecedb {
+	padding: 24px;
+}
 </style>
diff --git a/packages/client/src/components/form/input.vue b/packages/client/src/components/form/input.vue
index 3533f4f27b..7165671af3 100644
--- a/packages/client/src/components/form/input.vue
+++ b/packages/client/src/components/form/input.vue
@@ -167,7 +167,7 @@ export default defineComponent({
 
 				// このコンポーネントが作成された時、非表示状態である場合がある
 				// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
-				const clock = setInterval(() => {
+				const clock = window.setInterval(() => {
 					if (prefixEl.value) {
 						if (prefixEl.value.offsetWidth) {
 							inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
@@ -181,7 +181,7 @@ export default defineComponent({
 				}, 100);
 
 				onUnmounted(() => {
-					clearInterval(clock);
+					window.clearInterval(clock);
 				});
 			});
 		});
diff --git a/packages/client/src/components/form/pagination.vue b/packages/client/src/components/form/pagination.vue
deleted file mode 100644
index 3d3b40a783..0000000000
--- a/packages/client/src/components/form/pagination.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<template>
-<FormSlot>
-	<template #label><slot name="label"></slot></template>
-	<div class="abcaccfa">
-		<slot :items="items"></slot>
-		<div v-if="empty" key="_empty_" class="empty">
-			<slot name="empty"></slot>
-		</div>
-		<MkButton v-show="more" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore">
-			<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
-			<template v-if="moreFetching"><MkLoading inline/></template>
-		</MkButton>
-	</div>
-</FormSlot>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@/components/ui/button.vue';
-import FormSlot from './slot.vue';
-import paging from '@/scripts/paging';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		FormSlot,
-	},
-
-	mixins: [
-		paging({}),
-	],
-
-	props: {
-		pagination: {
-			required: true
-		},
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.abcaccfa {
-}
-</style>
diff --git a/packages/client/src/components/form/select.vue b/packages/client/src/components/form/select.vue
index afc53ca9c8..87196027a8 100644
--- a/packages/client/src/components/form/select.vue
+++ b/packages/client/src/components/form/select.vue
@@ -117,7 +117,7 @@ export default defineComponent({
 
 				// このコンポーネントが作成された時、非表示状態である場合がある
 				// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
-				const clock = setInterval(() => {
+				const clock = window.setInterval(() => {
 					if (prefixEl.value) {
 						if (prefixEl.value.offsetWidth) {
 							inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
@@ -131,7 +131,7 @@ export default defineComponent({
 				}, 100);
 
 				onUnmounted(() => {
-					clearInterval(clock);
+					window.clearInterval(clock);
 				});
 			});
 		});
diff --git a/packages/client/src/components/global/a.vue b/packages/client/src/components/global/a.vue
index 77ee7525a4..cf7385ca22 100644
--- a/packages/client/src/components/global/a.vue
+++ b/packages/client/src/components/global/a.vue
@@ -4,130 +4,114 @@
 </a>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { inject } from 'vue';
 import * as os from '@/os';
 import copyToClipboard from '@/scripts/copy-to-clipboard';
 import { router } from '@/router';
 import { url } from '@/config';
-import { popout } from '@/scripts/popout';
-import { ColdDeviceStorage } from '@/store';
+import { popout as popout_ } from '@/scripts/popout';
+import { i18n } from '@/i18n';
+import { defaultStore } from '@/store';
 
-export default defineComponent({
-	inject: {
-		navHook: {
-			default: null
-		},
-		sideViewHook: {
-			default: null
+const props = withDefaults(defineProps<{
+	to: string;
+	activeClass?: null | string;
+	behavior?: null | 'window' | 'browser' | 'modalWindow';
+}>(), {
+	activeClass: null,
+	behavior: null,
+});
+
+const navHook = inject('navHook', null);
+const sideViewHook = inject('sideViewHook', null);
+
+const active = $computed(() => {
+	if (props.activeClass == null) return false;
+	const resolved = router.resolve(props.to);
+	if (resolved.path === router.currentRoute.value.path) return true;
+	if (resolved.name == null) return false;
+	if (router.currentRoute.value.name == null) return false;
+	return resolved.name === router.currentRoute.value.name;
+});
+
+function onContextmenu(ev) {
+	const selection = window.getSelection();
+	if (selection && selection.toString() !== '') return;
+	os.contextMenu([{
+		type: 'label',
+		text: props.to,
+	}, {
+		icon: 'fas fa-window-maximize',
+		text: i18n.locale.openInWindow,
+		action: () => {
+			os.pageWindow(props.to);
 		}
-	},
-
-	props: {
-		to: {
-			type: String,
-			required: true,
-		},
-		activeClass: {
-			type: String,
-			required: false,
-		},
-		behavior: {
-			type: String,
-			required: false,
-		},
-	},
-
-	computed: {
-		active() {
-			if (this.activeClass == null) return false;
-			const resolved = router.resolve(this.to);
-			if (resolved.path == this.$route.path) return true;
-			if (resolved.name == null) return false;
-			if (this.$route.name == null) return false;
-			return resolved.name == this.$route.name;
+	}, sideViewHook ? {
+		icon: 'fas fa-columns',
+		text: i18n.locale.openInSideView,
+		action: () => {
+			sideViewHook(props.to);
 		}
-	},
+	} : undefined, {
+		icon: 'fas fa-expand-alt',
+		text: i18n.locale.showInPage,
+		action: () => {
+			router.push(props.to);
+		}
+	}, null, {
+		icon: 'fas fa-external-link-alt',
+		text: i18n.locale.openInNewTab,
+		action: () => {
+			window.open(props.to, '_blank');
+		}
+	}, {
+		icon: 'fas fa-link',
+		text: i18n.locale.copyLink,
+		action: () => {
+			copyToClipboard(`${url}${props.to}`);
+		}
+	}], ev);
+}
 
-	methods: {
-		onContextmenu(e) {
-			if (window.getSelection().toString() !== '') return;
-			os.contextMenu([{
-				type: 'label',
-				text: this.to,
-			}, {
-				icon: 'fas fa-window-maximize',
-				text: this.$ts.openInWindow,
-				action: () => {
-					os.pageWindow(this.to);
-				}
-			}, this.sideViewHook ? {
-				icon: 'fas fa-columns',
-				text: this.$ts.openInSideView,
-				action: () => {
-					this.sideViewHook(this.to);
-				}
-			} : undefined, {
-				icon: 'fas fa-expand-alt',
-				text: this.$ts.showInPage,
-				action: () => {
-					this.$router.push(this.to);
-				}
-			}, null, {
-				icon: 'fas fa-external-link-alt',
-				text: this.$ts.openInNewTab,
-				action: () => {
-					window.open(this.to, '_blank');
-				}
-			}, {
-				icon: 'fas fa-link',
-				text: this.$ts.copyLink,
-				action: () => {
-					copyToClipboard(`${url}${this.to}`);
-				}
-			}], e);
-		},
+function openWindow() {
+	os.pageWindow(props.to);
+}
 
-		window() {
-			os.pageWindow(this.to);
-		},
+function modalWindow() {
+	os.modalPageWindow(props.to);
+}
 
-		modalWindow() {
-			os.modalPageWindow(this.to);
-		},
+function popout() {
+	popout_(props.to);
+}
 
-		popout() {
-			popout(this.to);
-		},
+function nav() {
+	if (props.behavior === 'browser') {
+		location.href = props.to;
+		return;
+	}
 
-		nav() {
-			if (this.behavior === 'browser') {
-				location.href = this.to;
-				return;
-			}
-
-			if (this.behavior) {
-				if (this.behavior === 'window') {
-					return this.window();
-				} else if (this.behavior === 'modalWindow') {
-					return this.modalWindow();
-				}
-			}
-
-			if (this.navHook) {
-				this.navHook(this.to);
-			} else {
-				if (this.$store.state.defaultSideView && this.sideViewHook && this.to !== '/') {
-					return this.sideViewHook(this.to);
-				}
-
-				if (this.$router.currentRoute.value.path === this.to) {
-					window.scroll({ top: 0, behavior: 'smooth' });
-				} else {
-					this.$router.push(this.to);
-				}
-			}
+	if (props.behavior) {
+		if (props.behavior === 'window') {
+			return openWindow();
+		} else if (props.behavior === 'modalWindow') {
+			return modalWindow();
 		}
 	}
-});
+
+	if (navHook) {
+		navHook(props.to);
+	} else {
+		if (defaultStore.state.defaultSideView && sideViewHook && props.to !== '/') {
+			return sideViewHook(props.to);
+		}
+
+		if (router.currentRoute.value.path === props.to) {
+			window.scroll({ top: 0, behavior: 'smooth' });
+		} else {
+			router.push(props.to);
+		}
+	}
+}
 </script>
diff --git a/packages/client/src/components/global/ad.vue b/packages/client/src/components/global/ad.vue
index 49046b00a7..180dabb2a2 100644
--- a/packages/client/src/components/global/ad.vue
+++ b/packages/client/src/components/global/ad.vue
@@ -20,7 +20,7 @@
 
 <script lang="ts">
 import { defineComponent, ref } from 'vue';
-import { Instance, instance } from '@/instance';
+import { instance } from '@/instance';
 import { host } from '@/config';
 import MkButton from '@/components/ui/button.vue';
 import { defaultStore } from '@/store';
@@ -48,9 +48,9 @@ export default defineComponent({
 			showMenu.value = !showMenu.value;
 		};
 
-		const choseAd = (): Instance['ads'][number] | null => {
+		const choseAd = (): (typeof instance)['ads'][number] | null => {
 			if (props.specify) {
-				return props.specify as Instance['ads'][number];
+				return props.specify as (typeof instance)['ads'][number];
 			}
 
 			const allAds = instance.ads.map(ad => defaultStore.state.mutedAds.includes(ad.id) ? {
diff --git a/packages/client/src/components/global/avatar.vue b/packages/client/src/components/global/avatar.vue
index 300e5e079f..27cfb6e4d4 100644
--- a/packages/client/src/components/global/avatar.vue
+++ b/packages/client/src/components/global/avatar.vue
@@ -1,74 +1,54 @@
 <template>
-<span v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat, square: $store.state.squareAvatars }" :title="acct(user)" @click="onClick">
+<span v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: $store.state.squareAvatars }" :style="{ color }" :title="acct(user)" @click="onClick">
 	<img class="inner" :src="url" decoding="async"/>
 	<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
 </span>
-<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat, square: $store.state.squareAvatars }" :to="userPage(user)" :title="acct(user)" :target="target">
+<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: $store.state.squareAvatars }" :style="{ color }" :to="userPage(user)" :title="acct(user)" :target="target">
 	<img class="inner" :src="url" decoding="async"/>
 	<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
 </MkA>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, watch } from 'vue';
+import * as misskey from 'misskey-js';
 import { getStaticImageUrl } from '@/scripts/get-static-image-url';
 import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
 import { acct, userPage } from '@/filters/user';
 import MkUserOnlineIndicator from '@/components/user-online-indicator.vue';
+import { defaultStore } from '@/store';
 
-export default defineComponent({
-	components: {
-		MkUserOnlineIndicator
-	},
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-		target: {
-			required: false,
-			default: null
-		},
-		disableLink: {
-			required: false,
-			default: false
-		},
-		disablePreview: {
-			required: false,
-			default: false
-		},
-		showIndicator: {
-			required: false,
-			default: false
-		}
-	},
-	emits: ['click'],
-	computed: {
-		cat(): boolean {
-			return this.user.isCat;
-		},
-		url(): string {
-			return this.$store.state.disableShowingAnimatedImages
-				? getStaticImageUrl(this.user.avatarUrl)
-				: this.user.avatarUrl;
-		},
-	},
-	watch: {
-		'user.avatarBlurhash'() {
-			if (this.$el == null) return;
-			this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash);
-		}
-	},
-	mounted() {
-		this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash);
-	},
-	methods: {
-		onClick(e) {
-			this.$emit('click', e);
-		},
-		acct,
-		userPage
-	}
+const props = withDefaults(defineProps<{
+	user: misskey.entities.User;
+	target?: string | null;
+	disableLink?: boolean;
+	disablePreview?: boolean;
+	showIndicator?: boolean;
+}>(), {
+	target: null,
+	disableLink: false,
+	disablePreview: false,
+	showIndicator: false,
+});
+
+const emit = defineEmits<{
+	(e: 'click', ev: MouseEvent): void;
+}>();
+
+const url = $computed(() => defaultStore.state.disableShowingAnimatedImages
+	? getStaticImageUrl(props.user.avatarUrl)
+	: props.user.avatarUrl);
+
+function onClick(ev: MouseEvent) {
+	emit('click', ev);
+}
+
+let color = $ref();
+
+watch(() => props.user.avatarBlurhash, () => {
+	color = extractAvgColorFromBlurhash(props.user.avatarBlurhash);
+}, {
+	immediate: true,
 });
 </script>
 
diff --git a/packages/client/src/components/global/loading.vue b/packages/client/src/components/global/loading.vue
index 7bde53c12e..43ea1395ed 100644
--- a/packages/client/src/components/global/loading.vue
+++ b/packages/client/src/components/global/loading.vue
@@ -4,27 +4,17 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 
-export default defineComponent({
-	props: {
-		inline: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		colored: {
-			type: Boolean,
-			required: false,
-			default: true
-		},
-		mini: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	}
+const props = withDefaults(defineProps<{
+	inline?: boolean;
+	colored?: boolean;
+	mini?: boolean;
+}>(), {
+	inline: false,
+	colored: true,
+	mini: false,
 });
 </script>
 
diff --git a/packages/client/src/components/global/misskey-flavored-markdown.vue b/packages/client/src/components/global/misskey-flavored-markdown.vue
index ab20404909..243d8614ba 100644
--- a/packages/client/src/components/global/misskey-flavored-markdown.vue
+++ b/packages/client/src/components/global/misskey-flavored-markdown.vue
@@ -1,15 +1,23 @@
 <template>
-<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }"/>
+<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :customEmojis="customEmojis" :isNote="isNote" class="havbbuyv" :class="{ nowrap }"/>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import MfmCore from '@/components/mfm';
 
-export default defineComponent({
-	components: {
-		MfmCore
-	}
+const props = withDefaults(defineProps<{
+	text: string;
+	plain?: boolean;
+	nowrap?: boolean;
+	author?: any;
+	customEmojis?: any;
+	isNote?: boolean;
+}>(), {
+	plain: false,
+	nowrap: false,
+	author: null,
+	isNote: true,
 });
 </script>
 
diff --git a/packages/client/src/components/global/sticky-container.vue b/packages/client/src/components/global/sticky-container.vue
index 859b2c1d73..89d397f082 100644
--- a/packages/client/src/components/global/sticky-container.vue
+++ b/packages/client/src/components/global/sticky-container.vue
@@ -45,7 +45,7 @@ export default defineComponent({
 			calc();
 
 			const observer = new MutationObserver(() => {
-				setTimeout(() => {
+				window.setTimeout(() => {
 					calc();
 				}, 100);
 			});
diff --git a/packages/client/src/components/global/time.vue b/packages/client/src/components/global/time.vue
index 6a330a2307..d2788264c5 100644
--- a/packages/client/src/components/global/time.vue
+++ b/packages/client/src/components/global/time.vue
@@ -1,73 +1,57 @@
 <template>
 <time :title="absolute">
-	<template v-if="mode == 'relative'">{{ relative }}</template>
-	<template v-else-if="mode == 'absolute'">{{ absolute }}</template>
-	<template v-else-if="mode == 'detail'">{{ absolute }} ({{ relative }})</template>
+	<template v-if="mode === 'relative'">{{ relative }}</template>
+	<template v-else-if="mode === 'absolute'">{{ absolute }}</template>
+	<template v-else-if="mode === 'detail'">{{ absolute }} ({{ relative }})</template>
 </time>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onUnmounted } from 'vue';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	props: {
-		time: {
-			type: [Date, String],
-			required: true
-		},
-		mode: {
-			type: String,
-			default: 'relative'
-		}
-	},
-	data() {
-		return {
-			tickId: null,
-			now: new Date()
-		};
-	},
-	computed: {
-		_time(): Date {
-			return typeof this.time == 'string' ? new Date(this.time) : this.time;
-		},
-		absolute(): string {
-			return this._time.toLocaleString();
-		},
-		relative(): string {
-			const time = this._time;
-			const ago = (this.now.getTime() - time.getTime()) / 1000/*ms*/;
-			return (
-				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.$ts._ago.justNow :
-				ago <  -1       ? this.$ts._ago.future :
-				this.$ts._ago.unknown);
-		}
-	},
-	created() {
-		if (this.mode == 'relative' || this.mode == 'detail') {
-			this.tickId = window.requestAnimationFrame(this.tick);
-		}
-	},
-	unmounted() {
-		if (this.mode === 'relative' || this.mode === 'detail') {
-			window.clearTimeout(this.tickId);
-		}
-	},
-	methods: {
-		tick() {
-			// TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する
-			this.now = new Date();
-
-			this.tickId = setTimeout(() => {
-				window.requestAnimationFrame(this.tick);
-			}, 10000);
-		}
-	}
+const props = withDefaults(defineProps<{
+	time: Date | string;
+	mode?: 'relative' | 'absolute' | 'detail';
+}>(), {
+	mode: 'relative',
 });
+
+const _time = typeof props.time == 'string' ? new Date(props.time) : props.time;
+const absolute = _time.toLocaleString();
+
+let now = $ref(new Date());
+const relative = $computed(() => {
+	const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/;
+	return (
+		ago >= 31536000 ? i18n.t('_ago.yearsAgo',   { n: (~~(ago / 31536000)).toString() }) :
+		ago >= 2592000  ? i18n.t('_ago.monthsAgo',  { n: (~~(ago / 2592000)).toString() }) :
+		ago >= 604800   ? i18n.t('_ago.weeksAgo',   { n: (~~(ago / 604800)).toString() }) :
+		ago >= 86400    ? i18n.t('_ago.daysAgo',    { n: (~~(ago / 86400)).toString() }) :
+		ago >= 3600     ? i18n.t('_ago.hoursAgo',   { n: (~~(ago / 3600)).toString() }) :
+		ago >= 60       ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
+		ago >= 10       ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
+		ago >= -1       ? i18n.locale._ago.justNow :
+		ago <  -1       ? i18n.locale._ago.future :
+		i18n.locale._ago.unknown);
+});
+
+function tick() {
+	// TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する
+	now = new Date();
+
+	tickId = window.setTimeout(() => {
+		window.requestAnimationFrame(tick);
+	}, 10000);
+}
+
+let tickId: number;
+
+if (props.mode === 'relative' || props.mode === 'detail') {
+	tickId = window.requestAnimationFrame(tick);
+
+	onUnmounted(() => {
+		window.clearTimeout(tickId);
+	});
+}
 </script>
diff --git a/packages/client/src/components/global/user-name.vue b/packages/client/src/components/global/user-name.vue
index bc93a8ea30..090de3df30 100644
--- a/packages/client/src/components/global/user-name.vue
+++ b/packages/client/src/components/global/user-name.vue
@@ -2,19 +2,14 @@
 <Mfm :text="user.name || user.username" :plain="true" :nowrap="nowrap" :custom-emojis="user.emojis"/>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
 
-export default defineComponent({
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-		nowrap: {
-			type: Boolean,
-			default: true
-		},
-	}
+const props = withDefaults(defineProps<{
+	user: misskey.entities.User;
+	nowrap?: boolean;
+}>(), {
+	nowrap: true,
 });
 </script>
diff --git a/packages/client/src/components/image-viewer.vue b/packages/client/src/components/image-viewer.vue
index 8584b91a61..c39076df16 100644
--- a/packages/client/src/components/image-viewer.vue
+++ b/packages/client/src/components/image-viewer.vue
@@ -1,8 +1,8 @@
 <template>
-<MkModal ref="modal" :z-priority="'middle'" @click="$refs.modal.close()" @closed="$emit('closed')">
+<MkModal ref="modal" :z-priority="'middle'" @click="modal.close()" @closed="emit('closed')">
 	<div class="xubzgfga">
 		<header>{{ image.name }}</header>
-		<img :src="image.url" :alt="image.comment" :title="image.comment" @click="$refs.modal.close()"/>
+		<img :src="image.url" :alt="image.comment" :title="image.comment" @click="modal.close()"/>
 		<footer>
 			<span>{{ image.type }}</span>
 			<span>{{ bytes(image.size) }}</span>
@@ -12,31 +12,23 @@
 </MkModal>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
 import bytes from '@/filters/bytes';
 import number from '@/filters/number';
 import MkModal from '@/components/ui/modal.vue';
 
-export default defineComponent({
-	components: {
-		MkModal,
-	},
-
-	props: {
-		image: {
-			type: Object,
-			required: true
-		},
-	},
-
-	emits: ['closed'],
-
-	methods: {
-		bytes,
-		number,
-	}
+const props = withDefaults(defineProps<{
+	image: misskey.entities.DriveFile;
+}>(), {
 });
+
+const emit = defineEmits<{
+	(e: 'closed'): void;
+}>();
+
+const modal = $ref<InstanceType<typeof MkModal>>();
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/img-with-blurhash.vue b/packages/client/src/components/img-with-blurhash.vue
index a000c699b6..06ad764403 100644
--- a/packages/client/src/components/img-with-blurhash.vue
+++ b/packages/client/src/components/img-with-blurhash.vue
@@ -5,67 +5,43 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted } from 'vue';
 import { decode } from 'blurhash';
 
-export default defineComponent({
-	props: {
-		src: {
-			type: String,
-			required: false,
-			default: null
-		},
-		hash: {
-			type: String,
-			required: true
-		},
-		alt: {
-			type: String,
-			required: false,
-			default: '',
-		},
-		title: {
-			type: String,
-			required: false,
-			default: null,
-		},
-		size: {
-			type: Number,
-			required: false,
-			default: 64
-		},
-		cover: {
-			type: Boolean,
-			required: false,
-			default: true,
-		}
-	},
+const props = withDefaults(defineProps<{
+	src?: string | null;
+	hash: string;
+	alt?: string;
+	title?: string | null;
+	size?: number;
+	cover?: boolean;
+}>(), {
+	src: null,
+	alt: '',
+	title: null,
+	size: 64,
+	cover: true,
+});
 
-	data() {
-		return {
-			loaded: false,
-		};
-	},
+const canvas = $ref<HTMLCanvasElement>();
+let loaded = $ref(false);
 
-	mounted() {
-		this.draw();
-	},
+function draw() {
+	if (props.hash == null) return;
+	const pixels = decode(props.hash, props.size, props.size);
+	const ctx = canvas.getContext('2d');
+	const imageData = ctx!.createImageData(props.size, props.size);
+	imageData.data.set(pixels);
+	ctx!.putImageData(imageData, 0, 0);
+}
 
-	methods: {
-		draw() {
-			if (this.hash == null) return;
-			const pixels = decode(this.hash, this.size, this.size);
-			const ctx = (this.$refs.canvas as HTMLCanvasElement).getContext('2d');
-			const imageData = ctx!.createImageData(this.size, this.size);
-			imageData.data.set(pixels);
-			ctx!.putImageData(imageData, 0, 0);
-		},
+function onLoad() {
+	loaded = true;
+}
 
-		onLoad() {
-			this.loaded = true;
-		}
-	}
+onMounted(() => {
+	draw();
 });
 </script>
 
diff --git a/packages/client/src/components/instance-ticker.vue b/packages/client/src/components/instance-ticker.vue
index 1ce5a1c2c1..77fd8bb344 100644
--- a/packages/client/src/components/instance-ticker.vue
+++ b/packages/client/src/components/instance-ticker.vue
@@ -1,41 +1,22 @@
 <template>
 <div class="hpaizdrt" :style="bg">
-	<img v-if="info.faviconUrl" class="icon" :src="info.faviconUrl"/>
-	<span class="name">{{ info.name }}</span>
+	<img v-if="instance.faviconUrl" class="icon" :src="instance.faviconUrl"/>
+	<span class="name">{{ instance.name }}</span>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { instanceName } from '@/config';
+<script lang="ts" setup>
+import { } from 'vue';
 
-export default defineComponent({
-	props: {
-		instance: {
-			type: Object,
-			required: false
-		},
-	},
+const props = defineProps<{
+	instance: any; // TODO
+}>();
 
-	data() {
-		return {
-			info: this.instance || {
-				faviconUrl: '/favicon.ico',
-				name: instanceName,
-				themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content
-			}
-		}
-	},
+const themeColor = props.instance.themeColor || '#777777';
 
-	computed: {
-		bg(): any {
-			const themeColor = this.info.themeColor || '#777777';
-			return {
-				background: `linear-gradient(90deg, ${themeColor}, ${themeColor + '00'})`
-			};
-		}
-	}
-});
+const bg = {
+	background: `linear-gradient(90deg, ${themeColor}, ${themeColor + '00'})`
+};
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/link.vue b/packages/client/src/components/link.vue
index 8b8cde6510..317c931cec 100644
--- a/packages/client/src/components/link.vue
+++ b/packages/client/src/components/link.vue
@@ -1,82 +1,36 @@
 <template>
-<component :is="self ? 'MkA' : 'a'" class="xlcxczvw _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
+<component :is="self ? 'MkA' : 'a'" ref="el" class="xlcxczvw _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
 	:title="url"
-	@mouseover="onMouseover"
-	@mouseleave="onMouseleave"
 >
 	<slot></slot>
 	<i v-if="target === '_blank'" class="fas fa-external-link-square-alt icon"></i>
 </component>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import { url as local } from '@/config';
-import { isTouchUsing } from '@/scripts/touch';
+import { useTooltip } from '@/scripts/use-tooltip';
 import * as os from '@/os';
 
-export default defineComponent({
-	props: {
-		url: {
-			type: String,
-			required: true,
-		},
-		rel: {
-			type: String,
-			required: false,
-		}
-	},
-	data() {
-		const self = this.url.startsWith(local);
-		return {
-			local,
-			self: self,
-			attr: self ? 'to' : 'href',
-			target: self ? null : '_blank',
-			showTimer: null,
-			hideTimer: null,
-			checkTimer: null,
-			close: null,
-		};
-	},
-	methods: {
-		async showPreview() {
-			if (!document.body.contains(this.$el)) return;
-			if (this.close) return;
+const props = withDefaults(defineProps<{
+	url: string;
+	rel?: null | string;
+}>(), {
+});
 
-			const { dispose } = await os.popup(import('@/components/url-preview-popup.vue'), {
-				url: this.url,
-				source: this.$el
-			});
+const self = props.url.startsWith(local);
+const attr = self ? 'to' : 'href';
+const target = self ? null : '_blank';
 
-			this.close = () => {
-				dispose();
-			};
+const el = $ref();
 
-			this.checkTimer = setInterval(() => {
-				if (!document.body.contains(this.$el)) this.closePreview();
-			}, 1000);
-		},
-		closePreview() {
-			if (this.close) {
-				clearInterval(this.checkTimer);
-				this.close();
-				this.close = null;
-			}
-		},
-		onMouseover() {
-			if (isTouchUsing) return;
-			clearTimeout(this.showTimer);
-			clearTimeout(this.hideTimer);
-			this.showTimer = setTimeout(this.showPreview, 500);
-		},
-		onMouseleave() {
-			if (isTouchUsing) return;
-			clearTimeout(this.showTimer);
-			clearTimeout(this.hideTimer);
-			this.hideTimer = setTimeout(this.closePreview, 500);
-		}
-	}
+useTooltip($$(el), (showing) => {
+	os.popup(import('@/components/url-preview-popup.vue'), {
+		showing,
+		url: props.url,
+		source: el,
+	}, {}, 'closed');
 });
 </script>
 
diff --git a/packages/client/src/components/media-banner.vue b/packages/client/src/components/media-banner.vue
index 9dbfe3d0c6..5093f11e97 100644
--- a/packages/client/src/components/media-banner.vue
+++ b/packages/client/src/components/media-banner.vue
@@ -6,7 +6,7 @@
 		<span>{{ $ts.clickToShow }}</span>
 	</div>
 	<div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" class="audio">
-		<audio ref="audio"
+		<audio ref="audioEl"
 			class="audio"
 			:src="media.url"
 			:title="media.name"
@@ -25,34 +25,26 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@/os';
+<script lang="ts" setup>
+import { onMounted } from 'vue';
+import * as misskey from 'misskey-js';
 import { ColdDeviceStorage } from '@/store';
 
-export default defineComponent({
-	props: {
-		media: {
-			type: Object,
-			required: true
-		}
-	},
-	data() {
-		return {
-			hide: true,
-		};
-	},
-	mounted() {
-		const audioTag = this.$refs.audio as HTMLAudioElement;
-		if (audioTag) audioTag.volume = ColdDeviceStorage.get('mediaVolume');
-	},
-	methods: {
-		volumechange() {
-			const audioTag = this.$refs.audio as HTMLAudioElement;
-			ColdDeviceStorage.set('mediaVolume', audioTag.volume);
-		},
-	},
-})
+const props = withDefaults(defineProps<{
+	media: misskey.entities.DriveFile;
+}>(), {
+});
+
+const audioEl = $ref<HTMLAudioElement | null>();
+let hide = $ref(true);
+
+function volumechange() {
+	if (audioEl) ColdDeviceStorage.set('mediaVolume', audioEl.volume);
+}
+
+onMounted(() => {
+	if (audioEl) audioEl.volume = ColdDeviceStorage.get('mediaVolume');
+});
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/mini-chart.vue b/packages/client/src/components/mini-chart.vue
index 2eb9ae8cbe..8c74eae876 100644
--- a/packages/client/src/components/mini-chart.vue
+++ b/packages/client/src/components/mini-chart.vue
@@ -63,10 +63,10 @@ export default defineComponent({
 		this.draw();
 
 		// Vueが何故かWatchを発動させない場合があるので
-		this.clock = setInterval(this.draw, 1000);
+		this.clock = window.setInterval(this.draw, 1000);
 	},
 	beforeUnmount() {
-		clearInterval(this.clock);
+		window.clearInterval(this.clock);
 	},
 	methods: {
 		draw() {
diff --git a/packages/client/src/components/modal-page-window.vue b/packages/client/src/components/modal-page-window.vue
index 3de1980820..2e17d5d030 100644
--- a/packages/client/src/components/modal-page-window.vue
+++ b/packages/client/src/components/modal-page-window.vue
@@ -153,8 +153,8 @@ export default defineComponent({
 			this.$refs.window.close();
 		},
 
-		onContextmenu(e) {
-			os.contextMenu(this.contextmenu, e);
+		onContextmenu(ev: MouseEvent) {
+			os.contextMenu(this.contextmenu, ev);
 		}
 	},
 });
diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue
index a5cb2f0426..a3b30f726e 100644
--- a/packages/client/src/components/note-detailed.vue
+++ b/packages/client/src/components/note-detailed.vue
@@ -4,12 +4,13 @@
 	v-show="!isDeleted"
 	v-hotkey="keymap"
 	v-size="{ max: [500, 450, 350, 300] }"
+	ref="el"
 	class="lxwezrsl _block"
 	:tabindex="!isDeleted ? '-1' : null"
 	:class="{ renote: isRenote }"
 >
-	<XSub v-for="note in conversation" :key="note.id" class="reply-to-more" :note="note"/>
-	<XSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
+	<MkNoteSub v-for="note in conversation" :key="note.id" class="reply-to-more" :note="note"/>
+	<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
 	<div v-if="isRenote" class="renote">
 		<MkAvatar class="avatar" :user="note.user"/>
 		<i class="fas fa-retweet"></i>
@@ -107,7 +108,7 @@
 			</footer>
 		</div>
 	</article>
-	<XSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
+	<MkNoteSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
 </div>
 <div v-else class="_panel muted" @click="muted = false">
 	<I18n :src="$ts.userSaysSomething" tag="small">
@@ -120,765 +121,171 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
 import * as mfm from 'mfm-js';
-import { sum } from '@/scripts/array';
-import XSub from './note.sub.vue';
-import XNoteHeader from './note-header.vue';
+import * as misskey from 'misskey-js';
+import MkNoteSub from './MkNoteSub.vue';
 import XNoteSimple from './note-simple.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 XRenoteButton from './renote-button.vue';
+import MkUrlPreview from '@/components/url-preview.vue';
+import MkInstanceTicker from '@/components/instance-ticker.vue';
 import { pleaseLogin } from '@/scripts/please-login';
-import { focusPrev, focusNext } from '@/scripts/focus';
-import { url } from '@/config';
-import copyToClipboard from '@/scripts/copy-to-clipboard';
 import { checkWordMute } from '@/scripts/check-word-mute';
 import { userPage } from '@/filters/user';
 import { notePage } from '@/filters/note';
 import * as os from '@/os';
-import { stream } from '@/stream';
-import { noteActions, noteViewInterruptors } from '@/store';
+import { defaultStore, noteViewInterruptors } from '@/store';
 import { reactionPicker } from '@/scripts/reaction-picker';
 import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
-
-// TODO: note.vueとほぼ同じなので共通化したい
-export default defineComponent({
-	components: {
-		XSub,
-		XNoteHeader,
-		XNoteSimple,
-		XReactionsViewer,
-		XMediaList,
-		XCwButton,
-		XPoll,
-		XRenoteButton,
-		MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
-		MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')),
-	},
-
-	inject: {
-		inChannel: {
-			default: null
-		},
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-	},
-
-	emits: ['update:note'],
-
-	data() {
-		return {
-			connection: null,
-			conversation: [],
-			replies: [],
-			showContent: false,
-			isDeleted: false,
-			muted: false,
-			translation: null,
-			translating: false,
-			notePage,
-		};
-	},
-
-	computed: {
-		rs() {
-			return this.$store.state.reactions;
-		},
-		keymap(): any {
-			return {
-				'r': () => this.reply(true),
-				'e|a|plus': () => this.react(true),
-				'q': () => this.$refs.renoteButton.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.rs[0]),
-				'2': () => this.reactDirectly(this.rs[1]),
-				'3': () => this.reactDirectly(this.rs[2]),
-				'4': () => this.reactDirectly(this.rs[3]),
-				'5': () => this.reactDirectly(this.rs[4]),
-				'6': () => this.reactDirectly(this.rs[5]),
-				'7': () => this.reactDirectly(this.rs[6]),
-				'8': () => this.reactDirectly(this.rs[7]),
-				'9': () => this.reactDirectly(this.rs[8]),
-				'0': () => this.reactDirectly(this.rs[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.$i && (this.$i.id === this.appearNote.userId);
-		},
-
-		isMyRenote(): boolean {
-			return this.$i && (this.$i.id === this.note.userId);
-		},
-
-		reactionsCount(): number {
-			return this.appearNote.reactions
-				? sum(Object.values(this.appearNote.reactions))
-				: 0;
-		},
-
-		urls(): string[] {
-			if (this.appearNote.text) {
-				return extractUrlFromMfm(mfm.parse(this.appearNote.text));
-			} else {
-				return null;
-			}
-		},
-
-		showTicker() {
-			if (this.$store.state.instanceTicker === 'always') return true;
-			if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true;
-			return false;
-		}
-	},
-
-	async created() {
-		if (this.$i) {
-			this.connection = stream;
-		}
-
-		this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords);
-
-		// plugin
-		if (noteViewInterruptors.length > 0) {
-			let result = this.note;
-			for (const interruptor of noteViewInterruptors) {
-				result = await interruptor.handler(JSON.parse(JSON.stringify(result)));
-			}
-			this.$emit('update:note', Object.freeze(result));
-		}
-
-		os.api('notes/children', {
-			noteId: this.appearNote.id,
-			limit: 30
-		}).then(replies => {
-			this.replies = replies;
-		});
-
-		if (this.appearNote.replyId) {
-			os.api('notes/conversation', {
-				noteId: this.appearNote.replyId
-			}).then(conversation => {
-				this.conversation = conversation.reverse();
-			});
-		}
-	},
-
-	mounted() {
-		this.capture(true);
-
-		if (this.$i) {
-			this.connection.on('_connected_', this.onStreamConnected);
-		}
-	},
-
-	beforeUnmount() {
-		this.decapture(true);
-
-		if (this.$i) {
-			this.connection.off('_connected_', this.onStreamConnected);
-		}
-	},
-
-	methods: {
-		updateAppearNote(v) {
-			this.$emit('update:note', Object.freeze(this.isRenote ? {
-				...this.note,
-				renote: {
-					...this.note.renote,
-					...v
-				}
-			} : {
-				...this.note,
-				...v
-			}));
-		},
-
-		readPromo() {
-			os.api('promo/read', {
-				noteId: this.appearNote.id
-			});
-			this.isDeleted = true;
-		},
-
-		capture(withHandler = false) {
-			if (this.$i) {
-				// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
-				this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
-				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
-			}
-		},
-
-		decapture(withHandler = false) {
-			if (this.$i) {
-				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;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					if (body.emoji) {
-						const emojis = this.appearNote.emojis || [];
-						if (!emojis.includes(body.emoji)) {
-							n.emojis = [...emojis, body.emoji];
-						}
-					}
-
-					// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
-					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
-					// Increment the count
-					n.reactions = {
-						...this.appearNote.reactions,
-						[reaction]: currentCount + 1
-					};
-
-					if (body.userId === this.$i.id) {
-						n.myReaction = reaction;
-					}
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'unreacted': {
-					const reaction = body.reaction;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
-					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
-					// Decrement the count
-					n.reactions = {
-						...this.appearNote.reactions,
-						[reaction]: Math.max(0, currentCount - 1)
-					};
-
-					if (body.userId === this.$i.id) {
-						n.myReaction = null;
-					}
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'pollVoted': {
-					const choice = body.choice;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					const choices = [...this.appearNote.poll.choices];
-					choices[choice] = {
-						...choices[choice],
-						votes: choices[choice].votes + 1,
-						...(body.userId === this.$i.id ? {
-							isVoted: true
-						} : {})
-					};
-
-					n.poll = {
-						...this.appearNote.poll,
-						choices: choices
-					};
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'deleted': {
-					this.isDeleted = true;
-					break;
-				}
-			}
-		},
-
-		reply(viaKeyboard = false) {
-			pleaseLogin();
-			os.post({
-				reply: this.appearNote,
-				animation: !viaKeyboard,
-			}, () => {
-				this.focus();
-			});
-		},
-
-		renoteDirectly() {
-			os.apiWithDialog('notes/create', {
-				renoteId: this.appearNote.id
-			}, undefined, (res: any) => {
-				os.alert({
-					type: 'success',
-					text: this.$ts.renoted,
-				});
-			}, (e: Error) => {
-				if (e.id === 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4') {
-					os.alert({
-						type: 'error',
-						text: this.$ts.cantRenote,
-					});
-				} else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') {
-					os.alert({
-						type: 'error',
-						text: this.$ts.cantReRenote,
-					});
-				}
-			});
-		},
-
-		react(viaKeyboard = false) {
-			pleaseLogin();
-			this.blur();
-			reactionPicker.show(this.$refs.reactButton, reaction => {
-				os.api('notes/reactions/create', {
-					noteId: this.appearNote.id,
-					reaction: reaction
-				});
-			}, () => {
-				this.focus();
-			});
-		},
-
-		reactDirectly(reaction) {
-			os.api('notes/reactions/create', {
-				noteId: this.appearNote.id,
-				reaction: reaction
-			});
-		},
-
-		undoReact(note) {
-			const oldReaction = note.myReaction;
-			if (!oldReaction) return;
-			os.api('notes/reactions/delete', {
-				noteId: note.id
-			});
-		},
-
-		favorite() {
-			pleaseLogin();
-			os.apiWithDialog('notes/favorites/create', {
-				noteId: this.appearNote.id
-			}, undefined, (res: any) => {
-				os.alert({
-					type: 'success',
-					text: this.$ts.favorited,
-				});
-			}, (e: Error) => {
-				if (e.id === 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6') {
-					os.alert({
-						type: 'error',
-						text: this.$ts.alreadyFavorited,
-					});
-				} else if (e.id === '6dd26674-e060-4816-909a-45ba3f4da458') {
-					os.alert({
-						type: 'error',
-						text: this.$ts.cantFavorite,
-					});
-				}
-			});
-		},
-
-		del() {
-			os.confirm({
-				type: 'warning',
-				text: this.$ts.noteDeleteConfirm,
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				os.api('notes/delete', {
-					noteId: this.appearNote.id
-				});
-			});
-		},
-
-		delEdit() {
-			os.confirm({
-				type: 'warning',
-				text: this.$ts.deleteAndEditConfirm,
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				os.api('notes/delete', {
-					noteId: this.appearNote.id
-				});
-
-				os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel });
-			});
-		},
-
-		toggleFavorite(favorite: boolean) {
-			os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
-				noteId: this.appearNote.id
-			});
-		},
-
-		toggleWatch(watch: boolean) {
-			os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
-				noteId: this.appearNote.id
-			});
-		},
-
-		toggleThreadMute(mute: boolean) {
-			os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
-				noteId: this.appearNote.id
-			});
-		},
-
-		getMenu() {
-			let menu;
-			if (this.$i) {
-				const statePromise = os.api('notes/state', {
-					noteId: this.appearNote.id
-				});
-
-				menu = [{
-					icon: 'fas fa-copy',
-					text: this.$ts.copyContent,
-					action: this.copyContent
-				}, {
-					icon: 'fas fa-link',
-					text: this.$ts.copyLink,
-					action: this.copyLink
-				}, (this.appearNote.url || this.appearNote.uri) ? {
-					icon: 'fas fa-external-link-square-alt',
-					text: this.$ts.showOnRemote,
-					action: () => {
-						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
-					}
-				} : undefined,
-				{
-					icon: 'fas fa-share-alt',
-					text: this.$ts.share,
-					action: this.share
-				},
-				this.$instance.translatorAvailable ? {
-					icon: 'fas fa-language',
-					text: this.$ts.translate,
-					action: this.translate
-				} : undefined,
-				null,
-				statePromise.then(state => state.isFavorited ? {
-					icon: 'fas fa-star',
-					text: this.$ts.unfavorite,
-					action: () => this.toggleFavorite(false)
-				} : {
-					icon: 'fas fa-star',
-					text: this.$ts.favorite,
-					action: () => this.toggleFavorite(true)
-				}),
-				{
-					icon: 'fas fa-paperclip',
-					text: this.$ts.clip,
-					action: () => this.clip()
-				},
-				(this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? {
-					icon: 'fas fa-eye-slash',
-					text: this.$ts.unwatch,
-					action: () => this.toggleWatch(false)
-				} : {
-					icon: 'fas fa-eye',
-					text: this.$ts.watch,
-					action: () => this.toggleWatch(true)
-				}) : undefined,
-				statePromise.then(state => state.isMutedThread ? {
-					icon: 'fas fa-comment-slash',
-					text: this.$ts.unmuteThread,
-					action: () => this.toggleThreadMute(false)
-				} : {
-					icon: 'fas fa-comment-slash',
-					text: this.$ts.muteThread,
-					action: () => this.toggleThreadMute(true)
-				}),
-				this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
-					icon: 'fas fa-thumbtack',
-					text: this.$ts.unpin,
-					action: () => this.togglePin(false)
-				} : {
-					icon: 'fas fa-thumbtack',
-					text: this.$ts.pin,
-					action: () => this.togglePin(true)
-				} : undefined,
-				/*...(this.$i.isModerator || this.$i.isAdmin ? [
-					null,
-					{
-						icon: 'fas fa-bullhorn',
-						text: this.$ts.promote,
-						action: this.promote
-					}]
-					: []
-				),*/
-				...(this.appearNote.userId != this.$i.id ? [
-					null,
-					{
-						icon: 'fas fa-exclamation-circle',
-						text: this.$ts.reportAbuse,
-						action: () => {
-							const u = `${url}/notes/${this.appearNote.id}`;
-							os.popup(import('@/components/abuse-report-window.vue'), {
-								user: this.appearNote.user,
-								initialComment: `Note: ${u}\n-----\n`
-							}, {}, 'closed');
-						}
-					}]
-					: []
-				),
-				...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [
-					null,
-					this.appearNote.userId == this.$i.id ? {
-						icon: 'fas fa-edit',
-						text: this.$ts.deleteAndEdit,
-						action: this.delEdit
-					} : undefined,
-					{
-						icon: 'fas fa-trash-alt',
-						text: this.$ts.delete,
-						danger: true,
-						action: this.del
-					}]
-					: []
-				)]
-				.filter(x => x !== undefined);
-			} else {
-				menu = [{
-					icon: 'fas fa-copy',
-					text: this.$ts.copyContent,
-					action: this.copyContent
-				}, {
-					icon: 'fas fa-link',
-					text: this.$ts.copyLink,
-					action: this.copyLink
-				}, (this.appearNote.url || this.appearNote.uri) ? {
-					icon: 'fas fa-external-link-square-alt',
-					text: this.$ts.showOnRemote,
-					action: () => {
-						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
-					}
-				} : undefined]
-				.filter(x => x !== undefined);
-			}
-
-			if (noteActions.length > 0) {
-				menu = menu.concat([null, ...noteActions.map(action => ({
-					icon: 'fas fa-plug',
-					text: action.title,
-					action: () => {
-						action.handler(this.appearNote);
-					}
-				}))]);
-			}
-
-			return menu;
-		},
-
-		onContextmenu(e) {
-			const isLink = (el: HTMLElement) => {
-				if (el.tagName === 'A') return true;
-				if (el.parentElement) {
-					return isLink(el.parentElement);
-				}
-			};
-			if (isLink(e.target)) return;
-			if (window.getSelection().toString() !== '') return;
-
-			if (this.$store.state.useReactionPickerForContextMenu) {
-				e.preventDefault();
-				this.react();
-			} else {
-				os.contextMenu(this.getMenu(), e).then(this.focus);
-			}
-		},
-
-		menu(viaKeyboard = false) {
-			os.popupMenu(this.getMenu(), this.$refs.menuButton, {
-				viaKeyboard
-			}).then(this.focus);
-		},
-
-		showRenoteMenu(viaKeyboard = false) {
-			if (!this.isMyRenote) return;
-			os.popupMenu([{
-				text: this.$ts.unrenote,
-				icon: 'fas fa-trash-alt',
-				danger: true,
-				action: () => {
-					os.api('notes/delete', {
-						noteId: this.note.id
-					});
-					this.isDeleted = true;
-				}
-			}], this.$refs.renoteTime, {
-				viaKeyboard: viaKeyboard
-			});
-		},
-
-		toggleShowContent() {
-			this.showContent = !this.showContent;
-		},
-
-		copyContent() {
-			copyToClipboard(this.appearNote.text);
-			os.success();
-		},
-
-		copyLink() {
-			copyToClipboard(`${url}/notes/${this.appearNote.id}`);
-			os.success();
-		},
-
-		togglePin(pin: boolean) {
-			os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
-				noteId: this.appearNote.id
-			}, undefined, null, e => {
-				if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
-					os.alert({
-						type: 'error',
-						text: this.$ts.pinLimitExceeded
-					});
-				}
-			});
-		},
-
-		async clip() {
-			const clips = await os.api('clips/list');
-			os.popupMenu([{
-				icon: 'fas fa-plus',
-				text: this.$ts.createNew,
-				action: async () => {
-					const { canceled, result } = await os.form(this.$ts.createNewClip, {
-						name: {
-							type: 'string',
-							label: this.$ts.name
-						},
-						description: {
-							type: 'string',
-							required: false,
-							multiline: true,
-							label: this.$ts.description
-						},
-						isPublic: {
-							type: 'boolean',
-							label: this.$ts.public,
-							default: false
-						}
-					});
-					if (canceled) return;
-
-					const clip = await os.apiWithDialog('clips/create', result);
-
-					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
-				}
-			}, null, ...clips.map(clip => ({
-				text: clip.name,
-				action: () => {
-					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
-				}
-			}))], this.$refs.menuButton, {
-			}).then(this.focus);
-		},
-
-		async promote() {
-			const { canceled, result: days } = await os.inputNumber({
-				title: this.$ts.numberOfDays,
-			});
-
-			if (canceled) return;
-
-			os.apiWithDialog('admin/promo/create', {
-				noteId: this.appearNote.id,
-				expiresAt: Date.now() + (86400000 * days)
-			});
-		},
-
-		share() {
-			navigator.share({
-				title: this.$t('noteOf', { user: this.appearNote.user.name }),
-				text: this.appearNote.text,
-				url: `${url}/notes/${this.appearNote.id}`
-			});
-		},
-
-		async translate() {
-			if (this.translation != null) return;
-			this.translating = true;
-			const res = await os.api('notes/translate', {
-				noteId: this.appearNote.id,
-				targetLang: localStorage.getItem('lang') || navigator.language,
-			});
-			this.translating = false;
-			this.translation = res;
-		},
-
-		focus() {
-			this.$el.focus();
-		},
-
-		blur() {
-			this.$el.blur();
-		},
-
-		focusBefore() {
-			focusPrev(this.$el);
-		},
-
-		focusAfter() {
-			focusNext(this.$el);
-		},
-
-		userPage
-	}
+import { $i } from '@/account';
+import { i18n } from '@/i18n';
+import { getNoteMenu } from '@/scripts/get-note-menu';
+import { useNoteCapture } from '@/scripts/use-note-capture';
+
+const props = defineProps<{
+	note: misskey.entities.Note;
+	pinned?: boolean;
+}>();
+
+const inChannel = inject('inChannel', null);
+
+const isRenote = (
+	props.note.renote != null &&
+	props.note.text == null &&
+	props.note.fileIds.length === 0 &&
+	props.note.poll == null
+);
+
+const el = ref<HTMLElement>();
+const menuButton = ref<HTMLElement>();
+const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
+const renoteTime = ref<HTMLElement>();
+const reactButton = ref<HTMLElement>();
+let appearNote = $ref(isRenote ? props.note.renote as misskey.entities.Note : props.note);
+const isMyRenote = $i && ($i.id === props.note.userId);
+const showContent = ref(false);
+const isDeleted = ref(false);
+const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
+const translation = ref(null);
+const translating = ref(false);
+const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
+const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
+const conversation = ref<misskey.entities.Note[]>([]);
+const replies = ref<misskey.entities.Note[]>([]);
+
+const keymap = {
+	'r': () => reply(true),
+	'e|a|plus': () => react(true),
+	'q': () => renoteButton.value.renote(true),
+	'esc': blur,
+	'm|o': () => menu(true),
+	's': () => showContent.value != showContent.value,
+};
+
+useNoteCapture({
+	appearNote: $$(appearNote),
+	rootEl: el,
 });
+
+function reply(viaKeyboard = false): void {
+	pleaseLogin();
+	os.post({
+		reply: appearNote,
+		animation: !viaKeyboard,
+	}, () => {
+		focus();
+	});
+}
+
+function react(viaKeyboard = false): void {
+	pleaseLogin();
+	blur();
+	reactionPicker.show(reactButton.value, reaction => {
+		os.api('notes/reactions/create', {
+			noteId: appearNote.id,
+			reaction: reaction
+		});
+	}, () => {
+		focus();
+	});
+}
+
+function undoReact(note): void {
+	const oldReaction = note.myReaction;
+	if (!oldReaction) return;
+	os.api('notes/reactions/delete', {
+		noteId: note.id
+	});
+}
+
+function onContextmenu(ev: MouseEvent): void {
+	const isLink = (el: HTMLElement) => {
+		if (el.tagName === 'A') return true;
+		if (el.parentElement) {
+			return isLink(el.parentElement);
+		}
+	};
+	if (isLink(ev.target)) return;
+	if (window.getSelection().toString() !== '') return;
+
+	if (defaultStore.state.useReactionPickerForContextMenu) {
+		ev.preventDefault();
+		react();
+	} else {
+		os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), ev).then(focus);
+	}
+}
+
+function menu(viaKeyboard = false): void {
+	os.popupMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), menuButton.value, {
+		viaKeyboard
+	}).then(focus);
+}
+
+function showRenoteMenu(viaKeyboard = false): void {
+	if (!isMyRenote) return;
+	os.popupMenu([{
+		text: i18n.locale.unrenote,
+		icon: 'fas fa-trash-alt',
+		danger: true,
+		action: () => {
+			os.api('notes/delete', {
+				noteId: props.note.id
+			});
+			isDeleted.value = true;
+		}
+	}], renoteTime.value, {
+		viaKeyboard: viaKeyboard
+	});
+}
+
+function focus() {
+	el.value.focus();
+}
+
+function blur() {
+	el.value.blur();
+}
+
+os.api('notes/children', {
+	noteId: appearNote.id,
+	limit: 30
+}).then(res => {
+	replies.value = res;
+});
+
+if (appearNote.replyId) {
+	os.api('notes/conversation', {
+		noteId: appearNote.replyId
+	}).then(res => {
+		conversation.value = res.reverse();
+	});
+}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/note-header.vue b/packages/client/src/components/note-header.vue
index 26e725c6b8..56a3a37e75 100644
--- a/packages/client/src/components/note-header.vue
+++ b/packages/client/src/components/note-header.vue
@@ -19,30 +19,16 @@
 </header>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
 import { notePage } from '@/filters/note';
 import { userPage } from '@/filters/user';
-import * as os from '@/os';
 
-export default defineComponent({
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	methods: {
-		notePage,
-		userPage
-	}
-});
+defineProps<{
+	note: misskey.entities.Note;
+	pinned?: boolean;
+}>();
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/note-preview.vue b/packages/client/src/components/note-preview.vue
index bdcb8d5eed..a78b499654 100644
--- a/packages/client/src/components/note-preview.vue
+++ b/packages/client/src/components/note-preview.vue
@@ -14,20 +14,12 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 
-export default defineComponent({
-	components: {
-	},
-
-	props: {
-		text: {
-			type: String,
-			required: true
-		}
-	},
-});
+const props = defineProps<{
+	text: string;
+}>();
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/note-simple.vue b/packages/client/src/components/note-simple.vue
index 135f06602d..c6907787b5 100644
--- a/packages/client/src/components/note-simple.vue
+++ b/packages/client/src/components/note-simple.vue
@@ -9,40 +9,26 @@
 				<XCwButton v-model="showContent" :note="note"/>
 			</p>
 			<div v-show="note.cw == null || showContent" class="content">
-				<XSubNote-content class="text" :note="note"/>
+				<MkNoteSubNoteContent class="text" :note="note"/>
 			</div>
 		</div>
 	</div>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
 import XNoteHeader from './note-header.vue';
-import XSubNoteContent from './sub-note-content.vue';
+import MkNoteSubNoteContent from './sub-note-content.vue';
 import XCwButton from './cw-button.vue';
-import * as os from '@/os';
 
-export default defineComponent({
-	components: {
-		XNoteHeader,
-		XSubNoteContent,
-		XCwButton,
-	},
+const props = defineProps<{
+	note: misskey.entities.Note;
+	pinned?: boolean;
+}>();
 
-	props: {
-		note: {
-			type: Object,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			showContent: false
-		};
-	}
-});
+const showContent = $ref(false);
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue
index 3cf924928a..fc89c2777b 100644
--- a/packages/client/src/components/note.vue
+++ b/packages/client/src/components/note.vue
@@ -2,20 +2,21 @@
 <div
 	v-if="!muted"
 	v-show="!isDeleted"
+	ref="el"
 	v-hotkey="keymap"
 	v-size="{ max: [500, 450, 350, 300] }"
 	class="tkcbzcuz"
 	:tabindex="!isDeleted ? '-1' : null"
 	:class="{ renote: isRenote }"
 >
-	<XSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
-	<div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ $ts.pinnedNote }}</div>
-	<div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ $ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ $ts.hideThisNote }} <i class="fas fa-times"></i></button></div>
-	<div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ $ts.featured }}</div>
+	<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
+	<div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ i18n.locale.pinnedNote }}</div>
+	<div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.locale.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.locale.hideThisNote }} <i class="fas fa-times"></i></button></div>
+	<div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.locale.featured }}</div>
 	<div v-if="isRenote" class="renote">
 		<MkAvatar class="avatar" :user="note.user"/>
 		<i class="fas fa-retweet"></i>
-		<I18n :src="$ts.renotedBy" tag="span">
+		<I18n :src="i18n.locale.renotedBy" tag="span">
 			<template #user>
 				<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
 					<MkUserName :user="note.user"/>
@@ -47,7 +48,7 @@
 				</p>
 				<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed }">
 					<div class="text">
-						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $ts.private }})</span>
+						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.locale.private }})</span>
 						<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
 						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
 						<a v-if="appearNote.renote != null" class="rp">RN:</a>
@@ -66,7 +67,7 @@
 					<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
 					<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div>
 					<button v-if="collapsed" class="fade _button" @click="collapsed = false">
-						<span>{{ $ts.showMore }}</span>
+						<span>{{ i18n.locale.showMore }}</span>
 					</button>
 				</div>
 				<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
@@ -93,7 +94,7 @@
 	</article>
 </div>
 <div v-else class="muted" @click="muted = false">
-	<I18n :src="$ts.userSaysSomething" tag="small">
+	<I18n :src="i18n.locale.userSaysSomething" tag="small">
 		<template #name>
 			<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
 				<MkUserName :user="appearNote.user"/>
@@ -103,11 +104,11 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
 import * as mfm from 'mfm-js';
-import { sum } from '@/scripts/array';
-import XSub from './note.sub.vue';
+import * as misskey from 'misskey-js';
+import MkNoteSub from './MkNoteSub.vue';
 import XNoteHeader from './note-header.vue';
 import XNoteSimple from './note-simple.vue';
 import XReactionsViewer from './reactions-viewer.vue';
@@ -115,745 +116,164 @@ import XMediaList from './media-list.vue';
 import XCwButton from './cw-button.vue';
 import XPoll from './poll.vue';
 import XRenoteButton from './renote-button.vue';
+import MkUrlPreview from '@/components/url-preview.vue';
+import MkInstanceTicker from '@/components/instance-ticker.vue';
 import { pleaseLogin } from '@/scripts/please-login';
 import { focusPrev, focusNext } from '@/scripts/focus';
-import { url } from '@/config';
-import copyToClipboard from '@/scripts/copy-to-clipboard';
 import { checkWordMute } from '@/scripts/check-word-mute';
 import { userPage } from '@/filters/user';
 import * as os from '@/os';
-import { stream } from '@/stream';
-import { noteActions, noteViewInterruptors } from '@/store';
+import { defaultStore, noteViewInterruptors } from '@/store';
 import { reactionPicker } from '@/scripts/reaction-picker';
 import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
-
-export default defineComponent({
-	components: {
-		XSub,
-		XNoteHeader,
-		XNoteSimple,
-		XReactionsViewer,
-		XMediaList,
-		XCwButton,
-		XPoll,
-		XRenoteButton,
-		MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
-		MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')),
-	},
-
-	inject: {
-		inChannel: {
-			default: null
-		},
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		pinned: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	emits: ['update:note'],
-
-	data() {
-		return {
-			connection: null,
-			replies: [],
-			showContent: false,
-			collapsed: false,
-			isDeleted: false,
-			muted: false,
-			translation: null,
-			translating: false,
-		};
-	},
-
-	computed: {
-		rs() {
-			return this.$store.state.reactions;
-		},
-		keymap(): any {
-			return {
-				'r': () => this.reply(true),
-				'e|a|plus': () => this.react(true),
-				'q': () => this.$refs.renoteButton.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.rs[0]),
-				'2': () => this.reactDirectly(this.rs[1]),
-				'3': () => this.reactDirectly(this.rs[2]),
-				'4': () => this.reactDirectly(this.rs[3]),
-				'5': () => this.reactDirectly(this.rs[4]),
-				'6': () => this.reactDirectly(this.rs[5]),
-				'7': () => this.reactDirectly(this.rs[6]),
-				'8': () => this.reactDirectly(this.rs[7]),
-				'9': () => this.reactDirectly(this.rs[8]),
-				'0': () => this.reactDirectly(this.rs[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.$i && (this.$i.id === this.appearNote.userId);
-		},
-
-		isMyRenote(): boolean {
-			return this.$i && (this.$i.id === this.note.userId);
-		},
-
-		reactionsCount(): number {
-			return this.appearNote.reactions
-				? sum(Object.values(this.appearNote.reactions))
-				: 0;
-		},
-
-		urls(): string[] {
-			if (this.appearNote.text) {
-				return extractUrlFromMfm(mfm.parse(this.appearNote.text));
-			} else {
-				return null;
-			}
-		},
-
-		showTicker() {
-			if (this.$store.state.instanceTicker === 'always') return true;
-			if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true;
-			return false;
-		}
-	},
-
-	async created() {
-		if (this.$i) {
-			this.connection = stream;
-		}
-
-		this.collapsed = this.appearNote.cw == null && this.appearNote.text && (
-			(this.appearNote.text.split('\n').length > 9) ||
-			(this.appearNote.text.length > 500)
-		);
-		this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords);
-
-		// plugin
-		if (noteViewInterruptors.length > 0) {
-			let result = this.note;
-			for (const interruptor of noteViewInterruptors) {
-				result = await interruptor.handler(JSON.parse(JSON.stringify(result)));
-			}
-			this.$emit('update:note', Object.freeze(result));
-		}
-	},
-
-	mounted() {
-		this.capture(true);
-
-		if (this.$i) {
-			this.connection.on('_connected_', this.onStreamConnected);
-		}
-	},
-
-	beforeUnmount() {
-		this.decapture(true);
-
-		if (this.$i) {
-			this.connection.off('_connected_', this.onStreamConnected);
-		}
-	},
-
-	methods: {
-		updateAppearNote(v) {
-			this.$emit('update:note', Object.freeze(this.isRenote ? {
-				...this.note,
-				renote: {
-					...this.note.renote,
-					...v
-				}
-			} : {
-				...this.note,
-				...v
-			}));
-		},
-
-		readPromo() {
-			os.api('promo/read', {
-				noteId: this.appearNote.id
-			});
-			this.isDeleted = true;
-		},
-
-		capture(withHandler = false) {
-			if (this.$i) {
-				// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
-				this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
-				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
-			}
-		},
-
-		decapture(withHandler = false) {
-			if (this.$i) {
-				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;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					if (body.emoji) {
-						const emojis = this.appearNote.emojis || [];
-						if (!emojis.includes(body.emoji)) {
-							n.emojis = [...emojis, body.emoji];
-						}
-					}
-
-					// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
-					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
-					// Increment the count
-					n.reactions = {
-						...this.appearNote.reactions,
-						[reaction]: currentCount + 1
-					};
-
-					if (body.userId === this.$i.id) {
-						n.myReaction = reaction;
-					}
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'unreacted': {
-					const reaction = body.reaction;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
-					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
-					// Decrement the count
-					n.reactions = {
-						...this.appearNote.reactions,
-						[reaction]: Math.max(0, currentCount - 1)
-					};
-
-					if (body.userId === this.$i.id) {
-						n.myReaction = null;
-					}
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'pollVoted': {
-					const choice = body.choice;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					const choices = [...this.appearNote.poll.choices];
-					choices[choice] = {
-						...choices[choice],
-						votes: choices[choice].votes + 1,
-						...(body.userId === this.$i.id ? {
-							isVoted: true
-						} : {})
-					};
-
-					n.poll = {
-						...this.appearNote.poll,
-						choices: choices
-					};
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'deleted': {
-					this.isDeleted = true;
-					break;
-				}
-			}
-		},
-
-		reply(viaKeyboard = false) {
-			pleaseLogin();
-			os.post({
-				reply: this.appearNote,
-				animation: !viaKeyboard,
-			}, () => {
-				this.focus();
-			});
-		},
-
-		renoteDirectly() {
-			os.apiWithDialog('notes/create', {
-				renoteId: this.appearNote.id
-			}, undefined, (res: any) => {
-				os.alert({
-					type: 'success',
-					text: this.$ts.renoted,
-				});
-			}, (e: Error) => {
-				if (e.id === 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4') {
-					os.alert({
-						type: 'error',
-						text: this.$ts.cantRenote,
-					});
-				} else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') {
-					os.alert({
-						type: 'error',
-						text: this.$ts.cantReRenote,
-					});
-				}
-			});
-		},
-
-		react(viaKeyboard = false) {
-			pleaseLogin();
-			this.blur();
-			reactionPicker.show(this.$refs.reactButton, reaction => {
-				os.api('notes/reactions/create', {
-					noteId: this.appearNote.id,
-					reaction: reaction
-				});
-			}, () => {
-				this.focus();
-			});
-		},
-
-		reactDirectly(reaction) {
-			os.api('notes/reactions/create', {
-				noteId: this.appearNote.id,
-				reaction: reaction
-			});
-		},
-
-		undoReact(note) {
-			const oldReaction = note.myReaction;
-			if (!oldReaction) return;
-			os.api('notes/reactions/delete', {
-				noteId: note.id
-			});
-		},
-
-		favorite() {
-			pleaseLogin();
-			os.apiWithDialog('notes/favorites/create', {
-				noteId: this.appearNote.id
-			}, undefined, (res: any) => {
-				os.alert({
-					type: 'success',
-					text: this.$ts.favorited,
-				});
-			}, (e: Error) => {
-				if (e.id === 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6') {
-					os.alert({
-						type: 'error',
-						text: this.$ts.alreadyFavorited,
-					});
-				} else if (e.id === '6dd26674-e060-4816-909a-45ba3f4da458') {
-					os.alert({
-						type: 'error',
-						text: this.$ts.cantFavorite,
-					});
-				}
-			});
-		},
-
-		del() {
-			os.confirm({
-				type: 'warning',
-				text: this.$ts.noteDeleteConfirm,
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				os.api('notes/delete', {
-					noteId: this.appearNote.id
-				});
-			});
-		},
-
-		delEdit() {
-			os.confirm({
-				type: 'warning',
-				text: this.$ts.deleteAndEditConfirm,
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				os.api('notes/delete', {
-					noteId: this.appearNote.id
-				});
-
-				os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel });
-			});
-		},
-
-		toggleFavorite(favorite: boolean) {
-			os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
-				noteId: this.appearNote.id
-			});
-		},
-
-		toggleWatch(watch: boolean) {
-			os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
-				noteId: this.appearNote.id
-			});
-		},
-
-		toggleThreadMute(mute: boolean) {
-			os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
-				noteId: this.appearNote.id
-			});
-		},
-
-		getMenu() {
-			let menu;
-			if (this.$i) {
-				const statePromise = os.api('notes/state', {
-					noteId: this.appearNote.id
-				});
-
-				menu = [{
-					icon: 'fas fa-copy',
-					text: this.$ts.copyContent,
-					action: this.copyContent
-				}, {
-					icon: 'fas fa-link',
-					text: this.$ts.copyLink,
-					action: this.copyLink
-				}, (this.appearNote.url || this.appearNote.uri) ? {
-					icon: 'fas fa-external-link-square-alt',
-					text: this.$ts.showOnRemote,
-					action: () => {
-						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
-					}
-				} : undefined,
-				{
-					icon: 'fas fa-share-alt',
-					text: this.$ts.share,
-					action: this.share
-				},
-				this.$instance.translatorAvailable ? {
-					icon: 'fas fa-language',
-					text: this.$ts.translate,
-					action: this.translate
-				} : undefined,
-				null,
-				statePromise.then(state => state.isFavorited ? {
-					icon: 'fas fa-star',
-					text: this.$ts.unfavorite,
-					action: () => this.toggleFavorite(false)
-				} : {
-					icon: 'fas fa-star',
-					text: this.$ts.favorite,
-					action: () => this.toggleFavorite(true)
-				}),
-				{
-					icon: 'fas fa-paperclip',
-					text: this.$ts.clip,
-					action: () => this.clip()
-				},
-				(this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? {
-					icon: 'fas fa-eye-slash',
-					text: this.$ts.unwatch,
-					action: () => this.toggleWatch(false)
-				} : {
-					icon: 'fas fa-eye',
-					text: this.$ts.watch,
-					action: () => this.toggleWatch(true)
-				}) : undefined,
-				statePromise.then(state => state.isMutedThread ? {
-					icon: 'fas fa-comment-slash',
-					text: this.$ts.unmuteThread,
-					action: () => this.toggleThreadMute(false)
-				} : {
-					icon: 'fas fa-comment-slash',
-					text: this.$ts.muteThread,
-					action: () => this.toggleThreadMute(true)
-				}),
-				this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
-					icon: 'fas fa-thumbtack',
-					text: this.$ts.unpin,
-					action: () => this.togglePin(false)
-				} : {
-					icon: 'fas fa-thumbtack',
-					text: this.$ts.pin,
-					action: () => this.togglePin(true)
-				} : undefined,
-				/*
-				...(this.$i.isModerator || this.$i.isAdmin ? [
-					null,
-					{
-						icon: 'fas fa-bullhorn',
-						text: this.$ts.promote,
-						action: this.promote
-					}]
-					: []
-				),*/
-				...(this.appearNote.userId != this.$i.id ? [
-					null,
-					{
-						icon: 'fas fa-exclamation-circle',
-						text: this.$ts.reportAbuse,
-						action: () => {
-							const u = `${url}/notes/${this.appearNote.id}`;
-							os.popup(import('@/components/abuse-report-window.vue'), {
-								user: this.appearNote.user,
-								initialComment: `Note: ${u}\n-----\n`
-							}, {}, 'closed');
-						}
-					}]
-					: []
-				),
-				...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [
-					null,
-					this.appearNote.userId == this.$i.id ? {
-						icon: 'fas fa-edit',
-						text: this.$ts.deleteAndEdit,
-						action: this.delEdit
-					} : undefined,
-					{
-						icon: 'fas fa-trash-alt',
-						text: this.$ts.delete,
-						danger: true,
-						action: this.del
-					}]
-					: []
-				)]
-				.filter(x => x !== undefined);
-			} else {
-				menu = [{
-					icon: 'fas fa-copy',
-					text: this.$ts.copyContent,
-					action: this.copyContent
-				}, {
-					icon: 'fas fa-link',
-					text: this.$ts.copyLink,
-					action: this.copyLink
-				}, (this.appearNote.url || this.appearNote.uri) ? {
-					icon: 'fas fa-external-link-square-alt',
-					text: this.$ts.showOnRemote,
-					action: () => {
-						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
-					}
-				} : undefined]
-				.filter(x => x !== undefined);
-			}
-
-			if (noteActions.length > 0) {
-				menu = menu.concat([null, ...noteActions.map(action => ({
-					icon: 'fas fa-plug',
-					text: action.title,
-					action: () => {
-						action.handler(this.appearNote);
-					}
-				}))]);
-			}
-
-			return menu;
-		},
-
-		onContextmenu(e) {
-			const isLink = (el: HTMLElement) => {
-				if (el.tagName === 'A') return true;
-				if (el.parentElement) {
-					return isLink(el.parentElement);
-				}
-			};
-			if (isLink(e.target)) return;
-			if (window.getSelection().toString() !== '') return;
-
-			if (this.$store.state.useReactionPickerForContextMenu) {
-				e.preventDefault();
-				this.react();
-			} else {
-				os.contextMenu(this.getMenu(), e).then(this.focus);
-			}
-		},
-
-		menu(viaKeyboard = false) {
-			os.popupMenu(this.getMenu(), this.$refs.menuButton, {
-				viaKeyboard
-			}).then(this.focus);
-		},
-
-		showRenoteMenu(viaKeyboard = false) {
-			if (!this.isMyRenote) return;
-			os.popupMenu([{
-				text: this.$ts.unrenote,
-				icon: 'fas fa-trash-alt',
-				danger: true,
-				action: () => {
-					os.api('notes/delete', {
-						noteId: this.note.id
-					});
-					this.isDeleted = true;
-				}
-			}], this.$refs.renoteTime, {
-				viaKeyboard: viaKeyboard
-			});
-		},
-
-		toggleShowContent() {
-			this.showContent = !this.showContent;
-		},
-
-		copyContent() {
-			copyToClipboard(this.appearNote.text);
-			os.success();
-		},
-
-		copyLink() {
-			copyToClipboard(`${url}/notes/${this.appearNote.id}`);
-			os.success();
-		},
-
-		togglePin(pin: boolean) {
-			os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
-				noteId: this.appearNote.id
-			}, undefined, null, e => {
-				if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
-					os.alert({
-						type: 'error',
-						text: this.$ts.pinLimitExceeded
-					});
-				}
-			});
-		},
-
-		async clip() {
-			const clips = await os.api('clips/list');
-			os.popupMenu([{
-				icon: 'fas fa-plus',
-				text: this.$ts.createNew,
-				action: async () => {
-					const { canceled, result } = await os.form(this.$ts.createNewClip, {
-						name: {
-							type: 'string',
-							label: this.$ts.name
-						},
-						description: {
-							type: 'string',
-							required: false,
-							multiline: true,
-							label: this.$ts.description
-						},
-						isPublic: {
-							type: 'boolean',
-							label: this.$ts.public,
-							default: false
-						}
-					});
-					if (canceled) return;
-
-					const clip = await os.apiWithDialog('clips/create', result);
-
-					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
-				}
-			}, null, ...clips.map(clip => ({
-				text: clip.name,
-				action: () => {
-					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
-				}
-			}))], this.$refs.menuButton, {
-			}).then(this.focus);
-		},
-
-		async promote() {
-			const { canceled, result: days } = await os.inputNumber({
-				title: this.$ts.numberOfDays,
-			});
-
-			if (canceled) return;
-
-			os.apiWithDialog('admin/promo/create', {
-				noteId: this.appearNote.id,
-				expiresAt: Date.now() + (86400000 * days)
-			});
-		},
-
-		share() {
-			navigator.share({
-				title: this.$t('noteOf', { user: this.appearNote.user.name }),
-				text: this.appearNote.text,
-				url: `${url}/notes/${this.appearNote.id}`
-			});
-		},
-
-		async translate() {
-			if (this.translation != null) return;
-			this.translating = true;
-			const res = await os.api('notes/translate', {
-				noteId: this.appearNote.id,
-				targetLang: localStorage.getItem('lang') || navigator.language,
-			});
-			this.translating = false;
-			this.translation = res;
-		},
-
-		focus() {
-			this.$el.focus();
-		},
-
-		blur() {
-			this.$el.blur();
-		},
-
-		focusBefore() {
-			focusPrev(this.$el);
-		},
-
-		focusAfter() {
-			focusNext(this.$el);
-		},
-
-		userPage
-	}
+import { $i } from '@/account';
+import { i18n } from '@/i18n';
+import { getNoteMenu } from '@/scripts/get-note-menu';
+import { useNoteCapture } from '@/scripts/use-note-capture';
+
+const props = defineProps<{
+	note: misskey.entities.Note;
+	pinned?: boolean;
+}>();
+
+const inChannel = inject('inChannel', null);
+
+const isRenote = (
+	props.note.renote != null &&
+	props.note.text == null &&
+	props.note.fileIds.length === 0 &&
+	props.note.poll == null
+);
+
+const el = ref<HTMLElement>();
+const menuButton = ref<HTMLElement>();
+const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
+const renoteTime = ref<HTMLElement>();
+const reactButton = ref<HTMLElement>();
+let appearNote = $ref(isRenote ? props.note.renote as misskey.entities.Note : props.note);
+const isMyRenote = $i && ($i.id === props.note.userId);
+const showContent = ref(false);
+const collapsed = ref(appearNote.cw == null && appearNote.text != null && (
+	(appearNote.text.split('\n').length > 9) ||
+	(appearNote.text.length > 500)
+));
+const isDeleted = ref(false);
+const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
+const translation = ref(null);
+const translating = ref(false);
+const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
+const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
+
+const keymap = {
+	'r': () => reply(true),
+	'e|a|plus': () => react(true),
+	'q': () => renoteButton.value.renote(true),
+	'up|k|shift+tab': focusBefore,
+	'down|j|tab': focusAfter,
+	'esc': blur,
+	'm|o': () => menu(true),
+	's': () => showContent.value != showContent.value,
+};
+
+useNoteCapture({
+	appearNote: $$(appearNote),
+	rootEl: el,
 });
+
+function reply(viaKeyboard = false): void {
+	pleaseLogin();
+	os.post({
+		reply: appearNote,
+		animation: !viaKeyboard,
+	}, () => {
+		focus();
+	});
+}
+
+function react(viaKeyboard = false): void {
+	pleaseLogin();
+	blur();
+	reactionPicker.show(reactButton.value, reaction => {
+		os.api('notes/reactions/create', {
+			noteId: appearNote.id,
+			reaction: reaction
+		});
+	}, () => {
+		focus();
+	});
+}
+
+function undoReact(note): void {
+	const oldReaction = note.myReaction;
+	if (!oldReaction) return;
+	os.api('notes/reactions/delete', {
+		noteId: note.id
+	});
+}
+
+function onContextmenu(ev: MouseEvent): void {
+	const isLink = (el: HTMLElement) => {
+		if (el.tagName === 'A') return true;
+		if (el.parentElement) {
+			return isLink(el.parentElement);
+		}
+	};
+	if (isLink(ev.target)) return;
+	if (window.getSelection().toString() !== '') return;
+
+	if (defaultStore.state.useReactionPickerForContextMenu) {
+		ev.preventDefault();
+		react();
+	} else {
+		os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), ev).then(focus);
+	}
+}
+
+function menu(viaKeyboard = false): void {
+	os.popupMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), menuButton.value, {
+		viaKeyboard
+	}).then(focus);
+}
+
+function showRenoteMenu(viaKeyboard = false): void {
+	if (!isMyRenote) return;
+	os.popupMenu([{
+		text: i18n.locale.unrenote,
+		icon: 'fas fa-trash-alt',
+		danger: true,
+		action: () => {
+			os.api('notes/delete', {
+				noteId: props.note.id
+			});
+			isDeleted.value = true;
+		}
+	}], renoteTime.value, {
+		viaKeyboard: viaKeyboard
+	});
+}
+
+function focus() {
+	el.value.focus();
+}
+
+function blur() {
+	el.value.blur();
+}
+
+function focusBefore() {
+	focusPrev(el.value);
+}
+
+function focusAfter() {
+	focusNext(el.value);
+}
+
+function readPromo() {
+	os.api('promo/read', {
+		noteId: appearNote.id
+	});
+	isDeleted.value = true;
+}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/notes.vue b/packages/client/src/components/notes.vue
index 4136f72b1b..41bec5a579 100644
--- a/packages/client/src/components/notes.vue
+++ b/packages/client/src/components/notes.vue
@@ -1,114 +1,42 @@
 <template>
-<transition name="fade" mode="out-in">
-	<MkLoading v-if="fetching"/>
-
-	<MkError v-else-if="error" @retry="init()"/>
-
-	<div v-else-if="empty" class="_fullinfo">
-		<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
-		<div>{{ $ts.noNotes }}</div>
-	</div>
-
-	<div v-else class="giivymft" :class="{ noGap }">
-		<div v-show="more && reversed" style="margin-bottom: var(--margin);">
-			<MkButton style="margin: 0 auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMoreFeature">
-				<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
-				<template v-if="moreFetching"><MkLoading inline/></template>
-			</MkButton>
+<MkPagination ref="pagingComponent" :pagination="pagination">
+	<template #empty>
+		<div class="_fullinfo">
+			<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+			<div>{{ $ts.noNotes }}</div>
 		</div>
+	</template>
 
-		<XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap" :ad="true" class="notes">
-			<XNote :key="note._featuredId_ || note._prId_ || note.id" class="qtqtichx" :note="note" @update:note="updated(note, $event)"/>
-		</XList>
-
-		<div v-show="more && !reversed" style="margin-top: var(--margin);">
-			<MkButton v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" style="margin: 0 auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore">
-				<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
-				<template v-if="moreFetching"><MkLoading inline/></template>
-			</MkButton>
+	<template #default="{ items: notes }">
+		<div class="giivymft" :class="{ noGap }">
+			<XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" :ad="true" class="notes">
+				<XNote :key="note._featuredId_ || note._prId_ || note.id" class="qtqtichx" :note="note"/>
+			</XList>
 		</div>
-	</div>
-</transition>
+	</template>
+</MkPagination>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-import paging from '@/scripts/paging';
-import XNote from './note.vue';
-import XList from './date-separated-list.vue';
-import MkButton from '@/components/ui/button.vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
+import XNote from '@/components/note.vue';
+import XList from '@/components/date-separated-list.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import { Paging } from '@/components/ui/pagination.vue';
 
-export default defineComponent({
-	components: {
-		XNote, XList, MkButton,
-	},
+const props = defineProps<{
+	pagination: Paging;
+	noGap?: boolean;
+}>();
 
-	mixins: [
-		paging({
-			before: (self) => {
-				self.$emit('before');
-			},
+const pagingComponent = ref<InstanceType<typeof MkPagination>>();
 
-			after: (self, e) => {
-				self.$emit('after', e);
-			}
-		}),
-	],
-
-	props: {
-		pagination: {
-			required: true
-		},
-		prop: {
-			type: String,
-			required: false
-		},
-		noGap: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	emits: ['before', 'after'],
-
-	computed: {
-		notes(): any[] {
-			return this.prop ? this.items.map(item => item[this.prop]) : this.items;
-		},
-
-		reversed(): boolean {
-			return this.pagination.reversed;
-		}
-	},
-
-	methods: {
-		updated(oldValue, newValue) {
-			const i = this.notes.findIndex(n => n === oldValue);
-			if (this.prop) {
-				this.items[i][this.prop] = newValue;
-			} else {
-				this.items[i] = newValue;
-			}
-		},
-
-		focus() {
-			this.$refs.notes.focus();
-		}
-	}
+defineExpose({
+	pagingComponent,
 });
 </script>
 
 <style lang="scss" scoped>
-.fade-enter-active,
-.fade-leave-active {
-	transition: opacity 0.125s ease;
-}
-.fade-enter-from,
-.fade-leave-to {
-	opacity: 0;
-}
-
 .giivymft {
 	&.noGap {
 		> .notes {
diff --git a/packages/client/src/components/notification-toast.vue b/packages/client/src/components/notification-toast.vue
index 5449409ccc..fbd8467a6e 100644
--- a/packages/client/src/components/notification-toast.vue
+++ b/packages/client/src/components/notification-toast.vue
@@ -29,7 +29,7 @@ export default defineComponent({
 		};
 	},
 	mounted() {
-		setTimeout(() => {
+		window.setTimeout(() => {
 			this.showing = false;
 		}, 6000);
 	}
diff --git a/packages/client/src/components/notifications.vue b/packages/client/src/components/notifications.vue
index 328888c355..5a77b5487e 100644
--- a/packages/client/src/components/notifications.vue
+++ b/packages/client/src/components/notifications.vue
@@ -1,159 +1,77 @@
 <template>
-<transition name="fade" mode="out-in">
-	<MkLoading v-if="fetching"/>
+<MkPagination ref="pagingComponent" :pagination="pagination">
+	<template #empty>
+		<div class="_fullinfo">
+			<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+			<div>{{ $ts.noNotifications }}</div>
+		</div>
+	</template>
 
-	<MkError v-else-if="error" @retry="init()"/>
-
-	<p v-else-if="empty" class="mfcuwfyp">{{ $ts.noNotifications }}</p>
-
-	<div v-else>
-		<XList v-slot="{ item: notification }" class="elsfgstc" :items="items" :no-gap="true">
-			<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" @update:note="noteUpdated(notification.note, $event)"/>
+	<template #default="{ items: notifications }">
+		<XList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true">
+			<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
 			<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
 		</XList>
-
-		<MkButton v-show="more" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" primary style="margin: var(--margin) auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore">
-			<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
-			<template v-if="moreFetching"><MkLoading inline/></template>
-		</MkButton>
-	</div>
-</transition>
+	</template>
+</MkPagination>
 </template>
 
-<script lang="ts">
-import { defineComponent, PropType, markRaw } from 'vue';
-import paging from '@/scripts/paging';
-import XNotification from './notification.vue';
-import XList from './date-separated-list.vue';
-import XNote from './note.vue';
+<script lang="ts" setup>
+import { defineComponent, PropType, markRaw, onUnmounted, onMounted, computed, ref } from 'vue';
 import { notificationTypes } from 'misskey-js';
+import MkPagination from '@/components/ui/pagination.vue';
+import { Paging } from '@/components/ui/pagination.vue';
+import XNotification from '@/components/notification.vue';
+import XList from '@/components/date-separated-list.vue';
+import XNote from '@/components/note.vue';
 import * as os from '@/os';
 import { stream } from '@/stream';
-import MkButton from '@/components/ui/button.vue';
+import { $i } from '@/account';
 
-export default defineComponent({
-	components: {
-		XNotification,
-		XList,
-		XNote,
-		MkButton,
-	},
+const props = defineProps<{
+	includeTypes?: PropType<typeof notificationTypes[number][]>;
+	unreadOnly?: boolean;
+}>();
 
-	mixins: [
-		paging({}),
-	],
+const pagingComponent = ref<InstanceType<typeof MkPagination>>();
 
-	props: {
-		includeTypes: {
-			type: Array as PropType<typeof notificationTypes[number][]>,
-			required: false,
-			default: null,
-		},
-		unreadOnly: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-	},
+const allIncludeTypes = computed(() => props.includeTypes ?? notificationTypes.filter(x => !$i.mutingNotificationTypes.includes(x)));
 
-	data() {
-		return {
-			connection: null,
-			pagination: {
-				endpoint: 'i/notifications',
-				limit: 10,
-				params: () => ({
-					includeTypes: this.allIncludeTypes || undefined,
-					unreadOnly: this.unreadOnly,
-				})
-			},
-		};
-	},
+const pagination: Paging = {
+	endpoint: 'i/notifications' as const,
+	limit: 10,
+	params: computed(() => ({
+		includeTypes: allIncludeTypes.value || undefined,
+		unreadOnly: props.unreadOnly,
+	})),
+};
 
-	computed: {
-		allIncludeTypes() {
-			return this.includeTypes ?? notificationTypes.filter(x => !this.$i.mutingNotificationTypes.includes(x));
-		}
-	},
-
-	watch: {
-		includeTypes: {
-			handler() {
-				this.reload();
-			},
-			deep: true
-		},
-		unreadOnly: {
-			handler() {
-				this.reload();
-			},
-		},
-		// TODO: vue/vuexのバグか仕様かは不明なものの、プロフィール更新するなどして $i が更新されると、
-		// mutingNotificationTypes に変化が無くてもこのハンドラーが呼び出され無駄なリロードが発生するのを直す
-		'$i.mutingNotificationTypes': {
-			handler() {
-				if (this.includeTypes === null) {
-					this.reload();
-				}
-			},
-			deep: true
-		}
-	},
-
-	mounted() {
-		this.connection = markRaw(stream.useChannel('main'));
-		this.connection.on('notification', this.onNotification);
-	},
-
-	beforeUnmount() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		onNotification(notification) {
-			const isMuted = !this.allIncludeTypes.includes(notification.type);
-			if (isMuted || document.visibilityState === 'visible') {
-				stream.send('readNotification', {
-					id: notification.id
-				});
-			}
-
-			if (!isMuted) {
-				this.prepend({
-					...notification,
-					isRead: document.visibilityState === 'visible'
-				});
-			}
-		},
-
-		noteUpdated(oldValue, newValue) {
-			const i = this.items.findIndex(n => n.note === oldValue);
-			this.items[i] = {
-				...this.items[i],
-				note: newValue
-			};
-		},
+const onNotification = (notification) => {
+	const isMuted = !allIncludeTypes.value.includes(notification.type);
+	if (isMuted || document.visibilityState === 'visible') {
+		stream.send('readNotification', {
+			id: notification.id
+		});
 	}
+
+	if (!isMuted) {
+		pagingComponent.value.prepend({
+			...notification,
+			isRead: document.visibilityState === 'visible'
+		});
+	}
+};
+
+onMounted(() => {
+	const connection = stream.useChannel('main');
+	connection.on('notification', onNotification);
+	onUnmounted(() => {
+		connection.dispose();
+	});
 });
 </script>
 
 <style lang="scss" scoped>
-.fade-enter-active,
-.fade-leave-active {
-	transition: opacity 0.125s ease;
-}
-.fade-enter-from,
-.fade-leave-to {
-	opacity: 0;
-}
-
-.mfcuwfyp {
-	margin: 0;
-	padding: 16px;
-	text-align: center;
-	color: var(--fg);
-}
-
 .elsfgstc {
 	background: var(--panel);
 }
diff --git a/packages/client/src/components/post-form-attaches.vue b/packages/client/src/components/post-form-attaches.vue
index 0782ce22e5..0c8181b481 100644
--- a/packages/client/src/components/post-form-attaches.vue
+++ b/packages/client/src/components/post-form-attaches.vue
@@ -10,7 +10,7 @@
 			</div>
 		</template>
 	</XDraggable>
-	<p class="remain">{{ 4 - files.length }}/4</p>
+	<p class="remain">{{ 16 - files.length }}/16</p>
 </div>
 </template>
 
@@ -41,7 +41,6 @@ export default defineComponent({
 	data() {
 		return {
 			menu: null as Promise<null> | null,
-
 		};
 	},
 
@@ -99,10 +98,12 @@ export default defineComponent({
 			}, {
 				done: result => {
 					if (!result || result.canceled) return;
-					let comment = result.result;
+					let comment = result.result.length == 0 ? null : result.result;
 					os.api('drive/files/update', {
 						fileId: file.id,
-						comment: comment.length == 0 ? null : comment
+						comment: comment,
+					}).then(() => {
+						file.comment = comment;
 					});
 				}
 			}, 'closed');
diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue
index 24f35da2e9..0dcec26932 100644
--- a/packages/client/src/components/post-form.vue
+++ b/packages/client/src/components/post-form.vue
@@ -8,25 +8,28 @@
 >
 	<header>
 		<button v-if="!fixed" class="cancel _button" @click="cancel"><i class="fas fa-times"></i></button>
+		<button v-click-anime v-tooltip="i18n.locale.switchAccount" class="account _button" @click="openAccountMenu">
+			<MkAvatar :user="postAccount ?? $i" class="avatar"/>
+		</button>
 		<div>
-			<span class="text-count" :class="{ over: textLength > max }">{{ max - textLength }}</span>
+			<span class="text-count" :class="{ over: textLength > maxTextLength }">{{ maxTextLength - textLength }}</span>
 			<span v-if="localOnly" class="local-only"><i class="fas fa-biohazard"></i></span>
-			<button ref="visibilityButton" v-tooltip="$ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
+			<button ref="visibilityButton" v-tooltip="i18n.locale.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
 				<span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span>
 				<span v-if="visibility === 'home'"><i class="fas fa-home"></i></span>
 				<span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span>
 				<span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span>
 			</button>
-			<button v-tooltip="$ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button>
+			<button v-tooltip="i18n.locale.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button>
 			<button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button>
 		</div>
 	</header>
 	<div class="form" :class="{ fixed }">
 		<XNoteSimple v-if="reply" class="preview" :note="reply"/>
 		<XNoteSimple v-if="renote" class="preview" :note="renote"/>
-		<div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ $ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
+		<div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ i18n.locale.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
 		<div v-if="visibility === 'specified'" class="to-specified">
-			<span style="margin-right: 8px;">{{ $ts.recipient }}</span>
+			<span style="margin-right: 8px;">{{ i18n.locale.recipient }}</span>
 			<div class="visibleUsers">
 				<span v-for="u in visibleUsers" :key="u.id">
 					<MkAcct :user="u"/>
@@ -35,21 +38,21 @@
 				<button class="_buttonPrimary" @click="addVisibleUser"><i class="fas fa-plus fa-fw"></i></button>
 			</div>
 		</div>
-		<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ $ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ $ts.add }}</button></MkInfo>
-		<input v-show="useCw" ref="cw" v-model="cw" class="cw" :placeholder="$ts.annotation" @keydown="onKeydown">
-		<textarea ref="text" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
-		<input v-show="withHashtags" ref="hashtags" v-model="hashtags" class="hashtags" :placeholder="$ts.hashtags" list="hashtags">
+		<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.locale.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.locale.add }}</button></MkInfo>
+		<input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.locale.annotation" @keydown="onKeydown">
+		<textarea ref="textareaEl" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
+		<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.locale.hashtags" list="hashtags">
 		<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
 		<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
 		<XNotePreview v-if="showPreview" class="preview" :text="text"/>
 		<footer>
-			<button v-tooltip="$ts.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
-			<button v-tooltip="$ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button>
-			<button v-tooltip="$ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button>
-			<button v-tooltip="$ts.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button>
-			<button v-tooltip="$ts.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button>
-			<button v-tooltip="$ts.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
-			<button v-if="postFormActions.length > 0" v-tooltip="$ts.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button>
+			<button v-tooltip="i18n.locale.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
+			<button v-tooltip="i18n.locale.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button>
+			<button v-tooltip="i18n.locale.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button>
+			<button v-tooltip="i18n.locale.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button>
+			<button v-tooltip="i18n.locale.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button>
+			<button v-tooltip="i18n.locale.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
+			<button v-if="postFormActions.length > 0" v-tooltip="i18n.locale.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button>
 		</footer>
 		<datalist id="hashtags">
 			<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/>
@@ -58,667 +61,628 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
+<script lang="ts" setup>
+import { inject, watch, nextTick, onMounted } from 'vue';
+import * as mfm from 'mfm-js';
+import * as misskey from 'misskey-js';
 import insertTextAtCursor from 'insert-text-at-cursor';
 import { length } from 'stringz';
 import { toASCII } from 'punycode/';
 import XNoteSimple from './note-simple.vue';
 import XNotePreview from './note-preview.vue';
-import * as mfm from 'mfm-js';
+import XPostFormAttaches from './post-form-attaches.vue';
+import XPollEditor from './poll-editor.vue';
 import { host, url } from '@/config';
 import { erase, unique } from '@/scripts/array';
 import { extractMentions } from '@/scripts/extract-mentions';
 import * as Acct from 'misskey-js/built/acct';
 import { formatTimeString } from '@/scripts/format-time-string';
 import { Autocomplete } from '@/scripts/autocomplete';
-import { noteVisibilities } from 'misskey-js';
 import * as os from '@/os';
 import { stream } from '@/stream';
 import { selectFiles } from '@/scripts/select-file';
 import { defaultStore, notePostInterruptors, postFormActions } from '@/store';
 import { throttle } from 'throttle-debounce';
 import MkInfo from '@/components/ui/info.vue';
+import { i18n } from '@/i18n';
+import { instance } from '@/instance';
+import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
 
-export default defineComponent({
-	components: {
-		XNoteSimple,
-		XNotePreview,
-		XPostFormAttaches: defineAsyncComponent(() => import('./post-form-attaches.vue')),
-		XPollEditor: defineAsyncComponent(() => import('./poll-editor.vue')),
-		MkInfo,
-	},
+const modal = inject('modal');
 
-	inject: ['modal'],
+const props = withDefaults(defineProps<{
+	reply?: misskey.entities.Note;
+	renote?: misskey.entities.Note;
+	channel?: any; // TODO
+	mention?: misskey.entities.User;
+	specified?: misskey.entities.User;
+	initialText?: string;
+	initialVisibility?: typeof misskey.noteVisibilities;
+	initialFiles?: misskey.entities.DriveFile[];
+	initialLocalOnly?: boolean;
+	initialVisibleUsers?: misskey.entities.User[];
+	initialNote?: misskey.entities.Note;
+	share?: boolean;
+	fixed?: boolean;
+	autofocus?: boolean;
+}>(), {
+	initialVisibleUsers: [],
+	autofocus: true,
+});
 
-	props: {
-		reply: {
-			type: Object,
-			required: false
-		},
-		renote: {
-			type: Object,
-			required: false
-		},
-		channel: {
-			type: Object,
-			required: false
-		},
-		mention: {
-			type: Object,
-			required: false
-		},
-		specified: {
-			type: Object,
-			required: false
-		},
-		initialText: {
-			type: String,
-			required: false
-		},
-		initialVisibility: {
-			type: String,
-			required: false
-		},
-		initialFiles: {
-			type: Array,
-			required: false
-		},
-		initialLocalOnly: {
-			type: Boolean,
-			required: false
-		},
-		initialVisibleUsers: {
-			type: Array,
-			required: false,
-			default: () => []
-		},
-		initialNote: {
-			type: Object,
-			required: false
-		},
-		share: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		fixed: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		autofocus: {
-			type: Boolean,
-			required: false,
-			default: true
-		},
-	},
+const emit = defineEmits<{
+	(e: 'posted'): void;
+	(e: 'cancel'): void;
+	(e: 'esc'): void;
+}>();
 
-	emits: ['posted', 'cancel', 'esc'],
+const textareaEl = $ref<HTMLTextAreaElement | null>(null);
+const cwInputEl = $ref<HTMLInputElement | null>(null);
+const hashtagsInputEl = $ref<HTMLInputElement | null>(null);
+const visibilityButton = $ref<HTMLElement | null>(null);
 
-	data() {
-		return {
-			posting: false,
-			text: '',
-			files: [],
-			poll: null,
-			useCw: false,
-			showPreview: false,
-			cw: null,
-			localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly,
-			visibility: (this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility) as typeof noteVisibilities[number],
-			visibleUsers: [],
-			autocomplete: null,
-			draghover: false,
-			quoteId: null,
-			hasNotSpecifiedMentions: false,
-			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
-			imeText: '',
-			typing: throttle(3000, () => {
-				if (this.channel) {
-					stream.send('typingOnChannel', { channel: this.channel.id });
-				}
-			}),
-			postFormActions,
-		};
-	},
+let posting = $ref(false);
+let text = $ref(props.initialText ?? '');
+let files = $ref(props.initialFiles ?? []);
+let poll = $ref<{
+	choices: string[];
+	multiple: boolean;
+	expiresAt: string;
+	expiredAfter: string;
+} | null>(null);
+let useCw = $ref(false);
+let showPreview = $ref(false);
+let cw = $ref<string | null>(null);
+let localOnly = $ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly);
+let visibility = $ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof misskey.noteVisibilities[number]);
+let visibleUsers = $ref(props.initialVisibleUsers ?? []);
+let autocomplete = $ref(null);
+let draghover = $ref(false);
+let quoteId = $ref(null);
+let hasNotSpecifiedMentions = $ref(false);
+let recentHashtags = $ref(JSON.parse(localStorage.getItem('hashtags') || '[]'));
+let imeText = $ref('');
 
-	computed: {
-		draftKey(): string {
-			let key = this.channel ? `channel:${this.channel.id}` : '';
+const typing = throttle(3000, () => {
+	if (props.channel) {
+		stream.send('typingOnChannel', { channel: props.channel.id });
+	}
+});
 
-			if (this.renote) {
-				key += `renote:${this.renote.id}`;
-			} else if (this.reply) {
-				key += `reply:${this.reply.id}`;
-			} else {
-				key += 'note';
-			}
+const draftKey = $computed((): string => {
+	let key = props.channel ? `channel:${props.channel.id}` : '';
 
-			return key;
-		},
+	if (props.renote) {
+		key += `renote:${props.renote.id}`;
+	} else if (props.reply) {
+		key += `reply:${props.reply.id}`;
+	} else {
+		key += 'note';
+	}
 
-		placeholder(): string {
-			if (this.renote) {
-				return this.$ts._postForm.quotePlaceholder;
-			} else if (this.reply) {
-				return this.$ts._postForm.replyPlaceholder;
-			} else if (this.channel) {
-				return this.$ts._postForm.channelPlaceholder;
-			} else {
-				const xs = [
-					this.$ts._postForm._placeholders.a,
-					this.$ts._postForm._placeholders.b,
-					this.$ts._postForm._placeholders.c,
-					this.$ts._postForm._placeholders.d,
-					this.$ts._postForm._placeholders.e,
-					this.$ts._postForm._placeholders.f
-				];
-				return xs[Math.floor(Math.random() * xs.length)];
-			}
-		},
+	return key;
+});
 
-		submitText(): string {
-			return this.renote
-				? this.$ts.quote
-				: this.reply
-					? this.$ts.reply
-					: this.$ts.note;
-		},
+const placeholder = $computed((): string => {
+	if (props.renote) {
+		return i18n.locale._postForm.quotePlaceholder;
+	} else if (props.reply) {
+		return i18n.locale._postForm.replyPlaceholder;
+	} else if (props.channel) {
+		return i18n.locale._postForm.channelPlaceholder;
+	} else {
+		const xs = [
+			i18n.locale._postForm._placeholders.a,
+			i18n.locale._postForm._placeholders.b,
+			i18n.locale._postForm._placeholders.c,
+			i18n.locale._postForm._placeholders.d,
+			i18n.locale._postForm._placeholders.e,
+			i18n.locale._postForm._placeholders.f
+		];
+		return xs[Math.floor(Math.random() * xs.length)];
+	}
+});
 
-		textLength(): number {
-			return length((this.text + this.imeText).trim());
-		},
+const submitText = $computed((): string => {
+	return props.renote
+		? i18n.locale.quote
+		: props.reply
+			? i18n.locale.reply
+			: i18n.locale.note;
+});
 
-		canPost(): boolean {
-			return !this.posting &&
-				(1 <= this.textLength || 1 <= this.files.length || !!this.poll || !!this.renote) &&
-				(this.textLength <= this.max) &&
-				(!this.poll || this.poll.choices.length >= 2);
-		},
+const textLength = $computed((): number => {
+	return length((text + imeText).trim());
+});
 
-		max(): number {
-			return this.$instance ? this.$instance.maxNoteTextLength : 1000;
-		},
+const maxTextLength = $computed((): number => {
+	return instance ? instance.maxNoteTextLength : 1000;
+});
 
-		withHashtags: defaultStore.makeGetterSetter('postFormWithHashtags'),
-		hashtags: defaultStore.makeGetterSetter('postFormHashtags'),
-	},
+const canPost = $computed((): boolean => {
+	return !posting &&
+		(1 <= textLength || 1 <= files.length || !!poll || !!props.renote) &&
+		(textLength <= maxTextLength) &&
+		(!poll || poll.choices.length >= 2);
+});
 
-	watch: {
-		text() {
-			this.checkMissingMention();
-		},
-		visibleUsers: {
-			handler() {
-				this.checkMissingMention();
-			},
-			deep: true
-		}
-	},
+const withHashtags = $computed(defaultStore.makeGetterSetter('postFormWithHashtags'));
+const hashtags = $computed(defaultStore.makeGetterSetter('postFormHashtags'));
 
-	mounted() {
-		if (this.initialText) {
-			this.text = this.initialText;
-		}
+watch($$(text), () => {
+	checkMissingMention();
+});
 
-		if (this.initialVisibility) {
-			this.visibility = this.initialVisibility;
-		}
+watch($$(visibleUsers), () => {
+	checkMissingMention();
+}, {
+	deep: true,
+});
 
-		if (this.initialFiles) {
-			this.files = this.initialFiles;
-		}
+if (props.mention) {
+	text = props.mention.host ? `@${props.mention.username}@${toASCII(props.mention.host)}` : `@${props.mention.username}`;
+	text += ' ';
+}
 
-		if (typeof this.initialLocalOnly === 'boolean') {
-			this.localOnly = this.initialLocalOnly;
-		}
+if (props.reply && (props.reply.user.username != $i.username || (props.reply.user.host != null && props.reply.user.host != host))) {
+	text = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `;
+}
 
-		if (this.initialVisibleUsers) {
-			this.visibleUsers = this.initialVisibleUsers;
-		}
+if (props.reply && props.reply.text != null) {
+	const ast = mfm.parse(props.reply.text);
+	const otherHost = props.reply.user.host;
 
-		if (this.mention) {
-			this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
-			this.text += ' ';
-		}
+	for (const x of extractMentions(ast)) {
+		const mention = x.host ?
+											`@${x.username}@${toASCII(x.host)}` :
+											(otherHost == null || otherHost == host) ?
+												`@${x.username}` :
+												`@${x.username}@${toASCII(otherHost)}`;
 
-		if (this.reply && (this.reply.user.username != this.$i.username || (this.reply.user.host != null && this.reply.user.host != host))) {
-			this.text = `@${this.reply.user.username}${this.reply.user.host != null ? '@' + toASCII(this.reply.user.host) : ''} `;
-		}
+		// 自分は除外
+		if ($i.username == x.username && x.host == null) continue;
+		if ($i.username == x.username && x.host == host) continue;
 
-		if (this.reply && this.reply.text != null) {
-			const ast = mfm.parse(this.reply.text);
-			const otherHost = this.reply.user.host;
+		// 重複は除外
+		if (text.indexOf(`${mention} `) != -1) continue;
 
-			for (const x of extractMentions(ast)) {
-				const mention = x.host ?
-													`@${x.username}@${toASCII(x.host)}` :
-													(otherHost == null || otherHost == host) ?
-														`@${x.username}` :
-														`@${x.username}@${toASCII(otherHost)}`;
+		text += `${mention} `;
+	}
+}
 
-				// 自分は除外
-				if (this.$i.username == x.username && x.host == null) continue;
-				if (this.$i.username == x.username && x.host == host) continue;
+if (props.channel) {
+	visibility = 'public';
+	localOnly = true; // TODO: チャンネルが連合するようになった折には消す
+}
 
-				// 重複は除外
-				if (this.text.indexOf(`${mention} `) != -1) continue;
-
-				this.text += `${mention} `;
-			}
-		}
-
-		if (this.channel) {
-			this.visibility = 'public';
-			this.localOnly = true; // TODO: チャンネルが連合するようになった折には消す
-		}
-
-		// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
-		if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
-			this.visibility = this.reply.visibility;
-			if (this.reply.visibility === 'specified') {
-				os.api('users/show', {
-					userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$i.id && uid !== this.reply.userId)
-				}).then(users => {
-					this.visibleUsers.push(...users);
-				});
-
-				if (this.reply.userId !== this.$i.id) {
-					os.api('users/show', { userId: this.reply.userId }).then(user => {
-						this.visibleUsers.push(user);
-					});
-				}
-			}
-		}
-
-		if (this.specified) {
-			this.visibility = 'specified';
-			this.visibleUsers.push(this.specified);
-		}
-
-		// keep cw when reply
-		if (this.$store.state.keepCw && this.reply && this.reply.cw) {
-			this.useCw = true;
-			this.cw = this.reply.cw;
-		}
-
-		if (this.autofocus) {
-			this.focus();
-
-			this.$nextTick(() => {
-				this.focus();
-			});
-		}
-
-		// TODO: detach when unmount
-		new Autocomplete(this.$refs.text, this, { model: 'text' });
-		new Autocomplete(this.$refs.cw, this, { model: 'cw' });
-		new Autocomplete(this.$refs.hashtags, this, { model: 'hashtags' });
-
-		this.$nextTick(() => {
-			// 書きかけの投稿を復元
-			if (!this.share && !this.mention && !this.specified) {
-				const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
-				if (draft) {
-					this.text = draft.data.text;
-					this.useCw = draft.data.useCw;
-					this.cw = draft.data.cw;
-					this.visibility = draft.data.visibility;
-					this.localOnly = draft.data.localOnly;
-					this.files = (draft.data.files || []).filter(e => e);
-					if (draft.data.poll) {
-						this.poll = draft.data.poll;
-					}
-				}
-			}
-
-			// 削除して編集
-			if (this.initialNote) {
-				const init = this.initialNote;
-				this.text = init.text ? init.text : '';
-				this.files = init.files;
-				this.cw = init.cw;
-				this.useCw = init.cw != null;
-				if (init.poll) {
-					this.poll = {
-						choices: init.poll.choices.map(x => x.text),
-						multiple: init.poll.multiple,
-						expiresAt: init.poll.expiresAt,
-						expiredAfter: init.poll.expiredAfter,
-					};
-				}
-				this.visibility = init.visibility;
-				this.localOnly = init.localOnly;
-				this.quoteId = init.renote ? init.renote.id : null;
-			}
-
-			this.$nextTick(() => this.watch());
+// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
+if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visibility)) {
+	visibility = props.reply.visibility;
+	if (props.reply.visibility === 'specified') {
+		os.api('users/show', {
+			userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId)
+		}).then(users => {
+			visibleUsers.push(...users);
 		});
-	},
 
-	methods: {
-		watch() {
-			this.$watch('text', () => this.saveDraft());
-			this.$watch('useCw', () => this.saveDraft());
-			this.$watch('cw', () => this.saveDraft());
-			this.$watch('poll', () => this.saveDraft());
-			this.$watch('files', () => this.saveDraft(), { deep: true });
-			this.$watch('visibility', () => this.saveDraft());
-			this.$watch('localOnly', () => this.saveDraft());
-		},
-
-		checkMissingMention() {
-			if (this.visibility === 'specified') {
-				const ast = mfm.parse(this.text);
-
-				for (const x of extractMentions(ast)) {
-					if (!this.visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) {
-						this.hasNotSpecifiedMentions = true;
-						return;
-					}
-				}
-				this.hasNotSpecifiedMentions = false;
-			}
-		},
-
-		addMissingMention() {
-			const ast = mfm.parse(this.text);
-
-			for (const x of extractMentions(ast)) {
-				if (!this.visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) {
-					os.api('users/show', { username: x.username, host: x.host }).then(user => {
-						this.visibleUsers.push(user);
-					});
-				}
-			}
-		},
-
-		togglePoll() {
-			if (this.poll) {
-				this.poll = null;
-			} else {
-				this.poll = {
-					choices: ['', ''],
-					multiple: false,
-					expiresAt: null,
-					expiredAfter: null,
-				};
-			}
-		},
-
-		addTag(tag: string) {
-			insertTextAtCursor(this.$refs.text, ` #${tag} `);
-		},
-
-		focus() {
-			(this.$refs.text as any).focus();
-		},
-
-		chooseFileFrom(ev) {
-			selectFiles(ev.currentTarget || ev.target, this.$ts.attachFile).then(files => {
-				for (const file of files) {
-					this.files.push(file);
-				}
+		if (props.reply.userId !== $i.id) {
+			os.api('users/show', { userId: props.reply.userId }).then(user => {
+				visibleUsers.push(user);
 			});
-		},
-
-		detachFile(id) {
-			this.files = this.files.filter(x => x.id != id);
-		},
-
-		updateFiles(files) {
-			this.files = files;
-		},
-
-		updateFileSensitive(file, sensitive) {
-			this.files[this.files.findIndex(x => x.id === file.id)].isSensitive = sensitive;
-		},
-
-		updateFileName(file, name) {
-			this.files[this.files.findIndex(x => x.id === file.id)].name = name;
-		},
-
-		upload(file: File, name?: string) {
-			os.upload(file, this.$store.state.uploadFolder, name).then(res => {
-				this.files.push(res);
-			});
-		},
-
-		onPollUpdate(poll) {
-			this.poll = poll;
-			this.saveDraft();
-		},
-
-		setVisibility() {
-			if (this.channel) {
-				// TODO: information dialog
-				return;
-			}
-
-			os.popup(import('./visibility-picker.vue'), {
-				currentVisibility: this.visibility,
-				currentLocalOnly: this.localOnly,
-				src: this.$refs.visibilityButton
-			}, {
-				changeVisibility: visibility => {
-					this.visibility = visibility;
-					if (this.$store.state.rememberNoteVisibility) {
-						this.$store.set('visibility', visibility);
-					}
-				},
-				changeLocalOnly: localOnly => {
-					this.localOnly = localOnly;
-					if (this.$store.state.rememberNoteVisibility) {
-						this.$store.set('localOnly', localOnly);
-					}
-				}
-			}, 'closed');
-		},
-
-		addVisibleUser() {
-			os.selectUser().then(user => {
-				this.visibleUsers.push(user);
-			});
-		},
-
-		removeVisibleUser(user) {
-			this.visibleUsers = erase(user, this.visibleUsers);
-		},
-
-		clear() {
-			this.text = '';
-			this.files = [];
-			this.poll = null;
-			this.quoteId = null;
-		},
-
-		onKeydown(e: KeyboardEvent) {
-			if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
-			if (e.which === 27) this.$emit('esc');
-			this.typing();
-		},
-
-		onCompositionUpdate(e: CompositionEvent) {
-			this.imeText = e.data;
-			this.typing();
-		},
-
-		onCompositionEnd(e: CompositionEvent) {
-			this.imeText = '';
-		},
-
-		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.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
-					this.upload(file, formatted);
-				}
-			}
-
-			const paste = e.clipboardData.getData('text');
-
-			if (!this.renote && !this.quoteId && paste.startsWith(url + '/notes/')) {
-				e.preventDefault();
-
-				os.confirm({
-					type: 'info',
-					text: this.$ts.quoteQuestion,
-				}).then(({ canceled }) => {
-					if (canceled) {
-						insertTextAtCursor(this.$refs.text, paste);
-						return;
-					}
-
-					this.quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
-				});
-			}
-		},
-
-		onDragover(e) {
-			if (!e.dataTransfer.items[0]) return;
-			const isFile = e.dataTransfer.items[0].kind == 'file';
-			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
-			if (isFile || isDriveFile) {
-				e.preventDefault();
-				this.draghover = true;
-				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
-			}
-		},
-
-		onDragenter(e) {
-			this.draghover = true;
-		},
-
-		onDragleave(e) {
-			this.draghover = false;
-		},
-
-		onDrop(e): void {
-			this.draghover = false;
-
-			// ファイルだったら
-			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(_DATA_TRANSFER_DRIVE_FILE_);
-			if (driveFile != null && driveFile != '') {
-				const file = JSON.parse(driveFile);
-				this.files.push(file);
-				e.preventDefault();
-			}
-			//#endregion
-		},
-
-		saveDraft() {
-			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
-
-			data[this.draftKey] = {
-				updatedAt: new Date(),
-				data: {
-					text: this.text,
-					useCw: this.useCw,
-					cw: this.cw,
-					visibility: this.visibility,
-					localOnly: this.localOnly,
-					files: this.files,
-					poll: this.poll
-				}
-			};
-
-			localStorage.setItem('drafts', JSON.stringify(data));
-		},
-
-		deleteDraft() {
-			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
-
-			delete data[this.draftKey];
-
-			localStorage.setItem('drafts', JSON.stringify(data));
-		},
-
-		async post() {
-			let data = {
-				text: this.text == '' ? undefined : this.text,
-				fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
-				replyId: this.reply ? this.reply.id : undefined,
-				renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined,
-				channelId: this.channel ? this.channel.id : undefined,
-				poll: this.poll,
-				cw: this.useCw ? this.cw || '' : undefined,
-				localOnly: this.localOnly,
-				visibility: this.visibility,
-				visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
-			};
-
-			if (this.withHashtags && this.hashtags && this.hashtags.trim() !== '') {
-				const hashtags = this.hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
-				data.text = data.text ? `${data.text} ${hashtags}` : hashtags;
-			}
-
-			// plugin
-			if (notePostInterruptors.length > 0) {
-				for (const interruptor of notePostInterruptors) {
-					data = await interruptor.handler(JSON.parse(JSON.stringify(data)));
-				}
-			}
-
-			this.posting = true;
-			os.api('notes/create', data).then(() => {
-				this.clear();
-				this.$nextTick(() => {
-					this.deleteDraft();
-					this.$emit('posted');
-					if (data.text && data.text != '') {
-						const hashtags = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
-						const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
-						localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
-					}
-					this.posting = false;
-				});
-			}).catch(err => {
-				this.posting = false;
-				os.alert({
-					type: 'error',
-					text: err.message + '\n' + (err as any).id,
-				});
-			});
-		},
-
-		cancel() {
-			this.$emit('cancel');
-		},
-
-		insertMention() {
-			os.selectUser().then(user => {
-				insertTextAtCursor(this.$refs.text, '@' + Acct.toString(user) + ' ');
-			});
-		},
-
-		async insertEmoji(ev) {
-			os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
-		},
-
-		showActions(ev) {
-			os.popupMenu(postFormActions.map(action => ({
-				text: action.title,
-				action: () => {
-					action.handler({
-						text: this.text
-					}, (key, value) => {
-						if (key === 'text') { this.text = value; }
-					});
-				}
-			})), ev.currentTarget || ev.target);
 		}
 	}
+}
+
+if (props.specified) {
+	visibility = 'specified';
+	visibleUsers.push(props.specified);
+}
+
+// keep cw when reply
+if (defaultStore.state.keepCw && props.reply && props.reply.cw) {
+	useCw = true;
+	cw = props.reply.cw;
+}
+
+function watchForDraft() {
+	watch($$(text), () => saveDraft());
+	watch($$(useCw), () => saveDraft());
+	watch($$(cw), () => saveDraft());
+	watch($$(poll), () => saveDraft());
+	watch($$(files), () => saveDraft(), { deep: true });
+	watch($$(visibility), () => saveDraft());
+	watch($$(localOnly), () => saveDraft());
+}
+
+function checkMissingMention() {
+	if (visibility === 'specified') {
+		const ast = mfm.parse(text);
+
+		for (const x of extractMentions(ast)) {
+			if (!visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) {
+				hasNotSpecifiedMentions = true;
+				return;
+			}
+		}
+		hasNotSpecifiedMentions = false;
+	}
+}
+
+function addMissingMention() {
+	const ast = mfm.parse(text);
+
+	for (const x of extractMentions(ast)) {
+		if (!visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) {
+			os.api('users/show', { username: x.username, host: x.host }).then(user => {
+				visibleUsers.push(user);
+			});
+		}
+	}
+}
+
+function togglePoll() {
+	if (poll) {
+		poll = null;
+	} else {
+		poll = {
+			choices: ['', ''],
+			multiple: false,
+			expiresAt: null,
+			expiredAfter: null,
+		};
+	}
+}
+
+function addTag(tag: string) {
+	insertTextAtCursor(textareaEl, ` #${tag} `);
+}
+
+function focus() {
+	textareaEl.focus();
+}
+
+function chooseFileFrom(ev) {
+	selectFiles(ev.currentTarget || ev.target, i18n.locale.attachFile).then(files_ => {
+		for (const file of files_) {
+			files.push(file);
+		}
+	});
+}
+
+function detachFile(id) {
+	files = files.filter(x => x.id != id);
+}
+
+function updateFiles(_files) {
+	files = _files;
+}
+
+function updateFileSensitive(file, sensitive) {
+	files[files.findIndex(x => x.id === file.id)].isSensitive = sensitive;
+}
+
+function updateFileName(file, name) {
+	files[files.findIndex(x => x.id === file.id)].name = name;
+}
+
+function upload(file: File, name?: string) {
+	os.upload(file, defaultStore.state.uploadFolder, name).then(res => {
+		files.push(res);
+	});
+}
+
+function onPollUpdate(poll) {
+	poll = poll;
+	saveDraft();
+}
+
+function setVisibility() {
+	if (props.channel) {
+		// TODO: information dialog
+		return;
+	}
+
+	os.popup(import('./visibility-picker.vue'), {
+		currentVisibility: visibility,
+		currentLocalOnly: localOnly,
+		src: visibilityButton,
+	}, {
+		changeVisibility: v => {
+			visibility = v;
+			if (defaultStore.state.rememberNoteVisibility) {
+				defaultStore.set('visibility', visibility);
+			}
+		},
+		changeLocalOnly: v => {
+			localOnly = v;
+			if (defaultStore.state.rememberNoteVisibility) {
+				defaultStore.set('localOnly', localOnly);
+			}
+		}
+	}, 'closed');
+}
+
+function addVisibleUser() {
+	os.selectUser().then(user => {
+		visibleUsers.push(user);
+	});
+}
+
+function removeVisibleUser(user) {
+	visibleUsers = erase(user, visibleUsers);
+}
+
+function clear() {
+	text = '';
+	files = [];
+	poll = null;
+	quoteId = null;
+}
+
+function onKeydown(e: KeyboardEvent) {
+	if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && canPost) post();
+	if (e.which === 27) emit('esc');
+	typing();
+}
+
+function onCompositionUpdate(e: CompositionEvent) {
+	imeText = e.data;
+	typing();
+}
+
+function onCompositionEnd(e: CompositionEvent) {
+	imeText = '';
+}
+
+async function 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), defaultStore.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
+			upload(file, formatted);
+		}
+	}
+
+	const paste = e.clipboardData.getData('text');
+
+	if (!props.renote && !quoteId && paste.startsWith(url + '/notes/')) {
+		e.preventDefault();
+
+		os.confirm({
+			type: 'info',
+			text: i18n.locale.quoteQuestion,
+		}).then(({ canceled }) => {
+			if (canceled) {
+				insertTextAtCursor(textareaEl, paste);
+				return;
+			}
+
+			quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
+		});
+	}
+}
+
+function onDragover(e) {
+	if (!e.dataTransfer.items[0]) return;
+	const isFile = e.dataTransfer.items[0].kind == 'file';
+	const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+	if (isFile || isDriveFile) {
+		e.preventDefault();
+		draghover = true;
+		e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+	}
+}
+
+function onDragenter(e) {
+	draghover = true;
+}
+
+function onDragleave(e) {
+	draghover = false;
+}
+
+function onDrop(e): void {
+	draghover = false;
+
+	// ファイルだったら
+	if (e.dataTransfer.files.length > 0) {
+		e.preventDefault();
+		for (const x of Array.from(e.dataTransfer.files)) upload(x);
+		return;
+	}
+
+	//#region ドライブのファイル
+	const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+	if (driveFile != null && driveFile != '') {
+		const file = JSON.parse(driveFile);
+		files.push(file);
+		e.preventDefault();
+	}
+	//#endregion
+}
+
+function saveDraft() {
+	const data = JSON.parse(localStorage.getItem('drafts') || '{}');
+
+	data[draftKey] = {
+		updatedAt: new Date(),
+		data: {
+			text: text,
+			useCw: useCw,
+			cw: cw,
+			visibility: visibility,
+			localOnly: localOnly,
+			files: files,
+			poll: poll
+		}
+	};
+
+	localStorage.setItem('drafts', JSON.stringify(data));
+}
+
+function deleteDraft() {
+	const data = JSON.parse(localStorage.getItem('drafts') || '{}');
+
+	delete data[draftKey];
+
+	localStorage.setItem('drafts', JSON.stringify(data));
+}
+
+async function post() {
+	let data = {
+		text: text == '' ? undefined : text,
+		fileIds: files.length > 0 ? files.map(f => f.id) : undefined,
+		replyId: props.reply ? props.reply.id : undefined,
+		renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined,
+		channelId: props.channel ? props.channel.id : undefined,
+		poll: poll,
+		cw: useCw ? cw || '' : undefined,
+		localOnly: localOnly,
+		visibility: visibility,
+		visibleUserIds: visibility == 'specified' ? visibleUsers.map(u => u.id) : undefined,
+	};
+
+	if (withHashtags && hashtags && hashtags.trim() !== '') {
+		const hashtags = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
+		data.text = data.text ? `${data.text} ${hashtags}` : hashtags;
+	}
+
+	// plugin
+	if (notePostInterruptors.length > 0) {
+		for (const interruptor of notePostInterruptors) {
+			data = await interruptor.handler(JSON.parse(JSON.stringify(data)));
+		}
+	}
+
+	let token = undefined;
+
+	if (postAccount) {
+		const storedAccounts = await getAccounts();
+		token = storedAccounts.find(x => x.id === postAccount.id)?.token;
+	}
+
+	posting = true;
+	os.api('notes/create', data, token).then(() => {
+		clear();
+		nextTick(() => {
+			deleteDraft();
+			emit('posted');
+			if (data.text && data.text != '') {
+				const hashtags = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
+				const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
+				localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
+			}
+			posting = false;
+			postAccount = null;
+		});
+	}).catch(err => {
+		posting = false;
+		os.alert({
+			type: 'error',
+			text: err.message + '\n' + (err as any).id,
+		});
+	});
+}
+
+function cancel() {
+	emit('cancel');
+}
+
+function insertMention() {
+	os.selectUser().then(user => {
+		insertTextAtCursor(textareaEl, '@' + Acct.toString(user) + ' ');
+	});
+}
+
+async function insertEmoji(ev: MouseEvent) {
+	os.openEmojiPicker(ev.currentTarget || ev.target, {}, textareaEl);
+}
+
+function showActions(ev) {
+	os.popupMenu(postFormActions.map(action => ({
+		text: action.title,
+		action: () => {
+			action.handler({
+				text: text
+			}, (key, value) => {
+				if (key === 'text') { text = value; }
+			});
+		}
+	})), ev.currentTarget || ev.target);
+}
+
+let postAccount = $ref<misskey.entities.UserDetailed | null>(null);
+
+function openAccountMenu(ev: MouseEvent) {
+	openAccountMenu_({
+		withExtraOperation: false,
+		includeCurrentAccount: true,
+		active: postAccount != null ? postAccount.id : $i.id,
+		onChoose: (account) => {
+			if (account.id === $i.id) {
+				postAccount = null;
+			} else {
+				postAccount = account;
+			}
+		},
+	}, ev);
+}
+
+onMounted(() => {
+	if (props.autofocus) {
+		focus();
+
+		nextTick(() => {
+			focus();
+		});
+	}
+
+	// TODO: detach when unmount
+	new Autocomplete(textareaEl, $$(text));
+	new Autocomplete(cwInputEl, $$(cw));
+	new Autocomplete(hashtagsInputEl, $$(hashtags));
+
+	nextTick(() => {
+		// 書きかけの投稿を復元
+		if (!props.share && !props.mention && !props.specified) {
+			const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[draftKey];
+			if (draft) {
+				text = draft.data.text;
+				useCw = draft.data.useCw;
+				cw = draft.data.cw;
+				visibility = draft.data.visibility;
+				localOnly = draft.data.localOnly;
+				files = (draft.data.files || []).filter(e => e);
+				if (draft.data.poll) {
+					poll = draft.data.poll;
+				}
+			}
+		}
+
+		// 削除して編集
+		if (props.initialNote) {
+			const init = props.initialNote;
+			text = init.text ? init.text : '';
+			files = init.files;
+			cw = init.cw;
+			useCw = init.cw != null;
+			if (init.poll) {
+				poll = {
+					choices: init.poll.choices.map(x => x.text),
+					multiple: init.poll.multiple,
+					expiresAt: init.poll.expiresAt,
+					expiredAfter: init.poll.expiredAfter,
+				};
+			}
+			visibility = init.visibility;
+			localOnly = init.localOnly;
+			quoteId = init.renote ? init.renote.id : null;
+		}
+
+		nextTick(() => watchForDraft());
+	});
 });
 </script>
 
@@ -742,6 +706,19 @@ export default defineComponent({
 			line-height: 66px;
 		}
 
+		> .account {
+			height: 100%;
+			aspect-ratio: 1/1;
+			display: inline-flex;
+			vertical-align: bottom;
+
+			> .avatar {
+				width: 28px;
+				height: 28px;
+				margin: auto;
+			}
+		}
+
 		> div {
 			position: absolute;
 			top: 0;
diff --git a/packages/client/src/components/reaction-icon.vue b/packages/client/src/components/reaction-icon.vue
index c0ec955e32..5638c9a816 100644
--- a/packages/client/src/components/reaction-icon.vue
+++ b/packages/client/src/components/reaction-icon.vue
@@ -1,25 +1,13 @@
 <template>
-<MkEmoji :emoji="reaction" :custom-emojis="customEmojis" :is-reaction="true" :normal="true" :no-style="noStyle"/>
+<MkEmoji :emoji="reaction" :custom-emojis="customEmojis || []" :is-reaction="true" :normal="true" :no-style="noStyle"/>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 
-export default defineComponent({
-	props: {
-		reaction: {
-			type: String,
-			required: true
-		},
-		customEmojis: {
-			required: false,
-			default: () => []
-		},
-		noStyle: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-});
+const props = defineProps<{
+	reaction: string;
+	customEmojis?: any[]; // TODO
+	noStyle?: boolean;
+}>();
 </script>
diff --git a/packages/client/src/components/reaction-tooltip.vue b/packages/client/src/components/reaction-tooltip.vue
index dda8e7c6d7..1b2a024e21 100644
--- a/packages/client/src/components/reaction-tooltip.vue
+++ b/packages/client/src/components/reaction-tooltip.vue
@@ -1,5 +1,5 @@
 <template>
-<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="$emit('closed')">
+<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')">
 	<div class="beeadbfb">
 		<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
 		<div class="name">{{ reaction.replace('@.', '') }}</div>
@@ -7,31 +7,20 @@
 </MkTooltip>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import MkTooltip from './ui/tooltip.vue';
 import XReactionIcon from './reaction-icon.vue';
 
-export default defineComponent({
-	components: {
-		MkTooltip,
-		XReactionIcon,
-	},
-	props: {
-		reaction: {
-			type: String,
-			required: true,
-		},
-		emojis: {
-			type: Array,
-			required: true,
-		},
-		source: {
-			required: true,
-		}
-	},
-	emits: ['closed'],
-})
+const props = defineProps<{
+	reaction: string;
+	emojis: any[]; // TODO
+	source: any; // TODO
+}>();
+
+const emit = defineEmits<{
+	(e: 'closed'): void;
+}>();
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/reactions-viewer.details.vue b/packages/client/src/components/reactions-viewer.details.vue
index d6374517a2..8cec8dfa2f 100644
--- a/packages/client/src/components/reactions-viewer.details.vue
+++ b/packages/client/src/components/reactions-viewer.details.vue
@@ -1,5 +1,5 @@
 <template>
-<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="$emit('closed')">
+<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')">
 	<div class="bqxuuuey">
 		<div class="reaction">
 			<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
@@ -16,39 +16,22 @@
 </MkTooltip>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import MkTooltip from './ui/tooltip.vue';
 import XReactionIcon from './reaction-icon.vue';
 
-export default defineComponent({
-	components: {
-		MkTooltip,
-		XReactionIcon
-	},
-	props: {
-		reaction: {
-			type: String,
-			required: true,
-		},
-		users: {
-			type: Array,
-			required: true,
-		},
-		count: {
-			type: Number,
-			required: true,
-		},
-		emojis: {
-			type: Array,
-			required: true,
-		},
-		source: {
-			required: true,
-		}
-	},
-	emits: ['closed'],
-})
+const props = defineProps<{
+	reaction: string;
+	users: any[]; // TODO
+	count: number;
+	emojis: any[]; // TODO
+	source: any; // TODO
+}>();
+
+const emit = defineEmits<{
+	(e: 'closed'): void;
+}>();
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/reactions-viewer.vue b/packages/client/src/components/reactions-viewer.vue
index 59fcbb7129..a9bf51f65f 100644
--- a/packages/client/src/components/reactions-viewer.vue
+++ b/packages/client/src/components/reactions-viewer.vue
@@ -4,31 +4,19 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as misskey from 'misskey-js';
+import { $i } from '@/account';
 import XReaction from './reactions-viewer.reaction.vue';
 
-export default defineComponent({
-	components: {
-		XReaction
-	},
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-	},
-	data() {
-		return {
-			initialReactions: new Set(Object.keys(this.note.reactions))
-		};
-	},
-	computed: {
-		isMe(): boolean {
-			return this.$i && this.$i.id === this.note.userId;
-		},
-	},
-});
+const props = defineProps<{
+	note: misskey.entities.Note;
+}>();
+
+const initialReactions = new Set(Object.keys(props.note.reactions));
+
+const isMe = computed(() => $i && $i.id === props.note.userId);
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/renote.details.vue b/packages/client/src/components/renote.details.vue
index e3ef15c753..cdbc71bdce 100644
--- a/packages/client/src/components/renote.details.vue
+++ b/packages/client/src/components/renote.details.vue
@@ -1,5 +1,5 @@
 <template>
-<MkTooltip ref="tooltip" :source="source" :max-width="250" @closed="$emit('closed')">
+<MkTooltip ref="tooltip" :source="source" :max-width="250" @closed="emit('closed')">
 	<div class="beaffaef">
 		<div v-for="u in users" :key="u.id" class="user">
 			<MkAvatar class="avatar" :user="u"/>
@@ -10,29 +10,19 @@
 </MkTooltip>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import MkTooltip from './ui/tooltip.vue';
 
-export default defineComponent({
-	components: {
-		MkTooltip,
-	},
-	props: {
-		users: {
-			type: Array,
-			required: true,
-		},
-		count: {
-			type: Number,
-			required: true,
-		},
-		source: {
-			required: true,
-		}
-	},
-	emits: ['closed'],
-})
+const props = defineProps<{
+	users: any[]; // TODO
+	count: number;
+	source: any; // TODO
+}>();
+
+const emit = defineEmits<{
+	(e: 'closed'): void;
+}>();
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/ripple.vue b/packages/client/src/components/ripple.vue
index 272eacbc6e..401e78e304 100644
--- a/packages/client/src/components/ripple.vue
+++ b/packages/client/src/components/ripple.vue
@@ -94,7 +94,7 @@ export default defineComponent({
 		}
 
 		onMounted(() => {
-			setTimeout(() => {
+			window.setTimeout(() => {
 				context.emit('end');
 			}, 1100);
 		});
diff --git a/packages/client/src/components/signin-dialog.vue b/packages/client/src/components/signin-dialog.vue
index 2edd10f539..5c2048e7b0 100644
--- a/packages/client/src/components/signin-dialog.vue
+++ b/packages/client/src/components/signin-dialog.vue
@@ -2,8 +2,8 @@
 <XModalWindow ref="dialog"
 	:width="370"
 	:height="400"
-	@close="$refs.dialog.close()"
-	@closed="$emit('closed')"
+	@close="dialog.close()"
+	@closed="emit('closed')"
 >
 	<template #header>{{ $ts.login }}</template>
 
@@ -11,32 +11,26 @@
 </XModalWindow>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import XModalWindow from '@/components/ui/modal-window.vue';
 import MkSignin from './signin.vue';
 
-export default defineComponent({
-	components: {
-		MkSignin,
-		XModalWindow,
-	},
-
-	props: {
-		autoSet: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
-
-	emits: ['done', 'closed'],
-
-	methods: {
-		onLogin(res) {
-			this.$emit('done', res);
-			this.$refs.dialog.close();
-		}
-	}
+const props = withDefaults(defineProps<{
+	autoSet?: boolean;
+}>(), {
+	autoSet: false,
 });
+
+const emit = defineEmits<{
+	(e: 'done'): void;
+	(e: 'closed'): void;
+}>();
+
+const dialog = $ref<InstanceType<typeof XModalWindow>>();
+
+function onLogin(res) {
+	emit('done', res);
+	dialog.close();
+}
 </script>
diff --git a/packages/client/src/components/signup-dialog.vue b/packages/client/src/components/signup-dialog.vue
index 30fe3bf7d3..bda2495ba7 100644
--- a/packages/client/src/components/signup-dialog.vue
+++ b/packages/client/src/components/signup-dialog.vue
@@ -2,7 +2,7 @@
 <XModalWindow ref="dialog"
 	:width="366"
 	:height="500"
-	@close="$refs.dialog.close()"
+	@close="dialog.close()"
 	@closed="$emit('closed')"
 >
 	<template #header>{{ $ts.signup }}</template>
@@ -15,36 +15,30 @@
 </XModalWindow>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import XModalWindow from '@/components/ui/modal-window.vue';
 import XSignup from './signup.vue';
 
-export default defineComponent({
-	components: {
-		XSignup,
-		XModalWindow,
-	},
-
-	props: {
-		autoSet: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
-
-	emits: ['done', 'closed'],
-
-	methods: {
-		onSignup(res) {
-			this.$emit('done', res);
-			this.$refs.dialog.close();
-		},
-
-		onSignupEmailPending() {
-			this.$refs.dialog.close();
-		}
-	}
+const props = withDefaults(defineProps<{
+	autoSet?: boolean;
+}>(), {
+	autoSet: false,
 });
+
+const emit = defineEmits<{
+	(e: 'done'): void;
+	(e: 'closed'): void;
+}>();
+
+const dialog = $ref<InstanceType<typeof XModalWindow>>();
+
+function onSignup(res) {
+	emit('done', res);
+	dialog.close();
+}
+
+function onSignupEmailPending() {
+	dialog.close();
+}
 </script>
diff --git a/packages/client/src/components/sub-note-content.vue b/packages/client/src/components/sub-note-content.vue
index efa202ce2f..d6a37d07be 100644
--- a/packages/client/src/components/sub-note-content.vue
+++ b/packages/client/src/components/sub-note-content.vue
@@ -21,35 +21,21 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import XPoll from './poll.vue';
 import XMediaList from './media-list.vue';
-import * as os from '@/os';
+import * as misskey from 'misskey-js';
 
-export default defineComponent({
-	components: {
-		XPoll,
-		XMediaList,
-	},
-	props: {
-		note: {
-			type: Object,
-			required: true
-		}
-	},
-	data() {
-		return {
-			collapsed: false,
-		};
-	},
-	created() {
-		this.collapsed = this.note.cw == null && this.note.text && (
-			(this.note.text.split('\n').length > 9) ||
-			(this.note.text.length > 500)
-		);
-	}
-});
+const props = defineProps<{
+	note: misskey.entities.Note;
+}>();
+
+const collapsed = $ref(
+	props.note.cw == null && props.note.text != null && (
+		(props.note.text.split('\n').length > 9) ||
+		(props.note.text.length > 500)
+	));
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/timeline.vue b/packages/client/src/components/timeline.vue
index 53697671b2..59956b9526 100644
--- a/packages/client/src/components/timeline.vue
+++ b/packages/client/src/components/timeline.vue
@@ -1,184 +1,143 @@
 <template>
-<XNotes ref="tl" :no-gap="!$store.state.showGapBetweenNotesInTimeline" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)" @queue="$emit('queue', $event)"/>
+<XNotes ref="tlComponent" :no-gap="!$store.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)"/>
 </template>
 
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { ref, computed, provide, onUnmounted } from 'vue';
 import XNotes from './notes.vue';
 import * as os from '@/os';
 import { stream } from '@/stream';
 import * as sound from '@/scripts/sound';
+import { $i } from '@/account';
 
-export default defineComponent({
-	components: {
-		XNotes
-	},
+const props = defineProps<{
+	src: string;
+	list?: string;
+	antenna?: string;
+	channel?: string;
+	sound?: boolean;
+}>();
 
-	provide() {
-		return {
-			inChannel: this.src === 'channel'
-		};
-	},
+const emit = defineEmits<{
+	(e: 'note'): void;
+	(e: 'queue', count: number): void;
+}>();
 
-	props: {
-		src: {
-			type: String,
-			required: true
-		},
-		list: {
-			type: String,
-			required: false
-		},
-		antenna: {
-			type: String,
-			required: false
-		},
-		channel: {
-			type: String,
-			required: false
-		},
-		sound: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
+provide('inChannel', computed(() => props.src === 'channel'));
 
-	emits: ['note', 'queue', 'before', 'after'],
+const tlComponent: InstanceType<typeof XNotes> = $ref();
 
-	data() {
-		return {
-			connection: null,
-			connection2: null,
-			pagination: null,
-			baseQuery: {
-				includeMyRenotes: this.$store.state.showMyRenotes,
-				includeRenotedMyNotes: this.$store.state.showRenotedMyNotes,
-				includeLocalRenotes: this.$store.state.showLocalRenotes
-			},
-			query: {},
-			date: null
-		};
-	},
+const prepend = note => {
+	tlComponent.pagingComponent?.prepend(note);
 
-	created() {
-		const prepend = note => {
-			(this.$refs.tl as any).prepend(note);
+	emit('note');
 
-			this.$emit('note');
-
-			if (this.sound) {
-				sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
-			}
-		};
-
-		const onUserAdded = () => {
-			(this.$refs.tl as any).reload();
-		};
-
-		const onUserRemoved = () => {
-			(this.$refs.tl as any).reload();
-		};
-
-		const onChangeFollowing = () => {
-			if (!this.$refs.tl.backed) {
-				this.$refs.tl.reload();
-			}
-		};
-
-		let endpoint;
-
-		if (this.src == 'antenna') {
-			endpoint = 'antennas/notes';
-			this.query = {
-				antennaId: this.antenna
-			};
-			this.connection = markRaw(stream.useChannel('antenna', {
-				antennaId: this.antenna
-			}));
-			this.connection.on('note', prepend);
-		} else if (this.src == 'home') {
-			endpoint = 'notes/timeline';
-			this.connection = markRaw(stream.useChannel('homeTimeline'));
-			this.connection.on('note', prepend);
-
-			this.connection2 = markRaw(stream.useChannel('main'));
-			this.connection2.on('follow', onChangeFollowing);
-			this.connection2.on('unfollow', onChangeFollowing);
-		} else if (this.src == 'local') {
-			endpoint = 'notes/local-timeline';
-			this.connection = markRaw(stream.useChannel('localTimeline'));
-			this.connection.on('note', prepend);
-		} else if (this.src == 'social') {
-			endpoint = 'notes/hybrid-timeline';
-			this.connection = markRaw(stream.useChannel('hybridTimeline'));
-			this.connection.on('note', prepend);
-		} else if (this.src == 'global') {
-			endpoint = 'notes/global-timeline';
-			this.connection = markRaw(stream.useChannel('globalTimeline'));
-			this.connection.on('note', prepend);
-		} else if (this.src == 'mentions') {
-			endpoint = 'notes/mentions';
-			this.connection = markRaw(stream.useChannel('main'));
-			this.connection.on('mention', prepend);
-		} else if (this.src == 'directs') {
-			endpoint = 'notes/mentions';
-			this.query = {
-				visibility: 'specified'
-			};
-			const onNote = note => {
-				if (note.visibility == 'specified') {
-					prepend(note);
-				}
-			};
-			this.connection = markRaw(stream.useChannel('main'));
-			this.connection.on('mention', onNote);
-		} else if (this.src == 'list') {
-			endpoint = 'notes/user-list-timeline';
-			this.query = {
-				listId: this.list
-			};
-			this.connection = markRaw(stream.useChannel('userList', {
-				listId: this.list
-			}));
-			this.connection.on('note', prepend);
-			this.connection.on('userAdded', onUserAdded);
-			this.connection.on('userRemoved', onUserRemoved);
-		} else if (this.src == 'channel') {
-			endpoint = 'channels/timeline';
-			this.query = {
-				channelId: this.channel
-			};
-			this.connection = markRaw(stream.useChannel('channel', {
-				channelId: this.channel
-			}));
-			this.connection.on('note', prepend);
-		}
-
-		this.pagination = {
-			endpoint: endpoint,
-			limit: 10,
-			params: init => ({
-				untilDate: this.date?.getTime(),
-				...this.baseQuery, ...this.query
-			})
-		};
-	},
-
-	beforeUnmount() {
-		this.connection.dispose();
-		if (this.connection2) this.connection2.dispose();
-	},
-
-	methods: {
-		focus() {
-			this.$refs.tl.focus();
-		},
-
-		timetravel(date?: Date) {
-			this.date = date;
-			this.$refs.tl.reload();
-		}
+	if (props.sound) {
+		sound.play($i && (note.userId === $i.id) ? 'noteMy' : 'note');
 	}
+};
+
+const onUserAdded = () => {
+	tlComponent.pagingComponent?.reload();
+};
+
+const onUserRemoved = () => {
+	tlComponent.pagingComponent?.reload();
+};
+
+const onChangeFollowing = () => {
+	if (!tlComponent.pagingComponent?.backed) {
+		tlComponent.pagingComponent?.reload();
+	}
+};
+
+let endpoint;
+let query;
+let connection;
+let connection2;
+
+if (props.src === 'antenna') {
+	endpoint = 'antennas/notes';
+	query = {
+		antennaId: props.antenna
+	};
+	connection = stream.useChannel('antenna', {
+		antennaId: props.antenna
+	});
+	connection.on('note', prepend);
+} else if (props.src === 'home') {
+	endpoint = 'notes/timeline';
+	connection = stream.useChannel('homeTimeline');
+	connection.on('note', prepend);
+
+	connection2 = stream.useChannel('main');
+	connection2.on('follow', onChangeFollowing);
+	connection2.on('unfollow', onChangeFollowing);
+} else if (props.src === 'local') {
+	endpoint = 'notes/local-timeline';
+	connection = stream.useChannel('localTimeline');
+	connection.on('note', prepend);
+} else if (props.src === 'social') {
+	endpoint = 'notes/hybrid-timeline';
+	connection = stream.useChannel('hybridTimeline');
+	connection.on('note', prepend);
+} else if (props.src === 'global') {
+	endpoint = 'notes/global-timeline';
+	connection = stream.useChannel('globalTimeline');
+	connection.on('note', prepend);
+} else if (props.src === 'mentions') {
+	endpoint = 'notes/mentions';
+	connection = stream.useChannel('main');
+	connection.on('mention', prepend);
+} else if (props.src === 'directs') {
+	endpoint = 'notes/mentions';
+	query = {
+		visibility: 'specified'
+	};
+	const onNote = note => {
+		if (note.visibility == 'specified') {
+			prepend(note);
+		}
+	};
+	connection = stream.useChannel('main');
+	connection.on('mention', onNote);
+} else if (props.src === 'list') {
+	endpoint = 'notes/user-list-timeline';
+	query = {
+		listId: props.list
+	};
+	connection = stream.useChannel('userList', {
+		listId: props.list
+	});
+	connection.on('note', prepend);
+	connection.on('userAdded', onUserAdded);
+	connection.on('userRemoved', onUserRemoved);
+} else if (props.src === 'channel') {
+	endpoint = 'channels/timeline';
+	query = {
+		channelId: props.channel
+	};
+	connection = stream.useChannel('channel', {
+		channelId: props.channel
+	});
+	connection.on('note', prepend);
+}
+
+const pagination = {
+	endpoint: endpoint,
+	limit: 10,
+	params: query,
+};
+
+onUnmounted(() => {
+	connection.dispose();
+	if (connection2) connection2.dispose();
 });
+
+/* TODO
+const timetravel = (date?: Date) => {
+	this.date = date;
+	this.$refs.tl.reload();
+};
+*/
 </script>
diff --git a/packages/client/src/components/toast.vue b/packages/client/src/components/toast.vue
index 869182d8e1..031aa45633 100644
--- a/packages/client/src/components/toast.vue
+++ b/packages/client/src/components/toast.vue
@@ -26,7 +26,7 @@ const showing = ref(true);
 const zIndex = os.claimZIndex('high');
 
 onMounted(() => {
-	setTimeout(() => {
+	window.setTimeout(() => {
 		showing.value = false;
 	}, 4000);
 });
diff --git a/packages/client/src/components/ui/button.vue b/packages/client/src/components/ui/button.vue
index 804a2e2720..c7b6c8ba96 100644
--- a/packages/client/src/components/ui/button.vue
+++ b/packages/client/src/components/ui/button.vue
@@ -117,14 +117,14 @@ export default defineComponent({
 
 			const scale = calcCircleScale(e.target.clientWidth, e.target.clientHeight, circleCenterX, circleCenterY);
 
-			setTimeout(() => {
+			window.setTimeout(() => {
 				ripple.style.transform = 'scale(' + (scale / 2) + ')';
 			}, 1);
-			setTimeout(() => {
+			window.setTimeout(() => {
 				ripple.style.transition = 'all 1s ease';
 				ripple.style.opacity = '0';
 			}, 1000);
-			setTimeout(() => {
+			window.setTimeout(() => {
 				if (this.$refs.ripples) this.$refs.ripples.removeChild(ripple);
 			}, 2000);
 		}
diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue
index 6f3f277b11..41165c8d33 100644
--- a/packages/client/src/components/ui/menu.vue
+++ b/packages/client/src/components/ui/menu.vue
@@ -24,7 +24,7 @@
 			<span>{{ item.text }}</span>
 			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
 		</a>
-		<button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" @click="clicked(item.action, $event)">
+		<button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)">
 			<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
 			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
 		</button>
diff --git a/packages/client/src/components/ui/modal.vue b/packages/client/src/components/ui/modal.vue
index 3e2e59b27c..c691c8c6d0 100644
--- a/packages/client/src/components/ui/modal.vue
+++ b/packages/client/src/components/ui/modal.vue
@@ -211,7 +211,7 @@ export default defineComponent({
 				contentClicking = true;
 				window.addEventListener('mouseup', e => {
 					// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
-					setTimeout(() => {
+					window.setTimeout(() => {
 						contentClicking = false;
 					}, 100);
 				}, { passive: true, once: true });
diff --git a/packages/client/src/components/ui/pagination.vue b/packages/client/src/components/ui/pagination.vue
index 64af4a54f7..9c18fc5ce5 100644
--- a/packages/client/src/components/ui/pagination.vue
+++ b/packages/client/src/components/ui/pagination.vue
@@ -13,43 +13,269 @@
 		</slot>
 	</div>
 
-	<div v-else class="cxiknjgy">
+	<div v-else ref="rootEl">
 		<slot :items="items"></slot>
-		<div v-show="more" key="_more_" class="more _gap">
-			<MkButton v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
-				<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
-				<template v-if="moreFetching"><MkLoading inline/></template>
+		<div v-show="more" key="_more_" class="cxiknjgy _gap">
+			<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
+				{{ $ts.loadMore }}
 			</MkButton>
+			<MkLoading v-else class="loading"/>
 		</div>
 	</div>
 </transition>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from './button.vue';
-import paging from '@/scripts/paging';
+<script lang="ts" setup>
+import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, watch } from 'vue';
+import * as misskey from 'misskey-js';
+import * as os from '@/os';
+import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
+import MkButton from '@/components/ui/button.vue';
 
-export default defineComponent({
-	components: {
-		MkButton
-	},
+const SECOND_FETCH_LIMIT = 30;
 
-	mixins: [
-		paging({}),
-	],
+export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> = {
+	endpoint: E;
+	limit: number;
+	params?: misskey.Endpoints[E]['req'] | ComputedRef<misskey.Endpoints[E]['req']>;
 
-	props: {
-		pagination: {
-			required: true
-		},
+	/**
+	 * 検索APIのような、ページング不可なエンドポイントを利用する場合
+	 * (そのようなAPIをこの関数で使うのは若干矛盾してるけど)
+	 */
+	noPaging?: boolean;
 
-		disableAutoLoad: {
-			type: Boolean,
-			required: false,
-			default: false,
+	/**
+	 * items 配列の中身を逆順にする(新しい方が最後)
+	 */
+	reversed?: boolean;
+
+	offsetMode?: boolean;
+};
+
+const props = withDefaults(defineProps<{
+	pagination: Paging;
+	disableAutoLoad?: boolean;
+	displayLimit?: number;
+}>(), {
+	displayLimit: 30,
+});
+
+const emit = defineEmits<{
+	(e: 'queue', count: number): void;
+}>();
+
+type Item = { id: string; [another: string]: unknown; };
+
+const rootEl = ref<HTMLElement>();
+const items = ref<Item[]>([]);
+const queue = ref<Item[]>([]);
+const offset = ref(0);
+const fetching = ref(true);
+const moreFetching = ref(false);
+const more = ref(false);
+const backed = ref(false); // 遡り中か否か
+const isBackTop = ref(false);
+const empty = computed(() => items.value.length === 0);
+const error = ref(false);
+
+const init = async (): Promise<void> => {
+	queue.value = [];
+	fetching.value = true;
+	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
+	await os.api(props.pagination.endpoint, {
+		...params,
+		limit: props.pagination.noPaging ? (props.pagination.limit || 10) : (props.pagination.limit || 10) + 1,
+	}).then(res => {
+		for (let i = 0; i < res.length; i++) {
+			const item = res[i];
+			if (props.pagination.reversed) {
+				if (i === res.length - 2) item._shouldInsertAd_ = true;
+			} else {
+				if (i === 3) item._shouldInsertAd_ = true;
+			}
 		}
-	},
+		if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) {
+			res.pop();
+			items.value = props.pagination.reversed ? [...res].reverse() : res;
+			more.value = true;
+		} else {
+			items.value = props.pagination.reversed ? [...res].reverse() : res;
+			more.value = false;
+		}
+		offset.value = res.length;
+		error.value = false;
+		fetching.value = false;
+	}, e => {
+		error.value = true;
+		fetching.value = false;
+	});
+};
+
+const reload = (): void => {
+	items.value = [];
+	init();
+};
+
+const fetchMore = async (): Promise<void> => {
+	if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
+	moreFetching.value = true;
+	backed.value = true;
+	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
+	await os.api(props.pagination.endpoint, {
+		...params,
+		limit: SECOND_FETCH_LIMIT + 1,
+		...(props.pagination.offsetMode ? {
+			offset: offset.value,
+		} : {
+			untilId: props.pagination.reversed ? items.value[0].id : items.value[items.value.length - 1].id,
+		}),
+	}).then(res => {
+		for (let i = 0; i < res.length; i++) {
+			const item = res[i];
+			if (props.pagination.reversed) {
+				if (i === res.length - 9) item._shouldInsertAd_ = true;
+			} else {
+				if (i === 10) item._shouldInsertAd_ = true;
+			}
+		}
+		if (res.length > SECOND_FETCH_LIMIT) {
+			res.pop();
+			items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
+			more.value = true;
+		} else {
+			items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
+			more.value = false;
+		}
+		offset.value += res.length;
+		moreFetching.value = false;
+	}, e => {
+		moreFetching.value = false;
+	});
+};
+
+const fetchMoreAhead = async (): Promise<void> => {
+	if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
+	moreFetching.value = true;
+	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
+	await os.api(props.pagination.endpoint, {
+		...params,
+		limit: SECOND_FETCH_LIMIT + 1,
+		...(props.pagination.offsetMode ? {
+			offset: offset.value,
+		} : {
+			sinceId: props.pagination.reversed ? items.value[0].id : items.value[items.value.length - 1].id,
+		}),
+	}).then(res => {
+		if (res.length > SECOND_FETCH_LIMIT) {
+			res.pop();
+			items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
+			more.value = true;
+		} else {
+			items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
+			more.value = false;
+		}
+		offset.value += res.length;
+		moreFetching.value = false;
+	}, e => {
+		moreFetching.value = false;
+	});
+};
+
+const prepend = (item: Item): void => {
+	if (props.pagination.reversed) {
+		if (rootEl.value) {
+			const container = getScrollContainer(rootEl.value);
+			if (container == null) return; // TODO?
+
+			const pos = getScrollPosition(rootEl.value);
+			const viewHeight = container.clientHeight;
+			const height = container.scrollHeight;
+			const isBottom = (pos + viewHeight > height - 32);
+			if (isBottom) {
+				// オーバーフローしたら古いアイテムは捨てる
+				if (items.value.length >= props.displayLimit) {
+					// このやり方だとVue 3.2以降アニメーションが動かなくなる
+					//items.value = items.value.slice(-props.displayLimit);
+					while (items.value.length >= props.displayLimit) {
+						items.value.shift();
+					}
+					more.value = true;
+				}
+			}
+		}
+		items.value.push(item);
+		// TODO
+	} else {
+		// 初回表示時はunshiftだけでOK
+		if (!rootEl.value) {
+			items.value.unshift(item);
+			return;
+		}
+
+		const isTop = isBackTop.value || (document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
+
+		if (isTop) {
+			// Prepend the item
+			items.value.unshift(item);
+
+			// オーバーフローしたら古いアイテムは捨てる
+			if (items.value.length >= props.displayLimit) {
+				// このやり方だとVue 3.2以降アニメーションが動かなくなる
+				//this.items = items.value.slice(0, props.displayLimit);
+				while (items.value.length >= props.displayLimit) {
+					items.value.pop();
+				}
+				more.value = true;
+			}
+		} else {
+			queue.value.push(item);
+			onScrollTop(rootEl.value, () => {
+				for (const item of queue.value) {
+					prepend(item);
+				}
+				queue.value = [];
+			});
+		}
+	}
+};
+
+const append = (item: Item): void => {
+	items.value.push(item);
+};
+
+const updateItem = (id: Item['id'], replacer: (old: Item) => Item): void => {
+	const i = items.value.findIndex(item => item.id === id);
+	items.value[i] = replacer(items.value[i]);
+};
+
+if (props.pagination.params && isRef(props.pagination.params)) {
+	watch(props.pagination.params, init, { deep: true });
+}
+
+watch(queue, (a, b) => {
+	if (a.length === 0 && b.length === 0) return;
+	emit('queue', queue.value.length);
+}, { deep: true });
+
+init();
+
+onActivated(() => {
+	isBackTop.value = false;
+});
+
+onDeactivated(() => {
+	isBackTop.value = window.scrollY === 0;
+});
+
+defineExpose({
+	items,
+	backed,
+	reload,
+	fetchMoreAhead,
+	prepend,
+	append,
+	updateItem,
 });
 </script>
 
@@ -64,11 +290,9 @@ export default defineComponent({
 }
 
 .cxiknjgy {
-	> .more > .button {
+	> .button {
 		margin-left: auto;
 		margin-right: auto;
-		height: 48px;
-		min-width: 150px;
 	}
 }
 </style>
diff --git a/packages/client/src/components/ui/window.vue b/packages/client/src/components/ui/window.vue
index bd33289ccc..fa32ecfdef 100644
--- a/packages/client/src/components/ui/window.vue
+++ b/packages/client/src/components/ui/window.vue
@@ -147,9 +147,9 @@ export default defineComponent({
 			}
 		},
 
-		onContextmenu(e) {
+		onContextmenu(ev: MouseEvent) {
 			if (this.contextmenu) {
-				os.contextMenu(this.contextmenu, e);
+				os.contextMenu(this.contextmenu, ev);
 			}
 		},
 
diff --git a/packages/client/src/components/url-preview.vue b/packages/client/src/components/url-preview.vue
index dff74800ed..bf3b358797 100644
--- a/packages/client/src/components/url-preview.vue
+++ b/packages/client/src/components/url-preview.vue
@@ -4,7 +4,7 @@
 	<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="tweetId && tweetExpanded" ref="twitter" class="twitter">
-	<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', left: `${tweetLeft}px`, width: `${tweetLeft < 0 ? 'auto' : '100%'}`, height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
+	<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
 </div>
 <div v-else v-size="{ max: [400, 350] }" class="mk-url-preview">
 	<transition name="zoom" mode="out-in">
@@ -32,110 +32,80 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, onUnmounted } from 'vue';
 import { url as local, lang } from '@/config';
-import * as os from '@/os';
 
-export default defineComponent({
-	props: {
-		url: {
-			type: String,
-			require: true
-		},
+const props = withDefaults(defineProps<{
+	url: string;
+	detail?: boolean;
+	compact?: boolean;
+}>(), {
+	detail: false,
+	compact: false,
+});
 
-		detail: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
+const self = props.url.startsWith(local);
+const attr = self ? 'to' : 'href';
+const target = self ? null : '_blank';
+let fetching = $ref(true);
+let title = $ref<string | null>(null);
+let description = $ref<string | null>(null);
+let thumbnail = $ref<string | null>(null);
+let icon = $ref<string | null>(null);
+let sitename = $ref<string | null>(null);
+let player = $ref({
+	url: null,
+	width: null,
+	height: null
+});
+let playerEnabled = $ref(false);
+let tweetId = $ref<string | null>(null);
+let tweetExpanded = $ref(props.detail);
+const embedId = `embed${Math.random().toString().replace(/\D/,'')}`;
+let tweetHeight = $ref(150);
 
-		compact: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
+const requestUrl = new URL(props.url);
 
-	data() {
-		const self = this.url.startsWith(local);
-		return {
-			local,
-			fetching: true,
-			title: null,
-			description: null,
-			thumbnail: null,
-			icon: null,
-			sitename: null,
-			player: {
-				url: null,
-				width: null,
-				height: null
-			},
-			tweetId: null,
-			tweetExpanded: this.detail,
-			embedId: `embed${Math.random().toString().replace(/\D/,'')}`,
-			tweetHeight: 150,
-			tweetLeft: 0,
-			playerEnabled: false,
-			self: self,
-			attr: self ? 'to' : 'href',
-			target: self ? null : '_blank',
-		};
-	},
+if (requestUrl.hostname == 'twitter.com') {
+	const m = requestUrl.pathname.match(/^\/.+\/status(?:es)?\/(\d+)/);
+	if (m) tweetId = m[1];
+}
 
-	created() {
-		const requestUrl = new URL(this.url);
+if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/(?:watch|channel)')) {
+	requestUrl.hostname = 'www.youtube.com';
+}
 
-		if (requestUrl.hostname == 'twitter.com') {
-			const m = requestUrl.pathname.match(/^\/.+\/status(?:es)?\/(\d+)/);
-			if (m) this.tweetId = m[1];
-		}
+const requestLang = (lang || 'ja-JP').replace('ja-KS', 'ja-JP');
 
-		if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/(?:watch|channel)')) {
-			requestUrl.hostname = 'www.youtube.com';
-		}
+requestUrl.hash = '';
 
-		const requestLang = (lang || 'ja-JP').replace('ja-KS', 'ja-JP');
+fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => {
+	res.json().then(info => {
+		if (info.url == null) return;
+		title = info.title;
+		description = info.description;
+		thumbnail = info.thumbnail;
+		icon = info.icon;
+		sitename = info.sitename;
+		fetching = false;
+		player = info.player;
+	})
+});
 
-		requestUrl.hash = '';
+function adjustTweetHeight(message: any) {
+	if (message.origin !== 'https://platform.twitter.com') return;
+	const embed = message.data?.['twttr.embed'];
+	if (embed?.method !== 'twttr.private.resize') return;
+	if (embed?.id !== embedId) return;
+	const height = embed?.params[0]?.height;
+	if (height) tweetHeight = height;
+}
 
-		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;
-			})
-		});
+(window as any).addEventListener('message', adjustTweetHeight);
 
-		(window as any).addEventListener('message', this.adjustTweetHeight);
-	},
-
-	mounted() {
-		// 300pxないと絶対右にはみ出るので左に移動してしまう
-		const areaWidth = (this.$el as any)?.clientWidth;
-		if (areaWidth && areaWidth < 300) this.tweetLeft = areaWidth - 241;
-	},
-
-	beforeUnmount() {
-		(window as any).removeEventListener('message', this.adjustTweetHeight);
-	},
-
-	methods: {
-		adjustTweetHeight(message: any) {
-			if (message.origin !== 'https://platform.twitter.com') return;
-			const embed = message.data?.['twttr.embed'];
-			if (embed?.method !== 'twttr.private.resize') return;
-			if (embed?.id !== this.embedId) return;
-			const height = embed?.params[0]?.height;
-			if (height) this.tweetHeight = height;
- 		},
-	},
+onUnmounted(() => {
+	(window as any).removeEventListener('message', adjustTweetHeight);
 });
 </script>
 
diff --git a/packages/client/src/components/user-list.vue b/packages/client/src/components/user-list.vue
index 2148dab608..3e273721c7 100644
--- a/packages/client/src/components/user-list.vue
+++ b/packages/client/src/components/user-list.vue
@@ -1,91 +1,39 @@
 <template>
-<MkError v-if="error" @retry="init()"/>
+<MkPagination ref="pagingComponent" :pagination="pagination">
+	<template #empty>
+		<div class="_fullinfo">
+			<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+			<div>{{ $ts.noUsers }}</div>
+		</div>
+	</template>
 
-<div v-else class="efvhhmdq _isolated">
-	<div v-if="empty" class="no-users">
-		<p>{{ $ts.noUsers }}</p>
-	</div>
-	<div class="users">
-		<MkUserInfo v-for="user in users" :key="user.id" class="user" :user="user"/>
-	</div>
-	<button v-show="more" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" class="more" :class="{ fetching: moreFetching }" :disabled="moreFetching" @click="fetchMore">
-		<template v-if="moreFetching"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>{{ moreFetching ? $ts.loading : $ts.loadMore }}
-	</button>
-</div>
+	<template #default="{ items: users }">
+		<div class="efvhhmdq">
+			<MkUserInfo v-for="user in users" :key="user.id" class="user" :user="user"/>
+		</div>
+	</template>
+</MkPagination>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-import paging from '@/scripts/paging';
-import MkUserInfo from './user-info.vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
+import MkUserInfo from '@/components/user-info.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import { Paging } from '@/components/ui/pagination.vue';
 import { userPage } from '@/filters/user';
 
-export default defineComponent({
-	components: {
-		MkUserInfo,
-	},
+const props = defineProps<{
+	pagination: Paging;
+	noGap?: boolean;
+}>();
 
-	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;
-		}
-	},
-
-	methods: {
-		userPage
-	}
-});
+const pagingComponent = ref<InstanceType<typeof MkPagination>>();
 </script>
 
 <style lang="scss" scoped>
 .efvhhmdq {
-	> .no-users {
-		text-align: center;
-	}
-
-	> .users {
-		display: grid;
-		grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
-		grid-gap: var(--margin);
-	}
-
-	> .more {
-		display: block;
-		width: 100%;
-		padding: 16px;
-
-		&:hover {
-			background: rgba(#000, 0.025);
-		}
-
-		&:active {
-			background: rgba(#000, 0.05);
-		}
-
-		&.fetching {
-			cursor: wait;
-		}
-
-		> i {
-			margin-right: 4px;
-		}
-	}
+	display: grid;
+	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
+	grid-gap: var(--margin);
 }
 </style>
diff --git a/packages/client/src/components/user-online-indicator.vue b/packages/client/src/components/user-online-indicator.vue
index 93e9dea57b..a87b0aeff5 100644
--- a/packages/client/src/components/user-online-indicator.vue
+++ b/packages/client/src/components/user-online-indicator.vue
@@ -2,26 +2,21 @@
 <div v-tooltip="text" class="fzgwjkgc" :class="user.onlineStatus"></div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-	},
+const props = defineProps<{
+	user: misskey.entities.User;
+}>();
 
-	computed: {
-		text(): string {
-			switch (this.user.onlineStatus) {
-				case 'online': return this.$ts.online;
-				case 'active': return this.$ts.active;
-				case 'offline': return this.$ts.offline;
-				case 'unknown': return this.$ts.unknown;
-			}
-		}
+const text = $computed(() => {
+	switch (props.user.onlineStatus) {
+		case 'online': return i18n.locale.online;
+		case 'active': return i18n.locale.active;
+		case 'offline': return i18n.locale.offline;
+		case 'unknown': return i18n.locale.unknown;
 	}
 });
 </script>
diff --git a/packages/client/src/components/visibility-picker.vue b/packages/client/src/components/visibility-picker.vue
index 4200f4354e..4b20063a51 100644
--- a/packages/client/src/components/visibility-picker.vue
+++ b/packages/client/src/components/visibility-picker.vue
@@ -1,28 +1,28 @@
 <template>
-<MkModal ref="modal" :z-priority="'high'" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
+<MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
 	<div class="gqyayizv _popup">
-		<button key="public" class="_button" :class="{ active: v == 'public' }" data-index="1" @click="choose('public')">
+		<button key="public" class="_button" :class="{ active: v === 'public' }" data-index="1" @click="choose('public')">
 			<div><i class="fas fa-globe"></i></div>
 			<div>
 				<span>{{ $ts._visibility.public }}</span>
 				<span>{{ $ts._visibility.publicDescription }}</span>
 			</div>
 		</button>
-		<button key="home" class="_button" :class="{ active: v == 'home' }" data-index="2" @click="choose('home')">
+		<button key="home" class="_button" :class="{ active: v === 'home' }" data-index="2" @click="choose('home')">
 			<div><i class="fas fa-home"></i></div>
 			<div>
 				<span>{{ $ts._visibility.home }}</span>
 				<span>{{ $ts._visibility.homeDescription }}</span>
 			</div>
 		</button>
-		<button key="followers" class="_button" :class="{ active: v == 'followers' }" data-index="3" @click="choose('followers')">
+		<button key="followers" class="_button" :class="{ active: v === 'followers' }" data-index="3" @click="choose('followers')">
 			<div><i class="fas fa-unlock"></i></div>
 			<div>
 				<span>{{ $ts._visibility.followers }}</span>
 				<span>{{ $ts._visibility.followersDescription }}</span>
 			</div>
 		</button>
-		<button key="specified" :disabled="localOnly" class="_button" :class="{ active: v == 'specified' }" data-index="4" @click="choose('specified')">
+		<button key="specified" :disabled="localOnly" class="_button" :class="{ active: v === 'specified' }" data-index="4" @click="choose('specified')">
 			<div><i class="fas fa-envelope"></i></div>
 			<div>
 				<span>{{ $ts._visibility.specified }}</span>
@@ -42,49 +42,40 @@
 </MkModal>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { nextTick, watch } from 'vue';
+import * as misskey from 'misskey-js';
 import MkModal from '@/components/ui/modal.vue';
 
-export default defineComponent({
-	components: {
-		MkModal,
-	},
-	props: {
-		currentVisibility: {
-			type: String,
-			required: true
-		},
-		currentLocalOnly: {
-			type: Boolean,
-			required: true
-		},
-		src: {
-			required: false
-		},
-	},
-	emits: ['change-visibility', 'change-local-only', 'closed'],
-	data() {
-		return {
-			v: this.currentVisibility,
-			localOnly: this.currentLocalOnly,
-		}
-	},
-	watch: {
-		localOnly() {
-			this.$emit('change-local-only', this.localOnly);
-		}
-	},
-	methods: {
-		choose(visibility) {
-			this.v = visibility;
-			this.$emit('change-visibility', visibility);
-			this.$nextTick(() => {
-				this.$refs.modal.close();
-			});
-		},
-	}
+const modal = $ref<InstanceType<typeof MkModal>>();
+
+const props = withDefaults(defineProps<{
+	currentVisibility: typeof misskey.noteVisibilities[number];
+	currentLocalOnly: boolean;
+	src?: HTMLElement;
+}>(), {
 });
+
+const emit = defineEmits<{
+	(e: 'changeVisibility', v: typeof misskey.noteVisibilities[number]): void;
+	(e: 'changeLocalOnly', v: boolean): void;
+	(e: 'closed'): void;
+}>();
+
+let v = $ref(props.currentVisibility);
+let localOnly = $ref(props.currentLocalOnly);
+
+watch($$(localOnly), () => {
+	emit('changeLocalOnly', localOnly);
+});
+
+function choose(visibility: typeof misskey.noteVisibilities[number]): void {
+	v = visibility;
+	emit('changeVisibility', visibility);
+	nextTick(() => {
+		modal.close();
+	});
+}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/waiting-dialog.vue b/packages/client/src/components/waiting-dialog.vue
index 10aedbd8f6..7dfcc55695 100644
--- a/packages/client/src/components/waiting-dialog.vue
+++ b/packages/client/src/components/waiting-dialog.vue
@@ -1,5 +1,5 @@
 <template>
-<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="success ? done() : () => {}" @closed="$emit('closed')">
+<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="success ? done() : () => {}" @closed="emit('closed')">
 	<div class="iuyakobc" :class="{ iconOnly: (text == null) || success }">
 		<i v-if="success" class="fas fa-check icon success"></i>
 		<i v-else class="fas fa-spinner fa-pulse icon waiting"></i>
@@ -8,49 +8,30 @@
 </MkModal>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { watch, ref } from 'vue';
 import MkModal from '@/components/ui/modal.vue';
 
-export default defineComponent({
-	components: {
-		MkModal,
-	},
+const modal = ref<InstanceType<typeof MkModal>>();
 
-	props: {
-		success: {
-			type: Boolean,
-			required: true,
-		},
-		showing: {
-			type: Boolean,
-			required: true,
-		},
-		text: {
-			type: String,
-			required: false,
-		},
-	},
+const props = defineProps<{
+	success: boolean;
+	showing: boolean;
+	text?: string;
+}>();
 
-	emits: ['done', 'closed'],
+const emit = defineEmits<{
+	(e: 'done');
+	(e: 'closed');
+}>();
 
-	data() {
-		return {
-		};
-	},
+function done() {
+	emit('done');
+	modal.value.close();
+}
 
-	watch: {
-		showing() {
-			if (!this.showing) this.done();
-		}
-	},
-
-	methods: {
-		done() {
-			this.$emit('done');
-			this.$refs.modal.close();
-		},
-	}
+watch(() => props.showing, () => {
+	if (!props.showing) done();
 });
 </script>
 
diff --git a/packages/client/src/components/widgets.vue b/packages/client/src/components/widgets.vue
index 12f7129253..ccde5fbe55 100644
--- a/packages/client/src/components/widgets.vue
+++ b/packages/client/src/components/widgets.vue
@@ -10,7 +10,7 @@
 			<MkButton inline @click="$emit('exit')">{{ $ts.close }}</MkButton>
 		</header>
 		<XDraggable
-			v-model="_widgets"
+			v-model="widgets_"
 			item-key="id"
 			animation="150"
 		>
@@ -18,7 +18,7 @@
 				<div class="customize-container">
 					<button class="config _button" @click.prevent.stop="configWidget(element.id)"><i class="fas fa-cog"></i></button>
 					<button class="remove _button" @click.prevent.stop="removeWidget(element)"><i class="fas fa-times"></i></button>
-					<component :is="`mkw-${element.name}`" :widget="element" :setting-callback="setting => settings[element.id] = setting" @updateProps="updateWidget(element.id, $event)"/>
+					<component :ref="el => widgetRefs[element.id] = el" :is="`mkw-${element.name}`" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
 				</div>
 			</template>
 		</XDraggable>
@@ -28,7 +28,7 @@
 </template>
 
 <script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
+import { defineComponent, defineAsyncComponent, reactive, ref, computed } from 'vue';
 import { v4 as uuid } from 'uuid';
 import MkSelect from '@/components/form/select.vue';
 import MkButton from '@/components/ui/button.vue';
@@ -54,50 +54,47 @@ export default defineComponent({
 
 	emits: ['updateWidgets', 'addWidget', 'removeWidget', 'updateWidget', 'exit'],
 
-	data() {
-		return {
-			widgetAdderSelected: null,
-			widgetDefs,
-			settings: {},
+	setup(props, context) {
+		const widgetRefs = reactive({});
+		const configWidget = (id: string) => {
+			widgetRefs[id].configure();
 		};
-	},
+		const widgetAdderSelected = ref(null);
+		const addWidget = () => {
+			if (widgetAdderSelected.value == null) return;
 
-	computed: {
-		_widgets: {
-			get() {
-				return this.widgets;
-			},
-			set(value) {
-				this.$emit('updateWidgets', value);
-			}
-		}
-	},
-
-	methods: {
-		configWidget(id) {
-			this.settings[id]();
-		},
-
-		addWidget() {
-			if (this.widgetAdderSelected == null) return;
-
-			this.$emit('addWidget', {
-				name: this.widgetAdderSelected,
+			context.emit('addWidget', {
+				name: widgetAdderSelected.value,
 				id: uuid(),
-				data: {}
+				data: {},
 			});
 
-			this.widgetAdderSelected = null;
-		},
+			widgetAdderSelected.value = null;
+		};
+		const removeWidget = (widget) => {
+			context.emit('removeWidget', widget);
+		};
+		const updateWidget = (id, data) => {
+			context.emit('updateWidget', { id, data });
+		};
+		const widgets_ = computed({
+			get: () => props.widgets,
+			set: (value) => {
+				context.emit('updateWidgets', value);
+			},
+		});
 
-		removeWidget(widget) {
-			this.$emit('removeWidget', widget);
-		},
-
-		updateWidget(id, data) {
-			this.$emit('updateWidget', { id, data });
-		},
-	}
+		return {
+			widgetRefs,
+			configWidget,
+			widgetAdderSelected,
+			widgetDefs,
+			addWidget,
+			removeWidget,
+			updateWidget,
+			widgets_,
+		};
+	},
 });
 </script>
 
diff --git a/packages/client/src/directives/anim.ts b/packages/client/src/directives/anim.ts
index 1ceef984d8..04e1c6a404 100644
--- a/packages/client/src/directives/anim.ts
+++ b/packages/client/src/directives/anim.ts
@@ -10,7 +10,7 @@ export default {
 	},
 
 	mounted(src, binding, vn) {
-		setTimeout(() => {
+		window.setTimeout(() => {
 			src.style.opacity = '1';
 			src.style.transform = 'none';
 		}, 1);
diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts
index e14ee81dff..fffde14874 100644
--- a/packages/client/src/directives/tooltip.ts
+++ b/packages/client/src/directives/tooltip.ts
@@ -21,7 +21,7 @@ export default {
 
 		self.close = () => {
 			if (self._close) {
-				clearInterval(self.checkTimer);
+				window.clearInterval(self.checkTimer);
 				self._close();
 				self._close = null;
 			}
@@ -61,19 +61,19 @@ export default {
 		});
 
 		el.addEventListener(start, () => {
-			clearTimeout(self.showTimer);
-			clearTimeout(self.hideTimer);
-			self.showTimer = setTimeout(self.show, delay);
+			window.clearTimeout(self.showTimer);
+			window.clearTimeout(self.hideTimer);
+			self.showTimer = window.setTimeout(self.show, delay);
 		}, { passive: true });
 
 		el.addEventListener(end, () => {
-			clearTimeout(self.showTimer);
-			clearTimeout(self.hideTimer);
-			self.hideTimer = setTimeout(self.close, delay);
+			window.clearTimeout(self.showTimer);
+			window.clearTimeout(self.hideTimer);
+			self.hideTimer = window.setTimeout(self.close, delay);
 		}, { passive: true });
 
 		el.addEventListener('click', () => {
-			clearTimeout(self.showTimer);
+			window.clearTimeout(self.showTimer);
 			self.close();
 		});
 	},
@@ -85,6 +85,6 @@ export default {
 
 	unmounted(el, binding, vn) {
 		const self = el._tooltipDirective_;
-		clearInterval(self.checkTimer);
+		window.clearInterval(self.checkTimer);
 	},
 } as Directive;
diff --git a/packages/client/src/directives/user-preview.ts b/packages/client/src/directives/user-preview.ts
index 68d9e2816c..cdd2afa194 100644
--- a/packages/client/src/directives/user-preview.ts
+++ b/packages/client/src/directives/user-preview.ts
@@ -30,11 +30,11 @@ export class UserPreview {
 			source: this.el
 		}, {
 			mouseover: () => {
-				clearTimeout(this.hideTimer);
+				window.clearTimeout(this.hideTimer);
 			},
 			mouseleave: () => {
-				clearTimeout(this.showTimer);
-				this.hideTimer = setTimeout(this.close, 500);
+				window.clearTimeout(this.showTimer);
+				this.hideTimer = window.setTimeout(this.close, 500);
 			},
 		}, 'closed');
 
@@ -44,10 +44,10 @@ export class UserPreview {
 			}
 		};
 
-		this.checkTimer = setInterval(() => {
+		this.checkTimer = window.setInterval(() => {
 			if (!document.body.contains(this.el)) {
-				clearTimeout(this.showTimer);
-				clearTimeout(this.hideTimer);
+				window.clearTimeout(this.showTimer);
+				window.clearTimeout(this.hideTimer);
 				this.close();
 			}
 		}, 1000);
@@ -56,7 +56,7 @@ export class UserPreview {
 	@autobind
 	private close() {
 		if (this.promise) {
-			clearInterval(this.checkTimer);
+			window.clearInterval(this.checkTimer);
 			this.promise.cancel();
 			this.promise = null;
 		}
@@ -64,21 +64,21 @@ export class UserPreview {
 
 	@autobind
 	private onMouseover() {
-		clearTimeout(this.showTimer);
-		clearTimeout(this.hideTimer);
-		this.showTimer = setTimeout(this.show, 500);
+		window.clearTimeout(this.showTimer);
+		window.clearTimeout(this.hideTimer);
+		this.showTimer = window.setTimeout(this.show, 500);
 	}
 
 	@autobind
 	private onMouseleave() {
-		clearTimeout(this.showTimer);
-		clearTimeout(this.hideTimer);
-		this.hideTimer = setTimeout(this.close, 500);
+		window.clearTimeout(this.showTimer);
+		window.clearTimeout(this.hideTimer);
+		this.hideTimer = window.setTimeout(this.close, 500);
 	}
 
 	@autobind
 	private onClick() {
-		clearTimeout(this.showTimer);
+		window.clearTimeout(this.showTimer);
 		this.close();
 	}
 
@@ -94,7 +94,7 @@ export class UserPreview {
 		this.el.removeEventListener('mouseover', this.onMouseover);
 		this.el.removeEventListener('mouseleave', this.onMouseleave);
 		this.el.removeEventListener('click', this.onClick);
-		clearInterval(this.checkTimer);
+		window.clearInterval(this.checkTimer);
 	}
 }
 
diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts
index 2263b4ca3c..af70aec70a 100644
--- a/packages/client/src/init.ts
+++ b/packages/client/src/init.ts
@@ -172,7 +172,6 @@ const app = createApp(await (
 	!$i                               ? import('@/ui/visitor.vue') :
 	ui === 'deck'                     ? import('@/ui/deck.vue') :
 	ui === 'desktop'                  ? import('@/ui/desktop.vue') :
-	ui === 'chat'                     ? import('@/ui/chat/index.vue') :
 	ui === 'classic'                  ? import('@/ui/classic.vue') :
 	import('@/ui/universal.vue')
 ).then(x => x.default));
diff --git a/packages/client/src/menu.ts b/packages/client/src/menu.ts
index ea6f801fec..184779f21f 100644
--- a/packages/client/src/menu.ts
+++ b/packages/client/src/menu.ts
@@ -163,11 +163,6 @@ export const menuDef = reactive({
 		icon: 'fas fa-laugh',
 		to: '/emojis',
 	},
-	games: {
-		title: 'games',
-		icon: 'fas fa-gamepad',
-		to: '/games/reversi',
-	},
 	scratchpad: {
 		title: 'scratchpad',
 		icon: 'fas fa-terminal',
@@ -198,13 +193,6 @@ export const menuDef = reactive({
 					localStorage.setItem('ui', 'classic');
 					unisonReload();
 				}
-			}, {
-				text: 'Chat (β)',
-				active: ui === 'chat',
-				action: () => {
-					localStorage.setItem('ui', 'chat');
-					unisonReload();
-				}
 			}, /*{
 				text: i18n.locale.desktop + ' (β)',
 				active: ui === 'desktop',
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index e6dd4567f7..378523e1bc 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -83,7 +83,7 @@ export function promiseDialog<T extends Promise<any>>(
 			onSuccess(res);
 		} else {
 			success.value = true;
-			setTimeout(() => {
+			window.setTimeout(() => {
 				showing.value = false;
 			}, 1000);
 		}
@@ -139,7 +139,7 @@ export async function popup(component: Component | typeof import('*.vue') | Prom
 	const id = ++popupIdCount;
 	const dispose = () => {
 		// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ?
-		setTimeout(() => {
+		window.setTimeout(() => {
 			popups.value = popups.value.filter(popup => popup.id !== id);
 		}, 0);
 	};
@@ -329,7 +329,7 @@ export function select(props: {
 export function success() {
 	return new Promise((resolve, reject) => {
 		const showing = ref(true);
-		setTimeout(() => {
+		window.setTimeout(() => {
 			showing.value = false;
 		}, 1000);
 		popup(import('@/components/waiting-dialog.vue'), {
@@ -541,7 +541,7 @@ export const uploads = ref<{
 	img: string;
 }[]>([]);
 
-export function upload(file: File, folder?: any, name?: string) {
+export function upload(file: File, folder?: any, name?: string): Promise<Misskey.entities.DriveFile> {
 	if (folder && typeof folder == 'object') folder = folder.id;
 
 	return new Promise((resolve, reject) => {
diff --git a/packages/client/src/pages/_error_.vue b/packages/client/src/pages/_error_.vue
index 2f8f08b5cf..7540995707 100644
--- a/packages/client/src/pages/_error_.vue
+++ b/packages/client/src/pages/_error_.vue
@@ -1,68 +1,61 @@
 <template>
-<MkLoading v-if="!loaded" />
+<MkLoading v-if="!loaded"/>
 <transition :name="$store.state.animation ? 'zoom' : ''" appear>
 	<div v-show="loaded" class="mjndxjch">
 		<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
-		<p><b><i class="fas fa-exclamation-triangle"></i> {{ $ts.pageLoadError }}</b></p>
-		<p v-if="version === meta.version">{{ $ts.pageLoadErrorDescription }}</p>
-		<p v-else-if="serverIsDead">{{ $ts.serverIsDead }}</p>
+		<p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.locale.pageLoadError }}</b></p>
+		<p v-if="meta && (version === meta.version)">{{ i18n.locale.pageLoadErrorDescription }}</p>
+		<p v-else-if="serverIsDead">{{ i18n.locale.serverIsDead }}</p>
 		<template v-else>
-			<p>{{ $ts.newVersionOfClientAvailable }}</p>
-			<p>{{ $ts.youShouldUpgradeClient }}</p>
-			<MkButton class="button primary" @click="reload">{{ $ts.reload }}</MkButton>
+			<p>{{ i18n.locale.newVersionOfClientAvailable }}</p>
+			<p>{{ i18n.locale.youShouldUpgradeClient }}</p>
+			<MkButton class="button primary" @click="reload">{{ i18n.locale.reload }}</MkButton>
 		</template>
-		<p><MkA to="/docs/general/troubleshooting" class="_link">{{ $ts.troubleshooting }}</MkA></p>
+		<p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.locale.troubleshooting }}</MkA></p>
 		<p v-if="error" class="error">ERROR: {{ error }}</p>
 	</div>
 </transition>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
 import MkButton from '@/components/ui/button.vue';
 import * as symbols from '@/symbols';
 import { version } from '@/config';
 import * as os from '@/os';
 import { unisonReload } from '@/scripts/unison-reload';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		MkButton,
-	},
-	props: {
-		error: {
-			required: false,
-		}
-	},
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.error,
-				icon: 'fas fa-exclamation-triangle'
-			},
-			loaded: false,
-			serverIsDead: false,
-			meta: {} as any,
-			version,
-		};
-	},
-	created() {
-		os.api('meta', {
-			detail: false
-		}).then(meta => {
-			this.loaded = true;
-			this.serverIsDead = false;
-			this.meta = meta;
-			localStorage.setItem('v', meta.version);
-		}, () => {
-			this.loaded = true;
-			this.serverIsDead = true;
-		});
-	},
-	methods: {
-		reload() {
-			unisonReload();
-		},
+const props = withDefaults(defineProps<{
+	error?: Error;
+}>(), {
+});
+
+let loaded = $ref(false);
+let serverIsDead = $ref(false);
+let meta = $ref<misskey.entities.LiteInstanceMetadata | null>(null);
+
+os.api('meta', {
+	detail: false,
+}).then(res => {
+	loaded = true;
+	serverIsDead = false;
+	meta = res;
+	localStorage.setItem('v', res.version);
+}, () => {
+	loaded = true;
+	serverIsDead = true;
+});
+
+function reload() {
+	unisonReload();
+}
+
+defineExpose({
+	[symbols.PAGE_INFO]: {
+		title: i18n.locale.error,
+		icon: 'fas fa-exclamation-triangle',
 	},
 });
 </script>
diff --git a/packages/client/src/pages/about-misskey.vue b/packages/client/src/pages/about-misskey.vue
index 855a21e493..8119f33051 100644
--- a/packages/client/src/pages/about-misskey.vue
+++ b/packages/client/src/pages/about-misskey.vue
@@ -3,36 +3,39 @@
 	<MkSpacer :content-max="600" :margin-min="20">
 		<div class="_formRoot znqjceqz">
 			<div id="debug"></div>
-			<div ref="about" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }">
+			<div ref="containerEl" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }">
 				<img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/>
 				<div class="misskey">Misskey</div>
 				<div class="version">v{{ version }}</div>
 				<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span>
 			</div>
 			<div class="_formBlock" style="text-align: center;">
-				{{ $ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ $ts.learnMore }}</a>
+				{{ i18n.locale._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.locale.learnMore }}</a>
+			</div>
+			<div class="_formBlock" style="text-align: center;">
+				<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>
 			</div>
 			<FormSection>
 				<div class="_formLinks">
 					<FormLink to="https://github.com/misskey-dev/misskey" external>
 						<template #icon><i class="fas fa-code"></i></template>
-						{{ $ts._aboutMisskey.source }}
+						{{ i18n.locale._aboutMisskey.source }}
 						<template #suffix>GitHub</template>
 					</FormLink>
 					<FormLink to="https://crowdin.com/project/misskey" external>
 						<template #icon><i class="fas fa-language"></i></template>
-						{{ $ts._aboutMisskey.translation }}
+						{{ i18n.locale._aboutMisskey.translation }}
 						<template #suffix>Crowdin</template>
 					</FormLink>
 					<FormLink to="https://www.patreon.com/syuilo" external>
 						<template #icon><i class="fas fa-hand-holding-medical"></i></template>
-						{{ $ts._aboutMisskey.donate }}
+						{{ i18n.locale._aboutMisskey.donate }}
 						<template #suffix>Patreon</template>
 					</FormLink>
 				</div>
 			</FormSection>
 			<FormSection>
-				<template #label>{{ $ts._aboutMisskey.contributors }}</template>
+				<template #label>{{ i18n.locale._aboutMisskey.contributors }}</template>
 				<div class="_formLinks">
 					<FormLink to="https://github.com/syuilo" external>@syuilo</FormLink>
 					<FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink>
@@ -44,27 +47,30 @@
 					<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink>
 					<FormLink to="https://github.com/marihachi" external>@marihachi</FormLink>
 				</div>
-				<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ $ts._aboutMisskey.allContributors }}</MkLink></template>
+				<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.locale._aboutMisskey.allContributors }}</MkLink></template>
 			</FormSection>
 			<FormSection>
-				<template #label><Mfm text="$[jelly ❤]"/> {{ $ts._aboutMisskey.patrons }}</template>
+				<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.locale._aboutMisskey.patrons }}</template>
 				<div v-for="patron in patrons" :key="patron">{{ patron }}</div>
-				<template #caption>{{ $ts._aboutMisskey.morePatrons }}</template>
+				<template #caption>{{ i18n.locale._aboutMisskey.morePatrons }}</template>
 			</FormSection>
 		</div>
 	</MkSpacer>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { nextTick, onBeforeUnmount } from 'vue';
 import { version } from '@/config';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
-import MkKeyValue from '@/components/key-value.vue';
+import MkButton from '@/components/ui/button.vue';
 import MkLink from '@/components/link.vue';
 import { physics } from '@/scripts/physics';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
+import { defaultStore } from '@/store';
+import * as os from '@/os';
 
 const patrons = [
 	'まっちゃとーにゅ',
@@ -145,58 +151,52 @@ const patrons = [
 	'蝉暮せせせ',
 ];
 
-export default defineComponent({
-	components: {
-		FormSection,
-		FormLink,
-		MkKeyValue,
-		MkLink,
-	},
+let easterEggReady = false;
+let easterEggEmojis = $ref([]);
+let easterEggEngine = $ref(null);
+const containerEl = $ref<HTMLElement>();
 
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.aboutMisskey,
-				icon: null
-			},
-			version,
-			patrons,
-			easterEggReady: false,
-			easterEggEmojis: [],
-			easterEggEngine: null,
-		}
-	},
-
-	beforeUnmount() {
-		if (this.easterEggEngine) {
-			this.easterEggEngine.stop();
-		}
-	},
-
-	methods: {
-		iconLoaded() {
-			const emojis = this.$store.state.reactions;
-			const containerWidth = this.$refs.about.offsetWidth;
-			for (let i = 0; i < 32; i++) {
-				this.easterEggEmojis.push({
-					id: i.toString(),
-					top: -(128 + (Math.random() * 256)),
-					left: (Math.random() * containerWidth),
-					emoji: emojis[Math.floor(Math.random() * emojis.length)],
-				});
-			}
-
-			this.$nextTick(() => {
-				this.easterEggReady = true;
-			});
-		},
-
-		gravity() {
-			if (!this.easterEggReady) return;
-			this.easterEggReady = false;
-			this.easterEggEngine = physics(this.$refs.about);
-		}
+function iconLoaded() {
+	const emojis = defaultStore.state.reactions;
+	const containerWidth = containerEl.offsetWidth;
+	for (let i = 0; i < 32; i++) {
+		easterEggEmojis.push({
+			id: i.toString(),
+			top: -(128 + (Math.random() * 256)),
+			left: (Math.random() * containerWidth),
+			emoji: emojis[Math.floor(Math.random() * emojis.length)],
+		});
 	}
+
+	nextTick(() => {
+		easterEggReady = true;
+	});
+}
+
+function gravity() {
+	if (!easterEggReady) return;
+	easterEggReady = false;
+	easterEggEngine = physics(containerEl);
+}
+
+function iLoveMisskey() {
+	os.post({
+		initialText: 'I $[jelly ❤] #Misskey',
+	});
+}
+
+onBeforeUnmount(() => {
+	if (easterEggEngine) {
+		easterEggEngine.stop();
+	}
+});
+
+defineExpose({
+	[symbols.PAGE_INFO]: {
+		title: i18n.locale.aboutMisskey,
+		icon: null,
+		bg: 'var(--bg)',
+	},
 });
 </script>
 
diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue
index 8df20097b3..92f93797ce 100644
--- a/packages/client/src/pages/admin/abuses.vue
+++ b/packages/client/src/pages/admin/abuses.vue
@@ -34,27 +34,7 @@
 			-->
 
 			<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
-				<div v-for="report in items" :key="report.id" class="bcekxzvu _card _gap">
-					<div class="_content target">
-						<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>
-						<div class="info">
-							<MkUserName class="name" :user="report.targetUser"/>
-							<div class="acct">@{{ acct(report.targetUser) }}</div>
-						</div>
-					</div>
-					<div class="_content">
-						<div>
-							<Mfm :text="report.comment"/>
-						</div>
-						<hr>
-						<div>Reporter: <MkAcct :user="report.reporter"/></div>
-						<div><MkTime :time="report.createdAt"/></div>
-					</div>
-					<div class="_footer">
-						<div v-if="report.assignee">Assignee: <MkAcct :user="report.assignee"/></div>
-						<MkButton v-if="!report.resolved" primary @click="resolve(report)">{{ $ts.abuseMarkAsResolved }}</MkButton>
-					</div>
-				</div>
+				<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
 			</MkPagination>
 		</div>
 	</div>
@@ -62,22 +42,21 @@
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue';
+import { computed, defineComponent } from 'vue';
 
-import MkButton from '@/components/ui/button.vue';
 import MkInput from '@/components/form/input.vue';
 import MkSelect from '@/components/form/select.vue';
 import MkPagination from '@/components/ui/pagination.vue';
-import { acct } from '@/filters/user';
+import XAbuseReport from '@/components/abuse-report.vue';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
 
 export default defineComponent({
 	components: {
-		MkButton,
 		MkInput,
 		MkSelect,
 		MkPagination,
+		XAbuseReport,
 	},
 
 	emits: ['info'],
@@ -95,44 +74,20 @@ export default defineComponent({
 			reporterOrigin: 'combined',
 			targetUserOrigin: 'combined',
 			pagination: {
-				endpoint: 'admin/abuse-user-reports',
+				endpoint: 'admin/abuse-user-reports' as const,
 				limit: 10,
-				params: () => ({
+				params: computed(() => ({
 					state: this.state,
 					reporterOrigin: this.reporterOrigin,
 					targetUserOrigin: this.targetUserOrigin,
-				}),
+				})),
 			},
 		}
 	},
 
-	watch: {
-		state() {
-			this.$refs.reports.reload();
-		},
-
-		reporterOrigin() {
-			this.$refs.reports.reload();
-		},
-
-		targetUserOrigin() {
-			this.$refs.reports.reload();
-		},
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
-		acct,
-
-		resolve(report) {
-			os.apiWithDialog('admin/resolve-abuse-user-report', {
-				reportId: report.id,
-			}).then(() => {
-				this.$refs.reports.removeItem(item => item.id === report.id);
-			});
+		resolved(reportId) {
+			this.$refs.reports.removeItem(item => item.id === reportId);
 		},
 	}
 });
@@ -142,29 +97,4 @@ export default defineComponent({
 .lcixvhis {
 	margin: var(--margin);
 }
-
-.bcekxzvu {
-	> .target {
-		display: flex;
-		width: 100%;
-		box-sizing: border-box;
-		text-align: left;
-		align-items: center;
-					
-		> .avatar {
-			width: 42px;
-			height: 42px;
-		}
-
-		> .info {
-			margin-left: 0.3em;
-			padding: 0 8px;
-			flex: 1;
-
-			> .name {
-				font-weight: bold;
-			}
-		}
-	}
-}
 </style>
diff --git a/packages/client/src/pages/admin/ads.vue b/packages/client/src/pages/admin/ads.vue
index 0396dae10c..8f164caa99 100644
--- a/packages/client/src/pages/admin/ads.vue
+++ b/packages/client/src/pages/admin/ads.vue
@@ -87,10 +87,6 @@ export default defineComponent({
 		});
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		add() {
 			this.ads.unshift({
diff --git a/packages/client/src/pages/admin/announcements.vue b/packages/client/src/pages/admin/announcements.vue
index 3614cb1441..a0d720bb29 100644
--- a/packages/client/src/pages/admin/announcements.vue
+++ b/packages/client/src/pages/admin/announcements.vue
@@ -61,10 +61,6 @@ export default defineComponent({
 		});
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		add() {
 			this.announcements.unshift({
diff --git a/packages/client/src/pages/admin/bot-protection.vue b/packages/client/src/pages/admin/bot-protection.vue
index 81b09fb4d9..82ab155317 100644
--- a/packages/client/src/pages/admin/bot-protection.vue
+++ b/packages/client/src/pages/admin/bot-protection.vue
@@ -82,10 +82,6 @@ export default defineComponent({
 		}
 	},
 
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async init() {
 			const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/database.vue b/packages/client/src/pages/admin/database.vue
index c1088afd77..3a835eeafa 100644
--- a/packages/client/src/pages/admin/database.vue
+++ b/packages/client/src/pages/admin/database.vue
@@ -37,10 +37,6 @@ export default defineComponent({
 		}
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		bytes, number,
 	}
diff --git a/packages/client/src/pages/admin/email-settings.vue b/packages/client/src/pages/admin/email-settings.vue
index 0799755a4d..6491a453ab 100644
--- a/packages/client/src/pages/admin/email-settings.vue
+++ b/packages/client/src/pages/admin/email-settings.vue
@@ -93,10 +93,6 @@ export default defineComponent({
 		}
 	},
 
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async init() {
 			const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/emoji-edit-dialog.vue b/packages/client/src/pages/admin/emoji-edit-dialog.vue
index a45d92fa16..2e3903426e 100644
--- a/packages/client/src/pages/admin/emoji-edit-dialog.vue
+++ b/packages/client/src/pages/admin/emoji-edit-dialog.vue
@@ -95,7 +95,7 @@ export default defineComponent({
 			});
 			if (canceled) return;
 
-			os.api('admin/emoji/remove', {
+			os.api('admin/emoji/delete', {
 				id: this.emoji.id
 			}).then(() => {
 				this.$emit('done', {
diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue
index df5d234d6f..5b1dfe565a 100644
--- a/packages/client/src/pages/admin/emojis.vue
+++ b/packages/client/src/pages/admin/emojis.vue
@@ -6,11 +6,22 @@
 				<template #prefix><i class="fas fa-search"></i></template>
 				<template #label>{{ $ts.search }}</template>
 			</MkInput>
-			<MkPagination ref="emojis" :pagination="pagination">
+			<MkSwitch v-model="selectMode" style="margin: 8px 0;">
+				<template #label>Select mode</template>
+			</MkSwitch>
+			<div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+				<MkButton inline @click="selectAll">Select all</MkButton>
+				<MkButton inline @click="setCategoryBulk">Set category</MkButton>
+				<MkButton inline @click="addTagBulk">Add tag</MkButton>
+				<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
+				<MkButton inline @click="setTagBulk">Set tag</MkButton>
+				<MkButton inline danger @click="delBulk">Delete</MkButton>
+			</div>
+			<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
 				<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
 				<template v-slot="{items}">
 					<div class="ldhfsamy">
-						<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="edit(emoji)">
+						<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
 							<img :src="emoji.url" class="img" :alt="emoji.name"/>
 							<div class="body">
 								<div class="name _monospace">{{ emoji.name }}</div>
@@ -32,7 +43,7 @@
 					<template #label>{{ $ts.host }}</template>
 				</MkInput>
 			</FormSplit>
-			<MkPagination ref="remoteEmojis" :pagination="remotePagination">
+			<MkPagination :pagination="remotePagination">
 				<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
 				<template v-slot="{items}">
 					<div class="ldhfsamy">
@@ -51,148 +62,233 @@
 </MkSpacer>
 </template>
 
-<script lang="ts">
-import { computed, defineComponent, toRef } from 'vue';
+<script lang="ts" setup>
+import { computed, defineComponent, ref, toRef } from 'vue';
 import MkButton from '@/components/ui/button.vue';
 import MkInput from '@/components/form/input.vue';
 import MkPagination from '@/components/ui/pagination.vue';
 import MkTab from '@/components/tab.vue';
+import MkSwitch from '@/components/form/switch.vue';
 import FormSplit from '@/components/form/split.vue';
-import { selectFiles } from '@/scripts/select-file';
+import { selectFile, selectFiles } from '@/scripts/select-file';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		MkTab,
-		MkButton,
-		MkInput,
-		MkPagination,
-		FormSplit,
-	},
+const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>();
 
-	emits: ['info'],
+const tab = ref('local');
+const query = ref(null);
+const queryRemote = ref(null);
+const host = ref(null);
+const selectMode = ref(false);
+const selectedEmojis = ref<string[]>([]);
 
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => ({
-				title: this.$ts.customEmojis,
-				icon: 'fas fa-laugh',
-				bg: 'var(--bg)',
-				actions: [{
-					asFullButton: true,
-					icon: 'fas fa-plus',
-					text: this.$ts.addEmoji,
-					handler: this.add,
-				}, {
-					icon: 'fas fa-ellipsis-h',
-					handler: this.menu,
-				}],
-				tabs: [{
-					active: this.tab === 'local',
-					title: this.$ts.local,
-					onClick: () => { this.tab = 'local'; },
-				}, {
-					active: this.tab === 'remote',
-					title: this.$ts.remote,
-					onClick: () => { this.tab = 'remote'; },
-				},]
-			})),
-			tab: 'local',
-			query: null,
-			queryRemote: null,
-			host: '',
-			pagination: {
-				endpoint: 'admin/emoji/list',
-				limit: 30,
-				params: computed(() => ({
-					query: (this.query && this.query !== '') ? this.query : null
-				}))
-			},
-			remotePagination: {
-				endpoint: 'admin/emoji/list-remote',
-				limit: 30,
-				params: computed(() => ({
-					query: (this.queryRemote && this.queryRemote !== '') ? this.queryRemote : null,
-					host: (this.host && this.host !== '') ? this.host : null
-				}))
-			},
-		}
-	},
+const pagination = {
+	endpoint: 'admin/emoji/list' as const,
+	limit: 30,
+	params: computed(() => ({
+		query: (query.value && query.value !== '') ? query.value : null,
+	})),
+};
 
-	async mounted() {
-		this.$emit('info', toRef(this, symbols.PAGE_INFO));
-	},
+const remotePagination = {
+	endpoint: 'admin/emoji/list-remote' as const,
+	limit: 30,
+	params: computed(() => ({
+		query: (queryRemote.value && queryRemote.value !== '') ? queryRemote.value : null,
+		host: (host.value && host.value !== '') ? host.value : null,
+	})),
+};
 
-	methods: {
-		async add(e) {
-			const files = await selectFiles(e.currentTarget || e.target, null);
-
-			const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
-				fileId: file.id,
-			})));
-			promise.then(() => {
-				this.$refs.emojis.reload();
-			});
-			os.promiseDialog(promise);
-		},
-
-		edit(emoji) {
-			os.popup(import('./emoji-edit-dialog.vue'), {
-				emoji: emoji
-			}, {
-				done: result => {
-					if (result.updated) {
-						this.$refs.emojis.replaceItem(item => item.id === emoji.id, {
-							...emoji,
-							...result.updated
-						});
-					} else if (result.deleted) {
-						this.$refs.emojis.removeItem(item => item.id === emoji.id);
-					}
-				},
-			}, 'closed');
-		},
-
-		im(emoji) {
-			os.apiWithDialog('admin/emoji/copy', {
-				emojiId: emoji.id,
-			});
-		},
-
-		remoteMenu(emoji, ev) {
-			os.popupMenu([{
-				type: 'label',
-				text: ':' + emoji.name + ':',
-			}, {
-				text: this.$ts.import,
-				icon: 'fas fa-plus',
-				action: () => { this.im(emoji) }
-			}], ev.currentTarget || ev.target);
-		},
-
-		menu(ev) {
-			os.popupMenu([{
-				icon: 'fas fa-download',
-				text: this.$ts.export,
-				action: async () => {
-					os.api('export-custom-emojis', {
-					})
-					.then(() => {
-						os.alert({
-							type: 'info',
-							text: this.$ts.exportRequested,
-						});
-					}).catch((e) => {
-						os.alert({
-							type: 'error',
-							text: e.message,
-						});
-					});
-				}
-			}], ev.currentTarget || ev.target);
-		}
+const selectAll = () => {
+	if (selectedEmojis.value.length > 0) {
+		selectedEmojis.value = [];
+	} else {
+		selectedEmojis.value = emojisPaginationComponent.value.items.map(item => item.id);
 	}
+};
+
+const toggleSelect = (emoji) => {
+	if (selectedEmojis.value.includes(emoji.id)) {
+		selectedEmojis.value = selectedEmojis.value.filter(x => x !== emoji.id);
+	} else {
+		selectedEmojis.value.push(emoji.id);
+	}
+};
+
+const add = async (ev: MouseEvent) => {
+	const files = await selectFiles(ev.currentTarget || ev.target, null);
+
+	const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
+		fileId: file.id,
+	})));
+	promise.then(() => {
+		emojisPaginationComponent.value.reload();
+	});
+	os.promiseDialog(promise);
+};
+
+const edit = (emoji) => {
+	os.popup(import('./emoji-edit-dialog.vue'), {
+		emoji: emoji
+	}, {
+		done: result => {
+			if (result.updated) {
+				emojisPaginationComponent.value.replaceItem(item => item.id === emoji.id, {
+					...emoji,
+					...result.updated
+				});
+			} else if (result.deleted) {
+				emojisPaginationComponent.value.removeItem(item => item.id === emoji.id);
+			}
+		},
+	}, 'closed');
+};
+
+const im = (emoji) => {
+	os.apiWithDialog('admin/emoji/copy', {
+		emojiId: emoji.id,
+	});
+};
+
+const remoteMenu = (emoji, ev: MouseEvent) => {
+	os.popupMenu([{
+		type: 'label',
+		text: ':' + emoji.name + ':',
+	}, {
+		text: i18n.locale.import,
+		icon: 'fas fa-plus',
+		action: () => { im(emoji) }
+	}], ev.currentTarget || ev.target);
+};
+
+const menu = (ev: MouseEvent) => {
+	os.popupMenu([{
+		icon: 'fas fa-download',
+		text: i18n.locale.export,
+		action: async () => {
+			os.api('export-custom-emojis', {
+			})
+			.then(() => {
+				os.alert({
+					type: 'info',
+					text: i18n.locale.exportRequested,
+				});
+			}).catch((e) => {
+				os.alert({
+					type: 'error',
+					text: e.message,
+				});
+			});
+		}
+	}, {
+		icon: 'fas fa-upload',
+		text: i18n.locale.import,
+		action: async () => {
+			const file = await selectFile(ev.currentTarget || ev.target);
+			os.api('admin/emoji/import-zip', {
+				fileId: file.id,
+			})
+			.then(() => {
+				os.alert({
+					type: 'info',
+					text: i18n.locale.importRequested,
+				});
+			}).catch((e) => {
+				os.alert({
+					type: 'error',
+					text: e.message,
+				});
+			});
+		}
+	}], ev.currentTarget || ev.target);
+};
+
+const setCategoryBulk = async () => {
+	const { canceled, result } = await os.inputText({
+		title: 'Category',
+	});
+	if (canceled) return;
+	await os.apiWithDialog('admin/emoji/set-category-bulk', {
+		ids: selectedEmojis.value,
+		category: result,
+	});
+	emojisPaginationComponent.value.reload();
+};
+
+const addTagBulk = async () => {
+	const { canceled, result } = await os.inputText({
+		title: 'Tag',
+	});
+	if (canceled) return;
+	await os.apiWithDialog('admin/emoji/add-aliases-bulk', {
+		ids: selectedEmojis.value,
+		aliases: result.split(' '),
+	});
+	emojisPaginationComponent.value.reload();
+};
+
+const removeTagBulk = async () => {
+	const { canceled, result } = await os.inputText({
+		title: 'Tag',
+	});
+	if (canceled) return;
+	await os.apiWithDialog('admin/emoji/remove-aliases-bulk', {
+		ids: selectedEmojis.value,
+		aliases: result.split(' '),
+	});
+	emojisPaginationComponent.value.reload();
+};
+
+const setTagBulk = async () => {
+	const { canceled, result } = await os.inputText({
+		title: 'Tag',
+	});
+	if (canceled) return;
+	await os.apiWithDialog('admin/emoji/set-aliases-bulk', {
+		ids: selectedEmojis.value,
+		aliases: result.split(' '),
+	});
+	emojisPaginationComponent.value.reload();
+};
+
+const delBulk = async () => {
+	const { canceled } = await os.confirm({
+		type: 'warning',
+		text: i18n.locale.deleteConfirm,
+	});
+	if (canceled) return;
+	await os.apiWithDialog('admin/emoji/delete-bulk', {
+		ids: selectedEmojis.value,
+	});
+	emojisPaginationComponent.value.reload();
+};
+
+defineExpose({
+	[symbols.PAGE_INFO]: computed(() => ({
+		title: i18n.locale.customEmojis,
+		icon: 'fas fa-laugh',
+		bg: 'var(--bg)',
+		actions: [{
+			asFullButton: true,
+			icon: 'fas fa-plus',
+			text: i18n.locale.addEmoji,
+			handler: add,
+		}, {
+			icon: 'fas fa-ellipsis-h',
+			handler: menu,
+		}],
+		tabs: [{
+			active: tab.value === 'local',
+			title: i18n.locale.local,
+			onClick: () => { tab.value = 'local'; },
+		}, {
+			active: tab.value === 'remote',
+			title: i18n.locale.remote,
+			onClick: () => { tab.value = 'remote'; },
+		},]
+	})),
 });
 </script>
 
@@ -212,11 +308,16 @@ export default defineComponent({
 			> .emoji {
 				display: flex;
 				align-items: center;
-				padding: 12px;
+				padding: 11px;
 				text-align: left;
+				border: solid 1px var(--panel);
 
 				&:hover {
-					color: var(--accent);
+					border-color: var(--inputBorderHover);
+				}
+
+				&.selected {
+					border-color: var(--accent);
 				}
 
 				> .img {
diff --git a/packages/client/src/pages/admin/files.vue b/packages/client/src/pages/admin/files.vue
index 032e394a66..87dd12f489 100644
--- a/packages/client/src/pages/admin/files.vue
+++ b/packages/client/src/pages/admin/files.vue
@@ -19,7 +19,7 @@
 					<option value="local">{{ $ts.local }}</option>
 					<option value="remote">{{ $ts.remote }}</option>
 				</MkSelect>
-				<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params().origin === 'local'">
+				<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'">
 					<template #label>{{ $ts.host }}</template>
 				</MkInput>
 			</div>
@@ -55,7 +55,7 @@
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue';
+import { computed, defineComponent } from 'vue';
 import MkButton from '@/components/ui/button.vue';
 import MkInput from '@/components/form/input.vue';
 import MkSelect from '@/components/form/select.vue';
@@ -95,33 +95,17 @@ export default defineComponent({
 			type: null,
 			searchHost: '',
 			pagination: {
-				endpoint: 'admin/drive/files',
+				endpoint: 'admin/drive/files' as const,
 				limit: 10,
-				params: () => ({
+				params: computed(() => ({
 					type: (this.type && this.type !== '') ? this.type : null,
 					origin: this.origin,
-					hostname: (this.hostname && this.hostname !== '') ? this.hostname : null,
-				}),
+					hostname: (this.searchHost && this.searchHost !== '') ? this.searchHost : null,
+				})),
 			},
 		}
 	},
 
-	watch: {
-		type() {
-			this.$refs.files.reload();
-		},
-		origin() {
-			this.$refs.files.reload();
-		},
-		searchHost() {
-			this.$refs.files.reload();
-		},
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		clear() {
 			os.confirm({
diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue
index b7160de11d..350e7defc6 100644
--- a/packages/client/src/pages/admin/index.vue
+++ b/packages/client/src/pages/admin/index.vue
@@ -19,7 +19,7 @@
 	<div class="main">
 		<MkStickyContainer>
 			<template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template>
-			<component :is="component" :key="page" v-bind="pageProps" @info="onInfo"/>
+			<component :is="component" :ref="el => pageChanged(el)" :key="page" v-bind="pageProps"/>
 		</MkStickyContainer>
 	</div>
 </div>
@@ -66,7 +66,9 @@ export default defineComponent({
 		const narrow = ref(false);
 		const view = ref(null);
 		const el = ref(null);
-		const onInfo = (viewInfo) => {
+		const pageChanged = (page) => {
+			if (page == null) return;
+			const viewInfo = page[symbols.PAGE_INFO];
 			if (isRef(viewInfo)) {
 				watch(viewInfo, () => {
 					childInfo.value = viewInfo.value;
@@ -311,7 +313,7 @@ export default defineComponent({
 			narrow,
 			view,
 			el,
-			onInfo,
+			pageChanged,
 			childInfo,
 			pageProps,
 			component,
diff --git a/packages/client/src/pages/admin/instance-block.vue b/packages/client/src/pages/admin/instance-block.vue
index d1f7914ee4..6cadc7df39 100644
--- a/packages/client/src/pages/admin/instance-block.vue
+++ b/packages/client/src/pages/admin/instance-block.vue
@@ -40,10 +40,6 @@ export default defineComponent({
 		}
 	},
 
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async init() {
 			const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/integrations.discord.vue b/packages/client/src/pages/admin/integrations.discord.vue
index 8303afa3b0..8fc340150a 100644
--- a/packages/client/src/pages/admin/integrations.discord.vue
+++ b/packages/client/src/pages/admin/integrations.discord.vue
@@ -58,10 +58,6 @@ export default defineComponent({
 		}
 	},
 
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async init() {
 			const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/integrations.github.vue b/packages/client/src/pages/admin/integrations.github.vue
index c0316c317a..d9db9c00f1 100644
--- a/packages/client/src/pages/admin/integrations.github.vue
+++ b/packages/client/src/pages/admin/integrations.github.vue
@@ -58,10 +58,6 @@ export default defineComponent({
 		}
 	},
 
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async init() {
 			const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/integrations.twitter.vue b/packages/client/src/pages/admin/integrations.twitter.vue
index 5feabcc39d..1f8074535a 100644
--- a/packages/client/src/pages/admin/integrations.twitter.vue
+++ b/packages/client/src/pages/admin/integrations.twitter.vue
@@ -58,10 +58,6 @@ export default defineComponent({
 		}
 	},
 
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async init() {
 			const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/integrations.vue b/packages/client/src/pages/admin/integrations.vue
index 455fb6f4d6..91d03fef31 100644
--- a/packages/client/src/pages/admin/integrations.vue
+++ b/packages/client/src/pages/admin/integrations.vue
@@ -60,10 +60,6 @@ export default defineComponent({
 		}
 	},
 
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async init() {
 			const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/object-storage.vue b/packages/client/src/pages/admin/object-storage.vue
index 0f74865b10..6c5be220f8 100644
--- a/packages/client/src/pages/admin/object-storage.vue
+++ b/packages/client/src/pages/admin/object-storage.vue
@@ -118,10 +118,6 @@ export default defineComponent({
 		}
 	},
 
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async init() {
 			const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/other-settings.vue b/packages/client/src/pages/admin/other-settings.vue
index d21d0c5992..6b588e88aa 100644
--- a/packages/client/src/pages/admin/other-settings.vue
+++ b/packages/client/src/pages/admin/other-settings.vue
@@ -42,10 +42,6 @@ export default defineComponent({
 		}
 	},
 
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async init() {
 			const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue
index 564a63fda0..b8ae8ad9e1 100644
--- a/packages/client/src/pages/admin/overview.vue
+++ b/packages/client/src/pages/admin/overview.vue
@@ -116,8 +116,6 @@ export default defineComponent({
 	},
 
 	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-
 		os.api('meta', { detail: true }).then(meta => {
 			this.meta = meta;
 		});
diff --git a/packages/client/src/pages/admin/proxy-account.vue b/packages/client/src/pages/admin/proxy-account.vue
index 9878df9122..5c4fbffa0c 100644
--- a/packages/client/src/pages/admin/proxy-account.vue
+++ b/packages/client/src/pages/admin/proxy-account.vue
@@ -44,10 +44,6 @@ export default defineComponent({
 		}
 	},
 
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async init() {
 			const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/queue.vue b/packages/client/src/pages/admin/queue.vue
index 719a3c2651..522210d933 100644
--- a/packages/client/src/pages/admin/queue.vue
+++ b/packages/client/src/pages/admin/queue.vue
@@ -38,8 +38,6 @@ export default defineComponent({
 	},
 
 	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-
 		this.$nextTick(() => {
 			this.connection.send('requestLog', {
 				id: Math.random().toString().substr(2, 8),
diff --git a/packages/client/src/pages/admin/relays.vue b/packages/client/src/pages/admin/relays.vue
index 5ab02002b4..bb840db0a2 100644
--- a/packages/client/src/pages/admin/relays.vue
+++ b/packages/client/src/pages/admin/relays.vue
@@ -48,10 +48,6 @@ export default defineComponent({
 		this.refresh();
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async addRelay() {
 			const { canceled, result: inbox } = await os.inputText({
diff --git a/packages/client/src/pages/admin/security.vue b/packages/client/src/pages/admin/security.vue
index 276c514f16..d069891647 100644
--- a/packages/client/src/pages/admin/security.vue
+++ b/packages/client/src/pages/admin/security.vue
@@ -70,10 +70,6 @@ export default defineComponent({
 		}
 	},
 
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async init() {
 			const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue
index 802d7463ec..a4bac93834 100644
--- a/packages/client/src/pages/admin/settings.vue
+++ b/packages/client/src/pages/admin/settings.vue
@@ -197,10 +197,6 @@ export default defineComponent({
 		}
 	},
 
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async init() {
 			const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/users.vue b/packages/client/src/pages/admin/users.vue
index e7a3437167..03e155ddcf 100644
--- a/packages/client/src/pages/admin/users.vue
+++ b/packages/client/src/pages/admin/users.vue
@@ -30,7 +30,7 @@
 				<template #prefix>@</template>
 				<template #label>{{ $ts.username }}</template>
 			</MkInput>
-			<MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params().origin === 'local'" @update:modelValue="$refs.users.reload()">
+			<MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()">
 				<template #prefix>@</template>
 				<template #label>{{ $ts.host }}</template>
 			</MkInput>
@@ -62,7 +62,7 @@
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue';
+import { computed, defineComponent } from 'vue';
 import MkButton from '@/components/ui/button.vue';
 import MkInput from '@/components/form/input.vue';
 import MkSelect from '@/components/form/select.vue';
@@ -110,36 +110,20 @@ export default defineComponent({
 			searchUsername: '',
 			searchHost: '',
 			pagination: {
-				endpoint: 'admin/show-users',
+				endpoint: 'admin/show-users' as const,
 				limit: 10,
-				params: () => ({
+				params: computed(() => ({
 					sort: this.sort,
 					state: this.state,
 					origin: this.origin,
 					username: this.searchUsername,
 					hostname: this.searchHost,
-				}),
+				})),
 				offsetMode: true
 			},
 		}
 	},
 
-	watch: {
-		sort() {
-			this.$refs.users.reload();
-		},
-		state() {
-			this.$refs.users.reload();
-		},
-		origin() {
-			this.$refs.users.reload();
-		},
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		lookupUser,
 
diff --git a/packages/client/src/pages/announcements.vue b/packages/client/src/pages/announcements.vue
index ca94640dda..53727823a4 100644
--- a/packages/client/src/pages/announcements.vue
+++ b/packages/client/src/pages/announcements.vue
@@ -36,7 +36,7 @@ export default defineComponent({
 				bg: 'var(--bg)',
 			},
 			pagination: {
-				endpoint: 'announcements',
+				endpoint: 'announcements' as const,
 				limit: 10,
 			},
 		};
diff --git a/packages/client/src/pages/channel.vue b/packages/client/src/pages/channel.vue
index 67ab2d8981..c9a8f36844 100644
--- a/packages/client/src/pages/channel.vue
+++ b/packages/client/src/pages/channel.vue
@@ -67,11 +67,11 @@ export default defineComponent({
 			channel: null,
 			showBanner: true,
 			pagination: {
-				endpoint: 'channels/timeline',
+				endpoint: 'channels/timeline' as const,
 				limit: 10,
-				params: () => ({
+				params: computed(() => ({
 					channelId: this.channelId,
-				})
+				}))
 			},
 		};
 	},
diff --git a/packages/client/src/pages/channels.vue b/packages/client/src/pages/channels.vue
index 48877ab3ec..4e538a6da3 100644
--- a/packages/client/src/pages/channels.vue
+++ b/packages/client/src/pages/channels.vue
@@ -60,15 +60,15 @@ export default defineComponent({
 			})),
 			tab: 'featured',
 			featuredPagination: {
-				endpoint: 'channels/featured',
+				endpoint: 'channels/featured' as const,
 				noPaging: true,
 			},
 			followingPagination: {
-				endpoint: 'channels/followed',
+				endpoint: 'channels/followed' as const,
 				limit: 5,
 			},
 			ownedPagination: {
-				endpoint: 'channels/owned',
+				endpoint: 'channels/owned' as const,
 				limit: 5,
 			},
 		};
diff --git a/packages/client/src/pages/clip.vue b/packages/client/src/pages/clip.vue
index 077a6ac8b5..6b49221d32 100644
--- a/packages/client/src/pages/clip.vue
+++ b/packages/client/src/pages/clip.vue
@@ -50,11 +50,11 @@ export default defineComponent({
 			} : null),
 			clip: null,
 			pagination: {
-				endpoint: 'clips/notes',
+				endpoint: 'clips/notes' as const,
 				limit: 10,
-				params: () => ({
+				params: computed(() => ({
 					clipId: this.clipId,
-				})
+				}))
 			},
 		};
 	},
diff --git a/packages/client/src/pages/drive.vue b/packages/client/src/pages/drive.vue
index f30000367f..1e17bea0cc 100644
--- a/packages/client/src/pages/drive.vue
+++ b/packages/client/src/pages/drive.vue
@@ -4,27 +4,21 @@
 </div>
 </template>
 
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
 import XDrive from '@/components/drive.vue';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		XDrive
-	},
+let folder = $ref(null);
 
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: computed(() => this.folder ? this.folder.name : this.$ts.drive),
-				icon: 'fas fa-cloud',
-				bg: 'var(--bg)',
-				hideHeader: true,
-			},
-			folder: null,
-		};
-	},
+defineExpose({
+	[symbols.PAGE_INFO]: computed(() => ({
+		title: folder ? folder.name : i18n.locale.drive,
+		icon: 'fas fa-cloud',
+		bg: 'var(--bg)',
+		hideHeader: true,
+	})),
 });
 </script>
diff --git a/packages/client/src/pages/emojis.emoji.vue b/packages/client/src/pages/emojis.emoji.vue
index 5dab72daea..83539ce7a3 100644
--- a/packages/client/src/pages/emojis.emoji.vue
+++ b/packages/client/src/pages/emojis.emoji.vue
@@ -8,35 +8,29 @@
 </button>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import * as os from '@/os';
 import copyToClipboard from '@/scripts/copy-to-clipboard';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	props: {
-		emoji: {
-			type: Object,
-			required: true,
-		}
-	},
+const props = defineProps<{
+	emoji: Record<string, unknown>; // TODO
+}>();
 
-	methods: {
-		menu(ev) {
-			os.popupMenu([{
-				type: 'label',
-				text: ':' + this.emoji.name + ':',
-			}, {
-				text: this.$ts.copy,
-				icon: 'fas fa-copy',
-				action: () => {
-					copyToClipboard(`:${this.emoji.name}:`);
-					os.success();
-				}
-			}], ev.currentTarget || ev.target);
+function menu(ev) {
+	os.popupMenu([{
+		type: 'label',
+		text: ':' + props.emoji.name + ':',
+	}, {
+		text: i18n.locale.copy,
+		icon: 'fas fa-copy',
+		action: () => {
+			copyToClipboard(`:${props.emoji.name}:`);
+			os.success();
 		}
-	}
-});
+	}], ev.currentTarget || ev.target);
+}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/pages/emojis.vue b/packages/client/src/pages/emojis.vue
index 2adb5345e2..6577f5abd9 100644
--- a/packages/client/src/pages/emojis.vue
+++ b/packages/client/src/pages/emojis.vue
@@ -4,55 +4,47 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, computed } from 'vue';
+<script lang="ts" setup>
+import { ref, computed } from 'vue';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
 import XCategory from './emojis.category.vue';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		XCategory,
-	},
+const tab = ref('category');
 
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => ({
-				title: this.$ts.customEmojis,
-				icon: 'fas fa-laugh',
-				bg: 'var(--bg)',
-				actions: [{
-					icon: 'fas fa-ellipsis-h',
-					handler: this.menu
-				}],
-			})),
-			tab: 'category',
+function menu(ev) {
+	os.popupMenu([{
+		icon: 'fas fa-download',
+		text: i18n.locale.export,
+		action: async () => {
+			os.api('export-custom-emojis', {
+			})
+			.then(() => {
+				os.alert({
+					type: 'info',
+					text: i18n.locale.exportRequested,
+				});
+			}).catch((e) => {
+				os.alert({
+					type: 'error',
+					text: e.message,
+				});
+			});
 		}
-	},
+	}], ev.currentTarget || ev.target);
+}
 
-	methods: {
-		menu(ev) {
-			os.popupMenu([{
-				icon: 'fas fa-download',
-				text: this.$ts.export,
-				action: async () => {
-					os.api('export-custom-emojis', {
-					})
-					.then(() => {
-						os.alert({
-							type: 'info',
-							text: this.$ts.exportRequested,
-						});
-					}).catch((e) => {
-						os.alert({
-							type: 'error',
-							text: e.message,
-						});
-					});
-				}
-			}], ev.currentTarget || ev.target);
-		}
-	}
+defineExpose({
+	[symbols.PAGE_INFO]: {
+		title: i18n.locale.customEmojis,
+		icon: 'fas fa-laugh',
+		bg: 'var(--bg)',
+		actions: [{
+			icon: 'fas fa-ellipsis-h',
+			handler: menu,
+		}],
+	},
 });
 </script>
 
diff --git a/packages/client/src/pages/explore.vue b/packages/client/src/pages/explore.vue
index a3c3b771f2..04cc3662a7 100644
--- a/packages/client/src/pages/explore.vue
+++ b/packages/client/src/pages/explore.vue
@@ -156,7 +156,7 @@ export default defineComponent({
 				sort: '+createdAt',
 			} },
 			searchPagination: {
-				endpoint: 'users/search',
+				endpoint: 'users/search' as const,
 				limit: 10,
 				params: computed(() => (this.searchQuery && this.searchQuery !== '') ? {
 					query: this.searchQuery,
@@ -178,7 +178,7 @@ export default defineComponent({
 		},
 		tagUsers(): any {
 			return {
-				endpoint: 'hashtags/users',
+				endpoint: 'hashtags/users' as const,
 				limit: 30,
 				params: {
 					tag: this.tag,
diff --git a/packages/client/src/pages/favorites.vue b/packages/client/src/pages/favorites.vue
index 588b0fa66c..8965b30d60 100644
--- a/packages/client/src/pages/favorites.vue
+++ b/packages/client/src/pages/favorites.vue
@@ -1,21 +1,37 @@
 <template>
 <MkSpacer :content-max="800">
-	<XNotes :pagination="pagination" :detail="true" :prop="'note'"/>
+	<MkPagination ref="pagingComponent" :pagination="pagination">
+		<template #empty>
+			<div class="_fullinfo">
+				<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+				<div>{{ $ts.noNotes }}</div>
+			</div>
+		</template>
+
+		<template #default="{ items }">
+			<XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false">
+				<XNote :key="item.id" :note="item.note" :class="$style.note"/>
+			</XList>
+		</template>
+	</MkPagination>
 </MkSpacer>
 </template>
 
 <script lang="ts" setup>
-import XNotes from '@/components/notes.vue';
+import { ref } from 'vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import XNote from '@/components/note.vue';
+import XList from '@/components/date-separated-list.vue';
 import * as symbols from '@/symbols';
 import { i18n } from '@/i18n';
 
 const pagination = {
-	endpoint: 'i/favorites',
+	endpoint: 'i/favorites' as const,
 	limit: 10,
-	params: () => ({
-	}),
 };
 
+const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+
 defineExpose({
 	[symbols.PAGE_INFO]: {
 		title: i18n.locale.favorites,
@@ -24,3 +40,10 @@ defineExpose({
 	},
 });
 </script>
+
+<style lang="scss" module>
+.note {
+	background: var(--panel);
+	border-radius: var(--radius);
+}
+</style>
diff --git a/packages/client/src/pages/featured.vue b/packages/client/src/pages/featured.vue
index efa74ca599..725c70f0f7 100644
--- a/packages/client/src/pages/featured.vue
+++ b/packages/client/src/pages/featured.vue
@@ -10,7 +10,7 @@ import * as symbols from '@/symbols';
 import { i18n } from '@/i18n';
 
 const pagination = {
-	endpoint: 'notes/featured',
+	endpoint: 'notes/featured' as const,
 	limit: 10,
 	offsetMode: true,
 };
diff --git a/packages/client/src/pages/federation.vue b/packages/client/src/pages/federation.vue
index a467c5eeb8..6a4a28b6b4 100644
--- a/packages/client/src/pages/federation.vue
+++ b/packages/client/src/pages/federation.vue
@@ -95,8 +95,8 @@
 </MkSpacer>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
 import MkButton from '@/components/ui/button.vue';
 import MkInput from '@/components/form/input.vue';
 import MkSelect from '@/components/form/select.vue';
@@ -104,68 +104,41 @@ import MkPagination from '@/components/ui/pagination.vue';
 import FormSplit from '@/components/form/split.vue';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkSelect,
-		MkPagination,
-		FormSplit,
+let host = $ref('');
+let state = $ref('federating');
+let sort = $ref('+pubSub');
+const pagination = {
+	endpoint: 'federation/instances' as const,
+	limit: 10,
+	offsetMode: true,
+	params: computed(() => ({
+		sort: sort,
+		host: host != '' ? host : null,
+		...(
+			state === 'federating' ? { federating: true } :
+			state === 'subscribing' ? { subscribing: true } :
+			state === 'publishing' ? { publishing: true } :
+			state === 'suspended' ? { suspended: true } :
+			state === 'blocked' ? { blocked: true } :
+			state === 'notResponding' ? { notResponding: true } :
+			{})
+	}))
+};
+
+function getStatus(instance) {
+	if (instance.isSuspended) return 'suspended';
+	if (instance.isNotResponding) return 'error';
+	return 'alive';
+};
+
+defineExpose({
+	[symbols.PAGE_INFO]: {
+		title: i18n.locale.federation,
+		icon: 'fas fa-globe',
+		bg: 'var(--bg)',
 	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.federation,
-				icon: 'fas fa-globe',
-				bg: 'var(--bg)',
-			},
-			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 } :
-						{})
-				})
-			},
-		}
-	},
-
-	watch: {
-		host() {
-			this.$refs.instances.reload();
-		},
-		state() {
-			this.$refs.instances.reload();
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		getStatus(instance) {
-			if (instance.isSuspended) return 'suspended';
-			if (instance.isNotResponding) return 'error';
-			return 'alive';
-		},
-	}
 });
 </script>
 
diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue
index 54d695091d..764daa0d3e 100644
--- a/packages/client/src/pages/follow-requests.vue
+++ b/packages/client/src/pages/follow-requests.vue
@@ -1,6 +1,6 @@
 <template>
 <div>
-	<MkPagination ref="list" :pagination="pagination" class="mk-follow-requests">
+	<MkPagination ref="paginationComponent" :pagination="pagination">
 		<template #empty>
 			<div class="_fullinfo">
 				<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
@@ -8,19 +8,21 @@
 			</div>
 		</template>
 		<template v-slot="{items}">
-			<div v-for="req in items" :key="req.id" class="user _panel">
-				<MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/>
-				<div class="body">
-					<div class="name">
-						<MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA>
-						<p class="acct">@{{ acct(req.follower) }}</p>
-					</div>
-					<div v-if="req.follower.description" class="description" :title="req.follower.description">
-						<Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
-					</div>
-					<div class="actions">
-						<button class="_button" @click="accept(req.follower)"><i class="fas fa-check"></i></button>
-						<button class="_button" @click="reject(req.follower)"><i class="fas fa-times"></i></button>
+			<div class="mk-follow-requests">
+				<div v-for="req in items" :key="req.id" class="user _panel">
+					<MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/>
+					<div class="body">
+						<div class="name">
+							<MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA>
+							<p class="acct">@{{ acct(req.follower) }}</p>
+						</div>
+						<div v-if="req.follower.description" class="description" :title="req.follower.description">
+							<Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
+						</div>
+						<div class="actions">
+							<button class="_button" @click="accept(req.follower)"><i class="fas fa-check"></i></button>
+							<button class="_button" @click="reject(req.follower)"><i class="fas fa-times"></i></button>
+						</div>
 					</div>
 				</div>
 			</div>
@@ -29,45 +31,39 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref, computed } from 'vue';
 import MkPagination from '@/components/ui/pagination.vue';
 import { userPage, acct } from '@/filters/user';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		MkPagination
-	},
+const paginationComponent = ref<InstanceType<typeof MkPagination>>();
 
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.followRequests,
-				icon: 'fas fa-user-clock',
-			},
-			pagination: {
-				endpoint: 'following/requests/list',
-				limit: 10,
-			},
-		};
-	},
+const pagination = {
+	endpoint: 'following/requests/list' as const,
+	limit: 10,
+};
 
-	methods: {
-		accept(user) {
-			os.api('following/requests/accept', { userId: user.id }).then(() => {
-				this.$refs.list.reload();
-			});
-		},
-		reject(user) {
-			os.api('following/requests/reject', { userId: user.id }).then(() => {
-				this.$refs.list.reload();
-			});
-		},
-		userPage,
-		acct
-	}
+function accept(user) {
+	os.api('following/requests/accept', { userId: user.id }).then(() => {
+		paginationComponent.value.reload();
+	});
+}
+
+function reject(user) {
+	os.api('following/requests/reject', { userId: user.id }).then(() => {
+		paginationComponent.value.reload();
+	});
+}
+
+defineExpose({
+	[symbols.PAGE_INFO]: computed(() => ({
+		title: i18n.locale.followRequests,
+		icon: 'fas fa-user-clock',
+		bg: 'var(--bg)',
+	})),
 });
 </script>
 
diff --git a/packages/client/src/pages/gallery/index.vue b/packages/client/src/pages/gallery/index.vue
index cd0d2a40e4..a19d69d5c2 100644
--- a/packages/client/src/pages/gallery/index.vue
+++ b/packages/client/src/pages/gallery/index.vue
@@ -81,19 +81,19 @@ export default defineComponent({
 			},
 			tab: 'explore',
 			recentPostsPagination: {
-				endpoint: 'gallery/posts',
+				endpoint: 'gallery/posts' as const,
 				limit: 6,
 			},
 			popularPostsPagination: {
-				endpoint: 'gallery/featured',
+				endpoint: 'gallery/featured' as const,
 				limit: 5,
 			},
 			myPostsPagination: {
-				endpoint: 'i/gallery/posts',
+				endpoint: 'i/gallery/posts' as const,
 				limit: 5,
 			},
 			likedPostsPagination: {
-				endpoint: 'i/gallery/likes',
+				endpoint: 'i/gallery/likes' as const,
 				limit: 5,
 			},
 			tags: [],
@@ -106,7 +106,7 @@ export default defineComponent({
 		},
 		tagUsers(): any {
 			return {
-				endpoint: 'hashtags/users',
+				endpoint: 'hashtags/users' as const,
 				limit: 30,
 				params: {
 					tag: this.tag,
diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue
index 096947e6f8..fff2b6a74e 100644
--- a/packages/client/src/pages/gallery/post.vue
+++ b/packages/client/src/pages/gallery/post.vue
@@ -93,11 +93,11 @@ export default defineComponent({
 				}]
 			} : null),
 			otherPostsPagination: {
-				endpoint: 'users/gallery/posts',
+				endpoint: 'users/gallery/posts' as const,
 				limit: 6,
-				params: () => ({
+				params: computed(() => ({
 					userId: this.post.user.id
-				})
+				})),
 			},
 			post: null,
 			error: null,
diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue
index 475107ab6d..fa36db0659 100644
--- a/packages/client/src/pages/instance-info.vue
+++ b/packages/client/src/pages/instance-info.vue
@@ -28,7 +28,7 @@
 		<FormSection v-if="iAmModerator">
 			<template #label>Moderation</template>
 			<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.stopActivityDelivery }}</FormSwitch>
-			<FormSwitch :model-value="isBlocked" class="switch" @update:modelValue="changeBlock">{{ $ts.blockThisInstance }}</FormSwitch>
+			<FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ $ts.blockThisInstance }}</FormSwitch>
 		</FormSection>
 
 		<FormSection>
@@ -104,15 +104,14 @@
 </MkSpacer>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
 import MkChart from '@/components/chart.vue';
 import MkObjectView from '@/components/object-view.vue';
-import FormTextarea from '@/components/form/textarea.vue';
 import FormLink from '@/components/form/link.vue';
 import MkLink from '@/components/link.vue';
 import FormSection from '@/components/form/section.vue';
-import FormButton from '@/components/ui/button.vue';
 import MkKeyValue from '@/components/key-value.vue';
 import MkSelect from '@/components/form/select.vue';
 import FormSwitch from '@/components/form/switch.vue';
@@ -120,87 +119,57 @@ import * as os from '@/os';
 import number from '@/filters/number';
 import bytes from '@/filters/bytes';
 import * as symbols from '@/symbols';
+import { iAmModerator } from '@/account';
 
-export default defineComponent({
-	components: {
-		FormTextarea,
-		MkObjectView,
-		FormButton,
-		FormLink,
-		FormSection,
-		FormSwitch,
-		MkKeyValue,
-		MkSelect,
-		MkChart,
-		MkLink,
+const props = defineProps<{
+	host: string;
+}>();
+
+let meta = $ref<misskey.entities.DetailedInstanceMetadata | null>(null);
+let instance = $ref<misskey.entities.Instance | null>(null);
+let suspended = $ref(false);
+let isBlocked = $ref(false);
+let chartSrc = $ref('instance-requests');
+let chartSpan = $ref('hour');
+
+async function fetch() {
+	meta = await os.api('meta', { detail: true });
+	instance = await os.api('federation/show-instance', {
+		host: props.host,
+	});
+	suspended = instance.isSuspended;
+	isBlocked = meta.blockedHosts.includes(instance.host);
+}
+
+async function toggleBlock(ev) {
+	if (meta == null) return;
+	await os.api('admin/update-meta', {
+		blockedHosts: isBlocked ? meta.blockedHosts.concat([instance.host]) : meta.blockedHosts.filter(x => x !== instance.host)
+	});
+}
+
+async function toggleSuspend(v) {
+	await os.api('admin/federation/update-instance', {
+		host: instance.host,
+		isSuspended: suspended,
+	});
+}
+
+fetch();
+
+defineExpose({
+	[symbols.PAGE_INFO]: {
+		title: props.host,
+		icon: 'fas fa-info-circle',
+		bg: 'var(--bg)',
+		actions: [{
+			text: `https://${props.host}`,
+			icon: 'fas fa-external-link-alt',
+			handler: () => {
+				window.open(`https://${props.host}`, '_blank');
+			}
+		}],
 	},
-
-	props: {
-		host: {
-			type: String,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.host,
-				icon: 'fas fa-info-circle',
-				bg: 'var(--bg)',
-				actions: [{
-					text: `https://${this.host}`,
-					icon: 'fas fa-external-link-alt',
-					handler: () => {
-						window.open(`https://${this.host}`, '_blank');
-					}
-				}],
-			},
-			instance: null,
-			suspended: false,
-			chartSrc: 'instance-requests',
-			chartSpan: 'hour',
-		}
-	},
-
-	computed: {
-		iAmModerator(): boolean {
-			return this.$i && (this.$i.isAdmin || this.$i.isModerator);
-		},
-
-		isBlocked() {
-			return this.instance && this.$instance && this.$instance.blockedHosts && this.$instance.blockedHosts.includes(this.instance.host);
-		}
-	},
-
-	mounted() {
-		this.fetch();
-	},
-
-	methods: {
-		number,
-		bytes,
-
-		async fetch() {
-			this.instance = await os.api('federation/show-instance', {
-				host: this.host
-			});
-			this.suspended = this.instance.isSuspended;
-		},
-
-		changeBlock(e) {
-			os.api('admin/update-meta', {
-				blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
-			});
-		},
-
-		async toggleSuspend(v) {
-			await os.api('admin/federation/update-instance', {
-				host: this.instance.host,
-				isSuspended: this.suspended
-			});
-		},
-	}
 });
 </script>
 
diff --git a/packages/client/src/pages/mentions.vue b/packages/client/src/pages/mentions.vue
index ea23c6a2f6..bda56fc729 100644
--- a/packages/client/src/pages/mentions.vue
+++ b/packages/client/src/pages/mentions.vue
@@ -10,7 +10,7 @@ import * as symbols from '@/symbols';
 import { i18n } from '@/i18n';
 
 const pagination = {
-	endpoint: 'notes/mentions',
+	endpoint: 'notes/mentions' as const,
 	limit: 10,
 };
 
diff --git a/packages/client/src/pages/messages.vue b/packages/client/src/pages/messages.vue
index 448aa0241f..8efdc55586 100644
--- a/packages/client/src/pages/messages.vue
+++ b/packages/client/src/pages/messages.vue
@@ -10,7 +10,7 @@ import * as symbols from '@/symbols';
 import { i18n } from '@/i18n';
 
 const pagination = {
-	endpoint: 'notes/mentions',
+	endpoint: 'notes/mentions' as const,
 	limit: 10,
 	params: () => ({
 		visibility: 'specified'
diff --git a/packages/client/src/pages/messaging/messaging-room.vue b/packages/client/src/pages/messaging/messaging-room.vue
index 1bcee01d29..9a34551ddd 100644
--- a/packages/client/src/pages/messaging/messaging-room.vue
+++ b/packages/client/src/pages/messaging/messaging-room.vue
@@ -162,7 +162,7 @@ const Component = defineComponent({
 				// もっと見るの交差検知を発火させないためにfetchは
 				// スクロールが終わるまでfalseにしておく
 				// scrollendのようなイベントはないのでsetTimeoutで
-				setTimeout(() => this.fetching = false, 300);
+				window.setTimeout(() => this.fetching = false, 300);
 			});
 		},
 
@@ -300,9 +300,9 @@ const Component = defineComponent({
 				this.showIndicator = false;
 			});
 
-			if (this.timer) clearTimeout(this.timer);
+			if (this.timer) window.clearTimeout(this.timer);
 
-			this.timer = setTimeout(() => {
+			this.timer = window.setTimeout(() => {
 				this.showIndicator = false;
 			}, 4000);
 		},
diff --git a/packages/client/src/pages/my-antennas/create.vue b/packages/client/src/pages/my-antennas/create.vue
index 173807475a..427c9935c3 100644
--- a/packages/client/src/pages/my-antennas/create.vue
+++ b/packages/client/src/pages/my-antennas/create.vue
@@ -4,45 +4,37 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@/components/ui/button.vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import XAntenna from './editor.vue';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
+import { router } from '@/router';
 
-export default defineComponent({
-	components: {
-		MkButton,
-		XAntenna,
+let draft = $ref({
+	name: '',
+	src: 'all',
+	userListId: null,
+	userGroupId: null,
+	users: [],
+	keywords: [],
+	excludeKeywords: [],
+	withReplies: false,
+	caseSensitive: false,
+	withFile: false,
+	notify: false
+});
+
+function onAntennaCreated() {
+	router.push('/my/antennas');
+}
+
+defineExpose({
+	[symbols.PAGE_INFO]: {
+		title: i18n.locale.manageAntennas,
+		icon: 'fas fa-satellite',
+		bg: 'var(--bg)',
 	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.manageAntennas,
-				icon: 'fas fa-satellite',
-			},
-			draft: {
-				name: '',
-				src: 'all',
-				userListId: null,
-				userGroupId: null,
-				users: [],
-				keywords: [],
-				excludeKeywords: [],
-				withReplies: false,
-				caseSensitive: false,
-				withFile: false,
-				notify: false
-			},
-		};
-	},
-
-	methods: {
-		onAntennaCreated() {
-			this.$router.push('/my/antennas');
-		},
-	}
 });
 </script>
 
diff --git a/packages/client/src/pages/my-antennas/index.vue b/packages/client/src/pages/my-antennas/index.vue
index d185e796c3..7138d269a9 100644
--- a/packages/client/src/pages/my-antennas/index.vue
+++ b/packages/client/src/pages/my-antennas/index.vue
@@ -38,7 +38,7 @@ export default defineComponent({
 				}
 			},
 			pagination: {
-				endpoint: 'antennas/list',
+				endpoint: 'antennas/list' as const,
 				limit: 10,
 			},
 		};
diff --git a/packages/client/src/pages/my-clips/index.vue b/packages/client/src/pages/my-clips/index.vue
index a5bbc3fd2d..97b563f6f8 100644
--- a/packages/client/src/pages/my-clips/index.vue
+++ b/packages/client/src/pages/my-clips/index.vue
@@ -3,7 +3,7 @@
 	<div class="qtcaoidl">
 		<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
 
-		<MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="list">
+		<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="list">
 			<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
 				<b>{{ item.name }}</b>
 				<div v-if="item.description" class="description">{{ item.description }}</div>
@@ -13,71 +13,64 @@
 </MkSpacer>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import MkPagination from '@/components/ui/pagination.vue';
 import MkButton from '@/components/ui/button.vue';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
+import i18n from '@/components/global/i18n';
 
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkButton,
+const pagination = {
+	endpoint: 'clips/list' as const,
+	limit: 10,
+};
+
+const pagingComponent = $ref<InstanceType<typeof MkPagination>>();
+
+async function create() {
+	const { canceled, result } = await os.form(i18n.locale.createNewClip, {
+		name: {
+			type: 'string',
+			label: i18n.locale.name,
+		},
+		description: {
+			type: 'string',
+			required: false,
+			multiline: true,
+			label: i18n.locale.description,
+		},
+		isPublic: {
+			type: 'boolean',
+			label: i18n.locale.public,
+			default: false,
+		},
+	});
+	if (canceled) return;
+
+	os.apiWithDialog('clips/create', result);
+
+	pagingComponent.reload();
+}
+
+function onClipCreated() {
+	pagingComponent.reload();
+}
+
+function onClipDeleted() {
+	pagingComponent.reload();
+}
+
+defineExpose({
+	[symbols.PAGE_INFO]: {
+		title: i18n.locale.clip,
+		icon: 'fas fa-paperclip',
+		bg: 'var(--bg)',
+		action: {
+			icon: 'fas fa-plus',
+			handler: create
+		},
 	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.clip,
-				icon: 'fas fa-paperclip',
-				bg: 'var(--bg)',
-				action: {
-					icon: 'fas fa-plus',
-					handler: this.create
-				}
-			},
-			pagination: {
-				endpoint: 'clips/list',
-				limit: 10,
-			},
-			draft: null,
-		};
-	},
-
-	methods: {
-		async create() {
-			const { canceled, result } = await os.form(this.$ts.createNewClip, {
-				name: {
-					type: 'string',
-					label: this.$ts.name
-				},
-				description: {
-					type: 'string',
-					required: false,
-					multiline: true,
-					label: this.$ts.description
-				},
-				isPublic: {
-					type: 'boolean',
-					label: this.$ts.public,
-					default: false
-				}
-			});
-			if (canceled) return;
-
-			os.apiWithDialog('clips/create', result);
-		},
-
-		onClipCreated() {
-			this.$refs.list.reload();
-			this.draft = null;
-		},
-
-		onClipDeleted() {
-			this.$refs.list.reload();
-		},
-	}
 });
 </script>
 
diff --git a/packages/client/src/pages/my-groups/index.vue b/packages/client/src/pages/my-groups/index.vue
index db5ccde466..4b2b2963a8 100644
--- a/packages/client/src/pages/my-groups/index.vue
+++ b/packages/client/src/pages/my-groups/index.vue
@@ -87,15 +87,15 @@ export default defineComponent({
 			})),
 			tab: 'owned',
 			ownedPagination: {
-				endpoint: 'users/groups/owned',
+				endpoint: 'users/groups/owned' as const,
 				limit: 10,
 			},
 			joinedPagination: {
-				endpoint: 'users/groups/joined',
+				endpoint: 'users/groups/joined' as const,
 				limit: 10,
 			},
 			invitationPagination: {
-				endpoint: 'i/user-group-invites',
+				endpoint: 'i/user-group-invites' as const,
 				limit: 10,
 			},
 		};
diff --git a/packages/client/src/pages/my-lists/index.vue b/packages/client/src/pages/my-lists/index.vue
index 94a869b9ff..e6fcba1b34 100644
--- a/packages/client/src/pages/my-lists/index.vue
+++ b/packages/client/src/pages/my-lists/index.vue
@@ -3,7 +3,7 @@
 	<div class="qkcjvfiv">
 		<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton>
 
-		<MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="lists _content">
+		<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="lists _content">
 			<MkA v-for="list in items" :key="list.id" class="list _panel" :to="`/my/lists/${ list.id }`">
 				<div class="name">{{ list.name }}</div>
 				<MkAvatars :user-ids="list.userIds"/>
@@ -13,50 +13,41 @@
 </MkSpacer>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import MkPagination from '@/components/ui/pagination.vue';
 import MkButton from '@/components/ui/button.vue';
 import MkAvatars from '@/components/avatars.vue';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkButton,
-		MkAvatars,
-	},
+const pagingComponent = $ref<InstanceType<typeof MkPagination>>();
 
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.manageLists,
-				icon: 'fas fa-list-ul',
-				bg: 'var(--bg)',
-				action: {
-					icon: 'fas fa-plus',
-					handler: this.create
-				},
-			},
-			pagination: {
-				endpoint: 'users/lists/list',
-				limit: 10,
-			},
-		};
-	},
+const pagination = {
+	endpoint: 'users/lists/list' as const,
+	limit: 10,
+};
 
-	methods: {
-		async create() {
-			const { canceled, result: name } = await os.inputText({
-				title: this.$ts.enterListName,
-			});
-			if (canceled) return;
-			await os.api('users/lists/create', { name: name });
-			this.$refs.list.reload();
-			os.success();
+async function create() {
+	const { canceled, result: name } = await os.inputText({
+		title: i18n.locale.enterListName,
+	});
+	if (canceled) return;
+	await os.apiWithDialog('users/lists/create', { name: name });
+	pagingComponent.reload();
+}
+
+defineExpose({
+	[symbols.PAGE_INFO]: {
+		title: i18n.locale.manageLists,
+		icon: 'fas fa-list-ul',
+		bg: 'var(--bg)',
+		action: {
+			icon: 'fas fa-plus',
+			handler: create,
 		},
-	}
+	},
 });
 </script>
 
diff --git a/packages/client/src/pages/note.vue b/packages/client/src/pages/note.vue
index d40082381c..72ac85ee90 100644
--- a/packages/client/src/pages/note.vue
+++ b/packages/client/src/pages/note.vue
@@ -82,21 +82,21 @@ export default defineComponent({
 			showNext: false,
 			error: null,
 			prev: {
-				endpoint: 'users/notes',
+				endpoint: 'users/notes' as const,
 				limit: 10,
-				params: init => ({
+				params: computed(() => ({
 					userId: this.note.userId,
 					untilId: this.note.id,
-				})
+				})),
 			},
 			next: {
 				reversed: true,
-				endpoint: 'users/notes',
+				endpoint: 'users/notes' as const,
 				limit: 10,
-				params: init => ({
+				params: computed(() => ({
 					userId: this.note.userId,
 					sinceId: this.note.id,
-				})
+				})),
 			},
 		};
 	},
diff --git a/packages/client/src/pages/notifications.vue b/packages/client/src/pages/notifications.vue
index 695c54a535..090e80f99a 100644
--- a/packages/client/src/pages/notifications.vue
+++ b/packages/client/src/pages/notifications.vue
@@ -6,70 +6,62 @@
 </MkSpacer>
 </template>
 
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
 import XNotifications from '@/components/notifications.vue';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
 import { notificationTypes } from 'misskey-js';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		XNotifications
-	},
+let tab = $ref('all');
+let includeTypes = $ref<string[] | null>(null);
 
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => ({
-				title: this.$ts.notifications,
-				icon: 'fas fa-bell',
-				bg: 'var(--bg)',
-				actions: [{
-					text: this.$ts.filter,
-					icon: 'fas fa-filter',
-					highlighted: this.includeTypes != null,
-					handler: this.setFilter,
-				}, {
-					text: this.$ts.markAllAsRead,
-					icon: 'fas fa-check',
-					handler: () => {
-						os.apiWithDialog('notifications/mark-all-as-read');
-					},
-				}],
-				tabs: [{
-					active: this.tab === 'all',
-					title: this.$ts.all,
-					onClick: () => { this.tab = 'all'; },
-				}, {
-					active: this.tab === 'unread',
-					title: this.$ts.unread,
-					onClick: () => { this.tab = 'unread'; },
-				},]
-			})),
-			tab: 'all',
-			includeTypes: null,
-		};
-	},
-
-	methods: {
-		setFilter(ev) {
-			const typeItems = notificationTypes.map(t => ({
-				text: this.$t(`_notification._types.${t}`),
-				active: this.includeTypes && this.includeTypes.includes(t),
-				action: () => {
-					this.includeTypes = [t];
-				}
-			}));
-			const items = this.includeTypes != null ? [{
-				icon: 'fas fa-times',
-				text: this.$ts.clear,
-				action: () => {
-					this.includeTypes = null;
-				}
-			}, null, ...typeItems] : typeItems;
-			os.popupMenu(items, ev.currentTarget || ev.target);
+function setFilter(ev) {
+	const typeItems = notificationTypes.map(t => ({
+		text: i18n.t(`_notification._types.${t}`),
+		active: includeTypes && includeTypes.includes(t),
+		action: () => {
+			includeTypes = [t];
 		}
-	}
+	}));
+	const items = includeTypes != null ? [{
+		icon: 'fas fa-times',
+		text: i18n.locale.clear,
+		action: () => {
+			includeTypes = null;
+		}
+	}, null, ...typeItems] : typeItems;
+	os.popupMenu(items, ev.currentTarget || ev.target);
+}
+
+defineExpose({
+	[symbols.PAGE_INFO]: computed(() => ({
+		title: i18n.locale.notifications,
+		icon: 'fas fa-bell',
+		bg: 'var(--bg)',
+		actions: [{
+			text: i18n.locale.filter,
+			icon: 'fas fa-filter',
+			highlighted: includeTypes != null,
+			handler: setFilter,
+		}, {
+			text: i18n.locale.markAllAsRead,
+			icon: 'fas fa-check',
+			handler: () => {
+				os.apiWithDialog('notifications/mark-all-as-read');
+			},
+		}],
+		tabs: [{
+			active: tab === 'all',
+			title: i18n.locale.all,
+			onClick: () => { tab = 'all'; },
+		}, {
+			active: tab === 'unread',
+			title: i18n.locale.unread,
+			onClick: () => { tab = 'unread'; },
+		},]
+	})),
 });
 </script>
 
diff --git a/packages/client/src/pages/page.vue b/packages/client/src/pages/page.vue
index 3a4803c3a3..429d1ddea2 100644
--- a/packages/client/src/pages/page.vue
+++ b/packages/client/src/pages/page.vue
@@ -106,11 +106,11 @@ export default defineComponent({
 			page: null,
 			error: null,
 			otherPostsPagination: {
-				endpoint: 'users/pages',
+				endpoint: 'users/pages' as const,
 				limit: 6,
-				params: () => ({
+				params: computed(() => ({
 					userId: this.page.user.id
-				})
+				})),
 			},
 		};
 	},
diff --git a/packages/client/src/pages/pages.vue b/packages/client/src/pages/pages.vue
index f1dd64f119..dcccf7f7c4 100644
--- a/packages/client/src/pages/pages.vue
+++ b/packages/client/src/pages/pages.vue
@@ -62,15 +62,15 @@ export default defineComponent({
 			})),
 			tab: 'featured',
 			featuredPagesPagination: {
-				endpoint: 'pages/featured',
+				endpoint: 'pages/featured' as const,
 				noPaging: true,
 			},
 			myPagesPagination: {
-				endpoint: 'i/pages',
+				endpoint: 'i/pages' as const,
 				limit: 5,
 			},
 			likedPagesPagination: {
-				endpoint: 'i/page-likes',
+				endpoint: 'i/page-likes' as const,
 				limit: 5,
 			},
 		};
diff --git a/packages/client/src/pages/preview.vue b/packages/client/src/pages/preview.vue
index 9d1ebb74ed..8eb4549516 100644
--- a/packages/client/src/pages/preview.vue
+++ b/packages/client/src/pages/preview.vue
@@ -4,24 +4,18 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
 import MkSample from '@/components/sample.vue';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		MkSample,
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.preview,
-				icon: 'fas fa-eye',
-			},
-		}
-	},
+defineExpose({
+	[symbols.PAGE_INFO]: computed(() => ({
+		title: i18n.locale.preview,
+		icon: 'fas fa-eye',
+		bg: 'var(--bg)',
+	})),
 });
 </script>
 
diff --git a/packages/client/src/pages/reset-password.vue b/packages/client/src/pages/reset-password.vue
index e0608654c7..8ef73858f6 100644
--- a/packages/client/src/pages/reset-password.vue
+++ b/packages/client/src/pages/reset-password.vue
@@ -3,62 +3,51 @@
 	<div class="_formRoot">
 		<FormInput v-model="password" type="password" class="_formBlock">
 			<template #prefix><i class="fas fa-lock"></i></template>
-			<template #label>{{ $ts.newPassword }}</template>
+			<template #label>{{ i18n.locale.newPassword }}</template>
 		</FormInput>
 		
-		<FormButton primary class="_formBlock" @click="save">{{ $ts.save }}</FormButton>
+		<FormButton primary class="_formBlock" @click="save">{{ i18n.locale.save }}</FormButton>
 	</div>
 </MkSpacer>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted } from 'vue';
 import FormInput from '@/components/form/input.vue';
 import FormButton from '@/components/ui/button.vue';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
+import { router } from '@/router';
 
-export default defineComponent({
-	components: {
-		FormInput,
-		FormButton,
-	},
+const props = defineProps<{
+	token?: string;
+}>();
 
-	props: {
-		token: {
-			type: String,
-			required: false
-		}
-	},
+let password = $ref('');
 
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.resetPassword,
-				icon: 'fas fa-lock',
-				bg: 'var(--bg)',
-			},
-			password: '',
-		}
-	},
+async function save() {
+	await os.apiWithDialog('reset-password', {
+		token: props.token,
+		password: password,
+	});
+	router.push('/');
+}
 
-	mounted() {
-		if (this.token == null) {
-			os.popup(import('@/components/forgot-password.vue'), {}, {}, 'closed');
-			this.$router.push('/');
-		}
-	},
-
-	methods: {
-		async save() {
-			await os.apiWithDialog('reset-password', {
-				token: this.token,
-				password: this.password,
-			});
-			this.$router.push('/');
-		}
+onMounted(() => {
+	if (props.token == null) {
+		os.popup(import('@/components/forgot-password.vue'), {}, {}, 'closed');
+		router.push('/');
 	}
 });
+
+defineExpose({
+	[symbols.PAGE_INFO]: {
+		title: i18n.locale.resetPassword,
+		icon: 'fas fa-lock',
+		bg: 'var(--bg)',
+	},
+});
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/pages/reversi/game.board.vue b/packages/client/src/pages/reversi/game.board.vue
deleted file mode 100644
index eb6fef2799..0000000000
--- a/packages/client/src/pages/reversi/game.board.vue
+++ /dev/null
@@ -1,528 +0,0 @@
-<template>
-<div class="xqnhankfuuilcwvhgsopeqncafzsquya">
-	<header><b><MkA :to="userPage(blackUser)"><MkUserName :user="blackUser"/></MkA></b>({{ $ts._reversi.black }}) vs <b><MkA :to="userPage(whiteUser)"><MkUserName :user="whiteUser"/></MkA></b>({{ $ts._reversi.white }})</header>
-
-	<div style="overflow: hidden; line-height: 28px;">
-		<p v-if="!iAmPlayer && !game.isEnded" class="turn">
-			<Mfm :key="'turn:' + turnUser().name" :text="$t('_reversi.turnOf', { name: turnUser().name })" :plain="true" :custom-emojis="turnUser().emojis"/>
-			<MkEllipsis/>
-		</p>
-		<p v-if="logPos != logs.length" class="turn">
-			<Mfm :key="'past-turn-of:' + turnUser().name" :text="$t('_reversi.pastTurnOf', { name: turnUser().name })" :plain="true" :custom-emojis="turnUser().emojis"/>
-		</p>
-		<p v-if="iAmPlayer && !game.isEnded && !isMyTurn()" class="turn1">{{ $ts._reversi.opponentTurn }}<MkEllipsis/></p>
-		<p v-if="iAmPlayer && !game.isEnded && isMyTurn()" class="turn2" style="animation: tada 1s linear infinite both;">{{ $ts._reversi.myTurn }}</p>
-		<p v-if="game.isEnded && logPos == logs.length" class="result">
-			<template v-if="game.winner">
-				<Mfm :key="'won'" :text="$t('_reversi.won', { name: game.winner.name })" :plain="true" :custom-emojis="game.winner.emojis"/>
-				<span v-if="game.surrendered != null"> ({{ $ts._reversi.surrendered }})</span>
-			</template>
-			<template v-else>{{ $ts._reversi.drawn }}</template>
-		</p>
-	</div>
-
-	<div class="board">
-		<div v-if="$store.state.gamesReversiShowBoardLabels" class="labels-x">
-			<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
-		</div>
-		<div class="flex">
-			<div v-if="$store.state.gamesReversiShowBoardLabels" class="labels-y">
-				<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 }"
-					:title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`"
-					@click="set(i)"
-				>
-					<template v-if="$store.state.gamesReversiUseAvatarStones || true">
-						<img v-if="stone === true" :src="blackUser.avatarUrl" alt="black">
-						<img v-if="stone === false" :src="whiteUser.avatarUrl" alt="white">
-					</template>
-					<template v-else>
-						<i v-if="stone === true" class="fas fa-circle"></i>
-						<i v-if="stone === false" class="far fa-circle"></i>
-					</template>
-				</div>
-			</div>
-			<div v-if="$store.state.gamesReversiShowBoardLabels" class="labels-y">
-				<div v-for="i in game.map.length">{{ i }}</div>
-			</div>
-		</div>
-		<div v-if="$store.state.gamesReversiShowBoardLabels" class="labels-x">
-			<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
-		</div>
-	</div>
-
-	<p class="status"><b>{{ $t('_reversi.turnCount', { count: logPos }) }}</b> {{ $ts._reversi.black }}:{{ o.blackCount }} {{ $ts._reversi.white }}:{{ o.whiteCount }} {{ $ts._reversi.total }}:{{ o.blackCount + o.whiteCount }}</p>
-
-	<div v-if="!game.isEnded && iAmPlayer" class="actions">
-		<MkButton inline @click="surrender">{{ $ts._reversi.surrender }}</MkButton>
-	</div>
-
-	<div v-if="game.isEnded" class="player">
-		<span>{{ logPos }} / {{ logs.length }}</span>
-		<div v-if="!autoplaying" class="buttons">
-			<MkButton inline :disabled="logPos == 0" @click="logPos = 0"><i class="fas fa-angle-double-left"></i></MkButton>
-			<MkButton inline :disabled="logPos == 0" @click="logPos--"><i class="fas fa-angle-left"></i></MkButton>
-			<MkButton inline :disabled="logPos == logs.length" @click="logPos++"><i class="fas fa-angle-right"></i></MkButton>
-			<MkButton inline :disabled="logPos == logs.length" @click="logPos = logs.length"><i class="fas fa-angle-double-right"></i></MkButton>
-		</div>
-		<MkButton :disabled="autoplaying" style="margin: var(--margin) auto 0 auto;" @click="autoplay()"><i class="fas fa-play"></i></MkButton>
-	</div>
-
-	<div class="info">
-		<p v-if="game.isLlotheo">{{ $ts._reversi.isLlotheo }}</p>
-		<p v-if="game.loopedBoard">{{ $ts._reversi.loopedMap }}</p>
-		<p v-if="game.canPutEverywhere">{{ $ts._reversi.canPutEverywhere }}</p>
-	</div>
-
-	<div class="watchers">
-		<MkAvatar v-for="user in watchers" :key="user.id" :user="user" class="avatar"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as CRC32 from 'crc-32';
-import Reversi, { Color } from '@/scripts/games/reversi/core';
-import { url } from '@/config';
-import MkButton from '@/components/ui/button.vue';
-import { userPage } from '@/filters/user';
-import * as os from '@/os';
-import * as sound from '@/scripts/sound';
-
-export default defineComponent({
-	components: {
-		MkButton
-	},
-
-	props: {
-		initGame: {
-			type: Object,
-			require: true
-		},
-		connection: {
-			type: Object,
-			require: true
-		},
-	},
-
-	data() {
-		return {
-			game: JSON.parse(JSON.stringify(this.initGame)),
-			o: null as Reversi,
-			logs: [],
-			logPos: 0,
-			watchers: [],
-			pollingClock: null,
-		};
-	},
-
-	computed: {
-		iAmPlayer(): boolean {
-			if (!this.$i) return false;
-			return this.game.user1Id == this.$i.id || this.game.user2Id == this.$i.id;
-		},
-
-		myColor(): Color {
-			if (!this.iAmPlayer) return null;
-			if (this.game.user1Id == this.$i.id && this.game.black == 1) return true;
-			if (this.game.user2Id == this.$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;
-		},
-
-		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;
-			const 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)) {
-				o.put(log.color, log.pos);
-			}
-			this.o = o;
-			//this.$forceUpdate();
-		}
-	},
-
-	created() {
-		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);
-		this.connection.on('watchers', this.onWatchers);
-	},
-
-	beforeUnmount() {
-		this.connection.off('set', this.onSet);
-		this.connection.off('rescue', this.onRescue);
-		this.connection.off('ended', this.onEnded);
-		this.connection.off('watchers', this.onWatchers);
-
-		clearInterval(this.pollingClock);
-	},
-
-	methods: {
-		userPage,
-
-		// this.o がリアクティブになった折にはcomputedにできる
-		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;
-			}
-		},
-
-		// this.o がリアクティブになった折にはcomputedにできる
-		isMyTurn(): boolean {
-			if (!this.iAmPlayer) return false;
-			if (this.turnUser() == null) return false;
-			return this.turnUser().id == this.$i.id;
-		},
-
-		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);
-
-			// サウンドを再生する
-			sound.play(this.myColor ? 'reversiPutBlack' : 'reversiPutWhite');
-
-			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 (x.color !== this.myColor) {
-				sound.play(x.color ? 'reversiPutBlack' : 'reversiPutWhite');
-			}
-		},
-
-		onEnded(x) {
-			this.game = JSON.parse(JSON.stringify(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 = JSON.parse(JSON.stringify(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();
-		},
-
-		onWatchers(users) {
-			this.watchers = users;
-		},
-
-		surrender() {
-			os.api('games/reversi/games/surrender', {
-				gameId: this.game.id
-			});
-		},
-
-		autoplay() {
-			this.autoplaying = true;
-			this.logPos = 0;
-
-			setTimeout(() => {
-				this.logPos = 1;
-
-				let i = 1;
-				let previousLog = this.game.logs[0];
-				const tick = () => {
-					const log = this.game.logs[i];
-					const time = new Date(log.at).getTime() - new Date(previousLog.at).getTime()
-					setTimeout(() => {
-						i++;
-						this.logPos++;
-						previousLog = log;
-
-						if (i < this.game.logs.length) {
-							tick();
-						} else {
-							this.autoplaying = false;
-						}
-					}, time);
-				};
-
-				tick();
-			}, 1000);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-
-@use "sass:math";
-
-.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(--divider);
-	}
-
-	> .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: 0.8em;
-
-				&:first-child {
-					margin-left: -(math.div($gap, 2));
-				}
-
-				&:last-child {
-					margin-right: -(math.div($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: -(math.div($gap, 2));
-					}
-
-					&:last-child {
-						margin-bottom: -(math.div($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(--divider);
-					}
-
-					&.empty.can {
-						border-color: var(--accent);
-					}
-
-					&.empty.myTurn {
-						border-color: var(--divider);
-
-						&.can {
-							border-color: var(--accent);
-							cursor: pointer;
-
-							&:hover {
-								background: var(--accent);
-							}
-						}
-					}
-
-					&.prev {
-						box-shadow: 0 0 0 4px var(--accent);
-					}
-
-					&.isEnded {
-						border-color: var(--divider);
-					}
-
-					&.none {
-						border-color: transparent !important;
-					}
-
-					> svg, > img {
-						display: block;
-						width: 100%;
-						height: 100%;
-					}
-				}
-			}
-		}
-	}
-
-	> .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;
-		}
-
-		> .buttons {
-			display: flex;
-
-			> * {
-				flex: 1;
-			}
-		}
-	}
-
-	> .watchers {
-		padding: 0 0 16px 0;
-
-		&:empty {
-			display: none;
-		}
-
-		> .avatar {
-			width: 32px;
-			height: 32px;
-		}
-	}
-}
-</style>
diff --git a/packages/client/src/pages/reversi/game.setting.vue b/packages/client/src/pages/reversi/game.setting.vue
deleted file mode 100644
index 28bc598cfd..0000000000
--- a/packages/client/src/pages/reversi/game.setting.vue
+++ /dev/null
@@ -1,390 +0,0 @@
-<template>
-<div class="urbixznjwwuukfsckrwzwsqzsxornqij">
-	<header><b><MkUserName :user="game.user1"/></b> vs <b><MkUserName :user="game.user2"/></b></header>
-
-	<div>
-		<p>{{ $ts._reversi.gameSettings }}</p>
-
-		<div class="card map _panel">
-			<header>
-				<select v-model="mapName" :placeholder="$ts._reversi.chooseBoard" @change="onMapChange">
-					<option v-if="mapName == '-Custom-'" label="-Custom-" :value="mapName"/>
-					<option :label="$ts.random" :value="null"/>
-					<optgroup v-for="c in mapCategories" :key="c" :label="c">
-						<option v-for="m in Object.values(maps).filter(m => m.category == c)" :key="m.name" :label="m.name" :value="m.name">{{ m.name }}</option>
-					</optgroup>
-				</select>
-			</header>
-
-			<div>
-				<div v-if="game.map == null" class="random"><i class="fas fa-dice"></i></div>
-				<div v-else class="board" :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('')" :class="{ none: x == ' ' }" @click="onPixelClick(i, x)">
-						<i v-if="x === 'b'" class="fas fa-circle"></i>
-						<i v-if="x === 'w'" class="far fa-circle"></i>
-					</div>
-				</div>
-			</div>
-		</div>
-
-		<div class="card _panel">
-			<header>
-				<span>{{ $ts._reversi.blackOrWhite }}</span>
-			</header>
-
-			<div>
-				<MkRadio v-model="game.bw" value="random" @update:modelValue="updateSettings('bw')">{{ $ts.random }}</MkRadio>
-				<MkRadio v-model="game.bw" :value="'1'" @update:modelValue="updateSettings('bw')">
-					<I18n :src="$ts._reversi.blackIs" tag="span">
-						<template #name>
-							<b><MkUserName :user="game.user1"/></b>
-						</template>
-					</I18n>
-				</MkRadio>
-				<MkRadio v-model="game.bw" :value="'2'" @update:modelValue="updateSettings('bw')">
-					<I18n :src="$ts._reversi.blackIs" tag="span">
-						<template #name>
-							<b><MkUserName :user="game.user2"/></b>
-						</template>
-					</I18n>
-				</MkRadio>
-			</div>
-		</div>
-
-		<div class="card _panel">
-			<header>
-				<span>{{ $ts._reversi.rules }}</span>
-			</header>
-
-			<div>
-				<MkSwitch v-model="game.isLlotheo" @update:modelValue="updateSettings('isLlotheo')">{{ $ts._reversi.isLlotheo }}</MkSwitch>
-				<MkSwitch v-model="game.loopedBoard" @update:modelValue="updateSettings('loopedBoard')">{{ $ts._reversi.loopedMap }}</MkSwitch>
-				<MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ $ts._reversi.canPutEverywhere }}</MkSwitch>
-			</div>
-		</div>
-
-		<div v-if="form" class="card form _panel">
-			<header>
-				<span>{{ $ts._reversi.botSettings }}</span>
-			</header>
-
-			<div>
-				<template v-for="item in form">
-					<MkSwitch v-if="item.type == 'switch'" :key="item.id" v-model="item.value" @change="onChangeForm(item)">{{ item.label || item.desc || '' }}</MkSwitch>
-
-					<div v-if="item.type == 'radio'" :key="item.id" class="card">
-						<header>
-							<span>{{ item.label }}</span>
-						</header>
-
-						<div>
-							<MkRadio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :value="r.value" @update:modelValue="onChangeForm(item)">{{ r.label }}</MkRadio>
-						</div>
-					</div>
-
-					<div v-if="item.type == 'slider'" :key="item.id" class="card">
-						<header>
-							<span>{{ item.label }}</span>
-						</header>
-
-						<div>
-							<input v-model="item.value" type="range" :min="item.min" :max="item.max" :step="item.step || 1" @change="onChangeForm(item)"/>
-						</div>
-					</div>
-
-					<div v-if="item.type == 'textbox'" :key="item.id" class="card">
-						<header>
-							<span>{{ item.label }}</span>
-						</header>
-
-						<div>
-							<input v-model="item.value" @change="onChangeForm(item)"/>
-						</div>
-					</div>
-				</template>
-			</div>
-		</div>
-	</div>
-
-	<footer class="_acrylic">
-		<p class="status">
-			<template v-if="isAccepted && isOpAccepted">{{ $ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template>
-			<template v-if="isAccepted && !isOpAccepted">{{ $ts._reversi.waitingForOther }}<MkEllipsis/></template>
-			<template v-if="!isAccepted && isOpAccepted">{{ $ts._reversi.waitingForMe }}</template>
-			<template v-if="!isAccepted && !isOpAccepted">{{ $ts._reversi.waitingBoth }}<MkEllipsis/></template>
-		</p>
-
-		<div class="actions">
-			<MkButton inline @click="exit">{{ $ts.cancel }}</MkButton>
-			<MkButton v-if="!isAccepted" inline primary @click="accept">{{ $ts._reversi.ready }}</MkButton>
-			<MkButton v-if="isAccepted" inline primary @click="cancel">{{ $ts._reversi.cancelReady }}</MkButton>
-		</div>
-	</footer>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as maps from '@/scripts/games/reversi/maps';
-import MkButton from '@/components/ui/button.vue';
-import MkSwitch from '@/components/form/switch.vue';
-import MkRadio from '@/components/form/radio.vue';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkSwitch,
-		MkRadio,
-	},
-
-	props: {
-		initGame: {
-			type: Object,
-			require: true
-		},
-		connection: {
-			type: Object,
-			require: true
-		},
-	},
-
-	data() {
-		return {
-			game: this.initGame,
-			o: null,
-			isLlotheo: false,
-			mapName: maps.eighteight.name,
-			maps: maps,
-			form: null,
-			messages: [],
-		};
-	},
-
-	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.$i.id && this.game.user1Accepted) return true;
-			if (this.game.user2Id == this.$i.id && this.game.user2Accepted) return true;
-			return false;
-		},
-		isOpAccepted(): boolean {
-			if (this.game.user1Id != this.$i.id && this.game.user1Accepted) return true;
-			if (this.game.user2Id != this.$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.$i.id && this.game.form1) this.form = this.game.form1;
-		if (this.game.user2Id != this.$i.id && this.game.form2) this.form = this.game.form2;
-	},
-
-	beforeUnmount() {
-		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;
-		},
-
-		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.$i.id) return;
-			this.form = x.form;
-		},
-
-		onMessage(x) {
-			if (x.userId == this.$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.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.game.map[y] = line.join('');
-			this.updateSettings('map');
-		}
-	}
-});
-</script>
-
-<style lang="scss" 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(--inputBorder);
-						border-radius: 4px;
-						color: var(--fg);
-						cursor: pointer;
-						transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
-						-webkit-appearance: none;
-						-moz-appearance: none;
-						appearance: none;
-
-						&:focus-visible,
-						&:active {
-							border-color: var(--accent);
-						}
-					}
-				}
-
-				> div {
-					> .random {
-						padding: 32px 0;
-						font-size: 64px;
-						color: var(--fg);
-						opacity: 0.7;
-					}
-
-					> .board {
-						display: grid;
-						grid-gap: 4px;
-						width: 300px;
-						height: 300px;
-						margin: 0 auto;
-						color: var(--fg);
-
-						> div {
-							background: transparent;
-							border: solid 2px var(--divider);
-							border-radius: 6px;
-							overflow: hidden;
-							cursor: pointer;
-
-							* {
-								pointer-events: none;
-								user-select: none;
-								width: 100%;
-								height: 100%;
-							}
-
-							&.none {
-								border-color: transparent;
-							}
-						}
-					}
-				}
-			}
-
-			&.form {
-				> div {
-					> .card + .card {
-						margin-top: 16px;
-					}
-
-					input[type='range'] {
-						width: 100%;
-					}
-				}
-			}
-		}
-
-		.card {
-			max-width: 400px;
-
-			> header {
-				padding: 18px 20px;
-				border-bottom: 1px solid var(--divider);
-			}
-
-			> div {
-				padding: 20px;
-				color: var(--fg);
-			}
-		}
-	}
-
-	> footer {
-		position: sticky;
-		bottom: 0;
-		padding: 16px;
-		border-top: solid 1px var(--divider);
-
-		> .status {
-			margin: 0 0 16px 0;
-		}
-	}
-}
-</style>
diff --git a/packages/client/src/pages/reversi/game.vue b/packages/client/src/pages/reversi/game.vue
deleted file mode 100644
index 697d2898b9..0000000000
--- a/packages/client/src/pages/reversi/game.vue
+++ /dev/null
@@ -1,77 +0,0 @@
-<template>
-<div v-if="game == null"><MkLoading/></div>
-<GameSetting v-else-if="!game.isStarted" :init-game="game" :connection="connection"/>
-<GameBoard v-else :init-game="game" :connection="connection"/>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import GameSetting from './game.setting.vue';
-import GameBoard from './game.board.vue';
-import * as os from '@/os';
-import { stream } from '@/stream';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
-	components: {
-		GameSetting,
-		GameBoard,
-	},
-
-	props: {
-		gameId: {
-			type: String,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts._reversi.reversi,
-				icon: 'fas fa-gamepad'
-			},
-			game: null,
-			connection: null,
-		};
-	},
-
-	watch: {
-		gameId() {
-			this.fetch();
-		}
-	},
-
-	mounted() {
-		this.fetch();
-	},
-
-	beforeUnmount() {
-		if (this.connection) {
-			this.connection.dispose();
-		}
-	},
-
-	methods: {
-		fetch() {
-			os.api('games/reversi/games/show', {
-				gameId: this.gameId
-			}).then(game => {
-				this.game = game;
-
-				if (this.connection) {
-					this.connection.dispose();
-				}
-				this.connection = markRaw(stream.useChannel('gamesReversiGame', {
-					gameId: this.game.id
-				}));
-				this.connection.on('started', this.onStarted);
-			});
-		},
-
-		onStarted(game) {
-			Object.assign(this.game, game);
-		},
-	}
-});
-</script>
diff --git a/packages/client/src/pages/reversi/index.vue b/packages/client/src/pages/reversi/index.vue
deleted file mode 100644
index 93c22c02f3..0000000000
--- a/packages/client/src/pages/reversi/index.vue
+++ /dev/null
@@ -1,280 +0,0 @@
-<template>
-<div v-if="!matching" class="bgvwxkhb">
-	<h1>Misskey {{ $ts._reversi.reversi }}</h1>
-
-	<div class="play">
-		<MkButton primary round style="margin: var(--margin) auto 0 auto;" @click="match">{{ $ts.invite }}</MkButton>
-	</div>
-
-	<div class="_section">
-		<MkFolder v-if="invitations.length > 0">
-			<template #header>{{ $ts.invitations }}</template>
-			<div class="nfcacttm">
-				<button v-for="invitation in invitations" class="invitation _panel _button" tabindex="-1" @click="accept(invitation)">
-					<MkAvatar class="avatar" :user="invitation.parent" :show-indicator="true"/>
-					<span class="name"><b><MkUserName :user="invitation.parent"/></b></span>
-					<span class="username">@{{ invitation.parent.username }}</span>
-					<MkTime :time="invitation.createdAt" class="time"/>
-				</button>
-			</div>
-		</MkFolder>
-
-		<MkFolder v-if="myGames.length > 0">
-			<template #header>{{ $ts._reversi.myGames }}</template>
-			<div class="knextgwz">
-				<MkA v-for="g in myGames" :key="g.id" class="game _panel" tabindex="-1" :to="`/games/reversi/${g.id}`">
-					<div class="players">
-						<MkAvatar class="avatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar class="avatar" :user="g.user2"/>
-					</div>
-					<footer><span class="state" :class="{ playing: !g.isEnded }">{{ g.isEnded ? $ts._reversi.ended : $ts._reversi.playing }}</span><MkTime class="time" :time="g.createdAt"/></footer>
-				</MkA>
-			</div>
-		</MkFolder>
-
-		<MkFolder v-if="games.length > 0">
-			<template #header>{{ $ts._reversi.allGames }}</template>
-			<div class="knextgwz">
-				<MkA v-for="g in games" :key="g.id" class="game _panel" tabindex="-1" :to="`/games/reversi/${g.id}`">
-					<div class="players">
-						<MkAvatar class="avatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar class="avatar" :user="g.user2"/>
-					</div>
-					<footer><span class="state" :class="{ playing: !g.isEnded }">{{ g.isEnded ? $ts._reversi.ended : $ts._reversi.playing }}</span><MkTime class="time" :time="g.createdAt"/></footer>
-				</MkA>
-			</div>
-		</MkFolder>
-	</div>
-</div>
-<div v-else class="sazhgisb">
-	<h1>
-		<I18n :src="$ts.waitingFor" tag="span">
-			<template #x>
-				<b><MkUserName :user="matching"/></b>
-			</template>
-		</I18n>
-		<MkEllipsis/>
-	</h1>
-	<div class="cancel">
-		<MkButton inline round @click="cancel">{{ $ts.cancel }}</MkButton>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import * as os from '@/os';
-import { stream } from '@/stream';
-import MkButton from '@/components/ui/button.vue';
-import MkFolder from '@/components/ui/folder.vue';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton, MkFolder,
-	},
-
-	inject: ['navHook'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts._reversi.reversi,
-				icon: 'fas fa-gamepad'
-			},
-			games: [],
-			gamesFetching: true,
-			gamesMoreFetching: false,
-			myGames: [],
-			matching: null,
-			invitations: [],
-			connection: null,
-			pingClock: null,
-		};
-	},
-
-	mounted() {
-		if (this.$i) {
-			this.connection = markRaw(stream.useChannel('gamesReversi'));
-
-			this.connection.on('invited', this.onInvited);
-
-			this.connection.on('matched', this.onMatched);
-
-			this.pingClock = setInterval(() => {
-				if (this.matching) {
-					this.connection.send('ping', {
-						id: this.matching.id
-					});
-				}
-			}, 3000);
-
-			os.api('games/reversi/games', {
-				my: true
-			}).then(games => {
-				this.myGames = games;
-			});
-
-			os.api('games/reversi/invitations').then(invitations => {
-				this.invitations = this.invitations.concat(invitations);
-			});
-		}
-
-		os.api('games/reversi/games').then(games => {
-			this.games = games;
-			this.gamesFetching = false;
-		});
-	},
-
-	beforeUnmount() {
-		if (this.connection) {
-			this.connection.dispose();
-			clearInterval(this.pingClock);
-		}
-	},
-
-	methods: {
-		go(game) {
-			const url = '/games/reversi/' + game.id;
-			if (this.navHook) {
-				this.navHook(url);
-			} else {
-				this.$router.push(url);
-			}
-		},
-
-		async match() {
-			const user = await os.selectUser({ local: true });
-			if (user == null) return;
-			os.api('games/reversi/match', {
-				userId: user.id
-			}).then(res => {
-				if (res == null) {
-					this.matching = user;
-				} else {
-					this.go(res);
-				}
-			});
-		},
-
-		cancel() {
-			this.matching = null;
-			os.api('games/reversi/match/cancel');
-		},
-
-		accept(invitation) {
-			os.api('games/reversi/match', {
-				userId: invitation.parent.id
-			}).then(game => {
-				if (game) {
-					this.go(game);
-				}
-			});
-		},
-
-		onMatched(game) {
-			this.go(game);
-		},
-
-		onInvited(invite) {
-			this.invitations.unshift(invite);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.bgvwxkhb {
-	> h1 {
-		margin: 0;
-		padding: 24px;
-		text-align: center;
-		font-size: 1.5em;
-		background: linear-gradient(0deg, #43c583, #438881);
-		color: #fff;
-	}
-
-	> .play {
-		text-align: center;
-	}
-}
-
-.sazhgisb {
-	text-align: center;
-}
-
-.nfcacttm {
-	> .invitation {
-		display: flex;
-		box-sizing: border-box;
-		width: 100%;
-		padding: 16px;
-		line-height: 32px;
-		text-align: left;
-
-		> .avatar {
-			width: 32px;
-			height: 32px;
-			margin-right: 8px;
-		}
-
-		> .name {
-			margin-right: 8px;
-		}
-
-		> .username {
-			margin-right: 8px;
-			opacity: 0.7;
-		}
-
-		> .time {
-			margin-left: auto;
-			opacity: 0.7;
-		}
-	}
-}
-
-.knextgwz {
-	display: grid;
-	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
-	grid-gap: var(--margin);
-
-	> .game {
-		> .players {
-			text-align: center;
-			padding: 16px;
-			line-height: 32px;
-
-			> .avatar {
-				width: 32px;
-				height: 32px;
-
-				&:first-child {
-					margin-right: 8px;
-				}
-
-				&:last-child {
-					margin-left: 8px;
-				}
-			}
-		}
-
-		> footer {
-			display: flex;
-			align-items: baseline;
-			border-top: solid 0.5px var(--divider);
-			padding: 6px 8px;
-			font-size: 0.9em;
-
-			> .state {
-				&.playing {
-					color: var(--accent);
-				}
-			}
-
-			> .time {
-				margin-left: auto;
-				opacity: 0.7;
-			}
-		}
-	}
-}
-</style>
diff --git a/packages/client/src/pages/search.vue b/packages/client/src/pages/search.vue
index 85d19bb255..ce2b7035da 100644
--- a/packages/client/src/pages/search.vue
+++ b/packages/client/src/pages/search.vue
@@ -6,37 +6,31 @@
 </div>
 </template>
 
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
 import XNotes from '@/components/notes.vue';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		XNotes
-	},
+const props = defineProps<{
+	query: string;
+	channel?: string;
+}>();
 
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: computed(() => this.$t('searchWith', { q: this.$route.query.q })),
-				icon: 'fas fa-search',
-			},
-			pagination: {
-				endpoint: 'notes/search',
-				limit: 10,
-				params: () => ({
-					query: this.$route.query.q,
-					channelId: this.$route.query.channel,
-				})
-			},
-		};
-	},
+const pagination = {
+	endpoint: 'notes/search' as const,
+	limit: 10,
+	params: computed(() => ({
+		query: props.query,
+		channelId: props.channel,
+	}))
+};
 
-	watch: {
-		$route() {
-			(this.$refs.notes as any).reload();
-		}
-	},
+defineExpose({
+	[symbols.PAGE_INFO]: computed(() => ({
+		title: i18n.t('searchWith', { q: props.query }),
+		icon: 'fas fa-search',
+		bg: 'var(--bg)',
+	})),
 });
 </script>
diff --git a/packages/client/src/pages/settings/account-info.vue b/packages/client/src/pages/settings/account-info.vue
index 1d6afd9d58..c98ad056f6 100644
--- a/packages/client/src/pages/settings/account-info.vue
+++ b/packages/client/src/pages/settings/account-info.vue
@@ -94,10 +94,6 @@
 			<template #key>{{ $ts.driveUsage }}</template>
 			<template #value>{{ bytes(stats.driveUsage) }}</template>
 		</MkKeyValue>
-		<MkKeyValue oneline style="margin: 1em 0;">
-			<template #key>{{ $ts.reversiCount }}</template>
-			<template #value>{{ number(stats.reversiCount) }}</template>
-		</MkKeyValue>
 	</FormSection>
 
 	<FormSection>
@@ -158,8 +154,6 @@ export default defineComponent({
 	},
 
 	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-
 		os.api('users/stats', {
 			userId: this.$i.id
 		}).then(stats => {
diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue
index 9ff11adda3..c795ede8ac 100644
--- a/packages/client/src/pages/settings/accounts.vue
+++ b/packages/client/src/pages/settings/accounts.vue
@@ -53,10 +53,6 @@ export default defineComponent({
 		};
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		menu(account, ev) {
 			os.popupMenu([{
diff --git a/packages/client/src/pages/settings/api.vue b/packages/client/src/pages/settings/api.vue
index 1a51b526f2..20ff2a8d96 100644
--- a/packages/client/src/pages/settings/api.vue
+++ b/packages/client/src/pages/settings/api.vue
@@ -32,10 +32,6 @@ export default defineComponent({
 		};
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		generateToken() {
 			os.popup(import('@/components/token-generate-window.vue'), {}, {
diff --git a/packages/client/src/pages/settings/apps.vue b/packages/client/src/pages/settings/apps.vue
index 68952bbbdb..9c0fa8a54d 100644
--- a/packages/client/src/pages/settings/apps.vue
+++ b/packages/client/src/pages/settings/apps.vue
@@ -58,7 +58,7 @@ export default defineComponent({
 				bg: 'var(--bg)',
 			},
 			pagination: {
-				endpoint: 'i/apps',
+				endpoint: 'i/apps' as const,
 				limit: 100,
 				params: {
 					sort: '+lastUsedAt'
@@ -67,10 +67,6 @@ export default defineComponent({
 		};
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		revoke(token) {
 			os.api('i/revoke-token', { tokenId: token.id }).then(() => {
diff --git a/packages/client/src/pages/settings/custom-css.vue b/packages/client/src/pages/settings/custom-css.vue
index 6dbb8c2ae9..556ee30c1d 100644
--- a/packages/client/src/pages/settings/custom-css.vue
+++ b/packages/client/src/pages/settings/custom-css.vue
@@ -37,8 +37,6 @@ export default defineComponent({
 	},
 
 	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-
 		this.$watch('localCustomCss', this.apply);
 	},
 
diff --git a/packages/client/src/pages/settings/deck.vue b/packages/client/src/pages/settings/deck.vue
index e290b095ac..46b90d3d1a 100644
--- a/packages/client/src/pages/settings/deck.vue
+++ b/packages/client/src/pages/settings/deck.vue
@@ -83,10 +83,6 @@ export default defineComponent({
 		}
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async setProfile() {
 			const { canceled, result: name } = await os.inputText({
diff --git a/packages/client/src/pages/settings/delete-account.vue b/packages/client/src/pages/settings/delete-account.vue
index 17501d9510..7edc81a309 100644
--- a/packages/client/src/pages/settings/delete-account.vue
+++ b/packages/client/src/pages/settings/delete-account.vue
@@ -33,10 +33,6 @@ export default defineComponent({
 		}
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async deleteAccount() {
 			{
diff --git a/packages/client/src/pages/settings/drive.vue b/packages/client/src/pages/settings/drive.vue
index c123159b61..f1016ebd84 100644
--- a/packages/client/src/pages/settings/drive.vue
+++ b/packages/client/src/pages/settings/drive.vue
@@ -99,10 +99,6 @@ export default defineComponent({
 		}
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		chooseUploadFolder() {
 			os.selectDriveFolder(false).then(async folder => {
diff --git a/packages/client/src/pages/settings/email.vue b/packages/client/src/pages/settings/email.vue
index e9010fbe42..54557f8773 100644
--- a/packages/client/src/pages/settings/email.vue
+++ b/packages/client/src/pages/settings/email.vue
@@ -111,8 +111,6 @@ export default defineComponent({
 		});
 
 		onMounted(() => {
-			context.emit('info', INFO);
-
 			watch(emailAddress, () => {
 				saveEmailAddress();
 			});
diff --git a/packages/client/src/pages/settings/general.vue b/packages/client/src/pages/settings/general.vue
index 734bc78442..2e159e56a9 100644
--- a/packages/client/src/pages/settings/general.vue
+++ b/packages/client/src/pages/settings/general.vue
@@ -195,10 +195,6 @@ export default defineComponent({
 		},
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async reloadAsk() {
 			const { canceled } = await os.confirm({
diff --git a/packages/client/src/pages/settings/import-export.vue b/packages/client/src/pages/settings/import-export.vue
index a1dd6a1539..21031c559e 100644
--- a/packages/client/src/pages/settings/import-export.vue
+++ b/packages/client/src/pages/settings/import-export.vue
@@ -133,10 +133,6 @@ export default defineComponent({
 			os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError);
 		};
 
-		onMounted(() => {
-			context.emit('info', INFO);
-		});
-
 		return {
 			[symbols.PAGE_INFO]: INFO,
 			excludeMutingUsers,
diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue
index c9acf2c63c..66c8b147bb 100644
--- a/packages/client/src/pages/settings/index.vue
+++ b/packages/client/src/pages/settings/index.vue
@@ -14,7 +14,7 @@
 			</div>
 			<div class="main">
 				<div class="bkzroven">
-					<component :is="component" :key="page" v-bind="pageProps" @info="onInfo"/>
+					<component :is="component" :ref="el => pageChanged(el)" :key="page" v-bind="pageProps"/>
 				</div>
 			</div>
 		</div>
@@ -250,8 +250,9 @@ export default defineComponent({
 
 		const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified));
 
-		const onInfo = (info) => {
-			childInfo.value = info;
+		const pageChanged = (page) => {
+			if (page == null) return;
+			childInfo.value = page[symbols.PAGE_INFO];
 		};
 
 		return {
@@ -264,7 +265,7 @@ export default defineComponent({
 			pageProps,
 			component,
 			emailNotConfigured,
-			onInfo,
+			pageChanged,
 			childInfo,
 		};
 	},
diff --git a/packages/client/src/pages/settings/instance-mute.vue b/packages/client/src/pages/settings/instance-mute.vue
index 584a21e4bd..f84a209b60 100644
--- a/packages/client/src/pages/settings/instance-mute.vue
+++ b/packages/client/src/pages/settings/instance-mute.vue
@@ -47,11 +47,6 @@ export default defineComponent({
 		},
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-
 	async created() {
 		this.instanceMutes = this.$i.mutedInstances.join('\n');
 	},
diff --git a/packages/client/src/pages/settings/integration.vue b/packages/client/src/pages/settings/integration.vue
index e3dbc6fde9..ca36c91665 100644
--- a/packages/client/src/pages/settings/integration.vue
+++ b/packages/client/src/pages/settings/integration.vue
@@ -73,8 +73,6 @@ export default defineComponent({
 	},
 
 	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-
 		document.cookie = `igi=${this.$i.token}; path=/;` +
 			` max-age=31536000;` +
 			(document.location.protocol.startsWith('https') ? ' secure' : '');
diff --git a/packages/client/src/pages/settings/menu.vue b/packages/client/src/pages/settings/menu.vue
index 26404f3adf..6e38cd5dfe 100644
--- a/packages/client/src/pages/settings/menu.vue
+++ b/packages/client/src/pages/settings/menu.vue
@@ -67,10 +67,6 @@ export default defineComponent({
 		},
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async addItem() {
 			const menu = Object.keys(this.menuDef).filter(k => !this.$store.state.menu.includes(k));
diff --git a/packages/client/src/pages/settings/mute-block.vue b/packages/client/src/pages/settings/mute-block.vue
index 6a63c9eb21..f4f9ebf8dd 100644
--- a/packages/client/src/pages/settings/mute-block.vue
+++ b/packages/client/src/pages/settings/mute-block.vue
@@ -27,8 +27,8 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import MkPagination from '@/components/ui/pagination.vue';
 import MkTab from '@/components/tab.vue';
 import FormInfo from '@/components/ui/info.vue';
@@ -36,42 +36,25 @@ import FormLink from '@/components/form/link.vue';
 import { userPage } from '@/filters/user';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkTab,
-		FormInfo,
-		FormLink,
+let tab = $ref('mute');
+
+const mutingPagination = {
+	endpoint: 'mute/list' as const,
+	limit: 10,
+};
+
+const blockingPagination = {
+	endpoint: 'blocking/list' as const,
+	limit: 10,
+};
+
+defineExpose({
+	[symbols.PAGE_INFO]: {
+		title: i18n.locale.muteAndBlock,
+		icon: 'fas fa-ban',
+		bg: 'var(--bg)',
 	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.muteAndBlock,
-				icon: 'fas fa-ban',
-				bg: 'var(--bg)',
-			},
-			tab: 'mute',
-			mutingPagination: {
-				endpoint: 'mute/list',
-				limit: 10,
-			},
-			blockingPagination: {
-				endpoint: 'blocking/list',
-				limit: 10,
-			},
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		userPage
-	}
 });
 </script>
diff --git a/packages/client/src/pages/settings/notifications.vue b/packages/client/src/pages/settings/notifications.vue
index ab26d7d558..12171530bb 100644
--- a/packages/client/src/pages/settings/notifications.vue
+++ b/packages/client/src/pages/settings/notifications.vue
@@ -37,10 +37,6 @@ export default defineComponent({
 		}
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		readAllUnreadNotes() {
 			os.api('i/read-all-unread-notes');
diff --git a/packages/client/src/pages/settings/other.vue b/packages/client/src/pages/settings/other.vue
index 7547013832..6e48cb58a6 100644
--- a/packages/client/src/pages/settings/other.vue
+++ b/packages/client/src/pages/settings/other.vue
@@ -47,10 +47,6 @@ export default defineComponent({
 		reportError: defaultStore.makeGetterSetter('reportError'),
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		changeDebug(v) {
 			console.log(v);
diff --git a/packages/client/src/pages/settings/plugin.install.vue b/packages/client/src/pages/settings/plugin.install.vue
index bf494fa719..d35d20d17a 100644
--- a/packages/client/src/pages/settings/plugin.install.vue
+++ b/packages/client/src/pages/settings/plugin.install.vue
@@ -45,10 +45,6 @@ export default defineComponent({
 		}
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		installPlugin({ id, meta, ast, token }) {
 			ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
diff --git a/packages/client/src/pages/settings/plugin.vue b/packages/client/src/pages/settings/plugin.vue
index d411ad2961..7a3ab9d152 100644
--- a/packages/client/src/pages/settings/plugin.vue
+++ b/packages/client/src/pages/settings/plugin.vue
@@ -64,10 +64,6 @@ export default defineComponent({
 		}
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		uninstall(plugin) {
 			ColdDeviceStorage.set('plugins', this.plugins.filter(x => x.id !== plugin.id));
diff --git a/packages/client/src/pages/settings/privacy.vue b/packages/client/src/pages/settings/privacy.vue
index 78a0ea8b8d..dd13ba4bd0 100644
--- a/packages/client/src/pages/settings/privacy.vue
+++ b/packages/client/src/pages/settings/privacy.vue
@@ -47,8 +47,8 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import FormSwitch from '@/components/form/switch.vue';
 import FormSelect from '@/components/form/select.vue';
 import FormSection from '@/components/form/section.vue';
@@ -56,67 +56,39 @@ import FormGroup from '@/components/form/group.vue';
 import * as os from '@/os';
 import { defaultStore } from '@/store';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
+import { $i } from '@/account';
 
-export default defineComponent({
-	components: {
-		FormSelect,
-		FormSection,
-		FormGroup,
-		FormSwitch,
+let isLocked = $ref($i.isLocked);
+let autoAcceptFollowed = $ref($i.autoAcceptFollowed);
+let noCrawle = $ref($i.noCrawle);
+let isExplorable = $ref($i.isExplorable);
+let hideOnlineStatus = $ref($i.hideOnlineStatus);
+let publicReactions = $ref($i.publicReactions);
+let ffVisibility = $ref($i.ffVisibility);
+
+let defaultNoteVisibility = $computed(defaultStore.makeGetterSetter('defaultNoteVisibility'));
+let defaultNoteLocalOnly = $computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly'));
+let rememberNoteVisibility = $computed(defaultStore.makeGetterSetter('rememberNoteVisibility'));
+let keepCw = $computed(defaultStore.makeGetterSetter('keepCw'));
+
+function save() {
+	os.api('i/update', {
+		isLocked: !!isLocked,
+		autoAcceptFollowed: !!autoAcceptFollowed,
+		noCrawle: !!noCrawle,
+		isExplorable: !!isExplorable,
+		hideOnlineStatus: !!hideOnlineStatus,
+		publicReactions: !!publicReactions,
+		ffVisibility: ffVisibility,
+	});
+}
+
+defineExpose({
+	[symbols.PAGE_INFO]: {
+		title: i18n.locale.privacy,
+		icon: 'fas fa-lock-open',
+		bg: 'var(--bg)',
 	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.privacy,
-				icon: 'fas fa-lock-open',
-				bg: 'var(--bg)',
-			},
-			isLocked: false,
-			autoAcceptFollowed: false,
-			noCrawle: false,
-			isExplorable: false,
-			hideOnlineStatus: false,
-			publicReactions: false,
-			ffVisibility: 'public',
-		}
-	},
-
-	computed: {
-		defaultNoteVisibility: defaultStore.makeGetterSetter('defaultNoteVisibility'),
-		defaultNoteLocalOnly: defaultStore.makeGetterSetter('defaultNoteLocalOnly'),
-		rememberNoteVisibility: defaultStore.makeGetterSetter('rememberNoteVisibility'),
-		keepCw: defaultStore.makeGetterSetter('keepCw'),
-	},
-
-	created() {
-		this.isLocked = this.$i.isLocked;
-		this.autoAcceptFollowed = this.$i.autoAcceptFollowed;
-		this.noCrawle = this.$i.noCrawle;
-		this.isExplorable = this.$i.isExplorable;
-		this.hideOnlineStatus = this.$i.hideOnlineStatus;
-		this.publicReactions = this.$i.publicReactions;
-		this.ffVisibility = this.$i.ffVisibility;
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		save() {
-			os.api('i/update', {
-				isLocked: !!this.isLocked,
-				autoAcceptFollowed: !!this.autoAcceptFollowed,
-				noCrawle: !!this.noCrawle,
-				isExplorable: !!this.isExplorable,
-				hideOnlineStatus: !!this.hideOnlineStatus,
-				publicReactions: !!this.publicReactions,
-				ffVisibility: this.ffVisibility,
-			});
-		}
-	}
 });
 </script>
diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue
index 2eaf9a9f83..f875146a2c 100644
--- a/packages/client/src/pages/settings/profile.vue
+++ b/packages/client/src/pages/settings/profile.vue
@@ -3,50 +3,50 @@
 	<div class="llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
 		<div class="avatar _acrylic">
 			<MkAvatar class="avatar" :user="$i" :disable-link="true" @click="changeAvatar"/>
-			<MkButton primary class="avatarEdit" @click="changeAvatar">{{ $ts._profile.changeAvatar }}</MkButton>
+			<MkButton primary class="avatarEdit" @click="changeAvatar">{{ i18n.locale._profile.changeAvatar }}</MkButton>
 		</div>
-		<MkButton primary class="bannerEdit" @click="changeBanner">{{ $ts._profile.changeBanner }}</MkButton>
+		<MkButton primary class="bannerEdit" @click="changeBanner">{{ i18n.locale._profile.changeBanner }}</MkButton>
 	</div>
 
-	<FormInput v-model="name" :max="30" manual-save class="_formBlock">
-		<template #label>{{ $ts._profile.name }}</template>
+	<FormInput v-model="profile.name" :max="30" manual-save class="_formBlock">
+		<template #label>{{ i18n.locale._profile.name }}</template>
 	</FormInput>
 
-	<FormTextarea v-model="description" :max="500" tall manual-save class="_formBlock">
-		<template #label>{{ $ts._profile.description }}</template>
-		<template #caption>{{ $ts._profile.youCanIncludeHashtags }}</template>
+	<FormTextarea v-model="profile.description" :max="500" tall manual-save class="_formBlock">
+		<template #label>{{ i18n.locale._profile.description }}</template>
+		<template #caption>{{ i18n.locale._profile.youCanIncludeHashtags }}</template>
 	</FormTextarea>
 
-	<FormInput v-model="location" manual-save class="_formBlock">
-		<template #label>{{ $ts.location }}</template>
+	<FormInput v-model="profile.location" manual-save class="_formBlock">
+		<template #label>{{ i18n.locale.location }}</template>
 		<template #prefix><i class="fas fa-map-marker-alt"></i></template>
 	</FormInput>
 
-	<FormInput v-model="birthday" type="date" manual-save class="_formBlock">
-		<template #label>{{ $ts.birthday }}</template>
+	<FormInput v-model="profile.birthday" type="date" manual-save class="_formBlock">
+		<template #label>{{ i18n.locale.birthday }}</template>
 		<template #prefix><i class="fas fa-birthday-cake"></i></template>
 	</FormInput>
 
-	<FormSelect v-model="lang" class="_formBlock">
-		<template #label>{{ $ts.language }}</template>
+	<FormSelect v-model="profile.lang" class="_formBlock">
+		<template #label>{{ i18n.locale.language }}</template>
 		<option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option>
 	</FormSelect>
 
 	<FormSlot>
-		<MkButton @click="editMetadata">{{ $ts._profile.metadataEdit }}</MkButton>
-		<template #caption>{{ $ts._profile.metadataDescription }}</template>
+		<MkButton @click="editMetadata">{{ i18n.locale._profile.metadataEdit }}</MkButton>
+		<template #caption>{{ i18n.locale._profile.metadataDescription }}</template>
 	</FormSlot>
 
-	<FormSwitch v-model="isCat" class="_formBlock">{{ $ts.flagAsCat }}<template #caption>{{ $ts.flagAsCatDescription }}</template></FormSwitch>
+	<FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.locale.flagAsCat }}<template #caption>{{ i18n.locale.flagAsCatDescription }}</template></FormSwitch>
 
-	<FormSwitch v-model="isBot" class="_formBlock">{{ $ts.flagAsBot }}<template #caption>{{ $ts.flagAsBotDescription }}</template></FormSwitch>
+	<FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.locale.flagAsBot }}<template #caption>{{ i18n.locale.flagAsBotDescription }}</template></FormSwitch>
 
-	<FormSwitch v-model="alwaysMarkNsfw" class="_formBlock">{{ $ts.alwaysMarkSensitive }}</FormSwitch>
+	<FormSwitch v-model="profile.alwaysMarkNsfw" class="_formBlock">{{ i18n.locale.alwaysMarkSensitive }}</FormSwitch>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { defineComponent, reactive, watch } from 'vue';
 import MkButton from '@/components/ui/button.vue';
 import FormInput from '@/components/form/input.vue';
 import FormTextarea from '@/components/form/textarea.vue';
@@ -57,198 +57,149 @@ import { host, langs } from '@/config';
 import { selectFile } from '@/scripts/select-file';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
+import { $i } from '@/account';
 
-export default defineComponent({
-	components: {
-		MkButton,
-		FormInput,
-		FormTextarea,
-		FormSwitch,
-		FormSelect,
-		FormSlot,
-	},
-	
-	emits: ['info'],
+const profile = reactive({
+	name: $i.name,
+	description: $i.description,
+	location: $i.location,
+	birthday: $i.birthday,
+	lang: $i.lang,
+	isBot: $i.isBot,
+	isCat: $i.isCat,
+	alwaysMarkNsfw: $i.alwaysMarkNsfw,
+});
 
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.profile,
-				icon: 'fas fa-user',
-				bg: 'var(--bg)',
-			},
-			host,
-			langs,
-			name: null,
-			description: null,
-			birthday: null,
-			lang: 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,
-			alwaysMarkNsfw: false,
-			saving: false,
-		}
-	},
+const additionalFields = reactive({
+	fieldName0: $i.fields[0] ? $i.fields[0].name : null,
+	fieldValue0: $i.fields[0] ? $i.fields[0].value : null,
+	fieldName1: $i.fields[1] ? $i.fields[1].name : null,
+	fieldValue1: $i.fields[1] ? $i.fields[1].value : null,
+	fieldName2: $i.fields[2] ? $i.fields[2].name : null,
+	fieldValue2: $i.fields[2] ? $i.fields[2].value : null,
+	fieldName3: $i.fields[3] ? $i.fields[3].name : null,
+	fieldValue3: $i.fields[3] ? $i.fields[3].value : null,
+});
 
-	created() {
-		this.name = this.$i.name;
-		this.description = this.$i.description;
-		this.location = this.$i.location;
-		this.birthday = this.$i.birthday;
-		this.lang = this.$i.lang;
-		this.avatarId = this.$i.avatarId;
-		this.bannerId = this.$i.bannerId;
-		this.isBot = this.$i.isBot;
-		this.isCat = this.$i.isCat;
-		this.alwaysMarkNsfw = this.$i.alwaysMarkNsfw;
+watch(() => profile, () => {
+	save();
+}, {
+	deep: true,
+});
 
-		this.fieldName0 = this.$i.fields[0] ? this.$i.fields[0].name : null;
-		this.fieldValue0 = this.$i.fields[0] ? this.$i.fields[0].value : null;
-		this.fieldName1 = this.$i.fields[1] ? this.$i.fields[1].name : null;
-		this.fieldValue1 = this.$i.fields[1] ? this.$i.fields[1].value : null;
-		this.fieldName2 = this.$i.fields[2] ? this.$i.fields[2].name : null;
-		this.fieldValue2 = this.$i.fields[2] ? this.$i.fields[2].value : null;
-		this.fieldName3 = this.$i.fields[3] ? this.$i.fields[3].name : null;
-		this.fieldValue3 = this.$i.fields[3] ? this.$i.fields[3].value : null;
+function save() {
+	os.apiWithDialog('i/update', {
+		name: profile.name || null,
+		description: profile.description || null,
+		location: profile.location || null,
+		birthday: profile.birthday || null,
+		lang: profile.lang || null,
+		isBot: !!profile.isBot,
+		isCat: !!profile.isCat,
+		alwaysMarkNsfw: !!profile.alwaysMarkNsfw,
+	});
+}
 
-		this.$watch('name', this.save);
-		this.$watch('description', this.save);
-		this.$watch('location', this.save);
-		this.$watch('birthday', this.save);
-		this.$watch('lang', this.save);
-		this.$watch('isBot', this.save);
-		this.$watch('isCat', this.save);
-		this.$watch('alwaysMarkNsfw', this.save);
-	},
+function changeAvatar(ev) {
+	selectFile(ev.currentTarget || ev.target, i18n.locale.avatar).then(async (file) => {
+		const i = await os.apiWithDialog('i/update', {
+			avatarId: file.id,
+		});
+		$i.avatarId = i.avatarId;
+		$i.avatarUrl = i.avatarUrl;
+	});
+}
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
+function changeBanner(ev) {
+	selectFile(ev.currentTarget || ev.target, i18n.locale.banner).then(async (file) => {
+		const i = await os.apiWithDialog('i/update', {
+			bannerId: file.id,
+		});
+		$i.bannerId = i.bannerId;
+		$i.bannerUrl = i.bannerUrl;
+	});
+}
 
-	methods: {
-		changeAvatar(e) {
-			selectFile(e.currentTarget || e.target, this.$ts.avatar).then(file => {
-				os.api('i/update', {
-					avatarId: file.id,
-				});
-			});
+async function editMetadata() {
+	const { canceled, result } = await os.form(i18n.locale._profile.metadata, {
+		fieldName0: {
+			type: 'string',
+			label: i18n.locale._profile.metadataLabel + ' 1',
+			default: additionalFields.fieldName0,
 		},
-
-		changeBanner(e) {
-			selectFile(e.currentTarget || e.target, this.$ts.banner).then(file => {
-				os.api('i/update', {
-					bannerId: file.id,
-				});
-			});
+		fieldValue0: {
+			type: 'string',
+			label: i18n.locale._profile.metadataContent + ' 1',
+			default: additionalFields.fieldValue0,
 		},
-
-		async editMetadata() {
-			const { canceled, result } = await os.form(this.$ts._profile.metadata, {
-				fieldName0: {
-					type: 'string',
-					label: this.$ts._profile.metadataLabel + ' 1',
-					default: this.fieldName0,
-				},
-				fieldValue0: {
-					type: 'string',
-					label: this.$ts._profile.metadataContent + ' 1',
-					default: this.fieldValue0,
-				},
-				fieldName1: {
-					type: 'string',
-					label: this.$ts._profile.metadataLabel + ' 2',
-					default: this.fieldName1,
-				},
-				fieldValue1: {
-					type: 'string',
-					label: this.$ts._profile.metadataContent + ' 2',
-					default: this.fieldValue1,
-				},
-				fieldName2: {
-					type: 'string',
-					label: this.$ts._profile.metadataLabel + ' 3',
-					default: this.fieldName2,
-				},
-				fieldValue2: {
-					type: 'string',
-					label: this.$ts._profile.metadataContent + ' 3',
-					default: this.fieldValue2,
-				},
-				fieldName3: {
-					type: 'string',
-					label: this.$ts._profile.metadataLabel + ' 4',
-					default: this.fieldName3,
-				},
-				fieldValue3: {
-					type: 'string',
-					label: this.$ts._profile.metadataContent + ' 4',
-					default: this.fieldValue3,
-				},
-			});
-			if (canceled) return;
-
-			this.fieldName0 = result.fieldName0;
-			this.fieldValue0 = result.fieldValue0;
-			this.fieldName1 = result.fieldName1;
-			this.fieldValue1 = result.fieldValue1;
-			this.fieldName2 = result.fieldName2;
-			this.fieldValue2 = result.fieldValue2;
-			this.fieldName3 = result.fieldName3;
-			this.fieldValue3 = result.fieldValue3;
-
-			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 },
-			];
-
-			os.api('i/update', {
-				fields,
-			}).then(i => {
-				os.success();
-			}).catch(err => {
-				os.alert({
-					type: 'error',
-					text: err.id
-				});
-			});
+		fieldName1: {
+			type: 'string',
+			label: i18n.locale._profile.metadataLabel + ' 2',
+			default: additionalFields.fieldName1,
 		},
-
-		save() {
-			this.saving = true;
-
-			os.apiWithDialog('i/update', {
-				name: this.name || null,
-				description: this.description || null,
-				location: this.location || null,
-				birthday: this.birthday || null,
-				lang: this.lang || null,
-				isBot: !!this.isBot,
-				isCat: !!this.isCat,
-				alwaysMarkNsfw: !!this.alwaysMarkNsfw,
-			}).then(i => {
-				this.saving = false;
-				this.$i.avatarId = i.avatarId;
-				this.$i.avatarUrl = i.avatarUrl;
-				this.$i.bannerId = i.bannerId;
-				this.$i.bannerUrl = i.bannerUrl;
-			}).catch(err => {
-				this.saving = false;
-			});
+		fieldValue1: {
+			type: 'string',
+			label: i18n.locale._profile.metadataContent + ' 2',
+			default: additionalFields.fieldValue1,
 		},
-	}
+		fieldName2: {
+			type: 'string',
+			label: i18n.locale._profile.metadataLabel + ' 3',
+			default: additionalFields.fieldName2,
+		},
+		fieldValue2: {
+			type: 'string',
+			label: i18n.locale._profile.metadataContent + ' 3',
+			default: additionalFields.fieldValue2,
+		},
+		fieldName3: {
+			type: 'string',
+			label: i18n.locale._profile.metadataLabel + ' 4',
+			default: additionalFields.fieldName3,
+		},
+		fieldValue3: {
+			type: 'string',
+			label: i18n.locale._profile.metadataContent + ' 4',
+			default: additionalFields.fieldValue3,
+		},
+	});
+	if (canceled) return;
+
+	additionalFields.fieldName0 = result.fieldName0;
+	additionalFields.fieldValue0 = result.fieldValue0;
+	additionalFields.fieldName1 = result.fieldName1;
+	additionalFields.fieldValue1 = result.fieldValue1;
+	additionalFields.fieldName2 = result.fieldName2;
+	additionalFields.fieldValue2 = result.fieldValue2;
+	additionalFields.fieldName3 = result.fieldName3;
+	additionalFields.fieldValue3 = result.fieldValue3;
+
+	const fields = [
+		{ name: additionalFields.fieldName0, value: additionalFields.fieldValue0 },
+		{ name: additionalFields.fieldName1, value: additionalFields.fieldValue1 },
+		{ name: additionalFields.fieldName2, value: additionalFields.fieldValue2 },
+		{ name: additionalFields.fieldName3, value: additionalFields.fieldValue3 },
+	];
+
+	os.api('i/update', {
+		fields,
+	}).then(i => {
+		os.success();
+	}).catch(err => {
+		os.alert({
+			type: 'error',
+			text: err.id
+		});
+	});
+}
+
+defineExpose({
+	[symbols.PAGE_INFO]: {
+		title: i18n.locale.profile,
+		icon: 'fas fa-user',
+		bg: 'var(--bg)',
+	},
 });
 </script>
 
diff --git a/packages/client/src/pages/settings/reaction.vue b/packages/client/src/pages/settings/reaction.vue
index 0d4db46936..e5b1189947 100644
--- a/packages/client/src/pages/settings/reaction.vue
+++ b/packages/client/src/pages/settings/reaction.vue
@@ -100,10 +100,6 @@ export default defineComponent({
 		}
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		save() {
 			this.$store.set('reactions', this.reactions);
diff --git a/packages/client/src/pages/settings/security.vue b/packages/client/src/pages/settings/security.vue
index 82a21d5b16..6fb3f1c413 100644
--- a/packages/client/src/pages/settings/security.vue
+++ b/packages/client/src/pages/settings/security.vue
@@ -12,7 +12,7 @@
 	
 	<FormSection>
 		<template #label>{{ $ts.signinHistory }}</template>
-		<FormPagination :pagination="pagination">
+		<MkPagination :pagination="pagination">
 			<template v-slot="{items}">
 				<div>
 					<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
@@ -25,7 +25,7 @@
 					</div>
 				</div>
 			</template>
-		</FormPagination>
+		</MkPagination>
 	</FormSection>
 
 	<FormSection>
@@ -42,7 +42,7 @@ import { defineComponent } from 'vue';
 import FormSection from '@/components/form/section.vue';
 import FormSlot from '@/components/form/slot.vue';
 import FormButton from '@/components/ui/button.vue';
-import FormPagination from '@/components/form/pagination.vue';
+import MkPagination from '@/components/ui/pagination.vue';
 import X2fa from './2fa.vue';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
@@ -51,7 +51,7 @@ export default defineComponent({
 	components: {
 		FormSection,
 		FormButton,
-		FormPagination,
+		MkPagination,
 		FormSlot,
 		X2fa,
 	},
@@ -66,16 +66,12 @@ export default defineComponent({
 				bg: 'var(--bg)',
 			},
 			pagination: {
-				endpoint: 'i/signin-history',
+				endpoint: 'i/signin-history' as const,
 				limit: 5,
 			},
 		}
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async change() {
 			const { canceled: canceled1, result: currentPassword } = await os.inputText({
diff --git a/packages/client/src/pages/settings/sounds.vue b/packages/client/src/pages/settings/sounds.vue
index 0977dd8322..490a1b5514 100644
--- a/packages/client/src/pages/settings/sounds.vue
+++ b/packages/client/src/pages/settings/sounds.vue
@@ -94,12 +94,6 @@ export default defineComponent({
 		this.sounds.chatBg = ColdDeviceStorage.get('sound_chatBg');
 		this.sounds.antenna = ColdDeviceStorage.get('sound_antenna');
 		this.sounds.channel = ColdDeviceStorage.get('sound_channel');
-		this.sounds.reversiPutBlack = ColdDeviceStorage.get('sound_reversiPutBlack');
-		this.sounds.reversiPutWhite = ColdDeviceStorage.get('sound_reversiPutWhite');
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
 	},
 
 	methods: {
diff --git a/packages/client/src/pages/settings/theme.install.vue b/packages/client/src/pages/settings/theme.install.vue
index 52935c75dc..e2a3f042b9 100644
--- a/packages/client/src/pages/settings/theme.install.vue
+++ b/packages/client/src/pages/settings/theme.install.vue
@@ -1,18 +1,18 @@
 <template>
 <div class="_formRoot">
 	<FormTextarea v-model="installThemeCode" class="_formBlock">
-		<template #label>{{ $ts._theme.code }}</template>
+		<template #label>{{ i18n.locale._theme.code }}</template>
 	</FormTextarea>
 
 	<div class="_formBlock" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
-		<FormButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton>
-		<FormButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="fas fa-check"></i> {{ $ts.install }}</FormButton>
+		<FormButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="fas fa-eye"></i> {{ i18n.locale.preview }}</FormButton>
+		<FormButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="fas fa-check"></i> {{ i18n.locale.install }}</FormButton>
 	</div>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import * as JSON5 from 'json5';
 import FormTextarea from '@/components/form/textarea.vue';
 import FormButton from '@/components/ui/button.vue';
@@ -20,75 +20,60 @@ import { applyTheme, validateTheme } from '@/scripts/theme';
 import * as os from '@/os';
 import { addTheme, getThemes } from '@/theme-store';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		FormTextarea,
-		FormButton,
-	},
+let installThemeCode = $ref(null);
 
-	emits: ['info'],
+function parseThemeCode(code: string) {
+	let theme;
 
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts._theme.install,
-				icon: 'fas fa-download',
-				bg: 'var(--bg)',
-			},
-			installThemeCode: null,
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		parseThemeCode(code) {
-			let theme;
-
-			try {
-				theme = JSON5.parse(code);
-			} catch (e) {
-				os.alert({
-					type: 'error',
-					text: this.$ts._theme.invalid
-				});
-				return false;
-			}
-			if (!validateTheme(theme)) {
-				os.alert({
-					type: 'error',
-					text: this.$ts._theme.invalid
-				});
-				return false;
-			}
-			if (getThemes().some(t => t.id === theme.id)) {
-				os.alert({
-					type: 'info',
-					text: this.$ts._theme.alreadyInstalled
-				});
-				return false;
-			}
-
-			return theme;
-		},
-
-		preview(code) {
-			const theme = this.parseThemeCode(code);
-			if (theme) applyTheme(theme, false);
-		},
-
-		async install(code) {
-			const theme = this.parseThemeCode(code);
-			if (!theme) return;
-			await addTheme(theme);
-			os.alert({
-				type: 'success',
-				text: this.$t('_theme.installed', { name: theme.name })
-			});
-		},
+	try {
+		theme = JSON5.parse(code);
+	} catch (e) {
+		os.alert({
+			type: 'error',
+			text: i18n.locale._theme.invalid
+		});
+		return false;
 	}
+	if (!validateTheme(theme)) {
+		os.alert({
+			type: 'error',
+			text: i18n.locale._theme.invalid
+		});
+		return false;
+	}
+	if (getThemes().some(t => t.id === theme.id)) {
+		os.alert({
+			type: 'info',
+			text: i18n.locale._theme.alreadyInstalled
+		});
+		return false;
+	}
+
+	return theme;
+}
+
+function preview(code: string): void {
+	const theme = parseThemeCode(code);
+	if (theme) applyTheme(theme, false);
+}
+
+async function install(code: string): Promise<void> {
+	const theme = parseThemeCode(code);
+	if (!theme) return;
+	await addTheme(theme);
+	os.alert({
+		type: 'success',
+		text: i18n.t('_theme.installed', { name: theme.name })
+	});
+}
+
+defineExpose({
+	[symbols.PAGE_INFO]: {
+		title: i18n.locale._theme.install,
+		icon: 'fas fa-download',
+		bg: 'var(--bg)',
+	},
 });
 </script>
diff --git a/packages/client/src/pages/settings/theme.manage.vue b/packages/client/src/pages/settings/theme.manage.vue
index a913ba4748..a1e849b540 100644
--- a/packages/client/src/pages/settings/theme.manage.vue
+++ b/packages/client/src/pages/settings/theme.manage.vue
@@ -78,10 +78,6 @@ export default defineComponent({
 		},
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		copyThemeCode() {
 			copyToClipboard(this.selectedThemeCode);
diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue
index 6c88b65699..658e36ec05 100644
--- a/packages/client/src/pages/settings/theme.vue
+++ b/packages/client/src/pages/settings/theme.vue
@@ -163,10 +163,6 @@ export default defineComponent({
 			location.reload();
 		});
 
-		onMounted(() => {
-			emit('info', INFO);
-		});
-
 		onActivated(() => {
 			fetchThemes().then(() => {
 				installedThemes.value = getThemes();
diff --git a/packages/client/src/pages/settings/word-mute.vue b/packages/client/src/pages/settings/word-mute.vue
index 34edd0492c..19980dea14 100644
--- a/packages/client/src/pages/settings/word-mute.vue
+++ b/packages/client/src/pages/settings/word-mute.vue
@@ -87,10 +87,6 @@ export default defineComponent({
 		this.hardWordMutedNotesCount = (await os.api('i/get-word-muted-notes-count', {})).count;
 	},
 
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
 	methods: {
 		async save() {
 			this.$store.set('mutedWords', this.softMutedWords.trim().split('\n').map(x => x.trim().split(' ')));
diff --git a/packages/client/src/pages/share.vue b/packages/client/src/pages/share.vue
index bdd8500ee4..5df6256fb2 100644
--- a/packages/client/src/pages/share.vue
+++ b/packages/client/src/pages/share.vue
@@ -169,7 +169,7 @@ export default defineComponent({
 			window.close();
 
 			// 閉じなければ100ms後タイムラインに
-			setTimeout(() => {
+			window.setTimeout(() => {
 				this.$router.push('/');
 			}, 100);
 		}
diff --git a/packages/client/src/pages/signup-complete.vue b/packages/client/src/pages/signup-complete.vue
index 89375e05d2..a10af1a4cc 100644
--- a/packages/client/src/pages/signup-complete.vue
+++ b/packages/client/src/pages/signup-complete.vue
@@ -1,50 +1,36 @@
 <template>
 <div>
-	{{ $ts.processing }}
+	{{ i18n.locale.processing }}
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted } from 'vue';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
 import { login } from '@/account';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
+const props = defineProps<{
+	code: string;
+}>();
 
+onMounted(async () => {
+	await os.alert({
+		type: 'info',
+		text: i18n.t('clickToFinishEmailVerification', { ok: i18n.locale.gotIt }),
+	});
+	const res = await os.apiWithDialog('signup-pending', {
+		code: props.code,
+	});
+	login(res.i, '/');
+});
+
+defineExpose({
+	[symbols.PAGE_INFO]: {
+		title: i18n.locale.signup,
+		icon: 'fas fa-user',
 	},
-
-	props: {
-		code: {
-			type: String,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.signup,
-				icon: 'fas fa-user'
-			},
-		}
-	},
-
-	async mounted() {
-		await os.alert({
-			type: 'info',
-			text: this.$t('clickToFinishEmailVerification', { ok: this.$ts.gotIt }),
-		});
-		const res = await os.apiWithDialog('signup-pending', {
-			code: this.code,
-		});
-		login(res.i, '/');
-	},
-
-	methods: {
-
-	}
 });
 </script>
 
diff --git a/packages/client/src/pages/tag.vue b/packages/client/src/pages/tag.vue
index a0c8367849..045f1ef259 100644
--- a/packages/client/src/pages/tag.vue
+++ b/packages/client/src/pages/tag.vue
@@ -1,46 +1,31 @@
 <template>
 <div class="_section">
-	<XNotes ref="notes" class="_content" :pagination="pagination"/>
+	<XNotes class="_content" :pagination="pagination"/>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
 import XNotes from '@/components/notes.vue';
 import * as symbols from '@/symbols';
 
-export default defineComponent({
-	components: {
-		XNotes
-	},
+const props = defineProps<{
+	tag: string;
+}>();
 
-	props: {
-		tag: {
-			type: String,
-			required: true
-		}
-	},
+const pagination = {
+	endpoint: 'notes/search-by-tag' as const,
+	limit: 10,
+	params: computed(() => ({
+		tag: props.tag,
+	})),
+};
 
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.tag,
-				icon: 'fas fa-hashtag'
-			},
-			pagination: {
-				endpoint: 'notes/search-by-tag',
-				limit: 10,
-				params: () => ({
-					tag: this.tag,
-				})
-			},
-		};
-	},
-
-	watch: {
-		tag() {
-			(this.$refs.notes as any).reload();
-		}
-	},
+defineExpose({
+	[symbols.PAGE_INFO]: computed(() => ({
+		title: props.tag,
+		icon: 'fas fa-hashtag',
+		bg: 'var(--bg)',
+	})),
 });
 </script>
diff --git a/packages/client/src/pages/theme-editor.vue b/packages/client/src/pages/theme-editor.vue
index c4917e2270..80b8c7806c 100644
--- a/packages/client/src/pages/theme-editor.vue
+++ b/packages/client/src/pages/theme-editor.vue
@@ -2,7 +2,7 @@
 <MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
 	<div class="cwepdizn _formRoot">
 		<FormFolder :default-open="true" class="_formBlock">
-			<template #label>{{ $ts.backgroundColor }}</template>
+			<template #label>{{ i18n.locale.backgroundColor }}</template>
 			<div class="cwepdizn-colors">
 				<div class="row">
 					<button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)">
@@ -18,7 +18,7 @@
 		</FormFolder>
 
 		<FormFolder :default-open="true" class="_formBlock">
-			<template #label>{{ $ts.accentColor }}</template>
+			<template #label>{{ i18n.locale.accentColor }}</template>
 			<div class="cwepdizn-colors">
 				<div class="row">
 					<button v-for="color in accentColors" :key="color" class="color rounded _button" :class="{ active: theme.props.accent === color }" @click="setAccentColor(color)">
@@ -29,7 +29,7 @@
 		</FormFolder>
 
 		<FormFolder :default-open="true" class="_formBlock">
-			<template #label>{{ $ts.textColor }}</template>
+			<template #label>{{ i18n.locale.textColor }}</template>
 			<div class="cwepdizn-colors">
 				<div class="row">
 					<button v-for="color in fgColors" :key="color" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }" @click="setFgColor(color)">
@@ -41,22 +41,22 @@
 
 		<FormFolder :default-open="false" class="_formBlock">
 			<template #icon><i class="fas fa-code"></i></template>
-			<template #label>{{ $ts.editCode }}</template>
+			<template #label>{{ i18n.locale.editCode }}</template>
 
 			<div class="_formRoot">
 				<FormTextarea v-model="themeCode" tall class="_formBlock">
-					<template #label>{{ $ts._theme.code }}</template>
+					<template #label>{{ i18n.locale._theme.code }}</template>
 				</FormTextarea>
-				<FormButton primary class="_formBlock" @click="applyThemeCode">{{ $ts.apply }}</FormButton>
+				<FormButton primary class="_formBlock" @click="applyThemeCode">{{ i18n.locale.apply }}</FormButton>
 			</div>
 		</FormFolder>
 
 		<FormFolder :default-open="false" class="_formBlock">
-			<template #label>{{ $ts.addDescription }}</template>
+			<template #label>{{ i18n.locale.addDescription }}</template>
 
 			<div class="_formRoot">
 				<FormTextarea v-model="description">
-					<template #label>{{ $ts._theme.description }}</template>
+					<template #label>{{ i18n.locale._theme.description }}</template>
 				</FormTextarea>
 			</div>
 		</FormFolder>
@@ -64,8 +64,8 @@
 </MkSpacer>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { watch } from 'vue';
 import { toUnicode } from 'punycode/';
 import * as tinycolor from 'tinycolor2';
 import { v4 as uuid} from 'uuid';
@@ -78,181 +78,147 @@ import FormFolder from '@/components/form/folder.vue';
 import { Theme, applyTheme, darkTheme, lightTheme } from '@/scripts/theme';
 import { host } from '@/config';
 import * as os from '@/os';
-import { ColdDeviceStorage } from '@/store';
+import { ColdDeviceStorage, defaultStore } from '@/store';
 import { addTheme } from '@/theme-store';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
+import { useLeaveGuard } from '@/scripts/use-leave-guard';
 
-export default defineComponent({
-	components: {
-		FormButton,
-		FormTextarea,
-		FormFolder,
-	},
+const bgColors = [
+	{ color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' },
+	{ color: '#f0eee9', kind: 'light', forPreview: '#f3e2b9' },
+	{ color: '#e9eff0', kind: 'light', forPreview: '#bfe3e8' },
+	{ color: '#f0e9ee', kind: 'light', forPreview: '#f1d1e8' },
+	{ color: '#dce2e0', kind: 'light', forPreview: '#a4dccc' },
+	{ color: '#e2e0dc', kind: 'light', forPreview: '#d8c7a5' },
+	{ color: '#d5dbe0', kind: 'light', forPreview: '#b0cae0' },
+	{ color: '#dad5d5', kind: 'light', forPreview: '#d6afaf' },
+	{ color: '#2b2b2b', kind: 'dark', forPreview: '#444444' },
+	{ color: '#362e29', kind: 'dark', forPreview: '#735c4d' },
+	{ color: '#303629', kind: 'dark', forPreview: '#506d2f' },
+	{ color: '#293436', kind: 'dark', forPreview: '#258192' },
+	{ color: '#2e2936', kind: 'dark', forPreview: '#504069' },
+	{ color: '#252722', kind: 'dark', forPreview: '#3c462f' },
+	{ color: '#212525', kind: 'dark', forPreview: '#303e3e' },
+	{ color: '#191919', kind: 'dark', forPreview: '#272727' },
+] as const;
+const accentColors = ['#e36749', '#f29924', '#98c934', '#34c9a9', '#34a1c9', '#606df7', '#8d34c9', '#e84d83'];
+const fgColors = [
+	{ color: 'none', forLight: '#5f5f5f', forDark: '#dadada', forPreview: null },
+	{ color: 'red', forLight: '#7f6666', forDark: '#e4d1d1', forPreview: '#ca4343' },
+	{ color: 'yellow', forLight: '#736955', forDark: '#e0d5c0', forPreview: '#d49923' },
+	{ color: 'green', forLight: '#586d5b', forDark: '#d1e4d4', forPreview: '#4cbd5c' },
+	{ color: 'cyan', forLight: '#5d7475', forDark: '#d1e3e4', forPreview: '#2abdc3' },
+	{ color: 'blue', forLight: '#676880', forDark: '#d1d2e4', forPreview: '#7275d8' },
+	{ color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' },
+];
 
-	async beforeRouteLeave(to, from) {
-		if (this.changed && !(await this.leaveConfirm())) {
-			return false;
-		}
-	},
+const theme = $ref<Partial<Theme>>({
+	base: 'light',
+	props: lightTheme.props,
+});
+let description = $ref<string | null>(null);
+let themeCode = $ref<string | null>(null);
+let changed = $ref(false);
 
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.themeEditor,
-				icon: 'fas fa-palette',
-				bg: 'var(--bg)',
-				actions: [{
-					asFullButton: true,
-					icon: 'fas fa-eye',
-					text: this.$ts.preview,
-					handler: this.showPreview,
-				}, {
-					asFullButton: true,
-					icon: 'fas fa-check',
-					text: this.$ts.saveAs,
-					handler: this.saveAs,
-				}],
-			},
-			theme: {
-				base: 'light',
-				props: lightTheme.props
-			} as Theme,
-			description: null,
-			themeCode: null,
-			bgColors: [
-				{ color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' },
-				{ color: '#f0eee9', kind: 'light', forPreview: '#f3e2b9' },
-				{ color: '#e9eff0', kind: 'light', forPreview: '#bfe3e8' },
-				{ color: '#f0e9ee', kind: 'light', forPreview: '#f1d1e8' },
-				{ color: '#dce2e0', kind: 'light', forPreview: '#a4dccc' },
-				{ color: '#e2e0dc', kind: 'light', forPreview: '#d8c7a5' },
-				{ color: '#d5dbe0', kind: 'light', forPreview: '#b0cae0' },
-				{ color: '#dad5d5', kind: 'light', forPreview: '#d6afaf' },
-				{ color: '#2b2b2b', kind: 'dark', forPreview: '#444444' },
-				{ color: '#362e29', kind: 'dark', forPreview: '#735c4d' },
-				{ color: '#303629', kind: 'dark', forPreview: '#506d2f' },
-				{ color: '#293436', kind: 'dark', forPreview: '#258192' },
-				{ color: '#2e2936', kind: 'dark', forPreview: '#504069' },
-				{ color: '#252722', kind: 'dark', forPreview: '#3c462f' },
-				{ color: '#212525', kind: 'dark', forPreview: '#303e3e' },
-				{ color: '#191919', kind: 'dark', forPreview: '#272727' },
-			],
-			accentColors: ['#e36749', '#f29924', '#98c934', '#34c9a9', '#34a1c9', '#606df7', '#8d34c9', '#e84d83'],
-			fgColors: [
-				{ color: 'none', forLight: '#5f5f5f', forDark: '#dadada', forPreview: null },
-				{ color: 'red', forLight: '#7f6666', forDark: '#e4d1d1', forPreview: '#ca4343' },
-				{ color: 'yellow', forLight: '#736955', forDark: '#e0d5c0', forPreview: '#d49923' },
-				{ color: 'green', forLight: '#586d5b', forDark: '#d1e4d4', forPreview: '#4cbd5c' },
-				{ color: 'cyan', forLight: '#5d7475', forDark: '#d1e3e4', forPreview: '#2abdc3' },
-				{ color: 'blue', forLight: '#676880', forDark: '#d1d2e4', forPreview: '#7275d8' },
-				{ color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' },
-			],
-			changed: false,
-		}
-	},
+useLeaveGuard($$(changed));
 
-	created() {
-		this.$watch('theme', this.apply, { deep: true });
-		window.addEventListener('beforeunload', this.beforeunload);
-	},
+function showPreview() {
+	os.pageWindow('preview');
+}
 
-	beforeUnmount() {
-		window.removeEventListener('beforeunload', this.beforeunload);
-	},
-
-	methods: {
-		beforeunload(e: BeforeUnloadEvent) {
-			if (this.changed) {
-				e.preventDefault();
-				e.returnValue = '';
-			}
-		},
-
-		async leaveConfirm(): Promise<boolean> {
-			const { canceled } = await os.confirm({
-				type: 'warning',
-				text: this.$ts.leaveConfirm,
-			});
-			return !canceled;
-		},
-
-		showPreview() {
-			os.pageWindow('preview');
-		},
-
-		setBgColor(color) {
-			if (this.theme.base != color.kind) {
-				const base = color.kind === 'dark' ? darkTheme : lightTheme;
-				for (const prop of Object.keys(base.props)) {
-					if (prop === 'accent') continue;
-					if (prop === 'fg') continue;
-					this.theme.props[prop] = base.props[prop];
-				}
-			}
-			this.theme.base = color.kind;
-			this.theme.props.bg = color.color;
-
-			if (this.theme.props.fg) {
-				const matchedFgColor = this.fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(this.theme.props.fg).toRgbString()));
-				if (matchedFgColor) this.setFgColor(matchedFgColor);
-			}
-		},
-
-		setAccentColor(color) {
-			this.theme.props.accent = color;
-		},
-
-		setFgColor(color) {
-			this.theme.props.fg = this.theme.base === 'light' ? color.forLight : color.forDark;
-		},
-
-		apply() {
-			this.themeCode = JSON5.stringify(this.theme, null, '\t');
-			applyTheme(this.theme, false);
-			this.changed = true;
-		},
-
-		applyThemeCode() {
-			let parsed;
-
-			try {
-				parsed = JSON5.parse(this.themeCode);
-			} catch (e) {
-				os.alert({
-					type: 'error',
-					text: this.$ts._theme.invalid
-				});
-				return;
-			}
-
-			this.theme = parsed;
-		},
-
-		async saveAs() {
-			const { canceled, result: name } = await os.inputText({
-				title: this.$ts.name,
-				allowEmpty: false
-			});
-			if (canceled) return;
-
-			this.theme.id = uuid();
-			this.theme.name = name;
-			this.theme.author = `@${this.$i.username}@${toUnicode(host)}`;
-			if (this.description) this.theme.desc = this.description;
-			addTheme(this.theme);
-			applyTheme(this.theme);
-			if (this.$store.state.darkMode) {
-				ColdDeviceStorage.set('darkTheme', this.theme);
-			} else {
-				ColdDeviceStorage.set('lightTheme', this.theme);
-			}
-			this.changed = false;
-			os.alert({
-				type: 'success',
-				text: this.$t('_theme.installed', { name: this.theme.name })
-			});
+function setBgColor(color: typeof bgColors[number]) {
+	if (theme.base != color.kind) {
+		const base = color.kind === 'dark' ? darkTheme : lightTheme;
+		for (const prop of Object.keys(base.props)) {
+			if (prop === 'accent') continue;
+			if (prop === 'fg') continue;
+			theme.props[prop] = base.props[prop];
 		}
 	}
+	theme.base = color.kind;
+	theme.props.bg = color.color;
+
+	if (theme.props.fg) {
+		const matchedFgColor = fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(theme.props.fg).toRgbString()));
+		if (matchedFgColor) setFgColor(matchedFgColor);
+	}
+}
+
+function setAccentColor(color) {
+	theme.props.accent = color;
+}
+
+function setFgColor(color) {
+	theme.props.fg = theme.base === 'light' ? color.forLight : color.forDark;
+}
+
+function apply() {
+	themeCode = JSON5.stringify(theme, null, '\t');
+	applyTheme(theme, false);
+	changed = true;
+}
+
+function applyThemeCode() {
+	let parsed;
+
+	try {
+		parsed = JSON5.parse(themeCode);
+	} catch (err) {
+		os.alert({
+			type: 'error',
+			text: i18n.locale._theme.invalid,
+		});
+		return;
+	}
+
+	theme = parsed;
+}
+
+async function saveAs() {
+	const { canceled, result: name } = await os.inputText({
+		title: i18n.locale.name,
+		allowEmpty: false,
+	});
+	if (canceled) return;
+
+	theme.id = uuid();
+	theme.name = name;
+	theme.author = `@${$i.username}@${toUnicode(host)}`;
+	if (description) theme.desc = description;
+	addTheme(theme);
+	applyTheme(theme);
+	if (defaultStore.state.darkMode) {
+		ColdDeviceStorage.set('darkTheme', theme);
+	} else {
+		ColdDeviceStorage.set('lightTheme', theme);
+	}
+	changed = false;
+	os.alert({
+		type: 'success',
+		text: i18n.t('_theme.installed', { name: theme.name }),
+	});
+}
+
+watch($$(theme), apply, { deep: true });
+
+defineExpose({
+	[symbols.PAGE_INFO]: {
+		title: i18n.locale.themeEditor,
+		icon: 'fas fa-palette',
+		bg: 'var(--bg)',
+		actions: [{
+			asFullButton: true,
+			icon: 'fas fa-eye',
+			text: i18n.locale.preview,
+			handler: showPreview,
+		}, {
+			asFullButton: true,
+			icon: 'fas fa-check',
+			text: i18n.locale.saveAs,
+			handler: saveAs,
+		}],
+	},
 });
 </script>
 
diff --git a/packages/client/src/pages/timeline.tutorial.vue b/packages/client/src/pages/timeline.tutorial.vue
index 3775796940..432d28c60b 100644
--- a/packages/client/src/pages/timeline.tutorial.vue
+++ b/packages/client/src/pages/timeline.tutorial.vue
@@ -65,26 +65,14 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
 import MkButton from '@/components/ui/button.vue';
+import { defaultStore } from '@/store';
 
-export default defineComponent({
-	components: {
-		MkButton,
-	},
-
-	data() {
-		return {
-		}
-	},
-
-	computed: {
-		tutorial: {
-			get() { return this.$store.reactiveState.tutorial.value || 0; },
-			set(value) { this.$store.set('tutorial', value); }
-		},
-	},
+const tutorial = computed({
+	get() { return defaultStore.reactiveState.tutorial.value || 0; },
+	set(value) { defaultStore.set('tutorial', value); }
 });
 </script>
 
diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue
index 216b3c34ea..ecd1ae6257 100644
--- a/packages/client/src/pages/timeline.vue
+++ b/packages/client/src/pages/timeline.vue
@@ -1,6 +1,6 @@
 <template>
 <MkSpacer :content-max="800">
-	<div v-hotkey.global="keymap" class="cmuxhskf">
+	<div ref="rootEl" v-hotkey.global="keymap" class="cmuxhskf">
 		<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/>
 		<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/>
 
@@ -17,163 +17,139 @@
 </MkSpacer>
 </template>
 
-<script lang="ts">
-import { defineComponent, defineAsyncComponent, computed } from 'vue';
+<script lang="ts" setup>
+import { defineAsyncComponent, computed, watch } from 'vue';
 import XTimeline from '@/components/timeline.vue';
 import XPostForm from '@/components/post-form.vue';
 import { scroll } from '@/scripts/scroll';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
+import { defaultStore } from '@/store';
+import { i18n } from '@/i18n';
+import { instance } from '@/instance';
+import { $i } from '@/account';
 
-export default defineComponent({
-	name: 'timeline',
+const XTutorial = defineAsyncComponent(() => import('./timeline.tutorial.vue'));
 
-	components: {
-		XTimeline,
-		XTutorial: defineAsyncComponent(() => import('./timeline.tutorial.vue')),
-		XPostForm,
-	},
+const isLocalTimelineAvailable = !instance.disableLocalTimeline || ($i != null && ($i.isModerator || $i.isAdmin));
+const isGlobalTimelineAvailable = !instance.disableGlobalTimeline || ($i != null && ($i.isModerator || $i.isAdmin));
+const keymap = {
+	't': focus,
+};
 
-	data() {
-		return {
-			src: 'home',
-			queue: 0,
-			[symbols.PAGE_INFO]: computed(() => ({
-				title: this.$ts.timeline,
-				icon: this.src === 'local' ? 'fas fa-comments' : this.src === 'social' ? 'fas fa-share-alt' : this.src === 'global' ? 'fas fa-globe' : 'fas fa-home',
-				bg: 'var(--bg)',
-				actions: [{
-					icon: 'fas fa-list-ul',
-					text: this.$ts.lists,
-					handler: this.chooseList
-				}, {
-					icon: 'fas fa-satellite',
-					text: this.$ts.antennas,
-					handler: this.chooseAntenna
-				}, {
-					icon: 'fas fa-satellite-dish',
-					text: this.$ts.channel,
-					handler: this.chooseChannel
-				}, {
-					icon: 'fas fa-calendar-alt',
-					text: this.$ts.jumpToSpecifiedDate,
-					handler: this.timetravel
-				}],
-				tabs: [{
-					active: this.src === 'home',
-					title: this.$ts._timelines.home,
-					icon: 'fas fa-home',
-					iconOnly: true,
-					onClick: () => { this.src = 'home'; this.saveSrc(); },
-				}, ...(this.isLocalTimelineAvailable ? [{
-					active: this.src === 'local',
-					title: this.$ts._timelines.local,
-					icon: 'fas fa-comments',
-					iconOnly: true,
-					onClick: () => { this.src = 'local'; this.saveSrc(); },
-				}, {
-					active: this.src === 'social',
-					title: this.$ts._timelines.social,
-					icon: 'fas fa-share-alt',
-					iconOnly: true,
-					onClick: () => { this.src = 'social'; this.saveSrc(); },
-				}] : []), ...(this.isGlobalTimelineAvailable ? [{
-					active: this.src === 'global',
-					title: this.$ts._timelines.global,
-					icon: 'fas fa-globe',
-					iconOnly: true,
-					onClick: () => { this.src = 'global'; this.saveSrc(); },
-				}] : [])],
-			})),
-		};
-	},
+const tlComponent = $ref<InstanceType<typeof XTimeline>>();
+const rootEl = $ref<HTMLElement>();
 
-	computed: {
-		keymap(): any {
-			return {
-				't': this.focus
-			};
-		},
+let src = $ref<'home' | 'local' | 'social' | 'global'>(defaultStore.state.tl.src);
+let queue = $ref(0);
 
-		isLocalTimelineAvailable(): boolean {
-			return !this.$instance.disableLocalTimeline || this.$i.isModerator || this.$i.isAdmin;
-		},
+function queueUpdated(q: number): void {
+	queue = q;
+}
 
-		isGlobalTimelineAvailable(): boolean {
-			return !this.$instance.disableGlobalTimeline || this.$i.isModerator || this.$i.isAdmin;
-		},
-	},
+function top(): void {
+	scroll(rootEl, { top: 0 });
+}
 
-	watch: {
-		src() {
-			this.showNav = false;
-		},
-	},
+async function chooseList(ev: MouseEvent): Promise<void> {
+	const lists = await os.api('users/lists/list');
+	const items = lists.map(list => ({
+		type: 'link',
+		text: list.name,
+		to: `/timeline/list/${list.id}`,
+	}));
+	os.popupMenu(items, ev.currentTarget || ev.target);
+}
 
-	created() {
-		this.src = this.$store.state.tl.src;
-	},
+async function chooseAntenna(ev: MouseEvent): Promise<void> {
+	const antennas = await os.api('antennas/list');
+	const items = antennas.map(antenna => ({
+		type: 'link',
+		text: antenna.name,
+		indicate: antenna.hasUnreadNote,
+		to: `/timeline/antenna/${antenna.id}`,
+	}));
+	os.popupMenu(items, ev.currentTarget || ev.target);
+}
 
-	methods: {
-		queueUpdated(q) {
-			this.queue = q;
-		},
+async function chooseChannel(ev: MouseEvent): Promise<void> {
+	const channels = await os.api('channels/followed');
+	const items = channels.map(channel => ({
+		type: 'link',
+		text: channel.name,
+		indicate: channel.hasUnreadNote,
+		to: `/channels/${channel.id}`,
+	}));
+	os.popupMenu(items, ev.currentTarget || ev.target);
+}
 
-		top() {
-			scroll(this.$el, { top: 0 });
-		},
+function saveSrc(): void {
+	defaultStore.set('tl', {
+		src: src,
+	});
+}
 
-		async chooseList(ev) {
-			const lists = await os.api('users/lists/list');
-			const items = lists.map(list => ({
-				type: 'link',
-				text: list.name,
-				to: `/timeline/list/${list.id}`
-			}));
-			os.popupMenu(items, ev.currentTarget || ev.target);
-		},
+async function timetravel(): Promise<void> {
+	const { canceled, result: date } = await os.inputDate({
+		title: i18n.locale.date,
+	});
+	if (canceled) return;
 
-		async chooseAntenna(ev) {
-			const antennas = await os.api('antennas/list');
-			const items = antennas.map(antenna => ({
-				type: 'link',
-				text: antenna.name,
-				indicate: antenna.hasUnreadNote,
-				to: `/timeline/antenna/${antenna.id}`
-			}));
-			os.popupMenu(items, ev.currentTarget || ev.target);
-		},
+	tlComponent.timetravel(date);
+}
 
-		async chooseChannel(ev) {
-			const channels = await os.api('channels/followed');
-			const items = channels.map(channel => ({
-				type: 'link',
-				text: channel.name,
-				indicate: channel.hasUnreadNote,
-				to: `/channels/${channel.id}`
-			}));
-			os.popupMenu(items, ev.currentTarget || ev.target);
-		},
+function focus(): void {
+	tlComponent.focus();
+}
 
-		saveSrc() {
-			this.$store.set('tl', {
-				src: this.src,
-			});
-		},
-
-		async timetravel() {
-			const { canceled, result: date } = await os.inputDate({
-				title: this.$ts.date,
-			});
-			if (canceled) return;
-
-			this.$refs.tl.timetravel(date);
-		},
-
-		focus() {
-			(this.$refs.tl as any).focus();
-		}
-	}
+defineExpose({
+	[symbols.PAGE_INFO]: computed(() => ({
+		title: i18n.locale.timeline,
+		icon: src === 'local' ? 'fas fa-comments' : src === 'social' ? 'fas fa-share-alt' : src === 'global' ? 'fas fa-globe' : 'fas fa-home',
+		bg: 'var(--bg)',
+		actions: [{
+			icon: 'fas fa-list-ul',
+			text: i18n.locale.lists,
+			handler: chooseList,
+		}, {
+			icon: 'fas fa-satellite',
+			text: i18n.locale.antennas,
+			handler: chooseAntenna,
+		}, {
+			icon: 'fas fa-satellite-dish',
+			text: i18n.locale.channel,
+			handler: chooseChannel,
+		}, {
+			icon: 'fas fa-calendar-alt',
+			text: i18n.locale.jumpToSpecifiedDate,
+			handler: timetravel,
+		}],
+		tabs: [{
+			active: src === 'home',
+			title: i18n.locale._timelines.home,
+			icon: 'fas fa-home',
+			iconOnly: true,
+			onClick: () => { src = 'home'; saveSrc(); },
+		}, ...(isLocalTimelineAvailable ? [{
+			active: src === 'local',
+			title: i18n.locale._timelines.local,
+			icon: 'fas fa-comments',
+			iconOnly: true,
+			onClick: () => { src = 'local'; saveSrc(); },
+		}, {
+			active: src === 'social',
+			title: i18n.locale._timelines.social,
+			icon: 'fas fa-share-alt',
+			iconOnly: true,
+			onClick: () => { src = 'social'; saveSrc(); },
+		}] : []), ...(isGlobalTimelineAvailable ? [{
+			active: src === 'global',
+			title: i18n.locale._timelines.global,
+			icon: 'fas fa-globe',
+			iconOnly: true,
+			onClick: () => { src = 'global'; saveSrc(); },
+		}] : [])],
+	})),
 });
 </script>
 
diff --git a/packages/client/src/pages/user/clips.vue b/packages/client/src/pages/user/clips.vue
index aad5317ce0..870e6f7174 100644
--- a/packages/client/src/pages/user/clips.vue
+++ b/packages/client/src/pages/user/clips.vue
@@ -28,7 +28,7 @@ export default defineComponent({
 	data() {
 		return {
 			pagination: {
-				endpoint: 'users/clips',
+				endpoint: 'users/clips' as const,
 				limit: 20,
 				params: {
 					userId: this.user.id,
diff --git a/packages/client/src/pages/user/follow-list.vue b/packages/client/src/pages/user/follow-list.vue
index 9fb8943fb8..98a1fc0f86 100644
--- a/packages/client/src/pages/user/follow-list.vue
+++ b/packages/client/src/pages/user/follow-list.vue
@@ -1,6 +1,6 @@
 <template>
 <div>
-	<MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="mk-following-or-followers">
+	<MkPagination v-slot="{items}" ref="list" :pagination="type === 'following' ? followingPagination : followersPagination" class="mk-following-or-followers">
 		<div class="users _isolated">
 			<MkUserInfo v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" class="user" :user="user"/>
 		</div>
@@ -8,50 +8,32 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as misskey from 'misskey-js';
 import MkUserInfo from '@/components/user-info.vue';
 import MkPagination from '@/components/ui/pagination.vue';
 
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkUserInfo,
-	},
+const props = defineProps<{
+	user: misskey.entities.User;
+	type: 'following' | 'followers';
+}>();
 
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-		type: {
-			type: String,
-			required: true
-		},
-	},
+const followingPagination = {
+	endpoint: 'users/following' as const,
+	limit: 20,
+	params: computed(() => ({
+		userId: props.user.id,
+	})),
+};
 
-	data() {
-		return {
-			pagination: {
-				endpoint: () => this.type === 'following' ? 'users/following' : 'users/followers',
-				limit: 20,
-				params: {
-					userId: this.user.id,
-				}
-			},
-		};
-	},
-
-	watch: {
-		type() {
-			this.$refs.list.reload();
-		},
-
-		user() {
-			this.$refs.list.reload();
-		}
-	}
-});
+const followersPagination = {
+	endpoint: 'users/followers' as const,
+	limit: 20,
+	params: computed(() => ({
+		userId: props.user.id,
+	})),
+};
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/pages/user/gallery.vue b/packages/client/src/pages/user/gallery.vue
index 860aa9f44f..07dda4a292 100644
--- a/packages/client/src/pages/user/gallery.vue
+++ b/packages/client/src/pages/user/gallery.vue
@@ -9,7 +9,7 @@
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue';
+import { computed, defineComponent } from 'vue';
 import MkGalleryPostPreview from '@/components/gallery-post-preview.vue';
 import MkPagination from '@/components/ui/pagination.vue';
 
@@ -29,20 +29,14 @@ export default defineComponent({
 	data() {
 		return {
 			pagination: {
-				endpoint: 'users/gallery/posts',
+				endpoint: 'users/gallery/posts' as const,
 				limit: 6,
-				params: () => ({
+				params: computed(() => ({
 					userId: this.user.id
-				})
+				})),
 			},
 		};
 	},
-
-	watch: {
-		user() {
-			this.$refs.list.reload();
-		}
-	}
 });
 </script>
 
diff --git a/packages/client/src/pages/user/index.activity.vue b/packages/client/src/pages/user/index.activity.vue
index e51d6c6090..43a4f476f1 100644
--- a/packages/client/src/pages/user/index.activity.vue
+++ b/packages/client/src/pages/user/index.activity.vue
@@ -8,27 +8,16 @@
 </MkContainer>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@/os';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
 import MkContainer from '@/components/ui/container.vue';
 import MkChart from '@/components/chart.vue';
 
-export default defineComponent({
-	components: {
-		MkContainer,
-		MkChart,
-	},
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-		limit: {
-			type: Number,
-			required: false,
-			default: 40
-		}
-	},
+const props = withDefaults(defineProps<{
+	user: misskey.entities.User;
+	limit?: number;
+}>(), {
+	limit: 40,
 });
 </script>
diff --git a/packages/client/src/pages/user/index.timeline.vue b/packages/client/src/pages/user/index.timeline.vue
index 2ffa496979..a1329a7411 100644
--- a/packages/client/src/pages/user/index.timeline.vue
+++ b/packages/client/src/pages/user/index.timeline.vue
@@ -1,60 +1,36 @@
 <template>
 <div v-sticky-container class="yrzkoczt">
-	<MkTab v-model="with_" class="tab">
+	<MkTab v-model="include" class="tab">
 		<option :value="null">{{ $ts.notes }}</option>
 		<option value="replies">{{ $ts.notesAndReplies }}</option>
 		<option value="files">{{ $ts.withFiles }}</option>
 	</MkTab>
-	<XNotes ref="timeline" :no-gap="true" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/>
+	<XNotes :no-gap="true" :pagination="pagination"/>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref, computed } from 'vue';
+import * as misskey from 'misskey-js';
 import XNotes from '@/components/notes.vue';
 import MkTab from '@/components/tab.vue';
 import * as os from '@/os';
 
-export default defineComponent({
-	components: {
-		XNotes,
-		MkTab,
-	},
+const props = defineProps<{
+	user: misskey.entities.UserDetailed;
+}>();
 
-	props: {
-		user: {
-			type: Object,
-			required: true,
-		},
-	},
+const include = ref<string | null>(null);
 
-	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),
-				})
-			}
-		};
-	},
-
-	watch: {
-		user() {
-			this.$refs.timeline.reload();
-		},
-
-		with_() {
-			this.$refs.timeline.reload();
-		},
-	},
-});
+const pagination = {
+	endpoint: 'users/notes' as const,
+	limit: 10,
+	params: computed(() => ({
+		userId: props.user.id,
+		includeReplies: include.value === 'replies',
+		withFiles: include.value === 'files',
+	})),
+};
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/pages/user/pages.vue b/packages/client/src/pages/user/pages.vue
index 40d1fe3842..ad101158e0 100644
--- a/packages/client/src/pages/user/pages.vue
+++ b/packages/client/src/pages/user/pages.vue
@@ -6,42 +6,23 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as misskey from 'misskey-js';
 import MkPagePreview from '@/components/page-preview.vue';
 import MkPagination from '@/components/ui/pagination.vue';
 
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkPagePreview,
-	},
+const props = defineProps<{
+	user: misskey.entities.User;
+}>();
 
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			pagination: {
-				endpoint: 'users/pages',
-				limit: 20,
-				params: {
-					userId: this.user.id,
-				}
-			},
-		};
-	},
-
-	watch: {
-		user() {
-			this.$refs.list.reload();
-		}
-	}
-});
+const pagination = {
+	endpoint: 'users/pages' as const,
+	limit: 20,
+	params: computed(() => ({
+		userId: props.user.id,
+	})),
+};
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/pages/user/reactions.vue b/packages/client/src/pages/user/reactions.vue
index 69c27de55b..d2c1f92ebb 100644
--- a/packages/client/src/pages/user/reactions.vue
+++ b/packages/client/src/pages/user/reactions.vue
@@ -7,50 +7,30 @@
 				<MkReactionIcon class="reaction" :reaction="item.type" :custom-emojis="item.note.emojis" :no-style="true"/>
 				<MkTime :time="item.createdAt" class="createdAt"/>
 			</div>
-			<MkNote :key="item.id" :note="item.note" @update:note="updated(note, $event)"/>
+			<MkNote :key="item.id" :note="item.note"/>
 		</div>
 	</MkPagination>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as misskey from 'misskey-js';
 import MkPagination from '@/components/ui/pagination.vue';
 import MkNote from '@/components/note.vue';
 import MkReactionIcon from '@/components/reaction-icon.vue';
 
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkNote,
-		MkReactionIcon,
-	},
+const props = defineProps<{
+	user: misskey.entities.User;
+}>();
 
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			pagination: {
-				endpoint: 'users/reactions',
-				limit: 20,
-				params: {
-					userId: this.user.id,
-				}
-			},
-		};
-	},
-
-	watch: {
-		user() {
-			this.$refs.list.reload();
-		}
-	},
-});
+const pagination = {
+	endpoint: 'users/reactions' as const,
+	limit: 20,
+	params: computed(() => ({
+		userId: props.user.id,
+	})),
+};
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/pizzax.ts b/packages/client/src/pizzax.ts
index dbbbfc228a..fa35df5511 100644
--- a/packages/client/src/pizzax.ts
+++ b/packages/client/src/pizzax.ts
@@ -54,7 +54,7 @@ export class Storage<T extends StateDef> {
 
 		if ($i) {
 			// なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう)
-			setTimeout(() => {
+			window.setTimeout(() => {
 				api('i/registry/get-all', { scope: ['client', this.key] }).then(kvs => {
 					const cache = {};
 					for (const [k, v] of Object.entries(def)) {
diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts
index a5ae1177e8..ec48b76fdf 100644
--- a/packages/client/src/router.ts
+++ b/packages/client/src/router.ts
@@ -33,7 +33,7 @@ const defaultRoutes = [
 	{ path: '/explore/tags/:tag', props: true, component: page('explore') },
 	{ path: '/federation', component: page('federation') },
 	{ path: '/emojis', component: page('emojis') },
-	{ path: '/search', component: page('search') },
+	{ path: '/search', component: page('search'), props: route => ({ query: route.query.q, channel: route.query.channel }) },
 	{ path: '/pages', name: 'pages', component: page('pages') },
 	{ path: '/pages/new', component: page('page-editor/page-editor') },
 	{ path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
@@ -73,8 +73,6 @@ const defaultRoutes = [
 	{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) },
 	{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) },
 	{ path: '/instance-info/:host', component: page('instance-info'), props: route => ({ host: route.params.host }) },
-	{ path: '/games/reversi', component: page('reversi/index') },
-	{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) },
 	{ path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') },
 	{ path: '/api-console', component: page('api-console') },
 	{ path: '/preview', component: page('preview') },
@@ -117,11 +115,11 @@ export const router = createRouter({
 		window._scroll = () => { // さらにHacky
 			if (to.name === 'index') {
 				window.scroll({ top: indexScrollPos, behavior: 'instant' });
-				const i = setInterval(() => {
+				const i = window.setInterval(() => {
 					window.scroll({ top: indexScrollPos, behavior: 'instant' });
 				}, 10);
-				setTimeout(() => {
-					clearInterval(i);
+				window.setTimeout(() => {
+					window.clearInterval(i);
 				}, 500);
 			} else {
 				window.scroll({ top: 0, behavior: 'instant' });
diff --git a/packages/client/src/scripts/autocomplete.ts b/packages/client/src/scripts/autocomplete.ts
index f2d5806484..f4a3a4c0fc 100644
--- a/packages/client/src/scripts/autocomplete.ts
+++ b/packages/client/src/scripts/autocomplete.ts
@@ -1,4 +1,4 @@
-import { Ref, ref } from 'vue';
+import { nextTick, Ref, ref } from 'vue';
 import * as getCaretCoordinates from 'textarea-caret';
 import { toASCII } from 'punycode/';
 import { popup } from '@/os';
@@ -10,26 +10,23 @@ export class Autocomplete {
 		q: Ref<string | null>;
 		close: Function;
 	} | null;
-	private textarea: any;
-	private vm: any;
+	private textarea: HTMLInputElement | HTMLTextAreaElement;
 	private currentType: string;
-	private opts: {
-		model: string;
-	};
+	private textRef: Ref<string>;
 	private opening: boolean;
 
 	private get text(): string {
-		return this.vm[this.opts.model];
+		return this.textRef.value;
 	}
 
 	private set text(text: string) {
-		this.vm[this.opts.model] = text;
+		this.textRef.value = text;
 	}
 
 	/**
 	 * 対象のテキストエリアを与えてインスタンスを初期化します。
 	 */
-	constructor(textarea, vm, opts) {
+	constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) {
 		//#region BIND
 		this.onInput = this.onInput.bind(this);
 		this.complete = this.complete.bind(this);
@@ -38,8 +35,7 @@ export class Autocomplete {
 
 		this.suggestion = null;
 		this.textarea = textarea;
-		this.vm = vm;
-		this.opts = opts;
+		this.textRef = textRef;
 		this.opening = false;
 
 		this.attach();
@@ -218,7 +214,7 @@ export class Autocomplete {
 			this.text = `${trimmedBefore}@${acct} ${after}`;
 
 			// キャレットを戻す
-			this.vm.$nextTick(() => {
+			nextTick(() => {
 				this.textarea.focus();
 				const pos = trimmedBefore.length + (acct.length + 2);
 				this.textarea.setSelectionRange(pos, pos);
@@ -234,7 +230,7 @@ export class Autocomplete {
 			this.text = `${trimmedBefore}#${value} ${after}`;
 
 			// キャレットを戻す
-			this.vm.$nextTick(() => {
+			nextTick(() => {
 				this.textarea.focus();
 				const pos = trimmedBefore.length + (value.length + 2);
 				this.textarea.setSelectionRange(pos, pos);
@@ -250,7 +246,7 @@ export class Autocomplete {
 			this.text = trimmedBefore + value + after;
 
 			// キャレットを戻す
-			this.vm.$nextTick(() => {
+			nextTick(() => {
 				this.textarea.focus();
 				const pos = trimmedBefore.length + value.length;
 				this.textarea.setSelectionRange(pos, pos);
@@ -266,7 +262,7 @@ export class Autocomplete {
 			this.text = `${trimmedBefore}$[${value} ]${after}`;
 
 			// キャレットを戻す
-			this.vm.$nextTick(() => {
+			nextTick(() => {
 				this.textarea.focus();
 				const pos = trimmedBefore.length + (value.length + 3);
 				this.textarea.setSelectionRange(pos, pos);
diff --git a/packages/client/src/scripts/check-word-mute.ts b/packages/client/src/scripts/check-word-mute.ts
index 3b1fa75b1e..55637bb3b3 100644
--- a/packages/client/src/scripts/check-word-mute.ts
+++ b/packages/client/src/scripts/check-word-mute.ts
@@ -1,4 +1,4 @@
-export async function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: string[][]): Promise<boolean> {
+export function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: string[][]): boolean {
 	// 自分自身
 	if (me && (note.userId === me.id)) return false;
 
diff --git a/packages/client/src/scripts/emojilist.ts b/packages/client/src/scripts/emojilist.ts
index de7591f5a0..bd8689e4f8 100644
--- a/packages/client/src/scripts/emojilist.ts
+++ b/packages/client/src/scripts/emojilist.ts
@@ -1,7 +1,11 @@
-// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
-export const emojilist = require('../emojilist.json') as {
+export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'] as const;
+
+export type UnicodeEmojiDef = {
 	name: string;
 	keywords: string[];
 	char: string;
-	category: 'people' | 'animals_and_nature' | 'food_and_drink' | 'activity' | 'travel_and_places' | 'objects' | 'symbols' | 'flags';
-}[];
+	category: typeof unicodeEmojiCategories[number];
+}
+
+// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
+export const emojilist = require('../emojilist.json') as UnicodeEmojiDef[];
diff --git a/packages/client/src/scripts/form.ts b/packages/client/src/scripts/form.ts
index 7bf6cec452..7f321cc0ae 100644
--- a/packages/client/src/scripts/form.ts
+++ b/packages/client/src/scripts/form.ts
@@ -21,11 +21,39 @@ export type FormItem = {
 	default: string | null;
 	hidden?: boolean;
 	enum: string[];
+} | {
+	label?: string;
+	type: 'radio';
+	default: unknown | null;
+	hidden?: boolean;
+	options: {
+		label: string;
+		value: unknown;
+	}[];
+} | {
+	label?: string;
+	type: 'object';
+	default: Record<string, unknown> | null;
+	hidden: true;
 } | {
 	label?: string;
 	type: 'array';
 	default: unknown[] | null;
-	hidden?: boolean;
+	hidden: true;
 };
 
 export type Form = Record<string, FormItem>;
+
+type GetItemType<Item extends FormItem> =
+	Item['type'] extends 'string' ? string :
+	Item['type'] extends 'number' ? number :
+	Item['type'] extends 'boolean' ? boolean :
+	Item['type'] extends 'radio' ? unknown :
+	Item['type'] extends 'enum' ? string :
+	Item['type'] extends 'array' ? unknown[] :
+	Item['type'] extends 'object' ? Record<string, unknown>
+	: never;
+
+export type GetFormResultType<F extends Form> = {
+	[P in keyof F]: GetItemType<F[P]>;
+};
diff --git a/packages/client/src/scripts/games/reversi/core.ts b/packages/client/src/scripts/games/reversi/core.ts
deleted file mode 100644
index 0cb8922e19..0000000000
--- a/packages/client/src/scripts/games/reversi/core.ts
+++ /dev/null
@@ -1,263 +0,0 @@
-import { count, concat } from '@/scripts/array';
-
-// MISSKEY REVERSI ENGINE
-
-/**
- * true ... 黒
- * false ... 白
- */
-export type Color = boolean;
-const BLACK = true;
-const WHITE = false;
-
-export type MapPixel = 'null' | 'empty';
-
-export type Options = {
-	isLlotheo: boolean;
-	canPutEverywhere: boolean;
-	loopedBoard: boolean;
-};
-
-export type Undo = {
-	/**
-	 * 色
-	 */
-	color: Color;
-
-	/**
-	 * どこに打ったか
-	 */
-	pos: number;
-
-	/**
-	 * 反転した石の位置の配列
-	 */
-	effects: number[];
-
-	/**
-	 * ターン
-	 */
-	turn: Color | null;
-};
-
-/**
- * リバーシエンジン
- */
-export default class Reversi {
-	public map: MapPixel[];
-	public mapWidth: number;
-	public mapHeight: number;
-	public board: (Color | null | undefined)[];
-	public turn: Color | null = BLACK;
-	public opts: Options;
-
-	public prevPos = -1;
-	public prevColor: Color | null = null;
-
-	private logs: Undo[] = [];
-
-	/**
-	 * ゲームを初期化します
-	 */
-	constructor(map: string[], opts: Options) {
-		//#region binds
-		this.put = this.put.bind(this);
-		//#endregion
-
-		//#region Options
-		this.opts = opts;
-		if (this.opts.isLlotheo == null) this.opts.isLlotheo = false;
-		if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false;
-		if (this.opts.loopedBoard == null) this.opts.loopedBoard = false;
-		//#endregion
-
-		//#region Parse map data
-		this.mapWidth = map[0].length;
-		this.mapHeight = map.length;
-		const mapData = map.join('');
-
-		this.board = mapData.split('').map(d => d === '-' ? null : d === 'b' ? BLACK : d === 'w' ? WHITE : undefined);
-
-		this.map = mapData.split('').map(d => d === '-' || d === 'b' || d === 'w' ? 'empty' : 'null');
-		//#endregion
-
-		// ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある
-		if (!this.canPutSomewhere(BLACK))
-			this.turn = this.canPutSomewhere(WHITE) ? WHITE : null;
-	}
-
-	/**
-	 * 黒石の数
-	 */
-	public get blackCount() {
-		return count(BLACK, this.board);
-	}
-
-	/**
-	 * 白石の数
-	 */
-	public get whiteCount() {
-		return count(WHITE, this.board);
-	}
-
-	public transformPosToXy(pos: number): number[] {
-		const x = pos % this.mapWidth;
-		const y = Math.floor(pos / this.mapWidth);
-		return [x, y];
-	}
-
-	public transformXyToPos(x: number, y: number): number {
-		return x + (y * this.mapWidth);
-	}
-
-	/**
-	 * 指定のマスに石を打ちます
-	 * @param color 石の色
-	 * @param pos 位置
-	 */
-	public put(color: Color, pos: number) {
-		this.prevPos = pos;
-		this.prevColor = color;
-
-		this.board[pos] = color;
-
-		// 反転させられる石を取得
-		const effects = this.effects(color, pos);
-
-		// 反転させる
-		for (const pos of effects) {
-			this.board[pos] = color;
-		}
-
-		const turn = this.turn;
-
-		this.logs.push({
-			color,
-			pos,
-			effects,
-			turn
-		});
-
-		this.calcTurn();
-	}
-
-	private calcTurn() {
-		// ターン計算
-		this.turn =
-			this.canPutSomewhere(!this.prevColor) ? !this.prevColor :
-			this.canPutSomewhere(this.prevColor!) ? this.prevColor :
-			null;
-	}
-
-	public undo() {
-		const undo = this.logs.pop()!;
-		this.prevColor = undo.color;
-		this.prevPos = undo.pos;
-		this.board[undo.pos] = null;
-		for (const pos of undo.effects) {
-			const color = this.board[pos];
-			this.board[pos] = !color;
-		}
-		this.turn = undo.turn;
-	}
-
-	/**
-	 * 指定した位置のマップデータのマスを取得します
-	 * @param pos 位置
-	 */
-	public mapDataGet(pos: number): MapPixel {
-		const [x, y] = this.transformPosToXy(pos);
-		return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? 'null' : this.map[pos];
-	}
-
-	/**
-	 * 打つことができる場所を取得します
-	 */
-	public puttablePlaces(color: Color): number[] {
-		return Array.from(this.board.keys()).filter(i => this.canPut(color, i));
-	}
-
-	/**
-	 * 打つことができる場所があるかどうかを取得します
-	 */
-	public canPutSomewhere(color: Color): boolean {
-		return this.puttablePlaces(color).length > 0;
-	}
-
-	/**
-	 * 指定のマスに石を打つことができるかどうかを取得します
-	 * @param color 自分の色
-	 * @param pos 位置
-	 */
-	public canPut(color: Color, pos: number): boolean {
-		return (
-			this.board[pos] !== null ? false : // 既に石が置いてある場所には打てない
-			this.opts.canPutEverywhere ? this.mapDataGet(pos) == 'empty' : // 挟んでなくても置けるモード
-			this.effects(color, pos).length !== 0); // 相手の石を1つでも反転させられるか
-	}
-
-	/**
-	 * 指定のマスに石を置いた時の、反転させられる石を取得します
-	 * @param color 自分の色
-	 * @param initPos 位置
-	 */
-	public effects(color: Color, initPos: number): number[] {
-		const enemyColor = !color;
-
-		const diffVectors: [number, number][] = [
-			[  0,  -1], // 上
-			[ +1,  -1], // 右上
-			[ +1,   0], // 右
-			[ +1,  +1], // 右下
-			[  0,  +1], // 下
-			[ -1,  +1], // 左下
-			[ -1,   0], // 左
-			[ -1,  -1]  // 左上
-		];
-
-		const effectsInLine = ([dx, dy]: [number, number]): number[] => {
-			const nextPos = (x: number, y: number): [number, number] => [x + dx, y + dy];
-
-			const found: number[] = []; // 挟めるかもしれない相手の石を入れておく配列
-			let [x, y] = this.transformPosToXy(initPos);
-			while (true) {
-				[x, y] = nextPos(x, y);
-
-				// 座標が指し示す位置がボード外に出たとき
-				if (this.opts.loopedBoard && this.transformXyToPos(
-					(x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth),
-					(y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) === initPos)
-						// 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ)
-					return found;
-				else if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight)
-					return []; // 挟めないことが確定 (盤面外に到達)
-
-				const pos = this.transformXyToPos(x, y);
-				if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達)
-				const stone = this.board[pos];
-				if (stone === null) return []; // 挟めないことが確定 (石が置かれていないマスに到達)
-				if (stone === enemyColor) found.push(pos); // 挟めるかもしれない (相手の石を発見)
-				if (stone === color) return found; // 挟めることが確定 (対となる自分の石を発見)
-			}
-		};
-
-		return concat(diffVectors.map(effectsInLine));
-	}
-
-	/**
-	 * ゲームが終了したか否か
-	 */
-	public get isEnded(): boolean {
-		return this.turn === null;
-	}
-
-	/**
-	 * ゲームの勝者 (null = 引き分け)
-	 */
-	public get winner(): Color | null {
-		return this.isEnded ?
-			this.blackCount == this.whiteCount ? null :
-			this.opts.isLlotheo === this.blackCount > this.whiteCount ? WHITE : BLACK :
-			undefined as never;
-	}
-}
diff --git a/packages/client/src/scripts/games/reversi/maps.ts b/packages/client/src/scripts/games/reversi/maps.ts
deleted file mode 100644
index dc0d1bf9d0..0000000000
--- a/packages/client/src/scripts/games/reversi/maps.ts
+++ /dev/null
@@ -1,896 +0,0 @@
-/**
- * 組み込みマップ定義
- *
- * データ値:
- * (スペース) ... マス無し
- * - ... マス
- * b ... 初期配置される黒石
- * w ... 初期配置される白石
- */
-
-export type Map = {
-	name?: string;
-	category?: string;
-	author?: string;
-	data: string[];
-};
-
-export const fourfour: Map = {
-	name: '4x4',
-	category: '4x4',
-	data: [
-		'----',
-		'-wb-',
-		'-bw-',
-		'----'
-	]
-};
-
-export const sixsix: Map = {
-	name: '6x6',
-	category: '6x6',
-	data: [
-		'------',
-		'------',
-		'--wb--',
-		'--bw--',
-		'------',
-		'------'
-	]
-};
-
-export const roundedSixsix: Map = {
-	name: '6x6 rounded',
-	category: '6x6',
-	author: 'syuilo',
-	data: [
-		' ---- ',
-		'------',
-		'--wb--',
-		'--bw--',
-		'------',
-		' ---- '
-	]
-};
-
-export const roundedSixsix2: Map = {
-	name: '6x6 rounded 2',
-	category: '6x6',
-	author: 'syuilo',
-	data: [
-		'  --  ',
-		' ---- ',
-		'--wb--',
-		'--bw--',
-		' ---- ',
-		'  --  '
-	]
-};
-
-export const eighteight: Map = {
-	name: '8x8',
-	category: '8x8',
-	data: [
-		'--------',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'--------'
-	]
-};
-
-export const eighteightH1: Map = {
-	name: '8x8 handicap 1',
-	category: '8x8',
-	data: [
-		'b-------',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'--------'
-	]
-};
-
-export const eighteightH2: Map = {
-	name: '8x8 handicap 2',
-	category: '8x8',
-	data: [
-		'b-------',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'-------b'
-	]
-};
-
-export const eighteightH3: Map = {
-	name: '8x8 handicap 3',
-	category: '8x8',
-	data: [
-		'b------b',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'-------b'
-	]
-};
-
-export const eighteightH4: Map = {
-	name: '8x8 handicap 4',
-	category: '8x8',
-	data: [
-		'b------b',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'b------b'
-	]
-};
-
-export const eighteightH28: Map = {
-	name: '8x8 handicap 28',
-	category: '8x8',
-	data: [
-		'bbbbbbbb',
-		'b------b',
-		'b------b',
-		'b--wb--b',
-		'b--bw--b',
-		'b------b',
-		'b------b',
-		'bbbbbbbb'
-	]
-};
-
-export const roundedEighteight: Map = {
-	name: '8x8 rounded',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		' ------ ',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		' ------ '
-	]
-};
-
-export const roundedEighteight2: Map = {
-	name: '8x8 rounded 2',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'  ----  ',
-		' ------ ',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		' ------ ',
-		'  ----  '
-	]
-};
-
-export const roundedEighteight3: Map = {
-	name: '8x8 rounded 3',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'   --   ',
-		'  ----  ',
-		' ------ ',
-		'---wb---',
-		'---bw---',
-		' ------ ',
-		'  ----  ',
-		'   --   '
-	]
-};
-
-export const eighteightWithNotch: Map = {
-	name: '8x8 with notch',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'---  ---',
-		'--------',
-		'--------',
-		' --wb-- ',
-		' --bw-- ',
-		'--------',
-		'--------',
-		'---  ---'
-	]
-};
-
-export const eighteightWithSomeHoles: Map = {
-	name: '8x8 with some holes',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'--- ----',
-		'----- --',
-		'-- -----',
-		'---wb---',
-		'---bw- -',
-		' -------',
-		'--- ----',
-		'--------'
-	]
-};
-
-export const circle: Map = {
-	name: 'Circle',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'   --   ',
-		' ------ ',
-		' ------ ',
-		'---wb---',
-		'---bw---',
-		' ------ ',
-		' ------ ',
-		'   --   '
-	]
-};
-
-export const smile: Map = {
-	name: 'Smile',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		' ------ ',
-		'--------',
-		'-- -- --',
-		'---wb---',
-		'-- bw --',
-		'---  ---',
-		'--------',
-		' ------ '
-	]
-};
-
-export const window: Map = {
-	name: 'Window',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'--------',
-		'-  --  -',
-		'-  --  -',
-		'---wb---',
-		'---bw---',
-		'-  --  -',
-		'-  --  -',
-		'--------'
-	]
-};
-
-export const reserved: Map = {
-	name: 'Reserved',
-	category: '8x8',
-	author: 'Aya',
-	data: [
-		'w------b',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'b------w'
-	]
-};
-
-export const x: Map = {
-	name: 'X',
-	category: '8x8',
-	author: 'Aya',
-	data: [
-		'w------b',
-		'-w----b-',
-		'--w--b--',
-		'---wb---',
-		'---bw---',
-		'--b--w--',
-		'-b----w-',
-		'b------w'
-	]
-};
-
-export const parallel: Map = {
-	name: 'Parallel',
-	category: '8x8',
-	author: 'Aya',
-	data: [
-		'--------',
-		'--------',
-		'--------',
-		'---bb---',
-		'---ww---',
-		'--------',
-		'--------',
-		'--------'
-	]
-};
-
-export const lackOfBlack: Map = {
-	name: 'Lack of Black',
-	category: '8x8',
-	data: [
-		'--------',
-		'--------',
-		'--------',
-		'---w----',
-		'---bw---',
-		'--------',
-		'--------',
-		'--------'
-	]
-};
-
-export const squareParty: Map = {
-	name: 'Square Party',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'--------',
-		'-wwwbbb-',
-		'-w-wb-b-',
-		'-wwwbbb-',
-		'-bbbwww-',
-		'-b-bw-w-',
-		'-bbbwww-',
-		'--------'
-	]
-};
-
-export const minesweeper: Map = {
-	name: 'Minesweeper',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'b-b--w-w',
-		'-w-wb-b-',
-		'w-b--w-b',
-		'-b-wb-w-',
-		'-w-bw-b-',
-		'b-w--b-w',
-		'-b-bw-w-',
-		'w-w--b-b'
-	]
-};
-
-export const tenthtenth: Map = {
-	name: '10x10',
-	category: '10x10',
-	data: [
-		'----------',
-		'----------',
-		'----------',
-		'----------',
-		'----wb----',
-		'----bw----',
-		'----------',
-		'----------',
-		'----------',
-		'----------'
-	]
-};
-
-export const hole: Map = {
-	name: 'The Hole',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'----------',
-		'----------',
-		'--wb--wb--',
-		'--bw--bw--',
-		'----  ----',
-		'----  ----',
-		'--wb--wb--',
-		'--bw--bw--',
-		'----------',
-		'----------'
-	]
-};
-
-export const grid: Map = {
-	name: 'Grid',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'----------',
-		'- - -- - -',
-		'----------',
-		'- - -- - -',
-		'----wb----',
-		'----bw----',
-		'- - -- - -',
-		'----------',
-		'- - -- - -',
-		'----------'
-	]
-};
-
-export const cross: Map = {
-	name: 'Cross',
-	category: '10x10',
-	author: 'Aya',
-	data: [
-		'   ----   ',
-		'   ----   ',
-		'   ----   ',
-		'----------',
-		'----wb----',
-		'----bw----',
-		'----------',
-		'   ----   ',
-		'   ----   ',
-		'   ----   '
-	]
-};
-
-export const charX: Map = {
-	name: 'Char X',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'---    ---',
-		'----  ----',
-		'----------',
-		' -------- ',
-		'  --wb--  ',
-		'  --bw--  ',
-		' -------- ',
-		'----------',
-		'----  ----',
-		'---    ---'
-	]
-};
-
-export const charY: Map = {
-	name: 'Char Y',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'---    ---',
-		'----  ----',
-		'----------',
-		' -------- ',
-		'  --wb--  ',
-		'  --bw--  ',
-		'  ------  ',
-		'  ------  ',
-		'  ------  ',
-		'  ------  '
-	]
-};
-
-export const walls: Map = {
-	name: 'Walls',
-	category: '10x10',
-	author: 'Aya',
-	data: [
-		' bbbbbbbb ',
-		'w--------w',
-		'w--------w',
-		'w--------w',
-		'w---wb---w',
-		'w---bw---w',
-		'w--------w',
-		'w--------w',
-		'w--------w',
-		' bbbbbbbb '
-	]
-};
-
-export const cpu: Map = {
-	name: 'CPU',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		' b b  b b ',
-		'w--------w',
-		' -------- ',
-		'w--------w',
-		' ---wb--- ',
-		' ---bw--- ',
-		'w--------w',
-		' -------- ',
-		'w--------w',
-		' b b  b b '
-	]
-};
-
-export const checker: Map = {
-	name: 'Checker',
-	category: '10x10',
-	author: 'Aya',
-	data: [
-		'----------',
-		'----------',
-		'----------',
-		'---wbwb---',
-		'---bwbw---',
-		'---wbwb---',
-		'---bwbw---',
-		'----------',
-		'----------',
-		'----------'
-	]
-};
-
-export const japaneseCurry: Map = {
-	name: 'Japanese curry',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'w-b-b-b-b-',
-		'-w-b-b-b-b',
-		'w-w-b-b-b-',
-		'-w-w-b-b-b',
-		'w-w-wwb-b-',
-		'-w-wbb-b-b',
-		'w-w-w-b-b-',
-		'-w-w-w-b-b',
-		'w-w-w-w-b-',
-		'-w-w-w-w-b'
-	]
-};
-
-export const mosaic: Map = {
-	name: 'Mosaic',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'- - - - - ',
-		' - - - - -',
-		'- - - - - ',
-		' - w w - -',
-		'- - b b - ',
-		' - w w - -',
-		'- - b b - ',
-		' - - - - -',
-		'- - - - - ',
-		' - - - - -',
-	]
-};
-
-export const arena: Map = {
-	name: 'Arena',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'- - -- - -',
-		' - -  - - ',
-		'- ------ -',
-		' -------- ',
-		'- --wb-- -',
-		'- --bw-- -',
-		' -------- ',
-		'- ------ -',
-		' - -  - - ',
-		'- - -- - -'
-	]
-};
-
-export const reactor: Map = {
-	name: 'Reactor',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'-w------b-',
-		'b- -  - -w',
-		'- --wb-- -',
-		'---b  w---',
-		'- b wb w -',
-		'- w bw b -',
-		'---w  b---',
-		'- --bw-- -',
-		'w- -  - -b',
-		'-b------w-'
-	]
-};
-
-export const sixeight: Map = {
-	name: '6x8',
-	category: 'Special',
-	data: [
-		'------',
-		'------',
-		'------',
-		'--wb--',
-		'--bw--',
-		'------',
-		'------',
-		'------'
-	]
-};
-
-export const spark: Map = {
-	name: 'Spark',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		' -      - ',
-		'----------',
-		' -------- ',
-		' -------- ',
-		' ---wb--- ',
-		' ---bw--- ',
-		' -------- ',
-		' -------- ',
-		'----------',
-		' -      - '
-	]
-};
-
-export const islands: Map = {
-	name: 'Islands',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		'--------  ',
-		'---wb---  ',
-		'---bw---  ',
-		'--------  ',
-		'  -    -  ',
-		'  -    -  ',
-		'  --------',
-		'  --------',
-		'  --------',
-		'  --------'
-	]
-};
-
-export const galaxy: Map = {
-	name: 'Galaxy',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		'   ------   ',
-		'  --www---  ',
-		' ------w--- ',
-		'---bbb--w---',
-		'--b---b-w-b-',
-		'-b--wwb-w-b-',
-		'-b-w-bww--b-',
-		'-b-w-b---b--',
-		'---w--bbb---',
-		' ---w------ ',
-		'  ---www--  ',
-		'   ------   '
-	]
-};
-
-export const triangle: Map = {
-	name: 'Triangle',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		'    --    ',
-		'    --    ',
-		'   ----   ',
-		'   ----   ',
-		'  --wb--  ',
-		'  --bw--  ',
-		' -------- ',
-		' -------- ',
-		'----------',
-		'----------'
-	]
-};
-
-export const iphonex: Map = {
-	name: 'iPhone X',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		' --  -- ',
-		'--------',
-		'--------',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'--------',
-		'--------',
-		' ------ '
-	]
-};
-
-export const dealWithIt: Map = {
-	name: 'Deal with it!',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		'------------',
-		'--w-b-------',
-		' --b-w------',
-		'  --w-b---- ',
-		'   -------  '
-	]
-};
-
-export const experiment: Map = {
-	name: 'Let\'s experiment',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		' ------------ ',
-		'------wb------',
-		'------bw------',
-		'--------------',
-		'    -    -    ',
-		'------  ------',
-		'bbbbbb  wwwwww',
-		'bbbbbb  wwwwww',
-		'bbbbbb  wwwwww',
-		'bbbbbb  wwwwww',
-		'wwwwww  bbbbbb'
-	]
-};
-
-export const bigBoard: Map = {
-	name: 'Big board',
-	category: 'Special',
-	data: [
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'-------wb-------',
-		'-------bw-------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------'
-	]
-};
-
-export const twoBoard: Map = {
-	name: 'Two board',
-	category: 'Special',
-	author: 'Aya',
-	data: [
-		'-------- --------',
-		'-------- --------',
-		'-------- --------',
-		'---wb--- ---wb---',
-		'---bw--- ---bw---',
-		'-------- --------',
-		'-------- --------',
-		'-------- --------'
-	]
-};
-
-export const test1: Map = {
-	name: 'Test1',
-	category: 'Test',
-	data: [
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------'
-	]
-};
-
-export const test2: Map = {
-	name: 'Test2',
-	category: 'Test',
-	data: [
-		'------',
-		'------',
-		'-b--w-',
-		'-w--b-',
-		'-w--b-'
-	]
-};
-
-export const test3: Map = {
-	name: 'Test3',
-	category: 'Test',
-	data: [
-		'-w-',
-		'--w',
-		'w--',
-		'-w-',
-		'--w',
-		'w--',
-		'-w-',
-		'--w',
-		'w--',
-		'-w-',
-		'---',
-		'b--',
-	]
-};
-
-export const test4: Map = {
-	name: 'Test4',
-	category: 'Test',
-	data: [
-		'-w--b-',
-		'-w--b-',
-		'------',
-		'-w--b-',
-		'-w--b-'
-	]
-};
-
-// 検証用: この盤面で藍(lv3)が黒で始めると何故か(?)A1に打ってしまう
-export const test6: Map = {
-	name: 'Test6',
-	category: 'Test',
-	data: [
-		'--wwwww-',
-		'wwwwwwww',
-		'wbbbwbwb',
-		'wbbbbwbb',
-		'wbwbbwbb',
-		'wwbwbbbb',
-		'--wbbbbb',
-		'-wwwww--',
-	]
-};
-
-// 検証用: この盤面で藍(lv3)が黒で始めると何故か(?)G7に打ってしまう
-export const test7: Map = {
-	name: 'Test7',
-	category: 'Test',
-	data: [
-		'b--w----',
-		'b-wwww--',
-		'bwbwwwbb',
-		'wbwwwwb-',
-		'wwwwwww-',
-		'-wwbbwwb',
-		'--wwww--',
-		'--wwww--',
-	]
-};
-
-// 検証用: この盤面で藍(lv5)が黒で始めると何故か(?)A1に打ってしまう
-export const test8: Map = {
-	name: 'Test8',
-	category: 'Test',
-	data: [
-		'--------',
-		'-----w--',
-		'w--www--',
-		'wwwwww--',
-		'bbbbwww-',
-		'wwwwww--',
-		'--www---',
-		'--ww----',
-	]
-};
diff --git a/packages/client/src/scripts/games/reversi/package.json b/packages/client/src/scripts/games/reversi/package.json
deleted file mode 100644
index a4415ad141..0000000000
--- a/packages/client/src/scripts/games/reversi/package.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-  "name": "misskey-reversi",
-  "version": "0.0.5",
-  "description": "Misskey reversi engine",
-  "keywords": [
-    "misskey"
-  ],
-  "author": "syuilo <i@syuilo.com>",
-  "license": "MIT",
-  "repository": "https://github.com/misskey-dev/misskey.git",
-  "bugs": "https://github.com/misskey-dev/misskey/issues",
-  "main": "./built/core.js",
-  "types": "./built/core.d.ts",
-  "scripts": {
-    "build": "tsc"
-  },
-  "dependencies": {}
-}
diff --git a/packages/client/src/scripts/games/reversi/tsconfig.json b/packages/client/src/scripts/games/reversi/tsconfig.json
deleted file mode 100644
index 851fb6b7e4..0000000000
--- a/packages/client/src/scripts/games/reversi/tsconfig.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
-	"compilerOptions": {
-		"noEmitOnError": false,
-		"noImplicitAny": false,
-		"noImplicitReturns": true,
-		"noFallthroughCasesInSwitch": true,
-		"experimentalDecorators": true,
-		"declaration": true,
-		"sourceMap": false,
-		"target": "es2017",
-		"module": "commonjs",
-		"removeComments": false,
-		"noLib": false,
-		"outDir": "./built",
-		"rootDir": "./"
-	},
-	"compileOnSave": false,
-	"include": [
-		"./core.ts"
-	]
-}
diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts
new file mode 100644
index 0000000000..3634f39632
--- /dev/null
+++ b/packages/client/src/scripts/get-note-menu.ts
@@ -0,0 +1,310 @@
+import { Ref } from 'vue';
+import * as misskey from 'misskey-js';
+import { $i } from '@/account';
+import { i18n } from '@/i18n';
+import { instance } from '@/instance';
+import * as os from '@/os';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import { url } from '@/config';
+import { noteActions } from '@/store';
+import { pleaseLogin } from './please-login';
+
+export function getNoteMenu(props: {
+	note: misskey.entities.Note;
+	menuButton: Ref<HTMLElement>;
+	translation: Ref<any>;
+	translating: Ref<boolean>;
+}) {
+	const isRenote = (
+		props.note.renote != null &&
+		props.note.text == null &&
+		props.note.fileIds.length === 0 &&
+		props.note.poll == null
+	);
+
+	let appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note;
+
+	function del(): void {
+		os.confirm({
+			type: 'warning',
+			text: i18n.locale.noteDeleteConfirm,
+		}).then(({ canceled }) => {
+			if (canceled) return;
+
+			os.api('notes/delete', {
+				noteId: appearNote.id
+			});
+		});
+	}
+
+	function delEdit(): void {
+		os.confirm({
+			type: 'warning',
+			text: i18n.locale.deleteAndEditConfirm,
+		}).then(({ canceled }) => {
+			if (canceled) return;
+
+			os.api('notes/delete', {
+				noteId: appearNote.id
+			});
+
+			os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel });
+		});
+	}
+
+	function toggleFavorite(favorite: boolean): void {
+		os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
+			noteId: appearNote.id
+		});
+	}
+
+	function toggleWatch(watch: boolean): void {
+		os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
+			noteId: appearNote.id
+		});
+	}
+
+	function toggleThreadMute(mute: boolean): void {
+		os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
+			noteId: appearNote.id
+		});
+	}
+
+	function copyContent(): void {
+		copyToClipboard(appearNote.text);
+		os.success();
+	}
+
+	function copyLink(): void {
+		copyToClipboard(`${url}/notes/${appearNote.id}`);
+		os.success();
+	}
+
+	function togglePin(pin: boolean): void {
+		os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
+			noteId: appearNote.id
+		}, undefined, null, e => {
+			if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
+				os.alert({
+					type: 'error',
+					text: i18n.locale.pinLimitExceeded
+				});
+			}
+		});
+	}
+
+	async function clip(): Promise<void> {
+		const clips = await os.api('clips/list');
+		os.popupMenu([{
+			icon: 'fas fa-plus',
+			text: i18n.locale.createNew,
+			action: async () => {
+				const { canceled, result } = await os.form(i18n.locale.createNewClip, {
+					name: {
+						type: 'string',
+						label: i18n.locale.name
+					},
+					description: {
+						type: 'string',
+						required: false,
+						multiline: true,
+						label: i18n.locale.description
+					},
+					isPublic: {
+						type: 'boolean',
+						label: i18n.locale.public,
+						default: false
+					}
+				});
+				if (canceled) return;
+
+				const clip = await os.apiWithDialog('clips/create', result);
+
+				os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
+			}
+		}, null, ...clips.map(clip => ({
+			text: clip.name,
+			action: () => {
+				os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
+			}
+		}))], props.menuButton.value, {
+		}).then(focus);
+	}
+
+	async function promote(): Promise<void> {
+		const { canceled, result: days } = await os.inputNumber({
+			title: i18n.locale.numberOfDays,
+		});
+
+		if (canceled) return;
+
+		os.apiWithDialog('admin/promo/create', {
+			noteId: appearNote.id,
+			expiresAt: Date.now() + (86400000 * days),
+		});
+	}
+
+	function share(): void {
+		navigator.share({
+			title: i18n.t('noteOf', { user: appearNote.user.name }),
+			text: appearNote.text,
+			url: `${url}/notes/${appearNote.id}`,
+		});
+	}
+
+	async function translate(): Promise<void> {
+		if (props.translation.value != null) return;
+		props.translating.value = true;
+		const res = await os.api('notes/translate', {
+			noteId: appearNote.id,
+			targetLang: localStorage.getItem('lang') || navigator.language,
+		});
+		props.translating.value = false;
+		props.translation.value = res;
+	}
+
+	let menu;
+	if ($i) {
+		const statePromise = os.api('notes/state', {
+			noteId: appearNote.id
+		});
+
+		menu = [{
+			icon: 'fas fa-copy',
+			text: i18n.locale.copyContent,
+			action: copyContent
+		}, {
+			icon: 'fas fa-link',
+			text: i18n.locale.copyLink,
+			action: copyLink
+		}, (appearNote.url || appearNote.uri) ? {
+			icon: 'fas fa-external-link-square-alt',
+			text: i18n.locale.showOnRemote,
+			action: () => {
+				window.open(appearNote.url || appearNote.uri, '_blank');
+			}
+		} : undefined,
+		{
+			icon: 'fas fa-share-alt',
+			text: i18n.locale.share,
+			action: share
+		},
+		instance.translatorAvailable ? {
+			icon: 'fas fa-language',
+			text: i18n.locale.translate,
+			action: translate
+		} : undefined,
+		null,
+		statePromise.then(state => state.isFavorited ? {
+			icon: 'fas fa-star',
+			text: i18n.locale.unfavorite,
+			action: () => toggleFavorite(false)
+		} : {
+			icon: 'fas fa-star',
+			text: i18n.locale.favorite,
+			action: () => toggleFavorite(true)
+		}),
+		{
+			icon: 'fas fa-paperclip',
+			text: i18n.locale.clip,
+			action: () => clip()
+		},
+		(appearNote.userId != $i.id) ? statePromise.then(state => state.isWatching ? {
+			icon: 'fas fa-eye-slash',
+			text: i18n.locale.unwatch,
+			action: () => toggleWatch(false)
+		} : {
+			icon: 'fas fa-eye',
+			text: i18n.locale.watch,
+			action: () => toggleWatch(true)
+		}) : undefined,
+		statePromise.then(state => state.isMutedThread ? {
+			icon: 'fas fa-comment-slash',
+			text: i18n.locale.unmuteThread,
+			action: () => toggleThreadMute(false)
+		} : {
+			icon: 'fas fa-comment-slash',
+			text: i18n.locale.muteThread,
+			action: () => toggleThreadMute(true)
+		}),
+		appearNote.userId == $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? {
+			icon: 'fas fa-thumbtack',
+			text: i18n.locale.unpin,
+			action: () => togglePin(false)
+		} : {
+			icon: 'fas fa-thumbtack',
+			text: i18n.locale.pin,
+			action: () => togglePin(true)
+		} : undefined,
+		/*
+		...($i.isModerator || $i.isAdmin ? [
+			null,
+			{
+				icon: 'fas fa-bullhorn',
+				text: i18n.locale.promote,
+				action: promote
+			}]
+			: []
+		),*/
+		...(appearNote.userId != $i.id ? [
+			null,
+			{
+				icon: 'fas fa-exclamation-circle',
+				text: i18n.locale.reportAbuse,
+				action: () => {
+					const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`;
+					os.popup(import('@/components/abuse-report-window.vue'), {
+						user: appearNote.user,
+						initialComment: `Note: ${u}\n-----\n`
+					}, {}, 'closed');
+				}
+			}]
+			: []
+		),
+		...(appearNote.userId == $i.id || $i.isModerator || $i.isAdmin ? [
+			null,
+			appearNote.userId == $i.id ? {
+				icon: 'fas fa-edit',
+				text: i18n.locale.deleteAndEdit,
+				action: delEdit
+			} : undefined,
+			{
+				icon: 'fas fa-trash-alt',
+				text: i18n.locale.delete,
+				danger: true,
+				action: del
+			}]
+			: []
+		)]
+		.filter(x => x !== undefined);
+	} else {
+		menu = [{
+			icon: 'fas fa-copy',
+			text: i18n.locale.copyContent,
+			action: copyContent
+		}, {
+			icon: 'fas fa-link',
+			text: i18n.locale.copyLink,
+			action: copyLink
+		}, (appearNote.url || appearNote.uri) ? {
+			icon: 'fas fa-external-link-square-alt',
+			text: i18n.locale.showOnRemote,
+			action: () => {
+				window.open(appearNote.url || appearNote.uri, '_blank');
+			}
+		} : undefined]
+		.filter(x => x !== undefined);
+	}
+
+	if (noteActions.length > 0) {
+		menu = menu.concat([null, ...noteActions.map(action => ({
+			icon: 'fas fa-plug',
+			text: action.title,
+			action: () => {
+				action.handler(appearNote);
+			}
+		}))]);
+	}
+
+	return menu;
+}
diff --git a/packages/client/src/scripts/get-user-menu.ts b/packages/client/src/scripts/get-user-menu.ts
index ebe101bc0f..7b910a0083 100644
--- a/packages/client/src/scripts/get-user-menu.ts
+++ b/packages/client/src/scripts/get-user-menu.ts
@@ -5,7 +5,7 @@ import * as Acct from 'misskey-js/built/acct';
 import * as os from '@/os';
 import { userActions } from '@/store';
 import { router } from '@/router';
-import { $i } from '@/account';
+import { $i, iAmModerator } from '@/account';
 
 export function getUserMenu(user) {
 	const meId = $i ? $i.id : null;
@@ -175,7 +175,7 @@ export function getUserMenu(user) {
 			action: reportAbuse
 		}]);
 
-		if ($i && ($i.isAdmin || $i.isModerator)) {
+		if (iAmModerator) {
 			menu = menu.concat([null, {
 				icon: 'fas fa-microphone-slash',
 				text: user.isSilenced ? i18n.locale.unsilence : i18n.locale.silence,
diff --git a/packages/client/src/scripts/paging.ts b/packages/client/src/scripts/paging.ts
deleted file mode 100644
index ef63ecc450..0000000000
--- a/packages/client/src/scripts/paging.ts
+++ /dev/null
@@ -1,246 +0,0 @@
-import { markRaw } from 'vue';
-import * as os from '@/os';
-import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from './scroll';
-
-const SECOND_FETCH_LIMIT = 30;
-
-// reversed: items 配列の中身を逆順にする(新しい方が最後)
-
-export default (opts) => ({
-	emits: ['queue'],
-
-	data() {
-		return {
-			items: [],
-			queue: [],
-			offset: 0,
-			fetching: true,
-			moreFetching: false,
-			inited: false,
-			more: false,
-			backed: false, // 遡り中か否か
-			isBackTop: false,
-		};
-	},
-
-	computed: {
-		empty(): boolean {
-			return this.items.length === 0 && !this.fetching && this.inited;
-		},
-
-		error(): boolean {
-			return !this.fetching && !this.inited;
-		},
-	},
-
-	watch: {
-		pagination: {
-			handler() {
-				this.init();
-			},
-			deep: true
-		},
-
-		queue: {
-			handler(a, b) {
-				if (a.length === 0 && b.length === 0) return;
-				this.$emit('queue', this.queue.length);
-			},
-			deep: true
-		}
-	},
-
-	created() {
-		opts.displayLimit = opts.displayLimit || 30;
-		this.init();
-	},
-
-	activated() {
-		this.isBackTop = false;
-	},
-
-	deactivated() {
-		this.isBackTop = window.scrollY === 0;
-	},
-
-	methods: {
-		reload() {
-			this.items = [];
-			this.init();
-		},
-
-		replaceItem(finder, data) {
-			const i = this.items.findIndex(finder);
-			this.items[i] = data;
-		},
-
-		removeItem(finder) {
-			const i = this.items.findIndex(finder);
-			this.items.splice(i, 1);
-		},
-
-		async init() {
-			this.queue = [];
-			this.fetching = true;
-			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;
-			if (params === null) return;
-			const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
-			await os.api(endpoint, {
-				...params,
-				limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
-			}).then(items => {
-				for (let i = 0; i < items.length; i++) {
-					const item = items[i];
-					markRaw(item);
-					if (this.pagination.reversed) {
-						if (i === items.length - 2) item._shouldInsertAd_ = true;
-					} else {
-						if (i === 3) item._shouldInsertAd_ = true;
-					}
-				}
-				if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) {
-					items.pop();
-					this.items = this.pagination.reversed ? [...items].reverse() : items;
-					this.more = true;
-				} else {
-					this.items = this.pagination.reversed ? [...items].reverse() : items;
-					this.more = false;
-				}
-				this.offset = items.length;
-				this.inited = true;
-				this.fetching = false;
-				if (opts.after) opts.after(this, null);
-			}, e => {
-				this.fetching = false;
-				if (opts.after) opts.after(this, e);
-			});
-		},
-
-		async fetchMore() {
-			if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return;
-			this.moreFetching = true;
-			this.backed = true;
-			let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params;
-			if (params && params.then) params = await params;
-			const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
-			await os.api(endpoint, {
-				...params,
-				limit: SECOND_FETCH_LIMIT + 1,
-				...(this.pagination.offsetMode ? {
-					offset: this.offset,
-				} : {
-					untilId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id,
-				}),
-			}).then(items => {
-				for (let i = 0; i < items.length; i++) {
-					const item = items[i];
-					markRaw(item);
-					if (this.pagination.reversed) {
-						if (i === items.length - 9) item._shouldInsertAd_ = true;
-					} else {
-						if (i === 10) item._shouldInsertAd_ = true;
-					}
-				}
-				if (items.length > SECOND_FETCH_LIMIT) {
-					items.pop();
-					this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
-					this.more = true;
-				} else {
-					this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
-					this.more = false;
-				}
-				this.offset += items.length;
-				this.moreFetching = false;
-			}, e => {
-				this.moreFetching = false;
-			});
-		},
-
-		async fetchMoreFeature() {
-			if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return;
-			this.moreFetching = true;
-			let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params;
-			if (params && params.then) params = await params;
-			const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
-			await os.api(endpoint, {
-				...params,
-				limit: SECOND_FETCH_LIMIT + 1,
-				...(this.pagination.offsetMode ? {
-					offset: this.offset,
-				} : {
-					sinceId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id,
-				}),
-			}).then(items => {
-				for (const item of items) {
-					markRaw(item);
-				}
-				if (items.length > SECOND_FETCH_LIMIT) {
-					items.pop();
-					this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
-					this.more = true;
-				} else {
-					this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
-					this.more = false;
-				}
-				this.offset += items.length;
-				this.moreFetching = false;
-			}, e => {
-				this.moreFetching = false;
-			});
-		},
-
-		prepend(item) {
-			if (this.pagination.reversed) {
-				const container = getScrollContainer(this.$el);
-				const pos = getScrollPosition(this.$el);
-				const viewHeight = container.clientHeight;
-				const height = container.scrollHeight;
-				const isBottom = (pos + viewHeight > height - 32);
-				if (isBottom) {
-					// オーバーフローしたら古いアイテムは捨てる
-					if (this.items.length >= opts.displayLimit) {
-						// このやり方だとVue 3.2以降アニメーションが動かなくなる
-						//this.items = this.items.slice(-opts.displayLimit);
-						while (this.items.length >= opts.displayLimit) {
-							this.items.shift();
-						}
-						this.more = true;
-					}
-				}
-				this.items.push(item);
-				// TODO
-			} else {
-				const isTop = this.isBackTop || (document.body.contains(this.$el) && isTopVisible(this.$el));
-
-				if (isTop) {
-					// Prepend the item
-					this.items.unshift(item);
-
-					// オーバーフローしたら古いアイテムは捨てる
-					if (this.items.length >= opts.displayLimit) {
-						// このやり方だとVue 3.2以降アニメーションが動かなくなる
-						//this.items = this.items.slice(0, opts.displayLimit);
-						while (this.items.length >= opts.displayLimit) {
-							this.items.pop();
-						}
-						this.more = true;
-					}
-				} else {
-					this.queue.push(item);
-					onScrollTop(this.$el, () => {
-						for (const item of this.queue) {
-							this.prepend(item);
-						}
-						this.queue = [];
-					});
-				}
-			}
-		},
-
-		append(item) {
-			this.items.push(item);
-		},
-	}
-});
diff --git a/packages/client/src/scripts/physics.ts b/packages/client/src/scripts/physics.ts
index 445b6296eb..36e476b6f9 100644
--- a/packages/client/src/scripts/physics.ts
+++ b/packages/client/src/scripts/physics.ts
@@ -136,7 +136,7 @@ export function physics(container: HTMLElement) {
 	}
 
 	// 奈落に落ちたオブジェクトは消す
-	const intervalId = setInterval(() => {
+	const intervalId = window.setInterval(() => {
 		for (const obj of objs) {
 			if (obj.position.y > (containerHeight + 1024)) Matter.World.remove(world, obj);
 		}
@@ -146,7 +146,7 @@ export function physics(container: HTMLElement) {
 		stop: () => {
 			stop = true;
 			Matter.Runner.stop(runner);
-			clearInterval(intervalId);
+			window.clearInterval(intervalId);
 		}
 	};
 }
diff --git a/packages/client/src/scripts/theme.ts b/packages/client/src/scripts/theme.ts
index 3b7f003d0f..85c087331b 100644
--- a/packages/client/src/scripts/theme.ts
+++ b/packages/client/src/scripts/theme.ts
@@ -34,11 +34,11 @@ export const builtinThemes = [
 let timeout = null;
 
 export function applyTheme(theme: Theme, persist = true) {
-	if (timeout) clearTimeout(timeout);
+	if (timeout) window.clearTimeout(timeout);
 
 	document.documentElement.classList.add('_themeChanging_');
 
-	timeout = setTimeout(() => {
+	timeout = window.setTimeout(() => {
 		document.documentElement.classList.remove('_themeChanging_');
 	}, 1000);
 
diff --git a/packages/client/src/scripts/touch.ts b/packages/client/src/scripts/touch.ts
index 06b4f8b2ed..5251bc2e27 100644
--- a/packages/client/src/scripts/touch.ts
+++ b/packages/client/src/scripts/touch.ts
@@ -14,6 +14,10 @@ if (isTouchSupported) {
 	}, { passive: true });
 	
 	window.addEventListener('touchend', () => {
+		// 子要素のtouchstartイベントでstopPropagation()が呼ばれると親要素に伝搬されずタッチされたと判定されないため、
+		// touchendイベントでもtouchstartイベントと同様にtrueにする
+		isTouchUsing = true;
+
 		isScreenTouching = false;
 	}, { passive: true });
 }
diff --git a/packages/client/src/scripts/use-leave-guard.ts b/packages/client/src/scripts/use-leave-guard.ts
new file mode 100644
index 0000000000..3984256251
--- /dev/null
+++ b/packages/client/src/scripts/use-leave-guard.ts
@@ -0,0 +1,46 @@
+import { inject, onUnmounted, Ref } from 'vue';
+import { onBeforeRouteLeave } from 'vue-router';
+import { i18n } from '@/i18n';
+import * as os from '@/os';
+
+export function useLeaveGuard(enabled: Ref<boolean>) {
+	const setLeaveGuard = inject('setLeaveGuard');
+
+	if (setLeaveGuard) {
+		setLeaveGuard(async () => {
+			if (!enabled.value) return false;
+
+			const { canceled } = await os.confirm({
+				type: 'warning',
+				text: i18n.locale.leaveConfirm,
+			});
+
+			return canceled;
+		});
+	} else {
+		onBeforeRouteLeave(async (to, from) => {
+			if (!enabled.value) return true;
+
+			const { canceled } = await os.confirm({
+				type: 'warning',
+				text: i18n.locale.leaveConfirm,
+			});
+
+			return !canceled;
+		});
+	}
+
+	/*
+	function onBeforeLeave(ev: BeforeUnloadEvent) {
+		if (enabled.value) {
+			ev.preventDefault();
+			ev.returnValue = '';
+		}
+	}
+
+	window.addEventListener('beforeunload', onBeforeLeave);
+	onUnmounted(() => {
+		window.removeEventListener('beforeunload', onBeforeLeave);
+	});
+	*/
+}
diff --git a/packages/client/src/scripts/use-note-capture.ts b/packages/client/src/scripts/use-note-capture.ts
new file mode 100644
index 0000000000..bb00e464e3
--- /dev/null
+++ b/packages/client/src/scripts/use-note-capture.ts
@@ -0,0 +1,123 @@
+import { onUnmounted, Ref } from 'vue';
+import * as misskey from 'misskey-js';
+import { stream } from '@/stream';
+import { $i } from '@/account';
+
+export function useNoteCapture(props: {
+	rootEl: Ref<HTMLElement>;
+	appearNote: Ref<misskey.entities.Note>;
+}) {
+	const appearNote = props.appearNote;
+	const connection = $i ? stream : null;
+
+	function onStreamNoteUpdated(data): void {
+		const { type, id, body } = data;
+
+		if (id !== appearNote.value.id) return;
+
+		switch (type) {
+			case 'reacted': {
+				const reaction = body.reaction;
+
+				const updated = JSON.parse(JSON.stringify(appearNote.value));
+
+				if (body.emoji) {
+					const emojis = appearNote.value.emojis || [];
+					if (!emojis.includes(body.emoji)) {
+						updated.emojis = [...emojis, body.emoji];
+					}
+				}
+
+				// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
+				const currentCount = (appearNote.value.reactions || {})[reaction] || 0;
+
+				updated.reactions[reaction] = currentCount + 1;
+
+				if ($i && (body.userId === $i.id)) {
+					updated.myReaction = reaction;
+				}
+
+				appearNote.value = updated;
+				break;
+			}
+
+			case 'unreacted': {
+				const reaction = body.reaction;
+
+				const updated = JSON.parse(JSON.stringify(appearNote.value));
+
+				// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
+				const currentCount = (appearNote.value.reactions || {})[reaction] || 0;
+
+				updated.reactions[reaction] = Math.max(0, currentCount - 1);
+
+				if ($i && (body.userId === $i.id)) {
+					updated.myReaction = null;
+				}
+
+				appearNote.value = updated;
+				break;
+			}
+
+			case 'pollVoted': {
+				const choice = body.choice;
+
+				const updated = JSON.parse(JSON.stringify(appearNote.value));
+
+				const choices = [...appearNote.value.poll.choices];
+				choices[choice] = {
+					...choices[choice],
+					votes: choices[choice].votes + 1,
+					...($i && (body.userId === $i.id) ? {
+						isVoted: true
+					} : {})
+				};
+
+				updated.poll.choices = choices;
+
+				appearNote.value = updated;
+				break;
+			}
+
+			case 'deleted': {
+				const updated = JSON.parse(JSON.stringify(appearNote.value));
+				updated.value = true;
+				appearNote.value = updated;
+				break;
+			}
+		}
+	}
+
+	function capture(withHandler = false): void {
+		if (connection) {
+			// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
+			connection.send(document.body.contains(props.rootEl.value) ? 'sr' : 's', { id: appearNote.value.id });
+			if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated);
+		}
+	}
+
+	function decapture(withHandler = false): void {
+		if (connection) {
+			connection.send('un', {
+				id: appearNote.value.id,
+			});
+			if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated);
+		}
+	}
+
+	function onStreamConnected() {
+		capture(false);
+	}
+	
+	capture(true);
+	if (connection) {
+		connection.on('_connected_', onStreamConnected);
+	}
+	
+	onUnmounted(() => {
+		decapture(true);
+		if (connection) {
+			connection.off('_connected_', onStreamConnected);
+		}
+	});
+}
diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts
index f2732c57d3..cd358d29d0 100644
--- a/packages/client/src/store.ts
+++ b/packages/client/src/store.ts
@@ -97,7 +97,7 @@ export const defaultStore = markRaw(new Storage('base', {
 	tl: {
 		where: 'deviceAccount',
 		default: {
-			src: 'home',
+			src: 'home' as 'home' | 'local' | 'social' | 'global',
 			arg: null
 		}
 	},
@@ -160,7 +160,7 @@ export const defaultStore = markRaw(new Storage('base', {
 	},
 	useReactionPickerForContextMenu: {
 		where: 'device',
-		default: true
+		default: false
 	},
 	showGapBetweenNotesInTimeline: {
 		where: 'device',
@@ -255,8 +255,6 @@ export class ColdDeviceStorage {
 		sound_chatBg: { type: 'syuilo/waon', volume: 1 },
 		sound_antenna: { type: 'syuilo/triple', volume: 1 },
 		sound_channel: { type: 'syuilo/square-pico', volume: 1 },
-		sound_reversiPutBlack: { type: 'syuilo/kick', volume: 0.3 },
-		sound_reversiPutWhite: { type: 'syuilo/snare', volume: 0.3 },
 	};
 
 	public static watchers = [];
diff --git a/packages/client/src/style.scss b/packages/client/src/style.scss
index b95a5c3950..c1d47ffd08 100644
--- a/packages/client/src/style.scss
+++ b/packages/client/src/style.scss
@@ -26,6 +26,7 @@ html {
 	background-size: cover;
 	background-position: center;
 	color: var(--fg);
+	accent-color: var(--accent);
 	overflow: auto;
 	overflow-wrap: break-word;
 	font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
diff --git a/packages/client/src/ui/_common_/sidebar-for-mobile.vue b/packages/client/src/ui/_common_/sidebar-for-mobile.vue
index 5babdb98a8..afcc50725b 100644
--- a/packages/client/src/ui/_common_/sidebar-for-mobile.vue
+++ b/packages/client/src/ui/_common_/sidebar-for-mobile.vue
@@ -61,7 +61,11 @@ export default defineComponent({
 			otherMenuItemIndicated,
 			post: os.post,
 			search,
-			openAccountMenu,
+			openAccountMenu:(ev) => {
+				openAccountMenu({
+					withExtraOperation: true,
+				}, ev);
+			},
 			more: () => {
 				os.popup(import('@/components/launch-pad.vue'), {}, {
 				}, 'closed');
diff --git a/packages/client/src/ui/_common_/sidebar.vue b/packages/client/src/ui/_common_/sidebar.vue
index fa712ba45d..94baacbee9 100644
--- a/packages/client/src/ui/_common_/sidebar.vue
+++ b/packages/client/src/ui/_common_/sidebar.vue
@@ -76,7 +76,11 @@ export default defineComponent({
 			iconOnly,
 			post: os.post,
 			search,
-			openAccountMenu,
+			openAccountMenu:(ev) => {
+				openAccountMenu({
+					withExtraOperation: true,
+				}, ev);
+			},
 			more: () => {
 				os.popup(import('@/components/launch-pad.vue'), {}, {
 				}, 'closed');
diff --git a/packages/client/src/ui/_common_/stream-indicator.vue b/packages/client/src/ui/_common_/stream-indicator.vue
index c75c6d1c0a..5e811e1b88 100644
--- a/packages/client/src/ui/_common_/stream-indicator.vue
+++ b/packages/client/src/ui/_common_/stream-indicator.vue
@@ -8,39 +8,28 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@/os';
+<script lang="ts" setup>
+import { onUnmounted } from 'vue';
 import { stream } from '@/stream';
 
-export default defineComponent({
-	data() {
-		return {
-			hasDisconnected: false,
-		}
-	},
-	computed: {
-		stream() {
-			return stream;
-		},
-	},
-	created() {
-		stream.on('_disconnected_', this.onDisconnected);
-	},
-	beforeUnmount() {
-		stream.off('_disconnected_', this.onDisconnected);
-	},
-	methods: {
-		onDisconnected() {
-			this.hasDisconnected = true;
-		},
-		resetDisconnected() {
-			this.hasDisconnected = false;
-		},
-		reload() {
-			location.reload();
-		},
-	}
+let hasDisconnected = $ref(false);
+
+function onDisconnected() {
+	hasDisconnected = true;
+}
+
+function resetDisconnected() {
+	hasDisconnected = false;
+}
+
+function reload() {
+	location.reload();
+}
+
+stream.on('_disconnected_', onDisconnected);
+
+onUnmounted(() => {
+	stream.off('_disconnected_', onDisconnected);
 });
 </script>
 
diff --git a/packages/client/src/ui/_common_/upload.vue b/packages/client/src/ui/_common_/upload.vue
index a1c5dcdecc..ab7678a505 100644
--- a/packages/client/src/ui/_common_/upload.vue
+++ b/packages/client/src/ui/_common_/upload.vue
@@ -17,18 +17,12 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import * as os from '@/os';
 
-export default defineComponent({
-	data() {
-		return {
-			uploads: os.uploads,
-			zIndex: os.claimZIndex('high'),
-		};
-	},
-});
+const uploads = os.uploads;
+const zIndex = os.claimZIndex('high');
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/ui/chat/date-separated-list.vue b/packages/client/src/ui/chat/date-separated-list.vue
deleted file mode 100644
index 1a36aca6dd..0000000000
--- a/packages/client/src/ui/chat/date-separated-list.vue
+++ /dev/null
@@ -1,157 +0,0 @@
-<script lang="ts">
-import { defineComponent, h, PropType, TransitionGroup } from 'vue';
-import MkAd from '@/components/global/ad.vue';
-
-export default defineComponent({
-	props: {
-		items: {
-			type: Array as PropType<{ id: string; createdAt: string; _shouldInsertAd_: boolean; }[]>,
-			required: true,
-		},
-		reversed: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		ad: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	render() {
-		const 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()
-			});
-		}
-
-		return h(this.reversed ? 'div' : TransitionGroup, {
-			class: 'hmjzthxl',
-			name: this.reversed ? 'list-reversed' : 'list',
-			tag: 'div',
-		}, this.items.map((item, i) => {
-			const el = this.$slots.default({
-				item: item
-			})[0];
-			if (el.key == null && item.id) el.key = item.id;
-
-			if (
-				i != this.items.length - 1 &&
-				new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate()
-			) {
-				const separator = h('div', {
-					class: 'separator',
-					key: item.id + ':separator',
-				}, h('p', {
-					class: 'date'
-				}, [
-					h('span', [
-						h('i', {
-							class: 'fas fa-angle-up icon',
-						}),
-						getDateText(item.createdAt)
-					]),
-					h('span', [
-						getDateText(this.items[i + 1].createdAt),
-						h('i', {
-							class: 'fas fa-angle-down icon',
-						})
-					])
-				]));
-
-				return [el, separator];
-			} else {
-				if (this.ad && item._shouldInsertAd_) {
-					return [h(MkAd, {
-						class: 'a', // advertiseの意(ブロッカー対策)
-						key: item.id + ':ad',
-						prefer: ['horizontal', 'horizontal-big'],
-					}), el];
-				} else {
-					return el;
-				}
-			}
-		}));
-	},
-});
-</script>
-
-<style lang="scss">
-.hmjzthxl {
-	> .list-move {
-		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
-	}
-	> .list-enter-active {
-		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
-	}
-	> .list-enter-from {
-		opacity: 0;
-		transform: translateY(-64px);
-	}
-
-	> .list-reversed-enter-active, > .list-reversed-leave-active {
-		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
-	}
-	> .list-reversed-enter-from {
-		opacity: 0;
-		transform: translateY(64px);
-	}
-}
-</style>
-
-<style lang="scss">
-.hmjzthxl {
-	> .separator {
-		text-align: center;
-		position: relative;
-
-		&:before {
-			content: "";
-			display: block;
-			position: absolute;
-			top: 50%;
-			left: 0;
-			right: 0;
-			margin: auto;
-			width: calc(100% - 32px);
-			height: 1px;
-			background: var(--divider);
-		}
-
-		> .date {
-			display: inline-block;
-			position: relative;
-			margin: 0;
-			padding: 0 16px;
-			line-height: 32px;
-			text-align: center;
-			font-size: 12px;
-			color: var(--dateLabelFg);
-			background: var(--panel);
-
-			> span {
-				&:first-child {
-					margin-right: 8px;
-
-					> .icon {
-						margin-right: 8px;
-					}
-				}
-
-				&:last-child {
-					margin-left: 8px;
-
-					> .icon {
-						margin-left: 8px;
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/packages/client/src/ui/chat/header-clock.vue b/packages/client/src/ui/chat/header-clock.vue
deleted file mode 100644
index 3488289c21..0000000000
--- a/packages/client/src/ui/chat/header-clock.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<template>
-<div class="acemodlh _monospace">
-	<div>
-		<span v-text="y"></span>/<span v-text="m"></span>/<span v-text="d"></span>
-	</div>
-	<div>
-		<span v-text="hh"></span>
-		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
-		<span v-text="mm"></span>
-		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
-		<span v-text="ss"></span>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@/os';
-
-export default defineComponent({
-	data() {
-		return {
-			clock: null,
-			y: null,
-			m: null,
-			d: null,
-			hh: null,
-			mm: null,
-			ss: null,
-			showColon: true,
-		};
-	},
-	created() {
-		this.tick();
-		this.clock = setInterval(this.tick, 1000);
-	},
-	beforeUnmount() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		tick() {
-			const now = new Date();
-			this.y = now.getFullYear().toString();
-			this.m = (now.getMonth() + 1).toString().padStart(2, '0');
-			this.d = now.getDate().toString().padStart(2, '0');
-			this.hh = now.getHours().toString().padStart(2, '0');
-			this.mm = now.getMinutes().toString().padStart(2, '0');
-			this.ss = now.getSeconds().toString().padStart(2, '0');
-			this.showColon = now.getSeconds() % 2 === 0;
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.acemodlh {
-	opacity: 0.7;
-	font-size: 0.85em;
-	line-height: 1em;
-	text-align: center;
-}
-</style>
diff --git a/packages/client/src/ui/chat/index.vue b/packages/client/src/ui/chat/index.vue
deleted file mode 100644
index f66ab4dcee..0000000000
--- a/packages/client/src/ui/chat/index.vue
+++ /dev/null
@@ -1,463 +0,0 @@
-<template>
-<div class="mk-app" @contextmenu.self.prevent="onContextmenu">
-	<XSidebar ref="menu" class="menu" :default-hidden="true"/>
-
-	<div class="nav">
-		<header class="header">
-			<div class="left">
-				<button class="_button account" @click="openAccountMenu">
-					<MkAvatar :user="$i" class="avatar"/><!--<MkAcct class="text" :user="$i"/>-->
-				</button>
-			</div>
-			<div class="right">
-				<MkA v-tooltip="$ts.messaging" class="item" to="/my/messaging"><i class="fas fa-comments icon"></i><span v-if="$i.hasUnreadMessagingMessage" class="indicator"><i class="fas fa-circle"></i></span></MkA>
-				<MkA v-tooltip="$ts.directNotes" class="item" to="/my/messages"><i class="fas fa-envelope icon"></i><span v-if="$i.hasUnreadSpecifiedNotes" class="indicator"><i class="fas fa-circle"></i></span></MkA>
-				<MkA v-tooltip="$ts.mentions" class="item" to="/my/mentions"><i class="fas fa-at icon"></i><span v-if="$i.hasUnreadMentions" class="indicator"><i class="fas fa-circle"></i></span></MkA>
-				<MkA v-tooltip="$ts.notifications" class="item" to="/my/notifications"><i class="fas fa-bell icon"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></MkA>
-			</div>
-		</header>
-		<div class="body">
-			<div class="container">
-				<div class="header">{{ $ts.timeline }}</div>
-				<div class="body">
-					<MkA to="/timeline/home" class="item" :class="{ active: tl === 'home' }"><i class="fas fa-home icon"></i>{{ $ts._timelines.home }}</MkA>
-					<MkA to="/timeline/local" class="item" :class="{ active: tl === 'local' }"><i class="fas fa-comments icon"></i>{{ $ts._timelines.local }}</MkA>
-					<MkA to="/timeline/social" class="item" :class="{ active: tl === 'social' }"><i class="fas fa-share-alt icon"></i>{{ $ts._timelines.social }}</MkA>
-					<MkA to="/timeline/global" class="item" :class="{ active: tl === 'global' }"><i class="fas fa-globe icon"></i>{{ $ts._timelines.global }}</MkA>
-				</div>
-			</div>
-			<div v-if="followedChannels" class="container">
-				<div class="header">{{ $ts.channel }} ({{ $ts.following }})<button class="_button add" @click="addChannel"><i class="fas fa-plus"></i></button></div>
-				<div class="body">
-					<MkA v-for="channel in followedChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }`, read: !channel.hasUnreadNote }"><i class="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA>
-				</div>
-			</div>
-			<div v-if="featuredChannels" class="container">
-				<div class="header">{{ $ts.channel }}<button class="_button add" @click="addChannel"><i class="fas fa-plus"></i></button></div>
-				<div class="body">
-					<MkA v-for="channel in featuredChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }` }"><i class="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA>
-				</div>
-			</div>
-			<div v-if="lists" class="container">
-				<div class="header">{{ $ts.lists }}<button class="_button add" @click="addList"><i class="fas fa-plus"></i></button></div>
-				<div class="body">
-					<MkA v-for="list in lists" :key="list.id" :to="`/my/list/${ list.id }`" class="item" :class="{ active: tl === `list:${ list.id }` }"><i class="fas fa-list-ul icon"></i>{{ list.name }}</MkA>
-				</div>
-			</div>
-			<div v-if="antennas" class="container">
-				<div class="header">{{ $ts.antennas }}<button class="_button add" @click="addAntenna"><i class="fas fa-plus"></i></button></div>
-				<div class="body">
-					<MkA v-for="antenna in antennas" :key="antenna.id" :to="`/my/antenna/${ antenna.id }`" class="item" :class="{ active: tl === `antenna:${ antenna.id }` }"><i class="fas fa-satellite icon"></i>{{ antenna.name }}</MkA>
-				</div>
-			</div>
-			<div class="container">
-				<div class="body">
-					<MkA to="/my/favorites" class="item"><i class="fas fa-star icon"></i>{{ $ts.favorites }}</MkA>
-				</div>
-			</div>
-			<MkAd class="a" :prefer="['square']"/>
-		</div>
-		<footer class="footer">
-			<div class="left">
-				<button class="_button menu" @click="showMenu">
-					<i class="fas fa-bars icon"></i>
-				</button>
-			</div>
-			<div class="right">
-				<button v-tooltip="$ts.search" class="_button item search" @click="search">
-					<i class="fas fa-search icon"></i>
-				</button>
-				<MkA v-tooltip="$ts.settings" class="item" to="/settings"><i class="fas fa-cog icon"></i></MkA>
-			</div>
-		</footer>
-	</div>
-
-	<main class="main" @contextmenu.stop="onContextmenu">
-		<header class="header">
-			<MkHeader class="header" :info="pageInfo" :menu="menu" :center="false" @click="onHeaderClick"/>
-		</header>
-		<router-view v-slot="{ Component }">
-			<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
-				<keep-alive :include="['timeline']">
-					<component :is="Component" :ref="changePage" class="body"/>
-				</keep-alive>
-			</transition>
-		</router-view>
-	</main>
-
-	<XSide ref="side" class="side" @open="sideViewOpening = true" @close="sideViewOpening = false"/>
-	<div class="side widgets" :class="{ sideViewOpening }">
-		<XWidgets/>
-	</div>
-
-	<XCommon/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import { instanceName, url } from '@/config';
-import XSidebar from '@/ui/_common_/sidebar.vue';
-import XWidgets from './widgets.vue';
-import XCommon from '../_common_/common.vue';
-import XSide from './side.vue';
-import XHeaderClock from './header-clock.vue';
-import * as os from '@/os';
-import { router } from '@/router';
-import { menuDef } from '@/menu';
-import { search } from '@/scripts/search';
-import copyToClipboard from '@/scripts/copy-to-clipboard';
-import { store } from './store';
-import * as symbols from '@/symbols';
-import { openAccountMenu } from '@/account';
-
-export default defineComponent({
-	components: {
-		XCommon,
-		XSidebar,
-		XWidgets,
-		XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
-		XHeaderClock,
-	},
-
-	provide() {
-		return {
-			sideViewHook: (path) => {
-				this.$refs.side.navigate(path);
-			}
-		};
-	},
-
-	data() {
-		return {
-			pageInfo: null,
-			lists: null,
-			antennas: null,
-			followedChannels: null,
-			featuredChannels: null,
-			currentChannel: null,
-			menuDef: menuDef,
-			sideViewOpening: false,
-			instanceName,
-		};
-	},
-
-	computed: {
-		menu() {
-			return [{
-				icon: 'fas fa-columns',
-				text: this.$ts.openInSideView,
-				action: () => {
-					this.$refs.side.navigate(this.$route.path);
-				}
-			}, {
-				icon: 'fas fa-window-maximize',
-				text: this.$ts.openInWindow,
-				action: () => {
-					os.pageWindow(this.$route.path);
-				}
-			}];
-		}
-	},
-
-	created() {
-		if (window.innerWidth < 1024) {
-			localStorage.setItem('ui', 'default');
-			location.reload();
-		}
-
-		os.api('users/lists/list').then(lists => {
-			this.lists = lists;
-		});
-
-		os.api('antennas/list').then(antennas => {
-			this.antennas = antennas;
-		});
-
-		os.api('channels/followed', { limit: 20 }).then(channels => {
-			this.followedChannels = channels;
-		});
-
-		// TODO: pagination
-		os.api('channels/featured', { limit: 20 }).then(channels => {
-			this.featuredChannels = channels;
-		});
-	},
-
-	methods: {
-		changePage(page) {
-			console.log(page);
-			if (page == null) return;
-			if (page[symbols.PAGE_INFO]) {
-				this.pageInfo = page[symbols.PAGE_INFO];
-				document.title = `${this.pageInfo.title} | ${instanceName}`;
-			}
-		},
-
-		showMenu() {
-			this.$refs.menu.show();
-		},
-
-		post() {
-			os.post();
-		},
-
-		search() {
-			search();
-		},
-
-		back() {
-			history.back();
-		},
-
-		top() {
-			window.scroll({ top: 0, behavior: 'smooth' });
-		},
-
-		onTransition() {
-			if (window._scroll) window._scroll();
-		},
-
-		onHeaderClick() {
-			window.scroll({ top: 0, behavior: 'smooth' });
-		},
-
-		onContextmenu(e) {
-			const isLink = (el: HTMLElement) => {
-				if (el.tagName === 'A') return true;
-				if (el.parentElement) {
-					return isLink(el.parentElement);
-				}
-			};
-			if (isLink(e.target)) return;
-			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
-			if (window.getSelection().toString() !== '') return;
-			const path = this.$route.path;
-			os.contextMenu([{
-				type: 'label',
-				text: path,
-			}, {
-				icon: 'fas fa-columns',
-				text: this.$ts.openInSideView,
-				action: () => {
-					this.$refs.side.navigate(path);
-				}
-			}, {
-				icon: 'fas fa-window-maximize',
-				text: this.$ts.openInWindow,
-				action: () => {
-					os.pageWindow(path);
-				}
-			}], e);
-		},
-
-		openAccountMenu,
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-app {
-	$header-height: 54px; // TODO: どこかに集約したい
-	$ui-font-size: 1em; // TODO: どこかに集約したい
-
-	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
-	height: calc(var(--vh, 1vh) * 100);
-	display: flex;
-
-	> .nav {
-		display: flex;
-		flex-direction: column;
-		width: 250px;
-		height: 100vh;
-		border-right: solid 4px var(--divider);
-
-		> .header, > .footer {
-			$padding: 8px;
-			display: flex;
-			align-items: center;
-			z-index: 1000;
-			height: $header-height;
-			padding: $padding;
-			box-sizing: border-box;
-			user-select: none;
-
-			&.header {
-				border-bottom: solid 0.5px var(--divider);
-			}
-
-			&.footer {
-				border-top: solid 0.5px var(--divider);
-			}
-
-			> .left, > .right {
-				> .item, > .menu {
-					display: inline-flex;
-					vertical-align: middle;
-					height: ($header-height - ($padding * 2));
-					width: ($header-height - ($padding * 2));
-					box-sizing: border-box;
-					//opacity: 0.6;
-					position: relative;
-					border-radius: 5px;
-
-					&:hover {
-						background: rgba(0, 0, 0, 0.05);
-					}
-
-					> .icon {
-						margin: auto;
-					}
-
-					> .indicator {
-						position: absolute;
-						top: 8px;
-						right: 8px;
-						color: var(--indicator);
-						font-size: 8px;
-						line-height: 8px;
-						animation: blink 1s infinite;
-					}
-				}
-			}
-
-			> .left {
-				flex: 1;
-				min-width: 0;
-
-				> .account {
-					display: flex;
-					align-items: center;
-					padding: 0 8px;
-
-					> .avatar {
-						width: 26px;
-						height: 26px;
-						margin-right: 8px;
-					}
-
-					> .text {
-						white-space: nowrap;
-						overflow: hidden;
-						text-overflow: ellipsis;
-						font-size: 0.9em;
-					}
-				}
-			}
-
-			> .right {
-				margin-left: auto;
-			}
-		}
-
-		> .body {
-			flex: 1;
-			min-width: 0;
-			overflow: auto;
-
-			> .container {
-				margin-top: 8px;
-				margin-bottom: 8px;
-
-				& + .container {
-					margin-top: 16px;
-				}
-
-				> .header {
-					display: flex;
-					font-size: 0.9em;
-					padding: 8px 16px;
-					position: sticky;
-					top: 0;
-					background: var(--X17);
-					-webkit-backdrop-filter: var(--blur, blur(8px));
-					backdrop-filter: var(--blur, blur(8px));
-					z-index: 1;
-					color: var(--fgTransparentWeak);
-
-					> .add {
-						margin-left: auto;
-						color: var(--fgTransparentWeak);
-
-						&:hover {
-							color: var(--fg);
-						}
-					}
-				}
-
-				> .body {
-					padding: 0 8px;
-
-					> .item {
-						display: block;
-						padding: 6px 8px;
-						border-radius: 4px;
-						white-space: nowrap;
-						overflow: hidden;
-						text-overflow: ellipsis;
-
-						&:hover {
-							text-decoration: none;
-							background: rgba(0, 0, 0, 0.05);
-						}
-
-						&.active, &.active:hover {
-							background: var(--accent);
-							color: #fff !important;
-						}
-
-						&.read {
-							color: var(--fgTransparent);
-						}
-
-						> .icon {
-							margin-right: 8px;
-							opacity: 0.6;
-						}
-					}
-				}
-			}
-
-			> .a {
-				margin: 12px;
-			}
-		}
-	}
-
-	> .main {
-		display: flex;
-		flex: 1;
-		flex-direction: column;
-		min-width: 0;
-		height: 100vh;
-		position: relative;
-		background: var(--panel);
-
-		> .header {
-			z-index: 1000;
-			height: $header-height;
-			background-color: var(--panel);
-			border-bottom: solid 0.5px var(--divider);
-			user-select: none;
-		}
-
-		> .body {
-			width: 100%;
-			box-sizing: border-box;
-			overflow: auto;
-		}
-	}
-
-	> .side {
-		width: 350px;
-		border-left: solid 4px var(--divider);
-		background: var(--panel);
-
-		&.widgets.sideViewOpening {
-			@media (max-width: 1400px) {
-				display: none;
-			}
-		}
-	}
-}
-</style>
diff --git a/packages/client/src/ui/chat/note-header.vue b/packages/client/src/ui/chat/note-header.vue
deleted file mode 100644
index 5f87fdd14e..0000000000
--- a/packages/client/src/ui/chat/note-header.vue
+++ /dev/null
@@ -1,99 +0,0 @@
-<template>
-<header class="dehvdgxo">
-	<MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)">
-		<MkUserName :user="note.user"/>
-	</MkA>
-	<span v-if="note.user.isBot" class="is-bot">bot</span>
-	<span class="username"><MkAcct :user="note.user"/></span>
-	<div class="info">
-		<MkA class="created-at" :to="notePage(note)">
-			<MkTime :time="note.createdAt"/>
-		</MkA>
-		<span v-if="note.visibility !== 'public'" class="visibility">
-			<i v-if="note.visibility === 'home'" class="fas fa-home"></i>
-			<i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
-			<i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i>
-		</span>
-		<span v-if="note.localOnly" class="localOnly"><i class="fas fa-biohazard"></i></span>
-	</div>
-</header>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { notePage } from '@/filters/note';
-import { userPage } from '@/filters/user';
-import * as os from '@/os';
-
-export default defineComponent({
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	methods: {
-		notePage,
-		userPage
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.dehvdgxo {
-	display: flex;
-	align-items: baseline;
-	white-space: nowrap;
-	font-size: 0.9em;
-
-	> .name {
-		display: block;
-		margin: 0 .5em 0 0;
-		padding: 0;
-		overflow: hidden;
-		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%;
-		border: solid 0.5px var(--divider);
-		border-radius: 3px;
-	}
-
-	> .username {
-		margin: 0 .5em 0 0;
-		overflow: hidden;
-		text-overflow: ellipsis;
-	}
-
-	> .info {
-		font-size: 0.9em;
-		opacity: 0.7;
-
-		> .visibility {
-			margin-left: 8px;
-		}
-
-		> .localOnly {
-			margin-left: 8px;
-		}
-	}
-}
-</style>
diff --git a/packages/client/src/ui/chat/note-preview.vue b/packages/client/src/ui/chat/note-preview.vue
deleted file mode 100644
index c28591815e..0000000000
--- a/packages/client/src/ui/chat/note-preview.vue
+++ /dev/null
@@ -1,112 +0,0 @@
-<template>
-<div class="hduudsxk">
-	<MkAvatar class="avatar" :user="note.user"/>
-	<div class="main">
-		<XNoteHeader class="header" :note="note" :mini="true"/>
-		<div class="body">
-			<p v-if="note.cw != null" class="cw">
-				<span v-if="note.cw != ''" class="text">{{ note.cw }}</span>
-				<XCwButton v-model="showContent" :note="note"/>
-			</p>
-			<div v-show="note.cw == null || showContent" class="content">
-				<XSubNote-content class="text" :note="note"/>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XNoteHeader from './note-header.vue';
-import XSubNoteContent from './sub-note-content.vue';
-import XCwButton from '@/components/cw-button.vue';
-import * as os from '@/os';
-
-export default defineComponent({
-	components: {
-		XNoteHeader,
-		XSubNoteContent,
-		XCwButton,
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			showContent: false
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.hduudsxk {
-	display: flex;
-	margin: 0;
-	padding: 0;
-	overflow: hidden;
-	font-size: 0.95em;
-
-	> .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;
-
-				> .text {
-					margin-right: 8px;
-				}
-			}
-
-			> .content {
-				> .text {
-					cursor: default;
-					margin: 0;
-					padding: 0;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/packages/client/src/ui/chat/note.sub.vue b/packages/client/src/ui/chat/note.sub.vue
deleted file mode 100644
index b61b7521a8..0000000000
--- a/packages/client/src/ui/chat/note.sub.vue
+++ /dev/null
@@ -1,137 +0,0 @@
-<template>
-<div class="wrpstxzv" :class="{ children }">
-	<div class="main">
-		<MkAvatar class="avatar" :user="note.user"/>
-		<div class="body">
-			<XNoteHeader 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="$i" :custom-emojis="note.emojis"/>
-					<XCwButton v-model="showContent" :note="note"/>
-				</p>
-				<div v-show="note.cw == null || showContent" class="content">
-					<XSubNote-content class="text" :note="note"/>
-				</div>
-			</div>
-		</div>
-	</div>
-	<XSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :children="true"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XNoteHeader from './note-header.vue';
-import XSubNoteContent from './sub-note-content.vue';
-import XCwButton from '@/components/cw-button.vue';
-import * as os from '@/os';
-
-export default defineComponent({
-	name: 'XSub',
-
-	components: {
-		XNoteHeader,
-		XSubNoteContent,
-		XCwButton,
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		detail: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		children: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		// TODO
-		truncate: {
-			type: Boolean,
-			default: true
-		}
-	},
-
-	data() {
-		return {
-			showContent: false,
-			replies: [],
-		};
-	},
-
-	created() {
-		if (this.detail) {
-			os.api('notes/children', {
-				noteId: this.note.id,
-				limit: 5
-			}).then(replies => {
-				this.replies = replies;
-			});
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.wrpstxzv {
-	padding: 16px 16px;
-	font-size: 0.8em;
-
-	&.children {
-		padding: 10px 0 0 16px;
-		font-size: 1em;
-	}
-
-	> .main {
-		display: flex;
-
-		> .avatar {
-			flex-shrink: 0;
-			display: block;
-			margin: 0 8px 0 0;
-			width: 36px;
-			height: 36px;
-		}
-
-		> .body {
-			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;
-					}
-				}
-			}
-		}
-	}
-
-	> .reply {
-		border-left: solid 0.5px var(--divider);
-		margin-top: 10px;
-	}
-}
-</style>
diff --git a/packages/client/src/ui/chat/note.vue b/packages/client/src/ui/chat/note.vue
deleted file mode 100644
index fa5faa4ec3..0000000000
--- a/packages/client/src/ui/chat/note.vue
+++ /dev/null
@@ -1,1143 +0,0 @@
-<template>
-<div
-	v-if="!muted"
-	v-show="!isDeleted"
-	v-hotkey="keymap"
-	class="vfzoeqcg"
-	:tabindex="!isDeleted ? '-1' : null"
-	:class="{ renote: isRenote, highlighted: appearNote._prId_ || appearNote._featuredId_, operating }"
->
-	<XSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
-	<div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ $ts.pinnedNote }}</div>
-	<div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ $ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ $ts.hideThisNote }} <i class="fas fa-times"></i></button></div>
-	<div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ $ts.featured }}</div>
-	<div v-if="isRenote" class="renote">
-		<MkAvatar class="avatar" :user="note.user"/>
-		<i class="fas fa-retweet"></i>
-		<I18n :src="$ts.renotedBy" tag="span">
-			<template #user>
-				<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
-					<MkUserName :user="note.user"/>
-				</MkA>
-			</template>
-		</I18n>
-		<div class="info">
-			<button ref="renoteTime" class="_button time" @click="showRenoteMenu()">
-				<i v-if="isMyRenote" class="fas fa-ellipsis-h dropdownIcon"></i>
-				<MkTime :time="note.createdAt"/>
-			</button>
-			<span v-if="note.visibility !== 'public'" class="visibility">
-				<i v-if="note.visibility === 'home'" class="fas fa-home"></i>
-				<i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
-				<i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i>
-			</span>
-			<span v-if="note.localOnly" class="localOnly"><i class="fas fa-biohazard"></i></span>
-		</div>
-	</div>
-	<article class="article" @contextmenu.stop="onContextmenu">
-		<MkAvatar class="avatar" :user="appearNote.user"/>
-		<div class="main">
-			<XNoteHeader class="header" :note="appearNote" :mini="true"/>
-			<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
-			<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="$i" :custom-emojis="appearNote.emojis"/>
-					<XCwButton v-model="showContent" :note="appearNote"/>
-				</p>
-				<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed }">
-					<div class="text">
-						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $ts.private }})</span>
-						<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
-						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
-						<a v-if="appearNote.renote != null" class="rp">RN:</a>
-					</div>
-					<div v-if="appearNote.files.length > 0" class="files">
-						<XMediaList :media-list="appearNote.files"/>
-					</div>
-					<XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
-					<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
-					<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div>
-					<button v-if="collapsed" class="fade _button" @click="collapsed = false">
-						<span>{{ $ts.showMore }}</span>
-					</button>
-				</div>
-				<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
-			</div>
-			<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
-			<footer class="footer _panel">
-				<button v-tooltip="$ts.reply" class="button _button" @click="reply()">
-					<template v-if="appearNote.reply"><i class="fas fa-reply-all"></i></template>
-					<template v-else><i class="fas fa-reply"></i></template>
-					<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
-				</button>
-				<button v-if="canRenote" ref="renoteButton" v-tooltip="$ts.renote" class="button _button" @click="renote()">
-					<i class="fas fa-retweet"></i><p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p>
-				</button>
-				<button v-else class="button _button">
-					<i class="fas fa-ban"></i>
-				</button>
-				<button v-if="appearNote.myReaction == null" ref="reactButton" v-tooltip="$ts.reaction" class="button _button" @click="react()">
-					<i class="fas fa-plus"></i>
-				</button>
-				<button v-if="appearNote.myReaction != null" ref="reactButton" v-tooltip="$ts.reaction" class="button _button reacted" @click="undoReact(appearNote)">
-					<i class="fas fa-minus"></i>
-				</button>
-				<button ref="menuButton" class="button _button" @click="menu()">
-					<i class="fas fa-ellipsis-h"></i>
-				</button>
-			</footer>
-		</div>
-	</article>
-</div>
-<div v-else class="muted" @click="muted = false">
-	<I18n :src="$ts.userSaysSomething" tag="small">
-		<template #name>
-			<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
-				<MkUserName :user="appearNote.user"/>
-			</MkA>
-		</template>
-	</I18n>
-</div>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
-import * as mfm from 'mfm-js';
-import { sum } from '@/scripts/array';
-import XSub from './note.sub.vue';
-import XNoteHeader from './note-header.vue';
-import XNoteSimple from './note-preview.vue';
-import XReactionsViewer from '@/components/reactions-viewer.vue';
-import XMediaList from '@/components/media-list.vue';
-import XCwButton from '@/components/cw-button.vue';
-import XPoll from '@/components/poll.vue';
-import { pleaseLogin } from '@/scripts/please-login';
-import { focusPrev, focusNext } from '@/scripts/focus';
-import { url } from '@/config';
-import copyToClipboard from '@/scripts/copy-to-clipboard';
-import { checkWordMute } from '@/scripts/check-word-mute';
-import { userPage } from '@/filters/user';
-import * as os from '@/os';
-import { stream } from '@/stream';
-import { noteActions, noteViewInterruptors } from '@/store';
-import { reactionPicker } from '@/scripts/reaction-picker';
-import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
-
-export default defineComponent({
-	components: {
-		XSub,
-		XNoteHeader,
-		XNoteSimple,
-		XReactionsViewer,
-		XMediaList,
-		XCwButton,
-		XPoll,
-		MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
-		MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')),
-	},
-
-	inject: {
-		inChannel: {
-			default: null
-		},
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		pinned: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	emits: ['update:note'],
-
-	data() {
-		return {
-			connection: null,
-			replies: [],
-			showContent: false,
-			collapsed: false,
-			isDeleted: false,
-			muted: false,
-			operating: false,
-		};
-	},
-
-	computed: {
-		rs() {
-			return this.$store.state.reactions;
-		},
-		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.rs[0]),
-				'2': () => this.reactDirectly(this.rs[1]),
-				'3': () => this.reactDirectly(this.rs[2]),
-				'4': () => this.reactDirectly(this.rs[3]),
-				'5': () => this.reactDirectly(this.rs[4]),
-				'6': () => this.reactDirectly(this.rs[5]),
-				'7': () => this.reactDirectly(this.rs[6]),
-				'8': () => this.reactDirectly(this.rs[7]),
-				'9': () => this.reactDirectly(this.rs[8]),
-				'0': () => this.reactDirectly(this.rs[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.$i && (this.$i.id === this.appearNote.userId);
-		},
-
-		isMyRenote(): boolean {
-			return this.$i && (this.$i.id === this.note.userId);
-		},
-
-		canRenote(): boolean {
-			return ['public', 'home'].includes(this.appearNote.visibility) || this.isMyNote;
-		},
-
-		reactionsCount(): number {
-			return this.appearNote.reactions
-				? sum(Object.values(this.appearNote.reactions))
-				: 0;
-		},
-
-		urls(): string[] {
-			if (this.appearNote.text) {
-				return extractUrlFromMfm(mfm.parse(this.appearNote.text));
-			} else {
-				return null;
-			}
-		},
-
-		showTicker() {
-			if (this.$store.state.instanceTicker === 'always') return true;
-			if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true;
-			return false;
-		}
-	},
-
-	async created() {
-		if (this.$i) {
-			this.connection = stream;
-		}
-
-		this.collapsed = this.appearNote.cw == null && this.appearNote.text && (
-			(this.appearNote.text.split('\n').length > 9) ||
-			(this.appearNote.text.length > 500)
-		);
-		this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords);
-
-		// plugin
-		if (noteViewInterruptors.length > 0) {
-			let result = this.note;
-			for (const interruptor of noteViewInterruptors) {
-				result = await interruptor.handler(JSON.parse(JSON.stringify(result)));
-			}
-			this.$emit('update:note', Object.freeze(result));
-		}
-	},
-
-	mounted() {
-		this.capture(true);
-
-		if (this.$i) {
-			this.connection.on('_connected_', this.onStreamConnected);
-		}
-	},
-
-	beforeUnmount() {
-		this.decapture(true);
-
-		if (this.$i) {
-			this.connection.off('_connected_', this.onStreamConnected);
-		}
-	},
-
-	methods: {
-		updateAppearNote(v) {
-			this.$emit('update:note', Object.freeze(this.isRenote ? {
-				...this.note,
-				renote: {
-					...this.note.renote,
-					...v
-				}
-			} : {
-				...this.note,
-				...v
-			}));
-		},
-
-		readPromo() {
-			os.api('promo/read', {
-				noteId: this.appearNote.id
-			});
-			this.isDeleted = true;
-		},
-
-		capture(withHandler = false) {
-			if (this.$i) {
-				// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
-				this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
-				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
-			}
-		},
-
-		decapture(withHandler = false) {
-			if (this.$i) {
-				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;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					if (body.emoji) {
-						const emojis = this.appearNote.emojis || [];
-						if (!emojis.includes(body.emoji)) {
-							n.emojis = [...emojis, body.emoji];
-						}
-					}
-
-					// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
-					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
-					// Increment the count
-					n.reactions = {
-						...this.appearNote.reactions,
-						[reaction]: currentCount + 1
-					};
-
-					if (body.userId === this.$i.id) {
-						n.myReaction = reaction;
-					}
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'unreacted': {
-					const reaction = body.reaction;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
-					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
-					// Decrement the count
-					n.reactions = {
-						...this.appearNote.reactions,
-						[reaction]: Math.max(0, currentCount - 1)
-					};
-
-					if (body.userId === this.$i.id) {
-						n.myReaction = null;
-					}
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'pollVoted': {
-					const choice = body.choice;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					const choices = [...this.appearNote.poll.choices];
-					choices[choice] = {
-						...choices[choice],
-						votes: choices[choice].votes + 1,
-						...(body.userId === this.$i.id ? {
-							isVoted: true
-						} : {})
-					};
-
-					n.poll = {
-						...this.appearNote.poll,
-						choices: choices
-					};
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'deleted': {
-					this.isDeleted = true;
-					break;
-				}
-			}
-		},
-
-		reply(viaKeyboard = false) {
-			pleaseLogin();
-			this.operating = true;
-			os.post({
-				reply: this.appearNote,
-				animation: !viaKeyboard,
-			}, () => {
-				this.operating = false;
-				this.focus();
-			});
-		},
-
-		renote(viaKeyboard = false) {
-			pleaseLogin();
-			this.operating = true;
-			this.blur();
-			os.popupMenu([{
-				text: this.$ts.renote,
-				icon: 'fas fa-retweet',
-				action: () => {
-					os.api('notes/create', {
-						renoteId: this.appearNote.id
-					});
-				}
-			}, {
-				text: this.$ts.quote,
-				icon: 'fas fa-quote-right',
-				action: () => {
-					os.post({
-						renote: this.appearNote,
-					});
-				}
-			}], this.$refs.renoteButton, {
-				viaKeyboard
-			}).then(() => {
-				this.operating = false;
-			});
-		},
-
-		renoteDirectly() {
-			os.apiWithDialog('notes/create', {
-				renoteId: this.appearNote.id
-			}, undefined, (res: any) => {
-				os.alert({
-					type: 'success',
-					text: this.$ts.renoted,
-				});
-			}, (e: Error) => {
-				if (e.id === 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4') {
-					os.alert({
-						type: 'error',
-						text: this.$ts.cantRenote,
-					});
-				} else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') {
-					os.alert({
-						type: 'error',
-						text: this.$ts.cantReRenote,
-					});
-				}
-			});
-		},
-
-		async react(viaKeyboard = false) {
-			pleaseLogin();
-			this.operating = true;
-			this.blur();
-			reactionPicker.show(this.$refs.reactButton, reaction => {
-				os.api('notes/reactions/create', {
-					noteId: this.appearNote.id,
-					reaction: reaction
-				});
-			}, () => {
-				this.operating = false;
-				this.focus();
-			});
-		},
-
-		reactDirectly(reaction) {
-			os.api('notes/reactions/create', {
-				noteId: this.appearNote.id,
-				reaction: reaction
-			});
-		},
-
-		undoReact(note) {
-			const oldReaction = note.myReaction;
-			if (!oldReaction) return;
-			os.api('notes/reactions/delete', {
-				noteId: note.id
-			});
-		},
-
-		favorite() {
-			pleaseLogin();
-			os.apiWithDialog('notes/favorites/create', {
-				noteId: this.appearNote.id
-			}, undefined, (res: any) => {
-				os.alert({
-					type: 'success',
-					text: this.$ts.favorited,
-				});
-			}, (e: Error) => {
-				if (e.id === 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6') {
-					os.alert({
-						type: 'error',
-						text: this.$ts.alreadyFavorited,
-					});
-				} else if (e.id === '6dd26674-e060-4816-909a-45ba3f4da458') {
-					os.alert({
-						type: 'error',
-						text: this.$ts.cantFavorite,
-					});
-				}
-			});
-		},
-
-		del() {
-			os.confirm({
-				type: 'warning',
-				text: this.$ts.noteDeleteConfirm,
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				os.api('notes/delete', {
-					noteId: this.appearNote.id
-				});
-			});
-		},
-
-		delEdit() {
-			os.confirm({
-				type: 'warning',
-				text: this.$ts.deleteAndEditConfirm,
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				os.api('notes/delete', {
-					noteId: this.appearNote.id
-				});
-
-				os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel });
-			});
-		},
-
-		toggleFavorite(favorite: boolean) {
-			os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
-				noteId: this.appearNote.id
-			});
-		},
-
-		toggleWatch(watch: boolean) {
-			os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
-				noteId: this.appearNote.id
-			});
-		},
-
-		getMenu() {
-			let menu;
-			if (this.$i) {
-				const statePromise = os.api('notes/state', {
-					noteId: this.appearNote.id
-				});
-
-				menu = [{
-					icon: 'fas fa-copy',
-					text: this.$ts.copyContent,
-					action: this.copyContent
-				}, {
-					icon: 'fas fa-link',
-					text: this.$ts.copyLink,
-					action: this.copyLink
-				}, (this.appearNote.url || this.appearNote.uri) ? {
-					icon: 'fas fa-external-link-square-alt',
-					text: this.$ts.showOnRemote,
-					action: () => {
-						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
-					}
-				} : undefined,
-				{
-					icon: 'fas fa-share-alt',
-					text: this.$ts.share,
-					action: this.share
-				},
-				null,
-				statePromise.then(state => state.isFavorited ? {
-					icon: 'fas fa-star',
-					text: this.$ts.unfavorite,
-					action: () => this.toggleFavorite(false)
-				} : {
-					icon: 'fas fa-star',
-					text: this.$ts.favorite,
-					action: () => this.toggleFavorite(true)
-				}),
-				{
-					icon: 'fas fa-paperclip',
-					text: this.$ts.clip,
-					action: () => this.clip()
-				},
-				(this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? {
-					icon: 'fas fa-eye-slash',
-					text: this.$ts.unwatch,
-					action: () => this.toggleWatch(false)
-				} : {
-					icon: 'fas fa-eye',
-					text: this.$ts.watch,
-					action: () => this.toggleWatch(true)
-				}) : undefined,
-				this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
-					icon: 'fas fa-thumbtack',
-					text: this.$ts.unpin,
-					action: () => this.togglePin(false)
-				} : {
-					icon: 'fas fa-thumbtack',
-					text: this.$ts.pin,
-					action: () => this.togglePin(true)
-				} : undefined,
-				/*
-				...(this.$i.isModerator || this.$i.isAdmin ? [
-					null,
-					{
-						icon: 'fas fa-bullhorn',
-						text: this.$ts.promote,
-						action: this.promote
-					}]
-					: []
-				),*/
-				...(this.appearNote.userId != this.$i.id ? [
-					null,
-					{
-						icon: 'fas fa-exclamation-circle',
-						text: this.$ts.reportAbuse,
-						action: () => {
-							const u = `${url}/notes/${this.appearNote.id}`;
-							os.popup(import('@/components/abuse-report-window.vue'), {
-								user: this.appearNote.user,
-								initialComment: `Note: ${u}\n-----\n`
-							}, {}, 'closed');
-						}
-					}]
-					: []
-				),
-				...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [
-					null,
-					this.appearNote.userId == this.$i.id ? {
-						icon: 'fas fa-edit',
-						text: this.$ts.deleteAndEdit,
-						action: this.delEdit
-					} : undefined,
-					{
-						icon: 'fas fa-trash-alt',
-						text: this.$ts.delete,
-						danger: true,
-						action: this.del
-					}]
-					: []
-				)]
-				.filter(x => x !== undefined);
-			} else {
-				menu = [{
-					icon: 'fas fa-copy',
-					text: this.$ts.copyContent,
-					action: this.copyContent
-				}, {
-					icon: 'fas fa-link',
-					text: this.$ts.copyLink,
-					action: this.copyLink
-				}, (this.appearNote.url || this.appearNote.uri) ? {
-					icon: 'fas fa-external-link-square-alt',
-					text: this.$ts.showOnRemote,
-					action: () => {
-						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
-					}
-				} : undefined]
-				.filter(x => x !== undefined);
-			}
-
-			if (noteActions.length > 0) {
-				menu = menu.concat([null, ...noteActions.map(action => ({
-					icon: 'fas fa-plug',
-					text: action.title,
-					action: () => {
-						action.handler(this.appearNote);
-					}
-				}))]);
-			}
-
-			return menu;
-		},
-
-		onContextmenu(e) {
-			const isLink = (el: HTMLElement) => {
-				if (el.tagName === 'A') return true;
-				if (el.parentElement) {
-					return isLink(el.parentElement);
-				}
-			};
-			if (isLink(e.target)) return;
-			if (window.getSelection().toString() !== '') return;
-			
-			if (this.$store.state.useReactionPickerForContextMenu) {
-				e.preventDefault();
-				this.react();
-			} else {
-				os.contextMenu(this.getMenu(), e).then(this.focus);
-			}
-		},
-
-		menu(viaKeyboard = false) {
-			this.operating = true;
-			os.popupMenu(this.getMenu(), this.$refs.menuButton, {
-				viaKeyboard
-			}).then(() => {
-				this.operating = false;
-				this.focus();
-			});
-		},
-
-		showRenoteMenu(viaKeyboard = false) {
-			if (!this.isMyRenote) return;
-			os.popupMenu([{
-				text: this.$ts.unrenote,
-				icon: 'fas fa-trash-alt',
-				danger: true,
-				action: () => {
-					os.api('notes/delete', {
-						noteId: this.note.id
-					});
-					this.isDeleted = true;
-				}
-			}], this.$refs.renoteTime, {
-				viaKeyboard: viaKeyboard
-			});
-		},
-
-		toggleShowContent() {
-			this.showContent = !this.showContent;
-		},
-
-		copyContent() {
-			copyToClipboard(this.appearNote.text);
-			os.success();
-		},
-
-		copyLink() {
-			copyToClipboard(`${url}/notes/${this.appearNote.id}`);
-			os.success();
-		},
-
-		togglePin(pin: boolean) {
-			os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
-				noteId: this.appearNote.id
-			}, undefined, null, e => {
-				if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
-					os.alert({
-						type: 'error',
-						text: this.$ts.pinLimitExceeded
-					});
-				}
-			});
-		},
-
-		async clip() {
-			const clips = await os.api('clips/list');
-			os.popupMenu([{
-				icon: 'fas fa-plus',
-				text: this.$ts.createNew,
-				action: async () => {
-					const { canceled, result } = await os.form(this.$ts.createNewClip, {
-						name: {
-							type: 'string',
-							label: this.$ts.name
-						},
-						description: {
-							type: 'string',
-							required: false,
-							multiline: true,
-							label: this.$ts.description
-						},
-						isPublic: {
-							type: 'boolean',
-							label: this.$ts.public,
-							default: false
-						}
-					});
-					if (canceled) return;
-
-					const clip = await os.apiWithDialog('clips/create', result);
-
-					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
-				}
-			}, null, ...clips.map(clip => ({
-				text: clip.name,
-				action: () => {
-					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
-				}
-			}))], this.$refs.menuButton, {
-			}).then(this.focus);
-		},
-
-		async promote() {
-			const { canceled, result: days } = await os.inputNumber({
-				title: this.$ts.numberOfDays,
-			});
-
-			if (canceled) return;
-
-			os.apiWithDialog('admin/promo/create', {
-				noteId: this.appearNote.id,
-				expiresAt: Date.now() + (86400000 * days)
-			});
-		},
-
-		share() {
-			navigator.share({
-				title: this.$t('noteOf', { user: this.appearNote.user.name }),
-				text: this.appearNote.text,
-				url: `${url}/notes/${this.appearNote.id}`
-			});
-		},
-
-		focus() {
-			this.$el.focus();
-		},
-
-		blur() {
-			this.$el.blur();
-		},
-
-		focusBefore() {
-			focusPrev(this.$el);
-		},
-
-		focusAfter() {
-			focusNext(this.$el);
-		},
-
-		userPage
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.vfzoeqcg {
-	position: relative;
-	contain: content;
-
-	// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
-	// 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう
-	// ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、
-	// 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
-	// 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?)
-	//content-visibility: auto;
-  //contain-intrinsic-size: 0 128px;
-
-	&:focus-visible {
-		outline: none;
-	}
-
-	&:hover {
-		background: rgba(0, 0, 0, 0.05);
-	}
-
-	&:hover, &.operating {
-		> .article > .main > .footer {
-			display: block;
-		}
-	}
-
-	&.renote {
-		background: rgba(128, 255, 0, 0.05);
-	}
-
-	&.highlighted {
-		background: rgba(255, 128, 0, 0.05);
-	}
-
-	> .info {
-		display: flex;
-		align-items: center;
-		padding: 12px 16px 4px 16px;
-		line-height: 24px;
-		font-size: 85%;
-		white-space: pre;
-		color: #d28a3f;
-
-		> i {
-			margin-right: 4px;
-		}
-
-		> .hide {
-			margin-left: 16px;
-			color: inherit;
-			opacity: 0.7;
-		}
-	}
-
-	> .info + .article {
-		padding-top: 8px;
-	}
-
-	> .reply-to {
-		opacity: 0.7;
-		padding-bottom: 0;
-	}
-
-	> .renote {
-		display: flex;
-		align-items: center;
-		padding: 12px 16px 4px 16px;
-		line-height: 28px;
-		white-space: pre;
-		color: var(--renote);
-		font-size: 0.9em;
-
-		> .avatar {
-			flex-shrink: 0;
-			display: inline-block;
-			width: 28px;
-			height: 28px;
-			margin: 0 8px 0 0;
-			border-radius: 6px;
-		}
-
-		> i {
-			margin-right: 4px;
-		}
-
-		> span {
-			overflow: hidden;
-			flex-shrink: 1;
-			text-overflow: ellipsis;
-			white-space: nowrap;
-
-			> .name {
-				font-weight: bold;
-			}
-		}
-
-		> .info {
-			margin-left: 8px;
-			font-size: 0.9em;
-			opacity: 0.7;
-
-			> .time {
-				flex-shrink: 0;
-				color: inherit;
-
-				> .dropdownIcon {
-					margin-right: 4px;
-				}
-			}
-
-			> .visibility {
-				margin-left: 8px;
-			}
-
-			> .localOnly {
-				margin-left: 8px;
-			}
-		}
-	}
-
-	> .renote + .article {
-		padding-top: 8px;
-	}
-
-	> .article {
-		display: flex;
-		padding: 12px 16px;
-
-		> .avatar {
-			flex-shrink: 0;
-			display: block;
-			position: sticky;
-			top: 0;
-			margin: 0 14px 0 0;
-			width: 46px;
-			height: 46px;
-		}
-
-		> .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 {
-					&.collapsed {
-						position: relative;
-						max-height: 9em;
-						overflow: hidden;
-
-						> .fade {
-							display: block;
-							position: absolute;
-							bottom: 0;
-							left: 0;
-							width: 100%;
-							height: 64px;
-							background: linear-gradient(0deg, var(--panel), var(--X15));
-
-							> span {
-								display: inline-block;
-								background: var(--panel);
-								padding: 6px 10px;
-								font-size: 0.8em;
-								border-radius: 999px;
-								box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
-							}
-
-							&:hover {
-								> span {
-									background: var(--panelHighlight);
-								}
-							}
-						}
-					}
-
-					> .text {
-						overflow-wrap: break-word;
-
-						> .reply {
-							color: var(--accent);
-							margin-right: 0.5em;
-						}
-
-						> .rp {
-							margin-left: 4px;
-							font-style: oblique;
-							color: var(--renote);
-						}
-					}
-
-					> .files {
-						max-width: 500px;
-					}
-
-					> .url-preview {
-						margin-top: 8px;
-						max-width: 500px;
-					}
-
-					> .poll {
-						font-size: 80%;
-						max-width: 500px;
-					}
-
-					> .renote {
-						padding: 8px 0;
-
-						> * {
-							padding: 16px;
-							border: dashed 1px var(--renote);
-							border-radius: 8px;
-						}
-					}
-				}
-
-				> .channel {
-					opacity: 0.7;
-					font-size: 80%;
-				}
-			}
-
-			> .footer {
-				display: none;
-				position: absolute;
-				top: 8px;
-				right: 8px;
-				padding: 0 6px;
-				opacity: 0.7;
-
-				&:hover {
-					opacity: 1;
-				}
-
-				> .button {
-					margin: 0;
-					padding: 8px;
-					opacity: 0.7;
-
-					&:hover {
-						color: var(--accent);
-					}
-
-					> .count {
-						display: inline;
-						margin: 0 0 0 8px;
-						opacity: 0.7;
-					}
-
-					&.reacted {
-						color: var(--accent);
-					}
-				}
-			}
-		}
-	}
-
-	> .reply {
-		border-top: solid 0.5px var(--divider);
-	}
-}
-
-.muted {
-	padding: 8px 16px;
-	opacity: 0.7;
-
-	&:hover {
-		background: rgba(0, 0, 0, 0.05);
-	}
-}
-</style>
diff --git a/packages/client/src/ui/chat/notes.vue b/packages/client/src/ui/chat/notes.vue
deleted file mode 100644
index 51d4afcf54..0000000000
--- a/packages/client/src/ui/chat/notes.vue
+++ /dev/null
@@ -1,94 +0,0 @@
-<template>
-<div class="">
-	<div v-if="empty" class="_fullinfo">
-		<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
-		<div>{{ $ts.noNotes }}</div>
-	</div>
-
-	<MkLoading v-if="fetching"/>
-
-	<MkError v-if="error" @retry="init()"/>
-
-	<div v-show="more && reversed" style="margin-bottom: var(--margin);">
-		<MkButton style="margin: 0 auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore">
-			<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
-			<template v-if="moreFetching"><MkLoading inline/></template>
-		</MkButton>
-	</div>
-
-	<XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :ad="true">
-		<XNote :key="note._featuredId_ || note._prId_ || note.id" :note="note" @update:note="updated(note, $event)"/>
-	</XList>
-
-	<div v-show="more && !reversed" style="margin-top: var(--margin);">
-		<MkButton v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" style="margin: 0 auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore">
-			<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
-			<template v-if="moreFetching"><MkLoading inline/></template>
-		</MkButton>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import paging from '@/scripts/paging';
-import XNote from './note.vue';
-import XList from './date-separated-list.vue';
-import MkButton from '@/components/ui/button.vue';
-
-export default defineComponent({
-	components: {
-		XNote, XList, MkButton,
-	},
-
-	mixins: [
-		paging({
-			before: (self) => {
-				self.$emit('before');
-			},
-
-			after: (self, e) => {
-				self.$emit('after', e);
-			}
-		}),
-	],
-
-	props: {
-		pagination: {
-			required: true
-		},
-
-		prop: {
-			type: String,
-			required: false
-		}
-	},
-
-	emits: ['before', 'after'],
-
-	computed: {
-		notes(): any[] {
-			return this.prop ? this.items.map(item => item[this.prop]) : this.items;
-		},
-
-		reversed(): boolean {
-			return this.pagination.reversed;
-		}
-	},
-
-	methods: {
-		updated(oldValue, newValue) {
-			const i = this.notes.findIndex(n => n === oldValue);
-			if (this.prop) {
-				this.items[i][this.prop] = newValue;
-			} else {
-				this.items[i] = newValue;
-			}
-		},
-
-		focus() {
-			this.$refs.notes.focus();
-		}
-	}
-});
-</script>
diff --git a/packages/client/src/ui/chat/pages/channel.vue b/packages/client/src/ui/chat/pages/channel.vue
deleted file mode 100644
index 2755cc92b7..0000000000
--- a/packages/client/src/ui/chat/pages/channel.vue
+++ /dev/null
@@ -1,259 +0,0 @@
-<template>
-<div v-if="channel" class="hhizbblb">
-	<div v-if="date" class="info">
-		<MkInfo>{{ $ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ $ts.clear }}</button></MkInfo>
-	</div>
-	<div ref="body" class="tl">
-		<div v-if="queue > 0" class="new" :style="{ width: width + 'px', bottom: bottom + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div>
-		<XNotes ref="tl" v-follow="true" class="tl" :pagination="pagination" @queue="queueUpdated"/>
-	</div>
-	<div class="bottom">
-		<div v-if="typers.length > 0" class="typers">
-			<I18n :src="$ts.typingUsers" text-tag="span" class="users">
-				<template #users>
-					<b v-for="user in typers" :key="user.id" class="user">{{ user.username }}</b>
-				</template>
-			</I18n>
-			<MkEllipsis/>
-		</div>
-		<XPostForm :channel="channel"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, markRaw } from 'vue';
-import * as Misskey from 'misskey-js';
-import XNotes from '../notes.vue';
-import * as os from '@/os';
-import { stream } from '@/stream';
-import * as sound from '@/scripts/sound';
-import { scrollToBottom, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
-import follow from '@/directives/follow-append';
-import XPostForm from '../post-form.vue';
-import MkInfo from '@/components/ui/info.vue';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
-	components: {
-		XNotes,
-		XPostForm,
-		MkInfo,
-	},
-
-	directives: {
-		follow
-	},
-	
-	provide() {
-		return {
-			inChannel: true
-		};
-	},
-
-	props: {
-		channelId: {
-			type: String,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			channel: null as Misskey.entities.Channel | null,
-			connection: null,
-			pagination: null,
-			baseQuery: {
-				includeMyRenotes: this.$store.state.showMyRenotes,
-				includeRenotedMyNotes: this.$store.state.showRenotedMyNotes,
-				includeLocalRenotes: this.$store.state.showLocalRenotes
-			},
-			queue: 0,
-			width: 0,
-			top: 0,
-			bottom: 0,
-			typers: [],
-			date: null,
-			[symbols.PAGE_INFO]: computed(() => ({
-				title: this.channel ? this.channel.name : '-',
-				subtitle: this.channel ? this.channel.description : '-',
-				icon: 'fas fa-satellite-dish',
-				actions: [{
-					icon: this.channel?.isFollowing ? 'fas fa-star' : 'far fa-star',
-					text: this.channel?.isFollowing ? this.$ts.unfollow : this.$ts.follow,
-					highlighted: this.channel?.isFollowing,
-					handler: this.toggleChannelFollow
-				}, {
-					icon: 'fas fa-search',
-					text: this.$ts.inChannelSearch,
-					handler: this.inChannelSearch
-				}, {
-					icon: 'fas fa-calendar-alt',
-					text: this.$ts.jumpToSpecifiedDate,
-					handler: this.timetravel
-				}]
-			})),
-		};
-	},
-
-	async created() {
-		this.channel = await os.api('channels/show', { channelId: this.channelId });
-
-		const prepend = note => {
-			(this.$refs.tl as any).prepend(note);
-
-			this.$emit('note');
-
-			sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
-		};
-
-		this.connection = markRaw(stream.useChannel('channel', {
-			channelId: this.channelId
-		}));
-		this.connection.on('note', prepend);
-		this.connection.on('typers', typers => {
-			this.typers = this.$i ? typers.filter(u => u.id !== this.$i.id) : typers;
-		});
-
-		this.pagination = {
-			endpoint: 'channels/timeline',
-			reversed: true,
-			limit: 10,
-			params: init => ({
-				channelId: this.channelId,
-				untilDate: this.date?.getTime(),
-				...this.baseQuery
-			})
-		};
-	},
-
-	mounted() {
-
-	},
-
-	beforeUnmount() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		focus() {
-			this.$refs.body.focus();
-		},
-
-		goTop() {
-			const container = getScrollContainer(this.$refs.body);
-			container.scrollTop = 0;
-		},
-
-		queueUpdated(q) {
-			if (this.$refs.body.offsetWidth !== 0) {
-				const rect = this.$refs.body.getBoundingClientRect();
-				this.width = this.$refs.body.offsetWidth;
-				this.top = rect.top;
-				this.bottom = this.$refs.body.offsetHeight;
-			}
-			this.queue = q;
-		},
-
-		async inChannelSearch() {
-			const { canceled, result: query } = await os.inputText({
-				title: this.$ts.inChannelSearch,
-			});
-			if (canceled || query == null || query === '') return;
-			router.push(`/search?q=${encodeURIComponent(query)}&channel=${this.channelId}`);
-		},
-
-		async toggleChannelFollow() {
-			if (this.channel.isFollowing) {
-				await os.apiWithDialog('channels/unfollow', {
-					channelId: this.channel.id
-				});
-				this.channel.isFollowing = false;
-			} else {
-				await os.apiWithDialog('channels/follow', {
-					channelId: this.channel.id
-				});
-				this.channel.isFollowing = true;
-			}
-		},
-
-		openChannelMenu(ev) {
-			os.popupMenu([{
-				text: this.$ts.copyUrl,
-				icon: 'fas fa-link',
-				action: () => {
-					copyToClipboard(`${url}/channels/${this.currentChannel.id}`);
-				}
-			}], ev.currentTarget || ev.target);
-		},
-
-		timetravel(date?: Date) {
-			this.date = date;
-			this.$refs.tl.reload();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.hhizbblb {
-	display: flex;
-	flex-direction: column;
-	flex: 1;
-	overflow: auto;
-
-	> .info {
-		padding: 16px 16px 0 16px;
-	}
-
-	> .top {
-		padding: 16px 16px 0 16px;
-	}
-
-	> .bottom {
-		padding: 0 16px 16px 16px;
-		position: relative;
-
-		> .typers {
-			position: absolute;
-			bottom: 100%;
-			padding: 0 8px 0 8px;
-			font-size: 0.9em;
-			background: var(--panel);
-			border-radius: 0 8px 0 0;
-			color: var(--fgTransparentWeak);
-
-			> .users {
-				> .user + .user:before {
-					content: ", ";
-					font-weight: normal;
-				}
-
-				> .user:last-of-type:after {
-					content: " ";
-				}
-			}
-		}
-	}
-
-	> .tl {
-		position: relative;
-		padding: 16px 0;
-		flex: 1;
-		min-width: 0;
-		overflow: auto;
-
-		> .new {
-			position: fixed;
-			z-index: 1000;
-
-			> button {
-				display: block;
-				margin: 16px auto;
-				padding: 8px 16px;
-				border-radius: 32px;
-			}
-		}
-	}
-}
-</style>
diff --git a/packages/client/src/ui/chat/pages/timeline.vue b/packages/client/src/ui/chat/pages/timeline.vue
deleted file mode 100644
index f67d333398..0000000000
--- a/packages/client/src/ui/chat/pages/timeline.vue
+++ /dev/null
@@ -1,222 +0,0 @@
-<template>
-<div class="dbiokgaf">
-	<div v-if="date" class="info">
-		<MkInfo>{{ $ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ $ts.clear }}</button></MkInfo>
-	</div>
-	<div class="top">
-		<XPostForm/>
-	</div>
-	<div ref="body" class="tl">
-		<div v-if="queue > 0" class="new" :style="{ width: width + 'px', top: top + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div>
-		<XNotes ref="tl" class="tl" :pagination="pagination" @queue="queueUpdated"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, markRaw } from 'vue';
-import XNotes from '../notes.vue';
-import * as os from '@/os';
-import { stream } from '@/stream';
-import * as sound from '@/scripts/sound';
-import { scrollToBottom, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
-import follow from '@/directives/follow-append';
-import XPostForm from '../post-form.vue';
-import MkInfo from '@/components/ui/info.vue';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
-	components: {
-		XNotes,
-		XPostForm,
-		MkInfo,
-	},
-
-	directives: {
-		follow
-	},
-
-	props: {
-		src: {
-			type: String,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			connection: null,
-			connection2: null,
-			pagination: null,
-			baseQuery: {
-				includeMyRenotes: this.$store.state.showMyRenotes,
-				includeRenotedMyNotes: this.$store.state.showRenotedMyNotes,
-				includeLocalRenotes: this.$store.state.showLocalRenotes
-			},
-			query: {},
-			queue: 0,
-			width: 0,
-			top: 0,
-			bottom: 0,
-			typers: [],
-			date: null,
-			[symbols.PAGE_INFO]: computed(() => ({
-				title: this.$ts.timeline,
-				icon: 'fas fa-home',
-				actions: [{
-					icon: 'fas fa-calendar-alt',
-					text: this.$ts.jumpToSpecifiedDate,
-					handler: this.timetravel
-				}]
-			})),
-		};
-	},
-
-	created() {
-		const prepend = note => {
-			(this.$refs.tl as any).prepend(note);
-
-			this.$emit('note');
-
-			sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
-		};
-
-		const onChangeFollowing = () => {
-			if (!this.$refs.tl.backed) {
-				this.$refs.tl.reload();
-			}
-		};
-
-		let endpoint;
-
-		if (this.src == 'home') {
-			endpoint = 'notes/timeline';
-			this.connection = markRaw(stream.useChannel('homeTimeline'));
-			this.connection.on('note', prepend);
-
-			this.connection2 = markRaw(stream.useChannel('main'));
-			this.connection2.on('follow', onChangeFollowing);
-			this.connection2.on('unfollow', onChangeFollowing);
-		} else if (this.src == 'local') {
-			endpoint = 'notes/local-timeline';
-			this.connection = markRaw(stream.useChannel('localTimeline'));
-			this.connection.on('note', prepend);
-		} else if (this.src == 'social') {
-			endpoint = 'notes/hybrid-timeline';
-			this.connection = markRaw(stream.useChannel('hybridTimeline'));
-			this.connection.on('note', prepend);
-		} else if (this.src == 'global') {
-			endpoint = 'notes/global-timeline';
-			this.connection = markRaw(stream.useChannel('globalTimeline'));
-			this.connection.on('note', prepend);
-		}
-
-		this.pagination = {
-			endpoint: endpoint,
-			limit: 10,
-			params: init => ({
-				untilDate: this.date?.getTime(),
-				...this.baseQuery, ...this.query
-			})
-		};
-	},
-
-	mounted() {
-
-	},
-
-	beforeUnmount() {
-		this.connection.dispose();
-		if (this.connection2) this.connection2.dispose();
-	},
-
-	methods: {
-		focus() {
-			this.$refs.body.focus();
-		},
-
-		goTop() {
-			const container = getScrollContainer(this.$refs.body);
-			container.scrollTop = 0;
-		},
-
-		queueUpdated(q) {
-			if (this.$refs.body.offsetWidth !== 0) {
-				const rect = this.$refs.body.getBoundingClientRect();
-				this.width = this.$refs.body.offsetWidth;
-				this.top = rect.top;
-				this.bottom = this.$refs.body.offsetHeight;
-			}
-			this.queue = q;
-		},
-
-		timetravel(date?: Date) {
-			this.date = date;
-			this.$refs.tl.reload();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.dbiokgaf {
-	display: flex;
-	flex-direction: column;
-	flex: 1;
-	overflow: auto;
-
-	> .info {
-		padding: 16px 16px 0 16px;
-	}
-
-	> .top {
-		padding: 16px 16px 0 16px;
-	}
-
-	> .bottom {
-		padding: 0 16px 16px 16px;
-		position: relative;
-
-		> .typers {
-			position: absolute;
-			bottom: 100%;
-			padding: 0 8px 0 8px;
-			font-size: 0.9em;
-			background: var(--panel);
-			border-radius: 0 8px 0 0;
-			color: var(--fgTransparentWeak);
-
-			> .users {
-				> .user + .user:before {
-					content: ", ";
-					font-weight: normal;
-				}
-
-				> .user:last-of-type:after {
-					content: " ";
-				}
-			}
-		}
-	}
-
-	> .tl {
-		position: relative;
-		padding: 16px 0;
-		flex: 1;
-		min-width: 0;
-		overflow: auto;
-
-		> .new {
-			position: fixed;
-			z-index: 1000;
-
-			> button {
-				display: block;
-				margin: 16px auto;
-				padding: 8px 16px;
-				border-radius: 32px;
-			}
-		}
-	}
-}
-</style>
diff --git a/packages/client/src/ui/chat/post-form.vue b/packages/client/src/ui/chat/post-form.vue
deleted file mode 100644
index 0f04096653..0000000000
--- a/packages/client/src/ui/chat/post-form.vue
+++ /dev/null
@@ -1,770 +0,0 @@
-<template>
-<div class="pxiwixjf"
-	@dragover.stop="onDragover"
-	@dragenter="onDragenter"
-	@dragleave="onDragleave"
-	@drop.stop="onDrop"
->
-	<div class="form">
-		<div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ $ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
-		<div v-if="visibility === 'specified'" class="to-specified">
-			<span style="margin-right: 8px;">{{ $ts.recipient }}</span>
-			<div class="visibleUsers">
-				<span v-for="u in visibleUsers" :key="u.id">
-					<MkAcct :user="u"/>
-					<button class="_button" @click="removeVisibleUser(u)"><i class="fas fa-times"></i></button>
-				</span>
-				<button class="_buttonPrimary" @click="addVisibleUser"><i class="fas fa-plus fa-fw"></i></button>
-			</div>
-		</div>
-		<input v-show="useCw" ref="cw" v-model="cw" class="cw" :placeholder="$ts.annotation" @keydown="onKeydown">
-		<textarea ref="text" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
-		<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
-		<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
-		<footer>
-			<div class="left">
-				<button v-tooltip="$ts.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
-				<button v-tooltip="$ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button>
-				<button v-tooltip="$ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button>
-				<button v-tooltip="$ts.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button>
-				<button v-tooltip="$ts.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
-				<button v-if="postFormActions.length > 0" v-tooltip="$ts.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button>
-			</div>
-			<div class="right">
-				<span class="text-count" :class="{ over: textLength > max }">{{ max - textLength }}</span>
-				<span v-if="localOnly" class="local-only"><i class="fas fa-biohazard"></i></span>
-				<button ref="visibilityButton" v-tooltip="$ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
-					<span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span>
-					<span v-if="visibility === 'home'"><i class="fas fa-home"></i></span>
-					<span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span>
-					<span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span>
-				</button>
-				<button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button>
-			</div>
-		</footer>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import insertTextAtCursor from 'insert-text-at-cursor';
-import { length } from 'stringz';
-import { toASCII } from 'punycode/';
-import * as mfm from 'mfm-js';
-import { host, url } from '@/config';
-import { erase, unique } from '@/scripts/array';
-import { extractMentions } from '@/scripts/extract-mentions';
-import * as Acct from 'misskey-js/built/acct';
-import { formatTimeString } from '@/scripts/format-time-string';
-import { Autocomplete } from '@/scripts/autocomplete';
-import * as os from '@/os';
-import { stream } from '@/stream';
-import { selectFiles } from '@/scripts/select-file';
-import { notePostInterruptors, postFormActions } from '@/store';
-import { throttle } from 'throttle-debounce';
-
-export default defineComponent({
-	components: {
-		XPostFormAttaches: defineAsyncComponent(() => import('@/components/post-form-attaches.vue')),
-		XPollEditor: defineAsyncComponent(() => import('@/components/poll-editor.vue'))
-	},
-
-	props: {
-		reply: {
-			type: Object,
-			required: false
-		},
-		renote: {
-			type: Object,
-			required: false
-		},
-		channel: {
-			type: String,
-			required: false
-		},
-		mention: {
-			type: Object,
-			required: false
-		},
-		specified: {
-			type: Object,
-			required: false
-		},
-		initialText: {
-			type: String,
-			required: false
-		},
-		initialNote: {
-			type: Object,
-			required: false
-		},
-		share: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		autofocus: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	emits: ['posted', 'cancel', 'esc'],
-
-	data() {
-		return {
-			posting: false,
-			text: '',
-			files: [],
-			poll: null,
-			useCw: false,
-			cw: null,
-			localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly,
-			visibility: this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility,
-			visibleUsers: [],
-			autocomplete: null,
-			draghover: false,
-			quoteId: null,
-			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
-			imeText: '',
-			typing: throttle(3000, () => {
-				if (this.channel) {
-					stream.send('typingOnChannel', { channel: this.channel });
-				}
-			}),
-			postFormActions,
-		};
-	},
-
-	computed: {
-		draftKey(): string {
-			let key = this.channel ? `channel:${this.channel}` : '';
-
-			if (this.renote) {
-				key += `renote:${this.renote.id}`;
-			} else if (this.reply) {
-				key += `reply:${this.reply.id}`;
-			} else {
-				key += 'note';
-			}
-
-			return key;
-		},
-
-		placeholder(): string {
-			if (this.renote) {
-				return this.$ts._postForm.quotePlaceholder;
-			} else if (this.reply) {
-				return this.$ts._postForm.replyPlaceholder;
-			} else if (this.channel) {
-				return this.$ts._postForm.channelPlaceholder;
-			} else {
-				const xs = [
-					this.$ts._postForm._placeholders.a,
-					this.$ts._postForm._placeholders.b,
-					this.$ts._postForm._placeholders.c,
-					this.$ts._postForm._placeholders.d,
-					this.$ts._postForm._placeholders.e,
-					this.$ts._postForm._placeholders.f
-				];
-				return xs[Math.floor(Math.random() * xs.length)];
-			}
-		},
-
-		submitText(): string {
-			return this.renote
-				? this.$ts.quote
-				: this.reply
-					? this.$ts.reply
-					: this.$ts.note;
-		},
-
-		textLength(): number {
-			return length((this.text + this.imeText).trim());
-		},
-
-		canPost(): boolean {
-			return !this.posting &&
-				(1 <= this.textLength || 1 <= this.files.length || !!this.poll || !!this.renote) &&
-				(this.textLength <= this.max) &&
-				(!this.poll || this.poll.choices.length >= 2);
-		},
-
-		max(): number {
-			return this.$instance ? this.$instance.maxNoteTextLength : 1000;
-		}
-	},
-
-	mounted() {
-		if (this.initialText) {
-			this.text = this.initialText;
-		}
-
-		if (this.mention) {
-			this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
-			this.text += ' ';
-		}
-
-		if (this.reply && (this.reply.user.username != this.$i.username || (this.reply.user.host != null && this.reply.user.host != host))) {
-			this.text = `@${this.reply.user.username}${this.reply.user.host != null ? '@' + toASCII(this.reply.user.host) : ''} `;
-		}
-
-		if (this.reply && this.reply.text != null) {
-			const ast = mfm.parse(this.reply.text);
-
-			for (const x of extractMentions(ast)) {
-				const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
-
-				// 自分は除外
-				if (this.$i.username == x.username && x.host == null) continue;
-				if (this.$i.username == x.username && x.host == host) continue;
-
-				// 重複は除外
-				if (this.text.indexOf(`${mention} `) != -1) continue;
-
-				this.text += `${mention} `;
-			}
-		}
-
-		if (this.channel) {
-			this.visibility = 'public';
-			this.localOnly = true; // TODO: チャンネルが連合するようになった折には消す
-		}
-
-		// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
-		if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
-			this.visibility = this.reply.visibility;
-			if (this.reply.visibility === 'specified') {
-				os.api('users/show', {
-					userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$i.id && uid !== this.reply.userId)
-				}).then(users => {
-					this.visibleUsers.push(...users);
-				});
-
-				if (this.reply.userId !== this.$i.id) {
-					os.api('users/show', { userId: this.reply.userId }).then(user => {
-						this.visibleUsers.push(user);
-					});
-				}
-			}
-		}
-
-		if (this.specified) {
-			this.visibility = 'specified';
-			this.visibleUsers.push(this.specified);
-		}
-
-		// keep cw when reply
-		if (this.$store.state.keepCw && this.reply && this.reply.cw) {
-			this.useCw = true;
-			this.cw = this.reply.cw;
-		}
-
-		if (this.autofocus) {
-			this.focus();
-
-			this.$nextTick(() => {
-				this.focus();
-			});
-		}
-
-		// TODO: detach when unmount
-		new Autocomplete(this.$refs.text, this, { model: 'text' });
-		new Autocomplete(this.$refs.cw, this, { model: 'cw' });
-
-		this.$nextTick(() => {
-			// 書きかけの投稿を復元
-			if (!this.share && !this.mention && !this.specified) {
-				const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
-				if (draft) {
-					this.text = draft.data.text;
-					this.useCw = draft.data.useCw;
-					this.cw = draft.data.cw;
-					this.visibility = draft.data.visibility;
-					this.localOnly = draft.data.localOnly;
-					this.files = (draft.data.files || []).filter(e => e);
-					if (draft.data.poll) {
-						this.poll = draft.data.poll;
-					}
-				}
-			}
-
-			// 削除して編集
-			if (this.initialNote) {
-				const init = this.initialNote;
-				this.text = init.text ? init.text : '';
-				this.files = init.files;
-				this.cw = init.cw;
-				this.useCw = init.cw != null;
-				if (init.poll) {
-					this.poll = init.poll;
-				}
-				this.visibility = init.visibility;
-				this.localOnly = init.localOnly;
-				this.quoteId = init.renote ? init.renote.id : null;
-			}
-
-			this.$nextTick(() => this.watch());
-		});
-	},
-
-	methods: {
-		watch() {
-			this.$watch('text', () => this.saveDraft());
-			this.$watch('useCw', () => this.saveDraft());
-			this.$watch('cw', () => this.saveDraft());
-			this.$watch('poll', () => this.saveDraft());
-			this.$watch('files', () => this.saveDraft(), { deep: true });
-			this.$watch('visibility', () => this.saveDraft());
-			this.$watch('localOnly', () => this.saveDraft());
-		},
-
-		togglePoll() {
-			if (this.poll) {
-				this.poll = null;
-			} else {
-				this.poll = {
-					choices: ['', ''],
-					multiple: false,
-					expiresAt: null,
-					expiredAfter: null,
-				};
-			}
-		},
-
-		addTag(tag: string) {
-			insertTextAtCursor(this.$refs.text, ` #${tag} `);
-		},
-
-		focus() {
-			(this.$refs.text as any).focus();
-		},
-
-		chooseFileFrom(ev) {
-			selectFiles(ev.currentTarget || ev.target, this.$ts.attachFile).then(files => {
-				for (const file of files) {
-					this.files.push(file);
-				}
-			});
-		},
-
-		detachFile(id) {
-			this.files = this.files.filter(x => x.id != id);
-		},
-
-		updateFiles(files) {
-			this.files = files;
-		},
-
-		updateFileSensitive(file, sensitive) {
-			this.files[this.files.findIndex(x => x.id === file.id)].isSensitive = sensitive;
-		},
-
-		updateFileName(file, name) {
-			this.files[this.files.findIndex(x => x.id === file.id)].name = name;
-		},
-
-		upload(file: File, name?: string) {
-			os.upload(file, this.$store.state.uploadFolder, name).then(res => {
-				this.files.push(res);
-			});
-		},
-
-		onPollUpdate(poll) {
-			this.poll = poll;
-			this.saveDraft();
-		},
-
-		setVisibility() {
-			if (this.channel) {
-				// TODO: information dialog
-				return;
-			}
-
-			os.popup(import('@/components/visibility-picker.vue'), {
-				currentVisibility: this.visibility,
-				currentLocalOnly: this.localOnly,
-				src: this.$refs.visibilityButton
-			}, {
-				changeVisibility: visibility => {
-					this.visibility = visibility;
-					if (this.$store.state.rememberNoteVisibility) {
-						this.$store.set('visibility', visibility);
-					}
-				},
-				changeLocalOnly: localOnly => {
-					this.localOnly = localOnly;
-					if (this.$store.state.rememberNoteVisibility) {
-						this.$store.set('localOnly', localOnly);
-					}
-				}
-			}, 'closed');
-		},
-
-		addVisibleUser() {
-			os.selectUser().then(user => {
-				this.visibleUsers.push(user);
-			});
-		},
-
-		removeVisibleUser(user) {
-			this.visibleUsers = erase(user, this.visibleUsers);
-		},
-
-		clear() {
-			this.text = '';
-			this.files = [];
-			this.poll = null;
-			this.quoteId = null;
-		},
-
-		onKeydown(e: KeyboardEvent) {
-			if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
-			if (e.which === 27) this.$emit('esc');
-			this.typing();
-		},
-
-		onCompositionUpdate(e: CompositionEvent) {
-			this.imeText = e.data;
-			this.typing();
-		},
-
-		onCompositionEnd(e: CompositionEvent) {
-			this.imeText = '';
-		},
-
-		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.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
-					this.upload(file, formatted);
-				}
-			}
-
-			const paste = e.clipboardData.getData('text');
-
-			if (!this.renote && !this.quoteId && paste.startsWith(url + '/notes/')) {
-				e.preventDefault();
-
-				os.confirm({
-					type: 'info',
-					text: this.$ts.quoteQuestion,
-				}).then(({ canceled }) => {
-					if (canceled) {
-						insertTextAtCursor(this.$refs.text, paste);
-						return;
-					}
-
-					this.quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
-				});
-			}
-		},
-
-		onDragover(e) {
-			if (!e.dataTransfer.items[0]) return;
-			const isFile = e.dataTransfer.items[0].kind == 'file';
-			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
-			if (isFile || isDriveFile) {
-				e.preventDefault();
-				this.draghover = true;
-				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
-			}
-		},
-
-		onDragenter(e) {
-			this.draghover = true;
-		},
-
-		onDragleave(e) {
-			this.draghover = false;
-		},
-
-		onDrop(e): void {
-			this.draghover = false;
-
-			// ファイルだったら
-			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(_DATA_TRANSFER_DRIVE_FILE_);
-			if (driveFile != null && driveFile != '') {
-				const file = JSON.parse(driveFile);
-				this.files.push(file);
-				e.preventDefault();
-			}
-			//#endregion
-		},
-
-		saveDraft() {
-			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
-
-			data[this.draftKey] = {
-				updatedAt: new Date(),
-				data: {
-					text: this.text,
-					useCw: this.useCw,
-					cw: this.cw,
-					visibility: this.visibility,
-					localOnly: this.localOnly,
-					files: this.files,
-					poll: this.poll
-				}
-			};
-
-			localStorage.setItem('drafts', JSON.stringify(data));
-		},
-
-		deleteDraft() {
-			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
-
-			delete data[this.draftKey];
-
-			localStorage.setItem('drafts', JSON.stringify(data));
-		},
-
-		async post() {
-			let data = {
-				text: this.text == '' ? undefined : this.text,
-				fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
-				replyId: this.reply ? this.reply.id : undefined,
-				renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined,
-				channelId: this.channel ? this.channel : undefined,
-				poll: this.poll,
-				cw: this.useCw ? this.cw || '' : undefined,
-				localOnly: this.localOnly,
-				visibility: this.visibility,
-				visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
-			};
-
-			// plugin
-			if (notePostInterruptors.length > 0) {
-				for (const interruptor of notePostInterruptors) {
-					data = await interruptor.handler(JSON.parse(JSON.stringify(data)));
-				}
-			}
-
-			this.posting = true;
-			os.api('notes/create', data).then(() => {
-				this.clear();
-				this.$nextTick(() => {
-					this.deleteDraft();
-					this.$emit('posted');
-					if (this.text && this.text != '') {
-						const hashtags = mfm.parse(this.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
-						const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
-						localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
-					}
-					this.posting = false;
-				});
-			}).catch(err => {
-				this.posting = false;
-				os.alert({
-					type: 'error',
-					text: err.message + '\n' + (err as any).id,
-				});
-			});
-		},
-
-		cancel() {
-			this.$emit('cancel');
-		},
-
-		insertMention() {
-			os.selectUser().then(user => {
-				insertTextAtCursor(this.$refs.text, '@' + Acct.toString(user) + ' ');
-			});
-		},
-
-		async insertEmoji(ev) {
-			os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
-		},
-
-		showActions(ev) {
-			os.popupMenu(postFormActions.map(action => ({
-				text: action.title,
-				action: () => {
-					action.handler({
-						text: this.text
-					}, (key, value) => {
-						if (key === 'text') { this.text = value; }
-					});
-				}
-			})), ev.currentTarget || ev.target);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.pxiwixjf {
-	position: relative;
-	border: solid 0.5px var(--divider);
-	border-radius: 8px;
-
-	> .form {
-		> .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;
-
-			> .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(--X4);
-
-					> button {
-						padding: 4px 8px;
-					}
-				}
-			}
-		}
-
-		> .cw,
-		> .text {
-			display: block;
-			box-sizing: border-box;
-			padding: 16px;
-			margin: 0;
-			width: 100%;
-			font-size: 16px;
-			border: none;
-			border-radius: 0;
-			background: transparent;
-			color: var(--fg);
-			font-family: inherit;
-
-			&:focus {
-				outline: none;
-			}
-
-			&:disabled {
-				opacity: 0.5;
-			}
-		}
-
-		> .cw {
-			z-index: 1;
-			padding-bottom: 8px;
-			border-bottom: solid 0.5px var(--divider);
-		}
-
-		> .text {
-			max-width: 100%;
-			min-width: 100%;
-			min-height: 60px;
-
-			&.withCw {
-				padding-top: 8px;
-			}
-		}
-
-		> footer {
-			$height: 44px;
-			display: flex;
-			padding: 0 8px 8px 8px;
-			line-height: $height;
-
-			> .left {
-				> button {
-					display: inline-block;
-					padding: 0;
-					margin: 0;
-					font-size: 16px;
-					width: $height;
-					height: $height;
-					border-radius: 6px;
-
-					&:hover {
-						background: var(--X5);
-					}
-
-					&.active {
-						color: var(--accent);
-					}
-				}
-			}
-
-			> .right {
-				margin-left: auto;
-
-				> .text-count {
-					opacity: 0.7;
-				}
-
-				> .visibility {
-					width: $height;
-					margin: 0 8px;
-
-					& + .localOnly {
-						margin-left: 0 !important;
-					}
-				}
-				
-				> .local-only {
-					margin: 0 0 0 12px;
-					opacity: 0.7;
-				}
-
-				> .submit {
-					margin: 0;
-					padding: 0 12px;
-					line-height: 34px;
-					font-weight: bold;
-					border-radius: 4px;
-
-					&:disabled {
-						opacity: 0.7;
-					}
-
-					> i {
-						margin-left: 6px;
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/packages/client/src/ui/chat/side.vue b/packages/client/src/ui/chat/side.vue
deleted file mode 100644
index 548a46102b..0000000000
--- a/packages/client/src/ui/chat/side.vue
+++ /dev/null
@@ -1,157 +0,0 @@
-<template>
-<div v-if="component" class="mrajymqm _narrow_">
-	<header class="header" @contextmenu.prevent.stop="onContextmenu">
-		<MkHeader class="title" :info="pageInfo" :center="false"/>
-	</header>
-	<component :is="component" v-bind="props" :ref="changePage" class="body"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@/os';
-import copyToClipboard from '@/scripts/copy-to-clipboard';
-import { resolve } from '@/router';
-import { url } from '@/config';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
-	components: {
-	},
-
-	provide() {
-		return {
-			navHook: (path) => {
-				this.navigate(path);
-			}
-		};
-	},
-
-	data() {
-		return {
-			path: null,
-			component: null,
-			props: {},
-			pageInfo: null,
-			history: [],
-		};
-	},
-
-	computed: {
-		url(): string {
-			return url + this.path;
-		}
-	},
-
-	methods: {
-		changePage(page) {
-			if (page == null) return;
-			if (page[symbols.PAGE_INFO]) {
-				this.pageInfo = page[symbols.PAGE_INFO];
-			}
-		},
-
-		navigate(path, record = true) {
-			if (record && this.path) this.history.push(this.path);
-			this.path = path;
-			const { component, props } = resolve(path);
-			this.component = component;
-			this.props = props;
-			this.$emit('open');
-		},
-
-		back() {
-			this.navigate(this.history.pop(), false);
-		},
-
-		close() {
-			this.path = null;
-			this.component = null;
-			this.props = {};
-			this.$emit('close');
-		},
-
-		onContextmenu(e) {
-			os.contextMenu([{
-				type: 'label',
-				text: this.path,
-			}, {
-				icon: 'fas fa-expand-alt',
-				text: this.$ts.showInPage,
-				action: () => {
-					this.$router.push(this.path);
-					this.close();
-				}
-			}, {
-				icon: 'fas fa-window-maximize',
-				text: this.$ts.openInWindow,
-				action: () => {
-					os.pageWindow(this.path);
-					this.close();
-				}
-			}, null, {
-				icon: 'fas fa-external-link-alt',
-				text: this.$ts.openInNewTab,
-				action: () => {
-					window.open(this.url, '_blank');
-					this.close();
-				}
-			}, {
-				icon: 'fas fa-link',
-				text: this.$ts.copyLink,
-				action: () => {
-					copyToClipboard(this.url);
-				}
-			}], e);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mrajymqm {
-	$header-height: 54px; // TODO: どこかに集約したい
-
-	--root-margin: 16px;
-	--margin: var(--marginHalf);
-
-	height: 100%;
-	overflow: auto;
-	box-sizing: border-box;
-
-	> .header {
-		display: flex;
-		position: sticky;
-		z-index: 1000;
-		top: 0;
-		height: $header-height;
-		width: 100%;
-		font-weight: bold;
-		//background-color: var(--panel);
-		-webkit-backdrop-filter: var(--blur, blur(32px));
-		backdrop-filter: var(--blur, blur(32px));
-		background-color: var(--header);
-		border-bottom: solid 0.5px var(--divider);
-		box-sizing: border-box;
-
-		> ._button {
-			height: $header-height;
-			width: $header-height;
-
-			&:hover {
-				color: var(--fgHighlighted);
-			}
-		}
-
-		> .title {
-			flex: 1;
-			position: relative;
-		}
-	}
-
-	> .body {
-
-	}
-}
-</style>
-
diff --git a/packages/client/src/ui/chat/store.ts b/packages/client/src/ui/chat/store.ts
deleted file mode 100644
index 389d56afb6..0000000000
--- a/packages/client/src/ui/chat/store.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { markRaw } from 'vue';
-import { Storage } from '../../pizzax';
-
-export const store = markRaw(new Storage('chatUi', {
-	widgets: {
-		where: 'account',
-		default: [] as {
-			name: string;
-			id: string;
-			data: Record<string, any>;
-		}[]
-	},
-	tl: {
-		where: 'deviceAccount',
-		default: 'home'
-	},
-}));
diff --git a/packages/client/src/ui/chat/sub-note-content.vue b/packages/client/src/ui/chat/sub-note-content.vue
deleted file mode 100644
index a85096ebc9..0000000000
--- a/packages/client/src/ui/chat/sub-note-content.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<template>
-<div class="wrmlmaau">
-	<div class="body">
-		<span v-if="note.isHidden" style="opacity: 0.5">({{ $ts.private }})</span>
-		<span v-if="note.deletedAt" style="opacity: 0.5">({{ $ts.deleted }})</span>
-		<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA>
-		<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
-		<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
-	</div>
-	<details v-if="note.files.length > 0">
-		<summary>({{ $t('withNFiles', { n: note.files.length }) }})</summary>
-		<XMediaList :media-list="note.files"/>
-	</details>
-	<details v-if="note.poll">
-		<summary>{{ $ts.poll }}</summary>
-		<XPoll :note="note"/>
-	</details>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XPoll from '@/components/poll.vue';
-import XMediaList from '@/components/media-list.vue';
-import * as os from '@/os';
-
-export default defineComponent({
-	components: {
-		XPoll,
-		XMediaList,
-	},
-	props: {
-		note: {
-			type: Object,
-			required: true
-		}
-	},
-	data() {
-		return {
-		};
-	}
-});
-</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/packages/client/src/ui/chat/widgets.vue b/packages/client/src/ui/chat/widgets.vue
deleted file mode 100644
index 337d5a7b58..0000000000
--- a/packages/client/src/ui/chat/widgets.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<template>
-<div class="qydbhufi">
-	<XWidgets :edit="edit" :widgets="widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
-
-	<button v-if="edit" class="_textButton" style="font-size: 0.9em;" @click="edit = false">{{ $ts.editWidgetsExit }}</button>
-	<button v-else class="_textButton" style="font-size: 0.9em;" @click="edit = true">{{ $ts.editWidgets }}</button>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import XWidgets from '@/components/widgets.vue';
-import { store } from './store';
-
-export default defineComponent({
-	components: {
-		XWidgets,
-	},
-
-	data() {
-		return {
-			edit: false,
-			widgets: store.reactiveState.widgets
-		};
-	},
-
-	methods: {
-		addWidget(widget) {
-			store.set('widgets', [widget, ...store.state.widgets]);
-		},
-
-		removeWidget(widget) {
-			store.set('widgets', store.state.widgets.filter(w => w.id != widget.id));
-		},
-
-		updateWidget({ id, data }) {
-			// TODO: throttleしたい
-			store.set('widgets', store.state.widgets.map(w => w.id === id ? {
-				...w,
-				data: data
-			} : w));
-		},
-
-		updateWidgets(widgets) {
-			store.set('widgets', widgets);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.qydbhufi {
-	height: 100%;
-	box-sizing: border-box;
-	overflow: auto;
-	padding: var(--margin);
-
-	::v-deep(._panel) {
-		box-shadow: none;
-	}
-}
-</style>
diff --git a/packages/client/src/ui/classic.header.vue b/packages/client/src/ui/classic.header.vue
index 3563e8a888..699b992668 100644
--- a/packages/client/src/ui/classic.header.vue
+++ b/packages/client/src/ui/classic.header.vue
@@ -105,7 +105,11 @@ export default defineComponent({
 			}, 'closed');
 		},
 
-		openAccountMenu,
+		openAccountMenu:(ev) => {
+			openAccountMenu({
+				withExtraOperation: true,
+			}, ev);
+		},
 	}
 });
 </script>
diff --git a/packages/client/src/ui/classic.side.vue b/packages/client/src/ui/classic.side.vue
index ede658626a..f816834141 100644
--- a/packages/client/src/ui/classic.side.vue
+++ b/packages/client/src/ui/classic.side.vue
@@ -72,7 +72,7 @@ export default defineComponent({
 			this.props = {};
 		},
 
-		onContextmenu(e) {
+		onContextmenu(ev: MouseEvent) {
 			os.contextMenu([{
 				type: 'label',
 				text: this.path,
@@ -103,7 +103,7 @@ export default defineComponent({
 				action: () => {
 					copyToClipboard(this.url);
 				}
-			}], e);
+			}], ev);
 		}
 	}
 });
diff --git a/packages/client/src/ui/classic.sidebar.vue b/packages/client/src/ui/classic.sidebar.vue
index cc9d7a9b48..afbca06c8e 100644
--- a/packages/client/src/ui/classic.sidebar.vue
+++ b/packages/client/src/ui/classic.sidebar.vue
@@ -125,7 +125,11 @@ export default defineComponent({
 			}, 'closed');
 		},
 
-		openAccountMenu,
+		openAccountMenu:(ev) => {
+			openAccountMenu({
+				withExtraOperation: true,
+			}, ev);
+		},
 	}
 });
 </script>
diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue
index 41da973152..1603ea6399 100644
--- a/packages/client/src/ui/classic.vue
+++ b/packages/client/src/ui/classic.vue
@@ -167,15 +167,15 @@ export default defineComponent({
 			if (window._scroll) window._scroll();
 		},
 
-		onContextmenu(e) {
+		onContextmenu(ev: MouseEvent) {
 			const isLink = (el: HTMLElement) => {
 				if (el.tagName === 'A') return true;
 				if (el.parentElement) {
 					return isLink(el.parentElement);
 				}
 			};
-			if (isLink(e.target)) return;
-			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
+			if (isLink(ev.target)) return;
+			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
 			if (window.getSelection().toString() !== '') return;
 			const path = this.$route.path;
 			os.contextMenu([{
@@ -193,7 +193,7 @@ export default defineComponent({
 				action: () => {
 					os.pageWindow(path);
 				}
-			}], e);
+			}], ev);
 		},
 
 		onAiClick(ev) {
diff --git a/packages/client/src/ui/deck/column.vue b/packages/client/src/ui/deck/column.vue
index d3c7cf8213..f1ce3ca838 100644
--- a/packages/client/src/ui/deck/column.vue
+++ b/packages/client/src/ui/deck/column.vue
@@ -207,8 +207,8 @@ export default defineComponent({
 			return items;
 		},
 
-		onContextmenu(e) {
-			os.contextMenu(this.getMenu(), e);
+		onContextmenu(ev: MouseEvent) {
+			os.contextMenu(this.getMenu(), ev);
 		},
 
 		goTop() {
@@ -224,7 +224,7 @@ export default defineComponent({
 
 			// Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう
 			// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
-			setTimeout(() => {
+			window.setTimeout(() => {
 				this.dragging = true;
 			}, 10);
 		},
diff --git a/packages/client/src/ui/deck/direct-column.vue b/packages/client/src/ui/deck/direct-column.vue
index 6ef733dfd0..ca70f693c3 100644
--- a/packages/client/src/ui/deck/direct-column.vue
+++ b/packages/client/src/ui/deck/direct-column.vue
@@ -2,43 +2,26 @@
 <XColumn :column="column" :is-stacked="isStacked">
 	<template #header><i class="fas fa-envelope" style="margin-right: 8px;"></i>{{ column.name }}</template>
 
-	<XNotes :pagination="pagination" @before="before()" @after="after()"/>
+	<XNotes :pagination="pagination"/>
 </XColumn>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
 import XColumn from './column.vue';
 import XNotes from '@/components/notes.vue';
 import * as os from '@/os';
 
-export default defineComponent({
-	components: {
-		XColumn,
-		XNotes
-	},
+const props = defineProps<{
+	column: Record<string, unknown>; // TODO
+	isStacked: boolean;
+}>();
 
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			pagination: {
-				endpoint: 'notes/mentions',
-				limit: 10,
-				params: () => ({
-					visibility: 'specified'
-				})
-			},
-		}
-	},
-});
+const pagination = {
+	point: 'notes/mentions' as const,
+	limit: 10,
+	params: computed(() => ({
+		visibility: 'specified' as const,
+	})),
+};
 </script>
diff --git a/packages/client/src/ui/deck/main-column.vue b/packages/client/src/ui/deck/main-column.vue
index 744056881c..b77d6f35cc 100644
--- a/packages/client/src/ui/deck/main-column.vue
+++ b/packages/client/src/ui/deck/main-column.vue
@@ -64,15 +64,15 @@ export default defineComponent({
 			history.back();
 		},
 
-		onContextmenu(e) {
+		onContextmenu(ev: MouseEvent) {
 			const isLink = (el: HTMLElement) => {
 				if (el.tagName === 'A') return true;
 				if (el.parentElement) {
 					return isLink(el.parentElement);
 				}
 			};
-			if (isLink(e.target)) return;
-			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
+			if (isLink(ev.target)) return;
+			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
 			if (window.getSelection().toString() !== '') return;
 			const path = this.$route.path;
 			os.contextMenu([{
@@ -84,7 +84,7 @@ export default defineComponent({
 				action: () => {
 					os.pageWindow(path);
 				}
-			}], e);
+			}], ev);
 		},
 	}
 });
diff --git a/packages/client/src/ui/deck/mentions-column.vue b/packages/client/src/ui/deck/mentions-column.vue
index 4b8dc0c4ee..6822e7ef06 100644
--- a/packages/client/src/ui/deck/mentions-column.vue
+++ b/packages/client/src/ui/deck/mentions-column.vue
@@ -2,40 +2,23 @@
 <XColumn :column="column" :is-stacked="isStacked">
 	<template #header><i class="fas fa-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
 
-	<XNotes :pagination="pagination" @before="before()" @after="after()"/>
+	<XNotes :pagination="pagination"/>
 </XColumn>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import XColumn from './column.vue';
 import XNotes from '@/components/notes.vue';
 import * as os from '@/os';
 
-export default defineComponent({
-	components: {
-		XColumn,
-		XNotes
-	},
+const props = defineProps<{
+	column: Record<string, unknown>; // TODO
+	isStacked: boolean;
+}>();
 
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			pagination: {
-				endpoint: 'notes/mentions',
-				limit: 10,
-			},
-		}
-	},
-});
+const pagination = {
+	endpoint: 'notes/mentions' as const,
+	limit: 10,
+};
 </script>
diff --git a/packages/client/src/widgets/activity.vue b/packages/client/src/widgets/activity.vue
index d322f4758a..acbbb7a97a 100644
--- a/packages/client/src/widgets/activity.vue
+++ b/packages/client/src/widgets/activity.vue
@@ -1,82 +1,89 @@
 <template>
-<MkContainer :show-header="props.showHeader" :naked="props.transparent">
+<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent">
 	<template #header><i class="fas fa-chart-bar"></i>{{ $ts._widgets.activity }}</template>
 	<template #func><button class="_button" @click="toggleView()"><i class="fas fa-sort"></i></button></template>
 
 	<div>
 		<MkLoading v-if="fetching"/>
 		<template v-else>
-			<XCalendar v-show="props.view === 0" :data="[].concat(activity)"/>
-			<XChart v-show="props.view === 1" :data="[].concat(activity)"/>
+			<XCalendar v-show="widgetProps.view === 0" :data="[].concat(activity)"/>
+			<XChart v-show="widgetProps.view === 1" :data="[].concat(activity)"/>
 		</template>
 	</div>
 </MkContainer>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
+import * as os from '@/os';
 import MkContainer from '@/components/ui/container.vue';
-import define from './define';
 import XCalendar from './activity.calendar.vue';
 import XChart from './activity.chart.vue';
-import * as os from '@/os';
+import { $i } from '@/account';
 
-const widget = define({
-	name: 'activity',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-		transparent: {
-			type: 'boolean',
-			default: false,
-		},
-		view: {
-			type: 'number',
-			default: 0,
-			hidden: true,
-		},
-	})
+const name = 'activity';
+
+const widgetPropsDef = {
+	showHeader: {
+		type: 'boolean' as const,
+		default: true,
+	},
+	transparent: {
+		type: 'boolean' as const,
+		default: false,
+	},
+	view: {
+		type: 'number' as const,
+		default: 0,
+		hidden: true,
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure, save } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const activity = ref(null);
+const fetching = ref(true);
+
+const toggleView = () => {
+	if (widgetProps.view === 1) {
+		widgetProps.view = 0;
+	} else {
+		widgetProps.view++;
+	}
+	save();
+};
+
+os.api('charts/user/notes', {
+	userId: $i.id,
+	span: 'day',
+	limit: 7 * 21,
+}).then(res => {
+	activity.value = res.diffs.normal.map((_, i) => ({
+		total: res.diffs.normal[i] + res.diffs.reply[i] + res.diffs.renote[i],
+		notes: res.diffs.normal[i],
+		replies: res.diffs.reply[i],
+		renotes: res.diffs.renote[i]
+	}));
+	fetching.value = false;
 });
 
-export default defineComponent({
-	components: {
-		MkContainer,
-		XCalendar,
-		XChart,
-	},
-	extends: widget,
-	data() {
-		return {
-			fetching: true,
-			activity: null,
-		};
-	},
-	mounted() {
-		os.api('charts/user/notes', {
-			userId: this.$i.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: {
-		toggleView() {
-			if (this.props.view === 1) {
-				this.props.view = 0;
-			} else {
-				this.props.view++;
-			}
-			this.save();
-		}
-	}
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
diff --git a/packages/client/src/widgets/aichan.vue b/packages/client/src/widgets/aichan.vue
index 891b7454d1..03e394b976 100644
--- a/packages/client/src/widgets/aichan.vue
+++ b/packages/client/src/widgets/aichan.vue
@@ -1,51 +1,65 @@
 <template>
-<MkContainer :naked="props.transparent" :show-header="false">
+<MkContainer :naked="widgetProps.transparent" :show-header="false">
 	<iframe ref="live2d" class="dedjhjmo" src="https://misskey-dev.github.io/mascot-web/?scale=1.5&y=1.1&eyeY=100" @click="touched"></iframe>
 </MkContainer>
 </template>
 
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import define from './define';
-import MkContainer from '@/components/ui/container.vue';
-import * as os from '@/os';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, reactive, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 
-const widget = define({
-	name: 'ai',
-	props: () => ({
-		transparent: {
-			type: 'boolean',
-			default: false,
-		},
-	})
+const name = 'ai';
+
+const widgetPropsDef = {
+	transparent: {
+		type: 'boolean' as const,
+		default: false,
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const live2d = ref<HTMLIFrameElement>();
+
+const touched = () => {
+	//if (this.live2d) this.live2d.changeExpression('gurugurume');
+};
+
+onMounted(() => {
+	const onMousemove = (ev: MouseEvent) => {
+		const iframeRect = live2d.value.getBoundingClientRect();
+		live2d.value.contentWindow.postMessage({
+			type: 'moveCursor',
+			body: {
+				x: ev.clientX - iframeRect.left,
+				y: ev.clientY - iframeRect.top,
+			}
+		}, '*');
+	};
+
+	window.addEventListener('mousemove', onMousemove, { passive: true });
+	onUnmounted(() => {
+		window.removeEventListener('mousemove', onMousemove);
+	});
 });
 
-export default defineComponent({
-	components: {
-		MkContainer,
-	},
-	extends: widget,
-	data() {
-		return {
-		};
-	},
-	mounted() {
-		window.addEventListener('mousemove', ev => {
-			const iframeRect = this.$refs.live2d.getBoundingClientRect();
-			this.$refs.live2d.contentWindow.postMessage({
-				type: 'moveCursor',
-				body: {
-					x: ev.clientX - iframeRect.left,
-					y: ev.clientY - iframeRect.top,
-				}
-			}, '*');
-		}, { passive: true });
-	},
-	methods: {
-		touched() {
-			//if (this.live2d) this.live2d.changeExpression('gurugurume');
-		}
-	}
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
 
diff --git a/packages/client/src/widgets/aiscript.vue b/packages/client/src/widgets/aiscript.vue
index 46c5094ee9..0a5c0d614d 100644
--- a/packages/client/src/widgets/aiscript.vue
+++ b/packages/client/src/widgets/aiscript.vue
@@ -1,9 +1,9 @@
 <template>
-<MkContainer :show-header="props.showHeader">
+<MkContainer :show-header="widgetProps.showHeader">
 	<template #header><i class="fas fa-terminal"></i>{{ $ts._widgets.aiscript }}</template>
 
 	<div class="uylguesu _monospace">
-		<textarea v-model="props.script" placeholder="(1 + 1)"></textarea>
+		<textarea v-model="widgetProps.script" placeholder="(1 + 1)"></textarea>
 		<button class="_buttonPrimary" @click="run">RUN</button>
 		<div class="logs">
 			<div v-for="log in logs" :key="log.id" class="log" :class="{ print: log.print }">{{ log.text }}</div>
@@ -12,97 +12,109 @@
 </MkContainer>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkContainer from '@/components/ui/container.vue';
-import define from './define';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref, watch } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 import * as os from '@/os';
+import MkContainer from '@/components/ui/container.vue';
 import { AiScript, parse, utils } from '@syuilo/aiscript';
 import { createAiScriptEnv } from '@/scripts/aiscript/api';
+import { $i } from '@/account';
 
-const widget = define({
-	name: 'aiscript',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-		script: {
-			type: 'string',
-			multiline: true,
-			default: '(1 + 1)',
-			hidden: true,
-		},
-	})
-});
+const name = 'aiscript';
 
-export default defineComponent({
-	components: {
-		MkContainer
+const widgetPropsDef = {
+	showHeader: {
+		type: 'boolean' as const,
+		default: true,
 	},
-	extends: widget,
-
-	data() {
-		return {
-			logs: [],
-		};
+	script: {
+		type: 'string' as const,
+		multiline: true,
+		default: '(1 + 1)',
+		hidden: true,
 	},
+};
 
-	methods: {
-		async run() {
-			this.logs = [];
-			const aiscript = new AiScript(createAiScriptEnv({
-				storageKey: 'widget',
-				token: this.$i?.token,
-			}), {
-				in: (q) => {
-					return new Promise(ok => {
-						os.inputText({
-							title: q,
-						}).then(({ canceled, result: a }) => {
-							ok(a);
-						});
-					});
-				},
-				out: (value) => {
-					this.logs.push({
-						id: Math.random(),
-						text: value.type === 'str' ? value.value : utils.valToString(value),
-						print: true
-					});
-				},
-				log: (type, params) => {
-					switch (type) {
-						case 'end': this.logs.push({
-							id: Math.random(),
-							text: utils.valToString(params.val, true),
-							print: false
-						}); break;
-						default: break;
-					}
-				}
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const logs = ref<{
+	id: string;
+	text: string;
+	print: boolean;
+}[]>([]);
+
+const run = async () => {
+	logs.value = [];
+	const aiscript = new AiScript(createAiScriptEnv({
+		storageKey: 'widget',
+		token: $i?.token,
+	}), {
+		in: (q) => {
+			return new Promise(ok => {
+				os.inputText({
+					title: q,
+				}).then(({ canceled, result: a }) => {
+					ok(a);
+				});
 			});
-
-			let ast;
-			try {
-				ast = parse(this.props.script);
-			} catch (e) {
-				os.alert({
-					type: 'error',
-					text: 'Syntax error :('
-				});
-				return;
-			}
-			try {
-				await aiscript.exec(ast);
-			} catch (e) {
-				os.alert({
-					type: 'error',
-					text: e
-				});
-			}
 		},
+		out: (value) => {
+			logs.value.push({
+				id: Math.random().toString(),
+				text: value.type === 'str' ? value.value : utils.valToString(value),
+				print: true,
+			});
+		},
+		log: (type, params) => {
+			switch (type) {
+				case 'end': logs.value.push({
+					id: Math.random().toString(),
+					text: utils.valToString(params.val, true),
+					print: false,
+				}); break;
+				default: break;
+			}
+		}
+	});
+
+	let ast;
+	try {
+		ast = parse(widgetProps.script);
+	} catch (e) {
+		os.alert({
+			type: 'error',
+			text: 'Syntax error :(',
+		});
+		return;
 	}
+	try {
+		await aiscript.exec(ast);
+	} catch (e) {
+		os.alert({
+			type: 'error',
+			text: e,
+		});
+	}
+};
+
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
 
diff --git a/packages/client/src/widgets/button.vue b/packages/client/src/widgets/button.vue
index e98570862e..a33afd6e7a 100644
--- a/packages/client/src/widgets/button.vue
+++ b/packages/client/src/widgets/button.vue
@@ -1,90 +1,99 @@
 <template>
 <div class="mkw-button">
-	<MkButton :primary="props.colored" full @click="run">
-		{{ props.label }}
+	<MkButton :primary="widgetProps.colored" full @click="run">
+		{{ widgetProps.label }}
 	</MkButton>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-import define from './define';
-import MkButton from '@/components/ui/button.vue';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref, watch } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 import * as os from '@/os';
 import { AiScript, parse, utils } from '@syuilo/aiscript';
 import { createAiScriptEnv } from '@/scripts/aiscript/api';
+import { $i } from '@/account';
+import MkButton from '@/components/ui/button.vue';
 
-const widget = define({
-	name: 'button',
-	props: () => ({
-		label: {
-			type: 'string',
-			default: 'BUTTON',
-		},
-		colored: {
-			type: 'boolean',
-			default: true,
-		},
-		script: {
-			type: 'string',
-			multiline: true,
-			default: 'Mk:dialog("hello" "world")',
-		},
-	})
-});
+const name = 'button';
 
-export default defineComponent({
-	components: {
-		MkButton
+const widgetPropsDef = {
+	label: {
+		type: 'string' as const,
+		default: 'BUTTON',
 	},
-	extends: widget,
-	data() {
-		return {
-		};
+	colored: {
+		type: 'boolean' as const,
+		default: true,
 	},
-	methods: {
-		async run() {
-			const aiscript = new AiScript(createAiScriptEnv({
-				storageKey: 'widget',
-				token: this.$i?.token,
-			}), {
-				in: (q) => {
-					return new Promise(ok => {
-						os.inputText({
-							title: q,
-						}).then(({ canceled, result: a }) => {
-							ok(a);
-						});
-					});
-				},
-				out: (value) => {
-					// nop
-				},
-				log: (type, params) => {
-					// nop
-				}
+	script: {
+		type: 'string' as const,
+		multiline: true,
+		default: 'Mk:dialog("hello" "world")',
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const run = async () => {
+	const aiscript = new AiScript(createAiScriptEnv({
+		storageKey: 'widget',
+		token: $i?.token,
+	}), {
+		in: (q) => {
+			return new Promise(ok => {
+				os.inputText({
+					title: q,
+				}).then(({ canceled, result: a }) => {
+					ok(a);
+				});
 			});
-
-			let ast;
-			try {
-				ast = parse(this.props.script);
-			} catch (e) {
-				os.alert({
-					type: 'error',
-					text: 'Syntax error :('
-				});
-				return;
-			}
-			try {
-				await aiscript.exec(ast);
-			} catch (e) {
-				os.alert({
-					type: 'error',
-					text: e
-				});
-			}
 		},
+		out: (value) => {
+			// nop
+		},
+		log: (type, params) => {
+			// nop
+		}
+	});
+
+	let ast;
+	try {
+		ast = parse(widgetProps.script);
+	} catch (e) {
+		os.alert({
+			type: 'error',
+			text: 'Syntax error :(',
+		});
+		return;
 	}
+	try {
+		await aiscript.exec(ast);
+	} catch (e) {
+		os.alert({
+			type: 'error',
+			text: e,
+		});
+	}
+};
+
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
 
diff --git a/packages/client/src/widgets/calendar.vue b/packages/client/src/widgets/calendar.vue
index c8b52d7afc..b0e3edcb12 100644
--- a/packages/client/src/widgets/calendar.vue
+++ b/packages/client/src/widgets/calendar.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mkw-calendar" :class="{ _panel: !props.transparent }">
+<div class="mkw-calendar" :class="{ _panel: !widgetProps.transparent }">
 	<div class="calendar" :class="{ isHoliday }">
 		<p class="month-and-year">
 			<span class="year">{{ $t('yearX', { year }) }}</span>
@@ -32,77 +32,87 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-import define from './define';
+<script lang="ts" setup>
+import { onUnmounted, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
+import { i18n } from '@/i18n';
 
-const widget = define({
-	name: 'calendar',
-	props: () => ({
-		transparent: {
-			type: 'boolean',
-			default: false,
-		},
-	})
+const name = 'calendar';
+
+const widgetPropsDef = {
+	transparent: {
+		type: 'boolean' as const,
+		default: false,
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const year = ref(0);
+const month = ref(0);
+const day = ref(0);
+const weekDay = ref('');
+const yearP = ref(0);
+const monthP = ref(0);
+const dayP = ref(0);
+const isHoliday = ref(false);
+const tick = () => {
+	const now = new Date();
+	const nd = now.getDate();
+	const nm = now.getMonth();
+	const ny = now.getFullYear();
+
+	year.value = ny;
+	month.value = nm + 1;
+	day.value = nd;
+	weekDay.value = [
+		i18n.locale._weekday.sunday,
+		i18n.locale._weekday.monday,
+		i18n.locale._weekday.tuesday,
+		i18n.locale._weekday.wednesday,
+		i18n.locale._weekday.thursday,
+		i18n.locale._weekday.friday,
+		i18n.locale._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();
+
+	dayP.value   = dayNumer   / dayDenom   * 100;
+	monthP.value = monthNumer / monthDenom * 100;
+	yearP.value  = yearNumer  / yearDenom  * 100;
+
+	isHoliday.value = now.getDay() === 0 || now.getDay() === 6;
+};
+
+tick();
+
+const intervalId = window.setInterval(tick, 1000);
+onUnmounted(() => {
+	window.clearInterval(intervalId);
 });
 
-export default defineComponent({
-	extends: widget,
-	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);
-	},
-	beforeUnmount() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		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.$ts._weekday.sunday,
-				this.$ts._weekday.monday,
-				this.$ts._weekday.tuesday,
-				this.$ts._weekday.wednesday,
-				this.$ts._weekday.thursday,
-				this.$ts._weekday.friday,
-				this.$ts._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;
-		}
-	}
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
 
diff --git a/packages/client/src/widgets/clock.vue b/packages/client/src/widgets/clock.vue
index 6ca7ecd430..6acb10d74d 100644
--- a/packages/client/src/widgets/clock.vue
+++ b/packages/client/src/widgets/clock.vue
@@ -1,45 +1,56 @@
 <template>
-<MkContainer :naked="props.transparent" :show-header="false">
+<MkContainer :naked="widgetProps.transparent" :show-header="false">
 	<div class="vubelbmv">
-		<MkAnalogClock class="clock" :thickness="props.thickness"/>
+		<MkAnalogClock class="clock" :thickness="widgetProps.thickness"/>
 	</div>
 </MkContainer>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-import define from './define';
+<script lang="ts" setup>
+import { } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 import MkContainer from '@/components/ui/container.vue';
 import MkAnalogClock from '@/components/analog-clock.vue';
-import * as os from '@/os';
 
-const widget = define({
-	name: 'clock',
-	props: () => ({
-		transparent: {
-			type: 'boolean',
-			default: false,
-		},
-		thickness: {
-			type: 'radio',
-			default: 0.1,
-			options: [{
-				value: 0.1, label: 'thin'
-			}, {
-				value: 0.2, label: 'medium'
-			}, {
-				value: 0.3, label: 'thick'
-			}]
-		}
-	})
-});
+const name = 'clock';
 
-export default defineComponent({
-	components: {
-		MkContainer,
-		MkAnalogClock
+const widgetPropsDef = {
+	transparent: {
+		type: 'boolean' as const,
+		default: false,
 	},
-	extends: widget,
+	thickness: {
+		type: 'radio' as const,
+		default: 0.1,
+		options: [{
+			value: 0.1, label: 'thin'
+		}, {
+			value: 0.2, label: 'medium'
+		}, {
+			value: 0.3, label: 'thick'
+		}],
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
 
diff --git a/packages/client/src/widgets/define.ts b/packages/client/src/widgets/define.ts
deleted file mode 100644
index 08a346d97c..0000000000
--- a/packages/client/src/widgets/define.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { defineComponent } from 'vue';
-import { throttle } from 'throttle-debounce';
-import { Form } from '@/scripts/form';
-import * as os from '@/os';
-
-export default function <T extends Form>(data: {
-	name: string;
-	props?: () => T;
-}) {
-	return defineComponent({
-		props: {
-			widget: {
-				type: Object,
-				required: false
-			},
-			settingCallback: {
-				required: false
-			}
-		},
-
-		emits: ['updateProps'],
-
-		data() {
-			return {
-				props: this.widget ? JSON.parse(JSON.stringify(this.widget.data)) : {},
-				save: throttle(3000, () => {
-					this.$emit('updateProps', this.props);
-				}),
-			};
-		},
-
-		computed: {
-			id(): string {
-				return this.widget ? this.widget.id : null;
-			},
-		},
-
-		created() {
-			this.mergeProps();
-
-			this.$watch('props', () => {
-				this.mergeProps();
-			}, { deep: true });
-
-			if (this.settingCallback) this.settingCallback(this.setting);
-		},
-
-		methods: {
-			mergeProps() {
-				if (data.props) {
-					const defaultProps = data.props();
-					for (const prop of Object.keys(defaultProps)) {
-						if (this.props.hasOwnProperty(prop)) continue;
-						this.props[prop] = defaultProps[prop].default;
-					}
-				}
-			},
-
-			async setting() {
-				const form = data.props();
-				for (const item of Object.keys(form)) {
-					form[item].default = this.props[item];
-				}
-				const { canceled, result } = await os.form(data.name, form);
-				if (canceled) return;
-
-				for (const key of Object.keys(result)) {
-					this.props[key] = result[key];
-				}
-
-				this.save();
-			},
-		}
-	});
-}
diff --git a/packages/client/src/widgets/digital-clock.vue b/packages/client/src/widgets/digital-clock.vue
index fbf632d2de..62f052a692 100644
--- a/packages/client/src/widgets/digital-clock.vue
+++ b/packages/client/src/widgets/digital-clock.vue
@@ -1,73 +1,84 @@
 <template>
-<div class="mkw-digitalClock _monospace" :class="{ _panel: !props.transparent }" :style="{ fontSize: `${props.fontSize}em` }">
+<div class="mkw-digitalClock _monospace" :class="{ _panel: !widgetProps.transparent }" :style="{ fontSize: `${widgetProps.fontSize}em` }">
 	<span>
 		<span v-text="hh"></span>
 		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
 		<span v-text="mm"></span>
 		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
 		<span v-text="ss"></span>
-		<span v-if="props.showMs" :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
-		<span v-if="props.showMs" v-text="ms"></span>
+		<span v-if="widgetProps.showMs" :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
+		<span v-if="widgetProps.showMs" v-text="ms"></span>
 	</span>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-import define from './define';
-import * as os from '@/os';
+<script lang="ts" setup>
+import { onUnmounted, ref, watch } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 
-const widget = define({
-	name: 'digitalClock',
-	props: () => ({
-		transparent: {
-			type: 'boolean',
-			default: false,
-		},
-		fontSize: {
-			type: 'number',
-			default: 1.5,
-			step: 0.1,
-		},
-		showMs: {
-			type: 'boolean',
-			default: true,
-		},
-	})
+const name = 'digitalClock';
+
+const widgetPropsDef = {
+	transparent: {
+		type: 'boolean' as const,
+		default: false,
+	},
+	fontSize: {
+		type: 'number' as const,
+		default: 1.5,
+		step: 0.1,
+	},
+	showMs: {
+		type: 'boolean' as const,
+		default: true,
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+let intervalId;
+const hh = ref('');
+const mm = ref('');
+const ss = ref('');
+const ms = ref('');
+const showColon = ref(true);
+const tick = () => {
+	const now = new Date();
+	hh.value = now.getHours().toString().padStart(2, '0');
+	mm.value = now.getMinutes().toString().padStart(2, '0');
+	ss.value = now.getSeconds().toString().padStart(2, '0');
+	ms.value = Math.floor(now.getMilliseconds() / 10).toString().padStart(2, '0');
+	showColon.value = now.getSeconds() % 2 === 0;
+};
+
+tick();
+
+watch(() => widgetProps.showMs, () => {
+	if (intervalId) window.clearInterval(intervalId);
+	intervalId = window.setInterval(tick, widgetProps.showMs ? 10 : 1000);
+}, { immediate: true });
+
+onUnmounted(() => {
+	window.clearInterval(intervalId);
 });
 
-export default defineComponent({
-	extends: widget,
-	data() {
-		return {
-			clock: null,
-			hh: null,
-			mm: null,
-			ss: null,
-			ms: null,
-			showColon: true,
-		};
-	},
-	created() {
-		this.tick();
-		this.$watch(() => this.props.showMs, () => {
-			if (this.clock) clearInterval(this.clock);
-			this.clock = setInterval(this.tick, this.props.showMs ? 10 : 1000);
-		}, { immediate: true });
-	},
-	beforeUnmount() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		tick() {
-			const now = new Date();
-			this.hh = now.getHours().toString().padStart(2, '0');
-			this.mm = now.getMinutes().toString().padStart(2, '0');
-			this.ss = now.getSeconds().toString().padStart(2, '0');
-			this.ms = Math.floor(now.getMilliseconds() / 10).toString().padStart(2, '0');
-			this.showColon = now.getSeconds() % 2 === 0;
-		}
-	}
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
 
diff --git a/packages/client/src/widgets/federation.vue b/packages/client/src/widgets/federation.vue
index 736a91c52e..ed7350188e 100644
--- a/packages/client/src/widgets/federation.vue
+++ b/packages/client/src/widgets/federation.vue
@@ -1,5 +1,5 @@
 <template>
-<MkContainer :show-header="props.showHeader" :foldable="foldable" :scrollable="scrollable">
+<MkContainer :show-header="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable">
 	<template #header><i class="fas fa-globe"></i>{{ $ts._widgets.federation }}</template>
 
 	<div class="wbrkwalb">
@@ -18,66 +18,64 @@
 </MkContainer>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 import MkContainer from '@/components/ui/container.vue';
-import define from './define';
 import MkMiniChart from '@/components/mini-chart.vue';
 import * as os from '@/os';
 
-const widget = define({
-	name: 'federation',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-	})
+const name = 'federation';
+
+const widgetPropsDef = {
+	showHeader: {
+		type: 'boolean' as const,
+		default: true,
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps> & { foldable?: boolean; scrollable?: boolean; }>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; foldable?: boolean; scrollable?: boolean; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const instances = ref([]);
+const charts = ref([]);
+const fetching = ref(true);
+
+const fetch = async () => {
+	const instances = await os.api('federation/instances', {
+		sort: '+lastCommunicatedAt',
+		limit: 5
+	});
+	const charts = await Promise.all(instances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
+	instances.value = instances;
+	charts.value = charts;
+	fetching.value = false;
+};
+
+onMounted(() => {
+	fetch();
+	const intervalId = window.setInterval(fetch, 1000 * 60);
+	onUnmounted(() => {
+		window.clearInterval(intervalId);
+	});
 });
 
-export default defineComponent({
-	components: {
-		MkContainer, MkMiniChart
-	},
-	extends: widget,
-	props: {
-		foldable: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		scrollable: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-	data() {
-		return {
-			instances: [],
-			charts: [],
-			fetching: true,
-		};
-	},
-	mounted() {
-		this.fetch();
-		this.clock = setInterval(this.fetch, 1000 * 60);
-	},
-	beforeUnmount() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		async fetch() {
-			const instances = await os.api('federation/instances', {
-				sort: '+lastCommunicatedAt',
-				limit: 5
-			});
-			const charts = await Promise.all(instances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
-			this.instances = instances;
-			this.charts = charts;
-			this.fetching = false;
-		}
-	}
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
 
diff --git a/packages/client/src/widgets/job-queue.vue b/packages/client/src/widgets/job-queue.vue
index 1b7c71de67..4a2a3cf233 100644
--- a/packages/client/src/widgets/job-queue.vue
+++ b/packages/client/src/widgets/job-queue.vue
@@ -1,134 +1,146 @@
 <template>
-<div class="mkw-jobQueue _monospace" :class="{ _panel: !props.transparent }">
+<div class="mkw-jobQueue _monospace" :class="{ _panel: !widgetProps.transparent }">
 	<div class="inbox">
-		<div class="label">Inbox queue<i v-if="inbox.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div>
+		<div class="label">Inbox queue<i v-if="current.inbox.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div>
 		<div class="values">
 			<div>
 				<div>Process</div>
-				<div :class="{ inc: inbox.activeSincePrevTick > prev.inbox.activeSincePrevTick, dec: inbox.activeSincePrevTick < prev.inbox.activeSincePrevTick }">{{ number(inbox.activeSincePrevTick) }}</div>
+				<div :class="{ inc: current.inbox.activeSincePrevTick > prev.inbox.activeSincePrevTick, dec: current.inbox.activeSincePrevTick < prev.inbox.activeSincePrevTick }">{{ number(current.inbox.activeSincePrevTick) }}</div>
 			</div>
 			<div>
 				<div>Active</div>
-				<div :class="{ inc: inbox.active > prev.inbox.active, dec: inbox.active < prev.inbox.active }">{{ number(inbox.active) }}</div>
+				<div :class="{ inc: current.inbox.active > prev.inbox.active, dec: current.inbox.active < prev.inbox.active }">{{ number(current.inbox.active) }}</div>
 			</div>
 			<div>
 				<div>Delayed</div>
-				<div :class="{ inc: inbox.delayed > prev.inbox.delayed, dec: inbox.delayed < prev.inbox.delayed }">{{ number(inbox.delayed) }}</div>
+				<div :class="{ inc: current.inbox.delayed > prev.inbox.delayed, dec: current.inbox.delayed < prev.inbox.delayed }">{{ number(current.inbox.delayed) }}</div>
 			</div>
 			<div>
 				<div>Waiting</div>
-				<div :class="{ inc: inbox.waiting > prev.inbox.waiting, dec: inbox.waiting < prev.inbox.waiting }">{{ number(inbox.waiting) }}</div>
+				<div :class="{ inc: current.inbox.waiting > prev.inbox.waiting, dec: current.inbox.waiting < prev.inbox.waiting }">{{ number(current.inbox.waiting) }}</div>
 			</div>
 		</div>
 	</div>
 	<div class="deliver">
-		<div class="label">Deliver queue<i v-if="deliver.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div>
+		<div class="label">Deliver queue<i v-if="current.deliver.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div>
 		<div class="values">
 			<div>
 				<div>Process</div>
-				<div :class="{ inc: deliver.activeSincePrevTick > prev.deliver.activeSincePrevTick, dec: deliver.activeSincePrevTick < prev.deliver.activeSincePrevTick }">{{ number(deliver.activeSincePrevTick) }}</div>
+				<div :class="{ inc: current.deliver.activeSincePrevTick > prev.deliver.activeSincePrevTick, dec: current.deliver.activeSincePrevTick < prev.deliver.activeSincePrevTick }">{{ number(current.deliver.activeSincePrevTick) }}</div>
 			</div>
 			<div>
 				<div>Active</div>
-				<div :class="{ inc: deliver.active > prev.deliver.active, dec: deliver.active < prev.deliver.active }">{{ number(deliver.active) }}</div>
+				<div :class="{ inc: current.deliver.active > prev.deliver.active, dec: current.deliver.active < prev.deliver.active }">{{ number(current.deliver.active) }}</div>
 			</div>
 			<div>
 				<div>Delayed</div>
-				<div :class="{ inc: deliver.delayed > prev.deliver.delayed, dec: deliver.delayed < prev.deliver.delayed }">{{ number(deliver.delayed) }}</div>
+				<div :class="{ inc: current.deliver.delayed > prev.deliver.delayed, dec: current.deliver.delayed < prev.deliver.delayed }">{{ number(current.deliver.delayed) }}</div>
 			</div>
 			<div>
 				<div>Waiting</div>
-				<div :class="{ inc: deliver.waiting > prev.deliver.waiting, dec: deliver.waiting < prev.deliver.waiting }">{{ number(deliver.waiting) }}</div>
+				<div :class="{ inc: current.deliver.waiting > prev.deliver.waiting, dec: current.deliver.waiting < prev.deliver.waiting }">{{ number(current.deliver.waiting) }}</div>
 			</div>
 		</div>
 	</div>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import define from './define';
-import * as os from '@/os';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, reactive, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 import { stream } from '@/stream';
 import number from '@/filters/number';
 import * as sound from '@/scripts/sound';
+import * as os from '@/os';
 
-const widget = define({
-	name: 'jobQueue',
-	props: () => ({
-		transparent: {
-			type: 'boolean',
-			default: false,
-		},
-		sound: {
-			type: 'boolean',
-			default: false,
-		},
-	})
+const name = 'jobQueue';
+
+const widgetPropsDef = {
+	transparent: {
+		type: 'boolean' as const,
+		default: false,
+	},
+	sound: {
+		type: 'boolean' as const,
+		default: false,
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const connection = stream.useChannel('queueStats');
+const current = reactive({
+	inbox: {
+		activeSincePrevTick: 0,
+		active: 0,
+		waiting: 0,
+		delayed: 0,
+	},
+	deliver: {
+		activeSincePrevTick: 0,
+		active: 0,
+		waiting: 0,
+		delayed: 0,
+	},
+});
+const prev = reactive({} as typeof current);
+const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1);
+
+for (const domain of ['inbox', 'deliver']) {
+	prev[domain] = JSON.parse(JSON.stringify(current[domain]));
+}
+
+const onStats = (stats) => {
+	for (const domain of ['inbox', 'deliver']) {
+		prev[domain] = JSON.parse(JSON.stringify(current[domain]));
+		current[domain].activeSincePrevTick = stats[domain].activeSincePrevTick;
+		current[domain].active = stats[domain].active;
+		current[domain].waiting = stats[domain].waiting;
+		current[domain].delayed = stats[domain].delayed;
+
+		if (current[domain].waiting > 0 && widgetProps.sound && jammedSound.paused) {
+			jammedSound.play();
+		}
+	}
+};
+
+const onStatsLog = (statsLog) => {
+	for (const stats of [...statsLog].reverse()) {
+		onStats(stats);
+	}
+};
+
+connection.on('stats', onStats);
+connection.on('statsLog', onStatsLog);
+
+connection.send('requestLog', {
+	id: Math.random().toString().substr(2, 8),
+	length: 1,
 });
 
-export default defineComponent({
-	extends: widget,
-	data() {
-		return {
-			connection: markRaw(stream.useChannel('queueStats')),
-			inbox: {
-				activeSincePrevTick: 0,
-				active: 0,
-				waiting: 0,
-				delayed: 0,
-			},
-			deliver: {
-				activeSincePrevTick: 0,
-				active: 0,
-				waiting: 0,
-				delayed: 0,
-			},
-			prev: {},
-			sound: sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1)
-		};
-	},
-	created() {
-		for (const domain of ['inbox', 'deliver']) {
-			this.prev[domain] = JSON.parse(JSON.stringify(this[domain]));
-		}
-	
-		this.connection.on('stats', this.onStats);
-		this.connection.on('statsLog', this.onStatsLog);
+onUnmounted(() => {
+	connection.off('stats', onStats);
+	connection.off('statsLog', onStatsLog);
+	connection.dispose();
+});
 
-		this.connection.send('requestLog', {
-			id: Math.random().toString().substr(2, 8),
-			length: 1
-		});
-	},
-	beforeUnmount() {
-		this.connection.off('stats', this.onStats);
-		this.connection.off('statsLog', this.onStatsLog);
-		this.connection.dispose();
-	},
-	methods: {
-		onStats(stats) {
-			for (const domain of ['inbox', 'deliver']) {
-				this.prev[domain] = JSON.parse(JSON.stringify(this[domain]));
-				this[domain].activeSincePrevTick = stats[domain].activeSincePrevTick;
-				this[domain].active = stats[domain].active;
-				this[domain].waiting = stats[domain].waiting;
-				this[domain].delayed = stats[domain].delayed;
-
-				if (this[domain].waiting > 0 && this.props.sound && this.sound.paused) {
-					this.sound.play();
-				}
-			}
-		},
-
-		onStatsLog(statsLog) {
-			for (const stats of [...statsLog].reverse()) {
-				this.onStats(stats);
-			}
-		},
-
-		number
-	}
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
 
diff --git a/packages/client/src/widgets/memo.vue b/packages/client/src/widgets/memo.vue
index 9b51ada220..450598f65a 100644
--- a/packages/client/src/widgets/memo.vue
+++ b/packages/client/src/widgets/memo.vue
@@ -1,5 +1,5 @@
 <template>
-<MkContainer :show-header="props.showHeader">
+<MkContainer :show-header="widgetProps.showHeader">
 	<template #header><i class="fas fa-sticky-note"></i>{{ $ts._widgets.memo }}</template>
 
 	<div class="otgbylcu">
@@ -9,56 +9,60 @@
 </MkContainer>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkContainer from '@/components/ui/container.vue';
-import define from './define';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 import * as os from '@/os';
+import MkContainer from '@/components/ui/container.vue';
+import { defaultStore } from '@/store';
 
-const widget = define({
-	name: 'memo',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-	})
+const name = 'memo';
+
+const widgetPropsDef = {
+	showHeader: {
+		type: 'boolean' as const,
+		default: true,
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const text = ref<string | null>(defaultStore.state.memo);
+const changed = ref(false);
+let timeoutId;
+
+const saveMemo = () => {
+	defaultStore.set('memo', text.value);
+	changed.value = false;
+};
+
+const onChange = () => {
+	changed.value = true;
+	window.clearTimeout(timeoutId);
+	timeoutId = window.setTimeout(saveMemo, 1000);
+};
+
+watch(() => defaultStore.reactiveState.memo, newText => {
+	text.value = newText.value;
 });
 
-export default defineComponent({
-	components: {
-		MkContainer
-	},
-	extends: widget,
-
-	data() {
-		return {
-			text: null,
-			changed: false,
-			timeoutId: null,
-		};
-	},
-
-	created() {
-		this.text = this.$store.state.memo;
-
-		this.$watch(() => this.$store.reactiveState.memo, text => {
-			this.text = text;
-		});
-	},
-
-	methods: {
-		onChange() {
-			this.changed = true;
-			clearTimeout(this.timeoutId);
-			this.timeoutId = setTimeout(this.saveMemo, 1000);
-		},
-
-		saveMemo() {
-			this.$store.set('memo', this.text);
-			this.changed = false;
-		}
-	}
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
 
diff --git a/packages/client/src/widgets/notifications.vue b/packages/client/src/widgets/notifications.vue
index 568705b661..8cf29c9271 100644
--- a/packages/client/src/widgets/notifications.vue
+++ b/packages/client/src/widgets/notifications.vue
@@ -1,65 +1,68 @@
 <template>
-<MkContainer :style="`height: ${props.height}px;`" :show-header="props.showHeader" :scrollable="true">
+<MkContainer :style="`height: ${widgetProps.height}px;`" :show-header="widgetProps.showHeader" :scrollable="true">
 	<template #header><i class="fas fa-bell"></i>{{ $ts.notifications }}</template>
-	<template #func><button class="_button" @click="configure()"><i class="fas fa-cog"></i></button></template>
+	<template #func><button class="_button" @click="configureNotification()"><i class="fas fa-cog"></i></button></template>
 
 	<div>
-		<XNotifications :include-types="props.includingTypes"/>
+		<XNotifications :include-types="widgetProps.includingTypes"/>
 	</div>
 </MkContainer>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 import MkContainer from '@/components/ui/container.vue';
 import XNotifications from '@/components/notifications.vue';
-import define from './define';
 import * as os from '@/os';
 
-const widget = define({
-	name: 'notifications',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-		height: {
-			type: 'number',
-			default: 300,
-		},
-		includingTypes: {
-			type: 'array',
-			hidden: true,
-			default: null,
-		},
-	})
-});
+const name = 'notifications';
 
-export default defineComponent({
-
-	components: {
-		MkContainer,
-		XNotifications,
+const widgetPropsDef = {
+	showHeader: {
+		type: 'boolean' as const,
+		default: true,
 	},
-	extends: widget,
-
-	data() {
-		return {
-		};
+	height: {
+		type: 'number' as const,
+		default: 300,
 	},
+	includingTypes: {
+		type: 'array' as const,
+		hidden: true,
+		default: null,
+	},
+};
 
-	methods: {
-		configure() {
-			os.popup(import('@/components/notification-setting-window.vue'), {
-				includingTypes: this.props.includingTypes,
-			}, {
-				done: async (res) => {
-					const { includingTypes } = res;
-					this.props.includingTypes = includingTypes;
-					this.save();
-				}
-			}, 'closed');
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure, save } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const configureNotification = () => {
+	os.popup(import('@/components/notification-setting-window.vue'), {
+		includingTypes: widgetProps.includingTypes,
+	}, {
+		done: async (res) => {
+			const { includingTypes } = res;
+			widgetProps.includingTypes = includingTypes;
+			save();
 		}
-	}
+	}, 'closed');
+};
+
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
diff --git a/packages/client/src/widgets/online-users.vue b/packages/client/src/widgets/online-users.vue
index 5b889f4816..1746a8314e 100644
--- a/packages/client/src/widgets/online-users.vue
+++ b/packages/client/src/widgets/online-users.vue
@@ -1,48 +1,60 @@
 <template>
-<div class="mkw-onlineUsers" :class="{ _panel: !props.transparent, pad: !props.transparent }">
+<div class="mkw-onlineUsers" :class="{ _panel: !widgetProps.transparent, pad: !widgetProps.transparent }">
 	<I18n v-if="onlineUsersCount" :src="$ts.onlineUsersCount" text-tag="span" class="text">
 		<template #n><b>{{ onlineUsersCount }}</b></template>
 	</I18n>
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-import define from './define';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 import * as os from '@/os';
 
-const widget = define({
-	name: 'onlineUsers',
-	props: () => ({
-		transparent: {
-			type: 'boolean',
-			default: true,
-		},
-	})
+const name = 'onlineUsers';
+
+const widgetPropsDef = {
+	transparent: {
+		type: 'boolean' as const,
+		default: true,
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const onlineUsersCount = ref(0);
+
+const tick = () => {
+	os.api('get-online-users-count').then(res => {
+		onlineUsersCount.value = res.count;
+	});
+};
+
+onMounted(() => {
+	tick();
+	const intervalId = window.setInterval(tick, 1000 * 15);
+	onUnmounted(() => {
+		window.clearInterval(intervalId);
+	});
 });
 
-export default defineComponent({
-	extends: widget,
-	data() {
-		return {
-			onlineUsersCount: null,
-			clock: null,
-		};
-	},
-	created() {
-		this.tick();
-		this.clock = setInterval(this.tick, 1000 * 15);
-	},
-	beforeUnmount() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		tick() {
-			os.api('get-online-users-count').then(res => {
-				this.onlineUsersCount = res.count;
-			});
-		}
-	}
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
 
diff --git a/packages/client/src/widgets/photos.vue b/packages/client/src/widgets/photos.vue
index 7a0b54027b..8f948dc643 100644
--- a/packages/client/src/widgets/photos.vue
+++ b/packages/client/src/widgets/photos.vue
@@ -1,5 +1,5 @@
 <template>
-<MkContainer :show-header="props.showHeader" :naked="props.transparent" :class="$style.root" :data-transparent="props.transparent ? true : null">
+<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" :class="$style.root" :data-transparent="widgetProps.transparent ? true : null">
 	<template #header><i class="fas fa-camera"></i>{{ $ts._widgets.photos }}</template>
 
 	<div class="">
@@ -14,70 +14,77 @@
 </MkContainer>
 </template>
 
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import MkContainer from '@/components/ui/container.vue';
-import define from './define';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, reactive, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
+import { stream } from '@/stream';
 import { getStaticImageUrl } from '@/scripts/get-static-image-url';
 import * as os from '@/os';
-import { stream } from '@/stream';
+import MkContainer from '@/components/ui/container.vue';
+import { defaultStore } from '@/store';
 
-const widget = define({
-	name: 'photos',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-		transparent: {
-			type: 'boolean',
-			default: false,
-		},
-	})
+const name = 'photos';
+
+const widgetPropsDef = {
+	showHeader: {
+		type: 'boolean' as const,
+		default: true,
+	},
+	transparent: {
+		type: 'boolean' as const,
+		default: false,
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const connection = stream.useChannel('main');
+const images = ref([]);
+const fetching = ref(true);
+
+const onDriveFileCreated = (file) => {
+	if (/^image\/.+$/.test(file.type)) {
+		images.value.unshift(file);
+		if (images.value.length > 9) images.value.pop();
+	}
+};
+
+const thumbnail = (image: any): string => {
+	return defaultStore.state.disableShowingAnimatedImages
+		? getStaticImageUrl(image.thumbnailUrl)
+		: image.thumbnailUrl;
+};
+
+os.api('drive/stream', {
+	type: 'image/*',
+	limit: 9
+}).then(res => {
+	images.value = res;
+	fetching.value = false;
 });
 
-export default defineComponent({
-	components: {
-		MkContainer,
-	},
-	extends: widget,
-	data() {
-		return {
-			images: [],
-			fetching: true,
-			connection: null,
-		};
-	},
-	mounted() {
-		this.connection = markRaw(stream.useChannel('main'));
+connection.on('driveFileCreated', onDriveFileCreated);
+onUnmounted(() => {
+	connection.dispose();
+});
 
-		this.connection.on('driveFileCreated', this.onDriveFileCreated);
-
-		os.api('drive/stream', {
-			type: 'image/*',
-			limit: 9
-		}).then(images => {
-			this.images = images;
-			this.fetching = false;
-		});
-	},
-	beforeUnmount() {
-		this.connection.dispose();
-	},
-	methods: {
-		onDriveFileCreated(file) {
-			if (/^image\/.+$/.test(file.type)) {
-				this.images.unshift(file);
-				if (this.images.length > 9) this.images.pop();
-			}
-		},
-
-		thumbnail(image: any): string {
-			return this.$store.state.disableShowingAnimatedImages
-				? getStaticImageUrl(image.thumbnailUrl)
-				: image.thumbnailUrl;
-		},
-	}
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
 
diff --git a/packages/client/src/widgets/post-form.vue b/packages/client/src/widgets/post-form.vue
index 6de0574cc1..51aa8fcf6b 100644
--- a/packages/client/src/widgets/post-form.vue
+++ b/packages/client/src/widgets/post-form.vue
@@ -2,22 +2,34 @@
 <XPostForm class="_panel" :fixed="true" :autofocus="false"/>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 import XPostForm from '@/components/post-form.vue';
-import define from './define';
 
-const widget = define({
-	name: 'postForm',
-	props: () => ({
-	})
-});
+const name = 'postForm';
 
-export default defineComponent({
+const widgetPropsDef = {
+};
 
-	components: {
-		XPostForm,
-	},
-	extends: widget,
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
diff --git a/packages/client/src/widgets/rss.vue b/packages/client/src/widgets/rss.vue
index b2dc77854e..9e2e503602 100644
--- a/packages/client/src/widgets/rss.vue
+++ b/packages/client/src/widgets/rss.vue
@@ -1,7 +1,7 @@
 <template>
-<MkContainer :show-header="props.showHeader">
+<MkContainer :show-header="widgetProps.showHeader">
 	<template #header><i class="fas fa-rss-square"></i>RSS</template>
-	<template #func><button class="_button" @click="setting"><i class="fas fa-cog"></i></button></template>
+	<template #func><button class="_button" @click="configure"><i class="fas fa-cog"></i></button></template>
 
 	<div class="ekmkgxbj">
 		<MkLoading v-if="fetching"/>
@@ -12,57 +12,66 @@
 </MkContainer>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkContainer from '@/components/ui/container.vue';
-import define from './define';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref, watch } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 import * as os from '@/os';
+import MkContainer from '@/components/ui/container.vue';
 
-const widget = define({
-	name: 'rss',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-		url: {
-			type: 'string',
-			default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
-		},
-	})
+const name = 'rss';
+
+const widgetPropsDef = {
+	showHeader: {
+		type: 'boolean' as const,
+		default: true,
+	},
+	url: {
+		type: 'string' as const,
+		default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const items = ref([]);
+const fetching = ref(true);
+
+const tick = () => {
+	fetch(`https://api.rss2json.com/v1/api.json?rss_url=${widgetProps.url}`, {}).then(res => {
+		res.json().then(feed => {
+			items.value = feed.items;
+			fetching.value = false;
+		});
+	});
+};
+
+watch(() => widgetProps.url, tick);
+
+onMounted(() => {
+	tick();
+	const intervalId = window.setInterval(tick, 60000);
+	onUnmounted(() => {
+		window.clearInterval(intervalId);
+	});
 });
 
-export default defineComponent({
-	components: {
-		MkContainer
-	},
-	extends: widget,
-	data() {
-		return {
-			items: [],
-			fetching: true,
-			clock: null,
-		};
-	},
-	mounted() {
-		this.fetch();
-		this.clock = setInterval(this.fetch, 60000);
-		this.$watch(() => this.props.url, this.fetch);
-	},
-	beforeUnmount() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		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;
-				});
-			});
-		},
-	}
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
 
diff --git a/packages/client/src/widgets/server-metric/disk.vue b/packages/client/src/widgets/server-metric/disk.vue
index 650101b0ee..052991b554 100644
--- a/packages/client/src/widgets/server-metric/disk.vue
+++ b/packages/client/src/widgets/server-metric/disk.vue
@@ -10,32 +10,19 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import XPie from './pie.vue';
 import bytes from '@/filters/bytes';
 
-export default defineComponent({
-	components: {
-		XPie
-	},
-	props: {
-		meta: {
-			required: true,
-		}
-	},
-	data() {
-		return {
-			usage: this.meta.fs.used / this.meta.fs.total,
-			total: this.meta.fs.total,
-			used: this.meta.fs.used,
-			available: this.meta.fs.total - this.meta.fs.used,
-		};
-	},
-	methods: {
-		bytes
-	}
-});
+const props = defineProps<{
+	meta: any; // TODO
+}>();
+
+const usage = $computed(() => props.meta.fs.used / props.meta.fs.total);
+const total = $computed(() => props.meta.fs.total);
+const used = $computed(() => props.meta.fs.used);
+const available = $computed(() => props.meta.fs.total - props.meta.fs.used);
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/widgets/server-metric/index.vue b/packages/client/src/widgets/server-metric/index.vue
index 107b750906..2caa73fa74 100644
--- a/packages/client/src/widgets/server-metric/index.vue
+++ b/packages/client/src/widgets/server-metric/index.vue
@@ -1,21 +1,22 @@
 <template>
-<MkContainer :show-header="props.showHeader" :naked="props.transparent">
+<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent">
 	<template #header><i class="fas fa-server"></i>{{ $ts._widgets.serverMetric }}</template>
 	<template #func><button class="_button" @click="toggleView()"><i class="fas fa-sort"></i></button></template>
 
 	<div v-if="meta" class="mkw-serverMetric">
-		<XCpuMemory v-if="props.view === 0" :connection="connection" :meta="meta"/>
-		<XNet v-if="props.view === 1" :connection="connection" :meta="meta"/>
-		<XCpu v-if="props.view === 2" :connection="connection" :meta="meta"/>
-		<XMemory v-if="props.view === 3" :connection="connection" :meta="meta"/>
-		<XDisk v-if="props.view === 4" :connection="connection" :meta="meta"/>
+		<XCpuMemory v-if="widgetProps.view === 0" :connection="connection" :meta="meta"/>
+		<XNet v-else-if="widgetProps.view === 1" :connection="connection" :meta="meta"/>
+		<XCpu v-else-if="widgetProps.view === 2" :connection="connection" :meta="meta"/>
+		<XMemory v-else-if="widgetProps.view === 3" :connection="connection" :meta="meta"/>
+		<XDisk v-else-if="widgetProps.view === 4" :connection="connection" :meta="meta"/>
 	</div>
 </MkContainer>
 </template>
 
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import define from '../define';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from '../widget';
 import MkContainer from '@/components/ui/container.vue';
 import XCpuMemory from './cpu-mem.vue';
 import XNet from './net.vue';
@@ -25,59 +26,61 @@ import XDisk from './disk.vue';
 import * as os from '@/os';
 import { stream } from '@/stream';
 
-const widget = define({
-	name: 'serverMetric',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-		transparent: {
-			type: 'boolean',
-			default: false,
-		},
-		view: {
-			type: 'number',
-			default: 0,
-			hidden: true,
-		},
-	})
+const name = 'serverMetric';
+
+const widgetPropsDef = {
+	showHeader: {
+		type: 'boolean' as const,
+		default: true,
+	},
+	transparent: {
+		type: 'boolean' as const,
+		default: false,
+	},
+	view: {
+		type: 'number' as const,
+		default: 0,
+		hidden: true,
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure, save } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const meta = ref(null);
+
+os.api('server-info', {}).then(res => {
+	meta.value = res;
 });
 
-export default defineComponent({
-	components: {
-		MkContainer,
-		XCpuMemory,
-		XNet,
-		XCpu,
-		XMemory,
-		XDisk,
-	},
-	extends: widget,
-	data() {
-		return {
-			meta: null,
-			connection: null,
-		};
-	},
-	created() {
-		os.api('server-info', {}).then(res => {
-			this.meta = res;
-		});
-		this.connection = markRaw(stream.useChannel('serverStats'));
-	},
-	unmounted() {
-		this.connection.dispose();
-	},
-	methods: {
-		toggleView() {
-			if (this.props.view == 4) {
-				this.props.view = 0;
-			} else {
-				this.props.view++;
-			}
-			this.save();
-		},
+const toggleView = () => {
+	if (widgetProps.view == 4) {
+		widgetProps.view = 0;
+	} else {
+		widgetProps.view++;
 	}
+	save();
+};
+
+const connection = stream.useChannel('serverStats');
+onUnmounted(() => {
+	connection.dispose();
+});
+
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
diff --git a/packages/client/src/widgets/server-metric/pie.vue b/packages/client/src/widgets/server-metric/pie.vue
index 38dcf6fcd9..868dbc0484 100644
--- a/packages/client/src/widgets/server-metric/pie.vue
+++ b/packages/client/src/widgets/server-metric/pie.vue
@@ -20,30 +20,17 @@
 </svg>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 
-export default defineComponent({
-	props: {
-		value: {
-			type: Number,
-			required: true
-		}
-	},
-	data() {
-		return {
-			r: 0.45
-		};
-	},
-	computed: {
-		color(): string {
-			return `hsl(${180 - (this.value * 180)}, 80%, 70%)`;
-		},
-		strokeDashoffset(): number {
-			return (1 - this.value) * (Math.PI * (this.r * 2));
-		}
-	}
-});
+const props = defineProps<{
+	value: number;
+}>();
+
+const r = 0.45;
+
+const color = $computed(() => `hsl(${180 - (props.value * 180)}, 80%, 70%)`);
+const strokeDashoffset = $computed(() => (1 - props.value) * (Math.PI * (r * 2)));
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/widgets/slideshow.vue b/packages/client/src/widgets/slideshow.vue
index 0909bda67c..7b2e539685 100644
--- a/packages/client/src/widgets/slideshow.vue
+++ b/packages/client/src/widgets/slideshow.vue
@@ -1,126 +1,116 @@
 <template>
-<div class="kvausudm _panel">
+<div class="kvausudm _panel" :style="{ height: widgetProps.height + 'px' }">
 	<div @click="choose">
-		<p v-if="props.folderId == null">
-			<template v-if="isCustomizeMode">{{ $t('folder-customize-mode') }}</template>
-			<template v-else>{{ $ts.folder }}</template>
+		<p v-if="widgetProps.folderId == null">
+			{{ $ts.folder }}
 		</p>
-		<p v-if="props.folderId != null && images.length === 0 && !fetching">{{ $t('no-image') }}</p>
+		<p v-if="widgetProps.folderId != null && 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 { defineComponent } from 'vue';
-import define from './define';
+<script lang="ts" setup>
+import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 import * as os from '@/os';
 
-const widget = define({
-	name: 'slideshow',
-	props: () => ({
-		height: {
-			type: 'number',
-			default: 300,
-		},
-		folderId: {
-			type: 'string',
-			default: null,
-			hidden: true,
-		},
-	})
+const name = 'slideshow';
+
+const widgetPropsDef = {
+	height: {
+		type: 'number' as const,
+		default: 300,
+	},
+	folderId: {
+		type: 'string' as const,
+		default: null,
+		hidden: true,
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure, save } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const images = ref([]);
+const fetching = ref(true);
+const slideA = ref<HTMLElement>();
+const slideB = ref<HTMLElement>();
+
+const change = () => {
+	if (images.value.length == 0) return;
+
+	const index = Math.floor(Math.random() * images.value.length);
+	const img = `url(${ images.value[index].url })`;
+
+	slideB.value.style.backgroundImage = img;
+
+	slideB.value.classList.add('anime');
+	window.setTimeout(() => {
+		// 既にこのウィジェットがunmountされていたら要素がない
+		if (slideA.value == null) return;
+
+		slideA.value.style.backgroundImage = img;
+
+		slideB.value.classList.remove('anime');
+	}, 1000);
+};
+
+const fetch = () => {
+	fetching.value = true;
+
+	os.api('drive/files', {
+		folderId: widgetProps.folderId,
+		type: 'image/*',
+		limit: 100
+	}).then(res => {
+		images.value = res;
+		fetching.value = false;
+		slideA.value.style.backgroundImage = '';
+		slideB.value.style.backgroundImage = '';
+		change();
+	});
+};
+
+const choose = () => {
+	os.selectDriveFolder(false).then(folder => {
+		if (folder == null) {
+			return;
+		}
+		widgetProps.folderId = folder.id;
+		save();
+		fetch();
+	});
+};
+
+onMounted(() => {
+	if (widgetProps.folderId != null) {
+		fetch();
+	}
+
+	const intervalId = window.setInterval(change, 10000);
+	onUnmounted(() => {
+		window.clearInterval(intervalId);
+	});
 });
 
-export default defineComponent({
-	extends: widget,
-	data() {
-		return {
-			images: [],
-			fetching: true,
-			clock: null
-		};
-	},
-	mounted() {
-		this.$nextTick(() => {
-			this.applySize();
-		});
-
-		if (this.props.folderId != null) {
-			this.fetch();
-		}
-
-		this.clock = setInterval(this.change, 10000);
-	},
-	beforeUnmount() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		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;
-
-			this.$refs.slideB.classList.add('anime');
-			setTimeout(() => {
-				// 既にこのウィジェットがunmountされていたら要素がない
-				if ((this.$refs.slideA as any) == null) return;
-
-				(this.$refs.slideA as any).style.backgroundImage = img;
-
-				this.$refs.slideB.classList.remove('anime');
-			}, 1000);
-		},
-		fetch() {
-			this.fetching = true;
-
-			os.api('drive/files', {
-				folderId: this.props.folderId,
-				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() {
-			os.selectDriveFolder(false).then(folder => {
-				if (folder == null) {
-					return;
-				}
-				this.props.folderId = folder.id;
-				this.save();
-				this.fetch();
-			});
-		}
-	}
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
 
diff --git a/packages/client/src/widgets/timeline.vue b/packages/client/src/widgets/timeline.vue
index aee6a35b1d..fa700cc8ee 100644
--- a/packages/client/src/widgets/timeline.vue
+++ b/packages/client/src/widgets/timeline.vue
@@ -1,116 +1,129 @@
 <template>
-<MkContainer :show-header="props.showHeader" :style="`height: ${props.height}px;`" :scrollable="true">
+<MkContainer :show-header="widgetProps.showHeader" :style="`height: ${widgetProps.height}px;`" :scrollable="true">
 	<template #header>
 		<button class="_button" @click="choose">
-			<i v-if="props.src === 'home'" class="fas fa-home"></i>
-			<i v-else-if="props.src === 'local'" class="fas fa-comments"></i>
-			<i v-else-if="props.src === 'social'" class="fas fa-share-alt"></i>
-			<i v-else-if="props.src === 'global'" class="fas fa-globe"></i>
-			<i v-else-if="props.src === 'list'" class="fas fa-list-ul"></i>
-			<i v-else-if="props.src === 'antenna'" class="fas fa-satellite"></i>
-			<span style="margin-left: 8px;">{{ props.src === 'list' ? props.list.name : props.src === 'antenna' ? props.antenna.name : $t('_timelines.' + props.src) }}</span>
+			<i v-if="widgetProps.src === 'home'" class="fas fa-home"></i>
+			<i v-else-if="widgetProps.src === 'local'" class="fas fa-comments"></i>
+			<i v-else-if="widgetProps.src === 'social'" class="fas fa-share-alt"></i>
+			<i v-else-if="widgetProps.src === 'global'" class="fas fa-globe"></i>
+			<i v-else-if="widgetProps.src === 'list'" class="fas fa-list-ul"></i>
+			<i v-else-if="widgetProps.src === 'antenna'" class="fas fa-satellite"></i>
+			<span style="margin-left: 8px;">{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : $t('_timelines.' + widgetProps.src) }}</span>
 			<i :class="menuOpened ? 'fas fa-angle-up' : 'fas fa-angle-down'" style="margin-left: 8px;"></i>
 		</button>
 	</template>
 
 	<div>
-		<XTimeline :key="props.src === 'list' ? `list:${props.list.id}` : props.src === 'antenna' ? `antenna:${props.antenna.id}` : props.src" :src="props.src" :list="props.list ? props.list.id : null" :antenna="props.antenna ? props.antenna.id : null"/>
+		<XTimeline :key="widgetProps.src === 'list' ? `list:${widgetProps.list.id}` : widgetProps.src === 'antenna' ? `antenna:${widgetProps.antenna.id}` : widgetProps.src" :src="widgetProps.src" :list="widgetProps.list ? widgetProps.list.id : null" :antenna="widgetProps.antenna ? widgetProps.antenna.id : null"/>
 	</div>
 </MkContainer>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
+import * as os from '@/os';
 import MkContainer from '@/components/ui/container.vue';
 import XTimeline from '@/components/timeline.vue';
-import define from './define';
-import * as os from '@/os';
+import { $i } from '@/account';
+import { i18n } from '@/i18n';
 
-const widget = define({
-	name: 'timeline',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-		height: {
-			type: 'number',
-			default: 300,
-		},
-		src: {
-			type: 'string',
-			default: 'home',
-			hidden: true,
-		},
-		list: {
-			type: 'object',
-			default: null,
-			hidden: true,
-		},
-	})
-});
+const name = 'timeline';
 
-export default defineComponent({
-	components: {
-		MkContainer,
-		XTimeline,
+const widgetPropsDef = {
+	showHeader: {
+		type: 'boolean' as const,
+		default: true,
 	},
-	extends: widget,
-
-	data() {
-		return {
-			menuOpened: false,
-		};
+	height: {
+		type: 'number' as const,
+		default: 300,
 	},
+	src: {
+		type: 'string' as const,
+		default: 'home',
+		hidden: true,
+	},
+	antenna: {
+		type: 'object' as const,
+		default: null,
+		hidden: true,
+	},
+	list: {
+		type: 'object' as const,
+		default: null,
+		hidden: true,
+	},
+};
 
-	methods: {
-		async choose(ev) {
-			this.menuOpened = true;
-			const [antennas, lists] = await Promise.all([
-				os.api('antennas/list'),
-				os.api('users/lists/list')
-			]);
-			const antennaItems = antennas.map(antenna => ({
-				text: antenna.name,
-				icon: 'fas fa-satellite',
-				action: () => {
-					this.props.antenna = antenna;
-					this.setSrc('antenna');
-				}
-			}));
-			const listItems = lists.map(list => ({
-				text: list.name,
-				icon: 'fas fa-list-ul',
-				action: () => {
-					this.props.list = list;
-					this.setSrc('list');
-				}
-			}));
-			os.popupMenu([{
-				text: this.$ts._timelines.home,
-				icon: 'fas fa-home',
-				action: () => { this.setSrc('home') }
-			}, {
-				text: this.$ts._timelines.local,
-				icon: 'fas fa-comments',
-				action: () => { this.setSrc('local') }
-			}, {
-				text: this.$ts._timelines.social,
-				icon: 'fas fa-share-alt',
-				action: () => { this.setSrc('social') }
-			}, {
-				text: this.$ts._timelines.global,
-				icon: 'fas fa-globe',
-				action: () => { this.setSrc('global') }
-			}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget || ev.target).then(() => {
-				this.menuOpened = false;
-			});
-		},
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
 
-		setSrc(src) {
-			this.props.src = src;
-			this.save();
-		},
-	}
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure, save } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const menuOpened = ref(false);
+
+const setSrc = (src) => {
+	widgetProps.src = src;
+	save();
+};
+
+const choose = async (ev) => {
+	menuOpened.value = true;
+	const [antennas, lists] = await Promise.all([
+		os.api('antennas/list'),
+		os.api('users/lists/list')
+	]);
+	const antennaItems = antennas.map(antenna => ({
+		text: antenna.name,
+		icon: 'fas fa-satellite',
+		action: () => {
+			widgetProps.antenna = antenna;
+			setSrc('antenna');
+		}
+	}));
+	const listItems = lists.map(list => ({
+		text: list.name,
+		icon: 'fas fa-list-ul',
+		action: () => {
+			widgetProps.list = list;
+			setSrc('list');
+		}
+	}));
+	os.popupMenu([{
+		text: i18n.locale._timelines.home,
+		icon: 'fas fa-home',
+		action: () => { setSrc('home') }
+	}, {
+		text: i18n.locale._timelines.local,
+		icon: 'fas fa-comments',
+		action: () => { setSrc('local') }
+	}, {
+		text: i18n.locale._timelines.social,
+		icon: 'fas fa-share-alt',
+		action: () => { setSrc('social') }
+	}, {
+		text: i18n.locale._timelines.global,
+		icon: 'fas fa-globe',
+		action: () => { setSrc('global') }
+	}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget || ev.target).then(() => {
+		menuOpened.value = false;
+	});
+};
+
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
diff --git a/packages/client/src/widgets/trends.vue b/packages/client/src/widgets/trends.vue
index ffad93c02b..5768a8d5d1 100644
--- a/packages/client/src/widgets/trends.vue
+++ b/packages/client/src/widgets/trends.vue
@@ -1,5 +1,5 @@
 <template>
-<MkContainer :show-header="props.showHeader">
+<MkContainer :show-header="widgetProps.showHeader">
 	<template #header><i class="fas fa-hashtag"></i>{{ $ts._widgets.trends }}</template>
 
 	<div class="wbrkwala">
@@ -17,49 +17,59 @@
 </MkContainer>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 import MkContainer from '@/components/ui/container.vue';
-import define from './define';
 import MkMiniChart from '@/components/mini-chart.vue';
 import * as os from '@/os';
 
-const widget = define({
-	name: 'hashtags',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-	})
+const name = 'hashtags';
+
+const widgetPropsDef = {
+	showHeader: {
+		type: 'boolean' as const,
+		default: true,
+	},
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+	widgetPropsDef,
+	props,
+	emit,
+);
+
+const stats = ref([]);
+const fetching = ref(true);
+
+const fetch = () => {
+	os.api('hashtags/trend').then(stats => {
+		stats.value = stats;
+		fetching.value = false;
+	});
+};
+
+onMounted(() => {
+	fetch();
+	const intervalId = window.setInterval(fetch, 1000 * 60);
+	onUnmounted(() => {
+		window.clearInterval(intervalId);
+	});
 });
 
-export default defineComponent({
-	components: {
-		MkContainer, MkMiniChart
-	},
-	extends: widget,
-	data() {
-		return {
-			stats: [],
-			fetching: true,
-		};
-	},
-	mounted() {
-		this.fetch();
-		this.clock = setInterval(this.fetch, 1000 * 60);
-	},
-	beforeUnmount() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		fetch() {
-			os.api('hashtags/trend').then(stats => {
-				this.stats = stats;
-				this.fetching = false;
-			});
-		}
-	}
+defineExpose<WidgetComponentExpose>({
+	name,
+	configure,
+	id: props.widget ? props.widget.id : null,
 });
 </script>
 
diff --git a/packages/client/src/widgets/widget.ts b/packages/client/src/widgets/widget.ts
new file mode 100644
index 0000000000..81239bfb3b
--- /dev/null
+++ b/packages/client/src/widgets/widget.ts
@@ -0,0 +1,71 @@
+import { reactive, watch } from 'vue';
+import { throttle } from 'throttle-debounce';
+import { Form, GetFormResultType } from '@/scripts/form';
+import * as os from '@/os';
+
+export type Widget<P extends Record<string, unknown>> = {
+	id: string;
+	data: Partial<P>;
+};
+
+export type WidgetComponentProps<P extends Record<string, unknown>> = {
+	widget?: Widget<P>;
+};
+
+export type WidgetComponentEmits<P extends Record<string, unknown>> = {
+	(e: 'updateProps', props: P);
+};
+
+export type WidgetComponentExpose = {
+	name: string;
+	id: string | null;
+	configure: () => void;
+};
+
+export const useWidgetPropsManager = <F extends Form & Record<string, { default: any; }>>(
+	name: string,
+	propsDef: F,
+	props: Readonly<WidgetComponentProps<GetFormResultType<F>>>,
+	emit: WidgetComponentEmits<GetFormResultType<F>>,
+): {
+	widgetProps: GetFormResultType<F>;
+	save: () => void;
+	configure: () => void;
+} => {
+	const widgetProps = reactive(props.widget ? JSON.parse(JSON.stringify(props.widget.data)) : {});
+
+	const mergeProps = () => {
+		for (const prop of Object.keys(propsDef)) {
+			if (widgetProps.hasOwnProperty(prop)) continue;
+			widgetProps[prop] = propsDef[prop].default;
+		}
+	};
+	watch(widgetProps, () => {
+		mergeProps();
+	}, { deep: true, immediate: true, });
+
+	const save = throttle(3000, () => {
+		emit('updateProps', widgetProps)
+	});
+
+	const configure = async () => {
+		const form = JSON.parse(JSON.stringify(propsDef));
+		for (const item of Object.keys(form)) {
+			form[item].default = widgetProps[item];
+		}
+		const { canceled, result } = await os.form(name, form);
+		if (canceled) return;
+
+		for (const key of Object.keys(result)) {
+			widgetProps[key] = result[key];
+		}
+
+		save();
+	};
+
+	return {
+		widgetProps,
+		save,
+		configure,
+	};
+};
diff --git a/packages/client/yarn.lock b/packages/client/yarn.lock
index 80cf1fc982..d983d01ba8 100644
--- a/packages/client/yarn.lock
+++ b/packages/client/yarn.lock
@@ -177,29 +177,30 @@
     "@nodelib/fs.scandir" "2.1.3"
     fastq "^1.6.0"
 
-"@redocly/ajv@^8.6.2":
-  version "8.6.2"
-  resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.6.2.tgz#8c4e485e72f7864f91fae40093bed548ec2619b2"
-  integrity sha512-tU8fQs0D76ZKhJ2cWtnfQthWqiZgGBx0gH0+5D8JvaBEBaqA8foPPBt3Nonwr3ygyv5xrw2IzKWgIY86BlGs+w==
+"@redocly/ajv@^8.6.4":
+  version "8.6.4"
+  resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.6.4.tgz#94053e7a9d4146d1a4feacd3813892873f229a85"
+  integrity sha512-y9qNj0//tZtWB2jfXNK3BX18BSBp9zNR7KE7lMysVHwbZtY392OJCjm6Rb/h4UHH2r1AqjNEHFD6bRn+DqU9Mw==
   dependencies:
     fast-deep-equal "^3.1.1"
     json-schema-traverse "^1.0.0"
     require-from-string "^2.0.2"
     uri-js "^4.2.2"
 
-"@redocly/openapi-core@1.0.0-beta.54":
-  version "1.0.0-beta.54"
-  resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.0.0-beta.54.tgz#42575a849c4dd54b9d0c6413fb8ca547e087cd11"
-  integrity sha512-uYs0N1Trjkh7u8IMIuCU2VxCXhMyGWSZUkP/WNdTR1OgBUtvNdF9C32zoQV+hyCIH4gVu42ROHkjisy333ZX+w==
+"@redocly/openapi-core@1.0.0-beta.79":
+  version "1.0.0-beta.79"
+  resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.0.0-beta.79.tgz#7512b3507ab99dc78226f9069669c5302abb0969"
+  integrity sha512-do79vGt3iiHsaVG9LKY8dH+d1E7TLHr+3T+CQ1lqagtWVjYOxqGaoxAT8tRD7R1W0z8BmS4e2poNON6c1sxP5g==
   dependencies:
-    "@redocly/ajv" "^8.6.2"
+    "@redocly/ajv" "^8.6.4"
     "@types/node" "^14.11.8"
     colorette "^1.2.0"
     js-levenshtein "^1.1.6"
-    js-yaml "^3.14.1"
+    js-yaml "^4.1.0"
     lodash.isequal "^4.5.0"
     minimatch "^3.0.4"
     node-fetch "^2.6.1"
+    pluralize "^8.0.0"
     yaml-ast-parser "0.0.43"
 
 "@sideway/address@^4.1.0":
@@ -270,10 +271,10 @@
   resolved "https://registry.yarnpkg.com/@types/dateformat/-/dateformat-3.0.1.tgz#98d747a2e5e9a56070c6bf14e27bff56204e34cc"
   integrity sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g==
 
-"@types/escape-regexp@0.0.0":
-  version "0.0.0"
-  resolved "https://registry.yarnpkg.com/@types/escape-regexp/-/escape-regexp-0.0.0.tgz#bff0225f9ef30d0dbdbe0e2a24283ee5342990c3"
-  integrity sha512-HTansGo4tJ7K7W9I9LBdQqnHtPB/Y7tlS+EMrkboaAQLsRPhRpHaqAHe01K1HVXM5e1u1IplRd8EBh+pJrp7Dg==
+"@types/escape-regexp@0.0.1":
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/@types/escape-regexp/-/escape-regexp-0.0.1.tgz#f1a977ccdf2ef059e9862bd3af5e92cbbe723e0e"
+  integrity sha512-ogj/ZTIdeFkiuxDwawYuZSIgC6suFGgBeZPr6Xs5lHEcvIXTjXGtH+/n8f1XhZhespaUwJ5LIGRICPji972FLw==
 
 "@types/eslint-scope@^3.7.0":
   version "3.7.0"
@@ -311,10 +312,10 @@
   resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5"
   integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==
 
-"@types/fluent-ffmpeg@2.1.17":
-  version "2.1.17"
-  resolved "https://registry.yarnpkg.com/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.17.tgz#6958dda400fe1b33c21f3683db76905cb210d053"
-  integrity sha512-/bdvjKw/mtBHlJ2370d04nt4CsWqU5MrwS/NtO96V01jxitJ4+iq8OFNcqc5CegeV3TQOK3uueK02kvRK+zjUg==
+"@types/fluent-ffmpeg@2.1.20":
+  version "2.1.20"
+  resolved "https://registry.yarnpkg.com/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.20.tgz#3b5f42fc8263761d58284fa46ee6759a64ce54ac"
+  integrity sha512-B+OvhCdJ3LgEq2PhvWNOiB/EfwnXLElfMCgc4Z1K5zXgSfo9I6uGKwR/lqmNPFQuebNnes7re3gqkV77SyypLg==
   dependencies:
     "@types/node" "*"
 
@@ -415,11 +416,6 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.2.tgz#331b7b9f8621c638284787c5559423822fdffc50"
   integrity sha512-LSw8TZt12ZudbpHc6EkIyDM3nHVWKYrAvGy6EAJfNfjusbwnThqjqxUKKRwuV3iWYeW/LYMzNgaq3MaLffQ2xA==
 
-"@types/node@16.11.12":
-  version "16.11.12"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.12.tgz#ac7fb693ac587ee182c3780c26eb65546a1a3c10"
-  integrity sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==
-
 "@types/node@^14.11.8", "@types/node@^14.14.31":
   version "14.17.9"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.9.tgz#b97c057e6138adb7b720df2bd0264b03c9f504fd"
@@ -476,10 +472,10 @@
   resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f"
   integrity sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA==
 
-"@types/sinonjs__fake-timers@^6.0.2":
-  version "6.0.3"
-  resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.3.tgz#79df6f358ae8f79e628fe35a63608a0ea8e7cf08"
-  integrity sha512-E1dU4fzC9wN2QK2Cr1MLCfyHM8BoNnRFvuf45LYMPNDA+WqbNzC45S4UzPxvp1fFJ1rvSGU0bPvdd35VLmXG8g==
+"@types/sinonjs__fake-timers@8.1.1":
+  version "8.1.1"
+  resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3"
+  integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==
 
 "@types/sizzle@^2.3.2":
   version "2.3.3"
@@ -530,10 +526,10 @@
   dependencies:
     "@types/undertaker-registry" "*"
 
-"@types/uuid@8.3.3":
-  version "8.3.3"
-  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.3.tgz#c6a60686d953dbd1b1d45e66f4ecdbd5d471b4d0"
-  integrity sha512-0LbEEx1zxrYB3pgpd1M5lEhLcXjKJnYghvhTRgaBeUivLHMDM1TzF3IJ6hXU2+8uA4Xz+5BA63mtZo5DjVT8iA==
+"@types/uuid@8.3.4":
+  version "8.3.4"
+  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
+  integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
 
 "@types/vinyl-fs@*":
   version "2.4.11"
@@ -618,13 +614,14 @@
   dependencies:
     "@types/node" "*"
 
-"@typescript-eslint/eslint-plugin@5.8.1":
-  version "5.8.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.8.1.tgz#97dfaa39f38e99f86801fdf34f9f1bed66704258"
-  integrity sha512-wTZ5oEKrKj/8/366qTM366zqhIKAp6NCMweoRONtfuC07OAU9nVI2GZZdqQ1qD30WAAtcPdkH+npDwtRFdp4Rw==
+"@typescript-eslint/eslint-plugin@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.10.0.tgz#e90afea96dff8620892ad216b0e4ccdf8ee32d3a"
+  integrity sha512-XXVKnMsq2fuu9K2KsIxPUGqb6xAImz8MEChClbXmE3VbveFtBUU5bzM6IPVWqzyADIgdkS2Ws/6Xo7W2TeZWjQ==
   dependencies:
-    "@typescript-eslint/experimental-utils" "5.8.1"
-    "@typescript-eslint/scope-manager" "5.8.1"
+    "@typescript-eslint/scope-manager" "5.10.0"
+    "@typescript-eslint/type-utils" "5.10.0"
+    "@typescript-eslint/utils" "5.10.0"
     debug "^4.3.2"
     functional-red-black-tree "^1.0.1"
     ignore "^5.1.8"
@@ -632,60 +629,69 @@
     semver "^7.3.5"
     tsutils "^3.21.0"
 
-"@typescript-eslint/experimental-utils@5.8.1":
-  version "5.8.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.8.1.tgz#01861eb2f0749f07d02db342b794145a66ed346f"
-  integrity sha512-fbodVnjIDU4JpeXWRDsG5IfIjYBxEvs8EBO8W1+YVdtrc2B9ppfof5sZhVEDOtgTfFHnYQJDI8+qdqLYO4ceww==
+"@typescript-eslint/parser@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.10.0.tgz#8f59e036f5f1cffc178cacbd5ccdd02aeb96c91c"
+  integrity sha512-pJB2CCeHWtwOAeIxv8CHVGJhI5FNyJAIpx5Pt72YkK3QfEzt6qAlXZuyaBmyfOdM62qU0rbxJzNToPTVeJGrQw==
   dependencies:
-    "@types/json-schema" "^7.0.9"
-    "@typescript-eslint/scope-manager" "5.8.1"
-    "@typescript-eslint/types" "5.8.1"
-    "@typescript-eslint/typescript-estree" "5.8.1"
-    eslint-scope "^5.1.1"
-    eslint-utils "^3.0.0"
-
-"@typescript-eslint/parser@5.8.1":
-  version "5.8.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.8.1.tgz#380f5f1e596b540059998aa3fc80d78f0f9b0d0a"
-  integrity sha512-K1giKHAjHuyB421SoXMXFHHVI4NdNY603uKw92++D3qyxSeYvC10CBJ/GE5Thpo4WTUvu1mmJI2/FFkz38F2Gw==
-  dependencies:
-    "@typescript-eslint/scope-manager" "5.8.1"
-    "@typescript-eslint/types" "5.8.1"
-    "@typescript-eslint/typescript-estree" "5.8.1"
+    "@typescript-eslint/scope-manager" "5.10.0"
+    "@typescript-eslint/types" "5.10.0"
+    "@typescript-eslint/typescript-estree" "5.10.0"
     debug "^4.3.2"
 
-"@typescript-eslint/scope-manager@5.8.1":
-  version "5.8.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.8.1.tgz#7fc0604f7ade8833e4d42cebaa1e2debf8b932e4"
-  integrity sha512-DGxJkNyYruFH3NIZc3PwrzwOQAg7vvgsHsHCILOLvUpupgkwDZdNq/cXU3BjF4LNrCsVg0qxEyWasys5AiJ85Q==
+"@typescript-eslint/scope-manager@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.10.0.tgz#bb5d872e8b9e36203908595507fbc4d3105329cb"
+  integrity sha512-tgNgUgb4MhqK6DoKn3RBhyZ9aJga7EQrw+2/OiDk5hKf3pTVZWyqBi7ukP+Z0iEEDMF5FDa64LqODzlfE4O/Dg==
   dependencies:
-    "@typescript-eslint/types" "5.8.1"
-    "@typescript-eslint/visitor-keys" "5.8.1"
+    "@typescript-eslint/types" "5.10.0"
+    "@typescript-eslint/visitor-keys" "5.10.0"
 
-"@typescript-eslint/types@5.8.1":
-  version "5.8.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.8.1.tgz#04c6b49ebc8c99238238a6b8b43f2fc613983b5a"
-  integrity sha512-L/FlWCCgnjKOLefdok90/pqInkomLnAcF9UAzNr+DSqMC3IffzumHTQTrINXhP1gVp9zlHiYYjvozVZDPleLcA==
-
-"@typescript-eslint/typescript-estree@5.8.1":
-  version "5.8.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.8.1.tgz#a592855be688e7b729a1e9411d7d74ec992ed6ef"
-  integrity sha512-26lQ8l8tTbG7ri7xEcCFT9ijU5Fk+sx/KRRyyzCv7MQ+rZZlqiDPtMKWLC8P7o+dtCnby4c+OlxuX1tp8WfafQ==
+"@typescript-eslint/type-utils@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.10.0.tgz#8524b9479c19c478347a7df216827e749e4a51e5"
+  integrity sha512-TzlyTmufJO5V886N+hTJBGIfnjQDQ32rJYxPaeiyWKdjsv2Ld5l8cbS7pxim4DeNs62fKzRSt8Q14Evs4JnZyQ==
   dependencies:
-    "@typescript-eslint/types" "5.8.1"
-    "@typescript-eslint/visitor-keys" "5.8.1"
+    "@typescript-eslint/utils" "5.10.0"
+    debug "^4.3.2"
+    tsutils "^3.21.0"
+
+"@typescript-eslint/types@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.10.0.tgz#beb3cb345076f5b088afe996d57bcd1dfddaa75c"
+  integrity sha512-wUljCgkqHsMZbw60IbOqT/puLfyqqD5PquGiBo1u1IS3PLxdi3RDGlyf032IJyh+eQoGhz9kzhtZa+VC4eWTlQ==
+
+"@typescript-eslint/typescript-estree@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.0.tgz#4be24a3dea0f930bb1397c46187d0efdd955a224"
+  integrity sha512-x+7e5IqfwLwsxTdliHRtlIYkgdtYXzE0CkFeV6ytAqq431ZyxCFzNMNR5sr3WOlIG/ihVZr9K/y71VHTF/DUQA==
+  dependencies:
+    "@typescript-eslint/types" "5.10.0"
+    "@typescript-eslint/visitor-keys" "5.10.0"
     debug "^4.3.2"
     globby "^11.0.4"
     is-glob "^4.0.3"
     semver "^7.3.5"
     tsutils "^3.21.0"
 
-"@typescript-eslint/visitor-keys@5.8.1":
-  version "5.8.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.8.1.tgz#58a2c566265d5511224bc316149890451c1bbab0"
-  integrity sha512-SWgiWIwocK6NralrJarPZlWdr0hZnj5GXHIgfdm8hNkyKvpeQuFyLP6YjSIe9kf3YBIfU6OHSZLYkQ+smZwtNg==
+"@typescript-eslint/utils@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.10.0.tgz#c3d152a85da77c400e37281355561c72fb1b5a65"
+  integrity sha512-IGYwlt1CVcFoE2ueW4/ioEwybR60RAdGeiJX/iDAw0t5w0wK3S7QncDwpmsM70nKgGTuVchEWB8lwZwHqPAWRg==
   dependencies:
-    "@typescript-eslint/types" "5.8.1"
+    "@types/json-schema" "^7.0.9"
+    "@typescript-eslint/scope-manager" "5.10.0"
+    "@typescript-eslint/types" "5.10.0"
+    "@typescript-eslint/typescript-estree" "5.10.0"
+    eslint-scope "^5.1.1"
+    eslint-utils "^3.0.0"
+
+"@typescript-eslint/visitor-keys@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.0.tgz#770215497ad67cd15a572b52089991d5dfe06281"
+  integrity sha512-GMxj0K1uyrFLPKASLmZzCuSddmjZVbVj3Ouy5QVuIGKZopxvOr24JsS7gruz6C3GExE01mublZ3mIBOaon9zuQ==
+  dependencies:
+    "@typescript-eslint/types" "5.10.0"
     eslint-visitor-keys "^3.0.0"
 
 "@ungap/promise-all-settled@1.1.2":
@@ -693,95 +699,95 @@
   resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
   integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
 
-"@vue/compiler-core@3.2.26":
-  version "3.2.26"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.26.tgz#9ab92ae624da51f7b6064f4679c2d4564f437cc8"
-  integrity sha512-N5XNBobZbaASdzY9Lga2D9Lul5vdCIOXvUMd6ThcN8zgqQhPKfCV+wfAJNNJKQkSHudnYRO2gEB+lp0iN3g2Tw==
+"@vue/compiler-core@3.2.28":
+  version "3.2.28"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.28.tgz#7f6aa4b167f0ae0413f3c36e507c898db06e8fe8"
+  integrity sha512-mQpfEjmHVxmWKaup0HL6tLMv2HqjjJu7XT4/q0IoUXYXC4xKG8lIVn5YChJqxBTLPuQjzas7u7i9L4PAWJZRtA==
   dependencies:
     "@babel/parser" "^7.16.4"
-    "@vue/shared" "3.2.26"
+    "@vue/shared" "3.2.28"
     estree-walker "^2.0.2"
     source-map "^0.6.1"
 
-"@vue/compiler-dom@3.2.26":
-  version "3.2.26"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.26.tgz#c7a7b55d50a7b7981dd44fc28211df1450482667"
-  integrity sha512-smBfaOW6mQDxcT3p9TKT6mE22vjxjJL50GFVJiI0chXYGU/xzC05QRGrW3HHVuJrmLTLx5zBhsZ2dIATERbarg==
+"@vue/compiler-dom@3.2.28":
+  version "3.2.28"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.28.tgz#cc32a987fee50673f25430df35ea943f252c23e6"
+  integrity sha512-KA4yXceLteKC7VykvPnViUixemQw3A+oii+deSbZJOQKQKVh1HLosI10qxa8ImPCyun41+wG3uGR+tW7eu1W6Q==
   dependencies:
-    "@vue/compiler-core" "3.2.26"
-    "@vue/shared" "3.2.26"
+    "@vue/compiler-core" "3.2.28"
+    "@vue/shared" "3.2.28"
 
-"@vue/compiler-sfc@3.2.26":
-  version "3.2.26"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.26.tgz#3ce76677e4aa58311655a3bea9eb1cb804d2273f"
-  integrity sha512-ePpnfktV90UcLdsDQUh2JdiTuhV0Skv2iYXxfNMOK/F3Q+2BO0AulcVcfoksOpTJGmhhfosWfMyEaEf0UaWpIw==
+"@vue/compiler-sfc@3.2.28":
+  version "3.2.28"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.28.tgz#0a576c09abc72d6a76b153133de6fd7599c182c3"
+  integrity sha512-zB0WznfEBb4CbGBHzhboHDKVO5nxbkbxxFo9iVlxObP7a9/qvA5kkZEuT7nXP52f3b3qEfmVTjIT23Lo1ndZdQ==
   dependencies:
     "@babel/parser" "^7.16.4"
-    "@vue/compiler-core" "3.2.26"
-    "@vue/compiler-dom" "3.2.26"
-    "@vue/compiler-ssr" "3.2.26"
-    "@vue/reactivity-transform" "3.2.26"
-    "@vue/shared" "3.2.26"
+    "@vue/compiler-core" "3.2.28"
+    "@vue/compiler-dom" "3.2.28"
+    "@vue/compiler-ssr" "3.2.28"
+    "@vue/reactivity-transform" "3.2.28"
+    "@vue/shared" "3.2.28"
     estree-walker "^2.0.2"
     magic-string "^0.25.7"
     postcss "^8.1.10"
     source-map "^0.6.1"
 
-"@vue/compiler-ssr@3.2.26":
-  version "3.2.26"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.26.tgz#fd049523341fbf4ab5e88e25eef566d862894ba7"
-  integrity sha512-2mywLX0ODc4Zn8qBoA2PDCsLEZfpUGZcyoFRLSOjyGGK6wDy2/5kyDOWtf0S0UvtoyVq95OTSGIALjZ4k2q/ag==
+"@vue/compiler-ssr@3.2.28":
+  version "3.2.28"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.28.tgz#411e8b3bdc3183b2acd35e6551734b34366d64e5"
+  integrity sha512-z8rck1PDTu20iLyip9lAvIhaO40DUJrw3Zv0mS4Apfh3PlfWpF5dhsO5g0dgt213wgYsQIYVIlU9cfrYapqRgg==
   dependencies:
-    "@vue/compiler-dom" "3.2.26"
-    "@vue/shared" "3.2.26"
+    "@vue/compiler-dom" "3.2.28"
+    "@vue/shared" "3.2.28"
 
-"@vue/reactivity-transform@3.2.26":
-  version "3.2.26"
-  resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.26.tgz#6d8f20a4aa2d19728f25de99962addbe7c4d03e9"
-  integrity sha512-XKMyuCmzNA7nvFlYhdKwD78rcnmPb7q46uoR00zkX6yZrUmcCQ5OikiwUEVbvNhL5hBJuvbSO95jB5zkUon+eQ==
+"@vue/reactivity-transform@3.2.28":
+  version "3.2.28"
+  resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.28.tgz#e0abf86694f4d182f974fbac934fc3e23e0a6d9b"
+  integrity sha512-zE8idNkOPnBDd2tKSIk84hOQZ+jXKvSy5FoIIVlcNEJHnCFnQ3maqeSJ9KoB2Rf6EXUhFTiTDNRlYlXmT2uHbQ==
   dependencies:
     "@babel/parser" "^7.16.4"
-    "@vue/compiler-core" "3.2.26"
-    "@vue/shared" "3.2.26"
+    "@vue/compiler-core" "3.2.28"
+    "@vue/shared" "3.2.28"
     estree-walker "^2.0.2"
     magic-string "^0.25.7"
 
-"@vue/reactivity@3.2.26":
-  version "3.2.26"
-  resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.26.tgz#d529191e581521c3c12e29ef986d4c8a933a0f83"
-  integrity sha512-h38bxCZLW6oFJVDlCcAiUKFnXI8xP8d+eO0pcDxx+7dQfSPje2AO6M9S9QO6MrxQB7fGP0DH0dYQ8ksf6hrXKQ==
+"@vue/reactivity@3.2.28":
+  version "3.2.28"
+  resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.28.tgz#1c3c7f434372edd867f937151897fca7efc4be18"
+  integrity sha512-WamM5LGv7JIarW+EYAzYFqYonZXjTnOjNW0sBO93jRE9I1ReAwfH8NvQXkPA3JZ3fuF6SGDdG8Y9/+dKjd/1Gw==
   dependencies:
-    "@vue/shared" "3.2.26"
+    "@vue/shared" "3.2.28"
 
-"@vue/runtime-core@3.2.26":
-  version "3.2.26"
-  resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.26.tgz#5c59cc440ed7a39b6dbd4c02e2d21c8d1988f0de"
-  integrity sha512-BcYi7qZ9Nn+CJDJrHQ6Zsmxei2hDW0L6AB4vPvUQGBm2fZyC0GXd/4nVbyA2ubmuhctD5RbYY8L+5GUJszv9mQ==
+"@vue/runtime-core@3.2.28":
+  version "3.2.28"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.28.tgz#69d8eede42957a1660b964004aa002982ae36a41"
+  integrity sha512-sVbBMFUt42JatTlXbdH6tVcLPw1eEOrrVQWI+j6/nJVzR852RURaT6DhdR0azdYscxq4xmmBctE0VQmlibBOFw==
   dependencies:
-    "@vue/reactivity" "3.2.26"
-    "@vue/shared" "3.2.26"
+    "@vue/reactivity" "3.2.28"
+    "@vue/shared" "3.2.28"
 
-"@vue/runtime-dom@3.2.26":
-  version "3.2.26"
-  resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.26.tgz#84d3ae2584488747717c2e072d5d9112c0d2e6c2"
-  integrity sha512-dY56UIiZI+gjc4e8JQBwAifljyexfVCkIAu/WX8snh8vSOt/gMSEGwPRcl2UpYpBYeyExV8WCbgvwWRNt9cHhQ==
+"@vue/runtime-dom@3.2.28":
+  version "3.2.28"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.28.tgz#b5a0cf38daed5534edbc95790f4eeac97dff2003"
+  integrity sha512-Jg7cxZanEXXGu1QnZILFLnDrM+MIFN8VAullmMZiJEZziHvhygRMpi0ahNy/8OqGwtTze1JNhLdHRBO+q2hbmg==
   dependencies:
-    "@vue/runtime-core" "3.2.26"
-    "@vue/shared" "3.2.26"
+    "@vue/runtime-core" "3.2.28"
+    "@vue/shared" "3.2.28"
     csstype "^2.6.8"
 
-"@vue/server-renderer@3.2.26":
-  version "3.2.26"
-  resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.26.tgz#f16a4b9fbcc917417b4cea70c99afce2701341cf"
-  integrity sha512-Jp5SggDUvvUYSBIvYEhy76t4nr1vapY/FIFloWmQzn7UxqaHrrBpbxrqPcTrSgGrcaglj0VBp22BKJNre4aA1w==
+"@vue/server-renderer@3.2.28":
+  version "3.2.28"
+  resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.28.tgz#235944dc4d969fadd387f62acc2eb8b8d50008a2"
+  integrity sha512-S+MhurgkPabRvhdDl8R6efKBmniJqBbbWIYTXADaJIKFLFLQCW4gcYUTbxuebzk6j3z485vpekhrHHymTF52Pg==
   dependencies:
-    "@vue/compiler-ssr" "3.2.26"
-    "@vue/shared" "3.2.26"
+    "@vue/compiler-ssr" "3.2.28"
+    "@vue/shared" "3.2.28"
 
-"@vue/shared@3.2.26":
-  version "3.2.26"
-  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.26.tgz#7acd1621783571b9a82eca1f041b4a0a983481d9"
-  integrity sha512-vPV6Cq+NIWbH5pZu+V+2QHE9y1qfuTq49uNWw4f7FDEeZaDU2H2cx5jcUZOAKW7qTrUS4k6qZPbMy1x4N96nbA==
+"@vue/shared@3.2.28":
+  version "3.2.28"
+  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.28.tgz#5b0b1840432031d0ea1adff633b356a503e87048"
+  integrity sha512-eMQ8s9j8FpbGHlgUAaj/coaG3Q8YtMsoWL/RIHTsE3Ex7PUTyr7V91vB5HqWB5Sn8m4RXTHGO22/skoTUYvp0A==
 
 "@webassemblyjs/ast@1.11.0":
   version "1.11.0"
@@ -1341,6 +1347,11 @@ balanced-match@^1.0.0:
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
   integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
 
+base64-js@^1.3.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
 bcrypt-pbkdf@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
@@ -1368,7 +1379,7 @@ blob-util@^2.0.2:
   resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb"
   integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==
 
-bluebird@3.7.2:
+bluebird@3.7.2, bluebird@^3.7.2:
   version "3.7.2"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
   integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
@@ -1470,6 +1481,14 @@ buffer-from@^1.0.0:
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
   integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
 
+buffer@^5.6.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
+  dependencies:
+    base64-js "^1.3.1"
+    ieee754 "^1.1.13"
+
 bufferutil@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.1.tgz#3a177e8e5819a1243fe16b63a199951a7ad8d4a7"
@@ -1623,15 +1642,14 @@ cli-cursor@^3.1.0:
   dependencies:
     restore-cursor "^3.1.0"
 
-cli-table3@~0.6.0:
-  version "0.6.0"
-  resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee"
-  integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==
+cli-table3@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8"
+  integrity sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==
   dependencies:
-    object-assign "^4.1.0"
     string-width "^4.2.0"
   optionalDependencies:
-    colors "^1.1.2"
+    colors "1.4.0"
 
 cli-truncate@^2.1.0:
   version "2.1.0"
@@ -1716,7 +1734,7 @@ colorette@^2.0.14:
   resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.15.tgz#8e634aa0429b110d24be82eac4d42f5ea65ab2d5"
   integrity sha512-lIFQhufWaVvwi4wOlX9Gx5b0Nmw3XAZ8HzHNH9dfxhe+JaKNTmX6QLk4o7UHyI+tUY8ClvyfaHUm5bf61O3psA==
 
-colors@^1.1.2:
+colors@1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
   integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
@@ -1758,10 +1776,10 @@ common-tags@^1.8.0:
   resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937"
   integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==
 
-compare-versions@4.1.2:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-4.1.2.tgz#a7b1678c897000d03a70a0e01efee43e7b04dda7"
-  integrity sha512-LAfbAbAgjnIwPsr2fvJLfrSyqAhK5nj/ffIg7a5aigry9RXJfNzVnOu0Egw8Z+G8LMDu1Qig2q48bpBzjyjZoQ==
+compare-versions@4.1.3:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-4.1.3.tgz#8f7b8966aef7dc4282b45dfa6be98434fc18a1a4"
+  integrity sha512-WQfnbDcrYnGr55UwbxKiQKASnTtNnaAWVi8jZyy8NTpVAXWACSne8lMD1iaIo9AiU6mnuLvSVshCzewVuWxHUg==
 
 concat-map@0.0.1:
   version "0.0.1"
@@ -1914,52 +1932,52 @@ cssesc@^3.0.0:
   resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
   integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
 
-cssnano-preset-default@^5.1.9:
-  version "5.1.9"
-  resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.1.9.tgz#79628ac48eccbdad570f70b4018cc38d43d1b7df"
-  integrity sha512-RhkEucqlQ+OxEi14K1p8gdXcMQy1mSpo7P1oC44oRls7BYIj8p+cht4IFBFV3W4iOjTP8EUB33XV1fX9KhDzyA==
+cssnano-preset-default@^5.1.10:
+  version "5.1.10"
+  resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.1.10.tgz#9350765fdf3c49bf78fac7673354fa58fa95daa4"
+  integrity sha512-BcpSzUVygHMOnp9uG5rfPzTOCb0GAHQkqtUQx8j1oMNF9A1Q8hziOOhiM4bdICpmrBIU85BE64RD5XGYsVQZNA==
   dependencies:
     css-declaration-sorter "^6.0.3"
-    cssnano-utils "^2.0.1"
-    postcss-calc "^8.0.0"
-    postcss-colormin "^5.2.2"
+    cssnano-utils "^3.0.0"
+    postcss-calc "^8.2.0"
+    postcss-colormin "^5.2.3"
     postcss-convert-values "^5.0.2"
     postcss-discard-comments "^5.0.1"
     postcss-discard-duplicates "^5.0.1"
     postcss-discard-empty "^5.0.1"
-    postcss-discard-overridden "^5.0.1"
+    postcss-discard-overridden "^5.0.2"
     postcss-merge-longhand "^5.0.4"
-    postcss-merge-rules "^5.0.3"
-    postcss-minify-font-values "^5.0.1"
-    postcss-minify-gradients "^5.0.3"
-    postcss-minify-params "^5.0.2"
-    postcss-minify-selectors "^5.1.0"
+    postcss-merge-rules "^5.0.4"
+    postcss-minify-font-values "^5.0.2"
+    postcss-minify-gradients "^5.0.4"
+    postcss-minify-params "^5.0.3"
+    postcss-minify-selectors "^5.1.1"
     postcss-normalize-charset "^5.0.1"
-    postcss-normalize-display-values "^5.0.1"
-    postcss-normalize-positions "^5.0.1"
-    postcss-normalize-repeat-style "^5.0.1"
-    postcss-normalize-string "^5.0.1"
-    postcss-normalize-timing-functions "^5.0.1"
-    postcss-normalize-unicode "^5.0.1"
+    postcss-normalize-display-values "^5.0.2"
+    postcss-normalize-positions "^5.0.2"
+    postcss-normalize-repeat-style "^5.0.2"
+    postcss-normalize-string "^5.0.2"
+    postcss-normalize-timing-functions "^5.0.2"
+    postcss-normalize-unicode "^5.0.2"
     postcss-normalize-url "^5.0.4"
-    postcss-normalize-whitespace "^5.0.1"
-    postcss-ordered-values "^5.0.2"
+    postcss-normalize-whitespace "^5.0.2"
+    postcss-ordered-values "^5.0.3"
     postcss-reduce-initial "^5.0.2"
-    postcss-reduce-transforms "^5.0.1"
+    postcss-reduce-transforms "^5.0.2"
     postcss-svgo "^5.0.3"
     postcss-unique-selectors "^5.0.2"
 
-cssnano-utils@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-2.0.1.tgz#8660aa2b37ed869d2e2f22918196a9a8b6498ce2"
-  integrity sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==
+cssnano-utils@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.0.0.tgz#c0b9fcd6e4f05c5155b07e9ab11bf94b97163057"
+  integrity sha512-Pzs7/BZ6OgT+tXXuF12DKR8SmSbzUeVYCtMBbS8lI0uAm3mrYmkyqCXXPsQESI6kmLfEVBppbdVY/el3hg3nAA==
 
-cssnano@5.0.14:
-  version "5.0.14"
-  resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.0.14.tgz#99bc550f663b48c38e9b8e0ae795697c9de84b47"
-  integrity sha512-qzhRkFvBhv08tbyKCIfWbxBXmkIpLl1uNblt8SpTHkgLfON5OCPX/CCnkdNmEosvo8bANQYmTTMEgcVBlisHaw==
+cssnano@5.0.15:
+  version "5.0.15"
+  resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.0.15.tgz#8779eaf60e3665e6a12687c814d375cc9f78db76"
+  integrity sha512-ppZsS7oPpi2sfiyV5+i+NbB/3GtQ+ab2Vs1azrZaXWujUSN4o+WdTxlCZIMcT9yLW3VO/5yX3vpyDaQ1nIn8CQ==
   dependencies:
-    cssnano-preset-default "^5.1.9"
+    cssnano-preset-default "^5.1.10"
     lilconfig "^2.0.3"
     yaml "^1.10.2"
 
@@ -1982,24 +2000,25 @@ csstype@^2.6.8:
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.13.tgz#a6893015b90e84dd6e85d0e3b442a1e84f2dbe0f"
   integrity sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A==
 
-cypress@9.2.0:
-  version "9.2.0"
-  resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.2.0.tgz#727c20b4662167890db81d5f6ba615231835b17d"
-  integrity sha512-Jn26Tprhfzh/a66Sdj9SoaYlnNX6Mjfmj5PHu2a7l3YHXhrgmavM368wjCmgrxC6KHTOv9SpMQGhAJn+upDViA==
+cypress@9.3.1:
+  version "9.3.1"
+  resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.3.1.tgz#8116f52d49d6daf90a91e88f3eafd940234d2958"
+  integrity sha512-BODdPesxX6bkVUnH8BVsV8I/jn57zQtO1FEOUTiuG2us3kslW7g0tcuwiny7CKCmJUZz8S/D587ppC+s58a+5Q==
   dependencies:
     "@cypress/request" "^2.88.10"
     "@cypress/xvfb" "^1.2.4"
     "@types/node" "^14.14.31"
-    "@types/sinonjs__fake-timers" "^6.0.2"
+    "@types/sinonjs__fake-timers" "8.1.1"
     "@types/sizzle" "^2.3.2"
     arch "^2.2.0"
     blob-util "^2.0.2"
-    bluebird "3.7.2"
+    bluebird "^3.7.2"
+    buffer "^5.6.0"
     cachedir "^2.3.0"
     chalk "^4.1.0"
     check-more-types "^2.24.0"
     cli-cursor "^3.1.0"
-    cli-table3 "~0.6.0"
+    cli-table3 "~0.6.1"
     commander "^5.1.0"
     common-tags "^1.8.0"
     dayjs "^1.10.4"
@@ -2304,7 +2323,7 @@ enhanced-resolve@^5.8.3:
     graceful-fs "^4.2.4"
     tapable "^2.2.0"
 
-enquirer@^2.3.5, enquirer@^2.3.6:
+enquirer@^2.3.6:
   version "2.3.6"
   resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
   integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
@@ -2444,38 +2463,37 @@ eslint-import-resolver-node@^0.3.6:
     debug "^3.2.7"
     resolve "^1.20.0"
 
-eslint-module-utils@^2.7.1:
-  version "2.7.1"
-  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.1.tgz#b435001c9f8dd4ab7f6d0efcae4b9696d4c24b7c"
-  integrity sha512-fjoetBXQZq2tSTWZ9yWVl2KuFrTZZH3V+9iD1V1RfpDgxzJR+mPd/KZmMiA8gbPqdBzpNiEHOuT7IYEWxrH0zQ==
+eslint-module-utils@^2.7.2:
+  version "2.7.2"
+  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.2.tgz#1d0aa455dcf41052339b63cada8ab5fd57577129"
+  integrity sha512-zquepFnWCY2ISMFwD/DqzaM++H+7PDzOpUvotJWm/y1BAFt5R4oeULgdrTejKqLkz7MA/tgstsUMNYc7wNdTrg==
   dependencies:
     debug "^3.2.7"
     find-up "^2.1.0"
-    pkg-dir "^2.0.0"
 
-eslint-plugin-import@2.25.3:
-  version "2.25.3"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.3.tgz#a554b5f66e08fb4f6dc99221866e57cfff824766"
-  integrity sha512-RzAVbby+72IB3iOEL8clzPLzL3wpDrlwjsTBAQXgyp5SeTqqY+0bFubwuo+y/HLhNZcXV4XqTBO4LGsfyHIDXg==
+eslint-plugin-import@2.25.4:
+  version "2.25.4"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz#322f3f916a4e9e991ac7af32032c25ce313209f1"
+  integrity sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==
   dependencies:
     array-includes "^3.1.4"
     array.prototype.flat "^1.2.5"
     debug "^2.6.9"
     doctrine "^2.1.0"
     eslint-import-resolver-node "^0.3.6"
-    eslint-module-utils "^2.7.1"
+    eslint-module-utils "^2.7.2"
     has "^1.0.3"
     is-core-module "^2.8.0"
     is-glob "^4.0.3"
     minimatch "^3.0.4"
     object.values "^1.1.5"
     resolve "^1.20.0"
-    tsconfig-paths "^3.11.0"
+    tsconfig-paths "^3.12.0"
 
-eslint-plugin-vue@8.2.0:
-  version "8.2.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-8.2.0.tgz#b404bc10e3f43b2b7aad4ebb3b38090a58040202"
-  integrity sha512-cLIdTuOAMXyHeQ4drYKcZfoyzdwdBpH279X8/N0DgmotEI9yFKb5O/cAgoie/CkQZCH/MOmh0xw/KEfS90zY2A==
+eslint-plugin-vue@8.3.0:
+  version "8.3.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-8.3.0.tgz#2ae4f915ed3541a58c4a4c1145c1e60b86aa7e85"
+  integrity sha512-IIuLHw4vQxGlHcoP2dG6t/2OVdQf2qoyAzEGAxreU1afZOHGA7y3TWq8I+r3ZA6Wjs6xpeUWGHlT31QGr9Rb5g==
   dependencies:
     eslint-utils "^3.0.0"
     natural-compare "^1.4.0"
@@ -2528,10 +2546,15 @@ eslint-visitor-keys@^3.1.0:
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz#eee4acea891814cda67a7d8812d9647dd0179af2"
   integrity sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==
 
-eslint@8.6.0:
-  version "8.6.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.6.0.tgz#4318c6a31c5584838c1a2e940c478190f58d558e"
-  integrity sha512-UvxdOJ7mXFlw7iuHZA4jmzPaUqIw54mZrv+XPYKNbKdLR0et4rf60lIZUU9kiNtnzzMzGWxMV+tQ7uG7JG8DPw==
+eslint-visitor-keys@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz#6fbb166a6798ee5991358bc2daa1ba76cc1254a1"
+  integrity sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==
+
+eslint@8.7.0:
+  version "8.7.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.7.0.tgz#22e036842ee5b7cf87b03fe237731675b4d3633c"
+  integrity sha512-ifHYzkBGrzS2iDU7KjhCAVMGCvF6M3Xfs8X8b37cgrUlDt6bWRTpRh6T/gtSXv1HJ/BUGgmjvNvOEGu85Iif7w==
   dependencies:
     "@eslint/eslintrc" "^1.0.5"
     "@humanwhocodes/config-array" "^0.9.2"
@@ -2540,11 +2563,10 @@ eslint@8.6.0:
     cross-spawn "^7.0.2"
     debug "^4.3.2"
     doctrine "^3.0.0"
-    enquirer "^2.3.5"
     escape-string-regexp "^4.0.0"
     eslint-scope "^7.1.0"
     eslint-utils "^3.0.0"
-    eslint-visitor-keys "^3.1.0"
+    eslint-visitor-keys "^3.2.0"
     espree "^9.3.0"
     esquery "^1.4.0"
     esutils "^2.0.2"
@@ -2553,7 +2575,7 @@ eslint@8.6.0:
     functional-red-black-tree "^1.0.1"
     glob-parent "^6.0.1"
     globals "^13.6.0"
-    ignore "^4.0.6"
+    ignore "^5.2.0"
     import-fresh "^3.0.0"
     imurmurhash "^0.1.4"
     is-glob "^4.0.0"
@@ -2564,9 +2586,7 @@ eslint@8.6.0:
     minimatch "^3.0.4"
     natural-compare "^1.4.0"
     optionator "^0.9.1"
-    progress "^2.0.0"
     regexpp "^3.2.0"
-    semver "^7.2.1"
     strip-ansi "^6.0.1"
     strip-json-comments "^3.1.0"
     text-table "^0.2.0"
@@ -3128,6 +3148,11 @@ graceful-fs@^4.2.0:
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
   integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
 
+graceful-fs@^4.2.9:
+  version "4.2.9"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96"
+  integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==
+
 growl@1.10.5:
   version "1.10.5"
   resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
@@ -3245,13 +3270,18 @@ icss-utils@^5.0.0, icss-utils@^5.1.0:
   resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
   integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
 
-idb-keyval@6.0.3:
-  version "6.0.3"
-  resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.0.3.tgz#e47246a15e55d0fff9fa204fd9ca06f90ff30c52"
-  integrity sha512-yh8V7CnE6EQMu9YDwQXhRxwZh4nv+8xm/HV4ZqK4IiYFJBWYGjJuykADJbSP+F/GDXUBwCSSNn/14IpGL81TuA==
+idb-keyval@6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.1.0.tgz#e659cff41188e6097d7fadd69926f6adbbe70041"
+  integrity sha512-u/qHZ75rlD3gH+Zah8dAJVJcGW/RfCnfNrFkElC5RpRCnpsCXXhqjVk+6MoVKJ3WhmNbRYdI6IIVP88e+5sxGw==
   dependencies:
     safari-14-idb-fix "^3.0.0"
 
+ieee754@^1.1.13:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
 ignore@^4.0.6:
   version "4.0.6"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
@@ -3267,6 +3297,11 @@ ignore@^5.1.8:
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.9.tgz#9ec1a5cbe8e1446ec60d4420060d43aa6e7382fb"
   integrity sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==
 
+ignore@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
+  integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
+
 immutable@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
@@ -3673,7 +3708,7 @@ js-yaml@4.0.0:
   dependencies:
     argparse "^2.0.1"
 
-js-yaml@^3.13.1, js-yaml@^3.14.1:
+js-yaml@^3.13.1:
   version "3.14.1"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
   integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
@@ -3823,10 +3858,10 @@ jws@^4.0.0:
     jwa "^2.0.0"
     safe-buffer "^5.0.1"
 
-katex@0.15.1:
-  version "0.15.1"
-  resolved "https://registry.yarnpkg.com/katex/-/katex-0.15.1.tgz#cf4ce2fa1257c3279cc7a7fe0c8d1fab40800893"
-  integrity sha512-KIk+gizli0gl1XaJlCYS8/donGMbzXYTka6BbH3AgvDJTOwyDY4hJ+YmzJ1F0y/3XzX5B9ED8AqB2Hmn2AZ0uA==
+katex@0.15.2:
+  version "0.15.2"
+  resolved "https://registry.yarnpkg.com/katex/-/katex-0.15.2.tgz#c05ece41ab497597b17abca2cecde3e4c0127f9d"
+  integrity sha512-FfZ/f6f8bQdLmJ3McXDNTkKenQkoXkItpW0I9bsG2wgb+8JAY5bwpXFtI8ZVrg5hc1wo1X/UIhdkVMpok46tEQ==
   dependencies:
     commander "^8.0.0"
 
@@ -4050,10 +4085,10 @@ merge@^2.1.0:
   resolved "https://registry.yarnpkg.com/merge/-/merge-2.1.1.tgz#59ef4bf7e0b3e879186436e8481c06a6c162ca98"
   integrity sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==
 
-mfm-js@0.20.0:
-  version "0.20.0"
-  resolved "https://registry.yarnpkg.com/mfm-js/-/mfm-js-0.20.0.tgz#3afdcd7959461fd825aa8af9b9e8a57cdbddc290"
-  integrity sha512-1+3tV3nWUKQNh/ztX3wXu5iLBtdsg6q3wUhl+XyOhc2H3sQdG+sih/w2c0nR9TIawjN+Z1/pvgGzxMJHfmKQmA==
+mfm-js@0.21.0:
+  version "0.21.0"
+  resolved "https://registry.yarnpkg.com/mfm-js/-/mfm-js-0.21.0.tgz#954cc6e7071700b0b1872c78a90bada10be7f772"
+  integrity sha512-nyQXaipa7rmAw9ER9uYigMvGcdCwhSv93abZBwccnSnPOc1W3S/WW0+sN28g3YSmlHDCA0i2q9aAFc9EgOi5KA==
   dependencies:
     twemoji-parser "13.1.x"
 
@@ -4104,10 +4139,10 @@ minimist@^1.2.0, minimist@^1.2.5:
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
   integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
 
-misskey-js@0.0.12:
-  version "0.0.12"
-  resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.12.tgz#c8fc2fc823c69b0b7d1cb153a5a29afc33f0ff4d"
-  integrity sha512-Aq74/XskxaFN5CeCLeKPp5UP/xTFHvVnOV677G/zoSIShJRTeLsN5YnzwFpOVI2KN21JQ/ExesKDLoWlvQHtNA==
+misskey-js@0.0.13:
+  version "0.0.13"
+  resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.13.tgz#03a4e469186e28752d599dc4093519eb64647970"
+  integrity sha512-kBdJdfe281gtykzzsrN3IAxWUQIimzPiJGyKWf863ggWJlWYVPmP9hTFlX2z8oPOaypgVBPEPHyw/jNUdc2DbQ==
   dependencies:
     autobind-decorator "^2.4.0"
     eventemitter3 "^4.0.7"
@@ -4274,7 +4309,7 @@ oauth@0.9.15:
   resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
   integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
 
-object-assign@^4.1.0, object-assign@^4.1.1:
+object-assign@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
   integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
@@ -4537,9 +4572,9 @@ performance-now@^2.1.0:
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
   integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
 
-"photoswipe@git://github.com/dimsemenov/photoswipe#v5-beta":
+"photoswipe@git+https://github.com/dimsemenov/photoswipe#v5-beta":
   version "5.1.7"
-  resolved "git://github.com/dimsemenov/photoswipe#60040164333bd257409669e715e4327afdb3aec7"
+  resolved "git+https://github.com/dimsemenov/photoswipe#60040164333bd257409669e715e4327afdb3aec7"
 
 picocolors@^1.0.0:
   version "1.0.0"
@@ -4556,13 +4591,6 @@ pify@^2.2.0:
   resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
   integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
 
-pkg-dir@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
-  integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=
-  dependencies:
-    find-up "^2.1.0"
-
 pkg-dir@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
@@ -4570,6 +4598,11 @@ pkg-dir@^4.2.0:
   dependencies:
     find-up "^4.0.0"
 
+pluralize@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
+  integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
+
 pngjs@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
@@ -4583,18 +4616,18 @@ portscanner@2.2.0:
     async "^2.6.0"
     is-number-like "^1.0.3"
 
-postcss-calc@^8.0.0:
-  version "8.0.0"
-  resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.0.0.tgz#a05b87aacd132740a5db09462a3612453e5df90a"
-  integrity sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g==
+postcss-calc@^8.2.0:
+  version "8.2.2"
+  resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.2.tgz#9706e7399e8ec8b61a47830dcf1f21391af23373"
+  integrity sha512-B5R0UeB4zLJvxNt1FVCaDZULdzsKLPc6FhjFJ+xwFiq7VG4i9cuaJLxVjNtExNK8ocm3n2o4unXXLiVX1SCqxA==
   dependencies:
     postcss-selector-parser "^6.0.2"
     postcss-value-parser "^4.0.2"
 
-postcss-colormin@^5.2.2:
-  version "5.2.2"
-  resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.2.2.tgz#019cd6912bef9e7e0924462c5e4ffae241e2f437"
-  integrity sha512-tSEe3NpqWARUTidDlF0LntPkdlhXqfDFuA1yslqpvvGAfpZ7oBaw+/QXd935NKm2U9p4PED0HDZlzmMk7fVC6g==
+postcss-colormin@^5.2.3:
+  version "5.2.3"
+  resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.2.3.tgz#da7fb80e81ad80d2867ea9e38672a892add5df15"
+  integrity sha512-dra4xoAjub2wha6RUXAgadHEn2lGxbj8drhFcIGLOMn914Eu7DkPUurugDXgstwttCYkJtZ/+PkWRWdp3UHRIA==
   dependencies:
     browserslist "^4.16.6"
     caniuse-api "^3.0.0"
@@ -4623,10 +4656,10 @@ postcss-discard-empty@^5.0.1:
   resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz#ee136c39e27d5d2ed4da0ee5ed02bc8a9f8bf6d8"
   integrity sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==
 
-postcss-discard-overridden@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz#454b41f707300b98109a75005ca4ab0ff2743ac6"
-  integrity sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==
+postcss-discard-overridden@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.0.2.tgz#e6f51d83e66feffcf05ed94c4ad20b814d0aab5f"
+  integrity sha512-+56BLP6NSSUuWUXjRgAQuho1p5xs/hU5Sw7+xt9S3JSg+7R6+WMGnJW7Hre/6tTuZ2xiXMB42ObkiZJ2hy/Pew==
 
 postcss-loader@6.2.1:
   version "6.2.1"
@@ -4645,46 +4678,46 @@ postcss-merge-longhand@^5.0.4:
     postcss-value-parser "^4.1.0"
     stylehacks "^5.0.1"
 
-postcss-merge-rules@^5.0.3:
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.0.3.tgz#b5cae31f53129812a77e3eb1eeee448f8cf1a1db"
-  integrity sha512-cEKTMEbWazVa5NXd8deLdCnXl+6cYG7m2am+1HzqH0EnTdy8fRysatkaXb2dEnR+fdaDxTvuZ5zoBdv6efF6hg==
+postcss-merge-rules@^5.0.4:
+  version "5.0.4"
+  resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.0.4.tgz#a50640fd832380f322bd2861a9b33fbde4219f9b"
+  integrity sha512-yOj7bW3NxlQxaERBB0lEY1sH5y+RzevjbdH4DBJurjKERNpknRByFNdNe+V72i5pIZL12woM9uGdS5xbSB+kDQ==
   dependencies:
     browserslist "^4.16.6"
     caniuse-api "^3.0.0"
-    cssnano-utils "^2.0.1"
+    cssnano-utils "^3.0.0"
     postcss-selector-parser "^6.0.5"
 
-postcss-minify-font-values@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz#a90cefbfdaa075bd3dbaa1b33588bb4dc268addf"
-  integrity sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA==
+postcss-minify-font-values@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.0.2.tgz#4603e956d85cd0719156e2b3eb68e3cd2f917092"
+  integrity sha512-R6MJZryq28Cw0AmnyhXrM7naqJZZLoa1paBltIzh2wM7yb4D45TLur+eubTQ4jCmZU9SGeZdWsc5KcSoqTMeTg==
   dependencies:
-    postcss-value-parser "^4.1.0"
+    postcss-value-parser "^4.2.0"
 
-postcss-minify-gradients@^5.0.3:
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.0.3.tgz#f970a11cc71e08e9095e78ec3a6b34b91c19550e"
-  integrity sha512-Z91Ol22nB6XJW+5oe31+YxRsYooxOdFKcbOqY/V8Fxse1Y3vqlNRpi1cxCqoACZTQEhl+xvt4hsbWiV5R+XI9Q==
+postcss-minify-gradients@^5.0.4:
+  version "5.0.4"
+  resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.0.4.tgz#f13146950513f5a201015306914e3c76d10b591d"
+  integrity sha512-RVwZA7NC4R4J76u8X0Q0j+J7ItKUWAeBUJ8oEEZWmtv3Xoh19uNJaJwzNpsydQjk6PkuhRrK+YwwMf+c+68EYg==
   dependencies:
     colord "^2.9.1"
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
+    cssnano-utils "^3.0.0"
+    postcss-value-parser "^4.2.0"
 
-postcss-minify-params@^5.0.2:
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.0.2.tgz#1b644da903473fbbb18fbe07b8e239883684b85c"
-  integrity sha512-qJAPuBzxO1yhLad7h2Dzk/F7n1vPyfHfCCh5grjGfjhi1ttCnq4ZXGIW77GSrEbh9Hus9Lc/e/+tB4vh3/GpDg==
+postcss-minify-params@^5.0.3:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.0.3.tgz#9f933d37098ef1dcf007e159a47bb2c1cf06989d"
+  integrity sha512-NY92FUikE+wralaiVexFd5gwb7oJTIDhgTNeIw89i1Ymsgt4RWiPXfz3bg7hDy4NL6gepcThJwOYNtZO/eNi7Q==
   dependencies:
     alphanum-sort "^1.0.2"
     browserslist "^4.16.6"
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
+    cssnano-utils "^3.0.0"
+    postcss-value-parser "^4.2.0"
 
-postcss-minify-selectors@^5.1.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz#4385c845d3979ff160291774523ffa54eafd5a54"
-  integrity sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og==
+postcss-minify-selectors@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.1.1.tgz#20ae03b411f7fb397451e3d7d85b989f944b871c"
+  integrity sha512-TOzqOPXt91O2luJInaVPiivh90a2SIK5Nf1Ea7yEIM/5w+XA5BGrZGUSW8aEx9pJ/oNj7ZJBhjvigSiBV+bC1Q==
   dependencies:
     alphanum-sort "^1.0.2"
     postcss-selector-parser "^6.0.5"
@@ -4722,51 +4755,48 @@ postcss-normalize-charset@^5.0.1:
   resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz#121559d1bebc55ac8d24af37f67bd4da9efd91d0"
   integrity sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==
 
-postcss-normalize-display-values@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz#62650b965981a955dffee83363453db82f6ad1fd"
-  integrity sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ==
+postcss-normalize-display-values@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.2.tgz#8b5273c6c7d0a445e6ef226b8a5bb3204a55fb99"
+  integrity sha512-RxXoJPUR0shSjkMMzgEZDjGPrgXUVYyWA/YwQRicb48H15OClPuaDR7tYokLAlGZ2tCSENEN5WxjgxSD5m4cUw==
   dependencies:
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
+    postcss-value-parser "^4.2.0"
 
-postcss-normalize-positions@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz#868f6af1795fdfa86fbbe960dceb47e5f9492fe5"
-  integrity sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg==
+postcss-normalize-positions@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.0.2.tgz#799fa494b352a5da183be8f050024af6d92fa29c"
+  integrity sha512-tqghWFVDp2btqFg1gYob1etPNxXLNh3uVeWgZE2AQGh6b2F8AK2Gj36v5Vhyh+APwIzNjmt6jwZ9pTBP+/OM8g==
   dependencies:
-    postcss-value-parser "^4.1.0"
+    postcss-value-parser "^4.2.0"
 
-postcss-normalize-repeat-style@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz#cbc0de1383b57f5bb61ddd6a84653b5e8665b2b5"
-  integrity sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w==
+postcss-normalize-repeat-style@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.2.tgz#fd9bddba3e6fd5f5d95c18dfb42a09ecd563adea"
+  integrity sha512-/rIZn8X9bBzC7KvY4iKUhXUGW3MmbXwfPF23jC9wT9xTi7kAvgj8sEgwxjixBmoL6MVa4WOgxNz2hAR6wTK8tw==
   dependencies:
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
+    postcss-value-parser "^4.2.0"
 
-postcss-normalize-string@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz#d9eafaa4df78c7a3b973ae346ef0e47c554985b0"
-  integrity sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA==
+postcss-normalize-string@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.0.2.tgz#1b2bbf91526f61266f28abf7f773e4136b2c4bd2"
+  integrity sha512-zaI1yzwL+a/FkIzUWMQoH25YwCYxi917J4pYm1nRXtdgiCdnlTkx5eRzqWEC64HtRa06WCJ9TIutpb6GmW4gFw==
   dependencies:
-    postcss-value-parser "^4.1.0"
+    postcss-value-parser "^4.2.0"
 
-postcss-normalize-timing-functions@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz#8ee41103b9130429c6cbba736932b75c5e2cb08c"
-  integrity sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q==
+postcss-normalize-timing-functions@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.2.tgz#db4f4f49721f47667afd1fdc5edb032f8d9cdb2e"
+  integrity sha512-Ao0PP6MoYsRU1LxeVUW740ioknvdIUmfr6uAA3xWlQJ9s69/Tupy8qwhuKG3xWfl+KvLMAP9p2WXF9cwuk/7Bg==
   dependencies:
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
+    postcss-value-parser "^4.2.0"
 
-postcss-normalize-unicode@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz#82d672d648a411814aa5bf3ae565379ccd9f5e37"
-  integrity sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA==
+postcss-normalize-unicode@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.2.tgz#c4db89a0116066716b9e9fcb6444ce63178f5ced"
+  integrity sha512-3y/V+vjZ19HNcTizeqwrbZSUsE69ZMRHfiiyLAJb7C7hJtYmM4Gsbajy7gKagu97E8q5rlS9k8FhojA8cpGhWw==
   dependencies:
-    browserslist "^4.16.0"
-    postcss-value-parser "^4.1.0"
+    browserslist "^4.16.6"
+    postcss-value-parser "^4.2.0"
 
 postcss-normalize-url@^5.0.4:
   version "5.0.4"
@@ -4776,20 +4806,20 @@ postcss-normalize-url@^5.0.4:
     normalize-url "^6.0.1"
     postcss-value-parser "^4.2.0"
 
-postcss-normalize-whitespace@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz#b0b40b5bcac83585ff07ead2daf2dcfbeeef8e9a"
-  integrity sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA==
-  dependencies:
-    postcss-value-parser "^4.1.0"
-
-postcss-ordered-values@^5.0.2:
+postcss-normalize-whitespace@^5.0.2:
   version "5.0.2"
-  resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.0.2.tgz#1f351426977be00e0f765b3164ad753dac8ed044"
-  integrity sha512-8AFYDSOYWebJYLyJi3fyjl6CqMEG/UVworjiyK1r573I56kb3e879sCJLGvR3merj+fAdPpVplXKQZv+ey6CgQ==
+  resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.2.tgz#92c5eaffe5255b5c43fca0baf19227e607c534db"
+  integrity sha512-CXBx+9fVlzSgbk0IXA/dcZn9lXixnQRndnsPC5ht3HxlQ1bVh77KQDL1GffJx1LTzzfae8ftMulsjYmO2yegxA==
   dependencies:
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
+    postcss-value-parser "^4.2.0"
+
+postcss-ordered-values@^5.0.3:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.0.3.tgz#d80a8565f2e21efe8a06abacd60629a783bbcf54"
+  integrity sha512-T9pDS+P9bWeFvqivXd5ACzQmrCmHjv3ZP+djn8E1UZY7iK79pFSm7i3WbKw2VSmFmdbMm8sQ12OPcNpzBo3Z2w==
+  dependencies:
+    cssnano-utils "^3.0.0"
+    postcss-value-parser "^4.2.0"
 
 postcss-reduce-initial@^5.0.2:
   version "5.0.2"
@@ -4799,13 +4829,12 @@ postcss-reduce-initial@^5.0.2:
     browserslist "^4.16.6"
     caniuse-api "^3.0.0"
 
-postcss-reduce-transforms@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz#93c12f6a159474aa711d5269923e2383cedcf640"
-  integrity sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA==
+postcss-reduce-transforms@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.2.tgz#9242758629f9ad4d90312eadbc921259d15bee4d"
+  integrity sha512-25HeDeFsgiPSUx69jJXZn8I06tMxLQJJNF5h7i9gsUg8iP4KOOJ8EX8fj3seeoLt3SLU2YDD6UPnDYVGUO7DEA==
   dependencies:
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
+    postcss-value-parser "^4.2.0"
 
 postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
   version "6.0.4"
@@ -4893,10 +4922,10 @@ printj@~1.1.0:
   resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
   integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==
 
-prismjs@1.25.0:
-  version "1.25.0"
-  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756"
-  integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==
+prismjs@1.26.0:
+  version "1.26.0"
+  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.26.0.tgz#16881b594828bb6b45296083a8cbab46b0accd47"
+  integrity sha512-HUoH9C5Z3jKkl3UunCyiD5jwk0+Hz0fIgQ2nbwU2Oo/ceuTAQAg+pPVnfdt2TJWRVLcxKh9iuoYDUSc8clb5UQ==
 
 private-ip@2.3.3:
   version "2.3.3"
@@ -4908,11 +4937,6 @@ private-ip@2.3.3:
     is-ip "^3.1.0"
     netmask "^2.0.2"
 
-progress@^2.0.0:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
-  integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
-
 promise-limit@2.7.0:
   version "2.7.0"
   resolved "https://registry.yarnpkg.com/promise-limit/-/promise-limit-2.7.0.tgz#eb5737c33342a030eaeaecea9b3d3a93cb592b26"
@@ -5297,10 +5321,10 @@ sass-loader@12.4.0:
     klona "^2.0.4"
     neo-async "^2.6.2"
 
-sass@1.45.2:
-  version "1.45.2"
-  resolved "https://registry.yarnpkg.com/sass/-/sass-1.45.2.tgz#130b428c1692201cfa181139835d6fc378a33323"
-  integrity sha512-cKfs+F9AMPAFlbbTXNsbGvg3y58nV0mXA3E94jqaySKcC8Kq3/8983zVKQ0TLMUrHw7hF9Tnd3Bz9z5Xgtrl9g==
+sass@1.49.0:
+  version "1.49.0"
+  resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.0.tgz#65ec1b1d9a6bc1bae8d2c9d4b392c13f5d32c078"
+  integrity sha512-TVwVdNDj6p6b4QymJtNtRS2YtLJ/CqZriGg0eIAbAKMlN8Xy6kbv33FsEZSF7FufFFM705SQviHjjThfaQ4VNw==
   dependencies:
     chokidar ">=3.0.0 <4.0.0"
     immutable "^4.0.0"
@@ -5339,7 +5363,7 @@ seedrandom@3.0.5:
   resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7"
   integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==
 
-semver@^7.2.1, semver@^7.3.2, semver@^7.3.4:
+semver@^7.3.2, semver@^7.3.4:
   version "7.3.4"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
   integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
@@ -5771,10 +5795,10 @@ textarea-caret@3.1.0:
   resolved "https://registry.yarnpkg.com/textarea-caret/-/textarea-caret-3.1.0.tgz#5d5a35bb035fd06b2ff0e25d5359e97f2655087f"
   integrity sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==
 
-three@0.117.1:
-  version "0.117.1"
-  resolved "https://registry.yarnpkg.com/three/-/three-0.117.1.tgz#a49bcb1a6ddea2f250003e42585dc3e78e92b9d3"
-  integrity sha512-t4zeJhlNzUIj9+ub0l6nICVimSuRTZJOqvk3Rmlu+YGdTOJ49Wna8p7aumpkXJakJfITiybfpYE1XN1o1Z34UQ==
+three@0.136.0:
+  version "0.136.0"
+  resolved "https://registry.yarnpkg.com/three/-/three-0.136.0.tgz#b1504db021b46398ef468aa7849f3dcabb814f50"
+  integrity sha512-+fEMX7nYLz2ZesVP/dyifli5Jf8gR3XPAnFJveQ80aMhibFduzrADnjMbARXh8+W9qLK7rshJCjAIL/6cDxC+A==
 
 throttle-debounce@3.0.1:
   version "3.0.1"
@@ -5873,7 +5897,7 @@ tsc-alias@1.5.0:
     mylas "^2.1.6"
     normalize-path "^3.0.0"
 
-tsconfig-paths@3.12.0:
+tsconfig-paths@3.12.0, tsconfig-paths@^3.12.0:
   version "3.12.0"
   resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz#19769aca6ee8f6a1a341e38c8fa45dd9fb18899b"
   integrity sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==
@@ -5883,16 +5907,6 @@ tsconfig-paths@3.12.0:
     minimist "^1.2.0"
     strip-bom "^3.0.0"
 
-tsconfig-paths@^3.11.0:
-  version "3.11.0"
-  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36"
-  integrity sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==
-  dependencies:
-    "@types/json5" "^0.0.29"
-    json5 "^1.0.1"
-    minimist "^1.2.0"
-    strip-bom "^3.0.0"
-
 tslib@^1.8.1, tslib@^1.9.0:
   version "1.11.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
@@ -5961,10 +5975,10 @@ typedarray-to-buffer@^3.1.5:
   dependencies:
     is-typedarray "^1.0.0"
 
-typescript@4.5.4:
-  version "4.5.4"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8"
-  integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==
+typescript@4.5.5:
+  version "4.5.5"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3"
+  integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==
 
 unbox-primitive@^1.0.1:
   version "1.0.1"
@@ -6139,16 +6153,16 @@ vue-svg-loader@0.17.0-beta.2:
     semver "^7.3.2"
     svgo "^1.3.2"
 
-vue@3.2.26:
-  version "3.2.26"
-  resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.26.tgz#5db575583ecae495c7caa5c12fd590dffcbb763e"
-  integrity sha512-KD4lULmskL5cCsEkfhERVRIOEDrfEL9CwAsLYpzptOGjaGFNWo3BQ9g8MAb7RaIO71rmVOziZ/uEN/rHwcUIhg==
+vue@3.2.28:
+  version "3.2.28"
+  resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.28.tgz#840d193bf9713f57a365ef115c4b1286d43e0e5d"
+  integrity sha512-U+jBwVh3RQ9AgceLFdT7i2FFujoC+kYuGrKo5y8aLluWKZWPS40WgA2pyYHaiSX9ydCbEGr3rc/JzdqskzD95g==
   dependencies:
-    "@vue/compiler-dom" "3.2.26"
-    "@vue/compiler-sfc" "3.2.26"
-    "@vue/runtime-dom" "3.2.26"
-    "@vue/server-renderer" "3.2.26"
-    "@vue/shared" "3.2.26"
+    "@vue/compiler-dom" "3.2.28"
+    "@vue/compiler-sfc" "3.2.28"
+    "@vue/runtime-dom" "3.2.28"
+    "@vue/server-renderer" "3.2.28"
+    "@vue/shared" "3.2.28"
 
 vuedraggable@4.0.1:
   version "4.0.1"
@@ -6235,10 +6249,10 @@ webpack-sources@^3.2.2:
   resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.2.tgz#d88e3741833efec57c4c789b6010db9977545260"
   integrity sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw==
 
-webpack@5.65.0:
-  version "5.65.0"
-  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.65.0.tgz#ed2891d9145ba1f0d318e4ea4f89c3fa18e6f9be"
-  integrity sha512-Q5or2o6EKs7+oKmJo7LaqZaMOlDWQse9Tm5l1WAfU/ujLGN5Pb0SqGeVkN/4bpPmEqEP5RnVhiqsOtWtUVwGRw==
+webpack@5.66.0:
+  version "5.66.0"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.66.0.tgz#789bf36287f407fc92b3e2d6f978ddff1bfc2dbb"
+  integrity sha512-NJNtGT7IKpGzdW7Iwpn/09OXz9inIkeIQ/ibY6B+MdV1x6+uReqz/5z1L89ezWnpPDWpXF0TY5PCYKQdWVn8Vg==
   dependencies:
     "@types/eslint-scope" "^3.7.0"
     "@types/estree" "^0.0.50"
@@ -6254,7 +6268,7 @@ webpack@5.65.0:
     eslint-scope "5.1.1"
     events "^3.2.0"
     glob-to-regexp "^0.4.1"
-    graceful-fs "^4.2.4"
+    graceful-fs "^4.2.9"
     json-parse-better-errors "^1.0.2"
     loader-runner "^4.2.0"
     mime-types "^2.1.27"
@@ -6391,10 +6405,10 @@ wrappy@1:
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
-ws@8.4.0:
-  version "8.4.0"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.0.tgz#f05e982a0a88c604080e8581576e2a063802bed6"
-  integrity sha512-IHVsKe2pjajSUIl4KYMQOdlyliovpEPquKkqbwswulszzI7r0SfQrxnXdWAEqOlDCLrVSJzo+O1hAwdog2sKSQ==
+ws@8.4.2:
+  version "8.4.2"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.2.tgz#18e749868d8439f2268368829042894b6907aa0b"
+  integrity sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA==
 
 xml-js@^1.6.11:
   version "1.6.11"
diff --git a/yarn.lock b/yarn.lock
index 7f74114102..82d2272f6b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,10 +2,10 @@
 # yarn lockfile v1
 
 
-"@cypress/request@^2.88.7":
-  version "2.88.7"
-  resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.7.tgz#386d960ab845a96953723348088525d5a75aaac4"
-  integrity sha512-FTULIP2rnDJvZDT9t6B4nSfYR40ue19tVmv3wUcY05R9/FPCoMl1nAPJkzWzBCo7ltVn5ThQTbxiMoGBN7k0ig==
+"@cypress/request@^2.88.10":
+  version "2.88.10"
+  resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce"
+  integrity sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==
   dependencies:
     aws-sign2 "~0.7.0"
     aws4 "^1.8.0"
@@ -14,8 +14,7 @@
     extend "~3.0.2"
     forever-agent "~0.6.1"
     form-data "~2.3.2"
-    har-validator "~5.1.3"
-    http-signature "~1.2.0"
+    http-signature "~1.3.6"
     is-typedarray "~1.0.0"
     isstream "~0.1.2"
     json-stringify-safe "~5.0.1"
@@ -68,29 +67,30 @@
     "@nodelib/fs.scandir" "2.1.5"
     fastq "^1.6.0"
 
-"@redocly/ajv@^8.6.2":
-  version "8.6.2"
-  resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.6.2.tgz#8c4e485e72f7864f91fae40093bed548ec2619b2"
-  integrity sha512-tU8fQs0D76ZKhJ2cWtnfQthWqiZgGBx0gH0+5D8JvaBEBaqA8foPPBt3Nonwr3ygyv5xrw2IzKWgIY86BlGs+w==
+"@redocly/ajv@^8.6.4":
+  version "8.6.4"
+  resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.6.4.tgz#94053e7a9d4146d1a4feacd3813892873f229a85"
+  integrity sha512-y9qNj0//tZtWB2jfXNK3BX18BSBp9zNR7KE7lMysVHwbZtY392OJCjm6Rb/h4UHH2r1AqjNEHFD6bRn+DqU9Mw==
   dependencies:
     fast-deep-equal "^3.1.1"
     json-schema-traverse "^1.0.0"
     require-from-string "^2.0.2"
     uri-js "^4.2.2"
 
-"@redocly/openapi-core@1.0.0-beta.54":
-  version "1.0.0-beta.54"
-  resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.0.0-beta.54.tgz#42575a849c4dd54b9d0c6413fb8ca547e087cd11"
-  integrity sha512-uYs0N1Trjkh7u8IMIuCU2VxCXhMyGWSZUkP/WNdTR1OgBUtvNdF9C32zoQV+hyCIH4gVu42ROHkjisy333ZX+w==
+"@redocly/openapi-core@1.0.0-beta.79":
+  version "1.0.0-beta.79"
+  resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.0.0-beta.79.tgz#7512b3507ab99dc78226f9069669c5302abb0969"
+  integrity sha512-do79vGt3iiHsaVG9LKY8dH+d1E7TLHr+3T+CQ1lqagtWVjYOxqGaoxAT8tRD7R1W0z8BmS4e2poNON6c1sxP5g==
   dependencies:
-    "@redocly/ajv" "^8.6.2"
+    "@redocly/ajv" "^8.6.4"
     "@types/node" "^14.11.8"
     colorette "^1.2.0"
     js-levenshtein "^1.1.6"
-    js-yaml "^3.14.1"
+    js-yaml "^4.1.0"
     lodash.isequal "^4.5.0"
     minimatch "^3.0.4"
     node-fetch "^2.6.1"
+    pluralize "^8.0.0"
     yaml-ast-parser "0.0.43"
 
 "@sideway/address@^4.1.0":
@@ -125,10 +125,10 @@
   resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5"
   integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==
 
-"@types/fluent-ffmpeg@2.1.17":
-  version "2.1.17"
-  resolved "https://registry.yarnpkg.com/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.17.tgz#6958dda400fe1b33c21f3683db76905cb210d053"
-  integrity sha512-/bdvjKw/mtBHlJ2370d04nt4CsWqU5MrwS/NtO96V01jxitJ4+iq8OFNcqc5CegeV3TQOK3uueK02kvRK+zjUg==
+"@types/fluent-ffmpeg@2.1.20":
+  version "2.1.20"
+  resolved "https://registry.yarnpkg.com/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.20.tgz#3b5f42fc8263761d58284fa46ee6759a64ce54ac"
+  integrity sha512-B+OvhCdJ3LgEq2PhvWNOiB/EfwnXLElfMCgc4Z1K5zXgSfo9I6uGKwR/lqmNPFQuebNnes7re3gqkV77SyypLg==
   dependencies:
     "@types/node" "*"
 
@@ -181,10 +181,10 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.9.tgz#b97c057e6138adb7b720df2bd0264b03c9f504fd"
   integrity sha512-CMjgRNsks27IDwI785YMY0KLt3co/c0cQ5foxHYv/shC2w8oOnVwz5Ubq1QG5KzrcW+AXk6gzdnxIkDnTvzu3g==
 
-"@types/sinonjs__fake-timers@^6.0.2":
-  version "6.0.3"
-  resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.3.tgz#79df6f358ae8f79e628fe35a63608a0ea8e7cf08"
-  integrity sha512-E1dU4fzC9wN2QK2Cr1MLCfyHM8BoNnRFvuf45LYMPNDA+WqbNzC45S4UzPxvp1fFJ1rvSGU0bPvdd35VLmXG8g==
+"@types/sinonjs__fake-timers@8.1.1":
+  version "8.1.1"
+  resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3"
+  integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==
 
 "@types/sizzle@^2.3.2":
   version "2.3.3"
@@ -227,48 +227,48 @@
   dependencies:
     "@types/node" "*"
 
-"@typescript-eslint/parser@5.4.0":
-  version "5.4.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.4.0.tgz#3aa83ce349d66e39b84151f6d5464928044ca9e3"
-  integrity sha512-JoB41EmxiYpaEsRwpZEYAJ9XQURPFer8hpkIW9GiaspVLX8oqbqNM8P4EP8HOZg96yaALiLEVWllA2E8vwsIKw==
+"@typescript-eslint/parser@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.10.0.tgz#8f59e036f5f1cffc178cacbd5ccdd02aeb96c91c"
+  integrity sha512-pJB2CCeHWtwOAeIxv8CHVGJhI5FNyJAIpx5Pt72YkK3QfEzt6qAlXZuyaBmyfOdM62qU0rbxJzNToPTVeJGrQw==
   dependencies:
-    "@typescript-eslint/scope-manager" "5.4.0"
-    "@typescript-eslint/types" "5.4.0"
-    "@typescript-eslint/typescript-estree" "5.4.0"
+    "@typescript-eslint/scope-manager" "5.10.0"
+    "@typescript-eslint/types" "5.10.0"
+    "@typescript-eslint/typescript-estree" "5.10.0"
     debug "^4.3.2"
 
-"@typescript-eslint/scope-manager@5.4.0":
-  version "5.4.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.4.0.tgz#aaab08415f4a9cf32b870c7750ae8ba4607126a1"
-  integrity sha512-pRxFjYwoi8R+n+sibjgF9iUiAELU9ihPBtHzocyW8v8D8G8KeQvXTsW7+CBYIyTYsmhtNk50QPGLE3vrvhM5KA==
+"@typescript-eslint/scope-manager@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.10.0.tgz#bb5d872e8b9e36203908595507fbc4d3105329cb"
+  integrity sha512-tgNgUgb4MhqK6DoKn3RBhyZ9aJga7EQrw+2/OiDk5hKf3pTVZWyqBi7ukP+Z0iEEDMF5FDa64LqODzlfE4O/Dg==
   dependencies:
-    "@typescript-eslint/types" "5.4.0"
-    "@typescript-eslint/visitor-keys" "5.4.0"
+    "@typescript-eslint/types" "5.10.0"
+    "@typescript-eslint/visitor-keys" "5.10.0"
 
-"@typescript-eslint/types@5.4.0":
-  version "5.4.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.4.0.tgz#b1c130f4b381b77bec19696c6e3366f9781ce8f2"
-  integrity sha512-GjXNpmn+n1LvnttarX+sPD6+S7giO+9LxDIGlRl4wK3a7qMWALOHYuVSZpPTfEIklYjaWuMtfKdeByx0AcaThA==
+"@typescript-eslint/types@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.10.0.tgz#beb3cb345076f5b088afe996d57bcd1dfddaa75c"
+  integrity sha512-wUljCgkqHsMZbw60IbOqT/puLfyqqD5PquGiBo1u1IS3PLxdi3RDGlyf032IJyh+eQoGhz9kzhtZa+VC4eWTlQ==
 
-"@typescript-eslint/typescript-estree@5.4.0":
-  version "5.4.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.4.0.tgz#fe524fb308973c68ebeb7428f3b64499a6ba5fc0"
-  integrity sha512-nhlNoBdhKuwiLMx6GrybPT3SFILm5Gij2YBdPEPFlYNFAXUJWX6QRgvi/lwVoadaQEFsizohs6aFRMqsXI2ewA==
+"@typescript-eslint/typescript-estree@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.0.tgz#4be24a3dea0f930bb1397c46187d0efdd955a224"
+  integrity sha512-x+7e5IqfwLwsxTdliHRtlIYkgdtYXzE0CkFeV6ytAqq431ZyxCFzNMNR5sr3WOlIG/ihVZr9K/y71VHTF/DUQA==
   dependencies:
-    "@typescript-eslint/types" "5.4.0"
-    "@typescript-eslint/visitor-keys" "5.4.0"
+    "@typescript-eslint/types" "5.10.0"
+    "@typescript-eslint/visitor-keys" "5.10.0"
     debug "^4.3.2"
     globby "^11.0.4"
     is-glob "^4.0.3"
     semver "^7.3.5"
     tsutils "^3.21.0"
 
-"@typescript-eslint/visitor-keys@5.4.0":
-  version "5.4.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.4.0.tgz#09bc28efd3621f292fe88c86eef3bf4893364c8c"
-  integrity sha512-PVbax7MeE7tdLfW5SA0fs8NGVVr+buMPrcj+CWYWPXsZCH8qZ1THufDzbXm1xrZ2b2PA1iENJ0sRq5fuUtvsJg==
+"@typescript-eslint/visitor-keys@5.10.0":
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.0.tgz#770215497ad67cd15a572b52089991d5dfe06281"
+  integrity sha512-GMxj0K1uyrFLPKASLmZzCuSddmjZVbVj3Ouy5QVuIGKZopxvOr24JsS7gruz6C3GExE01mublZ3mIBOaon9zuQ==
   dependencies:
-    "@typescript-eslint/types" "5.4.0"
+    "@typescript-eslint/types" "5.10.0"
     eslint-visitor-keys "^3.0.0"
 
 aggregate-error@^3.0.0:
@@ -279,16 +279,6 @@ aggregate-error@^3.0.0:
     clean-stack "^2.0.0"
     indent-string "^4.0.0"
 
-ajv@^6.5.5:
-  version "6.12.5"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da"
-  integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==
-  dependencies:
-    fast-deep-equal "^3.1.1"
-    fast-json-stable-stringify "^2.0.0"
-    json-schema-traverse "^0.4.1"
-    uri-js "^4.2.2"
-
 alphanum-sort@^1.0.1, alphanum-sort@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
@@ -584,6 +574,11 @@ balanced-match@^1.0.0:
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
   integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
 
+base64-js@^1.3.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
 base@^0.11.1:
   version "0.11.2"
   resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
@@ -631,7 +626,7 @@ blob-util@^2.0.2:
   resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb"
   integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==
 
-bluebird@3.7.2:
+bluebird@3.7.2, bluebird@^3.7.2:
   version "3.7.2"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
   integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
@@ -690,6 +685,14 @@ buffer-from@^1.0.0:
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
   integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
 
+buffer@^5.6.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
+  dependencies:
+    base64-js "^1.3.1"
+    ieee754 "^1.1.13"
+
 cache-base@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
@@ -827,15 +830,14 @@ cli-cursor@^3.1.0:
   dependencies:
     restore-cursor "^3.1.0"
 
-cli-table3@~0.6.0:
-  version "0.6.0"
-  resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee"
-  integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==
+cli-table3@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8"
+  integrity sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==
   dependencies:
-    object-assign "^4.1.0"
     string-width "^4.2.0"
   optionalDependencies:
-    colors "^1.1.2"
+    colors "1.4.0"
 
 cli-truncate@^2.1.0:
   version "2.1.0"
@@ -971,7 +973,7 @@ colormin@^1.0.5:
     css-color-names "0.0.4"
     has "^1.0.1"
 
-colors@^1.1.2:
+colors@1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
   integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
@@ -1115,24 +1117,25 @@ csso@~2.3.1:
     clap "^1.0.9"
     source-map "^0.5.3"
 
-cypress@9.1.0:
-  version "9.1.0"
-  resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.1.0.tgz#5d23c1b363b7d4853009c74a422a083a8ad2601c"
-  integrity sha512-fyXcCN51vixkPrz/vO/Qy6WL3hKYJzCQFeWofOpGOFewVVXrGfmfSOGFntXpzWBXsIwPn3wzW0HOFw51jZajNQ==
+cypress@9.3.1:
+  version "9.3.1"
+  resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.3.1.tgz#8116f52d49d6daf90a91e88f3eafd940234d2958"
+  integrity sha512-BODdPesxX6bkVUnH8BVsV8I/jn57zQtO1FEOUTiuG2us3kslW7g0tcuwiny7CKCmJUZz8S/D587ppC+s58a+5Q==
   dependencies:
-    "@cypress/request" "^2.88.7"
+    "@cypress/request" "^2.88.10"
     "@cypress/xvfb" "^1.2.4"
     "@types/node" "^14.14.31"
-    "@types/sinonjs__fake-timers" "^6.0.2"
+    "@types/sinonjs__fake-timers" "8.1.1"
     "@types/sizzle" "^2.3.2"
     arch "^2.2.0"
     blob-util "^2.0.2"
-    bluebird "3.7.2"
+    bluebird "^3.7.2"
+    buffer "^5.6.0"
     cachedir "^2.3.0"
     chalk "^4.1.0"
     check-more-types "^2.24.0"
     cli-cursor "^3.1.0"
-    cli-table3 "~0.6.0"
+    cli-table3 "~0.6.1"
     commander "^5.1.0"
     common-tags "^1.8.0"
     dayjs "^1.10.4"
@@ -1389,11 +1392,6 @@ esprima@^2.6.0:
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
   integrity sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=
 
-esprima@^4.0.0:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
-  integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
-
 event-stream@=3.3.4:
   version "3.3.4"
   resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571"
@@ -1557,11 +1555,6 @@ fast-glob@^3.1.1:
     merge2 "^1.3.0"
     micromatch "^4.0.4"
 
-fast-json-stable-stringify@^2.0.0:
-  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==
-
 fastq@^1.6.0:
   version "1.13.0"
   resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
@@ -1978,19 +1971,6 @@ gulplog@^1.0.0:
   dependencies:
     glogg "^1.0.0"
 
-har-schema@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
-  integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
-
-har-validator@~5.1.3:
-  version "5.1.3"
-  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
-  integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
-  dependencies:
-    ajv "^6.5.5"
-    har-schema "^2.0.0"
-
 has-ansi@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -2068,14 +2048,14 @@ html-comment-regex@^1.1.0:
   resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7"
   integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==
 
-http-signature@~1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
-  integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+http-signature@~1.3.6:
+  version "1.3.6"
+  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9"
+  integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==
   dependencies:
     assert-plus "^1.0.0"
-    jsprim "^1.2.2"
-    sshpk "^1.7.0"
+    jsprim "^2.0.2"
+    sshpk "^1.14.1"
 
 human-signals@^1.1.1:
   version "1.1.1"
@@ -2087,6 +2067,11 @@ human-signals@^2.1.0:
   resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
   integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
 
+ieee754@^1.1.13:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
 ignore@^5.1.4:
   version "5.1.9"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.9.tgz#9ec1a5cbe8e1446ec60d4420060d43aa6e7382fb"
@@ -2436,21 +2421,13 @@ js-levenshtein@^1.1.6:
   resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
   integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
 
-js-yaml@4.1.0:
+js-yaml@4.1.0, js-yaml@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
   integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
   dependencies:
     argparse "^2.0.1"
 
-js-yaml@^3.14.1:
-  version "3.14.1"
-  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
-  integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
-  dependencies:
-    argparse "^1.0.7"
-    esprima "^4.0.0"
-
 js-yaml@~3.7.0:
   version "3.7.0"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80"
@@ -2464,20 +2441,15 @@ jsbn@~0.1.0:
   resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
   integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
 
-json-schema-traverse@^0.4.1:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
-  integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
-
 json-schema-traverse@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
   integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
 
-json-schema@0.2.3:
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
-  integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+json-schema@0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
+  integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
 
 json-stable-stringify-without-jsonify@^1.0.1:
   version "1.0.1"
@@ -2498,14 +2470,14 @@ jsonfile@^6.0.1:
   optionalDependencies:
     graceful-fs "^4.1.6"
 
-jsprim@^1.2.2:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
-  integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+jsprim@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d"
+  integrity sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==
   dependencies:
     assert-plus "1.0.0"
     extsprintf "1.3.0"
-    json-schema "0.2.3"
+    json-schema "0.4.0"
     verror "1.10.0"
 
 just-debounce@^1.0.0:
@@ -3139,6 +3111,11 @@ plugin-error@^1.0.1:
     arr-union "^3.1.0"
     extend-shallow "^3.0.2"
 
+pluralize@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
+  integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
+
 posix-character-classes@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@@ -3947,10 +3924,10 @@ sprintf-js@~1.0.2:
   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
   integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
 
-sshpk@^1.7.0:
-  version "1.16.1"
-  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
-  integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
+sshpk@^1.14.1:
+  version "1.17.0"
+  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5"
+  integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==
   dependencies:
     asn1 "~0.2.3"
     assert-plus "^1.0.0"
@@ -4283,10 +4260,10 @@ typedarray@^0.0.6:
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
-typescript@4.5.2:
-  version "4.5.2"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.2.tgz#8ac1fba9f52256fdb06fb89e4122fa6a346c2998"
-  integrity sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==
+typescript@4.5.5:
+  version "4.5.5"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3"
+  integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==
 
 unc-path-regex@^0.1.2:
   version "0.1.2"