UNPKG

@mnightingale/react-native-image-cache-hoc

Version:

React Native Higher Order Component that adds advanced caching functionality to the react native Image component.

786 lines (665 loc) 31.2 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _extends = require('@babel/runtime/helpers/extends'); var React = require('react'); var reactNative = require('react-native'); var pathLib = require('path'); var RNFS = require('react-native-fs'); var sha1 = require('crypto-js/sha1'); var URL = require('url-parse'); var rxjs = require('rxjs'); var operators = require('rxjs/operators'); var uuid = require('react-native-uuid'); var traverse = require('traverse'); var validator = require('validator'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var _extends__default = /*#__PURE__*/_interopDefaultLegacy(_extends); var React__default = /*#__PURE__*/_interopDefaultLegacy(React); var pathLib__default = /*#__PURE__*/_interopDefaultLegacy(pathLib); var RNFS__default = /*#__PURE__*/_interopDefaultLegacy(RNFS); var sha1__default = /*#__PURE__*/_interopDefaultLegacy(sha1); var URL__default = /*#__PURE__*/_interopDefaultLegacy(URL); var uuid__default = /*#__PURE__*/_interopDefaultLegacy(uuid); var traverse__default = /*#__PURE__*/_interopDefaultLegacy(traverse); var validator__default = /*#__PURE__*/_interopDefaultLegacy(validator); /** * * This library acts as an interface to abstract the OS file system and is platform independent. * * File paths passed into this library should be relative, as the base file path is different * across platforms, and therefore is automatically set by this library. * */ /** * Resolves if 'unlink' resolves or if the file doesn't exist. * * @param {string} filename */ const RNFSUnlinkIfExists = filename => RNFS__default['default'].exists(filename).then(exists => { if (exists) { return RNFS__default['default'].unlink(filename); } return Promise.resolve(); }); class FileSystem { /** * All FileSystem instances will reference the cacheLock singleton "dictionary" to provide cache file locking in order to prevent concurrency race condition bugs. * * cacheLock structure: * * cacheLock = { * 'filename.jpg': { * 'componentIdOne': true, * 'componentIdTwo': true * } * } * * In the above example cache/filename.jpg cannot be deleted during cache pruning, * until both components release their locks on the dependant image file. * * One example (of many) of how a race condition could occur: * * 1st <CacheableImage> mounts and downloads image file into cache, calls render() (or not) etc. * 2nd <CacheableImage> mounts concurrently and during download runs pruneCache(), deleting 1st <CacheableImage>'s image file from cache. * 1st <CacheableImage> calls render() again at some point in the future, at this time it's image file no longer exists so the render fails. */ static lockCacheFile(fileName, componentId) { // If file is already locked, add additional component lock, else create initial file lock. if (FileSystem.cacheLock[fileName]) { FileSystem.cacheLock[fileName][componentId] = true; } else { const componentDict = {}; componentDict[componentId] = true; FileSystem.cacheLock[fileName] = componentDict; } } static unlockCacheFile(fileName, componentId) { // Delete component lock on cache file if (FileSystem.cacheLock[fileName]) { delete FileSystem.cacheLock[fileName][componentId]; // If no further component locks remain on cache file, delete filename property from cacheLock dictionary. if (Object.keys(FileSystem.cacheLock[fileName]).length === 0) { delete FileSystem.cacheLock[fileName]; if (FileSystem.cacheObservables[fileName]) { delete FileSystem.cacheObservables[fileName]; } } } } constructor(cachePruneTriggerLimit, fileDirName) { this.baseFilePath = void 0; this.cachePruneTriggerLimit = void 0; this.cachePruneTriggerLimit = cachePruneTriggerLimit || 1024 * 1024 * 15; // Maximum size of image file cache in bytes before pruning occurs. Defaults to 15 MB. fileDirName = fileDirName || 'react-native-image-cache-hoc'; // Namespace local file writing to this folder. this.baseFilePath = this._setBaseFilePath(fileDirName); } /** * * Sets the base file directory depending on platform. Prefix is used to avoid local file collisions. * * Apple: * Apple requires non-user generated files to be stored in cache dir, they can be flagged to persist. https://developer.apple.com/icloud/documentation/data-storage/index.html * it appears reactNativeFetchBlob will flag these files to persist behind the scenes, so cache dir is safe on apple. See: https://www.npmjs.com/package/rn-fetch-blob#cache-file-management * * Android: * Android appears to purge cache dir files so we should use document dir to play it safe (even with reactNativeFetchBlob abstraction) : https://developer.android.com/guide/topics/data/data-storage.html * * @returns {String} baseFilePath - base path that files are written to in local fs. * @private */ _setBaseFilePath(fileDirName) { let baseFilePath = reactNative.Platform.OS === 'ios' ? RNFS__default['default'].CachesDirectoryPath : RNFS__default['default'].DocumentDirectoryPath; baseFilePath += '/' + fileDirName + '/'; return baseFilePath; } /** * * Pseudo-chroot paths to the baseFilePath set in _setBaseFilePath(). * * This method should be called on any passed in path to prevent writing to invalid directories. * IE passing in a path of '../../../../../home/should/not/write/here/file.png' to break out of * the base file path directory. * * @param path {String} - local fs path. * @param absolute {String} - whether or not the passed in path is absolute or relative. Defaults to relative since base path differs across platforms. * @returns {boolean} - Whether or not the file path is valid. * @throws error on bad filepath. * @private */ _validatePath(path, absolute = false) { const relative = pathLib__default['default'].relative(this.baseFilePath, absolute ? path : this.baseFilePath + path); if (path !== '' && (!relative || relative.startsWith('..') || pathLib__default['default'].isAbsolute(relative))) { // resolve turns any path into an absolute path (ie: /folder1/folder2/../example.js resolves to /folder1/example.js) const resolvedPath = absolute ? pathLib__default['default'].resolve(path) : pathLib__default['default'].resolve(this.baseFilePath + path); throw new Error(resolvedPath + ' is not a valid file path.'); } else { return true; } } /** * * Wrapper for https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#existspathstringpromise * * @param path - local relative file path. * @returns {Promise} - boolean promise for if file exists at path or not. */ exists(path) { this._validatePath(path); return RNFS__default['default'].exists(pathLib__default['default'].resolve(this.baseFilePath + path)); } /** * * Creates a SHA1 hash filename from a url and normalizes extension. * * @param url {String} - An absolute url. * @throws error on invalid (non jpg, png, gif, bmp) url file type. NOTE file extension or content-type header does not guarantee file mime type. We are trusting that it is set correctly on the server side. * @returns fileName {string} - A SHA1 filename that is unique to the resource located at passed in URL and includes an appropriate extension. */ getFileNameFromUrl(url) { const urlParts = new URL__default['default'](url); const urlExt = urlParts.pathname.split('.').pop(); // react-native enforces Image src to default to a file extension of png const extension = urlExt === urlParts.pathname ? 'bin' : urlExt; return sha1__default['default'](url).toString() + '.' + extension; } /** * * Convenience method used to get the associated local file path of a web image that has been written to disk. * If the local file does not exist yet, the remote file is downloaded to local disk then the local filepath is returned. * * @param url {String} - url of file to download. * @returns {Promise<string|null>} promise that resolves to the local file path of downloaded url file. */ async getLocalFilePathFromUrl(url) { const fileName = this.getFileNameFromUrl(url); const requestId = uuid__default['default'].v4(); try { FileSystem.lockCacheFile(fileName, requestId); const { path } = await this.observable(url, requestId, 'immutable', fileName).pipe(operators.take(1)).toPromise(); return path; } finally { FileSystem.unlockCacheFile(fileName, requestId); } } /** * * Manually move or copy a local file to the cache. * Can be used to pre-warm caches. * If calling this method repeatedly to cache a long list of files, * be sure to use a queue and limit concurrency so your app performance does not suffer. * * @param local {String} - path to the local file. * @param url {String} - url of file to download. * @param move {Boolean} - whether the file should be copied or moved. * @param mtime {Date} - creation timestamp * @param ctime {Date} - modification timestamp (iOS only) * @returns {Promise} promise that resolves to an object that contains cached file info. */ async cacheLocalFile(local, url, move = false, mtime, ctime) { const fileName = this.getFileNameFromUrl(url); const path = this.baseFilePath + fileName; this._validatePath(path, true); if (!(await RNFS__default['default'].exists(local))) { return { url, path: null }; } // Logic here prunes cache directory on "cache" writes to ensure cache doesn't get too large. await this.pruneCache(); // Move or copy the file to the cache try { const cacheDirExists = await this.exists(''); if (!cacheDirExists) { await RNFS__default['default'].mkdir(this.baseFilePath); } await RNFSUnlinkIfExists(path); await (move ? RNFS__default['default'].moveFile(local, path) : RNFS__default['default'].copyFile(local, path)); // Update the modified and created times of the file otherwise the if-modified-since request will probably always await RNFS__default['default'].touch(path, mtime !== null && mtime !== void 0 ? mtime : new Date(), ctime !== null && ctime !== void 0 ? ctime : new Date()); } catch (error) { await RNFSUnlinkIfExists(path); return { url, path: null }; } // Publish to subscribers that the image for this url has been updated if (FileSystem.cacheObservables[fileName]) { FileSystem.cacheObservables[fileName].next({ path: 'file://' + path, fileName, md5: await RNFS__default['default'].hash(path, 'md5').catch(() => undefined) }); } return { url, path }; } /** * * Used to download files to local filesystem. * * @param url {String} - url of file to download. * @param fileName {String} - defaults to a sha1 hash of the url param with extension of same filetype. * @returns {Observable<CacheFileInfo>} observable that resolves to an object that contains the local path of the downloaded file and the filename. */ fetchFile(url, fileName = null, headers) { fileName = fileName || this.getFileNameFromUrl(url); const path = this.baseFilePath + fileName; this._validatePath(path, true); return rxjs.from(this.exists('')).pipe(operators.delayWhen(cacheDirExists => // Logic here prunes cache directory on "cache" writes to ensure cache doesn't get too large. rxjs.from(cacheDirExists ? this.pruneCache() : RNFS__default['default'].mkdir(this.baseFilePath))), operators.mergeMap(() => rxjs.from(RNFS__default['default'].exists(path))), operators.mergeMap(alreadyExists => // Hit network and download file to local disk. rxjs.defer(async () => { let beginResult; const downloadResult = await RNFS__default['default'].downloadFile({ fromUrl: url, toFile: path, headers, begin: res => { beginResult = res; } }).promise; return Promise.resolve({ downloadResult, beginResult }); }).pipe( // If "304 Not Modified" response touch local file operators.tap(({ downloadResult }) => { if (alreadyExists && downloadResult.statusCode === 304) { // Unfortunately react-native-fs only shares headers for status >=200 && status < 300 RNFS__default['default'].touch(path, new Date(), new Date()); } }), // Only need to emit if the local file has changed operators.filter(({ downloadResult }) => downloadResult.statusCode === 200), operators.mergeMap(({ downloadResult, beginResult }) => rxjs.defer(async () => { if (beginResult !== undefined && 'ETag' in beginResult.headers) { return { md5: beginResult.headers['ETag'] }; } return { md5: await RNFS__default['default'].hash(path, 'md5').catch(() => undefined) }; }).pipe(operators.map(extra => ({ downloadResult, beginResult, extra })))), operators.map(({ extra }) => { return { path: 'file://' + path, fileName: pathLib__default['default'].basename(path), md5: extra.md5 }; }))), operators.catchError(() => rxjs.from(RNFSUnlinkIfExists(path)).pipe(operators.mapTo({ path: null, fileName: pathLib__default['default'].basename(path) })))); } /** * Used to remove files from cache directory if the cache grows too large. * This function will delete files from the cache until the total cache size * is less than FileSystem.cachePruneTriggerLimit setting. * * @returns {Promise} */ async pruneCache() { // If cache directory does not exist yet there's no need for pruning. if (!(await this.exists(''))) { return; } // Get directory contents const dirContents = await RNFS__default['default'].readDir(this.baseFilePath); // Sort dirContents in order of oldest to newest file. dirContents.sort((a, b) => { var _a$mtime$getTime, _a$mtime, _b$mtime$getTime, _b$mtime; return ((_a$mtime$getTime = (_a$mtime = a.mtime) === null || _a$mtime === void 0 ? void 0 : _a$mtime.getTime()) !== null && _a$mtime$getTime !== void 0 ? _a$mtime$getTime : 0) - ((_b$mtime$getTime = (_b$mtime = b.mtime) === null || _b$mtime === void 0 ? void 0 : _b$mtime.getTime()) !== null && _b$mtime$getTime !== void 0 ? _b$mtime$getTime : 0); }); const currentCacheSize = dirContents.reduce((cacheSize, blobStatObject) => { return cacheSize + parseInt(blobStatObject.size); }, 0); // Prune cache if current cache size is too big. if (currentCacheSize > this.cachePruneTriggerLimit) { let overflowSize = currentCacheSize - this.cachePruneTriggerLimit; const unlinkPromises = []; // Keep deleting cached files so long as the current cache size is larger than the size required to trigger cache pruning, or until // all cache files have been evaluated. while (overflowSize > 0 && dirContents.length) { const contentFile = dirContents.shift(); // Only prune unlocked files from cache if (contentFile && !FileSystem.cacheLock[contentFile.name] && this._validatePath(contentFile.name)) { overflowSize -= parseInt(contentFile.size); unlinkPromises.push(RNFSUnlinkIfExists(this.baseFilePath + contentFile.name)); } } await Promise.all(unlinkPromises); } } /** * Used to delete local files and directories * * @param path - local relative file path. * @returns {Promise} - boolean promise for if deletion was successful. */ async unlink(path) { this._validatePath(path); try { await RNFSUnlinkIfExists(pathLib__default['default'].resolve(this.baseFilePath + path)); const obs$ = FileSystem.cacheObservables[path]; if (obs$) { obs$.next({ path: null, fileName: path }); } return true; } catch (error) { return false; } } /** * Gets a observable which emits when a url is resolved to a local file path * A cache lock is required @see {lockCacheFile} * * @param url {String} - url of file to download. * @param componentId {String} - Unique id of the requestor. * @param cacheStrategy {CacheStrategy} - The cache strategy to use, defaults to 'immutable'. * @param fileName {String} - defaults to a sha1 hash of the url param with extension of same filetype. * @param headers {HeaderFn} - Headers to include with requests * @returns {Observable<CacheFileInfo>} observable that resolves to an object that contains the local path of the downloaded file and the filename. */ observable(url, componentId, cacheStrategy = 'immutable', fileName = null, headers = {}) { if (!url) { return rxjs.of({ path: null, fileName: '' }); } // Check for invalid cache strategies if (cacheStrategy !== 'immutable' && cacheStrategy !== 'mutable') { throw new Error(`Invalid CacheStrategy ${cacheStrategy} is unhandled`); } fileName = fileName || this.getFileNameFromUrl(url); if (!FileSystem.cacheLock[fileName] || !FileSystem.cacheLock[fileName][componentId]) { throw new Error('A lock must be aquired before requesting an observable'); } if (!FileSystem.cacheObservables[fileName]) { this._validatePath(fileName); const subject$ = new rxjs.ReplaySubject(); const obs$ = rxjs.from(RNFS__default['default'].hash(this.baseFilePath + fileName, 'md5')).pipe(operators.catchError(() => rxjs.of(null)), operators.switchMap(md5 => { if (md5 !== null) { switch (cacheStrategy) { case 'immutable': { return rxjs.of({ path: 'file://' + this.baseFilePath + fileName, fileName, md5 }); } case 'mutable': { return rxjs.from([rxjs.of({ path: 'file://' + this.baseFilePath + fileName, fileName, md5 }), rxjs.defer(async () => headers instanceof Function ? await headers() : headers).pipe(operators.mergeMap(headers => this.fetchFile(url, fileName, { 'if-none-match': md5, ...headers })))]).pipe(operators.concatAll()); } } } // Download return rxjs.defer(async () => headers instanceof Function ? await headers() : headers).pipe(operators.mergeMap(headers => this.fetchFile(url, fileName, headers))); }), operators.publishReplay(1), operators.refCount()); // Subscribe obs$.subscribe(v => subject$.next(v)); return FileSystem.cacheObservables[fileName] = subject$; } return FileSystem.cacheObservables[fileName]; } } /** * Export FileSystem factory for convenience. * * @returns {FileSystem} */ FileSystem.cacheLock = {}; FileSystem.cacheObservables = {}; function FileSystemFactory(cachePruneTriggerLimit, fileDirName) { return new FileSystem(cachePruneTriggerLimit || null, fileDirName || null); } const imageCacheHoc = (Wrapped, options = {}) => { var _temp; // Validate options if (options.validProtocols && !Array.isArray(options.validProtocols)) { throw new Error('validProtocols option must be an array of protocol strings.'); } if (options.fileHostWhitelist && !Array.isArray(options.fileHostWhitelist)) { throw new Error('fileHostWhitelist option must be an array of host strings.'); } if (options.cachePruneTriggerLimit && !Number.isInteger(options.cachePruneTriggerLimit)) { throw new Error('cachePruneTriggerLimit option must be an integer.'); } if (options.fileDirName && typeof options.fileDirName !== 'string') { throw new Error('fileDirName option must be string'); } if (options.defaultPlaceholder && typeof options.defaultPlaceholder !== 'object') { throw new Error('defaultPlaceholder option must be a ReactNode'); } if (options.headers && !(typeof options.headers === 'function' || typeof options.headers === 'object')) { throw new Error('headers option must be a function or object'); } return _temp = class extends React__default['default'].PureComponent { /** * * Manually cache a file. * Can be used to pre-warm caches. * If calling this method repeatedly to cache a long list of files, * be sure to use a queue and limit concurrency so your app performance does not suffer. * * @param url {String} - url of file to download. * @returns {Promise} promise that resolves to an object that contains cached file info. */ static async cacheFile(url) { const localFilePath = await this.fileSystem().getLocalFilePathFromUrl(url); return { url: url, localFilePath }; } /** * * Manually move or copy a local file to the cache. * Can be used to pre-warm caches. * If calling this method repeatedly to cache a long list of files, * be sure to use a queue and limit concurrency so your app performance does not suffer. * * @param local {String} - path to the local file. * @param url {String} - url of file to download. * @param move {Boolean} - whether the file should be copied or moved. * @param mtime {Date} - creation timestamp * @param ctime {Date} - modification timestamp (iOS only) * @returns {Promise} promise that resolves to an object that contains cached file info. */ static async cacheLocalFile(local, url, move = false, mtime, ctime) { return this.fileSystem().cacheLocalFile(local, url, move, mtime, ctime); } /** * * Delete all locally stored image files created by react-native-image-cache-hoc. * Calling this method will cause a performance hit on your app until the local files are rebuilt. * * @returns {Promise} promise that resolves to an object that contains the flush results. */ static async flush() { return this.fileSystem().unlink(''); } /** * Export FileSystem for convenience. * * @returns {FileSystem} */ static fileSystem() { return FileSystemFactory(options.cachePruneTriggerLimit || null, options.fileDirName || null); } constructor(props) { var _options$headers; super(props); // Set initial state this.componentId = void 0; this.unmounted$ = void 0; this.options = void 0; this.fileSystem = void 0; this.subscription = void 0; this.invalidUrl = void 0; this.state = { source: undefined }; // Assign component unique ID for cache locking. this.componentId = uuid__default['default'].v4(); // Track component mount status to avoid calling setState() on unmounted component. this.unmounted$ = new rxjs.BehaviorSubject(true); // Set default options this.options = { validProtocols: options.validProtocols || ['https'], fileHostWhitelist: options.fileHostWhitelist || [], cachePruneTriggerLimit: options.cachePruneTriggerLimit || 1024 * 1024 * 15, // Maximum size of image file cache in bytes before pruning occurs. Defaults to 15 MB. fileDirName: options.fileDirName || null, // Namespace local file writing to this directory. Defaults to 'react-native-image-cache-hoc'. defaultPlaceholder: options.defaultPlaceholder || null, // Default placeholder component to render while remote image file is downloading. Can be overridden with placeholder prop. Defaults to <Image> component with style prop passed through. headers: (_options$headers = options.headers) !== null && _options$headers !== void 0 ? _options$headers : {} }; // Init file system lib this.fileSystem = FileSystemFactory(this.options.cachePruneTriggerLimit, this.options.fileDirName); // Validate input this.invalidUrl = !this._validateImageComponent(); } _validateImageComponent() { // Define validator options const validatorUrlOptions = { protocols: this.options.validProtocols, // eslint-disable-next-line @typescript-eslint/camelcase require_protocol: true }; if (this.options.fileHostWhitelist.length) { // eslint-disable-next-line @typescript-eslint/camelcase validatorUrlOptions.host_whitelist = this.options.fileHostWhitelist; } // Validate source prop to be a valid web accessible url. if (!traverse__default['default'](this.props).get(['source', 'uri']) || !validator__default['default'].isURL(traverse__default['default'](this.props).get(['source', 'uri']), validatorUrlOptions)) { console.warn('Invalid source prop. <CacheableImage> props.source.uri should be a web accessible url with a valid protocol and host. NOTE: Default valid protocol is https, default valid hosts are *.'); return false; } else { return true; } } // Async calls to local FS or network should occur here. // See: https://reactjs.org/docs/react-component.html#componentdidmount componentDidMount() { // Track component mount status to avoid calling setState() on unmounted component. this.unmounted$.next(false); // Set url from source prop const url = traverse__default['default'](this.props).get(['source', 'uri']); const cacheStrategy = traverse__default['default'](this.props).get(['source', 'cache']) || 'immutable'; const isFile = url && new URL__default['default'](url).protocol === 'file:'; if (isFile || !this.invalidUrl) { if (isFile) { this.onSourceLoaded({ path: url, fileName: this.fileSystem.getFileNameFromUrl(url) }); } else { // Add a cache lock to file with this name (prevents concurrent <CacheableImage> components from pruning a file with this name from cache). const fileName = this.fileSystem.getFileNameFromUrl(url); FileSystem.lockCacheFile(fileName, this.componentId); // Init the image cache logic this.subscription = this.fileSystem.observable(url, this.componentId, cacheStrategy, null, this.options.headers).pipe(operators.takeUntil(this.unmounted$.pipe(operators.skip(1)))).subscribe(info => this.onSourceLoaded(info)); } } } /** * * Enables caching logic to work if component source prop is updated (that is, the image url changes without mounting a new component). * See: https://github.com/billmalarky/react-native-image-cache-hoc/pull/15 * * @param prevProps {Object} - Previous props. */ componentDidUpdate(prevProps) { var _this$subscription; // Set urls from source prop data const url = traverse__default['default'](prevProps).get(['source', 'uri']); const nextUrl = traverse__default['default'](this.props).get(['source', 'uri']); const isFile = nextUrl && new URL__default['default'](nextUrl).protocol === 'file:'; // Do nothing if url has not changed. if (url === nextUrl) return; // Remove component cache lock on old image file, and add cache lock to new image file. const fileName = this.fileSystem.getFileNameFromUrl(url); const cacheStrategy = traverse__default['default'](this.props).get(['source', 'cache']) || 'immutable'; FileSystem.unlockCacheFile(fileName, this.componentId); (_this$subscription = this.subscription) === null || _this$subscription === void 0 ? void 0 : _this$subscription.unsubscribe(); this.invalidUrl = !this._validateImageComponent(); // Init the image cache logic if (isFile || !this.invalidUrl) { if (isFile) { this.onSourceLoaded({ path: nextUrl, fileName: this.fileSystem.getFileNameFromUrl(nextUrl) }); } else { // Add a cache lock to file with this name (prevents concurrent <CacheableImage> components from pruning a file with this name from cache). const nextFileName = this.fileSystem.getFileNameFromUrl(nextUrl); FileSystem.lockCacheFile(nextFileName, this.componentId); this.subscription = this.fileSystem.observable(nextUrl, this.componentId, cacheStrategy, null, this.options.headers).pipe(operators.takeUntil(this.unmounted$.pipe(operators.skip(1)))).subscribe(info => this.onSourceLoaded(info)); } } else { this.setState({ source: undefined }); } } componentWillUnmount() { // Track component mount status to avoid calling setState() on unmounted component. this.unmounted$.next(true); // Remove component cache lock on associated image file on component teardown. const fileName = this.fileSystem.getFileNameFromUrl(traverse__default['default'](this.props).get(['source', 'uri'])); FileSystem.unlockCacheFile(fileName, this.componentId); } onSourceLoaded({ path, md5 }) { this.setState({ source: path ? { uri: path + (md5 !== undefined ? '?' + md5 : '') } : undefined }); this.invalidUrl = path === null; if (path && this.props.onLoadFinished) { reactNative.Image.getSize(path, (width, height) => { if (!this.unmounted$.value && this.props.onLoadFinished) { this.props.onLoadFinished({ width, height }); } }); } } render() { // If media loaded, render full image component, else render placeholder. if (this.state.source) { // Android caches images in memory, if we are rendering the image should have changed locally so appending a timestamp to the path forces it to be loaded from disk // The internals of te Android behaviour have not been investigated but perhaps it would be beneficial to use the last modified date instead const props = { ...this.props, source: this.state.source }; return /*#__PURE__*/React__default['default'].createElement(Wrapped, _extends__default['default']({ key: this.componentId }, props)); } else { if (this.props.placeholder) { return this.props.placeholder; } else if (this.options.defaultPlaceholder) { return this.options.defaultPlaceholder; } else { // Extract props proprietary to this HOC before passing props through. const { source, ...filteredProps } = this.props; return /*#__PURE__*/React__default['default'].createElement(Wrapped, filteredProps); } } } }, _temp; }; exports.FileSystem = FileSystem; exports.FileSystemFactory = FileSystemFactory; exports.imageCacheHoc = imageCacheHoc;