tangled
alpha
login
or
join now
whey.party
/
red-dwarf
82
fork
atom
an independent Bluesky client using Constellation, PDS Queries, and other services
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
client
app
82
fork
atom
overview
issues
25
pulls
pipelines
Polls dedupe pfp
whey.party
1 month ago
e4567e6a
de728f06
+86
-42
2 changed files
expand all
collapse all
unified
split
src
components
UniversalPostRenderer.tsx
providers
PollMutationQueueProvider.tsx
+14
-16
src/components/UniversalPostRenderer.tsx
···
2372
2372
<div className="space-y-3">
2373
2373
{options.map((optionText, index) => {
2374
2374
const optionKey = ["a", "b", "c", "d"][index] as "a" | "b" | "c" | "d";
2375
2375
-
2375
2375
+
const { topVoterDids } = results[optionKey];
2376
2376
const optionState = results[optionKey];
2377
2377
const hasVotedForOption = optionState.hasVoted;
2378
2378
const votePercentage = totalVotes > 0 ? (optionState.count / totalVotes) * 100 : 0;
···
2422
2422
{/* Avatar circles and vote count */}
2423
2423
<div className="relative z-[2] flex items-center gap-2">
2424
2424
{/* Avatar circles - semi overlapping */}
2425
2425
-
2426
2426
-
{topVoters.length > 0 && (
2427
2427
-
<div className="flex -space-x-2">
2428
2428
-
{topVoters.map((voter, idx) => (
2429
2429
-
<div
2430
2430
-
key={voter.did} // Use DID as key, it's stable
2431
2431
-
className="w-5 h-5 rounded-full border-2 border-white dark:border-gray-900 overflow-hidden bg-gray-200"
2432
2432
-
style={{ zIndex: 5 - idx }}
2433
2433
-
>
2434
2434
-
{/* The Component handles the async fetch! */}
2435
2435
-
<PollOptionAvatar did={voter.did} />
2436
2436
-
</div>
2437
2437
-
))}
2438
2438
-
</div>
2439
2439
-
)}
2425
2425
+
{topVoterDids.length > 0 && (
2426
2426
+
<div className="flex -space-x-2">
2427
2427
+
{topVoterDids.map((did, idx) => (
2428
2428
+
<div
2429
2429
+
key={did}
2430
2430
+
className="w-5 h-5 rounded-full border-2 border-white dark:border-gray-900 overflow-hidden bg-gray-200"
2431
2431
+
style={{ zIndex: 5 - idx }}
2432
2432
+
>
2433
2433
+
<PollOptionAvatar did={did} />
2434
2434
+
</div>
2435
2435
+
))}
2436
2436
+
</div>
2437
2437
+
)}
2440
2438
2441
2439
{/* Vote count */}
2442
2440
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
+72
-26
src/providers/PollMutationQueueProvider.tsx
···
5
5
import { renderSnack } from "~/routes/__root";
6
6
import { localPollVotesAtom, type LocalVote } from "~/utils/atoms";
7
7
import { useGetOneToOneState } from "~/utils/followState";
8
8
+
import { useQueryConstellation } from "~/utils/useQuery";
8
9
9
10
// ------------------------------------------------------------------
10
11
// Types
···
254
255
];
255
256
}, [userVotesA, userVotesB, userVotesC, userVotesD]);
256
257
}
258
258
+
type VoterRef = { did: string };
257
259
258
260
export function usePollData(
259
261
pollUri: string,
···
261
263
isMultiple: boolean,
262
264
serverCounts: { a: number; b: number; c: number; d: number },
263
265
) {
266
266
+
const { agent } = useAuth();
267
267
+
const myDid = agent?.did;
268
268
+
264
269
const { castVoteRaw, getLocalVotes } = usePollMutationQueue();
265
265
-
const serverUserVotes = usePollSelfVotes(pollUri);
266
266
-
const localVotes = getLocalVotes(pollUri); // Returns ExtendedLocalVote[]
270
270
+
const serverUserVotes = usePollSelfVotes(pollUri); // Our own votes from server
271
271
+
const localVotes = getLocalVotes(pollUri); // Pending local actions
272
272
+
273
273
+
// 1. FETCHING - Move the logic here
274
274
+
// We only need the first page/subset to show avatars
275
275
+
const { data: votersA } = useQueryConstellation({
276
276
+
method: "/links", target: pollUri, collection: "app.reddwarf.poll.vote.a", path: ".subject.uri",
277
277
+
});
278
278
+
const { data: votersB } = useQueryConstellation({
279
279
+
method: "/links", target: pollUri, collection: "app.reddwarf.poll.vote.b", path: ".subject.uri",
280
280
+
});
281
281
+
const { data: votersC } = useQueryConstellation({
282
282
+
method: "/links", target: pollUri, collection: "app.reddwarf.poll.vote.c", path: ".subject.uri",
283
283
+
});
284
284
+
const { data: votersD } = useQueryConstellation({
285
285
+
method: "/links", target: pollUri, collection: "app.reddwarf.poll.vote.d", path: ".subject.uri",
286
286
+
});
267
287
268
288
const handleVote = useCallback((optionKey: string) => {
269
289
if (!pollCid) return;
···
271
291
}, [pollUri, pollCid, isMultiple, serverUserVotes, castVoteRaw]);
272
292
273
293
return useMemo(() => {
294
294
+
// Helper to clean a raw list: extract DIDs, Deduplicate, Remove Self
295
295
+
const processServerList = (data: any) => {
296
296
+
const records = data?.linking_records || [];
297
297
+
const dids = records.map((r: any) => r.did).filter(Boolean) as string[];
298
298
+
299
299
+
// 2. Deduplicate everyone (Set removes duplicates)
300
300
+
// 3. Remove self from the list (to ensure we don't appear twice or when we shouldn't)
301
301
+
const uniqueOthers = new Set(dids.filter((did) => did !== myDid));
302
302
+
303
303
+
return Array.from(uniqueOthers);
304
304
+
};
305
305
+
306
306
+
const serverLists = {
307
307
+
a: processServerList(votersA),
308
308
+
b: processServerList(votersB),
309
309
+
c: processServerList(votersC),
310
310
+
d: processServerList(votersD),
311
311
+
};
312
312
+
274
313
const calculateOptionState = (option: "a" | "b" | "c" | "d") => {
314
314
+
// --- LOGIC: Determine if we have voted (Boolean) ---
275
315
const localEntry = localVotes.find((v) => v.option === option);
276
276
-
const isServerVoted = serverUserVotes.some((uri) => uri.includes(`app.reddwarf.poll.vote.${option}`));
316
316
+
const isServerVoted = serverUserVotes.some((uri) =>
317
317
+
uri.includes(`app.reddwarf.poll.vote.${option}`)
318
318
+
);
277
319
278
278
-
// --- MERGE STATUS LOGIC ---
279
320
let hasVoted = false;
280
321
281
322
if (localEntry) {
282
282
-
// 1. If we have an explicit local action, it overrides everything for this option
283
283
-
// 'create' = true, 'delete' = false
284
323
hasVoted = localEntry.action === "create";
285
324
} else {
286
286
-
// 2. If no local action for this specific option...
287
325
if (isMultiple) {
288
288
-
// In multiple choice, server truth stands unless explicitly deleted (checked above)
289
326
hasVoted = isServerVoted;
290
327
} else {
291
291
-
// In single choice, we must check if we voted for *something else* locally
292
292
-
const hasSwitchedToOther = localVotes.some(v => v.option !== option && v.action === "create");
293
293
-
if (hasSwitchedToOther) {
294
294
-
hasVoted = false; // Implicitly unvoted because we switched
295
295
-
} else {
296
296
-
hasVoted = isServerVoted;
297
297
-
}
328
328
+
// Single choice: if we created a vote elsewhere locally, this one is false
329
329
+
const hasSwitched = localVotes.some((v) => v.option !== option && v.action === "create");
330
330
+
hasVoted = hasSwitched ? false : isServerVoted;
298
331
}
299
332
}
300
333
301
301
-
// --- MERGE COUNT LOGIC ---
334
334
+
// --- LOGIC: Calculate Count ---
302
335
let count = serverCounts[option] || 0;
336
336
+
if (hasVoted && !isServerVoted) count++;
337
337
+
if (!hasVoted && isServerVoted) count = Math.max(0, count - 1);
303
338
304
304
-
// Adjust counts based on our "Virtual" state vs "Server" state
305
305
-
// If we are Voted locally but Server doesn't know -> +1
306
306
-
if (hasVoted && !isServerVoted) {
307
307
-
count++;
308
308
-
}
309
309
-
// If we are NOT Voted locally (e.g. unvoted or switched) but Server thinks we are -> -1
310
310
-
if (!hasVoted && isServerVoted) {
311
311
-
count = Math.max(0, count - 1);
339
339
+
// --- LOGIC: Finalize Avatar List ---
340
340
+
// 4. Add back self purely using the hasVoted state
341
341
+
let finalVoters = serverLists[option];
342
342
+
343
343
+
if (hasVoted && myDid) {
344
344
+
finalVoters = [myDid, ...finalVoters];
312
345
}
313
346
314
314
-
return { hasVoted, count };
347
347
+
return {
348
348
+
hasVoted,
349
349
+
count,
350
350
+
// We only return the DIDs now, top 5
351
351
+
topVoterDids: finalVoters.slice(0, 5)
352
352
+
};
315
353
};
316
354
317
355
const stateA = calculateOptionState("a");
···
325
363
totalVotes: stateA.count + stateB.count + stateC.count + stateD.count,
326
364
handleVote,
327
365
};
328
328
-
}, [localVotes, serverUserVotes, serverCounts, isMultiple, handleVote]);
366
366
+
}, [
367
367
+
localVotes,
368
368
+
serverUserVotes,
369
369
+
serverCounts,
370
370
+
votersA, votersB, votersC, votersD, // Dependencies for fetching
371
371
+
isMultiple,
372
372
+
handleVote,
373
373
+
myDid,
374
374
+
]);
329
375
}