a tool for shared writing and social publishing

allow multiple poll options but only use the first for now

+65 -29
+17 -9
app/lish/[did]/[publication]/[rkey]/PublishedPollBlock.tsx
··· 1 1 "use client"; 2 2 3 - import { PubLeafletBlocksPoll, PubLeafletPollDefinition } from "lexicons/api"; 3 + import { PubLeafletBlocksPoll, PubLeafletPollDefinition, PubLeafletPollVote } from "lexicons/api"; 4 4 import { useState, useEffect } from "react"; 5 5 import { ButtonPrimary, ButtonSecondary } from "components/Buttons"; 6 6 import { useIdentityData } from "components/IdentityProvider"; ··· 10 10 import { Popover } from "components/Popover"; 11 11 import LoginForm from "app/login/LoginForm"; 12 12 import { BlueskyTiny } from "components/Icons/BlueskyTiny"; 13 + 14 + // Helper function to extract the first option from a vote record 15 + const getVoteOption = (voteRecord: any): string | null => { 16 + try { 17 + const record = voteRecord as PubLeafletPollVote.Record; 18 + return record.option && record.option.length > 0 ? record.option[0] : null; 19 + } catch { 20 + return null; 21 + } 22 + }; 13 23 14 24 export const PublishedPollBlock = (props: { 15 25 block: PubLeafletBlocksPoll.Main; ··· 191 201 ? [ 192 202 ...props.pollData.atp_poll_votes, 193 203 { 194 - option: props.optimisticVote.option, 195 204 voter_did: props.optimisticVote.voter_did, 196 - poll_uri: "", 197 - poll_cid: "", 198 - uri: "", 199 - record: {}, 200 - indexed_at: "", 205 + record: { 206 + $type: "pub.leaflet.poll.vote", 207 + option: [props.optimisticVote.option], 208 + }, 201 209 }, 202 210 ] 203 211 : props.pollData.atp_poll_votes; ··· 206 214 let pollRecord = props.pollData.record as PubLeafletPollDefinition.Record; 207 215 let optionsWithCount = pollRecord.options.map((o, index) => ({ 208 216 ...o, 209 - votes: allVotes.filter((v) => v.option == index.toString()), 217 + votes: allVotes.filter((v) => getVoteOption(v.record) == index.toString()), 210 218 })); 211 219 212 220 const highestVotes = Math.max(...optionsWithCount.map((o) => o.votes.length)); ··· 214 222 <> 215 223 {pollRecord.options.map((option, index) => { 216 224 const votes = allVotes.filter( 217 - (v) => v.option === index.toString(), 225 + (v) => getVoteOption(v.record) === index.toString(), 218 226 ).length; 219 227 const isWinner = totalVotes > 0 && votes === highestVotes; 220 228
+1 -1
app/lish/[did]/[publication]/[rkey]/fetchPollData.ts
··· 8 8 uri: string; 9 9 cid: string; 10 10 record: Json; 11 - atp_poll_votes: { option: string; voter_did: string }[]; 11 + atp_poll_votes: { record: Json; voter_did: string }[]; 12 12 }; 13 13 14 14 export async function fetchPollData(pollUris: string[]): Promise<PollData[]> {
+1 -2
app/lish/[did]/[publication]/[rkey]/voteOnPublishedPoll.ts
··· 30 30 uri: pollUri, 31 31 cid: pollCid, 32 32 }, 33 - option: selectedOption, 33 + option: [selectedOption], 34 34 }; 35 35 36 36 const rkey = TID.nextStr(); ··· 42 42 voter_did: identity.atp_did, 43 43 poll_uri: pollUri, 44 44 poll_cid: pollCid, 45 - option: selectedOption, 46 45 record: voteRecord as unknown as Json, 47 46 }); 48 47
-1
appview/index.ts
··· 182 182 voter_did: evt.did, 183 183 poll_uri: record.value.poll.uri, 184 184 poll_cid: record.value.poll.cid, 185 - option: record.value.option, 186 185 record: record.value as Json, 187 186 }); 188 187 }
+12 -1
drizzle/relations.ts
··· 1 1 import { relations } from "drizzle-orm/relations"; 2 - import { identities, publications, documents, comments_on_documents, bsky_profiles, entity_sets, entities, facts, email_auth_tokens, poll_votes_on_entity, permission_tokens, phone_rsvps_to_entity, custom_domains, custom_domain_routes, email_subscriptions_to_entity, bsky_follows, subscribers_to_publications, permission_token_on_homepage, documents_in_publications, document_mentions_in_bsky, bsky_posts, publication_domains, leaflets_in_publications, publication_subscriptions, permission_token_rights } from "./schema"; 2 + import { identities, publications, documents, comments_on_documents, bsky_profiles, entity_sets, entities, facts, email_auth_tokens, poll_votes_on_entity, permission_tokens, phone_rsvps_to_entity, custom_domains, custom_domain_routes, email_subscriptions_to_entity, atp_poll_records, atp_poll_votes, bsky_follows, subscribers_to_publications, permission_token_on_homepage, documents_in_publications, document_mentions_in_bsky, bsky_posts, publication_domains, leaflets_in_publications, publication_subscriptions, permission_token_rights } from "./schema"; 3 3 4 4 export const publicationsRelations = relations(publications, ({one, many}) => ({ 5 5 identity: one(identities, { ··· 180 180 fields: [email_subscriptions_to_entity.token], 181 181 references: [permission_tokens.id] 182 182 }), 183 + })); 184 + 185 + export const atp_poll_votesRelations = relations(atp_poll_votes, ({one}) => ({ 186 + atp_poll_record: one(atp_poll_records, { 187 + fields: [atp_poll_votes.poll_uri], 188 + references: [atp_poll_records.uri] 189 + }), 190 + })); 191 + 192 + export const atp_poll_recordsRelations = relations(atp_poll_records, ({many}) => ({ 193 + atp_poll_votes: many(atp_poll_votes), 183 194 })); 184 195 185 196 export const bsky_followsRelations = relations(bsky_follows, ({one}) => ({
+24 -1
drizzle/schema.ts
··· 204 204 indexed_at: timestamp("indexed_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 205 205 }); 206 206 207 + export const atp_poll_votes = pgTable("atp_poll_votes", { 208 + uri: text("uri").primaryKey().notNull(), 209 + record: jsonb("record").notNull(), 210 + voter_did: text("voter_did").notNull(), 211 + poll_uri: text("poll_uri").notNull().references(() => atp_poll_records.uri, { onDelete: "cascade", onUpdate: "cascade" } ), 212 + poll_cid: text("poll_cid").notNull(), 213 + option: text("option").notNull(), 214 + indexed_at: timestamp("indexed_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 215 + }, 216 + (table) => { 217 + return { 218 + poll_uri_idx: index("atp_poll_votes_poll_uri_idx").on(table.poll_uri), 219 + voter_did_idx: index("atp_poll_votes_voter_did_idx").on(table.voter_did), 220 + } 221 + }); 222 + 223 + export const atp_poll_records = pgTable("atp_poll_records", { 224 + uri: text("uri").primaryKey().notNull(), 225 + cid: text("cid").notNull(), 226 + record: jsonb("record").notNull(), 227 + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 228 + }); 229 + 207 230 export const oauth_session_store = pgTable("oauth_session_store", { 208 231 key: text("key").primaryKey().notNull(), 209 232 session: jsonb("session").notNull(), 210 233 }); 211 234 212 235 export const bsky_follows = pgTable("bsky_follows", { 213 - identity: text("identity").notNull().references(() => identities.atp_did, { onDelete: "cascade" } ), 236 + identity: text("identity").default('').notNull().references(() => identities.atp_did, { onDelete: "cascade" } ), 214 237 follows: text("follows").notNull().references(() => identities.atp_did, { onDelete: "cascade" } ), 215 238 }, 216 239 (table) => {
+4 -1
lexicons/api/lexicons.ts
··· 1603 1603 ref: 'lex:com.atproto.repo.strongRef', 1604 1604 }, 1605 1605 option: { 1606 - type: 'string', 1606 + type: 'array', 1607 + items: { 1608 + type: 'string', 1609 + }, 1607 1610 }, 1608 1611 }, 1609 1612 },
+1 -1
lexicons/api/types/pub/leaflet/poll/vote.ts
··· 18 18 export interface Record { 19 19 $type: 'pub.leaflet.poll.vote' 20 20 poll: ComAtprotoRepoStrongRef.Main 21 - option: string 21 + option: string[] 22 22 [k: string]: unknown 23 23 } 24 24
+4 -1
lexicons/pub/leaflet/poll/vote.json
··· 18 18 "ref": "com.atproto.repo.strongRef" 19 19 }, 20 20 "option": { 21 - "type": "string" 21 + "type": "array", 22 + "items": { 23 + "type": "string" 24 + } 22 25 } 23 26 } 24 27 }
+1 -1
lexicons/src/polls/index.ts
··· 40 40 required: ["poll", "option"], 41 41 properties: { 42 42 poll: { type: "ref", ref: "com.atproto.repo.strongRef" }, 43 - option: { type: "string" }, 43 + option: { type: "array", items: { type: "string" } }, 44 44 }, 45 45 }, 46 46 },
-10
supabase/database.types.ts
··· 58 58 atp_poll_votes: { 59 59 Row: { 60 60 indexed_at: string 61 - option: string 62 61 poll_cid: string 63 62 poll_uri: string 64 63 record: Json ··· 67 66 } 68 67 Insert: { 69 68 indexed_at?: string 70 - option: string 71 69 poll_cid: string 72 70 poll_uri: string 73 71 record: Json ··· 76 74 } 77 75 Update: { 78 76 indexed_at?: string 79 - option?: string 80 77 poll_cid?: string 81 78 poll_uri?: string 82 79 record?: Json ··· 90 87 isOneToOne: false 91 88 referencedRelation: "atp_poll_records" 92 89 referencedColumns: ["uri"] 93 - }, 94 - { 95 - foreignKeyName: "atp_poll_votes_voter_did_fkey" 96 - columns: ["voter_did"] 97 - isOneToOne: false 98 - referencedRelation: "bsky_profiles" 99 - referencedColumns: ["did"] 100 90 }, 101 91 ] 102 92 }