diff --git a/src/components/UniversalPostRenderer.tsx b/src/components/UniversalPostRenderer.tsx index 10bb9e0..987588c 100644 --- a/src/components/UniversalPostRenderer.tsx +++ b/src/components/UniversalPostRenderer.tsx @@ -1,5 +1,5 @@ import * as ATPAPI from "@atproto/api"; -import { useQuery } from "@tanstack/react-query"; +import { useQueryClient } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; import DOMPurify from "dompurify"; import { useAtom } from "jotai"; @@ -14,11 +14,11 @@ import { enableBridgyTextAtom, enableWafrnTextAtom, imgCDNAtom, + slingshotURLAtom, } from "~/utils/atoms"; import { useGetOneToOneState } from "~/utils/followState"; import { useHydratedEmbed } from "~/utils/useHydrated"; import { - constructConstellationQuery, useQueryArbitrary, useQueryConstellation, useQueryIdentity, @@ -2152,6 +2152,9 @@ function PollEmbed({ did, rkey }: { did: string; rkey: string }) { // Query vote counts for each option const [constellationurl] = useAtom(constellationURLAtom); + const [imgcdn] = useAtom(imgCDNAtom); + const [slingshoturl] = useAtom(slingshotURLAtom); + const queryClient = useQueryClient(); const { data: voteCountsA } = useQueryConstellation({ method: "/links/count/distinct-dids", @@ -2182,45 +2185,33 @@ function PollEmbed({ did, rkey }: { did: string; rkey: string }) { }); // Query first page of voters for each option to get PFPs - const { data: votersA } = useQuery( - constructConstellationQuery({ - constellation: constellationurl, - method: "/links", - target: pollUri, - collection: "app.reddwarf.poll.vote.a", - path: ".subject.uri", - }), - ); + const { data: votersA } = useQueryConstellation({ + method: "/links", + target: pollUri, + collection: "app.reddwarf.poll.vote.a", + path: ".subject.uri", + }); - const { data: votersB } = useQuery( - constructConstellationQuery({ - constellation: constellationurl, - method: "/links", - target: pollUri, - collection: "app.reddwarf.poll.vote.b", - path: ".subject.uri", - }), - ); + const { data: votersB } = useQueryConstellation({ + method: "/links", + target: pollUri, + collection: "app.reddwarf.poll.vote.b", + path: ".subject.uri", + }); - const { data: votersC } = useQuery( - constructConstellationQuery({ - constellation: constellationurl, - method: "/links", - target: pollUri, - collection: "app.reddwarf.poll.vote.c", - path: ".subject.uri", - }), - ); + const { data: votersC } = useQueryConstellation({ + method: "/links", + target: pollUri, + collection: "app.reddwarf.poll.vote.c", + path: ".subject.uri", + }); - const { data: votersD } = useQuery( - constructConstellationQuery({ - constellation: constellationurl, - method: "/links", - target: pollUri, - collection: "app.reddwarf.poll.vote.d", - path: ".subject.uri", - }), - ); + const { data: votersD } = useQueryConstellation({ + method: "/links", + target: pollUri, + collection: "app.reddwarf.poll.vote.d", + path: ".subject.uri", + }); // Check if user has already voted for each option in this poll const userVotesA = useGetOneToOneState( @@ -2267,26 +2258,9 @@ function PollEmbed({ did, rkey }: { did: string; rkey: string }) { : undefined, ); - if (isLoading) { - return ( -
-
-
-
-
-
-
-
-
-
- ); - } - if (error || !pollRecord?.value) { - return
Failed to load poll
; - } - const poll = pollRecord.value as { + const poll = pollRecord?.value as { a: string; b: string; c?: string; @@ -2297,44 +2271,64 @@ function PollEmbed({ did, rkey }: { did: string; rkey: string }) { }; const options = [poll.a, poll.b, poll.c, poll.d].filter(Boolean); - const isExpired = false //poll.expiry ? new Date(poll.expiry) < new Date() : false; - - // todo unused waiting for private polls - // undefined for public polls which equals never expires - const formattedDate = undefined; - // const formattedDate = poll.expiry - // ? new Date(poll.expiry).toLocaleDateString("en-US", { - // month: "short", - // day: "numeric", - // hour: "numeric", - // minute: "2-digit", - // }) - // : null; // Calculate vote counts const voteData = [ { option: "a", count: parseInt((voteCountsA as any)?.total || "0"), - voters: (votersA as any)?.linking_records || [], + voters: votersA?.linking_records || [], }, { option: "b", count: parseInt((voteCountsB as any)?.total || "0"), - voters: (votersB as any)?.linking_records || [], + voters: votersB?.linking_records || [], }, { option: "c", count: parseInt((voteCountsC as any)?.total || "0"), - voters: (votersC as any)?.linking_records || [], + voters: votersC?.linking_records || [], }, { option: "d", count: parseInt((voteCountsD as any)?.total || "0"), - voters: (votersD as any)?.linking_records || [], + voters: votersD?.linking_records || [], }, ].slice(0, options.length); + if (isLoading) { + return ( +
+
+
+
+
+
+
+
+
+
+ ); + } + + if (error || !pollRecord?.value) { + return
Failed to load poll
; + } + const isExpired = false; //poll.expiry ? new Date(poll.expiry) < new Date() : false; + + // todo unused waiting for private polls + // undefined for public polls which equals never expires + const formattedDate = undefined; + // const formattedDate = poll.expiry + // ? new Date(poll.expiry).toLocaleDateString("en-US", { + // month: "short", + // day: "numeric", + // hour: "numeric", + // minute: "2-digit", + // }) + // : null; + + const totalVotes = voteData.reduce((sum, item) => sum + item.count, 0); const handleVote = async (option: string) => { @@ -2460,6 +2454,7 @@ function PollEmbed({ did, rkey }: { did: string; rkey: string }) { } })(); + const rowData = voteData.find((v) => v.option === optionKey); const hasVotedForOption = userVotesForOption && userVotesForOption.length > 0; const voteCount = @@ -2467,6 +2462,11 @@ function PollEmbed({ did, rkey }: { did: string; rkey: string }) { const votePercentage = totalVotes > 0 ? (voteCount / totalVotes) * 100 : 0; + // Extract just the DIDs we want to show (top 2) + const topVoters = rowData?.voters + .filter(v => !!v.did) + .slice(0, 2) || []; + return (
- {/* Vote count */} - - {votePercentage.toFixed(0)}% - + {/* Avatar circles and vote count */} +
+ {/* Avatar circles - semi overlapping */} + + {topVoters.length > 0 && ( +
+ {topVoters.map((voter, idx) => ( +
+ {/* The Component handles the async fetch! */} + +
+ ))} +
+ )} + + {/* Vote count */} + + {votePercentage.toFixed(0)}% + +
); })} @@ -2509,9 +2531,15 @@ function PollEmbed({ did, rkey }: { did: string; rkey: string }) {
{/* Expires {formattedDate} */} - {formattedDate ? !isExpired ? ( - Expires {formattedDate} - ) : (Expired at {formattedDate}) : Never expires} + {formattedDate ? ( + !isExpired ? ( + Expires {formattedDate} + ) : ( + Expired at {formattedDate} + ) + ) : ( + Never expires + )}
{/* Status */} @@ -2531,6 +2559,38 @@ function PollEmbed({ did, rkey }: { did: string; rkey: string }) { ); } +function PollOptionAvatar({ + did, +}: { + did: string; +}) { + const [imgcdn] = useAtom(imgCDNAtom); + // Each avatar handles its own data fetching + // If this specific DID is already in cache, it loads instantly + const { data: profileRecord } = useQueryProfile(`at://${did}/app.bsky.actor.profile/self`) + + //const profile = profileRecord?.value as ATPAPI.AppBskyActorProfile.Record; + const avatarUrl = getAvatarUrl(profileRecord, did, imgcdn); + + if (!avatarUrl) { + // Fallback grey circle + return
; + } + + return ( + voter { + const target = e.target as HTMLImageElement; + target.style.display = "none"; + target.parentElement!.style.backgroundColor = "#6b7280"; + }} + /> + ); +} + function PostEmbeds({ embed, moderation,