From f1deb89e348eb8f1a39b51e33a0ae33d59529feb Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sun, 29 Jun 2025 15:11:25 +0900
Subject: refactor(frontend): improve pagination implementation

---
 .../src/components/MkStreamingNotesTimeline.vue    | 199 ++++++++++-----------
 1 file changed, 95 insertions(+), 104 deletions(-)

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

diff --git a/packages/frontend/src/components/MkStreamingNotesTimeline.vue b/packages/frontend/src/components/MkStreamingNotesTimeline.vue
index 7e72840b7b..69602f4c1c 100644
--- a/packages/frontend/src/components/MkStreamingNotesTimeline.vue
+++ b/packages/frontend/src/components/MkStreamingNotesTimeline.vue
@@ -56,14 +56,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, watch, onUnmounted, provide, useTemplateRef, TransitionGroup, onMounted, shallowRef, ref } from 'vue';
+import { computed, watch, onUnmounted, provide, useTemplateRef, TransitionGroup, onMounted, shallowRef, ref, markRaw } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useInterval } from '@@/js/use-interval.js';
 import { getScrollContainer, scrollToTop } from '@@/js/scroll.js';
 import type { BasicTimelineType } from '@/timelines.js';
-import type { PagingCtx } from '@/composables/use-pagination.js';
 import type { SoundStore } from '@/preferences/def.js';
-import { usePagination } from '@/composables/use-pagination.js';
 import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
 import { useStream } from '@/stream.js';
 import * as sound from '@/utility/sound.js';
@@ -76,6 +74,7 @@ import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
 import { globalEvents, useGlobalEvent } from '@/events.js';
 import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js';
