@oxy.so/services 12.1.0 → 12.2.1

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.
Files changed (126) hide show
  1. package/lib/commonjs/assets/assets/one/floral.jpg.d.ts +1 -1
  2. package/lib/commonjs/index.js +4 -3
  3. package/lib/commonjs/index.js.map +1 -1
  4. package/lib/commonjs/registerPlatform.js +20 -0
  5. package/lib/commonjs/registerPlatform.js.map +1 -0
  6. package/lib/commonjs/ui/client.js +15 -0
  7. package/lib/commonjs/ui/client.js.map +1 -1
  8. package/lib/commonjs/ui/components/OxyProvider.js +6 -5
  9. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  10. package/lib/commonjs/ui/components/SafeAreaRoot.js +139 -0
  11. package/lib/commonjs/ui/components/SafeAreaRoot.js.map +1 -0
  12. package/lib/commonjs/ui/components/fileManagement/FileViewer.js +17 -14
  13. package/lib/commonjs/ui/components/fileManagement/FileViewer.js.map +1 -1
  14. package/lib/commonjs/ui/components/photogrid/JustifiedPhotoGrid.js +10 -17
  15. package/lib/commonjs/ui/components/photogrid/JustifiedPhotoGrid.js.map +1 -1
  16. package/lib/commonjs/ui/index.js +1 -37
  17. package/lib/commonjs/ui/index.js.map +1 -1
  18. package/lib/commonjs/ui/navigation/surfaces.js +2 -2
  19. package/lib/commonjs/ui/one-video.js +3 -2
  20. package/lib/commonjs/ui/one-video.js.map +1 -1
  21. package/lib/commonjs/ui/screens/AvatarCropScreen.js +39 -25
  22. package/lib/commonjs/ui/screens/AvatarCropScreen.js.map +1 -1
  23. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +4 -2
  24. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
  25. package/lib/commonjs/ui/screens/OxyOneScreen.js +3 -2
  26. package/lib/commonjs/ui/screens/OxyOneScreen.js.map +1 -1
  27. package/lib/commonjs/ui/utils/imageSize.js +31 -0
  28. package/lib/commonjs/ui/utils/imageSize.js.map +1 -0
  29. package/lib/commonjs/ui/utils/imageSize.native.js +31 -0
  30. package/lib/commonjs/ui/utils/imageSize.native.js.map +1 -0
  31. package/lib/module/assets/assets/one/floral.jpg.d.ts +1 -1
  32. package/lib/module/index.js +2 -3
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/registerPlatform.js +19 -0
  35. package/lib/module/registerPlatform.js.map +1 -0
  36. package/lib/module/ui/client.js +7 -0
  37. package/lib/module/ui/client.js.map +1 -1
  38. package/lib/module/ui/components/OxyProvider.js +6 -5
  39. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  40. package/lib/module/ui/components/SafeAreaRoot.js +135 -0
  41. package/lib/module/ui/components/SafeAreaRoot.js.map +1 -0
  42. package/lib/module/ui/components/fileManagement/FileViewer.js +18 -15
  43. package/lib/module/ui/components/fileManagement/FileViewer.js.map +1 -1
  44. package/lib/module/ui/components/photogrid/JustifiedPhotoGrid.js +11 -18
  45. package/lib/module/ui/components/photogrid/JustifiedPhotoGrid.js.map +1 -1
  46. package/lib/module/ui/index.js +8 -3
  47. package/lib/module/ui/index.js.map +1 -1
  48. package/lib/module/ui/navigation/surfaces.js +2 -2
  49. package/lib/module/ui/one-video.js +4 -3
  50. package/lib/module/ui/one-video.js.map +1 -1
  51. package/lib/module/ui/screens/AvatarCropScreen.js +41 -25
  52. package/lib/module/ui/screens/AvatarCropScreen.js.map +1 -1
  53. package/lib/module/ui/screens/EditProfileFieldScreen.js +5 -3
  54. package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
  55. package/lib/module/ui/screens/OxyOneScreen.js +4 -3
  56. package/lib/module/ui/screens/OxyOneScreen.js.map +1 -1
  57. package/lib/module/ui/utils/imageSize.js +26 -0
  58. package/lib/module/ui/utils/imageSize.js.map +1 -0
  59. package/lib/module/ui/utils/imageSize.native.js +26 -0
  60. package/lib/module/ui/utils/imageSize.native.js.map +1 -0
  61. package/lib/typescript/assets/one/floral.jpg.d.ts +1 -1
  62. package/lib/typescript/commonjs/index.d.ts +1 -0
  63. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/registerPlatform.d.ts +2 -0
  65. package/lib/typescript/commonjs/registerPlatform.d.ts.map +1 -0
  66. package/lib/typescript/commonjs/ui/client.d.ts +3 -0
  67. package/lib/typescript/commonjs/ui/client.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts +3 -2
  69. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/ui/components/SafeAreaRoot.d.ts +28 -0
  71. package/lib/typescript/commonjs/ui/components/SafeAreaRoot.d.ts.map +1 -0
  72. package/lib/typescript/commonjs/ui/components/fileManagement/FileViewer.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/ui/components/photogrid/JustifiedPhotoGrid.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/ui/icons/MaterialCommunityIcons.d.ts +1 -1
  75. package/lib/typescript/commonjs/ui/index.d.ts +1 -0
  76. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/ui/one-video.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/ui/screens/AvatarCropScreen.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/ui/screens/OxyOneScreen.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/ui/utils/imageSize.d.ts +6 -0
  82. package/lib/typescript/commonjs/ui/utils/imageSize.d.ts.map +1 -0
  83. package/lib/typescript/commonjs/ui/utils/imageSize.native.d.ts +6 -0
  84. package/lib/typescript/commonjs/ui/utils/imageSize.native.d.ts.map +1 -0
  85. package/lib/typescript/module/index.d.ts +1 -0
  86. package/lib/typescript/module/index.d.ts.map +1 -1
  87. package/lib/typescript/module/registerPlatform.d.ts +2 -0
  88. package/lib/typescript/module/registerPlatform.d.ts.map +1 -0
  89. package/lib/typescript/module/ui/client.d.ts +3 -0
  90. package/lib/typescript/module/ui/client.d.ts.map +1 -1
  91. package/lib/typescript/module/ui/components/OxyProvider.d.ts +3 -2
  92. package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
  93. package/lib/typescript/module/ui/components/SafeAreaRoot.d.ts +28 -0
  94. package/lib/typescript/module/ui/components/SafeAreaRoot.d.ts.map +1 -0
  95. package/lib/typescript/module/ui/components/fileManagement/FileViewer.d.ts.map +1 -1
  96. package/lib/typescript/module/ui/components/photogrid/JustifiedPhotoGrid.d.ts.map +1 -1
  97. package/lib/typescript/module/ui/icons/MaterialCommunityIcons.d.ts +1 -1
  98. package/lib/typescript/module/ui/index.d.ts +1 -0
  99. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  100. package/lib/typescript/module/ui/one-video.d.ts.map +1 -1
  101. package/lib/typescript/module/ui/screens/AvatarCropScreen.d.ts.map +1 -1
  102. package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  103. package/lib/typescript/module/ui/screens/OxyOneScreen.d.ts.map +1 -1
  104. package/lib/typescript/module/ui/utils/imageSize.d.ts +6 -0
  105. package/lib/typescript/module/ui/utils/imageSize.d.ts.map +1 -0
  106. package/lib/typescript/module/ui/utils/imageSize.native.d.ts +6 -0
  107. package/lib/typescript/module/ui/utils/imageSize.native.d.ts.map +1 -0
  108. package/package.json +1 -1
  109. package/src/assets/one/floral.jpg.d.ts +1 -1
  110. package/src/index.ts +2 -3
  111. package/src/registerPlatform.ts +17 -0
  112. package/src/ui/client.ts +7 -0
  113. package/src/ui/components/OxyProvider.tsx +7 -6
  114. package/src/ui/components/SafeAreaRoot.tsx +123 -0
  115. package/src/ui/components/__tests__/SafeAreaRoot.test.tsx +162 -0
  116. package/src/ui/components/fileManagement/FileViewer.tsx +18 -13
  117. package/src/ui/components/photogrid/JustifiedPhotoGrid.tsx +13 -24
  118. package/src/ui/index.ts +7 -3
  119. package/src/ui/navigation/surfaces.ts +2 -2
  120. package/src/ui/one-video.tsx +3 -2
  121. package/src/ui/screens/AvatarCropScreen.tsx +30 -25
  122. package/src/ui/screens/EditProfileFieldScreen.tsx +7 -2
  123. package/src/ui/screens/OxyOneScreen.tsx +4 -3
  124. package/src/ui/utils/__tests__/imageSize.test.ts +61 -0
  125. package/src/ui/utils/imageSize.native.ts +26 -0
  126. package/src/ui/utils/imageSize.ts +26 -0
