diff --git a/fe_calckey/frontend/client/src/components/MkPagination.vue b/fe_calckey/frontend/client/src/components/MkPagination.vue index 0af7c4f..137ecbe 100644 --- a/fe_calckey/frontend/client/src/components/MkPagination.vue +++ b/fe_calckey/frontend/client/src/components/MkPagination.vue @@ -67,9 +67,11 @@ import { computed, ComputedRef, isRef, + nextTick, onActivated, onDeactivated, ref, + shallowRef, watch, } from "vue"; import * as misskey from "calckey-js"; @@ -78,6 +80,7 @@ import { getScrollContainer, getScrollPosition, isTopVisible, + onScrollBottom, onScrollTop, } from "@/scripts/scroll"; import MkButton from "@/components/MkButton.vue"; @@ -85,7 +88,7 @@ import { magTransProperty } from "@/scripts-mag/mag-util"; import { i18n } from "@/i18n"; export type Paging< - E extends keyof misskey.Endpoints = keyof misskey.Endpoints, + E extends keyof misskey.Endpoints = keyof misskey.Endpoints > = { endpoint: E; limit: number; @@ -119,7 +122,7 @@ const props = withDefaults( }>(), { displayLimit: 30, - }, + } ); const emit = defineEmits<{ @@ -129,8 +132,8 @@ const emit = defineEmits<{ type Item = { id: string; createdAt?: string; created_at?: string } & any; const rootEl = ref(); -const items = ref([]); -const queue = ref([]); +const items = shallowRef([]); +const queue = shallowRef([]); const offset = ref(0); const fetching = ref(true); const moreFetching = ref(false); @@ -166,7 +169,7 @@ const init = async (): Promise => { (err) => { error.value = true; fetching.value = false; - }, + } ); }; @@ -206,7 +209,7 @@ const refresh = async (): Promise => { (err) => { error.value = true; fetching.value = false; - }, + } ); }; @@ -241,8 +244,8 @@ const fetchMore = async (): Promise => { magTransProperty( lastItem, "createdAt", - "created_at", - ), + "created_at" + ) ).getTime() : undefined, untilId: lastItem?.id ?? undefined, @@ -259,7 +262,7 @@ const fetchMore = async (): Promise => { }, (err) => { moreFetching.value = false; - }, + } ); }; @@ -276,53 +279,66 @@ const isFresh = (): boolean => { const pos = getScrollPosition(rootEl.value); const viewHeight = container.clientHeight; const height = container.scrollHeight; - const isBottom = pos + viewHeight > height - 32; - return isBottom; + return pos + viewHeight > height - 32; } else { - const isTop = + return ( isBackTop.value || - (document.body.contains(rootEl.value) && - isTopVisible(rootEl.value)); - return isTop; + (document.body.contains(rootEl.value) && isTopVisible(rootEl.value)) + ); + } +}; + +const unqueue = () => { + const queueRemoved = [...queue.value].reverse(); + queue.value = []; + if (props.pagination.reversed) { + items.value = [...items.value, ...queueRemoved].slice( + 0, + -props.displayLimit + ); + } else { + items.value = [...queueRemoved, ...items.value].slice( + 0, + props.displayLimit + ); } }; const prepend = (item: Item): void => { - if (props.pagination.reversed) { - if (isFresh()) { - items.value = items.value.slice(-props.displayLimit); - hasMore.value = true; - } - - items.value.push(item); - } else { - if (isFresh()) { - // Prepend the item - items.value = [item, ...items.value].slice(0, props.displayLimit); - } else { - if (!rootEl.value) { - items.value.unshift(item); - return; - } - - if (!queue.value.length) { - onScrollTop(rootEl.value, () => { - const queueRemoved = [...queue.value].reverse(); - queue.value = []; - items.value = [...queueRemoved, ...items.value].slice( - 0, - props.displayLimit, - ); - }); - } - - queue.value = [...queue.value, item].slice(-props.displayLimit); - } + if (!rootEl.value) { + queue.value = [...queue.value, item].slice(-props.displayLimit); + return; } -}; -const append = (item: Item): void => { - items.value.push(item); + if (!isFresh()) { + if (!queue.value.length) { + (props.pagination.reversed ? onScrollBottom : onScrollTop)( + rootEl.value, + () => { + nextTick(unqueue); + } + ); + } + + queue.value = [...queue.value, item].slice(-props.displayLimit); + + return; + } + + if (items.value.length > props.displayLimit) { + queue.value = [...queue.value, item].slice(-props.displayLimit); + if (!queue.value.length) { + nextTick(unqueue); + } + + return; + } + + if (props.pagination.reversed) { + items.value = [...items.value, item]; + } else { + items.value = [item, ...items.value]; + } }; const removeItem = (finder: (item: Item) => boolean): boolean => { @@ -331,7 +347,7 @@ const removeItem = (finder: (item: Item) => boolean): boolean => { return false; } - items.value.splice(i, 1); + items.value = items.value.toSpliced(i, 1); return true; }; @@ -341,7 +357,9 @@ const updateItem = (id: Item["id"], replacer: (old: Item) => Item): boolean => { return false; } - items.value[i] = replacer(items.value[i]); + const newItems = [...items.value]; + newItems[i] = replacer(items.value[i]); + items.value = newItems; return true; }; @@ -349,14 +367,10 @@ if (props.pagination.params && isRef(props.pagination.params)) { watch(props.pagination.params, init, { deep: true }); } -watch( - queue, - (a, b) => { - if (a.length === 0 && b.length === 0) return; - emit("queue", queue.value.length); - }, - { deep: true }, -); +watch(queue, (a, b) => { + if (a.length === 0 && b.length === 0) return; + emit("queue", queue.value.length); +}); init(); @@ -375,7 +389,6 @@ defineExpose({ reload, refresh, prepend, - append, removeItem, updateItem, isFresh, @@ -387,6 +400,7 @@ defineExpose({ .fade-leave-active { transition: opacity 0.125s ease; } + .fade-enter-from, .fade-leave-to { opacity: 0; @@ -398,9 +412,11 @@ defineExpose({ margin-right: auto; } } + .list > :deep(._button) { margin-inline: auto; margin-bottom: 16px; + &:last-of-type:not(:first-child) { margin-top: 16px; } diff --git a/fe_calckey/frontend/magnetar-common/src/sse-listener.ts b/fe_calckey/frontend/magnetar-common/src/sse-listener.ts index 00c6fb8..7857811 100644 --- a/fe_calckey/frontend/magnetar-common/src/sse-listener.ts +++ b/fe_calckey/frontend/magnetar-common/src/sse-listener.ts @@ -113,7 +113,16 @@ export class MagEventChannel extends EventEmitter<{ let buf = ""; while (true) { - const res = await Promise.race([reader.read(), this.closePromise]); + const res = await Promise.race([ + reader.read().catch((e) => { + console.error(e); + return { + done: true, + value: undefined, + }; + }), + this.closePromise, + ]); if (res === "cancelled") break; @@ -122,7 +131,7 @@ export class MagEventChannel extends EventEmitter<{ setTimeout( () => this.connect(), this.backoffBase * - Math.pow(this.backoffFactor, this.attempts) + Math.pow(this.backoffFactor, this.attempts) ); this.attempts++; break;