diff --git a/actions/publishToPublication.ts b/actions/publishToPublication.ts index 33fb9fff..efc755d0 100644 --- a/actions/publishToPublication.ts +++ b/actions/publishToPublication.ts @@ -21,6 +21,8 @@ import { PubLeafletBlocksBlockquote, PubLeafletBlocksIframe, PubLeafletBlocksPage, + PubLeafletBlocksPoll, + PubLeafletPollDefinition, } from "lexicons/api"; import { Block } from "components/Blocks/Block"; import { TID } from "@atproto/common"; @@ -78,6 +80,7 @@ export async function publishToPublication({ facts, agent, root_entity, + credentialSession.did!, ); let existingRecord = @@ -137,6 +140,7 @@ async function processBlocksToPages( facts: Fact[], agent: AtpBaseClient, root_entity: string, + did: string, ) { let scan = scanIndexLocal(facts); let pages: { id: string; blocks: PubLeafletPagesLinearDocument.Block[] }[] = @@ -145,7 +149,7 @@ async function processBlocksToPages( let firstEntity = scan.eav(root_entity, "root/page")?.[0]; if (!firstEntity) throw new Error("No root page"); let blocks = getBlocksWithTypeLocal(facts, firstEntity?.data.value); - let b = await blocksToRecord(blocks); + let b = await blocksToRecord(blocks, did); return { firstPageBlocks: b, pages }; async function uploadImage(src: string) { @@ -159,6 +163,7 @@ async function processBlocksToPages( } async function blocksToRecord( blocks: Block[], + did: string, ): Promise { let parsedBlocks = parseBlocksToList(blocks); return ( @@ -174,7 +179,7 @@ async function processBlocksToPages( : alignmentValue === "right" ? "lex:pub.leaflet.pages.linearDocument#textAlignRight" : undefined; - let b = await blockToRecord(blockOrList.block); + let b = await blockToRecord(blockOrList.block, did); if (!b) return []; let block: PubLeafletPagesLinearDocument.Block = { $type: "pub.leaflet.pages.linearDocument#block", @@ -187,7 +192,7 @@ async function processBlocksToPages( $type: "pub.leaflet.pages.linearDocument#block", block: { $type: "pub.leaflet.blocks.unorderedList", - children: await childrenToRecord(blockOrList.children), + children: await childrenToRecord(blockOrList.children, did), }, }; return [block]; @@ -197,23 +202,23 @@ async function processBlocksToPages( ).flat(); } - async function childrenToRecord(children: List[]) { + async function childrenToRecord(children: List[], did: string) { return ( await Promise.all( children.map(async (child) => { - let content = await blockToRecord(child.block); + let content = await blockToRecord(child.block, did); if (!content) return []; let record: PubLeafletBlocksUnorderedList.ListItem = { $type: "pub.leaflet.blocks.unorderedList#listItem", content, - children: await childrenToRecord(child.children), + children: await childrenToRecord(child.children, did), }; return record; }), ) ).flat(); } - async function blockToRecord(b: Block) { + async function blockToRecord(b: Block, did: string) { const getBlockContent = (b: string) => { let [content] = scan.eav(b, "block/text"); if (!content) return ["", [] as PubLeafletRichtextFacet.Main[]] as const; @@ -231,7 +236,7 @@ async function processBlocksToPages( let blocks = getBlocksWithTypeLocal(facts, page.data.value); pages.push({ id: page.data.value, - blocks: await blocksToRecord(blocks), + blocks: await blocksToRecord(blocks, did), }); let block: $Typed = { $type: "pub.leaflet.blocks.page", @@ -357,6 +362,55 @@ async function processBlocksToPages( }; return block; } + if (b.type === "poll") { + // Get poll options from the entity + let pollOptions = scan.eav(b.value, "poll/options"); + let options: PubLeafletPollDefinition.Option[] = pollOptions.map( + (opt) => { + let optionName = scan.eav(opt.data.value, "poll-option/name")?.[0]; + return { + $type: "pub.leaflet.poll.definition#option", + text: optionName?.data.value || "", + }; + }, + ); + + // Create the poll definition record + let pollRecord: PubLeafletPollDefinition.Record = { + $type: "pub.leaflet.poll.definition", + name: "Poll", // Default name, can be customized + options, + }; + + // Upload the poll record + let { data: pollResult } = await agent.com.atproto.repo.putRecord({ + //use the entity id as the rkey so we can associate it in the editor + rkey: b.value, + repo: did, + collection: pollRecord.$type, + record: pollRecord, + validate: false, + }); + + // Optimistically write poll definition to database + console.log( + await supabaseServerClient.from("atp_poll_records").upsert({ + uri: pollResult.uri, + cid: pollResult.cid, + record: pollRecord as Json, + }), + ); + + // Return a poll block with reference to the poll record + let block: $Typed = { + $type: "pub.leaflet.blocks.poll", + pollRef: { + uri: pollResult.uri, + cid: pollResult.cid, + }, + }; + return block; + } return; } } diff --git a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx index a19a5458..308381d4 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx @@ -14,6 +14,7 @@ import { PubLeafletBlocksBskyPost, PubLeafletBlocksIframe, PubLeafletBlocksPage, + PubLeafletBlocksPoll, } from "lexicons/api"; import { blobRefToSrc } from "src/utils/blobRefToSrc"; @@ -28,6 +29,8 @@ import { PubBlueskyPostBlock } from "./PublishBskyPostBlock"; import { openPage } from "./PostPages"; import { PageLinkBlock } from "components/Blocks/PageLinkBlock"; import { PublishedPageLinkBlock } from "./PublishedPageBlock"; +import { PublishedPollBlock } from "./PublishedPollBlock"; +import { PollData } from "./fetchPollData"; export function PostContent({ blocks, @@ -38,6 +41,7 @@ export function PostContent({ bskyPostData, pageId, pages, + pollData, }: { blocks: PubLeafletPagesLinearDocument.Block[]; pageId?: string; @@ -47,6 +51,7 @@ export function PostContent({ prerenderedCodeBlocks?: Map; bskyPostData: AppBskyFeedDefs.PostView[]; pages: PubLeafletPagesLinearDocument.Main[]; + pollData: PollData[]; }) { return (
); })} @@ -84,6 +90,7 @@ let Block = ({ bskyPostData, pageId, pages, + pollData, }: { pageId?: string; preview?: boolean; @@ -95,6 +102,7 @@ let Block = ({ previousBlock?: PubLeafletPagesLinearDocument.Block; prerenderedCodeBlocks?: Map; bskyPostData: AppBskyFeedDefs.PostView[]; + pollData: PollData[]; }) => { let b = block; let blockProps = { @@ -168,11 +176,24 @@ let Block = ({ case PubLeafletBlocksHorizontalRule.isMain(b.block): { return
; } + case PubLeafletBlocksPoll.isMain(b.block): { + let { cid, uri } = b.block.pollRef; + const pollVoteData = pollData.find((p) => p.uri === uri && p.cid === cid); + if (!pollVoteData) return null; + return ( + + ); + } case PubLeafletBlocksUnorderedList.isMain(b.block): { return (
    {b.block.children.map((child, i) => ( (
    ({ pages: [] as string[], @@ -113,6 +114,7 @@ export function PostPages({ prerenderedCodeBlocks, bskyPostData, document_uri, + pollData, }: { document_uri: string; document: PostPageData; @@ -123,6 +125,7 @@ export function PostPages({ prerenderedCodeBlocks?: Map; bskyPostData: AppBskyFeedDefs.PostView[]; preferences: { showComments?: boolean }; + pollData: PollData[]; }) { let { identity } = useIdentityData(); let drawer = useDrawerOpen(document_uri); @@ -155,6 +158,7 @@ export function PostPages({ blocks={blocks} did={did} prerenderedCodeBlocks={prerenderedCodeBlocks} + pollData={pollData} /> { + const { identity } = useIdentityData(); + const [selectedOption, setSelectedOption] = useState(null); + const [isVoting, setIsVoting] = useState(false); + const [showResults, setShowResults] = useState(false); + let pollRecord = props.pollData.record as PubLeafletPollDefinition.Record; + let [isClient, setIsClient] = useState(false); + useEffect(() => { + setIsClient(true); + }, []); + + const handleVote = async () => { + if (!selectedOption) return; + + setIsVoting(true); + try { + const result = await voteOnPublishedPoll( + props.block.pollRef.uri, + props.block.pollRef.cid, + selectedOption, + ); + + if (result.success) { + setShowResults(true); + } else { + console.error("Failed to vote:", result.error); + } + } catch (error) { + console.error("Failed to vote:", error); + } finally { + setIsVoting(false); + } + }; + + const hasVoted = + !!identity?.atp_did && + !!props.pollData?.atp_poll_votes.find( + (v) => v.voter_did === identity?.atp_did, + ); + const displayResults = showResults || hasVoted; + + return ( +
    + {displayResults ? ( + + ) : ( + <> + {pollRecord.options.map((option, index) => ( + setSelectedOption(index.toString())} + /> + ))} +
    +
    + {identity?.atp_did && ( + + )} +
    + {identity?.atp_did ? ( + + {isVoting ? "Voting..." : "Vote!"} + + ) : ( + + Login to vote + + } + > + {isClient && ( + + )} + + )} +
    + + )} +
    + ); +}; + +const PollOptionButton = (props: { + option: PubLeafletPollDefinition.Option; + optionIndex: string; + selected: boolean; + onSelect: () => void; +}) => { + const ButtonComponent = props.selected ? ButtonPrimary : ButtonSecondary; + + return ( +
    + + {props.option.text} + +
    + ); +}; + +const PollResults = (props: { + pollData: PollData; + hasVoted: boolean; + setShowResults: (show: boolean) => void; +}) => { + const totalVotes = props.pollData.atp_poll_votes.length || 0; + let pollRecord = props.pollData.record as PubLeafletPollDefinition.Record; + let optionsWithCount = pollRecord.options.map((o, index) => ({ + ...o, + votes: props.pollData.atp_poll_votes.filter( + (v) => v.option == index.toString(), + ), + })); + + const highestVotes = Math.max(...optionsWithCount.map((o) => o.votes.length)); + return ( + <> + {pollRecord.options.map((option, index) => { + const votes = props.pollData?.atp_poll_votes.filter( + (v) => v.option === index.toString(), + ).length; + const isWinner = totalVotes > 0 && votes === highestVotes; + + return ( + + ); + })} + + ); +}; + +const PollResult = (props: { + option: PubLeafletPollDefinition.Option; + votes: number; + totalVotes: number; + winner: boolean; +}) => { + return ( +
    +
    +
    {props.option.text}
    +
    {props.votes}
    +
    +
    +
    +
    +
    +
    + ); +}; diff --git a/app/lish/[did]/[publication]/[rkey]/fetchPollData.ts b/app/lish/[did]/[publication]/[rkey]/fetchPollData.ts new file mode 100644 index 00000000..3d59cb1b --- /dev/null +++ b/app/lish/[did]/[publication]/[rkey]/fetchPollData.ts @@ -0,0 +1,25 @@ +"use server"; + +import { getIdentityData } from "actions/getIdentityData"; +import { Json } from "supabase/database.types"; +import { supabaseServerClient } from "supabase/serverClient"; + +export type PollData = { + uri: string; + cid: string; + record: Json; + atp_poll_votes: { option: string; voter_did: string }[]; +}; + +export async function fetchPollData(pollUris: string[]): Promise { + // Get current user's identity to check if they've voted + const identity = await getIdentityData(); + const userDid = identity?.atp_did; + + const { data } = await supabaseServerClient + .from("atp_poll_records") + .select(`*, atp_poll_votes(*)`) + .in("uri", pollUris); + + return data || []; +} diff --git a/app/lish/[did]/[publication]/[rkey]/page.tsx b/app/lish/[did]/[publication]/[rkey]/page.tsx index 2b92129d..362feebd 100644 --- a/app/lish/[did]/[publication]/[rkey]/page.tsx +++ b/app/lish/[did]/[publication]/[rkey]/page.tsx @@ -20,6 +20,7 @@ import { PostPageContextProvider } from "./PostPageContext"; import { PostPages } from "./PostPages"; import { extractCodeBlocks } from "./extractCodeBlocks"; import { LeafletLayout } from "components/LeafletLayout"; +import { fetchPollData } from "./fetchPollData"; export async function generateMetadata(props: { params: Promise<{ publication: string; did: string; rkey: string }>; @@ -115,6 +116,16 @@ export default async function Post(props: { { headers: {} }, ) : { data: { posts: [] } }; + + // Extract poll blocks and fetch vote data + let pollBlocks = record.pages.flatMap((p) => { + let page = p as PubLeafletPagesLinearDocument.Main; + return page.blocks?.filter( + (b) => b.block.$type === ids.PubLeafletBlocksPoll, + ) || []; + }); + let pollData = await fetchPollData(pollBlocks.map(b => (b.block as any).pollRef.uri)); + let firstPage = record.pages[0]; let blocks: PubLeafletPagesLinearDocument.Block[] = []; if (PubLeafletPagesLinearDocument.isMain(firstPage)) { @@ -165,6 +176,7 @@ export default async function Post(props: { did={did} blocks={blocks} prerenderedCodeBlocks={prerenderedCodeBlocks} + pollData={pollData} /> diff --git a/app/lish/[did]/[publication]/[rkey]/voteOnPublishedPoll.ts b/app/lish/[did]/[publication]/[rkey]/voteOnPublishedPoll.ts new file mode 100644 index 00000000..6512cf07 --- /dev/null +++ b/app/lish/[did]/[publication]/[rkey]/voteOnPublishedPoll.ts @@ -0,0 +1,65 @@ +"use server"; + +import { createOauthClient } from "src/atproto-oauth"; +import { getIdentityData } from "actions/getIdentityData"; +import { AtpBaseClient, AtUri } from "@atproto/api"; +import { PubLeafletPollVote } from "lexicons/api"; +import { supabaseServerClient } from "supabase/serverClient"; +import { Json } from "supabase/database.types"; +import { TID } from "@atproto/common"; + +export async function voteOnPublishedPoll( + pollUri: string, + pollCid: string, + selectedOption: string, +): Promise<{ success: boolean; error?: string }> { + try { + const identity = await getIdentityData(); + + if (!identity?.atp_did) { + return { success: false, error: "Not authenticated" }; + } + + const oauthClient = await createOauthClient(); + const session = await oauthClient.restore(identity.atp_did); + let agent = new AtpBaseClient(session.fetchHandler.bind(session)); + + const voteRecord: PubLeafletPollVote.Record = { + $type: "pub.leaflet.poll.vote", + poll: { + uri: pollUri, + cid: pollCid, + }, + option: selectedOption, + }; + + const rkey = TID.nextStr(); + const voteUri = AtUri.make(identity.atp_did, "pub.leaflet.poll.vote", rkey); + + // Write to database optimistically before creating the record + await supabaseServerClient.from("atp_poll_votes").upsert({ + uri: voteUri.toString(), + voter_did: identity.atp_did, + poll_uri: pollUri, + poll_cid: pollCid, + option: selectedOption, + record: voteRecord as unknown as Json, + }); + + // Create the record on ATP + await agent.com.atproto.repo.createRecord({ + repo: identity.atp_did, + collection: "pub.leaflet.poll.vote", + rkey, + record: voteRecord, + }); + + return { success: true }; + } catch (error) { + console.error("Failed to vote:", error); + return { + success: false, + error: error instanceof Error ? error.message : "Failed to vote", + }; + } +} diff --git a/appview/index.ts b/appview/index.ts index a4926f30..efa79754 100644 --- a/appview/index.ts +++ b/appview/index.ts @@ -9,6 +9,8 @@ import { PubLeafletGraphSubscription, PubLeafletPublication, PubLeafletComment, + PubLeafletPollVote, + PubLeafletPollDefinition, } from "lexicons/api"; import { AppBskyEmbedExternal, @@ -44,6 +46,8 @@ async function main() { ids.PubLeafletPublication, ids.PubLeafletGraphSubscription, ids.PubLeafletComment, + ids.PubLeafletPollVote, + ids.PubLeafletPollDefinition, // ids.AppBskyActorProfile, "app.bsky.feed.post", ], @@ -169,6 +173,44 @@ async function handleEvent(evt: Event) { .eq("uri", evt.uri.toString()); } } + if (evt.collection === ids.PubLeafletPollVote) { + if (evt.event === "create" || evt.event === "update") { + let record = PubLeafletPollVote.validateRecord(evt.record); + if (!record.success) return; + let { error } = await supabase.from("atp_poll_votes").upsert({ + uri: evt.uri.toString(), + 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, + }); + } + if (evt.event === "delete") { + await supabase + .from("atp_poll_votes") + .delete() + .eq("uri", evt.uri.toString()); + } + } + if (evt.collection === ids.PubLeafletPollDefinition) { + if (evt.event === "create" || evt.event === "update") { + let record = PubLeafletPollDefinition.validateRecord(evt.record); + if (!record.success) return; + let { error } = await supabase.from("atp_poll_records").upsert({ + uri: evt.uri.toString(), + cid: evt.cid.toString(), + record: record.value as Json, + }); + if (error) console.log("Error upserting poll definition:", error); + } + if (evt.event === "delete") { + await supabase + .from("atp_poll_records") + .delete() + .eq("uri", evt.uri.toString()); + } + } if (evt.collection === ids.PubLeafletGraphSubscription) { if (evt.event === "create" || evt.event === "update") { let record = PubLeafletGraphSubscription.validateRecord(evt.record); diff --git a/components/Blocks/BlockCommands.tsx b/components/Blocks/BlockCommands.tsx index de82808a..8219dd3a 100644 --- a/components/Blocks/BlockCommands.tsx +++ b/components/Blocks/BlockCommands.tsx @@ -235,7 +235,6 @@ export const blockCommands: Command[] = [ name: "Poll", icon: , type: "block", - hiddenInPublication: true, onSelect: async (rep, props, um) => { let entity = await createBlockWithType(rep, props, "poll"); let pollOptionEntity = v7(); diff --git a/components/Blocks/PollBlock.tsx b/components/Blocks/PollBlock.tsx index 66051152..1a8ba4a8 100644 --- a/components/Blocks/PollBlock.tsx +++ b/components/Blocks/PollBlock.tsx @@ -8,12 +8,16 @@ import { useEntitySetContext } from "components/EntitySetProvider"; import { theme } from "tailwind.config"; import { useEntity, useReplicache } from "src/replicache"; import { v7 } from "uuid"; -import { usePollData } from "components/PageSWRDataProvider"; +import { + useLeafletPublicationData, + usePollData, +} from "components/PageSWRDataProvider"; import { voteOnPoll } from "actions/pollActions"; import { create } from "zustand"; import { elementId } from "src/utils/elementId"; import { CheckTiny } from "components/Icons/CheckTiny"; import { CloseTiny } from "components/Icons/CloseTiny"; +import { PublicationPollBlock } from "./PublicationPollBlock"; export let usePollBlockUIState = create( () => @@ -21,7 +25,14 @@ export let usePollBlockUIState = create( [entity: string]: { state: "editing" | "voting" | "results" } | undefined; }, ); + export const PollBlock = (props: BlockProps) => { + let { data: pub } = useLeafletPublicationData(); + if (!pub) return ; + return ; +}; + +export const LeafletPollBlock = (props: BlockProps) => { let isSelected = useUIState((s) => s.selectedBlocks.find((b) => b.value === props.entityID), ); diff --git a/components/Blocks/PublicationPollBlock.tsx b/components/Blocks/PublicationPollBlock.tsx new file mode 100644 index 00000000..891f6bdd --- /dev/null +++ b/components/Blocks/PublicationPollBlock.tsx @@ -0,0 +1,186 @@ +import { useUIState } from "src/useUIState"; +import { BlockProps } from "./Block"; +import { useMemo } from "react"; +import { focusElement, AsyncValueInput } from "components/Input"; +import { useEntitySetContext } from "components/EntitySetProvider"; +import { useEntity, useReplicache } from "src/replicache"; +import { v7 } from "uuid"; +import { elementId } from "src/utils/elementId"; +import { CloseTiny } from "components/Icons/CloseTiny"; +import { useLeafletPublicationData } from "components/PageSWRDataProvider"; +import { + PubLeafletBlocksPoll, + PubLeafletDocument, + PubLeafletPagesLinearDocument, +} from "lexicons/api"; +import { ids } from "lexicons/api/lexicons"; + +/** + * PublicationPollBlock is used for editing polls in publication documents. + * It allows adding/editing options when the poll hasn't been published yet, + * but disables adding new options once the poll record exists (indicated by pollUri). + */ +export const PublicationPollBlock = (props: BlockProps) => { + let { data: publicationData } = useLeafletPublicationData(); + let isSelected = useUIState((s) => + s.selectedBlocks.find((b) => b.value === props.entityID), + ); + // Check if this poll has been published in a publication document + const isPublished = useMemo(() => { + if (!publicationData?.documents?.data) return false; + + const docRecord = publicationData.documents + .data as PubLeafletDocument.Record; + console.log(docRecord); + + // Search through all pages and blocks to find if this poll entity has been published + for (const page of docRecord.pages || []) { + if (page.$type === "pub.leaflet.pages.linearDocument") { + const linearPage = page as PubLeafletPagesLinearDocument.Main; + for (const blockWrapper of linearPage.blocks || []) { + if (blockWrapper.block?.$type === ids.PubLeafletBlocksPoll) { + const pollBlock = blockWrapper.block as PubLeafletBlocksPoll.Main; + console.log(pollBlock); + // Check if this poll's rkey matches our entity ID + const rkey = pollBlock.pollRef.uri.split("/").pop(); + if (rkey === props.entityID) { + return true; + } + } + } + } + } + return false; + }, [publicationData, props.entityID]); + + return ( +
    + +
    + ); +}; + +const EditPollForPublication = (props: { + entityID: string; + isPublished: boolean; +}) => { + let pollOptions = useEntity(props.entityID, "poll/options"); + let { rep } = useReplicache(); + let permission_set = useEntitySetContext(); + + return ( + <> + {props.isPublished && ( +
    + This poll has been published. You can't edit the options. +
    + )} + + {pollOptions.length === 0 && !props.isPublished && ( +
    + no options yet... +
    + )} + + {pollOptions.map((p) => ( + + ))} + + {!props.isPublished && ( + + )} + + ); +}; + +const EditPollOptionForPublication = (props: { + entityID: string; + pollEntity: string; + disabled: boolean; + canDelete: boolean; +}) => { + let { rep } = useReplicache(); + let optionName = useEntity(props.entityID, "poll-option/name")?.data.value; + + return ( +
    + { + await rep?.mutate.assertFact([ + { + entity: props.entityID, + attribute: "poll-option/name", + data: { type: "string", value: e.currentTarget.value }, + }, + ]); + }} + onKeyDown={(e) => { + if ( + props.canDelete && + e.key === "Backspace" && + !e.currentTarget.value + ) { + e.preventDefault(); + rep?.mutate.removePollOption({ optionEntity: props.entityID }); + } + }} + /> + + {props.canDelete && ( + + )} +
    + ); +}; diff --git a/lexicons/api/index.ts b/lexicons/api/index.ts index 54f4b4c5..bccef5c9 100644 --- a/lexicons/api/index.ts +++ b/lexicons/api/index.ts @@ -32,6 +32,7 @@ import * as PubLeafletBlocksIframe from './types/pub/leaflet/blocks/iframe' import * as PubLeafletBlocksImage from './types/pub/leaflet/blocks/image' import * as PubLeafletBlocksMath from './types/pub/leaflet/blocks/math' import * as PubLeafletBlocksPage from './types/pub/leaflet/blocks/page' +import * as PubLeafletBlocksPoll from './types/pub/leaflet/blocks/poll' import * as PubLeafletBlocksText from './types/pub/leaflet/blocks/text' import * as PubLeafletBlocksUnorderedList from './types/pub/leaflet/blocks/unorderedList' import * as PubLeafletBlocksWebsite from './types/pub/leaflet/blocks/website' @@ -39,6 +40,8 @@ import * as PubLeafletComment from './types/pub/leaflet/comment' import * as PubLeafletDocument from './types/pub/leaflet/document' import * as PubLeafletGraphSubscription from './types/pub/leaflet/graph/subscription' import * as PubLeafletPagesLinearDocument from './types/pub/leaflet/pages/linearDocument' +import * as PubLeafletPollDefinition from './types/pub/leaflet/poll/definition' +import * as PubLeafletPollVote from './types/pub/leaflet/poll/vote' import * as PubLeafletPublication from './types/pub/leaflet/publication' import * as PubLeafletRichtextFacet from './types/pub/leaflet/richtext/facet' import * as PubLeafletThemeBackgroundImage from './types/pub/leaflet/theme/backgroundImage' @@ -67,6 +70,7 @@ export * as PubLeafletBlocksIframe from './types/pub/leaflet/blocks/iframe' export * as PubLeafletBlocksImage from './types/pub/leaflet/blocks/image' export * as PubLeafletBlocksMath from './types/pub/leaflet/blocks/math' export * as PubLeafletBlocksPage from './types/pub/leaflet/blocks/page' +export * as PubLeafletBlocksPoll from './types/pub/leaflet/blocks/poll' export * as PubLeafletBlocksText from './types/pub/leaflet/blocks/text' export * as PubLeafletBlocksUnorderedList from './types/pub/leaflet/blocks/unorderedList' export * as PubLeafletBlocksWebsite from './types/pub/leaflet/blocks/website' @@ -74,6 +78,8 @@ export * as PubLeafletComment from './types/pub/leaflet/comment' export * as PubLeafletDocument from './types/pub/leaflet/document' export * as PubLeafletGraphSubscription from './types/pub/leaflet/graph/subscription' export * as PubLeafletPagesLinearDocument from './types/pub/leaflet/pages/linearDocument' +export * as PubLeafletPollDefinition from './types/pub/leaflet/poll/definition' +export * as PubLeafletPollVote from './types/pub/leaflet/poll/vote' export * as PubLeafletPublication from './types/pub/leaflet/publication' export * as PubLeafletRichtextFacet from './types/pub/leaflet/richtext/facet' export * as PubLeafletThemeBackgroundImage from './types/pub/leaflet/theme/backgroundImage' @@ -380,6 +386,7 @@ export class PubLeafletNS { blocks: PubLeafletBlocksNS graph: PubLeafletGraphNS pages: PubLeafletPagesNS + poll: PubLeafletPollNS richtext: PubLeafletRichtextNS theme: PubLeafletThemeNS @@ -388,6 +395,7 @@ export class PubLeafletNS { this.blocks = new PubLeafletBlocksNS(client) this.graph = new PubLeafletGraphNS(client) this.pages = new PubLeafletPagesNS(client) + this.poll = new PubLeafletPollNS(client) this.richtext = new PubLeafletRichtextNS(client) this.theme = new PubLeafletThemeNS(client) this.comment = new PubLeafletCommentRecord(client) @@ -505,6 +513,180 @@ export class PubLeafletPagesNS { } } +export class PubLeafletPollNS { + _client: XrpcClient + definition: PubLeafletPollDefinitionRecord + vote: PubLeafletPollVoteRecord + + constructor(client: XrpcClient) { + this._client = client + this.definition = new PubLeafletPollDefinitionRecord(client) + this.vote = new PubLeafletPollVoteRecord(client) + } +} + +export class PubLeafletPollDefinitionRecord { + _client: XrpcClient + + constructor(client: XrpcClient) { + this._client = client + } + + async list( + params: OmitKey, + ): Promise<{ + cursor?: string + records: { uri: string; value: PubLeafletPollDefinition.Record }[] + }> { + const res = await this._client.call('com.atproto.repo.listRecords', { + collection: 'pub.leaflet.poll.definition', + ...params, + }) + return res.data + } + + async get( + params: OmitKey, + ): Promise<{ + uri: string + cid: string + value: PubLeafletPollDefinition.Record + }> { + const res = await this._client.call('com.atproto.repo.getRecord', { + collection: 'pub.leaflet.poll.definition', + ...params, + }) + return res.data + } + + async create( + params: OmitKey< + ComAtprotoRepoCreateRecord.InputSchema, + 'collection' | 'record' + >, + record: Un$Typed, + headers?: Record, + ): Promise<{ uri: string; cid: string }> { + const collection = 'pub.leaflet.poll.definition' + const res = await this._client.call( + 'com.atproto.repo.createRecord', + undefined, + { collection, ...params, record: { ...record, $type: collection } }, + { encoding: 'application/json', headers }, + ) + return res.data + } + + async put( + params: OmitKey< + ComAtprotoRepoPutRecord.InputSchema, + 'collection' | 'record' + >, + record: Un$Typed, + headers?: Record, + ): Promise<{ uri: string; cid: string }> { + const collection = 'pub.leaflet.poll.definition' + const res = await this._client.call( + 'com.atproto.repo.putRecord', + undefined, + { collection, ...params, record: { ...record, $type: collection } }, + { encoding: 'application/json', headers }, + ) + return res.data + } + + async delete( + params: OmitKey, + headers?: Record, + ): Promise { + await this._client.call( + 'com.atproto.repo.deleteRecord', + undefined, + { collection: 'pub.leaflet.poll.definition', ...params }, + { headers }, + ) + } +} + +export class PubLeafletPollVoteRecord { + _client: XrpcClient + + constructor(client: XrpcClient) { + this._client = client + } + + async list( + params: OmitKey, + ): Promise<{ + cursor?: string + records: { uri: string; value: PubLeafletPollVote.Record }[] + }> { + const res = await this._client.call('com.atproto.repo.listRecords', { + collection: 'pub.leaflet.poll.vote', + ...params, + }) + return res.data + } + + async get( + params: OmitKey, + ): Promise<{ uri: string; cid: string; value: PubLeafletPollVote.Record }> { + const res = await this._client.call('com.atproto.repo.getRecord', { + collection: 'pub.leaflet.poll.vote', + ...params, + }) + return res.data + } + + async create( + params: OmitKey< + ComAtprotoRepoCreateRecord.InputSchema, + 'collection' | 'record' + >, + record: Un$Typed, + headers?: Record, + ): Promise<{ uri: string; cid: string }> { + const collection = 'pub.leaflet.poll.vote' + const res = await this._client.call( + 'com.atproto.repo.createRecord', + undefined, + { collection, ...params, record: { ...record, $type: collection } }, + { encoding: 'application/json', headers }, + ) + return res.data + } + + async put( + params: OmitKey< + ComAtprotoRepoPutRecord.InputSchema, + 'collection' | 'record' + >, + record: Un$Typed, + headers?: Record, + ): Promise<{ uri: string; cid: string }> { + const collection = 'pub.leaflet.poll.vote' + const res = await this._client.call( + 'com.atproto.repo.putRecord', + undefined, + { collection, ...params, record: { ...record, $type: collection } }, + { encoding: 'application/json', headers }, + ) + return res.data + } + + async delete( + params: OmitKey, + headers?: Record, + ): Promise { + await this._client.call( + 'com.atproto.repo.deleteRecord', + undefined, + { collection: 'pub.leaflet.poll.vote', ...params }, + { headers }, + ) + } +} + export class PubLeafletRichtextNS { _client: XrpcClient diff --git a/lexicons/api/lexicons.ts b/lexicons/api/lexicons.ts index 56145091..3a699216 100644 --- a/lexicons/api/lexicons.ts +++ b/lexicons/api/lexicons.ts @@ -1200,6 +1200,22 @@ export const schemaDict = { }, }, }, + PubLeafletBlocksPoll: { + lexicon: 1, + id: 'pub.leaflet.blocks.poll', + defs: { + main: { + type: 'object', + required: ['pollRef'], + properties: { + pollRef: { + type: 'ref', + ref: 'lex:com.atproto.repo.strongRef', + }, + }, + }, + }, + }, PubLeafletBlocksText: { lexicon: 1, id: 'pub.leaflet.blocks.text', @@ -1473,6 +1489,7 @@ export const schemaDict = { 'lex:pub.leaflet.blocks.horizontalRule', 'lex:pub.leaflet.blocks.bskyPost', 'lex:pub.leaflet.blocks.page', + 'lex:pub.leaflet.blocks.poll', ], }, alignment: { @@ -1526,6 +1543,73 @@ export const schemaDict = { }, }, }, + PubLeafletPollDefinition: { + lexicon: 1, + id: 'pub.leaflet.poll.definition', + defs: { + main: { + type: 'record', + key: 'tid', + description: 'Record declaring a poll', + record: { + type: 'object', + required: ['name', 'options'], + properties: { + name: { + type: 'string', + maxLength: 500, + maxGraphemes: 100, + }, + options: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:pub.leaflet.poll.definition#option', + }, + }, + endDate: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + option: { + type: 'object', + properties: { + text: { + type: 'string', + maxLength: 500, + maxGraphemes: 50, + }, + }, + }, + }, + }, + PubLeafletPollVote: { + lexicon: 1, + id: 'pub.leaflet.poll.vote', + defs: { + main: { + type: 'record', + key: 'tid', + description: 'Record declaring a vote on a poll', + record: { + type: 'object', + required: ['poll', 'option'], + properties: { + poll: { + type: 'ref', + ref: 'lex:com.atproto.repo.strongRef', + }, + option: { + type: 'string', + }, + }, + }, + }, + }, + }, PubLeafletPublication: { lexicon: 1, id: 'pub.leaflet.publication', @@ -1867,6 +1951,7 @@ export const ids = { PubLeafletBlocksImage: 'pub.leaflet.blocks.image', PubLeafletBlocksMath: 'pub.leaflet.blocks.math', PubLeafletBlocksPage: 'pub.leaflet.blocks.page', + PubLeafletBlocksPoll: 'pub.leaflet.blocks.poll', PubLeafletBlocksText: 'pub.leaflet.blocks.text', PubLeafletBlocksUnorderedList: 'pub.leaflet.blocks.unorderedList', PubLeafletBlocksWebsite: 'pub.leaflet.blocks.website', @@ -1874,6 +1959,8 @@ export const ids = { PubLeafletDocument: 'pub.leaflet.document', PubLeafletGraphSubscription: 'pub.leaflet.graph.subscription', PubLeafletPagesLinearDocument: 'pub.leaflet.pages.linearDocument', + PubLeafletPollDefinition: 'pub.leaflet.poll.definition', + PubLeafletPollVote: 'pub.leaflet.poll.vote', PubLeafletPublication: 'pub.leaflet.publication', PubLeafletRichtextFacet: 'pub.leaflet.richtext.facet', PubLeafletThemeBackgroundImage: 'pub.leaflet.theme.backgroundImage', diff --git a/lexicons/api/types/pub/leaflet/blocks/poll.ts b/lexicons/api/types/pub/leaflet/blocks/poll.ts new file mode 100644 index 00000000..92860d86 --- /dev/null +++ b/lexicons/api/types/pub/leaflet/blocks/poll.ts @@ -0,0 +1,31 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { type ValidationResult, BlobRef } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' +import { validate as _validate } from '../../../../lexicons' +import { + type $Typed, + is$typed as _is$typed, + type OmitKey, +} from '../../../../util' +import type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef' + +const is$typed = _is$typed, + validate = _validate +const id = 'pub.leaflet.blocks.poll' + +export interface Main { + $type?: 'pub.leaflet.blocks.poll' + pollRef: ComAtprotoRepoStrongRef.Main +} + +const hashMain = 'main' + +export function isMain(v: V) { + return is$typed(v, id, hashMain) +} + +export function validateMain(v: V) { + return validate
    (v, id, hashMain) +} diff --git a/lexicons/api/types/pub/leaflet/pages/linearDocument.ts b/lexicons/api/types/pub/leaflet/pages/linearDocument.ts index 4fe84e8e..ed5785b2 100644 --- a/lexicons/api/types/pub/leaflet/pages/linearDocument.ts +++ b/lexicons/api/types/pub/leaflet/pages/linearDocument.ts @@ -21,6 +21,7 @@ import type * as PubLeafletBlocksCode from '../blocks/code' import type * as PubLeafletBlocksHorizontalRule from '../blocks/horizontalRule' import type * as PubLeafletBlocksBskyPost from '../blocks/bskyPost' import type * as PubLeafletBlocksPage from '../blocks/page' +import type * as PubLeafletBlocksPoll from '../blocks/poll' const is$typed = _is$typed, validate = _validate @@ -57,6 +58,7 @@ export interface Block { | $Typed | $Typed | $Typed + | $Typed | { $type: string } alignment?: | 'lex:pub.leaflet.pages.linearDocument#textAlignLeft' diff --git a/lexicons/api/types/pub/leaflet/poll/definition.ts b/lexicons/api/types/pub/leaflet/poll/definition.ts new file mode 100644 index 00000000..f30467e6 --- /dev/null +++ b/lexicons/api/types/pub/leaflet/poll/definition.ts @@ -0,0 +1,48 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { type ValidationResult, BlobRef } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' +import { validate as _validate } from '../../../../lexicons' +import { + type $Typed, + is$typed as _is$typed, + type OmitKey, +} from '../../../../util' + +const is$typed = _is$typed, + validate = _validate +const id = 'pub.leaflet.poll.definition' + +export interface Record { + $type: 'pub.leaflet.poll.definition' + name: string + options: Option[] + endDate?: string + [k: string]: unknown +} + +const hashRecord = 'main' + +export function isRecord(v: V) { + return is$typed(v, id, hashRecord) +} + +export function validateRecord(v: V) { + return validate(v, id, hashRecord, true) +} + +export interface Option { + $type?: 'pub.leaflet.poll.definition#option' + text?: string +} + +const hashOption = 'option' + +export function isOption(v: V) { + return is$typed(v, id, hashOption) +} + +export function validateOption(v: V) { + return validate