From b1073714ba65e60ff90b448e71415c106eba623b Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 28 Oct 2024 12:46:39 +0900
Subject: [PATCH 01/13] Update about-misskey.vue

---
 packages/frontend/src/pages/about-misskey.vue | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index 68b98c2ab7..fbbfb6ea61 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -269,6 +269,9 @@ const patronsWithIcon = [{
 }, {
 	name: '如月ユカ',
 	icon: 'https://assets.misskey-hub.net/patrons/f24a042076a041b6811a2f124eb620ca.jpg',
+}, {
+	name: 'Yatoigawa',
+	icon: 'https://assets.misskey-hub.net/patrons/505e3568885a4a488431a8f22b4553d0.jpg',
 }];
 
 const patrons = [
@@ -375,6 +378,8 @@ const patrons = [
 	'はとぽぷさん',
 	'100の人 (エスパー・イーシア)',
 	'ケモナーのケシン',
+	'こまつぶり',
+	'まゆつな空高',
 ];
 
 const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));

From e927507886b9478c9f7197cf64ce375cf65a164c Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 28 Oct 2024 18:34:18 +0900
Subject: [PATCH 02/13] :art:

---
 packages/frontend/src/pages/emoji-edit-dialog.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index 969aa6bbf7..3b3f41d9b1 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	ref="windowEl"
 	:initialWidth="400"
 	:initialHeight="500"
-	:canResize="false"
+	:canResize="true"
 	@close="windowEl.close()"
 	@closed="$emit('closed')"
 >

