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 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