Skip to content

Commit 135cfeb

Browse files
authored
Merge pull request #36560 from kidroca/kidroca/feat/attachment-with-headers-2
Image Web/Desktop: Add support for http headers
2 parents c915e7f + 22a1de0 commit 135cfeb

7 files changed

Lines changed: 285 additions & 89 deletions

File tree

Lines changed: 200 additions & 0 deletions
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, () => {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {Image as ExpoImage} from 'expo-image';
2+
import type {ImageLoadEventData} from 'expo-image';
3+
import {useCallback} from 'react';
4+
import type {BaseImageProps} from './types';
5+
6+
function BaseImage({onLoad, ...props}: BaseImageProps) {
7+
const imageLoadedSuccessfully = useCallback(
8+
(event: ImageLoadEventData) => {
9+
if (!onLoad) {
10+
return;
11+
}
12+
13+
// We override `onLoad`, so both web and native have the same signature
14+
const {width, height} = event.source;
15+
onLoad({nativeEvent: {width, height}});
16+
},
17+
[onLoad],
18+
);
19+
20+
return (
21+
<ExpoImage
22+
// Only subscribe to onLoad when a handler is provided to avoid unnecessary event registrations, optimizing performance.
23+
onLoad={onLoad ? imageLoadedSuccessfully : undefined}
24+
// eslint-disable-next-line react/jsx-props-no-spreading
25+
{...props}
26+
/>
27+
);
28+
}
29+
30+
BaseImage.displayName = 'BaseImage';
31+
32+
export default BaseImage;

src/components/Image/BaseImage.tsx

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

src/components/Image/index.native.tsx

Lines changed: 0 additions & 51 deletions
This file was deleted.

src/components/Image/index.tsx

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,32 @@
1-
import React, {useEffect, useMemo} from 'react';
2-
import {Image as RNImage} from 'react-native';
1+
import React, {useMemo} from 'react';
32
import {withOnyx} from 'react-native-onyx';
4-
import useNetwork from '@hooks/useNetwork';
3+
import CONST from '@src/CONST';
54
import ONYXKEYS from '@src/ONYXKEYS';
5+
import BaseImage from './BaseImage';
66
import type {ImageOnyxProps, ImageOwnProps, ImageProps} from './types';
77

8-
function Image({source: propsSource, isAuthTokenRequired = false, onLoad, session, ...forwardedProps}: ImageProps) {
9-
const {isOffline} = useNetwork();
10-
8+
function Image({source: propsSource, isAuthTokenRequired = false, session, ...forwardedProps}: ImageProps) {
119
/**
1210
* Check if the image source is a URL - if so the `encryptedAuthToken` is appended
1311
* to the source.
1412
*/
1513
const source = useMemo(() => {
1614
const authToken = session?.encryptedAuthToken ?? null;
1715
if (isAuthTokenRequired && typeof propsSource === 'object' && 'uri' in propsSource && authToken) {
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-
return {uri: `${propsSource?.uri}?encryptedAuthToken=${encodeURIComponent(authToken)}`};
16+
return {
17+
...propsSource,
18+
headers: {
19+
[CONST.CHAT_ATTACHMENT_TOKEN_KEY]: authToken,
20+
},
21+
};
2222
}
2323
return propsSource;
2424
// 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.
2525
// eslint-disable-next-line react-hooks/exhaustive-deps
2626
}, [propsSource, isAuthTokenRequired]);
2727

28-
/**
29-
* The natural image dimensions are retrieved using the updated source
30-
* and as a result the `onLoad` event needs to be manually invoked to return these dimensions
31-
*/
32-
useEffect(() => {
33-
// If an onLoad callback was specified then manually call it and pass
34-
// the natural image dimensions to match the native API
35-
if (onLoad == null) {
36-
return;
37-
}
38-
39-
if (typeof source === 'object' && 'uri' in source && source.uri) {
40-
RNImage.getSize(source.uri, (width, height) => {
41-
onLoad({nativeEvent: {width, height}});
42-
});
43-
}
44-
}, [onLoad, source, isOffline]);
45-
4628
return (
47-
<RNImage
29+
<BaseImage
4830
// eslint-disable-next-line react/jsx-props-no-spreading
4931
{...forwardedProps}
5032
source={source}

0 commit comments

Comments
 (0)