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 pasting images

+128 -62
+27 -61
app/[doc_id]/Blocks.tsx
··· 4 4 import { TextBlock } from "../../components/TextBlock"; 5 5 import { generateKeyBetween } from "fractional-indexing"; 6 6 import { supabaseBrowserClient } from "../../supabase/browserClient"; 7 + import { useMemo } from "react"; 8 + import { addImage } from "../../utils/addImage"; 7 9 export function AddBlock(props: { entityID: string }) { 8 10 let rep = useReplicache(); 9 11 let blocks = useEntity(props.entityID, "card/block")?.sort((a, b) => { ··· 35 37 accept="image/*" 36 38 onChange={async (e) => { 37 39 let file = e.currentTarget.files?.[0]; 38 - if (!file) return; 39 - let client = supabaseBrowserClient(); 40 - let cache = await caches.open("minilink-user-assets"); 41 - let hash = await computeHash(file); 42 - let url = client.storage.from("minilink-user-assets").getPublicUrl(hash) 43 - .data.publicUrl; 44 - let dimensions = await getImageDimensions(file); 45 - await cache.put( 46 - url, 47 - new Response(file, { 48 - headers: { 49 - "Content-Type": file.type, 50 - "Content-Length": file.size.toString(), 51 - }, 52 - }), 53 - ); 54 - let newBlockEntity = crypto.randomUUID(); 55 - await rep?.rep?.mutate.addBlock({ 40 + if (!file || !rep?.rep) return; 41 + await addImage(file, rep.rep, { 56 42 parent: props.entityID, 57 43 position: generateKeyBetween(null, blocks[0]?.data.position || null), 58 - newEntityID: newBlockEntity, 59 - }); 60 - await rep?.rep?.mutate.assertFact({ 61 - entity: newBlockEntity, 62 - attribute: "block/image", 63 - data: { 64 - type: "image", 65 - src: url, 66 - height: dimensions.height, 67 - width: dimensions.width, 68 - }, 69 44 }); 70 - await client.storage.from("minilink-user-assets").upload(hash, file); 71 45 }} 72 46 /> 73 47 ); 74 48 } 75 49 76 - function getImageDimensions( 77 - file: File, 78 - ): Promise<{ width: number; height: number }> { 79 - let url = URL.createObjectURL(file); 80 - return new Promise((resolve, reject) => { 81 - const img = new Image(); 82 - img.onload = function () { 83 - resolve({ width: img.width, height: img.height }); 84 - URL.revokeObjectURL(url); 85 - }; 86 - img.onerror = reject; 87 - img.src = url; 88 - }); 89 - } 90 - 91 - async function computeHash(data: File): Promise<string> { 92 - let buffer = await data.arrayBuffer(); 93 - const buf = await crypto.subtle.digest("SHA-256", new Uint8Array(buffer)); 94 - return Array.from(new Uint8Array(buf), (b) => 95 - b.toString(16).padStart(2, "0"), 96 - ).join(""); 97 - } 98 - 99 50 export function Blocks(props: { entityID: string }) { 100 51 let blocks = useEntity(props.entityID, "card/block"); 101 52 ··· 129 80 nextPosition: string | null; 130 81 }) { 131 82 let image = useEntity(props.entityID, "block/image"); 83 + let virtualBlock = useMemo(() => { 84 + return crypto.randomUUID(); 85 + }, []); 86 + 132 87 if (image) 133 88 return ( 134 - <div className="border p-2 w-full"> 135 - <img 136 - alt={""} 137 - src={image.data.src} 138 - height={image.data.height} 139 - width={image.data.width} 140 - /> 141 - </div> 89 + <> 90 + <div className="border p-2 w-full"> 91 + <img 92 + alt={""} 93 + src={image.data.src} 94 + height={image.data.height} 95 + width={image.data.width} 96 + /> 97 + </div> 98 + <div className="border p-2 w-full"> 99 + <TextBlock 100 + parent={props.parent} 101 + previousBlock={{ value: props.entityID, position: props.position }} 102 + entityID={virtualBlock} 103 + nextPosition={props.nextPosition} 104 + position={generateKeyBetween(props.position, props.nextPosition)} 105 + /> 106 + </div> 107 + </> 142 108 ); 143 109 return ( 144 110 <div className="border p-2 w-full">
+30 -1
components/TextBlock.tsx
··· 2 2 import { elementId } from "../utils/elementId"; 3 3 import { baseKeymap, toggleMark } from "prosemirror-commands"; 4 4 import { keymap } from "prosemirror-keymap"; 5 + import { Schema } from "prosemirror-model"; 5 6 import * as Y from "yjs"; 6 7 import { ProseMirror, useEditorState } from "@nytimes/react-prosemirror"; 7 8 import * as base64 from "base64-js"; ··· 12 13 Fact, 13 14 } from "../replicache"; 14 15 16 + let schema = new Schema({ 17 + marks: { 18 + strong: marks.strong, 19 + em: marks.em, 20 + }, 21 + nodes: { doc: nodes.doc, paragraph: nodes.paragraph, text: nodes.text }, 22 + }); 23 + 15 24 import { EditorState, TextSelection } from "prosemirror-state"; 16 - import { schema } from "prosemirror-schema-basic"; 25 + import { marks, nodes } from "prosemirror-schema-basic"; 17 26 import { ySyncPlugin } from "y-prosemirror"; 18 27 import { Replicache } from "replicache"; 19 28 import { generateKeyBetween } from "fractional-indexing"; 20 29 import { create } from "zustand"; 21 30 import { RenderYJSFragment } from "./RenderYJSFragment"; 22 31 import { useInitialPageLoad } from "./InitialPageLoadProvider"; 32 + import { addImage } from "../utils/addImage"; 23 33 24 34 let useEditorStates = create( 25 35 () => ··· 176 186 }} 177 187 > 178 188 <pre 189 + onPaste={(e) => { 190 + if (!rep.rep) return; 191 + for (let item of e.clipboardData.items) { 192 + if (item?.type.includes("image")) { 193 + let file = item.getAsFile(); 194 + if (file) 195 + addImage(file, rep.rep, { 196 + parent: props.parent, 197 + position: generateKeyBetween( 198 + props.position, 199 + props.nextPosition, 200 + ), 201 + }); 202 + return; 203 + } 204 + } 205 + e.preventDefault(); 206 + e.stopPropagation(); 207 + }} 179 208 id={elementId.block(props.entityID).text} 180 209 className="w-full whitespace-pre-wrap outline-none" 181 210 ref={setMount}
+5
next.config.js
··· 2 2 3 3 /** @type {import('next').NextConfig} */ 4 4 const nextConfig = { 5 + images: { 6 + remotePatterns: [ 7 + { protocol: "http", hostname: "127.0.0.1", port: "54321" }, 8 + ], 9 + }, 5 10 transpilePackages: ["@adobe/react-spectrum", "@react-spectrum/color"], 6 11 experimental: { 7 12 instrumentationHook: true,
+1
package.json
··· 29 29 "postgres": "^3.4.4", 30 30 "prosemirror-commands": "^1.5.2", 31 31 "prosemirror-keymap": "^1.2.2", 32 + "prosemirror-model": "^1.21.0", 32 33 "prosemirror-schema-basic": "^1.2.2", 33 34 "prosemirror-state": "^1.4.3", 34 35 "react": "^18.3.1",
+65
utils/addImage.ts
··· 1 + import { Replicache } from "replicache"; 2 + import { ReplicacheMutators } from "../replicache"; 3 + import { supabaseBrowserClient } from "../supabase/browserClient"; 4 + 5 + export async function addImage( 6 + file: File, 7 + rep: Replicache<ReplicacheMutators>, 8 + args: { parent: string; position: string }, 9 + ) { 10 + let client = supabaseBrowserClient(); 11 + let cache = await caches.open("minilink-user-assets"); 12 + let hash = await computeHash(file); 13 + let url = client.storage.from("minilink-user-assets").getPublicUrl(hash) 14 + .data.publicUrl; 15 + let dimensions = await getImageDimensions(file); 16 + await cache.put( 17 + url, 18 + new Response(file, { 19 + headers: { 20 + "Content-Type": file.type, 21 + "Content-Length": file.size.toString(), 22 + }, 23 + }), 24 + ); 25 + let newBlockEntity = crypto.randomUUID(); 26 + await rep.mutate.addBlock({ 27 + parent: args.parent, 28 + position: args.position, 29 + newEntityID: newBlockEntity, 30 + }); 31 + await rep.mutate.assertFact({ 32 + entity: newBlockEntity, 33 + attribute: "block/image", 34 + data: { 35 + type: "image", 36 + src: url, 37 + height: dimensions.height, 38 + width: dimensions.width, 39 + }, 40 + }); 41 + await client.storage.from("minilink-user-assets").upload(hash, file); 42 + } 43 + 44 + function getImageDimensions( 45 + file: File, 46 + ): Promise<{ width: number; height: number }> { 47 + let url = URL.createObjectURL(file); 48 + return new Promise((resolve, reject) => { 49 + const img = new Image(); 50 + img.onload = function () { 51 + resolve({ width: img.width, height: img.height }); 52 + URL.revokeObjectURL(url); 53 + }; 54 + img.onerror = reject; 55 + img.src = url; 56 + }); 57 + } 58 + 59 + async function computeHash(data: File): Promise<string> { 60 + let buffer = await data.arrayBuffer(); 61 + const buf = await crypto.subtle.digest("SHA-256", new Uint8Array(buffer)); 62 + return Array.from(new Uint8Array(buf), (b) => 63 + b.toString(16).padStart(2, "0"), 64 + ).join(""); 65 + }