All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 41s
200 lines
6.3 KiB
TypeScript
200 lines
6.3 KiB
TypeScript
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<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;
|
|
|
|
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 (
|
|
<PageShell>
|
|
<p className="page-loading">
|
|
<Trans>Loading…</Trans>
|
|
</p>
|
|
</PageShell>
|
|
);
|
|
}
|
|
|
|
if (state.status === "error") {
|
|
return (
|
|
<PageError
|
|
message={state.error}
|
|
actions={
|
|
<Link to={`/users/${username}`} className="btn-border">
|
|
<Trans>← Back to profile</Trans>
|
|
</Link>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const { profileUser, items: votes, hasMore, loadingMore } = state;
|
|
const visibleDumps = votes.filter((d) =>
|
|
votedIds.has(d.id) || d.id in fading
|
|
);
|
|
|
|
return (
|
|
<PageShell
|
|
feed={visibleDumps.length > 0 && (
|
|
<>
|
|
<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>
|
|
{hasMore && <div ref={sentinelRef} />}
|
|
{loadingMore && (
|
|
<p className="feed-loading-more">
|
|
<Trans>Loading more…</Trans>
|
|
</p>
|
|
)}
|
|
{!hasMore && (
|
|
<p className="feed-end">
|
|
<Trans>You've reached the end.</Trans>
|
|
</p>
|
|
)}
|
|
</>
|
|
)}
|
|
>
|
|
<ProfileSubpageHeader
|
|
username={username!}
|
|
profileUser={profileUser}
|
|
title={t`Upvoted`}
|
|
/>
|
|
{visibleDumps.length === 0 && (
|
|
<p className="empty-state">
|
|
<Trans>Nothing here yet.</Trans>
|
|
</p>
|
|
)}
|
|
</PageShell>
|
|
);
|
|
}
|