a tool for shared writing and social publishing
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

handle copying sub-pages and images (#87)

* handle copying sub-pages and images

for images its easy, we just fetch and re-upload them

For subpages we get all their facts and stringify the array, base64
encode them and put them into a data attribute.

This is a bit jank, but should work in most places and won't create
anything wierd else where!

* handle adding link blocks

authored by

Jared Pereira and committed by
GitHub
686b17af f2afb9bc

+174 -14
+116 -4
components/Blocks/TextBlock/useHandlePaste.ts
··· 14 14 import { markdownToHtml } from "src/htmlMarkdownParsers"; 15 15 import { betterIsUrl, isUrl } from "src/utils/isURL"; 16 16 import { TextSelection } from "prosemirror-state"; 17 + import { FilterAttributes } from "src/replicache/attributes"; 18 + import { addLinkBlock } from "src/utils/addLinkBlock"; 17 19 18 20 const parser = ProsemirrorDOMParser.fromSchema(schema); 19 21 export const useHandlePaste = ( ··· 53 55 let children = flattenHTMLToTextBlocks(xml.body); 54 56 if ( 55 57 children.find((c) => 56 - ["P", "H1", "H2", "H3", "UL"].includes(c.tagName), 58 + ["P", "H1", "H2", "H3", "UL", "DIV", "IMG"].includes(c.tagName), 57 59 ) 58 60 ) { 59 61 children.forEach((child, index) => { ··· 183 185 type = "heading"; 184 186 break; 185 187 } 188 + case "DIV": { 189 + type = "card"; 190 + break; 191 + } 192 + case "IMG": { 193 + type = "image"; 194 + break; 195 + } 196 + case "A": { 197 + type = "link"; 198 + break; 199 + } 186 200 default: 187 201 type = null; 188 202 } ··· 215 229 }); 216 230 } 217 231 } 232 + if (child.tagName === "A") { 233 + let href = child.getAttribute("href"); 234 + if (href) { 235 + addLinkBlock(href, entityID, rep); 236 + } 237 + } 238 + if (child.tagName === "IMG") { 239 + let src = child.getAttribute("src"); 240 + if (src) { 241 + fetch(src) 242 + .then((res) => res.blob()) 243 + .then((Blob) => { 244 + const file = new File([Blob], "image.png", { type: Blob.type }); 245 + addImage(file, rep, { 246 + attribute: "block/image", 247 + entityID: entityID, 248 + }); 249 + }); 250 + } 251 + } 252 + 253 + if (child.tagName === "DIV" && child.getAttribute("data-entityID")) { 254 + let oldEntityID = child.getAttribute("data-entityID") as string; 255 + let factsData = child.getAttribute("data-facts"); 256 + if (factsData) { 257 + let facts = JSON.parse(atob(factsData)) as Fact<any>[]; 258 + 259 + let oldEntityIDToNewID = {} as { [k: string]: string }; 260 + let oldEntities = facts.reduce((acc, f) => { 261 + if (!acc.includes(f.entity)) acc.push(f.entity); 262 + return acc; 263 + }, [] as string[]); 264 + let newEntities = [] as string[]; 265 + for (let oldEntity of oldEntities) { 266 + let newEntity = v7(); 267 + oldEntityIDToNewID[oldEntity] = newEntity; 268 + newEntities.push(newEntity); 269 + } 270 + 271 + let newFacts = [] as Array< 272 + Pick<Fact<any>, "entity" | "attribute" | "data"> 273 + >; 274 + for (let fact of facts) { 275 + let entity = oldEntityIDToNewID[fact.entity]; 276 + let data = fact.data; 277 + if ( 278 + data.type === "ordered-reference" || 279 + data.type == "spatial-reference" || 280 + data.type === "reference" 281 + ) { 282 + data.value = oldEntityIDToNewID[data.value]; 283 + } 284 + if (data.type === "image") { 285 + //idk get it from the clipboard maybe? 286 + } 287 + newFacts.push({ entity, attribute: fact.attribute, data }); 288 + } 289 + rep.mutate.createEntity( 290 + newEntities.map((e) => ({ 291 + entityID: e, 292 + permission_set: entity_set.set, 293 + })), 294 + ); 295 + rep.mutate.assertFact(newFacts.filter((f) => f.data.type !== "image")); 296 + let newCardEntity = oldEntityIDToNewID[oldEntityID]; 297 + rep.mutate.assertFact({ 298 + entity: entityID, 299 + attribute: "block/card", 300 + data: { type: "reference", value: newCardEntity }, 301 + }); 302 + let images: Pick< 303 + Fact<keyof FilterAttributes<{ type: "image" }>>, 304 + "entity" | "data" | "attribute" 305 + >[] = newFacts.filter((f) => f.data.type === "image"); 306 + for (let image of images) { 307 + fetch(image.data.src) 308 + .then((res) => res.blob()) 309 + .then((Blob) => { 310 + const file = new File([Blob], "image.png", { type: Blob.type }); 311 + addImage(file, rep, { 312 + attribute: image.attribute, 313 + entityID: image.entity, 314 + }); 315 + }); 316 + } 317 + } 318 + } 218 319 219 320 if (child.tagName === "LI") { 220 321 let ul = Array.from(child.children).find((f) => f.tagName === "UL"); ··· 279 380 const elementNode = node as HTMLElement; 280 381 // Collect outer HTML for paragraph-like elements 281 382 if ( 282 - ["P", "H1", "H2", "H3", "H4", "H5", "H6", "LI", "UL"].includes( 283 - elementNode.tagName, 284 - ) 383 + [ 384 + "P", 385 + "H1", 386 + "H2", 387 + "H3", 388 + "H4", 389 + "H5", 390 + "H6", 391 + "LI", 392 + "UL", 393 + "IMG", 394 + "A", 395 + ].includes(elementNode.tagName) || 396 + elementNode.getAttribute("data-entityID") 285 397 ) { 286 398 htmlBlocks.push(elementNode); 287 399 } else {
+17 -6
src/replicache/mutations.ts
··· 308 308 } 309 309 }; 310 310 311 - const assertFact: Mutation< 312 - Omit<Fact<keyof typeof Attributes>, "id"> & { id?: string } 313 - > = async (args, ctx) => { 314 - await ctx.assertFact(args); 311 + type FactInput = Omit<Fact<keyof typeof Attributes>, "id"> & { id?: string }; 312 + const assertFact: Mutation<FactInput | Array<FactInput>> = async ( 313 + args, 314 + ctx, 315 + ) => { 316 + for (let f of [args].flat()) { 317 + await ctx.assertFact(f); 318 + } 315 319 }; 316 320 317 321 const increaseHeadingLevel: Mutation<{ entityID: string }> = async ( ··· 397 401 }); 398 402 }; 399 403 404 + const createEntity: Mutation< 405 + Array<{ entityID: string; permission_set: string }> 406 + > = async (args, ctx) => { 407 + for (let newentity of args) { 408 + await ctx.createEntity(newentity); 409 + } 410 + }; 411 + 400 412 const createDraft: Mutation<{ 401 413 mailboxEntity: string; 402 414 newEntity: string; ··· 502 514 | Array<keyof FilterAttributes<{ cardinality: "one" }>>; 503 515 }> = async (args, ctx) => { 504 516 for (let a of [args.attribute].flat()) { 505 - console.log(a); 506 517 let fact = (await ctx.scanIndex.eav(args.entity, a))[0]; 507 - console.log(fact); 508 518 if (fact) await ctx.retractFact(fact.id); 509 519 } 510 520 }; ··· 546 556 archiveDraft, 547 557 toggleTodoState, 548 558 createDraft, 559 + createEntity, 549 560 };
+5 -2
src/replicache/utils.ts
··· 42 42 } 43 43 44 44 export const scanIndex = (tx: ReadTransaction) => ({ 45 - async eav<A extends keyof typeof Attributes>(entity: string, attribute: A) { 45 + async eav<A extends keyof typeof Attributes>( 46 + entity: string, 47 + attribute: A | "", 48 + ) { 46 49 return ( 47 50 await tx 48 51 .scan<Fact<A>>({ indexName: "eav", prefix: `${entity}-${attribute}` }) 49 52 .toArray() 50 - ).filter((f) => f.attribute === attribute); 53 + ).filter((f) => attribute === "" || f.attribute === attribute); 51 54 }, 52 55 async vae< 53 56 A extends keyof FilterAttributes<{
+36 -2
src/utils/getBlocksAsHTML.tsx
··· 1 1 import { ReadTransaction, Replicache } from "replicache"; 2 - import { ReplicacheMutators } from "src/replicache"; 2 + import { Fact, ReplicacheMutators } from "src/replicache"; 3 3 import { scanIndex } from "src/replicache/utils"; 4 4 import { renderToStaticMarkup } from "react-dom/server"; 5 5 import * as Y from "yjs"; 6 6 import * as base64 from "base64-js"; 7 7 import { RenderYJSFragment } from "components/Blocks/TextBlock/RenderYJSFragment"; 8 8 import { Block } from "components/Blocks/Block"; 9 + import { getBlocksWithType } from "src/hooks/queries/useBlocks"; 9 10 10 11 export async function getBlocksAsHTML( 11 12 rep: Replicache<ReplicacheMutators>, ··· 45 46 }</li>`; 46 47 } 47 48 49 + async function getAllFacts( 50 + tx: ReadTransaction, 51 + entity: string, 52 + ): Promise<Array<Fact<any>>> { 53 + let facts = await scanIndex(tx).eav(entity, ""); 54 + let childFacts = ( 55 + await Promise.all( 56 + facts.map((f) => { 57 + if ( 58 + f.data.type === "reference" || 59 + f.data.type === "ordered-reference" || 60 + f.data.type === "spatial-reference" 61 + ) { 62 + return getAllFacts(tx, f.data.value); 63 + } 64 + return []; 65 + }), 66 + ) 67 + ).flat(); 68 + return [...facts, ...childFacts]; 69 + } 70 + 48 71 async function renderBlock( 49 72 b: Block, 50 73 tx: ReadTransaction, ··· 72 95 </a>, 73 96 ); 74 97 } 75 - if (b.type === "card" || b.type === "mailbox") { 98 + if (b.type === "card") { 99 + let [card] = await scanIndex(tx).eav(b.value, "block/card"); 100 + let facts = await getAllFacts(tx, card.data.value); 101 + return renderToStaticMarkup( 102 + <div 103 + data-type="card" 104 + data-facts={btoa(JSON.stringify(facts))} 105 + data-entityID={card.data.value} 106 + />, 107 + ); 108 + } 109 + if (b.type === "mailbox") { 76 110 return renderToStaticMarkup( 77 111 <div> 78 112 <a href={window.location.href} target="_blank">