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