react-native-fast-image 10.5.0 → 10.6.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
@@ -107,7 +107,7 @@ It's a single native view, which also takes React Native's View props, such as a
107
107
 
108
108
  #### `source`
109
109
 
110
- **Type:** `number | Source | Source[]`
110
+ **Type:** `number | Source | (number | Source)[]`
111
111
 
112
112
  Source for the remote image to load: a `Source`, a `require()`d image, or an array of the same image at different sizes.
113
113
 
@@ -129,6 +129,7 @@ When `source` changes, the image that's showing stays until the new one has load
129
129
  - When the view's size changes so that another size fits better, it loads that one (with its load events), and keeps showing the current image until then, without a `transition`.
130
130
  - Each size is cached separately. A `cacheKey` applies to its own entry: give each size its own key (or none), or the sizes would replace each other in the cache.
131
131
  - `FastImage.preload`, `getCachePath` and `writeToCache` take one size: pass the one a view will show. An array fails with `{ ok: false, error }`.
132
+ - A `require()`d image can be one of the sizes: its size is the file's.
132
133
  - An array of one is the same as that source.
133
134
 
134
135
  ---
@@ -291,7 +292,7 @@ Notes:
291
292
 
292
293
  **Type:** `number`
293
294
 
294
- **Deprecated:** Use `placeholder`, which does the same.
295
+ **Deprecated:** Use `placeholder`, which does the same. With both, `placeholder` shows.
295
296
 
296
297
  ---
297
298
 
@@ -21,6 +21,7 @@ import com.bumptech.glide.RequestManager;
21
21
  import com.facebook.react.bridge.ColorPropConverter;
22
22
  import com.facebook.react.bridge.Dynamic;
23
23
  import com.facebook.react.bridge.DynamicFromObject;
24
+ import com.facebook.react.bridge.JavaOnlyArray;
24
25
  import com.facebook.react.bridge.ReadableArray;
25
26
  import com.facebook.react.bridge.ReadableMap;
26
27
  import com.facebook.react.bridge.ReadableType;
@@ -307,23 +308,26 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl>
307
308
 
308
309
  @Override
309
310
  public void setSource(FastImageViewWithUrl view, Dynamic source) {
310
- view.setSource(source.isNull() ? null : source.asMap());
311
+ // One source, or several (an array).
312
+ view.setSource(source.isNull() ? null
313
+ : source.getType() == ReadableType.Array ? source.asArray()
314
+ : JavaOnlyArray.of(source.asMap()));
311
315
  }
312
316
 
313
- @Override
314
- public void setSources(FastImageViewWithUrl view, Dynamic sources) {
315
- view.setSources(sources.isNull() ? null : sources.asArray());
316
- }
317
-
318
- // The `placeholder` prop: a url to load apart from the image, as a source
319
- // (a map), or the resolved asset's uri or a data uri (a string).
317
+ // The `placeholder` prop: a thumbhash or blurDataUri, or a url to load
318
+ // apart from the image, as a source (a map), or the resolved asset's uri
319
+ // or a data uri (a string).
320
320
  @Override
