feat: rewrite MkPagination for fold
This commit is contained in:
parent
bfcadaa094
commit
564eb08386
6 changed files with 274 additions and 274 deletions
|
@ -26,7 +26,6 @@
|
||||||
: notification.reaction
|
: notification.reaction
|
||||||
"
|
"
|
||||||
:custom-emojis="notification.note.emojis"
|
:custom-emojis="notification.note.emojis"
|
||||||
:no-style="true"
|
|
||||||
/>
|
/>
|
||||||
<XReactionIcon
|
<XReactionIcon
|
||||||
v-else-if="
|
v-else-if="
|
||||||
|
@ -73,7 +72,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted, ref } from "vue";
|
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||||
import type { Connection } from "firefish-js/src/streaming";
|
import type { Connection } from "firefish-js/src/streaming";
|
||||||
import type { Channels } from "firefish-js/src/streaming.types";
|
import type { Channels } from "firefish-js/src/streaming.types";
|
||||||
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
||||||
|
@ -116,8 +115,10 @@ const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReact
|
||||||
? instance.defaultReaction
|
? instance.defaultReaction
|
||||||
: "⭐";
|
: "⭐";
|
||||||
|
|
||||||
const users = ref(props.notification.users.slice(0, 5));
|
const users = computed(() => props.notification.users.slice(0, 5));
|
||||||
const userleft = ref(props.notification.users.length - users.value.length);
|
const userleft = computed(
|
||||||
|
() => props.notification.users.length - users.value.length,
|
||||||
|
);
|
||||||
|
|
||||||
let readObserver: IntersectionObserver | undefined;
|
let readObserver: IntersectionObserver | undefined;
|
||||||
let connection: Connection<Channels["main"]> | null = null;
|
let connection: Connection<Channels["main"]> | null = null;
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
<MkPagination
|
||||||
|
ref="pagingComponent"
|
||||||
|
:pagination="pagination"
|
||||||
|
:folder="convertNotification"
|
||||||
|
>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
<img
|
<img
|
||||||
|
@ -11,9 +15,9 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="{ items: notifications }">
|
<template #default="{ foldedItems: notifications }">
|
||||||
<XList
|
<XList
|
||||||
:items="convertNotification(notifications)"
|
:items="notifications"
|
||||||
v-slot="{ item: notification }"
|
v-slot="{ item: notification }"
|
||||||
class="elsfgstc"
|
class="elsfgstc"
|
||||||
:no-gap="true"
|
:no-gap="true"
|
||||||
|
@ -92,7 +96,7 @@ const pagination = Object.assign(
|
||||||
},
|
},
|
||||||
shouldFold
|
shouldFold
|
||||||
? {
|
? {
|
||||||
limit: FETCH_LIMIT,
|
limit: 50,
|
||||||
secondFetchLimit: FETCH_LIMIT,
|
secondFetchLimit: FETCH_LIMIT,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
|
@ -134,11 +138,11 @@ const onNotification = (notification: entities.Notification) => {
|
||||||
|
|
||||||
let connection: StreamTypes.ChannelOf<"main"> | undefined;
|
let connection: StreamTypes.ChannelOf<"main"> | undefined;
|
||||||
|
|
||||||
function convertNotification(n: entities.Notification[]) {
|
function convertNotification(ns: entities.Notification[]) {
|
||||||
if (shouldFold) {
|
if (shouldFold) {
|
||||||
return foldNotifications(n, FETCH_LIMIT);
|
return foldNotifications(ns);
|
||||||
} else {
|
} else {
|
||||||
return n;
|
return ns;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkLoading v-else class="loading" />
|
<MkLoading v-else class="loading" />
|
||||||
</div>
|
</div>
|
||||||
<slot :items="items"></slot>
|
<slot :items="items" :foldedItems="foldedItems"></slot>
|
||||||
<div
|
<div
|
||||||
v-show="!pagination.reversed && more"
|
v-show="!pagination.reversed && more"
|
||||||
key="_more_"
|
key="_more_"
|
||||||
|
@ -66,8 +66,8 @@
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup generic="E extends PagingKey">
|
<script lang="ts" setup generic="E extends PagingKey, Fold extends PagingAble">
|
||||||
import type { ComponentPublicInstance, ComputedRef } from "vue";
|
import type { ComponentPublicInstance, ComputedRef, Ref } from "vue";
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
isRef,
|
isRef,
|
||||||
|
@ -79,12 +79,7 @@ import {
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import type { Endpoints, TypeUtils } from "firefish-js";
|
import type { Endpoints, TypeUtils } from "firefish-js";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import {
|
import { isTopVisible, onScrollTop } from "@/scripts/scroll";
|
||||||
getScrollContainer,
|
|
||||||
getScrollPosition,
|
|
||||||
isTopVisible,
|
|
||||||
onScrollTop,
|
|
||||||
} from "@/scripts/scroll";
|
|
||||||
import MkButton from "@/components/MkButton.vue";
|
import MkButton from "@/components/MkButton.vue";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from "@/store";
|
||||||
|
@ -105,11 +100,15 @@ export type MkPaginationType<
|
||||||
reload: () => Promise<void>;
|
reload: () => Promise<void>;
|
||||||
refresh: () => Promise<void>;
|
refresh: () => Promise<void>;
|
||||||
prepend: (item: Item) => Promise<void>;
|
prepend: (item: Item) => Promise<void>;
|
||||||
append: (item: Item) => Promise<void>;
|
append: (...item: Item[]) => Promise<void>;
|
||||||
removeItem: (finder: (item: Item) => boolean) => boolean;
|
removeItem: (finder: (item: Item) => boolean) => boolean;
|
||||||
updateItem: (id: string, replacer: (old: Item) => Item) => boolean;
|
updateItem: (id: string, replacer: (old: Item) => Item) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PagingAble = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type PagingKeyOf<T> = TypeUtils.EndpointsOf<T[]>;
|
export type PagingKeyOf<T> = TypeUtils.EndpointsOf<T[]>;
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: Used Intentionally
|
// biome-ignore lint/suspicious/noExplicitAny: Used Intentionally
|
||||||
export type PagingKey = PagingKeyOf<any>;
|
export type PagingKey = PagingKeyOf<any>;
|
||||||
|
@ -142,13 +141,18 @@ export interface Paging<E extends PagingKey = PagingKey> {
|
||||||
|
|
||||||
export type PagingOf<T> = Paging<TypeUtils.EndpointsOf<T[]>>;
|
export type PagingOf<T> = Paging<TypeUtils.EndpointsOf<T[]>>;
|
||||||
|
|
||||||
|
type Item = Endpoints[E]["res"][number];
|
||||||
|
type Param = Endpoints[E]["req"] | Record<string, never>;
|
||||||
|
|
||||||
const SECOND_FETCH_LIMIT_DEFAULT = 30;
|
const SECOND_FETCH_LIMIT_DEFAULT = 30;
|
||||||
|
const FIRST_FETCH_LIMIT_DEFAULT = 10;
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
pagination: Paging<E>;
|
pagination: Paging<E>;
|
||||||
disableAutoLoad?: boolean;
|
disableAutoLoad?: boolean;
|
||||||
displayLimit?: number;
|
displayLimit?: number;
|
||||||
|
folder?: (i: Item[]) => Fold[];
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
displayLimit: 30,
|
displayLimit: 30,
|
||||||
|
@ -156,7 +160,7 @@ const props = withDefaults(
|
||||||
);
|
);
|
||||||
|
|
||||||
const slots = defineSlots<{
|
const slots = defineSlots<{
|
||||||
default(props: { items: Item[] }): unknown;
|
default(props: { items: Item[]; foldedItems: Fold[] }): unknown;
|
||||||
empty(props: Record<string, never>): never;
|
empty(props: Record<string, never>): never;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -165,13 +169,59 @@ const emit = defineEmits<{
|
||||||
(ev: "status", hasError: boolean): void;
|
(ev: "status", hasError: boolean): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
type Param = Endpoints[E]["req"] | Record<string, never>;
|
|
||||||
type Item = Endpoints[E]["res"][number];
|
|
||||||
|
|
||||||
const rootEl = ref<HTMLElement>();
|
const rootEl = ref<HTMLElement>();
|
||||||
const items = ref<Item[]>([]);
|
const items = ref<Item[]>([]);
|
||||||
|
const foldedItems = ref([]) as Ref<Fold[]>;
|
||||||
|
|
||||||
|
// To improve performance, we do not use vue’s `computed` here
|
||||||
|
function calculateItems() {
|
||||||
|
function getItems<T>(folder: (ns: Item[]) => T[]) {
|
||||||
|
const res = [
|
||||||
|
folder(prepended.value.toReversed()),
|
||||||
|
...arrItems.value.map((arr) => folder(arr)),
|
||||||
|
folder(appended.value),
|
||||||
|
].flat(1);
|
||||||
|
if (props.pagination.reversed) {
|
||||||
|
res.reverse();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
items.value = getItems((x) => x);
|
||||||
|
if (props.folder) foldedItems.value = getItems(props.folder);
|
||||||
|
}
|
||||||
|
|
||||||
const queue = ref<Item[]>([]);
|
const queue = ref<Item[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cached elements inserted front by `prepend` function
|
||||||
|
*/
|
||||||
|
const prepended = ref<Item[]>([]);
|
||||||
|
/**
|
||||||
|
* The array of "frozen" items
|
||||||
|
*/
|
||||||
|
const arrItems = ref<Item[][]>([]);
|
||||||
|
/**
|
||||||
|
* The cached elements inserted back by `append` function
|
||||||
|
*/
|
||||||
|
const appended = ref<Item[]>([]);
|
||||||
|
|
||||||
|
const idMap = new Map<string, boolean>();
|
||||||
|
|
||||||
const offset = ref(0);
|
const offset = ref(0);
|
||||||
|
|
||||||
|
type PagingByParam =
|
||||||
|
| {
|
||||||
|
offset: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
sinceId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
untilId: string;
|
||||||
|
}
|
||||||
|
| Record<string, never>;
|
||||||
|
let nextPagingBy: PagingByParam = {};
|
||||||
|
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
const moreFetching = ref(false);
|
const moreFetching = ref(false);
|
||||||
const more = ref(false);
|
const more = ref(false);
|
||||||
|
@ -184,54 +234,14 @@ const init = async (): Promise<void> => {
|
||||||
queue.value = [];
|
queue.value = [];
|
||||||
fetching.value = true;
|
fetching.value = true;
|
||||||
|
|
||||||
const params = props.pagination.params ? unref(props.pagination.params) : {};
|
await fetch(true);
|
||||||
await os
|
|
||||||
.api(props.pagination.endpoint, {
|
|
||||||
...params,
|
|
||||||
limit: props.pagination.noPaging
|
|
||||||
? props.pagination.limit || 10
|
|
||||||
: (props.pagination.limit || 10) + 1,
|
|
||||||
...(props.pagination.ascending
|
|
||||||
? {
|
|
||||||
// An initial value smaller than all possible ids must be filled in here.
|
|
||||||
sinceId: "0",
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
})
|
|
||||||
.then(
|
|
||||||
(res: Item[]) => {
|
|
||||||
for (let i = 0; i < res.length; i++) {
|
|
||||||
const item = res[i];
|
|
||||||
if (props.pagination.reversed) {
|
|
||||||
if (i === res.length - 2) item._shouldInsertAd_ = true;
|
|
||||||
} else {
|
|
||||||
if (i === 3) item._shouldInsertAd_ = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!props.pagination.noPaging &&
|
|
||||||
res.length > (props.pagination.limit || 10)
|
|
||||||
) {
|
|
||||||
res.pop();
|
|
||||||
items.value = props.pagination.reversed ? res.toReversed() : res;
|
|
||||||
more.value = true;
|
|
||||||
} else {
|
|
||||||
items.value = props.pagination.reversed ? res.toReversed() : res;
|
|
||||||
more.value = false;
|
|
||||||
}
|
|
||||||
offset.value = res.length;
|
|
||||||
error.value = false;
|
|
||||||
fetching.value = false;
|
|
||||||
},
|
|
||||||
(_err) => {
|
|
||||||
error.value = true;
|
|
||||||
fetching.value = false;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const reload = (): Promise<void> => {
|
const reload = (): Promise<void> => {
|
||||||
items.value = [];
|
arrItems.value = [];
|
||||||
|
appended.value = [];
|
||||||
|
prepended.value = [];
|
||||||
|
idMap.clear();
|
||||||
return init();
|
return init();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -240,30 +250,18 @@ const refresh = async (): Promise<void> => {
|
||||||
await os
|
await os
|
||||||
.api(props.pagination.endpoint, {
|
.api(props.pagination.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
limit: items.value.length + 1,
|
limit: (items.value.length || foldedItems.value.length) + 1,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
})
|
})
|
||||||
.then(
|
.then(
|
||||||
(res: Item[]) => {
|
(res: Item[]) => {
|
||||||
const ids = items.value.reduce(
|
appended.value = [];
|
||||||
(a, b) => {
|
prepended.value = [];
|
||||||
a[b.id] = true;
|
|
||||||
return a;
|
|
||||||
},
|
|
||||||
{} as Record<string, boolean>,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (let i = 0; i < res.length; i++) {
|
// appended should be inserted into arrItems to fix the element position
|
||||||
const item = res[i];
|
arrItems.value = [res];
|
||||||
if (!updateItem(item.id, (_old) => item)) {
|
|
||||||
append(item);
|
|
||||||
}
|
|
||||||
delete ids[item.id];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const id in ids) {
|
calculateItems();
|
||||||
removeItem((i) => i.id === id);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
(_err) => {
|
(_err) => {
|
||||||
error.value = true;
|
error.value = true;
|
||||||
|
@ -272,155 +270,145 @@ const refresh = async (): Promise<void> => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchMore = async (): Promise<void> => {
|
async function fetch(firstFetching?: boolean) {
|
||||||
if (
|
let limit: number;
|
||||||
!more.value ||
|
|
||||||
fetching.value ||
|
if (firstFetching) {
|
||||||
moreFetching.value ||
|
limit = props.pagination.noPaging
|
||||||
items.value.length === 0
|
? props.pagination.limit || FIRST_FETCH_LIMIT_DEFAULT
|
||||||
)
|
: (props.pagination.limit || FIRST_FETCH_LIMIT_DEFAULT) + 1;
|
||||||
return;
|
|
||||||
moreFetching.value = true;
|
if (props.pagination.ascending) {
|
||||||
backed.value = true;
|
nextPagingBy = {
|
||||||
|
// An initial value smaller than all possible ids must be filled in here.
|
||||||
|
sinceId: "0",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
!more.value ||
|
||||||
|
fetching.value ||
|
||||||
|
moreFetching.value ||
|
||||||
|
items.value.length === 0
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
moreFetching.value = true;
|
||||||
|
backed.value = true;
|
||||||
|
|
||||||
|
limit =
|
||||||
|
(props.pagination.secondFetchLimit ?? SECOND_FETCH_LIMIT_DEFAULT) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
const params = props.pagination.params ? unref(props.pagination.params) : {};
|
const params = props.pagination.params ? unref(props.pagination.params) : {};
|
||||||
|
|
||||||
await os
|
await os
|
||||||
.api(props.pagination.endpoint, {
|
.api(props.pagination.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
limit:
|
limit,
|
||||||
(props.pagination.secondFetchLimit ?? SECOND_FETCH_LIMIT_DEFAULT) + 1,
|
...nextPagingBy,
|
||||||
...(props.pagination.offsetMode
|
|
||||||
? {
|
|
||||||
offset: offset.value,
|
|
||||||
}
|
|
||||||
: props.pagination.reversed
|
|
||||||
? {
|
|
||||||
sinceId: items.value[0].id,
|
|
||||||
}
|
|
||||||
: props.pagination.ascending
|
|
||||||
? {
|
|
||||||
sinceId: items.value[items.value.length - 1].id,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
untilId: items.value[items.value.length - 1].id,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
.then(
|
.then(
|
||||||
(res: Item[]) => {
|
(res: Item[]) => {
|
||||||
for (let i = 0; i < res.length; i++) {
|
if (!props.pagination.reversed)
|
||||||
const item = res[i];
|
for (let i = 0; i < res.length; i++) {
|
||||||
if (props.pagination.reversed) {
|
const item = res[i];
|
||||||
if (i === res.length - 9) item._shouldInsertAd_ = true;
|
if (props.pagination.reversed) {
|
||||||
} else {
|
if (i === res.length - (firstFetching ? 2 : 9))
|
||||||
if (i === 10) item._shouldInsertAd_ = true;
|
item._shouldInsertAd_ = true;
|
||||||
|
} else {
|
||||||
|
if (i === (firstFetching ? 3 : 10)) item._shouldInsertAd_ = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if (!props.pagination.noPaging && res.length > limit - 1) {
|
||||||
if (
|
|
||||||
res.length >
|
|
||||||
(props.pagination.secondFetchLimit ?? SECOND_FETCH_LIMIT_DEFAULT)
|
|
||||||
) {
|
|
||||||
res.pop();
|
res.pop();
|
||||||
items.value = props.pagination.reversed
|
|
||||||
? res.toReversed().concat(items.value)
|
|
||||||
: items.value.concat(res);
|
|
||||||
more.value = true;
|
more.value = true;
|
||||||
} else {
|
} else {
|
||||||
items.value = props.pagination.reversed
|
|
||||||
? res.toReversed().concat(items.value)
|
|
||||||
: items.value.concat(res);
|
|
||||||
more.value = false;
|
more.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
offset.value += res.length;
|
offset.value += res.length;
|
||||||
|
error.value = false;
|
||||||
|
fetching.value = false;
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
|
|
||||||
|
const lastRes = res[res.length - 1];
|
||||||
|
|
||||||
|
if (props.pagination.offsetMode) {
|
||||||
|
nextPagingBy = {
|
||||||
|
offset: offset.value,
|
||||||
|
};
|
||||||
|
} else if (props.pagination.ascending) {
|
||||||
|
nextPagingBy = {
|
||||||
|
sinceId: lastRes?.id,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
nextPagingBy = {
|
||||||
|
untilId: lastRes?.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstFetching && props.folder != null) {
|
||||||
|
// In this way, prepended has some initial values for folding
|
||||||
|
prepended.value = res.toReversed();
|
||||||
|
} else {
|
||||||
|
// For ascending and offset modes, append and prepend may cause item duplication
|
||||||
|
// so they need to be filtered out.
|
||||||
|
if (props.pagination.offsetMode || props.pagination.ascending) {
|
||||||
|
for (const item of appended.value) {
|
||||||
|
idMap.set(item.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/style/noParameterAssign: assign it intentially
|
||||||
|
res = res.filter((item) => {
|
||||||
|
if (idMap.has(item)) return false;
|
||||||
|
idMap.set(item, true);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// appended should be inserted into arrItems to fix the element position
|
||||||
|
arrItems.value.push(appended.value);
|
||||||
|
arrItems.value.push(res);
|
||||||
|
appended.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateItems();
|
||||||
},
|
},
|
||||||
(_err) => {
|
(_err) => {
|
||||||
|
error.value = true;
|
||||||
|
fetching.value = false;
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchMore = async (): Promise<void> => {
|
||||||
|
await fetch();
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchMoreAhead = async (): Promise<void> => {
|
const fetchMoreAhead = async (): Promise<void> => {
|
||||||
if (
|
await fetch();
|
||||||
!more.value ||
|
|
||||||
fetching.value ||
|
|
||||||
moreFetching.value ||
|
|
||||||
items.value.length === 0
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
moreFetching.value = true;
|
|
||||||
const params = props.pagination.params ? unref(props.pagination.params) : {};
|
|
||||||
await os
|
|
||||||
.api(props.pagination.endpoint, {
|
|
||||||
...params,
|
|
||||||
limit:
|
|
||||||
(props.pagination.secondFetchLimit ?? SECOND_FETCH_LIMIT_DEFAULT) + 1,
|
|
||||||
...(props.pagination.offsetMode
|
|
||||||
? {
|
|
||||||
offset: offset.value,
|
|
||||||
}
|
|
||||||
: props.pagination.reversed
|
|
||||||
? {
|
|
||||||
untilId: items.value[0].id,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
sinceId: items.value[items.value.length - 1].id,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then(
|
|
||||||
(res: Item[]) => {
|
|
||||||
if (
|
|
||||||
res.length >
|
|
||||||
(props.pagination.secondFetchLimit ?? SECOND_FETCH_LIMIT_DEFAULT)
|
|
||||||
) {
|
|
||||||
res.pop();
|
|
||||||
items.value = props.pagination.reversed
|
|
||||||
? res.toReversed().concat(items.value)
|
|
||||||
: items.value.concat(res);
|
|
||||||
more.value = true;
|
|
||||||
} else {
|
|
||||||
items.value = props.pagination.reversed
|
|
||||||
? res.toReversed().concat(items.value)
|
|
||||||
: items.value.concat(res);
|
|
||||||
more.value = false;
|
|
||||||
}
|
|
||||||
offset.value += res.length;
|
|
||||||
moreFetching.value = false;
|
|
||||||
},
|
|
||||||
(_err) => {
|
|
||||||
moreFetching.value = false;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const prepend = (item: Item): void => {
|
const prepend = (...item: Item[]): void => {
|
||||||
|
// If there are too many prepended, merge them into arrItems
|
||||||
|
if (
|
||||||
|
prepended.value.length >
|
||||||
|
(props.pagination.secondFetchLimit || SECOND_FETCH_LIMIT_DEFAULT)
|
||||||
|
) {
|
||||||
|
arrItems.value.unshift(prepended.value.toReversed());
|
||||||
|
prepended.value = [];
|
||||||
|
// We don't need to calculate here because it won't cause any changes in items
|
||||||
|
}
|
||||||
|
|
||||||
if (props.pagination.reversed) {
|
if (props.pagination.reversed) {
|
||||||
if (rootEl.value) {
|
prepended.value.push(...item);
|
||||||
const container = getScrollContainer(rootEl.value);
|
calculateItems();
|
||||||
if (container == null) {
|
|
||||||
// TODO?
|
|
||||||
} else {
|
|
||||||
const pos = getScrollPosition(rootEl.value);
|
|
||||||
const viewHeight = container.clientHeight;
|
|
||||||
const height = container.scrollHeight;
|
|
||||||
const isBottom = pos + viewHeight > height - 32;
|
|
||||||
if (isBottom) {
|
|
||||||
// オーバーフローしたら古いアイテムは捨てる
|
|
||||||
if (items.value.length >= props.displayLimit) {
|
|
||||||
// このやり方だとVue 3.2以降アニメーションが動かなくなる
|
|
||||||
// items.value = items.value.slice(-props.displayLimit);
|
|
||||||
while (items.value.length >= props.displayLimit) {
|
|
||||||
items.value.shift();
|
|
||||||
}
|
|
||||||
more.value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items.value.push(item);
|
|
||||||
// TODO
|
|
||||||
} else {
|
} else {
|
||||||
// 初回表示時はunshiftだけでOK
|
// When displaying for the first time, just do this is OK
|
||||||
if (!rootEl.value) {
|
if (!rootEl.value) {
|
||||||
items.value.unshift(item);
|
prepended.value.push(...item);
|
||||||
|
calculateItems();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,52 +417,63 @@ const prepend = (item: Item): void => {
|
||||||
(document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
|
(document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
|
||||||
|
|
||||||
if (isTop) {
|
if (isTop) {
|
||||||
// Prepend the item
|
prepended.value.push(...item);
|
||||||
items.value.unshift(item);
|
calculateItems();
|
||||||
|
|
||||||
// オーバーフローしたら古いアイテムは捨てる
|
|
||||||
if (items.value.length >= props.displayLimit) {
|
|
||||||
// このやり方だとVue 3.2以降アニメーションが動かなくなる
|
|
||||||
// this.items = items.value.slice(0, props.displayLimit);
|
|
||||||
while (items.value.length >= props.displayLimit) {
|
|
||||||
items.value.pop();
|
|
||||||
}
|
|
||||||
more.value = true;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
queue.value.push(item);
|
queue.value.push(...item);
|
||||||
onScrollTop(rootEl.value, () => {
|
onScrollTop(rootEl.value, () => {
|
||||||
for (const queueItem of queue.value) {
|
prepend(...queue.value);
|
||||||
prepend(queueItem);
|
|
||||||
}
|
|
||||||
queue.value = [];
|
queue.value = [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const append = (item: Item): void => {
|
const append = (...items: Item[]): void => {
|
||||||
items.value.push(item);
|
appended.value.push(...items);
|
||||||
|
calculateItems();
|
||||||
|
};
|
||||||
|
|
||||||
|
const _removeItem = (arr: Item[], finder: (item: Item) => boolean): boolean => {
|
||||||
|
const i = arr.findIndex(finder);
|
||||||
|
if (i === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
arr.splice(i, 1);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _updateItem = (
|
||||||
|
arr: Item[],
|
||||||
|
id: Item["id"],
|
||||||
|
replacer: (old: Item) => Item,
|
||||||
|
): boolean => {
|
||||||
|
const i = arr.findIndex((item) => item.id === id);
|
||||||
|
if (i === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
arr[i] = replacer(arr[i]);
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeItem = (finder: (item: Item) => boolean): boolean => {
|
const removeItem = (finder: (item: Item) => boolean): boolean => {
|
||||||
const i = items.value.findIndex(finder);
|
const res =
|
||||||
if (i === -1) {
|
_removeItem(prepended.value, finder) ||
|
||||||
return false;
|
_removeItem(appended.value, finder) ||
|
||||||
}
|
arrItems.value.filter((arr) => _removeItem(arr, finder)).length > 0;
|
||||||
|
calculateItems();
|
||||||
items.value.splice(i, 1);
|
return res;
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateItem = (id: Item["id"], replacer: (old: Item) => Item): boolean => {
|
const updateItem = (id: Item["id"], replacer: (old: Item) => Item): boolean => {
|
||||||
const i = items.value.findIndex((item) => item.id === id);
|
const res =
|
||||||
if (i === -1) {
|
_updateItem(prepended.value, id, replacer) ||
|
||||||
return false;
|
_updateItem(appended.value, id, replacer) ||
|
||||||
}
|
arrItems.value.filter((arr) => _updateItem(arr, id, replacer)).length > 0;
|
||||||
|
calculateItems();
|
||||||
items.value[i] = replacer(items.value[i]);
|
return res;
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (props.pagination.params && isRef<Param>(props.pagination.params)) {
|
if (props.pagination.params && isRef<Param>(props.pagination.params)) {
|
||||||
|
|
|
@ -338,7 +338,7 @@ defineExpose({
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -2px 0;
|
inset: -2px 0;
|
||||||
border: 2px solid var(--accentDarken);
|
border-bottom: 2px solid var(--accentDarken);
|
||||||
mask: linear-gradient(
|
mask: linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
transparent,
|
transparent,
|
||||||
|
|
|
@ -20,7 +20,6 @@ interface FoldOption {
|
||||||
*/
|
*/
|
||||||
export function foldItems<ItemFolded, Item>(
|
export function foldItems<ItemFolded, Item>(
|
||||||
ns: Item[],
|
ns: Item[],
|
||||||
fetch_limit: number,
|
|
||||||
classfier: (n: Item, index: number) => string,
|
classfier: (n: Item, index: number) => string,
|
||||||
aggregator: (ns: Item[], key: string) => ItemFolded,
|
aggregator: (ns: Item[], key: string) => ItemFolded,
|
||||||
_options?: FoldOption,
|
_options?: FoldOption,
|
||||||
|
@ -30,55 +29,48 @@ export function foldItems<ItemFolded, Item>(
|
||||||
const options: FoldOption = _options ?? {};
|
const options: FoldOption = _options ?? {};
|
||||||
options.skipSingleElement ??= true;
|
options.skipSingleElement ??= true;
|
||||||
|
|
||||||
for (let i = 0; i < ns.length; i += fetch_limit) {
|
const toAppendKeys: string[] = [];
|
||||||
const toFold = ns.slice(i, i + fetch_limit);
|
const foldMap = new Map<string, Item[]>();
|
||||||
const toAppendKeys: string[] = [];
|
|
||||||
const foldMap = new Map<string, Item[]>();
|
|
||||||
|
|
||||||
for (const [index, n] of toFold.entries()) {
|
for (const [index, n] of ns.entries()) {
|
||||||
const key = classfier(n, index);
|
const key = classfier(n, index);
|
||||||
const arr = foldMap.get(key);
|
const arr = foldMap.get(key);
|
||||||
if (arr != null) {
|
if (arr != null) {
|
||||||
arr.push(n);
|
arr.push(n);
|
||||||
} else {
|
} else {
|
||||||
foldMap.set(key, [n]);
|
foldMap.set(key, [n]);
|
||||||
toAppendKeys.push(key);
|
toAppendKeys.push(key);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res = res.concat(
|
|
||||||
toAppendKeys.map((key) => {
|
|
||||||
const arr = foldMap.get(key)!;
|
|
||||||
if (arr?.length === 1 && options?.skipSingleElement) {
|
|
||||||
return arr[0];
|
|
||||||
}
|
|
||||||
return aggregator(arr, key);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res = toAppendKeys.map((key) => {
|
||||||
|
const arr = foldMap.get(key)!;
|
||||||
|
if (arr?.length === 1 && options?.skipSingleElement) {
|
||||||
|
return arr[0];
|
||||||
|
}
|
||||||
|
return aggregator(arr, key);
|
||||||
|
});
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function foldNotifications(
|
export function foldNotifications(ns: entities.Notification[]) {
|
||||||
ns: entities.Notification[],
|
// By the implement of MkPagination, lastId is unique and is safe for key
|
||||||
fetch_limit: number,
|
const lastId = ns[ns.length - 1]?.id ?? "prepend";
|
||||||
) {
|
|
||||||
return foldItems(
|
return foldItems(
|
||||||
ns,
|
ns,
|
||||||
fetch_limit,
|
|
||||||
(n) => {
|
(n) => {
|
||||||
switch (n.type) {
|
switch (n.type) {
|
||||||
case "renote":
|
case "renote":
|
||||||
return `renote-of:${n.note.renote.id}`;
|
return `renote-${n.note.renote.id}`;
|
||||||
case "reaction":
|
case "reaction":
|
||||||
return `reaction:${n.reaction}:of:${n.note.id}`;
|
return `reaction-${n.reaction}-of-${n.note.id}`;
|
||||||
default: {
|
default: {
|
||||||
return `${n.id}`;
|
return `${n.id}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(ns) => {
|
(ns, key) => {
|
||||||
const represent = ns[0];
|
const represent = ns[0];
|
||||||
function check(
|
function check(
|
||||||
ns: entities.Notification[],
|
ns: entities.Notification[],
|
||||||
|
@ -94,6 +86,7 @@ export function foldNotifications(
|
||||||
userIds: ns.map((nn) => nn.userId),
|
userIds: ns.map((nn) => nn.userId),
|
||||||
users: ns.map((nn) => nn.user),
|
users: ns.map((nn) => nn.user),
|
||||||
notifications: ns!,
|
notifications: ns!,
|
||||||
|
id: `G-${lastId}-${key}`,
|
||||||
} as NotificationFolded;
|
} as NotificationFolded;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,6 +4,9 @@ export function getScrollContainer(el: HTMLElement | null): HTMLElement | null {
|
||||||
if (el == null) return null;
|
if (el == null) return null;
|
||||||
const overflow = window.getComputedStyle(el).getPropertyValue("overflow-y");
|
const overflow = window.getComputedStyle(el).getPropertyValue("overflow-y");
|
||||||
if (overflow === "scroll" || overflow === "auto") {
|
if (overflow === "scroll" || overflow === "auto") {
|
||||||
|
if (el.tagName === "HTML") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return el;
|
return el;
|
||||||
} else {
|
} else {
|
||||||
return getScrollContainer(el.parentElement);
|
return getScrollContainer(el.parentElement);
|
||||||
|
|
Loading…
Reference in a new issue