react-native-fast-image 9.3.7 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +87 -61
  2. package/RNFastImage.podspec +14 -9
  3. package/android/build.gradle +21 -47
  4. package/android/consumer-rules.pro +7 -7
  5. package/android/src/main/AndroidManifest.xml +1 -2
  6. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageAnimatable.java +1 -1
  7. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageAnimated.java +2 -2
  8. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageApng.java +2 -2
  9. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageBlur.java +4 -6
  10. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageCacheControl.java +1 -1
  11. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageCacheLimits.java +4 -1
  12. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageCacheWrite.java +1 -1
  13. package/android/src/main/java/com/fastimage/FastImageCrop.java +92 -0
  14. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageEvent.java +2 -3
  15. package/android/src/main/java/com/fastimage/FastImageEvents.java +74 -0
  16. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageGif.java +1 -1
  17. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageGlide.java +3 -3
  18. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageGlideModule.java +1 -1
  19. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageKeyedGlideUrl.java +1 -1
  20. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageOkHttpProgressGlideModule.java +36 -9
  21. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageRequestListener.java +6 -1
  22. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageSharedDownloads.java +1 -1
  23. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageSource.java +1 -1
  24. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageSourceSize.java +12 -2
  25. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageSvg.java +3 -3
  26. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageSvgRenderer.java +1 -1
  27. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageUrl.java +1 -1
  28. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageViewConverter.java +7 -8
  29. package/android/src/main/java/com/fastimage/FastImageViewManager.java +591 -0
  30. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageViewModule.java +31 -28
  31. package/android/src/main/java/com/fastimage/FastImageViewPackage.java +51 -0
  32. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageViewWithUrl.java +352 -126
  33. package/android/src/main/java/com/{dylanvann/fastimage → fastimage}/FastImageWebGlideUrl.java +1 -1
  34. package/app.plugin.js +2 -7
  35. package/dist/index.cjs.js +276 -157
  36. package/dist/index.cjs.js.flow +33 -17
  37. package/dist/index.cjs.web.js +211 -61
  38. package/dist/index.cjs.web.js.flow +33 -17
  39. package/dist/index.d.ts +83 -58
  40. package/dist/index.js +278 -159
  41. package/dist/index.js.flow +33 -17
  42. package/dist/index.web.js +212 -62
  43. package/dist/index.web.js.flow +33 -17
  44. package/dist/objectFit.d.ts +2 -2
  45. package/dist/objectPosition.d.ts +42 -0
  46. package/dist/specs/FastImageViewNativeComponent.d.ts +48 -0
  47. package/dist/specs/NativeFastImageModule.d.ts +11 -0
  48. package/dist/warnings.d.ts +3 -0
  49. package/ios/FastImage/FFFDownsampledImage.h +4 -8
  50. package/ios/FastImage/FFFDownsampledImage.m +0 -34
  51. package/ios/FastImage/FFFastImageModule.h +8 -0
  52. package/ios/FastImage/{FFFastImageViewManager.m → FFFastImageModule.mm} +70 -59
  53. package/ios/FastImage/FFFastImageView.h +36 -7
  54. package/ios/FastImage/FFFastImageView.m +201 -164
  55. package/ios/FastImage/FFFastImageViewComponentView.h +11 -0
  56. package/ios/FastImage/FFFastImageViewComponentView.mm +325 -0
  57. package/ios/FastImage/RCTConvert+FFFastImage.h +1 -1
  58. package/ios/FastImage/RCTConvert+FFFastImage.m +7 -10
  59. package/package.json +28 -9
  60. package/src/constants.ts +28 -0
  61. package/src/index.js.flow +179 -0
  62. package/src/index.tsx +1103 -0
  63. package/src/index.web.tsx +548 -0
  64. package/src/objectFit.ts +48 -0
  65. package/src/objectPosition.ts +243 -0
  66. package/src/specs/FastImageViewNativeComponent.ts +83 -0
  67. package/src/specs/NativeFastImageModule.ts +35 -0
  68. package/src/warnings.ts +37 -0
  69. package/android/src/main/AndroidManifestNew.xml +0 -3
  70. package/android/src/main/java/com/dylanvann/fastimage/FastImageCookieHandler.java +0 -63
  71. package/android/src/main/java/com/dylanvann/fastimage/FastImageEvents.java +0 -49
  72. package/android/src/main/java/com/dylanvann/fastimage/FastImageProgressListener.java +0 -18
  73. package/android/src/main/java/com/dylanvann/fastimage/FastImageShadowNode.java +0 -21
  74. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +0 -379
  75. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewPackage.java +0 -27
  76. package/ios/FastImage/FFFastImageViewManager.h +0 -5
