v4: what comes into a live list goes where its date puts it — a dump edited into a category stayed out of the index's feed of it until a reload, and a dump or a playlist made public went on top of the others' lists as if just posted. The index's feed now takes a dump edited into its scope, and orders the dumps that arrive by date, as the server pages them; a created playlist goes by date in the profile's and /playlists' lists, a new one first as before. Both use withByDate (utils/feed/pagedList.ts), which leaves an item older than every one loaded to a later page. The search runs again after a reconnection, for every page of dumps loaded, with its users and playlists; it shares refetchPages with usePagedList's resync. injectDump's doc said a private dump is never broadcast, while its poster gets it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-10 07:43:28 +00:00
parent 308a8299f3
commit 7c00db2b84
11 changed files with 278 additions and 61 deletions

View File

@@ -9,11 +9,11 @@ import { useEffect, useLayoutEffect, useRef } from "react";
import type { Dump } from "../../../shared/dump.ts";
import { useWS, useWSEvent } from "../ws/useWS.ts";
import { insertAt, usePositionMemory } from "../feed/usePositionMemory.ts";
import { withByDate } from "../../utils/feed/pagedList.ts";
/**
* Passed to {@link useDumpListSync} by the lists that need more than edits and
* deletions. The index's feed adds no dump through it, since it merges
* `WSContext`'s `recentDumps` itself.
* deletions.
*/
export interface DumpListSyncOptions {
/**
@@ -32,23 +32,11 @@ export interface DumpListSyncOptions {
* Also adds a dump `belongs` accepts that the list never held, where the
* newest-first order puts it among the dumps loaded: a new one first. One
* older than all of them is left to the next pages. For the lists sorted by
* `createdAt`, as a profile's dumps.
* `createdAt`: the index's feed, a profile's dumps, the followed users'.
*/
addNew?: boolean;
}
/**
* Returns `list` with `dump` before the first dump posted earlier, or as is
* when none was: the dump belongs to a later page.
*/
function insertByDate(list: Dump[], dump: Dump): Dump[] {
if (list.length === 0) return [dump];
const idx = list.findIndex((d) =>
d.createdAt.getTime() < dump.createdAt.getTime()
);
return idx === -1 ? list : insertAt(list, dump, idx);
}
/**
* Applies the socket's dump, comment and user events to `items`, the loaded
* list, which `setDumps` updates. A deleted dump is removed, an edited one is
@@ -96,7 +84,7 @@ export function useDumpListSync(
}
if (!accepted) return prev;
if (oldIdx !== undefined) return insertAt(prev, dump, oldIdx);
return addNew ? insertByDate(prev, dump) : prev;
return addNew ? withByDate(prev, dump) : prev;
});
});

View File

@@ -13,6 +13,7 @@ import {
firstPage,
mapItems,
type PagedList,
refetchPages,
restoredList,
} from "../../utils/feed/pagedList.ts";
import type { FeedCacheEntry } from "./useFeedCache.ts";
@@ -114,22 +115,8 @@ export function usePagedList<T extends { id: string }>(
const resync = useCallback(() => {
const { list, fetchPage } = latestRef.current;
if (!list) return;
const pages = Array.from({ length: list.page }, (_, i) => i + 1);
Promise.all(pages.map((page) => fetchPage(page)))
.then((results) => {
// An item added between two pages' fetches shifts the later page by
// one, which would list an item twice.
const byId = new Map<string, T>();
for (const item of results.flatMap((r) => r.items)) {
if (!byId.has(item.id)) byId.set(item.id, item);
}
const items = [...byId.values()];
setData((l) => ({
...l,
items,
hasMore: results[results.length - 1].hasMore,
}));
})
refetchPages(list.page, (page) => fetchPage(page))
.then(({ items, hasMore }) => setData((l) => ({ ...l, items, hasMore })))
.catch(() => {});
}, [setData]);

View File

@@ -10,6 +10,7 @@ import { useEffect, useLayoutEffect, useRef } from "react";
import type { Playlist } from "../../../shared/playlist.ts";
import { useWS, useWSEvent } from "../ws/useWS.ts";
import { insertAt, usePositionMemory } from "../feed/usePositionMemory.ts";
import { withByDate } from "../../utils/feed/pagedList.ts";
/** Passed to {@link usePlaylistListSync} by each page, to fit its list. */
export interface PlaylistListSyncOptions {
@@ -29,12 +30,12 @@ export interface PlaylistListSyncOptions {
}
/**
* Applies the socket's playlist events to `items`, the loaded list, which
* `setPlaylists` updates. A created playlist goes first, an updated one
* changes in place or leaves when it turns private (unless `isOwner`), one
* that left comes back where it was when it returns, a deleted one leaves,
* and a playlist's `dumpCount` follows its dumps. A renamed user's playlists
* take their new name.
* Applies the socket's playlist events to `items`, the loaded list, newest
* first, which `setPlaylists` updates. A created playlist goes where its date
* puts it, first when new, an updated one changes in place or leaves when it
* turns private (unless `isOwner`), one that left comes back where it was
* when it returns, a deleted one leaves, and a playlist's `dumpCount` follows
* its dumps. A renamed user's playlists take their new name.
*/
export function usePlaylistListSync(
items: Playlist[],
@@ -73,7 +74,7 @@ export function usePlaylistListSync(
if (oldIdx !== undefined) return insertAt(prev, playlist, oldIdx);
// An update never adds a playlist the list doesn't hold.
if (ev.type === "updated" || noNewEntries) return prev;
return [playlist, ...prev];
return withByDate(prev, playlist);
}
if (ev.type === "deleted") {