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

Polls refresh state button

+305 -132
+108 -62
src/components/UniversalPostRenderer.tsx
··· 408 408 setReplies( 409 409 links 410 410 ? links?.links?.["app.bsky.feed.post"]?.[".reply.parent.uri"] 411 - ?.records || 0 411 + ?.records || 0 412 412 : null, 413 413 ); 414 414 }, [links]); ··· 456 456 457 457 const replyAturis = repliesData 458 458 ? repliesData.pages.flatMap((page) => 459 - page 460 - ? page.linking_records.map((record) => { 461 - const aturi = `at://${record.did}/${record.collection}/${record.rkey}`; 462 - return aturi; 463 - }) 464 - : [], 465 - ) 459 + page 460 + ? page.linking_records.map((record) => { 461 + const aturi = `at://${record.did}/${record.collection}/${record.rkey}`; 462 + return aturi; 463 + }) 464 + : [], 465 + ) 466 466 : []; 467 467 468 468 //const [oldestOpsReply, setOldestOpsReply] = useState<string | undefined>(undefined); ··· 622 622 opacity: 0.5, 623 623 }} 624 624 className="dark:bg-[repeating-linear-gradient(to_bottom,var(--color-gray-500)_0,var(--color-gray-400)_4px,transparent_4px,transparent_8px)]" 625 - //className="border-gray-400 dark:border-gray-500" 625 + //className="border-gray-400 dark:border-gray-500" 626 626 /> 627 627 </div> 628 628 ··· 768 768 const isQuotewithImages = 769 769 isquotewithmedia && 770 770 (hasEmbed as ATPAPI.AppBskyEmbedRecordWithMedia.Main)?.media?.$type === 771 - "app.bsky.embed.images"; 771 + "app.bsky.embed.images"; 772 772 const isQuotewithVideo = 773 773 isquotewithmedia && 774 774 (hasEmbed as ATPAPI.AppBskyEmbedRecordWithMedia.Main)?.media?.$type === 775 - "app.bsky.embed.video"; 775 + "app.bsky.embed.video"; 776 776 777 777 const hasMedia = 778 778 hasEmbed && ··· 1258 1258 import defaultpfp from "~/../public/favicon.png"; 1259 1259 import { 1260 1260 usePollData, 1261 + usePollMutationQueue, 1261 1262 } from "~/providers/PollMutationQueueProvider"; 1262 1263 import { useAuth } from "~/providers/UnifiedAuthProvider"; 1263 1264 import { renderSnack } from "~/routes/__root"; ··· 1494 1495 1495 1496 const tags = unfediwafrnTags 1496 1497 ? unfediwafrnTags 1497 - .split("\n") 1498 - .map((t) => t.trim()) 1499 - .filter(Boolean) 1498 + .split("\n") 1499 + .map((t) => t.trim()) 1500 + .filter(Boolean) 1500 1501 : undefined; 1501 1502 1502 1503 const links = tags 1503 1504 ? tags 1504 - .map((tag) => { 1505 - const encoded = encodeURIComponent(tag); 1506 - return `<a href="https://${undfediwafrnHost}/search/${encoded}" target="_blank">#${tag.replaceAll(" ", "-")}</a>`; 1507 - }) 1508 - .join("<br>") 1505 + .map((tag) => { 1506 + const encoded = encodeURIComponent(tag); 1507 + return `<a href="https://${undfediwafrnHost}/search/${encoded}" target="_blank">#${tag.replaceAll(" ", "-")}</a>`; 1508 + }) 1509 + .join("<br>") 1509 1510 : ""; 1510 1511 1511 1512 const unfediwafrn = unfediwafrnPartial ··· 1518 1519 1519 1520 /* fuck you */ 1520 1521 const isMainItem = false; 1521 - const setMainItem = (any: any) => { }; 1522 + const setMainItem = (any: any) => {}; 1522 1523 // eslint-disable-next-line react-hooks/refs 1523 1524 //console.log("Received ref in UniversalPostRenderer:", usedref); 1524 1525 return ( ··· 1532 1533 : setMainItem 1533 1534 ? onPostClick 1534 1535 ? (e) => { 1535 - setMainItem({ post: post }); 1536 - onPostClick(e); 1537 - } 1536 + setMainItem({ post: post }); 1537 + onPostClick(e); 1538 + } 1538 1539 : () => { 1539 - setMainItem({ post: post }); 1540 - } 1540 + setMainItem({ post: post }); 1541 + } 1541 1542 : undefined 1542 1543 } 1543 1544 style={{ ··· 2020 2021 try { 2021 2022 await navigator.clipboard.writeText( 2022 2023 "https://bsky.app" + 2023 - "/profile/" + 2024 - post.author.handle + 2025 - "/post/" + 2026 - post.uri.split("/").pop(), 2024 + "/profile/" + 2025 + post.author.handle + 2026 + "/post/" + 2027 + post.uri.split("/").pop(), 2027 2028 ); 2028 2029 renderSnack({ 2029 2030 title: "Copied to clipboard!", ··· 2131 2132 | AppBskyEmbedVideo.View 2132 2133 | AppBskyEmbedExternal.View 2133 2134 | AppBskyEmbedRecordWithMedia.View 2134 - | { $type: string;[k: string]: unknown }; 2135 + | { $type: string; [k: string]: unknown }; 2135 2136 2136 2137 enum PostEmbedViewContext { 2137 2138 ThreadHighlighted = "ThreadHighlighted", ··· 2148 2149 2149 2150 function PollEmbed({ did, rkey }: { did: string; rkey: string }) { 2150 2151 const { agent } = useAuth(); 2152 + const { refreshPollData } = usePollMutationQueue(); 2151 2153 const pollUri = `at://${did}/app.reddwarf.embed.poll/${rkey}`; 2152 2154 const { data: pollRecord, isLoading, error } = useQueryArbitrary(pollUri); 2153 2155 ··· 2159 2161 target: pollUri, 2160 2162 collection: "app.reddwarf.poll.vote.a", 2161 2163 path: ".subject.uri", 2164 + customkey: "constellation-polls", 2162 2165 }); 2163 2166 2164 2167 const { data: voteCountsB } = useQueryConstellation({ ··· 2166 2169 target: pollUri, 2167 2170 collection: "app.reddwarf.poll.vote.b", 2168 2171 path: ".subject.uri", 2172 + customkey: "constellation-polls", 2169 2173 }); 2170 2174 2171 2175 const { data: voteCountsC } = useQueryConstellation({ ··· 2173 2177 target: pollUri, 2174 2178 collection: "app.reddwarf.poll.vote.c", 2175 2179 path: ".subject.uri", 2180 + customkey: "constellation-polls", 2176 2181 }); 2177 2182 2178 2183 const { data: voteCountsD } = useQueryConstellation({ ··· 2180 2185 target: pollUri, 2181 2186 collection: "app.reddwarf.poll.vote.d", 2182 2187 path: ".subject.uri", 2188 + customkey: "constellation-polls", 2183 2189 }); 2184 2190 2185 2191 // Query first page of voters for Avatars 2186 2192 const { data: votersA } = useQueryConstellation({ 2187 - method: "/links", target: pollUri, collection: "app.reddwarf.poll.vote.a", path: ".subject.uri", 2193 + method: "/links", 2194 + target: pollUri, 2195 + collection: "app.reddwarf.poll.vote.a", 2196 + path: ".subject.uri", 2197 + customkey: "constellation-polls", 2188 2198 }); 2189 2199 const { data: votersB } = useQueryConstellation({ 2190 - method: "/links", target: pollUri, collection: "app.reddwarf.poll.vote.b", path: ".subject.uri", 2200 + method: "/links", 2201 + target: pollUri, 2202 + collection: "app.reddwarf.poll.vote.b", 2203 + path: ".subject.uri", 2204 + customkey: "constellation-polls", 2191 2205 }); 2192 2206 const { data: votersC } = useQueryConstellation({ 2193 - method: "/links", target: pollUri, collection: "app.reddwarf.poll.vote.c", path: ".subject.uri", 2207 + method: "/links", 2208 + target: pollUri, 2209 + collection: "app.reddwarf.poll.vote.c", 2210 + path: ".subject.uri", 2211 + customkey: "constellation-polls", 2194 2212 }); 2195 2213 const { data: votersD } = useQueryConstellation({ 2196 - method: "/links", target: pollUri, collection: "app.reddwarf.poll.vote.d", path: ".subject.uri", 2214 + method: "/links", 2215 + target: pollUri, 2216 + collection: "app.reddwarf.poll.vote.d", 2217 + path: ".subject.uri", 2218 + customkey: "constellation-polls", 2197 2219 }); 2198 2220 2199 2221 // --- 2. Prepare Data --- ··· 2226 2248 pollUri, 2227 2249 pollRecord?.cid, 2228 2250 !!poll.multiple, 2229 - serverCounts 2251 + serverCounts, 2230 2252 ); 2231 2253 2232 2254 // --- 4. Render --- ··· 2366 2388 )} 2367 2389 {poll.multiple ? "Select one or more options" : "Select one option"} 2368 2390 </span> 2391 + 2392 + {/* Refresh Button */} 2393 + <button 2394 + onClick={(e) => { 2395 + e.stopPropagation(); 2396 + refreshPollData(pollUri); 2397 + }} 2398 + className="ml-auto rounded-full h-8 outline outline-gray-200 text-gray-700 dark:outline-gray-700 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors px-3 py-1 text-[12px] flex items-center gap-1" 2399 + title="Refresh poll data" 2400 + > 2401 + <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> 2402 + <path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" /> 2403 + </svg> 2404 + Refresh 2405 + </button> 2369 2406 </div> 2370 2407 2371 2408 {/* Options List with Results */} 2372 2409 <div className="space-y-3"> 2373 2410 {options.map((optionText, index) => { 2374 - const optionKey = ["a", "b", "c", "d"][index] as "a" | "b" | "c" | "d"; 2411 + const optionKey = ["a", "b", "c", "d"][index] as 2412 + | "a" 2413 + | "b" 2414 + | "c" 2415 + | "d"; 2375 2416 const { topVoterDids } = results[optionKey]; 2376 2417 const optionState = results[optionKey]; 2377 2418 const hasVotedForOption = optionState.hasVoted; 2378 - const votePercentage = totalVotes > 0 ? (optionState.count / totalVotes) * 100 : 0; 2419 + const votePercentage = 2420 + totalVotes > 0 ? (optionState.count / totalVotes) * 100 : 0; 2379 2421 2380 2422 // Helper to get voters for avatars 2381 2423 const votersData = (() => { 2382 - if (optionKey === 'a') return votersA?.linking_records || []; 2383 - if (optionKey === 'b') return votersB?.linking_records || []; 2384 - if (optionKey === 'c') return votersC?.linking_records || []; 2385 - if (optionKey === 'd') return votersD?.linking_records || []; 2424 + if (optionKey === "a") return votersA?.linking_records || []; 2425 + if (optionKey === "b") return votersB?.linking_records || []; 2426 + if (optionKey === "c") return votersC?.linking_records || []; 2427 + if (optionKey === "d") return votersD?.linking_records || []; 2386 2428 return []; 2387 2429 })(); 2388 - const topVoters = votersData.filter((v: any) => !!v.did).slice(0, 5); 2430 + const topVoters = votersData 2431 + .filter((v: any) => !!v.did) 2432 + .slice(0, 5); 2389 2433 2390 2434 return ( 2391 2435 <div 2392 2436 key={index} 2393 - className={`group relative h-12 items-center justify-between rounded-lg border px-4 flex overflow-hidden ${!isExpired 2437 + className={`group relative h-12 items-center justify-between rounded-lg border px-4 flex overflow-hidden ${ 2438 + !isExpired 2394 2439 ? hasVotedForOption 2395 2440 ? "bg-gray-100 dark:bg-gray-950 border-gray-200 dark:border-gray-700 hover:bg-gray-200 dark:hover:bg-gray-900 cursor-pointer outline-2 outline-gray-500 dark:outline-gray-400" 2396 2441 : "bg-gray-100 dark:bg-gray-950 border-gray-200 dark:border-gray-700 hover:bg-gray-200 dark:hover:bg-gray-900 cursor-pointer" 2397 2442 : "bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700" 2398 - }`} 2443 + }`} 2399 2444 onClick={(e) => { 2400 2445 e.stopPropagation(); 2401 2446 if (!isExpired) { ··· 2423 2468 <div className="relative z-[2] flex items-center gap-2"> 2424 2469 {/* Avatar circles - semi overlapping */} 2425 2470 {topVoterDids.length > 0 && ( 2426 - <div className="flex -space-x-2"> 2427 - {topVoterDids.map((did, idx) => ( 2428 - <div 2429 - key={did} 2430 - className="w-5 h-5 rounded-full border-2 border-white dark:border-gray-900 overflow-hidden bg-gray-200" 2431 - style={{ zIndex: 5 - idx }} 2432 - > 2433 - <PollOptionAvatar did={did} /> 2434 - </div> 2435 - ))} 2436 - </div> 2437 - )} 2471 + <div className="flex -space-x-2"> 2472 + {topVoterDids.map((did, idx) => ( 2473 + <div 2474 + key={did} 2475 + className="w-5 h-5 rounded-full border-2 border-white dark:border-gray-900 overflow-hidden bg-gray-200" 2476 + style={{ zIndex: 5 - idx }} 2477 + > 2478 + <PollOptionAvatar did={did} /> 2479 + </div> 2480 + ))} 2481 + </div> 2482 + )} 2438 2483 2439 2484 {/* Vote count */} 2440 2485 <span className="text-sm font-medium text-gray-600 dark:text-gray-400"> ··· 2846 2891 width: "100%", 2847 2892 aspectRatio: image.aspectRatio 2848 2893 ? (() => { 2849 - const { width, height } = image.aspectRatio; 2850 - const ratio = width / height; 2851 - return ratio < 0.5 ? "1 / 2" : `${width} / ${height}`; 2852 - })() 2894 + const { width, height } = image.aspectRatio; 2895 + const ratio = width / height; 2896 + return ratio < 0.5 ? "1 / 2" : `${width} / ${height}`; 2897 + })() 2853 2898 : "1 / 1", // fallback to square 2854 2899 //backgroundColor: theme.background, // fallback letterboxing color 2855 2900 borderRadius: 12, ··· 3547 3592 borderRadius: 12, 3548 3593 overflow: "hidden", 3549 3594 //border: `1px solid ${theme.border}`, 3550 - paddingTop: `${100 / (aspect ? aspect.width / aspect.height : 16 / 9) 3551 - }%`, // 16:9 = 56.25%, 4:3 = 75% 3595 + paddingTop: `${ 3596 + 100 / (aspect ? aspect.width / aspect.height : 16 / 9) 3597 + }%`, // 16:9 = 56.25%, 4:3 = 75% 3552 3598 }} 3553 3599 className="border border-gray-200 dark:border-gray-800 was7" 3554 3600 >
+174 -58
src/providers/PollMutationQueueProvider.tsx
··· 1 + import { useQueryClient } from "@tanstack/react-query"; 1 2 import { useAtom } from "jotai"; 2 3 import React, { createContext, use, useCallback, useMemo } from "react"; 3 4 ··· 27 28 ) => Promise<void>; 28 29 29 30 getLocalVotes: (pollUri: string) => ExtendedLocalVote[]; 31 + refreshPollData: (pollUri?: string) => void; 30 32 } 31 33 32 34 const PollMutationContext = createContext<PollMutationContextType | undefined>( ··· 43 45 children: React.ReactNode; 44 46 }) { 45 47 const { agent } = useAuth(); 48 + const queryClient = useQueryClient(); 46 49 const [localVotes, setLocalVotes] = useAtom(localPollVotesAtom); 47 50 48 51 const getLocalVotes = useCallback( ··· 53 56 ); 54 57 55 58 const updateLocalState = useCallback( 56 - (pollUri: string, updater: (prev: ExtendedLocalVote[]) => ExtendedLocalVote[]) => { 59 + ( 60 + pollUri: string, 61 + updater: (prev: ExtendedLocalVote[]) => ExtendedLocalVote[], 62 + ) => { 57 63 setLocalVotes((prev) => ({ 58 64 ...prev, 59 65 [pollUri]: updater((prev[pollUri] || []) as ExtendedLocalVote[]), ··· 62 68 [setLocalVotes], 63 69 ); 64 70 71 + const refreshPollData = useCallback( 72 + (pollUri?: string) => { 73 + // Clear all local pending votes for this poll or all polls 74 + if (pollUri) { 75 + // Clear local state for specific poll 76 + setLocalVotes((prev) => { 77 + const newState = { ...prev }; 78 + delete newState[pollUri]; 79 + return newState; 80 + }); 81 + } else { 82 + // Clear all local votes 83 + setLocalVotes({}); 84 + } 85 + 86 + // Invalidate all poll constellation queries using predicate function 87 + queryClient.invalidateQueries({ 88 + predicate: (query) => { 89 + const queryKey = query.queryKey; 90 + return ( 91 + Array.isArray(queryKey) && queryKey.includes("constellation-polls") 92 + ); 93 + }, 94 + }); 95 + 96 + // If specific poll URI provided, also invalidate that poll's data 97 + if (pollUri) { 98 + queryClient.invalidateQueries({ 99 + queryKey: ["arbitrary", pollUri], 100 + }); 101 + } 102 + }, 103 + [queryClient, setLocalVotes], 104 + ); 105 + 65 106 const castVoteRaw = useCallback( 66 107 async ( 67 108 pollUri: string, ··· 81 122 82 123 // Check if ANY server vote exists for this option 83 124 const hasServerVote = currentServerVotes.some((uri) => 84 - uri.includes(`app.reddwarf.poll.vote.${optionKey}`) 125 + uri.includes(`app.reddwarf.poll.vote.${optionKey}`), 85 126 ); 86 127 87 128 const isCurrentlyVoted = localEntry ··· 92 133 // ACTION: UNVOTE (Toggle Off) 93 134 // ------------------------------------------------------------ 94 135 if (isCurrentlyVoted) { 95 - 96 136 // Optimistic Update: Tombstone 97 137 updateLocalState(pollUri, (prev) => { 98 - const clean = prev.filter(v => v.option !== optionKey); 99 - return [...clean, { 100 - pollUri, 101 - option: optionKey, 102 - status: "pending", 103 - action: "delete", 104 - timestamp 105 - }]; 138 + const clean = prev.filter((v) => v.option !== optionKey); 139 + return [ 140 + ...clean, 141 + { 142 + pollUri, 143 + option: optionKey, 144 + status: "pending", 145 + action: "delete", 146 + timestamp, 147 + }, 148 + ]; 106 149 }); 107 150 108 151 try { 109 152 // FIX: Collect ALL URIs for this option (Server + Local) 110 153 // We want to nuke every record that matches this option to clean up state 111 - const serverUris = currentServerVotes.filter(uri => 112 - uri.includes(`app.reddwarf.poll.vote.${optionKey}`) 154 + const serverUris = currentServerVotes.filter((uri) => 155 + uri.includes(`app.reddwarf.poll.vote.${optionKey}`), 113 156 ); 114 157 115 158 const urisToDelete = [...serverUris]; ··· 122 165 123 166 // Parallel delete for everything found 124 167 await Promise.all( 125 - uniqueUris.map(uri => { 168 + uniqueUris.map((uri) => { 126 169 const match = uri.match(/at:\/\/(.+)\/(.+)\/(.+)/); 127 170 if (!match) return Promise.resolve(); 128 171 const [, repo, collection, rkey] = match; ··· 131 174 collection, 132 175 rkey, 133 176 }); 134 - }) 177 + }), 135 178 ); 136 - 137 179 } catch (e) { 138 180 console.error("Failed to unvote", e); 139 181 renderSnack({ title: "Failed to remove vote" }); 140 182 // Revert optimistic update 141 - updateLocalState(pollUri, (prev) => prev.filter(v => v.timestamp !== timestamp)); 183 + updateLocalState(pollUri, (prev) => 184 + prev.filter((v) => v.timestamp !== timestamp), 185 + ); 142 186 } 143 187 } 144 188 ··· 146 190 // ACTION: VOTE (Toggle On) 147 191 // ------------------------------------------------------------ 148 192 else { 149 - // ... (The Vote logic remains the same, as the Single Choice cleanup 193 + // ... (The Vote logic remains the same, as the Single Choice cleanup 150 194 // logic there already iterated over the entire array) ... 151 195 152 196 updateLocalState(pollUri, (prev) => { 153 - const newState = isMultiple ? [...prev] : prev.filter(v => v.action !== 'create'); 154 - const clean = newState.filter(v => v.option !== optionKey); 155 - return [...clean, { 156 - pollUri, 157 - option: optionKey, 158 - status: "pending", 159 - action: "create", 160 - timestamp 161 - }]; 197 + const newState = isMultiple 198 + ? [...prev] 199 + : prev.filter((v) => v.action !== "create"); 200 + const clean = newState.filter((v) => v.option !== optionKey); 201 + return [ 202 + ...clean, 203 + { 204 + pollUri, 205 + option: optionKey, 206 + status: "pending", 207 + action: "create", 208 + timestamp, 209 + }, 210 + ]; 162 211 }); 163 212 164 213 // Cleanup others if single choice 165 214 if (!isMultiple) { 166 215 const votesToDelete = [ 167 216 ...currentServerVotes, 168 - ...(currentLocal.filter(v => v.action === 'create' && v.uri).map(v => v.uri) as string[]) 217 + ...(currentLocal 218 + .filter((v) => v.action === "create" && v.uri) 219 + .map((v) => v.uri) as string[]), 169 220 ]; 170 221 171 222 // This was already safe because it iterates the whole array ··· 174 225 const match = voteUri.match(/at:\/\/(.+)\/(.+)\/(.+)/); 175 226 if (match) { 176 227 const [, repo, collection, rkey] = match; 177 - agent.com.atproto.repo.deleteRecord({ repo, collection, rkey }).catch(console.error); 228 + agent.com.atproto.repo 229 + .deleteRecord({ repo, collection, rkey }) 230 + .catch(console.error); 178 231 } 179 232 }); 180 233 } ··· 192 245 }); 193 246 194 247 updateLocalState(pollUri, (prev) => { 195 - const clean = prev.filter(v => v.option !== optionKey); 196 - return [...clean, { 197 - pollUri, 198 - option: optionKey, 199 - status: "confirmed", 200 - action: "create", 201 - uri: res.data.uri, 202 - timestamp: Date.now(), 203 - }]; 248 + const clean = prev.filter((v) => v.option !== optionKey); 249 + return [ 250 + ...clean, 251 + { 252 + pollUri, 253 + option: optionKey, 254 + status: "confirmed", 255 + action: "create", 256 + uri: res.data.uri, 257 + timestamp: Date.now(), 258 + }, 259 + ]; 204 260 }); 205 261 } catch (e) { 206 262 console.error("Vote failed", e); 207 263 renderSnack({ title: "Vote failed" }); 208 - updateLocalState(pollUri, (prev) => prev.filter(v => v.timestamp !== timestamp)); 264 + updateLocalState(pollUri, (prev) => 265 + prev.filter((v) => v.timestamp !== timestamp), 266 + ); 209 267 } 210 268 } 211 269 }, ··· 213 271 ); 214 272 215 273 return ( 216 - <PollMutationContext value={{ castVoteRaw, getLocalVotes }}> 274 + <PollMutationContext 275 + value={{ castVoteRaw, getLocalVotes, refreshPollData }} 276 + > 217 277 {children} 218 278 </PollMutationContext> 219 279 ); ··· 234 294 const agentDid = agent?.did; 235 295 236 296 const userVotesA = useGetOneToOneState( 237 - agentDid ? { target: pollUri, user: agentDid, collection: "app.reddwarf.poll.vote.a", path: ".subject.uri" } : undefined 297 + agentDid 298 + ? { 299 + target: pollUri, 300 + user: agentDid, 301 + collection: "app.reddwarf.poll.vote.a", 302 + path: ".subject.uri", 303 + } 304 + : undefined, 238 305 ); 239 306 const userVotesB = useGetOneToOneState( 240 - agentDid ? { target: pollUri, user: agentDid, collection: "app.reddwarf.poll.vote.b", path: ".subject.uri" } : undefined 307 + agentDid 308 + ? { 309 + target: pollUri, 310 + user: agentDid, 311 + collection: "app.reddwarf.poll.vote.b", 312 + path: ".subject.uri", 313 + } 314 + : undefined, 241 315 ); 242 316 const userVotesC = useGetOneToOneState( 243 - agentDid ? { target: pollUri, user: agentDid, collection: "app.reddwarf.poll.vote.c", path: ".subject.uri" } : undefined 317 + agentDid 318 + ? { 319 + target: pollUri, 320 + user: agentDid, 321 + collection: "app.reddwarf.poll.vote.c", 322 + path: ".subject.uri", 323 + } 324 + : undefined, 244 325 ); 245 326 const userVotesD = useGetOneToOneState( 246 - agentDid ? { target: pollUri, user: agentDid, collection: "app.reddwarf.poll.vote.d", path: ".subject.uri" } : undefined 327 + agentDid 328 + ? { 329 + target: pollUri, 330 + user: agentDid, 331 + collection: "app.reddwarf.poll.vote.d", 332 + path: ".subject.uri", 333 + } 334 + : undefined, 247 335 ); 248 336 249 337 return useMemo(() => { ··· 273 361 // 1. FETCHING - Move the logic here 274 362 // We only need the first page/subset to show avatars 275 363 const { data: votersA } = useQueryConstellation({ 276 - method: "/links", target: pollUri, collection: "app.reddwarf.poll.vote.a", path: ".subject.uri", 364 + method: "/links", 365 + target: pollUri, 366 + collection: "app.reddwarf.poll.vote.a", 367 + path: ".subject.uri", 368 + customkey: "constellation-polls", 277 369 }); 278 370 const { data: votersB } = useQueryConstellation({ 279 - method: "/links", target: pollUri, collection: "app.reddwarf.poll.vote.b", path: ".subject.uri", 371 + method: "/links", 372 + target: pollUri, 373 + collection: "app.reddwarf.poll.vote.b", 374 + path: ".subject.uri", 375 + customkey: "constellation-polls", 280 376 }); 281 377 const { data: votersC } = useQueryConstellation({ 282 - method: "/links", target: pollUri, collection: "app.reddwarf.poll.vote.c", path: ".subject.uri", 378 + method: "/links", 379 + target: pollUri, 380 + collection: "app.reddwarf.poll.vote.c", 381 + path: ".subject.uri", 382 + customkey: "constellation-polls", 283 383 }); 284 384 const { data: votersD } = useQueryConstellation({ 285 - method: "/links", target: pollUri, collection: "app.reddwarf.poll.vote.d", path: ".subject.uri", 385 + method: "/links", 386 + target: pollUri, 387 + collection: "app.reddwarf.poll.vote.d", 388 + path: ".subject.uri", 389 + customkey: "constellation-polls", 286 390 }); 287 391 288 - const handleVote = useCallback((optionKey: string) => { 289 - if (!pollCid) return; 290 - castVoteRaw(pollUri, pollCid, optionKey, isMultiple, serverUserVotes); 291 - }, [pollUri, pollCid, isMultiple, serverUserVotes, castVoteRaw]); 392 + const handleVote = useCallback( 393 + (optionKey: string) => { 394 + if (!pollCid) return; 395 + castVoteRaw(pollUri, pollCid, optionKey, isMultiple, serverUserVotes); 396 + }, 397 + [pollUri, pollCid, isMultiple, serverUserVotes, castVoteRaw], 398 + ); 292 399 293 400 return useMemo(() => { 294 401 // Helper to clean a raw list: extract DIDs, Deduplicate, Remove Self ··· 314 421 // --- LOGIC: Determine if we have voted (Boolean) --- 315 422 const localEntry = localVotes.find((v) => v.option === option); 316 423 const isServerVoted = serverUserVotes.some((uri) => 317 - uri.includes(`app.reddwarf.poll.vote.${option}`) 424 + uri.includes(`app.reddwarf.poll.vote.${option}`), 318 425 ); 319 426 320 427 let hasVoted = false; ··· 326 433 hasVoted = isServerVoted; 327 434 } else { 328 435 // Single choice: if we created a vote elsewhere locally, this one is false 329 - const hasSwitched = localVotes.some((v) => v.option !== option && v.action === "create"); 436 + const hasSwitched = localVotes.some( 437 + (v) => v.option !== option && v.action === "create", 438 + ); 330 439 hasVoted = hasSwitched ? false : isServerVoted; 331 440 } 332 441 } ··· 348 457 hasVoted, 349 458 count, 350 459 // We only return the DIDs now, top 5 351 - topVoterDids: finalVoters.slice(0, 5) 460 + topVoterDids: finalVoters.slice(0, 5), 352 461 }; 353 462 }; 354 463 ··· 359 468 360 469 return { 361 470 results: { a: stateA, b: stateB, c: stateC, d: stateD }, 362 - hasVotedAny: stateA.hasVoted || stateB.hasVoted || stateC.hasVoted || stateD.hasVoted, 471 + hasVotedAny: 472 + stateA.hasVoted || 473 + stateB.hasVoted || 474 + stateC.hasVoted || 475 + stateD.hasVoted, 363 476 totalVotes: stateA.count + stateB.count + stateC.count + stateD.count, 364 477 handleVote, 365 478 }; ··· 367 480 localVotes, 368 481 serverUserVotes, 369 482 serverCounts, 370 - votersA, votersB, votersC, votersD, // Dependencies for fetching 483 + votersA, 484 + votersB, 485 + votersC, 486 + votersD, // Dependencies for fetching 371 487 isMultiple, 372 488 handleVote, 373 489 myDid, 374 490 ]); 375 - } 491 + }
+14 -12
src/utils/followState.ts
··· 1 - import { type Agent,AtUri } from "@atproto/api"; 1 + import { type Agent, AtUri } from "@atproto/api"; 2 2 import { TID } from "@atproto/common-web"; 3 3 import type { QueryClient } from "@tanstack/react-query"; 4 4 5 - import { type linksRecordsResponse,useQueryConstellation } from "./useQuery"; 5 + import { type linksRecordsResponse, useQueryConstellation } from "./useQuery"; 6 6 7 7 export function useGetFollowState({ 8 8 target, ··· 21 21 path: ".subject", 22 22 dids: [user], 23 23 } 24 - : { method: "undefined", target: "whatever" } 24 + : { method: "undefined", target: "whatever" }, 25 25 // overloading sucks so much 26 26 ) as { data: linksRecordsResponse | undefined }; 27 27 const follows = followData?.linking_records.slice(0, 50) ?? []; ··· 60 60 61 61 const updateCache = ( 62 62 updater: ( 63 - oldData: linksRecordsResponse | undefined 64 - ) => linksRecordsResponse | undefined 63 + oldData: linksRecordsResponse | undefined, 64 + ) => linksRecordsResponse | undefined, 65 65 ) => { 66 66 queryClient.setQueryData( 67 67 queryKey, 68 - (oldData: linksRecordsResponse | undefined) => updater(oldData) 68 + (oldData: linksRecordsResponse | undefined) => updater(oldData), 69 69 ); 70 70 }; 71 71 ··· 122 122 linking_records: old.linking_records.filter( 123 123 (rec) => 124 124 !followRecords.includes( 125 - `at://${rec.did}/${rec.collection}/${rec.rkey}` 126 - ) 125 + `at://${rec.did}/${rec.collection}/${rec.rkey}`, 126 + ), 127 127 ), 128 128 }; 129 129 }); 130 130 } 131 - 132 - 133 131 134 132 export function useGetOneToOneState(params?: { 135 133 target: string; ··· 146 144 collection: params.collection, 147 145 path: params.path, 148 146 dids: [params.user], 147 + // todo disgusting hack please never code again 148 + customkey: params.collection.includes("reddwarf.poll.vote") 149 + ? "constellation-polls" 150 + : undefined, 149 151 } 150 - : { method: "undefined", target: "whatever" } 152 + : { method: "undefined", target: "whatever" }, 151 153 // overloading sucks so much 152 154 ) as { data: linksRecordsResponse | undefined }; 153 155 if (!params || !params.user) return undefined; ··· 160 162 } 161 163 162 164 return undefined; 163 - } 165 + }
+9
src/utils/useQuery.ts
··· 239 239 path?: string; 240 240 cursor?: string; 241 241 dids?: string[]; 242 + customkey?: string; 242 243 }) { 243 244 // : QueryOptions< 244 245 // | linksRecordsResponse ··· 257 258 query?.path, 258 259 query?.cursor, 259 260 query?.dids, 261 + query?.customkey, 260 262 ] as const, 261 263 queryFn: async () => { 262 264 if (!query || query.method === "undefined") return undefined as undefined; ··· 322 324 path: string; 323 325 cursor?: string; 324 326 dids?: string[]; 327 + customkey?: string; 325 328 }): UseQueryResult<linksRecordsResponse, Error>; 326 329 export function useQueryConstellation(query: { 327 330 method: "/links/distinct-dids"; ··· 329 332 collection: string; 330 333 path: string; 331 334 cursor?: string; 335 + customkey?: string; 332 336 }): UseQueryResult<linksDidsResponse, Error>; 333 337 export function useQueryConstellation(query: { 334 338 method: "/links/count"; ··· 336 340 collection: string; 337 341 path: string; 338 342 cursor?: string; 343 + customkey?: string; 339 344 }): UseQueryResult<linksCountResponse, Error>; 340 345 export function useQueryConstellation(query: { 341 346 method: "/links/count/distinct-dids"; ··· 343 348 collection: string; 344 349 path: string; 345 350 cursor?: string; 351 + customkey?: string; 346 352 }): UseQueryResult<linksCountResponse, Error>; 347 353 export function useQueryConstellation(query: { 348 354 method: "/links/all"; 349 355 target: string; 356 + customkey?: string; 350 357 }): UseQueryResult<linksAllResponse, Error>; 351 358 export function useQueryConstellation(): undefined; 352 359 export function useQueryConstellation(query: { 353 360 method: "undefined"; 354 361 target: string; 362 + customkey?: string; 355 363 }): undefined; 356 364 export function useQueryConstellation(query?: { 357 365 method: ··· 366 374 path?: string; 367 375 cursor?: string; 368 376 dids?: string[]; 377 + customkey?: string; 369 378 }): 370 379 | UseQueryResult< 371 380 | linksRecordsResponse