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}