321
321
  public void setPlaceholder(FastImageViewWithUrl view, Dynamic source) {
322
322
  if (source.getType() == ReadableType.Map) {
323
+ ReadableMap map = source.asMap();
324
+ boolean hash = map.hasKey("thumbhash") || map.hasKey("blurDataUri");
323
325
  view.setPlaceholder(null);
324
- view.setPlaceholderSource(source.asMap());
326
+ view.setPlaceholderHash(hash ? FastImageViewWithUrl.sourcePlaceholder(map) : null);
327
+ view.setPlaceholderSource(hash ? null : map);
325
328
  return;
326
329
  }
330
+ view.setPlaceholderHash(null);
327
331
  view.setPlaceholderSource(null);
328
332
  String uri = source.isNull() ? null : source.asString();
329
333
  if (uri != null && FastImageThumbHash.isDataUri(uri)) {
@@ -351,11 +355,6 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl>
351
355
  view.setRecyclingKey(recyclingKey);
352
356
  }
353
357
 
354
- @Override
355
- public void setSourcePlaceholders(FastImageViewWithUrl view, @Nullable ReadableArray placeholders) {
356
- view.setSourcePlaceholders(placeholders);
357
- }
358
-
359
358
  @Override
360
359
  public void setLoopCount(FastImageViewWithUrl view, int loopCount) {
361
360
  view.setLoopCount(loopCount);
@@ -48,6 +48,7 @@ import com.bumptech.glide.request.transition.Transition;
48
48
  import com.bumptech.glide.request.transition.TransitionFactory;
49
49
  import com.facebook.react.bridge.ReadableArray;
50
50
  import com.facebook.react.bridge.ReadableMap;
51
+ import com.facebook.react.bridge.ReadableType;
51
52
  import com.facebook.react.bridge.WritableMap;
52
53
  import com.facebook.react.bridge.WritableNativeMap;
53
54
  import com.facebook.react.modules.i18nmanager.I18nUtil;
@@ -58,6 +59,10 @@ import com.facebook.react.uimanager.PointerEvents;
58
59
  import com.facebook.react.uimanager.ReactPointerEventsView;
59
60
  import com.facebook.react.uimanager.style.LogicalEdge;
60
61
 
62
+ import java.util.ArrayList;
63
+ import java.util.HashMap;
64
+ import java.util.List;
65
+ import java.util.Objects;
61
66
  import javax.annotation.Nonnull;
62
67
 
63
68
  class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEventsView, ReactHitSlopView {
@@ -213,16 +218,72 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
213
218
  return false;
214
219
  }
215
220
 
216
- public void setSource(@Nullable ReadableMap source) {
217
- mNeedsReload = true;
218
- mPropSource = source;
219
- if (mSources == null) mSource = source;
221
+ // The `source` prop: one source, or several (sizes of one image), each
222
+ // with its thumbhash or blurDataUri if it has one. Only a change besides
223
+ // those loads again: a new thumbhash for the same image (e.g. one that
224
+ // arrives later) shows at once (see updatePlaceholder).
225
+ public void setSource(@Nullable ReadableArray sources) {
226
+ List<Object> key = sourceKey(sources);
227
+ if (!mHasSource || !Objects.equals(key, mSourceKey)) {
228
+ mHasSource = true;
229
+ mSourceKey = key;
230
+ mNeedsReload = true;
231
+ mSources = sources != null && sources.size() > 1 ? sources : null;
232
+ mSourceIndex = -1;
233
+ if (mSources == null) mSource = sources != null && sources.size() == 1 ? map(sources, 0) : null;
234
+ }
235
+ mSourcePlaceholders = new ArrayList<>();
236
+ for (int i = 0; sources != null && i < sources.size(); i++) {
237
+ mSourcePlaceholders.add(sourcePlaceholder(map(sources, i)));
238
+ }
239
+ updatePlaceholder();
240
+ }
241
+
242
+ // The sources without their thumbhashes and blurDataUris, to compare.
243
+ private boolean mHasSource = false;
244
+ @Nullable
245
+ private List<Object> mSourceKey = null;
246
+
247
+ @Nullable
248
+ private static List<Object> sourceKey(@Nullable ReadableArray sources) {
249
+ if (sources == null) return null;
250
+ List<Object> key = new ArrayList<>();
251
+ for (int i = 0; i < sources.size(); i++) {
252
+ ReadableMap source = map(sources, i);
253
+ key.add(source == null ? null : withoutPlaceholders(source));
254
+ }
255
+ return key;
256
+ }
257
+
258
+ // A source without its thumbhash and blurDataUri.
259
+ private static HashMap<String, Object> withoutPlaceholders(ReadableMap source) {
260
+ HashMap<String, Object> map = source.toHashMap();
261
+ map.remove("thumbhash");
262
+ map.remove("blurDataUri");
263
+ return map;
220
264
  }
221
265
 
222
- // The `source` prop. With several `sources`, mSource is the one picked for
223
- // the view's size instead.
224
266
  @Nullable
225
- private ReadableMap mPropSource = null;
267
+ private static ReadableMap map(ReadableArray array, int index) {
268
+ return array.getType(index) == ReadableType.Map ? array.getMap(index) : null;
269
+ }
270
+
271
+ // A source's (or `placeholder`'s) ThumbHash, or its blurDataUri if it's a
272
+ // data uri (JS warns about another), or null.
273
+ @Nullable
274
+ static String sourcePlaceholder(@Nullable ReadableMap source) {
275
+ if (source == null) return null;
276
+ String thumbhash = string(source, "thumbhash");
277
+ if (thumbhash != null && !thumbhash.isEmpty()) return thumbhash;
278
+ String blurDataUri = string(source, "blurDataUri");
279
+ return blurDataUri != null && FastImageThumbHash.isDataUri(blurDataUri) ? blurDataUri : null;
280
+ }
281
+
282
+ @Nullable
283
+ private static String string(ReadableMap map, String key) {
284
+ return map.hasKey(key) && map.getType(key) == ReadableType.String ? map.getString(key) : null;
285
+ }
286
+
226
287
  // Several sources (2 or more) of the same image at different sizes, or
227
288
  // null: the view loads the one whose size is closest to its own.
228
289
  @Nullable
@@ -240,13 +301,6 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
240
301
  @Nullable
241
302
  private FastImageViewManager mManager;
242
303
 
243
- public void setSources(@Nullable ReadableArray sources) {
244
- mNeedsReload = true;
245
- mSources = sources != null && sources.size() > 1 ? sources : null;
246
- mSourceIndex = -1;
247
- if (mSources == null) mSource = mPropSource;
248
- }
249
-
250
304
  // The box the image is drawn in: the view inside its padding, which is the
251
305
  // border widths (see updateBorderPadding).
252
306
  private int contentWidth() {
@@ -310,19 +364,20 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
310
364
  return true;
311
365
  }
312
366
 
367
+ // Shows at once, as a new source placeholder does (see updatePlaceholder),
368
+ // without loading the image again.
313
369
  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;
316
- mNeedsReload = true;
317
370
  mPlaceholder = placeholder;
371
+ updatePlaceholder();
318
372
  }
319
373
 
320
- // The `sourcePlaceholders` prop, by the source's index ("" for none; one
321
- // for a single source): the picked source's ThumbHash or blurDataUri (a
322
- // data uri) shows instead of the placeholder prop. JS sends them apart from
323
- // the sources, so a new one for the same image doesn't reload it.
374
+ // The sources' ThumbHashes or blurDataUris, by the source's index (null
375
+ // for none): the picked source's shows instead of the placeholder prop.
376
+ private List<String> mSourcePlaceholders = new ArrayList<>();
377
+ // `placeholder` as a thumbhash or blurDataUri: shows for the sources
378
+ // without their own.
324
379
  @Nullable
325
- private ReadableArray mSourcePlaceholders = null;
380
+ private String mPlaceholderHash = null;
326
381
  // What shows while the image loads and if it fails: the picked source's
327
382
  // ThumbHash or blurDataUri decoded (FastImageThumbHash), or the placeholder
328
383
  // prop. FastImage's own drawable either way, which Glide doesn't recycle.
@@ -337,8 +392,9 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
337
392
  @Nullable
338
393
  private String mInvalidPlaceholder = null;
339
394
 
340
- public void setSourcePlaceholders(@Nullable ReadableArray placeholders) {
341
- mSourcePlaceholders = placeholders;
395
+ public void setPlaceholderHash(@Nullable String hash) {
396
+ if (Objects.equals(hash, mPlaceholderHash)) return;
397
+ mPlaceholderHash = hash;
342
398
  updatePlaceholder();
343
399
  }
344
400
 
@@ -450,13 +506,15 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
450
506
  }
451
507
  }
452
508
 
453
- // The picked source's ThumbHash or blurDataUri, or null.
509
+ // The picked source's ThumbHash or blurDataUri, or `placeholder`'s, or
510
+ // null.
454
511
  @Nullable
455
512
  private String pickedSourcePlaceholder() {
456
513
  int index = mSources == null ? 0 : mSourceIndex;
457
- String key = mSourcePlaceholders != null && index >= 0 && index < mSourcePlaceholders.size()
458
- && !mSourcePlaceholders.isNull(index) ? mSourcePlaceholders.getString(index) : null;
459
- return key == null || key.isEmpty() ? null : key;
514
+ // None until one is picked.
515
+ if (index < 0) return null;
516
+ String key = index < mSourcePlaceholders.size() ? mSourcePlaceholders.get(index) : null;
517
+ return key != null ? key : mPlaceholderHash;
460
518
  }
461
519
 
462
520
  // Sets mShownPlaceholder for the picked source. A view showing the previous
@@ -1759,7 +1817,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1759
1817
  // resolved: fire onError and onLoadEnd and show the placeholder prop,
1760
1818
  // as on iOS (#1028, #945).
1761
1819
  if (mSource != null && (imageSource == null || imageSource.getUri().toString().length() == 0)) {
1762
- String error = "Invalid source: " + mSource;
1820
+ // Without its thumbhash or blurDataUri (a data uri can be long).
1821
+ String error = "Invalid source: " + withoutPlaceholders(mSource);
1763
1822
  WritableMap event = new WritableNativeMap();
1764
1823
  event.putString("error", error);
1765
1824
  FastImageEvents.send(this, REACT_ON_ERROR_EVENT, event);
package/dist/index.cjs.js CHANGED
@@ -280,16 +280,6 @@ function dataUri(value) {
280
280
  }
281
281
  //#endregion
282
282
  //#region src/index.tsx
283
- function withoutPlaceholders(source) {
284
- if (!hasSourcePlaceholder(source)) return source;
285
- const { thumbhash: _thumbhash, blurDataUri: _blurDataUri, ...rest } = source;
286
- return rest;
287
- }
288
- function sourcePlaceholder(source) {
289
- if (!source || typeof source !== "object") return "";
290
- const { thumbhash, blurDataUri } = source;
291
- return thumbhash || dataUri(blurDataUri) || "";
292
- }
293
283
  const resolvePlaceholder = (placeholder) => {
294
284
  if (typeof placeholder === "string") {
295
285
  if (isImageName(placeholder)) return react_native.Platform.OS === "android" ? placeholder : { uri: placeholder };
@@ -361,18 +351,12 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
361
351
  const resolvedTintColor = tintColor != null ? tintColor : fromStyle(style, "tintColor");
362
352
  const fit = resolveObjectFit(objectFit, style);
363
353
  const position = objectPositionOrCentered(objectPosition);
364
- const shared = hasSourcePlaceholder(placeholder) ? sourcePlaceholder(placeholder) : "";
365
- const { resolvedSource, sources, sourcePlaceholders } = (0, react.useMemo)(() => {
366
- const sources = Array.isArray(source) ? source.length === 1 ? void 0 : source : void 0;
367
- const single = Array.isArray(source) ? source[0] : source;
368
- const placeholders = (sources !== null && sources !== void 0 ? sources : [single]).map((entry) => sourcePlaceholder(entry) || shared);
369
- return {
370
- resolvedSource: sources ? void 0 : react_native.Image.resolveAssetSource(withoutPlaceholders(single)),
371
- sources: (sources === null || sources === void 0 ? void 0 : sources.some(hasSourcePlaceholder)) ? sources.map(withoutPlaceholders) : sources,
372
- sourcePlaceholders: placeholders.some(Boolean) ? placeholders : void 0
373
- };
374
- }, [source, shared]);
375
- const resolvedPlaceholder = (_placeholderSource = placeholderSource(placeholder)) !== null && _placeholderSource !== void 0 ? _placeholderSource : resolvePlaceholder(hasSourcePlaceholder(placeholder) ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
354
+ const resolvedSource = (0, react.useMemo)(() => {
355
+ for (const entry of [source].flat()) dataUri(entry === null || entry === void 0 ? void 0 : entry.blurDataUri);
356
+ const single = Array.isArray(source) && source.length === 1 ? source[0] : source;
357
+ return Array.isArray(single) ? single.map((entry) => react_native.Image.resolveAssetSource(entry)) : react_native.Image.resolveAssetSource(single);
358
+ }, [source]);
359
+ const resolvedPlaceholder = hasSourcePlaceholder(placeholder) ? placeholder.thumbhash || dataUri(placeholder.blurDataUri) ? placeholder : resolvePlaceholder(defaultSource) : (_placeholderSource = placeholderSource(placeholder)) !== null && _placeholderSource !== void 0 ? _placeholderSource : resolvePlaceholder(placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
376
360
  return /* @__PURE__ */ react.default.createElement(FastImageViewNativeComponent_default, {
377
361
  ...props,
378
362
  ref: forwardedRef,
@@ -381,8 +365,6 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
381
365
  loopCount: loopCount(loop),
382
366
  ...resolveTransition(transition, (_defaultTransition = defaultTransition) !== null && _defaultTransition !== void 0 ? _defaultTransition : builtInTransition()),
383
367
  source: resolvedSource,
384
- sources,
385
- sourcePlaceholders,
386
368
  placeholder: resolvedPlaceholder,
387
369
  onFastImageLoadStart: onLoadStart,
388
370
  onFastImageProgress: onProgress,
@@ -145,7 +145,7 @@ export type FastImageProps = $ReadOnly<{|
145
145
  onLoadStart?: ?() => void,
146
146
  onProgress?: ?(event: OnProgressEvent) => void,
147
147
 
148
- source?: ?(FastImageSource | Array<FastImageSource> | number),
148
+ source?: ?(FastImageSource | Array<FastImageSource | number> | number),
149
149
  placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
150
150
  // Deprecated: use placeholder, which is the same.
151
151
  defaultSource?: ?number,
@@ -303,7 +303,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
303
303
  const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
304
304
  const urlPlaceholder = (_placeholderSource = placeholderSource(placeholder)) === null || _placeholderSource === void 0 ? void 0 : _placeholderSource.uri;
305
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);
306
+ const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : urlPlaceholder ? void 0 : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
307
307
  const [result, setResult] = (0, react.useState)();
308
308
  const ok = result && result.key === key ? result.ok : void 0;
309
309
  const svgId = (0, react.useId)().replace(/[^\w-]/g, "");
@@ -416,10 +416,9 @@ function shared(source, key, preview) {
416
416
  function resolveUri(source) {
417
417
  let uri;
418
418
  if (typeof source === "number") {
419
- var _asset$scales$;
420
419
  const asset = (0, react_native_web_dist_modules_AssetRegistry.getAssetByID)(source);
421
420
  if (!asset) return void 0;
422
- const scale = asset.scales.reduce((best, scale) => Math.abs(scale - react_native.PixelRatio.get()) < Math.abs(best - react_native.PixelRatio.get()) ? scale : best, (_asset$scales$ = asset.scales[0]) !== null && _asset$scales$ !== void 0 ? _asset$scales$ : 1);
421
+ const scale = assetScale(asset);
423
422
  const suffix = scale !== 1 ? `@${scale}x` : "";
424
423
  uri = `${asset.httpServerLocation}/${asset.name}${suffix}.${asset.type}`;
425
424
  } else if (typeof source === "string") uri = source;
@@ -428,9 +427,24 @@ function resolveUri(source) {
428
427
  const svg = uri === null || uri === void 0 ? void 0 : uri.match(/^(data:image\/svg\+xml;utf8,)(.*)/);
429
428
  return svg ? svg[1] + encodeURIComponent(svg[2]) : uri || void 0;
430
429
  }
431
- function sizedSources(sources) {
430
+ function assetScale(asset) {
431
+ var _asset$scales$;
432
+ return asset.scales.reduce((best, scale) => Math.abs(scale - react_native.PixelRatio.get()) < Math.abs(best - react_native.PixelRatio.get()) ? scale : best, (_asset$scales$ = asset.scales[0]) !== null && _asset$scales$ !== void 0 ? _asset$scales$ : 1);
433
+ }
434
+ function sizedSource(source) {
435
+ if (typeof source !== "number") return source;
436
+ const asset = (0, react_native_web_dist_modules_AssetRegistry.getAssetByID)(source);
437
+ return asset ? {
438
+ uri: resolveUri(source),
439
+ width: asset.width,
440
+ height: asset.height,
441
+ scale: assetScale(asset)
442
+ } : void 0;
443
+ }
444
+ function sizedSources(entries) {
432
445
  var _largest$uri, _sources$find;
433
- const widths = sources.filter((source) => (source === null || source === void 0 ? void 0 : source.uri) && source.width).map((source) => {
446
+ const sources = entries.map(sizedSource);
447
+ const widths = sources.filter((source) => !!(source === null || source === void 0 ? void 0 : source.uri) && !!source.width).map((source) => {
434
448
  var _source$width, _source$scale;
435
449
  return {
436
450
  uri: source.uri,
@@ -145,7 +145,7 @@ export type FastImageProps = $ReadOnly<{|
145
145
  onLoadStart?: ?() => void,
146
146
  onProgress?: ?(event: OnProgressEvent) => void,
147
147
 
148
- source?: ?(FastImageSource | Array<FastImageSource> | number),
148
+ source?: ?(FastImageSource | Array<FastImageSource | number> | number),
149
149
  placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
150
150
  // Deprecated: use placeholder, which is the same.
151
151
  defaultSource?: ?number,
package/dist/index.d.ts CHANGED
@@ -331,9 +331,10 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
331
331
  * - `FastImage.preload`, `getCachePath` and `writeToCache` take one size:
332
332
  * pass the one a view will show. An array fails with
333
333
  * `{ ok: false, error }`.
334
+ * - A `require()`d image can be one of the sizes: its size is the file's.
334
335
  * - An array of one is the same as that source.
335
336
  */
336
- source?: Source | Source[] | ImageRequireSource;
337
+ source?: Source | Array<Source | ImageRequireSource> | ImageRequireSource;
337
338
  /**
338
339
  * A placeholder shown while the image loads, and if it fails to load:
339
340
  *
@@ -363,7 +364,8 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
363
364
  */
364
365
  placeholder?: ImageRequireSource | string | SourcePlaceholder | PlaceholderSource;
365
366
  /**
366
- * @deprecated Use `placeholder`, which does the same.
367
+ * @deprecated Use `placeholder`, which does the same. With both,
368
+ * `placeholder` shows.
367
369
  */
368
370
  defaultSource?: ImageRequireSource;
369
371
  /**
package/dist/index.js CHANGED
@@ -253,16 +253,6 @@ function dataUri(value) {
253
253
  }
254
254
  //#endregion
255
255
  //#region src/index.tsx
256
- function withoutPlaceholders(source) {
257
- if (!hasSourcePlaceholder(source)) return source;
258
- const { thumbhash: _thumbhash, blurDataUri: _blurDataUri, ...rest } = source;
259
- return rest;
260
- }
261
- function sourcePlaceholder(source) {
262
- if (!source || typeof source !== "object") return "";
263
- const { thumbhash, blurDataUri } = source;
264
- return thumbhash || dataUri(blurDataUri) || "";
265
- }
266
256
  const resolvePlaceholder = (placeholder) => {
267
257
  if (typeof placeholder === "string") {
268
258
  if (isImageName(placeholder)) return Platform.OS === "android" ? placeholder : { uri: placeholder };
@@ -334,18 +324,12 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
334
324
  const resolvedTintColor = tintColor != null ? tintColor : fromStyle(style, "tintColor");
335
325
  const fit = resolveObjectFit(objectFit, style);
336
326
  const position = objectPositionOrCentered(objectPosition);
337
- const shared = hasSourcePlaceholder(placeholder) ? sourcePlaceholder(placeholder) : "";
338
- const { resolvedSource, sources, sourcePlaceholders } = useMemo(() => {
339
- const sources = Array.isArray(source) ? source.length === 1 ? void 0 : source : void 0;
340
- const single = Array.isArray(source) ? source[0] : source;
341
- const placeholders = (sources !== null && sources !== void 0 ? sources : [single]).map((entry) => sourcePlaceholder(entry) || shared);
342
- return {
343
- resolvedSource: sources ? void 0 : Image.resolveAssetSource(withoutPlaceholders(single)),
344
- sources: (sources === null || sources === void 0 ? void 0 : sources.some(hasSourcePlaceholder)) ? sources.map(withoutPlaceholders) : sources,
345
- sourcePlaceholders: placeholders.some(Boolean) ? placeholders : void 0
346
- };
347
- }, [source, shared]);
348
- const resolvedPlaceholder = (_placeholderSource = placeholderSource(placeholder)) !== null && _placeholderSource !== void 0 ? _placeholderSource : resolvePlaceholder(hasSourcePlaceholder(placeholder) ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
327
+ const resolvedSource = useMemo(() => {
328
+ for (const entry of [source].flat()) dataUri(entry === null || entry === void 0 ? void 0 : entry.blurDataUri);
329
+ const single = Array.isArray(source) && source.length === 1 ? source[0] : source;
330
+ return Array.isArray(single) ? single.map((entry) => Image.resolveAssetSource(entry)) : Image.resolveAssetSource(single);
331
+ }, [source]);
332
+ const resolvedPlaceholder = hasSourcePlaceholder(placeholder) ? placeholder.thumbhash || dataUri(placeholder.blurDataUri) ? placeholder : resolvePlaceholder(defaultSource) : (_placeholderSource = placeholderSource(placeholder)) !== null && _placeholderSource !== void 0 ? _placeholderSource : resolvePlaceholder(placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
349
333
  return /* @__PURE__ */ React.createElement(FastImageViewNativeComponent_default, {
350
334
  ...props,
351
335
  ref: forwardedRef,
@@ -354,8 +338,6 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
354
338
  loopCount: loopCount(loop),
355
339
  ...resolveTransition(transition, (_defaultTransition = defaultTransition) !== null && _defaultTransition !== void 0 ? _defaultTransition : builtInTransition()),
356
340
  source: resolvedSource,
357
- sources,
358
- sourcePlaceholders,
359
341
  placeholder: resolvedPlaceholder,
360
342
  onFastImageLoadStart: onLoadStart,
361
343
  onFastImageProgress: onProgress,
@@ -145,7 +145,7 @@ export type FastImageProps = $ReadOnly<{|
145
145
  onLoadStart?: ?() => void,
146
146
  onProgress?: ?(event: OnProgressEvent) => void,
147
147
 
148
- source?: ?(FastImageSource | Array<FastImageSource> | number),
148
+ source?: ?(FastImageSource | Array<FastImageSource | number> | number),
149
149
  placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
150
150
  // Deprecated: use placeholder, which is the same.
151
151
  defaultSource?: ?number,
package/dist/index.web.js CHANGED
@@ -276,7 +276,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
276
276
  const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
277
277
  const urlPlaceholder = (_placeholderSource = placeholderSource(placeholder)) === null || _placeholderSource === void 0 ? void 0 : _placeholderSource.uri;
278
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);
279
+ const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : urlPlaceholder ? void 0 : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
280
280
  const [result, setResult] = useState();
281
281
  const ok = result && result.key === key ? result.ok : void 0;
282
282
  const svgId = useId().replace(/[^\w-]/g, "");
@@ -389,10 +389,9 @@ function shared(source, key, preview) {
389
389
  function resolveUri(source) {
390
390
  let uri;
391
391
  if (typeof source === "number") {
392
- var _asset$scales$;
393
392
  const asset = getAssetByID(source);
394
393
  if (!asset) return void 0;
395
- const scale = asset.scales.reduce((best, scale) => Math.abs(scale - PixelRatio.get()) < Math.abs(best - PixelRatio.get()) ? scale : best, (_asset$scales$ = asset.scales[0]) !== null && _asset$scales$ !== void 0 ? _asset$scales$ : 1);
394
+ const scale = assetScale(asset);
396
395
  const suffix = scale !== 1 ? `@${scale}x` : "";
397
396
  uri = `${asset.httpServerLocation}/${asset.name}${suffix}.${asset.type}`;
398
397
  } else if (typeof source === "string") uri = source;
@@ -401,9 +400,24 @@ function resolveUri(source) {
401
400
  const svg = uri === null || uri === void 0 ? void 0 : uri.match(/^(data:image\/svg\+xml;utf8,)(.*)/);
402
401
  return svg ? svg[1] + encodeURIComponent(svg[2]) : uri || void 0;
403
402
  }
404
- function sizedSources(sources) {
403
+ function assetScale(asset) {
404
+ var _asset$scales$;
405
+ return asset.scales.reduce((best, scale) => Math.abs(scale - PixelRatio.get()) < Math.abs(best - PixelRatio.get()) ? scale : best, (_asset$scales$ = asset.scales[0]) !== null && _asset$scales$ !== void 0 ? _asset$scales$ : 1);
406
+ }
407
+ function sizedSource(source) {
408
+ if (typeof source !== "number") return source;
409
+ const asset = getAssetByID(source);
410
+ return asset ? {
411
+ uri: resolveUri(source),
412
+ width: asset.width,
413
+ height: asset.height,
414
+ scale: assetScale(asset)
415
+ } : void 0;
416
+ }
417
+ function sizedSources(entries) {
405
418
  var _largest$uri, _sources$find;
406
- const widths = sources.filter((source) => (source === null || source === void 0 ? void 0 : source.uri) && source.width).map((source) => {
419
+ const sources = entries.map(sizedSource);
420
+ const widths = sources.filter((source) => !!(source === null || source === void 0 ? void 0 : source.uri) && !!source.width).map((source) => {
407
421
  var _source$width, _source$scale;
408
422
  return {
409
423
  uri: source.uri,
@@ -145,7 +145,7 @@ export type FastImageProps = $ReadOnly<{|
145
145
  onLoadStart?: ?() => void,
146
146
  onProgress?: ?(event: OnProgressEvent) => void,
147
147
 
148
- source?: ?(FastImageSource | Array<FastImageSource> | number),
148
+ source?: ?(FastImageSource | Array<FastImageSource | number> | number),
149
149
  placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
150
150
  // Deprecated: use placeholder, which is the same.
151
151
  defaultSource?: ?number,
@@ -20,9 +20,7 @@ type LoadEndEvent = Readonly<{
20
20
  }>;
21
21
  export interface NativeProps extends ViewProps {
22
22
  source?: CodegenTypes.UnsafeMixed;
23
- sources?: CodegenTypes.UnsafeMixed;
24
23
  placeholder?: CodegenTypes.UnsafeMixed;
25
- sourcePlaceholders?: ReadonlyArray<string>;
26
24
  objectFit?: CodegenTypes.WithDefault<string, 'cover'>;
27
25
  objectPositionX?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.5>;
28
26
  objectPositionXOffset?: CodegenTypes.Float;
@@ -15,6 +15,9 @@ NS_ASSUME_NONNULL_BEGIN
15
15
  // Main thread.
16
16
  + (nullable UIImage*) imageForPlaceholder: (nullable NSString*)placeholder;
17
17
 
18
+ // Whether a placeholder is a data uri (a blurDataUri) rather than a ThumbHash.
19
+ + (BOOL) isDataUri: (nullable NSString*)placeholder;
20
+
18
21
  // The placeholder for a base64 ThumbHash (standard or url-safe, with or
19
22
  // without padding), or nil for an empty or invalid one. Main thread.
20
23
  + (nullable UIImage*) imageForThumbHash: (nullable NSString*)thumbhash;
@@ -200,8 +200,12 @@ static UIImage* FFFBlurredPreview(UIImage* image) {
200
200
  return cache;
201
201
  }
202
202
 
203
+ + (BOOL) isDataUri: (NSString*)placeholder {
204
+ return placeholder && [placeholder rangeOfString: @"data:" options: NSCaseInsensitiveSearch | NSAnchoredSearch].location != NSNotFound;
205
+ }
206
+
203
207
  + (UIImage*) imageForPlaceholder: (NSString*)placeholder {
204
- if ([placeholder rangeOfString: @"data:" options: NSCaseInsensitiveSearch | NSAnchoredSearch].location == NSNotFound) {
208
+ if (![self isDataUri: placeholder]) {
205
209
  return [self imageForThumbHash: placeholder];
206
210
  }
207
211
  UIImage* cached = [[self cache] objectForKey: placeholder];
@@ -28,9 +28,8 @@ typedef NS_ENUM(NSInteger, FFFObjectFit) {
28
28
  @property (nonatomic, copy) RCTDirectEventBlock onFastImageLoad;
29
29
  @property (nonatomic, copy) RCTDirectEventBlock onFastImageLoadEnd;
30
30
  @property (nonatomic, assign) FFFObjectFit objectFit;
31
- @property (nonatomic, strong) FFFastImageSource *source;
32
- // Several sources (2 or more) of the same image at different sizes: the view
33
- // loads the one whose size is closest to its own.
31
+ // The `source` prop: one source, or several (sizes of one image), of which
32
+ // the view loads the one whose size is closest to its own.
34
33
  @property (nonatomic, copy) NSArray<FFFastImageSource *> *sources;
35
34
  // The `placeholder` prop.
36
35
  @property (nonatomic, strong) UIImage *placeholder;
@@ -38,11 +37,14 @@ typedef NS_ENUM(NSInteger, FFFObjectFit) {
38
37
  // image, at a high priority (or only from the cache, with cache cacheOnly),
39
38
  // it shows over the other placeholders once it has.
40
39
  @property (nonatomic, strong) FFFastImageSource *placeholderSource;
41
- // The `sourcePlaceholders` prop, by the source's index (an empty string for
42
- // none; one for a single source): the picked source's ThumbHash or
43
- // blurDataUri (a data uri) shows instead of placeholder. JS sends them apart
44
- // from the sources, so a new one for the same image doesn't load it again.
40
+ // The sources' ThumbHashes or blurDataUris (data uris), by the source's index
41
+ // (an empty string for none): the picked source's shows instead of
42
+ // placeholder. Set apart from the sources, so a new one for the same image
43
+ // doesn't load it again.
45
44
  @property (nonatomic, copy) NSArray<NSString *> *sourcePlaceholders;
45
+ // `placeholder` as a thumbhash or blurDataUri: shows for the sources without
46
+ // their own.
47
+ @property (nonatomic, copy) NSString *placeholderHash;
46
48
  @property (nonatomic, strong) UIColor *imageColor;
47
49
  // When it changes, the next image doesn't replace the current one: the view
48
50
  // clears first (for views reused for other content, like list rows).
@@ -44,9 +44,9 @@
44
44
  // downsample, or several sources: a load waits for the view's size (see
45
45
  // didSetProps).
46
46
  @property(nonatomic, assign) BOOL waitsForSize;
47
- // The `source` prop. With several `sources`, _source is the one picked for
48
- // the view's size instead.
49
- @property(nonatomic, strong) FFFastImageSource* propSource;
47
+ // The source that loads: the `source` prop's, or with several the one
48
+ // picked for the view's size.
49
+ @property(nonatomic, strong) FFFastImageSource* source;
50
50
  // Several sources: the next load switches to another one for a new view
51
51
  // size, and the one after it is loading. It's the same picture at another
52
52
  // size, so it doesn't fade in.
@@ -749,21 +749,11 @@ NSString *FFFErrorMessage(NSError *error)
749
749
  return event;
750
750
  }
751
751
 
752
- - (void) setSource: (FFFastImageSource*)source {
753
- if (self.propSource != source) {
754
- self.propSource = source;
755
- if (![self picksSource]) {
756
- _source = source;
757
- }
758
- _needsReload = YES;
759
- }
760
- }
761
-
762
752
  - (void) setSources: (NSArray<FFFastImageSource*>*)sources {
763
753
  if (_sources != sources) {
764
754
  _sources = [sources copy];
765
755
  if (![self picksSource]) {
766
- _source = self.propSource;
756
+ _source = _sources.firstObject;
767
757
  }
768
758
  _needsReload = YES;
769
759
  }
@@ -847,11 +837,16 @@ NSString *FFFErrorMessage(NSError *error)
847
837
  return [_dynamicRangeLimit isEqualToString: @"constrained"] || [_dynamicRangeLimit isEqualToString: @"no-limit"];
848
838
  }
849
839
 
840
+ // Shows at once, as a new source placeholder does (see updateHashImage),
841
+ // without loading the image again.
850
842
  - (void) setPlaceholder: (UIImage*)placeholder {
851
- if (_placeholder != placeholder) {
852
- _placeholder = placeholder;
853
- _needsReload = YES;
843
+ if (_placeholder == placeholder) {
844
+ return;
854
845
  }
846
+ UIImage* previous = [self shownPlaceholder];
847
+ BOOL showing = self.showsPlaceholder;
848
+ _placeholder = placeholder;
849
+ [self showChangedPlaceholder: previous showing: showing];
855
850
  }
856
851
 
857
852
  - (void) setSourcePlaceholders: (NSArray<NSString*>*)sourcePlaceholders {
@@ -859,6 +854,11 @@ NSString *FFFErrorMessage(NSError *error)
859
854
  [self updateHashImage];
860
855
  }
861
856
 
857
+ - (void) setPlaceholderHash: (NSString*)placeholderHash {
858
+ _placeholderHash = [placeholderHash copy];
859
+ [self updateHashImage];
860
+ }
861
+
862
862
  // What shows while the image loads and if it fails: placeholderSource's
863
863
  // image once it has loaded, the picked source's placeholder (its ThumbHash or
864
864
  // blurDataUri), or the placeholder prop.
@@ -912,6 +912,9 @@ NSString *FFFErrorMessage(NSError *error)
912
912
  }
913
913
 
914
914
  - (void) setPlaceholderSource: (FFFastImageSource*)placeholderSource {
915
+ if (!placeholderSource && !_placeholderSource) {
916
+ return;
917
+ }
915
918
  _placeholderSource = placeholderSource;
916
919
  [self cancelUrlPlaceholderLoad];
917
920
  [self showUrlPlaceholderImage: nil];
@@ -979,14 +982,18 @@ NSString *FFFErrorMessage(NSError *error)
979
982
  [self showChangedPlaceholder: previous showing: showing];
980
983
  }
981
984
 
982
- // The picked source's ThumbHash or blurDataUri, or nil.
985
+ // The picked source's ThumbHash or blurDataUri, or placeholderHash, or nil.
983
986
  - (NSString*) pickedSourcePlaceholder {
984
987
  NSUInteger index = 0;
985
988
  if ([self picksSource]) {
986
989
  index = _source ? [_sources indexOfObjectIdenticalTo: _source] : NSNotFound;
990
+ // None until one is picked.
991
+ if (index == NSNotFound) {
992
+ return nil;
993
+ }
987
994
  }
988
995
  NSString* hash = index < _sourcePlaceholders.count ? _sourcePlaceholders[index] : nil;
989
- return hash.length > 0 ? hash : nil;
996
+ return hash.length > 0 ? hash : _placeholderHash.length > 0 ? _placeholderHash : nil;
990
997
  }
991
998
 
992
999
  // Decodes the picked source's placeholder. A view showing the previous
@@ -8,6 +8,7 @@
8
8
  #import <react/renderer/components/RNFastImageSpec/RCTComponentViewHelpers.h>
9
9
 
10
10
  #import "FFFastImageView.h"
11
+ #import "FFFThumbHash.h"
11
12
  #import "RCTConvert+FFFastImage.h"
12
13
 
13
14
  using namespace facebook::react;
@@ -102,6 +103,41 @@ static id _Nullable FFFJSON(const folly::dynamic &value)
102
103
  return value.isNull() ? nil : FFFObjectFromDynamic(value);
103
104
  }
104
105
 
106
+ // A source (or `placeholder`) without its thumbhash and blurDataUri, to
107
+ // compare: one source, or each of several.
108
+ static folly::dynamic FFFWithoutPlaceholders(const folly::dynamic &value)
109
+ {
110
+ if (value.isArray()) {
111
+ folly::dynamic result = folly::dynamic::array;
112
+ for (const auto &entry : value) {
113
+ result.push_back(FFFWithoutPlaceholders(entry));
114
+ }
115
+ return result;
116
+ }
117
+ if (!value.isObject()) {
118
+ return value;
119
+ }
120
+ folly::dynamic result = value;
121
+ result.erase("thumbhash");
122
+ result.erase("blurDataUri");
123
+ return result;
124
+ }
125
+
126
+ // A source's (or `placeholder`'s) ThumbHash, or its blurDataUri if it's a data
127
+ // uri (JS warns about another), or nil.
128
+ static NSString *_Nullable FFFSourcePlaceholder(NSDictionary *source)
129
+ {
130
+ id thumbhash = source[@"thumbhash"];
131
+ if ([thumbhash isKindOfClass:[NSString class]] && [thumbhash length] > 0) {
132
+ return thumbhash;
133
+ }
134
+ id blurDataUri = source[@"blurDataUri"];
135
+ if ([blurDataUri isKindOfClass:[NSString class]] && [FFFThumbHash isDataUri:blurDataUri]) {
136
+ return blurDataUri;
137
+ }
138
+ return nil;
139
+ }
140
+
105
141
  // Whether the placeholder prop is a url to load apart from the image (a
106
142
  // source with an http or https uri), rather than an image to show at once: a
107
143
  // bundled image (served by the packager in development), a file or a data uri.
@@ -242,25 +278,36 @@ typedef NS_OPTIONS(int32_t, FFFEvent) {
242
278
  FFFastImageView *view = _imageView;
243
279
 
244
280
  if (all || oldViewProps.source != newViewProps.source) {
245
- view.source = [RCTConvert FFFastImageSource:FFFJSON(newViewProps.source)];
246
- }
247
- if (all || oldViewProps.sources != newViewProps.sources) {
248
- id sources = FFFJSON(newViewProps.sources);
249
- view.sources = sources ? [RCTConvert FFFastImageSourceArray:sources] : nil;
281
+ // One source, or several (an array). Only a change besides their
282
+ // thumbhashes and blurDataUris loads again: a new one for the same
283
+ // image (e.g. one that arrives later) shows at once.
284
+ BOOL loads = all || FFFWithoutPlaceholders(oldViewProps.source) != FFFWithoutPlaceholders(newViewProps.source);
285
+ id json = FFFJSON(newViewProps.source);
286
+ NSArray *entries = [json isKindOfClass:[NSArray class]] ? json : json ? @[ json ] : @[];
287
+ NSMutableArray<FFFastImageSource *> *sources = [NSMutableArray arrayWithCapacity:entries.count];
288
+ NSMutableArray<NSString *> *placeholders = [NSMutableArray arrayWithCapacity:entries.count];
289
+ for (id entry in entries) {
290
+ if ([entry isKindOfClass:[NSDictionary class]]) {
291
+ if (loads) {
292
+ [sources addObject:[RCTConvert FFFastImageSource:entry]];
293
+ }
294
+ [placeholders addObject:FFFSourcePlaceholder(entry) ?: @""];
295
+ }
296
+ }
297
+ if (loads) {
298
+ view.sources = sources.count > 0 ? sources : nil;
299
+ }
300
+ view.sourcePlaceholders = placeholders;
250
301
  }
251
302
  if (all || oldViewProps.placeholder != newViewProps.placeholder) {
303
+ // A thumbhash or blurDataUri, a url to load, or an image to show.
252
304
  id placeholder = FFFJSON(newViewProps.placeholder);
253
- BOOL loads = FFFIsUrlPlaceholder(placeholder);
254
- view.placeholder = placeholder && !loads ? [RCTConvert UIImage:placeholder] : nil;
305
+ BOOL hash = [placeholder isKindOfClass:[NSDictionary class]] && (placeholder[@"thumbhash"] || placeholder[@"blurDataUri"]);
306
+ BOOL loads = !hash && FFFIsUrlPlaceholder(placeholder);
307
+ view.placeholderHash = hash ? FFFSourcePlaceholder(placeholder) : nil;
308
+ view.placeholder = placeholder && !hash && !loads ? [RCTConvert UIImage:placeholder] : nil;
255
309
  view.placeholderSource = loads ? [RCTConvert FFFastImageSource:placeholder] : nil;
256
310
  }
257
- if (all || oldViewProps.sourcePlaceholders != newViewProps.sourcePlaceholders) {
258
- NSMutableArray<NSString *> *placeholders = [NSMutableArray arrayWithCapacity:newViewProps.sourcePlaceholders.size()];
259
- for (const auto &placeholder : newViewProps.sourcePlaceholders) {
260
- [placeholders addObject:[NSString stringWithUTF8String:placeholder.c_str()]];
261
- }
262
- view.sourcePlaceholders = placeholders.count > 0 ? placeholders : nil;
263
- }
264
311
  if (all || oldViewProps.objectFit != newViewProps.objectFit) {
265
312
  view.objectFit = [RCTConvert FFFObjectFit:FFFString(newViewProps.objectFit)];
266
313
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "10.5.0",
3
+ "version": "10.6.0",
4
4
  "description": "FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",
package/src/index.js.flow CHANGED
@@ -145,7 +145,7 @@ export type FastImageProps = $ReadOnly<{|
145
145
  onLoadStart?: ?() => void,
146
146
  onProgress?: ?(event: OnProgressEvent) => void,
147
147
 
148
- source?: ?(FastImageSource | Array<FastImageSource> | number),
148
+ source?: ?(FastImageSource | Array<FastImageSource | number> | number),
149
149
  placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
150
150
  // Deprecated: use placeholder, which is the same.
151
151
  defaultSource?: ?number,
package/src/index.tsx CHANGED
@@ -362,9 +362,10 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
362
362
  * - `FastImage.preload`, `getCachePath` and `writeToCache` take one size:
363
363
  * pass the one a view will show. An array fails with
364
364
  * `{ ok: false, error }`.
365
+ * - A `require()`d image can be one of the sizes: its size is the file's.
365
366
  * - An array of one is the same as that source.
366
367
  */
367
- source?: Source | Source[] | ImageRequireSource
368
+ source?: Source | Array<Source | ImageRequireSource> | ImageRequireSource
368
369
  /**
369
370
  * A placeholder shown while the image loads, and if it fails to load:
370
371
  *
@@ -398,7 +399,8 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
398
399
  | SourcePlaceholder
399
400
  | PlaceholderSource
400
401
  /**
401
- * @deprecated Use `placeholder`, which does the same.
402
+ * @deprecated Use `placeholder`, which does the same. With both,
403
+ * `placeholder` shows.
402
404
  */
403
405
  defaultSource?: ImageRequireSource
404
406
  /**
@@ -707,25 +709,6 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
707
709
  children?: "FastImage doesn't take children: use FastImageBackground for content over an image"
708
710
  }
709
711
 
710
- // A source without its thumbhash and blurDataUri.
711
- function withoutPlaceholders<T>(source: T): T {
712
- if (!hasSourcePlaceholder(source)) return source
713
- const {
714
- thumbhash: _thumbhash,
715
- blurDataUri: _blurDataUri,
716
- ...rest
717
- } = source as Source
718
- return rest as T
719
- }
720
-
721
- // What the native view shows while a source loads: its ThumbHash (decoded
722
- // natively), or its blurDataUri, or '' for neither.
723
- function sourcePlaceholder(source: unknown): string {
724
- if (!source || typeof source !== 'object') return ''
725
- const { thumbhash, blurDataUri } = source as Source
726
- return thumbhash || dataUri(blurDataUri) || ''
727
- }
728
-
729
712
  const resolvePlaceholder = (
730
713
  // A source object here has no url (e.g. { uri: name }, a bundled image).
731
714
  placeholder?: ImageRequireSource | string | PlaceholderSource,
@@ -911,53 +894,32 @@ function FastImageBase({
911
894
  const fit = resolveObjectFit(objectFit, style)
912
895
  const position = objectPositionOrCentered(objectPosition)
913
896
 
914
- // Memoized on source, so an unchanged source reaches the native view as
915
- // the same objects, and React's prop diff stops at them.
916
- // `placeholder` as a thumbhash or blurDataUri, for the sources without
917
- // their own: a string, so an object made on every render doesn't make
918
- // new sourcePlaceholders.
919
- const shared = hasSourcePlaceholder(placeholder)
920
- ? sourcePlaceholder(placeholder)
921
- : ''
922
- const { resolvedSource, sources, sourcePlaceholders } = useMemo(() => {
923
- // Several sources are picked from natively, for the view's size; one
924
- // in an array is a plain source.
925
- const sources = Array.isArray(source)
926
- ? source.length === 1
927
- ? undefined
928
- : source
929
- : undefined
930
- const single = Array.isArray(source) ? source[0] : source
931
- // Sources' thumbhashes and blurDataUris go to the native view apart
932
- // from the sources, so a new one for the same image doesn't load it
933
- // again, by the source's index ('' for none). A source's own wins
934
- // over `placeholder`'s.
935
- const placeholders = (sources ?? [single]).map(
936
- (entry) => sourcePlaceholder(entry) || shared,
937
- )
938
- return {
939
- resolvedSource: sources
940
- ? undefined
941
- : Image.resolveAssetSource(withoutPlaceholders(single) as any),
942
- sources: sources?.some(hasSourcePlaceholder)
943
- ? sources.map(withoutPlaceholders)
944
- : sources,
945
- sourcePlaceholders: placeholders.some(Boolean)
946
- ? placeholders
947
- : undefined,
897
+ // One source, or several in an array, picked from natively for the
898
+ // view's size (one in an array is a plain source), with their thumbhashes
899
+ // and blurDataUris: the native view doesn't load the image again when
900
+ // only those change. Memoized on source, so an unchanged source reaches
901
+ // the native view as the same object, and React's prop diff stops at it.
902
+ const resolvedSource = useMemo(() => {
903
+ // A blurDataUri that isn't a data uri is ignored, with a warning.
904
+ for (const entry of [source].flat()) {
905
+ dataUri((entry as Source | undefined)?.blurDataUri)
948
906
  }
949
- }, [source, shared])
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
- )
907
+ const single =
908
+ Array.isArray(source) && source.length === 1 ? source[0] : source
909
+ return Array.isArray(single)
910
+ ? single.map((entry) => Image.resolveAssetSource(entry as any))
911
+ : Image.resolveAssetSource(single as any)
912
+ }, [source])
913
+ // `placeholder` as the native view takes it: a thumbhash or blurDataUri
914
+ // as it is (for the sources without their own), a url as a source loaded
915
+ // apart from the image, or an image shown at once. defaultSource only
916
+ // applies without one (an empty thumbhash or blurDataUri is none).
917
+ const resolvedPlaceholder = hasSourcePlaceholder(placeholder)
918
+ ? placeholder.thumbhash || dataUri(placeholder.blurDataUri)
919
+ ? placeholder
920
+ : resolvePlaceholder(defaultSource)
921
+ : (placeholderSource(placeholder) ??
922
+ resolvePlaceholder(placeholder ?? defaultSource))
961
923
 
962
924
  return (
963
925
  <FastImageView
@@ -974,8 +936,6 @@ function FastImageBase({
974
936
  defaultTransition ?? builtInTransition(),
975
937
  )}
976
938
  source={resolvedSource}
977
- sources={sources}
978
- sourcePlaceholders={sourcePlaceholders}
979
939
  placeholder={resolvedPlaceholder}
980
940
  onFastImageLoadStart={onLoadStart}
981
941
  onFastImageProgress={onProgress}
package/src/index.web.tsx CHANGED
@@ -141,14 +141,18 @@ function FastImageBase({
141
141
  // like any image: it can't look in its cache only (cache: 'cacheOnly').
142
142
  const urlPlaceholder = placeholderSource(placeholder)?.uri
143
143
  const [failedPlaceholder, setFailedPlaceholder] = useState<string>()
144
- // A thumbhash or blurDataUri in `placeholder` isn't an image to show as it
145
- // is, nor is a url (above): defaultSource still is.
144
+ // defaultSource only shows without `placeholder`, as on iOS and Android,
145
+ // or in place of a thumbhash or blurDataUri the web can't show (one of
146
+ // several sizes', or a thumbhash without react-native-fast-image/thumbhash,
147
+ // or an empty one). Not with a url (above).
146
148
  const defaultUri =
147
149
  blurUri ??
148
150
  resolveUri(
149
- preview || urlPlaceholder
151
+ preview
150
152
  ? defaultSource
151
- : (placeholder ?? defaultSource),
153
+ : urlPlaceholder
154
+ ? undefined
155
+ : (placeholder ?? defaultSource),
152
156
  )
153
157
  // Whether the image for `key` loaded, or failed.
154
158
  const [result, setResult] = useState<{ key: string; ok: boolean }>()
@@ -341,14 +345,7 @@ function resolveUri(source: unknown): string | undefined {
341
345
  if (typeof source === 'number') {
342
346
  const asset: PackagerAsset | undefined = getAssetByID(source)
343
347
  if (!asset) return undefined
344
- const scale = asset.scales.reduce(
345
- (best, scale) =>
346
- Math.abs(scale - PixelRatio.get()) <
347
- Math.abs(best - PixelRatio.get())
348
- ? scale
349
- : best,
350
- asset.scales[0] ?? 1,
351
- )
348
+ const scale = assetScale(asset)
352
349
  const suffix = scale !== 1 ? `@${scale}x` : ''
353
350
  uri = `${asset.httpServerLocation}/${asset.name}${suffix}.${asset.type}`
354
351
  } else if (typeof source === 'string') {
@@ -370,6 +367,36 @@ type PackagerAsset = {
370
367
  name: string
371
368
  type: string
372
369
  scales: number[]
370
+ width?: number
371
+ height?: number
372
+ }
373
+
374
+ // The scale of an asset's file closest to the screen's.
375
+ function assetScale(asset: PackagerAsset): number {
376
+ return asset.scales.reduce(
377
+ (best, scale) =>
378
+ Math.abs(scale - PixelRatio.get()) <
379
+ Math.abs(best - PixelRatio.get())
380
+ ? scale
381
+ : best,
382
+ asset.scales[0] ?? 1,
383
+ )
384
+ }
385
+
386
+ // One of several sizes: a require()d image in react-native-web's asset
387
+ // registry as a source with its file's size (Expo's web builds make it a
388
+ // { uri, width, height } object already).
389
+ function sizedSource(source: Source | number): Source | undefined {
390
+ if (typeof source !== 'number') return source
391
+ const asset: PackagerAsset | undefined = getAssetByID(source)
392
+ return asset
393
+ ? {
394
+ uri: resolveUri(source),
395
+ width: asset.width,
396
+ height: asset.height,
397
+ scale: assetScale(asset),
398
+ }
399
+ : undefined
373
400
  }
374
401
 
375
402
  // Several sizes of an image, as a srcset (each size's width, times its
@@ -379,9 +406,10 @@ type PackagerAsset = {
379
406
  // is laid out at; it only applies to lazy images, and otherwise, or in
380
407
  // browsers without it, the next value, 100vw (the viewport's width). src is the largest, for browsers without
381
408
  // srcset.
382
- function sizedSources(sources: Source[]) {
409
+ function sizedSources(entries: (Source | number)[]) {
410
+ const sources = entries.map(sizedSource)
383
411
  const widths = sources
384
- .filter((source) => source?.uri && source.width)
412
+ .filter((source): source is Source => !!source?.uri && !!source.width)
385
413
  .map((source) => ({
386
414
  uri: source.uri as string,
387
415
  width: Math.round((source.width ?? 0) * (source.scale ?? 1)),
@@ -40,21 +40,18 @@ type LoadEndEvent = Readonly<{
40
40
  }>
41
41
 
42
42
  export interface NativeProps extends ViewProps {
43
- // A source ({ uri, headers, priority, cache, ... }), as the native
44
- // sides read it.
43
+ // The `source` prop: a source ({ uri, headers, priority, cache, ... },
44
+ // with its thumbhash or blurDataUri), or several (sizes of one image) in
45
+ // an array, picked from for the view's size. A change to the thumbhashes
46
+ // and blurDataUris only doesn't load the image again.
45
47
  source?: CodegenTypes.UnsafeMixed
46
- // Several sources (sizes of one image), picked from for the view's size.
47
- sources?: CodegenTypes.UnsafeMixed
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).
48
+ // The `placeholder` prop: a thumbhash or blurDataUri ({ thumbhash } or
49
+ // { blurDataUri }, for the sources without their own), a url to load
50
+ // apart from the image, as a source ({ uri, headers, cacheKey, cache }),
51
+ // or an image shown at once: a resolved asset source ({ uri, width,
52
+ // height, scale }) or a data uri ({ uri } on iOS; the uri on Android, as
53
+ // a string).
52
54
  placeholder?: CodegenTypes.UnsafeMixed
53
- // What shows in place of placeholder while a source loads: its ThumbHash,
54
- // or its blurDataUri (blurred natively), by the source's index ('' for
55
- // none; one for a single source). Apart from the sources, so a new one
56
- // for the same image doesn't load it again.
57
- sourcePlaceholders?: ReadonlyArray<string>
58
55
  // objectFit, resolved (fill, contain, cover, none or scale-down).
59
56
  objectFit?: CodegenTypes.WithDefault<string, 'cover'>
60
57
  // objectPosition, as CSS computes it: on each axis the image's left or top