@oxy.so/services 12.1.0 → 12.2.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/lib/commonjs/assets/assets/one/floral.jpg.d.ts +1 -1
- package/lib/commonjs/ui/components/OxyProvider.js +6 -5
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/components/SafeAreaRoot.js +139 -0
- package/lib/commonjs/ui/components/SafeAreaRoot.js.map +1 -0
- package/lib/commonjs/ui/components/fileManagement/FileViewer.js +17 -14
- package/lib/commonjs/ui/components/fileManagement/FileViewer.js.map +1 -1
- package/lib/commonjs/ui/components/photogrid/JustifiedPhotoGrid.js +10 -17
- package/lib/commonjs/ui/components/photogrid/JustifiedPhotoGrid.js.map +1 -1
- package/lib/commonjs/ui/index.js +0 -37
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/navigation/surfaces.js +2 -2
- package/lib/commonjs/ui/one-video.js +3 -2
- package/lib/commonjs/ui/one-video.js.map +1 -1
- package/lib/commonjs/ui/screens/AvatarCropScreen.js +39 -25
- package/lib/commonjs/ui/screens/AvatarCropScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +4 -2
- package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/OxyOneScreen.js +3 -2
- package/lib/commonjs/ui/screens/OxyOneScreen.js.map +1 -1
- package/lib/commonjs/ui/utils/imageSize.js +31 -0
- package/lib/commonjs/ui/utils/imageSize.js.map +1 -0
- package/lib/commonjs/ui/utils/imageSize.native.js +31 -0
- package/lib/commonjs/ui/utils/imageSize.native.js.map +1 -0
- package/lib/module/assets/assets/one/floral.jpg.d.ts +1 -1
- package/lib/module/ui/components/OxyProvider.js +6 -5
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/SafeAreaRoot.js +135 -0
- package/lib/module/ui/components/SafeAreaRoot.js.map +1 -0
- package/lib/module/ui/components/fileManagement/FileViewer.js +18 -15
- package/lib/module/ui/components/fileManagement/FileViewer.js.map +1 -1
- package/lib/module/ui/components/photogrid/JustifiedPhotoGrid.js +11 -18
- package/lib/module/ui/components/photogrid/JustifiedPhotoGrid.js.map +1 -1
- package/lib/module/ui/index.js +5 -3
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/navigation/surfaces.js +2 -2
- package/lib/module/ui/one-video.js +4 -3
- package/lib/module/ui/one-video.js.map +1 -1
- package/lib/module/ui/screens/AvatarCropScreen.js +41 -25
- package/lib/module/ui/screens/AvatarCropScreen.js.map +1 -1
- package/lib/module/ui/screens/EditProfileFieldScreen.js +5 -3
- package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
- package/lib/module/ui/screens/OxyOneScreen.js +4 -3
- package/lib/module/ui/screens/OxyOneScreen.js.map +1 -1
- package/lib/module/ui/utils/imageSize.js +26 -0
- package/lib/module/ui/utils/imageSize.js.map +1 -0
- package/lib/module/ui/utils/imageSize.native.js +26 -0
- package/lib/module/ui/utils/imageSize.native.js.map +1 -0
- package/lib/typescript/assets/one/floral.jpg.d.ts +1 -1
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts +3 -2
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/SafeAreaRoot.d.ts +28 -0
- package/lib/typescript/commonjs/ui/components/SafeAreaRoot.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/fileManagement/FileViewer.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/photogrid/JustifiedPhotoGrid.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/one-video.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/AvatarCropScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/OxyOneScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/imageSize.d.ts +6 -0
- package/lib/typescript/commonjs/ui/utils/imageSize.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/imageSize.native.d.ts +6 -0
- package/lib/typescript/commonjs/ui/utils/imageSize.native.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/OxyProvider.d.ts +3 -2
- package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/SafeAreaRoot.d.ts +28 -0
- package/lib/typescript/module/ui/components/SafeAreaRoot.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/fileManagement/FileViewer.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/photogrid/JustifiedPhotoGrid.d.ts.map +1 -1
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/one-video.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/AvatarCropScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/OxyOneScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/imageSize.d.ts +6 -0
- package/lib/typescript/module/ui/utils/imageSize.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/imageSize.native.d.ts +6 -0
- package/lib/typescript/module/ui/utils/imageSize.native.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/assets/one/floral.jpg.d.ts +1 -1
- package/src/ui/components/OxyProvider.tsx +7 -6
- package/src/ui/components/SafeAreaRoot.tsx +123 -0
- package/src/ui/components/__tests__/SafeAreaRoot.test.tsx +162 -0
- package/src/ui/components/fileManagement/FileViewer.tsx +18 -13
- package/src/ui/components/photogrid/JustifiedPhotoGrid.tsx +13 -24
- package/src/ui/index.ts +5 -4
- package/src/ui/navigation/surfaces.ts +2 -2
- package/src/ui/one-video.tsx +3 -2
- package/src/ui/screens/AvatarCropScreen.tsx +30 -25
- package/src/ui/screens/EditProfileFieldScreen.tsx +7 -2
- package/src/ui/screens/OxyOneScreen.tsx +4 -3
- package/src/ui/utils/__tests__/imageSize.test.ts +61 -0
- package/src/ui/utils/imageSize.native.ts +26 -0
- package/src/ui/utils/imageSize.ts +26 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React, { useMemo, useState, useCallback } from 'react';
|
|
2
|
-
import { View,
|
|
2
|
+
import { View, useWindowDimensions, type LayoutChangeEvent } from 'react-native';
|
|
3
3
|
import { Text } from '@oxy.so/bloom/typography';
|
|
4
4
|
import { useQuery, keepPreviousData } from '@tanstack/react-query';
|
|
5
5
|
import type { FileMetadata } from '@oxy.so/core';
|
|
6
|
+
import { getImageSize } from '../../utils/imageSize';
|
|
6
7
|
|
|
7
8
|
type PhotoSize = { width: number; height: number };
|
|
8
9
|
|
|
@@ -11,7 +12,7 @@ const EMPTY_DIMENSIONS: Record<string, PhotoSize> = {};
|
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Measure the intrinsic size of every photo whose thumbnail URL has resolved,
|
|
14
|
-
* via `
|
|
15
|
+
* via `getImageSize` (expo-image on a device). Pure/async (no React state) so it can be a React Query
|
|
15
16
|
* `queryFn`. Photos without a resolved URL are skipped and re-measured on the
|
|
16
17
|
* next signature change; a load error falls back to 1×1 (→ the 4/3 default).
|
|
17
18
|
*/
|
|
@@ -21,27 +22,15 @@ async function measurePhotoDimensions(
|
|
|
21
22
|
): Promise<Record<string, PhotoSize>> {
|
|
22
23
|
const measured: Record<string, PhotoSize> = {};
|
|
23
24
|
await Promise.all(
|
|
24
|
-
photos.map(
|
|
25
|
-
(photo)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
url,
|
|
34
|
-
(width, height) => {
|
|
35
|
-
measured[photo.id] = { width, height };
|
|
36
|
-
resolve();
|
|
37
|
-
},
|
|
38
|
-
() => {
|
|
39
|
-
measured[photo.id] = { width: 1, height: 1 };
|
|
40
|
-
resolve();
|
|
41
|
-
},
|
|
42
|
-
);
|
|
43
|
-
}),
|
|
44
|
-
),
|
|
25
|
+
photos.map(async (photo) => {
|
|
26
|
+
const url = getThumbUrl(photo);
|
|
27
|
+
if (!url) return;
|
|
28
|
+
try {
|
|
29
|
+
measured[photo.id] = await getImageSize(url);
|
|
30
|
+
} catch {
|
|
31
|
+
measured[photo.id] = { width: 1, height: 1 };
|
|
32
|
+
}
|
|
33
|
+
}),
|
|
45
34
|
);
|
|
46
35
|
return measured;
|
|
47
36
|
}
|
|
@@ -87,7 +76,7 @@ export interface JustifiedPhotoGridProps {
|
|
|
87
76
|
* Responsive justified photo grid that stretches to the provided containerWidth.
|
|
88
77
|
* Uses flex rows with proportional children widths instead of absolute pixel widths so it always fills.
|
|
89
78
|
*
|
|
90
|
-
* Owns its own intrinsic-dimension measurement: it calls `
|
|
79
|
+
* Owns its own intrinsic-dimension measurement: it calls `getImageSize` on each
|
|
91
80
|
* photo's resolved thumbnail URL and reflows once real aspect ratios are known
|
|
92
81
|
* (falling back to 4/3 until then). Measurement re-runs whenever a photo is added
|
|
93
82
|
* OR its (asynchronously-resolved) private-safe URL first appears.
|
package/src/ui/index.ts
CHANGED
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
/// <reference path="../types/react-native-classname.d.ts" />
|
|
16
16
|
/// <reference path="../types/react-native-web-style.d.ts" />
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
// No screen is imported here. `OxyProvider` loads the account screens as their
|
|
19
|
+
// own chunk at startup (`loadScreens`), and a surface opened before that chunk
|
|
20
|
+
// arrives waits for it. A static `screenComponents` import here put all of them
|
|
21
|
+
// in the first-paint bundle of every app that imports anything from `ui`
|
|
22
|
+
// (Allo: +340 KB gzipped).
|
|
22
23
|
|
|
23
24
|
// Components
|
|
24
25
|
export { default as OxyProvider } from './components/OxyProvider';
|
|
@@ -167,8 +167,8 @@ function presentInternal<K extends RouteName>(
|
|
|
167
167
|
bloomOpts,
|
|
168
168
|
);
|
|
169
169
|
|
|
170
|
-
// Never mount a partially-loaded frame.
|
|
171
|
-
//
|
|
170
|
+
// Never mount a partially-loaded frame. The root entry takes the sync path;
|
|
171
|
+
// `ui` and `ui/client` preload at provider startup, normally before the first press.
|
|
172
172
|
const result = screensReady()
|
|
173
173
|
? open()
|
|
174
174
|
: loadScreens().then(() => {
|
package/src/ui/one-video.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Optional video entry: never re-export from services or its UI root barrels. */
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
|
-
import { AppState,
|
|
3
|
+
import { AppState, StyleSheet, View } from 'react-native';
|
|
4
|
+
import { Image as ExpoImage } from 'expo-image';
|
|
4
5
|
import { useVideoPlayer } from 'expo-video';
|
|
5
6
|
import { VideoView } from '@oxy.so/bloom/video-view';
|
|
6
7
|
import { useReducedMotion } from 'react-native-reanimated';
|
|
@@ -29,7 +30,7 @@ export function OxyOneVideo() {
|
|
|
29
30
|
accessibilityElementsHidden
|
|
30
31
|
importantForAccessibility="no-hide-descendants"
|
|
31
32
|
>
|
|
32
|
-
<
|
|
33
|
+
<ExpoImage source={floralPoster} contentFit="cover" style={StyleSheet.absoluteFill} />
|
|
33
34
|
{!reducedMotion && (
|
|
34
35
|
<VideoView
|
|
35
36
|
player={player}
|
|
@@ -33,7 +33,7 @@ import { RiSubtractLine } from '@oxy.so/bloom/icons/RiSubtractLine';
|
|
|
33
33
|
|
|
34
34
|
import type React from 'react';
|
|
35
35
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
36
|
-
import { View, StyleSheet,
|
|
36
|
+
import { View, StyleSheet, Platform, AccessibilityInfo } from 'react-native';
|
|
37
37
|
import { Text } from '@oxy.so/bloom/typography';
|
|
38
38
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
39
39
|
import Animated, {
|
|
@@ -46,6 +46,7 @@ import Animated, {
|
|
|
46
46
|
withTiming,
|
|
47
47
|
} from 'react-native-reanimated';
|
|
48
48
|
import { useQuery, keepPreviousData } from '@tanstack/react-query';
|
|
49
|
+
import { Image as ExpoImage } from 'expo-image';
|
|
49
50
|
import { useTheme } from '@oxy.so/bloom/theme';
|
|
50
51
|
import { logger } from '@oxy.so/core';
|
|
51
52
|
import { useOxy } from '../context/oxyRuntimeContext';
|
|
@@ -54,6 +55,7 @@ import { useReduceMotion } from '../hooks/useReduceMotion';
|
|
|
54
55
|
import { useSurfaceHeader, type SurfaceHeaderContent } from '../hooks/useSurfaceHeader';
|
|
55
56
|
import { toast } from '@oxy.so/bloom/toast';
|
|
56
57
|
import type { BaseScreenProps } from '../types/navigation';
|
|
58
|
+
import { getImageSize } from '../utils/imageSize';
|
|
57
59
|
|
|
58
60
|
/** Component name used in `logger` context for filtered diagnostics. */
|
|
59
61
|
const LOG_COMPONENT = 'AvatarCropScreen';
|
|
@@ -138,25 +140,24 @@ interface ImageNaturalSize {
|
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
/**
|
|
141
|
-
* Measure an image's intrinsic size via `
|
|
142
|
-
* it can be a React Query `queryFn` (no component
|
|
143
|
-
*
|
|
144
|
-
* Mirrors the `measurePhotoDimensions` helper the
|
|
143
|
+
* Measure an image's intrinsic size via `getImageSize` (expo-image on a
|
|
144
|
+
* device), as a Promise so it can be a React Query `queryFn` (no component
|
|
145
|
+
* state / effects). Rejects on a load failure or degenerate dimensions so the
|
|
146
|
+
* query surfaces `isError`. Mirrors the `measurePhotoDimensions` helper the
|
|
147
|
+
* photo grid uses.
|
|
145
148
|
*/
|
|
146
|
-
function measureImageSize(uri: string): Promise<ImageNaturalSize> {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
);
|
|
159
|
-
});
|
|
149
|
+
async function measureImageSize(uri: string): Promise<ImageNaturalSize> {
|
|
150
|
+
let size: ImageNaturalSize;
|
|
151
|
+
try {
|
|
152
|
+
size = await getImageSize(uri);
|
|
153
|
+
} catch {
|
|
154
|
+
throw new Error('Image measurement failed');
|
|
155
|
+
}
|
|
156
|
+
const { width, height } = size;
|
|
157
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
158
|
+
throw new Error('Image reported invalid dimensions');
|
|
159
|
+
}
|
|
160
|
+
return { width, height };
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
/**
|
|
@@ -381,7 +382,7 @@ const AvatarCropScreen: React.FC<AvatarCropScreenProps> = ({
|
|
|
381
382
|
|
|
382
383
|
// Natural size of the source image. Known up front (device/camera sources pass
|
|
383
384
|
// the dimensions) OR measured lazily from `effectiveUri` via a React Query read
|
|
384
|
-
// (no effect) — the same
|
|
385
|
+
// (no effect) — the same getImageSize→useQuery pattern the photo grid uses.
|
|
385
386
|
const hasPassedDimensions = !!(sourceWidth && sourceHeight);
|
|
386
387
|
const measureQuery = useQuery({
|
|
387
388
|
queryKey: ['avatarCropMeasure', effectiveUri],
|
|
@@ -1047,11 +1048,15 @@ const AvatarCropScreen: React.FC<AvatarCropScreenProps> = ({
|
|
|
1047
1048
|
<GestureDetector gesture={composedGesture}>
|
|
1048
1049
|
<View style={styles.viewport}>
|
|
1049
1050
|
{baseFit && effectiveUri ? (
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1051
|
+
// Reanimated moves the box; expo-image draws the pixels, so the
|
|
1052
|
+
// crop stage never needs react-native's GIF/WebP decoders.
|
|
1053
|
+
<Animated.View style={[styles.image, imageAnimatedStyle]}>
|
|
1054
|
+
<ExpoImage
|
|
1055
|
+
source={{ uri: effectiveUri }}
|
|
1056
|
+
style={StyleSheet.absoluteFill}
|
|
1057
|
+
contentFit="cover"
|
|
1058
|
+
/>
|
|
1059
|
+
</Animated.View>
|
|
1055
1060
|
) : (
|
|
1056
1061
|
<SpinnerIcon color="#ffffff" />
|
|
1057
1062
|
)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState, useCallback, useMemo } from 'react';
|
|
2
|
-
import { View,
|
|
2
|
+
import { View, StyleSheet, type TextInputProps } from 'react-native';
|
|
3
|
+
import { Image as ExpoImage } from 'expo-image';
|
|
3
4
|
import Ionicons from '../icons/Ionicons';
|
|
4
5
|
import type { BaseScreenProps } from '../types/navigation';
|
|
5
6
|
import { useTheme } from '@oxy.so/bloom/theme';
|
|
@@ -657,7 +658,11 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
|
|
|
657
658
|
description={fieldType === 'links' ? String(item.url) : undefined}
|
|
658
659
|
icon={
|
|
659
660
|
fieldType === 'links' && item.image ? (
|
|
660
|
-
<
|
|
661
|
+
<ExpoImage
|
|
662
|
+
source={{ uri: item.image }}
|
|
663
|
+
style={styles.linkImage}
|
|
664
|
+
contentFit="cover"
|
|
665
|
+
/>
|
|
661
666
|
) : undefined
|
|
662
667
|
}
|
|
663
668
|
rightElement={
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useContext, useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { Linking, View } from 'react-native';
|
|
3
|
+
import { Image as ExpoImage } from 'expo-image';
|
|
3
4
|
import { SubscriptionPlans } from '@oxy.so/bloom/subscription-plans';
|
|
4
5
|
import {
|
|
5
6
|
SegmentedControl,
|
|
@@ -174,9 +175,9 @@ export default function OxyOneScreen() {
|
|
|
174
175
|
productName="Oxy One"
|
|
175
176
|
heroBackground={
|
|
176
177
|
oxyOneArtwork ?? (
|
|
177
|
-
<
|
|
178
|
+
<ExpoImage
|
|
178
179
|
source={floralPoster}
|
|
179
|
-
|
|
180
|
+
contentFit="cover"
|
|
180
181
|
style={{ width: '100%', height: '100%' }}
|
|
181
182
|
/>
|
|
182
183
|
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Both forks of `getImageSize` keep react-native's `Image.getSize` contract —
|
|
3
|
+
* the PIXEL size on success, a rejection when the image cannot load — so the
|
|
4
|
+
* grid, the viewer and the crop stage size their boxes the same on every
|
|
5
|
+
* platform.
|
|
6
|
+
*/
|
|
7
|
+
const loadAsync = jest.fn();
|
|
8
|
+
jest.mock('expo-image', () => ({ Image: { loadAsync } }), { virtual: true });
|
|
9
|
+
|
|
10
|
+
const getSize = jest.fn();
|
|
11
|
+
jest.mock('react-native', () => ({ Image: { getSize } }));
|
|
12
|
+
|
|
13
|
+
import { getImageSize as getNativeImageSize } from '../imageSize.native';
|
|
14
|
+
import { getImageSize as getWebImageSize } from '../imageSize';
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
loadAsync.mockReset();
|
|
18
|
+
getSize.mockReset();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('getImageSize (native: expo-image)', () => {
|
|
22
|
+
it('reports pixels and releases the decoded image', async () => {
|
|
23
|
+
const release = jest.fn();
|
|
24
|
+
loadAsync.mockResolvedValue({ width: 200, height: 150, scale: 2, release });
|
|
25
|
+
|
|
26
|
+
await expect(getNativeImageSize('https://cdn.example/a.gif')).resolves.toEqual({
|
|
27
|
+
width: 400,
|
|
28
|
+
height: 300,
|
|
29
|
+
});
|
|
30
|
+
expect(loadAsync).toHaveBeenCalledWith('https://cdn.example/a.gif');
|
|
31
|
+
expect(release).toHaveBeenCalledTimes(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('treats a missing scale as 1', async () => {
|
|
35
|
+
loadAsync.mockResolvedValue({ width: 64, height: 32, scale: 0, release: jest.fn() });
|
|
36
|
+
await expect(getNativeImageSize('file:///a.webp')).resolves.toEqual({ width: 64, height: 32 });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('rejects when the image cannot load', async () => {
|
|
40
|
+
loadAsync.mockRejectedValue(new Error('decode failed'));
|
|
41
|
+
await expect(getNativeImageSize('https://cdn.example/broken')).rejects.toThrow('decode failed');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('getImageSize (web: react-native-web getSize)', () => {
|
|
46
|
+
it('resolves what getSize reports', async () => {
|
|
47
|
+
getSize.mockImplementation((_uri: string, ok: (w: number, h: number) => void) => ok(640, 480));
|
|
48
|
+
await expect(getWebImageSize('https://cdn.example/a.png')).resolves.toEqual({
|
|
49
|
+
width: 640,
|
|
50
|
+
height: 480,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('rejects through the failure callback', async () => {
|
|
55
|
+
const failure = new Error('404');
|
|
56
|
+
getSize.mockImplementation((_uri: string, _ok: unknown, fail: (error: unknown) => void) =>
|
|
57
|
+
fail(failure),
|
|
58
|
+
);
|
|
59
|
+
await expect(getWebImageSize('https://cdn.example/missing')).rejects.toBe(failure);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native fork of `imageSize.ts`: measures through expo-image (Glide on
|
|
3
|
+
* Android, SDWebImage on iOS), the same pipeline and cache `ExpoImage` draws
|
|
4
|
+
* from. react-native's `Image.getSize` decodes through Fresco, whose GIF and
|
|
5
|
+
* animated-WebP support an Android app ships only while `expo-build-properties`
|
|
6
|
+
* keeps `gifEnabled`/`webpEnabled` on — so this file must never name
|
|
7
|
+
* react-native's `Image` (`__tests__/packaging/rnImageBoundary.test.ts`).
|
|
8
|
+
*/
|
|
9
|
+
import { Image as ExpoImage } from 'expo-image';
|
|
10
|
+
|
|
11
|
+
export interface ImageSize {
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function getImageSize(uri: string): Promise<ImageSize> {
|
|
17
|
+
const ref = await ExpoImage.loadAsync(uri);
|
|
18
|
+
try {
|
|
19
|
+
// `width`/`height` are logical; times `scale` is the pixel size
|
|
20
|
+
// `Image.getSize` reports, which the callers' layouts were written for.
|
|
21
|
+
const scale = ref.scale > 0 ? ref.scale : 1;
|
|
22
|
+
return { width: ref.width * scale, height: ref.height * scale };
|
|
23
|
+
} finally {
|
|
24
|
+
ref.release();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intrinsic pixel size of an image the SDK is about to draw, for the layouts
|
|
3
|
+
* that size a box from it (the justified photo grid, the file viewer, the
|
|
4
|
+
* avatar crop stage). Resolves `{ width, height }`; rejects when the image
|
|
5
|
+
* cannot be loaded, exactly as react-native's `Image.getSize` reports through
|
|
6
|
+
* its failure callback.
|
|
7
|
+
*
|
|
8
|
+
* This is the web (and `tsc`/Vite) fork: react-native-web's `getSize` is an
|
|
9
|
+
* `<img>` probe, which needs no CORS headers — expo-image's web `loadAsync`
|
|
10
|
+
* uses `fetch`, which does. The native fork (`imageSize.native.ts`) measures
|
|
11
|
+
* through expo-image, so no SDK image depends on react-native's GIF/WebP
|
|
12
|
+
* decoders on Android and an app may turn them off
|
|
13
|
+
* (`expo-build-properties` `android.gifEnabled`/`webpEnabled: false`).
|
|
14
|
+
*/
|
|
15
|
+
import { Image } from 'react-native';
|
|
16
|
+
|
|
17
|
+
export interface ImageSize {
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getImageSize(uri: string): Promise<ImageSize> {
|
|
23
|
+
return new Promise<ImageSize>((resolve, reject) => {
|
|
24
|
+
Image.getSize(uri, (width, height) => resolve({ width, height }), reject);
|
|
25
|
+
});
|
|
26
|
+
}
|