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
new file mode 100644
index 000000000000..4652e22662f0
--- /dev/null
+++ b/patches/react-native-web+0.19.9+005+image-header-support.patch
@@ -0,0 +1,200 @@
+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.js b/src/components/Image/BaseImage.js
new file mode 100644
index 000000000000..cd2326900c6c
--- /dev/null
+++ b/src/components/Image/BaseImage.js
@@ -0,0 +1,29 @@
+import React, {useCallback} from 'react';
+import {Image as RNImage} from 'react-native';
+import {defaultProps, imagePropTypes} from './imagePropTypes';
+
+function BaseImage({onLoad, ...props}) {
+ const imageLoadedSuccessfully = useCallback(
+ ({nativeEvent}) => {
+ // We override `onLoad`, so both web and native have the same signature
+ const {width, height} = nativeEvent.source;
+ onLoad({nativeEvent: {width, height}});
+ },
+ [onLoad],
+ );
+
+ return (
+
+ );
+}
+
+BaseImage.propTypes = imagePropTypes;
+BaseImage.defaultProps = defaultProps;
+BaseImage.displayName = 'BaseImage';
+
+export default BaseImage;
diff --git a/src/components/Image/BaseImage.native.js b/src/components/Image/BaseImage.native.js
new file mode 100644
index 000000000000..a621947267a1
--- /dev/null
+++ b/src/components/Image/BaseImage.native.js
@@ -0,0 +1,3 @@
+import RNFastImage from 'react-native-fast-image';
+
+export default RNFastImage;
diff --git a/src/components/Image/index.js b/src/components/Image/index.js
index ef1a69e19c12..8cee1cf95e14 100644
--- a/src/components/Image/index.js
+++ b/src/components/Image/index.js
@@ -1,51 +1,35 @@
import lodashGet from 'lodash/get';
-import React, {useEffect, useMemo} from 'react';
-import {Image as RNImage} from 'react-native';
+import React, {useMemo} from 'react';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import BaseImage from './BaseImage';
import {defaultProps, imagePropTypes} from './imagePropTypes';
import RESIZE_MODES from './resizeModes';
-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.
- */
+function Image({source: propsSource, isAuthTokenRequired, session, ...forwardedProps}) {
+ // Update the source to include the auth token if required
const source = useMemo(() => {
- 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 lodashGet(propsSource, 'uri') === 'number') {
+ return propsSource.uri;
}
+ 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} = evt.nativeEvent;
- dimensionsCache.set(source.uri, {width, height});
- if (props.onLoad) {
- props.onLoad(evt);
- }
- }}
- />
- );
-}
-
-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/RoomHeaderAvatars.js b/src/components/RoomHeaderAvatars.js
index bd7a24d15f1f..4a1d60a869ad 100644
--- a/src/components/RoomHeaderAvatars.js
+++ b/src/components/RoomHeaderAvatars.js
@@ -33,7 +33,6 @@ function RoomHeaderAvatars(props) {
@@ -78,7 +77,6 @@ function RoomHeaderAvatars(props) {
diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js
index 66345107dbb1..6d3f0198bbfe 100755
--- a/src/pages/DetailsPage.js
+++ b/src/pages/DetailsPage.js
@@ -134,7 +134,6 @@ function DetailsPage(props) {
{({show}) => (
diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js
index 97ec3f99da3c..ece75b7f6918 100755
--- a/src/pages/ProfilePage.js
+++ b/src/pages/ProfilePage.js
@@ -159,7 +159,6 @@ function ProfilePage(props) {