diff --git a/app/lish/[did]/[publication]/[rkey]/PublishedPollBlock.tsx b/app/lish/[did]/[publication]/[rkey]/PublishedPollBlock.tsx index 2c43e9fe..c0eac21e 100644 --- a/app/lish/[did]/[publication]/[rkey]/PublishedPollBlock.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PublishedPollBlock.tsx @@ -1,6 +1,6 @@ "use client"; -import { PubLeafletBlocksPoll, PubLeafletPollDefinition } from "lexicons/api"; +import { PubLeafletBlocksPoll, PubLeafletPollDefinition, PubLeafletPollVote } from "lexicons/api"; import { useState, useEffect } from "react"; import { ButtonPrimary, ButtonSecondary } from "components/Buttons"; import { useIdentityData } from "components/IdentityProvider"; @@ -11,6 +11,16 @@ import { Popover } from "components/Popover"; import LoginForm from "app/login/LoginForm"; import { BlueskyTiny } from "components/Icons/BlueskyTiny"; +// Helper function to extract the first option from a vote record +const getVoteOption = (voteRecord: any): string | null => { + try { + const record = voteRecord as PubLeafletPollVote.Record; + return record.option && record.option.length > 0 ? record.option[0] : null; + } catch { + return null; + } +}; + export const PublishedPollBlock = (props: { block: PubLeafletBlocksPoll.Main; pollData: PollData; @@ -191,13 +201,11 @@ const PollResults = (props: { ? [ ...props.pollData.atp_poll_votes, { - option: props.optimisticVote.option, voter_did: props.optimisticVote.voter_did, - poll_uri: "", - poll_cid: "", - uri: "", - record: {}, - indexed_at: "", + record: { + $type: "pub.leaflet.poll.vote", + option: [props.optimisticVote.option], + }, }, ] : props.pollData.atp_poll_votes; @@ -206,7 +214,7 @@ const PollResults = (props: { let pollRecord = props.pollData.record as PubLeafletPollDefinition.Record; let optionsWithCount = pollRecord.options.map((o, index) => ({ ...o, - votes: allVotes.filter((v) => v.option == index.toString()), + votes: allVotes.filter((v) => getVoteOption(v.record) == index.toString()), })); const highestVotes = Math.max(...optionsWithCount.map((o) => o.votes.length)); @@ -214,7 +222,7 @@ const PollResults = (props: { <> {pollRecord.options.map((option, index) => { const votes = allVotes.filter( - (v) => v.option === index.toString(), + (v) => getVoteOption(v.record) === index.toString(), ).length; const isWinner = totalVotes > 0 && votes === highestVotes; diff --git a/app/lish/[did]/[publication]/[rkey]/fetchPollData.ts b/app/lish/[did]/[publication]/[rkey]/fetchPollData.ts index 3d59cb1b..26311d55 100644 --- a/app/lish/[did]/[publication]/[rkey]/fetchPollData.ts +++ b/app/lish/[did]/[publication]/[rkey]/fetchPollData.ts @@ -8,7 +8,7 @@ export type PollData = { uri: string; cid: string; record: Json; - atp_poll_votes: { option: string; voter_did: string }[]; + atp_poll_votes: { record: Json; voter_did: string }[]; }; export async function fetchPollData(pollUris: string[]): Promise { diff --git a/app/lish/[did]/[publication]/[rkey]/voteOnPublishedPoll.ts b/app/lish/[did]/[publication]/[rkey]/voteOnPublishedPoll.ts index 6512cf07..e04d6161 100644 --- a/app/lish/[did]/[publication]/[rkey]/voteOnPublishedPoll.ts +++ b/app/lish/[did]/[publication]/[rkey]/voteOnPublishedPoll.ts @@ -30,7 +30,7 @@ export async function voteOnPublishedPoll( uri: pollUri, cid: pollCid, }, - option: selectedOption, + option: [selectedOption], }; const rkey = TID.nextStr(); @@ -42,7 +42,6 @@ export async function voteOnPublishedPoll( voter_did: identity.atp_did, poll_uri: pollUri, poll_cid: pollCid, - option: selectedOption, record: voteRecord as unknown as Json, }); diff --git a/appview/index.ts b/appview/index.ts index efa79754..d7eee4de 100644 --- a/appview/index.ts +++ b/appview/index.ts @@ -182,7 +182,6 @@ async function handleEvent(evt: Event) { voter_did: evt.did, poll_uri: record.value.poll.uri, poll_cid: record.value.poll.cid, - option: record.value.option, record: record.value as Json, }); } diff --git a/drizzle/relations.ts b/drizzle/relations.ts index 07370757..ee948385 100644 --- a/drizzle/relations.ts +++ b/drizzle/relations.ts @@ -1,5 +1,5 @@ import { relations } from "drizzle-orm/relations"; -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"; +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"; export const publicationsRelations = relations(publications, ({one, many}) => ({ identity: one(identities, { @@ -182,6 +182,17 @@ export const email_subscriptions_to_entityRelations = relations(email_subscripti }), })); +export const atp_poll_votesRelations = relations(atp_poll_votes, ({one}) => ({ + atp_poll_record: one(atp_poll_records, { + fields: [atp_poll_votes.poll_uri], + references: [atp_poll_records.uri] + }), +})); + +export const atp_poll_recordsRelations = relations(atp_poll_records, ({many}) => ({ + atp_poll_votes: many(atp_poll_votes), +})); + export const bsky_followsRelations = relations(bsky_follows, ({one}) => ({ identity_follows: one(identities, { fields: [bsky_follows.follows], diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 2eeb931b..b270a464 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -204,13 +204,36 @@ export const documents = pgTable("documents", { indexed_at: timestamp("indexed_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), }); +export const atp_poll_votes = pgTable("atp_poll_votes", { + uri: text("uri").primaryKey().notNull(), + record: jsonb("record").notNull(), + voter_did: text("voter_did").notNull(), + poll_uri: text("poll_uri").notNull().references(() => atp_poll_records.uri, { onDelete: "cascade", onUpdate: "cascade" } ), + poll_cid: text("poll_cid").notNull(), + option: text("option").notNull(), + indexed_at: timestamp("indexed_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, +(table) => { + return { + poll_uri_idx: index("atp_poll_votes_poll_uri_idx").on(table.poll_uri), + voter_did_idx: index("atp_poll_votes_voter_did_idx").on(table.voter_did), + } +}); + +export const atp_poll_records = pgTable("atp_poll_records", { + uri: text("uri").primaryKey().notNull(), + cid: text("cid").notNull(), + record: jsonb("record").notNull(), + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}); + export const oauth_session_store = pgTable("oauth_session_store", { key: text("key").primaryKey().notNull(), session: jsonb("session").notNull(), }); export const bsky_follows = pgTable("bsky_follows", { - identity: text("identity").notNull().references(() => identities.atp_did, { onDelete: "cascade" } ), + identity: text("identity").default('').notNull().references(() => identities.atp_did, { onDelete: "cascade" } ), follows: text("follows").notNull().references(() => identities.atp_did, { onDelete: "cascade" } ), }, (table) => { diff --git a/lexicons/api/lexicons.ts b/lexicons/api/lexicons.ts index 3a699216..64a67aed 100644 --- a/lexicons/api/lexicons.ts +++ b/lexicons/api/lexicons.ts @@ -1603,7 +1603,10 @@ export const schemaDict = { ref: 'lex:com.atproto.repo.strongRef', }, option: { - type: 'string', + type: 'array', + items: { + type: 'string', + }, }, }, }, diff --git a/lexicons/api/types/pub/leaflet/poll/vote.ts b/lexicons/api/types/pub/leaflet/poll/vote.ts index 5a86c62a..3887d689 100644 --- a/lexicons/api/types/pub/leaflet/poll/vote.ts +++ b/lexicons/api/types/pub/leaflet/poll/vote.ts @@ -18,7 +18,7 @@ const id = 'pub.leaflet.poll.vote' export interface Record { $type: 'pub.leaflet.poll.vote' poll: ComAtprotoRepoStrongRef.Main - option: string + option: string[] [k: string]: unknown } diff --git a/lexicons/pub/leaflet/poll/vote.json b/lexicons/pub/leaflet/poll/vote.json index a4349071..a2ae04cd 100644 --- a/lexicons/pub/leaflet/poll/vote.json +++ b/lexicons/pub/leaflet/poll/vote.json @@ -18,7 +18,10 @@ "ref": "com.atproto.repo.strongRef" }, "option": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } } } } diff --git a/lexicons/src/polls/index.ts b/lexicons/src/polls/index.ts index d1f9aefc..2da39e38 100644 --- a/lexicons/src/polls/index.ts +++ b/lexicons/src/polls/index.ts @@ -40,7 +40,7 @@ export const PubLeafletPollVote: LexiconDoc = { required: ["poll", "option"], properties: { poll: { type: "ref", ref: "com.atproto.repo.strongRef" }, - option: { type: "string" }, + option: { type: "array", items: { type: "string" } }, }, }, }, diff --git a/supabase/database.types.ts b/supabase/database.types.ts index 59f4676a..164a9af0 100644 --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -58,7 +58,6 @@ export type Database = { atp_poll_votes: { Row: { indexed_at: string - option: string poll_cid: string poll_uri: string record: Json @@ -67,7 +66,6 @@ export type Database = { } Insert: { indexed_at?: string - option: string poll_cid: string poll_uri: string record: Json @@ -76,7 +74,6 @@ export type Database = { } Update: { indexed_at?: string - option?: string poll_cid?: string poll_uri?: string record?: Json @@ -91,13 +88,6 @@ export type Database = { referencedRelation: "atp_poll_records" referencedColumns: ["uri"] }, - { - foreignKeyName: "atp_poll_votes_voter_did_fkey" - columns: ["voter_did"] - isOneToOne: false - referencedRelation: "bsky_profiles" - referencedColumns: ["did"] - }, ] } bsky_follows: {