diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 713c2e5fdd..8dd9d1c704 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -7,7 +7,9 @@
 		"ghcr.io/devcontainers/features/node:1": {
 			"version": "22.11.0"
 		},
-		"ghcr.io/devcontainers-contrib/features/corepack:1": {}
+		"ghcr.io/devcontainers-extra/features/corepack:1": {
+			"version": "0.31.0"
+		}
 	},
 	"forwardPorts": [3000],
 	"postCreateCommand": "/bin/bash .devcontainer/init.sh",
diff --git a/.github/workflows/ok-to-test.yml b/.github/workflows/ok-to-test.yml
deleted file mode 100644
index 8362c006eb..0000000000
--- a/.github/workflows/ok-to-test.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-# If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event
-name: Ok To Test
-
-on:
-  issue_comment:
-    types: [created]
-
-jobs:
-  ok-to-test:
-    runs-on: ubuntu-latest
-    # Only run for PRs, not issue comments
-    if: ${{ github.event.issue.pull_request }}
-    steps:
-    # Generate a GitHub App installation access token from an App ID and private key
-    # To create a new GitHub App:
-    #   https://developer.github.com/apps/building-github-apps/creating-a-github-app/
-    # See app.yml for an example app manifest
-    - name: Generate token
-      id: generate_token
-      uses: tibdex/github-app-token@v2
-      with:
-        app_id: ${{ secrets.DEPLOYBOT_APP_ID }}
-        private_key: ${{ secrets.DEPLOYBOT_PRIVATE_KEY }}
-
-    - name: Slash Command Dispatch
-      uses: peter-evans/slash-command-dispatch@v4
-      env:
-        TOKEN: ${{ steps.generate_token.outputs.token }}
-      with:
-        token: ${{ env.TOKEN }} # GitHub App installation access token
-        # token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work
-        reaction-token: ${{ secrets.GITHUB_TOKEN }}
-        issue-type: pull-request
-        commands: deploy
-        named-args: true
-        permission: write
diff --git a/.github/workflows/pr-preview-deploy.yml b/.github/workflows/pr-preview-deploy.yml
deleted file mode 100644
index 964d24c3d7..0000000000
--- a/.github/workflows/pr-preview-deploy.yml
+++ /dev/null
@@ -1,92 +0,0 @@
-# Run secret-dependent integration tests only after /deploy approval
-on:
-  repository_dispatch:
-    types: [deploy-command]
-
-name: Deploy preview environment
-
-jobs:
-  # Repo owner has commented /deploy on a (fork-based) pull request
-  deploy-preview-environment:
-    runs-on: ubuntu-latest
-    if:
-      github.event.client_payload.slash_command.sha != '' &&
-      contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha)
-    steps:
-    - uses: actions/github-script@v7.0.1
-      id: check-id
-      env:
-        number: ${{ github.event.client_payload.pull_request.number }}
-        job: ${{ github.job }}
-      with:
-        github-token: ${{ secrets.GITHUB_TOKEN }}
-        result-encoding: string
-        script: |
-          const { data: pull } = await github.rest.pulls.get({
-            ...context.repo,
-            pull_number: process.env.number
-          });
-          const ref = pull.head.sha;
-
-          const { data: checks } = await github.rest.checks.listForRef({
-            ...context.repo,
-            ref
-          });
-
-          const check = checks.check_runs.filter(c => c.name === process.env.job);
-
-          return check[0].id;
-
-    - uses: actions/github-script@v7.0.1
-      env:
-        check_id: ${{ steps.check-id.outputs.result }}
-        details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }}
-      with:
-        github-token: ${{ secrets.GITHUB_TOKEN }}
-        script: |
-          await github.rest.checks.update({
-            ...context.repo,
-            check_run_id: process.env.check_id,
-            status: 'in_progress',
-            details_url: process.env.details_url
-          });
-
-    # Check out merge commit
-    - name: Fork based /deploy checkout
-      uses: actions/checkout@v4.1.1
-      with:
-        ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge'
-
-    # <insert integration tests needing secrets>
-    - name: Context
-      uses: okteto/context@latest
-      with:
-        token: ${{ secrets.OKTETO_TOKEN }}
-
-    - name: Deploy preview environment
-      uses: ikuradon/deploy-preview@latest
-      env:
-        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      with:
-        name: pr-${{ github.event.client_payload.pull_request.number }}-syuilo
-        timeout: 15m
-
-    # Update check run called "integration-fork"
-    - uses: actions/github-script@v7.0.1
-      id: update-check-run
-      if: ${{ always() }}
-      env:
-        # Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run
-        conclusion: ${{ job.status }}
-        check_id: ${{ steps.check-id.outputs.result }}
-      with:
-        github-token: ${{ secrets.GITHUB_TOKEN }}
-        script: |
-          const { data: result } = await github.rest.checks.update({
-            ...context.repo,
-            check_run_id: process.env.check_id,
-            status: 'completed',
-            conclusion: process.env.conclusion
-          });
-
-          return result;
diff --git a/.github/workflows/pr-preview-destroy.yml b/.github/workflows/pr-preview-destroy.yml
deleted file mode 100644
index 8967eb2f94..0000000000
--- a/.github/workflows/pr-preview-destroy.yml
+++ /dev/null
@@ -1,54 +0,0 @@
-# file: .github/workflows/preview-closed.yaml
-on:
-  pull_request:
-    types:
-      - closed
-
-name: Destroy preview environment
-
-jobs:
-  destroy-preview-environment:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/github-script@v7.0.1
-        id: check-conclusion
-        env:
-          number: ${{ github.event.number }}
-        with:
-          github-token: ${{ secrets.GITHUB_TOKEN }}
-          result-encoding: string
-          script: |
-            const { data: pull } = await github.rest.pulls.get({
-              ...context.repo,
-              pull_number: process.env.number
-            });
-            const ref = pull.head.sha;
-
-            const { data: checks } = await github.rest.checks.listForRef({
-              ...context.repo,
-              ref
-            });
-
-            const check = checks.check_runs.filter(c => c.name === 'deploy-preview-environment');
-
-            if (check.length === 0) {
-              return;
-            }
-
-            const { data: result } = await github.rest.checks.get({
-              ...context.repo,
-              check_run_id: check[0].id,
-            });
-
-            return result.conclusion;
-      - name: Context
-        if: steps.check-conclusion.outputs.result == 'success'
-        uses: okteto/context@latest
-        with:
-          token: ${{ secrets.OKTETO_TOKEN }}
-
-      - name: Destroy preview environment
-        if: steps.check-conclusion.outputs.result == 'success'
-        uses: okteto/destroy-preview@latest
-        with:
-          name: pr-${{ github.event.number }}-syuilo
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d17921700..af62232510 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,10 +4,12 @@
 -
 
 ### Client
