react-native-fast-image 10.4.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.
@@ -268,13 +268,18 @@ function cssPosition({ x, y }) {
268
268
  }
269
269
  //#endregion
270
270
  //#region src/placeholders.ts
271
+ function placeholderSource(value) {
272
+ const uri = typeof value === "string" ? value : value && typeof value === "object" ? value.uri : void 0;
273
+ if (typeof uri !== "string" || !/^https?:/i.test(uri)) return void 0;
274
+ return typeof value === "string" ? { uri } : value;
275
+ }
271
276
  function hasSourcePlaceholder(value) {
272
277
  return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
273
278
  }
274
279
  function dataUri(value) {
275
280
  if (typeof value !== "string" || value === "") return void 0;
276
281
  if (/^data:/i.test(value)) return value;
277
- if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`placeholder\` and \`blurDataUri\` don't load urls: "${value.slice(0, 60)}" is ignored. Use a data uri (data:image/…), or for \`placeholder\`, a require()d image or the name of an image bundled with the app.`, "warn");
282
+ if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`blurDataUri\` takes a data uri (data:image/…): "${value.slice(0, 60)}" is ignored. For a placeholder loaded from a url, use \`placeholder\`.`, "warn");
278
283
  }
279
284
  //#endregion
280
285
  //#region src/index.web.tsx
@@ -284,7 +289,7 @@ const notSupported = {
284
289
  };
285
290
  const useClientLayoutEffect = globalThis.document === void 0 ? react.useEffect : react.useLayoutEffect;
286
291
  function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loading, sizes, dynamicRangeLimit, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
287
- var _resolveUri;
292
+ var _resolveUri, _placeholderSource;
288
293
  if (typeof __DEV__ !== "undefined" && __DEV__) warnIfChildren(children);
289
294
  const { accessibilityLabel, "aria-label": ariaLabel, resizeMode: _resizeMode, ...viewProps } = props;
290
295
  const label = ariaLabel || accessibilityLabel || void 0;
@@ -296,7 +301,9 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
296
301
  const priority = sourcePriority(source);
297
302
  const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
298
303
  const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
299
- const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
304
+ const urlPlaceholder = (_placeholderSource = placeholderSource(placeholder)) === null || _placeholderSource === void 0 ? void 0 : _placeholderSource.uri;
305
+ const [failedPlaceholder, setFailedPlaceholder] = (0, react.useState)();
306
+ const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : urlPlaceholder ? void 0 : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
300
307
  const [result, setResult] = (0, react.useState)();
301
308
  const ok = result && result.key === key ? result.ok : void 0;
302
309
  const svgId = (0, react.useId)().replace(/[^\w-]/g, "");
@@ -305,7 +312,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
305
312
  const fit = resolveObjectFit(objectFit, style);
306
313
  const position = cssPosition(objectPositionOrCentered(objectPosition));
