createTimeline: fix viewport and dupelication
This commit is contained in:
		
							parent
							
								
									295a905b8b
								
							
						
					
					
						commit
						043057557c
					
				
					 2 changed files with 78 additions and 155 deletions
				
			
		| 
						 | 
				
			
			@ -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<mastodon.v1.Status[], unknown>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function useTimeline(
 | 
			
		||||
  timeline: Accessor<Timeline>,
 | 
			
		||||
  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<mastodon.v1.Status[], unknown> | undefined;
 | 
			
		||||
  let opager: mastodon.Paginator<mastodon.v1.Status[], unknown> | 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<Timeline>,
 | 
			
		||||
  limit: Accessor<number>,
 | 
			
		||||
| 
						 | 
				
			
			@ -176,30 +81,13 @@ export function createTimelineSnapshot(
 | 
			
		|||
export type TimelineFetchDirection = mastodon.Direction;
 | 
			
		||||
 | 
			
		||||
export type TimelineChunk = {
 | 
			
		||||
  pager: mastodon.Paginator<mastodon.v1.Status[], unknown>;
 | 
			
		||||
  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<T> = {
 | 
			
		||||
  parent?: TreeNode<T>;
 | 
			
		||||
  value: T;
 | 
			
		||||
| 
						 | 
				
			
			@ -226,6 +114,38 @@ export function createTimeline(
 | 
			
		|||
  const lookup = new ReactiveMap<string, TreeNode<mastodon.v1.Status>>();
 | 
			
		||||
  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) =>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue