From 2b42e8f171e0a030af52ae9b00129282a13479c1 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 11:18:49 +0900
Subject: [PATCH 01/28] test

---
 .../src/components/MkNotifications.vue        | 30 +++++++++----------
 1 file changed, 14 insertions(+), 16 deletions(-)

diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 21f1967bfa..80ae5899ec 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -4,23 +4,21 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkPullToRefresh :refresher="() => reload()">
-	<MkPagination ref="pagingComponent" :pagination="pagination">
-		<template #empty>
-			<div class="_fullinfo">
-				<img :src="infoImageUrl" draggable="false"/>
-				<div>{{ i18n.ts.noNotifications }}</div>
-			</div>
-		</template>
+<MkPagination ref="pagingComponent" :pagination="pagination">
+	<template #empty>
+		<div class="_fullinfo">
+			<img :src="infoImageUrl" draggable="false"/>
+			<div>{{ i18n.ts.noNotifications }}</div>
+		</div>
+	</template>
 
-		<template #default="{ items: notifications }">
-			<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
-				<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
-				<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
-			</MkDateSeparatedList>
-		</template>
-	</MkPagination>
-</MkPullToRefresh>
+	<template #default="{ items: notifications }">
+		<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
+			<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
+			<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
+		</MkDateSeparatedList>
+	</template>
+</MkPagination>
 </template>
 
 <script lang="ts" setup>

From 3682c0069c1dee698fbd54adce65ec5316841fa1 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 12:27:13 +0900
Subject: [PATCH 02/28] Revert "test"

This reverts commit 2b42e8f171e0a030af52ae9b00129282a13479c1.
---
 .../src/components/MkNotifications.vue        | 30 ++++++++++---------
 1 file changed, 16 insertions(+), 14 deletions(-)

diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 80ae5899ec..21f1967bfa 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -4,21 +4,23 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkPagination ref="pagingComponent" :pagination="pagination">
-	<template #empty>
-		<div class="_fullinfo">
-			<img :src="infoImageUrl" draggable="false"/>
-			<div>{{ i18n.ts.noNotifications }}</div>
-		</div>
-	</template>
+<MkPullToRefresh :refresher="() => reload()">
+	<MkPagination ref="pagingComponent" :pagination="pagination">
+		<template #empty>
+			<div class="_fullinfo">
+				<img :src="infoImageUrl" draggable="false"/>
+				<div>{{ i18n.ts.noNotifications }}</div>
+			</div>
+		</template>
 
-	<template #default="{ items: notifications }">
-		<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
-			<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
-			<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
-		</MkDateSeparatedList>
-	</template>
-</MkPagination>
+		<template #default="{ items: notifications }">
+			<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
+				<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
+				<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
+			</MkDateSeparatedList>
+		</template>
+	</MkPagination>
+</MkPullToRefresh>
 </template>
 
 <script lang="ts" setup>

From fc88410c0dd9857cc0432832c4e5f6f6bf8db81a Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 13:34:53 +0900
Subject: [PATCH 03/28] refactor(frontend): tweak MkNotes and MkNotifications

---
 packages/frontend/src/components/MkNotes.vue  | 49 ++++++++++---------
 .../src/components/MkNotifications.vue        | 29 +++++------
 2 files changed, 38 insertions(+), 40 deletions(-)

diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index cee226ee14..1f0f5db42a 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -13,32 +13,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</template>
 
 	<template #default="{ items: notes }">
-		<div :class="[$style.root, { [$style.noGap]: noGap }]">
-			<MkDateSeparatedList
-				ref="notes"
-				v-slot="{ item: note }"
-				:items="notes"
-				:direction="pagination.reversed ? 'up' : 'down'"
-				:reversed="pagination.reversed"
-				:noGap="noGap"
-				:ad="true"
-				:class="$style.notes"
-			>
-				<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
-			</MkDateSeparatedList>
+		<div :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap }]">
+			<template v-for="(note, i) in notes" :key="note.id">
+				<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
+				<div v-if="note._shouldInsertAd_" :class="$style.ad">
+					<MkAd :prefer="['horizontal', 'horizontal-big']"/>
+				</div>
+			</template>
 		</div>
 	</template>
 </MkPagination>
 </template>
 
 <script lang="ts" setup>
-import { useTemplateRef } from 'vue';
+import { useTemplateRef, TransitionGroup } from 'vue';
 import type { Paging } from '@/components/MkPagination.vue';
 import MkNote from '@/components/MkNote.vue';
-import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import { i18n } from '@/i18n.js';
 import { infoImageUrl } from '@/instance.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	pagination: Paging;
