an independent Bluesky client using Constellation, PDS Queries, and other services reddwarf.app
frontend spa bluesky reddwarf microcosm client app
92
fork

Configure Feed

Select the types of activity you want to include in your feed.

Polls initial writes

+155 -11
+155 -11
src/components/UniversalPostRenderer.tsx
··· 1 1 import * as ATPAPI from "@atproto/api"; 2 + import { useQuery } from "@tanstack/react-query"; 2 3 import { useNavigate } from "@tanstack/react-router"; 3 4 import DOMPurify from "dompurify"; 4 5 import { useAtom } from "jotai"; ··· 16 17 } from "~/utils/atoms"; 17 18 import { useHydratedEmbed } from "~/utils/useHydrated"; 18 19 import { 20 + constructConstellationQuery, 19 21 useQueryArbitrary, 20 22 useQueryConstellation, 21 23 useQueryIdentity, ··· 2143 2145 }; 2144 2146 2145 2147 function PollEmbed({ did, rkey }: { did: string; rkey: string }) { 2148 + const { agent } = useAuth(); 2146 2149 const pollUri = `at://${did}/app.reddwarf.embed.poll/${rkey}`; 2147 2150 const { data: pollRecord, isLoading, error } = useQueryArbitrary(pollUri); 2148 2151 2152 + // Query vote counts for each option 2153 + const [constellationurl] = useAtom(constellationURLAtom); 2154 + 2155 + const { data: voteCountsA } = useQueryConstellation({ 2156 + method: "/links/count/distinct-dids", 2157 + target: pollUri, 2158 + collection: "app.reddwarf.poll.vote.a", 2159 + path: ".subject.uri", 2160 + }); 2161 + 2162 + const { data: voteCountsB } = useQueryConstellation({ 2163 + method: "/links/count/distinct-dids", 2164 + target: pollUri, 2165 + collection: "app.reddwarf.poll.vote.b", 2166 + path: ".subject.uri", 2167 + }); 2168 + 2169 + const { data: voteCountsC } = useQueryConstellation({ 2170 + method: "/links/count/distinct-dids", 2171 + target: pollUri, 2172 + collection: "app.reddwarf.poll.vote.c", 2173 + path: ".subject.uri", 2174 + }); 2175 + 2176 + const { data: voteCountsD } = useQueryConstellation({ 2177 + method: "/links/count/distinct-dids", 2178 + target: pollUri, 2179 + collection: "app.reddwarf.poll.vote.d", 2180 + path: ".subject.uri", 2181 + }); 2182 + 2183 + // Query first page of voters for each option to get PFPs 2184 + const { data: votersA } = useQuery( 2185 + constructConstellationQuery({ 2186 + constellation: constellationurl, 2187 + method: "/links", 2188 + target: pollUri, 2189 + collection: "app.reddwarf.poll.vote.a", 2190 + path: ".subject.uri", 2191 + }), 2192 + ); 2193 + 2194 + const { data: votersB } = useQuery( 2195 + constructConstellationQuery({ 2196 + constellation: constellationurl, 2197 + method: "/links", 2198 + target: pollUri, 2199 + collection: "app.reddwarf.poll.vote.b", 2200 + path: ".subject.uri", 2201 + }), 2202 + ); 2203 + 2204 + const { data: votersC } = useQuery( 2205 + constructConstellationQuery({ 2206 + constellation: constellationurl, 2207 + method: "/links", 2208 + target: pollUri, 2209 + collection: "app.reddwarf.poll.vote.c", 2210 + path: ".subject.uri", 2211 + }), 2212 + ); 2213 + 2214 + const { data: votersD } = useQuery( 2215 + constructConstellationQuery({ 2216 + constellation: constellationurl, 2217 + method: "/links", 2218 + target: pollUri, 2219 + collection: "app.reddwarf.poll.vote.d", 2220 + path: ".subject.uri", 2221 + }), 2222 + ); 2223 + 2149 2224 if (isLoading) { 2150 2225 return ( 2151 2226 <div className="animate-pulse"> ··· 2187 2262 }) 2188 2263 : null; 2189 2264 2265 + // Calculate vote counts 2266 + const voteData = [ 2267 + { 2268 + option: "a", 2269 + count: parseInt((voteCountsA as any)?.total || "0"), 2270 + voters: (votersA as any)?.linking_records || [], 2271 + }, 2272 + { 2273 + option: "b", 2274 + count: parseInt((voteCountsB as any)?.total || "0"), 2275 + voters: (votersB as any)?.linking_records || [], 2276 + }, 2277 + { 2278 + option: "c", 2279 + count: parseInt((voteCountsC as any)?.total || "0"), 2280 + voters: (votersC as any)?.linking_records || [], 2281 + }, 2282 + { 2283 + option: "d", 2284 + count: parseInt((voteCountsD as any)?.total || "0"), 2285 + voters: (votersD as any)?.linking_records || [], 2286 + }, 2287 + ].slice(0, options.length); 2288 + 2289 + const totalVotes = voteData.reduce((sum, item) => sum + item.count, 0); 2290 + 2291 + const handleVote = async (option: string) => { 2292 + if (!agent || isExpired) return; 2293 + 2294 + try { 2295 + await agent.com.atproto.repo.createRecord({ 2296 + collection: `app.reddwarf.poll.vote.${option}`, 2297 + repo: agent.assertDid, 2298 + record: { 2299 + $type: `app.reddwarf.poll.vote.${option}`, 2300 + subject: { 2301 + $type: "com.atproto.repo.strongRef", 2302 + uri: pollUri, 2303 + cid: pollRecord.cid, 2304 + }, 2305 + createdAt: new Date().toISOString(), 2306 + }, 2307 + // Let PDS generate rkey automatically 2308 + }); 2309 + } catch (error) { 2310 + console.error("Failed to vote:", error); 2311 + } 2312 + }; 2313 + 2190 2314 return ( 2191 2315 <div className="my-4"> 2192 2316 {/* Header */} ··· 2201 2325 <span className="text-sm font-normal text-gray-500 dark:text-gray-400"> 2202 2326 {poll.multiple ? "Select multiple options" : "Select one option"} 2203 2327 </span> 2328 + 2329 + {/* Total Votes */} 2330 + <span className="text-sm font-medium text-gray-600 dark:text-gray-400"> 2331 + {totalVotes} vote{totalVotes !== 1 ? "s" : ""} 2332 + </span> 2204 2333 </div> 2205 2334 2206 - {/* Options List */} 2335 + {/* Options List with Results */} 2207 2336 <div className="space-y-3"> 2208 - {options.map((optionText, index) => ( 2209 - <div 2210 - key={index} 2211 - className="flex h-12 items-center justify-start truncate rounded-lg bg-gray-100 dark:bg-gray-800 px-4 text-sm font-medium text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700" 2212 - > 2213 - <span className="truncate"> 2214 - {optionText} 2215 - </span> 2216 - </div> 2217 - ))} 2337 + {options.map((optionText, index) => { 2338 + const optionKey = ["a", "b", "c", "d"][index]; 2339 + 2340 + return ( 2341 + <div 2342 + key={index} 2343 + className={`relative h-12 items-center justify-between rounded-lg border px-4 flex overflow-hidden ${ 2344 + !isExpired 2345 + ? "bg-gray-100 dark:bg-gray-800 border-gray-200 dark:border-gray-700 hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer" 2346 + : "bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700" 2347 + }`} 2348 + onClick={() => !isExpired && handleVote(optionKey)} 2349 + > 2350 + {/* Option text */} 2351 + <span className="relative z-10 text-sm font-medium text-gray-900 dark:text-gray-100 truncate"> 2352 + {optionText} 2353 + </span> 2354 + 2355 + {/* Vote count */} 2356 + <span className="relative z-10 text-sm font-medium text-gray-600 dark:text-gray-400"> 2357 + {voteData.find(v => v.option === optionKey)?.count ?? 0} 2358 + </span> 2359 + </div> 2360 + ); 2361 + })} 2218 2362 </div> 2219 2363 2220 2364 {/* Footer */}