Frontend: SSE and pagination fixes
ci/woodpecker/push/ociImagePush Pipeline was successful Details

This commit is contained in:
Natty 2024-11-25 17:20:22 +01:00
parent ff6458ea2e
commit 8aa2a4dac4
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
2 changed files with 86 additions and 61 deletions

View File

@ -67,9 +67,11 @@ import {
computed, computed,
ComputedRef, ComputedRef,
isRef, isRef,
nextTick,
onActivated, onActivated,
onDeactivated, onDeactivated,
ref, ref,
shallowRef,
watch, watch,
} from "vue"; } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
@ -78,6 +80,7 @@ import {
getScrollContainer, getScrollContainer,
getScrollPosition, getScrollPosition,
isTopVisible, isTopVisible,
onScrollBottom,
onScrollTop, onScrollTop,
} from "@/scripts/scroll"; } from "@/scripts/scroll";
import MkButton from "@/components/MkButton.vue"; import MkButton from "@/components/MkButton.vue";
@ -85,7 +88,7 @@ import { magTransProperty } from "@/scripts-mag/mag-util";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
export type Paging< export type Paging<
E extends keyof misskey.Endpoints = keyof misskey.Endpoints, E extends keyof misskey.Endpoints = keyof misskey.Endpoints
> = { > = {
endpoint: E; endpoint: E;
limit: number; limit: number;
@ -119,7 +122,7 @@ const props = withDefaults(
}>(), }>(),
{ {
displayLimit: 30, displayLimit: 30,
}, }
); );
const emit = defineEmits<{ const emit = defineEmits<{
@ -129,8 +132,8 @@ const emit = defineEmits<{
type Item = { id: string; createdAt?: string; created_at?: string } & any; type Item = { id: string; createdAt?: string; created_at?: string } & any;
const rootEl = ref<HTMLElement>(); const rootEl = ref<HTMLElement>();
const items = ref<Item[]>([]); const items = shallowRef<Item[]>([]);
const queue = ref<Item[]>([]); const queue = shallowRef<Item[]>([]);
const offset = ref(0); const offset = ref(0);
const fetching = ref(true); const fetching = ref(true);
const moreFetching = ref(false); const moreFetching = ref(false);
@ -166,7 +169,7 @@ const init = async (): Promise<void> => {
(err) => { (err) => {
error.value = true; error.value = true;
fetching.value = false; fetching.value = false;
}, }
); );
}; };
@ -206,7 +209,7 @@ const refresh = async (): Promise<void> => {
(err) => { (err) => {
error.value = true; error.value = true;
fetching.value = false; fetching.value = false;
}, }
); );
}; };
@ -241,8 +244,8 @@ const fetchMore = async (): Promise<void> => {
magTransProperty( magTransProperty(
lastItem, lastItem,
"createdAt", "createdAt",
"created_at", "created_at"
), )
).getTime() ).getTime()
: undefined, : undefined,
untilId: lastItem?.id ?? undefined, untilId: lastItem?.id ?? undefined,
@ -259,7 +262,7 @@ const fetchMore = async (): Promise<void> => {
}, },
(err) => { (err) => {
moreFetching.value = false; moreFetching.value = false;
}, }
); );
}; };
@ -276,53 +279,66 @@ const isFresh = (): boolean => {
const pos = getScrollPosition(rootEl.value); const pos = getScrollPosition(rootEl.value);
const viewHeight = container.clientHeight; const viewHeight = container.clientHeight;
const height = container.scrollHeight; const height = container.scrollHeight;
const isBottom = pos + viewHeight > height - 32; return pos + viewHeight > height - 32;
return isBottom;
} else { } else {
const isTop = return (
isBackTop.value || isBackTop.value ||
(document.body.contains(rootEl.value) && (document.body.contains(rootEl.value) && isTopVisible(rootEl.value))
isTopVisible(rootEl.value)); );
return isTop; }
};
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 => { const prepend = (item: Item): void => {
if (props.pagination.reversed) { if (!rootEl.value) {
if (isFresh()) { queue.value = [...queue.value, item].slice(-props.displayLimit);
items.value = items.value.slice(-props.displayLimit); return;
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);
}
} }
};
const append = (item: Item): void => { if (!isFresh()) {
items.value.push(item); 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 => { const removeItem = (finder: (item: Item) => boolean): boolean => {
@ -331,7 +347,7 @@ const removeItem = (finder: (item: Item) => boolean): boolean => {
return false; return false;
} }
items.value.splice(i, 1); items.value = items.value.toSpliced(i, 1);
return true; return true;
}; };
@ -341,7 +357,9 @@ const updateItem = (id: Item["id"], replacer: (old: Item) => Item): boolean => {
return false; return false;
} }
items.value[i] = replacer(items.value[i]); const newItems = [...items.value];
newItems[i] = replacer(items.value[i]);
items.value = newItems;
return true; return true;
}; };
@ -349,14 +367,10 @@ if (props.pagination.params && isRef(props.pagination.params)) {
watch(props.pagination.params, init, { deep: true }); watch(props.pagination.params, init, { deep: true });
} }
watch( watch(queue, (a, b) => {
queue, if (a.length === 0 && b.length === 0) return;
(a, b) => { emit("queue", queue.value.length);
if (a.length === 0 && b.length === 0) return; });
emit("queue", queue.value.length);
},
{ deep: true },
);
init(); init();
@ -375,7 +389,6 @@ defineExpose({
reload, reload,
refresh, refresh,
prepend, prepend,
append,
removeItem, removeItem,
updateItem, updateItem,
isFresh, isFresh,
@ -387,6 +400,7 @@ defineExpose({
.fade-leave-active { .fade-leave-active {
transition: opacity 0.125s ease; transition: opacity 0.125s ease;
} }
.fade-enter-from, .fade-enter-from,
.fade-leave-to { .fade-leave-to {
opacity: 0; opacity: 0;
@ -398,9 +412,11 @@ defineExpose({
margin-right: auto; margin-right: auto;
} }
} }
.list > :deep(._button) { .list > :deep(._button) {
margin-inline: auto; margin-inline: auto;
margin-bottom: 16px; margin-bottom: 16px;
&:last-of-type:not(:first-child) { &:last-of-type:not(:first-child) {
margin-top: 16px; margin-top: 16px;
} }

View File

@ -113,7 +113,16 @@ export class MagEventChannel extends EventEmitter<{
let buf = ""; let buf = "";
while (true) { 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; if (res === "cancelled") break;
@ -122,7 +131,7 @@ export class MagEventChannel extends EventEmitter<{
setTimeout( setTimeout(
() => this.connect(), () => this.connect(),
this.backoffBase * this.backoffBase *
Math.pow(this.backoffFactor, this.attempts) Math.pow(this.backoffFactor, this.attempts)
); );
this.attempts++; this.attempts++;
break; break;