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:
@@ -6,7 +6,7 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useLayoutEffect, useRef } from "react";
|
||||
import { Link, useSearchParams } from "react-router";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
@@ -39,11 +39,13 @@ import {
|
||||
firstPage,
|
||||
type PagedList,
|
||||
pageUrl,
|
||||
refetchPages,
|
||||
} from "../utils/feed/pagedList.ts";
|
||||
import { useFetchState } from "../hooks/shared/useFetchState.ts";
|
||||
import { useLoadMore } from "../hooks/feed/usePagedList.ts";
|
||||
import { useDumpListSync } from "../hooks/dump/useDumpListSync.ts";
|
||||
import { usePlaylistListSync } from "../hooks/playlist/usePlaylistListSync.ts";
|
||||
import { useOnReconnect } from "../hooks/ws/useWS.ts";
|
||||
import {
|
||||
SEARCH_TABS,
|
||||
searchPath,
|
||||
@@ -64,6 +66,18 @@ interface Results {
|
||||
playlists: Playlist[];
|
||||
}
|
||||
|
||||
/** Returns the first page of a search's answer, as `Search` holds it. */
|
||||
function toResults(
|
||||
{ dumps, users, playlists }: Wire<SearchResults>,
|
||||
): Results {
|
||||
return {
|
||||
dumps: firstPage(dumps.items.map(deserializeDump), dumps.hasMore),
|
||||
dumpTotal: dumps.total ?? 0,
|
||||
users: users.map(deserializePublicUser),
|
||||
playlists: playlists.map(deserializePlaylist),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Names, for each tab a category narrows, the kind of its categories and the
|
||||
* parameter of `GET /api/search` that takes one.
|
||||
@@ -80,7 +94,8 @@ const TAB_CATEGORIES: Partial<
|
||||
* from `GET /api/search`, signed in when the viewer is, so they find their own
|
||||
* private dumps and playlists too. The dumps load a page at a time as the
|
||||
* "dumps" tab scrolls. A new `q` aborts the search under way. The socket keeps
|
||||
* the dumps and playlists found live, as edited or deleted, but adds none.
|
||||
* the dumps and playlists found live, as edited or deleted, but adds none; a
|
||||
* reconnection runs the search again, for every page of dumps loaded.
|
||||
*
|
||||
* On the "dumps" and "playlists" tabs, a menu narrows the results to a
|
||||
* category of their kind, kept in the URL's `?category=`, which another tab
|
||||
@@ -113,13 +128,7 @@ export function Search(): React.JSX.Element {
|
||||
categorySlug ?? ""
|
||||
}`
|
||||
: undefined,
|
||||
(signal) =>
|
||||
search(1, signal).then(({ dumps, users, playlists }) => ({
|
||||
dumps: firstPage(dumps.items.map(deserializeDump), dumps.hasMore),
|
||||
dumpTotal: dumps.total ?? 0,
|
||||
users: users.map(deserializePublicUser),
|
||||
playlists: playlists.map(deserializePlaylist),
|
||||
})),
|
||||
(signal) => search(1, signal).then(toResults),
|
||||
);
|
||||
|
||||
const dumps = state.status === "loaded" ? state.data.dumps : null;
|
||||
@@ -159,6 +168,33 @@ export function Search(): React.JSX.Element {
|
||||
{ isOwner: true, noNewEntries: true },
|
||||
);
|
||||
|
||||
// What changed while the socket was down: every page of dumps loaded is
|
||||
// fetched again, and the users and playlists, which come with the first.
|
||||
const searchRef = useRef(search);
|
||||
useLayoutEffect(() => {
|
||||
searchRef.current = search;
|
||||
});
|
||||
useOnReconnect(() => {
|
||||
if (state.status !== "loaded") return;
|
||||
const asked = search;
|
||||
let first: Wire<SearchResults> | undefined;
|
||||
refetchPages(state.data.dumps.page, (page) =>
|
||||
asked(page).then((res) => {
|
||||
if (page === 1) first = res;
|
||||
return {
|
||||
items: res.dumps.items.map(deserializeDump),
|
||||
hasMore: res.dumps.hasMore,
|
||||
};
|
||||
}))
|
||||
.then(({ items, hasMore }) => {
|
||||
// The query changed meanwhile, and its own results are on screen.
|
||||
if (!first || asked !== searchRef.current) return;
|
||||
const fresh = toResults(first);
|
||||
setData((d) => ({ ...fresh, dumps: { ...d.dumps, items, hasMore } }));
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
const sentinelRef = useInfiniteScroll(
|
||||
loadMore,
|
||||
tab === "dumps" && canLoadMore(dumps),
|
||||
|
||||
Reference in New Issue
Block a user