Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image: support ImageSource with headers #2442

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/react-native-web-examples/pages/image/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ const dataBase64Svg =
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMjAwJyBoZWlnaHQ9JzIwMCcgZmlsbD0iIzAwMDAwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEwMCAxMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGQ9Ik0yNS44NjcsNDguODUzQzMyLjgwNiw1MC4xNzYsNDYuNDYsNTIuNSw2MS4yMTUsNTIuNWgwLjAwNWM5LjcxLDAsMTguNDAxLTEuMDU3LDI1LjkzOC0yLjkxMyAgIGMwLjE1OS0wLjA0NiwwLjM1LTAuMTM1LDAuNTY1LTAuMTg3YzAuMjgyLTAuMDcyLDAuNTY1LTAuMTY0LDAuODQ0LTAuMjM4YzMuMTg0LTAuOTY0LDIuNTc3LTMuMDUxLDIuMTk5LTMuODUyICAgYy00LjE2Ni03LjcxOS0xNS4wODYtMjMuNDE1LTM1LjAyOC0yMy40MTVjLTIyLjE2OSwwLTMwLjI2MiwxMC42MzUtMzMuMTQsMTkuNTg5QzIyLjU0NSw0Mi4zMzMsMjIuNDA3LDQ3LjEzNSwyNS44NjcsNDguODUzeiAgICBNMjguNjc2LDM4LjAzMmMwLjAxMy0wLjAzNiwwLjYxNC0xLjYyNiwxLjkyMy0xLjAwOGMxLjEzMywwLjUzNSwwLjk2MSwxLjU2MywwLjg4NywxLjg1Yy0wLjAwNywwLjAyNC0wLjAxNCwwLjA0OC0wLjAyMSwwLjA3MyAgIGMwLDAuMDAxLTAuMDAxLDAuMDA0LTAuMDAxLDAuMDA0bDAsMGMtMC4yNDksMC45MjktMC40MDQsMi4wODYtMC4wMTcsMi44NmMwLjE2LDAuMzE5LDAuNDkyLDAuNzY4LDEuNTQyLDAuOTg3bDAuMzY2LDAuMDc3ICAgYzIwLjgxNiw0LjM2LDM2LDIuOTMzLDQ1LjY3OCwwLjYyNmwtMC4wMDQsMC4wMDJjMCwwLDAuMDA1LTAuMDAyLDAuMDA3LTAuMDAzYzAuMjEyLTAuMDUsMC40MjEtMC4xMDEsMC42MjgtMC4xNTIgICBjMC41MDktMC4wNSwxLjE3MywwLjA3OCwxLjM5OSwxYzAuMzUxLDEuNDI0LTAuOTczLDEuODk1LTEuMjE3LDEuOTY5Yy01LjMyNSwxLjI3OS0xMi4yNjYsMi4zMDYtMjAuODM1LDIuMzA3ICAgYy03LjUwNSwwLTE2LjI1NS0wLjc4Ny0yNi4yNTctMi44ODJsLTAuMzY0LTAuMDc3Yy0yLjEyLTAuNDQyLTMuMTExLTEuNjMzLTMuNTY5LTIuNTU1QzI3Ljk4NSw0MS40MjEsMjguMjgxLDM5LjQxNiwyOC42NzYsMzguMDMyICAgeiI+PC9wYXRoPjxjaXJjbGUgY3g9IjEwLjQ5MyIgY3k9IjIzLjQ1NSIgcj0iMC42MTkiPjwvY2lyY2xlPjxwYXRoIGQ9Ik0yLjA4LDI4LjMwOGMwLjY3Ni0wLjE3OCwwLjk4My0wLjM1MiwxLjE3NC0wLjVDNC42OSwyNi42OSw2LjUsMjcuNDgzLDcuNSwyOC4zNTd2MC4wMDJjMCwwLDEuNzExLDEuMjM1LDAuNzM3LDIuMjAyICAgYy0wLjk3NCwwLjk2NS0yLjMxOSwwLjAwNi0yLjMxOSwwLjAwNmwwLjAzNSwwLjAxNmMtMC4zMjctMC4yMDMtMC42LTAuNTYxLTAuNzgtMC41ODRjLTAuMzcsMC4yNi0wLjg3NiwwLjUtMS40NzYsMC41SDMuNyAgIGMwLDAtMS4zNDUsMC43MDksMC4xNzgsMS42NTJjMC4wMDEsMC4wMDEsMC4wMDIsMC4wNzIsMC4wMDQsMC4wNzNjMy45MzksMi4zNDIsOC4yNzEsNS43MDEsOC4yNzEsOC44OCAgIGMwLDAuNjkxLDAuMiwxNy4wNDIsMTcuNjI2LDI0LjczOWwwLjk2NywwLjQ0MmwtMC4xLDEuMDU5Yy0wLjQyMSw0LjM5LDEuMTQ1LDEwLjE5MSwxMC45OTMsMTIuODg4bDAuMTEzLDAuMDM4ICAgYzAuMDY3LDAuMDIzLDYuNzMyLDIuNDI5LDEwLjkwNywyLjQyOWMxLjU4NCwwLDIuMTU1LTAuMzUyLDIuMjQzLTAuNTYxYzAuMDg1LTAuMjAyLDAuNjEyLTIuMTY0LTYuMzMyLTkuMzg3bDAuMDAyLTAuMTgzICAgYzAsMC0yLjQ3Ny0zLjA3LDEuNTMzLTMuMDdjMC4wMSwwLDAuMDE5LDAsMC4wMjksMGMxLjI4NSwwLDIuNjA4LDAuMjE1LDMuOTgsMC4xODRjNC43NzEtMC4xMTcsOS4zMTYtMC40MjUsMTMuNTA2LTEuMDk2ICAgbDAuNDc0LTAuMDI4bDAuNjY4LDAuMTU4YzkuNjUxLDQuOTQ4LDE2LjczOCw3LjcxNiwxOS43MzgsNy43MTZ2MC4wMDZjMCwwLDAuMTY0LDAuMDExLDAuMjMsMC4wMDQgICBjLTAuMTg5LTAuNzIzLTIuMjMtMi44LTcuMjMtOS4wNzl2MC4wMjFjMCwwLTEuNTEyLTEuNjU4LDAuNzk3LTIuNjUzYzAuMDYzLTAuMDI2LDAuMDA4LDAuMDIzLDAuMDYtMC4wMDEgICBjOC42MzktMy41MDksMTMuNTAxLTguMjA0LDE1LjQxMS0xMS43NzVjMS4xNDUtMi4xMjksMC4yMDYtMi43ODQtMC42NTktMi45NzZjLTAuMzE3LTAuMDM4LTAuNjM0LTAuMDYyLTAuOTEyLTAuMDYyICAgYy0wLjIwNSwwLTAuMzc5LDAuMDEtMC41MjgsMC4wMjdsLTMuMTQzLDEuMjE0QzgzLjczMiw1My45MjYsNzMuMjE4LDU1LjUsNjEuMjIsNTUuNWMtMC4wMDIsMC0wLjAwNSwwLTAuMDA1LDAgICBjLTE1LjEyOCwwLTI5LjEwMS0yLjQzMi0zNi4wODMtMy43NzFsLTAuMTczLTAuMTExbC0wLjE2LTAuMTI2Yy01Ljg1OC0yLjY4MS01LjEzNy0xMC4yMDItNS4xMDMtMTAuNTE5bDAuMDYtMC4zICAgYzAuODk1LTIuODM4LDIuNDY3LTYuMzUyLDUuMjEzLTkuNzE5Yy0xLjgwOC0xLjM2OS00LjU5LTQuMTg4LTQuNDMtOC40OTRjMC4wNDYtMS4yNDQtMC40ODYtMi41MDgtMS40OTgtMy41NTkgICBjLTEuNDk4LTEuNTU1LTMuNzg1LTIuNDQ2LTYuMjc0LTIuNDQ2Yy0xLjc3LDAtMy41NTMsMC40NDItNS4yOTMsMS4zMTRjLTQuMDYxLDIuMDM1LTQuODU1LDQuNzM2LTUuNjkyLDcuNTk2ICAgYy0wLjEzNiwwLjQ2OC0wLjI4NCwwLjkzOS0wLjQzOCwxLjQxYy0wLjAwNiwwLjAxOS0wLjAyMiwwLjAzNS0wLjAyOCwwLjA1NkMwLjgzMywyOC40MjMsMS42OTEsMjguMzksMi4wOCwyOC4zMDh6IE0xMC40OTMsMTkuOTA4ICAgYzEuOTU2LDAsMy41NDgsMS41OTEsMy41NDgsMy41NDdjMCwxLjk1Ny0xLjU5MiwzLjU0OC0zLjU0OCwzLjU0OGMtMS45NTcsMC0zLjU0OC0xLjU5Mi0zLjU0OC0zLjU0OCAgIEM2Ljk0NCwyMS40OTksOC41MzYsMTkuOTA4LDEwLjQ5MywxOS45MDh6Ij48L3BhdGg+PC9nPjwvc3ZnPg==';
const dataSvg =
'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>';
const sourceWithHeaders = {
uri: placeholder,
headers: {
'x-token': '0012345'
}
};
const sourceWithHeadersAndRedirect = {
uri: source,
headers: {
'x-token': '0012345'
}
};

