react-native-fast-image 10.4.0 → 10.5.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
@@ -26,7 +26,7 @@ React Native's `Image` component handles image caching like browsers for the mos
26
26
  - **Responsive images**: give several sizes, and the one that fits the view loads ([`source`](#source)).
27
27
  - **Animated images** (GIF, WebP, APNG and AVIF) that can [loop](#loop) and [pause](#paused).
28
28
  - **Fades** in and between images ([`transition`](#transition)).
29
- - **Placeholders**: a blurred preview from a [ThumbHash](#sourcethumbhash) or a [tiny image](#sourceblurdatauri) while an image loads, or a bundled image ([`placeholder`](#placeholder)).
29
+ - **Placeholders**: a blurred preview from a [ThumbHash](#sourcethumbhash) or a [tiny image](#sourceblurdatauri) while an image loads, a bundled image, or a url, e.g. a thumbnail that's already cached ([`placeholder`](#placeholder)).
30
30
  - [**SVG**](docs/formats.md#svg-images) and [**photo library**](#photo-library-images-ios-and-tvos) images.
31
31
  - [**`FastImageBackground`**](#fastimagebackground) for content over an image.
32
32
  - **Expo and web support**, with a [config plugin](#expo-config-plugin) for Expo.
@@ -266,7 +266,7 @@ With several sources, the scale `width` and `height` are multiplied by.
266
266
 
267
267
  #### `placeholder`
268
268
 
269
- **Type:** `string | number | SourcePlaceholder`
269
+ **Type:** `string | number | PlaceholderSource | SourcePlaceholder`
270
270
 
271
271
  A placeholder shown while the image loads, and if it fails to load:
272
272
 
@@ -275,13 +275,15 @@ A placeholder shown while the image loads, and if it fails to load:
275
275
  - The name of an image bundled with the app (an iOS asset catalog image or an Android drawable): `placeholder="gray"`.
276
276
  - A [ThumbHash](#sourcethumbhash), shown blurred: `placeholder={{ thumbhash }}`.
277
277
  - A tiny image as a data uri, shown blurred: `placeholder={{ blurDataUri }}`.
278
+ - A url, loaded and cached like a source: `placeholder="https://example.com/placeholder.png"`, or a source with `headers` or a `cacheKey`: `placeholder={{ uri, headers }}`.
279
+ - A url only shown if it's already cached, e.g. a list's thumbnail while the full-size image loads: `placeholder={{ uri, cache: 'cacheOnly' }}`.
278
280
 
279
- A source's own `thumbhash` or `blurDataUri` is shown instead, if it has one.
281
+ A source's own `thumbhash` or `blurDataUri` is shown instead, if it has one; a url placeholder shows over it once it has loaded.
280
282
 
281
283
  Notes:
282
284
 
283
- - A url isn't loaded: it's ignored, with a warning in development.
284
285
  - On Android, a `require()`d image doesn't show in debug builds, where the dev server serves it. Release builds show it.
286
+ - On the web, a url loads like any image, also with `'cacheOnly'`: a browser can't look only in its cache.
285
287
 
286
288
  ---
287
289
 
@@ -844,6 +846,14 @@ Where the image is in the view: see [`objectPosition`](#objectposition).
844
846
 
845
847
  ---
846
848
 
849
+ ### `PlaceholderSource`
850
+
851
+ **Type:** `Pick<Source, 'uri' | 'headers' | 'cacheKey' | 'cache'>`
852
+
853
+ A url as `placeholder`, loaded like a source (see `placeholder`).
854
+
855
+ ---
856
+
847
857
  ### `PositionLength`
848
858
 
849
859
  **Type:** `` number | `${number}%` ``
@@ -27,7 +27,7 @@ import java.util.Map;
27
27
  import javax.annotation.Nullable;
28
28
 
29
29
  class FastImageViewConverter {
30
- private static final Drawable TRANSPARENT_DRAWABLE = new ColorDrawable(Color.TRANSPARENT);
30
+ static final Drawable TRANSPARENT_DRAWABLE = new ColorDrawable(Color.TRANSPARENT);
31
31
 
32
32
  private static final Map<String, FastImageCacheControl> FAST_IMAGE_CACHE_CONTROL_MAP =
33
33
  new HashMap<String, FastImageCacheControl>() {{
@@ -315,9 +315,16 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl>
315
315
  view.setSources(sources.isNull() ? null : sources.asArray());
316
316
  }
317
317
 
318
- // The `placeholder` prop: the resolved asset's uri, or a data uri.
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).
319
320
  @Override
320
321
  public void setPlaceholder(FastImageViewWithUrl view, Dynamic source) {
322
+ if (source.getType() == ReadableType.Map) {
323
+ view.setPlaceholder(null);
324
+ view.setPlaceholderSource(source.asMap());
325
+ return;
326
+ }
327
+ view.setPlaceholderSource(null);
321
328
  String uri = source.isNull() ? null : source.asString();
322
329
  if (uri != null && FastImageThumbHash.isDataUri(uri)) {
323
330
  Bitmap bitmap = FastImageThumbHash.dataUri(uri);
@@ -30,6 +30,7 @@ import androidx.annotation.RequiresApi;
30
30
  import androidx.appcompat.widget.AppCompatImageView;
31
31
 
32
32
  import com.bumptech.glide.GenericTransitionOptions;
33
+ import com.bumptech.glide.Priority;
33
34
  import com.bumptech.glide.RequestBuilder;
34
35
  import com.bumptech.glide.RequestManager;
35
36
  import com.bumptech.glide.load.DataSource;
@@ -39,6 +40,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
39
40
  import com.bumptech.glide.load.resource.gif.GifDrawable;
40
41
  import com.bumptech.glide.request.Request;
41
42
  import com.bumptech.glide.request.RequestOptions;
43
+ import com.bumptech.glide.request.target.CustomTarget;
42
44
  import com.bumptech.glide.request.target.DrawableImageViewTarget;
43
45
  import com.bumptech.glide.request.target.SizeReadyCallback;
44
46
  import com.bumptech.glide.request.transition.NoTransition;
@@ -309,6 +311,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
309
311
  }
310
312
 
311
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;
312
316
  mNeedsReload = true;
313
317
  mPlaceholder = placeholder;
314
318
  }
@@ -338,6 +342,114 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
338
342
  updatePlaceholder();
339
343
  }
340
344
 
345
+ // `placeholder` as a url (a source): loaded apart from the image, at the
346
+ // most urgent priority, as it only helps if it comes before the image (or
347
+ // only from Glide's caches, with cache cacheOnly).
348
+ // Once it has, a copy shows instead of the other placeholders: FastImage's
349
+ // own drawable, which Glide doesn't recycle, so a fade can still draw it
350
+ // after its request is cleared. A failure shows nothing more.
351
+ @Nullable
352
+ private ReadableMap mPlaceholderSource = null;
353
+ @Nullable
354
+ private Drawable mUrlPlaceholder = null;
355
+ @Nullable
356
+ private CustomTarget<Drawable> mUrlPlaceholderTarget = null;
357
+
358
+ // Its load waits for the view's size (see startUrlPlaceholderLoad).
359
+ private boolean mUrlPlaceholderWaitsForSize = false;
360
+
361
+ public void setPlaceholderSource(@Nullable ReadableMap source) {
362
+ if (source == null && mPlaceholderSource == null) return;
363
+ mPlaceholderSource = source;
364
+ clearUrlPlaceholderTarget();
365
+ mUrlPlaceholderWaitsForSize = false;
366
+ mUrlPlaceholder = null;
367
+ updatePlaceholder();
368
+ scheduleUrlPlaceholderLoad();
369
+ }
370
+
371
+ // Starts placeholderSource's load once the current callbacks have run: an
372
+ // image from the memory cache has shown by then, and needs none.
373
+ private void scheduleUrlPlaceholderLoad() {
374
+ post(new Runnable() {
375
+ @Override
376
+ public void run() {
377
+ startUrlPlaceholderLoad();
378
+ }
379
+ });
380
+ }
381
+
382
+ // Loads placeholderSource's image while the image loads (or after it
383
+ // failed), at the view's size, once it has one: not if the image showed,
384
+ // or it's already loaded or loading.
385
+ private void startUrlPlaceholderLoad() {
386
+ if (requestManager == null || mDropped || mUrlPlaceholder != null || mUrlPlaceholderTarget != null
387
+ || !FastImageViewConverter.hasUri(mPlaceholderSource)) return;
388
+ if (mLoadEnded && mShownRequest != null) return;
389
+ if (getWidth() <= 0 || getHeight() <= 0) {
390
+ mUrlPlaceholderWaitsForSize = true;
391
+ return;
392
+ }
393
+ FastImageSource imageSource = FastImageViewConverter.getImageSource(getContext(), mPlaceholderSource);
394
+ if (imageSource == null) return;
395
+ CustomTarget<Drawable> target = new CustomTarget<Drawable>(getWidth(), getHeight()) {
396
+ @Override
397
+ public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
398
+ if (this != mUrlPlaceholderTarget) return;
399
+ mUrlPlaceholder = ownCopy(resource);
400
+ updatePlaceholder();
401
+ // With the copy, Glide's can go back to its pools (a load
402
+ // can't be cleared from its own callback).
403
+ final CustomTarget<Drawable> done = this;
404
+ post(new Runnable() {
405
+ @Override
406
+ public void run() {
407
+ if (requestManager != null) requestManager.clear(done);
408
+ }
409
+ });
410
+ }
411
+
412
+ @Override
413
+ public void onLoadCleared(@Nullable Drawable placeholder) {
414
+ }
415
+ };
416
+ mUrlPlaceholderTarget = target;
417
+ requestManager
418
+ .load(imageSource.getSourceForLoad())
419
+ .apply(FastImageViewConverter.getOptions(getContext(), imageSource, mPlaceholderSource)
420
+ .priority(Priority.IMMEDIATE)
421
+ // Copied on a software canvas (see ownCopy).
422
+ .disallowHardwareConfig())
423
+ .into(target);
424
+ }
425
+
426
+ private void clearUrlPlaceholderTarget() {
427
+ if (mUrlPlaceholderTarget != null && requestManager != null) requestManager.clear(mUrlPlaceholderTarget);
428
+ mUrlPlaceholderTarget = null;
429
+ }
430
+
431
+ // A bitmap of the drawable (an animated one's first frame), scaled up
432
+ // smoothly as the other placeholders are, or null for one with no size.
433
+ @Nullable
434
+ private Drawable ownCopy(Drawable drawable) {
435
+ int width = drawable.getIntrinsicWidth();
436
+ int height = drawable.getIntrinsicHeight();
437
+ if (width <= 0 || height <= 0) return null;
438
+ try {
439
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
440
+ Canvas canvas = new Canvas(bitmap);
441
+ Drawable copy = drawable.getConstantState() != null ? drawable.getConstantState().newDrawable().mutate() : drawable;
442
+ copy.setBounds(0, 0, width, height);
443
+ copy.draw(canvas);
444
+ BitmapDrawable own = new BitmapDrawable(getResources(), bitmap);
445
+ own.setFilterBitmap(true);
446
+ return own;
447
+ } catch (OutOfMemoryError | IllegalStateException e) {
448
+ // As startFade: out of memory, or a hardware bitmap after all.
449
+ return null;
450
+ }
451
+ }
452
+
341
453
  // The picked source's ThumbHash or blurDataUri, or null.
342
454
  @Nullable
343
455
  private String pickedSourcePlaceholder() {
@@ -372,10 +484,16 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
372
484
  mInvalidPlaceholder = key;
373
485
  }
374
486
  }
487
+ // placeholderSource's image, once it has loaded, shows instead.
488
+ if (mUrlPlaceholder != null) placeholder = mUrlPlaceholder;
375
489
  if (placeholder == mShownPlaceholder) return;
376
490
  boolean showing = mShowsPlaceholder;
377
491
  mShownPlaceholder = placeholder;
378
- if (showing) setImageDrawable(placeholder);
492
+ // Also one that arrives while the view shows nothing (no placeholder
493
+ // while the image loads, or after it failed): never over an image.
494
+ Drawable shown = getDrawable();
495
+ boolean showsNothing = shown == null || shown == FastImageViewConverter.TRANSPARENT_DRAWABLE;
496
+ if (showing || (placeholder != null && showsNothing)) setImageDrawable(placeholder);
379
497
  }
380
498
 
381
499
  // The events JS has a handler for (the handledEvents prop): the view only
@@ -449,6 +567,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
449
567
  void drop() {
450
568
  mDropped = true;
451
569
  clearView(requestManager);
570
+ clearUrlPlaceholderTarget();
452
571
  endProgress();
453
572
  }
454
573
 
@@ -1183,10 +1302,11 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1183
1302
  setImageMatrix(matrix);
1184
1303
  return;
1185
1304
  }
1186
- // A source's placeholder (a ThumbHash or a blurDataUri) stands for an
1187
- // image whose size isn't known yet: with scale-down and none (which
1188
- // show an image at its own size), it's shown as with contain.
1189
- boolean hashFits = drawable == mShownPlaceholder && mPlaceholderKey != null;
1305
+ // A source's placeholder (a ThumbHash or a blurDataUri), or
1306
+ // placeholderSource's image, stands for an image whose size isn't
1307
+ // known yet: with scale-down and none (which show an image at its own
1308
+ // size), it's shown as with contain.
1309
+ boolean hashFits = drawable == mShownPlaceholder && (mPlaceholderKey != null || drawable == mUrlPlaceholder);
1190
1310
  if (mLayoutScaleType == ScaleType.CENTER_CROP || mLayoutScaleType == ScaleType.FIT_CENTER || hashFits) {
1191
1311
  float x = viewWidth / drawableWidth;
1192
1312
  float y = viewHeight / drawableHeight;
@@ -1352,6 +1472,17 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1352
1472
  // The loading image loaded (FastImageRequestListener).
1353
1473
  void onImageLoaded() {
1354
1474
  mLoadEnded = true;
1475
+ // The image came first: its placeholder isn't needed (not from
1476
+ // within Glide's callback).
1477
+ if (mUrlPlaceholder == null) {
1478
+ mUrlPlaceholderWaitsForSize = false;
1479
+ post(new Runnable() {
1480
+ @Override
1481
+ public void run() {
1482
+ if (mUrlPlaceholder == null && mLoadEnded && mShownRequest != null) clearUrlPlaceholderTarget();
1483
+ }
1484
+ });
1485
+ }
1355
1486
  endProgress();
1356
1487
  // Before the view shows it (the target does once this returns).
1357
1488
  applyObjectFit();
@@ -1380,6 +1511,10 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1380
1511
  invalidate();
1381
1512
  }
1382
1513
  if (!switchSourceIfResized()) reloadIfResized();
1514
+ if (mUrlPlaceholderWaitsForSize && w > 0 && h > 0) {
1515
+ mUrlPlaceholderWaitsForSize = false;
1516
+ scheduleUrlPlaceholderLoad();
1517
+ }
1383
1518
  }
1384
1519
 
1385
1520
  private void reloadIfResized() {
@@ -1508,6 +1643,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1508
1643
  }
1509
1644
  });
1510
1645
  builder.into(target);
1646
+ if (!quiet) scheduleUrlPlaceholderLoad();
1511
1647
  }
1512
1648
 
1513
1649
  // Loading the image that's showing again (at a new size, or with another
@@ -1524,6 +1660,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1524
1660
  endProgress();
1525
1661
  applyObjectFit();
1526
1662
  mShownRequest = null;
1663
+ // Shows instead of the other placeholders once it loads.
1664
+ scheduleUrlPlaceholderLoad();
1527
1665
  final int load = mLoadCount;
1528
1666
  // Not from within Glide's callback. Without a thumbnail Glide shows the
1529
1667
  // placeholder the request started with: a source placeholder that changed since
@@ -1605,6 +1743,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1605
1743
 
1606
1744
  // Clear the image.
1607
1745
  setImageDrawable(null);
1746
+ scheduleUrlPlaceholderLoad();
1608
1747
  return;
1609
1748
  }
1610
1749
 
@@ -1632,6 +1771,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1632
1771
 
1633
1772
  endProgress();
1634
1773
  setImageDrawable(mShownPlaceholder);
1774
+ scheduleUrlPlaceholderLoad();
1635
1775
  return;
1636
1776
  }
1637
1777
 
package/dist/index.cjs.js CHANGED
@@ -261,8 +261,14 @@ var FastImageViewNativeComponent_default = (0, react_native.codegenNativeCompone
261
261
  var NativeFastImageModule_default = react_native.TurboModuleRegistry.get("FastImageModule");
262
262
  //#endregion
263
263
  //#region src/placeholders.ts
264
+ const SCHEME = /^[a-z][a-z\d+.-]*:/i;
264
265
  function isImageName(value) {
265
- return !/^[a-z][a-z\d+.-]*:/i.test(value) && !value.startsWith("/");
266
+ return !SCHEME.test(value) && !value.startsWith("/");
267
+ }
268
+ function placeholderSource(value) {
269
+ const uri = typeof value === "string" ? value : value && typeof value === "object" ? value.uri : void 0;
270
+ if (typeof uri !== "string" || !/^https?:/i.test(uri)) return void 0;
271
+ return typeof value === "string" ? { uri } : value;
266
272
  }
267
273
  function hasSourcePlaceholder(value) {
268
274
  return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
@@ -270,7 +276,7 @@ function hasSourcePlaceholder(value) {
270
276
  function dataUri(value) {
271
277
  if (typeof value !== "string" || value === "") return void 0;
272
278
  if (/^data:/i.test(value)) return value;
273
- if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`placeholder\` and \`blurDataUri\` don't load urls: "${value.slice(0, 60)}" is ignored. Use a data uri (data:image/…), or for \`placeholder\`, a require()d image or the name of an image bundled with the app.`, "warn");
279
+ 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");
274
280
  }
275
281
  //#endregion
276
282
  //#region src/index.tsx
@@ -348,7 +354,7 @@ function builtInTransition() {
348
354
  }
349
355
  let defaultTransition;
350
356
  function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, loading: _loading, sizes: _sizes, forwardedRef, ...viewProps }) {
351
- var _props$recyclingKey, _defaultTransition;
357
+ var _placeholderSource, _props$recyclingKey, _defaultTransition;
352
358
  const { fallback, resizeMode: _resizeMode, ...props } = viewProps;
353
359
  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");
354
360
  if (__DEV__) warnIfChildren(children);
@@ -366,7 +372,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
366
372
  sourcePlaceholders: placeholders.some(Boolean) ? placeholders : void 0
367
373
  };
368
374
  }, [source, shared]);
369
- const resolvedPlaceholder = resolvePlaceholder(hasSourcePlaceholder(placeholder) ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
375
+ const resolvedPlaceholder = (_placeholderSource = placeholderSource(placeholder)) !== null && _placeholderSource !== void 0 ? _placeholderSource : resolvePlaceholder(hasSourcePlaceholder(placeholder) ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
370
376
  return /* @__PURE__ */ react.default.createElement(FastImageViewNativeComponent_default, {
371
377
  ...props,
372
378
  ref: forwardedRef,
@@ -110,6 +110,13 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
110
110
  export type PreloadFn = (
111
111
  sources: Array<FastImageSource>,
112
112
  ) => Promise<Array<PreloadResult>>
113
+ export type PlaceholderSource = {
114
+ uri?: string,
115
+ headers?: Object,
116
+ cacheKey?: string,
117
+ cache?: CacheControls,
118
+ }
119
+
113
120
  export type SourcePlaceholder = {
114
121
  thumbhash?: string,
115
122
  blurDataUri?: string,
@@ -139,7 +146,7 @@ export type FastImageProps = $ReadOnly<{|
139
146
  onProgress?: ?(event: OnProgressEvent) => void,
140
147
 
141
148
  source?: ?(FastImageSource | Array<FastImageSource> | number),
142
- placeholder?: ?(number | string | SourcePlaceholder),
149
+ placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
143
150
  // Deprecated: use placeholder, which is the same.
144
151
  defaultSource?: ?number,
145
152
 
@@ -268,13 +268,18 @@ function cssPosition({ x, y }) {
268
268
  }
269
269
  //#endregion
270
270
  //#region src/placeholders.ts
271
+ function placeholderSource(value) {
272
+ const uri = typeof value === "string" ? value : value && typeof value === "object" ? value.uri : void 0;
273
+ if (typeof uri !== "string" || !/^https?:/i.test(uri)) return void 0;
274
+ return typeof value === "string" ? { uri } : value;
275
+ }
271
276
  function hasSourcePlaceholder(value) {
272
277
  return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
273
278
  }
274
279
  function dataUri(value) {
275
280
  if (typeof value !== "string" || value === "") return void 0;
276
281
  if (/^data:/i.test(value)) return value;
277
- if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`placeholder\` and \`blurDataUri\` don't load urls: "${value.slice(0, 60)}" is ignored. Use a data uri (data:image/…), or for \`placeholder\`, a require()d image or the name of an image bundled with the app.`, "warn");
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");
278
283
  }
279
284
  //#endregion
280
285
  //#region src/index.web.tsx
@@ -284,7 +289,7 @@ const notSupported = {
284
289
  };
285
290
  const useClientLayoutEffect = globalThis.document === void 0 ? react.useEffect : react.useLayoutEffect;
286
291
  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 }) {
287
- var _resolveUri;
292
+ var _resolveUri, _placeholderSource;
288
293
  if (typeof __DEV__ !== "undefined" && __DEV__) warnIfChildren(children);
289
294
  const { accessibilityLabel, "aria-label": ariaLabel, resizeMode: _resizeMode, ...viewProps } = props;
290
295
  const label = ariaLabel || accessibilityLabel || void 0;
@@ -296,7 +301,9 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
296
301
  const priority = sourcePriority(source);
297
302
  const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
298
303
  const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
299
- const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
304
+ const urlPlaceholder = (_placeholderSource = placeholderSource(placeholder)) === null || _placeholderSource === void 0 ? void 0 : _placeholderSource.uri;
305
+ 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);
300
307
  const [result, setResult] = (0, react.useState)();
301
308
  const ok = result && result.key === key ? result.ok : void 0;
302
309
  const svgId = (0, react.useId)().replace(/[^\w-]/g, "");
@@ -305,7 +312,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
305
312
  const fit = resolveObjectFit(objectFit, style);
306
313
  const position = cssPosition(objectPositionOrCentered(objectPosition));
307
314
  const filter = [blurRadius ? `blur(${blurRadius}px)` : "", tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") || void 0;
308
- const showDefault = !!defaultUri && ok !== true;
315
+ const showDefault = ok !== true;
309
316
  useClientLayoutEffect(() => {
310
317
  if (key) onLoadStart === null || onLoadStart === void 0 || onLoadStart();
311
318
  }, [key]);
@@ -326,6 +333,18 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
326
333
  position,
327
334
  filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
328
335
  alt: key ? "" : label !== null && label !== void 0 ? label : ""
336
+ }) : null, urlPlaceholder && showDefault ? /* @__PURE__ */ react.default.createElement(Picture, {
337
+ key: `placeholder ${urlPlaceholder}`,
338
+ src: urlPlaceholder,
339
+ loading,
340
+ priority: "high",
341
+ dynamicRangeLimit,
342
+ fit: fit === "scale-down" || fit === "none" ? "contain" : fit,
343
+ position,
344
+ filter: tintColor ? `url(#${tintId})` : void 0,
345
+ hidden: failedPlaceholder === urlPlaceholder,
346
+ onError: () => setFailedPlaceholder(urlPlaceholder),
347
+ alt: ""
329
348
  }) : null, key && src ? /* @__PURE__ */ react.default.createElement(Picture, {
330
349
  key: several ? "sizes" : key,
331
350
  src,
@@ -110,6 +110,13 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
110
110
  export type PreloadFn = (
111
111
  sources: Array<FastImageSource>,
112
112
  ) => Promise<Array<PreloadResult>>
113
+ export type PlaceholderSource = {
114
+ uri?: string,
115
+ headers?: Object,
116
+ cacheKey?: string,
117
+ cache?: CacheControls,
118
+ }
119
+
113
120
  export type SourcePlaceholder = {
114
121
  thumbhash?: string,
115
122
  blurDataUri?: string,
@@ -139,7 +146,7 @@ export type FastImageProps = $ReadOnly<{|
139
146
  onProgress?: ?(event: OnProgressEvent) => void,
140
147
 
141
148
  source?: ?(FastImageSource | Array<FastImageSource> | number),
142
- placeholder?: ?(number | string | SourcePlaceholder),
149
+ placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
143
150
  // Deprecated: use placeholder, which is the same.
144
151
  defaultSource?: ?number,
145
152
 
package/dist/index.d.ts CHANGED
@@ -154,6 +154,8 @@ export type Source = {
154
154
  };
155
155
  /** A `thumbhash` or `blurDataUri` as `placeholder`. */
156
156
  export type SourcePlaceholder = Pick<Source, 'thumbhash' | 'blurDataUri'>;
157
+ /** A url as `placeholder`, loaded like a source (see `placeholder`). */
158
+ export type PlaceholderSource = Pick<Source, 'uri' | 'headers' | 'cacheKey' | 'cache'>;
157
159
  /** How the image fades in: see [`transition`](#transition). */
158
160
  export type Transition = {
159
161
  /**
@@ -343,17 +345,23 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
343
345
  * `placeholder={{ thumbhash }}`.
344
346
  * - A tiny image as a data uri, shown blurred:
345
347
  * `placeholder={{ blurDataUri }}`.
348
+ * - A url, loaded and cached like a source:
349
+ * `placeholder="https://example.com/placeholder.png"`, or a source with
350
+ * `headers` or a `cacheKey`: `placeholder={{ uri, headers }}`.
351
+ * - A url only shown if it's already cached, e.g. a list's thumbnail while
352
+ * the full-size image loads: `placeholder={{ uri, cache: 'cacheOnly' }}`.
346
353
  *
347
354
  * A source's own `thumbhash` or `blurDataUri` is shown instead, if it has
348
- * one.
355
+ * one; a url placeholder shows over it once it has loaded.
349
356
  *
350
357
  * Notes:
351
358
  *
352
- * - A url isn't loaded: it's ignored, with a warning in development.
353
359
  * - On Android, a `require()`d image doesn't show in debug builds, where
354
360
  * the dev server serves it. Release builds show it.
361
+ * - On the web, a url loads like any image, also with `'cacheOnly'`: a
362
+ * browser can't look only in its cache.
355
363
  */
356
- placeholder?: ImageRequireSource | string | SourcePlaceholder;
364
+ placeholder?: ImageRequireSource | string | SourcePlaceholder | PlaceholderSource;
357
365
  /**
358
366
  * @deprecated Use `placeholder`, which does the same.
359
367
  */
package/dist/index.js CHANGED
@@ -234,8 +234,14 @@ var FastImageViewNativeComponent_default = codegenNativeComponent("FastImageView
234
234
  var NativeFastImageModule_default = TurboModuleRegistry.get("FastImageModule");
235
235
  //#endregion
236
236
  //#region src/placeholders.ts
237
+ const SCHEME = /^[a-z][a-z\d+.-]*:/i;
237
238
  function isImageName(value) {
238
- return !/^[a-z][a-z\d+.-]*:/i.test(value) && !value.startsWith("/");
239
+ return !SCHEME.test(value) && !value.startsWith("/");
240
+ }
241
+ function placeholderSource(value) {
242
+ const uri = typeof value === "string" ? value : value && typeof value === "object" ? value.uri : void 0;
243
+ if (typeof uri !== "string" || !/^https?:/i.test(uri)) return void 0;
244
+ return typeof value === "string" ? { uri } : value;
239
245
  }
240
246
  function hasSourcePlaceholder(value) {
241
247
  return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
@@ -243,7 +249,7 @@ function hasSourcePlaceholder(value) {
243
249
  function dataUri(value) {
244
250
  if (typeof value !== "string" || value === "") return void 0;
245
251
  if (/^data:/i.test(value)) return value;
246
- if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`placeholder\` and \`blurDataUri\` don't load urls: "${value.slice(0, 60)}" is ignored. Use a data uri (data:image/…), or for \`placeholder\`, a require()d image or the name of an image bundled with the app.`, "warn");
252
+ 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");
247
253
  }
248
254
  //#endregion
249
255
  //#region src/index.tsx
@@ -321,7 +327,7 @@ function builtInTransition() {
321
327
  }
322
328
  let defaultTransition;
323
329
  function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, loading: _loading, sizes: _sizes, forwardedRef, ...viewProps }) {
324
- var _props$recyclingKey, _defaultTransition;
330
+ var _placeholderSource, _props$recyclingKey, _defaultTransition;
325
331
  const { fallback, resizeMode: _resizeMode, ...props } = viewProps;
326
332
  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");
327
333
  if (__DEV__) warnIfChildren(children);
@@ -339,7 +345,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
339
345
  sourcePlaceholders: placeholders.some(Boolean) ? placeholders : void 0
340
346
  };
341
347
  }, [source, shared]);
342
- const resolvedPlaceholder = resolvePlaceholder(hasSourcePlaceholder(placeholder) ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
348
+ const resolvedPlaceholder = (_placeholderSource = placeholderSource(placeholder)) !== null && _placeholderSource !== void 0 ? _placeholderSource : resolvePlaceholder(hasSourcePlaceholder(placeholder) ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
343
349
  return /* @__PURE__ */ React.createElement(FastImageViewNativeComponent_default, {
344
350
  ...props,
345
351
  ref: forwardedRef,
@@ -110,6 +110,13 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
110
110
  export type PreloadFn = (
111
111
  sources: Array<FastImageSource>,
112
112
  ) => Promise<Array<PreloadResult>>
113
+ export type PlaceholderSource = {
114
+ uri?: string,
115
+ headers?: Object,
116
+ cacheKey?: string,
117
+ cache?: CacheControls,
118
+ }
119
+
113
120
  export type SourcePlaceholder = {
114
121
  thumbhash?: string,
115
122
  blurDataUri?: string,
@@ -139,7 +146,7 @@ export type FastImageProps = $ReadOnly<{|
139
146
  onProgress?: ?(event: OnProgressEvent) => void,
140
147
 
141
148
  source?: ?(FastImageSource | Array<FastImageSource> | number),
142
- placeholder?: ?(number | string | SourcePlaceholder),
149
+ placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
143
150
  // Deprecated: use placeholder, which is the same.
144
151
  defaultSource?: ?number,
145
152
 
package/dist/index.web.js CHANGED
@@ -241,13 +241,18 @@ function cssPosition({ x, y }) {
241
241
  }
242
242
  //#endregion
243
243
  //#region src/placeholders.ts
244
+ function placeholderSource(value) {
245
+ const uri = typeof value === "string" ? value : value && typeof value === "object" ? value.uri : void 0;
246
+ if (typeof uri !== "string" || !/^https?:/i.test(uri)) return void 0;
247
+ return typeof value === "string" ? { uri } : value;
248
+ }
244
249
  function hasSourcePlaceholder(value) {
245
250
  return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
246
251
  }
247
252
  function dataUri(value) {
248
253
  if (typeof value !== "string" || value === "") return void 0;
249
254
  if (/^data:/i.test(value)) return value;
250
- if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`placeholder\` and \`blurDataUri\` don't load urls: "${value.slice(0, 60)}" is ignored. Use a data uri (data:image/…), or for \`placeholder\`, a require()d image or the name of an image bundled with the app.`, "warn");
255
+ 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");
251
256
  }
252
257
  //#endregion
253
258
  //#region src/index.web.tsx
@@ -257,7 +262,7 @@ const notSupported = {
257
262
  };
258
263
  const useClientLayoutEffect = globalThis.document === void 0 ? useEffect : useLayoutEffect;
259
264
  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 }) {
260
- var _resolveUri;
265
+ var _resolveUri, _placeholderSource;
261
266
  if (typeof __DEV__ !== "undefined" && __DEV__) warnIfChildren(children);
262
267
  const { accessibilityLabel, "aria-label": ariaLabel, resizeMode: _resizeMode, ...viewProps } = props;
263
268
  const label = ariaLabel || accessibilityLabel || void 0;
@@ -269,7 +274,9 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
269
274
  const priority = sourcePriority(source);
270
275
  const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
271
276
  const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
272
- const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
277
+ const urlPlaceholder = (_placeholderSource = placeholderSource(placeholder)) === null || _placeholderSource === void 0 ? void 0 : _placeholderSource.uri;
278
+ const [failedPlaceholder, setFailedPlaceholder] = useState();
279
+ const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview || urlPlaceholder ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
273
280
  const [result, setResult] = useState();
274
281
  const ok = result && result.key === key ? result.ok : void 0;
275
282
  const svgId = useId().replace(/[^\w-]/g, "");
@@ -278,7 +285,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
278
285
  const fit = resolveObjectFit(objectFit, style);
279
286
  const position = cssPosition(objectPositionOrCentered(objectPosition));
280
287
  const filter = [blurRadius ? `blur(${blurRadius}px)` : "", tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") || void 0;
281
- const showDefault = !!defaultUri && ok !== true;
288
+ const showDefault = ok !== true;
282
289
  useClientLayoutEffect(() => {
283
290
  if (key) onLoadStart === null || onLoadStart === void 0 || onLoadStart();
284
291
  }, [key]);
@@ -299,6 +306,18 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
299
306
  position,
300
307
  filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
301
308
  alt: key ? "" : label !== null && label !== void 0 ? label : ""
309
+ }) : null, urlPlaceholder && showDefault ? /* @__PURE__ */ React.createElement(Picture, {
310
+ key: `placeholder ${urlPlaceholder}`,
311
+ src: urlPlaceholder,
312
+ loading,
313
+ priority: "high",
314
+ dynamicRangeLimit,
315
+ fit: fit === "scale-down" || fit === "none" ? "contain" : fit,
316
+ position,
317
+ filter: tintColor ? `url(#${tintId})` : void 0,
318
+ hidden: failedPlaceholder === urlPlaceholder,
319
+ onError: () => setFailedPlaceholder(urlPlaceholder),
320
+ alt: ""
302
321
  }) : null, key && src ? /* @__PURE__ */ React.createElement(Picture, {
303
322
  key: several ? "sizes" : key,
304
323
  src,
@@ -110,6 +110,13 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
110
110
  export type PreloadFn = (
111
111
  sources: Array<FastImageSource>,
112
112
  ) => Promise<Array<PreloadResult>>
113
+ export type PlaceholderSource = {
114
+ uri?: string,
115
+ headers?: Object,
116
+ cacheKey?: string,
117
+ cache?: CacheControls,
118
+ }
119
+
113
120
  export type SourcePlaceholder = {
114
121
  thumbhash?: string,
115
122
  blurDataUri?: string,
@@ -139,7 +146,7 @@ export type FastImageProps = $ReadOnly<{|
139
146
  onProgress?: ?(event: OnProgressEvent) => void,
140
147
 
141
148
  source?: ?(FastImageSource | Array<FastImageSource> | number),
142
- placeholder?: ?(number | string | SourcePlaceholder),
149
+ placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
143
150
  // Deprecated: use placeholder, which is the same.
144
151
  defaultSource?: ?number,
145
152
 
@@ -1,4 +1,5 @@
1
- import type { SourcePlaceholder } from './index';
1
+ import type { PlaceholderSource, SourcePlaceholder } from './index';
2
2
  export declare function isImageName(value: string): boolean;
3
+ export declare function placeholderSource(value: unknown): PlaceholderSource | undefined;
3
4
  export declare function hasSourcePlaceholder(value: unknown): value is SourcePlaceholder;
4
5
  export declare function dataUri(value: unknown): string | undefined;
@@ -34,6 +34,10 @@ typedef NS_ENUM(NSInteger, FFFObjectFit) {
34
34
  @property (nonatomic, copy) NSArray<FFFastImageSource *> *sources;
35
35
  // The `placeholder` prop.
36
36
  @property (nonatomic, strong) UIImage *placeholder;
37
+ // `placeholder` as a url (a source): loaded apart from the
38
+ // image, at a high priority (or only from the cache, with cache cacheOnly),
39
+ // it shows over the other placeholders once it has.
40
+ @property (nonatomic, strong) FFFastImageSource *placeholderSource;
37
41
  // The `sourcePlaceholders` prop, by the source's index (an empty string for
38
42
  // none; one for a single source): the picked source's ThumbHash or
39
43
  // blurDataUri (a data uri) shows instead of placeholder. JS sends them apart
@@ -70,8 +70,18 @@
70
70
  // shownPlaceholder).
71
71
  @property(nonatomic, strong) UIImage* hashImage;
72
72
  @property(nonatomic, copy) NSString* hashImageKey;
73
- // Whether the view shows its placeholder (the placeholder prop or the
74
- // source's), and whether that's the source's (hashImage).
73
+ // placeholderSource's image once it has loaded, or nil: it shows instead of
74
+ // the other placeholders (see shownPlaceholder). Its load, and which it is.
75
+ @property(nonatomic, strong) UIImage* urlPlaceholderImage;
76
+ @property(nonatomic, strong) id<SDWebImageOperation> urlPlaceholderOperation;
77
+ @property(nonatomic, assign) NSUInteger urlPlaceholderLoad;
78
+ // placeholderSource changed: its load starts with the image's, or after the
79
+ // props if the image is still loading (see startUrlPlaceholderLoad).
80
+ @property(nonatomic, assign) BOOL needsUrlPlaceholderLoad;
81
+ // Whether the view shows a placeholder (the placeholder prop, the source's,
82
+ // or placeholderSource's), and whether it stands for the image (the source's
83
+ // or placeholderSource's): drawn as with contain for scale-down and none, as
84
+ // the image's size isn't known yet, scaled smoothly and not blurred.
75
85
  @property(nonatomic, assign) BOOL showsPlaceholder;
76
86
  @property(nonatomic, assign) BOOL showsHash;
77
87
  // dynamicRangeLimit switched between HDR and SDR: the image is decoded again,
@@ -444,7 +454,7 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
444
454
 
445
455
  - (void) setImage: (UIImage*)image {
446
456
  self.imageCount++;
447
- self.showsHash = image != nil && image == self.hashImage;
457
+ self.showsHash = image != nil && (image == self.hashImage || image == self.urlPlaceholderImage);
448
458
  self.showsPlaceholder = self.showsHash || (image != nil && image == _placeholder);
449
459
  if ([_imageRendering isEqualToString: @"pixelated"]) {
450
460
  // A source's placeholder is a blurred preview, scaled up smoothly
@@ -540,7 +550,7 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
540
550
  // Whether the image is blurred: a loaded image, not the placeholder prop (as
541
551
  // with React Native's Image, and on Android).
542
552
  - (BOOL) blurs: (UIImage*)image {
543
- return _blurRadius > 0 && image && image != _placeholder && image != self.hashImage;
553
+ return _blurRadius > 0 && image && image != _placeholder && image != self.hashImage && image != self.urlPlaceholderImage;
544
554
  }
545
555
 
546
556
  // Blurs the image off the main thread, and shows it (tinted as any image) unless
@@ -849,10 +859,124 @@ NSString *FFFErrorMessage(NSError *error)
849
859
  [self updateHashImage];
850
860
  }
851
861
 
852
- // What shows while the image loads and if it fails: the picked source's
853
- // placeholder (its ThumbHash or blurDataUri), or the placeholder prop.
862
+ // What shows while the image loads and if it fails: placeholderSource's
863
+ // image once it has loaded, the picked source's placeholder (its ThumbHash or
864
+ // blurDataUri), or the placeholder prop.
854
865
  - (UIImage*) shownPlaceholder {
855
- return self.hashImage ?: _placeholder;
866
+ return self.urlPlaceholderImage ?: self.hashImage ?: _placeholder;
867
+ }
868
+
869
+ // Shows the placeholder that shows now (shownPlaceholder) in place of
870
+ // `previous` if the view showed it, or if it showed nothing (one that arrived
871
+ // while nothing showed): never over a loaded image, or the image kept while
872
+ // a new source loads.
873
+ - (void) showChangedPlaceholder: (UIImage*)previous showing: (BOOL)showing {
874
+ UIImage* placeholder = [self shownPlaceholder];
875
+ if (placeholder == previous) {
876
+ return;
877
+ }
878
+ BOOL showsNothing = super.image == nil && !self.showsLoadedImage && !self.blursOverNothing;
879
+ if (showing || (placeholder && showsNothing)) {
880
+ [self setImage: placeholder];
881
+ }
882
+ }
883
+
884
+ // Images that views showed, by their source's cacheKey or url, while they're
885
+ // still in memory (held by a view or SDWebImage's memory cache), so
886
+ // placeholderSource finds one at once whatever size it was decoded at (the
887
+ // memory cache's key includes the size).
888
+ + (NSMapTable<NSString*, UIImage*>*) shownImages {
889
+ static NSMapTable* images;
890
+ static dispatch_once_t once;
891
+ dispatch_once(&once, ^{
892
+ images = [NSMapTable strongToWeakObjectsMapTable];
893
+ });
894
+ return images;
895
+ }
896
+
897
+ // The key a remote image (http or https) is recorded under: its cacheKey or
898
+ // url. nil for others (data uris, files), which aren't recorded.
899
+ + (NSString*) shownImageKey: (FFFastImageSource*)source {
900
+ NSString* scheme = source.url.scheme.lowercaseString;
901
+ if (![scheme isEqualToString: @"http"] && ![scheme isEqualToString: @"https"]) {
902
+ return nil;
903
+ }
904
+ return source.cacheKey.length > 0 ? source.cacheKey : source.url.absoluteString;
905
+ }
906
+
907
+ + (void) rememberShownImage: (UIImage*)image source: (FFFastImageSource*)source {
908
+ NSString* key = [self shownImageKey: source];
909
+ if (image && key) {
910
+ [[self shownImages] setObject: image forKey: key];
911
+ }
912
+ }
913
+
914
+ - (void) setPlaceholderSource: (FFFastImageSource*)placeholderSource {
915
+ _placeholderSource = placeholderSource;
916
+ [self cancelUrlPlaceholderLoad];
917
+ [self showUrlPlaceholderImage: nil];
918
+ self.needsUrlPlaceholderLoad = YES;
919
+ }
920
+
921
+ - (void) cancelUrlPlaceholderLoad {
922
+ self.urlPlaceholderLoad++;
923
+ [self.urlPlaceholderOperation cancel];
924
+ self.urlPlaceholderOperation = nil;
925
+ }
926
+
927
+ // Loads placeholderSource's image with the image's load (after layout, so at
928
+ // the view's size, with the props applied), or after the image failed: not
929
+ // if the image shows at once (from the memory cache, or a data uri), or the
930
+ // placeholder is already loaded or loading.
931
+ - (void) startUrlPlaceholderLoad {
932
+ self.needsUrlPlaceholderLoad = NO;
933
+ if (!_placeholderSource.url || self.urlPlaceholderImage || self.urlPlaceholderOperation) {
934
+ return;
935
+ }
936
+ if (_source && !self.hasErrored && [self loadsDuringLayout]) {
937
+ return;
938
+ }
939
+ [self loadUrlPlaceholder];
940
+ }
941
+
942
+ // Loads placeholderSource's image, as a source would load (its cache,
943
+ // headers and cacheKey), at a high priority, as it only helps if it comes
944
+ // before the image. One a view still shows (or that's still in memory) shows
945
+ // at once. A failure shows nothing more.
946
+ - (void) loadUrlPlaceholder {
947
+ [self cancelUrlPlaceholderLoad];
948
+ NSUInteger load = self.urlPlaceholderLoad;
949
+ FFFastImageSource* source = _placeholderSource;
950
+ NSString* key = [FFFastImageView shownImageKey: source];
951
+ UIImage* shown = key ? [[FFFastImageView shownImages] objectForKey: key] : nil;
952
+ if (shown) {
953
+ [self showUrlPlaceholderImage: shown];
954
+ return;
955
+ }
956
+ SDWebImageOptions options = [source cacheOptions] | SDWebImageHighPriority | SDWebImageRetryFailed | SDWebImageHandleCookies;
957
+ SDWebImageContext* context = [self contextForSource: source box: [self decodeBox] cover: NO];
958
+ __weak typeof(self) weakSelf = self;
959
+ self.urlPlaceholderOperation = [[SDWebImageManager sharedManager] loadImageWithURL: source.url options: options context: context progress: nil completed: ^(UIImage* _Nullable image, NSData* _Nullable data, NSError* _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL* _Nullable imageURL) {
960
+ if (!finished || weakSelf.urlPlaceholderLoad != load) {
961
+ return;
962
+ }
963
+ weakSelf.urlPlaceholderOperation = nil;
964
+ if (!image) {
965
+ return;
966
+ }
967
+ [FFFastImageView rememberShownImage: image source: source];
968
+ [weakSelf showUrlPlaceholderImage: image];
969
+ }];
970
+ }
971
+
972
+ - (void) showUrlPlaceholderImage: (UIImage*)image {
973
+ if (self.urlPlaceholderImage == image) {
974
+ return;
975
+ }
976
+ UIImage* previous = [self shownPlaceholder];
977
+ BOOL showing = self.showsPlaceholder;
978
+ self.urlPlaceholderImage = image;
979
+ [self showChangedPlaceholder: previous showing: showing];
856
980
  }
857
981
 
858
982
  // The picked source's ThumbHash or blurDataUri, or nil.
@@ -876,10 +1000,7 @@ NSString *FFFErrorMessage(NSError *error)
876
1000
  BOOL showing = self.showsPlaceholder;
877
1001
  self.hashImageKey = hash;
878
1002
  self.hashImage = [FFFThumbHash imageForPlaceholder: hash];
879
- UIImage* placeholder = [self shownPlaceholder];
880
- if (showing && placeholder != previous) {
881
- [self setImage: placeholder];
882
- }
1003
+ [self showChangedPlaceholder: previous showing: showing];
883
1004
  }
884
1005
 
885
1006
  - (void) didSetProps {
@@ -909,6 +1030,14 @@ NSString *FFFErrorMessage(NSError *error)
909
1030
  } else {
910
1031
  [self reloadIfResized];
911
1032
  }
1033
+ // A new placeholderSource while the image loads, or after it failed (a
1034
+ // load waiting for the view's size starts it with the image's).
1035
+ if (self.needsUrlPlaceholderLoad && !_needsReload && !self.waitsForSize) {
1036
+ if (!self.hasCompleted || self.hasErrored) {
1037
+ [self startUrlPlaceholderLoad];
1038
+ }
1039
+ self.needsUrlPlaceholderLoad = NO;
1040
+ }
912
1041
  }
913
1042
 
914
1043
  // The same image decoded again for a dynamicRangeLimit between HDR and SDR,
@@ -1003,6 +1132,7 @@ NSString *FFFErrorMessage(NSError *error)
1003
1132
  - (void) reloadImage {
1004
1133
  _needsReload = NO;
1005
1134
  self.needsDynamicRangeReload = NO;
1135
+ self.needsUrlPlaceholderLoad = NO;
1006
1136
  self.waitsForSize = NO;
1007
1137
  self.loadsSwitchedSource = self.switchesSource;
1008
1138
  self.switchesSource = NO;
@@ -1038,6 +1168,7 @@ NSString *FFFErrorMessage(NSError *error)
1038
1168
  [self setImage: [self shownPlaceholder]];
1039
1169
  self.showsLoadedImage = NO;
1040
1170
  [self sendOnError: @"Failed to decode the image"];
1171
+ [self startUrlPlaceholderLoad];
1041
1172
  if (self.onFastImageLoadEnd) {
1042
1173
  self.onFastImageLoadEnd([self loadEndEvent: NO]);
1043
1174
  }
@@ -1080,10 +1211,15 @@ NSString *FFFErrorMessage(NSError *error)
1080
1211
  self.retriedAfterBackground = NO;
1081
1212
  [self stopWaitingForActive];
1082
1213
 
1214
+ [self startUrlPlaceholderLoad];
1083
1215
  [self downloadImage: _source options: options context: [self loadContext] events: YES];
1084
- } else if ([self shownPlaceholder]) {
1085
- [self setImage: [self shownPlaceholder]];
1086
- self.showsLoadedImage = NO;
1216
+ } else {
1217
+ if ([self shownPlaceholder]) {
1218
+ [self setImage: [self shownPlaceholder]];
1219
+ self.showsLoadedImage = NO;
1220
+ }
1221
+ // No image: only placeholders show.
1222
+ [self startUrlPlaceholderLoad];
1087
1223
  }
1088
1224
  }
1089
1225
 
@@ -1319,6 +1455,8 @@ NSString *FFFErrorMessage(NSError *error)
1319
1455
  [weakSelf setImage: current];
1320
1456
  }
1321
1457
  weakSelf.showsLoadedImage = NO;
1458
+ // Shows instead of the other placeholders once it loads.
1459
+ [weakSelf startUrlPlaceholderLoad];
1322
1460
  [weakSelf sendOnError: [source errorMessage: error]];
1323
1461
  if (weakSelf.onFastImageLoadEnd) {
1324
1462
  weakSelf.onFastImageLoadEnd([weakSelf loadEndEvent: NO]);
@@ -1326,6 +1464,12 @@ NSString *FFFErrorMessage(NSError *error)
1326
1464
  } else {
1327
1465
  weakSelf.hasCompleted = YES;
1328
1466
  weakSelf.showsLoadedImage = YES;
1467
+ // For other views' placeholderSource.
1468
+ [FFFastImageView rememberShownImage: image source: source];
1469
+ // The image came first: its placeholder isn't needed.
1470
+ if (!weakSelf.urlPlaceholderImage) {
1471
+ [weakSelf cancelUrlPlaceholderLoad];
1472
+ }
1329
1473
  [weakSelf sendOnLoad: image];
1330
1474
  if (weakSelf.onFastImageLoadEnd) {
1331
1475
  weakSelf.onFastImageLoadEnd([weakSelf loadEndEvent: YES]);
@@ -1375,11 +1519,14 @@ NSString *FFFErrorMessage(NSError *error)
1375
1519
  self.waitsForSize = NO;
1376
1520
  [self stopWaitingForActive];
1377
1521
  [self sd_cancelLatestImageLoad];
1522
+ [self cancelUrlPlaceholderLoad];
1378
1523
  }
1379
1524
 
1380
1525
  - (void) dealloc {
1381
1526
  [self stopWaitingForActive];
1382
1527
  [self sd_cancelLatestImageLoad];
1528
+ // Fabric frees views it doesn't recycle without stop.
1529
+ [_urlPlaceholderOperation cancel];
1383
1530
  }
1384
1531
 
1385
1532
  @end
@@ -102,6 +102,22 @@ static id _Nullable FFFJSON(const folly::dynamic &value)
102
102
  return value.isNull() ? nil : FFFObjectFromDynamic(value);
103
103
  }
104
104
 
105
+ // Whether the placeholder prop is a url to load apart from the image (a
106
+ // source with an http or https uri), rather than an image to show at once: a
107
+ // bundled image (served by the packager in development), a file or a data uri.
108
+ static BOOL FFFIsUrlPlaceholder(id _Nullable placeholder)
109
+ {
110
+ if (![placeholder isKindOfClass:[NSDictionary class]] || placeholder[@"__packager_asset"]) {
111
+ return NO;
112
+ }
113
+ id uri = placeholder[@"uri"];
114
+ if (![uri isKindOfClass:[NSString class]]) {
115
+ return NO;
116
+ }
117
+ NSString *scheme = [NSURL URLWithString:uri].scheme.lowercaseString;
118
+ return [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"];
119
+ }
120
+
105
121
  // The events JS has a handler for: the handledEvents prop's bits (see
106
122
  // src/specs/FastImageViewNativeComponent.ts).
107
123
  typedef NS_OPTIONS(int32_t, FFFEvent) {
@@ -234,7 +250,9 @@ typedef NS_OPTIONS(int32_t, FFFEvent) {
234
250
  }
235
251
  if (all || oldViewProps.placeholder != newViewProps.placeholder) {
236
252
  id placeholder = FFFJSON(newViewProps.placeholder);
237
- view.placeholder = placeholder ? [RCTConvert UIImage:placeholder] : nil;
253
+ BOOL loads = FFFIsUrlPlaceholder(placeholder);
254
+ view.placeholder = placeholder && !loads ? [RCTConvert UIImage:placeholder] : nil;
255
+ view.placeholderSource = loads ? [RCTConvert FFFastImageSource:placeholder] : nil;
238
256
  }
239
257
  if (all || oldViewProps.sourcePlaceholders != newViewProps.sourcePlaceholders) {
240
258
  NSMutableArray<NSString *> *placeholders = [NSMutableArray arrayWithCapacity:newViewProps.sourcePlaceholders.size()];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "10.4.0",
3
+ "version": "10.5.0",
4
4
  "description": "FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",
package/src/index.js.flow CHANGED
@@ -110,6 +110,13 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
110
110
  export type PreloadFn = (
111
111
  sources: Array<FastImageSource>,
112
112
  ) => Promise<Array<PreloadResult>>
113
+ export type PlaceholderSource = {
114
+ uri?: string,
115
+ headers?: Object,
116
+ cacheKey?: string,
117
+ cache?: CacheControls,
118
+ }
119
+
113
120
  export type SourcePlaceholder = {
114
121
  thumbhash?: string,
115
122
  blurDataUri?: string,
@@ -139,7 +146,7 @@ export type FastImageProps = $ReadOnly<{|
139
146
  onProgress?: ?(event: OnProgressEvent) => void,
140
147
 
141
148
  source?: ?(FastImageSource | Array<FastImageSource> | number),
142
- placeholder?: ?(number | string | SourcePlaceholder),
149
+ placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
143
150
  // Deprecated: use placeholder, which is the same.
144
151
  defaultSource?: ?number,
145
152
 
package/src/index.tsx CHANGED
@@ -16,7 +16,12 @@ import { fromStyle, resolveObjectFit } from './objectFit'
16
16
  import { objectPositionOrCentered, type ObjectPosition } from './objectPosition'
17
17
  import FastImageView from './specs/FastImageViewNativeComponent'
18
18
  import NativeFastImageModule from './specs/NativeFastImageModule'
19
- import { dataUri, hasSourcePlaceholder, isImageName } from './placeholders'
19
+ import {
20
+ dataUri,
21
+ hasSourcePlaceholder,
22
+ isImageName,
23
+ placeholderSource,
24
+ } from './placeholders'
20
25
  import { warnIfChildren, warnOnce } from './warnings'
21
26
 
22
27
  /** How the image fits the view: see [`objectFit`](#objectfit). */
@@ -175,6 +180,12 @@ export type Source = {
175
180
  /** A `thumbhash` or `blurDataUri` as `placeholder`. */
176
181
  export type SourcePlaceholder = Pick<Source, 'thumbhash' | 'blurDataUri'>
177
182
 
183
+ /** A url as `placeholder`, loaded like a source (see `placeholder`). */
184
+ export type PlaceholderSource = Pick<
185
+ Source,
186
+ 'uri' | 'headers' | 'cacheKey' | 'cache'
187
+ >
188
+
178
189
  /** How the image fades in: see [`transition`](#transition). */
179
190
  export type Transition = {
180
191
  /**
@@ -365,17 +376,27 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
365
376
  * `placeholder={{ thumbhash }}`.
366
377
  * - A tiny image as a data uri, shown blurred:
367
378
  * `placeholder={{ blurDataUri }}`.
379
+ * - A url, loaded and cached like a source:
380
+ * `placeholder="https://example.com/placeholder.png"`, or a source with
381
+ * `headers` or a `cacheKey`: `placeholder={{ uri, headers }}`.
382
+ * - A url only shown if it's already cached, e.g. a list's thumbnail while
383
+ * the full-size image loads: `placeholder={{ uri, cache: 'cacheOnly' }}`.
368
384
  *
369
385
  * A source's own `thumbhash` or `blurDataUri` is shown instead, if it has
370
- * one.
386
+ * one; a url placeholder shows over it once it has loaded.
371
387
  *
372
388
  * Notes:
373
389
  *
374
- * - A url isn't loaded: it's ignored, with a warning in development.
375
390
  * - On Android, a `require()`d image doesn't show in debug builds, where
376
391
  * the dev server serves it. Release builds show it.
392
+ * - On the web, a url loads like any image, also with `'cacheOnly'`: a
393
+ * browser can't look only in its cache.
377
394
  */
378
- placeholder?: ImageRequireSource | string | SourcePlaceholder
395
+ placeholder?:
396
+ | ImageRequireSource
397
+ | string
398
+ | SourcePlaceholder
399
+ | PlaceholderSource
379
400
  /**
380
401
  * @deprecated Use `placeholder`, which does the same.
381
402
  */
@@ -706,7 +727,8 @@ function sourcePlaceholder(source: unknown): string {
706
727
  }
707
728
 
708
729
  const resolvePlaceholder = (
709
- placeholder?: ImageRequireSource | string,
730
+ // A source object here has no url (e.g. { uri: name }, a bundled image).
731
+ placeholder?: ImageRequireSource | string | PlaceholderSource,
710
732
  ): string | object | null => {
711
733
  if (typeof placeholder === 'string') {
712
734
  // An image bundled with the app, by name, as React Native's Image
@@ -725,7 +747,9 @@ const resolvePlaceholder = (
725
747
  if (!placeholder) {
726
748
  return null
727
749
  }
728
- const resolved = Image.resolveAssetSource(placeholder)
750
+ const resolved = Image.resolveAssetSource(
751
+ placeholder as ImageRequireSource | { uri: string },
752
+ )
729
753
  if (!resolved) {
730
754
  return null
731
755
  }
@@ -923,13 +947,17 @@ function FastImageBase({
923
947
  : undefined,
924
948
  }
925
949
  }, [source, shared])
926
- // A thumbhash or blurDataUri in `placeholder` goes in sourcePlaceholders
927
- // instead: it isn't an image to show as it is, but defaultSource still is.
928
- const resolvedPlaceholder = resolvePlaceholder(
929
- hasSourcePlaceholder(placeholder)
930
- ? defaultSource
931
- : (placeholder ?? defaultSource),
932
- )
950
+ // A url in `placeholder` goes to the native view as a source, loaded
951
+ // apart from the image, in place of defaultSource. A thumbhash or
952
+ // blurDataUri goes in sourcePlaceholders instead: it isn't an image to
953
+ // show as it is, but defaultSource still is.
954
+ const resolvedPlaceholder =
955
+ placeholderSource(placeholder) ??
956
+ resolvePlaceholder(
957
+ hasSourcePlaceholder(placeholder)
958
+ ? defaultSource
959
+ : (placeholder ?? defaultSource),
960
+ )
933
961
 
934
962
  return (
935
963
  <FastImageView
package/src/index.web.tsx CHANGED
@@ -32,7 +32,11 @@ import { getAssetByID } from 'react-native-web/dist/modules/AssetRegistry'
32
32
  import { cacheControl, priority, resizeMode } from './constants'
33
33
  import { resolveObjectFit } from './objectFit'
34
34
  import { cssPosition, objectPositionOrCentered } from './objectPosition'
35
- import { dataUri, hasSourcePlaceholder } from './placeholders'
35
+ import {
36
+ dataUri,
37
+ hasSourcePlaceholder,
38
+ placeholderSource,
39
+ } from './placeholders'
36
40
  import { warnIfChildren, warnOnce } from './warnings'
37
41
  import type {
38
42
  CachePathResult,
@@ -132,11 +136,20 @@ function FastImageBase({
132
136
  const blurUri =
133
137
  resolveUri(dataUri(shared(source, 'blurDataUri', preview))) ??
134
138
  thumbhashUri(shared(source, 'thumbhash', preview))
139
+ // `placeholder` as a url (or a source with one) shows over the other
140
+ // placeholders as it loads, as on iOS and Android. The browser loads it
141
+ // like any image: it can't look in its cache only (cache: 'cacheOnly').
142
+ const urlPlaceholder = placeholderSource(placeholder)?.uri
143
+ const [failedPlaceholder, setFailedPlaceholder] = useState<string>()
135
144
  // A thumbhash or blurDataUri in `placeholder` isn't an image to show as it
136
- // is: defaultSource still is.
145
+ // is, nor is a url (above): defaultSource still is.
137
146
  const defaultUri =
138
147
  blurUri ??
139
- resolveUri(preview ? defaultSource : (placeholder ?? defaultSource))
148
+ resolveUri(
149
+ preview || urlPlaceholder
150
+ ? defaultSource
151
+ : (placeholder ?? defaultSource),
152
+ )
140
153
  // Whether the image for `key` loaded, or failed.
141
154
  const [result, setResult] = useState<{ key: string; ok: boolean }>()
142
155
  const ok = result && result.key === key ? result.ok : undefined
@@ -156,7 +169,7 @@ function FastImageBase({
156
169
  .filter(Boolean)
157
170
  .join(' ') || undefined
158
171
  // The placeholder shows until the image has loaded, and if it fails.
159
- const showDefault = !!defaultUri && ok !== true
172
+ const showDefault = ok !== true
160
173
 
161
174
  useClientLayoutEffect(() => {
162
175
  if (key) onLoadStart?.()
@@ -207,6 +220,28 @@ function FastImageBase({
207
220
  alt={key ? '' : (label ?? '')}
208
221
  />
209
222
  ) : null}
223
+ {urlPlaceholder && showDefault ? (
224
+ <Picture
225
+ key={`placeholder ${urlPlaceholder}`}
226
+ src={urlPlaceholder}
227
+ loading={loading}
228
+ // It only helps if it comes before the image, as on iOS
229
+ // and Android.
230
+ priority="high"
231
+ dynamicRangeLimit={dynamicRangeLimit}
232
+ // It stands for the image, as a source's preview does.
233
+ fit={
234
+ fit === 'scale-down' || fit === 'none' ? 'contain' : fit
235
+ }
236
+ position={position}
237
+ // Tinted, not blurred, as on iOS and Android.
238
+ filter={tintColor ? `url(#${tintId})` : undefined}
239
+ // Nothing (no broken image icon) if it fails.
240
+ hidden={failedPlaceholder === urlPlaceholder}
241
+ onError={() => setFailedPlaceholder(urlPlaceholder)}
242
+ alt=""
243
+ />
244
+ ) : null}
210
245
  {key && src ? (
211
246
  <Picture
212
247
  key={several ? 'sizes' : key}
@@ -1,20 +1,37 @@
1
- // Placeholders given as strings (`placeholder`, and a source's `blurDataUri`)
2
- // are data uris, shared by the native (index.tsx) and web (index.web.tsx)
3
- // versions, or for `placeholder`, the name of an image bundled with the app.
4
- // They're decoded at once, so they show in the view's first frame; a url
5
- // would load like an image (and on iOS and Android, not at all), so it's
6
- // ignored, with a warning in development.
7
- import type { SourcePlaceholder } from './index'
1
+ // Placeholders given as strings, shared by the native (index.tsx) and web
2
+ // (index.web.tsx) versions: a source's `blurDataUri` is a data uri, decoded at
3
+ // once, so it shows in the view's first frame (a url there is ignored, with a
4
+ // warning in development). `placeholder` can also be the name of an image
5
+ // bundled with the app, or a url, loaded as a source (placeholderSource).
6
+ import type { PlaceholderSource, SourcePlaceholder } from './index'
8
7
  import { warnOnce } from './warnings'
9
8
 
10
9
  // Metro's (and React Native's Jest preset's) development flag; other web
11
10
  // bundlers may not define it.
12
11
  declare const __DEV__: boolean | undefined
13
12
 
13
+ const SCHEME = /^[a-z][a-z\d+.-]*:/i
14
+
14
15
  // Whether `placeholder` is the name of an image bundled with the app (an iOS
15
16
  // asset catalog image, an Android drawable): no scheme, not a path.
16
17
  export function isImageName(value: string): boolean {
17
- return !/^[a-z][a-z\d+.-]*:/i.test(value) && !value.startsWith('/')
18
+ return !SCHEME.test(value) && !value.startsWith('/')
19
+ }
20
+
21
+ // `placeholder` as a source to load: an http or https url, as a string or a
22
+ // source object's uri. Undefined for anything else (a local file or a data uri
23
+ // is decoded at once, as the native placeholder prop).
24
+ export function placeholderSource(
25
+ value: unknown,
26
+ ): PlaceholderSource | undefined {
27
+ const uri =
28
+ typeof value === 'string'
29
+ ? value
30
+ : value && typeof value === 'object'
31
+ ? (value as PlaceholderSource).uri
32
+ : undefined
33
+ if (typeof uri !== 'string' || !/^https?:/i.test(uri)) return undefined
34
+ return typeof value === 'string' ? { uri } : (value as PlaceholderSource)
18
35
  }
19
36
 
20
37
  // Whether a source has a thumbhash or blurDataUri, or `placeholder` is one
@@ -37,7 +54,7 @@ export function dataUri(value: unknown): string | undefined {
37
54
  if (typeof __DEV__ !== 'undefined' && __DEV__) {
38
55
  warnOnce(
39
56
  'placeholder-url',
40
- `react-native-fast-image: \`placeholder\` and \`blurDataUri\` don't load urls: "${value.slice(0, 60)}" is ignored. Use a data uri (data:image/…), or for \`placeholder\`, a require()d image or the name of an image bundled with the app.`,
57
+ `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\`.`,
41
58
  'warn',
42
59
  )
43
60
  }
@@ -45,8 +45,10 @@ export interface NativeProps extends ViewProps {
45
45
  source?: CodegenTypes.UnsafeMixed
46
46
  // Several sources (sizes of one image), picked from for the view's size.
47
47
  sources?: CodegenTypes.UnsafeMixed
48
- // The `placeholder` prop: a resolved asset source ({ uri, width, height,
49
- // scale }) or a data uri ({ uri } on iOS; the uri on Android).
48
+ // The `placeholder` prop: a url to load apart from the image, as a source
49
+ // ({ uri, headers, cacheKey, cache }), or an image shown at once: a
50
+ // resolved asset source ({ uri, width, height, scale }) or a data uri
51
+ // ({ uri } on iOS; the uri on Android, as a string).
50
52
  placeholder?: CodegenTypes.UnsafeMixed
51
53
  // What shows in place of placeholder while a source loads: its ThumbHash,
52
54
  // or its blurDataUri (blurred natively), by the source's index ('' for