119 lines
3.9 KiB
TypeScript
119 lines
3.9 KiB
TypeScript
/**
|
|
* Keeps the community feed for the components that show it, loaded from
|
|
* `GET /api/community/feed` and again whenever the socket tells of something
|
|
* the feed may hold. `App` mounts it around every page, so that the feed
|
|
* outlives the header it is shown in, which every page renders anew.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import {
|
|
type ReactNode,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import type { Wire } from "../../../shared/api.ts";
|
|
import type { CommunityFeed } from "../../../shared/community.ts";
|
|
import { COMMUNITY_FEED_URL } from "../../config/api.ts";
|
|
import { useAuth } from "../../hooks/auth/useAuth.ts";
|
|
import { useFetchState } from "../../hooks/shared/useFetchState.ts";
|
|
import { useOnReconnect, useWS, useWSEvent } from "../../hooks/ws/useWS.ts";
|
|
import { expectOk } from "../../utils/shared/api.ts";
|
|
import { deserializeCommunityFeed } from "../../utils/shared/deserialize.ts";
|
|
import {
|
|
CommunityFeedContext,
|
|
type CommunityFeedContextValue,
|
|
} from "./CommunityFeedContext.ts";
|
|
|
|
/** Passed to {@link CommunityFeedProvider} by `App`. */
|
|
export interface CommunityFeedProviderProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
// The feed is fetched again this long after the last change the socket told
|
|
// of, so that a run of upvotes costs one request.
|
|
const RELOAD_MS = 2000;
|
|
|
|
/**
|
|
* Provides {@link CommunityFeedContext}. It fetches the feed once a component
|
|
* watches it, for the signed-in user alone. The socket's messages don't carry
|
|
* a feed's lines, an upvote's naming neither its dump nor its voter, so each
|
|
* one that may change the feed has it fetched again, and the server's query
|
|
* stays the one place that decides what the feed holds. A change that comes
|
|
* while nothing watches is caught up with on the next watch.
|
|
*/
|
|
export function CommunityFeedProvider(
|
|
{ children }: CommunityFeedProviderProps,
|
|
): React.JSX.Element {
|
|
const { user, authFetch } = useAuth();
|
|
const [watched, setWatched] = useState(false);
|
|
const load = useCallback(
|
|
async (signal: AbortSignal) =>
|
|
deserializeCommunityFeed(
|
|
await expectOk<Wire<CommunityFeed>>(
|
|
await authFetch(COMMUNITY_FEED_URL, { signal }),
|
|
),
|
|
),
|
|
[authFetch],
|
|
);
|
|
const { state, reload } = useFetchState(user?.id, load, {
|
|
enabled: watched,
|
|
});
|
|
|
|
const watchers = useRef(0);
|
|
// A change came that the feed hasn't been fetched again for.
|
|
const due = useRef(false);
|
|
const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
const refresh = useCallback(() => {
|
|
if (!due.current || watchers.current === 0) return;
|
|
due.current = false;
|
|
reload();
|
|
}, [reload]);
|
|
const changed = useCallback(() => {
|
|
due.current = true;
|
|
clearTimeout(timer.current);
|
|
timer.current = setTimeout(refresh, RELOAD_MS);
|
|
}, [refresh]);
|
|
useEffect(() => () => clearTimeout(timer.current), []);
|
|
|
|
useWSEvent("dump", changed);
|
|
useWSEvent("vote", changed);
|
|
useWSEvent("comment", (e) => e.type !== "updated" && changed());
|
|
useWSEvent("playlist", (e) => e.type !== "dumps_updated" && changed());
|
|
// A rename changes the names the feed writes.
|
|
useWSEvent("user", changed);
|
|
useOnReconnect(changed);
|
|
// A deleted dump comes as no event, only as a change of this set.
|
|
const { deletedDumpIds } = useWS();
|
|
const seenDeleted = useRef(deletedDumpIds);
|
|
useEffect(() => {
|
|
if (seenDeleted.current === deletedDumpIds) return;
|
|
seenDeleted.current = deletedDumpIds;
|
|
changed();
|
|
}, [deletedDumpIds, changed]);
|
|
|
|
const watch = useCallback(() => {
|
|
watchers.current++;
|
|
setWatched(true);
|
|
refresh();
|
|
return () => {
|
|
watchers.current--;
|
|
};
|
|
}, [refresh]);
|
|
|
|
const feed = state.status === "loaded" ? state.data : null;
|
|
const value: CommunityFeedContextValue = useMemo(
|
|
() => ({ feed, watch }),
|
|
[feed, watch],
|
|
);
|
|
|
|
return (
|
|
<CommunityFeedContext.Provider value={value}>
|
|
{children}
|
|
</CommunityFeedContext.Provider>
|
|
);
|
|
}
|