a tool for shared writing and social publishing
at update/looseleafs 727 lines 24 kB view raw
1"use server"; 2 3import * as Y from "yjs"; 4import * as base64 from "base64-js"; 5import { createOauthClient } from "src/atproto-oauth"; 6import { getIdentityData } from "actions/getIdentityData"; 7import { 8 AtpBaseClient, 9 PubLeafletBlocksHeader, 10 PubLeafletBlocksImage, 11 PubLeafletBlocksText, 12 PubLeafletBlocksUnorderedList, 13 PubLeafletDocument, 14 PubLeafletPagesLinearDocument, 15 PubLeafletPagesCanvas, 16 PubLeafletRichtextFacet, 17 PubLeafletBlocksWebsite, 18 PubLeafletBlocksCode, 19 PubLeafletBlocksMath, 20 PubLeafletBlocksHorizontalRule, 21 PubLeafletBlocksBskyPost, 22 PubLeafletBlocksBlockquote, 23 PubLeafletBlocksIframe, 24 PubLeafletBlocksPage, 25 PubLeafletBlocksPoll, 26 PubLeafletBlocksButton, 27 PubLeafletPollDefinition, 28} from "lexicons/api"; 29import { Block } from "components/Blocks/Block"; 30import { TID } from "@atproto/common"; 31import { supabaseServerClient } from "supabase/serverClient"; 32import { scanIndexLocal } from "src/replicache/utils"; 33import type { Fact } from "src/replicache"; 34import type { Attribute } from "src/replicache/attributes"; 35import { 36 Delta, 37 YJSFragmentToString, 38} from "components/Blocks/TextBlock/RenderYJSFragment"; 39import { ids } from "lexicons/api/lexicons"; 40import { BlobRef } from "@atproto/lexicon"; 41import { AtUri } from "@atproto/syntax"; 42import { Json } from "supabase/database.types"; 43import { $Typed, UnicodeString } from "@atproto/api"; 44import { List, parseBlocksToList } from "src/utils/parseBlocksToList"; 45import { getBlocksWithTypeLocal } from "src/hooks/queries/useBlocks"; 46import { Lock } from "src/utils/lock"; 47import type { PubLeafletPublication } from "lexicons/api"; 48import { 49 ColorToRGB, 50 ColorToRGBA, 51} from "components/ThemeManager/colorToLexicons"; 52import { parseColor } from "@react-stately/color"; 53 54export async function publishToPublication({ 55 root_entity, 56 publication_uri, 57 leaflet_id, 58 title, 59 description, 60 entitiesToDelete, 61}: { 62 root_entity: string; 63 publication_uri?: string; 64 leaflet_id: string; 65 title?: string; 66 description?: string; 67 entitiesToDelete?: string[]; 68}) { 69 const oauthClient = await createOauthClient(); 70 let identity = await getIdentityData(); 71 if (!identity || !identity.atp_did) throw new Error("No Identity"); 72 73 let credentialSession = await oauthClient.restore(identity.atp_did); 74 let agent = new AtpBaseClient( 75 credentialSession.fetchHandler.bind(credentialSession), 76 ); 77 78 // Check if we're publishing to a publication or standalone 79 let draft: any = null; 80 let existingDocUri: string | null = null; 81 82 if (publication_uri) { 83 // Publishing to a publication - use leaflets_in_publications 84 let { data, error } = await supabaseServerClient 85 .from("publications") 86 .select("*, leaflets_in_publications(*, documents(*))") 87 .eq("uri", publication_uri) 88 .eq("leaflets_in_publications.leaflet", leaflet_id) 89 .single(); 90 console.log(error); 91 92 if (!data || identity.atp_did !== data?.identity_did) 93 throw new Error("No draft or not publisher"); 94 draft = data.leaflets_in_publications[0]; 95 existingDocUri = draft?.doc; 96 } else { 97 // Publishing standalone - use leaflets_to_documents 98 let { data } = await supabaseServerClient 99 .from("leaflets_to_documents") 100 .select("*, documents(*)") 101 .eq("leaflet", leaflet_id) 102 .single(); 103 draft = data; 104 existingDocUri = draft?.document; 105 } 106 107 // Heuristic: Remove title entities if this is the first time publishing 108 // (when coming from a standalone leaflet with entitiesToDelete passed in) 109 if (entitiesToDelete && entitiesToDelete.length > 0 && !existingDocUri) { 110 await supabaseServerClient 111 .from("entities") 112 .delete() 113 .in("id", entitiesToDelete); 114 } 115 116 let { data } = await supabaseServerClient.rpc("get_facts", { 117 root: root_entity, 118 }); 119 let facts = (data as unknown as Fact<Attribute>[]) || []; 120 121 let { pages } = await processBlocksToPages( 122 facts, 123 agent, 124 root_entity, 125 credentialSession.did!, 126 ); 127 128 let existingRecord = 129 (draft?.documents?.data as PubLeafletDocument.Record | undefined) || {}; 130 131 // Extract theme for standalone documents (not for publications) 132 let theme: PubLeafletPublication.Theme | undefined; 133 if (!publication_uri) { 134 theme = await extractThemeFromFacts(facts, root_entity, agent); 135 } 136 137 let record: PubLeafletDocument.Record = { 138 publishedAt: new Date().toISOString(), 139 ...existingRecord, 140 $type: "pub.leaflet.document", 141 author: credentialSession.did!, 142 ...(publication_uri && { publication: publication_uri }), 143 ...(theme && { theme }), 144 title: title || "Untitled", 145 description: description || "", 146 pages: pages.map((p) => { 147 if (p.type === "canvas") { 148 return { 149 $type: "pub.leaflet.pages.canvas" as const, 150 id: p.id, 151 blocks: p.blocks as PubLeafletPagesCanvas.Block[], 152 }; 153 } else { 154 return { 155 $type: "pub.leaflet.pages.linearDocument" as const, 156 id: p.id, 157 blocks: p.blocks as PubLeafletPagesLinearDocument.Block[], 158 }; 159 } 160 }), 161 }; 162 163 // Keep the same rkey if updating an existing document 164 let rkey = existingDocUri ? new AtUri(existingDocUri).rkey : TID.nextStr(); 165 let { data: result } = await agent.com.atproto.repo.putRecord({ 166 rkey, 167 repo: credentialSession.did!, 168 collection: record.$type, 169 record, 170 validate: false, //TODO publish the lexicon so we can validate! 171 }); 172 173 // Optimistically create database entries 174 await supabaseServerClient.from("documents").upsert({ 175 uri: result.uri, 176 data: record as Json, 177 }); 178 179 if (publication_uri) { 180 // Publishing to a publication - update both tables 181 await Promise.all([ 182 supabaseServerClient.from("documents_in_publications").upsert({ 183 publication: publication_uri, 184 document: result.uri, 185 }), 186 supabaseServerClient.from("leaflets_in_publications").upsert({ 187 doc: result.uri, 188 leaflet: leaflet_id, 189 publication: publication_uri, 190 title: title, 191 description: description, 192 }), 193 ]); 194 } else { 195 // Publishing standalone - update leaflets_to_documents 196 await supabaseServerClient.from("leaflets_to_documents").upsert({ 197 leaflet: leaflet_id, 198 document: result.uri, 199 title: title || "Untitled", 200 description: description || "", 201 }); 202 203 // Heuristic: Remove title entities if this is the first time publishing standalone 204 // (when entitiesToDelete is provided and there's no existing document) 205 if (entitiesToDelete && entitiesToDelete.length > 0 && !existingDocUri) { 206 await supabaseServerClient 207 .from("entities") 208 .delete() 209 .in("id", entitiesToDelete); 210 } 211 } 212 213 return { rkey, record: JSON.parse(JSON.stringify(record)) }; 214} 215 216async function processBlocksToPages( 217 facts: Fact<any>[], 218 agent: AtpBaseClient, 219 root_entity: string, 220 did: string, 221) { 222 let scan = scanIndexLocal(facts); 223 let pages: { 224 id: string; 225 blocks: 226 | PubLeafletPagesLinearDocument.Block[] 227 | PubLeafletPagesCanvas.Block[]; 228 type: "doc" | "canvas"; 229 }[] = []; 230 231 // Create a lock to serialize image uploads 232 const uploadLock = new Lock(); 233 234 let firstEntity = scan.eav(root_entity, "root/page")?.[0]; 235 if (!firstEntity) throw new Error("No root page"); 236 237 // Check if the first page is a canvas or linear document 238 let [pageType] = scan.eav(firstEntity.data.value, "page/type"); 239 240 if (pageType?.data.value === "canvas") { 241 // First page is a canvas 242 let canvasBlocks = await canvasBlocksToRecord(firstEntity.data.value, did); 243 pages.unshift({ 244 id: firstEntity.data.value, 245 blocks: canvasBlocks, 246 type: "canvas", 247 }); 248 } else { 249 // First page is a linear document 250 let blocks = getBlocksWithTypeLocal(facts, firstEntity?.data.value); 251 let b = await blocksToRecord(blocks, did); 252 pages.unshift({ 253 id: firstEntity.data.value, 254 blocks: b, 255 type: "doc", 256 }); 257 } 258 259 return { pages }; 260 261 async function uploadImage(src: string) { 262 let data = await fetch(src); 263 if (data.status !== 200) return; 264 let binary = await data.blob(); 265 return uploadLock.withLock(async () => { 266 let blob = await agent.com.atproto.repo.uploadBlob(binary, { 267 headers: { "Content-Type": binary.type }, 268 }); 269 return blob.data.blob; 270 }); 271 } 272 async function blocksToRecord( 273 blocks: Block[], 274 did: string, 275 ): Promise<PubLeafletPagesLinearDocument.Block[]> { 276 let parsedBlocks = parseBlocksToList(blocks); 277 return ( 278 await Promise.all( 279 parsedBlocks.map(async (blockOrList) => { 280 if (blockOrList.type === "block") { 281 let alignmentValue = scan.eav( 282 blockOrList.block.value, 283 "block/text-alignment", 284 )[0]?.data.value; 285 let alignment: ExcludeString< 286 PubLeafletPagesLinearDocument.Block["alignment"] 287 > = 288 alignmentValue === "center" 289 ? "lex:pub.leaflet.pages.linearDocument#textAlignCenter" 290 : alignmentValue === "right" 291 ? "lex:pub.leaflet.pages.linearDocument#textAlignRight" 292 : alignmentValue === "justify" 293 ? "lex:pub.leaflet.pages.linearDocument#textAlignJustify" 294 : alignmentValue === "left" 295 ? "lex:pub.leaflet.pages.linearDocument#textAlignLeft" 296 : undefined; 297 let b = await blockToRecord(blockOrList.block, did); 298 if (!b) return []; 299 let block: PubLeafletPagesLinearDocument.Block = { 300 $type: "pub.leaflet.pages.linearDocument#block", 301 alignment, 302 block: b, 303 }; 304 return [block]; 305 } else { 306 let block: PubLeafletPagesLinearDocument.Block = { 307 $type: "pub.leaflet.pages.linearDocument#block", 308 block: { 309 $type: "pub.leaflet.blocks.unorderedList", 310 children: await childrenToRecord(blockOrList.children, did), 311 }, 312 }; 313 return [block]; 314 } 315 }), 316 ) 317 ).flat(); 318 } 319 320 async function childrenToRecord(children: List[], did: string) { 321 return ( 322 await Promise.all( 323 children.map(async (child) => { 324 let content = await blockToRecord(child.block, did); 325 if (!content) return []; 326 let record: PubLeafletBlocksUnorderedList.ListItem = { 327 $type: "pub.leaflet.blocks.unorderedList#listItem", 328 content, 329 children: await childrenToRecord(child.children, did), 330 }; 331 return record; 332 }), 333 ) 334 ).flat(); 335 } 336 async function blockToRecord(b: Block, did: string) { 337 const getBlockContent = (b: string) => { 338 let [content] = scan.eav(b, "block/text"); 339 if (!content) return ["", [] as PubLeafletRichtextFacet.Main[]] as const; 340 let doc = new Y.Doc(); 341 const update = base64.toByteArray(content.data.value); 342 Y.applyUpdate(doc, update); 343 let nodes = doc.getXmlElement("prosemirror").toArray(); 344 let stringValue = YJSFragmentToString(nodes[0]); 345 let facets = YJSFragmentToFacets(nodes[0]); 346 return [stringValue, facets] as const; 347 }; 348 if (b.type === "card") { 349 let [page] = scan.eav(b.value, "block/card"); 350 if (!page) return; 351 let [pageType] = scan.eav(page.data.value, "page/type"); 352 353 if (pageType?.data.value === "canvas") { 354 let canvasBlocks = await canvasBlocksToRecord(page.data.value, did); 355 pages.push({ 356 id: page.data.value, 357 blocks: canvasBlocks, 358 type: "canvas", 359 }); 360 } else { 361 let blocks = getBlocksWithTypeLocal(facts, page.data.value); 362 pages.push({ 363 id: page.data.value, 364 blocks: await blocksToRecord(blocks, did), 365 type: "doc", 366 }); 367 } 368 369 let block: $Typed<PubLeafletBlocksPage.Main> = { 370 $type: "pub.leaflet.blocks.page", 371 id: page.data.value, 372 }; 373 return block; 374 } 375 376 if (b.type === "bluesky-post") { 377 let [post] = scan.eav(b.value, "block/bluesky-post"); 378 if (!post || !post.data.value.post) return; 379 let block: $Typed<PubLeafletBlocksBskyPost.Main> = { 380 $type: ids.PubLeafletBlocksBskyPost, 381 postRef: { 382 uri: post.data.value.post.uri, 383 cid: post.data.value.post.cid, 384 }, 385 }; 386 return block; 387 } 388 if (b.type === "horizontal-rule") { 389 let block: $Typed<PubLeafletBlocksHorizontalRule.Main> = { 390 $type: ids.PubLeafletBlocksHorizontalRule, 391 }; 392 return block; 393 } 394 395 if (b.type === "heading") { 396 let [headingLevel] = scan.eav(b.value, "block/heading-level"); 397 398 let [stringValue, facets] = getBlockContent(b.value); 399 let block: $Typed<PubLeafletBlocksHeader.Main> = { 400 $type: "pub.leaflet.blocks.header", 401 level: headingLevel?.data.value || 1, 402 plaintext: stringValue, 403 facets, 404 }; 405 return block; 406 } 407 408 if (b.type === "blockquote") { 409 let [stringValue, facets] = getBlockContent(b.value); 410 let block: $Typed<PubLeafletBlocksBlockquote.Main> = { 411 $type: ids.PubLeafletBlocksBlockquote, 412 plaintext: stringValue, 413 facets, 414 }; 415 return block; 416 } 417 418 if (b.type == "text") { 419 let [stringValue, facets] = getBlockContent(b.value); 420 let block: $Typed<PubLeafletBlocksText.Main> = { 421 $type: ids.PubLeafletBlocksText, 422 plaintext: stringValue, 423 facets, 424 }; 425 return block; 426 } 427 if (b.type === "embed") { 428 let [url] = scan.eav(b.value, "embed/url"); 429 let [height] = scan.eav(b.value, "embed/height"); 430 if (!url) return; 431 let block: $Typed<PubLeafletBlocksIframe.Main> = { 432 $type: "pub.leaflet.blocks.iframe", 433 url: url.data.value, 434 height: height?.data.value || 600, 435 }; 436 return block; 437 } 438 if (b.type == "image") { 439 let [image] = scan.eav(b.value, "block/image"); 440 if (!image) return; 441 let [altText] = scan.eav(b.value, "image/alt"); 442 let blobref = await uploadImage(image.data.src); 443 if (!blobref) return; 444 let block: $Typed<PubLeafletBlocksImage.Main> = { 445 $type: "pub.leaflet.blocks.image", 446 image: blobref, 447 aspectRatio: { 448 height: image.data.height, 449 width: image.data.width, 450 }, 451 alt: altText ? altText.data.value : undefined, 452 }; 453 return block; 454 } 455 if (b.type === "link") { 456 let [previewImage] = scan.eav(b.value, "link/preview"); 457 let [description] = scan.eav(b.value, "link/description"); 458 let [src] = scan.eav(b.value, "link/url"); 459 if (!src) return; 460 let blobref = previewImage 461 ? await uploadImage(previewImage?.data.src) 462 : undefined; 463 let [title] = scan.eav(b.value, "link/title"); 464 let block: $Typed<PubLeafletBlocksWebsite.Main> = { 465 $type: "pub.leaflet.blocks.website", 466 previewImage: blobref, 467 src: src.data.value, 468 description: description?.data.value, 469 title: title?.data.value, 470 }; 471 return block; 472 } 473 if (b.type === "code") { 474 let [language] = scan.eav(b.value, "block/code-language"); 475 let [code] = scan.eav(b.value, "block/code"); 476 let [theme] = scan.eav(root_entity, "theme/code-theme"); 477 let block: $Typed<PubLeafletBlocksCode.Main> = { 478 $type: "pub.leaflet.blocks.code", 479 language: language?.data.value, 480 plaintext: code?.data.value || "", 481 syntaxHighlightingTheme: theme?.data.value, 482 }; 483 return block; 484 } 485 if (b.type === "math") { 486 let [math] = scan.eav(b.value, "block/math"); 487 let block: $Typed<PubLeafletBlocksMath.Main> = { 488 $type: "pub.leaflet.blocks.math", 489 tex: math?.data.value || "", 490 }; 491 return block; 492 } 493 if (b.type === "poll") { 494 // Get poll options from the entity 495 let pollOptions = scan.eav(b.value, "poll/options"); 496 let options: PubLeafletPollDefinition.Option[] = pollOptions.map( 497 (opt) => { 498 let optionName = scan.eav(opt.data.value, "poll-option/name")?.[0]; 499 return { 500 $type: "pub.leaflet.poll.definition#option", 501 text: optionName?.data.value || "", 502 }; 503 }, 504 ); 505 506 // Create the poll definition record 507 let pollRecord: PubLeafletPollDefinition.Record = { 508 $type: "pub.leaflet.poll.definition", 509 name: "Poll", // Default name, can be customized 510 options, 511 }; 512 513 // Upload the poll record 514 let { data: pollResult } = await agent.com.atproto.repo.putRecord({ 515 //use the entity id as the rkey so we can associate it in the editor 516 rkey: b.value, 517 repo: did, 518 collection: pollRecord.$type, 519 record: pollRecord, 520 validate: false, 521 }); 522 523 // Optimistically write poll definition to database 524 console.log( 525 await supabaseServerClient.from("atp_poll_records").upsert({ 526 uri: pollResult.uri, 527 cid: pollResult.cid, 528 record: pollRecord as Json, 529 }), 530 ); 531 532 // Return a poll block with reference to the poll record 533 let block: $Typed<PubLeafletBlocksPoll.Main> = { 534 $type: "pub.leaflet.blocks.poll", 535 pollRef: { 536 uri: pollResult.uri, 537 cid: pollResult.cid, 538 }, 539 }; 540 return block; 541 } 542 if (b.type === "button") { 543 let [text] = scan.eav(b.value, "button/text"); 544 let [url] = scan.eav(b.value, "button/url"); 545 if (!text || !url) return; 546 let block: $Typed<PubLeafletBlocksButton.Main> = { 547 $type: "pub.leaflet.blocks.button", 548 text: text.data.value, 549 url: url.data.value, 550 }; 551 return block; 552 } 553 return; 554 } 555 556 async function canvasBlocksToRecord( 557 pageID: string, 558 did: string, 559 ): Promise<PubLeafletPagesCanvas.Block[]> { 560 let canvasBlocks = scan.eav(pageID, "canvas/block"); 561 return ( 562 await Promise.all( 563 canvasBlocks.map(async (canvasBlock) => { 564 let blockEntity = canvasBlock.data.value; 565 let position = canvasBlock.data.position; 566 567 // Get the block content 568 let blockType = scan.eav(blockEntity, "block/type")?.[0]; 569 if (!blockType) return null; 570 571 let block: Block = { 572 type: blockType.data.value, 573 value: blockEntity, 574 parent: pageID, 575 position: "", 576 factID: canvasBlock.id, 577 }; 578 579 let content = await blockToRecord(block, did); 580 if (!content) return null; 581 582 // Get canvas-specific properties 583 let width = 584 scan.eav(blockEntity, "canvas/block/width")?.[0]?.data.value || 360; 585 let rotation = scan.eav(blockEntity, "canvas/block/rotation")?.[0] 586 ?.data.value; 587 588 let canvasBlockRecord: PubLeafletPagesCanvas.Block = { 589 $type: "pub.leaflet.pages.canvas#block", 590 block: content, 591 x: Math.floor(position.x), 592 y: Math.floor(position.y), 593 width: Math.floor(width), 594 ...(rotation !== undefined && { rotation: Math.floor(rotation) }), 595 }; 596 597 return canvasBlockRecord; 598 }), 599 ) 600 ).filter((b): b is PubLeafletPagesCanvas.Block => b !== null); 601 } 602} 603 604function YJSFragmentToFacets( 605 node: Y.XmlElement | Y.XmlText | Y.XmlHook, 606): PubLeafletRichtextFacet.Main[] { 607 if (node.constructor === Y.XmlElement) { 608 return node 609 .toArray() 610 .map((f) => YJSFragmentToFacets(f)) 611 .flat(); 612 } 613 if (node.constructor === Y.XmlText) { 614 let facets: PubLeafletRichtextFacet.Main[] = []; 615 let delta = node.toDelta() as Delta[]; 616 let byteStart = 0; 617 for (let d of delta) { 618 let unicodestring = new UnicodeString(d.insert); 619 let facet: PubLeafletRichtextFacet.Main = { 620 index: { 621 byteStart, 622 byteEnd: byteStart + unicodestring.length, 623 }, 624 features: [], 625 }; 626 627 if (d.attributes?.strikethrough) 628 facet.features.push({ 629 $type: "pub.leaflet.richtext.facet#strikethrough", 630 }); 631 632 if (d.attributes?.code) 633 facet.features.push({ $type: "pub.leaflet.richtext.facet#code" }); 634 if (d.attributes?.highlight) 635 facet.features.push({ $type: "pub.leaflet.richtext.facet#highlight" }); 636 if (d.attributes?.underline) 637 facet.features.push({ $type: "pub.leaflet.richtext.facet#underline" }); 638 if (d.attributes?.strong) 639 facet.features.push({ $type: "pub.leaflet.richtext.facet#bold" }); 640 if (d.attributes?.em) 641 facet.features.push({ $type: "pub.leaflet.richtext.facet#italic" }); 642 if (d.attributes?.link) 643 facet.features.push({ 644 $type: "pub.leaflet.richtext.facet#link", 645 uri: d.attributes.link.href, 646 }); 647 if (facet.features.length > 0) facets.push(facet); 648 byteStart += unicodestring.length; 649 } 650 return facets; 651 } 652 return []; 653} 654 655type ExcludeString<T> = T extends string 656 ? string extends T 657 ? never 658 : T /* maybe literal, not the whole `string` */ 659 : T; /* not a string */ 660 661async function extractThemeFromFacts( 662 facts: Fact<any>[], 663 root_entity: string, 664 agent: AtpBaseClient, 665): Promise<PubLeafletPublication.Theme | undefined> { 666 let scan = scanIndexLocal(facts); 667 let pageBackground = scan.eav(root_entity, "theme/page-background")?.[0]?.data 668 .value; 669 let cardBackground = scan.eav(root_entity, "theme/card-background")?.[0]?.data 670 .value; 671 let primary = scan.eav(root_entity, "theme/primary")?.[0]?.data.value; 672 let accentBackground = scan.eav(root_entity, "theme/accent-background")?.[0] 673 ?.data.value; 674 let accentText = scan.eav(root_entity, "theme/accent-text")?.[0]?.data.value; 675 let showPageBackground = !scan.eav( 676 root_entity, 677 "theme/card-border-hidden", 678 )?.[0]?.data.value; 679 let backgroundImage = scan.eav(root_entity, "theme/background-image")?.[0]; 680 let backgroundImageRepeat = scan.eav( 681 root_entity, 682 "theme/background-image-repeat", 683 )?.[0]; 684 685 let theme: PubLeafletPublication.Theme = { 686 showPageBackground: showPageBackground ?? true, 687 }; 688 689 if (pageBackground) 690 theme.backgroundColor = ColorToRGBA(parseColor(`hsba(${pageBackground})`)); 691 if (cardBackground) 692 theme.pageBackground = ColorToRGBA(parseColor(`hsba(${cardBackground})`)); 693 if (primary) theme.primary = ColorToRGB(parseColor(`hsba(${primary})`)); 694 if (accentBackground) 695 theme.accentBackground = ColorToRGB( 696 parseColor(`hsba(${accentBackground})`), 697 ); 698 if (accentText) 699 theme.accentText = ColorToRGB(parseColor(`hsba(${accentText})`)); 700 701 // Upload background image if present 702 if (backgroundImage?.data) { 703 let imageData = await fetch(backgroundImage.data.src); 704 if (imageData.status === 200) { 705 let binary = await imageData.blob(); 706 let blob = await agent.com.atproto.repo.uploadBlob(binary, { 707 headers: { "Content-Type": binary.type }, 708 }); 709 710 theme.backgroundImage = { 711 $type: "pub.leaflet.theme.backgroundImage", 712 image: blob.data.blob, 713 repeat: backgroundImageRepeat?.data.value ? true : false, 714 ...(backgroundImageRepeat?.data.value && { 715 width: backgroundImageRepeat.data.value, 716 }), 717 }; 718 } 719 } 720 721 // Only return theme if at least one property is set 722 if (Object.keys(theme).length > 1 || theme.showPageBackground !== true) { 723 return theme; 724 } 725 726 return undefined; 727}