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 unoptimistic and slow but works i guess
whey.party
1 month ago
19fd01c3
78984d8a
+156
-16
1 changed file
expand all
collapse all
unified
split
src
components
UniversalPostRenderer.tsx
+156
-16
src/components/UniversalPostRenderer.tsx
···
15
15
enableWafrnTextAtom,
16
16
imgCDNAtom,
17
17
} from "~/utils/atoms";
18
18
+
import { useGetOneToOneState } from "~/utils/followState";
18
19
import { useHydratedEmbed } from "~/utils/useHydrated";
19
20
import {
20
21
constructConstellationQuery,
···
2221
2222
}),
2222
2223
);
2223
2224
2225
2225
+
// Check if user has already voted for each option in this poll
2226
2226
+
const userVotesA = useGetOneToOneState(
2227
2227
+
agent?.did
2228
2228
+
? {
2229
2229
+
target: pollUri,
2230
2230
+
user: agent?.did,
2231
2231
+
collection: "app.reddwarf.poll.vote.a",
2232
2232
+
path: ".subject.uri",
2233
2233
+
}
2234
2234
+
: undefined,
2235
2235
+
);
2236
2236
+
2237
2237
+
const userVotesB = useGetOneToOneState(
2238
2238
+
agent?.did
2239
2239
+
? {
2240
2240
+
target: pollUri,
2241
2241
+
user: agent?.did,
2242
2242
+
collection: "app.reddwarf.poll.vote.b",
2243
2243
+
path: ".subject.uri",
2244
2244
+
}
2245
2245
+
: undefined,
2246
2246
+
);
2247
2247
+
2248
2248
+
const userVotesC = useGetOneToOneState(
2249
2249
+
agent?.did
2250
2250
+
? {
2251
2251
+
target: pollUri,
2252
2252
+
user: agent?.did,
2253
2253
+
collection: "app.reddwarf.poll.vote.c",
2254
2254
+
path: ".subject.uri",
2255
2255
+
}
2256
2256
+
: undefined,
2257
2257
+
);
2258
2258
+
2259
2259
+
const userVotesD = useGetOneToOneState(
2260
2260
+
agent?.did
2261
2261
+
? {
2262
2262
+
target: pollUri,
2263
2263
+
user: agent?.did,
2264
2264
+
collection: "app.reddwarf.poll.vote.d",
2265
2265
+
path: ".subject.uri",
2266
2266
+
}
2267
2267
+
: undefined,
2268
2268
+
);
2269
2269
+
2224
2270
if (isLoading) {
2225
2271
return (
2226
2272
<div className="animate-pulse">
···
2292
2338
if (!agent || isExpired) return;
2293
2339
2294
2340
try {
2295
2295
-
await agent.com.atproto.repo.createRecord({
2296
2296
-
collection: `app.reddwarf.poll.vote.${option}`,
2297
2297
-
repo: agent.assertDid,
2298
2298
-
record: {
2299
2299
-
$type: `app.reddwarf.poll.vote.${option}`,
2300
2300
-
subject: {
2301
2301
-
$type: "com.atproto.repo.strongRef",
2302
2302
-
uri: pollUri,
2303
2303
-
cid: pollRecord.cid,
2341
2341
+
// Get existing votes for this option
2342
2342
+
const existingVotes = (() => {
2343
2343
+
switch (option) {
2344
2344
+
case "a":
2345
2345
+
return userVotesA;
2346
2346
+
case "b":
2347
2347
+
return userVotesB;
2348
2348
+
case "c":
2349
2349
+
return userVotesC;
2350
2350
+
case "d":
2351
2351
+
return userVotesD;
2352
2352
+
default:
2353
2353
+
return [];
2354
2354
+
}
2355
2355
+
})();
2356
2356
+
2357
2357
+
// If user has already voted for this option, delete all votes (unvote)
2358
2358
+
if (existingVotes && existingVotes.length > 0) {
2359
2359
+
for (const voteUri of existingVotes) {
2360
2360
+
const match = voteUri.match(/at:\/\/(.+)\/(.+)\/(.+)/);
2361
2361
+
if (match) {
2362
2362
+
const [, did, collection, rkey] = match;
2363
2363
+
await agent.com.atproto.repo.deleteRecord({
2364
2364
+
repo: did,
2365
2365
+
collection,
2366
2366
+
rkey,
2367
2367
+
});
2368
2368
+
}
2369
2369
+
}
2370
2370
+
} else {
2371
2371
+
// If not voted for this option, create new vote
2372
2372
+
// First, delete votes from other options if poll doesn't allow multiple votes
2373
2373
+
if (!poll.multiple) {
2374
2374
+
const otherVotes = [
2375
2375
+
...(userVotesA || []),
2376
2376
+
...(userVotesB || []),
2377
2377
+
...(userVotesC || []),
2378
2378
+
...(userVotesD || []),
2379
2379
+
].filter((vote) => {
2380
2380
+
// Filter out votes for the current option
2381
2381
+
return !vote.includes(`app.reddwarf.poll.vote.${option}`);
2382
2382
+
});
2383
2383
+
2384
2384
+
for (const voteUri of otherVotes) {
2385
2385
+
const match = voteUri.match(/at:\/\/(.+)\/(.+)\/(.+)/);
2386
2386
+
if (match) {
2387
2387
+
const [, did, collection, rkey] = match;
2388
2388
+
await agent.com.atproto.repo.deleteRecord({
2389
2389
+
repo: did,
2390
2390
+
collection,
2391
2391
+
rkey,
2392
2392
+
});
2393
2393
+
}
2394
2394
+
}
2395
2395
+
}
2396
2396
+
2397
2397
+
// Create new vote
2398
2398
+
await agent.com.atproto.repo.createRecord({
2399
2399
+
collection: `app.reddwarf.poll.vote.${option}`,
2400
2400
+
repo: agent.assertDid,
2401
2401
+
record: {
2402
2402
+
$type: `app.reddwarf.poll.vote.${option}`,
2403
2403
+
subject: {
2404
2404
+
$type: "com.atproto.repo.strongRef",
2405
2405
+
uri: pollUri,
2406
2406
+
cid: pollRecord.cid,
2407
2407
+
},
2408
2408
+
createdAt: new Date().toISOString(),
2304
2409
},
2305
2305
-
createdAt: new Date().toISOString(),
2306
2306
-
},
2307
2307
-
// Let PDS generate rkey automatically
2308
2308
-
});
2410
2410
+
// Let PDS generate rkey automatically
2411
2411
+
});
2412
2412
+
}
2309
2413
} catch (error) {
2310
2414
console.error("Failed to vote:", error);
2311
2415
}
···
2337
2441
{options.map((optionText, index) => {
2338
2442
const optionKey = ["a", "b", "c", "d"][index];
2339
2443
2444
2444
+
// Check if user has voted for this option
2445
2445
+
const userVotesForOption = (() => {
2446
2446
+
switch (optionKey) {
2447
2447
+
case "a":
2448
2448
+
return userVotesA;
2449
2449
+
case "b":
2450
2450
+
return userVotesB;
2451
2451
+
case "c":
2452
2452
+
return userVotesC;
2453
2453
+
case "d":
2454
2454
+
return userVotesD;
2455
2455
+
default:
2456
2456
+
return [];
2457
2457
+
}
2458
2458
+
})();
2459
2459
+
2460
2460
+
const hasVotedForOption =
2461
2461
+
userVotesForOption && userVotesForOption.length > 0;
2462
2462
+
const voteCount =
2463
2463
+
voteData.find((v) => v.option === optionKey)?.count ?? 0;
2464
2464
+
const votePercentage =
2465
2465
+
totalVotes > 0 ? (voteCount / totalVotes) * 100 : 0;
2466
2466
+
2340
2467
return (
2341
2468
<div
2342
2469
key={index}
2343
2343
-
className={`relative h-12 items-center justify-between rounded-lg border px-4 flex overflow-hidden ${
2470
2470
+
className={`group relative h-12 items-center justify-between rounded-lg border px-4 flex overflow-hidden ${
2344
2471
!isExpired
2345
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"
2472
2472
+
? hasVotedForOption
2473
2473
+
? "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"
2474
2474
+
: "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"
2346
2475
: "bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700"
2347
2476
}`}
2348
2477
onClick={() => !isExpired && handleVote(optionKey)}
2349
2478
>
2479
2479
+
{/* Vote percentage bar - always show */}
2480
2480
+
<div
2481
2481
+
className="absolute inset-y-0 left-0 bg-gray-300 dark:bg-gray-700 group-hover:bg-gray-400 dark:group-hover:bg-gray-600"
2482
2482
+
style={{ width: `${votePercentage}%` }}
2483
2483
+
/>
2484
2484
+
2350
2485
{/* Option text */}
2351
2486
<span className="relative z-10 text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
2352
2487
{optionText}
2488
2488
+
{hasVotedForOption && (
2489
2489
+
<span className="ml-2 text-gray-600 dark:text-gray-400">
2490
2490
+
{poll.multiple ? "✓" : "✓ (click to remove)"}
2491
2491
+
</span>
2492
2492
+
)}
2353
2493
</span>
2354
2494
2355
2495
{/* Vote count */}
2356
2496
<span className="relative z-10 text-sm font-medium text-gray-600 dark:text-gray-400">
2357
2357
-
{voteData.find(v => v.option === optionKey)?.count ?? 0}
2497
2497
+
{votePercentage.toFixed(0)}%
2358
2498
</span>
2359
2499
</div>
2360
2500
);