import { type mastodon } from "masto"; import { Accessor, createEffect, createResource } from "solid-js"; import { createStore } from "solid-js/store"; type TimelineFetchTips = { direction?: "new" | "old"; }; type Timeline = { list(params: { 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, ) { const [shot, {refetch}] = createResource( () => [timeline(), limit()] as const, async ([tl, limit]) => { const ls = await tl.list({ limit }).next(); return ls.value?.map((x) => [x]) ?? []; }, ); const [snapshot, setSnapshot] = createStore([] as mastodon.v1.Status[][]); createEffect(() => { const nls = shot(); if (!nls) return; const ols = Array.from(snapshot); // The algorithm below assumes the snapshot is not changing for (let i = 0; i < nls.length; i++) { if (i >= ols.length) { setSnapshot(i, nls[i]); } else { if (nls[i].length !== ols[i].length) { setSnapshot(i, nls[i]); } else { const oth = ols[i], nth = nls[i]; for (let j = 0; j < oth.length; j++) { const ost = oth[j], nst = nth[j]; for (const key of Object.keys( nst, ) as unknown as (keyof mastodon.v1.Status)[]) { if (ost[key] !== nst[key]) { setSnapshot(i, j, key, nst[key]); } } } } } } }); return [snapshot, shot, { refetch, mutate: setSnapshot }] as const; } export function createTimeline(timeline: Accessor) { // TODO }