react-native-fast-image 10.5.0 → 10.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import React, { Children, forwardRef, memo, useMemo } from "react";
2
- import { Image, Platform, StyleSheet, TurboModuleRegistry, View, codegenNativeComponent } from "react-native";
2
+ import { Image, Platform, StyleSheet, TurboModuleRegistry, View, codegenNativeCommands, codegenNativeComponent } from "react-native";
3
3
  //#region src/constants.ts
4
4
  const resizeMode = {
5
5
  contain: "contain",
@@ -228,6 +228,7 @@ function objectPositionOrCentered(position) {
228
228
  }
229
229
  //#endregion
230
230
  //#region src/specs/FastImageViewNativeComponent.ts
231
+ const Commands = codegenNativeCommands({ supportedCommands: ["reload"] });
231
232
  var FastImageViewNativeComponent_default = codegenNativeComponent("FastImageView");
232
233
  //#endregion
233
234
  //#region src/specs/NativeFastImageModule.ts
@@ -252,17 +253,28 @@ function dataUri(value) {
252
253
  if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`blurDataUri\` takes a data uri (data:image/…): "${value.slice(0, 60)}" is ignored. For a placeholder loaded from a url, use \`placeholder\`.`, "warn");
253
254
  }
254
255
  //#endregion
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) || "";
256
+ //#region src/composeRefs.ts
257
+ function composeRefs(...refs) {
258
+ return (value) => {
259
+ const cleanups = refs.map((ref) => {
260
+ if (typeof ref === "function") {
261
+ const cleanup = ref(value);
262
+ return typeof cleanup === "function" ? cleanup : () => {
263
+ ref(null);
264
+ };
265
+ }
266
+ if (ref) {
267
+ ref.current = value;
268
+ return () => {
269
+ ref.current = null;
270
+ };
271
+ }
272
+ });
273
+ return () => cleanups.forEach((cleanup) => cleanup === null || cleanup === void 0 ? void 0 : cleanup());
274
+ };
265
275
  }
276
+ //#endregion
277
+ //#region src/index.tsx
266
278
  const resolvePlaceholder = (placeholder) => {
267
279
  if (typeof placeholder === "string") {
268
280
  if (isImageName(placeholder)) return Platform.OS === "android" ? placeholder : { uri: placeholder };
@@ -334,28 +346,21 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
334
346
  const resolvedTintColor = tintColor != null ? tintColor : fromStyle(style, "tintColor");
335
347
  const fit = resolveObjectFit(objectFit, style);
336
348
  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);
349
+ const resolvedSource = useMemo(() => {
350
+ for (const entry of [source].flat()) dataUri(entry === null || entry === void 0 ? void 0 : entry.blurDataUri);
351
+ const single = Array.isArray(source) && source.length === 1 ? source[0] : source;
352
+ return Array.isArray(single) ? single.map((entry) => Image.resolveAssetSource(entry)) : Image.resolveAssetSource(single);
353
+ }, [source]);
354
+ 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);
355
+ const ref = useMemo(() => composeRefs(addReload, forwardedRef), [forwardedRef]);
349
356
  return /* @__PURE__ */ React.createElement(FastImageViewNativeComponent_default, {
350
357
  ...props,
351
- ref: forwardedRef,
358
+ ref,
352
359
  style: [styles.image, style],
353
360
  recyclingKey: (_props$recyclingKey = props.recyclingKey) !== null && _props$recyclingKey !== void 0 ? _props$recyclingKey : void 0,
354
361
  loopCount: loopCount(loop),
355
362
  ...resolveTransition(transition, (_defaultTransition = defaultTransition) !== null && _defaultTransition !== void 0 ? _defaultTransition : builtInTransition()),
356
363
  source: resolvedSource,
357
- sources,
358
- sourcePlaceholders,
359
364
  placeholder: resolvedPlaceholder,
360
365
  onFastImageLoadStart: onLoadStart,
361
366
  onFastImageProgress: onProgress,
@@ -377,6 +382,9 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
377
382
  objectPositionYOffset: position.y.offset
378
383
  });
379
384
  }
385
+ function addReload(view) {
386
+ if (view) view.reload = () => Commands.reload(view);
387
+ }
380
388
  const FastImageMemo = memo(FastImageBase);
