import { useCallback, useEffect, useRef, useState } from "react"; import { Link, useParams } from "react-router"; import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { API_URL } from "../config/api.ts"; import type { Dump } from "../model.ts"; import { deserializeDump } from "../model.ts"; import { useAuth } from "../hooks/useAuth.ts"; import { useWS } from "../hooks/useWS.ts"; import { useDumpListSync } from "../hooks/useDumpListSync.ts"; import { useFading } from "../hooks/useFading.ts"; import { useUserDumpFeed } from "../hooks/useUserDumpFeed.ts"; import { DumpCard } from "../components/DumpCard.tsx"; import { ProfileSubpageHeader } from "../components/ProfileSubpageHeader.tsx"; import { PageShell } from "../components/PageShell.tsx"; import { PageError } from "../components/PageError.tsx"; export function UserUpvoted() { const { username } = useParams(); const { user: me } = useAuth(); const { voteCounts, myVotes, lastVoteEvent, castVote, removeVote } = useWS(); const [votedIds, setVotedIds] = useState>(new Set()); const { fading, startFading, cancelFading, cancelAll } = useFading(); const prevMyVotesRef = useRef | null>(null); const onItemsAppended = useCallback((newItems: Dump[]) => { setVotedIds((prev) => new Set([...prev, ...newItems.map((d) => d.id)])); }, []); const { state, setState, setItems, sentinelRef } = useUserDumpFeed( username, "votes", `feed:user-upvoted-full:${username ?? ""}`, { onItemsAppended }, ); useDumpListSync(setItems); const profileUserId = state.status === "loaded" ? state.profileUser.id : null; const [prevUsername, setPrevUsername] = useState(username); if (prevUsername !== username) { setPrevUsername(username); setVotedIds(new Set()); } useEffect(() => { cancelAll(); prevMyVotesRef.current = null; }, [username, cancelAll]); const [prevStateStatus, setPrevStateStatus] = useState(state.status); const [prevStateItems, setPrevStateItems] = useState( state.status === "loaded" ? state.items : null, ); const currentItems = state.status === "loaded" ? state.items : null; if ( prevStateStatus !== state.status || prevStateItems !== currentItems ) { setPrevStateStatus(state.status); setPrevStateItems(currentItems); if (state.status === "loaded") { setVotedIds(new Set(state.items.map((d) => d.id))); } } useEffect(() => { if (!profileUserId || me?.id !== profileUserId) return; if (prevMyVotesRef.current === null) { // setVotedIds + prevMyVotesRef must be co-located to stay consistent. setVotedIds(new Set(myVotes)); prevMyVotesRef.current = new Set(myVotes); return; } const prev = prevMyVotesRef.current; setVotedIds(new Set(myVotes)); for (const id of prev) if (!myVotes.has(id)) startFading(id); for (const id of myVotes) if (!prev.has(id)) cancelFading(id); prevMyVotesRef.current = new Set(myVotes); }, [myVotes, me, profileUserId, startFading, cancelFading]); useEffect(() => { if (!lastVoteEvent || !profileUserId) return; const { dumpId, voterId, action } = lastVoteEvent; if (voterId !== profileUserId) return; if (action === "remove") { // setVotedIds and startFading must fire together to avoid a render with // stale votedIds between the two updates. // eslint-disable-next-line react-hooks/set-state-in-effect setVotedIds((prev) => { const n = new Set(prev); n.delete(dumpId); return n; }); startFading(dumpId); } else { setVotedIds((prev) => new Set([...prev, dumpId])); cancelFading(dumpId); fetch(`${API_URL}/api/dumps/${dumpId}`) .then((r) => r.json()) .then((body) => { if (!body.success) return; const dump = deserializeDump(body.data); setState((s) => { if (s.status !== "loaded" || s.items.some((d) => d.id === dumpId)) { return s; } return { ...s, items: [dump, ...s.items] }; }); }) .catch(() => {}); } }, [lastVoteEvent, profileUserId, startFading, cancelFading, setState]); if (state.status === "loading") { return (

Loading…

); } if (state.status === "error") { return ( ← Back to profile } /> ); } const { profileUser, items: votes, hasMore, loadingMore } = state; const visibleDumps = votes.filter((d) => votedIds.has(d.id) || d.id in fading ); return ( 0 && ( <>
    {visibleDumps.map((dump) => { const phase = fading[dump.id]; const extraCls = phase === "cooldown" ? "dump-card--fading" : phase === "dismissing" ? "dump-card--dismissing" : undefined; return ( ); })}
{hasMore &&
} {loadingMore && (

Loading more…

)} {!hasMore && (

You've reached the end.

)} )} > {visibleDumps.length === 0 && (

Nothing here yet.

)} ); }