@@ -55,20 +49,29 @@ defineExpose({
 
 <style lang="scss" module>
 .root {
+	container-type: inline-size;
+
 	&.noGap {
-		> .notes {
-			background: var(--MI_THEME-panel);
+		background: var(--MI_THEME-panel);
+
+		.note {
+			border-bottom: solid 0.5px var(--MI_THEME-divider);
+		}
+
+		.ad {
+			padding: 8px;
+			background-size: auto auto;
+			background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px);
+			border-bottom: solid 0.5px var(--MI_THEME-divider);
 		}
 	}
 
 	&:not(.noGap) {
-		> .notes {
-			background: var(--MI_THEME-bg);
+		background: var(--MI_THEME-bg);
 
-			.note {
-				background: var(--MI_THEME-panel);
-				border-radius: var(--MI-radius);
-			}
+		.note {
+			background: var(--MI_THEME-panel);
+			border-radius: var(--MI-radius);
 		}
 	}
 }
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 21f1967bfa..a8282fec2a 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -14,10 +14,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</template>
 
 		<template #default="{ items: notifications }">
-			<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
-				<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
-				<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
-			</MkDateSeparatedList>
+			<div :class="$style.notifications">
+				<template v-for="(notification, i) in notifications" :key="notification.id">
+					<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true"/>
+					<XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true"/>
+				</template>
+			</div>
 		</template>
 	</MkPagination>
 </MkPullToRefresh>
@@ -29,7 +31,6 @@ import * as Misskey from 'misskey-js';
 import type { notificationTypes } from '@@/js/const.js';
 import MkPagination from '@/components/MkPagination.vue';
 import XNotification from '@/components/MkNotification.vue';
-import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 import MkNote from '@/components/MkNote.vue';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
@@ -84,28 +85,22 @@ onMounted(() => {
 	connection.on('notificationFlushed', reload);
 });
 
-onActivated(() => {
-	pagingComponent.value?.reload();
-	connection = useStream().useChannel('main');
-	connection.on('notification', onNotification);
-	connection.on('notificationFlushed', reload);
-});
-
 onUnmounted(() => {
 	if (connection) connection.dispose();
 });
 
-onDeactivated(() => {
-	if (connection) connection.dispose();
-});
-
 defineExpose({
 	reload,
 });
 </script>
 
 <style lang="scss" module>
-.list {
+.notifications {
+	container-type: inline-size;
 	background: var(--MI_THEME-panel);
 }
+
+.item {
+	border-bottom: solid 0.5px var(--MI_THEME-divider);
+}
 </style>

From 3451c9a0dedb95d7b22cece61202a9cb027c374d Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 13:39:44 +0900
Subject: [PATCH 04/28] =?UTF-8?q?=F0=9F=8E=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../frontend/src/components/MkPagination.vue  | 65 +++++++------------
 1 file changed, 24 insertions(+), 41 deletions(-)

diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index d90db1748c..d23346971f 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -4,42 +4,34 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<Transition
-	:enterActiveClass="prefer.s.animation ? $style.transition_fade_enterActive : ''"
-	:leaveActiveClass="prefer.s.animation ? $style.transition_fade_leaveActive : ''"
-	:enterFromClass="prefer.s.animation ? $style.transition_fade_enterFrom : ''"
-	:leaveToClass="prefer.s.animation ? $style.transition_fade_leaveTo : ''"
-	mode="out-in"
->
-	<MkLoading v-if="fetching"/>
+<MkLoading v-if="fetching"/>
 
-	<MkError v-else-if="error" @retry="init()"/>
+<MkError v-else-if="error" @retry="init()"/>
 
-	<div v-else-if="empty" key="_empty_" class="empty">
-		<slot name="empty">
-			<div class="_fullinfo">
-				<img :src="infoImageUrl" draggable="false"/>
-				<div>{{ i18n.ts.nothing }}</div>
-			</div>
-		</slot>
-	</div>
-
-	<div v-else ref="rootEl" class="_gaps">
-		<div v-show="pagination.reversed && more" key="_more_">
-			<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMoreAhead : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMoreAhead">
-				{{ i18n.ts.loadMore }}
-			</MkButton>
-			<MkLoading v-else class="loading"/>
-		</div>
-		<slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot>
-		<div v-show="!pagination.reversed && more" key="_more_">
-			<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore">
-				{{ i18n.ts.loadMore }}
-			</MkButton>
-			<MkLoading v-else class="loading"/>
+<div v-else-if="empty" key="_empty_" class="empty">
+	<slot name="empty">
+		<div class="_fullinfo">
+			<img :src="infoImageUrl" draggable="false"/>
+			<div>{{ i18n.ts.nothing }}</div>
 		</div>
+	</slot>
+</div>
+
+<div v-else ref="rootEl" class="_gaps">
+	<div v-show="pagination.reversed && more" key="_more_">
+		<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMoreAhead : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMoreAhead">
+			{{ i18n.ts.loadMore }}
+		</MkButton>
+		<MkLoading v-else class="loading"/>
 	</div>
-</Transition>
+	<slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot>
+	<div v-show="!pagination.reversed && more" key="_more_">
+		<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore">
+			{{ i18n.ts.loadMore }}
+		</MkButton>
+		<MkLoading v-else class="loading"/>
+	</div>
+</div>
 </template>
 
 <script lang="ts">
@@ -491,15 +483,6 @@ defineExpose({
 </script>
 
 <style lang="scss" module>
-.transition_fade_enterActive,
-.transition_fade_leaveActive {
-	transition: opacity 0.125s ease;
-}
-.transition_fade_enterFrom,
-.transition_fade_leaveTo {
-	opacity: 0;
-}
-
 .more {
 	margin-left: auto;
 	margin-right: auto;

From 8dd8f636dc1c2c714e68f3c4a74d991f224b3184 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 13:52:15 +0900
Subject: [PATCH 05/28] =?UTF-8?q?=F0=9F=8E=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/ui/deck/column.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index 4841a78323..8ecac1dc8e 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -377,7 +377,7 @@ function onDrop(ev) {
 	font-size: 0.9em;
 	color: var(--MI_THEME-panelHeaderFg);
 	background: var(--MI_THEME-panelHeaderBg);
-	box-shadow: 0 1px 0 0 var(--MI_THEME-panelHeaderDivider);
+	box-shadow: 0 0.5px 0 0 var(--MI_THEME-panelHeaderDivider);
 	cursor: pointer;
 	user-select: none;
 }

From 33e76f9dfcd7198f5e1e3bffab2d2ee062f1c38f Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 13:55:05 +0900
Subject: [PATCH 06/28] =?UTF-8?q?Revert=20"=F0=9F=8E=A8"?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This reverts commit 3451c9a0dedb95d7b22cece61202a9cb027c374d.
---
 .../frontend/src/components/MkPagination.vue  | 65 ++++++++++++-------
 1 file changed, 41 insertions(+), 24 deletions(-)

diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index d23346971f..d90db1748c 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -4,34 +4,42 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkLoading v-if="fetching"/>
+<Transition
+	:enterActiveClass="prefer.s.animation ? $style.transition_fade_enterActive : ''"
+	:leaveActiveClass="prefer.s.animation ? $style.transition_fade_leaveActive : ''"
+	:enterFromClass="prefer.s.animation ? $style.transition_fade_enterFrom : ''"
+	:leaveToClass="prefer.s.animation ? $style.transition_fade_leaveTo : ''"
+	mode="out-in"
+>
+	<MkLoading v-if="fetching"/>
 
-<MkError v-else-if="error" @retry="init()"/>
+	<MkError v-else-if="error" @retry="init()"/>
 
-<div v-else-if="empty" key="_empty_" class="empty">
-	<slot name="empty">
-		<div class="_fullinfo">
-			<img :src="infoImageUrl" draggable="false"/>
-			<div>{{ i18n.ts.nothing }}</div>
+	<div v-else-if="empty" key="_empty_" class="empty">
+		<slot name="empty">
+			<div class="_fullinfo">
+				<img :src="infoImageUrl" draggable="false"/>
+				<div>{{ i18n.ts.nothing }}</div>
+			</div>
+		</slot>
+	</div>
+
+	<div v-else ref="rootEl" class="_gaps">
+		<div v-show="pagination.reversed && more" key="_more_">
+			<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMoreAhead : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMoreAhead">
+				{{ i18n.ts.loadMore }}
+			</MkButton>
+			<MkLoading v-else class="loading"/>
+		</div>
+		<slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot>
+		<div v-show="!pagination.reversed && more" key="_more_">
+			<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore">
+				{{ i18n.ts.loadMore }}
+			</MkButton>
+			<MkLoading v-else class="loading"/>
 		</div>
-	</slot>
-</div>
-
-<div v-else ref="rootEl" class="_gaps">
-	<div v-show="pagination.reversed && more" key="_more_">
-		<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMoreAhead : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMoreAhead">
-			{{ i18n.ts.loadMore }}
-		</MkButton>
-		<MkLoading v-else class="loading"/>
 	</div>
-	<slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot>
-	<div v-show="!pagination.reversed && more" key="_more_">
-		<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore">
-			{{ i18n.ts.loadMore }}
-		</MkButton>
-		<MkLoading v-else class="loading"/>
-	</div>
-</div>
+</Transition>
 </template>
 
 <script lang="ts">
@@ -483,6 +491,15 @@ defineExpose({
 </script>
 
 <style lang="scss" module>
+.transition_fade_enterActive,
+.transition_fade_leaveActive {
+	transition: opacity 0.125s ease;
+}
+.transition_fade_enterFrom,
+.transition_fade_leaveTo {
+	opacity: 0;
+}
+
 .more {
 	margin-left: auto;
 	margin-right: auto;

From 1af40810900d0eea9be563f80be48a9d071026e2 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 15:00:29 +0900
Subject: [PATCH 07/28] enhance(frontend): disable horizontal swipe for
 timeline/notifications to improve ux

---
 .../src/components/EmPagination.vue           |  4 +-
 packages/frontend-shared/js/scroll.ts         |  4 +-
 packages/frontend/eslint.config.js            |  7 ++-
 packages/frontend/src/components/MkNote.vue   |  2 -
 .../frontend/src/components/MkPagination.vue  |  4 +-
 .../src/components/global/PageWithHeader.vue  | 11 ++++-
 .../frontend/src/pages/antenna-timeline.vue   |  4 +-
 packages/frontend/src/pages/notifications.vue | 20 ++++----
 packages/frontend/src/pages/timeline.vue      | 47 +++++++++----------
 .../frontend/src/pages/user-list-timeline.vue |  4 +-
 10 files changed, 56 insertions(+), 51 deletions(-)

diff --git a/packages/frontend-embed/src/components/EmPagination.vue b/packages/frontend-embed/src/components/EmPagination.vue
index 4cf156ba23..94a91305f4 100644
--- a/packages/frontend-embed/src/components/EmPagination.vue
+++ b/packages/frontend-embed/src/components/EmPagination.vue
@@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
-import { onScrollTop, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isTailVisible, isHeadVisible } from '@@/js/scroll.js';
+import { onScrollTop, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scrollInContainer, isTailVisible, isHeadVisible } from '@@/js/scroll.js';
 import type { ComputedRef } from 'vue';
 import { misskeyApi } from '@/misskey-api.js';
 import { i18n } from '@/i18n.js';
@@ -252,7 +252,7 @@ const fetchMore = async (): Promise<void> => {
 
 			return nextTick(() => {
 				if (scrollableElement.value) {
-					scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
+					scrollInContainer(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
 				} else {
 					window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
 				}
diff --git a/packages/frontend-shared/js/scroll.ts b/packages/frontend-shared/js/scroll.ts
index 6c61c582e1..9057b896c6 100644
--- a/packages/frontend-shared/js/scroll.ts
+++ b/packages/frontend-shared/js/scroll.ts
@@ -93,7 +93,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
 	return removeListener;
 }
 
-export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) {
+export function scrollInContainer(el: HTMLElement, options: ScrollToOptions | undefined) {
 	const container = getScrollContainer(el);
 	if (container == null) {
 		window.scroll(options);
@@ -108,7 +108,7 @@ export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) {
  * @param options Scroll options
  */
 export function scrollToTop(el: HTMLElement, options: { behavior?: ScrollBehavior; } = {}) {
-	scroll(el, { top: 0, ...options });
+	scrollInContainer(el, { top: 0, ...options });
 }
 
 /**
diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js
index 05ac002b53..da0bad1f13 100644
--- a/packages/frontend/eslint.config.js
+++ b/packages/frontend/eslint.config.js
@@ -58,7 +58,8 @@ export default [
 			// location ... window.locationと衝突 or 紛らわしい
 			// document ... window.documentと衝突 or 紛らわしい
 			// history ... window.historyと衝突 or 紛らわしい
-			'id-denylist': ['warn', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history'],
+			// scroll ... window.scrollと衝突 or 紛らわしい
+			'id-denylist': ['warn', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll'],
 			'no-restricted-globals': [
 				'error',
 				{
@@ -85,6 +86,10 @@ export default [
 					'name': 'history',
 					'message': 'Use `window.history`.',
 				},
+				{
+					'name': 'scroll',
+					'message': 'Use `window.scroll`.',
+				},
 				{
 					'name': 'name',
 					'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている',
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 07da1bd4d9..91aed351c5 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -14,8 +14,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 >
 	<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
 	<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
-	<!--<div v-if="appearNote._prId_" class="tip"><i class="ti ti-speakerphone"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>-->
-	<!--<div v-if="appearNote._featuredId_" class="tip"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>-->
 	<div v-if="isRenote" :class="$style.renote">
 		<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
 		<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index d90db1748c..253397475b 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, useTemplateRef, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
-import { onScrollTop, isHeadVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isTailVisible } from '@@/js/scroll.js';
+import { onScrollTop, isHeadVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scrollInContainer, isTailVisible } from '@@/js/scroll.js';
 import type { ComputedRef } from 'vue';
 import type { MisskeyEntity } from '@/types/date-separated-list.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
@@ -258,7 +258,7 @@ const fetchMore = async (): Promise<void> => {
 
 			return nextTick(() => {
 				if (scrollableElement.value) {
-					scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
+					scrollInContainer(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
 				} else {
 					window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
 				}
diff --git a/packages/frontend/src/components/global/PageWithHeader.vue b/packages/frontend/src/components/global/PageWithHeader.vue
index fb813689ba..7ea0b5c97f 100644
--- a/packages/frontend/src/components/global/PageWithHeader.vue
+++ b/packages/frontend/src/components/global/PageWithHeader.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div :class="[$style.root, reversed ? '_pageScrollableReversed' : '_pageScrollable']">
+<div ref="rootEl" :class="[$style.root, reversed ? '_pageScrollableReversed' : '_pageScrollable']">
 	<MkStickyContainer>
 		<template #header><MkPageHeader v-model:tab="tab" :actions="actions" :tabs="tabs"/></template>
 		<div :class="$style.body">
@@ -16,6 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
+import { useTemplateRef } from 'vue';
+import { scrollInContainer } from '@@/js/scroll.js';
 import type { PageHeaderItem } from '@/types/page-header.js';
 import type { Tab } from './MkPageHeader.tabs.vue';
 
@@ -31,6 +33,13 @@ const props = withDefaults(defineProps<{
 });
 
 const tab = defineModel<string>('tab');
+const rootEl = useTemplateRef('rootEl');
+
+defineExpose({
+	scrollToTop: () => {
+		if (rootEl.value) scrollInContainer(rootEl.value, { top: 0, behavior: 'smooth' });
+	},
+});
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue
index 672da04c7e..030a2a46ad 100644
--- a/packages/frontend/src/pages/antenna-timeline.vue
+++ b/packages/frontend/src/pages/antenna-timeline.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, watch, ref, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
-import { scroll } from '@@/js/scroll.js';
+import { scrollInContainer } from '@@/js/scroll.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
@@ -49,7 +49,7 @@ function queueUpdated(q) {
 }
 
 function top() {
-	scroll(rootEl.value, { top: 0 });
+	scrollInContainer(rootEl.value, { top: 0 });
 }
 
 async function timetravel() {
diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue
index a7ff519a34..0a2bc02de5 100644
--- a/packages/frontend/src/pages/notifications.vue
+++ b/packages/frontend/src/pages/notifications.vue
@@ -6,17 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
 	<MkSpacer :contentMax="800">
-		<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
-			<div v-if="tab === 'all'">
-				<XNotifications :class="$style.notifications" :excludeTypes="excludeTypes"/>
-			</div>
-			<div v-else-if="tab === 'mentions'">
-				<MkNotes :pagination="mentionsPagination"/>
-			</div>
-			<div v-else-if="tab === 'directNotes'">
-				<MkNotes :pagination="directNotesPagination"/>
-			</div>
-		</MkHorizontalSwipe>
+		<div v-if="tab === 'all'">
+			<XNotifications :class="$style.notifications" :excludeTypes="excludeTypes"/>
+		</div>
+		<div v-else-if="tab === 'mentions'">
+			<MkNotes :pagination="mentionsPagination"/>
+		</div>
+		<div v-else-if="tab === 'directNotes'">
+			<MkNotes :pagination="directNotesPagination"/>
+		</div>
 	</MkSpacer>
 </PageWithHeader>
 </template>
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index a811f01ee3..7fdeacc96b 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -4,38 +4,33 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<PageWithHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true">
+<PageWithHeader ref="rootEl" v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true">
 	<MkSpacer :contentMax="800">
-		<MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin">
-			<div ref="rootEl">
-				<MkInfo v-if="isBasicTimeline(src) && !store.r.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
-					{{ i18n.ts._timelineDescription[src] }}
-				</MkInfo>
-				<MkPostForm v-if="prefer.r.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/>
-				<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
-				<div :class="$style.tl">
-					<MkTimeline
-						ref="tlComponent"
-						:key="src + withRenotes + withReplies + onlyFiles + withSensitive"
-						:src="src.split(':')[0]"
-						:list="src.split(':')[1]"
-						:withRenotes="withRenotes"
-						:withReplies="withReplies"
-						:withSensitive="withSensitive"
-						:onlyFiles="onlyFiles"
-						:sound="true"
-						@queue="queueUpdated"
-					/>
-				</div>
-			</div>
-		</MkHorizontalSwipe>
+		<MkInfo v-if="isBasicTimeline(src) && !store.r.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
+			{{ i18n.ts._timelineDescription[src] }}
+		</MkInfo>
+		<MkPostForm v-if="prefer.r.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/>
+		<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
+		<div :class="$style.tl">
+			<MkTimeline
+				ref="tlComponent"
+				:key="src + withRenotes + withReplies + onlyFiles + withSensitive"
+				:src="src.split(':')[0]"
+				:list="src.split(':')[1]"
+				:withRenotes="withRenotes"
+				:withReplies="withReplies"
+				:withSensitive="withSensitive"
+				:onlyFiles="onlyFiles"
+				:sound="true"
+				@queue="queueUpdated"
+			/>
+		</div>
 	</MkSpacer>
 </PageWithHeader>
 </template>
 
 <script lang="ts" setup>
 import { computed, watch, provide, useTemplateRef, ref, onMounted, onActivated } from 'vue';
-import { scroll } from '@@/js/scroll.js';
 import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
 import type { MenuItem } from '@/types/menu.js';
 import type { BasicTimelineType } from '@/timelines.js';
@@ -133,7 +128,7 @@ function queueUpdated(q: number): void {
 }
 
 function top(): void {
-	if (rootEl.value) scroll(rootEl.value, { top: 0, behavior: 'smooth' });
+	if (rootEl.value) rootEl.value.scrollToTop();
 }
 
 async function chooseList(ev: MouseEvent): Promise<void> {
diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue
index 8f5244ca1a..53081b0f16 100644
--- a/packages/frontend/src/pages/user-list-timeline.vue
+++ b/packages/frontend/src/pages/user-list-timeline.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, watch, ref, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
-import { scroll } from '@@/js/scroll.js';
+import { scrollInContainer } from '@@/js/scroll.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { definePage } from '@/page.js';
@@ -54,7 +54,7 @@ function queueUpdated(q) {
 }
 
 function top() {
-	scroll(rootEl.value, { top: 0 });
+	scrollInContainer(rootEl.value, { top: 0 });
 }
 
 function settings() {

From 490222fb78196738a351614b4566c5b30fc703a7 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 15:33:19 +0900
Subject: [PATCH 08/28] perf(frontend): avoid needless AsyncComponentWrapper

---
 packages/frontend/src/boot/common.ts    |  4 ++--
 packages/frontend/src/boot/main-boot.ts | 12 ++++++------
 packages/frontend/src/boot/sub-boot.ts  |  5 ++---
 3 files changed, 10 insertions(+), 11 deletions(-)

diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index 9a505ca9f8..7a88b938dd 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -29,7 +29,7 @@ import { fetchCustomEmojis } from '@/custom-emojis.js';
 import { prefer } from '@/preferences.js';
 import { $i } from '@/i.js';
 
-export async function common(createVue: () => App<Element>) {
+export async function common(createVue: () => Promise<App<Element>>) {
 	console.info(`Misskey v${version}`);
 
 	if (_DEV_) {
@@ -263,7 +263,7 @@ export async function common(createVue: () => App<Element>) {
 		});
 	});
 
-	const app = createVue();
+	const app = await createVue();
 
 	if (_DEV_) {
 		app.config.performance = true;
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index bac7128603..224c441beb 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -32,7 +32,7 @@ import { signout } from '@/signout.js';
 import { migrateOldSettings } from '@/pref-migrate.js';
 
 export async function mainBoot() {
-	const { isClientUpdated, lastVersion } = await common(() => {
+	const { isClientUpdated, lastVersion } = await common(async () => {
 		let uiStyle = ui;
 		const searchParams = new URLSearchParams(window.location.search);
 
@@ -46,19 +46,19 @@ export async function mainBoot() {
 		let rootComponent: Component;
 		switch (uiStyle) {
 			case 'zen':
-				rootComponent = defineAsyncComponent(() => import('@/ui/zen.vue'));
+				rootComponent = await import('@/ui/zen.vue').then(x => x.default);
 				break;
 			case 'deck':
-				rootComponent = defineAsyncComponent(() => import('@/ui/deck.vue'));
+				rootComponent = await import('@/ui/deck.vue').then(x => x.default);
 				break;
 			case 'visitor':
-				rootComponent = defineAsyncComponent(() => import('@/ui/visitor.vue'));
+				rootComponent = await import('@/ui/visitor.vue').then(x => x.default);
 				break;
 			case 'classic':
-				rootComponent = defineAsyncComponent(() => import('@/ui/classic.vue'));
+				rootComponent = await import('@/ui/classic.vue').then(x => x.default);
 				break;
 			default:
-				rootComponent = defineAsyncComponent(() => import('@/ui/universal.vue'));
+				rootComponent = await import('@/ui/universal.vue').then(x => x.default);
 				break;
 		}
 
diff --git a/packages/frontend/src/boot/sub-boot.ts b/packages/frontend/src/boot/sub-boot.ts
index e24c324dfb..036142bc4d 100644
--- a/packages/frontend/src/boot/sub-boot.ts
+++ b/packages/frontend/src/boot/sub-boot.ts
@@ -6,11 +6,10 @@
 import { createApp, defineAsyncComponent } from 'vue';
 import { common } from './common.js';
 import { emojiPicker } from '@/utility/emoji-picker.js';
+import UiMinimum from '@/ui/minimum.vue';
 
 export async function subBoot() {
-	const { isClientUpdated } = await common(() => createApp(
-		defineAsyncComponent(() => import('@/ui/minimum.vue')),
-	));
+	const { isClientUpdated } = await common(async () => createApp(UiMinimum));
 
 	emojiPicker.init();
 }

From 7d8c98767a12dd0f588179b8a6fe00c3c86d04a7 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 15:53:44 +0900
Subject: [PATCH 09/28] lint

---
 packages/frontend/src/components/MkHorizontalSwipe.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/MkHorizontalSwipe.vue b/packages/frontend/src/components/MkHorizontalSwipe.vue
index bc63bef0b6..1d0ffaea11 100644
--- a/packages/frontend/src/components/MkHorizontalSwipe.vue
+++ b/packages/frontend/src/components/MkHorizontalSwipe.vue
@@ -100,7 +100,7 @@ function touchMove(event: TouchEvent) {
 
 		pullDistance.value = 0;
 		isSwiping.value = false;
-		setTimeout(() => {
+		window.setTimeout(() => {
 			isSwipingForClass.value = false;
 		}, 400);
 

From 88e6bd1533eddc4b0f98875f85ad07f728059a04 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 15:55:22 +0900
Subject: [PATCH 10/28] Update eslint.config.js

---
 packages/frontend/eslint.config.js | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js
index da0bad1f13..57cc0a0a5d 100644
--- a/packages/frontend/eslint.config.js
+++ b/packages/frontend/eslint.config.js
@@ -59,7 +59,9 @@ export default [
 			// document ... window.documentと衝突 or 紛らわしい
 			// history ... window.historyと衝突 or 紛らわしい
 			// scroll ... window.scrollと衝突 or 紛らわしい
-			'id-denylist': ['warn', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll'],
+			// setTimeout ... window.setTimeoutと衝突 or 紛らわしい
+			// setInterval ... window.setIntervalと衝突 or 紛らわしい
+			'id-denylist': ['warn', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval'],
 			'no-restricted-globals': [
 				'error',
 				{
@@ -90,6 +92,14 @@ export default [
 					'name': 'scroll',
 					'message': 'Use `window.scroll`.',
 				},
+				{
+					'name': 'setTimeout',
+					'message': 'Use `window.setTimeout`.',
+				},
+				{
+					'name': 'setInterval',
+					'message': 'Use `window.setInterval`.',
+				},
 				{
 					'name': 'name',
 					'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている',

From 5a09e7a8b4120afc107d63ab69b8f29ee1e79755 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 15:57:34 +0900
Subject: [PATCH 11/28] lint

---
 packages/frontend/eslint.config.js                | 12 +++++++++++-
 packages/frontend/src/components/MkPagination.vue | 10 +++++-----
 2 files changed, 16 insertions(+), 6 deletions(-)

diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js
index 57cc0a0a5d..1b9a9b68c0 100644
--- a/packages/frontend/eslint.config.js
+++ b/packages/frontend/eslint.config.js
@@ -61,7 +61,9 @@ export default [
 			// scroll ... window.scrollと衝突 or 紛らわしい
 			// setTimeout ... window.setTimeoutと衝突 or 紛らわしい
 			// setInterval ... window.setIntervalと衝突 or 紛らわしい
-			'id-denylist': ['warn', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval'],
+			// clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい
+			// clearInterval ... window.clearIntervalと衝突 or 紛らわしい
+			'id-denylist': ['warn', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'],
 			'no-restricted-globals': [
 				'error',
 				{
@@ -100,6 +102,14 @@ export default [
 					'name': 'setInterval',
 					'message': 'Use `window.setInterval`.',
 				},
+				{
+					'name': 'clearTimeout',
+					'message': 'Use `window.clearTimeout`.',
+				},
+				{
+					'name': 'clearInterval',
+					'message': 'Use `window.clearInterval`.',
+				},
 				{
 					'name': 'name',
 					'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている',
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index 253397475b..c639b4dd18 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -357,7 +357,7 @@ watch(visibility, () => {
 		BACKGROUND_PAUSE_WAIT_SEC * 1000);
 	} else { // 'visible'
 		if (timerForSetPause) {
-			clearTimeout(timerForSetPause);
+			window.clearTimeout(timerForSetPause);
 			timerForSetPause = null;
 		} else {
 			isPausingUpdate = false;
@@ -453,11 +453,11 @@ onBeforeMount(() => {
 	init().then(() => {
 		if (props.pagination.reversed) {
 			nextTick(() => {
-				setTimeout(toBottom, 800);
+				window.setTimeout(toBottom, 800);
 
 				// scrollToBottomでmoreFetchingボタンが画面外まで出るまで
 				// more = trueを遅らせる
-				setTimeout(() => {
+				window.setTimeout(() => {
 					moreFetching.value = false;
 				}, 2000);
 			});
@@ -467,11 +467,11 @@ onBeforeMount(() => {
 
 onBeforeUnmount(() => {
 	if (timerForSetPause) {
-		clearTimeout(timerForSetPause);
+		window.clearTimeout(timerForSetPause);
 		timerForSetPause = null;
 	}
 	if (preventAppearFetchMoreTimer.value) {
-		clearTimeout(preventAppearFetchMoreTimer.value);
+		window.clearTimeout(preventAppearFetchMoreTimer.value);
 		preventAppearFetchMoreTimer.value = null;
 	}
 	scrollObserver.value?.disconnect();

From e5e439049490e69925eef55a354276e75affcc0c Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 16:01:51 +0900
Subject: [PATCH 12/28] fix(frontend): suppress inject warn

---
 packages/frontend/src/components/global/MkEmoji.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index 198c0d8ace..fa55fd888b 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -25,7 +25,7 @@ const props = defineProps<{
 	menuReaction?: boolean;
 }>();
 
-const react = inject(DI.mfmEmojiReactCallback);
+const react = inject(DI.mfmEmojiReactCallback, null);
 
 const char2path = prefer.s.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
 

From 6c2c3f08beae4ed5d056ec1636aa59d1a47e5965 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 16:04:01 +0900
Subject: [PATCH 13/28] refactor(frontend): use symbol for di

---
 packages/frontend/src/components/MkModal.vue    | 3 ++-
 packages/frontend/src/components/MkPostForm.vue | 2 +-
 packages/frontend/src/di.ts                     | 1 +
 3 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index b5c93df4ed..3bcf835ec9 100644
--- a/packages/frontend/src/components/MkModal.vue
+++ b/packages/frontend/src/components/MkModal.vue
@@ -50,6 +50,7 @@ import { deviceKind } from '@/utility/device-kind.js';
 import { focusTrap } from '@/utility/focus-trap.js';
 import { focusParent } from '@/utility/focus.js';
 import { prefer } from '@/preferences.js';
+import { DI } from '@/di.js';
 
 function getFixedContainer(el: Element | null): Element | null {
 	if (el == null || el.tagName === 'BODY') return null;
@@ -94,7 +95,7 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-provide('modal', true);
+provide(DI.inModal, true);
 
 const maxHeight = ref<number>();
 const fixed = ref(false);
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 0d37d973f0..78c69306b0 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -140,7 +140,7 @@ import { DI } from '@/di.js';
 
 const $i = ensureSignin();
 
-const modal = inject('modal');
+const modal = inject(DI.inModal, false);
 
 const props = withDefaults(defineProps<PostFormProps & {
 	fixed?: boolean;
diff --git a/packages/frontend/src/di.ts b/packages/frontend/src/di.ts
index b58c8c9659..541cdb76a8 100644
--- a/packages/frontend/src/di.ts
+++ b/packages/frontend/src/di.ts
@@ -15,4 +15,5 @@ export const DI = {
 	currentStickyTop: Symbol() as InjectionKey<Ref<number>>,
 	currentStickyBottom: Symbol() as InjectionKey<Ref<number>>,
 	mfmEmojiReactCallback: Symbol() as InjectionKey<(emoji: string) => void>,
+	inModal: Symbol() as InjectionKey<boolean>,
 };

From d9d796b204881fc14196b89230daa84564f599cf Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 16:09:27 +0900
Subject: [PATCH 14/28] lint fixes

---
 .../MkChannelFollowButton.stories.impl.ts         |  8 +++-----
 .../src/components/MkClickerGame.stories.impl.ts  |  8 +++-----
 .../src/components/MkEmojiPickerDialog.vue        |  2 +-
 packages/frontend/src/components/MkMediaVideo.vue |  2 +-
 .../frontend/src/components/MkPullToRefresh.vue   |  4 ++--
 .../global/MkPageHeader.stories.impl.ts           |  7 +++----
 .../src/components/global/MkPageHeader.tabs.vue   |  2 +-
 packages/frontend/src/components/grid/MkGrid.vue  | 15 +++++++--------
 .../frontend/src/pages/reversi/game.board.vue     |  8 ++++----
 .../frontend/src/pages/settings/preferences.vue   |  2 +-
 packages/frontend/src/utility/achievements.ts     |  2 +-
 packages/frontend/src/utility/confetti.ts         |  4 ++--
 packages/frontend/src/utility/hotkey.ts           |  4 ++--
 packages/frontend/src/utility/idle-render.ts      |  4 ++--
 packages/frontend/src/utility/sound.ts            |  6 +++---
 packages/frontend/src/utility/test-utils.ts       |  2 +-
 16 files changed, 37 insertions(+), 43 deletions(-)

diff --git a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts
index a42e80c27a..4304c2e2b7 100644
--- a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts
+++ b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts
@@ -2,20 +2,18 @@
  * SPDX-FileCopyrightText: syuilo and misskey-project
  * SPDX-License-Identifier: AGPL-3.0-only
  */
-
-/* eslint-disable @typescript-eslint/explicit-function-return-type */
-/* eslint-disable import/no-default-export */
-import type { StoryObj } from '@storybook/vue3';
+ 
 import { HttpResponse, http } from 'msw';
 import { action } from '@storybook/addon-actions';
 import { expect, userEvent, within } from '@storybook/test';
 import { channel } from '../../.storybook/fakes.js';
 import { commonHandlers } from '../../.storybook/mocks.js';
 import MkChannelFollowButton from './MkChannelFollowButton.vue';
+import type { StoryObj } from '@storybook/vue3';
 import { i18n } from '@/i18n.js';
 
 function sleep(ms: number) {
-	return new Promise(resolve => setTimeout(resolve, ms));
+	return new Promise(resolve => window.setTimeout(resolve, ms));
 }
 
 export const Default = {
diff --git a/packages/frontend/src/components/MkClickerGame.stories.impl.ts b/packages/frontend/src/components/MkClickerGame.stories.impl.ts
index eb7e61f294..6e1eb13d61 100644
--- a/packages/frontend/src/components/MkClickerGame.stories.impl.ts
+++ b/packages/frontend/src/components/MkClickerGame.stories.impl.ts
@@ -2,18 +2,16 @@
  * SPDX-FileCopyrightText: syuilo and misskey-project
  * SPDX-License-Identifier: AGPL-3.0-only
  */
-
-/* eslint-disable @typescript-eslint/explicit-function-return-type */
-/* eslint-disable import/no-default-export */
-import type { StoryObj } from '@storybook/vue3';
+ 
 import { HttpResponse, http } from 'msw';
 import { action } from '@storybook/addon-actions';
 import { expect, userEvent, within } from '@storybook/test';
 import { commonHandlers } from '../../.storybook/mocks.js';
 import MkClickerGame from './MkClickerGame.vue';
+import type { StoryObj } from '@storybook/vue3';
 
 function sleep(ms: number) {
-	return new Promise(resolve => setTimeout(resolve, ms));
+	return new Promise(resolve => window.setTimeout(resolve, ms));
 }
 
 export const Default = {
diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue
index 4978933f73..662e2a118d 100644
--- a/packages/frontend/src/components/MkEmojiPickerDialog.vue
+++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue
@@ -79,7 +79,7 @@ function opening() {
 	picker.value?.focus();
 
 	// 何故かちょっと待たないとフォーカスされない
-	setTimeout(() => {
+	window.setTimeout(() => {
 		picker.value?.focus();
 	}, 10);
 }
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 629679a971..1cd88fb1ab 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -339,7 +339,7 @@ const bufferedDataRatio = computed(() => {
 // MediaControl Events
 function onMouseOver() {
 	if (controlStateTimer) {
-		clearTimeout(controlStateTimer);
+		window.clearTimeout(controlStateTimer);
 	}
 	isHoverring.value = true;
 }
diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue
index 1fbf00d212..b1df282514 100644
--- a/packages/frontend/src/components/MkPullToRefresh.vue
+++ b/packages/frontend/src/components/MkPullToRefresh.vue
@@ -82,11 +82,11 @@ function moveBySystem(to: number): Promise<void> {
 			return;
 		}
 		const startTime = Date.now();
-		let intervalId = setInterval(() => {
+		let intervalId = window.setInterval(() => {
 			const time = Date.now() - startTime;
 			if (time > RELEASE_TRANSITION_DURATION) {
 				pullDistance.value = to;
-				clearInterval(intervalId);
+				window.clearInterval(intervalId);
 				r();
 				return;
 			}
diff --git a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts
index c9af5f4ea4..15938d0495 100644
--- a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts
@@ -2,11 +2,10 @@
  * SPDX-FileCopyrightText: syuilo and misskey-project
  * SPDX-License-Identifier: AGPL-3.0-only
  */
-
-/* eslint-disable @typescript-eslint/explicit-function-return-type */
+ 
 import { waitFor } from '@storybook/test';
-import type { StoryObj } from '@storybook/vue3';
 import MkPageHeader from './MkPageHeader.vue';
+import type { StoryObj } from '@storybook/vue3';
 export const Empty = {
 	render(args) {
 		return {
@@ -29,7 +28,7 @@ export const Empty = {
 		};
 	},
 	async play() {
-		const wait = new Promise((resolve) => setTimeout(resolve, 800));
+		const wait = new Promise((resolve) => window.setTimeout(resolve, 800));
 		await waitFor(async () => await wait);
 	},
 	args: {
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
index 358a17d3e8..255fca8f86 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
@@ -133,7 +133,7 @@ async function enter(el: Element) {
 		entering = false;
 	});
 
-	setTimeout(renderTab, 170);
+	window.setTimeout(renderTab, 170);
 }
 
 function afterEnter(el: Element) {
diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue
index 94f4f3dab1..c37f3df0d3 100644
--- a/packages/frontend/src/components/grid/MkGrid.vue
+++ b/packages/frontend/src/components/grid/MkGrid.vue
@@ -50,6 +50,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script setup lang="ts">
 import { computed, onMounted, ref, toRefs, watch } from 'vue';
+import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js';
+import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
+import type { GridContext, GridEvent } from '@/components/grid/grid-event.js';
+import type { GridColumn } from '@/components/grid/column.js';
+import type { GridRow, GridRowSetting } from '@/components/grid/row.js';
+import type { MenuItem } from '@/types/menu.js';
 import { GridEventEmitter } from '@/components/grid/grid.js';
 import MkDataRow from '@/components/grid/MkDataRow.vue';
 import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
@@ -68,13 +74,6 @@ import { createColumn } from '@/components/grid/column.js';
 import { createRow, defaultGridRowSetting, resetRow } from '@/components/grid/row.js';
 import { handleKeyEvent } from '@/utility/key-event.js';
 
-import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js';
-import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
-import type { GridContext, GridEvent } from '@/components/grid/grid-event.js';
-import type { GridColumn } from '@/components/grid/column.js';
-import type { GridRow, GridRowSetting } from '@/components/grid/row.js';
-import type { MenuItem } from '@/types/menu.js';
-
 type RowHolder = {
 	row: GridRow,
 	cells: GridCell[],
@@ -130,7 +129,7 @@ const bus = new GridEventEmitter();
  *
  * @see {@link onResize}
  */
-const resizeObserver = new ResizeObserver((entries) => setTimeout(() => onResize(entries)));
+const resizeObserver = new ResizeObserver((entries) => window.setTimeout(() => onResize(entries)));
 
 const rootEl = ref<InstanceType<typeof HTMLTableElement>>();
 /**
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue
index 403a760521..b7434bff9f 100644
--- a/packages/frontend/src/pages/reversi/game.board.vue
+++ b/packages/frontend/src/pages/reversi/game.board.vue
@@ -145,13 +145,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, onActivated, onDeactivated, onMounted, onUnmounted, ref, shallowRef, triggerRef, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import * as Reversi from 'misskey-reversi';
+import { useInterval } from '@@/js/use-interval.js';
+import { url } from '@@/js/config.js';
 import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import { deepClone } from '@/utility/clone.js';
-import { useInterval } from '@@/js/use-interval.js';
 import { ensureSignin } from '@/i.js';
-import { url } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { userPage } from '@/filters/user.js';
@@ -301,7 +301,7 @@ if (!props.game.isEnded) {
 
 		if (iAmPlayer.value) {
 			if ((isMyTurn.value && myTurnTimerRmain.value === 0) || (!isMyTurn.value && opTurnTimerRmain.value === 0)) {
-			props.connection!.send('claimTimeIsUp', {});
+				props.connection!.send('claimTimeIsUp', {});
 			}
 		}
 	}, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true });
@@ -424,7 +424,7 @@ function autoplay() {
 		const tick = () => {
 			const log = logs[i];
 			const time = log.time - previousLog.time;
-			setTimeout(() => {
+			window.setTimeout(() => {
 				i++;
 				logPos.value++;
 				previousLog = log;
diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue
index 816f8d7435..6fe5a50e19 100644
--- a/packages/frontend/src/pages/settings/preferences.vue
+++ b/packages/frontend/src/pages/settings/preferences.vue
@@ -786,7 +786,7 @@ function testNotification(): void {
 		smashCount = 0;
 	}
 	if (smashTimer) {
-		clearTimeout(smashTimer);
+		window.clearTimeout(smashTimer);
 	}
 	smashTimer = window.setTimeout(() => {
 		smashCount = 0;
diff --git a/packages/frontend/src/utility/achievements.ts b/packages/frontend/src/utility/achievements.ts
index f6ab587ae1..06b445ab0d 100644
--- a/packages/frontend/src/utility/achievements.ts
+++ b/packages/frontend/src/utility/achievements.ts
@@ -497,7 +497,7 @@ export async function claimAchievement(type: typeof ACHIEVEMENT_TYPES[number]) {
 	if (claimedAchievements.includes(type)) return;
 	claimingQueue.add(type);
 	claimedAchievements.push(type);
-	await new Promise(resolve => setTimeout(resolve, (claimingQueue.size - 1) * 500));
+	await new Promise(resolve => window.setTimeout(resolve, (claimingQueue.size - 1) * 500));
 	window.setTimeout(() => {
 		claimingQueue.delete(type);
 	}, 500);
diff --git a/packages/frontend/src/utility/confetti.ts b/packages/frontend/src/utility/confetti.ts
index 8e53a6ceeb..c19149875f 100644
--- a/packages/frontend/src/utility/confetti.ts
+++ b/packages/frontend/src/utility/confetti.ts
@@ -15,11 +15,11 @@ export function confetti(options: { duration?: number; } = {}) {
 		return Math.random() * (max - min) + min;
 	}
 
-	const interval = setInterval(() => {
+	const interval = window.setInterval(() => {
 		const timeLeft = animationEnd - Date.now();
 
 		if (timeLeft <= 0) {
-			return clearInterval(interval);
+			return window.clearInterval(interval);
 		}
 
 		const particleCount = 50 * (timeLeft / duration);
diff --git a/packages/frontend/src/utility/hotkey.ts b/packages/frontend/src/utility/hotkey.ts
index 81fc28d7c8..d728cdfcb0 100644
--- a/packages/frontend/src/utility/hotkey.ts
+++ b/packages/frontend/src/utility/hotkey.ts
@@ -2,7 +2,7 @@
  * SPDX-FileCopyrightText: syuilo and misskey-project
  * SPDX-License-Identifier: AGPL-3.0-only
  */
-import { getHTMLElementOrNull } from "@/utility/get-dom-node-or-null.js";
+import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
 
 //#region types
 export type Keymap = Record<string, CallbackFunction | CallbackObject>;
@@ -136,7 +136,7 @@ let lastHotKeyStoreTimer: number | null = null;
 
 const storePattern = (ev: KeyboardEvent, callback: CallbackFunction) => {
 	if (lastHotKeyStoreTimer != null) {
-		clearTimeout(lastHotKeyStoreTimer);
+		window.clearTimeout(lastHotKeyStoreTimer);
 	}
 
 	latestHotkey = {
diff --git a/packages/frontend/src/utility/idle-render.ts b/packages/frontend/src/utility/idle-render.ts
index 6adfedcb9f..32daa1df02 100644
--- a/packages/frontend/src/utility/idle-render.ts
+++ b/packages/frontend/src/utility/idle-render.ts
@@ -5,7 +5,7 @@
 
 const requestIdleCallback: typeof globalThis.requestIdleCallback = globalThis.requestIdleCallback ?? ((callback) => {
 	const start = performance.now();
-	const timeoutId = setTimeout(() => {
+	const timeoutId = window.setTimeout(() => {
 		callback({
 			didTimeout: false, // polyfill でタイムアウト発火することはない
 			timeRemaining() {
@@ -17,7 +17,7 @@ const requestIdleCallback: typeof globalThis.requestIdleCallback = globalThis.re
 	return timeoutId;
 });
 const cancelIdleCallback: typeof globalThis.cancelIdleCallback = globalThis.cancelIdleCallback ?? ((timeoutId) => {
-	clearTimeout(timeoutId);
+	window.clearTimeout(timeoutId);
 });
 
 class IdlingRenderScheduler {
diff --git a/packages/frontend/src/utility/sound.ts b/packages/frontend/src/utility/sound.ts
index f217bdfcd5..d3f82a37f2 100644
--- a/packages/frontend/src/utility/sound.ts
+++ b/packages/frontend/src/utility/sound.ts
@@ -158,7 +158,7 @@ export async function playMisskeySfxFile(soundStore: SoundStore): Promise<boolea
 	canPlay = false;
 	return await playMisskeySfxFileInternal(soundStore).finally(() => {
 		// ごく短時間に音が重複しないように
-		setTimeout(() => {
+		window.setTimeout(() => {
 			canPlay = true;
 		}, 25);
 	});
@@ -230,10 +230,10 @@ export async function getSoundDuration(file: string): Promise<number> {
 	const audioEl = window.document.createElement('audio');
 	audioEl.src = file;
 	return new Promise((resolve) => {
-		const si = setInterval(() => {
+		const si = window.setInterval(() => {
 			if (audioEl.readyState > 0) {
 				resolve(audioEl.duration * 1000);
-				clearInterval(si);
+				window.clearInterval(si);
 				audioEl.remove();
 			}
 		}, 100);
diff --git a/packages/frontend/src/utility/test-utils.ts b/packages/frontend/src/utility/test-utils.ts
index 52bb2d94e0..54742c1a9e 100644
--- a/packages/frontend/src/utility/test-utils.ts
+++ b/packages/frontend/src/utility/test-utils.ts
@@ -5,5 +5,5 @@
 
 export async function tick(): Promise<void> {
 	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-	await new Promise((globalThis.requestIdleCallback ?? setTimeout) as never);
+	await new Promise((globalThis.requestIdleCallback ?? window.setTimeout) as never);
 }

From 609a37742cb8fa2d7172aa633c8f78ca145fbdf6 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 16:11:15 +0900
Subject: [PATCH 15/28] clean up

---
 packages/frontend/src/components/MkPagination.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index c639b4dd18..9adc3d98da 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<MkError v-else-if="error" @retry="init()"/>
 
-	<div v-else-if="empty" key="_empty_" class="empty">
+	<div v-else-if="empty" key="_empty_">
 		<slot name="empty">
 			<div class="_fullinfo">
 				<img :src="infoImageUrl" draggable="false"/>
@@ -29,14 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMoreAhead : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMoreAhead">
 				{{ i18n.ts.loadMore }}
 			</MkButton>
-			<MkLoading v-else class="loading"/>
+			<MkLoading v-else/>
 		</div>
 		<slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot>
 		<div v-show="!pagination.reversed && more" key="_more_">
 			<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore">
 				{{ i18n.ts.loadMore }}
 			</MkButton>
-			<MkLoading v-else class="loading"/>
+			<MkLoading v-else/>
 		</div>
 	</div>
 </Transition>

From ae5957811561ddb3a64edbcd1e86bb6d04b5caa2 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 16:55:12 +0900
Subject: [PATCH 16/28] refactor

---
 packages/frontend/src/ui/universal.vue | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 5e64de6ffe..aeabcb3213 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -13,10 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<XAnnouncements v-if="$i"/>
 			<XStatusBars :class="$style.statusbars"/>
 		</div>
-		<div :class="$style.content">
-			<StackingRouterView v-if="prefer.s['experimental.stackingRouterView']"/>
-			<RouterView v-else/>
-		</div>
+		<StackingRouterView v-if="prefer.s['experimental.stackingRouterView']" :class="$style.content"/>
+		<RouterView v-else :class="$style.content"/>
 		<div v-if="isMobile" ref="navFooter" :class="$style.nav">
 			<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button>
 			<button :class="$style.navButton" class="_button" @click="mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button>

From 7fd3adedee86a6beafc864ef74869c069d639efc Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 17:00:01 +0900
Subject: [PATCH 17/28] fix tests

---
 packages/backend/src/core/WebhookTestService.ts | 1 +
 packages/backend/test/e2e/users.ts              | 2 ++
 2 files changed, 3 insertions(+)

diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts
index a6198f7686..222153fd2a 100644
--- a/packages/backend/src/core/WebhookTestService.ts
+++ b/packages/backend/src/core/WebhookTestService.ts
@@ -463,6 +463,7 @@ export class WebhookTestService {
 			followersVisibility: 'public',
 			followingVisibility: 'public',
 			chatScope: 'mutual',
+			canChat: true,
 			twoFactorEnabled: false,
 			usePasswordLessLogin: false,
 			securityKeys: false,
diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts
index e04d258c0d..a342bba64c 100644
--- a/packages/backend/test/e2e/users.ts
+++ b/packages/backend/test/e2e/users.ts
@@ -84,6 +84,7 @@ describe('ユーザー', () => {
 			followingVisibility: user.followingVisibility,
 			followersVisibility: user.followersVisibility,
 			chatScope: user.chatScope,
+			canChat: user.canChat,
 			roles: user.roles,
 			memo: user.memo,
 		});
@@ -346,6 +347,7 @@ describe('ユーザー', () => {
 		assert.strictEqual(response.followingVisibility, 'public');
 		assert.strictEqual(response.followersVisibility, 'public');
 		assert.strictEqual(response.chatScope, 'mutual');
+		assert.strictEqual(response.canChat, true);
 		assert.deepStrictEqual(response.roles, []);
 		assert.strictEqual(response.memo, null);
 

From b030e33856904d6fcd257433eb610b0bfad688d3 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 17:15:31 +0900
Subject: [PATCH 18/28] perf(frontend): improve performance of timeline page

---
 packages/frontend/src/pages/timeline.vue | 52 +++++++++++++-----------
 1 file changed, 28 insertions(+), 24 deletions(-)

diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 7fdeacc96b..e08fce7bc1 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -4,33 +4,37 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<PageWithHeader ref="rootEl" v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true">
-	<MkSpacer :contentMax="800">
-		<MkInfo v-if="isBasicTimeline(src) && !store.r.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
-			{{ i18n.ts._timelineDescription[src] }}
-		</MkInfo>
-		<MkPostForm v-if="prefer.r.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/>
-		<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
-		<div :class="$style.tl">
-			<MkTimeline
-				ref="tlComponent"
-				:key="src + withRenotes + withReplies + onlyFiles + withSensitive"
-				:src="src.split(':')[0]"
-				:list="src.split(':')[1]"
-				:withRenotes="withRenotes"
-				:withReplies="withReplies"
-				:withSensitive="withSensitive"
-				:onlyFiles="onlyFiles"
-				:sound="true"
-				@queue="queueUpdated"
-			/>
-		</div>
-	</MkSpacer>
-</PageWithHeader>
+<div ref="rootEl" class="_pageScrollable">
+	<MkStickyContainer>
+		<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin"/></template>
+		<MkSpacer :contentMax="800">
+			<MkInfo v-if="isBasicTimeline(src) && !store.r.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
+				{{ i18n.ts._timelineDescription[src] }}
+			</MkInfo>
+			<MkPostForm v-if="prefer.r.showFixedPostForm.value" :class="$style.postForm" class="_panel" fixed style="margin-bottom: var(--MI-margin);"/>
+			<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
+			<div :class="$style.tl">
+				<MkTimeline
+					ref="tlComponent"
+					:key="src + withRenotes + withReplies + onlyFiles + withSensitive"
+					:src="src.split(':')[0]"
+					:list="src.split(':')[1]"
+					:withRenotes="withRenotes"
+					:withReplies="withReplies"
+					:withSensitive="withSensitive"
+					:onlyFiles="onlyFiles"
+					:sound="true"
+					@queue="queueUpdated"
+				/>
+			</div>
+		</MkSpacer>
+	</MkStickyContainer>
+</div>
 </template>
 
 <script lang="ts" setup>
 import { computed, watch, provide, useTemplateRef, ref, onMounted, onActivated } from 'vue';
+import { scrollInContainer } from '@@/js/scroll.js';
 import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
 import type { MenuItem } from '@/types/menu.js';
 import type { BasicTimelineType } from '@/timelines.js';
@@ -128,7 +132,7 @@ function queueUpdated(q: number): void {
 }
 
 function top(): void {
-	if (rootEl.value) rootEl.value.scrollToTop();
+	if (rootEl.value) scrollInContainer(rootEl.value, { top: 0, behavior: 'instant' });
 }
 
 async function chooseList(ev: MouseEvent): Promise<void> {

From df3ed93f840cd4accd9e7c3f13c18d4c9bf57894 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 17:15:47 +0900
Subject: [PATCH 19/28] clean up

---
 packages/frontend/src/pages/timeline.vue | 2 --
 1 file changed, 2 deletions(-)

diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index e08fce7bc1..1226ebeab3 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -41,9 +41,7 @@ import type { BasicTimelineType } from '@/timelines.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkPostForm from '@/components/MkPostForm.vue';
-import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/i.js';

From 7c06ffc4226bed945b252eb38e108ff6b85b5e93 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 17:28:20 +0900
Subject: [PATCH 20/28] refactor

---
 .../src/components/MkPullToRefresh.vue        |  9 ++-----
 packages/frontend/src/pages/timeline.vue      | 27 +++++++++----------
 2 files changed, 15 insertions(+), 21 deletions(-)

diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue
index b1df282514..22ae563d13 100644
--- a/packages/frontend/src/components/MkPullToRefresh.vue
+++ b/packages/frontend/src/components/MkPullToRefresh.vue
@@ -16,9 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 		</div>
 	</div>
-	<div :class="{ [$style.slotClip]: isPullStart }">
-		<slot/>
-	</div>
+
+	<slot/>
 </div>
 </template>
 
@@ -261,8 +260,4 @@ defineExpose({
 		margin: 5px 0;
 	}
 }
-
-.slotClip {
-	overflow-y: clip;
-}
 </style>
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 1226ebeab3..fd6c4279fa 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -13,20 +13,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</MkInfo>
 			<MkPostForm v-if="prefer.r.showFixedPostForm.value" :class="$style.postForm" class="_panel" fixed style="margin-bottom: var(--MI-margin);"/>
 			<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
-			<div :class="$style.tl">
-				<MkTimeline
-					ref="tlComponent"
-					:key="src + withRenotes + withReplies + onlyFiles + withSensitive"
-					:src="src.split(':')[0]"
-					:list="src.split(':')[1]"
-					:withRenotes="withRenotes"
-					:withReplies="withReplies"
-					:withSensitive="withSensitive"
-					:onlyFiles="onlyFiles"
-					:sound="true"
-					@queue="queueUpdated"
-				/>
-			</div>
+			<MkTimeline
+				ref="tlComponent"
+				:key="src + withRenotes + withReplies + onlyFiles + withSensitive"
+				:class="$style.tl"
+				:src="src.split(':')[0]"
+				:list="src.split(':')[1]"
+				:withRenotes="withRenotes"
+				:withReplies="withReplies"
+				:withSensitive="withSensitive"
+				:onlyFiles="onlyFiles"
+				:sound="true"
+				@queue="queueUpdated"
+			/>
 		</MkSpacer>
 	</MkStickyContainer>
 </div>

From 2ad7b010e46f757cf42d85a04a414f41997812f6 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 17:57:03 +0900
Subject: [PATCH 21/28] =?UTF-8?q?=F0=9F=8E=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/pages/chat/XMessage.vue | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/packages/frontend/src/pages/chat/XMessage.vue b/packages/frontend/src/pages/chat/XMessage.vue
index 843d2fd79b..ab57020613 100644
--- a/packages/frontend/src/pages/chat/XMessage.vue
+++ b/packages/frontend/src/pages/chat/XMessage.vue
@@ -242,6 +242,10 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
 	font-size: 80%;
 }
 
+.fukidashi {
+	text-align: left;
+}
+
 .content {
 	overflow: clip;
 	overflow-wrap: break-word;

From 25db8c2fa9826b45072ffb5b238b44f018b47a3a Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 17:59:09 +0900
Subject: [PATCH 22/28] =?UTF-8?q?=F0=9F=8E=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/components/global/RouterView.vue | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue
index 1c0c35f34e..78ac6900a3 100644
--- a/packages/frontend/src/components/global/RouterView.vue
+++ b/packages/frontend/src/components/global/RouterView.vue
@@ -44,7 +44,9 @@ provide(DI.routerCurrentDepth, currentDepth + 1);
 
 const rootEl = useTemplateRef('rootEl');
 onMounted(() => {
-	rootEl.value.style.viewTransitionName = viewId; // view-transition-nameにcss varが使えないっぽいため直接代入
+	if (prefer.s.animation) {
+		rootEl.value.style.viewTransitionName = viewId; // view-transition-nameにcss varが使えないっぽいため直接代入
+	}
 });
 
 // view-transition-newなどの<pt-name-selector>にはcss varが使えず、v-bindできないため直接スタイルを生成

From ddd6d72dd7c85c96905fe1a713aab2756495c174 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 17:59:40 +0900
Subject: [PATCH 23/28] New Crowdin updates (#15716)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Italian)
---
 locales/ca-ES.yml |  3 +++
 locales/en-US.yml | 56 +++++++++++++++++++++++++++++++++++++++++++++++
 locales/it-IT.yml |  3 +++
 locales/zh-CN.yml |  3 +++
 locales/zh-TW.yml |  3 +++
 5 files changed, 68 insertions(+)

diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index 6022bc6a8c..b371a6b025 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -1335,6 +1335,7 @@ information: "Informació"
 chat: "Xat"
 migrateOldSettings: "Migració de la configuració antiga "
 migrateOldSettings_description: "Normalment això es fa automàticament, però si la transició no es fa, el procés es pot iniciar manualment. S'esborrarà la configuració actual."
+compress: "Comprimir "
 _chat:
   noMessagesYet: "Encara no tens missatges "
   newMessage: "Missatge nou"
@@ -1363,6 +1364,8 @@ _chat:
   newline: "Línia nova "
   muteThisRoom: "Silenciar aquesta sala"
   deleteRoom: "Esborrar la sala"
+  chatNotAvailableForThisAccountOrServer: "El xat no està disponible per aquest servidor o aquest compte."
+  chatNotAvailableInOtherAccount: "La funció de xat es troba desactivada al compte de l'altre usuari."
   cannotChatWithTheUser: "No pots xatejar amb aquest usuari"
   cannotChatWithTheUser_description: "El xat està desactivat o l'altra part encara no l'ha obert."
   chatWithThisUser: "Xateja amb aquest usuari"
diff --git a/locales/en-US.yml b/locales/en-US.yml
index a314a2e980..68d52c721d 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -301,6 +301,7 @@ uploadFromUrlMayTakeTime: "It may take some time until the upload is complete."
 explore: "Explore"
 messageRead: "Read"
 noMoreHistory: "There is no further history"
+startChat: "Start chat"
 nUsersRead: "read by {n}"
 agreeTo: "I agree to {0}"
 agree: "Agree"
@@ -1331,12 +1332,55 @@ emojiPalette: "Emoji palette"
 postForm: "Posting form"
 textCount: "Character count"
 information: "About"
+chat: "Chat"
+migrateOldSettings: "Migrate old client settings"
+migrateOldSettings_description: "This should be done automatically but if for some reason the migration was not successful, you can trigger the migration process yourself manually. The current configuration information will be overwritten."
+compress: "Compress"
 _chat:
+  noMessagesYet: "No messages yet"
+  newMessage: "New message"
+  individualChat: "Private Chat"
+  individualChat_description: "Have a private chat with another person."
+  roomChat: "Room Chat"
+  roomChat_description: "A chat room which can have multiple people.\nYou can also invite people who don't allow private chats if they accept the invite."
+  createRoom: "Create Room"
+  inviteUserToChat: "Invite users to start chatting"
+  yourRooms: "Created rooms"
+  joiningRooms: "Joined rooms"
   invitations: "Invite"
+  noInvitations: "No invitations"
+  history: "History"
   noHistory: "No history available"
+  noRooms: "No rooms found"
+  inviteUser: "Invite Users"
+  sentInvitations: "Sent Invites"
+  join: "Join"
+  ignore: "Ignore"
+  leave: "Leave room"
   members: "Members"
+  searchMessages: "Search messages"
   home: "Home"
   send: "Send"
+  newline: "New line"
+  muteThisRoom: "Mute room"
+  deleteRoom: "Delete room"
+  chatNotAvailableForThisAccountOrServer: "Chat is not enabled on this server or for this account."
+  chatNotAvailableInOtherAccount: "The chat function is disabled for the other user."
+  cannotChatWithTheUser: "Cannot start a chat with this user"
+  cannotChatWithTheUser_description: "Chat is either unavailable or the other party has not enabled chat."
+  chatWithThisUser: "Chat with user"
+  thisUserAllowsChatOnlyFromFollowers: "This user accepts chats from followers only."
+  thisUserAllowsChatOnlyFromFollowing: "This user accepts chats only from users they follow."
+  thisUserAllowsChatOnlyFromMutualFollowing: "This user only accepts chats from users who are mutual followers."
+  thisUserNotAllowedChatAnyone: "This user is not accepting chats from anyone."
+  chatAllowedUsers: "Who to allow chatting with"
+  chatAllowedUsers_note: "You can chat with anyone to whom you have sent a chat message regardless of this setting."
+  _chatAllowedUsers:
+    everyone: "Everyone"
+    followers: "Only your followers"
+    following: "Only users you are following"
+    mutual: "Mutual followers only"
+    none: "Nobody"
 _emojiPalette:
   palettes: "Palette"
   enableSyncBetweenDevicesForPalettes: "Enable palette sync between devices"
@@ -1362,6 +1406,12 @@ _settings:
   timelineAndNote: "Timeline and note"
   makeEveryTextElementsSelectable: "Make all text elements selectable"
   makeEveryTextElementsSelectable_description: "Enabling this may reduce usability in some situations."
+  showNavbarSubButtons: "Show sub-buttons on the navigation bar"
+  ifOn: "When turned on"
+  ifOff: "When turned off"
+  _chat:
+    showSenderName: "Show sender's name"
+    sendOnEnter: "Press Enter to send"
 _preferencesProfile:
   profileName: "Profile name"
   profileNameDescription: "Set a name that identifies this device."
@@ -1871,6 +1921,7 @@ _role:
     canImportFollowing: "Allow importing following"
     canImportMuting: "Allow importing muting"
     canImportUserLists: "Allow importing lists"
+    canChat: "Allow Chat"
   _condition:
     roleAssignedTo: "Assigned to manual roles"
     isLocal: "Local user"
@@ -2101,6 +2152,7 @@ _sfx:
   noteMy: "Own note"
   notification: "Notifications"
   reaction: "On choosing a reaction"
+  chatMessage: "Chat Messages"
 _soundSettings:
   driveFile: "Use an audio file in Drive."
   driveFileWarn: "Select an audio file from Drive."
@@ -2248,6 +2300,7 @@ _permissions:
   "read:federation": "Get federation data"
   "write:report-abuse": "Report violation"
   "write:chat": "Compose or delete chat messages"
+  "read:chat": "Browse Chat"
 _auth:
   shareAccessTitle: "Granting application permissions"
   shareAccess: "Would you like to authorize \"{name}\" to access this account?"
@@ -2496,6 +2549,7 @@ _notification:
   newNote: "New note"
   unreadAntennaNote: "Antenna {name}"
   roleAssigned: "Role given"
+  chatRoomInvitationReceived: "You have been invited to a chat room"
   emptyPushNotificationMessage: "Push notifications have been updated"
   achievementEarned: "Achievement unlocked"
   testNotification: "Test notification"
@@ -2524,6 +2578,7 @@ _notification:
     receiveFollowRequest: "Received follow requests"
     followRequestAccepted: "Accepted follow requests"
     roleAssigned: "Role given"
+    chatRoomInvitationReceived: "Invited to chat room"
     achievementEarned: "Achievement unlocked"
     exportCompleted: "The export has been completed"
     login: "Sign In"
@@ -2663,6 +2718,7 @@ _moderationLogTypes:
   deletePage: "Page deleted"
   deleteFlash: "Play deleted"
   deleteGalleryPost: "Gallery post deleted"
+  deleteChatRoom: "Deleted Chat Room"
   updateProxyAccountDescription: "Update the description of the proxy account"
 _fileViewer:
   title: "File details"
diff --git a/locales/it-IT.yml b/locales/it-IT.yml
index 5335ea6f0b..3ec8414ded 100644
--- a/locales/it-IT.yml
+++ b/locales/it-IT.yml
@@ -1335,6 +1335,7 @@ information: "Informazioni"
 chat: "Chat"
 migrateOldSettings: "Migrare le vecchie impostazioni"
 migrateOldSettings_description: "Di solito, viene fatto automaticamente. Se per qualche motivo non fossero migrate con successo, è possibile avviare il processo di migrazione manualmente, sovrascrivendo le configurazioni attuali."
+compress: "Comprimi"
 _chat:
   noMessagesYet: "Ancora nessun messaggio"
   newMessage: "Nuovo messaggio"
@@ -1363,6 +1364,8 @@ _chat:
   newline: "Nuova riga"
   muteThisRoom: "Silenzia stanza"
   deleteRoom: "Elimina stanza"
+  chatNotAvailableForThisAccountOrServer: "Questo server, o questo profilo ha disabilitato la chat."
+  chatNotAvailableInOtherAccount: "La chat non è disponibile nel profilo dell'altra persona."
   cannotChatWithTheUser: "Impossibile chattare con questa persona"
   cannotChatWithTheUser_description: "La chat potrebbe non essere disponibile, oppure l'altra persona potrebbe non esserlo."
   chatWithThisUser: "Chatta con questa persona"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index c4da76e80b..3f2fa089da 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -1335,6 +1335,7 @@ information: "关于"
 chat: "聊天"
 migrateOldSettings: "迁移旧设置信息"
 migrateOldSettings_description: "通常设置信息将自动迁移。但如果由于某种原因迁移不成功,则可以手动触发迁移过程。当前的配置信息将被覆盖。"
+compress: "压缩"
 _chat:
   noMessagesYet: "还没有消息"
   newMessage: "新消息"
@@ -1363,6 +1364,8 @@ _chat:
   newline: "换行"
   muteThisRoom: "静音此房间"
   deleteRoom: "删除房间"
+  chatNotAvailableForThisAccountOrServer: "此服务器或者账户还未开启聊天功能。"
+  chatNotAvailableInOtherAccount: "对方账户目前处于无法使用聊天的状态。"
   cannotChatWithTheUser: "无法与此用户聊天"
   cannotChatWithTheUser_description: "可能现在无法使用聊天,或者对方未开启聊天。"
   chatWithThisUser: "聊天"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index a87507bb84..5ff252eb49 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -1335,6 +1335,7 @@ information: "關於"
 chat: "聊天"
 migrateOldSettings: "遷移舊設定資訊"
 migrateOldSettings_description: "通常情況下,這會自動進行,但若因某些原因未能順利遷移,您可以手動觸發遷移處理。請注意,當前的設定資訊將會被覆寫。"
+compress: "壓縮"
 _chat:
   noMessagesYet: "尚無訊息"
   newMessage: "新訊息"
@@ -1363,6 +1364,8 @@ _chat:
   newline: "換行"
   muteThisRoom: "此聊天室已靜音"
   deleteRoom: "刪除聊天室"
+  chatNotAvailableForThisAccountOrServer: "這個伺服器或這個帳號的聊天功能尚未啟用。"
+  chatNotAvailableInOtherAccount: "對方的帳號無法使用聊天功能。"
   cannotChatWithTheUser: "無法與此使用者聊天"
   cannotChatWithTheUser_description: "聊天功能目前無法使用,或對方尚未開放聊天功能。"
   chatWithThisUser: "聊天"

From 05b23eda599b67c454b21a67ff1159d4ae2dee7a Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Sat, 29 Mar 2025 09:01:05 +0000
Subject: [PATCH 24/28] Bump version to 2025.3.2-beta.16

---
 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 60e85b40c2..98ad336712 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2025.3.2-beta.15",
+	"version": "2025.3.2-beta.16",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index a1f46e3dd8..89955e0f01 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2025.3.2-beta.15",
+	"version": "2025.3.2-beta.16",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From fb1542429f1af7f31959c8cd4cfdedbe537d4673 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 18:03:31 +0900
Subject: [PATCH 25/28] =?UTF-8?q?=F0=9F=8E=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/components/global/MkPageHeader.vue | 2 --
 1 file changed, 2 deletions(-)

diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index 6a926f7718..6b524785e4 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -69,7 +69,6 @@ const emit = defineEmits<{
 }>();
 
 const viewId = inject(DI.viewId);
-const viewTransitionName = computed(() => `${viewId}---pageHeader`);
 const injectedPageMetadata = inject(DI.pageMetadata);
 const pageMetadata = computed(() => props.overridePageMetadata ?? injectedPageMetadata.value);
 
@@ -130,7 +129,6 @@ onUnmounted(() => {
 	backdrop-filter: var(--MI-blur, blur(15px));
 	border-bottom: solid 0.5px var(--MI_THEME-divider);
 	width: 100%;
-	view-transition-name: v-bind(viewTransitionName);
 }
 
 .upper,

From 2787158a04c2aa810318d3cf91ccfc3eceb57f4a Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 18:41:52 +0900
Subject: [PATCH 26/28] test

---
 packages/frontend/src/ui/deck/column.vue | 4 ++--
 packages/frontend/src/ui/universal.vue   | 5 ++---
 2 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index 8ecac1dc8e..f60b6b6ae2 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onBeforeUnmount, onMounted, provide, watch, useTemplateRef, ref, computed } from 'vue';
+import { onBeforeUnmount, onMounted, provide, watch, ref, computed, shallowRef } from 'vue';
 import type { Column } from '@/deck.js';
 import type { MenuItem } from '@/types/menu.js';
 import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn } from '@/deck.js';
@@ -68,7 +68,7 @@ const emit = defineEmits<{
 	(ev: 'headerWheel', ctx: WheelEvent): void;
 }>();
 
-const body = useTemplateRef('body');
+const body = shallowRef();
 
 const dragging = ref(false);
 watch(dragging, v => os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd'));
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index aeabcb3213..663ec10888 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -91,11 +91,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, provide, onMounted, computed, ref, watch, useTemplateRef } from 'vue';
+import { defineAsyncComponent, provide, onMounted, computed, ref, watch, shallowRef } from 'vue';
 import { instanceName } from '@@/js/config.js';
 import { isLink } from '@@/js/is-link.js';
 import XCommon from './_common_/common.vue';
-import type { Ref } from 'vue';
 import type { PageMetadata } from '@/page.js';
 import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
 import * as os from '@/os.js';
@@ -130,7 +129,7 @@ window.addEventListener('resize', () => {
 
 const pageMetadata = ref<null | PageMetadata>(null);
 const widgetsShowing = ref(false);
-const navFooter = useTemplateRef('navFooter');
+const navFooter = shallowRef();
 
 provide(DI.router, mainRouter);
 provideMetadataReceiver((metadataGetter) => {

From e0b7c56458ea1c5c034abf1efa26517434c12923 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 19:23:30 +0900
Subject: [PATCH 27/28] Revert "test"

This reverts commit 2787158a04c2aa810318d3cf91ccfc3eceb57f4a.
---
 packages/frontend/src/ui/deck/column.vue | 4 ++--
 packages/frontend/src/ui/universal.vue   | 5 +++--
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index f60b6b6ae2..8ecac1dc8e 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onBeforeUnmount, onMounted, provide, watch, ref, computed, shallowRef } from 'vue';
+import { onBeforeUnmount, onMounted, provide, watch, useTemplateRef, ref, computed } from 'vue';
 import type { Column } from '@/deck.js';
 import type { MenuItem } from '@/types/menu.js';
 import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn } from '@/deck.js';
@@ -68,7 +68,7 @@ const emit = defineEmits<{
 	(ev: 'headerWheel', ctx: WheelEvent): void;
 }>();
 
-const body = shallowRef();
+const body = useTemplateRef('body');
 
 const dragging = ref(false);
 watch(dragging, v => os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd'));
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 663ec10888..aeabcb3213 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -91,10 +91,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, provide, onMounted, computed, ref, watch, shallowRef } from 'vue';
+import { defineAsyncComponent, provide, onMounted, computed, ref, watch, useTemplateRef } from 'vue';
 import { instanceName } from '@@/js/config.js';
 import { isLink } from '@@/js/is-link.js';
 import XCommon from './_common_/common.vue';
+import type { Ref } from 'vue';
 import type { PageMetadata } from '@/page.js';
 import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
 import * as os from '@/os.js';
@@ -129,7 +130,7 @@ window.addEventListener('resize', () => {
 
 const pageMetadata = ref<null | PageMetadata>(null);
 const widgetsShowing = ref(false);
-const navFooter = shallowRef();
+const navFooter = useTemplateRef('navFooter');
 
 provide(DI.router, mainRouter);
 provideMetadataReceiver((metadataGetter) => {

From 1b776a7e7e160b337434006fbf67ac242c539da3 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 29 Mar 2025 20:02:51 +0900
Subject: [PATCH 28/28] perf(frontend): reduce stack contexts

---
 packages/frontend/src/ui/universal.vue | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index aeabcb3213..4719595729 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -214,9 +214,6 @@ html,
 body {
 	width: 100%;
 	height: 100%;
-	position: fixed;
-	top: 0;
-	left: 0;
 	overscroll-behavior: none;
 }