summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components
diff options
context:
space:
mode:
authorsyuilo <4439005+syuilo@users.noreply.github.com>2025-06-29 15:11:25 +0900
committersyuilo <4439005+syuilo@users.noreply.github.com>2025-06-29 15:11:25 +0900
commitf1deb89e348eb8f1a39b51e33a0ae33d59529feb (patch)
tree2e92a7a21a1bf377719e1b125a9ac44bc14a529e /packages/frontend/src/components
parentfeat(backend): クリップ内でノートを検索できるように (diff)
downloadmisskey-f1deb89e348eb8f1a39b51e33a0ae33d59529feb.tar.gz
misskey-f1deb89e348eb8f1a39b51e33a0ae33d59529feb.tar.bz2
misskey-f1deb89e348eb8f1a39b51e33a0ae33d59529feb.zip
refactor(frontend): improve pagination implementation
Diffstat (limited to 'packages/frontend/src/components')
-rw-r--r--packages/frontend/src/components/MkChannelList.vue6
-rw-r--r--packages/frontend/src/components/MkDrive.vue46
-rw-r--r--packages/frontend/src/components/MkFileListForAdmin.vue6
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue17
-rw-r--r--packages/frontend/src/components/MkNoteDraftsDialog.vue15
-rw-r--r--packages/frontend/src/components/MkNotesTimeline.vue24
-rw-r--r--packages/frontend/src/components/MkPagination.vue60
-rw-r--r--packages/frontend/src/components/MkPaginationControl.vue35
-rw-r--r--packages/frontend/src/components/MkStreamingNotesTimeline.vue199
-rw-r--r--packages/frontend/src/components/MkStreamingNotificationsTimeline.vue38
-rw-r--r--packages/frontend/src/components/MkUserList.vue6
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Follow.vue17
12 files changed, 219 insertions, 250 deletions
diff --git a/packages/frontend/src/components/MkChannelList.vue b/packages/frontend/src/components/MkChannelList.vue
index 0968452ca7..7f82e531ae 100644
--- a/packages/frontend/src/components/MkChannelList.vue
+++ b/packages/frontend/src/components/MkChannelList.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkPagination :pagination="pagination">
+<MkPagination :paginator="paginator">
<template #empty><MkResult type="empty"/></template>
<template #default="{ items }">
@@ -14,13 +14,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import type { PagingCtx } from '@/composables/use-pagination.js';
+import type { Paginator } from '@/utility/paginator.js';
import MkChannelPreview from '@/components/MkChannelPreview.vue';
import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{
- pagination: PagingCtx;
+ paginator: Paginator;
noGap?: boolean;
extractor?: (item: any) => any;
}>(), {
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index 9d89c2f846..9fe1c7ef21 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -130,7 +130,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, useTemplateRef, watch, computed, TransitionGroup } from 'vue';
+import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, useTemplateRef, watch, computed, TransitionGroup, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from './MkButton.vue';
import type { MenuItem } from '@/types/menu.js';
@@ -146,10 +146,10 @@ import { prefer } from '@/preferences.js';
import { chooseFileFromPcAndUpload, selectDriveFolder } from '@/utility/drive.js';
import { store } from '@/store.js';
import { isSeparatorNeeded, getSeparatorInfo, makeDateGroupedTimelineComputedRef } from '@/utility/timeline-date-separate.js';
-import { usePagination } from '@/composables/use-pagination.js';
import { globalEvents, useGlobalEvent } from '@/events.js';
import { checkDragDataType, getDragData, setDragData } from '@/drag-and-drop.js';
import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
+import { Paginator } from '@/utility/paginator.js';
const props = withDefaults(defineProps<{
initialFolder?: Misskey.entities.DriveFolder['id'] | null;
@@ -195,33 +195,23 @@ const fetching = ref(true);
const sortModeSelect = ref<NonNullable<Misskey.entities.DriveFilesRequest['sort']>>('+createdAt');
-const filesPaginator = usePagination({
- ctx: {
- endpoint: 'drive/files',
- limit: 30,
- canFetchDetection: 'limit',
- params: computed(() => ({
- folderId: folder.value ? folder.value.id : null,
- type: props.type,
- sort: sortModeSelect.value,
- })),
- },
- autoInit: false,
- autoReInit: false,
-});
+const filesPaginator = markRaw(new Paginator('drive/files', {
+ limit: 30,
+ canFetchDetection: 'limit',
+ params: () => ({ // 自動でリロードしたくないためcomputedParamsは使わない
+ folderId: folder.value ? folder.value.id : null,
+ type: props.type,
+ sort: sortModeSelect.value,
+ }),
+}));
-const foldersPaginator = usePagination({
- ctx: {
- endpoint: 'drive/folders',
- limit: 30,
- canFetchDetection: 'limit',
- params: computed(() => ({
- folderId: folder.value ? folder.value.id : null,
- })),
- },
- autoInit: false,
- autoReInit: false,
-});
+const foldersPaginator = markRaw(new Paginator('drive/folders', {
+ limit: 30,
+ canFetchDetection: 'limit',
+ params: () => ({ // 自動でリロードしたくないためcomputedParamsは使わない
+ folderId: folder.value ? folder.value.id : null,
+ }),
+}));
const filesTimeline = makeDateGroupedTimelineComputedRef(filesPaginator.items, 'month');
diff --git a/packages/frontend/src/components/MkFileListForAdmin.vue b/packages/frontend/src/components/MkFileListForAdmin.vue
index 3b495c2807..a998c810f0 100644
--- a/packages/frontend/src/components/MkFileListForAdmin.vue
+++ b/packages/frontend/src/components/MkFileListForAdmin.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div>
- <MkPagination v-slot="{ items }" :pagination="pagination">
+ <MkPagination v-slot="{ items }" :paginator="paginator">
<div :class="[$style.fileList, { [$style.grid]: viewMode === 'grid', [$style.list]: viewMode === 'list', '_gaps_s': viewMode === 'list' }]">
<MkA
v-for="file in items"
@@ -40,15 +40,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
+import type { Paginator } from '@/utility/paginator.js';
import MkPagination from '@/components/MkPagination.vue';
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import bytes from '@/filters/bytes.js';
import { i18n } from '@/i18n.js';
import { dateString } from '@/filters/date.js';
-import type { PagingCtx } from '@/composables/use-pagination.js';
defineProps<{
- pagination: PagingCtx<'admin/drive/files'>;
+ paginator: Paginator<'admin/drive/files'>;
viewMode: 'grid' | 'list';
}>();
</script>
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index cc26b0d0dc..fb37bb1ae6 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -187,7 +187,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true"/>
</div>
<div v-else-if="tab === 'renotes'" :class="$style.tab_renotes">
- <MkPagination :pagination="renotesPagination" :disableAutoLoad="true">
+ <MkPagination :paginator="renotesPaginator">
<template #default="{ items }">
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); grid-gap: 12px;">
<MkA v-for="item in items" :key="item.id" :to="userPage(item.user)">
@@ -204,7 +204,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span style="margin-left: 4px;">{{ $appearNote.reactions[reaction] }}</span>
</button>
</div>
- <MkPagination v-if="reactionTabType" :key="reactionTabType" :pagination="reactionsPagination" :disableAutoLoad="true">
+ <MkPagination v-if="reactionTabType" :key="reactionTabType" :paginator="reactionsPaginator">
<template #default="{ items }">
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); grid-gap: 12px;">
<MkA v-for="item in items" :key="item.id" :to="userPage(item.user)">
@@ -228,7 +228,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, inject, onMounted, provide, ref, useTemplateRef } from 'vue';
+import { computed, inject, markRaw, onMounted, provide, ref, useTemplateRef } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js';
@@ -274,6 +274,7 @@ import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';
import { DI } from '@/di.js';
import { globalEvents, useGlobalEvent } from '@/events.js';
+import { Paginator } from '@/utility/paginator.js';
const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
@@ -376,21 +377,19 @@ provide(DI.mfmEmojiReactCallback, (reaction) => {
const tab = ref(props.initialTab);
const reactionTabType = ref<string | null>(null);
-const renotesPagination = computed(() => ({
- endpoint: 'notes/renotes',
+const renotesPaginator = markRaw(new Paginator('notes/renotes', {
limit: 10,
params: {
noteId: appearNote.id,
},
}));
-const reactionsPagination = computed(() => ({
- endpoint: 'notes/reactions',
+const reactionsPaginator = markRaw(new Paginator('notes/reactions', {
limit: 10,
- params: {
+ computedParams: computed(() => ({
noteId: appearNote.id,
type: reactionTabType.value,
- },
+ })),
}));
useTooltip(renoteButton, async (showing) => {
diff --git a/packages/frontend/src/components/MkNoteDraftsDialog.vue b/packages/frontend/src/components/MkNoteDraftsDialog.vue
index eb103a8423..7d41740264 100644
--- a/packages/frontend/src/components/MkNoteDraftsDialog.vue
+++ b/packages/frontend/src/components/MkNoteDraftsDialog.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.drafts }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }})
</template>
<div class="_spacer">
- <MkPagination ref="pagingEl" :pagination="paging" withControl>
+ <MkPagination :paginator="paginator" withControl>
<template #empty>
<MkResult type="empty" :text="i18n.ts._drafts.noDrafts"/>
</template>
@@ -100,9 +100,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref, shallowRef, useTemplateRef } from 'vue';
+import { ref, shallowRef, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
-import type { PagingCtx } from '@/composables/use-pagination.js';
import MkButton from '@/components/MkButton.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
@@ -111,6 +110,7 @@ import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { $i } from '@/i.js';
import { misskeyApi } from '@/utility/misskey-api';
+import { Paginator } from '@/utility/paginator.js';
const emit = defineEmits<{
(ev: 'restore', draft: Misskey.entities.NoteDraft): void;
@@ -118,12 +118,9 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
-const paging = {
- endpoint: 'notes/drafts/list',
+const paginator = markRaw(new Paginator('notes/drafts/list', {
limit: 10,
-} satisfies PagingCtx;
-
-const pagingComponent = useTemplateRef('pagingEl');
+}));
const currentDraftsCount = ref(0);
misskeyApi('notes/drafts/count').then((count) => {
@@ -151,7 +148,7 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) {
if (canceled) return;
os.apiWithDialog('notes/drafts/delete', { draftId: draft.id }).then(() => {
- pagingComponent.value?.paginator.reload();
+ paginator.reload();
});
}
</script>
diff --git a/packages/frontend/src/components/MkNotesTimeline.vue b/packages/frontend/src/components/MkNotesTimeline.vue
index 401cef62bb..1ae97fd0c0 100644
--- a/packages/frontend/src/components/MkNotesTimeline.vue
+++ b/packages/frontend/src/components/MkNotesTimeline.vue
@@ -4,17 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkPagination ref="pagingComponent" :pagination="pagination" :disableAutoLoad="disableAutoLoad" :pullToRefresh="pullToRefresh" :withControl="withControl">
+<MkPagination :paginator="paginator" :autoLoad="autoLoad" :pullToRefresh="pullToRefresh" :withControl="withControl">
<template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
<template #default="{ items: notes }">
<div :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap }]">
<template v-for="(note, i) in notes" :key="note.id">
- <div v-if="i > 0 && isSeparatorNeeded(pagingComponent.paginator.items.value[i -1].createdAt, note.createdAt)" :data-scroll-anchor="note.id">
+ <div v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i -1].createdAt, note.createdAt)" :data-scroll-anchor="note.id">
<div :class="$style.date">
- <span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(pagingComponent.paginator.items.value[i -1].createdAt, note.createdAt).prevText }}</span>
+ <span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt).prevText }}</span>
<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
- <span>{{ getSeparatorInfo(pagingComponent.paginator.items.value[i -1].createdAt, note.createdAt).nextText }} <i class="ti ti-chevron-down"></i></span>
+ <span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt).nextText }} <i class="ti ti-chevron-down"></i></span>
</div>
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
</div>
@@ -31,9 +31,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkPagination>
</template>
-<script lang="ts" setup generic="T extends PagingCtx">
-import { useTemplateRef } from 'vue';
-import type { PagingCtx } from '@/composables/use-pagination.js';
+<script lang="ts" setup generic="T extends Paginator">
+import type { Paginator } from '@/utility/paginator.js';
import MkNote from '@/components/MkNote.vue';
import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
@@ -41,24 +40,23 @@ import { globalEvents, useGlobalEvent } from '@/events.js';
import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js';
const props = withDefaults(defineProps<{
- pagination: T;
+ paginator: T;
noGap?: boolean;
- disableAutoLoad?: boolean;
+ autoLoad?: boolean;
pullToRefresh?: boolean;
withControl?: boolean;
}>(), {
+ autoLoad: true,
pullToRefresh: true,
withControl: true,
});
-const pagingComponent = useTemplateRef('pagingComponent');
-
useGlobalEvent('noteDeleted', (noteId) => {
- pagingComponent.value?.paginator.removeItem(noteId);
+ props.paginator.removeItem(noteId);
});
function reload() {
- return pagingComponent.value?.paginator.reload();
+ return props.paginator.reload();
}
defineExpose({
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index 679e8deaf5..fb9cd6e1f0 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<component :is="prefer.s.enablePullToRefresh && pullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => paginator.reload()" @contextmenu.prevent.stop="onContextmenu">
<div>
- <MkPaginationControl v-if="props.withControl" v-model:order="order" v-model:date="date" style="margin-bottom: 10px" @reload="paginator.reload()"/>
+ <MkPaginationControl v-if="props.withControl" :paginator="paginator" style="margin-bottom: 10px"/>
<!-- :css="prefer.s.animation" にしたいけどバグる(おそらくvueのバグ) https://github.com/misskey-dev/misskey/issues/16078 -->
<Transition
@@ -26,14 +26,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else key="_root_" class="_gaps">
<slot :items="paginator.items.value" :fetching="paginator.fetching.value || paginator.fetchingOlder.value"></slot>
- <div v-if="order === 'oldest'">
- <MkButton v-if="!paginator.fetchingNewer.value" :class="$style.more" :wait="paginator.fetchingNewer.value" primary rounded @click="paginator.fetchNewer">
+ <div v-if="paginator.order.value === 'oldest'">
+ <MkButton v-if="!paginator.fetchingNewer.value" :class="$style.more" :wait="paginator.fetchingNewer.value" primary rounded @click="paginator.fetchNewer()">
{{ i18n.ts.loadMore }}
</MkButton>
<MkLoading v-else/>
</div>
<div v-else v-show="paginator.canFetchOlder.value">
- <MkButton v-if="!paginator.fetchingOlder.value" :class="$style.more" :wait="paginator.fetchingOlder.value" primary rounded @click="paginator.fetchOlder">
+ <MkButton v-if="!paginator.fetchingOlder.value" :class="$style.more" :wait="paginator.fetchingOlder.value" primary rounded @click="paginator.fetchOlder()">
{{ i18n.ts.loadMore }}
</MkButton>
<MkLoading v-else/>
@@ -44,49 +44,29 @@ SPDX-License-Identifier: AGPL-3.0-only
</component>
</template>
-<script lang="ts" setup generic="T extends PagingCtx">
+<script lang="ts" setup generic="T extends Paginator, I = UnwrapRef<T['items']>">
import { isLink } from '@@/js/is-link.js';
-import { ref, watch } from 'vue';
+import { onMounted, watch } from 'vue';
import type { UnwrapRef } from 'vue';
-import type { PagingCtx } from '@/composables/use-pagination.js';
+import type { Paginator } from '@/utility/paginator.js';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
-import { usePagination } from '@/composables/use-pagination.js';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import MkPaginationControl from '@/components/MkPaginationControl.vue';
import * as os from '@/os.js';
-type Paginator = ReturnType<typeof usePagination<T['endpoint']>>;
-
const props = withDefaults(defineProps<{
- pagination: T;
- disableAutoLoad?: boolean;
- displayLimit?: number;
+ paginator: T;
+ autoLoad?: boolean;
pullToRefresh?: boolean;
withControl?: boolean;
}>(), {
- displayLimit: 20,
+ autoLoad: true,
pullToRefresh: true,
withControl: false,
});
-const order = ref<'newest' | 'oldest'>(props.pagination.order ?? 'newest');
-const date = ref<number | null>(null);
-
-const paginator: Paginator = usePagination({
- ctx: props.pagination,
-});
-
-watch([order, date], () => {
- paginator.updateCtx({
- ...props.pagination,
- order: order.value,
- initialDirection: order.value === 'oldest' ? 'newer' : 'older',
- initialDate: date.value,
- });
-}, { immediate: false });
-
function onContextmenu(ev: MouseEvent) {
if (ev.target && isLink(ev.target as HTMLElement)) return;
if (window.getSelection()?.toString() !== '') return;
@@ -96,19 +76,27 @@ function onContextmenu(ev: MouseEvent) {
icon: 'ti ti-refresh',
text: i18n.ts.reload,
action: () => {
- paginator.reload();
+ props.paginator.reload();
},
}], ev);
}
+if (props.autoLoad) {
+ onMounted(() => {
+ props.paginator.init();
+ });
+}
+
+if (props.paginator.computedParams) {
+ watch(props.paginator.computedParams, () => {
+ props.paginator.reload();
+ }, { immediate: false, deep: true });
+}
+
defineSlots<{
empty: () => void;
- default: (props: { items: UnwrapRef<Paginator['items']> }) => void;
+ default: (props: { items: I }) => void;
}>();
-
-defineExpose({
- paginator: paginator,
-});
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/components/MkPaginationControl.vue b/packages/frontend/src/components/MkPaginationControl.vue
index 8eab987d2a..91630eca35 100644
--- a/packages/frontend/src/components/MkPaginationControl.vue
+++ b/packages/frontend/src/components/MkPaginationControl.vue
@@ -9,10 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSelect v-model="order" :class="$style.order" :items="[{ label: i18n.ts._order.newest, value: 'newest' }, { label: i18n.ts._order.oldest, value: 'oldest' }]">
<template #prefix><i class="ti ti-arrows-sort"></i></template>
</MkSelect>
- <MkButton v-if="canSearch" v-tooltip="i18n.ts.search" iconOnly transparent rounded :active="searchOpened" @click="searchOpened = !searchOpened"><i class="ti ti-search"></i></MkButton>
+ <MkButton v-if="paginator.canSearch" v-tooltip="i18n.ts.search" iconOnly transparent rounded :active="searchOpened" @click="searchOpened = !searchOpened"><i class="ti ti-search"></i></MkButton>
<MkButton v-if="canFilter" v-tooltip="i18n.ts.filter" iconOnly transparent rounded :active="filterOpened" @click="filterOpened = !filterOpened"><i class="ti ti-filter"></i></MkButton>
<MkButton v-tooltip="i18n.ts.dateAndTime" iconOnly transparent rounded :active="date != null" @click="date = date == null ? Date.now() : null"><i class="ti ti-calendar-clock"></i></MkButton>
- <MkButton v-tooltip="i18n.ts.reload" iconOnly transparent rounded @click="emit('reload')"><i class="ti ti-refresh"></i></MkButton>
+ <MkButton v-tooltip="i18n.ts.reload" iconOnly transparent rounded @click="paginator.reload()"><i class="ti ti-refresh"></i></MkButton>
</div>
<MkInput
@@ -37,9 +37,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
-<script lang="ts" setup generic="T extends PagingCtx">
+<script lang="ts" setup generic="T extends Paginator">
import { ref, watch } from 'vue';
-import type { PagingCtx } from '@/composables/use-pagination.js';
+import type { Paginator } from '@/utility/paginator.js';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import MkSelect from '@/components/MkSelect.vue';
@@ -47,32 +47,35 @@ import MkInput from '@/components/MkInput.vue';
import { formatDateTimeString } from '@/utility/format-time-string.js';
const props = withDefaults(defineProps<{
- canSearch?: boolean;
+ paginator: T;
canFilter?: boolean;
filterOpened?: boolean;
}>(), {
- canSearch: false,
canFilter: false,
filterOpened: false,
});
-const emit = defineEmits<{
- (ev: 'reload'): void;
-}>();
-
const searchOpened = ref(false);
const filterOpened = ref(props.filterOpened);
-const order = defineModel<'newest' | 'oldest'>('order', {
- default: 'newest',
+const order = ref<'newest' | 'oldest'>('newest');
+const date = ref<number | null>(null);
+const q = ref<string | null>(null);
+
+watch(order, () => {
+ props.paginator.order.value = order.value;
+ props.paginator.initialDirection = order.value === 'oldest' ? 'newer' : 'older';
+ props.paginator.reload();
});
-const date = defineModel<number | null>('date', {
- default: null,
+watch(date, () => {
+ props.paginator.initialDate = date.value;
+ props.paginator.reload();
});
-const q = defineModel<string | null>('q', {
- default: null,
+watch(q, () => {
+ props.paginator.searchQuery.value = q.value;
+ props.paginator.reload();
});
</script>
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();
diff --git a/packages/frontend/src/components/MkStreamingNotificationsTimeline.vue b/packages/frontend/src/components/MkStreamingNotificationsTimeline.vue
index b12effd0d1..04b230277c 100644
--- a/packages/frontend/src/components/MkStreamingNotificationsTimeline.vue
+++ b/packages/frontend/src/components/MkStreamingNotificationsTimeline.vue
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup } from 'vue';
+import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup, markRaw, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js';
import type { notificationTypes } from '@@/js/const.js';
@@ -53,8 +53,8 @@ import { i18n } from '@/i18n.js';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import { prefer } from '@/preferences.js';
import { store } from '@/store.js';
-import { usePagination } from '@/composables/use-pagination.js';
import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js';
+import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{
excludeTypes?: typeof notificationTypes[number][];
@@ -62,21 +62,17 @@ const props = defineProps<{
const rootEl = useTemplateRef('rootEl');
-const paginator = usePagination({
- ctx: prefer.s.useGroupedNotifications ? {
- endpoint: 'i/notifications-grouped' as const,
- limit: 20,
- params: computed(() => ({
- excludeTypes: props.excludeTypes ?? undefined,
- })),
- } : {
- endpoint: 'i/notifications' as const,
- limit: 20,
- params: computed(() => ({
- excludeTypes: props.excludeTypes ?? undefined,
- })),
- },
-});
+const paginator = prefer.s.useGroupedNotifications ? markRaw(new Paginator('i/notifications-grouped', {
+ limit: 20,
+ computedParams: computed(() => ({
+ excludeTypes: props.excludeTypes ?? undefined,
+ })),
+})) : markRaw(new Paginator('i/notifications', {
+ limit: 20,
+ computedParams: computed(() => ({
+ excludeTypes: props.excludeTypes ?? undefined,
+ })),
+}));
const MIN_POLLING_INTERVAL = 1000 * 10;
const POLLING_INTERVAL =
@@ -116,6 +112,14 @@ function reload() {
let connection: Misskey.ChannelConnection<Misskey.Channels['main']> | null = null;
onMounted(() => {
+ paginator.init();
+
+ if (paginator.computedParams) {
+ watch(paginator.computedParams, () => {
+ paginator.reload();
+ }, { immediate: false, deep: true });
+ }
+
if (store.s.realtimeMode) {
connection = useStream().useChannel('main');
connection.on('notification', onNotification);
diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue
index 1d4cdfd5cb..761e1cdd04 100644
--- a/packages/frontend/src/components/MkUserList.vue
+++ b/packages/frontend/src/components/MkUserList.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkPagination :pagination="pagination">
+<MkPagination :paginator="paginator">
<template #empty><MkResult type="empty" :text="i18n.ts.noUsers"/></template>
<template #default="{ items }">
@@ -16,13 +16,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import type { PagingCtx } from '@/composables/use-pagination.js';
+import type { Paginator } from '@/utility/paginator.js';
import MkUserInfo from '@/components/MkUserInfo.vue';
import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{
- pagination: PagingCtx;
+ paginator: Paginator;
noGap?: boolean;
extractor?: (item: any) => any;
}>(), {
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
index 1441d69a6a..02171a123d 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts.recommended }}</template>
- <MkPagination :pagination="pinnedUsers">
+ <MkPagination :paginator="pinnedUsersPaginator">
<template #default="{ items }">
<div :class="$style.users">
<XUser v-for="item in (items as Misskey.entities.UserDetailed[])" :key="item.id" :user="item"/>
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts.popularUsers }}</template>
- <MkPagination :pagination="popularUsers">
+ <MkPagination :paginator="popularUsersPaginator">
<template #default="{ items }">
<div :class="$style.users">
<XUser v-for="item in (items as Misskey.entities.UserDetailed[])" :key="item.id" :user="item"/>
@@ -35,20 +35,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
+import { markRaw } from 'vue';
import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
import XUser from '@/components/MkUserSetupDialog.User.vue';
import MkPagination from '@/components/MkPagination.vue';
-import type { PagingCtx } from '@/composables/use-pagination.js';
+import { Paginator } from '@/utility/paginator.js';
-const pinnedUsers: PagingCtx = {
- endpoint: 'pinned-users',
+const pinnedUsersPaginator = markRaw(new Paginator('pinned-users', {
noPaging: true,
limit: 10,
-};
+}));
-const popularUsers: PagingCtx = {
- endpoint: 'users',
+const popularUsersPaginator = markRaw(new Paginator('users', {
limit: 10,
noPaging: true,
params: {
@@ -56,7 +55,7 @@ const popularUsers: PagingCtx = {
origin: 'local',
sort: '+follower',
},
-};
+}));
</script>
<style lang="scss" module>