@@ -1,8 +1,9 @@
1
1
  import React, { useMemo, useState, useCallback } from 'react';
2
- import { View, Image, useWindowDimensions, type LayoutChangeEvent } from 'react-native';
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 `Image.getSize`. Pure/async (no React state) so it can be a React Query
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
- new Promise<void>((resolve) => {
27
- const url = getThumbUrl(photo);
28
- if (!url) {
29
- resolve();
30
- return;
31
- }
32
- Image.getSize(
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 `Image.getSize` on each
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,14 @@
15
15
  /// <reference path="../types/react-native-classname.d.ts" />
16
16
  /// <reference path="../types/react-native-web-style.d.ts" />
17
17
 
18
- import { registerScreens } from './navigation/routes';
19
- import { screenComponents } from './navigation/screenComponents';
18
+ // Must stay the first runtime import: core reads the platform from it.
19
+ import '../registerPlatform';
20
20
 
21
- registerScreens(screenComponents);
21
+ // No screen is imported here. `OxyProvider` loads the account screens as their
22
+ // own chunk at startup (`loadScreens`), and a surface opened before that chunk
23
+ // arrives waits for it. A static `screenComponents` import here put all of them
24
+ // in the first-paint bundle of every app that imports anything from `ui`
25
+ // (Allo: +340 KB gzipped).
22
26
 
23
27
  // Components
24
28
  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. Full UI entries take the sync path;
171
- // the lean entry's startup preload normally finishes before the first press.
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(() => {
@@ -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, Image, StyleSheet, View } from 'react-native';
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
- <Image source={floralPoster} resizeMode="cover" style={StyleSheet.absoluteFill} />
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, Image, Platform, AccessibilityInfo } from 'react-native';
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 `Image.getSize`, wrapped as a Promise so
142
- * it can be a React Query `queryFn` (no component state / effects). Rejects on a
143
- * platform failure or degenerate dimensions so the query surfaces `isError`.
144
- * Mirrors the `measurePhotoDimensions` helper the photo grid uses.
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
- return new Promise<ImageNaturalSize>((resolve, reject) => {
148
- Image.getSize(
149
- uri,
150
- (width, height) => {
151
- if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
152
- reject(new Error('Image reported invalid dimensions'));
153
- return;
154
- }
155
- resolve({ width, height });
156
- },
157
- () => reject(new Error('Image measurement failed')),
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 Image.getSize→useQuery pattern the photo grid uses.
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
- <Animated.Image
1051
- source={{ uri: effectiveUri }}
1052
- style={[styles.image, imageAnimatedStyle]}
1053
- resizeMode="cover"
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, Image, StyleSheet, type TextInputProps } from 'react-native';
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
- <Image source={{ uri: item.image }} style={styles.linkImage} />
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 { Image, Linking, View } from 'react-native';
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
- <Image
178
+ <ExpoImage
178
179
  source={floralPoster}
179
- resizeMode="cover"
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
+ }