jazz-tools 0.16.5 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.svelte-kit/__package__/index.d.ts +1 -0
- package/.svelte-kit/__package__/index.d.ts.map +1 -1
- package/.svelte-kit/__package__/index.js +1 -0
- package/.svelte-kit/__package__/media/image.svelte +131 -0
- package/.svelte-kit/__package__/media/image.svelte.d.ts +10 -0
- package/.svelte-kit/__package__/media/image.svelte.d.ts.map +1 -0
- package/.svelte-kit/__package__/media/index.d.ts +2 -0
- package/.svelte-kit/__package__/media/index.d.ts.map +1 -0
- package/.svelte-kit/__package__/media/index.js +1 -0
- package/.svelte-kit/__package__/tests/media/image.svelte.test.d.ts +2 -0
- package/.svelte-kit/__package__/tests/media/image.svelte.test.d.ts.map +1 -0
- package/.svelte-kit/__package__/tests/media/image.svelte.test.js +430 -0
- package/.svelte-kit/__package__/tests/testUtils.d.ts +11 -0
- package/.svelte-kit/__package__/tests/testUtils.d.ts.map +1 -0
- package/.svelte-kit/__package__/tests/testUtils.js +17 -0
- package/.svelte-kit/__package__/tests/types.d.ts +3 -0
- package/.turbo/turbo-build.log +44 -48
- package/CHANGELOG.md +28 -0
- package/dist/{chunk-H3BIFFQG.js → chunk-2SH44VLX.js} +35 -40
- package/dist/chunk-2SH44VLX.js.map +1 -0
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/inspector/{custom-element-TUXKXSZU.js → custom-element-I7L56H6B.js} +3 -5
- package/dist/inspector/{custom-element-TUXKXSZU.js.map → custom-element-I7L56H6B.js.map} +1 -1
- package/dist/inspector/index.js +2 -4
- package/dist/inspector/index.js.map +1 -1
- package/dist/inspector/register-custom-element.js +1 -1
- package/dist/inspector/viewer/co-plain-text-view.d.ts +1 -1
- package/dist/inspector/viewer/co-plain-text-view.d.ts.map +1 -1
- package/dist/inspector/viewer/role-display.d.ts.map +1 -1
- package/dist/media/chunk-BBSS3NEY.js +211 -0
- package/dist/media/chunk-BBSS3NEY.js.map +1 -0
- package/dist/media/create-image.d.ts +48 -0
- package/dist/media/create-image.d.ts.map +1 -0
- package/dist/media/create-image.test.d.ts +2 -0
- package/dist/media/create-image.test.d.ts.map +1 -0
- package/dist/media/index.browser.d.ts +15 -0
- package/dist/media/index.browser.d.ts.map +1 -0
- package/dist/media/index.browser.js +113 -0
- package/dist/media/index.browser.js.map +1 -0
- package/dist/media/index.d.ts +53 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/index.js +13 -0
- package/dist/media/index.js.map +1 -0
- package/dist/media/index.native.d.ts +17 -0
- package/dist/media/index.native.d.ts.map +1 -0
- package/dist/media/index.native.js +126 -0
- package/dist/media/index.native.js.map +1 -0
- package/dist/media/utils.d.ts +17 -0
- package/dist/media/utils.d.ts.map +1 -0
- package/dist/media/utils.test.d.ts +2 -0
- package/dist/media/utils.test.d.ts.map +1 -0
- package/dist/react/index.d.ts +1 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +176 -59
- package/dist/react/index.js.map +1 -1
- package/dist/react/media/image.d.ts +62 -0
- package/dist/react/media/image.d.ts.map +1 -0
- package/dist/react/ssr.d.ts.map +1 -1
- package/dist/react/ssr.js.map +1 -1
- package/dist/react/tests/media/image.test.d.ts +2 -0
- package/dist/react/tests/media/image.test.d.ts.map +1 -0
- package/dist/react/tests/testUtils.d.ts.map +1 -1
- package/dist/react-core/auth/PassphraseAuth.d.ts +1 -1
- package/dist/react-core/auth/PassphraseAuth.d.ts.map +1 -1
- package/dist/react-core/index.js +1 -3
- package/dist/react-core/index.js.map +1 -1
- package/dist/react-core/tests/testUtils.d.ts.map +1 -1
- package/dist/react-core/tests/useDemoAuth.test.d.ts +2 -0
- package/dist/react-core/tests/useDemoAuth.test.d.ts.map +1 -0
- package/dist/react-native-core/index.d.ts +1 -1
- package/dist/react-native-core/index.d.ts.map +1 -1
- package/dist/react-native-core/index.js +84 -66
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/react-native-core/media/image.d.ts +93 -0
- package/dist/react-native-core/media/image.d.ts.map +1 -0
- package/dist/react-native-core/testing.d.ts +2 -0
- package/dist/react-native-core/testing.d.ts.map +1 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +1 -0
- package/dist/svelte/media/image.svelte +131 -0
- package/dist/svelte/media/image.svelte.d.ts +10 -0
- package/dist/svelte/media/image.svelte.d.ts.map +1 -0
- package/dist/svelte/media/index.d.ts +2 -0
- package/dist/svelte/media/index.d.ts.map +1 -0
- package/dist/svelte/media/index.js +1 -0
- package/dist/svelte/tests/media/image.svelte.test.d.ts +2 -0
- package/dist/svelte/tests/media/image.svelte.test.d.ts.map +1 -0
- package/dist/svelte/tests/media/image.svelte.test.js +430 -0
- package/dist/svelte/tests/testUtils.d.ts +11 -0
- package/dist/svelte/tests/testUtils.d.ts.map +1 -0
- package/dist/svelte/tests/testUtils.js +17 -0
- package/dist/svelte/tests/types.d.ts +3 -0
- package/dist/testing.js +1 -1
- package/dist/testing.js.map +1 -1
- package/dist/tools/coValues/coFeed.d.ts +15 -0
- package/dist/tools/coValues/coFeed.d.ts.map +1 -1
- package/dist/tools/coValues/deepLoading.d.ts +10 -10
- package/dist/tools/coValues/deepLoading.d.ts.map +1 -1
- package/dist/tools/coValues/extensions/imageDef.d.ts +3 -9
- package/dist/tools/coValues/extensions/imageDef.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts +1 -0
- package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/testing.d.ts.map +1 -1
- package/package.json +12 -12
- package/src/inspector/viewer/co-plain-text-view.tsx +1 -5
- package/src/inspector/viewer/co-stream-view.tsx +1 -1
- package/src/inspector/viewer/role-display.tsx +4 -1
- package/src/{browser-media-images/index.test.browser.ts → media/create-image.test.ts} +146 -24
- package/src/media/create-image.ts +180 -0
- package/src/media/index.browser.ts +150 -0
- package/src/media/index.native.ts +153 -0
- package/src/media/index.ts +61 -0
- package/src/media/utils.test.ts +327 -0
- package/src/media/utils.ts +202 -0
- package/src/react/index.ts +1 -2
- package/src/react/media/image.tsx +210 -0
- package/src/react/ssr.ts +1 -3
- package/src/react/tests/media/image.test.tsx +588 -0
- package/src/react/tests/testUtils.tsx +2 -10
- package/src/react-core/auth/PassphraseAuth.tsx +1 -5
- package/src/react-core/tests/testUtils.tsx +2 -10
- package/src/react-native-core/index.ts +1 -1
- package/src/react-native-core/media/image.tsx +159 -0
- package/src/svelte/index.ts +1 -0
- package/src/svelte/media/image.svelte +131 -0
- package/src/svelte/media/index.ts +1 -0
- package/src/svelte/tests/media/image.svelte.test.ts +583 -0
- package/src/svelte/tests/testUtils.ts +33 -0
- package/src/svelte/tests/types.d.ts +3 -0
- package/src/tools/coValues/coFeed.ts +40 -7
- package/src/tools/coValues/deepLoading.ts +46 -32
- package/src/tools/coValues/extensions/imageDef.ts +3 -49
- package/src/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.ts +6 -0
- package/src/tools/index.ts +0 -1
- package/src/tools/testing.ts +3 -1
- package/src/tools/tests/coList.test.ts +1 -1
- package/src/tools/tests/coMap.record.test-d.ts +105 -0
- package/src/tools/tests/coMap.record.test.ts +48 -2
- package/src/tools/tests/coMap.test-d.ts +50 -0
- package/src/tools/tests/coOptional.test.ts +3 -1
- package/tsconfig.json +1 -0
- package/tsup.config.ts +4 -9
- package/vitest.config.ts +14 -21
- package/dist/browser-media-images/index.d.ts +0 -9
- package/dist/browser-media-images/index.d.ts.map +0 -1
- package/dist/browser-media-images/index.js +0 -72
- package/dist/browser-media-images/index.js.map +0 -1
- package/dist/browser-media-images/index.test.browser.d.ts +0 -2
- package/dist/browser-media-images/index.test.browser.d.ts.map +0 -1
- package/dist/chunk-H3BIFFQG.js.map +0 -1
- package/dist/react/media.d.ts +0 -24
- package/dist/react/media.d.ts.map +0 -1
- package/dist/react-native-core/media.d.ts +0 -24
- package/dist/react-native-core/media.d.ts.map +0 -1
- package/dist/react-native-media-images/index.d.ts +0 -7
- package/dist/react-native-media-images/index.d.ts.map +0 -1
- package/dist/react-native-media-images/index.js +0 -177
- package/dist/react-native-media-images/index.js.map +0 -1
- package/dist/tools/tests/imageDef.test.d.ts +0 -2
- package/dist/tools/tests/imageDef.test.d.ts.map +0 -1
- package/src/browser-media-images/index.ts +0 -131
- package/src/react/media.tsx +0 -74
- package/src/react/scratch.tsx +0 -50
- package/src/react-native-core/media.tsx +0 -79
- package/src/react-native-media-images/index.ts +0 -238
- package/src/tools/tests/imageDef.test.ts +0 -278
@@ -0,0 +1,202 @@
|
|
1
|
+
import type { CoID } from "cojson";
|
2
|
+
import { Account, FileStream, ImageDefinition } from "jazz-tools";
|
3
|
+
|
4
|
+
export function highestResAvailable(
|
5
|
+
image: ImageDefinition,
|
6
|
+
wantedWidth: number,
|
7
|
+
wantedHeight: number,
|
8
|
+
): { width: number; height: number; image: FileStream } | null {
|
9
|
+
const availableSizes: [number, number, string][] = image._raw
|
10
|
+
.keys()
|
11
|
+
.filter((key) => /^\d+x\d+$/.test(key))
|
12
|
+
.map((key) => {
|
13
|
+
const [w, h] = key.split("x").map(Number) as [number, number];
|
14
|
+
return [w, h, key];
|
15
|
+
});
|
16
|
+
|
17
|
+
if (availableSizes.length === 0) {
|
18
|
+
return image.original
|
19
|
+
? {
|
20
|
+
width: image.originalSize[0],
|
21
|
+
height: image.originalSize[1],
|
22
|
+
image: image.original,
|
23
|
+
}
|
24
|
+
: null;
|
25
|
+
}
|
26
|
+
|
27
|
+
const sortedSizes = availableSizes
|
28
|
+
.map((size) => {
|
29
|
+
return {
|
30
|
+
size,
|
31
|
+
match: sizesMatchWanted(size[0], size[1], wantedWidth, wantedHeight),
|
32
|
+
isLoaded: isLoaded(image._raw.get(size[2]) as CoID<any> | undefined),
|
33
|
+
};
|
34
|
+
})
|
35
|
+
.sort((a, b) => a.match - b.match);
|
36
|
+
|
37
|
+
// We try to find the better already loaded image
|
38
|
+
// note: `toReversed` is not available in react-native.
|
39
|
+
const bestLoaded = [...sortedSizes]
|
40
|
+
.reverse()
|
41
|
+
.find((el) => el.isLoaded && image[el.size[2]]?.getChunks());
|
42
|
+
|
43
|
+
// if I can't find a good match, let's use the highest resolution
|
44
|
+
const bestTarget =
|
45
|
+
sortedSizes.find((el) => el.match > 0.95) || sortedSizes.at(-1);
|
46
|
+
|
47
|
+
// if the best target is already loaded, we are done
|
48
|
+
if (image[bestTarget!.size[2]]?.getChunks()) {
|
49
|
+
return image[bestTarget!.size[2]]
|
50
|
+
? {
|
51
|
+
width: bestTarget!.size[0],
|
52
|
+
height: bestTarget!.size[1],
|
53
|
+
image: image[bestTarget!.size[2]]!,
|
54
|
+
}
|
55
|
+
: null;
|
56
|
+
}
|
57
|
+
|
58
|
+
// if the best already loaded is not the best target
|
59
|
+
// let's trigger the load of the best target
|
60
|
+
if (bestLoaded) {
|
61
|
+
image[bestTarget!.size[2]]?.getChunks();
|
62
|
+
return image[bestLoaded.size[2]]
|
63
|
+
? {
|
64
|
+
width: bestLoaded.size[0],
|
65
|
+
height: bestLoaded.size[1],
|
66
|
+
image: image[bestLoaded.size[2]]!,
|
67
|
+
}
|
68
|
+
: null;
|
69
|
+
}
|
70
|
+
|
71
|
+
// if nothing is loaded, then start fetching all the images till the best
|
72
|
+
for (let size of sortedSizes) {
|
73
|
+
if (size.match <= bestTarget!.match) {
|
74
|
+
image[size.size[2]]?.getChunks();
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
return null;
|
79
|
+
}
|
80
|
+
|
81
|
+
function sizesMatchWanted(
|
82
|
+
w: number,
|
83
|
+
h: number,
|
84
|
+
wantedW: number,
|
85
|
+
wantedH: number,
|
86
|
+
): number {
|
87
|
+
const area1 = w * h;
|
88
|
+
const area2 = wantedW * wantedH;
|
89
|
+
|
90
|
+
const areaRatio = area1 / area2;
|
91
|
+
|
92
|
+
// // Below 0.95 means the image is too small, we don't want to upscale it
|
93
|
+
// if (areaRatio < 0.95) {
|
94
|
+
// return 9999;
|
95
|
+
// }
|
96
|
+
|
97
|
+
return areaRatio;
|
98
|
+
}
|
99
|
+
|
100
|
+
function isLoaded(id: CoID<any> | null | undefined): boolean {
|
101
|
+
if (!id) {
|
102
|
+
return false;
|
103
|
+
}
|
104
|
+
|
105
|
+
return !!Account.getMe()._raw.core.node.getLoaded(id);
|
106
|
+
}
|
107
|
+
|
108
|
+
export async function loadImage(
|
109
|
+
imageOrId: ImageDefinition | string,
|
110
|
+
): Promise<{ width: number; height: number; image: FileStream } | null> {
|
111
|
+
if (typeof imageOrId === "string") {
|
112
|
+
const image = await ImageDefinition.load(imageOrId, {
|
113
|
+
resolve: {
|
114
|
+
original: true,
|
115
|
+
},
|
116
|
+
});
|
117
|
+
|
118
|
+
if (image === null || image.original === null) {
|
119
|
+
return null;
|
120
|
+
}
|
121
|
+
|
122
|
+
return {
|
123
|
+
width: image.originalSize[0],
|
124
|
+
height: image.originalSize[1],
|
125
|
+
image: image.original,
|
126
|
+
};
|
127
|
+
}
|
128
|
+
|
129
|
+
await imageOrId.ensureLoaded({
|
130
|
+
resolve: {
|
131
|
+
original: true,
|
132
|
+
},
|
133
|
+
});
|
134
|
+
|
135
|
+
if (!imageOrId.original) {
|
136
|
+
console.warn("Unable to find the original image");
|
137
|
+
return null;
|
138
|
+
}
|
139
|
+
|
140
|
+
return {
|
141
|
+
width: imageOrId.originalSize[0],
|
142
|
+
height: imageOrId.originalSize[1],
|
143
|
+
image: imageOrId.original,
|
144
|
+
};
|
145
|
+
}
|
146
|
+
|
147
|
+
export async function loadImageBySize(
|
148
|
+
imageOrId: ImageDefinition | string,
|
149
|
+
wantedWidth: number,
|
150
|
+
wantedHeight: number,
|
151
|
+
): Promise<{ width: number; height: number; image: FileStream } | null> {
|
152
|
+
const image =
|
153
|
+
typeof imageOrId === "string"
|
154
|
+
? await ImageDefinition.load(imageOrId)
|
155
|
+
: imageOrId;
|
156
|
+
|
157
|
+
if (image === null) {
|
158
|
+
return null;
|
159
|
+
}
|
160
|
+
|
161
|
+
if (image.progressive === false) {
|
162
|
+
return loadImage(imageOrId);
|
163
|
+
}
|
164
|
+
|
165
|
+
const availableSizes: [number, number, string][] = image._raw
|
166
|
+
.keys()
|
167
|
+
.filter((key) => /^\d+x\d+$/.test(key))
|
168
|
+
.map((key) => {
|
169
|
+
const [w, h] = key.split("x").map(Number) as [number, number];
|
170
|
+
return [w, h, key];
|
171
|
+
});
|
172
|
+
|
173
|
+
if (availableSizes.length === 0) {
|
174
|
+
return null;
|
175
|
+
}
|
176
|
+
|
177
|
+
const sortedSizes = availableSizes
|
178
|
+
.map((size) => ({
|
179
|
+
size,
|
180
|
+
match: sizesMatchWanted(size[0], size[1], wantedWidth, wantedHeight),
|
181
|
+
}))
|
182
|
+
.sort((a, b) => a.match - b.match);
|
183
|
+
|
184
|
+
const bestTarget =
|
185
|
+
sortedSizes.find((el) => el.match > 0.95) || sortedSizes.at(-1)!;
|
186
|
+
|
187
|
+
const deepLoaded = await ImageDefinition.load(image.id, {
|
188
|
+
resolve: {
|
189
|
+
[bestTarget.size[2]]: true,
|
190
|
+
},
|
191
|
+
});
|
192
|
+
|
193
|
+
if (deepLoaded === null || deepLoaded[bestTarget.size[2]] === undefined) {
|
194
|
+
return null;
|
195
|
+
}
|
196
|
+
|
197
|
+
return {
|
198
|
+
width: bestTarget.size[0],
|
199
|
+
height: bestTarget.size[1],
|
200
|
+
image: deepLoaded[bestTarget.size[2]]!,
|
201
|
+
};
|
202
|
+
}
|
package/src/react/index.ts
CHANGED
@@ -0,0 +1,210 @@
|
|
1
|
+
import { ImageDefinition } from "jazz-tools";
|
2
|
+
import {
|
3
|
+
type JSX,
|
4
|
+
forwardRef,
|
5
|
+
useCallback,
|
6
|
+
useEffect,
|
7
|
+
useMemo,
|
8
|
+
useRef,
|
9
|
+
useState,
|
10
|
+
} from "react";
|
11
|
+
import { highestResAvailable } from "../../media/index.js";
|
12
|
+
import { useCoState } from "../hooks.js";
|
13
|
+
|
14
|
+
export type ImageProps = Omit<
|
15
|
+
JSX.IntrinsicElements["img"],
|
16
|
+
"src" | "srcSet" | "width" | "height"
|
17
|
+
> & {
|
18
|
+
/** The ID of the ImageDefinition to display */
|
19
|
+
imageId: string;
|
20
|
+
/**
|
21
|
+
* The desired width of the image. Can be a number in pixels or "original" to use the image's original width.
|
22
|
+
* When set to a number, the component will select the best available resolution and maintain aspect ratio.
|
23
|
+
*
|
24
|
+
* @example
|
25
|
+
* ```tsx
|
26
|
+
* // Use original width
|
27
|
+
* <Image imageId="123" width="original" />
|
28
|
+
*
|
29
|
+
* // Set width to 600px, height will be calculated to maintain aspect ratio
|
30
|
+
* <Image imageId="123" width={600} />
|
31
|
+
*
|
32
|
+
* // Set both width and height to maintain aspect ratio
|
33
|
+
* <Image imageId="123" width={600} height={400} />
|
34
|
+
* ```
|
35
|
+
*/
|
36
|
+
width?: number | "original";
|
37
|
+
/**
|
38
|
+
* The desired height of the image. Can be a number in pixels or "original" to use the image's original height.
|
39
|
+
* When set to a number, the component will select the best available resolution and maintain aspect ratio.
|
40
|
+
*
|
41
|
+
* @example
|
42
|
+
* ```tsx
|
43
|
+
* // Use original height
|
44
|
+
* <Image imageId="123" height="original" />
|
45
|
+
*
|
46
|
+
* // Set height to 400px, width will be calculated to maintain aspect ratio
|
47
|
+
* <Image imageId="123" height={400} />
|
48
|
+
*
|
49
|
+
* // Set both width and height to maintain aspect ratio
|
50
|
+
* <Image imageId="123" width={600} height={400} />
|
51
|
+
* ```
|
52
|
+
*/
|
53
|
+
height?: number | "original";
|
54
|
+
};
|
55
|
+
|
56
|
+
/**
|
57
|
+
* A React component for displaying images stored as ImageDefinition CoValues.
|
58
|
+
*
|
59
|
+
* @example
|
60
|
+
* ```tsx
|
61
|
+
* import { Image } from "jazz-tools/react";
|
62
|
+
*
|
63
|
+
* // Force specific dimensions (may crop or stretch)
|
64
|
+
* function Avatar({ imageId }: { imageId: string }) {
|
65
|
+
* return (
|
66
|
+
* <Image
|
67
|
+
* imageId={imageId}
|
68
|
+
* width={100}
|
69
|
+
* height={100}
|
70
|
+
* alt="Avatar"
|
71
|
+
* style={{ borderRadius: "50%", objectFit: "cover" }}
|
72
|
+
* />
|
73
|
+
* );
|
74
|
+
* }
|
75
|
+
* ```
|
76
|
+
*/
|
77
|
+
export const Image = forwardRef<HTMLImageElement, ImageProps>(function Image(
|
78
|
+
{ imageId, width, height, ...props },
|
79
|
+
ref,
|
80
|
+
) {
|
81
|
+
const image = useCoState(ImageDefinition, imageId);
|
82
|
+
const lastBestImage = useRef<[string, string] | null>(null);
|
83
|
+
|
84
|
+
/**
|
85
|
+
* For lazy loading, we use the browser's strategy for images with loading="lazy".
|
86
|
+
* We use an empty image, and when the browser triggers the load event, we load the best available image.
|
87
|
+
* On page loading, if the image url is already in browser's cache, the load event is triggered immediately.
|
88
|
+
* This is why we need to use a different blob url for every image.
|
89
|
+
*/
|
90
|
+
const [waitingLazyLoading, setWaitingLazyLoading] = useState(
|
91
|
+
props.loading === "lazy",
|
92
|
+
);
|
93
|
+
const lazyPlaceholder = useMemo(
|
94
|
+
() =>
|
95
|
+
waitingLazyLoading ? URL.createObjectURL(emptyPixelBlob) : undefined,
|
96
|
+
[waitingLazyLoading],
|
97
|
+
);
|
98
|
+
|
99
|
+
const dimensions: { width: number | undefined; height: number | undefined } =
|
100
|
+
useMemo(() => {
|
101
|
+
const originalWidth = image?.originalSize?.[0];
|
102
|
+
const originalHeight = image?.originalSize?.[1];
|
103
|
+
|
104
|
+
// Both width and height are "original"
|
105
|
+
if (width === "original" && height === "original") {
|
106
|
+
return { width: originalWidth, height: originalHeight };
|
107
|
+
}
|
108
|
+
|
109
|
+
// Width is "original", height is a number
|
110
|
+
if (width === "original" && typeof height === "number") {
|
111
|
+
if (originalWidth && originalHeight) {
|
112
|
+
return {
|
113
|
+
width: Math.round((height * originalWidth) / originalHeight),
|
114
|
+
height,
|
115
|
+
};
|
116
|
+
}
|
117
|
+
return { width: undefined, height };
|
118
|
+
}
|
119
|
+
|
120
|
+
// Height is "original", width is a number
|
121
|
+
if (height === "original" && typeof width === "number") {
|
122
|
+
if (originalWidth && originalHeight) {
|
123
|
+
return {
|
124
|
+
width,
|
125
|
+
height: Math.round((width * originalHeight) / originalWidth),
|
126
|
+
};
|
127
|
+
}
|
128
|
+
return { width, height: undefined };
|
129
|
+
}
|
130
|
+
|
131
|
+
// In all other cases, use the property value:
|
132
|
+
return {
|
133
|
+
width: width === "original" ? originalWidth : width,
|
134
|
+
height: height === "original" ? originalHeight : height,
|
135
|
+
};
|
136
|
+
}, [image?.originalSize, width, height]);
|
137
|
+
|
138
|
+
const src = useMemo(() => {
|
139
|
+
if (waitingLazyLoading) {
|
140
|
+
return lazyPlaceholder;
|
141
|
+
}
|
142
|
+
|
143
|
+
if (!image) return undefined;
|
144
|
+
|
145
|
+
const bestImage = highestResAvailable(
|
146
|
+
image,
|
147
|
+
dimensions.width || dimensions.height || 9999,
|
148
|
+
dimensions.height || dimensions.width || 9999,
|
149
|
+
);
|
150
|
+
|
151
|
+
if (!bestImage) return image.placeholderDataURL;
|
152
|
+
if (lastBestImage.current?.[0] === bestImage.image.id)
|
153
|
+
return lastBestImage.current?.[1];
|
154
|
+
|
155
|
+
const blob = bestImage.image.toBlob();
|
156
|
+
|
157
|
+
if (blob) {
|
158
|
+
const url = URL.createObjectURL(blob);
|
159
|
+
revokeObjectURL(lastBestImage.current?.[1]);
|
160
|
+
lastBestImage.current = [bestImage.image.id, url];
|
161
|
+
return url;
|
162
|
+
}
|
163
|
+
|
164
|
+
return image.placeholderDataURL;
|
165
|
+
}, [image, dimensions.width, dimensions.height, waitingLazyLoading]);
|
166
|
+
|
167
|
+
const onThresholdReached = useCallback(() => {
|
168
|
+
setWaitingLazyLoading(false);
|
169
|
+
}, []);
|
170
|
+
|
171
|
+
// Revoke object URL when component unmounts
|
172
|
+
useEffect(
|
173
|
+
() => () => {
|
174
|
+
// In development mode we don't revokeObjectURL on unmount because
|
175
|
+
// it triggers twice under StrictMode.
|
176
|
+
if (process.env.NODE_ENV === "development") return;
|
177
|
+
revokeObjectURL(lastBestImage.current?.[1]);
|
178
|
+
},
|
179
|
+
[],
|
180
|
+
);
|
181
|
+
|
182
|
+
return (
|
183
|
+
<img
|
184
|
+
ref={ref}
|
185
|
+
src={src}
|
186
|
+
width={dimensions.width}
|
187
|
+
height={dimensions.height}
|
188
|
+
onLoad={waitingLazyLoading ? onThresholdReached : undefined}
|
189
|
+
{...props}
|
190
|
+
/>
|
191
|
+
);
|
192
|
+
});
|
193
|
+
|
194
|
+
function revokeObjectURL(url: string | undefined) {
|
195
|
+
if (url && url.startsWith("blob:")) {
|
196
|
+
URL.revokeObjectURL(url);
|
197
|
+
}
|
198
|
+
}
|
199
|
+
|
200
|
+
const emptyPixelBlob = new Blob(
|
201
|
+
[
|
202
|
+
Uint8Array.from(
|
203
|
+
atob(
|
204
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
205
|
+
),
|
206
|
+
(c) => c.charCodeAt(0),
|
207
|
+
),
|
208
|
+
],
|
209
|
+
{ type: "image/png" },
|
210
|
+
);
|
package/src/react/ssr.ts
CHANGED
@@ -2,9 +2,7 @@ import { WebSocketPeerWithReconnection } from "cojson-transport-ws";
|
|
2
2
|
import { PureJSCrypto } from "cojson/dist/crypto/PureJSCrypto";
|
3
3
|
import { createAnonymousJazzContext } from "jazz-tools";
|
4
4
|
|
5
|
-
export function createSSRJazzAgent(opts: {
|
6
|
-
peer: string;
|
7
|
-
}) {
|
5
|
+
export function createSSRJazzAgent(opts: { peer: string }) {
|
8
6
|
const ssrNode = createAnonymousJazzContext({
|
9
7
|
crypto: new PureJSCrypto(),
|
10
8
|
peersToLoadFrom: [],
|