forked from mirrors/misskey
refactor(frontend): use* 関数の格納場所のフォルダ名を composables に変更 (#16004)
* refactor(frontend): use* 関数の格納場所を正式名称(composables)に変更 * migrate * move useLoading
This commit is contained in:
258
packages/frontend/src/composables/use-pagination.ts
Normal file
258
packages/frontend/src/composables/use-pagination.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
baseId?: MisskeyEntity['id'];
|
||||
direction?: 'newer' | 'older';
|
||||
};
|
||||
|
||||
export function usePagination<T extends MisskeyEntity>(props: {
|
||||
ctx: PagingCtx;
|
||||
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 canFetchOlder = ref(false);
|
||||
const error = ref(false);
|
||||
|
||||
// パラメータに何らかの変更があった際、再読込したい(チャンネル等のIDが変わったなど)
|
||||
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.baseId && props.ctx.direction === 'newer' ? {
|
||||
sinceId: props.ctx.baseId,
|
||||
} : props.ctx.baseId && props.ctx.direction === 'older' ? {
|
||||
untilId: props.ctx.baseId,
|
||||
} : {}),
|
||||
}).then(res => {
|
||||
// 逆順で返ってくるので
|
||||
if (props.ctx.baseId && props.ctx.direction === 'newer') {
|
||||
res.reverse();
|
||||
}
|
||||
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
const item = res[i];
|
||||
if (i === 3) item._shouldInsertAd_ = true;
|
||||
}
|
||||
|
||||
if (res.length === 0 || props.ctx.noPaging) {
|
||||
pushItems(res);
|
||||
canFetchOlder.value = false;
|
||||
} else {
|
||||
pushItems(res);
|
||||
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;
|
||||
}
|
||||
|
||||
if (res.length === 0) {
|
||||
canFetchOlder.value = false;
|
||||
fetchingOlder.value = false;
|
||||
} else {
|
||||
pushItems(res);
|
||||
canFetchOlder.value = true;
|
||||
fetchingOlder.value = false;
|
||||
}
|
||||
}, err => {
|
||||
fetchingOlder.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchNewer(options: {
|
||||
toQueue?: boolean;
|
||||
} = {}): Promise<void> {
|
||||
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 {
|
||||
unshiftItems(res.toReversed());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
return {
|
||||
items: items as DeepReadonly<ShallowRef<T[]>>,
|
||||
queuedAheadItemsCount,
|
||||
fetching,
|
||||
fetchingOlder,
|
||||
canFetchOlder,
|
||||
init,
|
||||
reload,
|
||||
fetchOlder,
|
||||
fetchNewer,
|
||||
unshiftItems,
|
||||
prepend,
|
||||
trim,
|
||||
removeItem,
|
||||
updateItem,
|
||||
enqueue,
|
||||
releaseQueue,
|
||||
error,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user