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