···11import { ImageResponse } from "next/og";
22import type { Fact } from "src/replicache";
33import type { Attribute } from "src/replicache/attributes";
44-import { Database } from "../../supabase/database.types";
44+import { Database } from "supabase/database.types";
55import { createServerClient } from "@supabase/ssr";
66import { parseHSBToRGB } from "src/utils/parseHSB";
77import { cookies } from "next/headers";
-124
app/home/page.tsx
···11-import { cookies } from "next/headers";
22-import { Fact, ReplicacheProvider, useEntity } from "src/replicache";
33-import type { Attribute } from "src/replicache/attributes";
44-import {
55- ThemeBackgroundProvider,
66- ThemeProvider,
77-} from "components/ThemeManager/ThemeProvider";
88-import { EntitySetProvider } from "components/EntitySetProvider";
99-import { createIdentity } from "actions/createIdentity";
1010-import { drizzle } from "drizzle-orm/node-postgres";
1111-import { IdentitySetter } from "./IdentitySetter";
1212-1313-import { getIdentityData } from "actions/getIdentityData";
1414-import { getFactsFromHomeLeaflets } from "app/api/rpc/[command]/getFactsFromHomeLeaflets";
1515-import { supabaseServerClient } from "supabase/serverClient";
1616-import { pool } from "supabase/pool";
1717-1818-import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout";
1919-import { HomeLayout } from "./HomeLayout";
2020-2121-export default async function Home() {
2222- let cookieStore = await cookies();
2323- let auth_res = await getIdentityData();
2424- let identity: string | undefined;
2525- if (auth_res) identity = auth_res.id;
2626- else identity = cookieStore.get("identity")?.value;
2727- let needstosetcookie = false;
2828- if (!identity) {
2929- const client = await pool.connect();
3030- const db = drizzle(client);
3131- let newIdentity = await createIdentity(db);
3232- client.release();
3333- identity = newIdentity.id;
3434- needstosetcookie = true;
3535- }
3636-3737- async function setCookie() {
3838- "use server";
3939-4040- (await cookies()).set("identity", identity as string, {
4141- sameSite: "strict",
4242- });
4343- }
4444-4545- let permission_token = auth_res?.home_leaflet;
4646- if (!permission_token) {
4747- let res = await supabaseServerClient
4848- .from("identities")
4949- .select(
5050- `*,
5151- permission_tokens!identities_home_page_fkey(*, permission_token_rights(*))
5252- `,
5353- )
5454- .eq("id", identity)
5555- .single();
5656- permission_token = res.data?.permission_tokens;
5757- }
5858-5959- if (!permission_token)
6060- return (
6161- <NotFoundLayout>
6262- <p className="font-bold">Sorry, we can't find this home!</p>
6363- <p>
6464- This may be a glitch on our end. If the issue persists please{" "}
6565- <a href="mailto:contact@leaflet.pub">send us a note</a>.
6666- </p>
6767- </NotFoundLayout>
6868- );
6969- let [homeLeafletFacts, allLeafletFacts] = await Promise.all([
7070- supabaseServerClient.rpc("get_facts", {
7171- root: permission_token.root_entity,
7272- }),
7373- auth_res
7474- ? getFactsFromHomeLeaflets.handler(
7575- {
7676- tokens: auth_res.permission_token_on_homepage.map(
7777- (r) => r.permission_tokens.root_entity,
7878- ),
7979- },
8080- { supabase: supabaseServerClient },
8181- )
8282- : undefined,
8383- ]);
8484- let initialFacts =
8585- (homeLeafletFacts.data as unknown as Fact<Attribute>[]) || [];
8686-8787- let root_entity = permission_token.root_entity;
8888- let home_docs_initialFacts = allLeafletFacts?.result || {};
8989-9090- return (
9191- <ReplicacheProvider
9292- rootEntity={root_entity}
9393- token={permission_token}
9494- name={root_entity}
9595- initialFacts={initialFacts}
9696- >
9797- <IdentitySetter cb={setCookie} call={needstosetcookie} />
9898- <EntitySetProvider
9999- set={permission_token.permission_token_rights[0].entity_set}
100100- >
101101- <ThemeProvider entityID={root_entity}>
102102- <ThemeBackgroundProvider entityID={root_entity}>
103103- <HomeLayout
104104- titles={{
105105- ...home_docs_initialFacts.titles,
106106- ...auth_res?.permission_token_on_homepage.reduce(
107107- (acc, tok) => {
108108- let title =
109109- tok.permission_tokens.leaflets_in_publications[0]?.title;
110110- if (title) acc[tok.permission_tokens.root_entity] = title;
111111- return acc;
112112- },
113113- {} as { [k: string]: string },
114114- ),
115115- }}
116116- entityID={root_entity}
117117- initialFacts={home_docs_initialFacts.facts || {}}
118118- />
119119- </ThemeBackgroundProvider>
120120- </ThemeProvider>
121121- </EntitySetProvider>
122122- </ReplicacheProvider>
123123- );
124124-}
···88import { theme } from "tailwind.config";
99import { useEntity, useReplicache } from "src/replicache";
1010import { v7 } from "uuid";
1111-import { usePollData } from "components/PageSWRDataProvider";
1111+import {
1212+ useLeafletPublicationData,
1313+ usePollData,
1414+} from "components/PageSWRDataProvider";
1215import { voteOnPoll } from "actions/pollActions";
1316import { create } from "zustand";
1417import { elementId } from "src/utils/elementId";
1518import { CheckTiny } from "components/Icons/CheckTiny";
1619import { CloseTiny } from "components/Icons/CloseTiny";
2020+import { PublicationPollBlock } from "./PublicationPollBlock";
17211822export let usePollBlockUIState = create(
1923 () =>
···2125 [entity: string]: { state: "editing" | "voting" | "results" } | undefined;
2226 },
2327);
2828+2429export const PollBlock = (props: BlockProps) => {
3030+ let { data: pub } = useLeafletPublicationData();
3131+ if (!pub) return <LeafletPollBlock {...props} />;
3232+ return <PublicationPollBlock {...props} />;
3333+};
3434+3535+export const LeafletPollBlock = (props: BlockProps) => {
2536 let isSelected = useUIState((s) =>
2637 s.selectedBlocks.find((b) => b.value === props.entityID),
2738 );
+187
components/Blocks/PublicationPollBlock.tsx
···11+import { useUIState } from "src/useUIState";
22+import { BlockProps } from "./Block";
33+import { useMemo } from "react";
44+import { focusElement, AsyncValueInput } from "components/Input";
55+import { useEntitySetContext } from "components/EntitySetProvider";
66+import { useEntity, useReplicache } from "src/replicache";
77+import { v7 } from "uuid";
88+import { elementId } from "src/utils/elementId";
99+import { CloseTiny } from "components/Icons/CloseTiny";
1010+import { useLeafletPublicationData } from "components/PageSWRDataProvider";
1111+import {
1212+ PubLeafletBlocksPoll,
1313+ PubLeafletDocument,
1414+ PubLeafletPagesLinearDocument,
1515+} from "lexicons/api";
1616+import { ids } from "lexicons/api/lexicons";
1717+1818+/**
1919+ * PublicationPollBlock is used for editing polls in publication documents.
2020+ * It allows adding/editing options when the poll hasn't been published yet,
2121+ * but disables adding new options once the poll record exists (indicated by pollUri).
2222+ */
2323+export const PublicationPollBlock = (props: BlockProps) => {
2424+ let { data: publicationData } = useLeafletPublicationData();
2525+ let isSelected = useUIState((s) =>
2626+ s.selectedBlocks.find((b) => b.value === props.entityID),
2727+ );
2828+ // Check if this poll has been published in a publication document
2929+ const isPublished = useMemo(() => {
3030+ if (!publicationData?.documents?.data) return false;
3131+3232+ const docRecord = publicationData.documents
3333+ .data as PubLeafletDocument.Record;
3434+ console.log(docRecord);
3535+3636+ // Search through all pages and blocks to find if this poll entity has been published
3737+ for (const page of docRecord.pages || []) {
3838+ if (page.$type === "pub.leaflet.pages.linearDocument") {
3939+ const linearPage = page as PubLeafletPagesLinearDocument.Main;
4040+ for (const blockWrapper of linearPage.blocks || []) {
4141+ if (blockWrapper.block?.$type === ids.PubLeafletBlocksPoll) {
4242+ const pollBlock = blockWrapper.block as PubLeafletBlocksPoll.Main;
4343+ console.log(pollBlock);
4444+ // Check if this poll's rkey matches our entity ID
4545+ const rkey = pollBlock.pollRef.uri.split("/").pop();
4646+ if (rkey === props.entityID) {
4747+ return true;
4848+ }
4949+ }
5050+ }
5151+ }
5252+ }
5353+ return false;
5454+ }, [publicationData, props.entityID]);
5555+5656+ return (
5757+ <div
5858+ className={`poll flex flex-col gap-2 p-3 w-full
5959+ ${isSelected ? "block-border-selected " : "block-border"}`}
6060+ style={{
6161+ backgroundColor:
6262+ "color-mix(in oklab, rgb(var(--accent-1)), rgb(var(--bg-page)) 85%)",
6363+ }}
6464+ >
6565+ <EditPollForPublication
6666+ entityID={props.entityID}
6767+ isPublished={isPublished}
6868+ />
6969+ </div>
7070+ );
7171+};
7272+7373+const EditPollForPublication = (props: {
7474+ entityID: string;
7575+ isPublished: boolean;
7676+}) => {
7777+ let pollOptions = useEntity(props.entityID, "poll/options");
7878+ let { rep } = useReplicache();
7979+ let permission_set = useEntitySetContext();
8080+8181+ return (
8282+ <>
8383+ {props.isPublished && (
8484+ <div className="text-sm italic text-tertiary">
8585+ This poll has been published. You can't edit the options.
8686+ </div>
8787+ )}
8888+8989+ {pollOptions.length === 0 && !props.isPublished && (
9090+ <div className="text-center italic text-tertiary text-sm">
9191+ no options yet...
9292+ </div>
9393+ )}
9494+9595+ {pollOptions.map((p) => (
9696+ <EditPollOptionForPublication
9797+ key={p.id}
9898+ entityID={p.data.value}
9999+ pollEntity={props.entityID}
100100+ disabled={props.isPublished}
101101+ canDelete={!props.isPublished}
102102+ />
103103+ ))}
104104+105105+ {!props.isPublished && permission_set.permissions.write && (
106106+ <button
107107+ className="pollAddOption w-fit flex gap-2 items-center justify-start text-sm text-accent-contrast"
108108+ onClick={async () => {
109109+ let pollOptionEntity = v7();
110110+ await rep?.mutate.addPollOption({
111111+ pollEntity: props.entityID,
112112+ pollOptionEntity,
113113+ pollOptionName: "",
114114+ permission_set: permission_set.set,
115115+ factID: v7(),
116116+ });
117117+118118+ focusElement(
119119+ document.getElementById(
120120+ elementId.block(props.entityID).pollInput(pollOptionEntity),
121121+ ) as HTMLInputElement | null,
122122+ );
123123+ }}
124124+ >
125125+ Add an Option
126126+ </button>
127127+ )}
128128+ </>
129129+ );
130130+};
131131+132132+const EditPollOptionForPublication = (props: {
133133+ entityID: string;
134134+ pollEntity: string;
135135+ disabled: boolean;
136136+ canDelete: boolean;
137137+}) => {
138138+ let { rep } = useReplicache();
139139+ let { permissions } = useEntitySetContext();
140140+ let optionName = useEntity(props.entityID, "poll-option/name")?.data.value;
141141+142142+ return (
143143+ <div className="flex gap-2 items-center">
144144+ <AsyncValueInput
145145+ id={elementId.block(props.pollEntity).pollInput(props.entityID)}
146146+ type="text"
147147+ className="pollOptionInput w-full input-with-border"
148148+ placeholder="Option here..."
149149+ disabled={props.disabled || !permissions.write}
150150+ value={optionName || ""}
151151+ onChange={async (e) => {
152152+ await rep?.mutate.assertFact([
153153+ {
154154+ entity: props.entityID,
155155+ attribute: "poll-option/name",
156156+ data: { type: "string", value: e.currentTarget.value },
157157+ },
158158+ ]);
159159+ }}
160160+ onKeyDown={(e) => {
161161+ if (
162162+ props.canDelete &&
163163+ e.key === "Backspace" &&
164164+ !e.currentTarget.value
165165+ ) {
166166+ e.preventDefault();
167167+ rep?.mutate.removePollOption({ optionEntity: props.entityID });
168168+ }
169169+ }}
170170+ />
171171+172172+ {permissions.write && props.canDelete && (
173173+ <button
174174+ tabIndex={-1}
175175+ className="text-accent-contrast"
176176+ onMouseDown={async () => {
177177+ await rep?.mutate.removePollOption({
178178+ optionEntity: props.entityID,
179179+ });
180180+ }}
181181+ >
182182+ <CloseTiny />
183183+ </button>
184184+ )}
185185+ </div>
186186+ );
187187+};
-59
components/Blocks/QuoteEmbedBlock.tsx
···11-import { GoToArrow } from "components/Icons/GoToArrow";
22-import { ExternalLinkBlock } from "./ExternalLinkBlock";
33-import { Separator } from "components/Layout";
44-55-export const QuoteEmbedBlockLine = () => {
66- return (
77- <div className="quoteEmbedBlock flex sm:mx-4 mx-3 my-3 sm:my-4 text-secondary text-sm italic">
88- <div className="w-2 h-full bg-border" />
99- <div className="flex flex-col pl-4">
1010- <div className="quoteEmbedContent ">
1111- Hello, this is a long quote that I am writing to you! I am so excited
1212- that you decided to quote my stuff. I would love to take a moments and
1313- just say whatever the heck i feel like. Unforunately for you, it is a
1414- rather boring todo list. I need to add an author and pub name, i need
1515- to add a back link, and i need to link about text formatting, if we
1616- want to handle it.
1717- </div>
1818- <div className="quoteEmbedFooter flex gap-2 pt-2 ">
1919- <div className="flex flex-col leading-tight grow">
2020- <div className="font-bold ">This was made to be quoted</div>
2121- <div className="text-tertiary text-xs">celine</div>
2222- </div>
2323- </div>
2424- </div>
2525- </div>
2626- );
2727-};
2828-2929-export const QuoteEmbedBlock = () => {
3030- return (
3131- <div className="quoteEmbedBlock transparent-container sm:mx-4 mx-3 my-3 sm:my-4 text-secondary text-sm">
3232- <div className="quoteEmbedContent p-3">
3333- Hello, this is a long quote that I am writing to you! I am so excited
3434- that you decided to quote my stuff. I would love to take a moments and
3535- just say whatever the heck i feel like. Unforunately for you, it is a
3636- rather boring todo list. I need to add an author and pub name, i need to
3737- add a back link, and i need to link about text formatting, if we want to
3838- handle it.
3939- </div>
4040- <hr className="border-border-light" />
4141- <a
4242- className="quoteEmbedFooter flex max-w-full gap-2 px-3 py-2 hover:no-underline! text-secondary"
4343- href="#"
4444- >
4545- <div className="flex flex-col w-[calc(100%-28px)] grow">
4646- <div className="font-bold w-full truncate">
4747- This was made to be quoted and if it's very long, to truncate
4848- </div>
4949- <div className="flex gap-[6px] text-tertiary text-xs items-center">
5050- <div className="underline">lab.leaflet.pub</div>
5151- <Separator classname="h-2" />
5252- <div>celine</div>
5353- </div>
5454- </div>
5555- <div className=" shrink-0 pt-px bg-test w-5 h-5 rounded-full"></div>
5656- </a>
5757- </div>
5858- );
5959-};
···22import { PubLeafletPublication } from "lexicons/api";
33import { useEntity, useReplicache } from "src/replicache";
4455-export function useCardBorderHidden(entityID: string) {
55+export function useCardBorderHidden(entityID: string | null) {
66 let { rootEntity } = useReplicache();
77 let { data: pub } = useLeafletPublicationData();
88 let rootCardBorderHidden = useEntity(rootEntity, "theme/card-border-hidden");
+1-1
components/PostLink.tsx
···99import { useSmoker } from "components/Toast";
1010import { PubLeafletDocument, PubLeafletPublication } from "lexicons/api";
1111import { blobRefToSrc } from "src/utils/blobRefToSrc";
1212-import type { Post } from "app/reader/getReaderFeed";
1212+import type { Post } from "app/(home-pages)/reader/getReaderFeed";
13131414import Link from "next/link";
1515import { InteractionPreview } from "./InteractionsPreview";
···2121import type * as PubLeafletBlocksHorizontalRule from '../blocks/horizontalRule'
2222import type * as PubLeafletBlocksBskyPost from '../blocks/bskyPost'
2323import type * as PubLeafletBlocksPage from '../blocks/page'
2424+import type * as PubLeafletBlocksPoll from '../blocks/poll'
24252526const is$typed = _is$typed,
2627 validate = _validate
···5758 | $Typed<PubLeafletBlocksHorizontalRule.Main>
5859 | $Typed<PubLeafletBlocksBskyPost.Main>
5960 | $Typed<PubLeafletBlocksPage.Main>
6161+ | $Typed<PubLeafletBlocksPoll.Main>
6062 | { $type: string }
6163 alignment?:
6264 | 'lex:pub.leaflet.pages.linearDocument#textAlignLeft'
+48
lexicons/api/types/pub/leaflet/poll/definition.ts
···11+/**
22+ * GENERATED CODE - DO NOT MODIFY
33+ */
44+import { type ValidationResult, BlobRef } from '@atproto/lexicon'
55+import { CID } from 'multiformats/cid'
66+import { validate as _validate } from '../../../../lexicons'
77+import {
88+ type $Typed,
99+ is$typed as _is$typed,
1010+ type OmitKey,
1111+} from '../../../../util'
1212+1313+const is$typed = _is$typed,
1414+ validate = _validate
1515+const id = 'pub.leaflet.poll.definition'
1616+1717+export interface Record {
1818+ $type: 'pub.leaflet.poll.definition'
1919+ name: string
2020+ options: Option[]
2121+ endDate?: string
2222+ [k: string]: unknown
2323+}
2424+2525+const hashRecord = 'main'
2626+2727+export function isRecord<V>(v: V) {
2828+ return is$typed(v, id, hashRecord)
2929+}
3030+3131+export function validateRecord<V>(v: V) {
3232+ return validate<Record & V>(v, id, hashRecord, true)
3333+}
3434+3535+export interface Option {
3636+ $type?: 'pub.leaflet.poll.definition#option'
3737+ text?: string
3838+}
3939+4040+const hashOption = 'option'
4141+4242+export function isOption<V>(v: V) {
4343+ return is$typed(v, id, hashOption)
4444+}
4545+4646+export function validateOption<V>(v: V) {
4747+ return validate<Option & V>(v, id, hashOption)
4848+}
+33
lexicons/api/types/pub/leaflet/poll/vote.ts
···11+/**
22+ * GENERATED CODE - DO NOT MODIFY
33+ */
44+import { type ValidationResult, BlobRef } from '@atproto/lexicon'
55+import { CID } from 'multiformats/cid'
66+import { validate as _validate } from '../../../../lexicons'
77+import {
88+ type $Typed,
99+ is$typed as _is$typed,
1010+ type OmitKey,
1111+} from '../../../../util'
1212+import type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef'
1313+1414+const is$typed = _is$typed,
1515+ validate = _validate
1616+const id = 'pub.leaflet.poll.vote'
1717+1818+export interface Record {
1919+ $type: 'pub.leaflet.poll.vote'
2020+ poll: ComAtprotoRepoStrongRef.Main
2121+ option: string[]
2222+ [k: string]: unknown
2323+}
2424+2525+const hashRecord = 'main'
2626+2727+export function isRecord<V>(v: V) {
2828+ return is$typed(v, id, hashRecord)
2929+}
3030+3131+export function validateRecord<V>(v: V) {
3232+ return validate<Record & V>(v, id, hashRecord, true)
3333+}
+3
lexicons/build.ts
···22import { BlockLexicons } from "./src/blocks";
33import { PubLeafletDocument } from "./src/document";
44import * as PublicationLexicons from "./src/publication";
55+import * as PollLexicons from "./src/polls";
56import { ThemeLexicons } from "./src/theme";
6778import * as fs from "fs";
···2122 PubLeafletComment,
2223 PubLeafletRichTextFacet,
2324 PageLexicons.PubLeafletPagesLinearDocument,
2525+ PageLexicons.PubLeafletPagesCanvasDocument,
2426 ...ThemeLexicons,
2527 ...BlockLexicons,
2628 ...Object.values(PublicationLexicons),
2929+ ...Object.values(PollLexicons),
2730];
28312932// Write each lexicon to a file
···11-// Generated with claude code, sonnet 4.5
22-/**
33- * Scrolls an element into view within a scrolling container using Intersection Observer
44- * and the scrollTo API, instead of the native scrollIntoView.
55- *
66- * @param elementId - The ID of the element to scroll into view
77- * @param scrollContainerId - The ID of the scrolling container (defaults to "pages")
88- * @param threshold - Intersection observer threshold (0-1, defaults to 0.2 for 20%)
99- */
11+import { scrollIntoViewIfNeeded } from "./scrollIntoViewIfNeeded";
22+103export function scrollIntoView(
114 elementId: string,
125 scrollContainerId: string = "pages",
136 threshold: number = 0.9,
147) {
158 const element = document.getElementById(elementId);
1616- const scrollContainer = document.getElementById(scrollContainerId);
1717-1818- if (!element || !scrollContainer) {
1919- console.warn(`scrollIntoView: element or container not found`, {
2020- elementId,
2121- scrollContainerId,
2222- element,
2323- scrollContainer,
2424- });
2525- return;
2626- }
2727-2828- // Create an intersection observer to check if element is visible
2929- const observer = new IntersectionObserver(
3030- (entries) => {
3131- const entry = entries[0];
3232-3333- // If element is not sufficiently visible, scroll to it
3434- if (!entry.isIntersecting || entry.intersectionRatio < threshold) {
3535- const elementRect = element.getBoundingClientRect();
3636- const containerRect = scrollContainer.getBoundingClientRect();
3737-3838- // Calculate the target scroll position
3939- // We want to center the element horizontally in the container
4040- const targetScrollLeft =
4141- scrollContainer.scrollLeft +
4242- elementRect.left -
4343- containerRect.left -
4444- (containerRect.width - elementRect.width) / 2;
4545-4646- scrollContainer.scrollTo({
4747- left: targetScrollLeft,
4848- behavior: "smooth",
4949- });
5050- }
5151-5252- // Disconnect after checking once
5353- observer.disconnect();
5454- },
5555- {
5656- root: scrollContainer,
5757- threshold: threshold,
5858- },
5959- );
6060-6161- observer.observe(element);
99+ scrollIntoViewIfNeeded(element, false, "smooth");
6210}
···11+create table "public"."atp_poll_votes" (
22+ "uri" text not null,
33+ "record" jsonb not null,
44+ "voter_did" text not null,
55+ "poll_uri" text not null,
66+ "poll_cid" text not null,
77+ "option" text not null,
88+ "indexed_at" timestamp with time zone not null default now()
99+);
1010+1111+alter table "public"."atp_poll_votes" enable row level security;
1212+1313+CREATE UNIQUE INDEX atp_poll_votes_pkey ON public.atp_poll_votes USING btree (uri);
1414+1515+alter table "public"."atp_poll_votes" add constraint "atp_poll_votes_pkey" PRIMARY KEY using index "atp_poll_votes_pkey";
1616+1717+CREATE INDEX atp_poll_votes_poll_uri_idx ON public.atp_poll_votes USING btree (poll_uri);
1818+1919+CREATE INDEX atp_poll_votes_voter_did_idx ON public.atp_poll_votes USING btree (voter_did);
2020+2121+grant delete on table "public"."atp_poll_votes" to "anon";
2222+2323+grant insert on table "public"."atp_poll_votes" to "anon";
2424+2525+grant references on table "public"."atp_poll_votes" to "anon";
2626+2727+grant select on table "public"."atp_poll_votes" to "anon";
2828+2929+grant trigger on table "public"."atp_poll_votes" to "anon";
3030+3131+grant truncate on table "public"."atp_poll_votes" to "anon";
3232+3333+grant update on table "public"."atp_poll_votes" to "anon";
3434+3535+grant delete on table "public"."atp_poll_votes" to "authenticated";
3636+3737+grant insert on table "public"."atp_poll_votes" to "authenticated";
3838+3939+grant references on table "public"."atp_poll_votes" to "authenticated";
4040+4141+grant select on table "public"."atp_poll_votes" to "authenticated";
4242+4343+grant trigger on table "public"."atp_poll_votes" to "authenticated";
4444+4545+grant truncate on table "public"."atp_poll_votes" to "authenticated";
4646+4747+grant update on table "public"."atp_poll_votes" to "authenticated";
4848+4949+grant delete on table "public"."atp_poll_votes" to "service_role";
5050+5151+grant insert on table "public"."atp_poll_votes" to "service_role";
5252+5353+grant references on table "public"."atp_poll_votes" to "service_role";
5454+5555+grant select on table "public"."atp_poll_votes" to "service_role";
5656+5757+grant trigger on table "public"."atp_poll_votes" to "service_role";
5858+5959+grant truncate on table "public"."atp_poll_votes" to "service_role";
6060+6161+grant update on table "public"."atp_poll_votes" to "service_role";
6262+6363+create table "public"."atp_poll_records" (
6464+ "uri" text not null,
6565+ "cid" text not null,
6666+ "record" jsonb not null,
6767+ "created_at" timestamp with time zone not null default now()
6868+);
6969+7070+7171+alter table "public"."atp_poll_records" enable row level security;
7272+7373+alter table "public"."bsky_follows" alter column "identity" set default ''::text;
7474+7575+CREATE UNIQUE INDEX atp_poll_records_pkey ON public.atp_poll_records USING btree (uri);
7676+7777+alter table "public"."atp_poll_records" add constraint "atp_poll_records_pkey" PRIMARY KEY using index "atp_poll_records_pkey";
7878+7979+alter table "public"."atp_poll_votes" add constraint "atp_poll_votes_poll_uri_fkey" FOREIGN KEY (poll_uri) REFERENCES atp_poll_records(uri) ON UPDATE CASCADE ON DELETE CASCADE not valid;
8080+8181+alter table "public"."atp_poll_votes" validate constraint "atp_poll_votes_poll_uri_fkey";
8282+8383+grant delete on table "public"."atp_poll_records" to "anon";
8484+8585+grant insert on table "public"."atp_poll_records" to "anon";
8686+8787+grant references on table "public"."atp_poll_records" to "anon";
8888+8989+grant select on table "public"."atp_poll_records" to "anon";
9090+9191+grant trigger on table "public"."atp_poll_records" to "anon";
9292+9393+grant truncate on table "public"."atp_poll_records" to "anon";
9494+9595+grant update on table "public"."atp_poll_records" to "anon";
9696+9797+grant delete on table "public"."atp_poll_records" to "authenticated";
9898+9999+grant insert on table "public"."atp_poll_records" to "authenticated";
100100+101101+grant references on table "public"."atp_poll_records" to "authenticated";
102102+103103+grant select on table "public"."atp_poll_records" to "authenticated";
104104+105105+grant trigger on table "public"."atp_poll_records" to "authenticated";
106106+107107+grant truncate on table "public"."atp_poll_records" to "authenticated";
108108+109109+grant update on table "public"."atp_poll_records" to "authenticated";
110110+111111+grant delete on table "public"."atp_poll_records" to "service_role";
112112+113113+grant insert on table "public"."atp_poll_records" to "service_role";
114114+115115+grant references on table "public"."atp_poll_records" to "service_role";
116116+117117+grant select on table "public"."atp_poll_records" to "service_role";
118118+119119+grant trigger on table "public"."atp_poll_records" to "service_role";
120120+121121+grant truncate on table "public"."atp_poll_records" to "service_role";
122122+123123+grant update on table "public"."atp_poll_records" to "service_role";