diff options
Diffstat (limited to 'packages/frontend/src')
67 files changed, 1063 insertions, 1137 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> diff --git a/packages/frontend/src/composables/use-pagination.ts b/packages/frontend/src/composables/use-pagination.ts deleted file mode 100644 index b3d8d36431..0000000000 --- a/packages/frontend/src/composables/use-pagination.ts +++ /dev/null @@ -1,316 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { computed, isRef, onMounted, ref, shallowRef, triggerRef, watch } from 'vue'; -import * as Misskey from 'misskey-js'; -import type { ComputedRef, DeepReadonly, Ref, ShallowRef } from 'vue'; -import { misskeyApi } from '@/utility/misskey-api.js'; - -const MAX_ITEMS = 30; -const MAX_QUEUE_ITEMS = 100; -const FIRST_FETCH_LIMIT = 15; -const SECOND_FETCH_LIMIT = 30; - -export type MisskeyEntity = { - id: string; - createdAt: string; - _shouldInsertAd_?: boolean; - [x: string]: any; -}; - -export type PagingCtx<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints> = { - endpoint: E; - limit?: number; - params?: Misskey.Endpoints[E]['req'] | ComputedRef<Misskey.Endpoints[E]['req']>; - - /** - * 検索APIのような、ページング不可なエンドポイントを利用する場合 - * (そのようなAPIをこの関数で使うのは若干矛盾してるけど) - */ - noPaging?: boolean; - - offsetMode?: boolean; - - initialId?: MisskeyEntity['id']; - initialDate?: number | null; - initialDirection?: 'newer' | 'older'; - - // 配列内の要素をどのような順序で並べるか - // newest: 新しいものが先頭 (default) - // oldest: 古いものが先頭 - // NOTE: このようなプロパティを用意してこっち側で並びを管理せずに、Setで持っておき参照者側が好きに並び変えるような設計の方がすっきりしそうなものの、Vueのレンダリングのたびに並び替え処理が発生することになったりしそうでパフォーマンス上の懸念がある - order?: 'newest' | 'oldest'; - - // 一部のAPIはさらに遡れる場合でもパフォーマンス上の理由でlimit以下の結果を返す場合があり、その場合はsafe、それ以外はlimitにすることを推奨 - canFetchDetection?: 'safe' | 'limit'; -}; - -export function usePagination<Endpoint extends keyof Misskey.Endpoints, T extends { id: string; } = (Misskey.Endpoints[Endpoint]['res'] extends (infer I)[] ? I extends { id: string } ? I : { id: string } : { id: string })>(props: { - ctx: PagingCtx<Endpoint>; - autoInit?: boolean; - autoReInit?: boolean; - useShallowRef?: boolean; -}) { - const items = props.useShallowRef ? shallowRef<T[]>([]) : ref<T[]>([]); - let aheadQueue: T[] = []; - const queuedAheadItemsCount = ref(0); - const fetching = ref(true); - const fetchingOlder = ref(false); - const fetchingNewer = ref(false); - const canFetchOlder = ref(false); - const error = ref(false); - - if (props.autoReInit !== false) { - watch(() => [props.ctx.endpoint, props.ctx.params], init, { deep: true }); - } - - function getNewestId(): string | null | undefined { - // 様々な要因により並び順は保証されないのでソートが必要 - if (aheadQueue.length > 0) { - return aheadQueue.map(x => x.id).sort().at(-1); - } - return items.value.map(x => x.id).sort().at(-1); - } - - function getOldestId(): string | null | undefined { - // 様々な要因により並び順は保証されないのでソートが必要 - return items.value.map(x => x.id).sort().at(0); - } - - async function init(): Promise<void> { - items.value = []; - aheadQueue = []; - queuedAheadItemsCount.value = 0; - fetching.value = true; - const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {}; - - await misskeyApi<T[]>(props.ctx.endpoint, { - ...params, - limit: props.ctx.limit ?? FIRST_FETCH_LIMIT, - allowPartial: true, - ...((props.ctx.initialId == null && props.ctx.initialDate == null) && props.ctx.initialDirection === 'newer' ? { - sinceId: '0', - } : props.ctx.initialDirection === 'newer' ? { - sinceId: props.ctx.initialId, - sinceDate: props.ctx.initialDate, - } : (props.ctx.initialId || props.ctx.initialDate) && props.ctx.initialDirection === 'older' ? { - untilId: props.ctx.initialId, - untilDate: props.ctx.initialDate, - } : {}), - }).then(res => { - // 逆順で返ってくるので - if ((props.ctx.initialId || props.ctx.initialDate) && props.ctx.initialDirection === 'newer') { - res.reverse(); - } - - for (let i = 0; i < res.length; i++) { - const item = res[i]; - if (i === 3) item._shouldInsertAd_ = true; - } - - pushItems(res); - - if (props.ctx.canFetchDetection === 'limit') { - if (res.length < FIRST_FETCH_LIMIT) { - canFetchOlder.value = false; - } else { - canFetchOlder.value = true; - } - } else if (props.ctx.canFetchDetection === 'safe' || props.ctx.canFetchDetection == null) { - if (res.length === 0 || props.ctx.noPaging) { - canFetchOlder.value = false; - } else { - canFetchOlder.value = true; - } - } - - error.value = false; - fetching.value = false; - }, err => { - error.value = true; - fetching.value = false; - }); - } - - function reload(): Promise<void> { - return init(); - } - - async function fetchOlder(): Promise<void> { - if (!canFetchOlder.value || fetching.value || fetchingOlder.value || items.value.length === 0) return; - fetchingOlder.value = true; - const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {}; - await misskeyApi<T[]>(props.ctx.endpoint, { - ...params, - limit: SECOND_FETCH_LIMIT, - ...(props.ctx.offsetMode ? { - offset: items.value.length, - } : { - untilId: getOldestId(), - }), - }).then(res => { - for (let i = 0; i < res.length; i++) { - const item = res[i]; - if (i === 10) item._shouldInsertAd_ = true; - } - - pushItems(res); - - if (props.ctx.canFetchDetection === 'limit') { - if (res.length < FIRST_FETCH_LIMIT) { - canFetchOlder.value = false; - } else { - canFetchOlder.value = true; - } - } else if (props.ctx.canFetchDetection === 'safe' || props.ctx.canFetchDetection == null) { - if (res.length === 0) { - canFetchOlder.value = false; - } else { - canFetchOlder.value = true; - } - } - }).finally(() => { - fetchingOlder.value = false; - }); - } - - async function fetchNewer(options: { - toQueue?: boolean; - } = {}): Promise<void> { - fetchingNewer.value = true; - const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {}; - await misskeyApi<T[]>(props.ctx.endpoint, { - ...params, - limit: SECOND_FETCH_LIMIT, - ...(props.ctx.offsetMode ? { - offset: items.value.length, - } : { - sinceId: getNewestId(), - }), - }).then(res => { - if (res.length === 0) return; // これやらないと余計なre-renderが走る - - if (options.toQueue) { - aheadQueue.unshift(...res.toReversed()); - if (aheadQueue.length > MAX_QUEUE_ITEMS) { - aheadQueue = aheadQueue.slice(0, MAX_QUEUE_ITEMS); - } - queuedAheadItemsCount.value = aheadQueue.length; - } else { - if (props.ctx.order === 'oldest') { - pushItems(res); - } else { - unshiftItems(res.toReversed()); - } - } - }).finally(() => { - fetchingNewer.value = false; - }); - } - - function trim(trigger = true) { - if (items.value.length >= MAX_ITEMS) canFetchOlder.value = true; - items.value = items.value.slice(0, MAX_ITEMS); - if (props.useShallowRef && trigger) triggerRef(items); - } - - function unshiftItems(newItems: T[]) { - if (newItems.length === 0) return; // これやらないと余計なre-renderが走る - items.value.unshift(...newItems.filter(x => !items.value.some(y => y.id === x.id))); // ストリーミングやポーリングのタイミングによっては重複することがあるため - trim(false); - if (props.useShallowRef) triggerRef(items); - } - - function pushItems(oldItems: T[]) { - if (oldItems.length === 0) return; // これやらないと余計なre-renderが走る - items.value.push(...oldItems); - if (props.useShallowRef) triggerRef(items); - } - - function prepend(item: T) { - if (items.value.some(x => x.id === item.id)) return; - items.value.unshift(item); - trim(false); - if (props.useShallowRef) triggerRef(items); - } - - function enqueue(item: T) { - aheadQueue.unshift(item); - if (aheadQueue.length > MAX_QUEUE_ITEMS) { - aheadQueue.pop(); - } - queuedAheadItemsCount.value = aheadQueue.length; - } - - function releaseQueue() { - if (aheadQueue.length === 0) return; // これやらないと余計なre-renderが走る - unshiftItems(aheadQueue); - aheadQueue = []; - queuedAheadItemsCount.value = 0; - } - - function removeItem(id: string) { - // TODO: queueからも消す - - const index = items.value.findIndex(x => x.id === id); - if (index !== -1) { - items.value.splice(index, 1); - if (props.useShallowRef) triggerRef(items); - } - } - - function updateItem(id: string, updator: (item: T) => T) { - // TODO: queueのも更新 - - const index = items.value.findIndex(x => x.id === id); - if (index !== -1) { - const item = items.value[index]!; - items.value[index] = updator(item); - if (props.useShallowRef) triggerRef(items); - } - } - - function updateCtx(ctx: PagingCtx<Endpoint>) { - props.ctx = ctx; - reload(); - } - - function updateCtxPartial(ctx: Partial<PagingCtx<Endpoint>>) { - props.ctx = { - ...props.ctx, - ...ctx, - }; - reload(); - } - - if (props.autoInit !== false) { - onMounted(() => { - init(); - }); - } - - return { - items: items as DeepReadonly<ShallowRef<T[]>>, - queuedAheadItemsCount, - fetching, - fetchingOlder, - fetchingNewer, - canFetchOlder, - init, - reload, - fetchOlder, - fetchNewer, - unshiftItems, - prepend, - trim, - removeItem, - updateItem, - enqueue, - releaseQueue, - error, - updateCtx, - updateCtxPartial, - }; -} diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue index 7e5abb4b34..47ec675d57 100644 --- a/packages/frontend/src/pages/about.federation.vue +++ b/packages/frontend/src/pages/about.federation.vue @@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only </FormSplit> </div> - <MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination"> + <MkPagination v-slot="{items}" ref="instances" :key="host + state" :paginator="paginator"> <div :class="$style.items"> <MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" :class="$style.item" :to="`/instance-info/${instance.host}`"> <MkInstanceCardMini :instance="instance"/> @@ -51,24 +51,22 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, ref } from 'vue'; +import { computed, markRaw, ref } from 'vue'; import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkPagination from '@/components/MkPagination.vue'; -import type { PagingCtx } from '@/composables/use-pagination.js'; import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue'; import FormSplit from '@/components/form/split.vue'; import { i18n } from '@/i18n.js'; +import { Paginator } from '@/utility/paginator.js'; const host = ref(''); const state = ref('federating'); const sort = ref('+pubSub'); -const pagination = { - endpoint: 'federation/instances' as const, +const paginator = markRaw(new Paginator('federation/instances', { limit: 10, - displayLimit: 50, offsetMode: true, - params: computed(() => ({ + computedParams: computed(() => ({ sort: sort.value, host: host.value !== '' ? host.value : null, ...( @@ -81,7 +79,7 @@ const pagination = { state.value === 'notResponding' ? { notResponding: true } : {}), })), -} as PagingCtx; +})); function getStatus(instance) { if (instance.isSuspended) return 'Suspended'; diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index b0862ccaa4..a194b9a94f 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -160,7 +160,7 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="archived">{{ i18n.ts.archived }}</option> </MkSelect> - <MkPagination :pagination="announcementsPagination"> + <MkPagination :paginator="announcementsPaginator"> <template #default="{ items }"> <div class="_gaps_s"> <div v-for="announcement in items" :key="announcement.id" v-panel :class="$style.announcementItem" @click="editAnnouncement(announcement)"> @@ -179,7 +179,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-else-if="tab === 'drive'" class="_gaps"> - <MkFileListForAdmin :pagination="filesPagination" viewMode="grid"/> + <MkFileListForAdmin :paginator="filesPaginator" viewMode="grid"/> </div> <div v-else-if="tab === 'chart'" class="_gaps_m"> @@ -211,7 +211,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, watch, ref } from 'vue'; +import { computed, defineAsyncComponent, watch, ref, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import { url } from '@@/js/config.js'; import MkChart from '@/components/MkChart.vue'; @@ -235,6 +235,7 @@ import { i18n } from '@/i18n.js'; import { iAmAdmin, $i, iAmModerator } from '@/i.js'; import MkRolePreview from '@/components/MkRolePreview.vue'; import MkPagination from '@/components/MkPagination.vue'; +import { Paginator } from '@/utility/paginator.js'; const props = withDefaults(defineProps<{ userId: string; @@ -255,24 +256,22 @@ const silenced = ref(false); const suspended = ref(false); const isSystem = ref(false); const moderationNote = ref(''); -const filesPagination = { - endpoint: 'admin/drive/files' as const, +const filesPaginator = markRaw(new Paginator('admin/drive/files', { limit: 10, - params: computed(() => ({ + computedParams: computed(() => ({ userId: props.userId, })), -}; +})); const announcementsStatus = ref<'active' | 'archived'>('active'); -const announcementsPagination = { - endpoint: 'admin/announcements/list' as const, +const announcementsPaginator = markRaw(new Paginator('admin/announcements/list', { limit: 10, - params: computed(() => ({ + computedParams: computed(() => ({ userId: props.userId, status: announcementsStatus.value, })), -}; +})); const expandedRoles = ref([]); function createFetcher() { diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue index 4dbb573ceb..ab462229a7 100644 --- a/packages/frontend/src/pages/admin/abuses.vue +++ b/packages/frontend/src/pages/admin/abuses.vue @@ -41,13 +41,13 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" :spellcheck="false"> <span>{{ i18n.ts.username }}</span> </MkInput> - <MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params().origin === 'local'"> + <MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" :spellcheck="false" :disabled="paginator.computedParams.value.origin === 'local'"> <span>{{ i18n.ts.host }}</span> </MkInput> </div> --> - <MkPagination v-slot="{items}" ref="reports" :pagination="pagination"> + <MkPagination v-slot="{items}" :paginator="paginator"> <div class="_gaps"> <XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/> </div> @@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, useTemplateRef, ref } from 'vue'; +import { computed, ref, markRaw } from 'vue'; import MkSelect from '@/components/MkSelect.vue'; import MkPagination from '@/components/MkPagination.vue'; import XAbuseReport from '@/components/MkAbuseReport.vue'; @@ -66,8 +66,7 @@ import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import MkButton from '@/components/MkButton.vue'; import { store } from '@/store.js'; - -const reports = useTemplateRef('reports'); +import { Paginator } from '@/utility/paginator.js'; const state = ref('unresolved'); const reporterOrigin = ref('combined'); @@ -75,18 +74,17 @@ const targetUserOrigin = ref('combined'); const searchUsername = ref(''); const searchHost = ref(''); -const pagination = { - endpoint: 'admin/abuse-user-reports' as const, +const paginator = markRaw(new Paginator('admin/abuse-user-reports', { limit: 10, - params: computed(() => ({ + computedParams: computed(() => ({ state: state.value, reporterOrigin: reporterOrigin.value, targetUserOrigin: targetUserOrigin.value, })), -}; +})); function resolved(reportId) { - reports.value?.paginator.removeItem(reportId); + paginator.removeItem(reportId); } const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue index 73b25277b3..ddc3ff7b79 100644 --- a/packages/frontend/src/pages/admin/federation.vue +++ b/packages/frontend/src/pages/admin/federation.vue @@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only </FormSplit> </div> - <MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination"> + <MkPagination v-slot="{items}" :key="host + state" :paginator="paginator"> <div :class="$style.instances"> <MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" :class="$style.instance" :to="`/instance-info/${instance.host}`"> <MkInstanceCardMini :instance="instance"/> @@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; -import { computed, ref } from 'vue'; +import { computed, markRaw, ref } from 'vue'; import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkPagination from '@/components/MkPagination.vue'; @@ -64,15 +64,15 @@ import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue'; import FormSplit from '@/components/form/split.vue'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; +import { Paginator } from '@/utility/paginator.js'; const host = ref(''); const state = ref('federating'); const sort = ref('+pubSub'); -const pagination = { - endpoint: 'federation/instances' as const, +const paginator = markRaw(new Paginator('federation/instances', { limit: 10, offsetMode: true, - params: computed(() => ({ + computedParams: computed(() => ({ sort: sort.value, host: host.value !== '' ? host.value : null, ...( @@ -85,7 +85,7 @@ const pagination = { state.value === 'notResponding' ? { notResponding: true } : {}), })), -}; +})); function getStatus(instance: Misskey.entities.FederationInstance) { switch (instance.suspensionState) { diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue index 4ea5756284..0f3a90b458 100644 --- a/packages/frontend/src/pages/admin/files.vue +++ b/packages/frontend/src/pages/admin/files.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="local">{{ i18n.ts.local }}</option> <option value="remote">{{ i18n.ts.remote }}</option> </MkSelect> - <MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'"> + <MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="paginator.computedParams.value.origin === 'local'"> <template #label>{{ i18n.ts.host }}</template> </MkInput> </div> @@ -26,14 +26,14 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>MIME type</template> </MkInput> </div> - <MkFileListForAdmin :pagination="pagination" :viewMode="viewMode"/> + <MkFileListForAdmin :paginator="paginator" :viewMode="viewMode"/> </div> </div> </PageWithHeader> </template> <script lang="ts" setup> -import { computed, ref } from 'vue'; +import { computed, markRaw, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; @@ -42,23 +42,22 @@ import * as os from '@/os.js'; import { lookupFile } from '@/utility/admin-lookup.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; -import type { PagingCtx } from '@/composables/use-pagination.js'; +import { Paginator } from '@/utility/paginator.js'; const origin = ref<Misskey.entities.AdminDriveFilesRequest['origin']>('local'); const type = ref<string | null>(null); const searchHost = ref(''); const userId = ref(''); const viewMode = ref<'grid' | 'list'>('grid'); -const pagination = { - endpoint: 'admin/drive/files' as const, +const paginator = markRaw(new Paginator('admin/drive/files', { limit: 10, - params: computed(() => ({ + computedParams: computed(() => ({ type: (type.value && type.value !== '') ? type.value : null, userId: (userId.value && userId.value !== '') ? userId.value : null, origin: origin.value, hostname: (searchHost.value && searchHost.value !== '') ? searchHost.value : null, })), -} satisfies PagingCtx<'admin/drive/files'>; +})); function clear() { os.confirm({ diff --git a/packages/frontend/src/pages/admin/invites.vue b/packages/frontend/src/pages/admin/invites.vue index f1584fc864..e1b4890513 100644 --- a/packages/frontend/src/pages/admin/invites.vue +++ b/packages/frontend/src/pages/admin/invites.vue @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInput v-if="!noExpirationDate" v-model="expiresAt" type="datetime-local"> <template #label>{{ i18n.ts.expirationDate }}</template> </MkInput> - <MkInput v-model="createCount" type="number" min="1"> + <MkInput v-model="createCount" type="number" :min="1"> <template #label>{{ i18n.ts.createCount }}</template> </MkInput> <MkButton primary rounded @click="createWithOptions">{{ i18n.ts.create }}</MkButton> @@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="-usedAt">{{ i18n.ts.usedAt }} ({{ i18n.ts.descendingOrder }})</option> </MkSelect> </div> - <MkPagination ref="pagingComponent" :pagination="pagination"> + <MkPagination :paginator="paginator"> <template #default="{ items }"> <div class="_gaps_s"> <MkInviteCode v-for="item in items" :key="item.id" :invite="(item as any)" :onDeleted="deleted" moderator/> @@ -54,8 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, ref, useTemplateRef } from 'vue'; -import type { PagingCtx } from '@/composables/use-pagination.js'; +import { computed, markRaw, ref, useTemplateRef } from 'vue'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -67,21 +66,19 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkInviteCode from '@/components/MkInviteCode.vue'; import { definePage } from '@/page.js'; - -const pagingComponent = useTemplateRef('pagingComponent'); +import { Paginator } from '@/utility/paginator.js'; const type = ref('all'); const sort = ref('+createdAt'); -const pagination: PagingCtx = { - endpoint: 'admin/invite/list' as const, +const paginator = markRaw(new Paginator('admin/invite/list', { limit: 10, - params: computed(() => ({ + computedParams: computed(() => ({ type: type.value, sort: sort.value, })), offsetMode: true, -}; +})); const expiresAt = ref(''); const noExpirationDate = ref(true); @@ -100,13 +97,11 @@ async function createWithOptions() { text: tickets.map(x => x.code).join('\n'), }); - tickets.forEach(ticket => pagingComponent.value?.paginator.prepend(ticket)); + tickets.forEach(ticket => paginator.prepend(ticket)); } function deleted(id: string) { - if (pagingComponent.value) { - pagingComponent.value.paginator.removeItem(id); - } + paginator.removeItem(id); } const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue index 76b313026b..6a6102749e 100644 --- a/packages/frontend/src/pages/admin/modlog.vue +++ b/packages/frontend/src/pages/admin/modlog.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <PageWithHeader :actions="headerActions" :tabs="headerTabs"> <div class="_spacer" style="--MI_SPACER-w: 900px;"> <div class="_gaps"> - <MkPaginationControl v-model:order="order" v-model:date="date" v-model:q="q" canSearch canFilter @reload="paginator.reload()"> + <MkPaginationControl :paginator="paginator" canFilter> <MkSelect v-model="type" style="margin: 0; flex: 1;"> <template #label>{{ i18n.ts.type }}</template> <option :value="null">{{ i18n.ts.all }}</option> @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, useTemplateRef, ref, watch } from 'vue'; +import { computed, ref, markRaw, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; import XModLog from './modlog.ModLog.vue'; import MkSelect from '@/components/MkSelect.vue'; @@ -54,37 +54,25 @@ import MkTl from '@/components/MkTl.vue'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { prefer } from '@/preferences.js'; -import { usePagination } from '@/composables/use-pagination.js'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import MkButton from '@/components/MkButton.vue'; import MkPaginationControl from '@/components/MkPaginationControl.vue'; +import { Paginator } from '@/utility/paginator.js'; -const order = ref<'newest' | 'oldest'>('newest'); -const date = ref<number | null>(null); const type = ref<string | null>(null); const moderatorId = ref(''); -const q = ref<string | null>(null); -const paginator = usePagination({ - ctx: { - endpoint: 'admin/show-moderation-logs', - limit: 20, - canFetchDetection: 'limit', - params: computed(() => ({ - type: type.value, - userId: moderatorId.value === '' ? null : moderatorId.value, - search: q.value, - })), - }, -}); +const paginator = markRaw(new Paginator('admin/show-moderation-logs', { + limit: 20, + canFetchDetection: 'limit', + canSearch: true, + computedParams: computed(() => ({ + type: type.value, + userId: moderatorId.value === '' ? null : moderatorId.value, + })), +})); -watch([order, date], () => { - paginator.updateCtxPartial({ - order: order.value, - initialDirection: order.value === 'oldest' ? 'newer' : 'older', - initialDate: date.value, - }); -}, { immediate: false }); +paginator.init(); const timeline = computed(() => { return paginator.items.value.map(x => ({ @@ -95,7 +83,7 @@ const timeline = computed(() => { }); function fetchMore() { - if (order.value === 'oldest') { + if (paginator.order.value === 'oldest') { paginator.fetchNewer(); } else { paginator.fetchOlder(); diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index 61d72777b8..64b6231398 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps"> <MkButton primary rounded @click="assign"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton> - <MkPagination :pagination="usersPagination"> + <MkPagination :paginator="usersPaginator"> <template #empty><MkResult type="empty" :text="i18n.ts.noUsers"/></template> <template #default="{ items }"> @@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, reactive, ref } from 'vue'; +import { computed, markRaw, reactive, ref } from 'vue'; import XEditor from './roles.editor.vue'; import MkFolder from '@/components/MkFolder.vue'; import * as os from '@/os.js'; @@ -66,6 +66,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkPagination from '@/components/MkPagination.vue'; import { useRouter } from '@/router.js'; +import { Paginator } from '@/utility/paginator.js'; const router = useRouter(); @@ -73,13 +74,12 @@ const props = defineProps<{ id?: string; }>(); -const usersPagination = { - endpoint: 'admin/roles/users' as const, +const usersPaginator = markRaw(new Paginator('admin/roles/users', { limit: 20, - params: computed(() => ({ + computedParams: computed(() => ({ roleId: props.id, })), -}; +})); const expandedItems = ref([]); diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue index 56cf8876f0..581eb7eb97 100644 --- a/packages/frontend/src/pages/admin/users.vue +++ b/packages/frontend/src/pages/admin/users.vue @@ -38,13 +38,13 @@ SPDX-License-Identifier: AGPL-3.0-only <template #prefix>@</template> <template #label>{{ i18n.ts.username }}</template> </MkInput> - <MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'"> + <MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="paginator.computedParams.value.origin === 'local'"> <template #prefix>@</template> <template #label>{{ i18n.ts.host }}</template> </MkInput> </div> - <MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination"> + <MkPagination v-slot="{items}" :paginator="paginator"> <div :class="$style.users"> <MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" :class="$style.user" :to="`/admin/user/${user.id}`"> <MkUserCardMini :user="user"/> @@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, useTemplateRef, ref, watchEffect } from 'vue'; +import { computed, markRaw, ref, watchEffect } from 'vue'; import { defaultMemoryStorage } from '@/memory-storage'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; @@ -69,6 +69,7 @@ import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import { dateString } from '@/filters/date.js'; +import { Paginator } from '@/utility/paginator.js'; type SearchQuery = { sort?: string; @@ -78,7 +79,6 @@ type SearchQuery = { hostname?: string; }; -const paginationComponent = useTemplateRef('paginationComponent'); const storedQuery = JSON.parse(defaultMemoryStorage.getItem('admin-users-query') ?? '{}') as SearchQuery; const sort = ref(storedQuery.sort ?? '+createdAt'); @@ -86,10 +86,9 @@ const state = ref(storedQuery.state ?? 'all'); const origin = ref(storedQuery.origin ?? 'local'); const searchUsername = ref(storedQuery.username ?? ''); const searchHost = ref(storedQuery.hostname ?? ''); -const pagination = { - endpoint: 'admin/show-users' as const, +const paginator = markRaw(new Paginator('admin/show-users', { limit: 10, - params: computed(() => ({ + computedParams: computed(() => ({ sort: sort.value, state: state.value, origin: origin.value, @@ -97,7 +96,7 @@ const pagination = { hostname: searchHost.value, })), offsetMode: true, -}; +})); function searchUser() { os.selectUser({ includeSelf: true }).then(user => { @@ -121,7 +120,7 @@ async function addUser() { username: username, password: password, }).then(res => { - paginationComponent.value?.paginator.reload(); + paginator.reload(); }); } diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue index 2c671c6b34..4c34c3c74b 100644 --- a/packages/frontend/src/pages/announcements.vue +++ b/packages/frontend/src/pages/announcements.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_spacer" style="--MI_SPACER-w: 800px;"> <div class="_gaps"> <MkInfo v-if="$i && $i.hasUnreadAnnouncement && tab === 'current'" warn>{{ i18n.ts.youHaveUnreadAnnouncements }}</MkInfo> - <MkPagination ref="paginationEl" :key="tab" v-slot="{items}" :pagination="tab === 'current' ? paginationCurrent : paginationPast" class="_gaps"> + <MkPagination v-slot="{items}" :paginator="paginator" class="_gaps"> <section v-for="announcement in items" :key="announcement.id" class="_panel" :class="$style.announcement"> <div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div> <div :class="$style.header"> @@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, computed, useTemplateRef } from 'vue'; +import { ref, computed, markRaw } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import MkInfo from '@/components/MkInfo.vue'; @@ -54,24 +54,14 @@ import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { $i } from '@/i.js'; import { updateCurrentAccountPartial } from '@/accounts.js'; +import { Paginator } from '@/utility/paginator.js'; -const paginationCurrent = { - endpoint: 'announcements' as const, +const paginator = markRaw(new Paginator('announcements', { limit: 10, - params: { - isActive: true, - }, -}; - -const paginationPast = { - endpoint: 'announcements' as const, - limit: 10, - params: { - isActive: false, - }, -}; - -const paginationEl = useTemplateRef('paginationEl'); + computedParams: computed(() => ({ + isActive: tab.value === 'current', + })), +})); const tab = ref('current'); @@ -85,8 +75,7 @@ async function read(target) { if (confirm.canceled) return; } - if (!paginationEl.value) return; - paginationEl.value.paginator.updateItem(target.id, a => ({ + paginator.updateItem(target.id, a => ({ ...a, isRead: true, })); diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 6eb390f743..116aabaee2 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkStreamingNotesTimeline :key="channelId" src="channel" :channel="channelId"/> </div> <div v-else-if="tab === 'featured'"> - <MkNotesTimeline :pagination="featuredPagination"/> + <MkNotesTimeline :paginator="featuredPaginator"/> </div> <div v-else-if="tab === 'search'"> <div v-if="notesSearchAvailable" class="_gaps"> @@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> <MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton> </div> - <MkNotesTimeline v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/> + <MkNotesTimeline v-if="searchPaginator" :key="searchKey" :paginator="searchPaginator"/> </div> <div v-else> <MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo> @@ -70,7 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, ref } from 'vue'; +import { computed, watch, ref, markRaw, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import { url } from '@@/js/config.js'; import { useInterval } from '@@/js/use-interval.js'; @@ -97,6 +97,7 @@ import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { notesSearchAvailable } from '@/utility/check-permissions.js'; import { miLocalStorage } from '@/local-storage.js'; import { useRouter } from '@/router.js'; +import { Paginator } from '@/utility/paginator.js'; const router = useRouter(); @@ -109,14 +110,13 @@ const tab = ref('overview'); const channel = ref<Misskey.entities.Channel | null>(null); const favorited = ref(false); const searchQuery = ref(''); -const searchPagination = ref(); +const searchPaginator = shallowRef(); const searchKey = ref(''); -const featuredPagination = computed(() => ({ - endpoint: 'notes/featured' as const, +const featuredPaginator = markRaw(new Paginator('channels/featured', { limit: 10, - params: { + computedParams: computed(() => ({ channelId: props.channelId, - }, + })), })); useInterval(() => { @@ -190,14 +190,13 @@ async function search() { if (query == null) return; - searchPagination.value = { - endpoint: 'notes/search', + searchPaginator.value = markRaw(new Paginator('notes/search', { limit: 10, params: { query: query, channelId: channel.value.id, }, - }; + })); searchKey.value = query; } diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue index b2b2bc02d2..324e0c573a 100644 --- a/packages/frontend/src/pages/channels.vue +++ b/packages/frontend/src/pages/channels.vue @@ -18,27 +18,27 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton> </div> - <MkFoldableSection v-if="channelPagination"> + <MkFoldableSection v-if="channelPaginator"> <template #header>{{ i18n.ts.searchResult }}</template> - <MkChannelList :key="key" :pagination="channelPagination"/> + <MkChannelList :key="key" :paginator="channelPaginator"/> </MkFoldableSection> </div> <div v-if="tab === 'featured'"> - <MkPagination v-slot="{items}" :pagination="featuredPagination"> + <MkPagination v-slot="{items}" :paginator="featuredPaginator"> <div :class="$style.root"> <MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/> </div> </MkPagination> </div> <div v-else-if="tab === 'favorites'"> - <MkPagination v-slot="{items}" :pagination="favoritesPagination"> + <MkPagination v-slot="{items}" :paginator="favoritesPaginator"> <div :class="$style.root"> <MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/> </div> </MkPagination> </div> <div v-else-if="tab === 'following'"> - <MkPagination v-slot="{items}" :pagination="followingPagination"> + <MkPagination v-slot="{items}" :paginator="followingPaginator"> <div :class="$style.root"> <MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/> </div> @@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-else-if="tab === 'owned'"> <MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton> - <MkPagination v-slot="{items}" :pagination="ownedPagination"> + <MkPagination v-slot="{items}" :paginator="ownedPaginator"> <div :class="$style.root"> <MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/> </div> @@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, onMounted, ref } from 'vue'; +import { computed, markRaw, onMounted, ref, shallowRef } from 'vue'; import MkChannelPreview from '@/components/MkChannelPreview.vue'; import MkChannelList from '@/components/MkChannelList.vue'; import MkPagination from '@/components/MkPagination.vue'; @@ -68,6 +68,7 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue'; import { definePage } from '@/page.js'; import { i18n } from '@/i18n.js'; import { useRouter } from '@/router.js'; +import { Paginator } from '@/utility/paginator.js'; const router = useRouter(); @@ -80,31 +81,27 @@ const key = ref(''); const tab = ref('featured'); const searchQuery = ref(''); const searchType = ref('nameAndDescription'); -const channelPagination = ref(); +const channelPaginator = shallowRef(); onMounted(() => { searchQuery.value = props.query ?? ''; searchType.value = props.type ?? 'nameAndDescription'; }); -const featuredPagination = { - endpoint: 'channels/featured' as const, +const featuredPaginator = markRaw(new Paginator('channels/featured', { limit: 10, noPaging: true, -}; -const favoritesPagination = { - endpoint: 'channels/my-favorites' as const, +})); +const favoritesPaginator = markRaw(new Paginator('channels/my-favorites', { limit: 100, noPaging: true, -}; -const followingPagination = { - endpoint: 'channels/followed' as const, +})); +const followingPaginator = markRaw(new Paginator('channels/followed', { limit: 10, -}; -const ownedPagination = { - endpoint: 'channels/owned' as const, +})); +const ownedPaginator = markRaw(new Paginator('channels/owned', { limit: 10, -}; +})); async function search() { const query = searchQuery.value.toString().trim(); @@ -113,14 +110,13 @@ async function search() { const type = searchType.value.toString().trim(); - channelPagination.value = { - endpoint: 'channels/search', + channelPaginator.value = markRaw(new Paginator('channels/search', { limit: 10, params: { query: searchQuery.value, type: type, }, - }; + })); key.value = query + type; } diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index dc043e2ce1..8843812544 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -23,14 +23,14 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> - <MkNotesTimeline :pagination="pagination" :detail="true"/> + <MkNotesTimeline :paginator="paginator" :detail="true"/> </div> </div> </PageWithHeader> </template> <script lang="ts" setup> -import { computed, watch, provide, ref } from 'vue'; +import { computed, watch, provide, ref, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import { url } from '@@/js/config.js'; import type { MenuItem } from '@/types/menu.js'; @@ -46,6 +46,7 @@ import { isSupportShare } from '@/utility/navigator.js'; import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { genEmbedCode } from '@/utility/get-embed-code.js'; import { assertServerContext, serverContext } from '@/server-context.js'; +import { Paginator } from '@/utility/paginator.js'; // contextは非ログイン状態の情報しかないためログイン時は利用できない const CTX_CLIP = !$i && assertServerContext(serverContext, 'clip') ? serverContext.clip : null; @@ -56,13 +57,13 @@ const props = defineProps<{ const clip = ref<Misskey.entities.Clip | null>(CTX_CLIP); const favorited = ref(false); -const pagination = { - endpoint: 'clips/notes' as const, +const paginator = markRaw(new Paginator('clips/notes', { limit: 10, - params: computed(() => ({ + canSearch: true, + computedParams: computed(() => ({ clipId: props.clipId, })), -}; +})); const isOwned = computed<boolean | null>(() => $i && clip.value && ($i.id === clip.value.userId)); diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index 36d638b210..1cb07017e9 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton inline @click="setLicenseBulk">Set License</MkButton> <MkButton inline danger @click="delBulk">Delete</MkButton> </div> - <MkPagination ref="emojisPaginationComponent" :pagination="pagination"> + <MkPagination ref="emojisPaginationComponent" :paginator="paginator"> <template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template> <template #default="{items}"> <div class="ldhfsamy"> @@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.host }}</template> </MkInput> </FormSplit> - <MkPagination :pagination="remotePagination"> + <MkPagination :paginator="remotePaginator"> <template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template> <template #default="{items}"> <div class="ldhfsamy"> @@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, ref, useTemplateRef } from 'vue'; +import { computed, defineAsyncComponent, markRaw, ref } from 'vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkPagination from '@/components/MkPagination.vue'; @@ -84,8 +84,7 @@ import { misskeyApi } from '@/utility/misskey-api.js'; import { getProxiedImageUrl } from '@/utility/media-proxy.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; - -const emojisPaginationComponent = useTemplateRef('emojisPaginationComponent'); +import { Paginator } from '@/utility/paginator.js'; const tab = ref('local'); const query = ref<string | null>(null); @@ -94,28 +93,26 @@ const host = ref<string | null>(null); const selectMode = ref(false); const selectedEmojis = ref<string[]>([]); -const pagination = { - endpoint: 'admin/emoji/list' as const, +const paginator = markRaw(new Paginator('admin/emoji/list', { limit: 30, - params: computed(() => ({ + computedParams: computed(() => ({ query: (query.value && query.value !== '') ? query.value : null, })), -}; +})); -const remotePagination = { - endpoint: 'admin/emoji/list-remote' as const, +const remotePaginator = markRaw(new Paginator('admin/emoji/list-remote', { limit: 30, - params: computed(() => ({ + computedParams: computed(() => ({ query: (queryRemote.value && queryRemote.value !== '') ? queryRemote.value : null, host: (host.value && host.value !== '') ? host.value : null, })), -}; +})); const selectAll = () => { if (selectedEmojis.value.length > 0) { selectedEmojis.value = []; } else { - selectedEmojis.value = emojisPaginationComponent.value?.paginator.items.value.map(item => item.id); + selectedEmojis.value = paginator.items.value.map(item => item.id); } }; @@ -132,7 +129,7 @@ const add = async (ev: MouseEvent) => { }, { done: result => { if (result.created) { - emojisPaginationComponent.value?.paginator.prepend(result.created); + paginator.prepend(result.created); } }, closed: () => dispose(), @@ -145,12 +142,12 @@ const edit = async (emoji) => { }, { done: result => { if (result.updated) { - emojisPaginationComponent.value?.paginator.updateItem(result.updated.id, (oldEmoji) => ({ + paginator.updateItem(result.updated.id, (oldEmoji) => ({ ...oldEmoji, ...result.updated, })); } else if (result.deleted) { - emojisPaginationComponent.value?.paginator.removeItem(emoji.id); + paginator.removeItem(emoji.id); } }, closed: () => dispose(), @@ -245,7 +242,7 @@ const setCategoryBulk = async () => { ids: selectedEmojis.value, category: result, }); - emojisPaginationComponent.value?.paginator.reload(); + paginator.reload(); }; const setLicenseBulk = async () => { @@ -257,7 +254,7 @@ const setLicenseBulk = async () => { ids: selectedEmojis.value, license: result, }); - emojisPaginationComponent.value?.paginator.reload(); + paginator.reload(); }; const addTagBulk = async () => { @@ -269,7 +266,7 @@ const addTagBulk = async () => { ids: selectedEmojis.value, aliases: result.split(' '), }); - emojisPaginationComponent.value?.paginator.reload(); + paginator.reload(); }; const removeTagBulk = async () => { @@ -281,7 +278,7 @@ const removeTagBulk = async () => { ids: selectedEmojis.value, aliases: result.split(' '), }); - emojisPaginationComponent.value?.paginator.reload(); + paginator.reload(); }; const setTagBulk = async () => { @@ -293,7 +290,7 @@ const setTagBulk = async () => { ids: selectedEmojis.value, aliases: result.split(' '), }); - emojisPaginationComponent.value?.paginator.reload(); + paginator.reload(); }; const delBulk = async () => { @@ -305,7 +302,7 @@ const delBulk = async () => { await os.apiWithDialog('admin/emoji/delete-bulk', { ids: selectedEmojis.value, }); - emojisPaginationComponent.value?.paginator.reload(); + paginator.reload(); }; const headerActions = computed(() => [{ diff --git a/packages/frontend/src/pages/drive.file.notes.vue b/packages/frontend/src/pages/drive.file.notes.vue index cf45470588..8427017c92 100644 --- a/packages/frontend/src/pages/drive.file.notes.vue +++ b/packages/frontend/src/pages/drive.file.notes.vue @@ -6,16 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps"> <MkInfo>{{ i18n.ts._fileViewer.thisPageCanBeSeenFromTheAuthor }}</MkInfo> - <MkNotesTimeline ref="tlComponent" :pagination="pagination"/> + <MkNotesTimeline :paginator="paginator"/> </div> </template> <script lang="ts" setup> -import { ref, computed } from 'vue'; +import { ref, computed, markRaw } from 'vue'; import { i18n } from '@/i18n.js'; -import type { PagingCtx } from '@/composables/use-pagination.js'; import MkInfo from '@/components/MkInfo.vue'; import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; +import { Paginator } from '@/utility/paginator.js'; const props = defineProps<{ fileId: string; @@ -23,11 +23,10 @@ const props = defineProps<{ const realFileId = computed(() => props.fileId); -const pagination = ref<PagingCtx>({ - endpoint: 'drive/files/attached-notes', +const paginator = markRaw(new Paginator('drive/files/attached-notes', { limit: 10, params: { fileId: realFileId.value, }, -}); +})); </script> diff --git a/packages/frontend/src/pages/explore.featured.vue b/packages/frontend/src/pages/explore.featured.vue index b8eb7eb8d5..abb816a956 100644 --- a/packages/frontend/src/pages/explore.featured.vue +++ b/packages/frontend/src/pages/explore.featured.vue @@ -9,30 +9,29 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="notes">{{ i18n.ts.notes }}</option> <option value="polls">{{ i18n.ts.poll }}</option> </MkTab> - <MkNotesTimeline v-if="tab === 'notes'" :pagination="paginationForNotes"/> - <MkNotesTimeline v-else-if="tab === 'polls'" :pagination="paginationForPolls"/> + <MkNotesTimeline v-if="tab === 'notes'" :paginator="paginatorForNotes"/> + <MkNotesTimeline v-else-if="tab === 'polls'" :paginator="paginatorForPolls"/> </div> </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { markRaw, ref } from 'vue'; import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; import MkTab from '@/components/MkTab.vue'; import { i18n } from '@/i18n.js'; +import { Paginator } from '@/utility/paginator.js'; -const paginationForNotes = { - endpoint: 'notes/featured' as const, +const paginatorForNotes = markRaw(new Paginator('notes/featured', { limit: 10, -}; +})); -const paginationForPolls = { - endpoint: 'notes/polls/recommendation' as const, +const paginatorForPolls = markRaw(new Paginator('notes/polls/recommendation', { limit: 10, offsetMode: true, params: { excludeChannels: true, }, -}; +})); const tab = ref('notes'); </script> diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue index e723f6a1e9..72f2a6813c 100644 --- a/packages/frontend/src/pages/explore.users.vue +++ b/packages/frontend/src/pages/explore.users.vue @@ -13,19 +13,19 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="tag == null"> <MkFoldableSection class="_margin" persistKey="explore-pinned-users"> <template #header><i class="ti ti-bookmark ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template> - <MkUserList :pagination="pinnedUsers"/> + <MkUserList :paginator="pinnedUsersPaginator"/> </MkFoldableSection> <MkFoldableSection class="_margin" persistKey="explore-popular-users"> <template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template> - <MkUserList :pagination="popularUsers"/> + <MkUserList :paginator="popularUsersPaginator"/> </MkFoldableSection> <MkFoldableSection class="_margin" persistKey="explore-recently-updated-users"> <template #header><i class="ti ti-message ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template> - <MkUserList :pagination="recentlyUpdatedUsers"/> + <MkUserList :paginator="recentlyUpdatedUsersPaginator"/> </MkFoldableSection> <MkFoldableSection class="_margin" persistKey="explore-recently-registered-users"> <template #header><i class="ti ti-plus ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyRegisteredUsers }}</template> - <MkUserList :pagination="recentlyRegisteredUsers"/> + <MkUserList :paginator="recentlyRegisteredUsersPaginator"/> </MkFoldableSection> </template> </div> @@ -41,21 +41,21 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFoldableSection v-if="tag != null" :key="`${tag}`" class="_margin"> <template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template> - <MkUserList :pagination="tagUsers"/> + <MkUserList :paginator="tagUsersPaginator"/> </MkFoldableSection> <template v-if="tag == null"> <MkFoldableSection class="_margin"> <template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template> - <MkUserList :pagination="popularUsersF"/> + <MkUserList :paginator="popularUsersFPaginator"/> </MkFoldableSection> <MkFoldableSection class="_margin"> <template #header><i class="ti ti-message ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template> - <MkUserList :pagination="recentlyUpdatedUsersF"/> + <MkUserList :paginator="recentlyUpdatedUsersFPaginator"/> </MkFoldableSection> <MkFoldableSection class="_margin"> <template #header><i class="ti ti-rocket ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyDiscoveredUsers }}</template> - <MkUserList :pagination="recentlyRegisteredUsersF"/> + <MkUserList :paginator="recentlyRegisteredUsersFPaginator"/> </MkFoldableSection> </template> </div> @@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch, ref, useTemplateRef, computed } from 'vue'; +import { watch, ref, useTemplateRef, computed, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import MkUserList from '@/components/MkUserList.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; @@ -71,6 +71,7 @@ import MkTab from '@/components/MkTab.vue'; import { misskeyApi } from '@/utility/misskey-api.js'; import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; +import { Paginator } from '@/utility/paginator.js'; const props = defineProps<{ tag?: string; @@ -85,8 +86,7 @@ watch(() => props.tag, () => { if (tagsEl.value) tagsEl.value.toggleContent(props.tag == null); }); -const tagUsers = computed(() => ({ - endpoint: 'hashtags/users' as const, +const tagUsersPaginator = markRaw(new Paginator('hashtags/users', { limit: 30, params: { tag: props.tag, @@ -95,34 +95,66 @@ const tagUsers = computed(() => ({ }, })); -const pinnedUsers = { endpoint: 'pinned-users', noPaging: true }; -const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: { - state: 'alive', - origin: 'local', - sort: '+follower', -} }; -const recentlyUpdatedUsers = { endpoint: 'users', limit: 10, noPaging: true, params: { - origin: 'local', - sort: '+updatedAt', -} }; -const recentlyRegisteredUsers = { endpoint: 'users', limit: 10, noPaging: true, params: { - origin: 'local', - state: 'alive', - sort: '+createdAt', -} }; -const popularUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: { - state: 'alive', - origin: 'remote', - sort: '+follower', -} }; -const recentlyUpdatedUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: { - origin: 'combined', - sort: '+updatedAt', -} }; -const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: { - origin: 'combined', - sort: '+createdAt', -} }; +const pinnedUsersPaginator = markRaw(new Paginator('pinned-users', { + noPaging: true, +})); + +const popularUsersPaginator = markRaw(new Paginator('users', { + limit: 10, + noPaging: true, + params: { + state: 'alive', + origin: 'local', + sort: '+follower', + }, +})); + +const recentlyUpdatedUsersPaginator = markRaw(new Paginator('users', { + limit: 10, + noPaging: true, + params: { + origin: 'local', + sort: '+updatedAt', + }, +})); + +const recentlyRegisteredUsersPaginator = markRaw(new Paginator('users', { + limit: 10, + noPaging: true, + params: { + origin: 'local', + state: 'alive', + sort: '+createdAt', + }, +})); + +const popularUsersFPaginator = markRaw(new Paginator('users', { + limit: 10, + noPaging: true, + params: { + state: 'alive', + origin: 'remote', + sort: '+follower', + }, +})); + +const recentlyUpdatedUsersFPaginator = markRaw(new Paginator('users', { + limit: 10, + noPaging: true, + params: { + origin: 'combined', + sort: '+updatedAt', + }, +})); + +const recentlyRegisteredUsersFPaginator = markRaw(new Paginator('users', { + limit: 10, + noPaging: true, + params: { + origin: 'combined', + sort: '+createdAt', + }, +})); misskeyApi('hashtags/list', { sort: '+attachedLocalUsers', diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue index b0a18987b4..72dd2b4a16 100644 --- a/packages/frontend/src/pages/favorites.vue +++ b/packages/frontend/src/pages/favorites.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <PageWithHeader> <div class="_spacer" style="--MI_SPACER-w: 800px;"> - <MkPagination :pagination="pagination"> + <MkPagination :paginator="paginator"> <template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template> <template #default="{ items }"> @@ -20,16 +20,17 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { markRaw } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; import MkNote from '@/components/MkNote.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; +import { Paginator } from '@/utility/paginator.js'; -const pagination = { - endpoint: 'i/favorites' as const, +const paginator = markRaw(new Paginator('i/favorites', { limit: 10, -}; +})); definePage(() => ({ title: i18n.ts.favorites, diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue index f3365fcedf..6e25df2df8 100644 --- a/packages/frontend/src/pages/flash/flash-index.vue +++ b/packages/frontend/src/pages/flash/flash-index.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :swipable="true"> <div class="_spacer" style="--MI_SPACER-w: 700px;"> <div v-if="tab === 'featured'"> - <MkPagination v-slot="{items}" :pagination="featuredFlashsPagination"> + <MkPagination v-slot="{items}" :paginator="featuredFlashsPaginator"> <div class="_gaps_s"> <MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/> </div> @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-else-if="tab === 'my'"> <div class="_gaps"> <MkButton gradate rounded style="margin: 0 auto;" @click="create()"><i class="ti ti-plus"></i></MkButton> - <MkPagination v-slot="{items}" :pagination="myFlashsPagination"> + <MkPagination v-slot="{items}" :paginator="myFlashsPaginator"> <div class="_gaps_s"> <MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/> </div> @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-else-if="tab === 'liked'"> - <MkPagination v-slot="{items}" :pagination="likedFlashsPagination"> + <MkPagination v-slot="{items}" :paginator="likedFlashsPaginator"> <div class="_gaps_s"> <MkFlashPreview v-for="like in items" :key="like.flash.id" :flash="like.flash"/> </div> @@ -37,31 +37,29 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, ref } from 'vue'; +import { computed, markRaw, ref } from 'vue'; import MkFlashPreview from '@/components/MkFlashPreview.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { useRouter } from '@/router.js'; +import { Paginator } from '@/utility/paginator.js'; const router = useRouter(); const tab = ref('featured'); -const featuredFlashsPagination = { - endpoint: 'flash/featured' as const, +const featuredFlashsPaginator = markRaw(new Paginator('flash/featured', { limit: 5, offsetMode: true, -}; -const myFlashsPagination = { - endpoint: 'flash/my' as const, +})); +const myFlashsPaginator = markRaw(new Paginator('flash/my', { limit: 5, -}; -const likedFlashsPagination = { - endpoint: 'flash/my-likes' as const, +})); +const likedFlashsPaginator = markRaw(new Paginator('flash/my-likes', { limit: 5, -}; +})); function create() { router.push('/play/new'); diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue index e02abdc393..35e259a571 100644 --- a/packages/frontend/src/pages/follow-requests.vue +++ b/packages/frontend/src/pages/follow-requests.vue @@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :swipable="true"> - <div class="_spacer" style="--MI_SPACER-w: 800px;"> - <MkPagination ref="paginationComponent" :pagination="pagination"> + <div :key="tab" class="_spacer" style="--MI_SPACER-w: 800px;"> + <MkPagination :paginator="paginator"> <template #empty><MkResult type="empty" :text="i18n.ts.noFollowRequests"/></template> <template #default="{items}"> <div class="mk-follow-requests _gaps"> @@ -35,8 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; -import { useTemplateRef, computed, ref } from 'vue'; -import type { PagingCtx } from '@/composables/use-pagination.js'; +import { computed, markRaw, ref, watch } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import { userPage, acct } from '@/filters/user.js'; @@ -44,32 +43,35 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { $i } from '@/i.js'; +import { Paginator } from '@/utility/paginator.js'; -const paginationComponent = useTemplateRef('paginationComponent'); +const tab = ref($i?.isLocked ? 'list' : 'sent'); + +let paginator: Paginator<'following/requests/list' | 'following/requests/sent'>; -const pagination = computed<PagingCtx>(() => tab.value === 'list' ? { - endpoint: 'following/requests/list', - limit: 10, -} : { - endpoint: 'following/requests/sent', - limit: 10, -}); +watch(tab, (newTab) => { + if (newTab === 'list') { + paginator = markRaw(new Paginator('following/requests/list', { limit: 10 })); + } else { + paginator = markRaw(new Paginator('following/requests/sent', { limit: 10 })); + } +}, { immediate: true }); function accept(user: Misskey.entities.UserLite) { os.apiWithDialog('following/requests/accept', { userId: user.id }).then(() => { - paginationComponent.value?.paginator.reload(); + paginator.reload(); }); } function reject(user: Misskey.entities.UserLite) { os.apiWithDialog('following/requests/reject', { userId: user.id }).then(() => { - paginationComponent.value?.paginator.reload(); + paginator.reload(); }); } function cancel(user: Misskey.entities.UserLite) { os.apiWithDialog('following/requests/cancel', { userId: user.id }).then(() => { - paginationComponent.value?.paginator.reload(); + paginator.reload(); }); } @@ -91,8 +93,6 @@ const headerTabs = computed(() => [ }, ]); -const tab = ref($i?.isLocked ? 'list' : 'sent'); - definePage(() => ({ title: i18n.ts.followRequests, icon: 'ti ti-user-plus', diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue index af46a4cb0f..2ed663932b 100644 --- a/packages/frontend/src/pages/gallery/index.vue +++ b/packages/frontend/src/pages/gallery/index.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="tab === 'explore'"> <MkFoldableSection class="_margin"> <template #header><i class="ti ti-clock"></i>{{ i18n.ts.recentPosts }}</template> - <MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disableAutoLoad="true"> + <MkPagination v-slot="{items}" :paginator="recentPostsPaginator"> <div :class="$style.items"> <MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> </div> @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFoldableSection> <MkFoldableSection class="_margin"> <template #header><i class="ti ti-comet"></i>{{ i18n.ts.popularPosts }}</template> - <MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disableAutoLoad="true"> + <MkPagination v-slot="{items}" :paginator="popularPostsPaginator"> <div :class="$style.items"> <MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> </div> @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFoldableSection> </div> <div v-else-if="tab === 'liked'"> - <MkPagination v-slot="{items}" :pagination="likedPostsPagination"> + <MkPagination v-slot="{items}" :paginator="likedPostsPaginator"> <div :class="$style.items"> <MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/> </div> @@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-else-if="tab === 'my'"> <MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="ti ti-plus"></i> {{ i18n.ts.postToGallery }}</MkA> - <MkPagination v-slot="{items}" :pagination="myPostsPagination"> + <MkPagination v-slot="{items}" :paginator="myPostsPaginator"> <div :class="$style.items"> <MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> </div> @@ -44,13 +44,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch, ref, computed } from 'vue'; +import { watch, ref, computed, markRaw } from 'vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue'; import { definePage } from '@/page.js'; import { i18n } from '@/i18n.js'; import { useRouter } from '@/router.js'; +import { Paginator } from '@/utility/paginator.js'; const router = useRouter(); @@ -59,34 +60,19 @@ const props = defineProps<{ }>(); const tab = ref('explore'); -const tags = ref([]); const tagsRef = ref(); -const recentPostsPagination = { - endpoint: 'gallery/posts' as const, +const recentPostsPaginator = markRaw(new Paginator('gallery/posts', { limit: 6, -}; -const popularPostsPagination = { - endpoint: 'gallery/featured' as const, +})); +const popularPostsPaginator = markRaw(new Paginator('gallery/featured', { noPaging: true, -}; -const myPostsPagination = { - endpoint: 'i/gallery/posts' as const, +})); +const myPostsPaginator = markRaw(new Paginator('i/gallery/posts', { limit: 5, -}; -const likedPostsPagination = { - endpoint: 'i/gallery/likes' as const, +})); +const likedPostsPaginator = markRaw(new Paginator('i/gallery/likes', { limit: 5, -}; - -const tagUsersPagination = computed(() => ({ - endpoint: 'hashtags/users' as const, - limit: 30, - params: { - tag: props.tag, - origin: 'combined', - sort: '+follower', - }, })); watch(() => props.tag, () => { diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue index 3db003d9e2..d02b72dd99 100644 --- a/packages/frontend/src/pages/gallery/post.vue +++ b/packages/frontend/src/pages/gallery/post.vue @@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkContainer :max-height="300" :foldable="true" class="other"> <template #icon><i class="ti ti-clock"></i></template> <template #header>{{ i18n.ts.recentPosts }}</template> - <MkPagination v-slot="{items}" :pagination="otherPostsPagination"> + <MkPagination v-slot="{items}" :paginator="otherPostsPaginator"> <div class="sdrarzaf"> <MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> </div> @@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, ref, defineAsyncComponent } from 'vue'; +import { computed, watch, ref, defineAsyncComponent, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import { url } from '@@/js/config.js'; import type { MenuItem } from '@/types/menu.js'; @@ -80,6 +80,7 @@ import { $i } from '@/i.js'; import { isSupportShare } from '@/utility/navigator.js'; import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { useRouter } from '@/router.js'; +import { Paginator } from '@/utility/paginator.js'; const router = useRouter(); @@ -89,13 +90,12 @@ const props = defineProps<{ const post = ref<Misskey.entities.GalleryPost | null>(null); const error = ref<any>(null); -const otherPostsPagination = { - endpoint: 'users/gallery/posts' as const, +const otherPostsPaginator = markRaw(new Paginator('users/gallery/posts', { limit: 6, - params: computed(() => ({ + computedParams: computed(() => ({ userId: post.value.user.id, })), -}; +})); function fetchPost() { post.value = null; diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 7c5191276d..14a64f0bd5 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -115,7 +115,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div v-else-if="tab === 'users'" class="_gaps_m"> - <MkPagination v-slot="{ items }" :pagination="usersPagination"> + <MkPagination v-slot="{ items }" :paginator="usersPaginator"> <div :class="$style.users"> <MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${user.updatedAt ? dateString(user.updatedAt) : 'unknown'}`" :to="`/admin/user/${user.id}`"> <MkUserCardMini :user="user"/> @@ -132,10 +132,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, computed, watch } from 'vue'; +import { ref, computed, watch, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import type { ChartSrc } from '@/components/MkChart.vue'; -import type { PagingCtx } from '@/composables/use-pagination.js'; import MkChart from '@/components/MkChart.vue'; import MkObjectView from '@/components/MkObjectView.vue'; import FormLink from '@/components/form/link.vue'; @@ -156,6 +155,7 @@ import MkPagination from '@/components/MkPagination.vue'; import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js'; import { dateString } from '@/filters/date.js'; import MkTextarea from '@/components/MkTextarea.vue'; +import { Paginator } from '@/utility/paginator.js'; const props = defineProps<{ host: string; @@ -173,8 +173,7 @@ const isMediaSilenced = ref(false); const faviconUrl = ref<string | null>(null); const moderationNote = ref(''); -const usersPagination = { - endpoint: iAmModerator ? 'admin/show-users' : 'users', +const usersPaginator = iAmModerator ? markRaw(new Paginator('admin/show-users', { limit: 10, params: { sort: '+updatedAt', @@ -182,7 +181,15 @@ const usersPagination = { hostname: props.host, }, offsetMode: true, -} satisfies PagingCtx<'admin/show-users' | 'users'>; +})) : markRaw(new Paginator('users', { + limit: 10, + params: { + sort: '+updatedAt', + state: 'all', + hostname: props.host, + }, + offsetMode: true, +})); if (iAmModerator) { watch(moderationNote, async () => { diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue index 98e3190e4b..d69b7984c0 100644 --- a/packages/frontend/src/pages/invite.vue +++ b/packages/frontend/src/pages/invite.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton inline primary rounded :disabled="currentInviteLimit !== null && currentInviteLimit <= 0" @click="create"><i class="ti ti-user-plus"></i> {{ i18n.ts.createInviteCode }}</MkButton> <div v-if="currentInviteLimit !== null">{{ i18n.tsx.createLimitRemaining({ limit: currentInviteLimit }) }}</div> - <MkPagination ref="pagingComponent" :pagination="pagination"> + <MkPagination :paginator="paginator"> <template #default="{ items }"> <div class="_gaps_s"> <MkInviteCode v-for="item in (items as Misskey.entities.InviteCode[])" :key="item.id" :invite="item" :onDeleted="deleted"/> @@ -27,9 +27,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, ref, useTemplateRef } from 'vue'; +import { computed, markRaw, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import type { PagingCtx } from '@/composables/use-pagination.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -39,16 +38,15 @@ import MkInviteCode from '@/components/MkInviteCode.vue'; import { definePage } from '@/page.js'; import { instance } from '@/instance.js'; import { $i } from '@/i.js'; +import { Paginator } from '@/utility/paginator.js'; -const pagingComponent = useTemplateRef('pagingComponent'); const currentInviteLimit = ref<null | number>(null); const inviteLimit = (($i != null && $i.policies.inviteLimit) || (($i == null && instance.policies.inviteLimit))) as number; const inviteLimitCycle = (($i != null && $i.policies.inviteLimitCycle) || ($i == null && instance.policies.inviteLimitCycle)) as number; -const pagination: PagingCtx = { - endpoint: 'invite/list' as const, +const paginator = markRaw(new Paginator('invite/list', { limit: 10, -}; +})); const resetCycle = computed<null | string>(() => { if (!inviteLimitCycle) return null; @@ -68,14 +66,12 @@ async function create() { text: ticket.code, }); - pagingComponent.value?.paginator.prepend(ticket); + paginator.prepend(ticket); update(); } function deleted(id: string) { - if (pagingComponent.value) { - pagingComponent.value.paginator.removeItem(id); - } + paginator.removeItem(id); update(); } diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue index 4477edf505..4c664a0951 100644 --- a/packages/frontend/src/pages/my-clips/index.vue +++ b/packages/frontend/src/pages/my-clips/index.vue @@ -12,44 +12,38 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="tab === 'my'" class="_gaps"> <MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> - <MkPagination v-slot="{ items }" ref="pagingComponent" :pagination="pagination" class="_gaps" withControl> + <MkPagination v-slot="{ items }" :paginator="paginator" class="_gaps" withControl> <MkClipPreview v-for="item in items" :key="item.id" :clip="item" :noUserInfo="true"/> </MkPagination> </div> - <div v-else-if="tab === 'favorites'" class="_gaps"> - <MkClipPreview v-for="item in favorites" :key="item.id" :clip="item"/> + <div v-else-if="tab === 'favorites'"> + <MkPagination v-slot="{ items }" :paginator="favoritesPaginator" class="_gaps" withControl> + <MkClipPreview v-for="item in items" :key="item.id" :clip="item" :noUserInfo="true"/> + </MkPagination> </div> </div> </PageWithHeader> </template> <script lang="ts" setup> -import { watch, ref, useTemplateRef, computed } from 'vue'; +import { watch, ref, computed, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import MkClipPreview from '@/components/MkClipPreview.vue'; import * as os from '@/os.js'; -import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { clipsCache } from '@/cache.js'; - -const pagination = { - endpoint: 'clips/list' as const, - noPaging: true, - limit: 10, -}; +import { Paginator } from '@/utility/paginator.js'; const tab = ref('my'); -const favorites = ref<Misskey.entities.Clip[] | null>(null); - -const pagingComponent = useTemplateRef('pagingComponent'); +const paginator = markRaw(new Paginator('clips/list', { +})); -watch(tab, async () => { - favorites.value = await misskeyApi('clips/my-favorites'); -}); +const favoritesPaginator = markRaw(new Paginator('clips/my-favorites', { +})); async function create() { const { canceled, result } = await os.form(i18n.ts.createNewClip, { @@ -76,15 +70,15 @@ async function create() { clipsCache.delete(); - pagingComponent.value?.paginator.reload(); + paginator.reload(); } function onClipCreated() { - pagingComponent.value?.paginator.reload(); + paginator.reload(); } function onClipDeleted() { - pagingComponent.value?.paginator.reload(); + paginator.reload(); } const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index e33125ac93..74ac47c571 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_s"> <MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton> - <MkPagination ref="paginationEl" :pagination="membershipsPagination" withControl> + <MkPagination :paginator="membershipsPaginator" withControl> <template #default="{ items }"> <div class="_gaps_s"> <div v-for="item in items" :key="item.id"> @@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, ref, useTemplateRef, watch } from 'vue'; +import { computed, markRaw, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; @@ -69,6 +69,7 @@ import { ensureSignin } from '@/i.js'; import MkPagination from '@/components/MkPagination.vue'; import { mainRouter } from '@/router.js'; import { prefer } from '@/preferences.js'; +import { Paginator } from '@/utility/paginator.js'; const $i = ensureSignin(); @@ -80,17 +81,15 @@ const props = defineProps<{ listId: string; }>(); -const paginationEl = useTemplateRef('paginationEl'); const list = ref<Misskey.entities.UserList | null>(null); const isPublic = ref(false); const name = ref(''); -const membershipsPagination = { - endpoint: 'users/lists/get-memberships' as const, +const membershipsPaginator = markRaw(new Paginator('users/lists/get-memberships', { limit: 30, - params: computed(() => ({ + computedParams: computed(() => ({ listId: props.listId, })), -}; +})); function fetchList() { misskeyApi('users/lists/show', { @@ -109,7 +108,7 @@ function addUser() { listId: list.value.id, userId: user.id, }).then(() => { - paginationEl.value?.paginator.reload(); + membershipsPaginator.reload(); }); }); } @@ -125,7 +124,7 @@ async function removeUser(item, ev) { listId: list.value.id, userId: item.userId, }).then(() => { - paginationEl.value?.paginator.removeItem(item.id); + membershipsPaginator.removeItem(item.id); }); }, }], ev.currentTarget ?? ev.target); @@ -147,7 +146,7 @@ async function showMembershipMenu(item, ev) { userId: item.userId, withReplies, }).then(() => { - paginationEl.value!.paginator.updateItem(item.id, (old) => ({ + membershipsPaginator.updateItem(item.id, (old) => ({ ...old, withReplies, })); diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue index ccb62749fa..b128cf5312 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in"> <div v-if="note"> <div v-if="showNext" class="_margin"> - <MkNotesTimeline :withControl="false" :pullToRefresh="false" class="" :pagination="showNext === 'channel' ? nextChannelPagination : nextUserPagination" :noGap="true" :disableAutoLoad="true"/> + <MkNotesTimeline :withControl="false" :pullToRefresh="false" class="" :paginator="showNext === 'channel' ? nextChannelPaginator : nextUserPaginator" :noGap="true"/> </div> <div class="_margin"> @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-if="showPrev" class="_margin"> - <MkNotesTimeline :withControl="false" :pullToRefresh="false" class="" :pagination="showPrev === 'channel' ? prevChannelPagination : prevUserPagination" :noGap="true"/> + <MkNotesTimeline :withControl="false" :pullToRefresh="false" class="" :paginator="showPrev === 'channel' ? prevChannelPaginator : prevUserPaginator" :noGap="true"/> </div> </div> <MkError v-else-if="error" @retry="fetchNote()"/> @@ -45,10 +45,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, ref } from 'vue'; +import { computed, watch, ref, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import { host } from '@@/js/config.js'; -import type { PagingCtx } from '@/composables/use-pagination.js'; import MkNoteDetailed from '@/components/MkNoteDetailed.vue'; import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; import MkRemoteCaution from '@/components/MkRemoteCaution.vue'; @@ -63,6 +62,7 @@ import { pleaseLogin } from '@/utility/please-login.js'; import { getAppearNote } from '@/utility/get-appear-note.js'; import { serverContext, assertServerContext } from '@/server-context.js'; import { $i } from '@/i.js'; +import { Paginator } from '@/utility/paginator.js'; // contextは非ログイン状態の情報しかないためログイン時は利用できない const CTX_NOTE = !$i && assertServerContext(serverContext, 'note') ? serverContext.note : null; @@ -78,45 +78,41 @@ const showPrev = ref<'user' | 'channel' | false>(false); const showNext = ref<'user' | 'channel' | false>(false); const error = ref(); -const prevUserPagination: PagingCtx = { - endpoint: 'users/notes', +const prevUserPaginator = markRaw(new Paginator('users/notes', { limit: 10, initialId: props.noteId, initialDirection: 'older', - params: computed(() => note.value ? ({ + computedParams: computed(() => note.value ? ({ userId: note.value.userId, }) : undefined), -}; +})); -const nextUserPagination: PagingCtx = { - endpoint: 'users/notes', +const nextUserPaginator = markRaw(new Paginator('users/notes', { limit: 10, initialId: props.noteId, initialDirection: 'newer', - params: computed(() => note.value ? ({ + computedParams: computed(() => note.value ? ({ userId: note.value.userId, }) : undefined), -}; +})); -const prevChannelPagination: PagingCtx = { - endpoint: 'channels/timeline', +const prevChannelPaginator = markRaw(new Paginator('channels/timeline', { limit: 10, initialId: props.noteId, initialDirection: 'older', - params: computed(() => note.value ? ({ + computedParams: computed(() => note.value ? ({ channelId: note.value.channelId, }) : undefined), -}; +})); -const nextChannelPagination: PagingCtx = { - endpoint: 'channels/timeline', +const nextChannelPaginator = markRaw(new Paginator('channels/timeline', { limit: 10, initialId: props.noteId, initialDirection: 'newer', - params: computed(() => note.value ? ({ + computedParams: computed(() => note.value ? ({ channelId: note.value.channelId, }) : undefined), -}; +})); function fetchNote() { showPrev.value = false; diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index db911c1202..a8c1fb654c 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -10,40 +10,39 @@ SPDX-License-Identifier: AGPL-3.0-only <MkStreamingNotificationsTimeline :class="$style.notifications" :excludeTypes="excludeTypes"/> </div> <div v-else-if="tab === 'mentions'"> - <MkNotesTimeline :pagination="mentionsPagination"/> + <MkNotesTimeline :paginator="mentionsPaginator"/> </div> <div v-else-if="tab === 'directNotes'"> - <MkNotesTimeline :pagination="directNotesPagination"/> + <MkNotesTimeline :paginator="directNotesPaginator"/> </div> </div> </PageWithHeader> </template> <script lang="ts" setup> -import { computed, ref } from 'vue'; +import { computed, markRaw, ref } from 'vue'; import { notificationTypes } from '@@/js/const.js'; import MkStreamingNotificationsTimeline from '@/components/MkStreamingNotificationsTimeline.vue'; import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; +import { Paginator } from '@/utility/paginator.js'; const tab = ref('all'); const includeTypes = ref<string[] | null>(null); const excludeTypes = computed(() => includeTypes.value ? notificationTypes.filter(t => !includeTypes.value.includes(t)) : null); -const mentionsPagination = { - endpoint: 'notes/mentions' as const, +const mentionsPaginator = markRaw(new Paginator('notes/mentions', { limit: 10, -}; +})); -const directNotesPagination = { - endpoint: 'notes/mentions' as const, +const directNotesPaginator = markRaw(new Paginator('notes/mentions', { limit: 10, params: { visibility: 'specified', }, -}; +})); function setFilter(ev) { const typeItems = notificationTypes.map(t => ({ diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index 2cd8718968..99c147c8cf 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -84,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkContainer :max-height="300" :foldable="true" class="other"> <template #icon><i class="ti ti-clock"></i></template> <template #header>{{ i18n.ts.recentPosts }}</template> - <MkPagination v-slot="{items}" :pagination="otherPostsPagination" :class="$style.relatedPagesRoot" class="_gaps"> + <MkPagination v-slot="{items}" :paginator="otherPostsPaginator" :class="$style.relatedPagesRoot" class="_gaps"> <MkPagePreview v-for="page in items" :key="page.id" :page="page" :class="$style.relatedPagesItem"/> </MkPagination> </MkContainer> @@ -97,7 +97,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, ref, defineAsyncComponent } from 'vue'; +import { computed, watch, ref, defineAsyncComponent, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import { url } from '@@/js/config.js'; import type { MenuItem } from '@/types/menu.js'; @@ -122,6 +122,7 @@ import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { useRouter } from '@/router.js'; import { prefer } from '@/preferences.js'; import { getPluginHandlers } from '@/plugin.js'; +import { Paginator } from '@/utility/paginator.js'; const router = useRouter(); @@ -132,13 +133,12 @@ const props = defineProps<{ const page = ref<Misskey.entities.Page | null>(null); const error = ref<any>(null); -const otherPostsPagination = { - endpoint: 'users/pages' as const, +const otherPostsPaginator = markRaw(new Paginator('users/pages', { limit: 6, - params: computed(() => ({ + computedParams: computed(() => ({ userId: page.value.user.id, })), -}; +})); const path = computed(() => props.username + '/' + props.pageName); function fetchPage() { diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue index 880c4deb25..780f0836a7 100644 --- a/packages/frontend/src/pages/pages.vue +++ b/packages/frontend/src/pages/pages.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :swipable="true"> <div class="_spacer" style="--MI_SPACER-w: 700px;"> <div v-if="tab === 'featured'"> - <MkPagination v-slot="{items}" :pagination="featuredPagesPagination"> + <MkPagination v-slot="{items}" :paginator="featuredPagesPaginator"> <div class="_gaps"> <MkPagePreview v-for="page in items" :key="page.id" :page="page"/> </div> @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-else-if="tab === 'my'" class="_gaps"> <MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton> - <MkPagination v-slot="{items}" :pagination="myPagesPagination"> + <MkPagination v-slot="{items}" :paginator="myPagesPaginator"> <div class="_gaps"> <MkPagePreview v-for="page in items" :key="page.id" :page="page"/> </div> @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-else-if="tab === 'liked'"> - <MkPagination v-slot="{items}" :pagination="likedPagesPagination"> + <MkPagination v-slot="{items}" :paginator="likedPagesPaginator"> <div class="_gaps"> <MkPagePreview v-for="like in items" :key="like.page.id" :page="like.page"/> </div> @@ -35,30 +35,28 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, ref } from 'vue'; +import { computed, markRaw, ref } from 'vue'; import MkPagePreview from '@/components/MkPagePreview.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { useRouter } from '@/router.js'; +import { Paginator } from '@/utility/paginator.js'; const router = useRouter(); const tab = ref('featured'); -const featuredPagesPagination = { - endpoint: 'pages/featured' as const, +const featuredPagesPaginator = markRaw(new Paginator('pages/featured', { noPaging: true, -}; -const myPagesPagination = { - endpoint: 'i/pages' as const, +})); +const myPagesPaginator = markRaw(new Paginator('i/pages', { limit: 5, -}; -const likedPagesPagination = { - endpoint: 'i/page-likes' as const, +})); +const likedPagesPaginator = markRaw(new Paginator('i/page-likes', { limit: 5, -}; +})); function create() { router.push('/pages/new'); diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue index f3252402d7..e4d921b8d2 100644 --- a/packages/frontend/src/pages/reversi/index.vue +++ b/packages/frontend/src/pages/reversi/index.vue @@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder v-if="$i" :defaultOpen="true"> <template #label>{{ i18n.ts._reversi.myGames }}</template> - <MkPagination :pagination="myGamesPagination" :disableAutoLoad="true"> + <MkPagination :paginator="myGamesPaginator"> <template #default="{ items }"> <div :class="$style.gamePreviews"> <MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`"> @@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder :defaultOpen="true"> <template #label>{{ i18n.ts._reversi.allGames }}</template> - <MkPagination :pagination="gamesPagination" :disableAutoLoad="true"> + <MkPagination :paginator="gamesPaginator"> <template #default="{ items }"> <div :class="$style.gamePreviews"> <MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`"> @@ -105,7 +105,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onDeactivated, onMounted, onUnmounted, ref } from 'vue'; +import { markRaw, onDeactivated, onMounted, onUnmounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { useInterval } from '@@/js/use-interval.js'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -120,19 +120,18 @@ import { useRouter } from '@/router.js'; import * as os from '@/os.js'; import { pleaseLogin } from '@/utility/please-login.js'; import * as sound from '@/utility/sound.js'; +import { Paginator } from '@/utility/paginator.js'; -const myGamesPagination = { - endpoint: 'reversi/games' as const, +const myGamesPaginator = markRaw(new Paginator('reversi/games', { limit: 10, params: { my: true, }, -}; +})); -const gamesPagination = { - endpoint: 'reversi/games' as const, +const gamesPaginator = markRaw(new Paginator('reversi/games', { limit: 10, -}; +})); const router = useRouter(); diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue index 42639cde9e..619c80edd1 100644 --- a/packages/frontend/src/pages/role.vue +++ b/packages/frontend/src/pages/role.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-else-if="tab === 'users'" class="_spacer" style="--MI_SPACER-w: 1200px;"> <div class="_gaps_s"> <div v-if="role">{{ role.description }}</div> - <MkUserList v-if="visible" :pagination="users" :extractor="(item) => item.user"/> + <MkUserList v-if="visible" :paginator="usersPaginator" :extractor="(item) => item.user"/> <MkResult v-else-if="!visible" type="empty" :text="i18n.ts.nothing"/> </div> </div> @@ -23,13 +23,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, ref } from 'vue'; +import { computed, watch, ref, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import { misskeyApi } from '@/utility/misskey-api.js'; import MkUserList from '@/components/MkUserList.vue'; import { definePage } from '@/page.js'; import { i18n } from '@/i18n.js'; import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue'; +import { Paginator } from '@/utility/paginator.js'; const props = withDefaults(defineProps<{ roleId: string; @@ -60,12 +61,11 @@ watch(() => props.roleId, () => { }); }, { immediate: true }); -const users = computed(() => ({ - endpoint: 'roles/users' as const, +const usersPaginator = markRaw(new Paginator('roles/users', { limit: 30, - params: { + computedParams: computed(() => ({ roleId: props.roleId, - }, + })), })); const headerTabs = computed(() => [{ diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue index 352564bc9c..f19c1e7efb 100644 --- a/packages/frontend/src/pages/search.note.vue +++ b/packages/frontend/src/pages/search.note.vue @@ -103,19 +103,18 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> - <MkFoldableSection v-if="notePagination"> + <MkFoldableSection v-if="paginator"> <template #header>{{ i18n.ts.searchResult }}</template> - <MkNotesTimeline :key="`searchNotes:${key}`" :pagination="notePagination"/> + <MkNotesTimeline :key="`searchNotes:${key}`" :paginator="paginator"/> </MkFoldableSection> </div> </template> <script lang="ts" setup> -import { computed, ref, shallowRef, toRef } from 'vue'; +import { computed, markRaw, ref, shallowRef, toRef } from 'vue'; +import { host as localHost } from '@@/js/config.js'; import type * as Misskey from 'misskey-js'; -import type { PagingCtx } from '@/composables/use-pagination.js'; import { $i } from '@/i.js'; -import { host as localHost } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import * as os from '@/os.js'; @@ -128,6 +127,7 @@ import MkInput from '@/components/MkInput.vue'; import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; +import { Paginator } from '@/utility/paginator.js'; const props = withDefaults(defineProps<{ query?: string; @@ -144,7 +144,7 @@ const props = withDefaults(defineProps<{ const router = useRouter(); const key = ref(0); -const notePagination = ref<PagingCtx<'notes/search'>>(); +const paginator = shallowRef<Paginator<'notes/search'> | null>(null); const searchQuery = ref(toRef(props, 'query').value); const hostInput = ref(toRef(props, 'host').value); @@ -299,13 +299,12 @@ async function search() { } } - notePagination.value = { - endpoint: 'notes/search', + paginator.value = markRaw(new Paginator('notes/search', { limit: 10, params: { ...searchParams.value, }, - }; + })); key.value++; } diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue index eaec043311..bd67d41a80 100644 --- a/packages/frontend/src/pages/search.user.vue +++ b/packages/frontend/src/pages/search.user.vue @@ -17,17 +17,16 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton> </div> - <MkFoldableSection v-if="userPagination"> + <MkFoldableSection v-if="paginator"> <template #header>{{ i18n.ts.searchResult }}</template> - <MkUserList :key="`searchUsers:${key}`" :pagination="userPagination"/> + <MkUserList :key="`searchUsers:${key}`" :paginator="paginator"/> </MkFoldableSection> </div> </template> <script lang="ts" setup> -import { ref, toRef } from 'vue'; +import { markRaw, ref, shallowRef, toRef } from 'vue'; import type { Endpoints } from 'misskey-js'; -import type { PagingCtx } from '@/composables/use-pagination.js'; import MkUserList from '@/components/MkUserList.vue'; import MkInput from '@/components/MkInput.vue'; import MkRadios from '@/components/MkRadios.vue'; @@ -38,6 +37,7 @@ import * as os from '@/os.js'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import { misskeyApi } from '@/utility/misskey-api.js'; import { useRouter } from '@/router.js'; +import { Paginator } from '@/utility/paginator.js'; const props = withDefaults(defineProps<{ query?: string, @@ -50,7 +50,7 @@ const props = withDefaults(defineProps<{ const router = useRouter(); const key = ref(0); -const userPagination = ref<PagingCtx<'users/search'>>(); +const paginator = shallowRef<Paginator<'users/search'> | null>(null); const searchQuery = ref(toRef(props, 'query').value); const searchOrigin = ref(toRef(props, 'origin').value); @@ -112,15 +112,14 @@ async function search() { } } - userPagination.value = { - endpoint: 'users/search', + paginator.value = markRaw(new Paginator('users/search', { limit: 10, offsetMode: true, params: { query: query, origin: instance.federation === 'none' ? 'local' : searchOrigin.value, }, - }; + })); key.value++; } diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue index ec45eb3487..5f51a5e079 100644 --- a/packages/frontend/src/pages/settings/apps.vue +++ b/packages/frontend/src/pages/settings/apps.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps_m"> - <FormPagination ref="list" :pagination="pagination"> + <MkPagination :paginator="paginator"> <template #empty><MkResult type="empty"/></template> <template #default="{items}"> <div class="_gaps"> @@ -44,35 +44,33 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> </div> </template> - </FormPagination> + </MkPagination> </div> </template> <script lang="ts" setup> -import { ref, computed, useTemplateRef } from 'vue'; +import { ref, computed, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; -import FormPagination from '@/components/MkPagination.vue'; +import MkPagination from '@/components/MkPagination.vue'; import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import MkKeyValue from '@/components/MkKeyValue.vue'; import MkButton from '@/components/MkButton.vue'; import MkFolder from '@/components/MkFolder.vue'; +import { Paginator } from '@/utility/paginator.js'; -const list = useTemplateRef('list'); - -const pagination = { - endpoint: 'i/apps' as const, +const paginator = markRaw(new Paginator('i/apps', { limit: 100, noPaging: true, params: { sort: '+lastUsedAt', }, -}; +})); function revoke(token) { misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => { - list.value?.paginator.reload(); + paginator.reload(); }); } diff --git a/packages/frontend/src/pages/settings/connect.vue b/packages/frontend/src/pages/settings/connect.vue index c2335ae69f..1e701096c5 100644 --- a/packages/frontend/src/pages/settings/connect.vue +++ b/packages/frontend/src/pages/settings/connect.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder :defaultOpen="true"> <template #label>{{ i18n.ts.manage }}</template> - <MkPagination :pagination="pagination" withControl> + <MkPagination :paginator="paginator" withControl> <template #default="{items}"> <div class="_gaps"> <FormLink v-for="webhook in items" :key="webhook.id" :to="`/settings/webhook/edit/${webhook.id}`"> @@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, ref, defineAsyncComponent } from 'vue'; +import { computed, ref, defineAsyncComponent, markRaw } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; import FormSection from '@/components/form/section.vue'; import FormLink from '@/components/form/link.vue'; @@ -72,14 +72,14 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import MkButton from '@/components/MkButton.vue'; import MkFolder from '@/components/MkFolder.vue'; +import { Paginator } from '@/utility/paginator.js'; const isDesktop = ref(window.innerWidth >= 1100); -const pagination = { - endpoint: 'i/webhooks/list' as const, +const paginator = markRaw(new Paginator('i/webhooks/list', { limit: 100, noPaging: true, -}; +})); async function generateToken() { const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkTokenGenerateWindow.vue').then(x => x.default), {}, { diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue index 6b73560174..24f0fa5153 100644 --- a/packages/frontend/src/pages/settings/drive-cleaner.vue +++ b/packages/frontend/src/pages/settings/drive-cleaner.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <option v-for="x in sortOptions" :key="x.value" :value="x.value">{{ x.displayName }}</option> </MkSelect> <div v-if="!fetching"> - <MkPagination v-slot="{items}" :pagination="pagination"> + <MkPagination v-slot="{items}" :paginator="paginator"> <div class="_gaps"> <div v-for="file in items" :key="file.id" @@ -48,9 +48,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script setup lang="ts"> -import { computed, ref, watch } from 'vue'; -import type { StyleValue } from 'vue'; +import { computed, markRaw, ref, watch } from 'vue'; import tinycolor from 'tinycolor2'; +import type { StyleValue } from 'vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import MkPagination from '@/components/MkPagination.vue'; @@ -60,13 +60,13 @@ import bytes from '@/filters/bytes.js'; import { definePage } from '@/page.js'; import MkSelect from '@/components/MkSelect.vue'; import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js'; +import { Paginator } from '@/utility/paginator.js'; const sortMode = ref('+size'); -const pagination = { - endpoint: 'drive/files' as const, +const paginator = markRaw(new Paginator('drive/files', { limit: 10, - params: computed(() => ({ sort: sortMode.value })), -}; + computedParams: computed(() => ({ sort: sortMode.value })), +})); const sortOptions = [ { value: 'sizeDesc', displayName: i18n.ts._drivecleaner.orderBySizeDesc }, diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index 755bc79b6a..3002cd0e89 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -80,7 +80,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ti ti-repeat-off"></i></template> <template #label><SearchLabel>{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</SearchLabel></template> - <MkPagination :pagination="renoteMutingPagination" withControl> + <MkPagination :paginator="renoteMutingPaginator" withControl> <template #empty><MkResult type="empty" :text="i18n.ts.noUsers"/></template> <template #default="{ items }"> @@ -111,7 +111,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ti ti-eye-off"></i></template> <template #label>{{ i18n.ts.mutedUsers }}</template> - <MkPagination :pagination="mutingPagination" withControl> + <MkPagination :paginator="mutingPaginator" withControl> <template #empty><MkResult type="empty" :text="i18n.ts.noUsers"/></template> <template #default="{ items }"> @@ -144,7 +144,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ti ti-ban"></i></template> <template #label>{{ i18n.ts.blockedUsers }}</template> - <MkPagination :pagination="blockingPagination" withControl> + <MkPagination :paginator="blockingPaginator" withControl> <template #empty><MkResult type="empty" :text="i18n.ts.noUsers"/></template> <template #default="{ items }"> @@ -174,7 +174,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, computed, watch } from 'vue'; +import { ref, computed, watch, markRaw } from 'vue'; import XEmojiMute from './mute-block.emoji-mute.vue'; import XInstanceMute from './mute-block.instance-mute.vue'; import XWordMute from './mute-block.word-mute.vue'; @@ -192,23 +192,21 @@ import MkSwitch from '@/components/MkSwitch.vue'; import { reloadAsk } from '@/utility/reload-ask.js'; import { prefer } from '@/preferences.js'; import MkFeatureBanner from '@/components/MkFeatureBanner.vue'; +import { Paginator } from '@/utility/paginator.js'; const $i = ensureSignin(); -const renoteMutingPagination = { - endpoint: 'renote-mute/list' as const, +const renoteMutingPaginator = markRaw(new Paginator('renote-mute/list', { limit: 10, -}; +})); -const mutingPagination = { - endpoint: 'mute/list' as const, +const mutingPaginator = markRaw(new Paginator('mute/list', { limit: 10, -}; +})); -const blockingPagination = { - endpoint: 'blocking/list' as const, +const blockingPaginator = markRaw(new Paginator('blocking/list', { limit: 10, -}; +})); const expandedRenoteMuteItems = ref([]); const expandedMuteItems = ref([]); diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue index c260ae4541..2562993be3 100644 --- a/packages/frontend/src/pages/settings/security.vue +++ b/packages/frontend/src/pages/settings/security.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection> <template #label>{{ i18n.ts.signinHistory }}</template> - <MkPagination :pagination="pagination" disableAutoLoad withControl> + <MkPagination :paginator="paginator" withControl> <template #default="{items}"> <div> <div v-for="item in items" :key="item.id" v-panel class="timnmucd"> @@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, markRaw } from 'vue'; import X2fa from './2fa.vue'; import FormSection from '@/components/form/section.vue'; import FormSlot from '@/components/form/slot.vue'; @@ -64,11 +64,11 @@ import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import MkFeatureBanner from '@/components/MkFeatureBanner.vue'; +import { Paginator } from '@/utility/paginator.js'; -const pagination = { - endpoint: 'i/signin-history' as const, +const paginator = markRaw(new Paginator('i/signin-history', { limit: 5, -}; +})); async function change() { const { canceled: canceled2, result: newPassword } = await os.inputText({ diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue index d1e5db5a5b..b5a4503b68 100644 --- a/packages/frontend/src/pages/tag.vue +++ b/packages/frontend/src/pages/tag.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <PageWithHeader :actions="headerActions" :tabs="headerTabs"> <div class="_spacer" style="--MI_SPACER-w: 800px;"> - <MkNotesTimeline ref="tlComponent" class="" :pagination="pagination"/> + <MkNotesTimeline :paginator="paginator"/> </div> <template v-if="$i" #footer> <div :class="$style.footer"> @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, ref, useTemplateRef } from 'vue'; +import { computed, markRaw, ref } from 'vue'; import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; import MkButton from '@/components/MkButton.vue'; import { definePage } from '@/page.js'; @@ -28,20 +28,18 @@ import { $i } from '@/i.js'; import { store } from '@/store.js'; import * as os from '@/os.js'; import { genEmbedCode } from '@/utility/get-embed-code.js'; +import { Paginator } from '@/utility/paginator.js'; const props = defineProps<{ tag: string; }>(); -const pagination = { - endpoint: 'notes/search-by-tag' as const, +const paginator = markRaw(new Paginator('notes/search-by-tag', { limit: 10, - params: computed(() => ({ + computedParams: computed(() => ({ tag: props.tag, })), -}; - -const tlComponent = useTemplateRef('tlComponent'); +})); async function post() { store.set('postFormHashtags', props.tag); @@ -49,7 +47,7 @@ async function post() { await os.post(); store.set('postFormHashtags', ''); store.set('postFormWithHashtags', false); - tlComponent.value?.reload(); + paginator.reload(); } const headerActions = computed(() => [{ diff --git a/packages/frontend/src/pages/user-tag.vue b/packages/frontend/src/pages/user-tag.vue index 959d449e40..ec4c854381 100644 --- a/packages/frontend/src/pages/user-tag.vue +++ b/packages/frontend/src/pages/user-tag.vue @@ -7,29 +7,29 @@ SPDX-License-Identifier: AGPL-3.0-only <PageWithHeader> <div class="_spacer" style="--MI_SPACER-w: 1200px;"> <div class="_gaps_s"> - <MkUserList :pagination="tagUsers"/> + <MkUserList :paginator="paginator"/> </div> </div> </PageWithHeader> </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, markRaw } from 'vue'; import MkUserList from '@/components/MkUserList.vue'; import { definePage } from '@/page.js'; +import { Paginator } from '@/utility/paginator.js'; const props = defineProps<{ tag: string; }>(); -const tagUsers = computed(() => ({ - endpoint: 'hashtags/users' as const, +const paginator = markRaw(new Paginator('hashtags/users', { limit: 30, - params: { + computedParams: computed(() => ({ tag: props.tag, origin: 'combined', sort: '+follower', - }, + })), })); definePage(() => ({ diff --git a/packages/frontend/src/pages/user/clips.vue b/packages/frontend/src/pages/user/clips.vue index 5b5bc3193f..57f7bc13a1 100644 --- a/packages/frontend/src/pages/user/clips.vue +++ b/packages/frontend/src/pages/user/clips.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_spacer" style="--MI_SPACER-w: 700px;"> <div> - <MkPagination v-slot="{items}" :pagination="pagination" withControl> + <MkPagination v-slot="{items}" :paginator="paginator" withControl> <MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" :class="$style.item" class="_panel _margin"> <b>{{ item.name }}</b> <div v-if="item.description" :class="$style.description">{{ item.description }}</div> @@ -17,21 +17,21 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import MkPagination from '@/components/MkPagination.vue'; +import { Paginator } from '@/utility/paginator.js'; const props = defineProps<{ user: Misskey.entities.User; }>(); -const pagination = { - endpoint: 'users/clips' as const, +const paginator = markRaw(new Paginator('users/clips', { limit: 20, - params: computed(() => ({ + computedParams: computed(() => ({ userId: props.user.id, })), -}; +})); </script> <style lang="scss" module> diff --git a/packages/frontend/src/pages/user/files.vue b/packages/frontend/src/pages/user/files.vue index 339daea257..e241dc8931 100644 --- a/packages/frontend/src/pages/user/files.vue +++ b/packages/frontend/src/pages/user/files.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_spacer" style="--MI_SPACER-w: 1100px;"> <div :class="$style.root"> - <MkPagination v-slot="{items}" :pagination="pagination" withControl> + <MkPagination v-slot="{items}" :paginator="paginator" withControl> <div :class="$style.stream"> <MkNoteMediaGrid v-for="note in items" :note="note" square/> </div> @@ -16,24 +16,23 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; - import MkNoteMediaGrid from '@/components/MkNoteMediaGrid.vue'; import MkPagination from '@/components/MkPagination.vue'; +import { Paginator } from '@/utility/paginator.js'; const props = defineProps<{ user: Misskey.entities.UserDetailed; }>(); -const pagination = { - endpoint: 'users/notes' as const, +const paginator = markRaw(new Paginator('users/notes', { limit: 15, - params: computed(() => ({ + computedParams: computed(() => ({ userId: props.user.id, withFiles: true, })), -}; +})); </script> <style lang="scss" module> diff --git a/packages/frontend/src/pages/user/flashs.vue b/packages/frontend/src/pages/user/flashs.vue index fc1d4e5968..034638fd0e 100644 --- a/packages/frontend/src/pages/user/flashs.vue +++ b/packages/frontend/src/pages/user/flashs.vue @@ -5,27 +5,27 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_spacer" style="--MI_SPACER-w: 700px;"> - <MkPagination v-slot="{items}" :pagination="pagination" withControl> + <MkPagination v-slot="{items}" :paginator="paginator" withControl> <MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash" class="_margin"/> </MkPagination> </div> </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import MkFlashPreview from '@/components/MkFlashPreview.vue'; import MkPagination from '@/components/MkPagination.vue'; +import { Paginator } from '@/utility/paginator.js'; const props = defineProps<{ user: Misskey.entities.User; }>(); -const pagination = { - endpoint: 'users/flashs' as const, +const paginator = markRaw(new Paginator('users/flashs', { limit: 20, - params: computed(() => ({ + computedParams: computed(() => ({ userId: props.user.id, })), -}; +})); </script> diff --git a/packages/frontend/src/pages/user/follow-list.vue b/packages/frontend/src/pages/user/follow-list.vue index 2c3eb40f72..6bb1360d42 100644 --- a/packages/frontend/src/pages/user/follow-list.vue +++ b/packages/frontend/src/pages/user/follow-list.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div> - <MkPagination v-slot="{items}" :pagination="type === 'following' ? followingPagination : followersPagination" withControl> + <MkPagination v-slot="{items}" :paginator="type === 'following' ? followingPaginator : followersPaginator" withControl> <div :class="$style.users"> <MkUserInfo v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" :user="user"/> </div> @@ -14,31 +14,30 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import MkUserInfo from '@/components/MkUserInfo.vue'; import MkPagination from '@/components/MkPagination.vue'; +import { Paginator } from '@/utility/paginator.js'; const props = defineProps<{ user: Misskey.entities.User; type: 'following' | 'followers'; }>(); -const followingPagination = { - endpoint: 'users/following' as const, +const followingPaginator = markRaw(new Paginator('users/following', { limit: 20, - params: computed(() => ({ + computedParams: computed(() => ({ userId: props.user.id, })), -}; +})); -const followersPagination = { - endpoint: 'users/followers' as const, +const followersPaginator = markRaw(new Paginator('users/followers', { limit: 20, - params: computed(() => ({ + computedParams: computed(() => ({ userId: props.user.id, })), -}; +})); </script> <style lang="scss" module> diff --git a/packages/frontend/src/pages/user/gallery.vue b/packages/frontend/src/pages/user/gallery.vue index 12a9d2b1ab..cabba34553 100644 --- a/packages/frontend/src/pages/user/gallery.vue +++ b/packages/frontend/src/pages/user/gallery.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_spacer" style="--MI_SPACER-w: 700px;"> - <MkPagination v-slot="{items}" :pagination="pagination" withControl> + <MkPagination v-slot="{items}" :paginator="paginator" withControl> <div :class="$style.root"> <MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> </div> @@ -14,23 +14,23 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue'; import MkPagination from '@/components/MkPagination.vue'; +import { Paginator } from '@/utility/paginator.js'; const props = withDefaults(defineProps<{ user: Misskey.entities.User; }>(), { }); -const pagination = { - endpoint: 'users/gallery/posts' as const, +const paginator = markRaw(new Paginator('users/gallery/posts', { limit: 6, - params: computed(() => ({ + computedParams: computed(() => ({ userId: props.user.id, })), -}; +})); </script> <style lang="scss" module> diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue index d8eca07a42..5e9e671252 100644 --- a/packages/frontend/src/pages/user/index.timeline.vue +++ b/packages/frontend/src/pages/user/index.timeline.vue @@ -13,16 +13,18 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="files">{{ i18n.ts.withFiles }}</option> </MkTab> </template> - <MkNotesTimeline :key="tab" :noGap="true" :pagination="pagination" :pullToRefresh="false" :class="$style.tl"/> + <MkNotesTimeline v-if="tab === 'featured'" :noGap="true" :paginator="featuredPaginator" :pullToRefresh="false" :class="$style.tl"/> + <MkNotesTimeline v-else :noGap="true" :paginator="notesPaginator" :pullToRefresh="false" :class="$style.tl"/> </MkStickyContainer> </template> <script lang="ts" setup> -import { ref, computed } from 'vue'; +import { ref, computed, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; import MkTab from '@/components/MkTab.vue'; import { i18n } from '@/i18n.js'; +import { Paginator } from '@/utility/paginator.js'; const props = defineProps<{ user: Misskey.entities.UserDetailed; @@ -30,23 +32,23 @@ const props = defineProps<{ const tab = ref<string>('all'); -const pagination = computed(() => tab.value === 'featured' ? { - endpoint: 'users/featured-notes' as const, +const featuredPaginator = markRaw(new Paginator('users/featured-notes', { limit: 10, params: { userId: props.user.id, }, -} : { - endpoint: 'users/notes' as const, +})); + +const notesPaginator = markRaw(new Paginator('users/notes', { limit: 10, - params: { + computedParams: computed(() => ({ userId: props.user.id, withRenotes: tab.value === 'all', withReplies: tab.value === 'all', withChannelNotes: tab.value === 'all', withFiles: tab.value === 'files', - }, -}); + })), +})); </script> <style lang="scss" module> diff --git a/packages/frontend/src/pages/user/lists.vue b/packages/frontend/src/pages/user/lists.vue index 5792c8faaf..6c9204ae22 100644 --- a/packages/frontend/src/pages/user/lists.vue +++ b/packages/frontend/src/pages/user/lists.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkStickyContainer> <div class="_spacer" style="--MI_SPACER-w: 700px;"> <div> - <MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" withControl> + <MkPagination v-slot="{items}" :paginator="paginator" withControl> <MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/list/${ list.id }`"> <div>{{ list.name }}</div> <MkAvatars :userIds="list.userIds"/> @@ -19,24 +19,24 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import {} from 'vue'; +import { markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import MkPagination from '@/components/MkPagination.vue'; import MkStickyContainer from '@/components/global/MkStickyContainer.vue'; import MkAvatars from '@/components/MkAvatars.vue'; +import { Paginator } from '@/utility/paginator.js'; const props = defineProps<{ user: Misskey.entities.UserDetailed; }>(); -const pagination = { - endpoint: 'users/lists/list' as const, +const paginator = markRaw(new Paginator('users/lists/list', { noPaging: true, limit: 10, params: { userId: props.user.id, }, -}; +})); </script> <style lang="scss" module> diff --git a/packages/frontend/src/pages/user/notes.vue b/packages/frontend/src/pages/user/notes.vue index c97177b6a5..b5e600da92 100644 --- a/packages/frontend/src/pages/user/notes.vue +++ b/packages/frontend/src/pages/user/notes.vue @@ -15,18 +15,20 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="files">{{ i18n.ts.withFiles }}</option> </MkTab> </template> - <MkNotesTimeline :key="tab" :noGap="true" :pagination="pagination" :class="$style.tl"/> + <MkNotesTimeline v-if="tab === 'featured'" :noGap="true" :paginator="featuredPaginator" :class="$style.tl"/> + <MkNotesTimeline v-else :noGap="true" :paginator="notesPaginator" :class="$style.tl"/> </MkStickyContainer> </div> </div> </template> <script lang="ts" setup> -import { ref, computed } from 'vue'; +import { ref, computed, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; import MkTab from '@/components/MkTab.vue'; import { i18n } from '@/i18n.js'; +import { Paginator } from '@/utility/paginator.js'; const props = defineProps<{ user: Misskey.entities.UserDetailed; @@ -34,23 +36,23 @@ const props = defineProps<{ const tab = ref<string>('all'); -const pagination = computed(() => tab.value === 'featured' ? { - endpoint: 'users/featured-notes' as const, +const featuredPaginator = markRaw(new Paginator('users/featured-notes', { limit: 10, params: { userId: props.user.id, }, -} : { - endpoint: 'users/notes' as const, +})); + +const notesPaginator = markRaw(new Paginator('users/notes', { limit: 10, - params: { + computedParams: computed(() => ({ userId: props.user.id, withRenotes: tab.value === 'all', withReplies: tab.value === 'all', withChannelNotes: tab.value === 'all', withFiles: tab.value === 'files', - }, -}); + })), +})); </script> <style lang="scss" module> diff --git a/packages/frontend/src/pages/user/pages.vue b/packages/frontend/src/pages/user/pages.vue index 1037f66b7e..18ba1c4c50 100644 --- a/packages/frontend/src/pages/user/pages.vue +++ b/packages/frontend/src/pages/user/pages.vue @@ -5,27 +5,27 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_spacer" style="--MI_SPACER-w: 700px;"> - <MkPagination v-slot="{items}" :pagination="pagination" withControl> + <MkPagination v-slot="{items}" :paginator="paginator" withControl> <MkPagePreview v-for="page in items" :key="page.id" :page="page" class="_margin"/> </MkPagination> </div> </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import MkPagePreview from '@/components/MkPagePreview.vue'; import MkPagination from '@/components/MkPagination.vue'; +import { Paginator } from '@/utility/paginator.js'; const props = defineProps<{ user: Misskey.entities.User; }>(); -const pagination = { - endpoint: 'users/pages' as const, +const paginator = markRaw(new Paginator('users/pages', { limit: 20, - params: computed(() => ({ + computedParams: computed(() => ({ userId: props.user.id, })), -}; +})); </script> diff --git a/packages/frontend/src/pages/user/reactions.vue b/packages/frontend/src/pages/user/reactions.vue index 28efd1a474..a59de32eab 100644 --- a/packages/frontend/src/pages/user/reactions.vue +++ b/packages/frontend/src/pages/user/reactions.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_spacer" style="--MI_SPACER-w: 700px;"> - <MkPagination v-slot="{items}" :pagination="pagination"> + <MkPagination v-slot="{items}" :paginator="paginator"> <div v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="_panel _margin"> <div :class="$style.header"> <MkAvatar :class="$style.avatar" :user="user"/> @@ -19,23 +19,23 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import MkPagination from '@/components/MkPagination.vue'; import MkNote from '@/components/MkNote.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; +import { Paginator } from '@/utility/paginator.js'; const props = defineProps<{ user: Misskey.entities.User; }>(); -const pagination = { - endpoint: 'users/reactions' as const, +const paginator = markRaw(new Paginator('users/reactions', { limit: 20, - params: computed(() => ({ + computedParams: computed(() => ({ userId: props.user.id, })), -}; +})); </script> <style lang="scss" module> diff --git a/packages/frontend/src/ui/deck/direct-column.vue b/packages/frontend/src/ui/deck/direct-column.vue index c8b174da09..c92ab49e31 100644 --- a/packages/frontend/src/ui/deck/direct-column.vue +++ b/packages/frontend/src/ui/deck/direct-column.vue @@ -7,35 +7,33 @@ SPDX-License-Identifier: AGPL-3.0-only <XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()"> <template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.direct }}</template> - <MkNotesTimeline ref="tlComponent" :pagination="pagination"/> + <MkNotesTimeline :paginator="paginator"/> </XColumn> </template> <script lang="ts" setup> -import { ref, useTemplateRef } from 'vue'; +import { markRaw, ref } from 'vue'; import XColumn from './column.vue'; import type { Column } from '@/deck.js'; import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; import { i18n } from '@/i18n.js'; +import { Paginator } from '@/utility/paginator.js'; defineProps<{ column: Column; isStacked: boolean; }>(); -const pagination = { - endpoint: 'notes/mentions' as const, +const paginator = markRaw(new Paginator('notes/mentions', { limit: 10, params: { visibility: 'specified', }, -}; - -const tlComponent = useTemplateRef('tlComponent'); +})); function reloadTimeline() { return new Promise<void>((res) => { - tlComponent.value?.reload().then(() => { + paginator.reload().then(() => { res(); }); }); diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue index 640e933f23..1244f68ef5 100644 --- a/packages/frontend/src/ui/deck/mentions-column.vue +++ b/packages/frontend/src/ui/deck/mentions-column.vue @@ -7,34 +7,32 @@ SPDX-License-Identifier: AGPL-3.0-only <XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()"> <template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.mentions }}</template> - <MkNotesTimeline ref="tlComponent" :pagination="pagination"/> + <MkNotesTimeline :paginator="paginator"/> </XColumn> </template> <script lang="ts" setup> -import { ref, useTemplateRef } from 'vue'; +import { markRaw, ref } from 'vue'; import XColumn from './column.vue'; import type { Column } from '@/deck.js'; import { i18n } from '@/i18n.js'; import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; +import { Paginator } from '@/utility/paginator.js'; defineProps<{ column: Column; isStacked: boolean; }>(); -const tlComponent = useTemplateRef('tlComponent'); +const paginator = markRaw(new Paginator('notes/mentions', { + limit: 10, +})); function reloadTimeline() { return new Promise<void>((res) => { - tlComponent.value?.reload().then(() => { + paginator.reload().then(() => { res(); }); }); } - -const pagination = { - endpoint: 'notes/mentions' as const, - limit: 10, -}; </script> diff --git a/packages/frontend/src/utility/paginator.ts b/packages/frontend/src/utility/paginator.ts new file mode 100644 index 0000000000..eb805df7a8 --- /dev/null +++ b/packages/frontend/src/utility/paginator.ts @@ -0,0 +1,322 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ref, shallowRef, triggerRef } from 'vue'; +import * as Misskey from 'misskey-js'; +import type { ComputedRef, DeepReadonly, Ref, ShallowRef } from 'vue'; +import { misskeyApi } from '@/utility/misskey-api.js'; + +const MAX_ITEMS = 30; +const MAX_QUEUE_ITEMS = 100; +const FIRST_FETCH_LIMIT = 15; +const SECOND_FETCH_LIMIT = 30; + +export type MisskeyEntity = { + id: string; + createdAt: string; + _shouldInsertAd_?: boolean; + [x: string]: any; +}; + +export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, T extends { id: string; } = (Misskey.Endpoints[Endpoint]['res'] extends (infer I)[] ? I extends { id: string } ? I : { id: string } : { id: string })> { + /** + * 外部から直接操作しないでください + */ + public items: ShallowRef<T[]> | Ref<T[]>; + + public queuedAheadItemsCount = ref(0); + public fetching = ref(true); + public fetchingOlder = ref(false); + public fetchingNewer = ref(false); + public canFetchOlder = ref(false); + public canSearch = false; + public error = ref(false); + private endpoint: Endpoint; + private limit: number; + private params: Misskey.Endpoints[Endpoint]['req'] | (() => Misskey.Endpoints[Endpoint]['req']); + public computedParams: ComputedRef<Misskey.Endpoints[Endpoint]['req']> | null; + public initialId: MisskeyEntity['id'] | null = null; + public initialDate: number | null = null; + public initialDirection: 'newer' | 'older'; + private offsetMode: boolean; + public noPaging: boolean; + public searchQuery = ref<null | string>(''); + private searchParamName: string; + private canFetchDetection: 'safe' | 'limit' | null = null; + private aheadQueue: T[] = []; + private useShallowRef: boolean; + + // 配列内の要素をどのような順序で並べるか + // newest: 新しいものが先頭 (default) + // oldest: 古いものが先頭 + // NOTE: このようなプロパティを用意してこっち側で並びを管理せずに、Setで持っておき参照者側が好きに並び変えるような設計の方がすっきりしそうなものの、Vueのレンダリングのたびに並び替え処理が発生することになったりしそうでパフォーマンス上の懸念がある + public order: Ref<'newest' | 'oldest'>; + + constructor(endpoint: Endpoint, props: { + limit?: number; + params?: Misskey.Endpoints[Endpoint]['req'] | (() => Misskey.Endpoints[Endpoint]['req']); + computedParams?: ComputedRef<Misskey.Endpoints[Endpoint]['req']>; + + /** + * 検索APIのような、ページング不可なエンドポイントを利用する場合 + * (そのようなAPIをこの関数で使うのは若干矛盾してるけど) + */ + noPaging?: boolean; + + offsetMode?: boolean; + + initialId?: MisskeyEntity['id']; + initialDate?: number | null; + initialDirection?: 'newer' | 'older'; + order?: 'newest' | 'oldest'; + + // 一部のAPIはさらに遡れる場合でもパフォーマンス上の理由でlimit以下の結果を返す場合があり、その場合はsafe、それ以外はlimitにすることを推奨 + canFetchDetection?: 'safe' | 'limit'; + + useShallowRef?: boolean; + + canSearch?: boolean; + searchParamName?: keyof Misskey.Endpoints[Endpoint]['req']; + }) { + this.endpoint = endpoint; + this.useShallowRef = props.useShallowRef ?? false; + this.items = this.useShallowRef ? shallowRef([] as T[]) : ref([] as T[]); + this.limit = props.limit ?? FIRST_FETCH_LIMIT; + this.params = props.params ?? {}; + this.computedParams = props.computedParams ?? null; + this.order = ref(props.order ?? 'newest'); + this.initialId = props.initialId ?? null; + this.initialDate = props.initialDate ?? null; + this.initialDirection = props.initialDirection ?? 'older'; + this.canFetchDetection = props.canFetchDetection ?? null; + this.noPaging = props.noPaging ?? false; + this.offsetMode = props.offsetMode ?? false; + this.canSearch = props.canSearch ?? false; + this.searchParamName = props.searchParamName ?? 'search'; + + this.getNewestId = this.getNewestId.bind(this); + this.getOldestId = this.getOldestId.bind(this); + this.init = this.init.bind(this); + this.reload = this.reload.bind(this); + this.fetchOlder = this.fetchOlder.bind(this); + this.fetchNewer = this.fetchNewer.bind(this); + this.unshiftItems = this.unshiftItems.bind(this); + this.pushItems = this.pushItems.bind(this); + this.prepend = this.prepend.bind(this); + this.enqueue = this.enqueue.bind(this); + this.releaseQueue = this.releaseQueue.bind(this); + this.removeItem = this.removeItem.bind(this); + this.updateItem = this.updateItem.bind(this); + } + + private getNewestId(): string | null | undefined { + // 様々な要因により並び順は保証されないのでソートが必要 + if (this.aheadQueue.length > 0) { + return this.aheadQueue.map(x => x.id).sort().at(-1); + } + return this.items.value.map(x => x.id).sort().at(-1); + } + + private getOldestId(): string | null | undefined { + // 様々な要因により並び順は保証されないのでソートが必要 + return this.items.value.map(x => x.id).sort().at(0); + } + + public async init(): Promise<void> { + this.items.value = []; + this.aheadQueue = []; + this.queuedAheadItemsCount.value = 0; + this.fetching.value = true; + + await misskeyApi<T[]>(this.endpoint, { + ...(typeof this.params === 'function' ? this.params() : this.params), + ...(this.computedParams ? this.computedParams.value : {}), + ...(this.searchQuery.value != null && this.searchQuery.value.trim() !== '' ? { [this.searchParamName]: this.searchQuery.value } : {}), + limit: this.limit ?? FIRST_FETCH_LIMIT, + allowPartial: true, + ...((this.initialId == null && this.initialDate == null) && this.initialDirection === 'newer' ? { + sinceId: '0', + } : this.initialDirection === 'newer' ? { + sinceId: this.initialId ?? undefined, + sinceDate: this.initialDate ?? undefined, + } : (this.initialId || this.initialDate) && this.initialDirection === 'older' ? { + untilId: this.initialId ?? undefined, + untilDate: this.initialDate ?? undefined, + } : {}), + }).then(res => { + // 逆順で返ってくるので + if ((this.initialId || this.initialDate) && this.initialDirection === 'newer') { + res.reverse(); + } + + for (let i = 0; i < res.length; i++) { + const item = res[i]; + if (i === 3) item._shouldInsertAd_ = true; + } + + this.pushItems(res); + + if (this.canFetchDetection === 'limit') { + if (res.length < FIRST_FETCH_LIMIT) { + this.canFetchOlder.value = false; + } else { + this.canFetchOlder.value = true; + } + } else if (this.canFetchDetection === 'safe' || this.canFetchDetection == null) { + if (res.length === 0 || this.noPaging) { + this.canFetchOlder.value = false; + } else { + this.canFetchOlder.value = true; + } + } + + this.error.value = false; + this.fetching.value = false; + }, err => { + this.error.value = true; + this.fetching.value = false; + }); + } + + public reload(): Promise<void> { + return this.init(); + } + + public async fetchOlder(): Promise<void> { + if (!this.canFetchOlder.value || this.fetching.value || this.fetchingOlder.value || this.items.value.length === 0) return; + this.fetchingOlder.value = true; + await misskeyApi<T[]>(this.endpoint, { + ...(typeof this.params === 'function' ? this.params() : this.params), + ...(this.computedParams ? this.computedParams.value : {}), + ...(this.searchQuery.value != null && this.searchQuery.value.trim() !== '' ? { [this.searchParamName]: this.searchQuery.value } : {}), + limit: SECOND_FETCH_LIMIT, + ...(this.offsetMode ? { + offset: this.items.value.length, + } : { + untilId: this.getOldestId(), + }), + }).then(res => { + for (let i = 0; i < res.length; i++) { + const item = res[i]; + if (i === 10) item._shouldInsertAd_ = true; + } + + this.pushItems(res); + + if (this.canFetchDetection === 'limit') { + if (res.length < FIRST_FETCH_LIMIT) { + this.canFetchOlder.value = false; + } else { + this.canFetchOlder.value = true; + } + } else if (this.canFetchDetection === 'safe' || this.canFetchDetection == null) { + if (res.length === 0) { + this.canFetchOlder.value = false; + } else { + this.canFetchOlder.value = true; + } + } + }).finally(() => { + this.fetchingOlder.value = false; + }); + } + + public async fetchNewer(options: { + toQueue?: boolean; + } = {}): Promise<void> { + this.fetchingNewer.value = true; + await misskeyApi<T[]>(this.endpoint, { + ...(typeof this.params === 'function' ? this.params() : this.params), + ...(this.computedParams ? this.computedParams.value : {}), + ...(this.searchQuery.value != null && this.searchQuery.value.trim() !== '' ? { [this.searchParamName]: this.searchQuery.value } : {}), + limit: SECOND_FETCH_LIMIT, + ...(this.offsetMode ? { + offset: this.items.value.length, + } : { + sinceId: this.getNewestId(), + }), + }).then(res => { + if (res.length === 0) return; // これやらないと余計なre-renderが走る + + if (options.toQueue) { + this.aheadQueue.unshift(...res.toReversed()); + if (this.aheadQueue.length > MAX_QUEUE_ITEMS) { + this.aheadQueue = this.aheadQueue.slice(0, MAX_QUEUE_ITEMS); + } + this.queuedAheadItemsCount.value = this.aheadQueue.length; + } else { + if (this.order.value === 'oldest') { + this.pushItems(res); + } else { + this.unshiftItems(res.toReversed()); + } + } + }).finally(() => { + this.fetchingNewer.value = false; + }); + } + + public trim(trigger = true): void { + if (this.items.value.length >= MAX_ITEMS) this.canFetchOlder.value = true; + this.items.value = this.items.value.slice(0, MAX_ITEMS); + if (this.useShallowRef && trigger) triggerRef(this.items); + } + + public unshiftItems(newItems: T[]): void { + if (newItems.length === 0) return; // これやらないと余計なre-renderが走る + this.items.value.unshift(...newItems.filter(x => !this.items.value.some(y => y.id === x.id))); // ストリーミングやポーリングのタイミングによっては重複することがあるため + this.trim(false); + if (this.useShallowRef) triggerRef(this.items); + } + + public pushItems(oldItems: T[]): void { + if (oldItems.length === 0) return; // これやらないと余計なre-renderが走る + this.items.value.push(...oldItems); + if (this.useShallowRef) triggerRef(this.items); + } + + public prepend(item: T): void { + if (this.items.value.some(x => x.id === item.id)) return; + this.items.value.unshift(item); + this.trim(false); + if (this.useShallowRef) triggerRef(this.items); + } + + public enqueue(item: T): void { + this.aheadQueue.unshift(item); + if (this.aheadQueue.length > MAX_QUEUE_ITEMS) { + this.aheadQueue.pop(); + } + this.queuedAheadItemsCount.value = this.aheadQueue.length; + } + + public releaseQueue(): void { + if (this.aheadQueue.length === 0) return; // これやらないと余計なre-renderが走る + this.unshiftItems(this.aheadQueue); + this.aheadQueue = []; + this.queuedAheadItemsCount.value = 0; + } + + public removeItem(id: string): void { + // TODO: queueからも消す + + const index = this.items.value.findIndex(x => x.id === id); + if (index !== -1) { + this.items.value.splice(index, 1); + if (this.useShallowRef) triggerRef(this.items); + } + } + + public updateItem(id: string, updator: (item: T) => T): void { + // TODO: queueのも更新 + + const index = this.items.value.findIndex(x => x.id === id); + if (index !== -1) { + const item = this.items.value[index]!; + this.items.value[index] = updator(item); + if (this.useShallowRef) triggerRef(this.items); + } + } +} |