diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2025-07-03 11:20:26 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-03 11:20:26 +0900 |
| commit | 09a5e4b10aad85d27875f3cdc8f32bd820615978 (patch) | |
| tree | b79819b40e0630314522461ea01ef365562fb255 /packages/frontend/src/utility | |
| parent | 🎨 (diff) | |
| download | misskey-09a5e4b10aad85d27875f3cdc8f32bd820615978.tar.gz misskey-09a5e4b10aad85d27875f3cdc8f32bd820615978.tar.bz2 misskey-09a5e4b10aad85d27875f3cdc8f32bd820615978.zip | |
fix(frontend): Paginatorの型エラー解消 (#16230)
* fix(frontend): fix paginator type error
* fix
* refactor
* fix
* fix
* fix(paginator): remove readonly type
* fix
* typo
* fix: R -> E
* remove any
---------
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Diffstat (limited to 'packages/frontend/src/utility')
| -rw-r--r-- | packages/frontend/src/utility/paginator.ts | 240 |
1 files changed, 156 insertions, 84 deletions
diff --git a/packages/frontend/src/utility/paginator.ts b/packages/frontend/src/utility/paginator.ts index eb805df7a8..63525b311a 100644 --- a/packages/frontend/src/utility/paginator.ts +++ b/packages/frontend/src/utility/paginator.ts @@ -5,7 +5,7 @@ import { ref, shallowRef, triggerRef } from 'vue'; import * as Misskey from 'misskey-js'; -import type { ComputedRef, DeepReadonly, Ref, ShallowRef } from 'vue'; +import type { ComputedRef, Ref, ShallowRef } from 'vue'; import { misskeyApi } from '@/utility/misskey-api.js'; const MAX_ITEMS = 30; @@ -17,14 +17,60 @@ 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 })> { +type FilterByEpRes<E extends Record<string, any>> = { + [K in keyof E]: E[K]['res'] extends Array<{ id: string }> ? K : never +}[keyof E]; +export type PaginatorCompatibleEndpointPaths = FilterByEpRes<Misskey.Endpoints>; +export type PaginatorCompatibleEndpoints = { + [K in PaginatorCompatibleEndpointPaths]: Misskey.Endpoints[K]; +}; + +export interface IPaginator<T = unknown, _T = T & MisskeyEntity> { + /** + * 外部から直接操作しないでください + */ + items: Ref<_T[]> | ShallowRef<_T[]>; + queuedAheadItemsCount: Ref<number>; + fetching: Ref<boolean>; + fetchingOlder: Ref<boolean>; + fetchingNewer: Ref<boolean>; + canFetchOlder: Ref<boolean>; + canSearch: boolean; + error: Ref<boolean>; + computedParams: ComputedRef<Misskey.Endpoints[PaginatorCompatibleEndpointPaths]['req'] | null | undefined> | null; + initialId: MisskeyEntity['id'] | null; + initialDate: number | null; + initialDirection: 'newer' | 'older'; + noPaging: boolean; + searchQuery: Ref<null | string>; + order: Ref<'newest' | 'oldest'>; + + init(): Promise<void>; + reload(): Promise<void>; + fetchOlder(): Promise<void>; + fetchNewer(options?: { toQueue?: boolean }): Promise<void>; + trim(trigger?: boolean): void; + unshiftItems(newItems: (_T)[]): void; + pushItems(oldItems: (_T)[]): void; + prepend(item: _T): void; + enqueue(item: _T): void; + releaseQueue(): void; + removeItem(id: string): void; + updateItem(id: string, updater: (item: _T) => _T): void; +} + +export class Paginator< + Endpoint extends PaginatorCompatibleEndpointPaths, + E extends PaginatorCompatibleEndpoints[Endpoint] = PaginatorCompatibleEndpoints[Endpoint], + T extends E['res'][number] & MisskeyEntity = E['res'][number] & MisskeyEntity, + SRef extends boolean = false, +> implements IPaginator { /** * 外部から直接操作しないでください */ - public items: ShallowRef<T[]> | Ref<T[]>; + public items: SRef extends true ? ShallowRef<T[]> : Ref<T[]>; public queuedAheadItemsCount = ref(0); public fetching = ref(true); @@ -35,18 +81,18 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey. 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; + private params: E['req'] | (() => E['req']); + public computedParams: ComputedRef<E['req'] | null | undefined> | 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 searchParamName: keyof E['req'] | 'search'; private canFetchDetection: 'safe' | 'limit' | null = null; private aheadQueue: T[] = []; - private useShallowRef: boolean; + private useShallowRef: SRef; // 配列内の要素をどのような順序で並べるか // newest: 新しいものが先頭 (default) @@ -56,8 +102,8 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey. constructor(endpoint: Endpoint, props: { limit?: number; - params?: Misskey.Endpoints[Endpoint]['req'] | (() => Misskey.Endpoints[Endpoint]['req']); - computedParams?: ComputedRef<Misskey.Endpoints[Endpoint]['req']>; + params?: E['req'] | (() => E['req']); + computedParams?: ComputedRef<E['req'] | null | undefined>; /** * 検索APIのような、ページング不可なエンドポイントを利用する場合 @@ -75,14 +121,19 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey. // 一部のAPIはさらに遡れる場合でもパフォーマンス上の理由でlimit以下の結果を返す場合があり、その場合はsafe、それ以外はlimitにすることを推奨 canFetchDetection?: 'safe' | 'limit'; - useShallowRef?: boolean; + useShallowRef?: SRef; canSearch?: boolean; - searchParamName?: keyof Misskey.Endpoints[Endpoint]['req']; + searchParamName?: keyof E['req']; }) { this.endpoint = endpoint; - this.useShallowRef = props.useShallowRef ?? false; - this.items = this.useShallowRef ? shallowRef([] as T[]) : ref([] as T[]); + this.useShallowRef = (props.useShallowRef ?? false) as SRef; + if (this.useShallowRef) { + this.items = shallowRef<T[]>([]); + } else { + this.items = ref<T[]>([]) as Ref<T[]>; + } + this.limit = props.limit ?? FIRST_FETCH_LIMIT; this.params = props.params ?? {}; this.computedParams = props.computedParams ?? null; @@ -130,7 +181,7 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey. this.queuedAheadItemsCount.value = 0; this.fetching.value = true; - await misskeyApi<T[]>(this.endpoint, { + const data: E['req'] = { ...(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 } : {}), @@ -145,39 +196,46 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey. 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; - } + const apiRes = (await misskeyApi(this.endpoint, data).catch(err => { + this.error.value = true; + this.fetching.value = false; + return null; + })) as T[] | null; + + if (apiRes == null) { + return; + } - this.pushItems(res); + // 逆順で返ってくるので + if ((this.initialId || this.initialDate) && this.initialDirection === 'newer') { + apiRes.reverse(); + } - 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; - } + for (let i = 0; i < apiRes.length; i++) { + const item = apiRes[i]; + if (i === 3) item._shouldInsertAd_ = true; + } + + this.pushItems(apiRes); + + if (this.canFetchDetection === 'limit') { + if (apiRes.length < FIRST_FETCH_LIMIT) { + this.canFetchOlder.value = false; + } else { + this.canFetchOlder.value = true; + } + } else if (this.canFetchDetection === 'safe' || this.canFetchDetection == null) { + if (apiRes.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; - }); + this.error.value = false; + this.fetching.value = false; } public reload(): Promise<void> { @@ -187,7 +245,8 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey. 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, { + + const data: E['req'] = { ...(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 } : {}), @@ -197,37 +256,46 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey. } : { untilId: this.getOldestId(), }), - }).then(res => { - for (let i = 0; i < res.length; i++) { - const item = res[i]; - if (i === 10) item._shouldInsertAd_ = true; - } + }; + + const apiRes = (await misskeyApi<T[]>(this.endpoint, data).catch(err => { + return null; + })) as T[] | null; + + this.fetchingOlder.value = false; + + if (apiRes == null) { + return; + } + + for (let i = 0; i < apiRes.length; i++) { + const item = apiRes[i]; + if (i === 10) item._shouldInsertAd_ = true; + } - this.pushItems(res); + this.pushItems(apiRes); - 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; - } + if (this.canFetchDetection === 'limit') { + if (apiRes.length < FIRST_FETCH_LIMIT) { + this.canFetchOlder.value = false; + } else { + this.canFetchOlder.value = true; + } + } else if (this.canFetchDetection === 'safe' || this.canFetchDetection == null) { + if (apiRes.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, { + + const data: E['req'] = { ...(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 } : {}), @@ -237,25 +305,29 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey. } : { 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; + const apiRes = (await misskeyApi<T[]>(this.endpoint, data).catch(err => { + return null; + })) as T[] | null; + + this.fetchingNewer.value = false; + + if (apiRes == null || apiRes.length === 0) return; // これやらないと余計なre-renderが走る + + if (options.toQueue) { + this.aheadQueue.unshift(...apiRes.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(apiRes); } else { - if (this.order.value === 'oldest') { - this.pushItems(res); - } else { - this.unshiftItems(res.toReversed()); - } + this.unshiftItems(apiRes.toReversed()); } - }).finally(() => { - this.fetchingNewer.value = false; - }); + } } public trim(trigger = true): void { @@ -309,13 +381,13 @@ export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey. } } - public updateItem(id: string, updator: (item: T) => T): void { + public updateItem(id: string, updater: (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); + this.items.value[index] = updater(item); if (this.useShallowRef) triggerRef(this.items); } } |