307
314
  const filter = [blurRadius ? `blur(${blurRadius}px)` : "", tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") || void 0;
308
- const showDefault = !!defaultUri && ok !== true;
315
+ const showDefault = ok !== true;
309
316
  useClientLayoutEffect(() => {
310
317
  if (key) onLoadStart === null || onLoadStart === void 0 || onLoadStart();
311
318
  }, [key]);
@@ -326,6 +333,18 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
326
333
  position,
327
334
  filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
328
335
  alt: key ? "" : label !== null && label !== void 0 ? label : ""
336
+ }) : null, urlPlaceholder && showDefault ? /* @__PURE__ */ react.default.createElement(Picture, {
337
+ key: `placeholder ${urlPlaceholder}`,
338
+ src: urlPlaceholder,
339
+ loading,
340
+ priority: "high",
341
+ dynamicRangeLimit,
342
+ fit: fit === "scale-down" || fit === "none" ? "contain" : fit,
343
+ position,
344
+ filter: tintColor ? `url(#${tintId})` : void 0,
345
+ hidden: failedPlaceholder === urlPlaceholder,
346
+ onError: () => setFailedPlaceholder(urlPlaceholder),
347
+ alt: ""
329
348
  }) : null, key && src ? /* @__PURE__ */ react.default.createElement(Picture, {
330
349
  key: several ? "sizes" : key,
331
350
  src,
@@ -397,10 +416,9 @@ function shared(source, key, preview) {
397
416
  function resolveUri(source) {
398
417
  let uri;
399
418
  if (typeof source === "number") {
400
- var _asset$scales$;
401
419
  const asset = (0, react_native_web_dist_modules_AssetRegistry.getAssetByID)(source);
402
420
  if (!asset) return void 0;
403
- 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);
404
422
  const suffix = scale !== 1 ? `@${scale}x` : "";
405
423
  uri = `${asset.httpServerLocation}/${asset.name}${suffix}.${asset.type}`;
406
424
  } else if (typeof source === "string") uri = source;
@@ -409,9 +427,24 @@ function resolveUri(source) {
409
427
  const svg = uri === null || uri === void 0 ? void 0 : uri.match(/^(data:image\/svg\+xml;utf8,)(.*)/);
410
428
  return svg ? svg[1] + encodeURIComponent(svg[2]) : uri || void 0;
411
429
  }
412
- 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) {
413
445
  var _largest$uri, _sources$find;
414
- 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) => {
415
448
  var _source$width, _source$scale;
416
449
  return {
417
450
  uri: source.uri,
@@ -110,6 +110,13 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
110
110
  export type PreloadFn = (
111
111
  sources: Array<FastImageSource>,
112
112
  ) => Promise<Array<PreloadResult>>
113
+ export type PlaceholderSource = {
114
+ uri?: string,
115
+ headers?: Object,
116
+ cacheKey?: string,
117
+ cache?: CacheControls,
118
+ }
119
+
113
120
  export type SourcePlaceholder = {
114
121
  thumbhash?: string,
115
122
  blurDataUri?: string,
@@ -138,8 +145,8 @@ export type FastImageProps = $ReadOnly<{|
138
145
  onLoadStart?: ?() => void,
139
146
  onProgress?: ?(event: OnProgressEvent) => void,
140
147
 
141
- source?: ?(FastImageSource | Array<FastImageSource> | number),
142
- placeholder?: ?(number | string | SourcePlaceholder),
148
+ source?: ?(FastImageSource | Array<FastImageSource | number> | number),
149
+ placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
143
150
  // Deprecated: use placeholder, which is the same.
144
151
  defaultSource?: ?number,
145
152
 
package/dist/index.d.ts CHANGED
@@ -154,6 +154,8 @@ export type Source = {
154
154
  };
155
155
  /** A `thumbhash` or `blurDataUri` as `placeholder`. */
156
156
  export type SourcePlaceholder = Pick<Source, 'thumbhash' | 'blurDataUri'>;
157
+ /** A url as `placeholder`, loaded like a source (see `placeholder`). */
158
+ export type PlaceholderSource = Pick<Source, 'uri' | 'headers' | 'cacheKey' | 'cache'>;
157
159
  /** How the image fades in: see [`transition`](#transition). */
158
160
  export type Transition = {
159
161
  /**
@@ -329,9 +331,10 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
329
331
  * - `FastImage.preload`, `getCachePath` and `writeToCache` take one size:
330
332
  * pass the one a view will show. An array fails with
331
333
  * `{ ok: false, error }`.
334
+ * - A `require()`d image can be one of the sizes: its size is the file's.
332
335
  * - An array of one is the same as that source.
333
336
  */
334
- source?: Source | Source[] | ImageRequireSource;
337
+ source?: Source | Array<Source | ImageRequireSource> | ImageRequireSource;
335
338
  /**
336
339
  * A placeholder shown while the image loads, and if it fails to load:
337
340
  *
@@ -343,19 +346,26 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
343
346
  * `placeholder={{ thumbhash }}`.
344
347
  * - A tiny image as a data uri, shown blurred:
345
348
  * `placeholder={{ blurDataUri }}`.
349
+ * - A url, loaded and cached like a source:
350
+ * `placeholder="https://example.com/placeholder.png"`, or a source with
351
+ * `headers` or a `cacheKey`: `placeholder={{ uri, headers }}`.
352
+ * - A url only shown if it's already cached, e.g. a list's thumbnail while
353
+ * the full-size image loads: `placeholder={{ uri, cache: 'cacheOnly' }}`.
346
354
  *
347
355
  * A source's own `thumbhash` or `blurDataUri` is shown instead, if it has
348
- * one.
356
+ * one; a url placeholder shows over it once it has loaded.
349
357
  *
350
358
  * Notes:
351
359
  *
352
- * - A url isn't loaded: it's ignored, with a warning in development.
353
360
  * - On Android, a `require()`d image doesn't show in debug builds, where
354
361
  * the dev server serves it. Release builds show it.
362
+ * - On the web, a url loads like any image, also with `'cacheOnly'`: a
363
+ * browser can't look only in its cache.
355
364
  */
356
- placeholder?: ImageRequireSource | string | SourcePlaceholder;
365
+ placeholder?: ImageRequireSource | string | SourcePlaceholder | PlaceholderSource;
357
366
  /**
358
- * @deprecated Use `placeholder`, which does the same.
367
+ * @deprecated Use `placeholder`, which does the same. With both,
368
+ * `placeholder` shows.
359
369
  */
360
370
  defaultSource?: ImageRequireSource;
361
371
  /**
package/dist/index.js CHANGED
@@ -234,8 +234,14 @@ var FastImageViewNativeComponent_default = codegenNativeComponent("FastImageView
234
234
  var NativeFastImageModule_default = TurboModuleRegistry.get("FastImageModule");
235
235
  //#endregion
236
236
  //#region src/placeholders.ts
237
+ const SCHEME = /^[a-z][a-z\d+.-]*:/i;
237
238
  function isImageName(value) {
238
- return !/^[a-z][a-z\d+.-]*:/i.test(value) && !value.startsWith("/");
239
+ return !SCHEME.test(value) && !value.startsWith("/");
240
+ }
241
+ function placeholderSource(value) {
242
+ const uri = typeof value === "string" ? value : value && typeof value === "object" ? value.uri : void 0;
243
+ if (typeof uri !== "string" || !/^https?:/i.test(uri)) return void 0;
244
+ return typeof value === "string" ? { uri } : value;
239
245
  }
240
246
  function hasSourcePlaceholder(value) {
241
247
  return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
@@ -243,20 +249,10 @@ function hasSourcePlaceholder(value) {
243
249
  function dataUri(value) {
244
250
  if (typeof value !== "string" || value === "") return void 0;
245
251
  if (/^data:/i.test(value)) return value;
246
- if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`placeholder\` and \`blurDataUri\` don't load urls: "${value.slice(0, 60)}" is ignored. Use a data uri (data:image/…), or for \`placeholder\`, a require()d image or the name of an image bundled with the app.`, "warn");
252
+ if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`blurDataUri\` takes a data uri (data:image/…): "${value.slice(0, 60)}" is ignored. For a placeholder loaded from a url, use \`placeholder\`.`, "warn");
247
253
  }
248
254
  //#endregion
249
255
  //#region src/index.tsx
250
- function withoutPlaceholders(source) {
251
- if (!hasSourcePlaceholder(source)) return source;
252
- const { thumbhash: _thumbhash, blurDataUri: _blurDataUri, ...rest } = source;
253
- return rest;
254
- }
255
- function sourcePlaceholder(source) {
256
- if (!source || typeof source !== "object") return "";
257
- const { thumbhash, blurDataUri } = source;
258
- return thumbhash || dataUri(blurDataUri) || "";
259
- }
260
256
  const resolvePlaceholder = (placeholder) => {
261
257
  if (typeof placeholder === "string") {
262
258
  if (isImageName(placeholder)) return Platform.OS === "android" ? placeholder : { uri: placeholder };
@@ -321,25 +317,19 @@ function builtInTransition() {
321
317
  }
322
318
  let defaultTransition;
323
319
  function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, loading: _loading, sizes: _sizes, forwardedRef, ...viewProps }) {
324
- var _props$recyclingKey, _defaultTransition;
320
+ var _placeholderSource, _props$recyclingKey, _defaultTransition;
325
321
  const { fallback, resizeMode: _resizeMode, ...props } = viewProps;
326
322
  if (__DEV__ && fallback !== void 0) warnOnce("fallback", "react-native-fast-image: `fallback` was removed in 10.0, and does nothing: images always load natively. Remove it, or use React Native's Image for those images.", "warn");
327
323
  if (__DEV__) warnIfChildren(children);
328
324
  const resolvedTintColor = tintColor != null ? tintColor : fromStyle(style, "tintColor");
329
325
  const fit = resolveObjectFit(objectFit, style);
330
326
  const position = objectPositionOrCentered(objectPosition);
331
- const shared = hasSourcePlaceholder(placeholder) ? sourcePlaceholder(placeholder) : "";
332
- const { resolvedSource, sources, sourcePlaceholders } = useMemo(() => {
333
- const sources = Array.isArray(source) ? source.length === 1 ? void 0 : source : void 0;
334
- const single = Array.isArray(source) ? source[0] : source;
335
- const placeholders = (sources !== null && sources !== void 0 ? sources : [single]).map((entry) => sourcePlaceholder(entry) || shared);
336
- return {
337
- resolvedSource: sources ? void 0 : Image.resolveAssetSource(withoutPlaceholders(single)),
338
- sources: (sources === null || sources === void 0 ? void 0 : sources.some(hasSourcePlaceholder)) ? sources.map(withoutPlaceholders) : sources,
339
- sourcePlaceholders: placeholders.some(Boolean) ? placeholders : void 0
340
- };
341
- }, [source, shared]);
342
- const resolvedPlaceholder = 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);
343
333
  return /* @__PURE__ */ React.createElement(FastImageViewNativeComponent_default, {
344
334
  ...props,
345
335
  ref: forwardedRef,
@@ -348,8 +338,6 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
348
338
  loopCount: loopCount(loop),
349
339
  ...resolveTransition(transition, (_defaultTransition = defaultTransition) !== null && _defaultTransition !== void 0 ? _defaultTransition : builtInTransition()),
350
340
  source: resolvedSource,
351
- sources,
352
- sourcePlaceholders,
353
341
  placeholder: resolvedPlaceholder,
354
342
  onFastImageLoadStart: onLoadStart,
355
343
  onFastImageProgress: onProgress,
@@ -110,6 +110,13 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
110
110
  export type PreloadFn = (
111
111
  sources: Array<FastImageSource>,
112
112
  ) => Promise<Array<PreloadResult>>
113
+ export type PlaceholderSource = {
114
+ uri?: string,
115
+ headers?: Object,
116
+ cacheKey?: string,
117
+ cache?: CacheControls,
118
+ }
119
+
113
120
  export type SourcePlaceholder = {
114
121
  thumbhash?: string,
115
122
  blurDataUri?: string,
@@ -138,8 +145,8 @@ export type FastImageProps = $ReadOnly<{|
138
145
  onLoadStart?: ?() => void,
139
146
  onProgress?: ?(event: OnProgressEvent) => void,
140
147
 
141
- source?: ?(FastImageSource | Array<FastImageSource> | number),
142
- placeholder?: ?(number | string | SourcePlaceholder),
148
+ source?: ?(FastImageSource | Array<FastImageSource | number> | number),
149
+ placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
143
150
  // Deprecated: use placeholder, which is the same.
144
151
  defaultSource?: ?number,
145
152
 
package/dist/index.web.js CHANGED
@@ -241,13 +241,18 @@ function cssPosition({ x, y }) {
241
241
  }
242
242
  //#endregion
243
243
  //#region src/placeholders.ts
244
+ function placeholderSource(value) {
245
+ const uri = typeof value === "string" ? value : value && typeof value === "object" ? value.uri : void 0;
246
+ if (typeof uri !== "string" || !/^https?:/i.test(uri)) return void 0;
247
+ return typeof value === "string" ? { uri } : value;
248
+ }
244
249
  function hasSourcePlaceholder(value) {
245
250
  return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
246
251
  }
247
252
  function dataUri(value) {
248
253
  if (typeof value !== "string" || value === "") return void 0;
249
254
  if (/^data:/i.test(value)) return value;
250
- if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`placeholder\` and \`blurDataUri\` don't load urls: "${value.slice(0, 60)}" is ignored. Use a data uri (data:image/…), or for \`placeholder\`, a require()d image or the name of an image bundled with the app.`, "warn");
255
+ if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`blurDataUri\` takes a data uri (data:image/…): "${value.slice(0, 60)}" is ignored. For a placeholder loaded from a url, use \`placeholder\`.`, "warn");
251
256
  }
252
257
  //#endregion
253
258
  //#region src/index.web.tsx
@@ -257,7 +262,7 @@ const notSupported = {
257
262
  };
258
263
  const useClientLayoutEffect = globalThis.document === void 0 ? useEffect : useLayoutEffect;
259
264
  function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loading, sizes, dynamicRangeLimit, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
260
- var _resolveUri;
265
+ var _resolveUri, _placeholderSource;
261
266
  if (typeof __DEV__ !== "undefined" && __DEV__) warnIfChildren(children);
262
267
  const { accessibilityLabel, "aria-label": ariaLabel, resizeMode: _resizeMode, ...viewProps } = props;
263
268
  const label = ariaLabel || accessibilityLabel || void 0;
@@ -269,7 +274,9 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
269
274
  const priority = sourcePriority(source);
270
275
  const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
271
276
  const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
272
- const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
277
+ const urlPlaceholder = (_placeholderSource = placeholderSource(placeholder)) === null || _placeholderSource === void 0 ? void 0 : _placeholderSource.uri;
278
+ const [failedPlaceholder, setFailedPlaceholder] = useState();
279
+ const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : urlPlaceholder ? void 0 : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
273
280
  const [result, setResult] = useState();
274
281
  const ok = result && result.key === key ? result.ok : void 0;
275
282
  const svgId = useId().replace(/[^\w-]/g, "");
@@ -278,7 +285,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
278
285
  const fit = resolveObjectFit(objectFit, style);
279
286
  const position = cssPosition(objectPositionOrCentered(objectPosition));
280
287
  const filter = [blurRadius ? `blur(${blurRadius}px)` : "", tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") || void 0;
281
- const showDefault = !!defaultUri && ok !== true;
288
+ const showDefault = ok !== true;
282
289
  useClientLayoutEffect(() => {
283
290
  if (key) onLoadStart === null || onLoadStart === void 0 || onLoadStart();
284
291
  }, [key]);
@@ -299,6 +306,18 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
299
306
  position,
300
307
  filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
301
308
  alt: key ? "" : label !== null && label !== void 0 ? label : ""
309
+ }) : null, urlPlaceholder && showDefault ? /* @__PURE__ */ React.createElement(Picture, {
310
+ key: `placeholder ${urlPlaceholder}`,
311
+ src: urlPlaceholder,
312
+ loading,
313
+ priority: "high",
314
+ dynamicRangeLimit,
315
+ fit: fit === "scale-down" || fit === "none" ? "contain" : fit,
316
+ position,
317
+ filter: tintColor ? `url(#${tintId})` : void 0,
318
+ hidden: failedPlaceholder === urlPlaceholder,
319
+ onError: () => setFailedPlaceholder(urlPlaceholder),
320
+ alt: ""
302
321
  }) : null, key && src ? /* @__PURE__ */ React.createElement(Picture, {
303
322
  key: several ? "sizes" : key,
304
323
  src,
@@ -370,10 +389,9 @@ function shared(source, key, preview) {
370
389
  function resolveUri(source) {
371
390
  let uri;
372
391
  if (typeof source === "number") {
373
- var _asset$scales$;
374
392
  const asset = getAssetByID(source);
375
393
  if (!asset) return void 0;
376
- 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);
377
395
  const suffix = scale !== 1 ? `@${scale}x` : "";
378
396
  uri = `${asset.httpServerLocation}/${asset.name}${suffix}.${asset.type}`;
379
397
  } else if (typeof source === "string") uri = source;
@@ -382,9 +400,24 @@ function resolveUri(source) {
382
400
  const svg = uri === null || uri === void 0 ? void 0 : uri.match(/^(data:image\/svg\+xml;utf8,)(.*)/);
383
401
  return svg ? svg[1] + encodeURIComponent(svg[2]) : uri || void 0;
384
402
  }
385
- 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) {
386
418
  var _largest$uri, _sources$find;
387
- 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) => {
388
421
  var _source$width, _source$scale;
389
422
  return {
390
423
  uri: source.uri,
@@ -110,6 +110,13 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
110
110
  export type PreloadFn = (
111
111
  sources: Array<FastImageSource>,
112
112
  ) => Promise<Array<PreloadResult>>
113
+ export type PlaceholderSource = {
114
+ uri?: string,
115
+ headers?: Object,
116
+ cacheKey?: string,
117
+ cache?: CacheControls,
118
+ }
119
+
113
120
  export type SourcePlaceholder = {
114
121
  thumbhash?: string,
115
122
  blurDataUri?: string,
@@ -138,8 +145,8 @@ export type FastImageProps = $ReadOnly<{|
138
145
  onLoadStart?: ?() => void,
139
146
  onProgress?: ?(event: OnProgressEvent) => void,
140
147
 
141
- source?: ?(FastImageSource | Array<FastImageSource> | number),
142
- placeholder?: ?(number | string | SourcePlaceholder),
148
+ source?: ?(FastImageSource | Array<FastImageSource | number> | number),
149
+ placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
143
150
  // Deprecated: use placeholder, which is the same.
144
151
  defaultSource?: ?number,
145
152
 
@@ -1,4 +1,5 @@
1
- import type { SourcePlaceholder } from './index';
1
+ import type { PlaceholderSource, SourcePlaceholder } from './index';
2
2
  export declare function isImageName(value: string): boolean;
3
+ export declare function placeholderSource(value: unknown): PlaceholderSource | undefined;
3
4
  export declare function hasSourcePlaceholder(value: unknown): value is SourcePlaceholder;
4
5
  export declare function dataUri(value: unknown): string | undefined;
@@ -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,17 +28,23 @@ 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;
37
- // The `sourcePlaceholders` prop, by the source's index (an empty string for
38
- // none; one for a single source): the picked source's ThumbHash or
39
- // blurDataUri (a data uri) shows instead of placeholder. JS sends them apart
40
- // from the sources, so a new one for the same image doesn't load it again.
36
+ // `placeholder` as a url (a source): loaded apart from the
37
+ // image, at a high priority (or only from the cache, with cache cacheOnly),
38
+ // it shows over the other placeholders once it has.
39
+ @property (nonatomic, strong) FFFastImageSource *placeholderSource;
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.
41
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;
42
48
  @property (nonatomic, strong) UIColor *imageColor;
43
49
  // When it changes, the next image doesn't replace the current one: the view
44
50
  // clears first (for views reused for other content, like list rows).