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 initial writes
whey.party
2 months ago
78984d8a
90bb7837
+155
-11
1 changed file
expand all
collapse all
unified
split
src
components
UniversalPostRenderer.tsx
+155
-11
src/components/UniversalPostRenderer.tsx
···
1
1
import * as ATPAPI from "@atproto/api";
2
2
+
import { useQuery } from "@tanstack/react-query";
2
3
import { useNavigate } from "@tanstack/react-router";
3
4
import DOMPurify from "dompurify";
4
5
import { useAtom } from "jotai";
···
16
17
} from "~/utils/atoms";
17
18
import { useHydratedEmbed } from "~/utils/useHydrated";
18
19
import {
20
20
+
constructConstellationQuery,
19
21
useQueryArbitrary,
20
22
useQueryConstellation,
21
23
useQueryIdentity,
···
2143
2145
};
2144
2146
2145
2147
function PollEmbed({ did, rkey }: { did: string; rkey: string }) {
2148
2148
+
const { agent } = useAuth();
2146
2149
const pollUri = `at://${did}/app.reddwarf.embed.poll/${rkey}`;
2147
2150
const { data: pollRecord, isLoading, error } = useQueryArbitrary(pollUri);
2148
2151
2152
2152
+
// Query vote counts for each option
2153
2153
+
const [constellationurl] = useAtom(constellationURLAtom);
2154
2154
+
2155
2155
+
const { data: voteCountsA } = useQueryConstellation({
2156
2156
+
method: "/links/count/distinct-dids",
2157
2157
+
target: pollUri,
2158
2158
+
collection: "app.reddwarf.poll.vote.a",
2159
2159
+
path: ".subject.uri",
2160
2160
+
});
2161
2161
+
2162
2162
+
const { data: voteCountsB } = useQueryConstellation({
2163
2163
+
method: "/links/count/distinct-dids",
2164
2164
+
target: pollUri,
2165
2165
+
collection: "app.reddwarf.poll.vote.b",
2166
2166
+
path: ".subject.uri",
2167
2167
+
});
2168
2168
+
2169
2169
+
const { data: voteCountsC } = useQueryConstellation({
2170
2170
+
method: "/links/count/distinct-dids",
2171
2171
+
target: pollUri,
2172
2172
+
collection: "app.reddwarf.poll.vote.c",
2173
2173
+
path: ".subject.uri",
2174
2174
+
});
2175
2175
+
2176
2176
+
const { data: voteCountsD } = useQueryConstellation({
2177
2177
+
method: "/links/count/distinct-dids",
2178
2178
+
target: pollUri,
2179
2179
+
collection: "app.reddwarf.poll.vote.d",
2180
2180
+
path: ".subject.uri",
2181
2181
+
});
2182
2182
+
2183
2183
+
// Query first page of voters for each option to get PFPs
2184
2184
+
const { data: votersA } = useQuery(
2185
2185
+
constructConstellationQuery({
2186
2186
+
constellation: constellationurl,
2187
2187
+
method: "/links",
2188
2188
+
target: pollUri,
2189
2189
+
collection: "app.reddwarf.poll.vote.a",
2190
2190
+
path: ".subject.uri",
2191
2191
+
}),
2192
2192
+
);
2193
2193
+
2194
2194
+
const { data: votersB } = useQuery(
2195
2195
+
constructConstellationQuery({
2196
2196
+
constellation: constellationurl,
2197
2197
+
method: "/links",
2198
2198
+
target: pollUri,
2199
2199
+
collection: "app.reddwarf.poll.vote.b",
2200
2200
+
path: ".subject.uri",
2201
2201
+
}),
2202
2202
+
);
2203
2203
+
2204
2204
+
const { data: votersC } = useQuery(
2205
2205
+
constructConstellationQuery({
2206
2206
+
constellation: constellationurl,
2207
2207
+
method: "/links",
2208
2208
+
target: pollUri,
2209
2209
+
collection: "app.reddwarf.poll.vote.c",
2210
2210
+
path: ".subject.uri",
2211
2211
+
}),
2212
2212
+
);
2213
2213
+
2214
2214
+
const { data: votersD } = useQuery(
2215
2215
+
constructConstellationQuery({
2216
2216
+
constellation: constellationurl,
2217
2217
+
method: "/links",
2218
2218
+
target: pollUri,
2219
2219
+
collection: "app.reddwarf.poll.vote.d",
2220
2220
+
path: ".subject.uri",
2221
2221
+
}),
2222
2222
+
);
2223
2223
+
2149
2224
if (isLoading) {
2150
2225
return (
2151
2226
<div className="animate-pulse">
···
2187
2262
})
2188
2263
: null;
2189
2264
2265
2265
+
// Calculate vote counts
2266
2266
+
const voteData = [
2267
2267
+
{
2268
2268
+
option: "a",
2269
2269
+
count: parseInt((voteCountsA as any)?.total || "0"),
2270
2270
+
voters: (votersA as any)?.linking_records || [],
2271
2271
+
},
2272
2272
+
{
2273
2273
+
option: "b",
2274
2274
+
count: parseInt((voteCountsB as any)?.total || "0"),
2275
2275
+
voters: (votersB as any)?.linking_records || [],
2276
2276
+
},
2277
2277
+
{
2278
2278
+
option: "c",
2279
2279
+
count: parseInt((voteCountsC as any)?.total || "0"),
2280
2280
+
voters: (votersC as any)?.linking_records || [],
2281
2281
+
},
2282
2282
+
{
2283
2283
+
option: "d",
2284
2284
+
count: parseInt((voteCountsD as any)?.total || "0"),
2285
2285
+
voters: (votersD as any)?.linking_records || [],
2286
2286
+
},
2287
2287
+
].slice(0, options.length);
2288
2288
+
2289
2289
+
const totalVotes = voteData.reduce((sum, item) => sum + item.count, 0);
2290
2290
+
2291
2291
+
const handleVote = async (option: string) => {
2292
2292
+
if (!agent || isExpired) return;
2293
2293
+
2294
2294
+
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,
2304
2304
+
},
2305
2305
+
createdAt: new Date().toISOString(),
2306
2306
+
},
2307
2307
+
// Let PDS generate rkey automatically
2308
2308
+
});
2309
2309
+
} catch (error) {
2310
2310
+
console.error("Failed to vote:", error);
2311
2311
+
}
2312
2312
+
};
2313
2313
+
2190
2314
return (
2191
2315
<div className="my-4">
2192
2316
{/* Header */}
···
2201
2325
<span className="text-sm font-normal text-gray-500 dark:text-gray-400">
2202
2326
{poll.multiple ? "Select multiple options" : "Select one option"}
2203
2327
</span>
2328
2328
+
2329
2329
+
{/* Total Votes */}
2330
2330
+
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
2331
2331
+
{totalVotes} vote{totalVotes !== 1 ? "s" : ""}
2332
2332
+
</span>
2204
2333
</div>
2205
2334
2206
2206
-
{/* Options List */}
2335
2335
+
{/* Options List with Results */}
2207
2336
<div className="space-y-3">
2208
2208
-
{options.map((optionText, index) => (
2209
2209
-
<div
2210
2210
-
key={index}
2211
2211
-
className="flex h-12 items-center justify-start truncate rounded-lg bg-gray-100 dark:bg-gray-800 px-4 text-sm font-medium text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700"
2212
2212
-
>
2213
2213
-
<span className="truncate">
2214
2214
-
{optionText}
2215
2215
-
</span>
2216
2216
-
</div>
2217
2217
-
))}
2337
2337
+
{options.map((optionText, index) => {
2338
2338
+
const optionKey = ["a", "b", "c", "d"][index];
2339
2339
+
2340
2340
+
return (
2341
2341
+
<div
2342
2342
+
key={index}
2343
2343
+
className={`relative h-12 items-center justify-between rounded-lg border px-4 flex overflow-hidden ${
2344
2344
+
!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"
2346
2346
+
: "bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700"
2347
2347
+
}`}
2348
2348
+
onClick={() => !isExpired && handleVote(optionKey)}
2349
2349
+
>
2350
2350
+
{/* Option text */}
2351
2351
+
<span className="relative z-10 text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
2352
2352
+
{optionText}
2353
2353
+
</span>
2354
2354
+
2355
2355
+
{/* Vote count */}
2356
2356
+
<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}
2358
2358
+
</span>
2359
2359
+
</div>
2360
2360
+
);
2361
2361
+
})}
2218
2362
</div>
2219
2363
2220
2364
{/* Footer */}