Skip to content

Commit efcd932

Browse files
authored
Merge pull request #13036 from kidroca/kidroca/feature/react-native-web-image-headers
Image Web/Desktop: Add support for http headers
2 parents 276b007 + 18000a2 commit efcd932

8 files changed

+250
-101
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
diff --git a/node_modules/react-native-web/dist/exports/Image/index.js b/node_modules/react-native-web/dist/exports/Image/index.js
2+
index 95355d5..19109fc 100644
3+
--- a/node_modules/react-native-web/dist/exports/Image/index.js
4+
+++ b/node_modules/react-native-web/dist/exports/Image/index.js
5+
@@ -135,7 +135,22 @@ function resolveAssetUri(source) {
6+
}
7+
return uri;
8+
}
9+
-var Image = /*#__PURE__*/React.forwardRef((props, ref) => {
10+
+function raiseOnErrorEvent(uri, _ref) {
11+
+ var onError = _ref.onError,
12+
+ onLoadEnd = _ref.onLoadEnd;
13+
+ if (onError) {
14+
+ onError({
15+
+ nativeEvent: {
16+
+ error: "Failed to load resource " + uri + " (404)"
17+
+ }
18+
+ });
19+
+ }
20+
+ if (onLoadEnd) onLoadEnd();
21+
+}
22+
+function hasSourceDiff(a, b) {
23+
+ return a.uri !== b.uri || JSON.stringify(a.headers) !== JSON.stringify(b.headers);
24+
+}
25+
+var BaseImage = /*#__PURE__*/React.forwardRef((props, ref) => {
26+
var ariaLabel = props['aria-label'],
27+
blurRadius = props.blurRadius,
28+
defaultSource = props.defaultSource,
29+
@@ -236,16 +251,10 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => {
30+
}
31+
}, function error() {
32+
updateState(ERRORED);
33+
- if (onError) {
34+
- onError({
35+
- nativeEvent: {
36+
- error: "Failed to load resource " + uri + " (404)"
37+
- }
38+
- });
39+
- }
40+
- if (onLoadEnd) {
41+
- onLoadEnd();
42+
- }
43+
+ raiseOnErrorEvent(uri, {
44+
+ onError,
45+
+ onLoadEnd
46+
+ });
47+
});
48+
}
49+
function abortPendingRequest() {
50+
@@ -277,10 +286,78 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => {
51+
suppressHydrationWarning: true
52+
}), hiddenImage, createTintColorSVG(tintColor, filterRef.current));
53+
});
54+
-Image.displayName = 'Image';
55+
+BaseImage.displayName = 'Image';
56+
+
57+
+/**
58+
+ * This component handles specifically loading an image source with headers
59+
+ * default source is never loaded using headers
60+
+ */
61+
+var ImageWithHeaders = /*#__PURE__*/React.forwardRef((props, ref) => {
62+
+ // $FlowIgnore: This component would only be rendered when `source` matches `ImageSource`
63+
+ var nextSource = props.source;
64+
+ var _React$useState3 = React.useState(''),
65+
+ blobUri = _React$useState3[0],
66+
+ setBlobUri = _React$useState3[1];
67+
+ var request = React.useRef({
68+
+ cancel: () => {},
69+
+ source: {
70+
+ uri: '',
71+
+ headers: {}
72+
+ },
73+
+ promise: Promise.resolve('')
74+
+ });
75+
+ var onError = props.onError,
76+
+ onLoadStart = props.onLoadStart,
77+
+ onLoadEnd = props.onLoadEnd;
78+
+ React.useEffect(() => {
79+
+ if (!hasSourceDiff(nextSource, request.current.source)) {
80+
+ return;
81+
+ }
82+
+
83+
+ // When source changes we want to clean up any old/running requests
84+
+ request.current.cancel();
85+
+ if (onLoadStart) {
86+
+ onLoadStart();
87+
+ }
88+
+
89+
+ // Store a ref for the current load request so we know what's the last loaded source,
90+
+ // and so we can cancel it if a different source is passed through props
91+
+ request.current = ImageLoader.loadWithHeaders(nextSource);
92+
+ request.current.promise.then(uri => setBlobUri(uri)).catch(() => raiseOnErrorEvent(request.current.source.uri, {
93+
+ onError,
94+
+ onLoadEnd
95+
+ }));
96+
+ }, [nextSource, onLoadStart, onError, onLoadEnd]);
97+
+
98+
+ // Cancel any request on unmount
99+
+ React.useEffect(() => request.current.cancel, []);
100+
+ var propsToPass = _objectSpread(_objectSpread({}, props), {}, {
101+
+ // `onLoadStart` is called from the current component
102+
+ // We skip passing it down to prevent BaseImage raising it a 2nd time
103+
+ onLoadStart: undefined,
104+
+ // Until the current component resolves the request (using headers)
105+
+ // we skip forwarding the source so the base component doesn't attempt
106+
+ // to load the original source
107+
+ source: blobUri ? _objectSpread(_objectSpread({}, nextSource), {}, {
108+
+ uri: blobUri
109+
+ }) : undefined
110+
+ });
111+
+ return /*#__PURE__*/React.createElement(BaseImage, _extends({
112+
+ ref: ref
113+
+ }, propsToPass));
114+
+});
115+
116+
// $FlowIgnore: This is the correct type, but casting makes it unhappy since the variables aren't defined yet
117+
-var ImageWithStatics = Image;
118+
+var ImageWithStatics = /*#__PURE__*/React.forwardRef((props, ref) => {
119+
+ if (props.source && props.source.headers) {
120+
+ return /*#__PURE__*/React.createElement(ImageWithHeaders, _extends({
121+
+ ref: ref
122+
+ }, props));
123+
+ }
124+
+ return /*#__PURE__*/React.createElement(BaseImage, _extends({
125+
+ ref: ref
126+
+ }, props));
127+
+});
128+
ImageWithStatics.getSize = function (uri, success, failure) {
129+
ImageLoader.getSize(uri, success, failure);
130+
};
131+
diff --git a/node_modules/react-native-web/dist/modules/ImageLoader/index.js b/node_modules/react-native-web/dist/modules/ImageLoader/index.js
132+
index bc06a87..e309394 100644
133+
--- a/node_modules/react-native-web/dist/modules/ImageLoader/index.js
134+
+++ b/node_modules/react-native-web/dist/modules/ImageLoader/index.js
135+
@@ -76,7 +76,7 @@ var ImageLoader = {
136+
var image = requests["" + requestId];
137+
if (image) {
138+
var naturalHeight = image.naturalHeight,
139+
- naturalWidth = image.naturalWidth;
140+
+ naturalWidth = image.naturalWidth;
141+
if (naturalHeight && naturalWidth) {
142+
success(naturalWidth, naturalHeight);
143+
complete = true;
144+
@@ -102,11 +102,19 @@ var ImageLoader = {
145+
id += 1;
146+
var image = new window.Image();
147+
image.onerror = onError;
148+
- image.onload = e => {
149+
+ image.onload = nativeEvent => {
150+
// avoid blocking the main thread
151+
- var onDecode = () => onLoad({
152+
- nativeEvent: e
153+
- });
154+
+ var onDecode = () => {
155+
+ // Append `source` to match RN's ImageLoadEvent interface
156+
+ nativeEvent.source = {
157+
+ uri: image.src,
158+
+ width: image.naturalWidth,
159+
+ height: image.naturalHeight
160+
+ };
161+
+ onLoad({
162+
+ nativeEvent
163+
+ });
164+
+ };
165+
if (typeof image.decode === 'function') {
166+
// Safari currently throws exceptions when decoding svgs.
167+
// We want to catch that error and allow the load handler
168+
@@ -120,6 +128,32 @@ var ImageLoader = {
169+
requests["" + id] = image;
170+
return id;
171+
},
172+
+ loadWithHeaders(source) {
173+
+ var uri;
174+
+ var abortController = new AbortController();
175+
+ var request = new Request(source.uri, {
176+
+ headers: source.headers,
177+
+ signal: abortController.signal
178+
+ });
179+
+ request.headers.append('accept', 'image/*');
180+
+ var promise = fetch(request).then(response => response.blob()).then(blob => {
181+
+ uri = URL.createObjectURL(blob);
182+
+ return uri;
183+
+ }).catch(error => {
184+
+ if (error.name === 'AbortError') {
185+
+ return '';
186+
+ }
187+
+ throw error;
188+
+ });
189+
+ return {
190+
+ promise,
191+
+ source,
192+
+ cancel: () => {
193+
+ abortController.abort();
194+
+ URL.revokeObjectURL(uri);
195+
+ }
196+
+ };
197+
+ },
198+
prefetch(uri) {
199+
return new Promise((resolve, reject) => {
200+
ImageLoader.load(uri, () => {

src/components/Image/BaseImage.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React, {useCallback} from 'react';
2+
import {Image as RNImage} from 'react-native';
3+
import {defaultProps, imagePropTypes} from './imagePropTypes';
4+
5+
function BaseImage({onLoad, ...props}) {
6+
const imageLoadedSuccessfully = useCallback(
7+
({nativeEvent}) => {
8+
// We override `onLoad`, so both web and native have the same signature
9+
const {width, height} = nativeEvent.source;
10+
onLoad({nativeEvent: {width, height}});
11+
},
12+
[onLoad],
13+
);
14+
15+
return (
16+
<RNImage
17+
// Only subscribe to onLoad when a handler is provided to avoid unnecessary event registrations, optimizing performance.
18+
onLoad={onLoad ? imageLoadedSuccessfully : undefined}
19+
// eslint-disable-next-line react/jsx-props-no-spreading
20+
{...props}
21+
/>
22+
);
23+
}
24+
25+
BaseImage.propTypes = imagePropTypes;
26+
BaseImage.defaultProps = defaultProps;
27+
BaseImage.displayName = 'BaseImage';
28+
29+
export default BaseImage;
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import RNFastImage from 'react-native-fast-image';
2+
3+
export default RNFastImage;

src/components/Image/index.js

+18-34
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,35 @@
11
import lodashGet from 'lodash/get';
2-
import React, {useEffect, useMemo} from 'react';
3-
import {Image as RNImage} from 'react-native';
2+
import React, {useMemo} from 'react';
43
import {withOnyx} from 'react-native-onyx';
5-
import _ from 'underscore';
4+
import CONST from '@src/CONST';
65
import ONYXKEYS from '@src/ONYXKEYS';
6+
import BaseImage from './BaseImage';
77
import {defaultProps, imagePropTypes} from './imagePropTypes';
88
import RESIZE_MODES from './resizeModes';
99

10-
function Image(props) {
11-
const {source: propsSource, isAuthTokenRequired, onLoad, session} = props;
12-
/**
13-
* Check if the image source is a URL - if so the `encryptedAuthToken` is appended
14-
* to the source.
15-
*/
10+
function Image({source: propsSource, isAuthTokenRequired, session, ...forwardedProps}) {
11+
// Update the source to include the auth token if required
1612
const source = useMemo(() => {
17-
if (isAuthTokenRequired) {
18-
// There is currently a `react-native-web` bug preventing the authToken being passed
19-
// in the headers of the image request so the authToken is added as a query param.
20-
// On native the authToken IS passed in the image request headers
21-
const authToken = lodashGet(session, 'encryptedAuthToken', null);
22-
return {uri: `${propsSource.uri}?encryptedAuthToken=${encodeURIComponent(authToken)}`};
13+
if (typeof lodashGet(propsSource, 'uri') === 'number') {
14+
return propsSource.uri;
2315
}
16+
if (typeof propsSource !== 'number' && isAuthTokenRequired) {
17+
const authToken = lodashGet(session, 'encryptedAuthToken');
18+
return {
19+
...propsSource,
20+
headers: {
21+
[CONST.CHAT_ATTACHMENT_TOKEN_KEY]: authToken,
22+
},
23+
};
24+
}
25+
2426
return propsSource;
2527
// 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.
2628
// eslint-disable-next-line react-hooks/exhaustive-deps
2729
}, [propsSource, isAuthTokenRequired]);
2830

29-
/**
30-
* The natural image dimensions are retrieved using the updated source
31-
* and as a result the `onLoad` event needs to be manually invoked to return these dimensions
32-
*/
33-
useEffect(() => {
34-
// If an onLoad callback was specified then manually call it and pass
35-
// the natural image dimensions to match the native API
36-
if (onLoad == null) {
37-
return;
38-
}
39-
RNImage.getSize(source.uri, (width, height) => {
40-
onLoad({nativeEvent: {width, height}});
41-
});
42-
}, [onLoad, source]);
43-
44-
// Omit the props which the underlying RNImage won't use
45-
const forwardedProps = _.omit(props, ['source', 'onLoad', 'session', 'isAuthTokenRequired']);
46-
4731
return (
48-
<RNImage
32+
<BaseImage
4933
// eslint-disable-next-line react/jsx-props-no-spreading
5034
{...forwardedProps}
5135
source={source}

src/components/Image/index.native.js

-63
This file was deleted.

src/components/RoomHeaderAvatars.js

-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ function RoomHeaderAvatars(props) {
3333
<AttachmentModal
3434
headerTitle={props.icons[0].name}
3535
source={UserUtils.getFullSizeAvatar(props.icons[0].source, props.icons[0].id)}
36-
isAuthTokenRequired
3736
isWorkspaceAvatar={props.icons[0].type === CONST.ICON_TYPE_WORKSPACE}
3837
originalFileName={props.icons[0].name}
3938
>
@@ -78,7 +77,6 @@ function RoomHeaderAvatars(props) {
7877
<AttachmentModal
7978
headerTitle={icon.name}
8079
source={UserUtils.getFullSizeAvatar(icon.source, icon.id)}
81-
isAuthTokenRequired
8280
originalFileName={icon.name}
8381
isWorkspaceAvatar={icon.type === CONST.ICON_TYPE_WORKSPACE}
8482
>

src/pages/DetailsPage.js

-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ function DetailsPage(props) {
134134
<AttachmentModal
135135
headerTitle={details.displayName}
136136
source={UserUtils.getFullSizeAvatar(details.avatar, details.accountID)}
137-
isAuthTokenRequired
138137
originalFileName={details.originalFileName}
139138
>
140139
{({show}) => (

0 commit comments

Comments
 (0)