From b34580951ff8e36d95a81cbf23ca97a05c41826e Mon Sep 17 00:00:00 2001 From: thislight Date: Sun, 13 Oct 2024 15:52:42 +0800 Subject: [PATCH] createTimeline: fix viewport and dupelication --- src/masto/timelines.ts | 221 +++++++++++--------------------- src/timelines/TimelinePanel.tsx | 12 +- 2 files changed, 78 insertions(+), 155 deletions(-) diff --git a/src/masto/timelines.ts b/src/masto/timelines.ts index 37383e9..b8082c2 100644 --- a/src/masto/timelines.ts +++ b/src/masto/timelines.ts @@ -11,114 +11,19 @@ import { } from "solid-js"; import { createStore } from "solid-js/store"; -type TimelineFetchTips = { - direction?: "new" | "old"; -}; - type Timeline = { list(params: { + /** Return results older than this ID. */ + readonly maxId?: string; + /** Return results newer than this ID. */ + readonly sinceId?: string; + /** Get a list of items with ID greater than this value excluding this ID */ + readonly minId?: string; + /** Maximum number of results to return per page. Defaults to 40. NOTE: Pagination is done with the Link header from the response. */ readonly limit?: number; }): mastodon.Paginator; }; -export function useTimeline( - timeline: Accessor, - cfg?: { - /** - * Use full refresh mode. This mode ignores paging, it will refetch the specified number - * of toots at every refetch(). - */ - fullRefresh?: number; - }, -) { - let otl: Timeline | undefined; - let npager: mastodon.Paginator | undefined; - let opager: mastodon.Paginator | undefined; - const [snapshot, { refetch }] = createResource< - { - records: mastodon.v1.Status[]; - direction: "new" | "old" | "items"; - tlChanged: boolean; - }, - [Timeline], - TimelineFetchTips | undefined - >( - () => [timeline()] as const, - async ([tl], info) => { - let tlChanged = false; - if (otl !== tl) { - npager = opager = undefined; - otl = tl; - tlChanged = true; - } - const fullRefresh = cfg?.fullRefresh; - if (typeof fullRefresh !== "undefined") { - const records = await tl - .list({ - limit: fullRefresh, - }) - .next(); - return { - direction: "items", - records: records.value ?? [], - end: records.done, - tlChanged, - }; - } - const direction = - typeof info.refetching !== "boolean" - ? (info.refetching?.direction ?? "old") - : "old"; - if (direction === "old") { - if (!opager) { - opager = tl.list({}).setDirection("next"); - } - const next = await opager.next(); - return { - direction, - records: next.value ?? [], - end: next.done, - tlChanged, - }; - } else { - if (!npager) { - npager = tl.list({}).setDirection("prev"); - } - const next = await npager.next(); - const page = next.value ?? []; - return { direction, records: page, end: next.done, tlChanged }; - } - }, - ); - - const [store, setStore] = createStore([] as mastodon.v1.Status[]); - - createEffect(() => { - const shot = snapshot(); - if (!shot) return; - const { direction, records, tlChanged } = shot; - if (tlChanged) { - setStore(() => []); - } - if (direction === "new") { - setStore((x) => [...records, ...x]); - } else if (direction === "old") { - setStore((x) => [...x, ...records]); - } else if (direction === "items") { - setStore(() => records); - } - }); - - return [ - store, - snapshot, - { - refetch, - mutate: setStore, - }, - ] as const; -} - export function createTimelineSnapshot( timeline: Accessor, limit: Accessor, @@ -176,30 +81,13 @@ export function createTimelineSnapshot( export type TimelineFetchDirection = mastodon.Direction; export type TimelineChunk = { - pager: mastodon.Paginator; + tl: Timeline; chunk: readonly mastodon.v1.Status[]; done?: boolean; direction: TimelineFetchDirection; limit: number; }; -function checkOrCreatePager( - timeline: Timeline, - limit: number, - lastPager: TimelineChunk["pager"] | undefined, - newDirection: TimelineFetchDirection, -) { - if (!lastPager) { - return timeline.list({}).setDirection(newDirection); - } else { - let pager = lastPager; - if (pager.getDirection() !== newDirection) { - pager = pager.setDirection(newDirection); - } - return pager; - } -} - type TreeNode = { parent?: TreeNode; value: T; @@ -226,6 +114,38 @@ export function createTimeline( const lookup = new ReactiveMap>(); const [threads, setThreads] = createStore([] as mastodon.v1.Status["id"][]); + let vpMaxId: string | undefined, vpMinId: string | undefined; + + const fetchExtendingPage = async ( + tl: Timeline, + direction: TimelineFetchDirection, + limit: number, + ) => { + switch (direction) { + case "next": { + const page = await tl + .list({ limit, sinceId: vpMaxId }) + .setDirection(direction) + .next(); + if ((page.value?.length ?? 0) > 0) { + vpMaxId = page.value![0].id; + } + return page; + } + + case "prev": { + const page = await tl + .list({ limit, maxId: vpMinId }) + .setDirection(direction) + .next(); + if ((page.value?.length ?? 0) > 0) { + vpMinId = page.value![page.value!.length - 1].id; + } + return page; + } + } + }; + const [chunk, { refetch }] = createResource( () => [timeline(), limit()] as const, async ( @@ -237,17 +157,16 @@ export function createTimeline( ) => { const direction = typeof info.refetching === "boolean" ? "prev" : info.refetching; - const rebuildTimeline = limit !== info.value?.limit; - const pager = rebuildTimeline - ? checkOrCreatePager(tl, limit, undefined, direction) - : checkOrCreatePager(tl, limit, info.value?.pager, direction); + const rebuildTimeline = tl !== info.value?.tl; if (rebuildTimeline) { + vpMaxId = undefined; + vpMinId = undefined; lookup.clear(); setThreads([]); } - const posts = await pager.next(); + const posts = await fetchExtendingPage(tl, direction, limit); return { - pager, + tl, chunk: posts.value ?? [], done: posts.done, direction, @@ -262,33 +181,39 @@ export function createTimeline( return; } - console.debug("fetched chunk", chk); + const existence = [] as boolean[]; - batch(() => { - for (const status of chk.chunk) { - lookup.set(status.id, { - value: status, - }); - } + for (const [idx, status] of chk.chunk.entries()) { + existence[idx] = !!untrack(() => lookup.get(status.id)); + lookup.set(status.id, { + value: status, + }); + } - for (const status of chk.chunk) { - const node = lookup.get(status.id)!; - if (status.inReplyToId) { - const parent = lookup.get(status.inReplyToId); - if (parent) { - const children = parent.children ?? []; - if (!children.find((x) => x.value.id == status.id)) { - children.push(node); - } - parent.children = children; - node.parent = parent; + for (const status of chk.chunk) { + const node = untrack(() => lookup.get(status.id))!; + if (status.inReplyToId) { + const parent = lookup.get(status.inReplyToId); + if (parent) { + const children = parent.children ?? []; + if (!children.find((x) => x.value.id == status.id)) { + children.push(node); } + parent.children = children; + node.parent = parent; } } - if (chk.direction === "next") { - setThreads((threads) => [...threads, ...chk.chunk.map((x) => x.id)]); - } else if (chk.direction === "prev") { - setThreads((threads) => [...chk.chunk.map((x) => x.id), ...threads]); + } + + const nThreadIds = chk.chunk + .filter((x, i) => !existence[i]) + .map((x) => x.id); + + batch(() => { + if (chk.direction === "prev") { + setThreads((threads) => [...threads, ...nThreadIds]); + } else if (chk.direction === "next") { + setThreads((threads) => [...nThreadIds, ...threads]); } setThreads((threads) => diff --git a/src/timelines/TimelinePanel.tsx b/src/timelines/TimelinePanel.tsx index 6750e24..b1a014d 100644 --- a/src/timelines/TimelinePanel.tsx +++ b/src/timelines/TimelinePanel.tsx @@ -21,7 +21,6 @@ const TimelinePanel: Component<{ client: mastodon.rest.Client; name: "home" | "public"; prefetch?: boolean; - fullRefetch?: number; openFullScreenToot: ( toot: mastodon.v1.Status, @@ -91,7 +90,7 @@ const TimelinePanel: Component<{ refetchTimeline("prev")} + onRefresh={() => refetchTimeline("next")} />
@@ -121,9 +120,8 @@ const TimelinePanel: Component<{ toots={toots} onBoost={onBoost} onBookmark={onBookmark} - onReply={ - ({ status }, element) => - props.openFullScreenToot(status, element, true) + onReply={({ status }, element) => + props.openFullScreenToot(status, element, true) } client={props.client} isExpended={(status) => status.id === expandedThreadId()} @@ -173,10 +171,10 @@ const TimelinePanel: Component<{ Retry - +