react-native-fast-image 10.1.1 → 10.3.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.
Files changed (35) hide show
  1. package/README.md +107 -14
  2. package/android/src/main/java/com/fastimage/FastImageBlur.java +3 -2
  3. package/android/src/main/java/com/fastimage/FastImageThumbHash.java +246 -0
  4. package/android/src/main/java/com/fastimage/FastImageViewConverter.java +1 -1
  5. package/android/src/main/java/com/fastimage/FastImageViewManager.java +20 -5
  6. package/android/src/main/java/com/fastimage/FastImageViewWithUrl.java +125 -48
  7. package/dist/index.cjs.js +47 -9
  8. package/dist/index.cjs.js.flow +11 -0
  9. package/dist/index.cjs.web.js +67 -14
  10. package/dist/index.cjs.web.js.flow +11 -0
  11. package/dist/index.d.ts +77 -13
  12. package/dist/index.js +48 -10
  13. package/dist/index.js.flow +11 -0
  14. package/dist/index.web.js +68 -15
  15. package/dist/index.web.js.flow +11 -0
  16. package/dist/placeholders.d.ts +4 -0
  17. package/dist/specs/FastImageViewNativeComponent.d.ts +2 -1
  18. package/dist/thumbhash.cjs.js +241 -0
  19. package/dist/thumbhash.d.ts +12 -0
  20. package/dist/thumbhash.js +240 -0
  21. package/ios/FastImage/FFFBoxBlur.h +7 -0
  22. package/ios/FastImage/FFFBoxBlur.m +29 -0
  23. package/ios/FastImage/FFFThumbHash.h +24 -0
  24. package/ios/FastImage/FFFThumbHash.m +269 -0
  25. package/ios/FastImage/FFFastImageView.h +7 -1
  26. package/ios/FastImage/FFFastImageView.m +93 -46
  27. package/ios/FastImage/FFFastImageViewComponentView.mm +11 -4
  28. package/package.json +4 -2
  29. package/src/index.js.flow +11 -0
  30. package/src/index.tsx +166 -31
  31. package/src/index.web.tsx +166 -33
  32. package/src/placeholders.ts +45 -0
  33. package/src/specs/FastImageViewNativeComponent.ts +8 -2
  34. package/src/thumbhash.ts +363 -0
  35. package/thumbhash/package.json +8 -0
@@ -61,7 +61,8 @@ import javax.annotation.Nonnull;
61
61
  class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEventsView, ReactHitSlopView {
62
62
  private boolean mNeedsReload = false;
63
63
  private ReadableMap mSource = null;
64
- private Drawable mDefaultSource = null;
64
+ // The `placeholder` prop.
65
+ private Drawable mPlaceholder = null;
65
66
 
66
67
  // Null when the view was created in a destroyed Activity (nothing loads).
67
68
  @Nullable
@@ -307,9 +308,74 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
307
308
  return true;
308
309
  }
309
310
 
