diff --git a/actions/publishToPublication.ts b/actions/publishToPublication.ts index 33fb9fff..16fb7ebc 100644 --- a/actions/publishToPublication.ts +++ b/actions/publishToPublication.ts @@ -12,6 +12,7 @@ import { PubLeafletBlocksUnorderedList, PubLeafletDocument, PubLeafletPagesLinearDocument, + PubLeafletPagesCanvas, PubLeafletRichtextFacet, PubLeafletBlocksWebsite, PubLeafletBlocksCode, @@ -95,11 +96,21 @@ export async function publishToPublication({ $type: "pub.leaflet.pages.linearDocument", blocks: firstPageBlocks, }, - ...pages.map((p) => ({ - $type: "pub.leaflet.pages.linearDocument", - id: p.id, - blocks: p.blocks, - })), + ...pages.map((p) => { + if (p.type === "canvas") { + return { + $type: "pub.leaflet.pages.canvas" as const, + id: p.id, + blocks: p.blocks as PubLeafletPagesCanvas.Block[], + }; + } else { + return { + $type: "pub.leaflet.pages.linearDocument" as const, + id: p.id, + blocks: p.blocks as PubLeafletPagesLinearDocument.Block[], + }; + } + }), ], }; let rkey = draft?.doc ? new AtUri(draft.doc).rkey : TID.nextStr(); @@ -139,8 +150,13 @@ async function processBlocksToPages( root_entity: string, ) { let scan = scanIndexLocal(facts); - let pages: { id: string; blocks: PubLeafletPagesLinearDocument.Block[] }[] = - []; + let pages: { + id: string; + blocks: + | PubLeafletPagesLinearDocument.Block[] + | PubLeafletPagesCanvas.Block[]; + type: "doc" | "canvas"; + }[] = []; let firstEntity = scan.eav(root_entity, "root/page")?.[0]; if (!firstEntity) throw new Error("No root page"); @@ -228,11 +244,24 @@ async function processBlocksToPages( if (b.type === "card") { let [page] = scan.eav(b.value, "block/card"); if (!page) return; - let blocks = getBlocksWithTypeLocal(facts, page.data.value); - pages.push({ - id: page.data.value, - blocks: await blocksToRecord(blocks), - }); + let [pageType] = scan.eav(page.data.value, "page/type"); + + if (pageType?.data.value === "canvas") { + let canvasBlocks = await canvasBlocksToRecord(page.data.value); + pages.push({ + id: page.data.value, + blocks: canvasBlocks, + type: "canvas", + }); + } else { + let blocks = getBlocksWithTypeLocal(facts, page.data.value); + pages.push({ + id: page.data.value, + blocks: await blocksToRecord(blocks), + type: "doc", + }); + } + let block: $Typed = { $type: "pub.leaflet.blocks.page", id: page.data.value, @@ -359,6 +388,52 @@ async function processBlocksToPages( } return; } + + async function canvasBlocksToRecord( + pageID: string, + ): Promise { + let canvasBlocks = scan.eav(pageID, "canvas/block"); + return ( + await Promise.all( + canvasBlocks.map(async (canvasBlock) => { + let blockEntity = canvasBlock.data.value; + let position = canvasBlock.data.position; + + // Get the block content + let blockType = scan.eav(blockEntity, "block/type")?.[0]; + if (!blockType) return null; + + let block: Block = { + type: blockType.data.value, + value: blockEntity, + parent: pageID, + position: "", + factID: canvasBlock.id, + }; + + let content = await blockToRecord(block); + if (!content) return null; + + // Get canvas-specific properties + let width = + scan.eav(blockEntity, "canvas/block/width")?.[0]?.data.value || 360; + let rotation = scan.eav(blockEntity, "canvas/block/rotation")?.[0] + ?.data.value; + + let canvasBlockRecord: PubLeafletPagesCanvas.Block = { + $type: "pub.leaflet.pages.canvas#block", + block: content, + x: position.x, + y: position.y, + width, + ...(rotation !== undefined && { rotation }), + }; + + return canvasBlockRecord; + }), + ) + ).filter((b): b is PubLeafletPagesCanvas.Block => b !== null); + } } function YJSFragmentToFacets( diff --git a/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx b/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx new file mode 100644 index 00000000..f7f1903a --- /dev/null +++ b/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx @@ -0,0 +1,240 @@ +"use client"; +import { + PubLeafletPagesCanvas, + PubLeafletPagesLinearDocument, + PubLeafletPublication, +} from "lexicons/api"; +import { PostPageData } from "./getPostPageData"; +import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; +import { AppBskyFeedDefs } from "@atproto/api"; +import { PageWrapper } from "components/Pages/Page"; +import { Block } from "./PostContent"; +import { CanvasBackgroundPattern } from "components/Canvas"; +import { + getCommentCount, + getQuoteCount, + Interactions, +} from "./Interactions/Interactions"; +import { Separator } from "components/Layout"; +import { Popover } from "components/Popover"; +import { InfoSmall } from "components/Icons/InfoSmall"; +import { PostHeader } from "./PostHeader/PostHeader"; +import { useDrawerOpen } from "./Interactions/InteractionDrawer"; + +export function CanvasPage({ + document, + blocks, + did, + profile, + preferences, + pubRecord, + prerenderedCodeBlocks, + bskyPostData, + document_uri, + pageId, + pageOptions, + fullPageScroll, + pages, +}: { + document_uri: string; + document: PostPageData; + blocks: PubLeafletPagesCanvas.Block[]; + profile: ProfileViewDetailed; + pubRecord: PubLeafletPublication.Record; + did: string; + prerenderedCodeBlocks?: Map; + bskyPostData: AppBskyFeedDefs.PostView[]; + preferences: { showComments?: boolean }; + pageId?: string; + pageOptions?: React.ReactNode; + fullPageScroll: boolean; + pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; +}) { + let hasPageBackground = !!pubRecord.theme?.showPageBackground; + let isSubpage = !!pageId; + let drawer = useDrawerOpen(document_uri); + + return ( + + + + + ); +} + +function CanvasContent({ + blocks, + did, + prerenderedCodeBlocks, + bskyPostData, + pageId, + pages, +}: { + blocks: PubLeafletPagesCanvas.Block[]; + did: string; + prerenderedCodeBlocks?: Map; + bskyPostData: AppBskyFeedDefs.PostView[]; + pageId?: string; + pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; +}) { + let height = blocks.length > 0 ? Math.max(...blocks.map((b) => b.y), 0) : 0; + + return ( +
+
+ + + {blocks + .sort((a, b) => { + if (a.y === b.y) { + return a.x - b.x; + } + return a.y - b.y; + }) + .map((canvasBlock, index) => { + return ( + + ); + })} +
+
+ ); +} + +function CanvasBlock({ + canvasBlock, + did, + prerenderedCodeBlocks, + bskyPostData, + pageId, + pages, + index, +}: { + canvasBlock: PubLeafletPagesCanvas.Block; + did: string; + prerenderedCodeBlocks?: Map; + bskyPostData: AppBskyFeedDefs.PostView[]; + pageId?: string; + pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; + index: number; +}) { + let { x, y, width, rotation } = canvasBlock; + let transform = `translate(${x}px, ${y}px)${rotation ? ` rotate(${rotation}deg)` : ""}`; + + // Wrap the block in a LinearDocument.Block structure for compatibility + let linearBlock: PubLeafletPagesLinearDocument.Block = { + $type: "pub.leaflet.pages.linearDocument#block", + block: canvasBlock.block, + }; + + return ( +
+
+ +
+
+ ); +} + +const CanvasMetadata = (props: { + pageId: string | undefined; + isSubpage: boolean | undefined; + data: PostPageData; + profile: ProfileViewDetailed; + preferences: { showComments?: boolean }; + quotesCount: number | undefined; + commentsCount: number | undefined; +}) => { + return ( +
+ + {!props.isSubpage && ( + <> + + } + > + + + + )} +
+ ); +}; + +const CanvasBackground = () => { + return ( +
+ +
+ ); +}; diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx index 91d30991..39e86998 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx @@ -5,10 +5,12 @@ import { flushSync } from "react-dom"; import type { Json } from "supabase/database.types"; import { create } from "zustand"; import type { Comment } from "./Comments"; -import { QuotePosition } from "../quotePosition"; +import { decodeQuotePosition, QuotePosition } from "../quotePosition"; import { useContext } from "react"; import { PostPageContext } from "../PostPageContext"; import { scrollIntoView } from "src/utils/scrollIntoView"; +import { PostPageData } from "../getPostPageData"; +import { PubLeafletComment } from "lexicons/api"; export type InteractionState = { drawerOpen: undefined | boolean; @@ -149,3 +151,32 @@ export const Interactions = (props: { ); }; + +export function getCommentCount(document: PostPageData, pageId?: string) { + if (!document) return; + + if (pageId) + return document.document_mentions_in_bsky.filter((q) => + q.link.includes(pageId), + ).length; + else + return document.document_mentions_in_bsky.filter((q) => { + const url = new URL(q.link); + const quoteParam = url.pathname.split("/l-quote/")[1]; + if (!quoteParam) return null; + const quotePosition = decodeQuotePosition(quoteParam); + return !quotePosition?.pageId; + }).length; +} + +export function getQuoteCount(document: PostPageData, pageId?: string) { + if (!document) return; + if (pageId) + return document.comments_on_documents.filter( + (c) => (c.record as PubLeafletComment.Record)?.onPage === pageId, + ).length; + else + return document.comments_on_documents.filter( + (c) => !(c.record as PubLeafletComment.Record)?.onPage, + ).length; +} diff --git a/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx b/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx new file mode 100644 index 00000000..57e20582 --- /dev/null +++ b/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx @@ -0,0 +1,135 @@ +"use client"; +import { + PubLeafletComment, + PubLeafletDocument, + PubLeafletPagesLinearDocument, + PubLeafletPublication, +} from "lexicons/api"; +import { PostPageData } from "./getPostPageData"; +import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; +import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; +import { SubscribeWithBluesky } from "app/lish/Subscribe"; +import { EditTiny } from "components/Icons/EditTiny"; +import { + getCommentCount, + getQuoteCount, + Interactions, +} from "./Interactions/Interactions"; +import { PostContent } from "./PostContent"; +import { PostHeader } from "./PostHeader/PostHeader"; +import { useIdentityData } from "components/IdentityProvider"; +import { AppBskyFeedDefs } from "@atproto/api"; +import { useDrawerOpen } from "./Interactions/InteractionDrawer"; +import { PageWrapper } from "components/Pages/Page"; +import { decodeQuotePosition } from "./quotePosition"; + +export function LinearDocumentPage({ + document, + blocks, + did, + profile, + preferences, + pubRecord, + prerenderedCodeBlocks, + bskyPostData, + document_uri, + pageId, + pageOptions, + fullPageScroll, +}: { + document_uri: string; + document: PostPageData; + blocks: PubLeafletPagesLinearDocument.Block[]; + profile?: ProfileViewDetailed; + pubRecord: PubLeafletPublication.Record; + did: string; + prerenderedCodeBlocks?: Map; + bskyPostData: AppBskyFeedDefs.PostView[]; + preferences: { showComments?: boolean }; + pageId?: string; + pageOptions?: React.ReactNode; + fullPageScroll: boolean; +}) { + let { identity } = useIdentityData(); + let drawer = useDrawerOpen(document_uri); + + if (!document || !document.documents_in_publications[0].publications) + return null; + + let hasPageBackground = !!pubRecord.theme?.showPageBackground; + let record = document.data as PubLeafletDocument.Record; + + const isSubpage = !!pageId; + + return ( + <> + + {!isSubpage && profile && ( + + )} + + + {!isSubpage && ( + <> +
+
+ {identity && + identity.atp_did === + document.documents_in_publications[0]?.publications + ?.identity_did ? ( + + Edit Post + + ) : ( + + )} +
+ + )} +
+ + ); +} diff --git a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx index a19a5458..ca3cfa83 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx @@ -9,6 +9,7 @@ import { PubLeafletBlocksWebsite, PubLeafletDocument, PubLeafletPagesLinearDocument, + PubLeafletPagesCanvas, PubLeafletBlocksHorizontalRule, PubLeafletBlocksBlockquote, PubLeafletBlocksBskyPost, @@ -46,7 +47,7 @@ export function PostContent({ className?: string; prerenderedCodeBlocks?: Map; bskyPostData: AppBskyFeedDefs.PostView[]; - pages: PubLeafletPagesLinearDocument.Main[]; + pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; }) { return (
; bskyPostData: AppBskyFeedDefs.PostView[]; @@ -136,6 +137,9 @@ let Block = ({ let id = b.block.id; let page = pages.find((p) => p.id === id); if (!page) return; + + const isCanvas = PubLeafletPagesCanvas.isMain(page); + return ( ); @@ -354,7 +360,7 @@ let Block = ({ function ListItem(props: { index: number[]; - pages: PubLeafletPagesLinearDocument.Main[]; + pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; item: PubLeafletBlocksUnorderedList.ListItem; did: string; className?: string; diff --git a/app/lish/[did]/[publication]/[rkey]/PostPages.tsx b/app/lish/[did]/[publication]/[rkey]/PostPages.tsx index 292a44bf..5a017669 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostPages.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostPages.tsx @@ -1,19 +1,12 @@ "use client"; import { - PubLeafletComment, PubLeafletDocument, PubLeafletPagesLinearDocument, + PubLeafletPagesCanvas, PubLeafletPublication, } from "lexicons/api"; import { PostPageData } from "./getPostPageData"; import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; -import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; -import { SubscribeWithBluesky } from "app/lish/Subscribe"; -import { EditTiny } from "components/Icons/EditTiny"; -import { Interactions } from "./Interactions/Interactions"; -import { PostContent } from "./PostContent"; -import { PostHeader } from "./PostHeader/PostHeader"; -import { useIdentityData } from "components/IdentityProvider"; import { AppBskyFeedDefs } from "@atproto/api"; import { create } from "zustand/react"; import { @@ -23,12 +16,13 @@ import { import { BookendSpacer, SandwichSpacer } from "components/LeafletLayout"; import { PageOptionButton } from "components/Pages/PageOptions"; import { CloseTiny } from "components/Icons/CloseTiny"; -import { PageWrapper } from "components/Pages/Page"; import { Fragment, useEffect } from "react"; import { flushSync } from "react-dom"; import { scrollIntoView } from "src/utils/scrollIntoView"; import { useParams } from "next/navigation"; import { decodeQuotePosition } from "./quotePosition"; +import { LinearDocumentPage } from "./LinearDocumentPage"; +import { CanvasPage } from "./CanvasPage"; const usePostPageUIState = create(() => ({ pages: [] as string[], @@ -124,7 +118,6 @@ export function PostPages({ bskyPostData: AppBskyFeedDefs.PostView[]; preferences: { showComments?: boolean }; }) { - let { identity } = useIdentityData(); let drawer = useDrawerOpen(document_uri); useInitializeOpenPages(); let pages = useOpenPages(); @@ -132,75 +125,24 @@ export function PostPages({ return null; let hasPageBackground = !!pubRecord.theme?.showPageBackground; - let fullPageScroll = !hasPageBackground && !drawer && pages.length === 0; let record = document.data as PubLeafletDocument.Record; + + let fullPageScroll = !hasPageBackground && !drawer && pages.length === 0; return ( <> {!fullPageScroll && } - - - - { - const url = new URL(q.link); - const quoteParam = url.pathname.split("/l-quote/")[1]; - if (!quoteParam) return null; - const quotePosition = decodeQuotePosition(quoteParam); - return !quotePosition?.pageId; - }).length - } - commentsCount={ - document.comments_on_documents.filter( - (c) => !(c.record as PubLeafletComment.Record)?.onPage, - ).length - } - /> -
-
- {identity && - identity.atp_did === - document.documents_in_publications[0]?.publications - ?.identity_did ? ( - - Edit Post - - ) : ( - - )} -
-
+ preferences={preferences} + pubRecord={pubRecord} + prerenderedCodeBlocks={prerenderedCodeBlocks} + bskyPostData={bskyPostData} + document_uri={document_uri} + /> {drawer && !drawer.pageId && ( { let page = record.pages.find( - (page) => (page as PubLeafletPagesLinearDocument.Main).id === p, - ) as PubLeafletPagesLinearDocument.Main | undefined; + (page) => + ( + page as + | PubLeafletPagesLinearDocument.Main + | PubLeafletPagesCanvas.Main + ).id === p, + ) as + | PubLeafletPagesLinearDocument.Main + | PubLeafletPagesCanvas.Main + | undefined; if (!page) return null; + + const isCanvas = PubLeafletPagesCanvas.isMain(page); + return ( - {/*JARED TODO : drawerOpen here is checking whether the drawer is open on the first page, rather than if it's open on this page. Please rewire this when you add drawers per page!*/} - closePage(page?.id!)} - hasPageBackground={hasPageBackground} - /> - } - > - - - q.link.includes(page.id!), - ).length + pages={record.pages as PubLeafletPagesLinearDocument.Main[]} + pageOptions={ + closePage(page?.id!)} + hasPageBackground={hasPageBackground} + /> } - commentsCount={ - document.comments_on_documents.filter( - (c) => - (c.record as PubLeafletComment.Record)?.onPage === - page.id, - ).length + /> + ) : ( + closePage(page?.id!)} + hasPageBackground={hasPageBackground} + /> } /> - + )} {drawer && drawer.pageId === page.id && ( ; bskyPostData: AppBskyFeedDefs.PostView[]; + isCanvas?: boolean; + pages?: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; }) { //switch to use actually state let openPages = useOpenPages(); @@ -56,7 +60,20 @@ export function PublishedPageLinkBlock(props: { openPage(props.parentPageId, props.pageId); }} > - + {props.isCanvas ? ( + + ) : ( + + )}
); } @@ -203,8 +220,8 @@ const Interactions = (props: { pageId: string; parentPageId?: string }) => { openInteractionDrawer("quotes", document_uri, props.pageId); else setInteractionState(document_uri, { drawerOpen: false }); }} - aria-label="Page quotes" > + Page quotes {quotes}{" "} )} @@ -221,11 +238,93 @@ const Interactions = (props: { pageId: string; parentPageId?: string }) => { openInteractionDrawer("comments", document_uri, props.pageId); else setInteractionState(document_uri, { drawerOpen: false }); }} - aria-label="Page comments" > + Page comments {comments}{" "} )} ); }; + +const CanvasLinkBlock = (props: { + blocks: PubLeafletPagesCanvas.Block[]; + did: string; + pageId: string; + bskyPostData: AppBskyFeedDefs.PostView[]; + pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; +}) => { + let pageWidth = `var(--page-width-unitless)`; + let height = + props.blocks.length > 0 ? Math.max(...props.blocks.map((b) => b.y), 0) : 0; + + return ( +
+
+
+
+ +
+ {props.blocks + .sort((a, b) => { + if (a.y === b.y) { + return a.x - b.x; + } + return a.y - b.y; + }) + .map((canvasBlock, index) => { + let { x, y, width, rotation } = canvasBlock; + let transform = `translate(${x}px, ${y}px)${rotation ? ` rotate(${rotation}deg)` : ""}`; + + // Wrap the block in a LinearDocument.Block structure for compatibility + let linearBlock: PubLeafletPagesLinearDocument.Block = { + $type: "pub.leaflet.pages.linearDocument#block", + block: canvasBlock.block, + }; + + return ( +
+
+ +
+
+ ); + })} +
+
+
+ ); +}; diff --git a/components/Blocks/BlockCommands.tsx b/components/Blocks/BlockCommands.tsx index de82808a..467eb02f 100644 --- a/components/Blocks/BlockCommands.tsx +++ b/components/Blocks/BlockCommands.tsx @@ -369,7 +369,6 @@ export const blockCommands: Command[] = [ name: "New Canvas", icon: , type: "page", - hiddenInPublication: true, onSelect: async (rep, props, um) => { props.entityID && clearCommandSearchText(props.entityID); let entity = await createBlockWithType(rep, props, "card"); diff --git a/components/Canvas.tsx b/components/Canvas.tsx index 9baf9da4..6f5b1af8 100644 --- a/components/Canvas.tsx +++ b/components/Canvas.tsx @@ -14,9 +14,24 @@ import { Media } from "./Media"; import { TooltipButton } from "./Buttons"; import { useBlockKeyboardHandlers } from "./Blocks/useBlockKeyboardHandlers"; import { AddSmall } from "./Icons/AddSmall"; +import { InfoSmall } from "./Icons/InfoSmall"; +import { Popover } from "./Popover"; +import { Separator } from "./Layout"; +import { CommentTiny } from "./Icons/CommentTiny"; +import { QuoteTiny } from "./Icons/QuoteTiny"; +import { PublicationMetadata } from "./Pages/PublicationMetadata"; +import { useLeafletPublicationData } from "./PageSWRDataProvider"; +import { + PubLeafletPublication, + PubLeafletPublicationRecord, +} from "lexicons/api"; import { useHandleCanvasDrop } from "./Blocks/useHandleCanvasDrop"; -export function Canvas(props: { entityID: string; preview?: boolean }) { +export function Canvas(props: { + entityID: string; + preview?: boolean; + first?: boolean; +}) { let entity_set = useEntitySetContext(); let ref = useRef(null); useEffect(() => { @@ -45,9 +60,6 @@ export function Canvas(props: { entityID: string; preview?: boolean }) { return () => abort.abort(); }); - let narrowWidth = useEntity(props.entityID, "canvas/narrow-width")?.data - .value; - return (
+ + + -
); } @@ -150,32 +164,40 @@ export function CanvasContent(props: { entityID: string; preview?: boolean }) { ); } -function CanvasWidthHandle(props: { entityID: string }) { - let canvasFocused = useUIState((s) => s.focusedEntity?.entityType === "page"); - let { rep } = useReplicache(); - let narrowWidth = useEntity(props.entityID, "canvas/narrow-width")?.data - .value; +const CanvasMetadata = (props: { isSubpage: boolean | undefined }) => { + let { data: pub } = useLeafletPublicationData(); + if (!pub || !pub.publications) return null; + + let pubRecord = pub.publications.record as PubLeafletPublication.Record; + let showComments = pubRecord.preferences?.showComments; + return ( -