a tool for shared writing and social publishing

add page block type to lexicon and basic component

+356 -267
+225 -260
actions/publishToPublication.ts
··· 20 20 PubLeafletBlocksBskyPost, 21 21 PubLeafletBlocksBlockquote, 22 22 PubLeafletBlocksIframe, 23 + PubLeafletBlocksPage, 23 24 } from "lexicons/api"; 24 25 import { Block } from "components/Blocks/Block"; 25 26 import { TID } from "@atproto/common"; ··· 72 73 root: root_entity, 73 74 }); 74 75 let facts = (data as unknown as Fact<Attribute>[]) || []; 75 - let scan = scanIndexLocal(facts); 76 - let firstEntity = scan.eav(root_entity, "root/page")?.[0]; 77 - if (!firstEntity) throw new Error("No root page"); 78 - let blocks = getBlocksWithTypeLocal(facts, firstEntity?.data.value); 79 76 80 - let images = blocks 81 - .filter((b) => b.type === "image") 82 - .map((b) => scan.eav(b.value, "block/image")[0]); 83 - let links = blocks 84 - .filter((b) => b.type == "link") 85 - .map((b) => scan.eav(b.value, "link/preview")[0]); 86 - let imageMap = new Map<string, BlobRef>(); 87 - await Promise.all( 88 - [...links, ...images].map(async (b) => { 89 - if (!b) return; 90 - let data = await fetch(b.data.src); 91 - if (data.status !== 200) return; 92 - let binary = await data.blob(); 93 - let blob = await agent.com.atproto.repo.uploadBlob(binary, { 94 - headers: { "Content-Type": binary.type }, 95 - }); 96 - imageMap.set(b.data.src, blob.data.blob); 97 - }), 98 - ); 99 - 100 - let b: PubLeafletPagesLinearDocument.Block[] = blocksToRecord( 101 - blocks, 102 - imageMap, 103 - scan, 77 + let { firstPageBlocks, pages } = await processBlocksToPages( 78 + facts, 79 + agent, 104 80 root_entity, 105 81 ); 106 82 ··· 117 93 pages: [ 118 94 { 119 95 $type: "pub.leaflet.pages.linearDocument", 120 - blocks: b, 96 + blocks: firstPageBlocks, 121 97 }, 98 + ...pages.map((p) => ({ 99 + $type: "pub.leaflet.pages.linearDocument", 100 + id: p.id, 101 + blocks: p.blocks, 102 + })), 122 103 ], 123 104 }; 124 105 let rkey = draft?.doc ? new AtUri(draft.doc).rkey : TID.nextStr(); ··· 152 133 return { rkey, record: JSON.parse(JSON.stringify(record)) }; 153 134 } 154 135 155 - function blocksToRecord( 156 - blocks: Block[], 157 - imageMap: Map<string, BlobRef>, 158 - scan: ReturnType<typeof scanIndexLocal>, 159 - root_entity: string, 160 - ): PubLeafletPagesLinearDocument.Block[] { 161 - let parsedBlocks = parseBlocksToList(blocks); 162 - return parsedBlocks.flatMap((blockOrList) => { 163 - if (blockOrList.type === "block") { 164 - let alignmentValue = 165 - scan.eav(blockOrList.block.value, "block/text-alignment")[0]?.data 166 - .value || "left"; 167 - let alignment = 168 - alignmentValue === "center" 169 - ? "lex:pub.leaflet.pages.linearDocument#textAlignCenter" 170 - : alignmentValue === "right" 171 - ? "lex:pub.leaflet.pages.linearDocument#textAlignRight" 172 - : undefined; 173 - let b = blockToRecord(blockOrList.block, imageMap, scan, root_entity); 174 - if (!b) return []; 175 - let block: PubLeafletPagesLinearDocument.Block = { 176 - $type: "pub.leaflet.pages.linearDocument#block", 177 - alignment, 178 - block: b, 179 - }; 180 - return [block]; 181 - } else { 182 - let block: PubLeafletPagesLinearDocument.Block = { 183 - $type: "pub.leaflet.pages.linearDocument#block", 184 - block: { 185 - $type: "pub.leaflet.blocks.unorderedList", 186 - children: childrenToRecord( 187 - blockOrList.children, 188 - imageMap, 189 - scan, 190 - root_entity, 191 - ), 192 - }, 193 - }; 194 - return [block]; 195 - } 196 - }); 197 - } 198 - 199 - function childrenToRecord( 200 - children: List[], 201 - imageMap: Map<string, BlobRef>, 202 - scan: ReturnType<typeof scanIndexLocal>, 136 + async function processBlocksToPages( 137 + facts: Fact<any>[], 138 + agent: AtpBaseClient, 203 139 root_entity: string, 204 140 ) { 205 - return children.flatMap((child) => { 206 - let content = blockToRecord(child.block, imageMap, scan, root_entity); 207 - if (!content) return []; 208 - let record: PubLeafletBlocksUnorderedList.ListItem = { 209 - $type: "pub.leaflet.blocks.unorderedList#listItem", 210 - content, 211 - children: childrenToRecord(child.children, imageMap, scan, root_entity), 212 - }; 213 - return record; 214 - }); 215 - } 216 - function blockToRecord( 217 - b: Block, 218 - imageMap: Map<string, BlobRef>, 219 - scan: ReturnType<typeof scanIndexLocal>, 220 - root_entity: string, 221 - ) { 222 - const getBlockContent = (b: string) => { 223 - let [content] = scan.eav(b, "block/text"); 224 - if (!content) return ["", [] as PubLeafletRichtextFacet.Main[]] as const; 225 - let doc = new Y.Doc(); 226 - const update = base64.toByteArray(content.data.value); 227 - Y.applyUpdate(doc, update); 228 - let nodes = doc.getXmlElement("prosemirror").toArray(); 229 - let stringValue = YJSFragmentToString(nodes[0]); 230 - let facets = YJSFragmentToFacets(nodes[0]); 231 - return [stringValue, facets] as const; 232 - }; 141 + let scan = scanIndexLocal(facts); 142 + let pages: { id: string; blocks: PubLeafletPagesLinearDocument.Block[] }[] = 143 + []; 144 + 145 + let firstEntity = scan.eav(root_entity, "root/page")?.[0]; 146 + if (!firstEntity) throw new Error("No root page"); 147 + let blocks = getBlocksWithTypeLocal(facts, firstEntity?.data.value); 148 + let b = await blocksToRecord(blocks); 149 + return { firstPageBlocks: b, pages }; 233 150 234 - if (b.type === "bluesky-post") { 235 - let [post] = scan.eav(b.value, "block/bluesky-post"); 236 - if (!post || !post.data.value.post) return; 237 - let block: $Typed<PubLeafletBlocksBskyPost.Main> = { 238 - $type: ids.PubLeafletBlocksBskyPost, 239 - postRef: { 240 - uri: post.data.value.post.uri, 241 - cid: post.data.value.post.cid, 242 - }, 243 - }; 244 - return block; 151 + async function uploadImage(src: string) { 152 + let data = await fetch(src); 153 + if (data.status !== 200) return; 154 + let binary = await data.blob(); 155 + let blob = await agent.com.atproto.repo.uploadBlob(binary, { 156 + headers: { "Content-Type": binary.type }, 157 + }); 158 + return blob.data.blob; 245 159 } 246 - if (b.type === "horizontal-rule") { 247 - let block: $Typed<PubLeafletBlocksHorizontalRule.Main> = { 248 - $type: ids.PubLeafletBlocksHorizontalRule, 249 - }; 250 - return block; 160 + async function blocksToRecord( 161 + blocks: Block[], 162 + ): Promise<PubLeafletPagesLinearDocument.Block[]> { 163 + let parsedBlocks = parseBlocksToList(blocks); 164 + return ( 165 + await Promise.all( 166 + parsedBlocks.map(async (blockOrList) => { 167 + if (blockOrList.type === "block") { 168 + let alignmentValue = 169 + scan.eav(blockOrList.block.value, "block/text-alignment")[0]?.data 170 + .value || "left"; 171 + let alignment = 172 + alignmentValue === "center" 173 + ? "lex:pub.leaflet.pages.linearDocument#textAlignCenter" 174 + : alignmentValue === "right" 175 + ? "lex:pub.leaflet.pages.linearDocument#textAlignRight" 176 + : undefined; 177 + let b = await blockToRecord(blockOrList.block); 178 + if (!b) return []; 179 + let block: PubLeafletPagesLinearDocument.Block = { 180 + $type: "pub.leaflet.pages.linearDocument#block", 181 + alignment, 182 + block: b, 183 + }; 184 + return [block]; 185 + } else { 186 + let block: PubLeafletPagesLinearDocument.Block = { 187 + $type: "pub.leaflet.pages.linearDocument#block", 188 + block: { 189 + $type: "pub.leaflet.blocks.unorderedList", 190 + children: await childrenToRecord(blockOrList.children), 191 + }, 192 + }; 193 + return [block]; 194 + } 195 + }), 196 + ) 197 + ).flat(); 251 198 } 252 199 253 - if (b.type === "heading") { 254 - let [headingLevel] = scan.eav(b.value, "block/heading-level"); 200 + async function childrenToRecord(children: List[]) { 201 + return ( 202 + await Promise.all( 203 + children.map(async (child) => { 204 + let content = await blockToRecord(child.block); 205 + if (!content) return []; 206 + let record: PubLeafletBlocksUnorderedList.ListItem = { 207 + $type: "pub.leaflet.blocks.unorderedList#listItem", 208 + content, 209 + children: await childrenToRecord(child.children), 210 + }; 211 + return record; 212 + }), 213 + ) 214 + ).flat(); 215 + } 216 + async function blockToRecord(b: Block) { 217 + const getBlockContent = (b: string) => { 218 + let [content] = scan.eav(b, "block/text"); 219 + if (!content) return ["", [] as PubLeafletRichtextFacet.Main[]] as const; 220 + let doc = new Y.Doc(); 221 + const update = base64.toByteArray(content.data.value); 222 + Y.applyUpdate(doc, update); 223 + let nodes = doc.getXmlElement("prosemirror").toArray(); 224 + let stringValue = YJSFragmentToString(nodes[0]); 225 + let facets = YJSFragmentToFacets(nodes[0]); 226 + return [stringValue, facets] as const; 227 + }; 228 + if (b.type === "card") { 229 + let [page] = scan.eav(b.value, "block/card"); 230 + if (!page) return; 231 + let blocks = getBlocksWithTypeLocal(facts, page.data.value); 232 + pages.push({ 233 + id: page.data.value, 234 + blocks: await blocksToRecord(blocks), 235 + }); 236 + let block: $Typed<PubLeafletBlocksPage.Main> = { 237 + $type: "pub.leaflet.blocks.page", 238 + id: page.data.value, 239 + }; 240 + return block; 241 + } 255 242 256 - let [stringValue, facets] = getBlockContent(b.value); 257 - let block: $Typed<PubLeafletBlocksHeader.Main> = { 258 - $type: "pub.leaflet.blocks.header", 259 - level: headingLevel?.data.value || 1, 260 - plaintext: stringValue, 261 - facets, 262 - }; 263 - return block; 264 - } 243 + if (b.type === "bluesky-post") { 244 + let [post] = scan.eav(b.value, "block/bluesky-post"); 245 + if (!post || !post.data.value.post) return; 246 + let block: $Typed<PubLeafletBlocksBskyPost.Main> = { 247 + $type: ids.PubLeafletBlocksBskyPost, 248 + postRef: { 249 + uri: post.data.value.post.uri, 250 + cid: post.data.value.post.cid, 251 + }, 252 + }; 253 + return block; 254 + } 255 + if (b.type === "horizontal-rule") { 256 + let block: $Typed<PubLeafletBlocksHorizontalRule.Main> = { 257 + $type: ids.PubLeafletBlocksHorizontalRule, 258 + }; 259 + return block; 260 + } 265 261 266 - if (b.type === "blockquote") { 267 - let [stringValue, facets] = getBlockContent(b.value); 268 - let block: $Typed<PubLeafletBlocksBlockquote.Main> = { 269 - $type: ids.PubLeafletBlocksBlockquote, 270 - plaintext: stringValue, 271 - facets, 272 - }; 273 - return block; 274 - } 262 + if (b.type === "heading") { 263 + let [headingLevel] = scan.eav(b.value, "block/heading-level"); 275 264 276 - if (b.type == "text") { 277 - let [stringValue, facets] = getBlockContent(b.value); 278 - let block: $Typed<PubLeafletBlocksText.Main> = { 279 - $type: ids.PubLeafletBlocksText, 280 - plaintext: stringValue, 281 - facets, 282 - }; 283 - return block; 284 - } 285 - if (b.type === "embed") { 286 - let [url] = scan.eav(b.value, "embed/url"); 287 - let [height] = scan.eav(b.value, "embed/height"); 288 - if (!url) return; 289 - let block: $Typed<PubLeafletBlocksIframe.Main> = { 290 - $type: "pub.leaflet.blocks.iframe", 291 - url: url.data.value, 292 - height: height?.data.value || 600, 293 - }; 294 - return block; 295 - } 296 - if (b.type == "image") { 297 - let [image] = scan.eav(b.value, "block/image"); 298 - if (!image) return; 299 - let [altText] = scan.eav(b.value, "image/alt"); 300 - let blobref = imageMap.get(image.data.src); 301 - if (!blobref) return; 302 - let block: $Typed<PubLeafletBlocksImage.Main> = { 303 - $type: "pub.leaflet.blocks.image", 304 - image: blobref, 305 - aspectRatio: { 306 - height: image.data.height, 307 - width: image.data.width, 308 - }, 309 - alt: altText ? altText.data.value : undefined, 310 - }; 311 - return block; 312 - } 313 - if (b.type === "link") { 314 - let [previewImage] = scan.eav(b.value, "link/preview"); 315 - let [description] = scan.eav(b.value, "link/description"); 316 - let [src] = scan.eav(b.value, "link/url"); 317 - if (!src) return; 318 - let blobref = previewImage 319 - ? imageMap.get(previewImage?.data.src) 320 - : undefined; 321 - let [title] = scan.eav(b.value, "link/title"); 322 - let block: $Typed<PubLeafletBlocksWebsite.Main> = { 323 - $type: "pub.leaflet.blocks.website", 324 - previewImage: blobref, 325 - src: src.data.value, 326 - description: description?.data.value, 327 - title: title?.data.value, 328 - }; 329 - return block; 330 - } 331 - if (b.type === "code") { 332 - let [language] = scan.eav(b.value, "block/code-language"); 333 - let [code] = scan.eav(b.value, "block/code"); 334 - let [theme] = scan.eav(root_entity, "theme/code-theme"); 335 - let block: $Typed<PubLeafletBlocksCode.Main> = { 336 - $type: "pub.leaflet.blocks.code", 337 - language: language?.data.value, 338 - plaintext: code?.data.value || "", 339 - syntaxHighlightingTheme: theme?.data.value, 340 - }; 341 - return block; 342 - } 343 - if (b.type === "math") { 344 - let [math] = scan.eav(b.value, "block/math"); 345 - let block: $Typed<PubLeafletBlocksMath.Main> = { 346 - $type: "pub.leaflet.blocks.math", 347 - tex: math?.data.value || "", 348 - }; 349 - return block; 350 - } 351 - return; 352 - } 265 + let [stringValue, facets] = getBlockContent(b.value); 266 + let block: $Typed<PubLeafletBlocksHeader.Main> = { 267 + $type: "pub.leaflet.blocks.header", 268 + level: headingLevel?.data.value || 1, 269 + plaintext: stringValue, 270 + facets, 271 + }; 272 + return block; 273 + } 353 274 354 - async function sendPostToEmailSubscribers( 355 - publication_uri: string, 356 - post: { content: string; title: string }, 357 - ) { 358 - let { data: publication } = await supabaseServerClient 359 - .from("publications") 360 - .select("*, subscribers_to_publications(*)") 361 - .eq("uri", publication_uri) 362 - .single(); 275 + if (b.type === "blockquote") { 276 + let [stringValue, facets] = getBlockContent(b.value); 277 + let block: $Typed<PubLeafletBlocksBlockquote.Main> = { 278 + $type: ids.PubLeafletBlocksBlockquote, 279 + plaintext: stringValue, 280 + facets, 281 + }; 282 + return block; 283 + } 363 284 364 - let res = await fetch("https://api.postmarkapp.com/email/batch", { 365 - method: "POST", 366 - headers: { 367 - "Content-Type": "application/json", 368 - "X-Postmark-Server-Token": process.env.POSTMARK_API_KEY!, 369 - }, 370 - body: JSON.stringify( 371 - publication?.subscribers_to_publications.map((sub) => ({ 372 - Headers: [ 373 - { 374 - Name: "List-Unsubscribe-Post", 375 - Value: "List-Unsubscribe=One-Click", 376 - }, 377 - { 378 - Name: "List-Unsubscribe", 379 - Value: `<${"TODO"}/mail/unsubscribe?sub_id=${sub.identity}>`, 380 - }, 381 - ], 382 - MessageStream: "broadcast", 383 - From: `${publication.name} <mailbox@leaflet.pub>`, 384 - Subject: post.title, 385 - To: sub.identity, 386 - HtmlBody: ` 387 - <h1>${publication.name}</h1> 388 - <hr style="margin-top: 1em; margin-bottom: 1em;"> 389 - ${post.content} 390 - <hr style="margin-top: 1em; margin-bottom: 1em;"> 391 - This is a super alpha release! Ask Jared if you want to unsubscribe (sorry) 392 - `, 393 - TextBody: post.content, 394 - })), 395 - ), 396 - }); 285 + if (b.type == "text") { 286 + let [stringValue, facets] = getBlockContent(b.value); 287 + let block: $Typed<PubLeafletBlocksText.Main> = { 288 + $type: ids.PubLeafletBlocksText, 289 + plaintext: stringValue, 290 + facets, 291 + }; 292 + return block; 293 + } 294 + if (b.type === "embed") { 295 + let [url] = scan.eav(b.value, "embed/url"); 296 + let [height] = scan.eav(b.value, "embed/height"); 297 + if (!url) return; 298 + let block: $Typed<PubLeafletBlocksIframe.Main> = { 299 + $type: "pub.leaflet.blocks.iframe", 300 + url: url.data.value, 301 + height: height?.data.value || 600, 302 + }; 303 + return block; 304 + } 305 + if (b.type == "image") { 306 + let [image] = scan.eav(b.value, "block/image"); 307 + if (!image) return; 308 + let [altText] = scan.eav(b.value, "image/alt"); 309 + let blobref = await uploadImage(image.data.src); 310 + if (!blobref) return; 311 + let block: $Typed<PubLeafletBlocksImage.Main> = { 312 + $type: "pub.leaflet.blocks.image", 313 + image: blobref, 314 + aspectRatio: { 315 + height: image.data.height, 316 + width: image.data.width, 317 + }, 318 + alt: altText ? altText.data.value : undefined, 319 + }; 320 + return block; 321 + } 322 + if (b.type === "link") { 323 + let [previewImage] = scan.eav(b.value, "link/preview"); 324 + let [description] = scan.eav(b.value, "link/description"); 325 + let [src] = scan.eav(b.value, "link/url"); 326 + if (!src) return; 327 + let blobref = previewImage 328 + ? await uploadImage(previewImage?.data.src) 329 + : undefined; 330 + let [title] = scan.eav(b.value, "link/title"); 331 + let block: $Typed<PubLeafletBlocksWebsite.Main> = { 332 + $type: "pub.leaflet.blocks.website", 333 + previewImage: blobref, 334 + src: src.data.value, 335 + description: description?.data.value, 336 + title: title?.data.value, 337 + }; 338 + return block; 339 + } 340 + if (b.type === "code") { 341 + let [language] = scan.eav(b.value, "block/code-language"); 342 + let [code] = scan.eav(b.value, "block/code"); 343 + let [theme] = scan.eav(root_entity, "theme/code-theme"); 344 + let block: $Typed<PubLeafletBlocksCode.Main> = { 345 + $type: "pub.leaflet.blocks.code", 346 + language: language?.data.value, 347 + plaintext: code?.data.value || "", 348 + syntaxHighlightingTheme: theme?.data.value, 349 + }; 350 + return block; 351 + } 352 + if (b.type === "math") { 353 + let [math] = scan.eav(b.value, "block/math"); 354 + let block: $Typed<PubLeafletBlocksMath.Main> = { 355 + $type: "pub.leaflet.blocks.math", 356 + tex: math?.data.value || "", 357 + }; 358 + return block; 359 + } 360 + return; 361 + } 397 362 } 398 363 399 364 function YJSFragmentToFacets(
+21 -2
app/lish/[did]/[publication]/[rkey]/PostContent.tsx
··· 1 + "use client"; 1 2 import { 2 3 PubLeafletBlocksMath, 3 4 PubLeafletBlocksCode, ··· 12 13 PubLeafletBlocksBlockquote, 13 14 PubLeafletBlocksBskyPost, 14 15 PubLeafletBlocksIframe, 16 + PubLeafletBlocksPage, 15 17 } from "lexicons/api"; 18 + 16 19 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 17 20 import { TextBlock } from "./TextBlock"; 18 21 import { Popover } from "components/Popover"; 19 22 import { theme } from "tailwind.config"; 20 23 import { ImageAltSmall } from "components/Icons/ImageAlt"; 21 - import { codeToHtml } from "shiki"; 22 - import Katex from "katex"; 23 24 import { StaticMathBlock } from "./StaticMathBlock"; 24 25 import { PubCodeBlock } from "./PubCodeBlock"; 25 26 import { AppBskyFeedDefs } from "@atproto/api"; 26 27 import { PubBlueskyPostBlock } from "./PublishBskyPostBlock"; 28 + import { openPage, usePostPageUIState } from "./PostPage"; 27 29 28 30 export function PostContent({ 29 31 blocks, ··· 32 34 className, 33 35 prerenderedCodeBlocks, 34 36 bskyPostData, 37 + pageId, 35 38 }: { 36 39 blocks: PubLeafletPagesLinearDocument.Block[]; 40 + pageId?: string; 37 41 did: string; 38 42 preview?: boolean; 39 43 className?: string; ··· 48 52 {blocks.map((b, index) => { 49 53 return ( 50 54 <Block 55 + pageId={pageId} 51 56 bskyPostData={bskyPostData} 52 57 block={b} 53 58 did={did} ··· 72 77 previousBlock, 73 78 prerenderedCodeBlocks, 74 79 bskyPostData, 80 + pageId, 75 81 }: { 82 + pageId?: string; 76 83 preview?: boolean; 77 84 index: number[]; 78 85 block: PubLeafletPagesLinearDocument.Block; ··· 114 121 `; 115 122 116 123 switch (true) { 124 + case PubLeafletBlocksPage.isMain(b.block): { 125 + let id = b.block.id; 126 + return ( 127 + <div 128 + onClick={() => { 129 + openPage(pageId, id); 130 + }} 131 + > 132 + ITS A BLOCK 133 + </div> 134 + ); 135 + } 117 136 case PubLeafletBlocksBskyPost.isMain(b.block): { 118 137 let uri = b.block.postRef.uri; 119 138 let post = bskyPostData.find((p) => p.uri === uri);
+55 -1
app/lish/[did]/[publication]/[rkey]/PostPage.tsx
··· 1 1 "use client"; 2 2 import { 3 + PubLeafletDocument, 3 4 PubLeafletPagesLinearDocument, 4 5 PubLeafletPublication, 5 6 } from "lexicons/api"; ··· 13 14 import { PostHeader } from "./PostHeader/PostHeader"; 14 15 import { useIdentityData } from "components/IdentityProvider"; 15 16 import { AppBskyFeedDefs } from "@atproto/api"; 17 + import { create } from "zustand/react"; 18 + export const usePostPageUIState = create(() => ({ 19 + pages: [] as string[], 20 + })); 21 + 22 + export const openPage = (parent: string | undefined, page: string) => 23 + usePostPageUIState.setState((state) => { 24 + let parentPosition = state.pages.findIndex((s) => s == parent); 25 + return { 26 + pages: 27 + parentPosition === -1 28 + ? [page] 29 + : [...state.pages.slice(0, parentPosition + 1), page], 30 + }; 31 + }); 32 + 33 + export const closePage = (page: string) => 34 + usePostPageUIState.setState((state) => { 35 + let parentPosition = state.pages.findIndex((s) => s == page); 36 + return { 37 + pages: state.pages.slice(0, parentPosition), 38 + }; 39 + }); 16 40 17 41 export function PostPage({ 18 42 document, ··· 37 61 }) { 38 62 let { identity } = useIdentityData(); 39 63 let { drawerOpen } = useInteractionState(); 64 + let pages = usePostPageUIState((s) => s.pages); 40 65 if (!document || !document.documents_in_publications[0].publications) 41 66 return null; 42 67 ··· 87 112 document.documents_in_publications[0]?.publications 88 113 ?.identity_did ? ( 89 114 <a 90 - href={`https://leaflet.pub/${document.leaflets_in_publications[0].leaflet}`} 115 + href={`https://leaflet.pub/${document.leaflets_in_publications[0]?.leaflet}`} 91 116 className="flex gap-2 items-center hover:!no-underline selected-outline px-2 py-0.5 bg-accent-1 text-accent-2 font-bold w-fit rounded-lg !border-accent-1 !outline-accent-1 mx-auto" 92 117 > 93 118 <EditTiny /> Edit Post ··· 108 133 )} 109 134 </div> 110 135 </div> 136 + {pages.map((p) => { 137 + let record = document.data as PubLeafletDocument.Record; 138 + let page = record.pages.find( 139 + (page) => (page as PubLeafletPagesLinearDocument.Main).id === p, 140 + ) as PubLeafletPagesLinearDocument.Main | undefined; 141 + if (!page) return null; 142 + return ( 143 + <div 144 + key={page.id} 145 + id="post-page" 146 + className={`postPageWrapper relative overflow-y-auto sm:mx-0 mx-[6px] w-full 147 + ${drawerOpen || hasPageBackground ? "max-w-[var(--page-width-units)] shrink-0 snap-center " : "w-full"} 148 + ${ 149 + hasPageBackground 150 + ? "h-full bg-[rgba(var(--bg-page),var(--bg-page-alpha))] rounded-lg border border-border " 151 + : "sm:h-[calc(100%+48px)] h-[calc(100%+24px)] sm:-my-6 -my-3 " 152 + }`} 153 + > 154 + <button onClick={() => closePage(page?.id!)}>close</button> 155 + <PostContent 156 + pageId={page.id} 157 + bskyPostData={bskyPostData} 158 + blocks={page.blocks} 159 + did={did} 160 + prerenderedCodeBlocks={prerenderedCodeBlocks} 161 + /> 162 + </div> 163 + ); 164 + })} 111 165 </> 112 166 ); 113 167 }
-1
components/Blocks/BlockCommands.tsx
··· 330 330 name: "New Page", 331 331 icon: <BlockDocPageSmall />, 332 332 type: "page", 333 - hiddenInPublication: true, 334 333 onSelect: async (rep, props, um) => { 335 334 props.entityID && clearCommandSearchText(props.entityID); 336 335 let entity = await createBlockWithType(rep, props, "card");
+3 -1
components/Pages/index.tsx
··· 144 144 </div> 145 145 )} 146 146 147 + {props.first && ( 148 + <PublicationMetadata cardBorderHidden={!!cardBorderHidden} /> 149 + )} 147 150 <PageContent entityID={props.entityID} /> 148 151 </div> 149 152 <Media mobile={false}> ··· 233 236 }} 234 237 /> 235 238 ) : null} 236 - <PublicationMetadata cardBorderHidden={!!cardBorderHidden} /> 237 239 <Blocks entityID={props.entityID} /> 238 240 {/* we handle page bg in this sepate div so that 239 241 we can apply an opacity the background image
+2
lexicons/api/index.ts
··· 16 16 import * as PubLeafletBlocksIframe from './types/pub/leaflet/blocks/iframe' 17 17 import * as PubLeafletBlocksImage from './types/pub/leaflet/blocks/image' 18 18 import * as PubLeafletBlocksMath from './types/pub/leaflet/blocks/math' 19 + import * as PubLeafletBlocksPage from './types/pub/leaflet/blocks/page' 19 20 import * as PubLeafletBlocksText from './types/pub/leaflet/blocks/text' 20 21 import * as PubLeafletBlocksUnorderedList from './types/pub/leaflet/blocks/unorderedList' 21 22 import * as PubLeafletBlocksWebsite from './types/pub/leaflet/blocks/website' ··· 50 51 export * as PubLeafletBlocksIframe from './types/pub/leaflet/blocks/iframe' 51 52 export * as PubLeafletBlocksImage from './types/pub/leaflet/blocks/image' 52 53 export * as PubLeafletBlocksMath from './types/pub/leaflet/blocks/math' 54 + export * as PubLeafletBlocksPage from './types/pub/leaflet/blocks/page' 53 55 export * as PubLeafletBlocksText from './types/pub/leaflet/blocks/text' 54 56 export * as PubLeafletBlocksUnorderedList from './types/pub/leaflet/blocks/unorderedList' 55 57 export * as PubLeafletBlocksWebsite from './types/pub/leaflet/blocks/website'
+21
lexicons/api/lexicons.ts
··· 405 405 }, 406 406 }, 407 407 }, 408 + PubLeafletBlocksPage: { 409 + lexicon: 1, 410 + id: 'pub.leaflet.blocks.page', 411 + defs: { 412 + main: { 413 + type: 'object', 414 + required: ['id'], 415 + properties: { 416 + id: { 417 + type: 'string', 418 + }, 419 + }, 420 + }, 421 + }, 422 + }, 408 423 PubLeafletBlocksText: { 409 424 lexicon: 1, 410 425 id: 'pub.leaflet.blocks.text', ··· 521 536 defs: { 522 537 main: { 523 538 type: 'object', 539 + required: ['blocks'], 524 540 properties: { 541 + id: { 542 + type: 'string', 543 + }, 525 544 blocks: { 526 545 type: 'array', 527 546 items: { ··· 549 568 'lex:pub.leaflet.blocks.code', 550 569 'lex:pub.leaflet.blocks.horizontalRule', 551 570 'lex:pub.leaflet.blocks.bskyPost', 571 + 'lex:pub.leaflet.blocks.page', 552 572 ], 553 573 }, 554 574 alignment: { ··· 1835 1855 PubLeafletBlocksIframe: 'pub.leaflet.blocks.iframe', 1836 1856 PubLeafletBlocksImage: 'pub.leaflet.blocks.image', 1837 1857 PubLeafletBlocksMath: 'pub.leaflet.blocks.math', 1858 + PubLeafletBlocksPage: 'pub.leaflet.blocks.page', 1838 1859 PubLeafletBlocksText: 'pub.leaflet.blocks.text', 1839 1860 PubLeafletBlocksUnorderedList: 'pub.leaflet.blocks.unorderedList', 1840 1861 PubLeafletBlocksWebsite: 'pub.leaflet.blocks.website',
+4 -1
lexicons/api/types/pub/leaflet/pages/linearDocument.ts
··· 16 16 import type * as PubLeafletBlocksCode from '../blocks/code' 17 17 import type * as PubLeafletBlocksHorizontalRule from '../blocks/horizontalRule' 18 18 import type * as PubLeafletBlocksBskyPost from '../blocks/bskyPost' 19 + import type * as PubLeafletBlocksPage from '../blocks/page' 19 20 20 21 const is$typed = _is$typed, 21 22 validate = _validate ··· 23 24 24 25 export interface Main { 25 26 $type?: 'pub.leaflet.pages.linearDocument' 26 - blocks?: Block[] 27 + id?: string 28 + blocks: Block[] 27 29 } 28 30 29 31 const hashMain = 'main' ··· 50 52 | $Typed<PubLeafletBlocksCode.Main> 51 53 | $Typed<PubLeafletBlocksHorizontalRule.Main> 52 54 | $Typed<PubLeafletBlocksBskyPost.Main> 55 + | $Typed<PubLeafletBlocksPage.Main> 53 56 | { $type: string } 54 57 alignment?: 55 58 | 'lex:pub.leaflet.pages.linearDocument#textAlignLeft'
+8 -1
lexicons/pub/leaflet/pages/linearDocument.json
··· 4 4 "defs": { 5 5 "main": { 6 6 "type": "object", 7 + "required": [ 8 + "blocks" 9 + ], 7 10 "properties": { 11 + "id": { 12 + "type": "string" 13 + }, 8 14 "blocks": { 9 15 "type": "array", 10 16 "items": { ··· 33 39 "pub.leaflet.blocks.math", 34 40 "pub.leaflet.blocks.code", 35 41 "pub.leaflet.blocks.horizontalRule", 36 - "pub.leaflet.blocks.bskyPost" 42 + "pub.leaflet.blocks.bskyPost", 43 + "pub.leaflet.blocks.page" 37 44 ] 38 45 }, 39 46 "alignment": {
+15
lexicons/src/blocks.ts
··· 19 19 }, 20 20 }; 21 21 22 + export const PubLeafletBlocksPage: LexiconDoc = { 23 + lexicon: 1, 24 + id: "pub.leaflet.blocks.page", 25 + defs: { 26 + main: { 27 + type: "object", 28 + required: ["id"], 29 + properties: { 30 + id: { type: "string" }, 31 + }, 32 + }, 33 + }, 34 + }; 35 + 22 36 export const PubLeafletBlocksBskyPost: LexiconDoc = { 23 37 lexicon: 1, 24 38 id: "pub.leaflet.blocks.bskyPost", ··· 232 246 PubLeafletBlocksCode, 233 247 PubLeafletBlocksHorizontalRule, 234 248 PubLeafletBlocksBskyPost, 249 + PubLeafletBlocksPage, 235 250 ]; 236 251 export const BlockUnion: LexRefUnion = { 237 252 type: "union",
+2
lexicons/src/pages/LinearDocument.ts
··· 7 7 defs: { 8 8 main: { 9 9 type: "object", 10 + required: ["blocks"], 10 11 properties: { 12 + id: { type: "string" }, 11 13 blocks: { type: "array", items: { type: "ref", ref: "#block" } }, 12 14 }, 13 15 },