310
- public void setDefaultSource(@Nullable Drawable source) {
311
+ public void setPlaceholder(@Nullable Drawable placeholder) {
311
312
  mNeedsReload = true;
312
- mDefaultSource = source;
313
+ mPlaceholder = placeholder;
314
+ }
315
+
316
+ // The `sourcePlaceholders` prop, by the source's index ("" for none; one
317
+ // for a single source): the picked source's ThumbHash or blurDataUri (a
318
+ // data uri) shows instead of the placeholder prop. JS sends them apart from
319
+ // the sources, so a new one for the same image doesn't reload it.
320
+ @Nullable
321
+ private ReadableArray mSourcePlaceholders = null;
322
+ // What shows while the image loads and if it fails: the picked source's
323
+ // ThumbHash or blurDataUri decoded (FastImageThumbHash), or the placeholder
324
+ // prop. FastImage's own drawable either way, which Glide doesn't recycle.
325
+ @Nullable
326
+ private Drawable mShownPlaceholder = null;
327
+ // The source placeholder mShownPlaceholder shows, or null for the
328
+ // placeholder prop.
329
+ @Nullable
330
+ private String mPlaceholderKey = null;
331
+ // The last source placeholder that failed to decode, so it isn't decoded
332
+ // again on every update.
333
+ @Nullable
334
+ private String mInvalidPlaceholder = null;
335
+
336
+ public void setSourcePlaceholders(@Nullable ReadableArray placeholders) {
337
+ mSourcePlaceholders = placeholders;
338
+ updatePlaceholder();
339
+ }
340
+
341
+ // The picked source's ThumbHash or blurDataUri, or null.
342
+ @Nullable
343
+ private String pickedSourcePlaceholder() {
344
+ int index = mSources == null ? 0 : mSourceIndex;
345
+ String key = mSourcePlaceholders != null && index >= 0 && index < mSourcePlaceholders.size()
346
+ && !mSourcePlaceholders.isNull(index) ? mSourcePlaceholders.getString(index) : null;
347
+ return key == null || key.isEmpty() ? null : key;
348
+ }
349
+
350
+ // Sets mShownPlaceholder for the picked source. A view showing the previous
351
+ // placeholder shows the new one at once, without reloading the image.
352
+ private void updatePlaceholder() {
353
+ String key = pickedSourcePlaceholder();
354
+ Drawable placeholder;
355
+ if (key != null && key.equals(mPlaceholderKey)) {
356
+ placeholder = mShownPlaceholder;
357
+ } else if (key != null && key.equals(mInvalidPlaceholder)) {
358
+ placeholder = mPlaceholder;
359
+ mPlaceholderKey = null;
360
+ } else {
361
+ Bitmap bitmap = FastImageThumbHash.placeholder(key);
362
+ if (bitmap != null) {
363
+ BitmapDrawable drawable = new BitmapDrawable(getResources(), bitmap);
364
+ // Scaled up smoothly, as a blurred preview (also with
365
+ // imageRendering pixelated, which is for the image).
366
+ drawable.setFilterBitmap(true);
367
+ placeholder = drawable;
368
+ mPlaceholderKey = key;
369
+ } else {
370
+ placeholder = mPlaceholder;
371
+ mPlaceholderKey = null;
372
+ mInvalidPlaceholder = key;
373
+ }
374
+ }
375
+ if (placeholder == mShownPlaceholder) return;
376
+ boolean showing = mShowsPlaceholder;
377
+ mShownPlaceholder = placeholder;
378
+ if (showing) setImageDrawable(placeholder);
313
379
  }
314
380
 
315
381
  // The events JS has a handler for (the handledEvents prop): the view only
@@ -482,7 +548,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
482
548
  private ValueAnimator mFade;
483
549
  @Nullable
484
550
  private Bitmap mFadeFrom;
485
- // Or, instead of a copy, the defaultSource the view showed, with the
551
+ // Or, instead of a copy, the placeholder prop the view showed, with the
486
552
  // matrix it drew it with: FastImage's own drawable, which Glide doesn't
487
553
  // recycle, so it's drawn again as it was, rather than copied into a
488
554
  // bitmap the size of the view for every image that fades in over it.
@@ -508,7 +574,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
508
574
  Drawable fromDrawable = null;
509
575
  Matrix fromMatrix = null;
510
576
  Drawable shown = getDrawable();
511
- if (shown != null && mShowsDefaultSource && mFade == null) {
577
+ if (shown != null && mShowsPlaceholder && mFade == null) {
512
578
  // Its own state, so its alpha (see drawFadeFrom) is only its own.
513
579
  fromDrawable = shown.mutate();
514
580
  fromMatrix = new Matrix(getImageMatrix());
@@ -564,7 +630,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
564
630
  invalidate();
565
631
  }
566
632
 
567
- // Draws the defaultSource the view showed when the fade started (with
633
+ // Draws the placeholder prop the view showed when the fade started (with
568
634
  // `alpha`), as ImageView draws its drawable: inside the padding
569
635
  // (cropToPadding), with the matrix it had then.
570
636
  private void drawFadeFrom(Canvas canvas, Drawable drawable, int alpha) {
@@ -1001,14 +1067,14 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1001
1067
  : null;
1002
1068
  }
1003
1069
 
1004
- // Whether the image on screen is a cover one Glide cropped to what the
1005
- // view shows (CenterCrop or FastImageCrop, at the position it was loaded
1006
- // for). Glide can't crop animated WebPs and AVIFs, APNGs and
1007
- // defaultSource, and doesn't crop pixelated images or ones decoded at full
1008
- // size (downsample false).
1070
+ // Whether the image on screen is a cover one Glide cropped to what the view
1071
+ // shows (CenterCrop or FastImageCrop, at the position it was loaded for).
1072
+ // Glide can't crop animated WebPs and AVIFs, APNGs and the placeholder
1073
+ // prop, and doesn't crop pixelated images or ones decoded at full size
1074
+ // (downsample false).
1009
1075
  private boolean shownCropped(Drawable drawable) {
1010
1076
  return mLayoutScaleType == ScaleType.CENTER_CROP && !mLayoutPixelated && mLayoutDownsampled
1011
- && drawable != mDefaultSource
1077
+ && drawable != mShownPlaceholder
1012
1078
  && (drawable instanceof BitmapDrawable || drawable instanceof GifDrawable);
1013
1079
  }
1014
1080
 
@@ -1025,13 +1091,14 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1025
1091
  updateImageMatrix();
1026
1092
  }
1027
1093
 
1028
- // Whether the view shows its defaultSource: FastImage's own drawable,
1029
- // which Glide doesn't recycle (see startFade).
1030
- private boolean mShowsDefaultSource = false;
1094
+ // Whether the view shows its placeholder (the placeholder prop, or a
1095
+ // source's): FastImage's own drawable, which Glide doesn't recycle (see
1096
+ // startFade).
1097
+ private boolean mShowsPlaceholder = false;
1031
1098
 
1032
1099
  @Override
1033
1100
  public void setImageDrawable(@Nullable Drawable drawable) {
1034
- mShowsDefaultSource = drawable != null && drawable == mDefaultSource;
1101
+ mShowsPlaceholder = drawable != null && drawable == mShownPlaceholder;
1035
1102
  super.setImageDrawable(drawable);
1036
1103
  updateImageMatrix();
1037
1104
  }
@@ -1082,7 +1149,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1082
1149
  // edges at (view - image) * fraction + offset. Glide decodes a scale-down
1083
1150
  // image no larger than that (CenterInside), a none one at its size, and
1084
1151
  // crops a cover one to what the view shows (FastImageCrop), so it's drawn
1085
- // at this size. defaultSource (a resource) is already at its size on
1152
+ // at this size. The placeholder prop (a resource) is already at its size on
1086
1153
  // screen, and so is a center image (its size in pixels, as ImageView's
1087
1154
  // CENTER_INSIDE draws it).
1088
1155
  private void updateImageMatrix() {
@@ -1116,13 +1183,17 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1116
1183
  setImageMatrix(matrix);
1117
1184
  return;
1118
1185
  }
1119
- if (mLayoutScaleType == ScaleType.CENTER_CROP || mLayoutScaleType == ScaleType.FIT_CENTER) {
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;
1190
+ if (mLayoutScaleType == ScaleType.CENTER_CROP || mLayoutScaleType == ScaleType.FIT_CENTER || hashFits) {
1120
1191
  float x = viewWidth / drawableWidth;
1121
1192
  float y = viewHeight / drawableHeight;
1122
1193
  float scale = mLayoutScaleType == ScaleType.CENTER_CROP ? Math.max(x, y) : Math.min(x, y);
1123
1194
  width = drawableWidth * scale;
1124
1195
  height = drawableHeight * scale;
1125
- } else if (drawable == mDefaultSource) {
1196
+ } else if (drawable == mShownPlaceholder) {
1126
1197
  width = drawableWidth;
1127
1198
  height = drawableHeight;
1128
1199
  } else if (mImageWidth > 0 && mImageHeight > 0) {
@@ -1239,12 +1310,12 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1239
1310
  private boolean mRestarting = false;
1240
1311
 
1241
1312
  // The request for the image the view shows (from when it has loaded until
1242
- // another image or defaultSource replaces it), and the one loading. A new
1243
- // source starts with the shown one as a thumbnail, from the cache only, so
1244
- // the image stays until the new one has loaded instead of flashing blank
1245
- // (#747), also if the source changes again meanwhile. This is Glide's safe
1246
- // way to do that: the previous bitmap can't be kept in the view itself,
1247
- // since Glide reuses it once its request is cleared.
1313
+ // another image or the placeholder prop replaces it), and the one loading.
1314
+ // A new source starts with the shown one as a thumbnail, from the cache
1315
+ // only, so the image stays until the new one has loaded instead of flashing
1316
+ // blank (#747), also if the source changes again meanwhile. This is Glide's
1317
+ // safe way to do that: the previous bitmap can't be kept in the view
1318
+ // itself, since Glide reuses it once its request is cleared.
1248
1319
  @Nullable
1249
1320
  private RequestBuilder<Drawable> mShownRequest;
1250
1321
  @Nullable
@@ -1270,8 +1341,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1270
1341
  mRecyclingKey = recyclingKey;
1271
1342
  if (changed) {
1272
1343
  // No thumbnail of the current image, so Glide clears the view to
1273
- // defaultSource (or nothing) while the next one loads, nor a fade
1274
- // from it. Reload even if the source is the same.
1344
+ // the placeholder prop (or nothing) while the next one loads, nor a
1345
+ // fade from it. Reload even if the source is the same.
1275
1346
  mShownRequest = null;
1276
1347
  endFade();
1277
1348
  mNeedsReload = true;
@@ -1356,12 +1427,12 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1356
1427
  // reloadForSize does: it's decoded again from the cache and blurred or
1357
1428
  // fitted on Glide's threads, and the view keeps the current image (and its
1358
1429
  // layout, see onImageLoaded) until then. If that fails, it shows the image
1359
- // from Glide's caches; cache: 'web' skips them, so the view shows
1360
- // defaultSource (or nothing) then. Also while it's loading again that way
1361
- // (e.g. the blur changes every frame): that load is replaced. A load that
1362
- // hasn't finished starts again with the new blur, without a second
1363
- // onLoadStart. Nothing loaded (defaultSource, which isn't blurred, or
1364
- // nothing): the next load has the new blur.
1430
+ // from Glide's caches; cache: 'web' skips them, so the view shows the
1431
+ // placeholder prop (or nothing) then. Also while it's loading again that
1432
+ // way (e.g. the blur changes every frame): that load is replaced. A load
1433
+ // that hasn't finished starts again with the new blur, without a second
1434
+ // onLoadStart. Nothing loaded (the placeholder prop, which isn't blurred,
1435
+ // or nothing): the next load has the new blur.
1365
1436
  @SuppressLint("CheckResult")
1366
1437
  private void reblur() {
1367
1438
  if (requestManager == null || mBaseRequest == null) return;
@@ -1445,20 +1516,24 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1445
1516
  mLoadEnded = true;
1446
1517
  }
1447
1518
 
1448
- // The loading image failed (FastImageRequestListener). Glide shows
1449
- // defaultSource then, but not over a thumbnail: show it here, as iOS does.
1519
+ // The loading image failed (FastImageRequestListener). Glide shows the
1520
+ // placeholder prop then, but not over a thumbnail: show it here, as iOS
1521
+ // does.
1450
1522
  void onImageFailed(boolean hadThumbnail) {
1451
1523
  mLoadEnded = true;
1452
1524
  endProgress();
1453
1525
  applyObjectFit();
1454
1526
  mShownRequest = null;
1455
- if (!hadThumbnail) return;
1456
1527
  final int load = mLoadCount;
1457
- // Not from within Glide's callback.
1528
+ // Not from within Glide's callback. Without a thumbnail Glide shows the
1529
+ // placeholder the request started with: a source placeholder that changed since
1530
+ // shows the new one.
1458
1531
  post(new Runnable() {
1459
1532
  @Override
1460
1533
  public void run() {
1461
- if (load == mLoadCount) setImageDrawable(mDefaultSource);
1534
+ if (load == mLoadCount && (hadThumbnail || getDrawable() != mShownPlaceholder)) {
1535
+ setImageDrawable(mShownPlaceholder);
1536
+ }
1462
1537
  }
1463
1538
  });
1464
1539
  }
@@ -1499,8 +1574,9 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1499
1574
  mWaitedForSize = false;
1500
1575
  pickSource();
1501
1576
  }
1577
+ updatePlaceholder();
1502
1578
  // Only reload for changes that affect the request (source,
1503
- // defaultSource, objectFit), not for every prop update.
1579
+ // the placeholder prop, objectFit), not for every prop update.
1504
1580
  mNeedsReload = false;
1505
1581
  mBlurChanged = false;
1506
1582
  mFitChanged = false;
@@ -1520,7 +1596,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1520
1596
  mModel = null;
1521
1597
 
1522
1598
  // Nothing to show.
1523
- if (mSource == null && mDefaultSource == null) {
1599
+ if (mSource == null && mShownPlaceholder == null) {
1524
1600
  mShownRequest = null;
1525
1601
  // Cancel existing requests.
1526
1602
  clearView(requestManager);
@@ -1541,8 +1617,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1541
1617
  : null;
1542
1618
 
1543
1619
  // A source without a uri (empty, missing or null), or one that can't be
1544
- // resolved: fire onError and onLoadEnd and show defaultSource, as on iOS
1545
- // (#1028, #945).
1620
+ // resolved: fire onError and onLoadEnd and show the placeholder prop,
1621
+ // as on iOS (#1028, #945).
1546
1622
  if (mSource != null && (imageSource == null || imageSource.getUri().toString().length() == 0)) {
1547
1623
  String error = "Invalid source: " + mSource;
1548
1624
  WritableMap event = new WritableNativeMap();
@@ -1555,12 +1631,13 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1555
1631
  clearView(requestManager);
1556
1632
 
1557
1633
  endProgress();
1558
- setImageDrawable(mDefaultSource);
1634
+ setImageDrawable(mShownPlaceholder);
1559
1635
  return;
1560
1636
  }
1561
1637
 
1562
- // `imageSource` may be null and we still continue, if `defaultSource` is not null.
1563
- // What Glide loads: a FastImageUrl for a remote image.
1638
+ // `imageSource` may be null and we still continue, if the placeholder
1639
+ // prop is not null. What Glide loads: a FastImageUrl for a remote
1640
+ // image.
1564
1641
  Object model = imageSource == null ? null : imageSource.getSourceForLoad();
1565
1642
 
1566
1643
  // Before the view clears (see shownCopy).
@@ -1600,8 +1677,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1600
1677
  .load(model)
1601
1678
  .apply(FastImageViewConverter
1602
1679
  .getOptions(getContext(), imageSource, mSource)
1603
- .placeholder(mDefaultSource) // show until loaded
1604
- .fallback(mDefaultSource)); // null will not be treated as error
1680
+ .placeholder(mShownPlaceholder) // show until loaded
1681
+ .fallback(mShownPlaceholder)); // null will not be treated as error
1605
1682
  mBaseRequest = model == null ? null : base;
1606
1683
  mModel = model;
1607
1684
  // What into() would apply for the scale type, with the size
@@ -1609,7 +1686,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1609
1686
  RequestBuilder<Drawable> builder = base.clone().apply(renderingOptions(model));
1610
1687
  RequestBuilder<Drawable> request = builder.clone();
1611
1688
 
1612
- // Only defaultSource: it replaces the image.
1689
+ // Only the placeholder prop: it replaces the image.
1613
1690
  if (model == null) {
1614
1691
  meanwhile = null;
1615
1692
  mShownRequest = null;
package/dist/index.cjs.js CHANGED
@@ -260,10 +260,39 @@ var FastImageViewNativeComponent_default = (0, react_native.codegenNativeCompone
260
260
  //#region src/specs/NativeFastImageModule.ts
261
261
  var NativeFastImageModule_default = react_native.TurboModuleRegistry.get("FastImageModule");
262
262
  //#endregion
263
+ //#region src/placeholders.ts
264
+ function isImageName(value) {
265
+ return !/^[a-z][a-z\d+.-]*:/i.test(value) && !value.startsWith("/");
266
+ }
267
+ function hasSourcePlaceholder(value) {
268
+ return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
269
+ }
270
+ function dataUri(value) {
271
+ if (typeof value !== "string" || value === "") return void 0;
272
+ 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");
274
+ }
275
+ //#endregion
263
276
  //#region src/index.tsx
264
- const resolveDefaultSource = (defaultSource) => {
265
- if (!defaultSource) return null;
266
- const resolved = react_native.Image.resolveAssetSource(defaultSource);
277
+ function withoutPlaceholders(source) {
278
+ if (!hasSourcePlaceholder(source)) return source;
279
+ const { thumbhash: _thumbhash, blurDataUri: _blurDataUri, ...rest } = source;
280
+ return rest;
281
+ }
282
+ function sourcePlaceholder(source) {
283
+ if (!source || typeof source !== "object") return "";
284
+ const { thumbhash, blurDataUri } = source;
285
+ return thumbhash || dataUri(blurDataUri) || "";
286
+ }
287
+ const resolvePlaceholder = (placeholder) => {
288
+ if (typeof placeholder === "string") {
289
+ if (isImageName(placeholder)) return react_native.Platform.OS === "android" ? placeholder : { uri: placeholder };
290
+ const uri = dataUri(placeholder);
291
+ if (!uri) return null;
292
+ return react_native.Platform.OS === "android" ? uri : { uri };
293
+ }
294
+ if (!placeholder) return null;
295
+ const resolved = react_native.Image.resolveAssetSource(placeholder);
267
296
  if (!resolved) return null;
268
297
  return react_native.Platform.OS === "android" ? resolved.uri : resolved;
269
298
  };
@@ -318,7 +347,7 @@ function builtInTransition() {
318
347
  return resolveTransition(react_native.Platform.OS === "android", NO_TRANSITION);
319
348
  }
320
349
  let defaultTransition;
321
- function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, forwardedRef, ...viewProps }) {
350
+ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, loading: _loading, sizes: _sizes, forwardedRef, ...viewProps }) {
322
351
  var _props$recyclingKey, _defaultTransition;
323
352
  const { fallback, resizeMode: _resizeMode, ...props } = viewProps;
324
353
  if (__DEV__ && fallback !== void 0) warnOnce("fallback", "react-native-fast-image: `fallback` was removed in 10.0, and does nothing: images always load natively. Remove it, or use React Native's Image for those images.", "warn");
@@ -326,10 +355,18 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
326
355
  const resolvedTintColor = tintColor != null ? tintColor : fromStyle(style, "tintColor");
327
356
  const fit = resolveObjectFit(objectFit, style);
328
357
  const position = objectPositionOrCentered(objectPosition);
329
- const sources = Array.isArray(source) ? source.length === 1 ? void 0 : source : void 0;
330
- const single = Array.isArray(source) ? source[0] : source;
331
- const resolvedSource = sources ? void 0 : react_native.Image.resolveAssetSource(single);
332
- const resolvedDefaultSource = resolveDefaultSource(defaultSource);
358
+ const shared = hasSourcePlaceholder(placeholder) ? sourcePlaceholder(placeholder) : "";
359
+ const { resolvedSource, sources, sourcePlaceholders } = (0, react.useMemo)(() => {
360
+ const sources = Array.isArray(source) ? source.length === 1 ? void 0 : source : void 0;
361
+ const single = Array.isArray(source) ? source[0] : source;
362
+ const placeholders = (sources !== null && sources !== void 0 ? sources : [single]).map((entry) => sourcePlaceholder(entry) || shared);
363
+ return {
364
+ resolvedSource: sources ? void 0 : react_native.Image.resolveAssetSource(withoutPlaceholders(single)),
365
+ sources: (sources === null || sources === void 0 ? void 0 : sources.some(hasSourcePlaceholder)) ? sources.map(withoutPlaceholders) : sources,
366
+ sourcePlaceholders: placeholders.some(Boolean) ? placeholders : void 0
367
+ };
368
+ }, [source, shared]);
369
+ const resolvedPlaceholder = resolvePlaceholder(hasSourcePlaceholder(placeholder) ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
333
370
  return /* @__PURE__ */ react.default.createElement(FastImageViewNativeComponent_default, {
334
371
  ...props,
335
372
  ref: forwardedRef,
@@ -339,7 +376,8 @@ function FastImageBase({ source, defaultSource, tintColor, onLoadStart, onProgre
339
376
  ...resolveTransition(transition, (_defaultTransition = defaultTransition) !== null && _defaultTransition !== void 0 ? _defaultTransition : builtInTransition()),
340
377
  source: resolvedSource,
341
378
  sources,
342
- defaultSource: resolvedDefaultSource,
379
+ sourcePlaceholders,
380
+ placeholder: resolvedPlaceholder,
343
381
  onFastImageLoadStart: onLoadStart,
344
382
  onFastImageProgress: onProgress,
345
383
  handledEvents: handledEvents({
@@ -110,6 +110,11 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
110
110
  export type PreloadFn = (
111
111
  sources: Array<FastImageSource>,
112
112
  ) => Promise<Array<PreloadResult>>
113
+ export type SourcePlaceholder = {
114
+ thumbhash?: string,
115
+ blurDataUri?: string,
116
+ }
117
+
113
118
  export type FastImageSource = {
114
119
  uri?: string,
115
120
  headers?: Object,
@@ -118,6 +123,8 @@ export type FastImageSource = {
118
123
  cacheKey?: string,
119
124
  memoryCache?: boolean,
120
125
  diskCache?: boolean,
126
+ thumbhash?: string,
127
+ blurDataUri?: string,
121
128
  width?: number,
122
129
  height?: number,
123
130
  scale?: number,
@@ -132,6 +139,8 @@ export type FastImageProps = $ReadOnly<{|
132
139
  onProgress?: ?(event: OnProgressEvent) => void,
133
140
 
134
141
  source?: ?(FastImageSource | Array<FastImageSource> | number),
142
+ placeholder?: ?(number | string | SourcePlaceholder),
143
+ // Deprecated: use placeholder, which is the same.
135
144
  defaultSource?: ?number,
136
145
 
137
146
  tintColor?: number | string,
@@ -140,6 +149,8 @@ export type FastImageProps = $ReadOnly<{|
140
149
  recyclingKey?: ?string,
141
150
  loop?: ?(boolean | number),
142
151
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
152
+ loading?: ?('lazy' | 'eager'),
153
+ sizes?: ?string,
143
154
  paused?: ?boolean,
144
155
  transition?: ?(boolean | number | Transition),
145
156
  downsample?: ?boolean,
@@ -267,17 +267,24 @@ function cssPosition({ x, y }) {
267
267
  return `${axis(x)} ${axis(y)}`;
268
268
  }
269
269
  //#endregion
270
+ //#region src/placeholders.ts
271
+ function hasSourcePlaceholder(value) {
272
+ return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
273
+ }
274
+ function dataUri(value) {
275
+ if (typeof value !== "string" || value === "") return void 0;
276
+ 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");
278
+ }
279
+ //#endregion
270
280
  //#region src/index.web.tsx
271
- var _useId;
272
281
  const notSupported = {
273
282
  ok: false,
274
283
  error: "Not supported on the web"
275
284
  };
276
- let ids = 0;
277
- const useCounterId = () => (0, react.useState)(() => `${++ids}`)[0];
278
- const useSvgId = (_useId = react.default.useId) !== null && _useId !== void 0 ? _useId : useCounterId;
279
285
  const useClientLayoutEffect = globalThis.document === void 0 ? react.useEffect : react.useLayoutEffect;
280
- function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
286
+ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loading, sizes, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
287
+ var _resolveUri;
281
288
  if (typeof __DEV__ !== "undefined" && __DEV__) warnIfChildren(children);
282
289
  const { accessibilityLabel, "aria-label": ariaLabel, resizeMode: _resizeMode, ...viewProps } = props;
283
290
  const label = ariaLabel || accessibilityLabel || void 0;
@@ -286,10 +293,15 @@ function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadSta
286
293
  const src = sized ? sized.src : resolveUri(source);
287
294
  const srcSet = sized === null || sized === void 0 ? void 0 : sized.srcSet;
288
295
  const key = srcSet !== null && srcSet !== void 0 ? srcSet : src;
289
- const defaultUri = resolveUri(defaultSource);
296
+ const priority = sourcePriority(source);
297
+ const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
298
+ 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);
290
300
  const [result, setResult] = (0, react.useState)();
291
301
  const ok = result && result.key === key ? result.ok : void 0;
292
- const tintId = `fast-image-tint-${useSvgId().replace(/[^\w-]/g, "")}`;
302
+ const svgId = (0, react.useId)().replace(/[^\w-]/g, "");
303
+ const tintId = `fast-image-tint-${svgId}`;
304
+ const blurId = `fast-image-blur-${svgId}`;
293
305
  const fit = resolveObjectFit(objectFit, style);
294
306
  const position = cssPosition(objectPositionOrCentered(objectPosition));
295
307
  const filter = [blurRadius ? `blur(${blurRadius}px)` : "", tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") || void 0;
@@ -307,15 +319,19 @@ function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadSta
307
319
  }, defaultUri && showDefault ? /* @__PURE__ */ react.default.createElement(Picture, {
308
320
  key: `default ${defaultUri}`,
309
321
  src: defaultUri,
310
- fit,
322
+ loading,
323
+ priority,
324
+ fit: blurUri && (fit === "scale-down" || fit === "none") ? "contain" : fit,
311
325
  position,
312
- filter,
326
+ filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
313
327
  alt: key ? "" : label !== null && label !== void 0 ? label : ""
314
328
  }) : null, key && src ? /* @__PURE__ */ react.default.createElement(Picture, {
315
329
  key: several ? "sizes" : key,
316
330
  src,
317
331
  srcSet,
318
- lazy: !!several,
332
+ sizes,
333
+ loading,
334
+ priority,
319
335
  fit,
320
336
  position,
321
337
  filter,
@@ -352,7 +368,29 @@ function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadSta
352
368
  }) : null, tintColor ? /* @__PURE__ */ react.default.createElement(TintFilter, {
353
369
  id: tintId,
354
370
  color: tintColor
355
- }) : null);
371
+ }) : null, blurUri && showDefault ? /* @__PURE__ */ react.default.createElement(PreviewBlur, { id: blurId }) : null);
372
+ }
373
+ function thumbhashUri(hash) {
374
+ if (!hash) return void 0;
375
+ const decode = globalThis[Symbol.for("react-native-fast-image/thumbhash")];
376
+ if (decode) return decode(hash);
377
+ if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("thumbhash-web", "react-native-fast-image: on the web, a `thumbhash` shows only with `import 'react-native-fast-image/thumbhash'` (it decodes them). Or make it a `blurDataUri` before it reaches the browser (e.g. on your server) with that module's thumbHashToBlurDataUri, which needs no JavaScript in the browser.", "warn");
378
+ }
379
+ function sourcePriority(source) {
380
+ const values = (Array.isArray(source) ? source : [source]).map((entry) => entry && typeof entry === "object" ? entry.priority : void 0);
381
+ return [
382
+ "high",
383
+ "normal",
384
+ "low"
385
+ ].find((priority) => values.includes(priority));
386
+ }
387
+ function shared(source, key, preview) {
388
+ const values = (Array.isArray(source) ? source : [source]).map((entry) => {
389
+ var _ref;
390
+ const own = entry && typeof entry === "object" ? entry : void 0;
391
+ return (_ref = (own === null || own === void 0 ? void 0 : own.thumbhash) || dataUri(own === null || own === void 0 ? void 0 : own.blurDataUri) ? own : preview) === null || _ref === void 0 ? void 0 : _ref[key];
392
+ });
393
+ return values.every((value) => value && value === values[0]) ? values[0] : void 0;
356
394
  }
357
395
  function resolveUri(source) {
358
396
  let uri;
@@ -400,7 +438,7 @@ const fill = {
400
438
  width: "100%",
401
439
  height: "100%"
402
440
  };
403
- function Picture({ src, srcSet, lazy, fit, position, filter, hidden, alt, onLoad, onError }) {
441
+ function Picture({ src, srcSet, sizes, loading, priority, fit, position, filter, hidden, alt, onLoad, onError }) {
404
442
  const image = (0, react.useRef)(null);
405
443
  const mounted = (0, react.useRef)(true);
406
444
  const handled = (0, react.useRef)(void 0);
@@ -437,8 +475,9 @@ function Picture({ src, srcSet, lazy, fit, position, filter, hidden, alt, onLoad
437
475
  ref: image,
438
476
  src,
439
477
  srcSet,
440
- sizes: srcSet ? "auto, 100vw" : void 0,
441
- loading: lazy ? "lazy" : void 0,
478
+ sizes: srcSet ? sizes !== null && sizes !== void 0 ? sizes : "auto, 100vw" : void 0,
479
+ loading: loading === "eager" ? void 0 : "lazy",
480
+ fetchPriority: priority === "high" || priority === "low" ? priority : void 0,
442
481
  alt,
443
482
  draggable: false,
444
483
  style: {
@@ -464,6 +503,20 @@ function TintFilter({ id, color }) {
464
503
  operator: "in"
465
504
  }))));
466
505
  }
506
+ function PreviewBlur({ id }) {
507
+ return /* @__PURE__ */ react.default.createElement("svg", { style: tintStyle }, /* @__PURE__ */ react.default.createElement("defs", null, /* @__PURE__ */ react.default.createElement("filter", {
508
+ id,
509
+ x: "0",
510
+ y: "0",
511
+ width: "1",
512
+ height: "1",
513
+ primitiveUnits: "objectBoundingBox",
514
+ colorInterpolationFilters: "sRGB"
515
+ }, /* @__PURE__ */ react.default.createElement("feGaussianBlur", { stdDeviation: "0.06" }), /* @__PURE__ */ react.default.createElement("feComponentTransfer", null, /* @__PURE__ */ react.default.createElement("feFuncA", {
516
+ type: "table",
517
+ tableValues: "0 1 1 1 1"
518
+ })))));
519
+ }
467
520
  const tintStyle = {
468
521
  position: "absolute",
469
522
  width: 0,
@@ -110,6 +110,11 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
110
110
  export type PreloadFn = (
111
111
  sources: Array<FastImageSource>,
112
112
  ) => Promise<Array<PreloadResult>>
113
+ export type SourcePlaceholder = {
114
+ thumbhash?: string,
115
+ blurDataUri?: string,
116
+ }
117
+
113
118
  export type FastImageSource = {
114
119
  uri?: string,
115
120
  headers?: Object,
@@ -118,6 +123,8 @@ export type FastImageSource = {
118
123
  cacheKey?: string,
119
124
  memoryCache?: boolean,
120
125
  diskCache?: boolean,
126
+ thumbhash?: string,
127
+ blurDataUri?: string,
121
128
  width?: number,
122
129
  height?: number,
123
130
  scale?: number,
@@ -132,6 +139,8 @@ export type FastImageProps = $ReadOnly<{|
132
139
  onProgress?: ?(event: OnProgressEvent) => void,
133
140
 
134
141
  source?: ?(FastImageSource | Array<FastImageSource> | number),
142
+ placeholder?: ?(number | string | SourcePlaceholder),
143
+ // Deprecated: use placeholder, which is the same.
135
144
  defaultSource?: ?number,
136
145
 
137
146
  tintColor?: number | string,
@@ -140,6 +149,8 @@ export type FastImageProps = $ReadOnly<{|
140
149
  recyclingKey?: ?string,
141
150
  loop?: ?(boolean | number),
142
151
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
152
+ loading?: ?('lazy' | 'eager'),
153
+ sizes?: ?string,
143
154
  paused?: ?boolean,
144
155
  transition?: ?(boolean | number | Transition),
145
156
  downsample?: ?boolean,