diff --git a/patches/react-native-web+0.19.9+005+image-header-support.patch b/patches/react-native-web+0.19.9+005+image-header-support.patch deleted file mode 100644 index 4652e22662f0..000000000000 --- a/patches/react-native-web+0.19.9+005+image-header-support.patch +++ /dev/null @@ -1,200 +0,0 @@ -diff --git a/node_modules/react-native-web/dist/exports/Image/index.js b/node_modules/react-native-web/dist/exports/Image/index.js -index 95355d5..19109fc 100644 ---- a/node_modules/react-native-web/dist/exports/Image/index.js -+++ b/node_modules/react-native-web/dist/exports/Image/index.js -@@ -135,7 +135,22 @@ function resolveAssetUri(source) { - } - return uri; - } --var Image = /*#__PURE__*/React.forwardRef((props, ref) => { -+function raiseOnErrorEvent(uri, _ref) { -+ var onError = _ref.onError, -+ onLoadEnd = _ref.onLoadEnd; -+ if (onError) { -+ onError({ -+ nativeEvent: { -+ error: "Failed to load resource " + uri + " (404)" -+ } -+ }); -+ } -+ if (onLoadEnd) onLoadEnd(); -+} -+function hasSourceDiff(a, b) { -+ return a.uri !== b.uri || JSON.stringify(a.headers) !== JSON.stringify(b.headers); -+} -+var BaseImage = /*#__PURE__*/React.forwardRef((props, ref) => { - var ariaLabel = props['aria-label'], - blurRadius = props.blurRadius, - defaultSource = props.defaultSource, -@@ -236,16 +251,10 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { - } - }, function error() { - updateState(ERRORED); -- if (onError) { -- onError({ -- nativeEvent: { -- error: "Failed to load resource " + uri + " (404)" -- } -- }); -- } -- if (onLoadEnd) { -- onLoadEnd(); -- } -+ raiseOnErrorEvent(uri, { -+ onError, -+ onLoadEnd -+ }); - }); - } - function abortPendingRequest() { -@@ -277,10 +286,78 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { - suppressHydrationWarning: true - }), hiddenImage, createTintColorSVG(tintColor, filterRef.current)); - }); --Image.displayName = 'Image'; -+BaseImage.displayName = 'Image'; -+ -+/** -+ * This component handles specifically loading an image source with headers -+ * default source is never loaded using headers -+ */ -+var ImageWithHeaders = /*#__PURE__*/React.forwardRef((props, ref) => { -+ // $FlowIgnore: This component would only be rendered when `source` matches `ImageSource` -+ var nextSource = props.source; -+ var _React$useState3 = React.useState(''), -+ blobUri = _React$useState3[0], -+ setBlobUri = _React$useState3[1]; -+ var request = React.useRef({ -+ cancel: () => {}, -+ source: { -+ uri: '', -+ headers: {} -+ }, -+ promise: Promise.resolve('') -+ }); -+ var onError = props.onError, -+ onLoadStart = props.onLoadStart, -+ onLoadEnd = props.onLoadEnd; -+ React.useEffect(() => { -+ if (!hasSourceDiff(nextSource, request.current.source)) { -+ return; -+ } -+ -+ // When source changes we want to clean up any old/running requests -+ request.current.cancel(); -+ if (onLoadStart) { -+ onLoadStart(); -+ } -+ -+ // Store a ref for the current load request so we know what's the last loaded source, -+ // and so we can cancel it if a different source is passed through props -+ request.current = ImageLoader.loadWithHeaders(nextSource); -+ request.current.promise.then(uri => setBlobUri(uri)).catch(() => raiseOnErrorEvent(request.current.source.uri, { -+ onError, -+ onLoadEnd -+ })); -+ }, [nextSource, onLoadStart, onError, onLoadEnd]); -+ -+ // Cancel any request on unmount -+ React.useEffect(() => request.current.cancel, []); -+ var propsToPass = _objectSpread(_objectSpread({}, props), {}, { -+ // `onLoadStart` is called from the current component -+ // We skip passing it down to prevent BaseImage raising it a 2nd time -+ onLoadStart: undefined, -+ // Until the current component resolves the request (using headers) -+ // we skip forwarding the source so the base component doesn't attempt -+ // to load the original source -+ source: blobUri ? _objectSpread(_objectSpread({}, nextSource), {}, { -+ uri: blobUri -+ }) : undefined -+ }); -+ return /*#__PURE__*/React.createElement(BaseImage, _extends({ -+ ref: ref -+ }, propsToPass)); -+}); - - // $FlowIgnore: This is the correct type, but casting makes it unhappy since the variables aren't defined yet --var ImageWithStatics = Image; -+var ImageWithStatics = /*#__PURE__*/React.forwardRef((props, ref) => { -+ if (props.source && props.source.headers) { -+ return /*#__PURE__*/React.createElement(ImageWithHeaders, _extends({ -+ ref: ref -+ }, props)); -+ } -+ return /*#__PURE__*/React.createElement(BaseImage, _extends({ -+ ref: ref -+ }, props)); -+}); - ImageWithStatics.getSize = function (uri, success, failure) { - ImageLoader.getSize(uri, success, failure); - }; -diff --git a/node_modules/react-native-web/dist/modules/ImageLoader/index.js b/node_modules/react-native-web/dist/modules/ImageLoader/index.js -index bc06a87..e309394 100644 ---- a/node_modules/react-native-web/dist/modules/ImageLoader/index.js -+++ b/node_modules/react-native-web/dist/modules/ImageLoader/index.js -@@ -76,7 +76,7 @@ var ImageLoader = { - var image = requests["" + requestId]; - if (image) { - var naturalHeight = image.naturalHeight, -- naturalWidth = image.naturalWidth; -+ naturalWidth = image.naturalWidth; - if (naturalHeight && naturalWidth) { - success(naturalWidth, naturalHeight); - complete = true; -@@ -102,11 +102,19 @@ var ImageLoader = { - id += 1; - var image = new window.Image(); - image.onerror = onError; -- image.onload = e => { -+ image.onload = nativeEvent => { - // avoid blocking the main thread -- var onDecode = () => onLoad({ -- nativeEvent: e -- }); -+ var onDecode = () => { -+ // Append `source` to match RN's ImageLoadEvent interface -+ nativeEvent.source = { -+ uri: image.src, -+ width: image.naturalWidth, -+ height: image.naturalHeight -+ }; -+ onLoad({ -+ nativeEvent -+ }); -+ }; - if (typeof image.decode === 'function') { - // Safari currently throws exceptions when decoding svgs. - // We want to catch that error and allow the load handler -@@ -120,6 +128,32 @@ var ImageLoader = { - requests["" + id] = image; - return id; - }, -+ loadWithHeaders(source) { -+ var uri; -+ var abortController = new AbortController(); -+ var request = new Request(source.uri, { -+ headers: source.headers, -+ signal: abortController.signal -+ }); -+ request.headers.append('accept', 'image/*'); -+ var promise = fetch(request).then(response => response.blob()).then(blob => { -+ uri = URL.createObjectURL(blob); -+ return uri; -+ }).catch(error => { -+ if (error.name === 'AbortError') { -+ return ''; -+ } -+ throw error; -+ }); -+ return { -+ promise, -+ source, -+ cancel: () => { -+ abortController.abort(); -+ URL.revokeObjectURL(uri); -+ } -+ }; -+ }, - prefetch(uri) { - return new Promise((resolve, reject) => { - ImageLoader.load(uri, () => { diff --git a/src/components/Image/BaseImage.native.tsx b/src/components/Image/BaseImage.native.tsx deleted file mode 100644 index c517efd04515..000000000000 --- a/src/components/Image/BaseImage.native.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import {Image as ExpoImage} from 'expo-image'; -import type {ImageProps as ExpoImageProps, ImageLoadEventData} from 'expo-image'; -import {useCallback} from 'react'; -import type {BaseImageProps} from './types'; - -function BaseImage({onLoad, ...props}: ExpoImageProps & BaseImageProps) { - const imageLoadedSuccessfully = useCallback( - (event: ImageLoadEventData) => { - if (!onLoad) { - return; - } - - // We override `onLoad`, so both web and native have the same signature - const {width, height} = event.source; - onLoad({nativeEvent: {width, height}}); - }, - [onLoad], - ); - - return ( - - ); -} - -BaseImage.displayName = 'BaseImage'; - -export default BaseImage; diff --git a/src/components/Image/BaseImage.tsx b/src/components/Image/BaseImage.tsx deleted file mode 100644 index ebdd76840267..000000000000 --- a/src/components/Image/BaseImage.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, {useCallback} from 'react'; -import {Image as RNImage} from 'react-native'; -import type {ImageLoadEventData, ImageProps as WebImageProps} from 'react-native'; -import type {BaseImageProps} from './types'; - -function BaseImage({onLoad, ...props}: WebImageProps & BaseImageProps) { - const imageLoadedSuccessfully = useCallback( - (event: {nativeEvent: ImageLoadEventData}) => { - if (!onLoad) { - return; - } - - // We override `onLoad`, so both web and native have the same signature - const {width, height} = event.nativeEvent.source; - onLoad({nativeEvent: {width, height}}); - }, - [onLoad], - ); - - return ( - - ); -} - -BaseImage.displayName = 'BaseImage'; - -export default BaseImage; diff --git a/src/components/Image/index.js b/src/components/Image/index.js index 8cee1cf95e14..ef1a69e19c12 100644 --- a/src/components/Image/index.js +++ b/src/components/Image/index.js @@ -1,35 +1,51 @@ import lodashGet from 'lodash/get'; -import React, {useMemo} from 'react'; +import React, {useEffect, useMemo} from 'react'; +import {Image as RNImage} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import CONST from '@src/CONST'; +import _ from 'underscore'; import ONYXKEYS from '@src/ONYXKEYS'; -import BaseImage from './BaseImage'; import {defaultProps, imagePropTypes} from './imagePropTypes'; import RESIZE_MODES from './resizeModes'; -function Image({source: propsSource, isAuthTokenRequired, session, ...forwardedProps}) { - // Update the source to include the auth token if required +function Image(props) { + const {source: propsSource, isAuthTokenRequired, onLoad, session} = props; + /** + * Check if the image source is a URL - if so the `encryptedAuthToken` is appended + * to the source. + */ const source = useMemo(() => { - if (typeof lodashGet(propsSource, 'uri') === 'number') { - return propsSource.uri; + if (isAuthTokenRequired) { + // There is currently a `react-native-web` bug preventing the authToken being passed + // in the headers of the image request so the authToken is added as a query param. + // On native the authToken IS passed in the image request headers + const authToken = lodashGet(session, 'encryptedAuthToken', null); + return {uri: `${propsSource.uri}?encryptedAuthToken=${encodeURIComponent(authToken)}`}; } - if (typeof propsSource !== 'number' && isAuthTokenRequired) { - const authToken = lodashGet(session, 'encryptedAuthToken'); - return { - ...propsSource, - headers: { - [CONST.CHAT_ATTACHMENT_TOKEN_KEY]: authToken, - }, - }; - } - return propsSource; // The session prop is not required, as it causes the image to reload whenever the session changes. For more information, please refer to issue #26034. // eslint-disable-next-line react-hooks/exhaustive-deps }, [propsSource, isAuthTokenRequired]); + /** + * The natural image dimensions are retrieved using the updated source + * and as a result the `onLoad` event needs to be manually invoked to return these dimensions + */ + useEffect(() => { + // If an onLoad callback was specified then manually call it and pass + // the natural image dimensions to match the native API + if (onLoad == null) { + return; + } + RNImage.getSize(source.uri, (width, height) => { + onLoad({nativeEvent: {width, height}}); + }); + }, [onLoad, source]); + + // Omit the props which the underlying RNImage won't use + const forwardedProps = _.omit(props, ['source', 'onLoad', 'session', 'isAuthTokenRequired']); + return ( - { + const {width, height, url} = evt.source; + dimensionsCache.set(url, {width, height}); + if (props.onLoad) { + props.onLoad({nativeEvent: {width, height}}); + } + }} + /> + ); +} + +Image.propTypes = imagePropTypes; +Image.defaultProps = defaultProps; +Image.displayName = 'Image'; +const ImageWithOnyx = withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, +})(Image); +ImageWithOnyx.resizeMode = RESIZE_MODES; +ImageWithOnyx.resolveDimensions = resolveDimensions; + +export default ImageWithOnyx; diff --git a/src/components/Image/types.ts b/src/components/Image/types.ts deleted file mode 100644 index 5a4c94364a46..000000000000 --- a/src/components/Image/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -type BaseImageProps = { - /** Event called with image dimensions when image is loaded */ - onLoad?: (event: {nativeEvent: {width: number; height: number}}) => void; -}; - -export type {BaseImageProps}; - -export default BaseImageProps; diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index df71bf47b0ac..e22efbf4deb1 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -136,6 +136,7 @@ function DetailsPage(props) {