This commit is contained in:
syuilo 2025-05-01 18:45:17 +09:00
parent 1857052b32
commit 7fe3b4f86c
5 changed files with 29 additions and 45 deletions

View File

@ -7,8 +7,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts"> <script lang="ts">
import { defineComponent, h, TransitionGroup, useCssModule } from 'vue'; import { defineComponent, h, TransitionGroup, useCssModule } from 'vue';
import type { PropType } from 'vue';
import type { MisskeyEntity } from '@/types/date-separated-list.js';
import MkAd from '@/components/global/MkAd.vue'; import MkAd from '@/components/global/MkAd.vue';
import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js'; import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
@ -19,7 +17,7 @@ import { getDateText } from '@/utility/timeline-date-separate.js';
export default defineComponent({ export default defineComponent({
props: { props: {
items: { items: {
type: Array as PropType<MisskeyEntity[]>, type: Array,
required: true, required: true,
}, },
direction: { direction: {

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkError v-else-if="paginator.error.value" @retry="paginator.init()"/> <MkError v-else-if="paginator.error.value" @retry="paginator.init()"/>
<div v-else-if="paginator.items.value.size === 0" key="_empty_"> <div v-else-if="paginator.items.value.length === 0" key="_empty_">
<slot name="empty"> <slot name="empty">
<div class="_fullinfo"> <div class="_fullinfo">
<img :src="infoImageUrl" draggable="false"/> <img :src="infoImageUrl" draggable="false"/>
@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:moveClass=" $style.transition_x_move" :moveClass=" $style.transition_x_move"
tag="div" tag="div"
> >
<template v-for="(notification, i) in Array.from(paginator.items.value.values())" :key="notification.id"> <template v-for="(notification, i) in paginator.items.value" :key="notification.id">
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true" :data-scroll-anchor="notification.id"/> <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true" :data-scroll-anchor="notification.id"/>
<XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true" :data-scroll-anchor="notification.id"/> <XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true" :data-scroll-anchor="notification.id"/>
</template> </template>

View File

@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkError v-else-if="paginator.error.value" @retry="paginator.init()"/> <MkError v-else-if="paginator.error.value" @retry="paginator.init()"/>
<div v-else-if="paginator.items.value.size === 0" key="_empty_"> <div v-else-if="paginator.items.value.length === 0" key="_empty_">
<slot name="empty"> <slot name="empty">
<div class="_fullinfo"> <div class="_fullinfo">
<img :src="infoImageUrl" draggable="false"/> <img :src="infoImageUrl" draggable="false"/>
@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkButton> </MkButton>
<MkLoading v-else/> <MkLoading v-else/>
</div> </div>
<slot :items="Array.from(paginator.items.value.values())" :fetching="paginator.fetching.value || paginator.moreFetching.value"></slot> <slot :items="paginator.items.value" :fetching="paginator.fetching.value || paginator.moreFetching.value"></slot>
<div v-show="!pagination.reversed && paginator.canFetchMore.value" key="_more_"> <div v-show="!pagination.reversed && paginator.canFetchMore.value" key="_more_">
<MkButton v-if="!paginator.moreFetching.value" v-appear="(prefer.s.enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :wait="paginator.moreFetching.value" primary rounded @click="paginator.fetchOlder"> <MkButton v-if="!paginator.moreFetching.value" v-appear="(prefer.s.enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :wait="paginator.moreFetching.value" primary rounded @click="paginator.fetchOlder">
{{ i18n.ts.loadMore }} {{ i18n.ts.loadMore }}

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkError v-else-if="paginator.error.value" @retry="paginator.init()"/> <MkError v-else-if="paginator.error.value" @retry="paginator.init()"/>
<div v-else-if="paginator.items.value.size === 0" key="_empty_"> <div v-else-if="paginator.items.value.length === 0" key="_empty_">
<slot name="empty"> <slot name="empty">
<div class="_fullinfo"> <div class="_fullinfo">
<img :src="infoImageUrl" draggable="false"/> <img :src="infoImageUrl" draggable="false"/>
@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:moveClass=" $style.transition_x_move" :moveClass=" $style.transition_x_move"
tag="div" tag="div"
> >
<template v-for="(note, i) in Array.from(paginator.items.value.values())" :key="note.id"> <template v-for="(note, i) in paginator.items.value" :key="note.id">
<div v-if="note._shouldInsertAd_" :class="[$style.noteWithAd, { '_gaps': !noGap }]" :data-scroll-anchor="note.id"> <div v-if="note._shouldInsertAd_" :class="[$style.noteWithAd, { '_gaps': !noGap }]" :data-scroll-anchor="note.id">
<MkNote :class="$style.note" :note="note" :withHardMute="true"/> <MkNote :class="$style.note" :note="note" :withHardMute="true"/>
<div :class="$style.ad"> <div :class="$style.ad">

View File

@ -38,26 +38,12 @@ export type PagingCtx<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoint
offsetMode?: boolean; offsetMode?: boolean;
}; };
function arrayToEntries(entities: MisskeyEntity[]): [string, MisskeyEntity][] { export function usePagination<T extends MisskeyEntity>(props: {
return entities.map(en => [en.id, en]); ctx: PagingCtx;
}
export function usePagination<Ctx extends PagingCtx, T = Misskey.Endpoints[Ctx['endpoint']]['res']>(props: {
ctx: Ctx;
}) { }) {
/** const items = ref<T[]>([]);
*
* 0
*/
const items = ref<Map<string, T>>(new Map());
const queue = ref<T[]>([]); const queue = ref<T[]>([]);
/**
* trueならMkLoadingで全て隠す
*/
const fetching = ref(true); const fetching = ref(true);
const moreFetching = ref(false); const moreFetching = ref(false);
const canFetchMore = ref(false); const canFetchMore = ref(false);
const error = ref(false); const error = ref(false);
@ -67,19 +53,19 @@ export function usePagination<Ctx extends PagingCtx, T = Misskey.Endpoints[Ctx['
function getNewestId() { function getNewestId() {
// 様々な要因により並び順は保証されないのでソートが必要 // 様々な要因により並び順は保証されないのでソートが必要
return Array.from(items.value.keys()).sort().at(-1); return items.value.map(x => x.id).sort().at(-1);
} }
function getOldestId() { function getOldestId() {
// 様々な要因により並び順は保証されないのでソートが必要 // 様々な要因により並び順は保証されないのでソートが必要
return Array.from(items.value.keys()).sort().at(0); return items.value.map(x => x.id).sort().at(0);
} }
async function init(): Promise<void> { async function init(): Promise<void> {
items.value = new Map(); items.value = [];
fetching.value = true; fetching.value = true;
const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {}; const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {};
await misskeyApi<MisskeyEntity[]>(props.ctx.endpoint, { await misskeyApi<T[]>(props.ctx.endpoint, {
...params, ...params,
limit: props.ctx.limit ?? FIRST_FETCH_LIMIT, limit: props.ctx.limit ?? FIRST_FETCH_LIMIT,
allowPartial: true, allowPartial: true,
@ -90,11 +76,11 @@ export function usePagination<Ctx extends PagingCtx, T = Misskey.Endpoints[Ctx['
} }
if (res.length === 0 || props.ctx.noPaging) { if (res.length === 0 || props.ctx.noPaging) {
concatItems(res); pushItems(res);
canFetchMore.value = false; canFetchMore.value = false;
} else { } else {
if (props.ctx.reversed) moreFetching.value = true; if (props.ctx.reversed) moreFetching.value = true;
concatItems(res); pushItems(res);
canFetchMore.value = true; canFetchMore.value = true;
} }
@ -111,14 +97,14 @@ export function usePagination<Ctx extends PagingCtx, T = Misskey.Endpoints[Ctx['
} }
async function fetchOlder(): Promise<void> { async function fetchOlder(): Promise<void> {
if (!canFetchMore.value || fetching.value || moreFetching.value || items.value.size === 0) return; if (!canFetchMore.value || fetching.value || moreFetching.value || items.value.length === 0) return;
moreFetching.value = true; moreFetching.value = true;
const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {}; const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {};
await misskeyApi<MisskeyEntity[]>(props.ctx.endpoint, { await misskeyApi<T[]>(props.ctx.endpoint, {
...params, ...params,
limit: SECOND_FETCH_LIMIT, limit: SECOND_FETCH_LIMIT,
...(props.ctx.offsetMode ? { ...(props.ctx.offsetMode ? {
offset: items.value.size, offset: items.value.length,
} : { } : {
untilId: getOldestId(), untilId: getOldestId(),
}), }),
@ -132,7 +118,7 @@ export function usePagination<Ctx extends PagingCtx, T = Misskey.Endpoints[Ctx['
canFetchMore.value = false; canFetchMore.value = false;
moreFetching.value = false; moreFetching.value = false;
} else { } else {
items.value = new Map([...items.value, ...arrayToEntries(res)]); items.value.push(...res);
canFetchMore.value = true; canFetchMore.value = true;
moreFetching.value = false; moreFetching.value = false;
} }
@ -145,11 +131,11 @@ export function usePagination<Ctx extends PagingCtx, T = Misskey.Endpoints[Ctx['
toQueue?: boolean; toQueue?: boolean;
} = {}): Promise<void> { } = {}): Promise<void> {
const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {}; const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {};
await misskeyApi<MisskeyEntity[]>(props.ctx.endpoint, { await misskeyApi<T[]>(props.ctx.endpoint, {
...params, ...params,
limit: SECOND_FETCH_LIMIT, limit: SECOND_FETCH_LIMIT,
...(props.ctx.offsetMode ? { ...(props.ctx.offsetMode ? {
offset: items.value.size, offset: items.value.length,
} : { } : {
sinceId: getNewestId(), sinceId: getNewestId(),
}), }),
@ -157,26 +143,26 @@ export function usePagination<Ctx extends PagingCtx, T = Misskey.Endpoints[Ctx['
if (options.toQueue) { if (options.toQueue) {
queue.value.unshift(...res.toReversed()); queue.value.unshift(...res.toReversed());
} else { } else {
items.value = new Map([...arrayToEntries(res.toReversed()), ...items.value]); items.value.unshift(...res.toReversed());
} }
}); });
} }
function trim() { function trim() {
if (items.value.size >= MAX_ITEMS) canFetchMore.value = true; if (items.value.length >= MAX_ITEMS) canFetchMore.value = true;
items.value = new Map([...items.value].slice(0, MAX_ITEMS)); items.value = items.value.slice(0, MAX_ITEMS);
} }
function unshiftItems(newItems: T[]) { function unshiftItems(newItems: T[]) {
items.value = new Map([...arrayToEntries(newItems), ...items.value]); items.value.unshift(...newItems);
} }
function concatItems(oldItems: T[]) { function pushItems(oldItems: T[]) {
items.value = new Map([...items.value, ...arrayToEntries(oldItems)]); items.value.push(...oldItems);
} }
function prepend(item: T) { function prepend(item: T) {
unshiftItems([item]); items.value.unshift(item);
} }
function enqueue(item: T) { function enqueue(item: T) {