171 lines
5.3 KiB
TypeScript
171 lines
5.3 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { Link, useParams } from "react-router";
|
|
|
|
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<Set<string>>(new Set());
|
|
const { fading, startFading, cancelFading, cancelAll } = useFading();
|
|
const prevMyVotesRef = useRef<Set<string> | 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;
|
|
|
|
// Reset vote tracking when username changes
|
|
useEffect(() => {
|
|
cancelAll();
|
|
setVotedIds(new Set());
|
|
prevMyVotesRef.current = null;
|
|
}, [username]);
|
|
|
|
// Seed votedIds once items are loaded
|
|
useEffect(() => {
|
|
if (state.status !== "loaded") return;
|
|
setVotedIds(new Set(state.items.map((d) => d.id)));
|
|
}, [state.status]);
|
|
|
|
// Own profile: keep votedIds in sync with myVotes
|
|
useEffect(() => {
|
|
if (!profileUserId || me?.id !== profileUserId) return;
|
|
if (prevMyVotesRef.current === null) {
|
|
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]);
|
|
|
|
// WS vote events
|
|
useEffect(() => {
|
|
if (!lastVoteEvent || !profileUserId) return;
|
|
const { dumpId, voterId, action } = lastVoteEvent;
|
|
if (voterId !== profileUserId) return;
|
|
|
|
if (action === "remove") {
|
|
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]);
|
|
|
|
if (state.status === "loading") {
|
|
return (
|
|
<PageShell>
|
|
<p className="page-loading">Loading…</p>
|
|
</PageShell>
|
|
);
|
|
}
|
|
|
|
if (state.status === "error") {
|
|
return (
|
|
<PageError
|
|
message={state.error}
|
|
actions={
|
|
<Link to={`/users/${username}`} className="btn-border">
|
|
← Back to profile
|
|
</Link>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const { profileUser, items: votes, hasMore, loadingMore } = state;
|
|
const visibleDumps = votes.filter((d) =>
|
|
votedIds.has(d.id) || d.id in fading
|
|
);
|
|
|
|
return (
|
|
<PageShell>
|
|
<ProfileSubpageHeader
|
|
username={username!}
|
|
profileUser={profileUser}
|
|
title="Upvoted"
|
|
/>
|
|
|
|
{visibleDumps.length === 0
|
|
? <p className="empty-state">Nothing here yet.</p>
|
|
: (
|
|
<ul className="dump-feed">
|
|
{visibleDumps.map((dump) => {
|
|
const phase = fading[dump.id];
|
|
const extraCls = phase === "cooldown"
|
|
? "dump-card--fading"
|
|
: phase === "dismissing"
|
|
? "dump-card--dismissing"
|
|
: undefined;
|
|
return (
|
|
<DumpCard
|
|
key={dump.id}
|
|
dump={dump}
|
|
voteCount={voteCounts[dump.id] ?? dump.voteCount}
|
|
voted={myVotes.has(dump.id)}
|
|
canVote={!!me}
|
|
castVote={castVote}
|
|
removeVote={removeVote}
|
|
className={extraCls}
|
|
isOwner={!!me && me.id === dump.userId}
|
|
/>
|
|
);
|
|
})}
|
|
</ul>
|
|
)}
|
|
|
|
<div ref={sentinelRef} />
|
|
{loadingMore && <p className="feed-loading-more">Loading more…</p>}
|
|
{!hasMore && visibleDumps.length > 0 && (
|
|
<p className="index-status">All {votes.length} upvoted dumps loaded.</p>
|
|
)}
|
|
</PageShell>
|
|
);
|
|
}
|