a tool for shared writing and social publishing

handle dropping images

+150 -26
+19 -1
components/Blocks/Block.tsx
··· 7 import { useBlockKeyboardHandlers } from "./useBlockKeyboardHandlers"; 8 import { useLongPress } from "src/hooks/useLongPress"; 9 import { focusBlock } from "src/utils/focusBlock"; 10 11 import { TextBlock } from "components/Blocks/TextBlock"; 12 import { ImageBlock } from "./ImageBlock"; ··· 15 import { EmbedBlock } from "./EmbedBlock"; 16 import { MailboxBlock } from "./MailboxBlock"; 17 import { AreYouSure } from "./DeleteBlock"; 18 - import { useEntitySetContext } from "components/EntitySetProvider"; 19 import { useIsMobile } from "src/hooks/isMobile"; 20 import { DateTimeBlock } from "./DateTimeBlock"; 21 import { RSVPBlock } from "./RSVPBlock"; ··· 63 // and shared styling like padding and flex for list layouting 64 65 let mouseHandlers = useBlockMouseHandlers(props); 66 67 let { isLongPress, handlers } = useLongPress(() => { 68 if (isTextBlock[props.type]) return; ··· 93 {...(!props.preview ? { ...mouseHandlers, ...handlers } : {})} 94 id={ 95 !props.preview ? elementId.block(props.entityID).container : undefined 96 } 97 className={` 98 blockWrapper relative
··· 7 import { useBlockKeyboardHandlers } from "./useBlockKeyboardHandlers"; 8 import { useLongPress } from "src/hooks/useLongPress"; 9 import { focusBlock } from "src/utils/focusBlock"; 10 + import { useHandleDrop } from "./useHandleDrop"; 11 + import { useEntitySetContext } from "components/EntitySetProvider"; 12 13 import { TextBlock } from "components/Blocks/TextBlock"; 14 import { ImageBlock } from "./ImageBlock"; ··· 17 import { EmbedBlock } from "./EmbedBlock"; 18 import { MailboxBlock } from "./MailboxBlock"; 19 import { AreYouSure } from "./DeleteBlock"; 20 import { useIsMobile } from "src/hooks/isMobile"; 21 import { DateTimeBlock } from "./DateTimeBlock"; 22 import { RSVPBlock } from "./RSVPBlock"; ··· 64 // and shared styling like padding and flex for list layouting 65 66 let mouseHandlers = useBlockMouseHandlers(props); 67 + let handleDrop = useHandleDrop({ 68 + parent: props.parent, 69 + position: props.position, 70 + nextPosition: props.nextPosition, 71 + }); 72 + let entity_set = useEntitySetContext(); 73 74 let { isLongPress, handlers } = useLongPress(() => { 75 if (isTextBlock[props.type]) return; ··· 100 {...(!props.preview ? { ...mouseHandlers, ...handlers } : {})} 101 id={ 102 !props.preview ? elementId.block(props.entityID).container : undefined 103 + } 104 + onDragOver={ 105 + !props.preview && entity_set.permissions.write 106 + ? (e) => { 107 + e.preventDefault(); 108 + e.stopPropagation(); 109 + } 110 + : undefined 111 + } 112 + onDrop={ 113 + !props.preview && entity_set.permissions.write ? handleDrop : undefined 114 } 115 className={` 116 blockWrapper relative
+46 -25
components/Blocks/ImageBlock.tsx
··· 51 } 52 }, [isSelected, props.preview, props.entityID]); 53 54 if (!image) { 55 if (!entity_set.permissions.write) return null; 56 return ( ··· 65 ${isSelected && !isLocked ? "border-2 border-tertiary font-bold" : "border border-border"} 66 ${props.pageType === "canvas" && "bg-bg-page"}`} 67 onMouseDown={(e) => e.preventDefault()} 68 > 69 <div className="flex gap-2"> 70 <BlockImageSmall ··· 79 accept="image/*" 80 onChange={async (e) => { 81 let file = e.currentTarget.files?.[0]; 82 - if (!file || !rep) return; 83 - let entity = props.entityID; 84 - if (!entity) { 85 - entity = v7(); 86 - await rep?.mutate.addBlock({ 87 - parent: props.parent, 88 - factID: v7(), 89 - permission_set: entity_set.set, 90 - type: "text", 91 - position: generateKeyBetween( 92 - props.position, 93 - props.nextPosition, 94 - ), 95 - newEntityID: entity, 96 - }); 97 - } 98 - await rep.mutate.assertFact({ 99 - entity, 100 - attribute: "block/type", 101 - data: { type: "block-type-union", value: "image" }, 102 - }); 103 - await addImage(file, rep, { 104 - entityID: entity, 105 - attribute: "block/image", 106 - }); 107 }} 108 /> 109 </label>
··· 51 } 52 }, [isSelected, props.preview, props.entityID]); 53 54 + const handleImageUpload = async (file: File) => { 55 + if (!rep) return; 56 + let entity = props.entityID; 57 + if (!entity) { 58 + entity = v7(); 59 + await rep?.mutate.addBlock({ 60 + parent: props.parent, 61 + factID: v7(), 62 + permission_set: entity_set.set, 63 + type: "text", 64 + position: generateKeyBetween( 65 + props.position, 66 + props.nextPosition, 67 + ), 68 + newEntityID: entity, 69 + }); 70 + } 71 + await rep.mutate.assertFact({ 72 + entity, 73 + attribute: "block/type", 74 + data: { type: "block-type-union", value: "image" }, 75 + }); 76 + await addImage(file, rep, { 77 + entityID: entity, 78 + attribute: "block/image", 79 + }); 80 + }; 81 + 82 if (!image) { 83 if (!entity_set.permissions.write) return null; 84 return ( ··· 93 ${isSelected && !isLocked ? "border-2 border-tertiary font-bold" : "border border-border"} 94 ${props.pageType === "canvas" && "bg-bg-page"}`} 95 onMouseDown={(e) => e.preventDefault()} 96 + onDragOver={(e) => { 97 + e.preventDefault(); 98 + e.stopPropagation(); 99 + }} 100 + onDrop={async (e) => { 101 + e.preventDefault(); 102 + e.stopPropagation(); 103 + if (isLocked) return; 104 + const files = e.dataTransfer.files; 105 + if (files && files.length > 0) { 106 + const file = files[0]; 107 + if (file.type.startsWith('image/')) { 108 + await handleImageUpload(file); 109 + } 110 + } 111 + }} 112 > 113 <div className="flex gap-2"> 114 <BlockImageSmall ··· 123 accept="image/*" 124 onChange={async (e) => { 125 let file = e.currentTarget.files?.[0]; 126 + if (!file) return; 127 + await handleImageUpload(file); 128 }} 129 /> 130 </label>
+11
components/Blocks/index.tsx
··· 17 import { useEffect } from "react"; 18 import { addShortcut } from "src/shortcuts"; 19 import { QuoteEmbedBlock } from "./QuoteEmbedBlock"; 20 21 export function Blocks(props: { entityID: string }) { 22 let rep = useReplicache(); ··· 231 }) => { 232 let { rep } = useReplicache(); 233 let entity_set = useEntitySetContext(); 234 235 if (!entity_set.permissions.write) return; 236 return ( ··· 267 }, 10); 268 } 269 }} 270 /> 271 ); 272 };
··· 17 import { useEffect } from "react"; 18 import { addShortcut } from "src/shortcuts"; 19 import { QuoteEmbedBlock } from "./QuoteEmbedBlock"; 20 + import { useHandleDrop } from "./useHandleDrop"; 21 22 export function Blocks(props: { entityID: string }) { 23 let rep = useReplicache(); ··· 232 }) => { 233 let { rep } = useReplicache(); 234 let entity_set = useEntitySetContext(); 235 + let handleDrop = useHandleDrop({ 236 + parent: props.entityID, 237 + position: props.lastRootBlock?.position || null, 238 + nextPosition: null, 239 + }); 240 241 if (!entity_set.permissions.write) return; 242 return ( ··· 273 }, 10); 274 } 275 }} 276 + onDragOver={(e) => { 277 + e.preventDefault(); 278 + e.stopPropagation(); 279 + }} 280 + onDrop={handleDrop} 281 /> 282 ); 283 };
+74
components/Blocks/useHandleDrop.ts
···
··· 1 + import { useCallback } from "react"; 2 + import { useReplicache } from "src/replicache"; 3 + import { generateKeyBetween } from "fractional-indexing"; 4 + import { addImage } from "src/utils/addImage"; 5 + import { useEntitySetContext } from "components/EntitySetProvider"; 6 + import { v7 } from "uuid"; 7 + 8 + export const useHandleDrop = (params: { 9 + parent: string; 10 + position: string | null; 11 + nextPosition: string | null; 12 + }) => { 13 + let { rep } = useReplicache(); 14 + let entity_set = useEntitySetContext(); 15 + 16 + return useCallback( 17 + async (e: React.DragEvent) => { 18 + e.preventDefault(); 19 + e.stopPropagation(); 20 + 21 + if (!rep) return; 22 + 23 + const files = e.dataTransfer.files; 24 + if (!files || files.length === 0) return; 25 + 26 + // Filter for image files only 27 + const imageFiles = Array.from(files).filter((file) => 28 + file.type.startsWith("image/"), 29 + ); 30 + 31 + if (imageFiles.length === 0) return; 32 + 33 + let currentPosition = params.position; 34 + 35 + // Calculate positions for all images first 36 + const imageBlocks = imageFiles.map((file) => { 37 + const entity = v7(); 38 + const position = generateKeyBetween( 39 + currentPosition, 40 + params.nextPosition, 41 + ); 42 + currentPosition = position; 43 + return { file, entity, position }; 44 + }); 45 + 46 + // Create all blocks in parallel 47 + await Promise.all( 48 + imageBlocks.map((block) => 49 + rep.mutate.addBlock({ 50 + parent: params.parent, 51 + factID: v7(), 52 + permission_set: entity_set.set, 53 + type: "image", 54 + position: block.position, 55 + newEntityID: block.entity, 56 + }), 57 + ), 58 + ); 59 + 60 + // Upload all images in parallel 61 + await Promise.all( 62 + imageBlocks.map((block) => 63 + addImage(block.file, rep, { 64 + entityID: block.entity, 65 + attribute: "block/image", 66 + }), 67 + ), 68 + ); 69 + 70 + return true; 71 + }, 72 + [rep, params.position, params.nextPosition, params.parent, entity_set.set], 73 + ); 74 + };