diff --git a/src/components/UniversalPostRenderer.tsx b/src/components/UniversalPostRenderer.tsx index 1e4ef61..4ebde6b 100644 --- a/src/components/UniversalPostRenderer.tsx +++ b/src/components/UniversalPostRenderer.tsx @@ -2372,7 +2372,7 @@ function PollEmbed({ did, rkey }: { did: string; rkey: string }) {
{options.map((optionText, index) => { const optionKey = ["a", "b", "c", "d"][index] as "a" | "b" | "c" | "d"; - + const { topVoterDids } = results[optionKey]; const optionState = results[optionKey]; const hasVotedForOption = optionState.hasVoted; const votePercentage = totalVotes > 0 ? (optionState.count / totalVotes) * 100 : 0; @@ -2422,21 +2422,19 @@ function PollEmbed({ did, rkey }: { did: string; rkey: string }) { {/* Avatar circles and vote count */}
{/* Avatar circles - semi overlapping */} - - {topVoters.length > 0 && ( -
- {topVoters.map((voter, idx) => ( -
- {/* The Component handles the async fetch! */} - -
- ))} -
- )} + {topVoterDids.length > 0 && ( +
+ {topVoterDids.map((did, idx) => ( +
+ +
+ ))} +
+ )} {/* Vote count */} diff --git a/src/providers/PollMutationQueueProvider.tsx b/src/providers/PollMutationQueueProvider.tsx index 25c2b8e..f253156 100644 --- a/src/providers/PollMutationQueueProvider.tsx +++ b/src/providers/PollMutationQueueProvider.tsx @@ -5,6 +5,7 @@ import { useAuth } from "~/providers/UnifiedAuthProvider"; import { renderSnack } from "~/routes/__root"; import { localPollVotesAtom, type LocalVote } from "~/utils/atoms"; import { useGetOneToOneState } from "~/utils/followState"; +import { useQueryConstellation } from "~/utils/useQuery"; // ------------------------------------------------------------------ // Types @@ -254,6 +255,7 @@ function usePollSelfVotes(pollUri: string) { ]; }, [userVotesA, userVotesB, userVotesC, userVotesD]); } +type VoterRef = { did: string }; export function usePollData( pollUri: string, @@ -261,9 +263,27 @@ export function usePollData( isMultiple: boolean, serverCounts: { a: number; b: number; c: number; d: number }, ) { + const { agent } = useAuth(); + const myDid = agent?.did; + const { castVoteRaw, getLocalVotes } = usePollMutationQueue(); - const serverUserVotes = usePollSelfVotes(pollUri); - const localVotes = getLocalVotes(pollUri); // Returns ExtendedLocalVote[] + const serverUserVotes = usePollSelfVotes(pollUri); // Our own votes from server + const localVotes = getLocalVotes(pollUri); // Pending local actions + + // 1. FETCHING - Move the logic here + // We only need the first page/subset to show avatars + const { data: votersA } = useQueryConstellation({ + method: "/links", target: pollUri, collection: "app.reddwarf.poll.vote.a", path: ".subject.uri", + }); + const { data: votersB } = useQueryConstellation({ + method: "/links", target: pollUri, collection: "app.reddwarf.poll.vote.b", path: ".subject.uri", + }); + const { data: votersC } = useQueryConstellation({ + method: "/links", target: pollUri, collection: "app.reddwarf.poll.vote.c", path: ".subject.uri", + }); + const { data: votersD } = useQueryConstellation({ + method: "/links", target: pollUri, collection: "app.reddwarf.poll.vote.d", path: ".subject.uri", + }); const handleVote = useCallback((optionKey: string) => { if (!pollCid) return; @@ -271,47 +291,65 @@ export function usePollData( }, [pollUri, pollCid, isMultiple, serverUserVotes, castVoteRaw]); return useMemo(() => { + // Helper to clean a raw list: extract DIDs, Deduplicate, Remove Self + const processServerList = (data: any) => { + const records = data?.linking_records || []; + const dids = records.map((r: any) => r.did).filter(Boolean) as string[]; + + // 2. Deduplicate everyone (Set removes duplicates) + // 3. Remove self from the list (to ensure we don't appear twice or when we shouldn't) + const uniqueOthers = new Set(dids.filter((did) => did !== myDid)); + + return Array.from(uniqueOthers); + }; + + const serverLists = { + a: processServerList(votersA), + b: processServerList(votersB), + c: processServerList(votersC), + d: processServerList(votersD), + }; + const calculateOptionState = (option: "a" | "b" | "c" | "d") => { + // --- LOGIC: Determine if we have voted (Boolean) --- const localEntry = localVotes.find((v) => v.option === option); - const isServerVoted = serverUserVotes.some((uri) => uri.includes(`app.reddwarf.poll.vote.${option}`)); + const isServerVoted = serverUserVotes.some((uri) => + uri.includes(`app.reddwarf.poll.vote.${option}`) + ); - // --- MERGE STATUS LOGIC --- let hasVoted = false; if (localEntry) { - // 1. If we have an explicit local action, it overrides everything for this option - // 'create' = true, 'delete' = false hasVoted = localEntry.action === "create"; } else { - // 2. If no local action for this specific option... if (isMultiple) { - // In multiple choice, server truth stands unless explicitly deleted (checked above) hasVoted = isServerVoted; } else { - // In single choice, we must check if we voted for *something else* locally - const hasSwitchedToOther = localVotes.some(v => v.option !== option && v.action === "create"); - if (hasSwitchedToOther) { - hasVoted = false; // Implicitly unvoted because we switched - } else { - hasVoted = isServerVoted; - } + // Single choice: if we created a vote elsewhere locally, this one is false + const hasSwitched = localVotes.some((v) => v.option !== option && v.action === "create"); + hasVoted = hasSwitched ? false : isServerVoted; } } - // --- MERGE COUNT LOGIC --- + // --- LOGIC: Calculate Count --- let count = serverCounts[option] || 0; + if (hasVoted && !isServerVoted) count++; + if (!hasVoted && isServerVoted) count = Math.max(0, count - 1); - // Adjust counts based on our "Virtual" state vs "Server" state - // If we are Voted locally but Server doesn't know -> +1 - if (hasVoted && !isServerVoted) { - count++; - } - // If we are NOT Voted locally (e.g. unvoted or switched) but Server thinks we are -> -1 - if (!hasVoted && isServerVoted) { - count = Math.max(0, count - 1); + // --- LOGIC: Finalize Avatar List --- + // 4. Add back self purely using the hasVoted state + let finalVoters = serverLists[option]; + + if (hasVoted && myDid) { + finalVoters = [myDid, ...finalVoters]; } - return { hasVoted, count }; + return { + hasVoted, + count, + // We only return the DIDs now, top 5 + topVoterDids: finalVoters.slice(0, 5) + }; }; const stateA = calculateOptionState("a"); @@ -325,5 +363,13 @@ export function usePollData( totalVotes: stateA.count + stateB.count + stateC.count + stateD.count, handleVote, }; - }, [localVotes, serverUserVotes, serverCounts, isMultiple, handleVote]); + }, [ + localVotes, + serverUserVotes, + serverCounts, + votersA, votersB, votersC, votersD, // Dependencies for fetching + isMultiple, + handleVote, + myDid, + ]); } \ No newline at end of file