a tool for shared writing and social publishing
1import { Replicache } from "replicache"; 2import { ReplicacheMutators } from "../replicache"; 3import { supabaseBrowserClient } from "supabase/browserClient"; 4import type { FilterAttributes } from "src/replicache/attributes"; 5import { rgbaToDataURL, rgbaToThumbHash, thumbHashToDataURL } from "thumbhash"; 6import { v7 } from "uuid"; 7 8export const localImages = new Map<string, boolean>(); 9export async function addImage( 10 file: File, 11 rep: Replicache<ReplicacheMutators>, 12 args: { 13 entityID: string; 14 attribute: keyof FilterAttributes<{ type: "image" }>; 15 }, 16) { 17 let client = supabaseBrowserClient(); 18 let cache = await caches.open("minilink-user-assets"); 19 let fileID = v7(); 20 let url = client.storage.from("minilink-user-assets").getPublicUrl(fileID) 21 .data.publicUrl; 22 let dimensions = await getImageDimensions(file); 23 await cache.put( 24 new URL(url + "?local"), 25 new Response(file, { 26 headers: { 27 "Content-Type": file.type, 28 "Content-Length": file.size.toString(), 29 }, 30 }), 31 ); 32 localImages.set(url, true); 33 34 let thumbhash = await getThumbHash(file); 35 if (navigator.serviceWorker) 36 await rep.mutate.assertFact({ 37 entity: args.entityID, 38 attribute: "block/image", 39 data: { 40 fallback: thumbhash, 41 type: "image", 42 local: rep.clientID, 43 src: url, 44 height: dimensions.height, 45 width: dimensions.width, 46 }, 47 }); 48 await client.storage.from("minilink-user-assets").upload(fileID, file, { 49 cacheControl: "public, max-age=31560000, immutable", 50 }); 51 await rep.mutate.assertFact({ 52 entity: args.entityID, 53 attribute: args.attribute, 54 data: { 55 fallback: thumbhash, 56 type: "image", 57 src: url, 58 height: dimensions.height, 59 width: dimensions.width, 60 }, 61 }); 62} 63 64async function getThumbHash(file: File) { 65 const arrayBuffer = await file.arrayBuffer(); 66 const blob = new Blob([arrayBuffer], { type: file.type }); 67 const imageBitmap = await createImageBitmap(blob); 68 69 const canvas = document.createElement("canvas"); 70 const context = canvas.getContext("2d") as CanvasRenderingContext2D; 71 const maxDimension = 100; 72 let width = imageBitmap.width; 73 let height = imageBitmap.height; 74 75 if (width > height) { 76 if (width > maxDimension) { 77 height *= maxDimension / width; 78 width = maxDimension; 79 } 80 } else { 81 if (height > maxDimension) { 82 width *= maxDimension / height; 83 height = maxDimension; 84 } 85 } 86 87 canvas.width = width; 88 canvas.height = height; 89 context.drawImage(imageBitmap, 0, 0, width, height); 90 91 const imageData = context.getImageData(0, 0, width, height); 92 const thumbHash = thumbHashToDataURL( 93 rgbaToThumbHash(imageData.width, imageData.height, imageData.data), 94 ); 95 return thumbHash; 96} 97 98function getImageDimensions( 99 file: File, 100): Promise<{ width: number; height: number }> { 101 let url = URL.createObjectURL(file); 102 return new Promise((resolve, reject) => { 103 const img = new Image(); 104 img.onload = function () { 105 resolve({ width: img.width, height: img.height }); 106 URL.revokeObjectURL(url); 107 }; 108 img.onerror = reject; 109 img.src = url; 110 }); 111}