--
+- Enhance: 投稿フォームの「迷惑になる可能性があります」のダイアログを表示する条件においてCWを考慮するように
+- Enhance: アンテナ、リスト等の名前をカラム名のデフォルト値にするように `#13992`
+- Enhance: クライアントエラー画面の多言語対応
 
 ### Server
--
+- Fix: `following/invalidate`でフォロワーを解除しようとしているユーザーの情報を返すように
 
 
 ## 2025.2.0
@@ -45,6 +47,7 @@
 	* β版として公開のため、旧画面も引き続き利用可能です
 
 ### Client
+- Feat: 投稿フォームで画像をプレビュー可能に
 - Enhance: PC画面でチャンネルが複数列で表示されるように  
   (Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13)
 - Enhance: 照会に失敗した場合、その理由を表示するように
diff --git a/locales/index.d.ts b/locales/index.d.ts
index ff9bdd3034..372560aed7 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -10969,6 +10969,52 @@ export interface Locale extends ILocale {
             };
         };
     };
+    "_bootErrors": {
+        /**
+         * 読み込みに失敗しました
+         */
+        "title": string;
+        /**
+         * 少し待ってからリロードしてもまだ問題が解決されない場合、以下のError IDを添えてサーバー管理者に連絡してください。
+         */
+        "serverError": string;
+        /**
+         * 以下を行うと解決する可能性があります。
+         */
+        "solution": string;
+        /**
+         * ブラウザおよびOSを最新バージョンに更新する
+         */
+        "solution1": string;
+        /**
+         * アドブロッカーを無効にする
+         */
+        "solution2": string;
+        /**
+         * ブラウザのキャッシュをクリアする
+         */
+        "solution3": string;
+        /**
+         * (Tor Browser) dom.webaudio.enabledをtrueに設定する
+         */
+        "solution4": string;
+        /**
+         * その他のオプション
+         */
+        "otherOption": string;
+        /**
+         * クライアント設定とキャッシュを削除
+         */
+        "otherOption1": string;
+        /**
+         * 簡易クライアントを起動
+         */
+        "otherOption2": string;
+        /**
+         * 修復ツールを起動
+         */
+        "otherOption3": string;
+    };
 }
 declare const locales: {
     [lang: string]: Locale;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 1713f395e5..ce307f4fe7 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2979,3 +2979,16 @@ _captcha:
     _unknown:
       title: "CAPTCHAエラー"
       text: "想定外のエラーが発生しました。"
+
+_bootErrors:
+  title: "読み込みに失敗しました"
+  serverError: "少し待ってからリロードしてもまだ問題が解決されない場合、以下のError IDを添えてサーバー管理者に連絡してください。"
+  solution: "以下を行うと解決する可能性があります。"
+  solution1: "ブラウザおよびOSを最新バージョンに更新する"
+  solution2: "アドブロッカーを無効にする"
+  solution3: "ブラウザのキャッシュをクリアする"
+  solution4: "(Tor Browser) dom.webaudio.enabledをtrueに設定する"
+  otherOption: "その他のオプション"
+  otherOption1: "クライアント設定とキャッシュを削除"
+  otherOption2: "簡易クライアントを起動"
+  otherOption3: "修復ツールを起動"
diff --git a/packages/backend/.swcrc b/packages/backend/.swcrc
index 845190b5f4..f4bf7a4d2a 100644
--- a/packages/backend/.swcrc
+++ b/packages/backend/.swcrc
@@ -1,5 +1,5 @@
 {
-	"$schema": "https://json.schemastore.org/swcrc",
+	"$schema": "https://swc.rs/schema.json",
 	"jsc": {
 		"parser": {
 			"syntax": "typescript",
diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts
index 8935c2c2da..b45d21410b 100644
--- a/packages/backend/src/server/api/endpoints/following/invalidate.ts
+++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts
@@ -96,7 +96,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			await this.userFollowingService.unfollow(follower, followee);
 
-			return await this.userEntityService.pack(followee.id, me);
+			return await this.userEntityService.pack(follower.id, me);
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts
index 3b20ec1321..ea64e32ee6 100644
--- a/packages/backend/src/server/api/openapi/gen-spec.ts
+++ b/packages/backend/src/server/api/openapi/gen-spec.ts
@@ -210,9 +210,15 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
 
 		spec.paths['/' + endpoint.name] = {
 			...(endpoint.meta.allowGet ? {
-				get: info,
+				get: {
+					...info,
+					operationId: 'get___' + info.operationId,
+				},
 			} : {}),
-			post: info,
+			post: {
+				...info,
+				operationId: 'post___' + info.operationId,
+			},
 		};
 	}
 
diff --git a/packages/backend/src/server/web/boot.embed.js b/packages/backend/src/server/web/boot.embed.js
index 48d1cd262b..9de1275380 100644
--- a/packages/backend/src/server/web/boot.embed.js
+++ b/packages/backend/src/server/web/boot.embed.js
@@ -114,13 +114,17 @@
 		if (document.readyState === 'loading') {
 			await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
 		}
+
+		const locale = JSON.parse(localStorage.getItem('locale') || '{}');
+
+		const title = locale?._bootErrors?.title || 'Failed to initialize Misskey';
+		const reload = locale?.reload || 'Reload';
+
 		document.body.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg>
-		<div class="message">読み込みに失敗しました</div>
-		<div class="submessage">Failed to initialize Misskey</div>
+		<div class="message">${title}</div>
 		<div class="submessage">Error Code: ${code}</div>
 		<button onclick="location.reload(!0)">
-			<div>リロード</div>
-			<div><small>Reload</small></div>
+			<div>${reload}</div>
 		</button>`;
 		addStyle(`
 		#misskey_app,
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index b3952b2fd0..c2a57fd730 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -156,6 +156,22 @@
 			await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
 		}
 
+		const locale = JSON.parse(localStorage.getItem('locale') || '{}');
+
+		const messages = Object.assign({
+			title: 'Failed to initialize Misskey',
+			solution: 'The following actions may solve the problem.',
+			solution1: 'Update your os and browser',
+			solution2: 'Disable an adblocker',
+			solution3: 'Clear the browser cache',
+			solution4: '(Tor Browser) Set dom.webaudio.enabled to true',
+			otherOption: 'Other options',
+			otherOption1: 'Clear preferences and cache',
+			otherOption2: 'Start the simple client',
+			otherOption3: 'Start the repair tool',
+		}, locale?._bootErrors || {});
+		const reload = locale?.reload || 'Reload';
+
 		let errorsElement = document.getElementById('errors');
 
 		if (!errorsElement) {
@@ -165,32 +181,32 @@
 				<path d="M12 9v2m0 4v.01"></path>
 				<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
 			</svg>
-			<h1>Failed to load<br>加載失敗しました</h1>
+			<h1>${messages.title}</h1>
 			<button class="button-big" onclick="location.reload(true);">
-				<span class="button-label-big">Reload / 重新加載</span>
+				<span class="button-label-big">${reload}</span>
 			</button>
-			<p><b>The following actions may solve the problem. / 執行以下操作可能會有所幫助。</b></p>
-			<p>Clear the browser cache / 清除瀏覽器緩存</p>
-			<p>Update your os and browser / 將您的瀏覽器和操作系統更新到最新版本</p>
-			<p>Disable an adblocker / 禁用廣告攔截器</p>
-			<p>&#40;Tor Browser&#41; Set dom.webaudio.enabled to true / 將dom.webaudio.enabled設定為true</p>
+			<p><b>${messages.solution}</b></p>
+			<p>${messages.solution1}</p>
+			<p>${messages.solution2}</p>
+			<p>${messages.solution3}</p>
+			<p>${messages.solution4}</p>
 			<details style="color: #e1aac6;">
-				<summary>Other options / 其他選項</summary>
+				<summary>${messages.otherOption}</summary>
 				<a href="/flush">
 					<button class="button-small">
-						<span class="button-label-small">Clear preferences and cache</span>
+						<span class="button-label-small">${messages.otherOption1}</span>
 					</button>
 				</a>
 				<br>
 				<a href="/cli">
 					<button class="button-small">
-						<span class="button-label-small">Start the simple client</span>
+						<span class="button-label-small">${messages.otherOption2}</span>
 					</button>
 				</a>
 				<br>
 				<a href="/bios">
 					<button class="button-small">
-						<span class="button-label-small">Start the repair tool</span>
+						<span class="button-label-small">${messages.otherOption3}</span>
 					</button>
 				</a>
 			</details>
diff --git a/packages/backend/test-server/.swcrc b/packages/backend/test-server/.swcrc
index e3d6935169..eeac7eabc6 100644
--- a/packages/backend/test-server/.swcrc
+++ b/packages/backend/test-server/.swcrc
@@ -1,5 +1,5 @@
 {
-	"$schema": "https://json.schemastore.org/swcrc",
+	"$schema": "https://swc.rs/schema.json",
 	"jsc": {
 		"parser": {
 			"syntax": "typescript",
diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx
index 8830523810..3cd08191f5 100644
--- a/packages/frontend/.storybook/generate.tsx
+++ b/packages/frontend/.storybook/generate.tsx
@@ -414,6 +414,7 @@ function toStories(component: string): Promise<string> {
 		glob('src/components/MkSignupServerRules.vue'),
 		glob('src/components/MkUserSetupDialog.vue'),
 		glob('src/components/MkUserSetupDialog.*.vue'),
+		glob('src/components/MkImgPreviewDialog.vue'),
 		glob('src/components/MkInstanceCardMini.vue'),
 		glob('src/components/MkInviteCode.vue'),
 		glob('src/components/MkTagItem.vue'),
diff --git a/packages/frontend/src/components/MkImgPreviewDialog.stories.impl.ts b/packages/frontend/src/components/MkImgPreviewDialog.stories.impl.ts
new file mode 100644
index 0000000000..339e6d10f3
--- /dev/null
+++ b/packages/frontend/src/components/MkImgPreviewDialog.stories.impl.ts
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { StoryObj } from '@storybook/vue3';
+import { file } from '../../.storybook/fakes.js';
+import MkImgPreviewDialog from './MkImgPreviewDialog.vue';
+export const Default = {
+	render(args) {
+		return {
+			components: {
+				MkImgPreviewDialog,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkImgPreviewDialog v-bind="props" />',
+		};
+	},
+	args: {
+		file: file(),
+	},
+	parameters: {
+		chromatic: {
+			// NOTE: ロードが終わるまで待つ
+			delay: 3000,
+		},
+		layout: 'centered',
+	},
+} satisfies StoryObj<typeof MkImgPreviewDialog>;
diff --git a/packages/frontend/src/components/MkImgPreviewDialog.vue b/packages/frontend/src/components/MkImgPreviewDialog.vue
new file mode 100644
index 0000000000..3e6e4e0ec9
--- /dev/null
+++ b/packages/frontend/src/components/MkImgPreviewDialog.vue
@@ -0,0 +1,58 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkModalWindow
+	ref="modal"
+	:width="1800"
+	:height="900"
+	@close="close"
+	@esc="close"
+	@click="close"
+>
+	<template #header>{{ file.name }}</template>
+	<div :class="$style.container">
+		<img :src="file.url" :alt="file.comment ?? file.name" :class="$style.img"/>
+	</div>
+</MkModalWindow>
+</template>
+<script lang="ts" setup>
+import { defineProps, ref } from 'vue';
+import MkModalWindow from './MkModalWindow.vue';
+import type * as Misskey from 'misskey-js';
+
+defineProps<{
+	file: Misskey.entities.DriveFile;
+}>();
+
+const modal = ref<typeof MkModalWindow | null>(null);
+
+function close() {
+	modal.value?.close();
+}
+
+</script>
+<style lang="scss" module>
+	.container {
+		box-sizing: border-box;
+		width: 100%;
+		height: 100%;
+		min-height: 0;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		overflow: hidden;
+
+		background-color: var(--MI_THEME-bg);
+		background-size: auto auto;
+		background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px);
+	}
+
+	.img {
+		width: 100%;
+		max-height: 100%;
+		object-fit: contain;
+	}
+</style>
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 58940257bc..d03c7e51bf 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -808,6 +808,14 @@ function deleteDraft() {
 	miLocalStorage.setItem('drafts', JSON.stringify(draftData));
 }
 
+function isAnnoying(text: string): boolean {
+	return text.includes('$[x2') ||
+		text.includes('$[x3') ||
+		text.includes('$[x4') ||
+		text.includes('$[scale') ||
+		text.includes('$[position');
+}
+
 async function post(ev?: MouseEvent) {
 	if (useCw.value && (cw.value == null || cw.value.trim() === '')) {
 		os.alert({
@@ -832,14 +840,10 @@ async function post(ev?: MouseEvent) {
 
 	if (props.mock) return;
 
-	const annoying =
-		text.value.includes('$[x2') ||
-		text.value.includes('$[x3') ||
-		text.value.includes('$[x4') ||
-		text.value.includes('$[scale') ||
-		text.value.includes('$[position');
-
-	if (annoying && visibility.value === 'public') {
+	if (visibility.value === 'public' && (
+		(useCw.value && cw.value != null && cw.value.trim() !== '' && isAnnoying(cw.value)) || // CWが迷惑になる場合
+		((!useCw.value || cw.value == null || cw.value.trim() === '') && text.value != null && text.value.trim() !== '' && isAnnoying(text.value)) // CWが無い かつ 本文が迷惑になる場合
+	)) {
 		const { canceled, result } = await os.actions({
 			type: 'warning',
 			text: i18n.ts.thisPostMayBeAnnoying,
diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index 1336b61356..33fe82b759 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -22,20 +22,24 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 		</template>
 	</Sortable>
-	<p :class="[$style.remain, {
-		[$style.exceeded]: props.modelValue.length > 16,
-	}]">{{ 16 - props.modelValue.length }}/16</p>
+	<p
+		:class="[$style.remain, {
+			[$style.exceeded]: props.modelValue.length > 16,
+		}]"
+	>
+		{{ 16 - props.modelValue.length }}/16
+	</p>
 </div>
 </template>
 
 <script lang="ts" setup>
 import { defineAsyncComponent, inject } from 'vue';
 import * as Misskey from 'misskey-js';
+import type { MenuItem } from '@/types/menu';
 import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import type { MenuItem } from '@/types/menu.js';
 
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 
@@ -168,6 +172,14 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
 			text: i18n.ts.cropImage,
 			icon: 'ti ti-crop',
 			action: () : void => { crop(file); },
+		}, {
+			text: i18n.ts.preview,
+			icon: 'ti ti-photo-search',
+			action: () => {
+				os.popup(defineAsyncComponent(() => import('@/components/MkImgPreviewDialog.vue')), {
+					file: file,
+				});
+			},
 		});
 	}
 
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 69cc62ad6e..6e48366092 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -310,6 +310,21 @@ export function inputText(props: {
 } | {
 	canceled: false; result: string;
 }>;
+// min lengthが指定されてたら result は null になり得ないことを保証する overload function
+export function inputText(props: {
+	type?: 'text' | 'email' | 'password' | 'url';
+	title?: string;
+	text?: string;
+	placeholder?: string | null;
+	autocomplete?: string;
+	default?: string;
+	minLength: number;
+	maxLength?: number;
+}): Promise<{
+	canceled: true; result: undefined;
+} | {
+	canceled: false; result: string;
+}>;
 export function inputText(props: {
 	type?: 'text' | 'email' | 'password' | 'url';
 	title?: string;
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index ce5b14b0e8..0b870fc185 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -193,7 +193,7 @@ const addColumn = async (ev) => {
 	addColumnToStore({
 		type: column,
 		id: uuid(),
-		name: i18n.ts._deck._columns[column],
+		name: null,
 		width: 330,
 		soundSetting: { type: null, volume: 1 },
 	});
diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue
index 07ae17b982..b79cd8408b 100644
--- a/packages/frontend/src/ui/deck/antenna-column.vue
+++ b/packages/frontend/src/ui/deck/antenna-column.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
 	<template #header>
-		<i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span>
+		<i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name || antennaName || i18n.ts._deck._columns.antenna }}</span>
 	</template>
 
 	<MkTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @note="onNote"/>
@@ -36,6 +36,7 @@ const props = defineProps<{
 
 const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
 const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
+const antennaName = ref<string | null>(null);
 
 onMounted(() => {
 	if (props.column.antennaId == null) {
@@ -43,6 +44,13 @@ onMounted(() => {
 	}
 });
 
+watch([() => props.column.name, () => props.column.antennaId], () => {
+	if (!props.column.name && props.column.antennaId) {
+		misskeyApi('antennas/show', { antennaId: props.column.antennaId })
+			.then(value => antennaName.value = value.name);
+	}
+});
+
 watch(soundSetting, v => {
 	updateColumn(props.column.id, { soundSetting: v });
 });
diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue
index d656eec60a..9e07c06639 100644
--- a/packages/frontend/src/ui/deck/channel-column.vue
+++ b/packages/frontend/src/ui/deck/channel-column.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
 	<template #header>
-		<i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name }}</span>
+		<i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name || channel?.name || i18n.ts._deck._columns.channel }}</span>
 	</template>
 
 	<template v-if="column.channelId">
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, shallowRef, watch } from 'vue';
+import { onMounted, ref, shallowRef, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import XColumn from './column.vue';
 import { updateColumn } from './deck-store.js';
@@ -44,9 +44,18 @@ const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
 const channel = shallowRef<Misskey.entities.Channel>();
 const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
 
-if (props.column.channelId == null) {
-	setChannel();
-}
+onMounted(() => {
+	if (props.column.channelId == null) {
+		setChannel();
+	}
+});
+
+watch([() => props.column.name, () => props.column.channelId], () => {
+	if (!props.column.name && props.column.channelId) {
+		misskeyApi('channels/show', { channelId: props.column.channelId })
+			.then(value => channel.value = value);
+	}
+});
 
 watch(soundSetting, v => {
 	updateColumn(props.column.id, { soundSetting: v });
diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index 34d7b2d4b5..f23e33c748 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -129,7 +129,8 @@ function getMenu() {
 		icon: 'ti ti-settings',
 		text: i18n.ts._deck.configureColumn,
 		action: async () => {
-			const { canceled, result } = await os.form(props.column.name, {
+			const name = props.column.name ?? i18n.ts._deck._columns[props.column.type];
+			const { canceled, result } = await os.form(name, {
 				name: {
 					type: 'string',
 					label: i18n.ts.name,
@@ -144,7 +145,7 @@ function getMenu() {
 				flexible: {
 					type: 'boolean',
 					label: i18n.ts._deck.flexible,
-					default: props.column.flexible,
+					default: props.column.flexible ?? null,
 				},
 			});
 			if (canceled) return;
diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts
index 710c6a7bfc..4782a2fec5 100644
--- a/packages/frontend/src/ui/deck/deck-store.ts
+++ b/packages/frontend/src/ui/deck/deck-store.ts
@@ -51,7 +51,7 @@ export type Column = {
 	withReplies?: boolean;
 	withSensitive?: boolean;
 	onlyFiles?: boolean;
-	soundSetting: SoundStore;
+	soundSetting?: SoundStore;
 };
 
 export const deckStore = markRaw(new Storage('deck', {
@@ -94,7 +94,7 @@ export const loadDeck = async () => {
 			key: deckStore.state.profile,
 		});
 	} catch (err) {
-		if (err.code === 'NO_SUCH_KEY') {
+		if (typeof err === 'object' && err != null && 'code' in err && err.code === 'NO_SUCH_KEY') {
 			// 後方互換性のため
 			if (deckStore.state.profile === 'default') {
 				saveDeck();
@@ -180,6 +180,7 @@ export function swapLeftColumn(id: Column['id']) {
 			}
 			return true;
 		}
+		return false;
 	});
 	saveDeck();
 }
@@ -196,6 +197,7 @@ export function swapRightColumn(id: Column['id']) {
 			}
 			return true;
 		}
+		return false;
 	});
 	saveDeck();
 }
@@ -216,6 +218,7 @@ export function swapUpColumn(id: Column['id']) {
 			}
 			return true;
 		}
+		return false;
 	});
 	saveDeck();
 }
@@ -236,6 +239,7 @@ export function swapDownColumn(id: Column['id']) {
 			}
 			return true;
 		}
+		return false;
 	});
 	saveDeck();
 }
@@ -286,7 +290,8 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
 	const columns = deepClone(deckStore.state.columns);
 	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
 	const column = deepClone(deckStore.state.columns[columnIndex]);
-	if (column == null || column.widgets == null) return;
+	if (column == null) return;
+	if (column.widgets == null) column.widgets = [];
 	column.widgets = column.widgets.filter(w => w.id !== widget.id);
 	columns[columnIndex] = column;
 	deckStore.set('columns', columns);
@@ -308,7 +313,8 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat
 	const columns = deepClone(deckStore.state.columns);
 	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
 	const column = deepClone(deckStore.state.columns[columnIndex]);
-	if (column == null || column.widgets == null) return;
+	if (column == null) return;
+	if (column.widgets == null) column.widgets = [];
 	column.widgets = column.widgets.map(w => w.id === widgetId ? {
 		...w,
 		data: widgetData,
diff --git a/packages/frontend/src/ui/deck/direct-column.vue b/packages/frontend/src/ui/deck/direct-column.vue
index 4337397c2a..2cecd6c669 100644
--- a/packages/frontend/src/ui/deck/direct-column.vue
+++ b/packages/frontend/src/ui/deck/direct-column.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()">
-	<template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name }}</template>
+	<template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.direct }}</template>
 
 	<MkNotes ref="tlComponent" :pagination="pagination"/>
 </XColumn>
@@ -16,6 +16,7 @@ import { ref } from 'vue';
 import XColumn from './column.vue';
 import type { Column } from './deck-store.js';
 import MkNotes from '@/components/MkNotes.vue';
+import { i18n } from '@/i18n.js';
 
 defineProps<{
 	column: Column;
diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue
index a22a8743b4..83961d02bc 100644
--- a/packages/frontend/src/ui/deck/list-column.vue
+++ b/packages/frontend/src/ui/deck/list-column.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
 	<template #header>
-		<i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span>
+		<i class="ti ti-list"></i><span style="margin-left: 8px;">{{ (column.name || listName) ?? i18n.ts._deck._columns.list }}</span>
 	</template>
 
 	<MkTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" :withRenotes="withRenotes" @note="onNote"/>
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { watch, shallowRef, ref } from 'vue';
+import { watch, shallowRef, ref, onMounted } from 'vue';
 import type { entities as MisskeyEntities } from 'misskey-js';
 import XColumn from './column.vue';
 import { updateColumn } from './deck-store.js';
@@ -37,10 +37,20 @@ const props = defineProps<{
 const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
 const withRenotes = ref(props.column.withRenotes ?? true);
 const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
+const listName = ref<string | null>(null);
 
-if (props.column.listId == null) {
-	setList();
-}
+onMounted(() => {
+	if (props.column.listId == null) {
+		setList();
+	}
+});
+
+watch([() => props.column.name, () => props.column.listId], () => {
+	if (!props.column.name && props.column.listId) {
+		misskeyApi('users/lists/show', { listId: props.column.listId })
+			.then(value => listName.value = value.name);
+	}
+});
 
 watch(withRenotes, v => {
 	updateColumn(props.column.id, {
diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue
index fde02eaf5b..233fba554b 100644
--- a/packages/frontend/src/ui/deck/mentions-column.vue
+++ b/packages/frontend/src/ui/deck/mentions-column.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()">
-	<template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
+	<template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.mentions }}</template>
 
 	<MkNotes ref="tlComponent" :pagination="pagination"/>
 </XColumn>
@@ -16,6 +16,7 @@ import { ref } from 'vue';
 import XColumn from './column.vue';
 import type { Column } from './deck-store.js';
 import MkNotes from '@/components/MkNotes.vue';
+import { i18n } from '../../i18n.js';
 
 defineProps<{
 	column: Column;
diff --git a/packages/frontend/src/ui/deck/notifications-column.vue b/packages/frontend/src/ui/deck/notifications-column.vue
index fdf80ea987..c0303e86dc 100644
--- a/packages/frontend/src/ui/deck/notifications-column.vue
+++ b/packages/frontend/src/ui/deck/notifications-column.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <XColumn :column="column" :isStacked="isStacked" :menu="menu" :refresher="async () => { await notificationsComponent?.reload() }">
-	<template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
+	<template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.notifications }}</template>
 
 	<XNotifications ref="notificationsComponent" :excludeTypes="props.column.excludeTypes"/>
 </XColumn>
diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue
index 8fb0729bf2..5b1420570d 100644
--- a/packages/frontend/src/ui/deck/role-timeline-column.vue
+++ b/packages/frontend/src/ui/deck/role-timeline-column.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
 	<template #header>
-		<i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span>
+		<i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name || roleName || i18n.ts._deck._columns.roleTimeline }}</span>
 	</template>
 
 	<MkTimeline v-if="column.roleId" ref="timeline" src="role" :role="column.roleId" @note="onNote"/>
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref, shallowRef, watch } from 'vue';
+import { computed, onMounted, ref, shallowRef, watch } from 'vue';
 import XColumn from './column.vue';
 import { updateColumn } from './deck-store.js';
 import type { Column } from './deck-store.js';
@@ -34,6 +34,7 @@ const props = defineProps<{
 
 const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
 const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
+const roleName = ref<string | null>(null);
 
 onMounted(() => {
 	if (props.column.roleId == null) {
@@ -41,6 +42,13 @@ onMounted(() => {
 	}
 });
 
+watch([() => props.column.name, () => props.column.roleId], () => {
+	if (!props.column.name && props.column.roleId) {
+		misskeyApi('roles/show', { roleId: props.column.roleId })
+			.then(value => roleName.value = value.name);
+	}
+});
+
 watch(soundSetting, v => {
 	updateColumn(props.column.id, { soundSetting: v });
 });
diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue
index 99f6482457..b9b3746abf 100644
--- a/packages/frontend/src/ui/deck/tl-column.vue
+++ b/packages/frontend/src/ui/deck/tl-column.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
 	<template #header>
 		<i v-if="column.tl != null" :class="basicTimelineIconClass(column.tl)"/>
-		<span style="margin-left: 8px;">{{ column.name }}</span>
+		<span style="margin-left: 8px;">{{ column.name || (column.tl ? i18n.ts._timelines[column.tl] : null) || i18n.ts._deck._columns.tl }}</span>
 	</template>
 
 	<div v-if="!isAvailableBasicTimeline(column.tl)" :class="$style.disabled">
diff --git a/packages/frontend/src/ui/deck/widgets-column.vue b/packages/frontend/src/ui/deck/widgets-column.vue
index a4d72b9255..20284d8c9f 100644
--- a/packages/frontend/src/ui/deck/widgets-column.vue
+++ b/packages/frontend/src/ui/deck/widgets-column.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <XColumn :menu="menu" :naked="true" :column="column" :isStacked="isStacked">
-	<template #header><i class="ti ti-apps" style="margin-right: 8px;"></i>{{ column.name }}</template>
+	<template #header><i class="ti ti-apps" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns[props.column.type] }}</template>
 
 	<div :class="$style.root">
 		<div v-if="!(column.widgets && column.widgets.length > 0) && !edit" :class="$style.intro">{{ i18n.ts._deck.widgetsIntroduction }}</div>
diff --git a/packages/misskey-js/.swcrc b/packages/misskey-js/.swcrc
index 0504a2d389..6fd7486370 100644
--- a/packages/misskey-js/.swcrc
+++ b/packages/misskey-js/.swcrc
@@ -1,5 +1,5 @@
 {
-	"$schema": "https://json.schemastore.org/swcrc",
+	"$schema": "https://swc.rs/schema.json",
 	"jsc": {
 		"parser": {
 			"syntax": "typescript",
diff --git a/packages/misskey-js/generator/src/generator.ts b/packages/misskey-js/generator/src/generator.ts
index 88f2ae9ee9..a77ffeedc2 100644
--- a/packages/misskey-js/generator/src/generator.ts
+++ b/packages/misskey-js/generator/src/generator.ts
@@ -1,8 +1,9 @@
-import { mkdir, writeFile } from 'fs/promises';
+import assert from 'assert';
+import { mkdir, readFile, writeFile } from 'fs/promises';
 import { OpenAPIV3_1 } from 'openapi-types';
 import { toPascal } from 'ts-case-convert';
 import OpenAPIParser from '@readme/openapi-parser';
-import openapiTS from 'openapi-typescript';
+import openapiTS, { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript';
 
 async function generateBaseTypes(
 	openApiDocs: OpenAPIV3_1.Document,
@@ -20,7 +21,29 @@ async function generateBaseTypes(
 	}
 	lines.push('');
 
-	const generatedTypes = await openapiTS(openApiJsonPath, {
+	// NOTE: Align `operationId` of GET and POST to avoid duplication of type definitions
+	const openApi = JSON.parse(await readFile(openApiJsonPath, 'utf8')) as OpenAPI3;
+	// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+	for (const [key, item] of Object.entries(openApi.paths!)) {
+		assert('post' in item);
+		// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+		openApi.paths![key] = {
+			...('get' in item ? {
+				get: {
+					...item.get,
+					// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+					operationId: ((item as PathItemObject).get as OperationObject).operationId!.replaceAll('get___', ''),
+				},
+			} : {}),
+			post: {
+				...item.post,
+				// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+				operationId: ((item as PathItemObject).post as OperationObject).operationId!.replaceAll('post___', ''),
+			},
+		};
+	}
+
+	const generatedTypes = await openapiTS(openApi, {
 		exportType: true,
 		transform(schemaObject) {
 			if ('format' in schemaObject && schemaObject.format === 'binary') {
@@ -78,7 +101,7 @@ async function generateEndpoints(
 	for (const operation of postPathItems) {
 		const path = operation._path_;
 		// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-		const operationId = operation.operationId!;
+		const operationId = operation.operationId!.replaceAll('get___', '').replaceAll('post___', '');
 		const endpoint = new Endpoint(path);
 		endpoints.push(endpoint);
 
@@ -195,7 +218,7 @@ async function generateApiClientJSDoc(
 
 	for (const operation of postPathItems) {
 		// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-		const operationId = operation.operationId!;
+		const operationId = operation.operationId!.replaceAll('get___', '').replaceAll('post___', '');
 
 		if (operation.description) {
 			endpoints.push({