a tool for shared writing and social publishing
fork

Configure Feed

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

at 5ee7a1ae811dedfdb06cb2d6972a7ee395cdec91 111 lines 3.2 kB view raw
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}