381
389
  const FastImageComponent = forwardRef((props, ref) => /* @__PURE__ */ React.createElement(FastImageMemo, {
382
390
  forwardedRef: ref,
@@ -145,7 +145,7 @@ export type FastImageProps = $ReadOnly<{|
145
145
  onLoadStart?: ?() => void,
146
146
  onProgress?: ?(event: OnProgressEvent) => void,
147
147
 
148
- source?: ?(FastImageSource | Array<FastImageSource> | number),
148
+ source?: ?(FastImageSource | Array<FastImageSource | number> | number),
149
149
  placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
150
150
  // Deprecated: use placeholder, which is the same.
151
151
  defaultSource?: ?number,
@@ -196,4 +196,6 @@ declare export default class FastImage extends React$Component<FastImageProps> {
196
196
  source: FastImageSource,
197
197
  file: string,
198
198
  ) => Promise<CachePathResult>;
199
+ // On a ref: loads the image again, e.g. to try again after onError.
200
+ reload(): void;
199
201
  }
@@ -1,6 +1,8 @@
1
1
  import React from 'react';
2
- import type { FastImageBackgroundProps, FastImageProps, FastImageStaticProperties } from './index';
2
+ import { View } from 'react-native';
3
+ import type { FastImageBackgroundProps, FastImageProps, FastImageRef, FastImageStaticProperties } from './index';
3
4
  export type * from './index';
4
- declare const FastImage: React.ComponentType<FastImageProps> & FastImageStaticProperties;
5
- export declare const FastImageBackground: React.ComponentType<FastImageBackgroundProps>;
5
+ type ViewRef = React.ComponentRef<typeof View>;
6
+ declare const FastImage: React.ForwardRefExoticComponent<FastImageProps & React.RefAttributes<FastImageRef>> & FastImageStaticProperties;
7
+ export declare const FastImageBackground: React.ForwardRefExoticComponent<FastImageBackgroundProps & React.RefAttributes<ViewRef>>;
6
8
  export default FastImage;
package/dist/index.web.js CHANGED
@@ -1,4 +1,4 @@
1
- import React, { Children, forwardRef, memo, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
1
+ import React, { Children, forwardRef, memo, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import { Image, PixelRatio, StyleSheet, View } from "react-native";
3
3
  import { getAssetByID } from "react-native-web/dist/modules/AssetRegistry";
4
4
  //#region src/constants.ts
@@ -255,6 +255,27 @@ function dataUri(value) {
255
255
  if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`blurDataUri\` takes a data uri (data:image/…): "${value.slice(0, 60)}" is ignored. For a placeholder loaded from a url, use \`placeholder\`.`, "warn");
256
256
  }
257
257
  //#endregion
258
+ //#region src/composeRefs.ts
259
+ function composeRefs(...refs) {
260
+ return (value) => {
261
+ const cleanups = refs.map((ref) => {
262
+ if (typeof ref === "function") {
263
+ const cleanup = ref(value);
264
+ return typeof cleanup === "function" ? cleanup : () => {
265
+ ref(null);
266
+ };
267
+ }
268
+ if (ref) {
269
+ ref.current = value;
270
+ return () => {
271
+ ref.current = null;
272
+ };
273
+ }
274
+ });
275
+ return () => cleanups.forEach((cleanup) => cleanup === null || cleanup === void 0 ? void 0 : cleanup());
276
+ };
277
+ }
278
+ //#endregion
258
279
  //#region src/index.web.tsx
259
280
  const notSupported = {
260
281
  ok: false,
@@ -271,12 +292,17 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
271
292
  const src = sized ? sized.src : resolveUri(source);
272
293
  const srcSet = sized === null || sized === void 0 ? void 0 : sized.srcSet;
273
294
  const key = srcSet !== null && srcSet !== void 0 ? srcSet : src;
295
+ const [reloads, setReloads] = useState(0);
296
+ const ref = useMemo(() => composeRefs((view) => {
297
+ if (view) view.reload = () => setReloads((count) => count + 1);
298
+ }, forwardedRef), [forwardedRef]);
274
299
  const priority = sourcePriority(source);
275
300
  const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
276
301
  const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
277
302
  const urlPlaceholder = (_placeholderSource = placeholderSource(placeholder)) === null || _placeholderSource === void 0 ? void 0 : _placeholderSource.uri;
303
+ const placeholderKey = `${urlPlaceholder} ${reloads}`;
278
304
  const [failedPlaceholder, setFailedPlaceholder] = useState();
279
- const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview || urlPlaceholder ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
305
+ const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : urlPlaceholder ? void 0 : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
280
306
  const [result, setResult] = useState();
281
307
  const ok = result && result.key === key ? result.ok : void 0;
282
308
  const svgId = useId().replace(/[^\w-]/g, "");
@@ -288,14 +314,14 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
288
314
  const showDefault = ok !== true;
289
315
  useClientLayoutEffect(() => {
290
316
  if (key) onLoadStart === null || onLoadStart === void 0 || onLoadStart();
291
- }, [key]);
317
+ }, [key, reloads]);
292
318
  return /* @__PURE__ */ React.createElement(View, {
293
319
  ...viewProps,
294
320
  "aria-label": label,
295
321
  style: [styles.container, style],
296
322
  onLayout,
297
323
  pointerEvents,
298
- ref: forwardedRef
324
+ ref
299
325
  }, defaultUri && showDefault ? /* @__PURE__ */ React.createElement(Picture, {
300
326
  key: `default ${defaultUri}`,
301
327
  src: defaultUri,
@@ -307,7 +333,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
307
333
  filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
308
334
  alt: key ? "" : label !== null && label !== void 0 ? label : ""
309
335
  }) : null, urlPlaceholder && showDefault ? /* @__PURE__ */ React.createElement(Picture, {
310
- key: `placeholder ${urlPlaceholder}`,
336
+ key: `placeholder ${placeholderKey}`,
311
337
  src: urlPlaceholder,
312
338
  loading,
313
339
  priority: "high",
@@ -315,11 +341,11 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
315
341
  fit: fit === "scale-down" || fit === "none" ? "contain" : fit,
316
342
  position,
317
343
  filter: tintColor ? `url(#${tintId})` : void 0,
318
- hidden: failedPlaceholder === urlPlaceholder,
319
- onError: () => setFailedPlaceholder(urlPlaceholder),
344
+ hidden: failedPlaceholder === placeholderKey,
345
+ onError: () => setFailedPlaceholder(placeholderKey),
320
346
  alt: ""
321
347
  }) : null, key && src ? /* @__PURE__ */ React.createElement(Picture, {
322
- key: several ? "sizes" : key,
348
+ key: `${several ? "sizes" : key} ${reloads}`,
323
349
  src,
324
350
  srcSet,
325
351
  sizes,
@@ -389,10 +415,9 @@ function shared(source, key, preview) {
389
415
  function resolveUri(source) {
390
416
  let uri;
391
417
  if (typeof source === "number") {
392
- var _asset$scales$;
393
418
  const asset = getAssetByID(source);
394
419
  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);
420
+ const scale = assetScale(asset);
396
421
  const suffix = scale !== 1 ? `@${scale}x` : "";
397
422
  uri = `${asset.httpServerLocation}/${asset.name}${suffix}.${asset.type}`;
398
423
  } else if (typeof source === "string") uri = source;
@@ -401,9 +426,24 @@ function resolveUri(source) {
401
426
  const svg = uri === null || uri === void 0 ? void 0 : uri.match(/^(data:image\/svg\+xml;utf8,)(.*)/);
402
427
  return svg ? svg[1] + encodeURIComponent(svg[2]) : uri || void 0;
403
428
  }
404
- function sizedSources(sources) {
429
+ function assetScale(asset) {
430
+ var _asset$scales$;
431
+ 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);
432
+ }
433
+ function sizedSource(source) {
434
+ if (typeof source !== "number") return source;
435
+ const asset = getAssetByID(source);
436
+ return asset ? {
437
+ uri: resolveUri(source),
438
+ width: asset.width,
439
+ height: asset.height,
440
+ scale: assetScale(asset)
441
+ } : void 0;
442
+ }
443
+ function sizedSources(entries) {
405
444
  var _largest$uri, _sources$find;
406
- const widths = sources.filter((source) => (source === null || source === void 0 ? void 0 : source.uri) && source.width).map((source) => {
445
+ const sources = entries.map(sizedSource);
446
+ const widths = sources.filter((source) => !!(source === null || source === void 0 ? void 0 : source.uri) && !!source.width).map((source) => {
407
447
  var _source$width, _source$scale;
408
448
  return {
409
449
  uri: source.uri,
@@ -563,11 +603,10 @@ FastImage.setDefaults = () => {};
563
603
  FastImage.writeToCache = () => Promise.resolve(notSupported);
564
604
  FastImage.getCachePath = () => Promise.resolve(notSupported);
565
605
  const styles = StyleSheet.create({ container: { overflow: "hidden" } });
566
- const FastImageWithRef = FastImageComponent;
567
606
  const FastImageBackground = forwardRef(({ style, imageStyle, imageRef, children, ...props }, ref) => /* @__PURE__ */ React.createElement(View, {
568
607
  style,
569
608
  ref
570
- }, /* @__PURE__ */ React.createElement(FastImageWithRef, {
609
+ }, /* @__PURE__ */ React.createElement(FastImageComponent, {
571
610
  ...props,
572
611
  style: [StyleSheet.absoluteFill, imageStyle],
573
612
  ref: imageRef
@@ -145,7 +145,7 @@ export type FastImageProps = $ReadOnly<{|
145
145
  onLoadStart?: ?() => void,
146
146
  onProgress?: ?(event: OnProgressEvent) => void,
147
147
 
148
- source?: ?(FastImageSource | Array<FastImageSource> | number),
148
+ source?: ?(FastImageSource | Array<FastImageSource | number> | number),
149
149
  placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
150
150
  // Deprecated: use placeholder, which is the same.
151
151
  defaultSource?: ?number,
@@ -196,4 +196,6 @@ declare export default class FastImage extends React$Component<FastImageProps> {
196
196
  source: FastImageSource,
197
197
  file: string,
198
198
  ) => Promise<CachePathResult>;
199
+ // On a ref: loads the image again, e.g. to try again after onError.
200
+ reload(): void;
199
201
  }
@@ -1,3 +1,4 @@
1
+ import type * as React from 'react';
1
2
  import type { CodegenTypes, ColorValue, HostComponent, ViewProps } from 'react-native';
2
3
  type LoadStartEvent = Readonly<{}>;
3
4
  type ProgressEvent = Readonly<{
@@ -20,9 +21,7 @@ type LoadEndEvent = Readonly<{
20
21
  }>;
21
22
  export interface NativeProps extends ViewProps {
22
23
  source?: CodegenTypes.UnsafeMixed;
23
- sources?: CodegenTypes.UnsafeMixed;
24
24
  placeholder?: CodegenTypes.UnsafeMixed;
25
- sourcePlaceholders?: ReadonlyArray<string>;
26
25
  objectFit?: CodegenTypes.WithDefault<string, 'cover'>;
27
26
  objectPositionX?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.5>;
28
27
  objectPositionXOffset?: CodegenTypes.Float;
@@ -46,5 +45,9 @@ export interface NativeProps extends ViewProps {
46
45
  onFastImageError?: CodegenTypes.DirectEventHandler<ErrorEvent>;
47
46
  onFastImageLoadEnd?: CodegenTypes.DirectEventHandler<LoadEndEvent>;
48
47
  }
48
+ interface NativeCommands {
49
+ reload: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
50
+ }
51
+ export declare const Commands: NativeCommands;
49
52
  declare const _default: HostComponent<NativeProps>;
50
53
  export default _default;
@@ -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).
@@ -80,6 +82,10 @@ typedef NS_ENUM(NSInteger, FFFObjectFit) {
80
82
  // the view has a size, when it needs one).
81
83
  - (void) didSetProps;
82
84
 
85
+ // Loads the image again, as a new source would (once the view has a size,
86
+ // when it needs one): the ref's reload().
87
+ - (void) reload;
88
+
83
89
  // Keeps the view's mask (React Native's, for its rounded corners) on the box
84
90
  // the view was laid out at, while objectPosition moves the view: after React
85
91
  // Native sets or removes it.
@@ -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
@@ -1040,6 +1047,11 @@ NSString *FFFErrorMessage(NSError *error)
1040
1047
  }
1041
1048
  }
1042
1049
 
1050
+ - (void) reload {
1051
+ _needsReload = YES;
1052
+ [self didSetProps];
1053
+ }
1054
+
1043
1055
  // The same image decoded again for a dynamicRangeLimit between HDR and SDR,
1044
1056
  // as a resize is (reloadIfResized): without events once it has loaded, and
1045
1057
  // the image showing stays if the load fails. A display setting shouldn't
@@ -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
  }
@@ -341,6 +388,17 @@ typedef NS_OPTIONS(int32_t, FFFEvent) {
341
388
  }
342
389
  }
343
390
 
391
+ // The view's commands (reload), from the ref's methods.
392
+ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
393
+ {
394
+ RCTFastImageViewHandleCommand(self, commandName, args);
395
+ }
396
+
397
+ - (void)reload
398
+ {
399
+ [_imageView reload];
400
+ }
401
+
344
402
  // React Native makes the image view's mask again when the appearance changes
345
403
  // (as after an update): where objectPosition moved the view, it moves back.
346
404
  - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
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.7.0",
4
4
  "description": "FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",
@@ -0,0 +1,29 @@
1
+ // Shared by the native (index.tsx) and web (index.web.tsx) versions.
2
+ import type React from 'react'
3
+
4
+ // One ref that sets each of these refs (as React Native's useMergeRefs does),
5
+ // and clears them when it's cleared.
6
+ export function composeRefs<T>(
7
+ ...refs: (React.Ref<T> | undefined)[]
8
+ ): React.RefCallback<T> {
9
+ return (value) => {
10
+ const cleanups = refs.map((ref) => {
11
+ if (typeof ref === 'function') {
12
+ const cleanup = ref(value)
13
+ return typeof cleanup === 'function'
14
+ ? cleanup
15
+ : () => {
16
+ ref(null)
17
+ }
18
+ }
19
+ if (ref) {
20
+ ref.current = value
21
+ return () => {
22
+ ref.current = null
23
+ }
24
+ }
25
+ return undefined
26
+ })
27
+ return () => cleanups.forEach((cleanup) => cleanup?.())
28
+ }
29
+ }