react-native-fast-image 10.5.0 → 10.7.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
@@ -101,13 +101,13 @@ const YourImage = () => (
101
101
 
102
102
  ### `FastImage`
103
103
 
104
- It's a single native view, which also takes React Native's View props, such as accessibility props, `testID`, `nativeID`, `onLayout`, `pointerEvents` and `hitSlop`. A ref to it is that view (e.g. for `measure`). It doesn't take children: use [`FastImageBackground`](#fastimagebackground) for content over an image.
104
+ It's a single native view, which also takes React Native's View props, such as accessibility props, `testID`, `nativeID`, `onLayout`, `pointerEvents` and `hitSlop`. A ref to it is that view (e.g. for `measure`), with [`reload()`](#refreload). It doesn't take children: use [`FastImageBackground`](#fastimagebackground) for content over an image.
105
105
 
106
106
  <!-- api:props start (generated from src/ by website/scripts/generate.mts) -->
107
107
 
108
108
  #### `source`
109
109
 
110
- **Type:** `number | Source | Source[]`
110
+ **Type:** `number | Source | (number | Source)[]`
111
111
 
112
112
  Source for the remote image to load: a `Source`, a `require()`d image, or an array of the same image at different sizes.
113
113
 
@@ -129,6 +129,7 @@ When `source` changes, the image that's showing stays until the new one has load
129
129
  - When the view's size changes so that another size fits better, it loads that one (with its load events), and keeps showing the current image until then, without a `transition`.
130
130
  - Each size is cached separately. A `cacheKey` applies to its own entry: give each size its own key (or none), or the sizes would replace each other in the cache.
131
131
  - `FastImage.preload`, `getCachePath` and `writeToCache` take one size: pass the one a view will show. An array fails with `{ ok: false, error }`.
132
+ - A `require()`d image can be one of the sizes: its size is the file's.
132
133
  - An array of one is the same as that source.
133
134
 
134
135
  ---
@@ -291,7 +292,7 @@ Notes:
291
292
 
292
293
  **Type:** `number`
293
294
 
294
- **Deprecated:** Use `placeholder`, which does the same.
295
+ **Deprecated:** Use `placeholder`, which does the same. With both, `placeholder` shows.
295
296
 
296
297
  ---
297
298
 
@@ -536,6 +537,27 @@ If supplied, changes the color of all the non-transparent pixels to the given co
536
537
 
537
538
  <!-- api:props end -->
538
539
 
540
+ ---
541
+
542
+ #### `ref.reload()`
543
+
544
+ Loads the image again, e.g. to try again after `onError`. The image showing stays until the new one has loaded (or the placeholder, if it failed), and the events are sent as for a new source. An image in the cache loads from it. In TypeScript, the ref's type is `FastImageRef`.
545
+
546
+ ```tsx
547
+ const image = useRef<FastImageRef>(null)
548
+
549
+ <FastImage ref={image} source={{ uri }} onError={() => setFailed(true)} />
550
+ {failed && (
551
+ <Button
552
+ title="Try again"
553
+ onPress={() => {
554
+ setFailed(false)
555
+ image.current?.reload()
556
+ }}
557
+ />
558
+ )}
559
+ ```
560
+
539
561
  ### `FastImageBackground`
540
562
 
541
563
  An image with content on top of it: a view that the image fills, with the children on top.
@@ -576,7 +598,7 @@ The image's style.
576
598
 
577
599
  #### `imageRef`
578
600
 
579
- **Type:** `Ref<any>`
601
+ **Type:** `Ref<FastImageRef>`
580
602
 
581
603
  A ref to the image (the FastImage inside).
582
604
 
@@ -21,6 +21,7 @@ import com.bumptech.glide.RequestManager;
21
21
  import com.facebook.react.bridge.ColorPropConverter;
22
22
  import com.facebook.react.bridge.Dynamic;
23
23
  import com.facebook.react.bridge.DynamicFromObject;
24
+ import com.facebook.react.bridge.JavaOnlyArray;
24
25
  import com.facebook.react.bridge.ReadableArray;
25
26
  import com.facebook.react.bridge.ReadableMap;
26
27
  import com.facebook.react.bridge.ReadableType;
@@ -307,23 +308,26 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl>
307
308
 
308
309
  @Override
309
310
  public void setSource(FastImageViewWithUrl view, Dynamic source) {
310
- view.setSource(source.isNull() ? null : source.asMap());
311
+ // One source, or several (an array).
312
+ view.setSource(source.isNull() ? null
313
+ : source.getType() == ReadableType.Array ? source.asArray()
314
+ : JavaOnlyArray.of(source.asMap()));
311
315
  }
312
316
 
313
- @Override
314
- public void setSources(FastImageViewWithUrl view, Dynamic sources) {
315
- view.setSources(sources.isNull() ? null : sources.asArray());
316
- }
317
-
318
- // The `placeholder` prop: a url to load apart from the image, as a source
319
- // (a map), or the resolved asset's uri or a data uri (a string).
317
+ // The `placeholder` prop: a thumbhash or blurDataUri, or a url to load
318
+ // apart from the image, as a source (a map), or the resolved asset's uri
319
+ // or a data uri (a string).
320
320
  @Override
321
321
  public void setPlaceholder(FastImageViewWithUrl view, Dynamic source) {
322
322
  if (source.getType() == ReadableType.Map) {
323
+ ReadableMap map = source.asMap();
324
+ boolean hash = map.hasKey("thumbhash") || map.hasKey("blurDataUri");
323
325
  view.setPlaceholder(null);
324
- view.setPlaceholderSource(source.asMap());
326
+ view.setPlaceholderHash(hash ? FastImageViewWithUrl.sourcePlaceholder(map) : null);
327
+ view.setPlaceholderSource(hash ? null : map);
325
328
  return;
326
329
  }
330
+ view.setPlaceholderHash(null);
327
331
  view.setPlaceholderSource(null);
328
332
  String uri = source.isNull() ? null : source.asString();
329
333
  if (uri != null && FastImageThumbHash.isDataUri(uri)) {
@@ -351,11 +355,6 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl>
351
355
  view.setRecyclingKey(recyclingKey);
352
356
  }
353
357
 
354
- @Override
355
- public void setSourcePlaceholders(FastImageViewWithUrl view, @Nullable ReadableArray placeholders) {
356
- view.setSourcePlaceholders(placeholders);
357
- }
358
-
359
358
  @Override
360
359
  public void setLoopCount(FastImageViewWithUrl view, int loopCount) {
361
360
  view.setLoopCount(loopCount);
@@ -436,6 +435,12 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl>
436
435
  view.setObjectFit(FastImageViewConverter.getScaleType(objectFit));
437
436
  }
438
437
 
438
+ // The ref's reload() (a command).
439
+ @Override
440
+ public void reload(FastImageViewWithUrl view) {
441
+ view.reload();
442
+ }
443
+
439
444
  @Override
440
445
  public void onDropViewInstance(@NonNull FastImageViewWithUrl view) {
441
446
  // Cancels its request, and nothing loads or tracks progress for it
@@ -48,6 +48,7 @@ import com.bumptech.glide.request.transition.Transition;
48
48
  import com.bumptech.glide.request.transition.TransitionFactory;
49
49
  import com.facebook.react.bridge.ReadableArray;
50
50
  import com.facebook.react.bridge.ReadableMap;
51
+ import com.facebook.react.bridge.ReadableType;
51
52
  import com.facebook.react.bridge.WritableMap;
52
53
  import com.facebook.react.bridge.WritableNativeMap;
53
54
  import com.facebook.react.modules.i18nmanager.I18nUtil;
@@ -58,6 +59,10 @@ import com.facebook.react.uimanager.PointerEvents;
58
59
  import com.facebook.react.uimanager.ReactPointerEventsView;
59
60
  import com.facebook.react.uimanager.style.LogicalEdge;
60
61
 
62
+ import java.util.ArrayList;
63
+ import java.util.HashMap;
64
+ import java.util.List;
65
+ import java.util.Objects;
61
66
  import javax.annotation.Nonnull;
62
67
 
63
68
  class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEventsView, ReactHitSlopView {
@@ -213,16 +218,72 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
213
218
  return false;
214
219
  }
215
220
 
216
- public void setSource(@Nullable ReadableMap source) {
217
- mNeedsReload = true;
218
- mPropSource = source;
219
- if (mSources == null) mSource = source;
221
+ // The `source` prop: one source, or several (sizes of one image), each
222
+ // with its thumbhash or blurDataUri if it has one. Only a change besides
223
+ // those loads again: a new thumbhash for the same image (e.g. one that
224
+ // arrives later) shows at once (see updatePlaceholder).
225
+ public void setSource(@Nullable ReadableArray sources) {
226
+ List<Object> key = sourceKey(sources);
227
+ if (!mHasSource || !Objects.equals(key, mSourceKey)) {
228
+ mHasSource = true;
229
+ mSourceKey = key;
230
+ mNeedsReload = true;
231
+ mSources = sources != null && sources.size() > 1 ? sources : null;
232
+ mSourceIndex = -1;
233
+ if (mSources == null) mSource = sources != null && sources.size() == 1 ? map(sources, 0) : null;
234
+ }
235
+ mSourcePlaceholders = new ArrayList<>();
236
+ for (int i = 0; sources != null && i < sources.size(); i++) {
237
+ mSourcePlaceholders.add(sourcePlaceholder(map(sources, i)));
238
+ }
239
+ updatePlaceholder();
240
+ }
241
+
242
+ // The sources without their thumbhashes and blurDataUris, to compare.
243
+ private boolean mHasSource = false;
244
+ @Nullable
245
+ private List<Object> mSourceKey = null;
246
+
247
+ @Nullable
248
+ private static List<Object> sourceKey(@Nullable ReadableArray sources) {
249
+ if (sources == null) return null;
250
+ List<Object> key = new ArrayList<>();
251
+ for (int i = 0; i < sources.size(); i++) {
252
+ ReadableMap source = map(sources, i);
253
+ key.add(source == null ? null : withoutPlaceholders(source));
254
+ }
255
+ return key;
256
+ }
257
+
258
+ // A source without its thumbhash and blurDataUri.
259
+ private static HashMap<String, Object> withoutPlaceholders(ReadableMap source) {
260
+ HashMap<String, Object> map = source.toHashMap();
261
+ map.remove("thumbhash");
262
+ map.remove("blurDataUri");
263
+ return map;
264
+ }
265
+
266
+ @Nullable
267
+ private static ReadableMap map(ReadableArray array, int index) {
268
+ return array.getType(index) == ReadableType.Map ? array.getMap(index) : null;
269
+ }
270
+
271
+ // A source's (or `placeholder`'s) ThumbHash, or its blurDataUri if it's a
272
+ // data uri (JS warns about another), or null.
273
+ @Nullable
274
+ static String sourcePlaceholder(@Nullable ReadableMap source) {
275
+ if (source == null) return null;
276
+ String thumbhash = string(source, "thumbhash");
277
+ if (thumbhash != null && !thumbhash.isEmpty()) return thumbhash;
278
+ String blurDataUri = string(source, "blurDataUri");
279
+ return blurDataUri != null && FastImageThumbHash.isDataUri(blurDataUri) ? blurDataUri : null;
220
280
  }
221
281
 
222
- // The `source` prop. With several `sources`, mSource is the one picked for
223
- // the view's size instead.
224
282
  @Nullable
225
- private ReadableMap mPropSource = null;
283
+ private static String string(ReadableMap map, String key) {
284
+ return map.hasKey(key) && map.getType(key) == ReadableType.String ? map.getString(key) : null;
285
+ }
286
+
226
287
  // Several sources (2 or more) of the same image at different sizes, or
227
288
  // null: the view loads the one whose size is closest to its own.
228
289
  @Nullable
@@ -240,13 +301,6 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
240
301
  @Nullable
241
302
  private FastImageViewManager mManager;
242
303
 
243
- public void setSources(@Nullable ReadableArray sources) {
244
- mNeedsReload = true;
245
- mSources = sources != null && sources.size() > 1 ? sources : null;
246
- mSourceIndex = -1;
247
- if (mSources == null) mSource = mPropSource;
248
- }
249
-
250
304
  // The box the image is drawn in: the view inside its padding, which is the
251
305
  // border widths (see updateBorderPadding).
252
306
  private int contentWidth() {
@@ -310,19 +364,29 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
310
364
  return true;
311
365
  }
312
366
 
313
- public void setPlaceholder(@Nullable Drawable placeholder) {
314
- // None again (the placeholder prop is a url) doesn't reload.
315
- if (placeholder == null && mPlaceholder == null) return;
367
+ // Loads the image again, as a new source would, keeping the image showing
368
+ // until then (the ref's reload()). Before the first props there's nothing
369
+ // to load yet.
370
+ void reload() {
371
+ if (mManager == null) return;
316
372
  mNeedsReload = true;
373
+ onAfterUpdate(mManager, requestManager);
374
+ }
375
+
376
+ // Shows at once, as a new source placeholder does (see updatePlaceholder),
377
+ // without loading the image again.
378
+ public void setPlaceholder(@Nullable Drawable placeholder) {
317
379
  mPlaceholder = placeholder;
380
+ updatePlaceholder();
318
381
  }
319
382
 
320
- // The `sourcePlaceholders` prop, by the source's index ("" for none; one
321
- // for a single source): the picked source's ThumbHash or blurDataUri (a
322
- // data uri) shows instead of the placeholder prop. JS sends them apart from
323
- // the sources, so a new one for the same image doesn't reload it.
383
+ // The sources' ThumbHashes or blurDataUris, by the source's index (null
384
+ // for none): the picked source's shows instead of the placeholder prop.
385
+ private List<String> mSourcePlaceholders = new ArrayList<>();
386
+ // `placeholder` as a thumbhash or blurDataUri: shows for the sources
387
+ // without their own.
324
388
  @Nullable
325
- private ReadableArray mSourcePlaceholders = null;
389
+ private String mPlaceholderHash = null;
326
390
  // What shows while the image loads and if it fails: the picked source's
327
391
  // ThumbHash or blurDataUri decoded (FastImageThumbHash), or the placeholder
328
392
  // prop. FastImage's own drawable either way, which Glide doesn't recycle.
@@ -337,8 +401,9 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
337
401
  @Nullable
338
402
  private String mInvalidPlaceholder = null;
339
403
 
340
- public void setSourcePlaceholders(@Nullable ReadableArray placeholders) {
341
- mSourcePlaceholders = placeholders;
404
+ public void setPlaceholderHash(@Nullable String hash) {
405
+ if (Objects.equals(hash, mPlaceholderHash)) return;
406
+ mPlaceholderHash = hash;
342
407
  updatePlaceholder();
343
408
  }
344
409
 
@@ -450,13 +515,15 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
450
515
  }
451
516
  }
452
517
 
453
- // The picked source's ThumbHash or blurDataUri, or null.
518
+ // The picked source's ThumbHash or blurDataUri, or `placeholder`'s, or
519
+ // null.
454
520
  @Nullable
455
521
  private String pickedSourcePlaceholder() {
456
522
  int index = mSources == null ? 0 : mSourceIndex;
457
- String key = mSourcePlaceholders != null && index >= 0 && index < mSourcePlaceholders.size()
458
- && !mSourcePlaceholders.isNull(index) ? mSourcePlaceholders.getString(index) : null;
459
- return key == null || key.isEmpty() ? null : key;
523
+ // None until one is picked.
524
+ if (index < 0) return null;
525
+ String key = index < mSourcePlaceholders.size() ? mSourcePlaceholders.get(index) : null;
526
+ return key != null ? key : mPlaceholderHash;
460
527
  }
461
528
 
462
529
  // Sets mShownPlaceholder for the picked source. A view showing the previous
@@ -1759,7 +1826,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1759
1826
  // resolved: fire onError and onLoadEnd and show the placeholder prop,
1760
1827
  // as on iOS (#1028, #945).
1761
1828
  if (mSource != null && (imageSource == null || imageSource.getUri().toString().length() == 0)) {
1762
- String error = "Invalid source: " + mSource;
1829
+ // Without its thumbhash or blurDataUri (a data uri can be long).
1830
+ String error = "Invalid source: " + withoutPlaceholders(mSource);
1763
1831
  WritableMap event = new WritableNativeMap();
1764
1832
  event.putString("error", error);
1765
1833
  FastImageEvents.send(this, REACT_ON_ERROR_EVENT, event);
@@ -0,0 +1,2 @@
1
+ import type React from 'react';
2
+ export declare function composeRefs<T>(...refs: (React.Ref<T> | undefined)[]): React.RefCallback<T>;
package/dist/index.cjs.js CHANGED
@@ -255,6 +255,7 @@ function objectPositionOrCentered(position) {
255
255
  }
256
256
  //#endregion
257
257
  //#region src/specs/FastImageViewNativeComponent.ts
258
+ const Commands = (0, react_native.codegenNativeCommands)({ supportedCommands: ["reload"] });
258
259
  var FastImageViewNativeComponent_default = (0, react_native.codegenNativeComponent)("FastImageView");
259
260
  //#endregion
260
261
  //#region src/specs/NativeFastImageModule.ts
@@ -279,17 +280,28 @@ function dataUri(value) {
279
280
  if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`blurDataUri\` takes a data uri (data:image/…): "${value.slice(0, 60)}" is ignored. For a placeholder loaded from a url, use \`placeholder\`.`, "warn");
280
281
  }
281
282
  //#endregion
282
- //#region src/index.tsx
283
- function withoutPlaceholders(source) {
284
- if (!hasSourcePlaceholder(source)) return source;
285
- const { thumbhash: _thumbhash, blurDataUri: _blurDataUri, ...rest } = source;
286
- return rest;
287
- }
288
- function sourcePlaceholder(source) {
289
- if (!source || typeof source !== "object") return "";
290
- const { thumbhash, blurDataUri } = source;
291
- return thumbhash || dataUri(blurDataUri) || "";
283
+ //#region src/composeRefs.ts
284
+ function composeRefs(...refs) {
285
+ return (value) => {
286
+ const cleanups = refs.map((ref) => {
287
+ if (typeof ref === "function") {
288
+ const cleanup = ref(value);
289
+ return typeof cleanup === "function" ? cleanup : () => {
290
+ ref(null);
291
+ };
292
+ }
293
+ if (ref) {
294
+ ref.current = value;
295
+ return () => {
296
+ ref.current = null;
297
+ };
298
+ }
299
+ });
300
+ return () => cleanups.forEach((cleanup) => cleanup === null || cleanup === void 0 ? void 0 : cleanup());
301
+ };
292
302
  }
303
+ //#endregion
304
+ //#region src/index.tsx
293
305
  const resolvePlaceholder = (placeholder) => {
294
306
  if (typeof placeholder === "string") {
295
307
  if (isImageName(placeholder)) return react_native.Platform.OS === "android" ? placeholder : { uri: placeholder };
@@ -361,28 +373,21 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
361
373
  const resolvedTintColor = tintColor != null ? tintColor : fromStyle(style, "tintColor");
362
374
  const fit = resolveObjectFit(objectFit, style);
363
375
  const position = objectPositionOrCentered(objectPosition);
364
- const shared = hasSourcePlaceholder(placeholder) ? sourcePlaceholder(placeholder) : "";
365
- const { resolvedSource, sources, sourcePlaceholders } = (0, react.useMemo)(() => {
366
- const sources = Array.isArray(source) ? source.length === 1 ? void 0 : source : void 0;
367
- const single = Array.isArray(source) ? source[0] : source;
368
- const placeholders = (sources !== null && sources !== void 0 ? sources : [single]).map((entry) => sourcePlaceholder(entry) || shared);
369
- return {
370
- resolvedSource: sources ? void 0 : react_native.Image.resolveAssetSource(withoutPlaceholders(single)),
371
- sources: (sources === null || sources === void 0 ? void 0 : sources.some(hasSourcePlaceholder)) ? sources.map(withoutPlaceholders) : sources,
372
- sourcePlaceholders: placeholders.some(Boolean) ? placeholders : void 0
373
- };
374
- }, [source, shared]);
375
- const resolvedPlaceholder = (_placeholderSource = placeholderSource(placeholder)) !== null && _placeholderSource !== void 0 ? _placeholderSource : resolvePlaceholder(hasSourcePlaceholder(placeholder) ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
376
+ const resolvedSource = (0, react.useMemo)(() => {
377
+ for (const entry of [source].flat()) dataUri(entry === null || entry === void 0 ? void 0 : entry.blurDataUri);
378
+ const single = Array.isArray(source) && source.length === 1 ? source[0] : source;
379
+ return Array.isArray(single) ? single.map((entry) => react_native.Image.resolveAssetSource(entry)) : react_native.Image.resolveAssetSource(single);
380
+ }, [source]);
381
+ const resolvedPlaceholder = hasSourcePlaceholder(placeholder) ? placeholder.thumbhash || dataUri(placeholder.blurDataUri) ? placeholder : resolvePlaceholder(defaultSource) : (_placeholderSource = placeholderSource(placeholder)) !== null && _placeholderSource !== void 0 ? _placeholderSource : resolvePlaceholder(placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
382
+ const ref = (0, react.useMemo)(() => composeRefs(addReload, forwardedRef), [forwardedRef]);
376
383
  return /* @__PURE__ */ react.default.createElement(FastImageViewNativeComponent_default, {
377
384
  ...props,
378
- ref: forwardedRef,
385
+ ref,
379
386
  style: [styles.image, style],
380
387
  recyclingKey: (_props$recyclingKey = props.recyclingKey) !== null && _props$recyclingKey !== void 0 ? _props$recyclingKey : void 0,
381
388
  loopCount: loopCount(loop),
382
389
  ...resolveTransition(transition, (_defaultTransition = defaultTransition) !== null && _defaultTransition !== void 0 ? _defaultTransition : builtInTransition()),
383
390
  source: resolvedSource,
384
- sources,
385
- sourcePlaceholders,
386
391
  placeholder: resolvedPlaceholder,
387
392
  onFastImageLoadStart: onLoadStart,
388
393
  onFastImageProgress: onProgress,
@@ -404,6 +409,9 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
404
409
  objectPositionYOffset: position.y.offset
405
410
  });
406
411
  }
412
+ function addReload(view) {
413
+ if (view) view.reload = () => Commands.reload(view);
414
+ }
407
415
  const FastImageMemo = (0, react.memo)(FastImageBase);
408
416
  const FastImageComponent = (0, react.forwardRef)((props, ref) => /* @__PURE__ */ react.default.createElement(FastImageMemo, {
409
417
  forwardedRef: ref,
@@ -145,7 +145,7 @@ export type FastImageProps = $ReadOnly<{|
145
145
  onLoadStart?: ?() => void,
146
146
  onProgress?: ?(event: OnProgressEvent) => void,
147
147
 
148
- source?: ?(FastImageSource | Array<FastImageSource> | number),
148
+ source?: ?(FastImageSource | Array<FastImageSource | number> | number),
149
149
  placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
150
150
  // Deprecated: use placeholder, which is the same.
151
151
  defaultSource?: ?number,
@@ -196,4 +196,6 @@ declare export default class FastImage extends React$Component<FastImageProps> {
196
196
  source: FastImageSource,
197
197
  file: string,
198
198
  ) => Promise<CachePathResult>;
199
+ // On a ref: loads the image again, e.g. to try again after onError.
200
+ reload(): void;
199
201
  }
@@ -282,6 +282,27 @@ function dataUri(value) {
282
282
  if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`blurDataUri\` takes a data uri (data:image/…): "${value.slice(0, 60)}" is ignored. For a placeholder loaded from a url, use \`placeholder\`.`, "warn");
283
283
  }
284
284
  //#endregion
285
+ //#region src/composeRefs.ts
286
+ function composeRefs(...refs) {
287
+ return (value) => {
288
+ const cleanups = refs.map((ref) => {
289
+ if (typeof ref === "function") {
290
+ const cleanup = ref(value);
291
+ return typeof cleanup === "function" ? cleanup : () => {
292
+ ref(null);
293
+ };
294
+ }
295
+ if (ref) {
296
+ ref.current = value;
297
+ return () => {
298
+ ref.current = null;
299
+ };
300
+ }
301
+ });
302
+ return () => cleanups.forEach((cleanup) => cleanup === null || cleanup === void 0 ? void 0 : cleanup());
303
+ };
304
+ }
305
+ //#endregion
285
306
  //#region src/index.web.tsx
286
307
  const notSupported = {
287
308
  ok: false,
@@ -298,12 +319,17 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
298
319
  const src = sized ? sized.src : resolveUri(source);
299
320
  const srcSet = sized === null || sized === void 0 ? void 0 : sized.srcSet;
300
321
  const key = srcSet !== null && srcSet !== void 0 ? srcSet : src;
322
+ const [reloads, setReloads] = (0, react.useState)(0);
323
+ const ref = (0, react.useMemo)(() => composeRefs((view) => {
324
+ if (view) view.reload = () => setReloads((count) => count + 1);
325
+ }, forwardedRef), [forwardedRef]);
301
326
  const priority = sourcePriority(source);
302
327
  const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
303
328
  const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
304
329
  const urlPlaceholder = (_placeholderSource = placeholderSource(placeholder)) === null || _placeholderSource === void 0 ? void 0 : _placeholderSource.uri;
330
+ const placeholderKey = `${urlPlaceholder} ${reloads}`;
305
331
  const [failedPlaceholder, setFailedPlaceholder] = (0, react.useState)();
306
- const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview || urlPlaceholder ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
332
+ const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : urlPlaceholder ? void 0 : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
307
333
  const [result, setResult] = (0, react.useState)();
308
334
  const ok = result && result.key === key ? result.ok : void 0;
309
335
  const svgId = (0, react.useId)().replace(/[^\w-]/g, "");
@@ -315,14 +341,14 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
315
341
  const showDefault = ok !== true;
316
342
  useClientLayoutEffect(() => {
317
343
  if (key) onLoadStart === null || onLoadStart === void 0 || onLoadStart();
318
- }, [key]);
344
+ }, [key, reloads]);
319
345
  return /* @__PURE__ */ react.default.createElement(react_native.View, {
320
346
  ...viewProps,
321
347
  "aria-label": label,
322
348
  style: [styles.container, style],
323
349
  onLayout,
324
350
  pointerEvents,
325
- ref: forwardedRef
351
+ ref
326
352
  }, defaultUri && showDefault ? /* @__PURE__ */ react.default.createElement(Picture, {
327
353
  key: `default ${defaultUri}`,
328
354
  src: defaultUri,
@@ -334,7 +360,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
334
360
  filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
335
361
  alt: key ? "" : label !== null && label !== void 0 ? label : ""
336
362
  }) : null, urlPlaceholder && showDefault ? /* @__PURE__ */ react.default.createElement(Picture, {
337
- key: `placeholder ${urlPlaceholder}`,
363
+ key: `placeholder ${placeholderKey}`,
338
364
  src: urlPlaceholder,
339
365
  loading,
340
366
  priority: "high",
@@ -342,11 +368,11 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
342
368
  fit: fit === "scale-down" || fit === "none" ? "contain" : fit,
343
369
  position,
344
370
  filter: tintColor ? `url(#${tintId})` : void 0,
345
- hidden: failedPlaceholder === urlPlaceholder,
346
- onError: () => setFailedPlaceholder(urlPlaceholder),
371
+ hidden: failedPlaceholder === placeholderKey,
372
+ onError: () => setFailedPlaceholder(placeholderKey),
347
373
  alt: ""
348
374
  }) : null, key && src ? /* @__PURE__ */ react.default.createElement(Picture, {
349
- key: several ? "sizes" : key,
375
+ key: `${several ? "sizes" : key} ${reloads}`,
350
376
  src,
351
377
  srcSet,
352
378
  sizes,
@@ -416,10 +442,9 @@ function shared(source, key, preview) {
416
442
  function resolveUri(source) {
417
443
  let uri;
418
444
  if (typeof source === "number") {
419
- var _asset$scales$;
420
445
  const asset = (0, react_native_web_dist_modules_AssetRegistry.getAssetByID)(source);
421
446
  if (!asset) return void 0;
422
- const scale = asset.scales.reduce((best, scale) => Math.abs(scale - react_native.PixelRatio.get()) < Math.abs(best - react_native.PixelRatio.get()) ? scale : best, (_asset$scales$ = asset.scales[0]) !== null && _asset$scales$ !== void 0 ? _asset$scales$ : 1);
447
+ const scale = assetScale(asset);
423
448
  const suffix = scale !== 1 ? `@${scale}x` : "";
424
449
  uri = `${asset.httpServerLocation}/${asset.name}${suffix}.${asset.type}`;
425
450
  } else if (typeof source === "string") uri = source;
@@ -428,9 +453,24 @@ function resolveUri(source) {
428
453
  const svg = uri === null || uri === void 0 ? void 0 : uri.match(/^(data:image\/svg\+xml;utf8,)(.*)/);
429
454
  return svg ? svg[1] + encodeURIComponent(svg[2]) : uri || void 0;
430
455
  }
431
- function sizedSources(sources) {
456
+ function assetScale(asset) {
457
+ var _asset$scales$;
458
+ return asset.scales.reduce((best, scale) => Math.abs(scale - react_native.PixelRatio.get()) < Math.abs(best - react_native.PixelRatio.get()) ? scale : best, (_asset$scales$ = asset.scales[0]) !== null && _asset$scales$ !== void 0 ? _asset$scales$ : 1);
459
+ }
460
+ function sizedSource(source) {
461
+ if (typeof source !== "number") return source;
462
+ const asset = (0, react_native_web_dist_modules_AssetRegistry.getAssetByID)(source);
463
+ return asset ? {
464
+ uri: resolveUri(source),
465
+ width: asset.width,
466
+ height: asset.height,
467
+ scale: assetScale(asset)
468
+ } : void 0;
469
+ }
470
+ function sizedSources(entries) {
432
471
  var _largest$uri, _sources$find;
433
- const widths = sources.filter((source) => (source === null || source === void 0 ? void 0 : source.uri) && source.width).map((source) => {
472
+ const sources = entries.map(sizedSource);
473
+ const widths = sources.filter((source) => !!(source === null || source === void 0 ? void 0 : source.uri) && !!source.width).map((source) => {
434
474
  var _source$width, _source$scale;
435
475
  return {
436
476
  uri: source.uri,
@@ -590,11 +630,10 @@ FastImage.setDefaults = () => {};
590
630
  FastImage.writeToCache = () => Promise.resolve(notSupported);
591
631
  FastImage.getCachePath = () => Promise.resolve(notSupported);
592
632
  const styles = react_native.StyleSheet.create({ container: { overflow: "hidden" } });
593
- const FastImageWithRef = FastImageComponent;
594
633
  const FastImageBackground = (0, react.forwardRef)(({ style, imageStyle, imageRef, children, ...props }, ref) => /* @__PURE__ */ react.default.createElement(react_native.View, {
595
634
  style,
596
635
  ref
597
- }, /* @__PURE__ */ react.default.createElement(FastImageWithRef, {
636
+ }, /* @__PURE__ */ react.default.createElement(FastImageComponent, {
598
637
  ...props,
599
638
  style: [react_native.StyleSheet.absoluteFill, imageStyle],
600
639
  ref: imageRef
@@ -145,7 +145,7 @@ export type FastImageProps = $ReadOnly<{|
145
145
  onLoadStart?: ?() => void,
146
146
  onProgress?: ?(event: OnProgressEvent) => void,
147
147
 
148
- source?: ?(FastImageSource | Array<FastImageSource> | number),
148
+ source?: ?(FastImageSource | Array<FastImageSource | number> | number),
149
149
  placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
150
150
  // Deprecated: use placeholder, which is the same.
151
151
  defaultSource?: ?number,
@@ -196,4 +196,6 @@ declare export default class FastImage extends React$Component<FastImageProps> {
196
196
  source: FastImageSource,
197
197
  file: string,
198
198
  ) => Promise<CachePathResult>;
199
+ // On a ref: loads the image again, e.g. to try again after onError.
200
+ reload(): void;
199
201
  }
package/dist/index.d.ts CHANGED
@@ -331,9 +331,10 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
331
331
  * - `FastImage.preload`, `getCachePath` and `writeToCache` take one size:
332
332
  * pass the one a view will show. An array fails with
333
333
  * `{ ok: false, error }`.
334
+ * - A `require()`d image can be one of the sizes: its size is the file's.
334
335
  * - An array of one is the same as that source.
335
336
  */
336
- source?: Source | Source[] | ImageRequireSource;
337
+ source?: Source | Array<Source | ImageRequireSource> | ImageRequireSource;
337
338
  /**
338
339
  * A placeholder shown while the image loads, and if it fails to load:
339
340
  *
@@ -363,7 +364,8 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
363
364
  */
364
365
  placeholder?: ImageRequireSource | string | SourcePlaceholder | PlaceholderSource;
365
366
  /**
366
- * @deprecated Use `placeholder`, which does the same.
367
+ * @deprecated Use `placeholder`, which does the same. With both,
368
+ * `placeholder` shows.
367
369
  */
368
370
  defaultSource?: ImageRequireSource;
369
371
  /**
@@ -662,6 +664,18 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
662
664
  children?: "FastImage doesn't take children: use FastImageBackground for content over an image";
663
665
  }
664
666
  type ViewRef = React.ComponentRef<typeof View>;
667
+ /**
668
+ * What a ref to `FastImage` gets: its native view, with a View's methods
669
+ * (e.g. `measure`), and `reload()`. E.g. `useRef<FastImageRef>(null)`.
670
+ */
671
+ export type FastImageRef = ViewRef & {
672
+ /**
673
+ * Loads the image again, e.g. to try again after `onError`. The image
674
+ * showing stays until the new one has loaded, and the events are sent as
675
+ * for a new source. An image in the cache loads from it.
676
+ */
677
+ reload(): void;
678
+ };
665
679
  /** A preloaded source that loaded (and is now cached). */
666
680
  export interface PreloadSuccess {
667
681
  /** The source's uri. */
@@ -760,14 +774,14 @@ export interface FastImageDefaults {
760
774
  */
761
775
  transition?: boolean | number | Transition | null;
762
776
  }
763
- declare const FastImage: React.ForwardRefExoticComponent<FastImageProps & React.RefAttributes<ViewRef>> & FastImageStaticProperties;
777
+ declare const FastImage: React.ForwardRefExoticComponent<FastImageProps & React.RefAttributes<FastImageRef>> & FastImageStaticProperties;
764
778
  export interface FastImageBackgroundProps extends Omit<FastImageProps, 'style' | 'children'> {
765
779
  /** The container's style; the image fills it. */
766
780
  style?: StyleProp<ViewStyle>;
767
781
  /** The image's style. */
768
782
  imageStyle?: StyleProp<ImageStyle>;
769
783
  /** A ref to the image (the FastImage inside). */
770
- imageRef?: React.Ref<any>;
784
+ imageRef?: React.Ref<FastImageRef>;
771
785
  /** Content shown on top of the image. */
772
786
  children?: React.ReactNode;
773
787
  }