From eecfac1dd933b65bee469a0a103d19f378d3fcef Mon Sep 17 00:00:00 2001
From: woxtu <woxtup@gmail.com>
Date: Mon, 28 Oct 2024 20:22:07 +0900
Subject: [PATCH 03/13] Remove undefined styles (#14858)

---
 packages/frontend/src/components/MkAbuseReport.vue     |  4 +---
 packages/frontend/src/components/MkAuthConfirm.vue     |  4 ++--
 packages/frontend/src/components/MkSignin.password.vue | 10 +++++-----
 packages/frontend/src/components/global/MkAd.vue       |  6 +-----
 4 files changed, 9 insertions(+), 15 deletions(-)

diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index b9413270ae..e48b6ef781 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</template>
 
-	<div :class="$style.root" class="_gaps_s">
+	<div class="_gaps_s">
 		<MkFolder :withSpacer="false">
 			<template #icon><MkAvatar :user="report.targetUser" style="width: 18px; height: 18px;"/></template>
 			<template #label>{{ i18n.ts.target }}: <MkAcct :user="report.targetUser"/></template>
@@ -151,6 +151,4 @@ function showMenu(ev: MouseEvent) {
 </script>
 
 <style lang="scss" module>
-.root {
-}
 </style>
diff --git a/packages/frontend/src/components/MkAuthConfirm.vue b/packages/frontend/src/components/MkAuthConfirm.vue
index f5f6d7f6cc..f78d2d38f0 100644
--- a/packages/frontend/src/components/MkAuthConfirm.vue
+++ b/packages/frontend/src/components/MkAuthConfirm.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 				<div :class="$style.headerText">{{ i18n.ts.pleaseSelectAccount }}</div>
 			</div>
-			<div :class="$style.accountSelectorRoot">
+			<div>
 				<div :class="$style.accountSelectorLabel">{{ i18n.ts.selectAccount }}</div>
 				<div :class="$style.accountSelectorList">
 					<template v-for="[id, user] in users">
@@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 			</div>
 			<slot name="consentAdditionalInfo"></slot>
-			<div :class="$style.accountSelectorRoot">
+			<div>
 				<div :class="$style.accountSelectorLabel">
 					{{ i18n.ts._auth.scopeUser }} <button class="_textButton" @click="clickBackToAccountSelect">{{ i18n.ts.switchAccount }}</button>
 				</div>
diff --git a/packages/frontend/src/components/MkSignin.password.vue b/packages/frontend/src/components/MkSignin.password.vue
index 5608122a39..cd003a39df 100644
--- a/packages/frontend/src/components/MkSignin.password.vue
+++ b/packages/frontend/src/components/MkSignin.password.vue
@@ -24,11 +24,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</MkInput>
 
 			<div v-if="needCaptcha">
-				<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
-				<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
-				<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
-				<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
-				<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha"/>
+				<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
+				<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
+				<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
+				<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
+				<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" provider="testcaptcha"/>
 			</div>
 
 			<MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index 0d68d02e35..08a78c8d81 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div v-if="chosen && !shouldHide" :class="$style.root">
+<div v-if="chosen && !shouldHide">
 	<div
 		v-if="!showMenu"
 		:class="[$style.main, {
@@ -120,10 +120,6 @@ function reduceFrequency(): void {
 </script>
 
 <style lang="scss" module>
-.root {
-
-}
-
 .main {
 	text-align: center;
 

From 74847bce303449124282a748fc50b1c6588288fc Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 28 Oct 2024 20:42:14 +0900
Subject: [PATCH 04/13] =?UTF-8?q?enhance:=20=E3=82=A2=E3=82=A4=E3=82=B3?=
 =?UTF-8?q?=E3=83=B3=E3=83=87=E3=82=B3=E3=83=AC=E3=83=BC=E3=82=B7=E3=83=A7?=
 =?UTF-8?q?=E3=83=B3=E7=AE=A1=E7=90=86=E7=94=BB=E9=9D=A2=E3=81=AE=E6=94=B9?=
 =?UTF-8?q?=E5=96=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGELOG.md                                  |   1 +
 locales/index.d.ts                            |   4 +
 locales/ja-JP.yml                             |   1 +
 .../admin/avatar-decorations/create.ts        |  57 ++++-
 .../admin/avatar-decorations/list.ts          |   3 -
 .../pages/avatar-decoration-edit-dialog.vue   | 220 ++++++++++++++++++
 .../frontend/src/pages/avatar-decorations.vue | 172 +++++---------
 .../settings/avatar-decoration.decoration.vue |   7 +-
 .../settings/avatar-decoration.dialog.vue     |   4 +-
 packages/misskey-js/etc/misskey-js.api.md     |   4 +
 packages/misskey-js/src/autogen/endpoint.ts   |   3 +-
 packages/misskey-js/src/autogen/entities.ts   |   1 +
 packages/misskey-js/src/autogen/types.ts      |  19 +-
 13 files changed, 374 insertions(+), 122 deletions(-)
 create mode 100644 packages/frontend/src/pages/avatar-decoration-edit-dialog.vue

diff --git a/CHANGELOG.md b/CHANGELOG.md
index cbd48b8b6c..52077f813f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
 - Enhance: Bull DashboardでRelationship Queueの状態も確認できるように  
   (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/751)
 - Enhance: ドライブでソートができるように
+- Enhance: アイコンデコレーション管理画面の改善
 - Enhance: 「単なるラッキー」の取得条件を変更
 - Enhance: 投稿フォームでEscキーを押したときIME入力中ならフォームを閉じないように( #10866 )  
 - Enhance: MiAuth, OAuthの認可画面の改善
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 9058c70496..440f24ac84 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5214,6 +5214,10 @@ export interface Locale extends ILocale {
      * アカウントを選択してください
      */
     "pleaseSelectAccount": string;
+    /**
+     * 利用可能なロール
+     */
+    "availableRoles": string;
     "_accountSettings": {
         /**
          * コンテンツの表示にログインを必須にする
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 1d426f1705..5d8e1a5e72 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1299,6 +1299,7 @@ yourNameContainsProhibitedWordsDescription: "名前に禁止されている文
 thisContentsAreMarkedAsSigninRequiredByAuthor: "投稿者により、表示にはログインが必要と設定されています"
 lockdown: "ロックダウン"
 pleaseSelectAccount: "アカウントを選択してください"
+availableRoles: "利用可能なロール"
 
 _accountSettings:
   requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
index fd21309818..87d80cbe80 100644
--- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
@@ -6,6 +6,7 @@
 import { Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
+import { IdService } from '@/core/IdService.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -13,6 +14,49 @@ export const meta = {
 	requireCredential: true,
 	requireRolePolicy: 'canManageAvatarDecorations',
 	kind: 'write:admin:avatar-decorations',
+
+	res: {
+		type: 'object',
+		optional: false, nullable: false,
+		properties: {
+			id: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+			createdAt: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'date-time',
+			},
+			updatedAt: {
+				type: 'string',
+				optional: false, nullable: true,
+				format: 'date-time',
+			},
+			name: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+			description: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+			url: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+			roleIdsThatCanBeUsedThisDecoration: {
+				type: 'array',
+				optional: false, nullable: false,
+				items: {
+					type: 'string',
+					optional: false, nullable: false,
+					format: 'id',
+				},
+			},
+		},
+	},
 } as const;
 
 export const paramDef = {
@@ -32,14 +76,25 @@ export const paramDef = {
 export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 	constructor(
 		private avatarDecorationService: AvatarDecorationService,
+		private idService: IdService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			await this.avatarDecorationService.create({
+			const created = await this.avatarDecorationService.create({
 				name: ps.name,
 				description: ps.description,
 				url: ps.url,
 				roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
 			}, me);
+
+			return {
+				id: created.id,
+				createdAt: this.idService.parse(created.id).date.toISOString(),
+				updatedAt: null,
+				name: created.name,
+				description: created.description,
+				url: created.url,
+				roleIdsThatCanBeUsedThisDecoration: created.roleIdsThatCanBeUsedThisDecoration,
+			};
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
index aee90023e1..d785f085ac 100644
--- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
@@ -4,10 +4,7 @@
  */
 
 import { Inject, Injectable } from '@nestjs/common';
-import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/_.js';
-import type { MiAnnouncement } from '@/models/Announcement.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import { QueryService } from '@/core/QueryService.js';
 import { DI } from '@/di-symbols.js';
 import { IdService } from '@/core/IdService.js';
 import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
diff --git a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
new file mode 100644
index 0000000000..a834f1c5fd
--- /dev/null
+++ b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
@@ -0,0 +1,220 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkWindow
+	ref="windowEl"
+	:initialWidth="400"
+	:initialHeight="500"
+	:canResize="true"
+	@close="windowEl?.close()"
+	@closed="emit('closed')"
+>
+	<template v-if="avatarDecoration" #header>{{ avatarDecoration.name }}</template>
+	<template v-else #header>New decoration</template>
+
+	<div style="display: flex; flex-direction: column; min-height: 100%;">
+		<MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;">
+			<div class="_gaps_m">
+				<div :class="$style.preview">
+					<div :class="[$style.previewItem, $style.light]">
+						<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="url != '' ? [{ url }] : []" forceShowDecoration/>
+					</div>
+					<div :class="[$style.previewItem, $style.dark]">
+						<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="url != '' ? [{ url }] : []" forceShowDecoration/>
+					</div>
+				</div>
+				<MkInput v-model="name">
+					<template #label>{{ i18n.ts.name }}</template>
+				</MkInput>
+				<MkInput v-model="url">
+					<template #label>{{ i18n.ts.imageUrl }}</template>
+				</MkInput>
+				<MkTextarea v-model="description">
+					<template #label>{{ i18n.ts.description }}</template>
+				</MkTextarea>
+				<MkFolder>
+					<template #label>{{ i18n.ts.availableRoles }}</template>
+					<template #suffix>{{ rolesThatCanBeUsedThisDecoration.length === 0 ? i18n.ts.all : rolesThatCanBeUsedThisDecoration.length }}</template>
+
+					<div class="_gaps">
+						<MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+
+						<div v-for="role in rolesThatCanBeUsedThisDecoration" :key="role.id" :class="$style.roleItem">
+							<MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/>
+							<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ti ti-x"></i></button>
+							<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
+						</div>
+					</div>
+				</MkFolder>
+				<MkButton v-if="avatarDecoration" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+			</div>
+		</MkSpacer>
+		<div :class="$style.footer">
+			<MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.avatarDecoration ? i18n.ts.update : i18n.ts.create }}</MkButton>
+		</div>
+	</div>
+</MkWindow>
+</template>
+
+<script lang="ts" setup>
+import { computed, watch, ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import MkWindow from '@/components/MkWindow.vue';
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { i18n } from '@/i18n.js';
+import MkSwitch from '@/components/MkSwitch.vue';
+import MkRolePreview from '@/components/MkRolePreview.vue';
+import MkTextarea from '@/components/MkTextarea.vue';
+import { signinRequired } from '@/account.js';
+
+const $i = signinRequired();
+
+const props = defineProps<{
+	avatarDecoration?: any,
+}>();
+
+const emit = defineEmits<{
+	(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
+	(ev: 'closed'): void
+}>();
+
+const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
+const url = ref<string>(props.avatarDecoration ? props.avatarDecoration.url : '');
+const name = ref<string>(props.avatarDecoration ? props.avatarDecoration.name : '');
+const description = ref<string>(props.avatarDecoration ? props.avatarDecoration.description : '');
+const roleIdsThatCanBeUsedThisDecoration = ref(props.avatarDecoration ? props.avatarDecoration.roleIdsThatCanBeUsedThisDecoration : []);
+const rolesThatCanBeUsedThisDecoration = ref<Misskey.entities.Role[]>([]);
+
+watch(roleIdsThatCanBeUsedThisDecoration, async () => {
+	rolesThatCanBeUsedThisDecoration.value = (await Promise.all(roleIdsThatCanBeUsedThisDecoration.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
+}, { immediate: true });
+
+async function addRole() {
+	const roles = await misskeyApi('admin/roles/list');
+	const currentRoleIds = rolesThatCanBeUsedThisDecoration.value.map(x => x.id);
+
+	const { canceled, result: role } = await os.select({
+		items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })),
+	});
+	if (canceled || role == null) return;
+
+	rolesThatCanBeUsedThisDecoration.value.push(role);
+}
+
+async function removeRole(role, ev) {
+	rolesThatCanBeUsedThisDecoration.value = rolesThatCanBeUsedThisDecoration.value.filter(x => x.id !== role.id);
+}
+
+async function done() {
+	const params = {
+		url: url.value,
+		name: name.value,
+		description: description.value,
+		roleIdsThatCanBeUsedThisDecoration: rolesThatCanBeUsedThisDecoration.value.map(x => x.id),
+	};
+
+	if (props.avatarDecoration) {
+		await os.apiWithDialog('admin/avatar-decorations/update', {
+			id: props.avatarDecoration.id,
+			...params,
+		});
+
+		emit('done', {
+			updated: {
+				id: props.avatarDecoration.id,
+				...params,
+			},
+		});
+
+		windowEl.value?.close();
+	} else {
+		const created = await os.apiWithDialog('admin/avatar-decorations/create', params);
+
+		emit('done', {
+			created: created,
+		});
+
+		windowEl.value?.close();
+	}
+}
+
+async function del() {
+	const { canceled } = await os.confirm({
+		type: 'warning',
+		text: i18n.tsx.removeAreYouSure({ x: name.value }),
+	});
+	if (canceled) return;
+
+	misskeyApi('admin/avatar-decorations/delete', {
+		id: props.avatarDecoration.id,
+	}).then(() => {
+		emit('done', {
+			deleted: true,
+		});
+		windowEl.value?.close();
+	});
+}
+</script>
+
+<style lang="scss" module>
+.preview {
+	display: grid;
+	place-items: center;
+	grid-template-columns: 1fr 1fr;
+	grid-template-rows: 1fr;
+	gap: var(--MI-margin);
+}
+
+.previewItem {
+	width: 100%;
+	height: 100%;
+	min-height: 160px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	border-radius: var(--MI-radius);
+
+	&.light {
+		background: #eee;
+	}
+
+	&.dark {
+		background: #222;
+	}
+}
+
+.roleItem {
+	display: flex;
+}
+
+.role {
+	flex: 1;
+}
+
+.roleUnassign {
+	width: 32px;
+	height: 32px;
+	margin-left: 8px;
+	align-self: center;
+}
+
+.footer {
+	position: sticky;
+	z-index: 10000;
+	bottom: 0;
+	left: 0;
+	padding: 12px;
+	border-top: solid 0.5px var(--MI_THEME-divider);
+	background: var(--MI_THEME-acrylicBg);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
+}
+</style>
diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue
index b97e7c0eea..a5cafb1678 100644
--- a/packages/frontend/src/pages/avatar-decorations.vue
+++ b/packages/frontend/src/pages/avatar-decorations.vue
@@ -5,92 +5,38 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkStickyContainer>
-	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
+	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="900">
 		<div class="_gaps">
-			<MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null">
-				<template #label>{{ avatarDecoration.name }}</template>
-				<template #caption>{{ avatarDecoration.description }}</template>
-
-				<div :class="$style.editorRoot">
-					<div :class="$style.editorWrapper">
-						<div :class="$style.preview">
-							<div :class="[$style.previewItem, $style.light]">
-								<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[avatarDecoration]" forceShowDecoration/>
-							</div>
-							<div :class="[$style.previewItem, $style.dark]">
-								<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[avatarDecoration]" forceShowDecoration/>
-							</div>
-						</div>
-						<div class="_gaps_m">
-							<MkInput v-model="avatarDecoration.name">
-								<template #label>{{ i18n.ts.name }}</template>
-							</MkInput>
-							<MkTextarea v-model="avatarDecoration.description">
-								<template #label>{{ i18n.ts.description }}</template>
-							</MkTextarea>
-							<MkInput v-model="avatarDecoration.url">
-								<template #label>{{ i18n.ts.imageUrl }}</template>
-							</MkInput>
-							<div class="_buttons">
-								<MkButton inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
-								<MkButton v-if="avatarDecoration.id != null" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
-							</div>
-						</div>
-					</div>
+			<div :class="$style.decorations">
+				<div
+					v-for="avatarDecoration in avatarDecorations"
+					:key="avatarDecoration.id"
+					v-panel
+					:class="$style.decoration"
+					@click="edit(avatarDecoration)"
+				>
+					<div :class="$style.decorationName"><MkCondensedLine :minScale="0.5">{{ avatarDecoration.name }}</MkCondensedLine></div>
+					<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: avatarDecoration.url }]" forceShowDecoration/>
 				</div>
-			</MkFolder>
+			</div>
 		</div>
 	</MkSpacer>
 </MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
-import { ref, computed } from 'vue';
+import { ref, computed, defineAsyncComponent } from 'vue';
 import * as Misskey from 'misskey-js';
-import MkButton from '@/components/MkButton.vue';
-import MkInput from '@/components/MkInput.vue';
-import MkTextarea from '@/components/MkTextarea.vue';
 import { signinRequired } from '@/account.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import MkFolder from '@/components/MkFolder.vue';
-
-const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]);
 
 const $i = signinRequired();
 
-function add() {
-	avatarDecorations.value.unshift({
-		_id: Math.random().toString(36),
-		id: null,
-		name: '',
-		description: '',
-		url: '',
-	});
-}
-
-function del(avatarDecoration) {
-	os.confirm({
-		type: 'warning',
-		text: i18n.tsx.deleteAreYouSure({ x: avatarDecoration.name }),
-	}).then(({ canceled }) => {
-		if (canceled) return;
-		avatarDecorations.value = avatarDecorations.value.filter(x => x !== avatarDecoration);
-		misskeyApi('admin/avatar-decorations/delete', avatarDecoration);
-	});
-}
-
-async function save(avatarDecoration) {
-	if (avatarDecoration.id == null) {
-		await os.apiWithDialog('admin/avatar-decorations/create', avatarDecoration);
-		load();
-	} else {
-		os.apiWithDialog('admin/avatar-decorations/update', avatarDecoration);
-	}
-}
+const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]);
 
 function load() {
 	misskeyApi('admin/avatar-decorations/list').then(_avatarDecorations => {
@@ -100,6 +46,37 @@ function load() {
 
 load();
 
+async function add(ev: MouseEvent) {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('./avatar-decoration-edit-dialog.vue')), {
+	}, {
+		done: result => {
+			if (result.created) {
+				avatarDecorations.value.unshift(result.created);
+			}
+		},
+		closed: () => dispose(),
+	});
+}
+
+function edit(avatarDecoration) {
+	const { dispose } = os.popup(defineAsyncComponent(() => import('./avatar-decoration-edit-dialog.vue')), {
+		avatarDecoration: avatarDecoration,
+	}, {
+		done: result => {
+			if (result.updated) {
+				const index = avatarDecorations.value.findIndex(x => x.id === avatarDecoration.id);
+				avatarDecorations.value[index] = {
+					...avatarDecorations.value[index],
+					...result.updated,
+				};
+			} else if (result.deleted) {
+				avatarDecorations.value = avatarDecorations.value.filter(x => x.id !== avatarDecoration.id);
+			}
+		},
+		closed: () => dispose(),
+	});
+}
+
 const headerActions = computed(() => [{
 	asFullButton: true,
 	icon: 'ti ti-plus',
@@ -116,53 +93,26 @@ definePageMetadata(() => ({
 </script>
 
 <style lang="scss" module>
-.editorRoot {
-	container: editor / inline-size;
-}
-
-.editorWrapper {
+.decorations {
 	display: grid;
-	grid-template-columns: 1fr;
-	grid-template-rows: auto auto;
-	gap: var(--MI-margin);
+	grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+	grid-gap: 12px;
 }
 
-.preview {
-	display: grid;
-	place-items: center;
-	grid-template-columns: 1fr 1fr;
-	grid-template-rows: 1fr;
-	gap: var(--MI-margin);
+.decoration {
+	cursor: pointer;
+	padding: 16px 16px 28px 16px;
+	border-radius: 8px;
+	text-align: center;
+	font-size: 90%;
+	overflow: clip;
+	contain: content;
 }
 
-.previewItem {
-	width: 100%;
-	height: 100%;
-	min-height: 160px;
-	display: flex;
-	align-items: center;
-	justify-content: center;
-	border-radius: var(--MI-radius);
-
-	&.light {
-		background: #eee;
-	}
-
-	&.dark {
-		background: #222;
-	}
-}
-
-@container editor (min-width: 600px) {
-	.editorWrapper {
-		grid-template-columns: 200px 1fr;
-		grid-template-rows: 1fr;
-		gap: calc(var(--MI-margin) * 2);
-	}
-
-	.preview {
-		grid-template-columns: 1fr;
-		grid-template-rows: 1fr 1fr;
-	}
+.decorationName {
+	position: relative;
+	z-index: 10;
+	font-weight: bold;
+	margin-bottom: 20px;
 }
 </style>
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
index f72a0b9383..3c9914b4e2 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
@@ -10,12 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 >
 	<div :class="$style.name"><MkCondensedLine :minScale="0.5">{{ decoration.name }}</MkCondensedLine></div>
 	<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: decoration.url, angle, flipH, offsetX, offsetY }]" forceShowDecoration/>
-	<i v-if="decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.lock" class="ti ti-lock"></i>
+	<i v-if="locked" :class="$style.lock" class="ti ti-lock"></i>
 </div>
 </template>
 
 <script lang="ts" setup>
-import { } from 'vue';
+import { computed } from 'vue';
 import { signinRequired } from '@/account.js';
 
 const $i = signinRequired();
@@ -37,6 +37,8 @@ const props = defineProps<{
 const emit = defineEmits<{
 	(ev: 'click'): void;
 }>();
+
+const locked = computed(() => props.decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => props.decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id)));
 </script>
 
 <style lang="scss" module>
@@ -67,5 +69,6 @@ const emit = defineEmits<{
 	position: absolute;
 	bottom: 12px;
 	right: 12px;
+	color: var(--MI_THEME-warn);
 }
 </style>
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
index 853e536ea3..aa899ac649 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
@@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.footer" class="_buttonsCenter">
 			<MkButton v-if="usingIndex != null" primary rounded @click="update"><i class="ti ti-check"></i> {{ i18n.ts.update }}</MkButton>
 			<MkButton v-if="usingIndex != null" rounded @click="detach"><i class="ti ti-x"></i> {{ i18n.ts.detach }}</MkButton>
-			<MkButton v-else :disabled="exceeded" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton>
+			<MkButton v-else :disabled="exceeded || locked" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton>
 		</div>
 	</div>
 </MkModalWindow>
@@ -61,6 +61,7 @@ const props = defineProps<{
 		id: string;
 		url: string;
 		name: string;
+		roleIdsThatCanBeUsedThisDecoration: string[];
 	};
 }>();
 
@@ -83,6 +84,7 @@ const emit = defineEmits<{
 
 const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 const exceeded = computed(() => ($i.policies.avatarDecorationLimit - $i.avatarDecorations.length) <= 0);
+const locked = computed(() => props.decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => props.decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id)));
 const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0);
 const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false);
 const offsetX = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetX : null) ?? 0);
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 61de8b8c7e..061b533b72 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -121,6 +121,9 @@ type AdminAnnouncementsUpdateRequest = operations['admin___announcements___updat
 // @public (undocumented)
 type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json'];
 
+// @public (undocumented)
+type AdminAvatarDecorationsCreateResponse = operations['admin___avatar-decorations___create']['responses']['200']['content']['application/json'];
+
 // @public (undocumented)
 type AdminAvatarDecorationsDeleteRequest = operations['admin___avatar-decorations___delete']['requestBody']['content']['application/json'];
 
@@ -1253,6 +1256,7 @@ declare namespace entities {
         AdminAnnouncementsListResponse,
         AdminAnnouncementsUpdateRequest,
         AdminAvatarDecorationsCreateRequest,
+        AdminAvatarDecorationsCreateResponse,
         AdminAvatarDecorationsDeleteRequest,
         AdminAvatarDecorationsListRequest,
         AdminAvatarDecorationsListResponse,
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index d0367d8496..5e6bc0a99c 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -31,6 +31,7 @@ import type {
 	AdminAnnouncementsListResponse,
 	AdminAnnouncementsUpdateRequest,
 	AdminAvatarDecorationsCreateRequest,
+	AdminAvatarDecorationsCreateResponse,
 	AdminAvatarDecorationsDeleteRequest,
 	AdminAvatarDecorationsListRequest,
 	AdminAvatarDecorationsListResponse,
@@ -597,7 +598,7 @@ export type Endpoints = {
 	'admin/announcements/delete': { req: AdminAnnouncementsDeleteRequest; res: EmptyResponse };
 	'admin/announcements/list': { req: AdminAnnouncementsListRequest; res: AdminAnnouncementsListResponse };
 	'admin/announcements/update': { req: AdminAnnouncementsUpdateRequest; res: EmptyResponse };
-	'admin/avatar-decorations/create': { req: AdminAvatarDecorationsCreateRequest; res: EmptyResponse };
+	'admin/avatar-decorations/create': { req: AdminAvatarDecorationsCreateRequest; res: AdminAvatarDecorationsCreateResponse };
 	'admin/avatar-decorations/delete': { req: AdminAvatarDecorationsDeleteRequest; res: EmptyResponse };
 	'admin/avatar-decorations/list': { req: AdminAvatarDecorationsListRequest; res: AdminAvatarDecorationsListResponse };
 	'admin/avatar-decorations/update': { req: AdminAvatarDecorationsUpdateRequest; res: EmptyResponse };
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index ced87c4c7e..f3ddf64481 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -34,6 +34,7 @@ export type AdminAnnouncementsListRequest = operations['admin___announcements___
 export type AdminAnnouncementsListResponse = operations['admin___announcements___list']['responses']['200']['content']['application/json'];
 export type AdminAnnouncementsUpdateRequest = operations['admin___announcements___update']['requestBody']['content']['application/json'];
 export type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json'];
+export type AdminAvatarDecorationsCreateResponse = operations['admin___avatar-decorations___create']['responses']['200']['content']['application/json'];
 export type AdminAvatarDecorationsDeleteRequest = operations['admin___avatar-decorations___delete']['requestBody']['content']['application/json'];
 export type AdminAvatarDecorationsListRequest = operations['admin___avatar-decorations___list']['requestBody']['content']['application/json'];
 export type AdminAvatarDecorationsListResponse = operations['admin___avatar-decorations___list']['responses']['200']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 560960f018..a5333d4f93 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -6324,9 +6324,22 @@ export type operations = {
       };
     };
     responses: {
-      /** @description OK (without any results) */
-      204: {
-        content: never;
+      /** @description OK (with results) */
+      200: {
+        content: {
+          'application/json': {
+            /** Format: id */
+            id: string;
+            /** Format: date-time */
+            createdAt: string;
+            /** Format: date-time */
+            updatedAt: string | null;
+            name: string;
+            description: string;
+            url: string;
+            roleIdsThatCanBeUsedThisDecoration: string[];
+          };
+        };
       };
       /** @description Client error */
       400: {

From 0472d43ee97f1ac0fd13969b2111d67b322a947f Mon Sep 17 00:00:00 2001
From: Pinapelz <donaldshan1@outlook.com>
Date: Mon, 28 Oct 2024 05:04:46 -0700
Subject: [PATCH 05/13] fix: encode RSS uris with escape sequences before
 fetching (#14826)

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
 CHANGELOG.md                                        | 1 +
 packages/frontend/src/ui/_common_/statusbar-rss.vue | 2 +-
 packages/frontend/src/widgets/WidgetRss.vue         | 2 +-
 packages/frontend/src/widgets/WidgetRssTicker.vue   | 2 +-
 4 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 52077f813f..0b2cb43e25 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,7 @@
 - Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正  
   (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
 - Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正
+- Fix: Encode RSS urls with escape sequences before fetching allowing query parameters to be used
 - Fix: リンク切れを修正
 
 ### Server
diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue
index 550fc39b00..da8fa8bb21 100644
--- a/packages/frontend/src/ui/_common_/statusbar-rss.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue
@@ -48,7 +48,7 @@ const fetching = ref(true);
 const key = ref(0);
 
 const tick = () => {
-	window.fetch(`/api/fetch-rss?url=${props.url}`, {}).then(res => {
+	window.fetch(`/api/fetch-rss?url=${encodeURIComponent(props.url)}`, {}).then(res => {
 		res.json().then((feed: Misskey.entities.FetchRssResponse) => {
 			if (props.shuffle) {
 				shuffle(feed.items);
diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue
index 3e43687709..92dc6d148e 100644
--- a/packages/frontend/src/widgets/WidgetRss.vue
+++ b/packages/frontend/src/widgets/WidgetRss.vue
@@ -70,7 +70,7 @@ const items = computed(() => rawItems.value.slice(0, widgetProps.maxEntries));
 const fetching = ref(true);
 const fetchEndpoint = computed(() => {
 	const url = new URL('/api/fetch-rss', base);
-	url.searchParams.set('url', widgetProps.url);
+	url.searchParams.set('url', encodeURIComponent(widgetProps.url));
 	return url;
 });
 const intervalClear = ref<(() => void) | undefined>();
diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue
index 4f594b720f..6957878572 100644
--- a/packages/frontend/src/widgets/WidgetRssTicker.vue
+++ b/packages/frontend/src/widgets/WidgetRssTicker.vue
@@ -99,7 +99,7 @@ const items = computed(() => {
 const fetching = ref(true);
 const fetchEndpoint = computed(() => {
 	const url = new URL('/api/fetch-rss', base);
-	url.searchParams.set('url', widgetProps.url);
+	url.searchParams.set('url', encodeURIComponent(widgetProps.url));
 	return url;
 });
 const intervalClear = ref<(() => void) | undefined>();

From 8eb7749e448d912bdbe2c4eadc35f5d5f1becf61 Mon Sep 17 00:00:00 2001
From: Tamme Schichler <tamme@schichler.dev>
Date: Mon, 28 Oct 2024 13:06:16 +0100
Subject: [PATCH 06/13] fix(backend): Accept arrays in ActivityPub `icon` and
 `image` properties (#14825)

This is allowed according to the Activity vocabulary: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon
The issue is noticeable in combination with Bridgy Fed: https://github.com/snarfed/bridgy-fed/issues/1408
---
 .../backend/src/core/activitypub/models/ApPersonService.ts  | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index 0e2934301b..c9de67b3a0 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -232,6 +232,12 @@ export class ApPersonService implements OnModuleInit {
 		if (user == null) throw new Error('failed to create user: user is null');
 
 		const [avatar, banner] = await Promise.all([icon, image].map(img => {
+			// icon and image may be arrays
+			// see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon
+			if (Array.isArray(img)) {
+				img = img.find(item => item && item.url) ?? null;
+			}
+			
 			// if we have an explicitly missing image, return an
 			// explicitly-null set of values
 			if ((img == null) || (typeof img === 'object' && img.url == null)) {

From f30d19051fb67f800185da283672ae7f9e8c535e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Mon, 28 Oct 2024 21:06:54 +0900
Subject: [PATCH 07/13] =?UTF-8?q?enhance(backend):=20check=5Fconnect.js=20?=
 =?UTF-8?q?=E3=81=A7=E5=85=A8Redis=E3=81=A8DB=E3=81=B8=E3=81=AE=E6=8E=A5?=
 =?UTF-8?q?=E7=B6=9A=E3=82=92=E7=A2=BA=E8=AA=8D=E3=81=99=E3=82=8B=E3=82=88?=
 =?UTF-8?q?=E3=81=86=E3=81=AB=20(#14853)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix race conditions in check_connect.js

(cherry picked from commit 524ddb96770690455b82522104a543c5b0b1f3b3)

* fix

* Update Changelog

---------

Co-authored-by: Hazelnoot <acomputerdog@gmail.com>
---
 CHANGELOG.md                              |  3 ++
 packages/backend/scripts/check_connect.js | 51 ++++++++++++++++++++---
 2 files changed, 49 insertions(+), 5 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b2cb43e25..23be962d9e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,9 @@
 - Fix: リンク切れを修正
 
 ### Server
+- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように  
+  (Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588)  
+  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715)
 - Fix: Nested proxy requestsを検出した際にブロックするように
   [ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236)
 - Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正  
diff --git a/packages/backend/scripts/check_connect.js b/packages/backend/scripts/check_connect.js
index ba25fd416c..bb149444b5 100644
--- a/packages/backend/scripts/check_connect.js
+++ b/packages/backend/scripts/check_connect.js
@@ -5,11 +5,52 @@
 
 import Redis from 'ioredis';
 import { loadConfig } from '../built/config.js';
+import { createPostgresDataSource } from '../built/postgres.js';
 
 const config = loadConfig();
-const redis = new Redis(config.redis);
 
-redis.on('connect', () => redis.disconnect());
-redis.on('error', (e) => {
-	throw e;
-});
+async function connectToPostgres() {
+	const source = createPostgresDataSource(config);
+	await source.initialize();
+	await source.destroy();
+}
+
+async function connectToRedis(redisOptions) {
+	return await new Promise(async (resolve, reject) => {
+		const redis = new Redis({
+			...redisOptions,
+			lazyConnect: true,
+			reconnectOnError: false,
+			showFriendlyErrorStack: true,
+		});
+		redis.on('error', e => reject(e));
+
+		try {
+			await redis.connect();
+			resolve();
+
+		} catch (e) {
+			reject(e);
+
+		} finally {
+			redis.disconnect(false);
+		}
+	});
+}
+
+// If not all of these are defined, the default one gets reused.
+// so we use a Set to only try connecting once to each **uniq** redis.
+const promises = Array
+	.from(new Set([
+		config.redis,
+		config.redisForPubsub,
+		config.redisForJobQueue,
+		config.redisForTimelines,
+		config.redisForReactions,
+	]))
+	.map(connectToRedis)
+	.concat([
+		connectToPostgres()
+	]);
+
+await Promise.allSettled(promises);

From a96f09cee352f8ae7cc11c3dd45e0182a5623350 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Mon, 28 Oct 2024 12:23:59 +0000
Subject: [PATCH 08/13] Bump version to 2024.10.2-alpha.2

---
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 6c598e11a3..55ae092967 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2024.10.2-alpha.1",
+	"version": "2024.10.2-alpha.2",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index ef3d84ee96..32d6c8b0cb 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.10.2-alpha.1",
+	"version": "2024.10.2-alpha.2",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From 7fc8a2a7b04d8550abdf55259bde4c857bd462a1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Wed, 30 Oct 2024 09:57:54 +0900
Subject: [PATCH 09/13] =?UTF-8?q?fix(frontend):=20=E4=B8=80=E9=83=A8?=
 =?UTF-8?q?=E3=81=AE=E3=83=8E=E3=83=BC=E3=83=88=E8=A1=A8=E7=A4=BA=E3=81=A7?=
 =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E3=81=AB=E3=81=8B=E3=81=8B=E3=82=8F=E3=82=89?=
 =?UTF-8?q?=E3=81=9A=E3=82=BB=E3=83=B3=E3=82=B7=E3=83=86=E3=82=A3=E3=83=96?=
 =?UTF-8?q?=E3=81=AA=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92=E5=90=AB?=
 =?UTF-8?q?=E3=82=80=E3=83=8E=E3=83=BC=E3=83=88=E3=81=8C=E6=9C=80=E5=B0=8F?=
 =?UTF-8?q?=E5=8C=96=E3=81=95=E3=82=8C=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92?=
 =?UTF-8?q?=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Fix https://github.com/misskey-dev/misskey/pull/14772#discussion_r1821707117
---
 packages/frontend/src/components/MkNote.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index be1339ecc4..3de69d6d09 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -227,7 +227,7 @@ const emit = defineEmits<{
 }>();
 
 const inTimeline = inject<boolean>('inTimeline', false);
-const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(false));
+const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
 const inChannel = inject('inChannel', null);
 const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
 

From 17d9aca5a7ec6149a8dbf0c1607c81ab188e7015 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Thu, 31 Oct 2024 13:46:42 +0900
Subject: [PATCH 10/13] =?UTF-8?q?refactor(frontend):=20as=E3=81=A8any?=
 =?UTF-8?q?=E3=82=92=E3=81=99=E3=81=90=E3=81=AA=E3=81=8A=E3=81=9B=E3=82=8B?=
 =?UTF-8?q?=E7=AF=84=E5=9B=B2=E3=81=A7=E9=99=A4=E5=8E=BB=20(#14848)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* refactor(frontend): できるだけanyを除去

* refactor

* lint

* fix

* remove unused

* Update packages/frontend/src/components/MkReactionsViewer.details.vue

* Update packages/frontend/src/components/MkUsersTooltip.vue

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
 .../src/components/MkAntennaEditor.vue        |  2 +-
 .../src/components/MkChannelPreview.vue       |  3 +-
 packages/frontend/src/components/MkDialog.vue |  4 +--
 packages/frontend/src/components/MkDrive.vue  |  8 ++---
 .../src/components/MkEmojiPicker.section.vue  |  2 +-
 .../frontend/src/components/MkEmojiPicker.vue |  4 +--
 .../src/components/MkExtensionInstaller.vue   |  2 +-
 packages/frontend/src/components/MkInput.vue  |  8 ++---
 .../components/MkNotificationSelectWindow.vue |  2 +-
 .../src/components/MkObjectView.value.vue     | 10 +++---
 .../frontend/src/components/MkPageWindow.vue  |  2 +-
 .../frontend/src/components/MkPopupMenu.vue   |  2 +-
 .../frontend/src/components/MkPostForm.vue    | 20 +++--------
 .../src/components/MkPostFormAttaches.vue     |  8 ++---
 .../src/components/MkPostFormDialog.vue       | 16 ++-------
 packages/frontend/src/components/MkRadio.vue  |  8 ++---
 .../components/MkReactionsViewer.details.vue  |  3 +-
 .../frontend/src/components/MkSuperMenu.vue   | 33 +++++++++++++++++--
 .../frontend/src/components/MkUrlPreview.vue  | 10 +++---
 .../MkUserAnnouncementEditDialog.vue          |  8 +++--
 .../src/components/MkUsersTooltip.vue         |  4 +--
 packages/frontend/src/components/MkWindow.vue | 11 +++++--
 .../frontend/src/components/form/suspense.vue |  6 ++--
 .../frontend/src/components/global/MkMfm.ts   |  4 +--
 packages/frontend/src/nirax.ts                | 12 ++++---
 packages/frontend/src/os.ts                   | 23 ++++++-------
 packages/frontend/src/pages/admin/users.vue   |  6 ++--
 .../src/pages/custom-emojis-manager.vue       | 26 +++++++--------
 .../frontend/src/pages/emoji-edit-dialog.vue  | 19 ++++++-----
 .../frontend/src/pages/follow-requests.vue    |  4 +--
 packages/frontend/src/pages/lookup.vue        |  2 +-
 .../frontend/src/pages/my-clips/index.vue     |  6 ++--
 packages/frontend/src/pages/my-lists/list.vue |  4 +--
 .../page-editor/els/page-editor.el.image.vue  |  5 +--
 .../page-editor/els/page-editor.el.note.vue   | 13 +++++---
 .../els/page-editor.el.section.vue            | 12 +++----
 .../page-editor/els/page-editor.el.text.vue   |  5 +--
 packages/frontend/src/pages/registry.keys.vue |  2 +-
 .../src/pages/reversi/game.setting.vue        |  6 ++--
 packages/frontend/src/pages/scratchpad.vue    |  6 +++-
 .../src/pages/settings/2fa.qrdialog.vue       |  7 ++--
 .../frontend/src/pages/settings/accounts.vue  |  2 +-
 packages/frontend/src/pages/settings/apps.vue |  3 +-
 .../settings/avatar-decoration.dialog.vue     |  8 ++---
 packages/frontend/src/pages/user/home.vue     |  2 +-
 packages/frontend/src/router/definition.ts    |  2 +-
 packages/frontend/src/router/main.ts          |  8 ++---
 .../frontend/src/scripts/check-word-mute.ts   |  3 +-
 packages/frontend/src/scripts/form.ts         | 16 ++++-----
 packages/frontend/src/scripts/misskey-api.ts  |  6 ++--
 packages/frontend/src/scripts/select-file.ts  |  6 ++--
 packages/frontend/src/scripts/shuffle.ts      |  5 +--
 packages/frontend/src/scripts/upload.ts       |  8 ++---
 packages/frontend/src/store.ts                |  9 ++---
 packages/frontend/src/types/post-form.ts      | 22 +++++++++++++
 .../frontend/src/widgets/WidgetPhotos.vue     |  4 +--
 56 files changed, 250 insertions(+), 192 deletions(-)
 create mode 100644 packages/frontend/src/types/post-form.ts

diff --git a/packages/frontend/src/components/MkAntennaEditor.vue b/packages/frontend/src/components/MkAntennaEditor.vue
index 2386ba6fa7..e622d57f1e 100644
--- a/packages/frontend/src/components/MkAntennaEditor.vue
+++ b/packages/frontend/src/components/MkAntennaEditor.vue
@@ -160,7 +160,7 @@ async function deleteAntenna() {
 function addUser() {
 	os.selectUser({ includeSelf: true }).then(user => {
 		users.value = users.value.trim();
-		users.value += '\n@' + Misskey.acct.toString(user as any);
+		users.value += '\n@' + Misskey.acct.toString(user);
 		users.value = users.value.trim();
 	});
 }
diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue
index 99580df5e2..c470042b79 100644
--- a/packages/frontend/src/components/MkChannelPreview.vue
+++ b/packages/frontend/src/components/MkChannelPreview.vue
@@ -47,11 +47,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, ref, watch } from 'vue';
+import * as Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
 import { miLocalStorage } from '@/local-storage.js';
 
 const props = defineProps<{
-	channel: Record<string, any>;
+	channel: Misskey.entities.Channel;
 }>();
 
 const getLastReadedAt = (): number | null => {
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index 22130d4fab..b095a1cd4a 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</template>
 		</MkSelect>
 		<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
-			<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
+			<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason != null" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
 			<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
 		</div>
 		<div v-if="actions" :class="$style.buttons">
@@ -98,7 +98,7 @@ const props = withDefaults(defineProps<{
 		text: string;
 		primary?: boolean,
 		danger?: boolean,
-		callback: (...args: any[]) => void;
+		callback: (...args: unknown[]) => void;
 	}[];
 	showOkButton?: boolean;
 	showCancelButton?: boolean;
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index 910b73c798..8be6d6f53d 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -157,7 +157,7 @@ const ilFilesObserver = new IntersectionObserver(
 	(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(),
 );
 
-const sortModeSelect = ref('+createdAt');
+const sortModeSelect = ref<NonNullable<Misskey.entities.DriveFilesRequest['sort']>>('+createdAt');
 
 watch(folder, () => emit('cd', folder.value));
 watch(sortModeSelect, () => {
@@ -198,7 +198,7 @@ function onStreamDriveFolderDeleted(folderId: string) {
 	removeFolder(folderId);
 }
 
-function onDragover(ev: DragEvent): any {
+function onDragover(ev: DragEvent) {
 	if (!ev.dataTransfer) return;
 
 	// ドラッグ元が自分自身の所有するアイテムだったら
@@ -243,7 +243,7 @@ function onDragleave() {
 	draghover.value = false;
 }
 
-function onDrop(ev: DragEvent): any {
+function onDrop(ev: DragEvent) {
 	draghover.value = false;
 
 	if (!ev.dataTransfer) return;
@@ -332,7 +332,7 @@ function createFolder() {
 		title: i18n.ts.createFolder,
 		placeholder: i18n.ts.folderName,
 	}).then(({ canceled, result: name }) => {
-		if (canceled) return;
+		if (canceled || name == null) return;
 		misskeyApi('drive/folders/create', {
 			name: name,
 			parentId: folder.value ? folder.value.id : undefined,
diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue
index f4caa730bf..b418ed3ae6 100644
--- a/packages/frontend/src/components/MkEmojiPicker.section.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.section.vue
@@ -90,7 +90,7 @@ function computeButtonTitle(ev: MouseEvent): void {
 	elm.title = getEmojiName(emoji);
 }
 
-function nestedChosen(emoji: any, ev: MouseEvent) {
+function nestedChosen(emoji: string, ev: MouseEvent) {
 	emit('chosen', emoji, ev);
 }
 </script>
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 219950f135..8187d991e7 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -409,7 +409,7 @@ function computeButtonTitle(ev: MouseEvent): void {
 	elm.title = getEmojiName(emoji);
 }
 
-function chosen(emoji: any, ev?: MouseEvent) {
+function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) {
 	const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
 	if (el) {
 		const rect = el.getBoundingClientRect();
@@ -426,7 +426,7 @@ function chosen(emoji: any, ev?: MouseEvent) {
 	// 最近使った絵文字更新
 	if (!pinned.value?.includes(key)) {
 		let recents = defaultStore.state.recentlyUsedEmojis;
-		recents = recents.filter((emoji: any) => emoji !== key);
+		recents = recents.filter((emoji) => emoji !== key);
 		recents.unshift(key);
 		defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
 	}
diff --git a/packages/frontend/src/components/MkExtensionInstaller.vue b/packages/frontend/src/components/MkExtensionInstaller.vue
index b41604b2c3..d59b20435e 100644
--- a/packages/frontend/src/components/MkExtensionInstaller.vue
+++ b/packages/frontend/src/components/MkExtensionInstaller.vue
@@ -73,7 +73,7 @@ export type Extension = {
 		author: string;
 		description?: string;
 		permissions?: string[];
-		config?: Record<string, any>;
+		config?: Record<string, unknown>;
 	};
 } | {
 	type: 'theme';
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index e01ff86c5a..08817fd6a8 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
+import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs, InputHTMLAttributes } from 'vue';
 import { debounce } from 'throttle-debounce';
 import MkButton from '@/components/MkButton.vue';
 import { useInterval } from '@@/js/use-interval.js';
@@ -53,7 +53,7 @@ import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js';
 
 const props = defineProps<{
 	modelValue: string | number | null;
-	type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
+	type?: InputHTMLAttributes['type'];
 	required?: boolean;
 	readonly?: boolean;
 	disabled?: boolean;
@@ -64,8 +64,8 @@ const props = defineProps<{
 	mfmAutocomplete?: boolean | SuggestionType[],
 	autocapitalize?: string;
 	spellcheck?: boolean;
-	inputmode?: 'none' | 'text' | 'search' | 'email' | 'url' | 'numeric' | 'tel' | 'decimal';
-	step?: any;
+	inputmode?: InputHTMLAttributes['inputmode'];
+	step?: InputHTMLAttributes['step'];
 	datalist?: string[];
 	min?: number;
 	max?: number;
diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue
index 47a9c79e45..d07827d11a 100644
--- a/packages/frontend/src/components/MkNotificationSelectWindow.vue
+++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue
@@ -53,7 +53,7 @@ const props = withDefaults(defineProps<{
 
 const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 
-const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as any);
+const typesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as TypesMap);
 
 function ok() {
 	emit('done', {
diff --git a/packages/frontend/src/components/MkObjectView.value.vue b/packages/frontend/src/components/MkObjectView.value.vue
index dabdd324fd..7fa8c23c6c 100644
--- a/packages/frontend/src/components/MkObjectView.value.vue
+++ b/packages/frontend/src/components/MkObjectView.value.vue
@@ -39,7 +39,7 @@ import number from '@/filters/number.js';
 import XValue from '@/components/MkObjectView.value.vue';
 
 const props = defineProps<{
-	value: any;
+	value: unknown;
 }>();
 
 const collapsed = reactive({});
@@ -50,19 +50,19 @@ if (isObject(props.value)) {
 	}
 }
 
-function isObject(v): boolean {
+function isObject(v: unknown): v is Record<PropertyKey, unknown> {
 	return typeof v === 'object' && !Array.isArray(v) && v !== null;
 }
 
-function isArray(v): boolean {
+function isArray(v: unknown): v is unknown[] {
 	return Array.isArray(v);
 }
 
-function isEmpty(v): boolean {
+function isEmpty(v: unknown): v is Record<PropertyKey, never> | never[] {
 	return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
 }
 
-function collapsable(v): boolean {
+function collapsable(v: unknown): boolean {
 	return (isObject(v) || isArray(v)) && !isEmpty(v);
 }
 </script>
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 4777da2848..02c84df447 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -58,7 +58,7 @@ const windowRouter = routerFactory(props.initialPath);
 const contents = shallowRef<HTMLElement | null>(null);
 const pageMetadata = ref<null | PageMetadata>(null);
 const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
-const history = ref<{ path: string; key: any; }[]>([{
+const history = ref<{ path: string; key: string; }[]>([{
 	path: windowRouter.getCurrentPath(),
 	key: windowRouter.getCurrentKey(),
 }]);
diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue
index 26c251a8d2..df664e49f7 100644
--- a/packages/frontend/src/components/MkPopupMenu.vue
+++ b/packages/frontend/src/components/MkPopupMenu.vue
@@ -19,7 +19,7 @@ defineProps<{
 	items: MenuItem[];
 	align?: 'center' | string;
 	width?: number;
-	src?: any;
+	src?: HTMLElement | null;
 	returnFocusTo?: HTMLElement | null;
 }>();
 
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index b6b80082d3..f2fe048449 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -129,25 +129,13 @@ import { miLocalStorage } from '@/local-storage.js';
 import { claimAchievement } from '@/scripts/achievements.js';
 import { emojiPicker } from '@/scripts/emoji-picker.js';
 import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
+import type { PostFormProps } from '@/types/post-form.js';
 
 const $i = signinRequired();
 
 const modal = inject('modal');
 
-const props = withDefaults(defineProps<{
-	reply?: Misskey.entities.Note;
-	renote?: Misskey.entities.Note;
-	channel?: Misskey.entities.Channel; // TODO
-	mention?: Misskey.entities.User;
-	specified?: Misskey.entities.UserDetailed;
-	initialText?: string;
-	initialCw?: string;
-	initialVisibility?: (typeof Misskey.noteVisibilities)[number];
-	initialFiles?: Misskey.entities.DriveFile[];
-	initialLocalOnly?: boolean;
-	initialVisibleUsers?: Misskey.entities.UserDetailed[];
-	initialNote?: Misskey.entities.Note;
-	instant?: boolean;
+const props = withDefaults(defineProps<PostFormProps & {
 	fixed?: boolean;
 	autofocus?: boolean;
 	freezeAfterPosted?: boolean;
@@ -955,8 +943,8 @@ function showActions(ev: MouseEvent) {
 			action.handler({
 				text: text.value,
 				cw: cw.value,
-			}, (key, value: any) => {
-				if (typeof key !== 'string') return;
+			}, (key, value) => {
+				if (typeof key !== 'string' || typeof value !== 'string') return;
 				if (key === 'text') { text.value = value; }
 				if (key === 'cw') { useCw.value = value !== null; cw.value = value; }
 			});
diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index ee7038df64..56e026aa3c 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div v-show="props.modelValue.length != 0" :class="$style.root">
 	<Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)">
-		<template #item="{element}">
+		<template #item="{ element }">
 			<div
 				:class="$style.file"
 				role="button"
@@ -38,14 +38,14 @@ import type { MenuItem } from '@/types/menu.js';
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 
 const props = defineProps<{
-	modelValue: any[];
+	modelValue: Misskey.entities.DriveFile[];
 	detachMediaFn?: (id: string) => void;
 }>();
 
 const mock = inject<boolean>('mock', false);
 
 const emit = defineEmits<{
-	(ev: 'update:modelValue', value: any[]): void;
+	(ev: 'update:modelValue', value: Misskey.entities.DriveFile[]): void;
 	(ev: 'detach', id: string): void;
 	(ev: 'changeSensitive', file: Misskey.entities.DriveFile, isSensitive: boolean): void;
 	(ev: 'changeName', file: Misskey.entities.DriveFile, newName: string): void;
@@ -113,7 +113,7 @@ async function rename(file) {
 	});
 }
 
-async function describe(file) {
+async function describe(file: Misskey.entities.DriveFile) {
 	if (mock) return;
 
 	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue
index d6bca29050..32d8df1504 100644
--- a/packages/frontend/src/components/MkPostFormDialog.vue
+++ b/packages/frontend/src/components/MkPostFormDialog.vue
@@ -11,23 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { shallowRef } from 'vue';
-import * as Misskey from 'misskey-js';
 import MkModal from '@/components/MkModal.vue';
 import MkPostForm from '@/components/MkPostForm.vue';
+import type { PostFormProps } from '@/types/post-form.js';
 
-const props = withDefaults(defineProps<{
-	reply?: Misskey.entities.Note;
-	renote?: Misskey.entities.Note;
-	channel?: any; // TODO
-	mention?: Misskey.entities.User;
-	specified?: Misskey.entities.UserDetailed;
-	initialText?: string;
-	initialCw?: string;
-	initialVisibility?: (typeof Misskey.noteVisibilities)[number];
-	initialFiles?: Misskey.entities.DriveFile[];
-	initialLocalOnly?: boolean;
-	initialVisibleUsers?: Misskey.entities.UserDetailed[];
-	initialNote?: Misskey.entities.Note;
+const props = withDefaults(defineProps<PostFormProps & {
 	instant?: boolean;
 	fixed?: boolean;
 	autofocus?: boolean;
diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue
index e735d9fff8..f16c8f6c2a 100644
--- a/packages/frontend/src/components/MkRadio.vue
+++ b/packages/frontend/src/components/MkRadio.vue
@@ -24,17 +24,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 </div>
 </template>
 
-<script lang="ts" setup>
+<script lang="ts" setup generic="T extends unknown">
 import { computed } from 'vue';
 
 const props = defineProps<{
-	modelValue: any;
-	value: any;
+	modelValue: T;
+	value: T;
 	disabled?: boolean;
 }>();
 
 const emit = defineEmits<{
-	(ev: 'update:modelValue', value: any): void;
+	(ev: 'update:modelValue', value: T): void;
 }>();
 
 const checked = computed(() => props.modelValue === props.value);
diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue
index f4c3643ba8..d24e0b15bf 100644
--- a/packages/frontend/src/components/MkReactionsViewer.details.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.details.vue
@@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { } from 'vue';
+import * as Misskey from 'misskey-js';
 import { getEmojiName } from '@@/js/emojilist.js';
 import MkTooltip from './MkTooltip.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
@@ -30,7 +31,7 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue';
 defineProps<{
 	showing: boolean;
 	reaction: string;
-	users: any[]; // TODO
+	users: Misskey.entities.UserLite[];
 	count: number;
 	targetElement: HTMLElement;
 }>();
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index 6e7a875dec..0caaed6f39 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -28,11 +28,38 @@ SPDX-License-Identifier: AGPL-3.0-only
 </div>
 </template>
 
-<script lang="ts" setup>
-import { } from 'vue';
+<script lang="ts">
+export type SuperMenuDef = {
+	title?: string;
+	items: ({
+		type: 'a';
+		href: string;
+		target?: string;
+		icon?: string;
+		text: string;
+		danger?: boolean;
+		active?: boolean;
+	} | {
+		type: 'button';
+		icon?: string;
+		text: string;
+		danger?: boolean;
+		active?: boolean;
+		action: (ev: MouseEvent) => void;
+	} | {
+		type: 'link';
+		to: string;
+		icon?: string;
+		text: string;
+		danger?: boolean;
+		active?: boolean;
+	})[];
+};
+</script>
 
+<script lang="ts" setup>
 defineProps<{
-	def: any[];
+	def: SuperMenuDef[];
 	grid?: boolean;
 }>();
 </script>
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index c287effadc..f0da8fd3f2 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -180,7 +180,7 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa
 		sensitive.value = info.sensitive ?? false;
 	});
 
-function adjustTweetHeight(message: any) {
+function adjustTweetHeight(message: MessageEvent) {
 	if (message.origin !== 'https://platform.twitter.com') return;
 	const embed = message.data?.['twttr.embed'];
 	if (embed?.method !== 'twttr.private.resize') return;
@@ -193,14 +193,16 @@ function openPlayer(): void {
 	const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), {
 		url: requestUrl.href,
 	}, {
-		// TODO
+		closed: () => {
+			dispose();
+		},
 	});
 }
 
-(window as any).addEventListener('message', adjustTweetHeight);
+window.addEventListener('message', adjustTweetHeight);
 
 onUnmounted(() => {
-	(window as any).removeEventListener('message', adjustTweetHeight);
+	window.removeEventListener('message', adjustTweetHeight);
 });
 </script>
 
diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
index 7a2b5f5ddc..7f0266d1d3 100644
--- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
+++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
@@ -62,9 +62,11 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkRadios from '@/components/MkRadios.vue';
 
+type AdminAnnouncementType = Misskey.entities.AdminAnnouncementsCreateRequest & { id: string; }
+
 const props = defineProps<{
 	user: Misskey.entities.User,
-	announcement?: Misskey.entities.Announcement,
+	announcement?: Required<AdminAnnouncementType>,
 }>();
 
 const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null);
@@ -75,7 +77,7 @@ const display = ref(props.announcement ? props.announcement.display : 'dialog');
 const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false);
 
 const emit = defineEmits<{
-	(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
+	(ev: 'done', v: { deleted?: boolean; updated?: AdminAnnouncementType; created?: AdminAnnouncementType; }): void,
 	(ev: 'closed'): void
 }>();
 
@@ -88,7 +90,7 @@ async function done() {
 		display: display.value,
 		needConfirmationToRead: needConfirmationToRead.value,
 		userId: props.user.id,
-	};
+	} satisfies Misskey.entities.AdminAnnouncementsCreateRequest;
 
 	if (props.announcement) {
 		await os.apiWithDialog('admin/announcements/update', {
diff --git a/packages/frontend/src/components/MkUsersTooltip.vue b/packages/frontend/src/components/MkUsersTooltip.vue
index 054a503257..0cb7f22e93 100644
--- a/packages/frontend/src/components/MkUsersTooltip.vue
+++ b/packages/frontend/src/components/MkUsersTooltip.vue
@@ -16,12 +16,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { } from 'vue';
+import * as Misskey from 'misskey-js';
 import MkTooltip from './MkTooltip.vue';
 
 defineProps<{
 	showing: boolean;
-	users: any[]; // TODO
+	users: Misskey.entities.UserLite[];
 	count: number;
 	targetElement: HTMLElement;
 }>();
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index 056b6a37ed..ed7e3867ce 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -60,6 +60,13 @@ import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { defaultStore } from '@/store.js';
 
+type WindowButton = {
+	title: string;
+	icon: string;
+	onClick: () => void;
+	highlighted?: boolean;
+};
+
 const minHeight = 50;
 const minWidth = 250;
 
@@ -87,8 +94,8 @@ const props = withDefaults(defineProps<{
 	mini?: boolean;
 	front?: boolean;
 	contextmenu?: MenuItem[] | null;
-	buttonsLeft?: any[];
-	buttonsRight?: any[];
+	buttonsLeft?: WindowButton[];
+	buttonsRight?: WindowButton[];
 }>(), {
 	initialWidth: 400,
 	initialHeight: null,
diff --git a/packages/frontend/src/components/form/suspense.vue b/packages/frontend/src/components/form/suspense.vue
index 5226c61d68..821f07510b 100644
--- a/packages/frontend/src/components/form/suspense.vue
+++ b/packages/frontend/src/components/form/suspense.vue
@@ -18,19 +18,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 </div>
 </template>
 
-<script lang="ts" setup>
+<script lang="ts" setup generic="T extends unknown">
 import { ref, watch } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
-	p: () => Promise<any>;
+	p: () => Promise<T>;
 }>();
 
 const pending = ref(true);
 const resolved = ref(false);
 const rejected = ref(false);
-const result = ref<any>(null);
+const result = ref<T | null>(null);
 
 const process = () => {
 	if (props.p == null) {
diff --git a/packages/frontend/src/components/global/MkMfm.ts b/packages/frontend/src/components/global/MkMfm.ts
index 0d4ae8cacb..0d138d1f1c 100644
--- a/packages/frontend/src/components/global/MkMfm.ts
+++ b/packages/frontend/src/components/global/MkMfm.ts
@@ -467,8 +467,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 			}
 
 			default: {
-				// eslint-disable-next-line @typescript-eslint/no-explicit-any
-				console.error('unrecognized ast type:', (token as any).type);
+				// @ts-expect-error 存在しないASTタイプ
+				console.error('unrecognized ast type:', token.type);
 
 				return [];
 			}
diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts
index 25f853453a..965bd6f0bc 100644
--- a/packages/frontend/src/nirax.ts
+++ b/packages/frontend/src/nirax.ts
@@ -36,6 +36,8 @@ interface RouteDefWithRedirect extends RouteDefBase {
 
 export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect;
 
+export type RouterFlag = 'forcePage';
+
 type ParsedPath = (string | {
 	name: string;
 	startsWith?: string;
@@ -107,7 +109,7 @@ export interface IRouter extends EventEmitter<RouterEvent> {
 	current: Resolved;
 	currentRef: ShallowRef<Resolved>;
 	currentRoute: ShallowRef<RouteDef>;
-	navHook: ((path: string, flag?: any) => boolean) | null;
+	navHook: ((path: string, flag?: RouterFlag) => boolean) | null;
 
 	/**
 	 * ルートの初期化(eventListenerの定義後に必ず呼び出すこと)
@@ -116,11 +118,11 @@ export interface IRouter extends EventEmitter<RouterEvent> {
 
 	resolve(path: string): Resolved | null;
 
-	getCurrentPath(): any;
+	getCurrentPath(): string;
 
 	getCurrentKey(): string;
 
-	push(path: string, flag?: any): void;
+	push(path: string, flag?: RouterFlag): void;
 
 	replace(path: string, key?: string | null): void;
 
@@ -197,7 +199,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 	private currentKey = Date.now().toString();
 	private redirectCount = 0;
 
-	public navHook: ((path: string, flag?: any) => boolean) | null = null;
+	public navHook: ((path: string, flag?: RouterFlag) => boolean) | null = null;
 
 	constructor(routes: Router['routes'], currentPath: Router['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) {
 		super();
@@ -404,7 +406,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 		return this.currentKey;
 	}
 
-	public push(path: string, flag?: any) {
+	public push(path: string, flag?: RouterFlag) {
 		const beforePath = this.currentPath;
 		if (path === beforePath) {
 			this.emit('same');
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 07d91a0644..ea1b673de9 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -28,12 +28,13 @@ import { pleaseLogin } from '@/scripts/please-login.js';
 import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
 import { focusParent } from '@/scripts/focus.js';
+import type { PostFormProps } from '@/types/post-form.js';
 
 export const openingWindowsCount = ref(0);
 
-export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
+export const apiWithDialog = (<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
 	endpoint: E,
-	data: P = {} as any,
+	data: P,
 	token?: string | null | undefined,
 	customErrors?: Record<string, { title?: string; text: string; }>,
 ) => {
@@ -94,7 +95,7 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
 
 export function promiseDialog<T extends Promise<any>>(
 	promise: T,
-	onSuccess?: ((res: any) => void) | null,
+	onSuccess?: ((res: Awaited<T>) => void) | null,
 	onFailure?: ((err: Misskey.api.APIError) => void) | null,
 	text?: string,
 ): T {
@@ -136,12 +137,12 @@ export function promiseDialog<T extends Promise<any>>(
 }
 
 let popupIdCount = 0;
-export const popups = ref([]) as Ref<{
+export const popups = ref<{
 	id: number;
 	component: Component;
 	props: Record<string, any>;
 	events: Record<string, any>;
-}[]>;
+}[]>([]);
 
 const zIndexes = {
 	veryLow: 500000,
@@ -458,7 +459,7 @@ type SelectItem<C> = {
 };
 
 // default が指定されていたら result は null になり得ないことを保証する overload function
-export function select<C = any>(props: {
+export function select<C = unknown>(props: {
 	title?: string;
 	text?: string;
 	default: string;
@@ -471,7 +472,7 @@ export function select<C = any>(props: {
 } | {
 	canceled: false; result: C;
 }>;
-export function select<C = any>(props: {
+export function select<C = unknown>(props: {
 	title?: string;
 	text?: string;
 	default?: string | null;
@@ -484,7 +485,7 @@ export function select<C = any>(props: {
 } | {
 	canceled: false; result: C | null;
 }>;
-export function select<C = any>(props: {
+export function select<C = unknown>(props: {
 	title?: string;
 	text?: string;
 	default?: string | null;
@@ -687,13 +688,13 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
 	}));
 }
 
-export function post(props: Record<string, any> = {}): Promise<void> {
+export function post(props: PostFormProps = {}): Promise<void> {
 	pleaseLogin({
 		openOnRemote: (props.initialText || props.initialNote ? {
 			type: 'share',
 			params: {
-				text: props.initialText ?? props.initialNote.text,
-				visibility: props.initialVisibility ?? props.initialNote?.visibility,
+				text: props.initialText ?? props.initialNote?.text ?? '',
+				visibility: props.initialVisibility ?? props.initialNote?.visibility ?? 'public',
 				localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
 			},
 		} : undefined),
diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue
index d1bbb5b734..870c3ce88b 100644
--- a/packages/frontend/src/pages/admin/users.vue
+++ b/packages/frontend/src/pages/admin/users.vue
@@ -99,19 +99,19 @@ async function addUser() {
 	const { canceled: canceled1, result: username } = await os.inputText({
 		title: i18n.ts.username,
 	});
-	if (canceled1) return;
+	if (canceled1 || username == null) return;
 
 	const { canceled: canceled2, result: password } = await os.inputText({
 		title: i18n.ts.password,
 		type: 'password',
 	});
-	if (canceled2) return;
+	if (canceled2 || password == null) return;
 
 	os.apiWithDialog('admin/accounts/create', {
 		username: username,
 		password: password,
 	}).then(res => {
-		paginationComponent.value.reload();
+		paginationComponent.value?.reload();
 	});
 }
 
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 1e416e22d3..cae3f3ede9 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -116,7 +116,7 @@ const selectAll = () => {
 	if (selectedEmojis.value.length > 0) {
 		selectedEmojis.value = [];
 	} else {
-		selectedEmojis.value = Array.from(emojisPaginationComponent.value.items.values(), item => item.id);
+		selectedEmojis.value = Array.from(emojisPaginationComponent.value?.items.values(), item => item.id);
 	}
 };
 
@@ -133,7 +133,7 @@ const add = async (ev: MouseEvent) => {
 	}, {
 		done: result => {
 			if (result.created) {
-				emojisPaginationComponent.value.prepend(result.created);
+				emojisPaginationComponent.value?.prepend(result.created);
 			}
 		},
 		closed: () => dispose(),
@@ -146,12 +146,12 @@ const edit = (emoji) => {
 	}, {
 		done: result => {
 			if (result.updated) {
-				emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
+				emojisPaginationComponent.value?.updateItem(result.updated.id, (oldEmoji) => ({
 					...oldEmoji,
 					...result.updated,
 				}));
 			} else if (result.deleted) {
-				emojisPaginationComponent.value.removeItem(emoji.id);
+				emojisPaginationComponent.value?.removeItem(emoji.id);
 			}
 		},
 		closed: () => dispose(),
@@ -226,7 +226,7 @@ const setCategoryBulk = async () => {
 		ids: selectedEmojis.value,
 		category: result,
 	});
-	emojisPaginationComponent.value.reload();
+	emojisPaginationComponent.value?.reload();
 };
 
 const setLicenseBulk = async () => {
@@ -238,43 +238,43 @@ const setLicenseBulk = async () => {
 		ids: selectedEmojis.value,
 		license: result,
 	});
-	emojisPaginationComponent.value.reload();
+	emojisPaginationComponent.value?.reload();
 };
 
 const addTagBulk = async () => {
 	const { canceled, result } = await os.inputText({
 		title: 'Tag',
 	});
-	if (canceled) return;
+	if (canceled || result == null) return;
 	await os.apiWithDialog('admin/emoji/add-aliases-bulk', {
 		ids: selectedEmojis.value,
 		aliases: result.split(' '),
 	});
-	emojisPaginationComponent.value.reload();
+	emojisPaginationComponent.value?.reload();
 };
 
 const removeTagBulk = async () => {
 	const { canceled, result } = await os.inputText({
 		title: 'Tag',
 	});
-	if (canceled) return;
+	if (canceled || result == null) return;
 	await os.apiWithDialog('admin/emoji/remove-aliases-bulk', {
 		ids: selectedEmojis.value,
 		aliases: result.split(' '),
 	});
-	emojisPaginationComponent.value.reload();
+	emojisPaginationComponent.value?.reload();
 };
 
 const setTagBulk = async () => {
 	const { canceled, result } = await os.inputText({
 		title: 'Tag',
 	});
-	if (canceled) return;
+	if (canceled || result == null) return;
 	await os.apiWithDialog('admin/emoji/set-aliases-bulk', {
 		ids: selectedEmojis.value,
 		aliases: result.split(' '),
 	});
-	emojisPaginationComponent.value.reload();
+	emojisPaginationComponent.value?.reload();
 };
 
 const delBulk = async () => {
@@ -286,7 +286,7 @@ const delBulk = async () => {
 	await os.apiWithDialog('admin/emoji/delete-bulk', {
 		ids: selectedEmojis.value,
 	});
-	emojisPaginationComponent.value.reload();
+	emojisPaginationComponent.value?.reload();
 };
 
 const headerActions = computed(() => [{
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index 3b3f41d9b1..2caba03675 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -95,14 +95,14 @@ import { selectFile } from '@/scripts/select-file.js';
 import MkRolePreview from '@/components/MkRolePreview.vue';
 
 const props = defineProps<{
-	emoji?: any,
+	emoji?: Misskey.entities.EmojiDetailed,
 }>();
 
 const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
 const name = ref<string>(props.emoji ? props.emoji.name : '');
-const category = ref<string>(props.emoji ? props.emoji.category : '');
+const category = ref<string>(props.emoji?.category ? props.emoji.category : '');
 const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : '');
-const license = ref<string>(props.emoji ? (props.emoji.license ?? '') : '');
+const license = ref<string>(props.emoji?.license ? props.emoji.license : '');
 const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false);
 const localOnly = ref(props.emoji ? props.emoji.localOnly : false);
 const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
@@ -116,11 +116,11 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
 const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
 
 const emit = defineEmits<{
-	(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
+	(ev: 'done', v: { deleted?: boolean; updated?: Misskey.entities.AdminEmojiUpdateRequest; created?: Misskey.entities.AdminEmojiUpdateRequest }): void,
 	(ev: 'closed'): void
 }>();
 
-async function changeImage(ev) {
+async function changeImage(ev: Event) {
 	file.value = await selectFile(ev.currentTarget ?? ev.target, null);
 	const candidate = file.value.name.replace(/\.(.+)$/, '');
 	if (candidate.match(/^[a-z0-9_]+$/)) {
@@ -140,7 +140,7 @@ async function addRole() {
 	rolesThatCanBeUsedThisEmojiAsReaction.value.push(role);
 }
 
-async function removeRole(role, ev) {
+async function removeRole(role: Misskey.entities.RoleLite, ev: Event) {
 	rolesThatCanBeUsedThisEmojiAsReaction.value = rolesThatCanBeUsedThisEmojiAsReaction.value.filter(x => x.id !== role.id);
 }
 
@@ -172,7 +172,7 @@ async function done() {
 			},
 		});
 
-		windowEl.value.close();
+		windowEl.value?.close();
 	} else {
 		const created = await os.apiWithDialog('admin/emoji/add', params);
 
@@ -180,11 +180,12 @@ async function done() {
 			created: created,
 		});
 
-		windowEl.value.close();
+		windowEl.value?.close();
 	}
 }
 
 async function del() {
+	if (!props.emoji) return;
 	const { canceled } = await os.confirm({
 		type: 'warning',
 		text: i18n.tsx.removeAreYouSure({ x: name.value }),
@@ -197,7 +198,7 @@ async function del() {
 		emit('done', {
 			deleted: true,
 		});
-		windowEl.value.close();
+		windowEl.value?.close();
 	});
 }
 </script>
diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue
index 8991af8086..a840d0d0b3 100644
--- a/packages/frontend/src/pages/follow-requests.vue
+++ b/packages/frontend/src/pages/follow-requests.vue
@@ -55,13 +55,13 @@ const pagination = {
 
 function accept(user) {
 	misskeyApi('following/requests/accept', { userId: user.id }).then(() => {
-		paginationComponent.value.reload();
+		paginationComponent.value?.reload();
 	});
 }
 
 function reject(user) {
 	misskeyApi('following/requests/reject', { userId: user.id }).then(() => {
-		paginationComponent.value.reload();
+		paginationComponent.value?.reload();
 	});
 }
 
diff --git a/packages/frontend/src/pages/lookup.vue b/packages/frontend/src/pages/lookup.vue
index 3233953942..6f10c69640 100644
--- a/packages/frontend/src/pages/lookup.vue
+++ b/packages/frontend/src/pages/lookup.vue
@@ -40,7 +40,7 @@ function fetch() {
 		return;
 	}
 
-	let promise: Promise<any>;
+	let promise: Promise<unknown>;
 
 	if (uri.startsWith('https://')) {
 		promise = misskeyApi('ap/show', {
diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue
index ece998a7a5..acf37a9a2f 100644
--- a/packages/frontend/src/pages/my-clips/index.vue
+++ b/packages/frontend/src/pages/my-clips/index.vue
@@ -77,15 +77,15 @@ async function create() {
 
 	clipsCache.delete();
 
-	pagingComponent.value.reload();
+	pagingComponent.value?.reload();
 }
 
 function onClipCreated() {
-	pagingComponent.value.reload();
+	pagingComponent.value?.reload();
 }
 
 function onClipDeleted() {
-	pagingComponent.value.reload();
+	pagingComponent.value?.reload();
 }
 
 const headerActions = computed(() => []);
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index 804a5ae8f8..69e404bd85 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -110,7 +110,7 @@ function addUser() {
 			listId: list.value.id,
 			userId: user.id,
 		}).then(() => {
-			paginationEl.value.reload();
+			paginationEl.value?.reload();
 		});
 	});
 }
@@ -126,7 +126,7 @@ async function removeUser(item, ev) {
 				listId: list.value.id,
 				userId: item.userId,
 			}).then(() => {
-				paginationEl.value.removeItem(item.id);
+				paginationEl.value?.removeItem(item.id);
 			});
 		},
 	}], ev.currentTarget ?? ev.target);
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
index 1cfe7a6d2d..247b8f61a1 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
@@ -30,11 +30,12 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
-	modelValue: any
+	modelValue: Misskey.entities.PageBlock & { type: 'image' };
 }>();
 
 const emit = defineEmits<{
-	(ev: 'update:modelValue', value: any): void;
+	(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'image' }): void;
+	(ev: 'remove'): void;
 }>();
 
 const file = ref<Misskey.entities.DriveFile | null>(null);
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
index 0a28386986..52b4f2aaa3 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
@@ -34,19 +34,24 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
-	modelValue: any
+	modelValue: Misskey.entities.PageBlock & { type: 'note' };
 }>();
 
 const emit = defineEmits<{
-	(ev: 'update:modelValue', value: any): void;
+	(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'note' }): void;
 }>();
 
-const id = ref<any>(props.modelValue.note);
+const id = ref(props.modelValue.note);
 const note = ref<Misskey.entities.Note | null>(null);
 
 watch(id, async () => {
 	if (id.value && (id.value.startsWith('http://') || id.value.startsWith('https://'))) {
-		id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop();
+		id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop() ?? null;
+	}
+
+	if (!id.value) {
+		note.value = null;
+		return;
 	}
 
 	emit('update:modelValue', {
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
index 0f8dc33143..eea52255c7 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
@@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 /* eslint-disable vue/no-mutating-props */
 import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
+import * as Misskey from 'misskey-js';
 import { v4 as uuid } from 'uuid';
 import XContainer from '../page-editor.container.vue';
 import * as os from '@/os.js';
@@ -33,14 +34,13 @@ import { getPageBlockList } from '@/pages/page-editor/common.js';
 
 const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue'));
 
-const props = withDefaults(defineProps<{
-	modelValue: any,
-}>(), {
-	modelValue: {},
-});
+const props = defineProps<{
+	modelValue: Misskey.entities.PageBlock & { type: 'section'; },
+}>();
 
 const emit = defineEmits<{
-	(ev: 'update:modelValue', value: any): void;
+	(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'section' }): void;
+	(ev: 'remove'): void;
 }>();
 
 const children = ref(deepClone(props.modelValue.children ?? []));
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
index f09f7e1acd..43a1e37f9e 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
@@ -17,16 +17,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 /* eslint-disable vue/no-mutating-props */
 import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue';
+import * as Misskey from 'misskey-js';
 import XContainer from '../page-editor.container.vue';
 import { i18n } from '@/i18n.js';
 import { Autocomplete } from '@/scripts/autocomplete.js';
 
 const props = defineProps<{
-	modelValue: any
+	modelValue: Misskey.entities.PageBlock & { type: 'text' }
 }>();
 
 const emit = defineEmits<{
-	(ev: 'update:modelValue', value: any): void;
+	(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'text' }): void;
 }>();
 
 let autocomplete: Autocomplete;
diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue
index bac1d2bb70..4cacbd0906 100644
--- a/packages/frontend/src/pages/registry.keys.vue
+++ b/packages/frontend/src/pages/registry.keys.vue
@@ -52,7 +52,7 @@ const props = defineProps<{
 
 const scope = computed(() => props.path ? props.path.split('/') : []);
 
-const keys = ref<any>(null);
+const keys = ref<[string, string][]>([]);
 
 function fetchKeys() {
 	misskeyApi('i/registry/keys-with-type', {
diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue
index dfb6e3f53e..437a1a2294 100644
--- a/packages/frontend/src/pages/reversi/game.setting.vue
+++ b/packages/frontend/src/pages/reversi/game.setting.vue
@@ -132,7 +132,7 @@ const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.
 
 const props = defineProps<{
 	game: Misskey.entities.ReversiGameDetailed;
-	connection: Misskey.ChannelConnection;
+	connection: Misskey.ChannelConnection<Misskey.Channels['reversiGame']>;
 }>();
 
 const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false });
@@ -217,14 +217,14 @@ function onChangeReadyStates(states) {
 	game.value.user2Ready = states.user2;
 }
 
-function updateSettings(key: keyof Misskey.entities.ReversiGameDetailed) {
+function updateSettings(key: typeof Misskey.reversiUpdateKeys[number]) {
 	props.connection.send('updateSettings', {
 		key: key,
 		value: game.value[key],
 	});
 }
 
-function onUpdateSettings({ userId, key, value }: { userId: string; key: keyof Misskey.entities.ReversiGameDetailed; value: any; }) {
+function onUpdateSettings<K extends typeof Misskey.reversiUpdateKeys[number]>({ userId, key, value }: { userId: string; key: K; value: Misskey.entities.ReversiGameDetailed[K]; }) {
 	if (userId === $i.id) return;
 	if (game.value[key] === value) return;
 	game.value[key] = value;
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index 2250e1ce60..88171f7d70 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -76,7 +76,11 @@ import { claimAchievement } from '@/scripts/achievements.js';
 const parser = new Parser();
 let aiscript: Interpreter;
 const code = ref('');
-const logs = ref<any[]>([]);
+const logs = ref<{
+	id: number;
+	text: string;
+	print: boolean;
+}[]>([]);
 const root = ref<AsUiRoot>();
 const components = ref<Ref<AsUiComponent>[]>([]);
 const uiKey = ref(0);
diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
index 2244047b31..18c82ffdf6 100644
--- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue
+++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
@@ -138,12 +138,13 @@ const token = ref<string | number | null>(null);
 const backupCodes = ref<string[]>();
 
 function cancel() {
-	dialog.value.close();
+	dialog.value?.close();
 }
 
 async function tokenDone() {
+	if (token.value == null) return;
 	const res = await os.apiWithDialog('i/2fa/done', {
-		token: token.value.toString(),
+		token: typeof token.value === 'string' ? token.value : token.value.toString(),
 	});
 
 	backupCodes.value = res.backupCodes;
@@ -166,7 +167,7 @@ function downloadBackupCodes() {
 }
 
 function allDone() {
-	dialog.value.close();
+	dialog.value?.close();
 }
 </script>
 
diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue
index 16f0716a12..97e960675f 100644
--- a/packages/frontend/src/pages/settings/accounts.vue
+++ b/packages/frontend/src/pages/settings/accounts.vue
@@ -90,7 +90,7 @@ function createAccount() {
 	});
 }
 
-async function switchAccount(account: any) {
+async function switchAccount(account: Misskey.entities.UserDetailed) {
 	const fetchedAccounts = await getAccounts();
 	const token = fetchedAccounts.find(x => x.id === account.id)!.token;
 	switchAccountWithToken(token);
diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue
index 68e36ef1bb..6515503505 100644
--- a/packages/frontend/src/pages/settings/apps.vue
+++ b/packages/frontend/src/pages/settings/apps.vue
@@ -55,6 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref, computed } from 'vue';
+import * as Misskey from 'misskey-js';
 import FormPagination from '@/components/MkPagination.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
@@ -77,7 +78,7 @@ const pagination = {
 
 function revoke(token) {
 	misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => {
-		list.value.reload();
+		list.value?.reload();
 	});
 }
 
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
index aa899ac649..40542ad5b2 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
@@ -110,7 +110,7 @@ const decorationsForPreview = computed(() => {
 });
 
 function cancel() {
-	dialog.value.close();
+	dialog.value?.close();
 }
 
 async function update() {
@@ -120,7 +120,7 @@ async function update() {
 		offsetX: offsetX.value,
 		offsetY: offsetY.value,
 	});
-	dialog.value.close();
+	dialog.value?.close();
 }
 
 async function attach() {
@@ -130,12 +130,12 @@ async function attach() {
 		offsetX: offsetX.value,
 		offsetY: offsetY.value,
 	});
-	dialog.value.close();
+	dialog.value?.close();
 }
 
 async function detach() {
 	emit('detach');
-	dialog.value.close();
+	dialog.value?.close();
 }
 </script>
 
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 00b5740639..2794db2821 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -257,7 +257,7 @@ function parallaxLoop() {
 }
 
 function parallax() {
-	const banner = bannerEl.value as any;
+	const banner = bannerEl.value;
 	if (banner == null) return;
 
 	const top = getScrollPosition(rootEl.value);
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index b5fd6b6ec3..e98e0b59b1 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -10,7 +10,7 @@ import { $i, iAmModerator } from '@/account.js';
 import MkLoading from '@/pages/_loading_.vue';
 import MkError from '@/pages/_error_.vue';
 
-export const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
+export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({
 	loader: loader,
 	loadingComponent: MkLoading,
 	errorComponent: MkError,
diff --git a/packages/frontend/src/router/main.ts b/packages/frontend/src/router/main.ts
index 6ee967e6f4..3c25e80d12 100644
--- a/packages/frontend/src/router/main.ts
+++ b/packages/frontend/src/router/main.ts
@@ -4,7 +4,7 @@
  */
 
 import { EventEmitter } from 'eventemitter3';
-import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js';
+import { IRouter, Resolved, RouteDef, RouterEvent, RouterFlag } from '@/nirax.js';
 
 import type { App, ShallowRef } from 'vue';
 
@@ -79,7 +79,7 @@ class MainRouterProxy implements IRouter {
 		return this.supplier().currentRoute;
 	}
 
-	get navHook(): ((path: string, flag?: any) => boolean) | null {
+	get navHook(): ((path: string, flag?: RouterFlag) => boolean) | null {
 		return this.supplier().navHook;
 	}
 
@@ -91,11 +91,11 @@ class MainRouterProxy implements IRouter {
 		return this.supplier().getCurrentKey();
 	}
 
-	getCurrentPath(): any {
+	getCurrentPath(): string {
 		return this.supplier().getCurrentPath();
 	}
 
-	push(path: string, flag?: any): void {
+	push(path: string, flag?: RouterFlag): void {
 		this.supplier().push(path, flag);
 	}
 
diff --git a/packages/frontend/src/scripts/check-word-mute.ts b/packages/frontend/src/scripts/check-word-mute.ts
index 67e896b4b9..0a37a08bf0 100644
--- a/packages/frontend/src/scripts/check-word-mute.ts
+++ b/packages/frontend/src/scripts/check-word-mute.ts
@@ -2,8 +2,9 @@
  * SPDX-FileCopyrightText: syuilo and misskey-project
  * SPDX-License-Identifier: AGPL-3.0-only
  */
+import * as Misskey from 'misskey-js';
 
-export function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: Array<string | string[]>): boolean {
+export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): boolean {
 	// 自分自身
 	if (me && (note.userId === me.id)) return false;
 
diff --git a/packages/frontend/src/scripts/form.ts b/packages/frontend/src/scripts/form.ts
index 242a504c3b..1032e97ac9 100644
--- a/packages/frontend/src/scripts/form.ts
+++ b/packages/frontend/src/scripts/form.ts
@@ -15,7 +15,7 @@ type Hidden = boolean | ((v: any) => boolean);
 export type FormItem = {
 	label?: string;
 	type: 'string';
-	default: string | null;
+	default?: string | null;
 	description?: string;
 	required?: boolean;
 	hidden?: Hidden;
@@ -24,7 +24,7 @@ export type FormItem = {
 } | {
 	label?: string;
 	type: 'number';
-	default: number | null;
+	default?: number | null;
 	description?: string;
 	required?: boolean;
 	hidden?: Hidden;
@@ -32,20 +32,20 @@ export type FormItem = {
 } | {
 	label?: string;
 	type: 'boolean';
-	default: boolean | null;
+	default?: boolean | null;
 	description?: string;
 	hidden?: Hidden;
 } | {
 	label?: string;
 	type: 'enum';
-	default: string | null;
+	default?: string | null;
 	required?: boolean;
 	hidden?: Hidden;
 	enum: EnumItem[];
 } | {
 	label?: string;
 	type: 'radio';
-	default: unknown | null;
+	default?: unknown | null;
 	required?: boolean;
 	hidden?: Hidden;
 	options: {
@@ -55,7 +55,7 @@ export type FormItem = {
 } | {
 	label?: string;
 	type: 'range';
-	default: number | null;
+	default?: number | null;
 	description?: string;
 	required?: boolean;
 	step?: number;
@@ -66,12 +66,12 @@ export type FormItem = {
 } | {
 	label?: string;
 	type: 'object';
-	default: Record<string, unknown> | null;
+	default?: Record<string, unknown> | null;
 	hidden: Hidden;
 } | {
 	label?: string;
 	type: 'array';
-	default: unknown[] | null;
+	default?: unknown[] | null;
 	hidden: Hidden;
 } | {
 	type: 'button';
diff --git a/packages/frontend/src/scripts/misskey-api.ts b/packages/frontend/src/scripts/misskey-api.ts
index 1b1159fd01..e7a92e2d5c 100644
--- a/packages/frontend/src/scripts/misskey-api.ts
+++ b/packages/frontend/src/scripts/misskey-api.ts
@@ -17,7 +17,7 @@ export function misskeyApi<
 	_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
 >(
 	endpoint: E,
-	data: P = {} as any,
+	data: P & { i?: string | null; } = {} as any,
 	token?: string | null | undefined,
 	signal?: AbortSignal,
 ): Promise<_ResT> {
@@ -30,8 +30,8 @@ export function misskeyApi<
 
 	const promise = new Promise<_ResT>((resolve, reject) => {
 		// Append a credential
-		if ($i) (data as any).i = $i.token;
-		if (token !== undefined) (data as any).i = token;
+		if ($i) data.i = $i.token;
+		if (token !== undefined) data.i = token;
 
 		// Send request
 		window.fetch(`${apiUrl}/${endpoint}`, {
diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts
index 9aa38178b2..b037aa8acc 100644
--- a/packages/frontend/src/scripts/select-file.ts
+++ b/packages/frontend/src/scripts/select-file.ts
@@ -80,7 +80,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
 	});
 }
 
-function select(src: any, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
+function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
 	return new Promise((res, rej) => {
 		const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
 
@@ -107,10 +107,10 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Miss
 	});
 }
 
-export function selectFile(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile> {
+export function selectFile(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> {
 	return select(src, label, false).then(files => files[0]);
 }
 
-export function selectFiles(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
+export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
 	return select(src, label, true);
 }
diff --git a/packages/frontend/src/scripts/shuffle.ts b/packages/frontend/src/scripts/shuffle.ts
index fed16bc71c..1f6ef1928c 100644
--- a/packages/frontend/src/scripts/shuffle.ts
+++ b/packages/frontend/src/scripts/shuffle.ts
@@ -6,8 +6,9 @@
 /**
  * 配列をシャッフル (破壊的)
  */
-export function shuffle<T extends any[]>(array: T): T {
-	let currentIndex = array.length, randomIndex;
+export function shuffle<T extends unknown[]>(array: T): T {
+	let currentIndex = array.length;
+	let randomIndex: number;
 
 	// While there remain elements to shuffle.
 	while (currentIndex !== 0) {
diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts
index 22dce609c6..713573a377 100644
--- a/packages/frontend/src/scripts/upload.ts
+++ b/packages/frontend/src/scripts/upload.ts
@@ -32,13 +32,13 @@ const mimeTypeMap = {
 
 export function uploadFile(
 	file: File,
-	folder?: any,
+	folder?: string | Misskey.entities.DriveFolder,
 	name?: string,
 	keepOriginal: boolean = defaultStore.state.keepOriginalUploading,
 ): Promise<Misskey.entities.DriveFile> {
 	if ($i == null) throw new Error('Not logged in');
 
-	if (folder && typeof folder === 'object') folder = folder.id;
+	const _folder = typeof folder === 'string' ? folder : folder?.id;
 
 	if (file.size > instance.maxFileSize) {
 		alert({
@@ -89,11 +89,11 @@ export function uploadFile(
 			}
 
 			const formData = new FormData();
-			formData.append('i', $i.token);
+			formData.append('i', $i!.token);
 			formData.append('force', 'true');
 			formData.append('file', resizedImage ?? file);
 			formData.append('name', ctx.name);
-			if (folder) formData.append('folderId', folder);
+			if (_folder) formData.append('folderId', _folder);
 
 			const xhr = new XMLHttpRequest();
 			xhr.open('POST', apiUrl + '/drive/files/create', true);
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index aab67e0b5c..911a463636 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -11,6 +11,7 @@ import darkTheme from '@@/themes/d-green-lime.json5';
 import { miLocalStorage } from './local-storage.js';
 import type { SoundType } from '@/scripts/sound.js';
 import { Storage } from '@/pizzax.js';
+import type { Ast } from '@syuilo/aiscript';
 
 interface PostFormAction {
 	title: string,
@@ -516,7 +517,7 @@ export type Plugin = {
 	token: string;
 	src: string | null;
 	version: string;
-	ast: any[];
+	ast: Ast.Node[];
 	author?: string;
 	description?: string;
 	permissions?: string[];
@@ -554,13 +555,13 @@ export class ColdDeviceStorage {
 	}
 
 	public static getAll(): Partial<typeof this.default> {
-		return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce((acc, key) => {
+		return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce<Partial<typeof this.default>>((acc, key) => {
 			const value = localStorage.getItem(PREFIX + key);
 			if (value != null) {
 				acc[key] = JSON.parse(value);
 			}
 			return acc;
-		}, {} as any);
+		}, {});
 	}
 
 	public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void {
@@ -605,7 +606,7 @@ export class ColdDeviceStorage {
 			get: () => {
 				return valueRef.value;
 			},
-			set: (value: unknown) => {
+			set: (value: typeof ColdDeviceStorage.default[K]) => {
 				const val = value;
 				ColdDeviceStorage.set(key, val);
 			},
diff --git a/packages/frontend/src/types/post-form.ts b/packages/frontend/src/types/post-form.ts
new file mode 100644
index 0000000000..5bb04a95a0
--- /dev/null
+++ b/packages/frontend/src/types/post-form.ts
@@ -0,0 +1,22 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+
+export interface PostFormProps {
+	reply?: Misskey.entities.Note;
+	renote?: Misskey.entities.Note;
+	channel?: Misskey.entities.Channel; // TODO
+	mention?: Misskey.entities.User;
+	specified?: Misskey.entities.UserDetailed;
+	initialText?: string;
+	initialCw?: string;
+	initialVisibility?: (typeof Misskey.noteVisibilities)[number];
+	initialFiles?: Misskey.entities.DriveFile[];
+	initialLocalOnly?: boolean;
+	initialVisibleUsers?: Misskey.entities.UserDetailed[];
+	initialNote?: Misskey.entities.Note;
+	instant?: boolean;
+};
diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue
index 34be8c5e57..40e2d8fbc7 100644
--- a/packages/frontend/src/widgets/WidgetPhotos.vue
+++ b/packages/frontend/src/widgets/WidgetPhotos.vue
@@ -68,10 +68,10 @@ const onDriveFileCreated = (file) => {
 	}
 };
 
-const thumbnail = (image: any): string => {
+const thumbnail = (image: Misskey.entities.DriveFile): string => {
 	return defaultStore.state.disableShowingAnimatedImages
 		? getStaticImageUrl(image.url)
-		: image.thumbnailUrl;
+		: image.thumbnailUrl ?? image.url;
 };
 
 misskeyApi('drive/stream', {

From ceb60d61b05e32fa340269122378ea93efb20517 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 31 Oct 2024 13:47:30 +0900
Subject: [PATCH 11/13] refactor

---
 packages/frontend/src/pages/emoji-edit-dialog.vue | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index 2caba03675..db3f436873 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:initialHeight="500"
 	:canResize="true"
 	@close="windowEl.close()"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 >
 	<template v-if="emoji" #header>:{{ emoji.name }}:</template>
 	<template v-else #header>New emoji</template>
@@ -98,6 +98,11 @@ const props = defineProps<{
 	emoji?: Misskey.entities.EmojiDetailed,
 }>();
 
+const emit = defineEmits<{
+	(ev: 'done', v: { deleted?: boolean; updated?: Misskey.entities.AdminEmojiUpdateRequest; created?: Misskey.entities.AdminEmojiUpdateRequest }): void,
+	(ev: 'closed'): void
+}>();
+
 const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
 const name = ref<string>(props.emoji ? props.emoji.name : '');
 const category = ref<string>(props.emoji?.category ? props.emoji.category : '');
@@ -115,11 +120,6 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
 
 const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
 
-const emit = defineEmits<{
-	(ev: 'done', v: { deleted?: boolean; updated?: Misskey.entities.AdminEmojiUpdateRequest; created?: Misskey.entities.AdminEmojiUpdateRequest }): void,
-	(ev: 'closed'): void
-}>();
-
 async function changeImage(ev: Event) {
 	file.value = await selectFile(ev.currentTarget ?? ev.target, null);
 	const candidate = file.value.name.replace(/\.(.+)$/, '');

From 724dea8136164dcfcd7238888ef93ccab8270fb9 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 31 Oct 2024 13:47:47 +0900
Subject: [PATCH 12/13] lint

---
 packages/frontend/src/pages/emoji-edit-dialog.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index db3f436873..3765319b25 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:initialWidth="400"
 	:initialHeight="500"
 	:canResize="true"
-	@close="windowEl.close()"
+	@close="windowEl?.close()"
 	@closed="emit('closed')"
 >
 	<template v-if="emoji" #header>:{{ emoji.name }}:</template>

From 224bbd486f8745cd471b77f38570b65be3b87cfc Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 31 Oct 2024 13:50:50 +0900
Subject: [PATCH 13/13] refactor

---
 packages/frontend/src/components/MkCropperDialog.vue |  2 +-
 .../src/components/MkCustomEmojiDetailedDialog.vue   |  2 +-
 .../frontend/src/components/MkEmbedCodeGenDialog.vue |  2 +-
 packages/frontend/src/components/MkFormDialog.vue    |  2 +-
 packages/frontend/src/components/MkPageWindow.vue    |  8 ++++----
 packages/frontend/src/components/MkSignupDialog.vue  |  2 +-
 .../src/components/MkTokenGenerateWindow.vue         |  2 +-
 .../src/components/MkUserAnnouncementEditDialog.vue  | 12 ++++++------
 .../frontend/src/components/MkUserSelectDialog.vue   |  2 +-
 packages/frontend/src/components/MkWidgets.vue       |  2 +-
 packages/frontend/src/components/MkWindow.vue        |  2 +-
 .../pages/page-editor/els/page-editor.el.image.vue   |  2 +-
 .../pages/page-editor/els/page-editor.el.note.vue    |  2 +-
 .../pages/page-editor/els/page-editor.el.section.vue |  4 ++--
 .../pages/page-editor/els/page-editor.el.text.vue    |  4 ++--
 .../src/pages/page-editor/page-editor.blocks.vue     |  2 +-
 16 files changed, 26 insertions(+), 26 deletions(-)

diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index c2a1aaf29a..0186cfc2c0 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:withOkButton="true"
 	@close="cancel()"
 	@ok="ok()"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 >
 	<template #header>{{ i18n.ts.cropImage }}</template>
 	<template #default="{ width, height }">
diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
index 949adc6a8e..ecbee864dc 100644
--- a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
+++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkModalWindow ref="dialogEl" @close="cancel()" @closed="$emit('closed')">
+<MkModalWindow ref="dialogEl" @close="cancel()" @closed="emit('closed')">
 	<template #header>:{{ emoji.name }}:</template>
 	<template #default>
 		<MkSpacer>
diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
index c2bb516c7c..6e9eb75920 100644
--- a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
+++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:scroll="false"
 	:withOkButton="false"
 	@close="cancel()"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 >
 	<template #header><i class="ti ti-code"></i> {{ i18n.ts._embedCodeGen.title }}</template>
 
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index 124f114111..a639eae208 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	@click="cancel()"
 	@ok="ok()"
 	@close="cancel()"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 >
 	<template #header>
 		{{ title }}
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 02c84df447..9547423227 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:buttonsLeft="buttonsLeft"
 	:buttonsRight="buttonsRight"
 	:contextmenu="contextmenu"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 >
 	<template #header>
 		<template v-if="pageMetadata">
@@ -30,17 +30,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
+import { url } from '@@/js/config.js';
+import { getScrollContainer } from '@@/js/scroll.js';
 import RouterView from '@/components/global/RouterView.vue';
 import MkWindow from '@/components/MkWindow.vue';
 import { popout as _popout } from '@/scripts/popout.js';
 import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { url } from '@@/js/config.js';
 import { useScrollPositionManager } from '@/nirax.js';
 import { i18n } from '@/i18n.js';
 import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
 import { openingWindowsCount } from '@/os.js';
 import { claimAchievement } from '@/scripts/achievements.js';
-import { getScrollContainer } from '@@/js/scroll.js';
 import { useRouterFactory } from '@/router/supplier.js';
 import { mainRouter } from '@/router/main.js';
 
@@ -48,7 +48,7 @@ const props = defineProps<{
 	initialPath: string;
 }>();
 
-defineEmits<{
+const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue
index 4f75a36fbe..6fb9d77837 100644
--- a/packages/frontend/src/components/MkSignupDialog.vue
+++ b/packages/frontend/src/components/MkSignupDialog.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:width="500"
 	:height="600"
 	@close="onClose"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 >
 	<template #header>{{ i18n.ts.signup }}</template>
 
diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue
index a7bc3f37f1..73aef68964 100644
--- a/packages/frontend/src/components/MkTokenGenerateWindow.vue
+++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:okButtonDisabled="false"
 	:canClose="false"
 	@close="dialog?.close()"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 	@ok="ok()"
 >
 	<template #header>{{ title || i18n.ts.generateAccessToken }}</template>
diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
index 7f0266d1d3..fe499fabbf 100644
--- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
+++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	ref="dialog"
 	:width="400"
 	@close="dialog?.close()"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 >
 	<template v-if="announcement" #header>:{{ announcement.title }}:</template>
 	<template v-else #header>New announcement</template>
@@ -69,6 +69,11 @@ const props = defineProps<{
 	announcement?: Required<AdminAnnouncementType>,
 }>();
 
+const emit = defineEmits<{
+	(ev: 'done', v: { deleted?: boolean; updated?: AdminAnnouncementType; created?: AdminAnnouncementType; }): void,
+	(ev: 'closed'): void
+}>();
+
 const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null);
 const title = ref(props.announcement ? props.announcement.title : '');
 const text = ref(props.announcement ? props.announcement.text : '');
@@ -76,11 +81,6 @@ const icon = ref(props.announcement ? props.announcement.icon : 'info');
 const display = ref(props.announcement ? props.announcement.display : 'dialog');
 const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false);
 
-const emit = defineEmits<{
-	(ev: 'done', v: { deleted?: boolean; updated?: AdminAnnouncementType; created?: AdminAnnouncementType; }): void,
-	(ev: 'closed'): void
-}>();
-
 async function done() {
 	const params = {
 		title: title.value,
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index 8e58a6c5a2..764bf74f21 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	@click="cancel()"
 	@close="cancel()"
 	@ok="ok()"
-	@closed="$emit('closed')"
+	@closed="emit('closed')"
 >
 	<template #header>{{ i18n.ts.selectUser }}</template>
 	<div>
diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue
index 492dd4cdc0..ba619f6063 100644
--- a/packages/frontend/src/components/MkWidgets.vue
+++ b/packages/frontend/src/components/MkWidgets.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
 			</MkSelect>
 			<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
-			<MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton>
+			<MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
 		</header>
 		<Sortable
 			:modelValue="props.widgets"
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index ed7e3867ce..2953f656d4 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:enterFromClass="defaultStore.state.animation ? $style.transition_window_enterFrom : ''"
 	:leaveToClass="defaultStore.state.animation ? $style.transition_window_leaveTo : ''"
 	appear
-	@afterLeave="$emit('closed')"
+	@afterLeave="emit('closed')"
 >
 	<div v-if="showing" ref="rootEl" :class="[$style.root, { [$style.maximized]: maximized }]">
 		<div :class="$style.body" class="_shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
index 247b8f61a1..c3ad6657b0 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
+<XContainer :draggable="true" @remove="() => emit('remove')">
 	<template #header><i class="ti ti-photo"></i> {{ i18n.ts._pages.blocks.image }}</template>
 	<template #func>
 		<button @click="choose()">
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
index 52b4f2aaa3..36e03b4790 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
+<XContainer :draggable="true" @remove="() => emit('remove')">
 	<template #header><i class="ti ti-note"></i> {{ i18n.ts._pages.blocks.note }}</template>
 
 	<section style="padding: 16px;" class="_gaps_s">
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
index eea52255c7..3fed07f7e8 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
+<XContainer :draggable="true" @remove="() => emit('remove')">
 	<template #header><i class="ti ti-note"></i> {{ props.modelValue.title }}</template>
 	<template #func>
 		<button class="_button" @click="rename()">
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-/* eslint-disable vue/no-mutating-props */
+ 
 import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { v4 as uuid } from 'uuid';
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
index 43a1e37f9e..5795b46c00 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
+<XContainer :draggable="true" @remove="() => emit('remove')">
 	<template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template>
 
 	<section>
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-/* eslint-disable vue/no-mutating-props */
+ 
 import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import XContainer from '../page-editor.container.vue';
diff --git a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
index 4967e73000..f191320180 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => $emit('update:modelValue', v)">
+<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => emit('update:modelValue', v)">
 	<template #item="{element}">
 		<div :class="$style.item">
 			<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->