react-native-fast-image 10.2.0 → 10.4.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/README.md CHANGED
@@ -71,7 +71,7 @@ npx expo install react-native-fast-image
71
71
 
72
72
  It has native code, so it needs a [development build](https://docs.expo.dev/develop/development-builds/introduction/) (e.g. `npx expo run:ios`), not Expo Go.
73
73
 
74
- Needs React Native 0.83 or later with the New Architecture (Expo SDK 55 or later), iOS/tvOS 15.1 or later, and Android 7.0 (API 24) or later; on tvOS, it works with [React Native tvOS](https://github.com/react-native-tvos/react-native-tvos). Apps on the legacy architecture or older React Native can use FastImage 9 (`npm install react-native-fast-image@9`). Upgrading from FastImage 9: see [Upgrading from 9.x](docs/upgrading.md).
74
+ Needs React Native 0.83 or later with the New Architecture (Expo SDK 55 or later) and React 19, iOS/tvOS 15.1 or later, and Android 7.0 (API 24) or later; on tvOS, it works with [React Native tvOS](https://github.com/react-native-tvos/react-native-tvos). Apps on the legacy architecture or older React Native can use FastImage 9 (`npm install react-native-fast-image@9`). Upgrading from FastImage 9: see [Upgrading from 9.x](docs/upgrading.md).
75
75
 
76
76
  To set the cache's size when the app starts, see [Native config](#native-config).
77
77
 
@@ -161,6 +161,8 @@ A hint for which images to start loading first when several are waiting: `'high'
161
161
  - `'normal'`: the default.
162
162
  - `'high'`: e.g. the image the screen is about.
163
163
 
164
+ On the web, it's the image's `fetchpriority`.
165
+
164
166
  ---
165
167
 
166
168
  ##### `source.cache`
@@ -365,6 +367,22 @@ How the image is filtered when it's drawn smaller or larger than its size (like
365
367
 
366
368
  ---
367
369
 
370
+ #### `loading`
371
+
372
+ **Type:** `'lazy' | 'eager'` · **Default:** `'lazy'` · Web only
373
+
374
+ Whether the image loads as it nears the screen (`'lazy'`) or at once (`'eager'`).
375
+
376
+ ---
377
+
378
+ #### `sizes`
379
+
380
+ **Type:** `string` · **Default:** `'auto, 100vw'` · Web only
381
+
382
+ With several sources, the width the image is shown at, as the image's [`sizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#sizes), so the browser loads the size that fits. The default is the width the image is laid out at, which needs `loading="lazy"` (otherwise, or in browsers without it, the window's width).
383
+
384
+ ---
385
+
368
386
  #### `paused`
369
387
 
370
388
  **Type:** `boolean`
@@ -406,6 +424,20 @@ If you control the images, serve them at the size they're shown (resized on your
406
424
 
407
425
  ---
408
426
 
427
+ #### `dynamicRangeLimit`
428
+
429
+ **Type:** `'standard' | 'constrained' | 'no-limit'` · iOS, Web only
430
+
431
+ How bright HDR photos (e.g. photos taken on an iPhone) can be, on displays that support it:
432
+
433
+ - `'standard'`: in standard range, like the rest of the screen.
434
+ - `'constrained'`: in HDR, their highlights brighter than the rest of the screen but held below the display's peak, e.g. for images among other content (a feed).
435
+ - `'no-limit'`: in full HDR, e.g. for a full-screen photo viewer.
436
+
437
+ Not set, iOS shows them in standard range, and the web as the browser does by default (usually HDR). On iOS and tvOS 17 and later; Android shows them in standard range. On the web it's CSS's `dynamic-range-limit`.
438
+
439
+ ---
440
+
409
441
  #### `blurRadius`
410
442
 
411
443
  **Type:** `number` · **Default:** `0`
@@ -932,14 +964,15 @@ The app needs access to the photo library, which it has if it got the url from t
932
964
 
933
965
  ## Web
934
966
 
935
- FastImage works on the web with [react-native-web](https://necolas.github.io/react-native-web/). Bundlers pick the web version through the package's `browser` field, or the `.web.js` files next to the native ones.
967
+ FastImage works on the web with [react-native-web](https://necolas.github.io/react-native-web/) and React 19 or later. Bundlers pick the web version through the package's `browser` field, or the `.web.js` files next to the native ones.
936
968
 
937
969
  Support is minimal:
938
970
 
939
- - Props that work: `source` (a `uri`, a `require()`d image, or several sizes, with `thumbhash` and `blurDataUri`), `placeholder` (and `defaultSource`), `objectFit`, `objectPosition`, `tintColor`, `blurRadius`, `style`, `onLoadStart`, `onLoad`, `onError`, `onLoadEnd`, and View props such as `testID`, accessibility props, `onLayout` and `pointerEvents`.
940
- - Ignored: `source.headers` (a browser can't send them for an image), `source.priority`, `source.cache`, `source.cacheKey`, `source.memoryCache`, `source.diskCache`, `recyclingKey`, `loop`, `imageRendering`, `paused`, `transition`, `downsample` and `onProgress`.
971
+ - Props that work: `source` (a `uri`, a `require()`d image, or several sizes, with `thumbhash` and `blurDataUri`), `placeholder` (and `defaultSource`), `objectFit`, `objectPosition`, `tintColor`, `blurRadius`, `style`, `source.priority`, the web-only `loading` and `sizes`, `onLoadStart`, `onLoad`, `onError`, `onLoadEnd`, and View props such as `testID`, accessibility props, `onLayout` and `pointerEvents`.
972
+ - Ignored: `source.headers` (a browser can't send them for an image), `source.cache`, `source.cacheKey`, `source.memoryCache`, `source.diskCache`, `recyclingKey`, `loop`, `imageRendering`, `paused`, `transition`, `downsample` and `onProgress`.
941
973
  - The image is an `<img>` element that fills the view inside its borders, with `objectFit` and `objectPosition` as its CSS `object-fit` and `object-position`. `tintColor` is an SVG filter and `blurRadius` a CSS `blur()`, and the placeholder is another `<img>`, under the image until it has loaded. A new `source` gets a new `<img>`, so the previous image doesn't stay while it loads (with several sizes, the browser shows it until the new one has loaded).
942
- - Several sizes are shown with a lazily loaded `<img>` whose `srcset` lists them, with `sizes="auto, 100vw"`: the browser loads the one for the width the image is shown at, in device pixels (usually the smallest that's at least as wide). Browsers that don't support `sizes="auto"` use the viewport's width.
974
+ - Images load lazily unless [`loading`](#loading) is `'eager'`, and `source.priority` `'high'` and `'low'` are the `<img>`'s `fetchpriority`.
975
+ - Several sizes are shown with an `<img>` whose `srcset` lists them, with `sizes="auto, 100vw"` (or [`sizes`](#sizes)): the browser loads the one for the width the image is shown at, in device pixels (usually the smallest that's at least as wide). Browsers that don't support `sizes="auto"`, or an image with `loading="eager"`, use the viewport's width.
943
976
  - `FastImage.preload` loads the images into the browser's cache and resolves with a result per source. `clearMemoryCache`, `clearDiskCache` and `configureCache` resolve without doing anything (the browser manages its cache), and `getCachePath` and `writeToCache` resolve with `{ ok: false, error: 'Not supported on the web' }`.
944
977
 
945
978
  ### ThumbHashes on the web
@@ -389,6 +389,12 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl>
389
389
  view.setDownsample(downsample);
390
390
  }
391
391
 
392
+ // iOS only: Android shows an HDR photo (a gain map) in HDR only when its
393
+ // whole window is in HDR color mode.
394
+ @Override
395
+ public void setDynamicRangeLimit(FastImageViewWithUrl view, @Nullable String dynamicRangeLimit) {
396
+ }
397
+
392
398
  // Which events JS has a handler for (FastImageEvents); with onProgress,
393
399
  // the view gets its download's progress (see onDownloadProgress).
394
400
  @Override
package/dist/index.cjs.js CHANGED
@@ -347,7 +347,7 @@ function builtInTransition() {
347
347
  return resolveTransition(react_native.Platform.OS === "android", NO_TRANSITION);
348
348
  }
349
349
  let defaultTransition;
350
- function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, forwardedRef, ...viewProps }) {
350
+ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, loading: _loading, sizes: _sizes, forwardedRef, ...viewProps }) {
351
351
  var _props$recyclingKey, _defaultTransition;
352
352
  const { fallback, resizeMode: _resizeMode, ...props } = viewProps;
353
353
  if (__DEV__ && fallback !== void 0) warnOnce("fallback", "react-native-fast-image: `fallback` was removed in 10.0, and does nothing: images always load natively. Remove it, or use React Native's Image for those images.", "warn");
@@ -149,6 +149,9 @@ export type FastImageProps = $ReadOnly<{|
149
149
  recyclingKey?: ?string,
150
150
  loop?: ?(boolean | number),
151
151
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
152
+ loading?: ?('lazy' | 'eager'),
153
+ sizes?: ?string,
154
+ dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
152
155
  paused?: ?boolean,
153
156
  transition?: ?(boolean | number | Transition),
154
157
  downsample?: ?boolean,
@@ -278,16 +278,12 @@ function dataUri(value) {
278
278
  }
279
279
  //#endregion
280
280
  //#region src/index.web.tsx
281
- var _useId;
282
281
  const notSupported = {
283
282
  ok: false,
284
283
  error: "Not supported on the web"
285
284
  };
286
- let ids = 0;
287
- const useCounterId = () => (0, react.useState)(() => `${++ids}`)[0];
288
- const useSvgId = (_useId = react.default.useId) !== null && _useId !== void 0 ? _useId : useCounterId;
289
285
  const useClientLayoutEffect = globalThis.document === void 0 ? react.useEffect : react.useLayoutEffect;
290
- function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
286
+ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loading, sizes, dynamicRangeLimit, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
291
287
  var _resolveUri;
292
288
  if (typeof __DEV__ !== "undefined" && __DEV__) warnIfChildren(children);
293
289
  const { accessibilityLabel, "aria-label": ariaLabel, resizeMode: _resizeMode, ...viewProps } = props;
@@ -297,12 +293,13 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
297
293
  const src = sized ? sized.src : resolveUri(source);
298
294
  const srcSet = sized === null || sized === void 0 ? void 0 : sized.srcSet;
299
295
  const key = srcSet !== null && srcSet !== void 0 ? srcSet : src;
296
+ const priority = sourcePriority(source);
300
297
  const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
301
298
  const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
302
299
  const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
303
300
  const [result, setResult] = (0, react.useState)();
304
301
  const ok = result && result.key === key ? result.ok : void 0;
305
- const svgId = useSvgId().replace(/[^\w-]/g, "");
302
+ const svgId = (0, react.useId)().replace(/[^\w-]/g, "");
306
303
  const tintId = `fast-image-tint-${svgId}`;
307
304
  const blurId = `fast-image-blur-${svgId}`;
308
305
  const fit = resolveObjectFit(objectFit, style);
@@ -322,6 +319,9 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
322
319
  }, defaultUri && showDefault ? /* @__PURE__ */ react.default.createElement(Picture, {
323
320
  key: `default ${defaultUri}`,
324
321
  src: defaultUri,
322
+ loading,
323
+ priority,
324
+ dynamicRangeLimit,
325
325
  fit: blurUri && (fit === "scale-down" || fit === "none") ? "contain" : fit,
326
326
  position,
327
327
  filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
@@ -330,7 +330,10 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
330
330
  key: several ? "sizes" : key,
331
331
  src,
332
332
  srcSet,
333
- lazy: !!several,
333
+ sizes,
334
+ loading,
335
+ priority,
336
+ dynamicRangeLimit,
334
337
  fit,
335
338
  position,
336
339
  filter,
@@ -375,6 +378,14 @@ function thumbhashUri(hash) {
375
378
  if (decode) return decode(hash);
376
379
  if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("thumbhash-web", "react-native-fast-image: on the web, a `thumbhash` shows only with `import 'react-native-fast-image/thumbhash'` (it decodes them). Or make it a `blurDataUri` before it reaches the browser (e.g. on your server) with that module's thumbHashToBlurDataUri, which needs no JavaScript in the browser.", "warn");
377
380
  }
381
+ function sourcePriority(source) {
382
+ const values = (Array.isArray(source) ? source : [source]).map((entry) => entry && typeof entry === "object" ? entry.priority : void 0);
383
+ return [
384
+ "high",
385
+ "normal",
386
+ "low"
387
+ ].find((priority) => values.includes(priority));
388
+ }
378
389
  function shared(source, key, preview) {
379
390
  const values = (Array.isArray(source) ? source : [source]).map((entry) => {
380
391
  var _ref;
@@ -429,7 +440,7 @@ const fill = {
429
440
  width: "100%",
430
441
  height: "100%"
431
442
  };
432
- function Picture({ src, srcSet, lazy, fit, position, filter, hidden, alt, onLoad, onError }) {
443
+ function Picture({ src, srcSet, sizes, loading, priority, dynamicRangeLimit, fit, position, filter, hidden, alt, onLoad, onError }) {
433
444
  const image = (0, react.useRef)(null);
434
445
  const mounted = (0, react.useRef)(true);
435
446
  const handled = (0, react.useRef)(void 0);
@@ -466,8 +477,9 @@ function Picture({ src, srcSet, lazy, fit, position, filter, hidden, alt, onLoad
466
477
  ref: image,
467
478
  src,
468
479
  srcSet,
469
- sizes: srcSet ? "auto, 100vw" : void 0,
470
- loading: lazy ? "lazy" : void 0,
480
+ sizes: srcSet ? sizes !== null && sizes !== void 0 ? sizes : "auto, 100vw" : void 0,
481
+ loading: loading === "eager" ? void 0 : "lazy",
482
+ fetchPriority: priority === "high" || priority === "low" ? priority : void 0,
471
483
  alt,
472
484
  draggable: false,
473
485
  style: {
@@ -475,7 +487,8 @@ function Picture({ src, srcSet, lazy, fit, position, filter, hidden, alt, onLoad
475
487
  objectFit: fit,
476
488
  objectPosition: position,
477
489
  filter,
478
- opacity: hidden ? 0 : void 0
490
+ opacity: hidden ? 0 : void 0,
491
+ dynamicRangeLimit
479
492
  },
480
493
  onLoad: (event) => loaded(event.currentTarget),
481
494
  onError: (event) => {
@@ -149,6 +149,9 @@ export type FastImageProps = $ReadOnly<{|
149
149
  recyclingKey?: ?string,
150
150
  loop?: ?(boolean | number),
151
151
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
152
+ loading?: ?('lazy' | 'eager'),
153
+ sizes?: ?string,
154
+ dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
152
155
  paused?: ?boolean,
153
156
  transition?: ?(boolean | number | Transition),
154
157
  downsample?: ?boolean,
package/dist/index.d.ts CHANGED
@@ -35,6 +35,8 @@ export type Source = {
35
35
  * - `'normal'`: the default.
36
36
  * - `'high'`: e.g. the image the screen is about.
37
37
  *
38
+ * On the web, it's the image's `fetchpriority`.
39
+ *
38
40
  * @default 'normal'
39
41
  */
40
42
  priority?: Priority;
@@ -452,6 +454,25 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
452
454
  * @default 'auto'
453
455
  */
454
456
  imageRendering?: 'auto' | 'smooth' | 'pixelated';
457
+ /**
458
+ * Whether the image loads as it nears the screen (`'lazy'`) or at once
459
+ * (`'eager'`).
460
+ *
461
+ * @default 'lazy'
462
+ * @platform web
463
+ */
464
+ loading?: 'lazy' | 'eager';
465
+ /**
466
+ * With several sources, the width the image is shown at, as the image's
467
+ * [`sizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#sizes),
468
+ * so the browser loads the size that fits. The default is the width the
469
+ * image is laid out at, which needs `loading="lazy"` (otherwise, or in
470
+ * browsers without it, the window's width).
471
+ *
472
+ * @default 'auto, 100vw'
473
+ * @platform web
474
+ */
475
+ sizes?: string;
455
476
  /**
456
477
  * Pauses an animated image (GIF, animated WebP, APNG, or animated AVIF) on
457
478
  * the frame it's showing; `false` plays it again from there
@@ -512,6 +533,25 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
512
533
  * @default true
513
534
  */
514
535
  downsample?: boolean;
536
+ /**
537
+ * How bright HDR photos (e.g. photos taken on an iPhone) can be, on
538
+ * displays that support it:
539
+ *
540
+ * - `'standard'`: in standard range, like the rest of the screen.
541
+ * - `'constrained'`: in HDR, their highlights brighter than the rest of
542
+ * the screen but held below the display's peak, e.g. for images among
543
+ * other content (a feed).
544
+ * - `'no-limit'`: in full HDR, e.g. for a full-screen photo viewer.
545
+ *
546
+ * Not set, iOS shows them in standard range, and the web as the browser
547
+ * does by default (usually HDR). On iOS and tvOS 17 and later; Android
548
+ * shows them in standard range. On the web it's CSS's
549
+ * `dynamic-range-limit`.
550
+ *
551
+ * @platform ios
552
+ * @platform web
553
+ */
554
+ dynamicRangeLimit?: 'standard' | 'constrained' | 'no-limit';
515
555
  /**
516
556
  * Blurs the image by this radius, in points (the same radius looks about
517
557
  * the same on iOS and Android). `0` is no blur.
package/dist/index.js CHANGED
@@ -320,7 +320,7 @@ function builtInTransition() {
320
320
  return resolveTransition(Platform.OS === "android", NO_TRANSITION);
321
321
  }
322
322
  let defaultTransition;
323
- function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, forwardedRef, ...viewProps }) {
323
+ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, loading: _loading, sizes: _sizes, forwardedRef, ...viewProps }) {
324
324
  var _props$recyclingKey, _defaultTransition;
325
325
  const { fallback, resizeMode: _resizeMode, ...props } = viewProps;
326
326
  if (__DEV__ && fallback !== void 0) warnOnce("fallback", "react-native-fast-image: `fallback` was removed in 10.0, and does nothing: images always load natively. Remove it, or use React Native's Image for those images.", "warn");
@@ -149,6 +149,9 @@ export type FastImageProps = $ReadOnly<{|
149
149
  recyclingKey?: ?string,
150
150
  loop?: ?(boolean | number),
151
151
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
152
+ loading?: ?('lazy' | 'eager'),
153
+ sizes?: ?string,
154
+ dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
152
155
  paused?: ?boolean,
153
156
  transition?: ?(boolean | number | Transition),
154
157
  downsample?: ?boolean,
package/dist/index.web.js CHANGED
@@ -1,4 +1,4 @@
1
- import React, { Children, forwardRef, memo, useEffect, useLayoutEffect, useRef, useState } from "react";
1
+ import React, { Children, forwardRef, memo, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
2
2
  import { Image, PixelRatio, StyleSheet, View } from "react-native";
3
3
  import { getAssetByID } from "react-native-web/dist/modules/AssetRegistry";
4
4
  //#region src/constants.ts
@@ -251,16 +251,12 @@ function dataUri(value) {
251
251
  }
252
252
  //#endregion
253
253
  //#region src/index.web.tsx
254
- var _useId;
255
254
  const notSupported = {
256
255
  ok: false,
257
256
  error: "Not supported on the web"
258
257
  };
259
- let ids = 0;
260
- const useCounterId = () => useState(() => `${++ids}`)[0];
261
- const useSvgId = (_useId = React.useId) !== null && _useId !== void 0 ? _useId : useCounterId;
262
258
  const useClientLayoutEffect = globalThis.document === void 0 ? useEffect : useLayoutEffect;
263
- function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
259
+ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loading, sizes, dynamicRangeLimit, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
264
260
  var _resolveUri;
265
261
  if (typeof __DEV__ !== "undefined" && __DEV__) warnIfChildren(children);
266
262
  const { accessibilityLabel, "aria-label": ariaLabel, resizeMode: _resizeMode, ...viewProps } = props;
@@ -270,12 +266,13 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
270
266
  const src = sized ? sized.src : resolveUri(source);
271
267
  const srcSet = sized === null || sized === void 0 ? void 0 : sized.srcSet;
272
268
  const key = srcSet !== null && srcSet !== void 0 ? srcSet : src;
269
+ const priority = sourcePriority(source);
273
270
  const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
274
271
  const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
275
272
  const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
276
273
  const [result, setResult] = useState();
277
274
  const ok = result && result.key === key ? result.ok : void 0;
278
- const svgId = useSvgId().replace(/[^\w-]/g, "");
275
+ const svgId = useId().replace(/[^\w-]/g, "");
279
276
  const tintId = `fast-image-tint-${svgId}`;
280
277
  const blurId = `fast-image-blur-${svgId}`;
281
278
  const fit = resolveObjectFit(objectFit, style);
@@ -295,6 +292,9 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
295
292
  }, defaultUri && showDefault ? /* @__PURE__ */ React.createElement(Picture, {
296
293
  key: `default ${defaultUri}`,
297
294
  src: defaultUri,
295
+ loading,
296
+ priority,
297
+ dynamicRangeLimit,
298
298
  fit: blurUri && (fit === "scale-down" || fit === "none") ? "contain" : fit,
299
299
  position,
300
300
  filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
@@ -303,7 +303,10 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
303
303
  key: several ? "sizes" : key,
304
304
  src,
305
305
  srcSet,
306
- lazy: !!several,
306
+ sizes,
307
+ loading,
308
+ priority,
309
+ dynamicRangeLimit,
307
310
  fit,
308
311
  position,
309
312
  filter,
@@ -348,6 +351,14 @@ function thumbhashUri(hash) {
348
351
  if (decode) return decode(hash);
349
352
  if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("thumbhash-web", "react-native-fast-image: on the web, a `thumbhash` shows only with `import 'react-native-fast-image/thumbhash'` (it decodes them). Or make it a `blurDataUri` before it reaches the browser (e.g. on your server) with that module's thumbHashToBlurDataUri, which needs no JavaScript in the browser.", "warn");
350
353
  }
354
+ function sourcePriority(source) {
355
+ const values = (Array.isArray(source) ? source : [source]).map((entry) => entry && typeof entry === "object" ? entry.priority : void 0);
356
+ return [
357
+ "high",
358
+ "normal",
359
+ "low"
360
+ ].find((priority) => values.includes(priority));
361
+ }
351
362
  function shared(source, key, preview) {
352
363
  const values = (Array.isArray(source) ? source : [source]).map((entry) => {
353
364
  var _ref;
@@ -402,7 +413,7 @@ const fill = {
402
413
  width: "100%",
403
414
  height: "100%"
404
415
  };
405
- function Picture({ src, srcSet, lazy, fit, position, filter, hidden, alt, onLoad, onError }) {
416
+ function Picture({ src, srcSet, sizes, loading, priority, dynamicRangeLimit, fit, position, filter, hidden, alt, onLoad, onError }) {
406
417
  const image = useRef(null);
407
418
  const mounted = useRef(true);
408
419
  const handled = useRef(void 0);
@@ -439,8 +450,9 @@ function Picture({ src, srcSet, lazy, fit, position, filter, hidden, alt, onLoad
439
450
  ref: image,
440
451
  src,
441
452
  srcSet,
442
- sizes: srcSet ? "auto, 100vw" : void 0,
443
- loading: lazy ? "lazy" : void 0,
453
+ sizes: srcSet ? sizes !== null && sizes !== void 0 ? sizes : "auto, 100vw" : void 0,
454
+ loading: loading === "eager" ? void 0 : "lazy",
455
+ fetchPriority: priority === "high" || priority === "low" ? priority : void 0,
444
456
  alt,
445
457
  draggable: false,
446
458
  style: {
@@ -448,7 +460,8 @@ function Picture({ src, srcSet, lazy, fit, position, filter, hidden, alt, onLoad
448
460
  objectFit: fit,
449
461
  objectPosition: position,
450
462
  filter,
451
- opacity: hidden ? 0 : void 0
463
+ opacity: hidden ? 0 : void 0,
464
+ dynamicRangeLimit
452
465
  },
453
466
  onLoad: (event) => loaded(event.currentTarget),
454
467
  onError: (event) => {
@@ -149,6 +149,9 @@ export type FastImageProps = $ReadOnly<{|
149
149
  recyclingKey?: ?string,
150
150
  loop?: ?(boolean | number),
151
151
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
152
+ loading?: ?('lazy' | 'eager'),
153
+ sizes?: ?string,
154
+ dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
152
155
  paused?: ?boolean,
153
156
  transition?: ?(boolean | number | Transition),
154
157
  downsample?: ?boolean,
@@ -37,6 +37,7 @@ export interface NativeProps extends ViewProps {
37
37
  transitionBetweenImages?: boolean;
38
38
  transitionSkipOnCacheHit?: string;
39
39
  downsample?: CodegenTypes.WithDefault<boolean, true>;
40
+ dynamicRangeLimit?: string;
40
41
  blurRadius?: CodegenTypes.Float;
41
42
  handledEvents?: CodegenTypes.Int32;
42
43
  onFastImageLoadStart?: CodegenTypes.DirectEventHandler<LoadStartEvent>;
@@ -25,12 +25,15 @@
25
25
  // loaded this way was decoded from, for onLoad, or zero for other images.
26
26
  + (CGSize) sourceSizeOfImage: (UIImage*)image;
27
27
 
28
- // SDWebImage puts an image it decoded smaller from the disk cache, for a view
29
- // that downsamples, into the memory cache under the full-size image's key too
30
- // (SDWebImageManager writes what its original cache query finds back there),
31
- // where a full-size load (a view that doesn't downsample, or a preload) would
32
- // find it: a blurry image, or a preload's size. Drops it there, so that load
33
- // decodes the file from the disk cache. Call it before a full-size load.
34
- + (void) forgetSmallerFullSizeImageForURL: (NSURL*)url context: (SDWebImageContext*)context;
28
+ // Drops the memory cache's image for a load (call it before the load) when
29
+ // the load can't use it, so it decodes the file from the disk cache:
30
+ // - for a full-size load (a view that doesn't downsample, or a preload), a
31
+ // smaller image: SDWebImage puts an image it decoded smaller from the disk
32
+ // cache, for a view that downsamples, under the full-size image's key too
33
+ // (SDWebImageManager writes what its original cache query finds back
34
+ // there), which would show blurry, or be a preload's size.
35
+ // - one decoded the other way, to HDR or SDR (dynamicRangeLimit), since
36
+ // SDWebImage's memory cache key is the same for both.
37
+ + (void) forgetUnusableImageForURL: (NSURL*)url context: (SDWebImageContext*)context;
35
38
 
36
39
  @end
@@ -77,7 +77,7 @@ static CGSize FFFPixelSize(NSData* data) {
77
77
  return [sourceSize isKindOfClass: [FFFSourceSize class]] ? ((FFFSourceSize*) sourceSize).size : CGSizeZero;
78
78
  }
79
79
 
80
- + (void) forgetSmallerFullSizeImageForURL: (NSURL*)url context: (SDWebImageContext*)context {
80
+ + (void) forgetUnusableImageForURL: (NSURL*)url context: (SDWebImageContext*)context {
81
81
  SDWebImageManager* manager = [SDWebImageManager sharedManager];
82
82
  if (!url || ![manager.imageCache isKindOfClass: [SDImageCache class]] || ![manager respondsToSelector: @selector(cacheKeyForURL:context:)]) {
83
83
  return;
@@ -85,12 +85,34 @@ static CGSize FFFPixelSize(NSData* data) {
85
85
  SDImageCache* cache = (SDImageCache*) manager.imageCache;
86
86
  NSString* key = [manager cacheKeyForURL: url context: context];
87
87
  UIImage* image = [cache imageFromMemoryCacheForKey: key];
88
- CGSize fullSize = [self sourceSizeOfImage: image];
89
- if (image && fullSize.width > 0 && fullSize.height > 0 && !CGSizeEqualToSize(image.size, fullSize)) {
88
+ if (image && ([self isSmallerFullSizeImage: image context: context] || [self isOtherDynamicRange: image context: context])) {
90
89
  [cache removeImageFromMemoryForKey: key];
91
90
  }
92
91
  }
93
92
 
93
+ // A smaller image under a full-size load's key.
94
+ + (BOOL) isSmallerFullSizeImage: (UIImage*)image context: (SDWebImageContext*)context {
95
+ if (context[SDWebImageContextImageThumbnailPixelSize]) {
96
+ return NO;
97
+ }
98
+ CGSize fullSize = [self sourceSizeOfImage: image];
99
+ return fullSize.width > 0 && fullSize.height > 0 && !CGSizeEqualToSize(image.size, fullSize);
100
+ }
101
+
102
+ // An image decoded to SDR for an HDR load, or to HDR for an SDR load. Only
103
+ // images SDWebImage decoded say how: not a photo library's, or an app's own
104
+ // loader's, which are kept. One decoded to HDR that came out SDR (most images
105
+ // have no HDR) is the same as an SDR decode, and an HDR file decoded without
106
+ // asking is HDR either way (e.g. an HDR10 HEIC on iOS 15 and 16).
107
+ + (BOOL) isOtherDynamicRange: (UIImage*)image context: (SDWebImageContext*)context {
108
+ if (!image.sd_decodeOptions) {
109
+ return NO;
110
+ }
111
+ BOOL wantsHDR = [context[SDWebImageContextImageDecodeToHDR] boolValue];
112
+ BOOL decodedToHDR = [image.sd_decodeOptions[SDImageCoderDecodeToHDR] boolValue];
113
+ return wantsHDR ? !decodedToHDR : decodedToHDR && image.sd_isHighDynamicRange;
114
+ }
115
+
94
116
  - (instancetype) initWithData: (NSData*)data scale: (CGFloat)scale options: (SDImageCoderOptions*)options {
95
117
  // An SVG isn't decoded smaller: it's a vector image, drawn at any size.
96
118
  // This class can't decode it, and SDWebImage would then give it to the
@@ -305,7 +305,7 @@ static NSDictionary *FFFFile(NSString *path)
305
305
  SDWebImageOptions options = FFFPreloadOptions(source);
306
306
  SDWebImageMutableContext *context = FFFPreloadContext(source);
307
307
  // It looks in the memory cache first, for the full-size image.
308
- [FFFDownsampledImage forgetSmallerFullSizeImageForURL:source.url context:context];
308
+ [FFFDownsampledImage forgetUnusableImageForURL:source.url context:context];
309
309
  if (!source.memoryCache) {
310
310
  // Not decoded (only its header is read, for the size), and not
311
311
  // kept in memory, also when it comes from the disk cache.
@@ -59,6 +59,10 @@ typedef NS_ENUM(NSInteger, FFFObjectFit) {
59
59
  @property (nonatomic, copy) NSString *transitionSkipOnCacheHit;
60
60
  // Decodes images at about the view's size instead of at full size.
61
61
  @property (nonatomic, assign) BOOL downsample;
62
+ // The dynamicRangeLimit prop: standard, constrained or no-limit (nil when
63
+ // not set). With constrained and no-limit, HDR images are decoded to HDR and
64
+ // shown in HDR (iOS and tvOS 17+).
65
+ @property (nonatomic, copy) NSString *dynamicRangeLimit;
62
66
  // Blurs the loaded image by this radius, in points (0 is no blur).
63
67
  @property (nonatomic, assign) CGFloat blurRadius;
64
68
  // objectPosition, as CSS computes it: on each axis the image's left or top
@@ -4,6 +4,7 @@
4
4
  #import <React/RCTUtils.h>
5
5
  #import <SDWebImage/SDWebImageError.h>
6
6
  #import <SDWebImage/SDImageTransformer.h>
7
+ #import <SDWebImage/SDImageCodersManager.h>
7
8
  #import "FFFDownsampledImage.h"
8
9
  #import "FFFThumbHash.h"
9
10
  #import "FFFBoxBlur.h"
@@ -73,6 +74,9 @@
73
74
  // source's), and whether that's the source's (hashImage).
74
75
  @property(nonatomic, assign) BOOL showsPlaceholder;
75
76
  @property(nonatomic, assign) BOOL showsHash;
77
+ // dynamicRangeLimit switched between HDR and SDR: the image is decoded again,
78
+ // quietly (see reloadForDynamicRange).
79
+ @property(nonatomic, assign) BOOL needsDynamicRangeReload;
76
80
 
77
81
  @end
78
82
 
@@ -643,6 +647,14 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
643
647
  if (image.size.width <= 0 || image.size.height <= 0) {
644
648
  return image;
645
649
  }
650
+ // Blurred in standard range (the bitmap below has 8 bits per channel): an
651
+ // HDR image (dynamicRangeLimit) tone mapped to it, rather than its
652
+ // highlights clipped.
653
+ if (@available(iOS 17.0, tvOS 17.0, *)) {
654
+ if (image.isHighDynamicRange) {
655
+ image = [image imageRestrictedToStandardDynamicRange];
656
+ }
657
+ }
646
658
  double fullWidth = MAX(1, round(image.size.width * image.scale * scale));
647
659
  double fullHeight = MAX(1, round(image.size.height * image.scale * scale));
648
660
  // At most the image's larger side (a larger box looks the same, and an
@@ -798,6 +810,33 @@ NSString *FFFErrorMessage(NSError *error)
798
810
  }
799
811
  }
800
812
 
813
+ - (void) setDynamicRangeLimit: (NSString*)dynamicRangeLimit {
814
+ if (_dynamicRangeLimit == dynamicRangeLimit || [_dynamicRangeLimit isEqualToString: dynamicRangeLimit]) {
815
+ return;
816
+ }
817
+ BOOL decodedToHDR = [self decodesToHDR];
818
+ _dynamicRangeLimit = [dynamicRangeLimit copy];
819
+ if (@available(iOS 17.0, tvOS 17.0, *)) {
820
+ // Constrained: the highlights brighter than SDR white, but not as
821
+ // bright as the display can go, as Apple suggests for images among
822
+ // other content.
823
+ self.preferredImageDynamicRange = [_dynamicRangeLimit isEqualToString: @"no-limit"] ? UIImageDynamicRangeHigh
824
+ : [_dynamicRangeLimit isEqualToString: @"constrained"] ? UIImageDynamicRangeConstrainedHigh
825
+ : [_dynamicRangeLimit isEqualToString: @"standard"] ? UIImageDynamicRangeStandard
826
+ : UIImageDynamicRangeUnspecified;
827
+ }
828
+ // Decoded again only to switch between HDR and SDR.
829
+ if ([self decodesToHDR] != decodedToHDR) {
830
+ self.needsDynamicRangeReload = YES;
831
+ }
832
+ }
833
+
834
+ // Whether HDR images are decoded to HDR (dynamicRangeLimit constrained or
835
+ // no-limit), rather than to SDR, their standard range version.
836
+ - (BOOL) decodesToHDR {
837
+ return [_dynamicRangeLimit isEqualToString: @"constrained"] || [_dynamicRangeLimit isEqualToString: @"no-limit"];
838
+ }
839
+
801
840
  - (void) setPlaceholder: (UIImage*)placeholder {
802
841
  if (_placeholder != placeholder) {
803
842
  _placeholder = placeholder;
@@ -865,11 +904,36 @@ NSString *FFFErrorMessage(NSError *error)
865
904
  return;
866
905
  }
867
906
  [self reloadImage];
907
+ } else if (self.needsDynamicRangeReload) {
908
+ [self reloadForDynamicRange];
868
909
  } else {
869
910
  [self reloadIfResized];
870
911
  }
871
912
  }
872
913
 
914
+ // The same image decoded again for a dynamicRangeLimit between HDR and SDR,
915
+ // as a resize is (reloadIfResized): without events once it has loaded, and
916
+ // the image showing stays if the load fails. A display setting shouldn't
917
+ // look like a new load.
918
+ - (void) reloadForDynamicRange {
919
+ self.needsDynamicRangeReload = NO;
920
+ // Nothing to load, it failed, or it waits for the view's size: the next
921
+ // load decodes as asked.
922
+ if (!_source || self.hasErrored || self.waitsForSize) {
923
+ return;
924
+ }
925
+ // A data uri is decoded at once, with its events (see reloadImage).
926
+ if ([[_source.url absoluteString] hasPrefix: @"data:image"]) {
927
+ [self reloadImage];
928
+ return;
929
+ }
930
+ SDWebImageOptions options = [self loadOptions];
931
+ if (self.showsLoadedImage) {
932
+ options |= SDWebImageDelayPlaceholder;
933
+ }
934
+ [self downloadImage: _source options: options context: [self loadContext] events: !self.hasCompleted];
935
+ }
936
+
873
937
  // Whether images are decoded at about the view's size (downsample, and always
874
938
  // for photo library images, which are large and usually shown small: Photos
875
939
  // makes them at the size asked for). Not for `none`, which shows it at its own
@@ -938,6 +1002,7 @@ NSString *FFFErrorMessage(NSError *error)
938
1002
 
939
1003
  - (void) reloadImage {
940
1004
  _needsReload = NO;
1005
+ self.needsDynamicRangeReload = NO;
941
1006
  self.waitsForSize = NO;
942
1007
  self.loadsSwitchedSource = self.switchesSource;
943
1008
  self.switchesSource = NO;
@@ -961,7 +1026,12 @@ NSString *FFFErrorMessage(NSError *error)
961
1026
  self.onFastImageLoadStart(@{});
962
1027
  }
963
1028
  // Use SDWebImage API to support external format like WebP images
964
- UIImage* image = [UIImage sd_imageWithData: [NSData dataWithContentsOfURL: _source.url]];
1029
+ // (as sd_imageWithData does), decoded to HDR or SDR as a
1030
+ // download is (see contextForSource).
1031
+ UIImage* image = [[SDImageCodersManager sharedManager] decodedImageWithData: [NSData dataWithContentsOfURL: _source.url] options: @{
1032
+ SDImageCoderDecodeScaleFactor: @1,
1033
+ SDImageCoderDecodeToHDR: @([self decodesToHDR]),
1034
+ }];
965
1035
  if (!image) {
966
1036
  // Not decodable: fail like a remote image, showing the
967
1037
  // placeholder.
@@ -1070,10 +1140,15 @@ NSString *FFFErrorMessage(NSError *error)
1070
1140
  context[SDWebImageContextDownloadRequestModifier] = source.requestModifier;
1071
1141
  context[SDWebImageContextImageLoader] = source.imageLoader;
1072
1142
  context[SDWebImageContextCacheKeyFilter] = source.cacheKeyFilter;
1143
+ if (@available(iOS 17.0, tvOS 17.0, *)) {
1144
+ // An HDR image (e.g. a photo with a gain map) decoded to HDR, or to
1145
+ // SDR, its standard range version.
1146
+ context[SDWebImageContextImageDecodeToHDR] = @([self decodesToHDR]);
1147
+ }
1073
1148
  if (CGSizeEqualToSize(box, CGSizeZero)) {
1074
1149
  [FFFDownsampledImage addFullSizeToContext: context];
1075
1150
  [self applyCacheOptions: context source: source fullSize: YES];
1076
- [FFFDownsampledImage forgetSmallerFullSizeImageForURL: source.url context: context];
1151
+ [FFFDownsampledImage forgetUnusableImageForURL: source.url context: context];
1077
1152
  return context;
1078
1153
  }
1079
1154
  [FFFDownsampledImage addToContext: context box: box cover: cover];
@@ -1085,6 +1160,7 @@ NSString *FFFErrorMessage(NSError *error)
1085
1160
  context[SDWebImageContextOriginalQueryCacheType] = @(SDImageCacheTypeNone);
1086
1161
  }
1087
1162
  [self applyCacheOptions: context source: source fullSize: NO];
1163
+ [FFFDownsampledImage forgetUnusableImageForURL: source.url context: context];
1088
1164
  return context;
1089
1165
  }
1090
1166
 
@@ -287,6 +287,9 @@ typedef NS_OPTIONS(int32_t, FFFEvent) {
287
287
  if (all || oldViewProps.downsample != newViewProps.downsample) {
288
288
  view.downsample = newViewProps.downsample;
289
289
  }
290
+ if (all || oldViewProps.dynamicRangeLimit != newViewProps.dynamicRangeLimit) {
291
+ view.dynamicRangeLimit = FFFString(newViewProps.dynamicRangeLimit);
292
+ }
290
293
  if (all || oldViewProps.blurRadius != newViewProps.blurRadius) {
291
294
  view.blurRadius = newViewProps.blurRadius;
292
295
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "10.2.0",
3
+ "version": "10.4.0",
4
4
  "description": "FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",
package/src/index.js.flow CHANGED
@@ -149,6 +149,9 @@ export type FastImageProps = $ReadOnly<{|
149
149
  recyclingKey?: ?string,
150
150
  loop?: ?(boolean | number),
151
151
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
152
+ loading?: ?('lazy' | 'eager'),
153
+ sizes?: ?string,
154
+ dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
152
155
  paused?: ?boolean,
153
156
  transition?: ?(boolean | number | Transition),
154
157
  downsample?: ?boolean,
package/src/index.tsx CHANGED
@@ -54,6 +54,8 @@ export type Source = {
54
54
  * - `'normal'`: the default.
55
55
  * - `'high'`: e.g. the image the screen is about.
56
56
  *
57
+ * On the web, it's the image's `fetchpriority`.
58
+ *
57
59
  * @default 'normal'
58
60
  */
59
61
  priority?: Priority
@@ -474,6 +476,25 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
474
476
  * @default 'auto'
475
477
  */
476
478
  imageRendering?: 'auto' | 'smooth' | 'pixelated'
479
+ /**
480
+ * Whether the image loads as it nears the screen (`'lazy'`) or at once
481
+ * (`'eager'`).
482
+ *
483
+ * @default 'lazy'
484
+ * @platform web
485
+ */
486
+ loading?: 'lazy' | 'eager'
487
+ /**
488
+ * With several sources, the width the image is shown at, as the image's
489
+ * [`sizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#sizes),
490
+ * so the browser loads the size that fits. The default is the width the
491
+ * image is laid out at, which needs `loading="lazy"` (otherwise, or in
492
+ * browsers without it, the window's width).
493
+ *
494
+ * @default 'auto, 100vw'
495
+ * @platform web
496
+ */
497
+ sizes?: string
477
498
  /**
478
499
  * Pauses an animated image (GIF, animated WebP, APNG, or animated AVIF) on
479
500
  * the frame it's showing; `false` plays it again from there
@@ -534,6 +555,25 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
534
555
  * @default true
535
556
  */
536
557
  downsample?: boolean
558
+ /**
559
+ * How bright HDR photos (e.g. photos taken on an iPhone) can be, on
560
+ * displays that support it:
561
+ *
562
+ * - `'standard'`: in standard range, like the rest of the screen.
563
+ * - `'constrained'`: in HDR, their highlights brighter than the rest of
564
+ * the screen but held below the display's peak, e.g. for images among
565
+ * other content (a feed).
566
+ * - `'no-limit'`: in full HDR, e.g. for a full-screen photo viewer.
567
+ *
568
+ * Not set, iOS shows them in standard range, and the web as the browser
569
+ * does by default (usually HDR). On iOS and tvOS 17 and later; Android
570
+ * shows them in standard range. On the web it's CSS's
571
+ * `dynamic-range-limit`.
572
+ *
573
+ * @platform ios
574
+ * @platform web
575
+ */
576
+ dynamicRangeLimit?: 'standard' | 'constrained' | 'no-limit'
537
577
  // The radius is converted from points to the bitmap's pixels on both
538
578
  // platforms, so it matches React Native's Image.
539
579
  /**
@@ -811,6 +851,9 @@ function FastImageBase({
811
851
  objectPosition,
812
852
  loop,
813
853
  transition,
854
+ // Web only.
855
+ loading: _loading,
856
+ sizes: _sizes,
814
857
  forwardedRef,
815
858
  ...viewProps
816
859
  }: FastImageProps & { forwardedRef: React.Ref<any> }) {
package/src/index.web.tsx CHANGED
@@ -11,15 +11,17 @@
11
11
  //
12
12
  // Works: source (uri, require(), or several sizes: see sizedSources, and
13
13
  // thumbhash and blurDataUri), placeholder (and defaultSource), objectFit,
14
- // objectPosition, tintColor, blurRadius, style,
15
- // onLoadStart, onLoad, onError, onLoadEnd, and View props (testID,
16
- // accessibility, onLayout, pointerEvents). The native-only props (cache,
17
- // priority, headers, transition, downsample, loop, paused, imageRendering,
18
- // recyclingKey) and onProgress are ignored.
14
+ // objectPosition, tintColor, blurRadius, style, source.priority (as
15
+ // fetchpriority), the web-only loading and sizes, onLoadStart, onLoad,
16
+ // onError, onLoadEnd, and View props (testID, accessibility, onLayout,
17
+ // pointerEvents). The native-only props (cache, headers, transition,
18
+ // downsample, loop, paused, imageRendering, recyclingKey) and onProgress
19
+ // are ignored.
19
20
  import React, {
20
21
  forwardRef,
21
22
  memo,
22
23
  useEffect,
24
+ useId,
23
25
  useLayoutEffect,
24
26
  useRef,
25
27
  useState,
@@ -40,6 +42,7 @@ import type {
40
42
  FastImageStaticProperties,
41
43
  ObjectFit,
42
44
  PreloadResult,
45
+ Priority,
43
46
  Source,
44
47
  SourcePlaceholder,
45
48
  } from './index'
@@ -52,15 +55,6 @@ const notSupported: CachePathResult = {
52
55
  error: 'Not supported on the web',
53
56
  }
54
57
 
55
- // An id that's the same on the server and in the browser: React's useId,
56
- // from React 18 (this package's React types are older), without the
57
- // characters an SVG id can't have (:r0: in React 18, «r0» in React 19). On
58
- // React 17, a counter's (which can differ from the server's).
59
- let ids = 0
60
- const useCounterId = () => useState(() => `${++ids}`)[0]
61
- const useSvgId: () => string =
62
- (React as unknown as { useId?: () => string }).useId ?? useCounterId
63
-
64
58
  // A layout effect in the browser (it runs before the image can show), and an
65
59
  // effect when rendering on a server, where layout effects warn.
66
60
  const useClientLayoutEffect =
@@ -86,6 +80,9 @@ function FastImageBase({
86
80
  children,
87
81
  objectFit,
88
82
  objectPosition,
83
+ loading,
84
+ sizes,
85
+ dynamicRangeLimit,
89
86
  forwardedRef,
90
87
  // On the wrapper, the view the layout and touches are for here (on
91
88
  // native, FastImage's only view).
@@ -125,6 +122,7 @@ function FastImageBase({
125
122
  // one doesn't stay while it loads; with several sizes, the <img> stays,
126
123
  // and the browser shows the previous image until the new one has loaded.
127
124
  const key = srcSet ?? src
125
+ const priority = sourcePriority(source)
128
126
  // The source's blurDataUri, or its ThumbHash made into one (with
129
127
  // `react-native-fast-image/thumbhash`), shows blurred (see PreviewBlur)
130
128
  // in place of `placeholder` (or `placeholder`'s, for a source without its
@@ -144,7 +142,7 @@ function FastImageBase({
144
142
  const ok = result && result.key === key ? result.ok : undefined
145
143
  // The same on the server and in the browser. React's ids have characters
146
144
  // an SVG id can't (:r0: in React 18, «r0» in React 19).
147
- const svgId = useSvgId().replace(/[^\w-]/g, '')
145
+ const svgId = useId().replace(/[^\w-]/g, '')
148
146
  const tintId = `fast-image-tint-${svgId}`
149
147
  const blurId = `fast-image-blur-${svgId}`
150
148
  // objectFit (the prop, then style's).
@@ -179,6 +177,10 @@ function FastImageBase({
179
177
  <Picture
180
178
  key={`default ${defaultUri}`}
181
179
  src={defaultUri}
180
+ // Loaded when the image is (a data uri needs no request).
181
+ loading={loading}
182
+ priority={priority}
183
+ dynamicRangeLimit={dynamicRangeLimit}
182
184
  // A source's placeholder stands for an image whose size
183
185
  // isn't known yet: as contain for scale-down and none,
184
186
  // which show an image at its own size.
@@ -210,7 +212,10 @@ function FastImageBase({
210
212
  key={several ? 'sizes' : key}
211
213
  src={src}
212
214
  srcSet={srcSet}
213
- lazy={!!several}
215
+ sizes={sizes}
216
+ loading={loading}
217
+ priority={priority}
218
+ dynamicRangeLimit={dynamicRangeLimit}
214
219
  fit={fit}
215
220
  position={position}
216
221
  filter={filter}
@@ -257,6 +262,20 @@ function thumbhashUri(hash: string | undefined): string | undefined {
257
262
  return undefined
258
263
  }
259
264
 
265
+ // A source's priority: one source's, or the highest of several sizes'.
266
+ function sourcePriority(
267
+ source: FastImageProps['source'],
268
+ ): Priority | undefined {
269
+ const values = (Array.isArray(source) ? source : [source]).map((entry) =>
270
+ entry && typeof entry === 'object'
271
+ ? (entry as Source).priority
272
+ : undefined,
273
+ )
274
+ return (['high', 'normal', 'low'] as const).find((priority) =>
275
+ values.includes(priority),
276
+ )
277
+ }
278
+
260
279
  // The thumbhash or blurDataUri a source shows: its own, or `placeholder`'s
261
280
  // (preview) if it has neither. With several sources, the one they all show.
262
281
  function shared(
@@ -321,9 +340,9 @@ type PackagerAsset = {
321
340
  // Several sizes of an image, as a srcset (each size's width, times its
322
341
  // scale), from which the browser loads the one for the image's width in
323
342
  // device pixels, usually the smallest at least as wide, as expo-image does.
324
- // sizes="auto" has the browser use the width the image is laid out at; it
325
- // only applies to lazy images, and browsers without it use the next value,
326
- // 100vw (the viewport's width). src is the largest, for browsers without
343
+ // sizes="auto" (the default sizes) has the browser use the width the image
344
+ // is laid out at; it only applies to lazy images, and otherwise, or in
345
+ // browsers without it, the next value, 100vw (the viewport's width). src is the largest, for browsers without
327
346
  // srcset.
328
347
  function sizedSources(sources: Source[]) {
329
348
  const widths = sources
@@ -395,7 +414,10 @@ const fill = {
395
414
  function Picture({
396
415
  src,
397
416
  srcSet,
398
- lazy,
417
+ sizes,
418
+ loading,
419
+ priority,
420
+ dynamicRangeLimit,
399
421
  fit,
400
422
  position,
401
423
  filter,
@@ -406,8 +428,14 @@ function Picture({
406
428
  }: {
407
429
  src: string
408
430
  srcSet?: string
409
- // Several sizes load lazily, for sizes="auto" (see sizedSources).
410
- lazy?: boolean
431
+ // The props (see sizedSources for sizes).
432
+ sizes?: string
433
+ loading?: 'lazy' | 'eager'
434
+ // source.priority, as the <img>'s fetchpriority.
435
+ priority?: Priority
436
+ // The prop, as CSS's dynamic-range-limit (the browser's default when
437
+ // not set).
438
+ dynamicRangeLimit?: 'standard' | 'constrained' | 'no-limit'
411
439
  fit: ObjectFit
412
440
  // objectPosition, as CSS.
413
441
  position: string
@@ -478,8 +506,12 @@ function Picture({
478
506
  ref={image}
479
507
  src={src}
480
508
  srcSet={srcSet}
481
- sizes={srcSet ? 'auto, 100vw' : undefined}
482
- loading={lazy ? 'lazy' : undefined}
509
+ sizes={srcSet ? (sizes ?? 'auto, 100vw') : undefined}
510
+ loading={loading === 'eager' ? undefined : 'lazy'}
511
+ // For 'high' and 'low' (the browser's own for 'normal').
512
+ fetchPriority={
513
+ priority === 'high' || priority === 'low' ? priority : undefined
514
+ }
483
515
  alt={alt}
484
516
  draggable={false}
485
517
  style={{
@@ -488,6 +520,8 @@ function Picture({
488
520
  objectPosition: position,
489
521
  filter,
490
522
  opacity: hidden ? 0 : undefined,
523
+ // Not in React's CSS types yet.
524
+ ...({ dynamicRangeLimit } as object),
491
525
  }}
492
526
  onLoad={(event) =>
493
527
  loaded(event.currentTarget as unknown as LoadedImage)
@@ -70,6 +70,9 @@ export interface NativeProps extends ViewProps {
70
70
  transitionBetweenImages?: boolean
71
71
  transitionSkipOnCacheHit?: string
72
72
  downsample?: CodegenTypes.WithDefault<boolean, true>
73
+ // standard, constrained or no-limit ('' when not set). iOS and tvOS 17+
74
+ // (Android ignores it).
75
+ dynamicRangeLimit?: string
73
76
  blurRadius?: CodegenTypes.Float
74
77
  // The events JS has a handler for, as bits: 1 onFastImageLoadStart,
75
78
  // 2 onFastImageProgress, 4 onFastImageLoad, 8 onFastImageError, 16