@@ -0,0 +1,548 @@
1
+ // FastImage on the web (react-native-web): a View with an <img> in it, with
2
+ // FastImage's props and events. Bundlers that resolve `.web` files, or the
3
+ // package's `browser` field, pick this file instead of index.tsx, which uses
4
+ // the native views.
5
+ //
6
+ // The View (a div) has FastImage's style, ref and View props. The <img> fills
7
+ // it inside its borders, with object-fit for objectFit (see Picture).
8
+ // defaultSource is a second <img> under it until the image has loaded (the
9
+ // browser draws the image over it as it arrives), and tintColor is an SVG
10
+ // filter next to them.
11
+ //
12
+ // Works: source (uri, require(), or several sizes: see sizedSources),
13
+ // defaultSource, objectFit, objectPosition, tintColor, blurRadius, style,
14
+ // onLoadStart, onLoad, onError, onLoadEnd, and View props (testID,
15
+ // accessibility, onLayout, pointerEvents). The native-only props (cache,
16
+ // priority, headers, transition, downsample, loop, paused, imageRendering,
17
+ // recyclingKey) and onProgress are ignored.
18
+ import React, {
19
+ forwardRef,
20
+ memo,
21
+ useEffect,
22
+ useLayoutEffect,
23
+ useRef,
24
+ useState,
25
+ } from 'react'
26
+ import { Image, PixelRatio, StyleSheet, View } from 'react-native'
27
+ // @ts-expect-error react-native-web has no type declarations.
28
+ import { getAssetByID } from 'react-native-web/dist/modules/AssetRegistry'
29
+ import { cacheControl, priority, resizeMode } from './constants'
30
+ import { resolveObjectFit } from './objectFit'
31
+ import { cssPosition, objectPositionOrCentered } from './objectPosition'
32
+ import { warnIfChildren } from './warnings'
33
+ import type {
34
+ CachePathResult,
35
+ CacheState,
36
+ FastImageBackgroundProps,
37
+ FastImageProps,
38
+ FastImageStaticProperties,
39
+ ObjectFit,
40
+ PreloadResult,
41
+ Source,
42
+ } from './index'
43
+
44
+ export type * from './index'
45
+
46
+ // The native versions never reject: these resolve with an error.
47
+ const notSupported: CachePathResult = {
48
+ ok: false,
49
+ error: 'Not supported on the web',
50
+ }
51
+
52
+ // An id that's the same on the server and in the browser: React's useId,
53
+ // from React 18 (this package's React types are older), without the
54
+ // characters an SVG id can't have (:r0: in React 18, «r0» in React 19). On
55
+ // React 17, a counter's (which can differ from the server's).
56
+ let ids = 0
57
+ const useCounterId = () => useState(() => `${++ids}`)[0]
58
+ const useSvgId: () => string =
59
+ (React as unknown as { useId?: () => string }).useId ?? useCounterId
60
+
61
+ // A layout effect in the browser (it runs before the image can show), and an
62
+ // effect when rendering on a server, where layout effects warn.
63
+ const useClientLayoutEffect =
64
+ (globalThis as { document?: unknown }).document === undefined
65
+ ? useEffect
66
+ : useLayoutEffect
67
+
68
+ // Metro's (and React Native's Jest preset's) development flag; other web
69
+ // bundlers may not define it.
70
+ declare const __DEV__: boolean | undefined
71
+
72
+ function FastImageBase({
73
+ source,
74
+ defaultSource,
75
+ tintColor,
76
+ blurRadius,
77
+ onLoadStart,
78
+ onLoad,
79
+ onError,
80
+ onLoadEnd,
81
+ style,
82
+ children,
83
+ objectFit,
84
+ objectPosition,
85
+ forwardedRef,
86
+ // On the wrapper, the view the layout and touches are for here (on
87
+ // native, FastImage's only view).
88
+ onLayout,
89
+ pointerEvents,
90
+ // Native only.
91
+ recyclingKey: _recyclingKey,
92
+ loop: _loop,
93
+ imageRendering: _imageRendering,
94
+ paused: _paused,
95
+ transition: _transition,
96
+ downsample: _downsample,
97
+ onProgress: _onProgress,
98
+ ...props
99
+ }: FastImageProps & { forwardedRef: React.Ref<any> }) {
100
+ // As on native (a single view there): children aren't rendered.
101
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
102
+ warnIfChildren(children)
103
+ }
104
+ // The label is the wrapper's aria-label, as react-native-web's Image gave
105
+ // it (its View warns that accessibilityLabel is deprecated), and the
106
+ // <img>'s alt text.
107
+ const {
108
+ accessibilityLabel,
109
+ 'aria-label': ariaLabel,
110
+ // Removed in 10.0 (objectFit instead): ignored.
111
+ resizeMode: _resizeMode,
112
+ ...viewProps
113
+ } = props as typeof props & { 'aria-label'?: string; resizeMode?: unknown }
114
+ const label = ariaLabel || accessibilityLabel || undefined
115
+ const several =
116
+ Array.isArray(source) && source.length > 1 ? source : undefined
117
+ const sized = several ? sizedSources(several) : undefined
118
+ const src = sized ? sized.src : resolveUri(source)
119
+ const srcSet = sized?.srcSet
120
+ // What the <img> loads. Another image gets a new <img>, so the previous
121
+ // one doesn't stay while it loads; with several sizes, the <img> stays,
122
+ // and the browser shows the previous image until the new one has loaded.
123
+ const key = srcSet ?? src
124
+ const defaultUri = resolveUri(defaultSource)
125
+ // Whether the image for `key` loaded, or failed.
126
+ const [result, setResult] = useState<{ key: string; ok: boolean }>()
127
+ const ok = result && result.key === key ? result.ok : undefined
128
+ // The same on the server and in the browser. React's ids have characters
129
+ // an SVG id can't (:r0: in React 18, «r0» in React 19).
130
+ const tintId = `fast-image-tint-${useSvgId().replace(/[^\w-]/g, '')}`
131
+ // objectFit (the prop, then style's).
132
+ const fit = resolveObjectFit(objectFit, style)
133
+ const position = cssPosition(objectPositionOrCentered(objectPosition))
134
+ const filter =
135
+ [
136
+ blurRadius ? `blur(${blurRadius}px)` : '',
137
+ tintColor ? `url(#${tintId})` : '',
138
+ ]
139
+ .filter(Boolean)
140
+ .join(' ') || undefined
141
+ // defaultSource shows until the image has loaded, and if it fails.
142
+ const showDefault = !!defaultUri && ok !== true
143
+
144
+ useClientLayoutEffect(() => {
145
+ if (key) onLoadStart?.()
146
+ // Once per image the browser is asked for.
147
+ // eslint-disable-next-line react-hooks/exhaustive-deps
148
+ }, [key])
149
+
150
+ return (
151
+ <View
152
+ {...viewProps}
153
+ {...({ 'aria-label': label } as {})}
154
+ style={[styles.container, style]}
155
+ onLayout={onLayout}
156
+ pointerEvents={pointerEvents}
157
+ ref={forwardedRef}
158
+ >
159
+ {defaultUri && showDefault ? (
160
+ <Picture
161
+ key={`default ${defaultUri}`}
162
+ src={defaultUri}
163
+ fit={fit}
164
+ position={position}
165
+ filter={filter}
166
+ // The image has the label, if there is one.
167
+ alt={key ? '' : (label ?? '')}
168
+ />
169
+ ) : null}
170
+ {key && src ? (
171
+ <Picture
172
+ key={several ? 'sizes' : key}
173
+ src={src}
174
+ srcSet={srcSet}
175
+ lazy={!!several}
176
+ fit={fit}
177
+ position={position}
178
+ filter={filter}
179
+ // Shown as it loads, over defaultSource, and not if it
180
+ // failed (no broken image icon or alt text).
181
+ hidden={ok === false}
182
+ alt={label ?? ''}
183
+ onLoad={(image) => {
184
+ const { width, height } = loadedSize(image, sized)
185
+ setResult({ key, ok: true })
186
+ onLoad?.({ nativeEvent: { width, height } })
187
+ onLoadEnd?.({ ok: true, width, height })
188
+ }}
189
+ onError={(uri) => {
190
+ const error = `Failed to load resource ${uri}`
191
+ setResult({ key, ok: false })
192
+ onError?.({ nativeEvent: { error } })
193
+ onLoadEnd?.({ ok: false, error })
194
+ }}
195
+ />
196
+ ) : null}
197
+ {tintColor ? <TintFilter id={tintId} color={tintColor} /> : null}
198
+ </View>
199
+ )
200
+ }
201
+
202
+ // A source's (or defaultSource's) uri. Expo's web builds make a require()d
203
+ // image a { uri, width, height } object (or a string). Other setups can make
204
+ // it a number in react-native-web's asset registry, resolved as its Image
205
+ // does: the file for the scale closest to the screen's.
206
+ function resolveUri(source: unknown): string | undefined {
207
+ let uri: string | undefined
208
+ if (typeof source === 'number') {
209
+ const asset: PackagerAsset | undefined = getAssetByID(source)
210
+ if (!asset) return undefined
211
+ const scale = asset.scales.reduce(
212
+ (best, scale) =>
213
+ Math.abs(scale - PixelRatio.get()) <
214
+ Math.abs(best - PixelRatio.get())
215
+ ? scale
216
+ : best,
217
+ asset.scales[0] ?? 1,
218
+ )
219
+ const suffix = scale !== 1 ? `@${scale}x` : ''
220
+ uri = `${asset.httpServerLocation}/${asset.name}${suffix}.${asset.type}`
221
+ } else if (typeof source === 'string') {
222
+ uri = source
223
+ } else if (Array.isArray(source)) {
224
+ // An array of one is that source.
225
+ return resolveUri(source[0])
226
+ } else if (source && typeof (source as Source).uri === 'string') {
227
+ uri = (source as Source).uri
228
+ }
229
+ // An SVG's markup in a data uri, escaped: a # in it would end the uri.
230
+ const svg = uri?.match(/^(data:image\/svg\+xml;utf8,)(.*)/)
231
+ return svg ? svg[1] + encodeURIComponent(svg[2]) : uri || undefined
232
+ }
233
+
234
+ // An image in react-native-web's asset registry.
235
+ type PackagerAsset = {
236
+ httpServerLocation: string
237
+ name: string
238
+ type: string
239
+ scales: number[]
240
+ }
241
+
242
+ // Several sizes of an image, as a srcset (each size's width, times its
243
+ // scale), from which the browser loads the one for the image's width in
244
+ // device pixels, usually the smallest at least as wide, as expo-image does.
245
+ // sizes="auto" has the browser use the width the image is laid out at; it
246
+ // only applies to lazy images, and browsers without it use the next value,
247
+ // 100vw (the viewport's width). src is the largest, for browsers without
248
+ // srcset.
249
+ function sizedSources(sources: Source[]) {
250
+ const widths = sources
251
+ .filter((source) => source?.uri && source.width)
252
+ .map((source) => ({
253
+ uri: source.uri as string,
254
+ width: Math.round((source.width ?? 0) * (source.scale ?? 1)),
255
+ }))
256
+ const largest = widths.reduce<(typeof widths)[number] | undefined>(
257
+ (best, source) => (!best || source.width > best.width ? source : best),
258
+ undefined,
259
+ )
260
+ return {
261
+ src: largest?.uri ?? sources.find((source) => source?.uri)?.uri,
262
+ srcSet:
263
+ widths.length > 0
264
+ ? widths
265
+ .map((source) => `${source.uri} ${source.width}w`)
266
+ .join(', ')
267
+ : undefined,
268
+ widths,
269
+ }
270
+ }
271
+
272
+ // An <img> that loaded. The DOM's types aren't in this package's TypeScript
273
+ // setup.
274
+ type LoadedImage = {
275
+ naturalWidth: number
276
+ naturalHeight: number
277
+ currentSrc: string
278
+ complete?: boolean
279
+ decode?: () => Promise<void>
280
+ }
281
+
282
+ // The size of the image that loaded, in pixels, as on native. With a srcset,
283
+ // the natural size is divided by the density the browser picked (the
284
+ // candidate's width over `sizes`): it's the picked candidate's size instead.
285
+ function loadedSize(
286
+ image: LoadedImage,
287
+ sized: ReturnType<typeof sizedSources> | undefined,
288
+ ) {
289
+ const picked = sized?.srcSet
290
+ ? sized.widths.find(
291
+ (source) =>
292
+ new URL(source.uri, image.currentSrc).href ===
293
+ image.currentSrc,
294
+ )
295
+ : undefined
296
+ return {
297
+ width: picked?.width ?? image.naturalWidth,
298
+ height: picked
299
+ ? Math.round(
300
+ (image.naturalHeight * picked.width) / image.naturalWidth,
301
+ )
302
+ : image.naturalHeight,
303
+ }
304
+ }
305
+
306
+ const fill = {
307
+ position: 'absolute',
308
+ top: 0,
309
+ left: 0,
310
+ width: '100%',
311
+ height: '100%',
312
+ } as const
313
+
314
+ // An image filling the view: an <img> with object-fit and object-position
315
+ // (objectFit's values are CSS's).
316
+ function Picture({
317
+ src,
318
+ srcSet,
319
+ lazy,
320
+ fit,
321
+ position,
322
+ filter,
323
+ hidden,
324
+ alt,
325
+ onLoad,
326
+ onError,
327
+ }: {
328
+ src: string
329
+ srcSet?: string
330
+ // Several sizes load lazily, for sizes="auto" (see sizedSources).
331
+ lazy?: boolean
332
+ fit: ObjectFit
333
+ // objectPosition, as CSS.
334
+ position: string
335
+ filter?: string
336
+ hidden?: boolean
337
+ alt: string
338
+ onLoad?: (image: LoadedImage) => void
339
+ onError?: (uri: string) => void
340
+ }) {
341
+ const image = useRef<HTMLImageElement>(null)
342
+ const mounted = useRef(true)
343
+ // The load that was handled: the file, for these sources. The image can
344
+ // have loaded when this mounts: from the memory cache (it also gets a load
345
+ // event), or before the page's JavaScript ran, in a page rendered on a
346
+ // server (it doesn't). The browser fires load for each new srcset, also
347
+ // when it keeps the same file, and can load another size later.
348
+ const handled = useRef<{ sources: string; file: string } | undefined>(
349
+ undefined,
350
+ )
351
+
352
+ const loaded = (target: LoadedImage) => {
353
+ const file = target.currentSrc || src
354
+ const sources = srcSet ?? src
355
+ if (
356
+ !mounted.current ||
357
+ (handled.current?.sources === sources &&
358
+ handled.current.file === file)
359
+ ) {
360
+ return
361
+ }
362
+ handled.current = { sources, file }
363
+ // Once it's decoded, as react-native-web's Image did, so it shows at
364
+ // once (e.g. in place of defaultSource). Safari can fail to decode an
365
+ // SVG, which still shows.
366
+ const done = () => {
367
+ if (mounted.current) onLoad?.(target)
368
+ }
369
+ Promise.resolve(target.decode?.()).then(done, done)
370
+ }
371
+
372
+ useEffect(() => {
373
+ mounted.current = true
374
+ return () => {
375
+ mounted.current = false
376
+ }
377
+ }, [])
378
+
379
+ // An image that loaded or failed before this mounted (in a page rendered
380
+ // on a server, before the page's JavaScript ran) had its event then. A
381
+ // broken image is complete with no size; so can an SVG without one be in
382
+ // some browsers, which decodes.
383
+ useClientLayoutEffect(() => {
384
+ const target = image.current as unknown as LoadedImage | null
385
+ if (!target?.complete) return
386
+ if (target.naturalWidth > 0) return loaded(target)
387
+ Promise.resolve(target.decode?.()).then(
388
+ () => loaded(target),
389
+ () => {
390
+ if (mounted.current) onError?.(target.currentSrc || src)
391
+ },
392
+ )
393
+ // When it mounts.
394
+ // eslint-disable-next-line react-hooks/exhaustive-deps
395
+ }, [])
396
+
397
+ return (
398
+ <img
399
+ ref={image}
400
+ src={src}
401
+ srcSet={srcSet}
402
+ sizes={srcSet ? 'auto, 100vw' : undefined}
403
+ loading={lazy ? 'lazy' : undefined}
404
+ alt={alt}
405
+ draggable={false}
406
+ style={{
407
+ ...fill,
408
+ objectFit: fit,
409
+ objectPosition: position,
410
+ filter,
411
+ opacity: hidden ? 0 : undefined,
412
+ }}
413
+ onLoad={(event) =>
414
+ loaded(event.currentTarget as unknown as LoadedImage)
415
+ }
416
+ onError={(event) => {
417
+ const target = event.currentTarget as unknown as
418
+ | LoadedImage
419
+ | undefined
420
+ if (mounted.current) onError?.(target?.currentSrc || src)
421
+ }}
422
+ />
423
+ )
424
+ }
425
+
426
+ // tintColor: an SVG filter that fills the image's pixels with the color,
427
+ // keeping their alpha.
428
+ function TintFilter({ id, color }: { id: string; color: unknown }) {
429
+ return (
430
+ <svg style={tintStyle}>
431
+ <defs>
432
+ <filter id={id}>
433
+ {/* A new element for a new color, which browsers apply. */}
434
+ <feFlood floodColor={String(color)} key={String(color)} />
435
+ <feComposite in2="SourceAlpha" operator="in" />
436
+ </filter>
437
+ </defs>
438
+ </svg>
439
+ )
440
+ }
441
+
442
+ const tintStyle = {
443
+ position: 'absolute',
444
+ width: 0,
445
+ height: 0,
446
+ visibility: 'hidden',
447
+ } as const
448
+
449
+ const FastImageMemo = memo(FastImageBase)
450
+
451
+ const FastImageComponent: React.ComponentType<FastImageProps> = forwardRef(
452
+ (props: FastImageProps, ref: React.Ref<any>) => (
453
+ <FastImageMemo forwardedRef={ref} {...props} />
454
+ ),
455
+ )
456
+
457
+ FastImageComponent.displayName = 'FastImage'
458
+
459
+ // The image's size, or 0 by 0 if it can't be read.
460
+ function getSize(uri: string) {
461
+ return new Promise<{ width: number; height: number }>((resolve) => {
462
+ Image.getSize(
463
+ uri,
464
+ (width, height) => resolve({ width, height }),
465
+ () => resolve({ width: 0, height: 0 }),
466
+ )
467
+ })
468
+ }
469
+
470
+ const FastImage: React.ComponentType<FastImageProps> &
471
+ FastImageStaticProperties = FastImageComponent as any
472
+
473
+ FastImage.cacheControl = cacheControl
474
+
475
+ FastImage.priority = priority
476
+
477
+ FastImage.resizeMode = resizeMode
478
+
479
+ // Loads each source into the browser's cache, with a result per source.
480
+ FastImage.preload = (sources: Source[]) =>
481
+ Promise.all(
482
+ sources.map((source): Promise<PreloadResult> => {
483
+ const uri = source ? source.uri : undefined
484
+ if (typeof uri !== 'string' || !uri) {
485
+ return Promise.resolve({
486
+ ok: false,
487
+ error: 'Invalid source: no uri',
488
+ uri,
489
+ })
490
+ }
491
+ return Image.prefetch(uri).then(
492
+ () => getSize(uri).then((size) => ({ ok: true, uri, ...size })),
493
+ () => ({ ok: false, error: 'Failed to load the image', uri }),
494
+ )
495
+ }),
496
+ )
497
+
498
+ // The browser manages its own cache: these don't clear it.
499
+ FastImage.clearMemoryCache = () => Promise.resolve(notSupported)
500
+
501
+ FastImage.clearDiskCache = () => Promise.resolve(notSupported)
502
+
503
+ FastImage.configureCache = (): Promise<CacheState> => Promise.resolve({})
504
+
505
+ // transition is native only.
506
+ FastImage.setDefaults = () => {}
507
+
508
+ FastImage.writeToCache = () => Promise.resolve(notSupported)
509
+
510
+ FastImage.getCachePath = () => Promise.resolve(notSupported)
511
+
512
+ const styles = StyleSheet.create({
513
+ container: {
514
+ overflow: 'hidden',
515
+ },
516
+ })
517
+
518
+ // FastImage forwards its ref, which its type doesn't say.
519
+ const FastImageWithRef = FastImageComponent as React.ComponentType<
520
+ FastImageProps & { ref?: React.Ref<any> }
521
+ >
522
+
523
+ export const FastImageBackground: React.ComponentType<FastImageBackgroundProps> =
524
+ forwardRef(
525
+ (
526
+ {
527
+ style,
528
+ imageStyle,
529
+ imageRef,
530
+ children,
531
+ ...props
532
+ }: FastImageBackgroundProps,
533
+ ref: React.Ref<any>,
534
+ ) => (
535
+ <View style={style} ref={ref}>
536
+ <FastImageWithRef
537
+ {...props}
538
+ style={[StyleSheet.absoluteFill, imageStyle]}
539
+ ref={imageRef}
540
+ />
541
+ {children}
542
+ </View>
543
+ ),
544
+ )
545
+
546
+ FastImageBackground.displayName = 'FastImageBackground'
547
+
548
+ export default FastImage
@@ -0,0 +1,48 @@
1
+ // objectFit, from the prop or the style, shared by the native (index.tsx) and
2
+ // web (index.web.tsx) versions.
3
+ import { StyleSheet } from 'react-native'
4
+ import type { ImageStyle, ObjectFit } from './index'
5
+
6
+ // Finds a key (tintColor, objectFit) in a style prop, where the last style
7
+ // that sets it wins, as with StyleSheet.flatten, but without flattening (which
8
+ // allocates a merged object for an array style on every render).
9
+ export function fromStyle<K extends 'tintColor' | 'objectFit'>(
10
+ style: unknown,
11
+ key: K,
12
+ ): ImageStyle[K] {
13
+ if (Array.isArray(style)) {
14
+ for (let i = style.length - 1; i >= 0; i--) {
15
+ const found = fromStyle(style[i], key)
16
+ if (found !== undefined) return found
17
+ }
18
+ return undefined
19
+ }
20
+ if (typeof style === 'number') {
21
+ // A registered style from StyleSheet.create on older React Native.
22
+ const flattened = StyleSheet.flatten(style as any) as
23
+ | ImageStyle
24
+ | undefined
25
+ return flattened ? flattened[key] : undefined
26
+ }
27
+ return style && typeof style === 'object'
28
+ ? (style as ImageStyle)[key]
29
+ : undefined
30
+ }
31
+
32
+ const FITS: Record<ObjectFit, true> = {
33
+ fill: true,
34
+ contain: true,
35
+ cover: true,
36
+ none: true,
37
+ 'scale-down': true,
38
+ }
39
+
40
+ // The objectFit prop, then style's, then cover (null, or a value that isn't
41
+ // one of objectFit's, counts as not set).
42
+ export function resolveObjectFit(
43
+ objectFit: ObjectFit | null | undefined,
44
+ style: unknown,
45
+ ): ObjectFit {
46
+ const fit = objectFit != null ? objectFit : fromStyle(style, 'objectFit')
47
+ return fit && FITS[fit] === true ? fit : 'cover'
48
+ }