function Divider() {
return <View style={styles.divider} />;
Expand Down Expand Up @@ -114,6 +126,17 @@ export default function ImagePage() {
/>
</View>
</View>
<Divider />
<View style={styles.row}>
<View style={styles.column}>
<Text style={[styles.text]}>With Headers</Text>
<Image source={sourceWithHeaders} style={styles.image} />
</View>
<View style={styles.column}>
<Text style={[styles.text]}>Headers & Redirect</Text>
<Image source={sourceWithHeadersAndRedirect} style={styles.image} />
</View>
</View>
</Example>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,14 +329,14 @@ exports[`components/Image prop "style" removes other unsupported View styles 1`]
>
<div
class="css-view-175oi2r r-backgroundColor-1niwhzg r-backgroundPosition-vvn4in r-backgroundRepeat-u6sd8q r-bottom-1p0dtai r-height-1pi2tsx r-left-1d2f490 r-position-u8s1d r-right-zchlnj r-top-ipm5af r-width-13qz1uu r-zIndex-1wyyakw r-backgroundSize-4gszlv"
style="filter: url(#tint-55);"
style="filter: url(#tint-66);"
/>
<svg
style="position: absolute; height: 0px; visibility: hidden; width: 0px;"
>
<defs>
<filter
id="tint-55"
id="tint-66"
>
<feflood
flood-color="blue"
Expand Down Expand Up @@ -379,7 +379,7 @@ exports[`components/Image prop "tintColor" convert to filter 1`] = `
>
<div
class="css-view-175oi2r r-backgroundColor-1niwhzg r-backgroundPosition-vvn4in r-backgroundRepeat-u6sd8q r-bottom-1p0dtai r-height-1pi2tsx r-left-1d2f490 r-position-u8s1d r-right-zchlnj r-top-ipm5af r-width-13qz1uu r-zIndex-1wyyakw r-backgroundSize-4gszlv"
style="background-image: url(https://google.com/favicon.ico); filter: url(#tint-56);"
style="background-image: url(https://google.com/favicon.ico); filter: url(#tint-67);"
/>
<img
alt=""
Expand All @@ -392,7 +392,7 @@ exports[`components/Image prop "tintColor" convert to filter 1`] = `
>
<defs>
<filter
id="tint-56"
id="tint-67"
>
<feflood
flood-color="red"
Expand Down
103 changes: 86 additions & 17 deletions packages/react-native-web/src/exports/Image/__tests__/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,24 @@ import Image from '../';
import ImageLoader, { ImageUriCache } from '../../../modules/ImageLoader';
import PixelRatio from '../../PixelRatio';
import React from 'react';
import { act, render } from '@testing-library/react';
import { act, render, waitFor } from '@testing-library/react';

const originalImage = window.Image;

describe('components/Image', () => {
beforeEach(() => {
ImageUriCache._entries = {};
window.Image = jest.fn(() => ({}));
ImageLoader.load = jest
.fn()
.mockImplementation((source, onLoad, onError) => {
act(() => onLoad({ source }));
});
ImageLoader.loadWithHeaders = jest.fn().mockImplementation((source) => ({
source,
promise: Promise.resolve(`blob:${Math.random()}`),
cancel: jest.fn()
}));
});

afterEach(() => {
Expand Down Expand Up @@ -102,10 +112,6 @@ describe('components/Image', () => {

describe('prop "onLoad"', () => {
test('is called after image is loaded from network', () => {
jest.useFakeTimers();
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
onLoad();
});
Comment on lines -105 to -108
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fake timers no longer needed to pass this test

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the fake timers are breaking the (new) tests that use Promises

For some reason unless we remove all calls to jest.useFakeTimers() - return waitFor(() => ...) does not work

const onLoadStartStub = jest.fn();
const onLoadStub = jest.fn();
const onLoadEndStub = jest.fn();
Expand All @@ -117,15 +123,10 @@ describe('components/Image', () => {
source="https://test.com/img.jpg"
/>
);
jest.runOnlyPendingTimers();
expect(onLoadStub).toBeCalled();
});

test('is called after image is loaded from cache', () => {
jest.useFakeTimers();
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
onLoad();
});
const onLoadStartStub = jest.fn();
const onLoadStub = jest.fn();
const onLoadEndStub = jest.fn();
Expand All @@ -139,7 +140,6 @@ describe('components/Image', () => {
source={uri}
/>
);
jest.runOnlyPendingTimers();
expect(onLoadStub).toBeCalled();
ImageUriCache.remove(uri);
});
Expand Down Expand Up @@ -223,6 +223,34 @@ describe('components/Image', () => {
});
});

describe('prop "onLoadStart"', () => {
test('is called on update if "headers" are modified', () => {
const onLoadStartStub = jest.fn();
const { rerender } = render(
<Image
onLoadStart={onLoadStartStub}
source={{
uri: 'https://test.com/img.jpg',
headers: { 'x-custom-header': 'abc123' }
}}
/>
);
act(() => {
rerender(
<Image
onLoadStart={onLoadStartStub}
source={{
uri: 'https://test.com/img.jpg',
headers: { 'x-custom-header': '123abc' }
}}
/>
);
});

expect(onLoadStartStub.mock.calls.length).toBe(2);
});
});

describe('prop "resizeMode"', () => {
['contain', 'cover', 'none', 'repeat', 'stretch', undefined].forEach(
(resizeMode) => {
Expand All @@ -241,7 +269,8 @@ describe('components/Image', () => {
'',
{},
{ uri: '' },
{ uri: 'https://google.com' }
{ uri: 'https://google.com' },
{ uri: 'https://google.com', headers: { 'x-custom-header': 'abc123' } }
];
sources.forEach((source) => {
expect(() => render(<Image source={source} />)).not.toThrow();
Expand All @@ -257,11 +286,6 @@ describe('components/Image', () => {

test('is set immediately if the image was preloaded', () => {
const uri = 'https://yahoo.com/favicon.ico';
ImageLoader.load = jest
.fn()
.mockImplementationOnce((_, onLoad, onError) => {
onLoad();
});
return Image.prefetch(uri).then(() => {
const source = { uri };
const { container } = render(<Image source={source} />, {
Expand Down Expand Up @@ -342,6 +366,51 @@ describe('components/Image', () => {
'http://localhost/static/img@2x.png'
);
});

test('it works with headers in 2 stages', async () => {
const uri = 'https://google.com/favicon.ico';
const headers = { 'x-custom-header': 'abc123' };
const source = { uri, headers };

// Stage 1
const loadRequest = {
promise: Promise.resolve('blob:123'),
cancel: jest.fn(),
source
};

ImageLoader.loadWithHeaders.mockReturnValue(loadRequest);

render(<Image source={source} />);

expect(ImageLoader.loadWithHeaders).toHaveBeenCalledWith(
expect.objectContaining(source)
);

// Stage 2
return waitFor(() => {
expect(ImageLoader.load).toHaveBeenCalledWith(
'blob:123',
expect.any(Function),
expect.any(Function)
);
});
});

// A common case is `source` declared as an inline object, which cause is to be a
// new object (with the same content) each time parent component renders
test('it still loads the image if source object is changed', () => {
const uri = 'https://google.com/favicon.ico';
const headers = { 'x-custom-header': 'abc123' };
const { rerender } = render(<Image source={{ uri, headers }} />);
rerender(<Image source={{ uri, headers }} />);

// when the underlying source didn't change we don't expect more than 1 load calls
return waitFor(() => {
expect(ImageLoader.loadWithHeaders).toHaveBeenCalledTimes(1);
expect(ImageLoader.load).toHaveBeenCalledTimes(1);
});
});
});

describe('prop "style"', () => {
Expand Down
109 changes: 91 additions & 18 deletions packages/react-native-web/src/exports/Image/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* @flow
*/

import type { ImageSource, LoadRequest } from '../../modules/ImageLoader';
import type { ImageProps } from './types';

import * as React from 'react';
Expand Down Expand Up @@ -165,6 +166,23 @@ function resolveAssetUri(source): ?string {
return uri;
}

function raiseOnErrorEvent(uri, { onError, onLoadEnd }) {
if (onError) {
onError({
nativeEvent: {
error: `Failed to load resource ${uri} (404)`
}
});
}
if (onLoadEnd) onLoadEnd();
}
Comment on lines +169 to +178
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was extracted for reuse out of the original Image component hook
(We reuse this in case loading with headers fails)


function hasSourceDiff(a: ImageSource, b: ImageSource) {
return (
a.uri !== b.uri || JSON.stringify(a.headers) !== JSON.stringify(b.headers)
);
}

interface ImageStatics {
getSize: (
uri: string,
Expand All @@ -177,10 +195,12 @@ interface ImageStatics {
) => Promise<{| [uri: string]: 'disk/memory' |}>;
}

const Image: React.AbstractComponent<
type ImageComponent = React.AbstractComponent<
ImageProps,
React.ElementRef<typeof View>
> = React.forwardRef((props, ref) => {
>;

const BaseImage: ImageComponent = React.forwardRef((props, ref) => {
const {
'aria-label': ariaLabel,
blurRadius,
Expand Down Expand Up @@ -300,16 +320,7 @@ const Image: React.AbstractComponent<
},
function error() {
updateState(ERRORED);
if (onError) {
onError({
nativeEvent: {
error: `Failed to load resource ${uri} (404)`
}
});
}
if (onLoadEnd) {
onLoadEnd();
}
raiseOnErrorEvent(uri, { onError, onLoadEnd });
}
);
}
Expand Down Expand Up @@ -353,14 +364,76 @@ const Image: React.AbstractComponent<
);
});

Image.displayName = 'Image';
BaseImage.displayName = 'Image';

/**
* This component handles specifically loading an image source with headers
* default source is never loaded using headers
*/
const ImageWithHeaders: ImageComponent = React.forwardRef((props, ref) => {
// $FlowIgnore: This component would only be rendered when `source` matches `ImageSource`
const nextSource: ImageSource = props.source;
Comment on lines +369 to +375
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps there's a way to declare ImageWithHeaders as an Image component where the source prop is exactly type ImageSource, but I don't know exactly how

I guess I need to declare ImageWithHeaderProp type that spreads the default Image prop type but changes the source. Should I do that?

const [blobUri, setBlobUri] = React.useState('');
const request = React.useRef<LoadRequest>({
cancel: () => {},
source: { uri: '', headers: {} },
promise: Promise.resolve('')
});

const { onError, onLoadStart, onLoadEnd } = props;

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, []);

const propsToPass = {
...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 ? { ...nextSource, uri: blobUri } : undefined
};

return <BaseImage ref={ref} {...propsToPass} />;
});

// $FlowIgnore: This is the correct type, but casting makes it unhappy since the variables aren't defined yet
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When this old $FlowIgnore is removed, there is a new type error:

Cannot assign React.forwardRef(...) to ImageWithStatics because property getSize is missing in AbstractComponent [1] but
exists in ImageStatics [2]. [incompatible-type]

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I've seen the original error the comment references at some point
Could it be the error was fixed by a flow version update, but now we have this other error?

const ImageWithStatics = (Image: React.AbstractComponent<
ImageProps,
React.ElementRef<typeof View>
> &
ImageStatics);
const ImageWithStatics: ImageComponent & ImageStatics = React.forwardRef(
(props, ref) => {
if (props.source && props.source.headers) {
return <ImageWithHeaders ref={ref} {...props} />;
}

return <BaseImage ref={ref} {...props} />;
}
);

ImageWithStatics.getSize = function (uri, success, failure) {
ImageLoader.getSize(uri, success, failure);
Expand Down
Loading