+import { Paginator } from '@/utility/paginator.js';
 
 const props = withDefaults(defineProps<{
 	src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
@@ -102,6 +101,97 @@ provide('inTimeline', true);
 provide('tl_withSensitive', computed(() => props.withSensitive));
 provide('inChannel', computed(() => props.src === 'channel'));
 
+let paginator: Paginator;
+
+if (props.src === 'antenna') {
+	paginator = markRaw(new Paginator('antennas/notes', {
+		computedParams: computed(() => ({
+			antennaId: props.antenna,
+		})),
+		useShallowRef: true,
+	}));
+} else if (props.src === 'home') {
+	paginator = markRaw(new Paginator('notes/timeline', {
+		computedParams: computed(() => ({
+			withRenotes: props.withRenotes,
+			withFiles: props.onlyFiles ? true : undefined,
+		})),
+		useShallowRef: true,
+	}));
+} else if (props.src === 'local') {
+	paginator = markRaw(new Paginator('notes/local-timeline', {
+		computedParams: computed(() => ({
+			withRenotes: props.withRenotes,
+			withReplies: props.withReplies,
+			withFiles: props.onlyFiles ? true : undefined,
+		})),
+		useShallowRef: true,
+	}));
+} else if (props.src === 'social') {
+	paginator = markRaw(new Paginator('notes/hybrid-timeline', {
+		computedParams: computed(() => ({
+			withRenotes: props.withRenotes,
+			withReplies: props.withReplies,
+			withFiles: props.onlyFiles ? true : undefined,
+		})),
+		useShallowRef: true,
+	}));
+} else if (props.src === 'global') {
+	paginator = markRaw(new Paginator('notes/global-timeline', {
+		computedParams: computed(() => ({
+			withRenotes: props.withRenotes,
+			withFiles: props.onlyFiles ? true : undefined,
+		})),
+		useShallowRef: true,
+	}));
+} else if (props.src === 'mentions') {
+	paginator = markRaw(new Paginator('notes/mentions', {
+		useShallowRef: true,
+	}));
+} else if (props.src === 'directs') {
+	paginator = markRaw(new Paginator('notes/mentions', {
+		params: {
+			visibility: 'specified',
+		},
+		useShallowRef: true,
+	}));
+} else if (props.src === 'list') {
+	paginator = markRaw(new Paginator('notes/user-list-timeline', {
+		computedParams: computed(() => ({
+			withRenotes: props.withRenotes,
+			withFiles: props.onlyFiles ? true : undefined,
+			listId: props.list,
+		})),
+		useShallowRef: true,
+	}));
+} else if (props.src === 'channel') {
+	paginator = markRaw(new Paginator('channels/timeline', {
+		computedParams: computed(() => ({
+			channelId: props.channel,
+		})),
+		useShallowRef: true,
+	}));
+} else if (props.src === 'role') {
+	paginator = markRaw(new Paginator('roles/notes', {
+		computedParams: computed(() => ({
+			roleId: props.role,
+		})),
+		useShallowRef: true,
+	}));
+} else {
+	throw new Error('Unrecognized timeline type: ' + props.src);
+}
+
+onMounted(() => {
+	paginator.init();
+
+	if (paginator.computedParams) {
+		watch(paginator.computedParams, () => {
+			paginator.reload();
+		}, { immediate: false, deep: true });
+	}
+});
+
 function isTop() {
 	if (scrollContainer == null) return true;
 	if (rootEl.value == null) return true;
@@ -133,17 +223,6 @@ onUnmounted(() => {
 	}
 });
 
-type TimelineQueryType = {
-	antennaId?: string,
-	withRenotes?: boolean,
-	withReplies?: boolean,
-	withFiles?: boolean,
-	visibility?: string,
-	listId?: string,
-	channelId?: string,
-	roleId?: string
-};
-
 let adInsertionCounter = 0;
 
 const MIN_POLLING_INTERVAL = 1000 * 10;
@@ -204,7 +283,6 @@ function prepend(note: Misskey.entities.Note) {
 
 let connection: Misskey.ChannelConnection | null = null;
 let connection2: Misskey.ChannelConnection | null = null;
-let paginationQuery: PagingCtx;
 
 const stream = store.s.realtimeMode ? useStream() : null;
 
@@ -274,100 +352,13 @@ function disconnectChannel() {
 	if (connection2) connection2.dispose();
 }
 
-function updatePaginationQuery() {
-	let endpoint: keyof Misskey.Endpoints | null;
-	let query: TimelineQueryType | null;
-
-	if (props.src === 'antenna') {
-		endpoint = 'antennas/notes';
-		query = {
-			antennaId: props.antenna,
-		};
-	} else if (props.src === 'home') {
-		endpoint = 'notes/timeline';
-		query = {
-			withRenotes: props.withRenotes,
-			withFiles: props.onlyFiles ? true : undefined,
-		};
-	} else if (props.src === 'local') {
-		endpoint = 'notes/local-timeline';
-		query = {
-			withRenotes: props.withRenotes,
-			withReplies: props.withReplies,
-			withFiles: props.onlyFiles ? true : undefined,
-		};
-	} else if (props.src === 'social') {
-		endpoint = 'notes/hybrid-timeline';
-		query = {
-			withRenotes: props.withRenotes,
-			withReplies: props.withReplies,
-			withFiles: props.onlyFiles ? true : undefined,
-		};
-	} else if (props.src === 'global') {
-		endpoint = 'notes/global-timeline';
-		query = {
-			withRenotes: props.withRenotes,
-			withFiles: props.onlyFiles ? true : undefined,
-		};
-	} else if (props.src === 'mentions') {
-		endpoint = 'notes/mentions';
-		query = null;
-	} else if (props.src === 'directs') {
-		endpoint = 'notes/mentions';
-		query = {
-			visibility: 'specified',
-		};
-	} else if (props.src === 'list') {
-		endpoint = 'notes/user-list-timeline';
-		query = {
-			withRenotes: props.withRenotes,
-			withFiles: props.onlyFiles ? true : undefined,
-			listId: props.list,
-		};
-	} else if (props.src === 'channel') {
-		endpoint = 'channels/timeline';
-		query = {
-			channelId: props.channel,
-		};
-	} else if (props.src === 'role') {
-		endpoint = 'roles/notes';
-		query = {
-			roleId: props.role,
-		};
-	} else {
-		throw new Error('Unrecognized timeline type: ' + props.src);
-	}
-
-	paginationQuery = {
-		endpoint: endpoint,
-		limit: 10,
-		params: query,
-	};
-}
-
-function refreshEndpointAndChannel() {
+watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], () => {
 	if (store.s.realtimeMode) {
 		disconnectChannel();
 		connectChannel();
 	}
-
-	updatePaginationQuery();
-}
-
-// デッキのリストカラムでwithRenotesを変更した場合に自動的に更新されるようにさせる
-// IDが切り替わったら切り替え先のTLを表示させたい
-watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel);
-
-// withSensitiveはクライアントで完結する処理のため、単にリロードするだけでOK
-watch(() => props.withSensitive, reloadTimeline);
-
-// 初回表示用
-refreshEndpointAndChannel();
-
-const paginator = usePagination({
-	ctx: paginationQuery,
-	useShallowRef: true,
 });
+watch(() => props.withSensitive, reloadTimeline);
 
 onUnmounted(() => {
 	disconnectChannel();
-- 
cgit v1.2.3-freya

