From bd843863d029b72b4a9c0b97ee7296e4e60ad8d6 Mon Sep 17 00:00:00 2001
From: Chocolate Pie <106949016+chocolate-pie@users.noreply.github.com>
Date: Sat, 8 Jul 2023 08:58:35 +0900
Subject: fix:
 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
 (#10973)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正 (misskey-dev/misskey#10922)

* Update CHANGELOG.md

* fix

* Update CHANGELOG.md

* Update CHANGELOG.md
---
 packages/frontend/src/components/MkPageWindow.vue | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

(limited to 'packages/frontend/src/components/MkPageWindow.vue')

diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 709b5a52df..08de5622f5 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -30,7 +30,8 @@ import MkWindow from '@/components/MkWindow.vue';
 import { popout as _popout } from '@/scripts/popout';
 import copyToClipboard from '@/scripts/copy-to-clipboard';
 import { url } from '@/config';
-import { mainRouter, routes } from '@/router';
+import { mainRouter, routes, page } from '@/router';
+import { $i } from '@/account';
 import { Router } from '@/nirax';
 import { i18n } from '@/i18n';
 import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
@@ -45,7 +46,7 @@ defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const router = new Router(routes, props.initialPath);
+const router = new Router(routes, props.initialPath, !!$i, page(() => import('@/pages/not-found.vue')));
 
 let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
 let windowEl = $shallowRef<InstanceType<typeof MkWindow>>();
-- 
cgit v1.2.3-freya


From 15683370f0379360c475a9ad77bb802c43f6d181 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 8 Jul 2023 15:30:36 +0900
Subject: fix(frontend): ページ遷移でスクロール位置が保持されない問題を修正
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Fix #11068
---
 CHANGELOG.md                                      |  9 +++---
 packages/frontend/src/components/MkPageWindow.vue | 10 +++++--
 packages/frontend/src/nirax.ts                    | 36 ++++++++++++++++++-----
 packages/frontend/src/router.ts                   | 31 +------------------
 packages/frontend/src/ui/deck/main-column.vue     | 11 +++++--
 packages/frontend/src/ui/universal.vue            |  3 ++
 6 files changed, 53 insertions(+), 47 deletions(-)

(limited to 'packages/frontend/src/components/MkPageWindow.vue')

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b2e54ea47..905c2ea2de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,15 +19,16 @@
 - サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
 
 ### Client
-- Fix: サーバーメトリクスが90度傾いている
-- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
-- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
-- Fix: ZenUIでポップアップの表示位置がおかしい問題を修正
 - deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
 - ドライブファイルのメニューで画像をクロップできるように
 - 画像を動画と同様に簡単に隠せるように
 - オリジナル画像を保持せずにアップロードする場合webpでアップロードされるように(Safari以外)
 - 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように
+- Fix: サーバーメトリクスが90度傾いている
+- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
+- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
+- Fix: ZenUIでポップアップの表示位置がおかしい問題を修正
+- Fix: ページ遷移でスクロール位置が保持されない問題を修正
 
 ### Server
 - JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 08de5622f5..6318a9fd70 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -17,14 +17,14 @@
 		</template>
 	</template>
 
-	<div :class="$style.root" style="container-type: inline-size;">
+	<div ref="contents" :class="$style.root" style="container-type: inline-size;">
 		<RouterView :key="reloadCount" :router="router"/>
 	</div>
 </MkWindow>
 </template>
 
 <script lang="ts" setup>
-import { ComputedRef, onMounted, onUnmounted, provide } from 'vue';
+import { ComputedRef, onMounted, onUnmounted, provide, shallowRef } from 'vue';
 import RouterView from '@/components/global/RouterView.vue';
 import MkWindow from '@/components/MkWindow.vue';
 import { popout as _popout } from '@/scripts/popout';
@@ -32,11 +32,12 @@ import copyToClipboard from '@/scripts/copy-to-clipboard';
 import { url } from '@/config';
 import { mainRouter, routes, page } from '@/router';
 import { $i } from '@/account';
-import { Router } from '@/nirax';
+import { Router, useScrollPositionManager } from '@/nirax';
 import { i18n } from '@/i18n';
 import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
 import { openingWindowsCount } from '@/os';
 import { claimAchievement } from '@/scripts/achievements';
+import { getScrollContainer } from '@/scripts/scroll';
 
 const props = defineProps<{
 	initialPath: string;
@@ -48,6 +49,7 @@ defineEmits<{
 
 const router = new Router(routes, props.initialPath, !!$i, page(() => import('@/pages/not-found.vue')));
 
+const contents = shallowRef<HTMLElement>();
 let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
 let windowEl = $shallowRef<InstanceType<typeof MkWindow>>();
 const history = $ref<{ path: string; key: any; }[]>([{
@@ -139,6 +141,8 @@ function popout() {
 	windowEl.close();
 }
 
+useScrollPositionManager(() => getScrollContainer(contents.value), router);
+
 onMounted(() => {
 	openingWindowsCount.value++;
 	if (openingWindowsCount.value >= 3) {
diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts
index 3fa6bd55e2..3a03444de2 100644
--- a/packages/frontend/src/nirax.ts
+++ b/packages/frontend/src/nirax.ts
@@ -1,7 +1,7 @@
 // NIRAX --- A lightweight router
 
 import { EventEmitter } from 'eventemitter3';
-import { Component, shallowRef, ShallowRef } from 'vue';
+import { Component, onMounted, shallowRef, ShallowRef } from 'vue';
 import { safeURIDecode } from '@/scripts/safe-uri-decode';
 
 type RouteDef = {
@@ -267,13 +267,33 @@ export class Router extends EventEmitter<{
 		});
 	}
 
-	public replace(path: string, key?: string | null, emitEvent = true) {
+	public replace(path: string, key?: string | null) {
 		this.navigate(path, key);
-		if (emitEvent) {
-			this.emit('replace', {
-				path,
-				key: this.currentKey,
-			});
-		}
 	}
 }
+
+export function useScrollPositionManager(getScrollContainer: () => HTMLElement, router: Router) {
+	const scrollPosStore = new Map<string, number>();
+
+	onMounted(() => {
+		const scrollContainer = getScrollContainer();
+
+		scrollContainer.addEventListener('scroll', () => {
+			scrollPosStore.set(router.getCurrentKey(), scrollContainer.scrollTop);
+		}, { passive: true });
+
+		router.addListener('change', ctx => {
+			const scrollPos = scrollPosStore.get(ctx.key) ?? 0;
+			scrollContainer.scroll({ top: scrollPos, behavior: 'instant' });
+			if (scrollPos !== 0) {
+				window.setTimeout(() => { // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール
+					scrollContainer.scroll({ top: scrollPos, behavior: 'instant' });
+				}, 100);
+			}
+		});
+
+		router.addListener('same', () => {
+			scrollContainer.scroll({ top: 0, behavior: 'smooth' });
+		});
+	});
+}
diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts
index 8540d4320f..873b372e0a 100644
--- a/packages/frontend/src/router.ts
+++ b/packages/frontend/src/router.ts
@@ -509,41 +509,12 @@ export const mainRouter = new Router(routes, location.pathname + location.search
 
 window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href);
 
-// TODO: このファイルでスクロール位置も管理する設計だとdeckに対応できないのでなんとかする
-// スクロール位置取得+スクロール位置設定関数をprovideする感じでも良いかも
-
-const scrollPosStore = new Map<string, number>();
-
-window.setInterval(() => {
-	scrollPosStore.set(window.history.state?.key, window.scrollY);
-}, 1000);
-
 mainRouter.addListener('push', ctx => {
 	window.history.pushState({ key: ctx.key }, '', ctx.path);
-	const scrollPos = scrollPosStore.get(ctx.key) ?? 0;
-	window.scroll({ top: scrollPos, behavior: 'instant' });
-	if (scrollPos !== 0) {
-		window.setTimeout(() => { // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール
-			window.scroll({ top: scrollPos, behavior: 'instant' });
-		}, 100);
-	}
-});
-
-mainRouter.addListener('replace', ctx => {
-	window.history.replaceState({ key: ctx.key }, '', ctx.path);
-});
-
-mainRouter.addListener('same', () => {
-	window.scroll({ top: 0, behavior: 'smooth' });
 });
 
 window.addEventListener('popstate', (event) => {
-	mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key, false);
-	const scrollPos = scrollPosStore.get(event.state?.key) ?? 0;
-	window.scroll({ top: scrollPos, behavior: 'instant' });
-	window.setTimeout(() => { // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール
-		window.scroll({ top: scrollPos, behavior: 'instant' });
-	}, 100);
+	mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
 });
 
 export function useRouter(): Router {
diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue
index 169fac70a2..0413307955 100644
--- a/packages/frontend/src/ui/deck/main-column.vue
+++ b/packages/frontend/src/ui/deck/main-column.vue
@@ -7,24 +7,29 @@
 		</template>
 	</template>
 
-	<RouterView @contextmenu.stop="onContextmenu"/>
+	<div ref="contents">
+		<RouterView @contextmenu.stop="onContextmenu"/>
+	</div>
 </XColumn>
 </template>
 
 <script lang="ts" setup>
-import { ComputedRef, provide } from 'vue';
+import { ComputedRef, provide, shallowRef } from 'vue';
 import XColumn from './column.vue';
 import { deckStore, Column } from '@/ui/deck/deck-store';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
 import { mainRouter } from '@/router';
 import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
+import { useScrollPositionManager } from '@/nirax';
+import { getScrollContainer } from '@/scripts/scroll';
 
 defineProps<{
 	column: Column;
 	isStacked: boolean;
 }>();
 
+const contents = shallowRef<HTMLElement>();
 let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
 
 provide('router', mainRouter);
@@ -61,4 +66,6 @@ function onContextmenu(ev: MouseEvent) {
 		},
 	}], ev);
 }
+
+useScrollPositionManager(() => getScrollContainer(contents.value), mainRouter);
 </script>
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 8abb20300f..9ae43c39d3 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -95,6 +95,7 @@ import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
 import { deviceKind } from '@/scripts/device-kind';
 import { miLocalStorage } from '@/local-storage';
 import { CURRENT_STICKY_BOTTOM } from '@/const';
+import { useScrollPositionManager } from '@/nirax';
 
 const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
 const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
@@ -213,6 +214,8 @@ watch($$(navFooter), () => {
 }, {
 	immediate: true,
 });
+
+useScrollPositionManager(() => contents.value.rootEl, mainRouter);
 </script>
 
 <style>
-- 
cgit v1.2.3-freya


From 2b6dbd4fcbec380d65b0c318932d9eeb3fcb3f7b Mon Sep 17 00:00:00 2001
From: okayurisotto <okayurisotto@proton.me>
Date: Fri, 14 Jul 2023 10:45:01 +0900
Subject: refactor: 可読性のため一部で`Array.prototype.at`を使うように (#11274)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* refactor: `Array.prototype.at`を使うように

* fixup! refactor: `Array.prototype.at`を使うように
---
 packages/backend/src/core/chart/core.ts                           | 2 +-
 packages/backend/src/misc/prelude/array.ts                        | 5 +++--
 .../src/queue/processors/CleanRemoteFilesProcessorService.ts      | 6 +++---
 .../backend/src/queue/processors/DeleteAccountProcessorService.ts | 4 ++--
 .../src/queue/processors/DeleteDriveFilesProcessorService.ts      | 6 +++---
 .../src/queue/processors/ExportBlockingProcessorService.ts        | 6 +++---
 .../src/queue/processors/ExportFavoritesProcessorService.ts       | 2 +-
 .../src/queue/processors/ExportFollowingProcessorService.ts       | 2 +-
 .../backend/src/queue/processors/ExportMutingProcessorService.ts  | 6 +++---
 .../backend/src/queue/processors/ExportNotesProcessorService.ts   | 2 +-
 packages/backend/src/server/ActivityPubServerService.ts           | 6 +++---
 packages/backend/test/utils.ts                                    | 8 ++++----
 packages/frontend/src/components/MkDrive.vue                      | 4 ++--
 packages/frontend/src/components/MkMiniChart.vue                  | 4 ++--
 packages/frontend/src/components/MkPageWindow.vue                 | 2 +-
 packages/frontend/src/components/MkPagination.vue                 | 4 ++--
 packages/frontend/src/scripts/array.ts                            | 5 +++--
 packages/frontend/src/widgets/server-metric/cpu-mem.vue           | 8 ++++----
 packages/frontend/src/widgets/server-metric/net.vue               | 8 ++++----
 19 files changed, 46 insertions(+), 44 deletions(-)

(limited to 'packages/frontend/src/components/MkPageWindow.vue')

diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts
index 5717024351..7a89233eda 100644
--- a/packages/backend/src/core/chart/core.ts
+++ b/packages/backend/src/core/chart/core.ts
@@ -627,7 +627,7 @@ export default abstract class Chart<T extends Schema> {
 			}
 
 		// 要求された範囲の最も古い箇所に位置するログが存在しなかったら
-		} else if (!isTimeSame(new Date(logs[logs.length - 1].date * 1000), gt)) {
+		} else if (!isTimeSame(new Date(logs.at(-1)!.date * 1000), gt)) {
 			// 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する
 			// (隙間埋めできないため)
 			const outdatedLog = await repository.findOne({
diff --git a/packages/backend/src/misc/prelude/array.ts b/packages/backend/src/misc/prelude/array.ts
index 0b2830cb7b..2524eacfb3 100644
--- a/packages/backend/src/misc/prelude/array.ts
+++ b/packages/backend/src/misc/prelude/array.ts
@@ -67,8 +67,9 @@ export function maximum(xs: number[]): number {
 export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
 	const groups = [] as T[][];
 	for (const x of xs) {
-		if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) {
-			groups[groups.length - 1].push(x);
+		const lastGroup = groups.at(-1);
+		if (lastGroup !== undefined && f(lastGroup[0], x)) {
+			lastGroup.push(x);
 		} else {
 			groups.push([x]);
 		}
diff --git a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts
index c54bf59ae4..6f887089eb 100644
--- a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts
+++ b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts
@@ -1,7 +1,7 @@
 import { Inject, Injectable } from '@nestjs/common';
 import { IsNull, MoreThan, Not } from 'typeorm';
 import { DI } from '@/di-symbols.js';
-import type { DriveFilesRepository } from '@/models/index.js';
+import type { DriveFile, DriveFilesRepository } from '@/models/index.js';
 import type { Config } from '@/config.js';
 import type Logger from '@/logger.js';
 import { DriveService } from '@/core/DriveService.js';
@@ -31,7 +31,7 @@ export class CleanRemoteFilesProcessorService {
 		this.logger.info('Deleting cached remote files...');
 
 		let deletedCount = 0;
-		let cursor: any = null;
+		let cursor: DriveFile['id'] | null = null;
 
 		while (true) {
 			const files = await this.driveFilesRepository.find({
@@ -51,7 +51,7 @@ export class CleanRemoteFilesProcessorService {
 				break;
 			}
 
-			cursor = files[files.length - 1].id;
+			cursor = files.at(-1)?.id ?? null;
 
 			await Promise.all(files.map(file => this.driveService.deleteFileSync(file, true)));
 
diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts
index 65ded170b7..b2886563f4 100644
--- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts
@@ -70,7 +70,7 @@ export class DeleteAccountProcessorService {
 					break;
 				}
 
-				cursor = notes[notes.length - 1].id;
+				cursor = notes.at(-1)?.id ?? null;
 
 				await this.notesRepository.delete(notes.map(note => note.id));
 
@@ -101,7 +101,7 @@ export class DeleteAccountProcessorService {
 					break;
 				}
 
-				cursor = files[files.length - 1].id;
+				cursor = files.at(-1)?.id ?? null;
 
 				for (const file of files) {
 					await this.driveService.deleteFileSync(file);
diff --git a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts
index 6772c5dc76..07e3762330 100644
--- a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts
@@ -1,7 +1,7 @@
 import { Inject, Injectable } from '@nestjs/common';
 import { MoreThan } from 'typeorm';
 import { DI } from '@/di-symbols.js';
-import type { UsersRepository, DriveFilesRepository } from '@/models/index.js';
+import type { UsersRepository, DriveFilesRepository, DriveFile } from '@/models/index.js';
 import type { Config } from '@/config.js';
 import type Logger from '@/logger.js';
 import { DriveService } from '@/core/DriveService.js';
@@ -40,7 +40,7 @@ export class DeleteDriveFilesProcessorService {
 		}
 
 		let deletedCount = 0;
-		let cursor: any = null;
+		let cursor: DriveFile['id'] | null = null;
 
 		while (true) {
 			const files = await this.driveFilesRepository.find({
@@ -59,7 +59,7 @@ export class DeleteDriveFilesProcessorService {
 				break;
 			}
 
-			cursor = files[files.length - 1].id;
+			cursor = files.at(-1)?.id ?? null;
 
 			for (const file of files) {
 				await this.driveService.deleteFileSync(file);
diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
index eb758e162d..d100c6d09f 100644
--- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
@@ -3,7 +3,7 @@ import { Inject, Injectable } from '@nestjs/common';
 import { MoreThan } from 'typeorm';
 import { format as dateFormat } from 'date-fns';
 import { DI } from '@/di-symbols.js';
-import type { UsersRepository, BlockingsRepository } from '@/models/index.js';
+import type { UsersRepository, BlockingsRepository, Blocking } from '@/models/index.js';
 import type { Config } from '@/config.js';
 import type Logger from '@/logger.js';
 import { DriveService } from '@/core/DriveService.js';
@@ -53,7 +53,7 @@ export class ExportBlockingProcessorService {
 			const stream = fs.createWriteStream(path, { flags: 'a' });
 
 			let exportedCount = 0;
-			let cursor: any = null;
+			let cursor: Blocking['id'] | null = null;
 
 			while (true) {
 				const blockings = await this.blockingsRepository.find({
@@ -72,7 +72,7 @@ export class ExportBlockingProcessorService {
 					break;
 				}
 
-				cursor = blockings[blockings.length - 1].id;
+				cursor = blockings.at(-1)?.id ?? null;
 
 				for (const block of blockings) {
 					const u = await this.usersRepository.findOneBy({ id: block.blockeeId });
diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
index 76c38a6b86..2be42b1a7a 100644
--- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
@@ -94,7 +94,7 @@ export class ExportFavoritesProcessorService {
 					break;
 				}
 
-				cursor = favorites[favorites.length - 1].id;
+				cursor = favorites.at(-1)?.id ?? null;
 
 				for (const favorite of favorites) {
 					let poll: Poll | undefined;
diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
index 8726cb1402..d54e5e0b34 100644
--- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
@@ -79,7 +79,7 @@ export class ExportFollowingProcessorService {
 					break;
 				}
 
-				cursor = followings[followings.length - 1].id;
+				cursor = followings.at(-1)?.id ?? null;
 
 				for (const following of followings) {
 					const u = await this.usersRepository.findOneBy({ id: following.followeeId });
diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
index 0f11a9e843..030e38931e 100644
--- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
@@ -3,7 +3,7 @@ import { Inject, Injectable } from '@nestjs/common';
 import { IsNull, MoreThan } from 'typeorm';
 import { format as dateFormat } from 'date-fns';
 import { DI } from '@/di-symbols.js';
-import type { MutingsRepository, UsersRepository, BlockingsRepository } from '@/models/index.js';
+import type { MutingsRepository, UsersRepository, BlockingsRepository, Muting } from '@/models/index.js';
 import type { Config } from '@/config.js';
 import type Logger from '@/logger.js';
 import { DriveService } from '@/core/DriveService.js';
@@ -56,7 +56,7 @@ export class ExportMutingProcessorService {
 			const stream = fs.createWriteStream(path, { flags: 'a' });
 
 			let exportedCount = 0;
-			let cursor: any = null;
+			let cursor: Muting['id'] | null = null;
 
 			while (true) {
 				const mutes = await this.mutingsRepository.find({
@@ -76,7 +76,7 @@ export class ExportMutingProcessorService {
 					break;
 				}
 
-				cursor = mutes[mutes.length - 1].id;
+				cursor = mutes.at(-1)?.id ?? null;
 
 				for (const mute of mutes) {
 					const u = await this.usersRepository.findOneBy({ id: mute.muteeId });
diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
index 24fb331883..75f32ffee3 100644
--- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
@@ -90,7 +90,7 @@ export class ExportNotesProcessorService {
 					break;
 				}
 
-				cursor = notes[notes.length - 1].id;
+				cursor = notes.at(-1)?.id ?? null;
 
 				for (const note of notes) {
 					let poll: Poll | undefined;
diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts
index f751709345..634f5f0a4e 100644
--- a/packages/backend/src/server/ActivityPubServerService.ts
+++ b/packages/backend/src/server/ActivityPubServerService.ts
@@ -181,7 +181,7 @@ export class ActivityPubServerService {
 				undefined,
 				inStock ? `${partOf}?${url.query({
 					page: 'true',
-					cursor: followings[followings.length - 1].id,
+					cursor: followings.at(-1)!.id,
 				})}` : undefined,
 			);
 
@@ -273,7 +273,7 @@ export class ActivityPubServerService {
 				undefined,
 				inStock ? `${partOf}?${url.query({
 					page: 'true',
-					cursor: followings[followings.length - 1].id,
+					cursor: followings.at(-1)!.id,
 				})}` : undefined,
 			);
 
@@ -398,7 +398,7 @@ export class ActivityPubServerService {
 				})}` : undefined,
 				notes.length ? `${partOf}?${url.query({
 					page: 'true',
-					until_id: notes[notes.length - 1].id,
+					until_id: notes.at(-1)!.id,
 				})}` : undefined,
 			);
 
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index 48947072e3..31ea3e5ab8 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -447,12 +447,12 @@ export async function testPaginationConsistency<Entity extends { id: string, cre
 	for (const limit of [1, 5, 10, 100, undefined]) {
 		// 1. sinceId/DateとuntilId/Dateで両端を指定して取得した結果が期待通りになっていること
 		if (ordering === 'desc') {
-			const end = expected[expected.length - 1];
+			const end = expected.at(-1)!;
 			let last = await fetchEntities(rangeToParam({ limit, since: end }));
 			const actual: Entity[] = [];
 			while (last.length !== 0) {
 				actual.push(...last);
-				last = await fetchEntities(rangeToParam({ limit, until: last[last.length - 1], since: end }));
+				last = await fetchEntities(rangeToParam({ limit, until: last.at(-1), since: end }));
 			}
 			actual.push(end);
 			assert.deepStrictEqual(
@@ -467,7 +467,7 @@ export async function testPaginationConsistency<Entity extends { id: string, cre
 			const actual: Entity[] = [];
 			while (last.length !== 0) {
 				actual.push(...last);
-				last = await fetchEntities(rangeToParam({ limit, since: last[last.length - 1] }));
+				last = await fetchEntities(rangeToParam({ limit, since: last.at(-1) }));
 			}
 			assert.deepStrictEqual(
 				actual.map(({ id, createdAt }) => id + ':' + createdAt),
@@ -480,7 +480,7 @@ export async function testPaginationConsistency<Entity extends { id: string, cre
 			const actual: Entity[] = [];
 			while (last.length !== 0) {
 				actual.push(...last);
-				last = await fetchEntities(rangeToParam({ limit, until: last[last.length - 1] }));
+				last = await fetchEntities(rangeToParam({ limit, until: last.at(-1) }));
 			}
 			assert.deepStrictEqual(
 				actual.map(({ id, createdAt }) => id + ':' + createdAt),
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index 201a6ccdc8..aff227da40 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -568,7 +568,7 @@ function fetchMoreFolders() {
 	os.api('drive/folders', {
 		folderId: folder.value ? folder.value.id : null,
 		type: props.type,
-		untilId: folders.value[folders.value.length - 1].id,
+		untilId: folders.value.at(-1)?.id,
 		limit: max + 1,
 	}).then(folders => {
 		if (folders.length === max + 1) {
@@ -591,7 +591,7 @@ function fetchMoreFiles() {
 	os.api('drive/files', {
 		folderId: folder.value ? folder.value.id : null,
 		type: props.type,
-		untilId: files.value[files.value.length - 1].id,
+		untilId: files.value.at(-1)?.id,
 		limit: max + 1,
 	}).then(files => {
 		if (files.length === max + 1) {
diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue
index 89050e10f0..e884455709 100644
--- a/packages/frontend/src/components/MkMiniChart.vue
+++ b/packages/frontend/src/components/MkMiniChart.vue
@@ -59,8 +59,8 @@ function draw(): void {
 
 	polygonPoints = `0,${ viewBoxY } ${ polylinePoints } ${ viewBoxX },${ viewBoxY }`;
 
-	headX = _polylinePoints[_polylinePoints.length - 1][0];
-	headY = _polylinePoints[_polylinePoints.length - 1][1];
+	headX = _polylinePoints.at(-1)![0];
+	headY = _polylinePoints.at(-1)![1];
 }
 
 watch(() => props.src, draw, { immediate: true });
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 6318a9fd70..6e35ad4241 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -120,7 +120,7 @@ const contextmenu = $computed(() => ([{
 
 function back() {
 	history.pop();
-	router.replace(history[history.length - 1].path, history[history.length - 1].key);
+	router.replace(history.at(-1)!.path, history.at(-1)!.key);
 }
 
 function reload() {
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index 661b04c365..b9a75f6002 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -233,7 +233,7 @@ const fetchMore = async (): Promise<void> => {
 		...(props.pagination.offsetMode ? {
 			offset: offset.value,
 		} : {
-			untilId: Array.from(items.value.keys())[items.value.size - 1],
+			untilId: Array.from(items.value.keys()).at(-1),
 		}),
 	}).then(res => {
 		for (let i = 0; i < res.length; i++) {
@@ -297,7 +297,7 @@ const fetchMoreAhead = async (): Promise<void> => {
 		...(props.pagination.offsetMode ? {
 			offset: offset.value,
 		} : {
-			sinceId: Array.from(items.value.keys())[items.value.size - 1],
+			sinceId: Array.from(items.value.keys()).at(-1),
 		}),
 	}).then(res => {
 		if (res.length === 0) {
diff --git a/packages/frontend/src/scripts/array.ts b/packages/frontend/src/scripts/array.ts
index 4620c8b735..c9a146e707 100644
--- a/packages/frontend/src/scripts/array.ts
+++ b/packages/frontend/src/scripts/array.ts
@@ -78,8 +78,9 @@ export function maximum(xs: number[]): number {
 export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
 	const groups = [] as T[][];
 	for (const x of xs) {
-		if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) {
-			groups[groups.length - 1].push(x);
+		const lastGroup = groups.at(-1);
+		if (lastGroup !== undefined && f(lastGroup[0], x)) {
+			lastGroup.push(x);
 		} else {
 			groups.push([x]);
 		}
diff --git a/packages/frontend/src/widgets/server-metric/cpu-mem.vue b/packages/frontend/src/widgets/server-metric/cpu-mem.vue
index c178ba5171..b9ba400b4d 100644
--- a/packages/frontend/src/widgets/server-metric/cpu-mem.vue
+++ b/packages/frontend/src/widgets/server-metric/cpu-mem.vue
@@ -121,10 +121,10 @@ function onStats(connStats) {
 	cpuPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${cpuPolylinePoints} ${viewBoxX},${viewBoxY}`;
 	memPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${memPolylinePoints} ${viewBoxX},${viewBoxY}`;
 
-	cpuHeadX = cpuPolylinePointsStats[cpuPolylinePointsStats.length - 1][0];
-	cpuHeadY = cpuPolylinePointsStats[cpuPolylinePointsStats.length - 1][1];
-	memHeadX = memPolylinePointsStats[memPolylinePointsStats.length - 1][0];
-	memHeadY = memPolylinePointsStats[memPolylinePointsStats.length - 1][1];
+	cpuHeadX = cpuPolylinePointsStats.at(-1)![0];
+	cpuHeadY = cpuPolylinePointsStats.at(-1)![1];
+	memHeadX = memPolylinePointsStats.at(-1)![0];
+	memHeadY = memPolylinePointsStats.at(-1)![1];
 
 	cpuP = (connStats.cpu * 100).toFixed(0);
 	memP = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0);
diff --git a/packages/frontend/src/widgets/server-metric/net.vue b/packages/frontend/src/widgets/server-metric/net.vue
index 5a9134078d..817a422e63 100644
--- a/packages/frontend/src/widgets/server-metric/net.vue
+++ b/packages/frontend/src/widgets/server-metric/net.vue
@@ -94,10 +94,10 @@ function onStats(connStats) {
 	inPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${inPolylinePoints} ${viewBoxX},${viewBoxY}`;
 	outPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${outPolylinePoints} ${viewBoxX},${viewBoxY}`;
 
-	inHeadX = inPolylinePointsStats[inPolylinePointsStats.length - 1][0];
-	inHeadY = inPolylinePointsStats[inPolylinePointsStats.length - 1][1];
-	outHeadX = outPolylinePointsStats[outPolylinePointsStats.length - 1][0];
-	outHeadY = outPolylinePointsStats[outPolylinePointsStats.length - 1][1];
+	inHeadX = inPolylinePointsStats.at(-1)![0];
+	inHeadY = inPolylinePointsStats.at(-1)![1];
+	outHeadX = outPolylinePointsStats.at(-1)![0];
+	outHeadY = outPolylinePointsStats.at(-1)![1];
 
 	inRecent = connStats.net.rx;
 	outRecent = connStats.net.tx;
-- 
cgit v1.2.3-freya

