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][Performance] Fast Image with Auth Token Header Caching #12648

Merged
merged 18 commits into from
Nov 23, 2022

Conversation

thomas-coldwell
Copy link
Contributor

@thomas-coldwell thomas-coldwell commented Nov 10, 2022

Details

This PR combines the draft PR from @Szymon20000 #11009 for adding fast image and the PoC auth token PR from @Beamanator #12146 to properly cache images using fast image with the auth token being passed in the headers rather than the URL.

Fixed Issues

#10792

$ GH_LINK
PROPOSAL: GH_LINK_ISSUE(COMMENT)

Tests

  1. Checkout this branch
  2. Run the app on both iOS and Android
  3. Open a chat with some image attachments
  4. They will load in as normal the first time, but reload the app and then you will see the cached images loaded instantly in the same chat
  • Verify that no errors appear in the JS console

QA Steps

  • Open a report chat and add a new image attachment and send so the image appears in the chat (this should display a loading indicator the first time it is requested)
  • Reload the app and you should see the cached image displayed (with no loading indicator)
  • Click on an image in the chat to open the attachment modal view, close it and reopen it to see the cached image displayed without a loading state
  • Verify that any attachments still download as normal without any errors (including image attachments and PDFs)

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • iOS / native
    • Android / native
    • iOS / Safari
    • Android / Chrome
    • MacOS / Chrome
    • MacOS / Desktop
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product was added in all src/languages/* files
    • I verified any copy / text that was added to the app is correct English and approved by marketing by adding the Waiting for Copy label for a copy review on the original GH to get the correct copy.
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.js or at the top of the file that uses the constant) are defined as such
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG)
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • I have checked off every checkbox in the PR author checklist, including those that don't apply to this PR.

PR Reviewer Checklist

The reviewer will copy/paste it into a new comment and complete it after the author checklist is completed

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified tests pass on all platforms & I tested again on:
    • iOS / native
    • Android / native
    • iOS / Safari
    • Android / Chrome
    • MacOS / Chrome
    • MacOS / Desktop
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick).
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product was added in all src/languages/* files
    • I verified any copy / text that was added to the app is correct English and approved by marketing by adding the Waiting for Copy label for a copy review on the original GH to get the correct copy.
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.js or at the top of the file that uses the constant) are defined as such
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG)
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots

Web

Mobile Web - Chrome

Mobile Web - Safari

Desktop

Screenshot 2022-11-16 at 12 26 40

iOS

Android

@thomas-coldwell thomas-coldwell requested a review from a team as a code owner November 10, 2022 19:20
@github-actions
Copy link
Contributor

github-actions bot commented Nov 10, 2022

CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅

@melvin-bot melvin-bot bot requested review from aldo-expensify and removed request for a team November 10, 2022 19:21
@melvin-bot
Copy link

melvin-bot bot commented Nov 10, 2022

@aldo-expensify Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@thomas-coldwell
Copy link
Contributor Author

I have read the CLA Document and I hereby sign the CLA

Copy link
Contributor

@aldo-expensify aldo-expensify left a comment

Choose a reason for hiding this comment

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

Please commit suggestion to fix linter errors

src/components/ImageWithSizeCalculation.js Outdated Show resolved Hide resolved
@aldo-expensify
Copy link
Contributor

aldo-expensify commented Nov 11, 2022

@thomas-coldwell can you retroactively sign your commits? please

We will also need you to complete the PR Author steps:

  • Check the checklist "PR Author Checklist"
  • Provide proof (video or picture) that this was tested in every platform

Thanks!

Asked here about it: #10792 (comment)

@thomas-coldwell
Copy link
Contributor Author

Will get those sorted now @aldo-expensify cheers!

thomas-coldwell and others added 3 commits November 11, 2022 13:14
Co-authored-by: Aldo Canepa Garay <[email protected]>
Signed-off-by: Thomas Coldwell <[email protected]>
@thomas-coldwell
Copy link
Contributor Author

Looks like we need to have a think about how this should work on Desktop and Web (inc. mWeb) before this PR is merged cc @Beamanator

@Beamanator
Copy link
Contributor

Beamanator commented Nov 11, 2022

Yo @thomas-coldwell so correct me if I'm wrong but I had two assumptions in regard to web / desktop:

  1. I thought react-native-fast-image wouldn't work well for Web / Desktop, is that incorrect? For some reason I thought there was some bug that prevent us from using that lib on web / desktop
  2. Iff there's a problem using react-native-fast-image on web i think that's fine, because we have this issue ([HOLD for payment 2024-03-29] [HOLD for payment 2024-03-26] [$250] [Feature] RNW Image: Add support for http properties in source #12603) that should fix a bug in react-native-web which will allow us to pass the auth token in an Image's header on web / desktop

@thomas-coldwell
Copy link
Contributor Author

Hey @Beamanator you are correct - there is no implementation of react-native-fast-image on web so we'll have to add that ourselves. I think if we add web support to fast-image (subject to that bug fix for the header caching on RNW) then that keeps it clean and the main app can then use fast-image and has the platform implementation details hidden when its used in the Expensify app and then this PR should start working perfectly (will test and provide screenshots of those missing platforms once that support is added). Does this sound good?

@thomas-coldwell
Copy link
Contributor Author

Just seen @kidroca 's comment here which matches up with my previous comment 😄

@thesahindia
Copy link
Member

Sorry I am going OOO, so I won't be able to take this. Please re-assign.

@Beamanator Beamanator requested review from Beamanator and removed request for thesahindia November 14, 2022 18:52
@OSBotify
Copy link
Contributor

🚀 Deployed to staging by @Beamanator in version: 1.2.31-0 🚀

platform result
🤖 android 🤖 success ✅
🖥 desktop 🖥 success ✅
🍎 iOS 🍎 success ✅
🕸 web 🕸 success ✅

@mountiny
Copy link
Contributor

@thomas-coldwell Seems like this PR has introduced a deploy blocker. Looking into this. I will prepare a revert of this PR if the solution is not obvious.

thomas-coldwell added a commit to margelo/expensify-app-fork that referenced this pull request Nov 24, 2022
…image-caching"

This reverts commit 360a167, reversing
changes made to 39a68d1.
luacmartins added a commit that referenced this pull request Nov 24, 2022
Revert "Merge pull request #12648 from margelo/@thomas/fast-image-cac…
OSBotify pushed a commit that referenced this pull request Nov 24, 2022
Revert "Merge pull request #12648 from margelo/@thomas/fast-image-cac…

(cherry picked from commit 01abc6f)
Copy link
Contributor

@kidroca kidroca left a comment

Choose a reason for hiding this comment

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

Since this was reverted and I studied the logic a bit, I'll leave a review to help the next PR


I have a question about the name FastImage

Why was it decided to call App's component FastImage instead of simply Image

Due to the differences in platform code, some confusion and quirks exists

  • onLoad is called with one parameter structure in FastImage and another in react-native (web) Image
    (FastImage onLoad | RN onLoad)
  • FastImage has different static methods exposed and methods like preload accept source and even source array, while in RN it's just a uri: string

When I see FastImage I would assume all the react-native-fast-image options are available to this component, but since our FastImage implementation for web/desktop so far is only a core Image component they won't work

I think it a possible improvement for a follow up PR is to

  • use a simple name like Image or AppImage for the component
  • add a specific propTypes.js interface that implement only the minimum props we use

This way we can declare the source we support ATM is number | string | { urI: string, headers: object } e.g. nothing conflicting with the differences between source structure between FastImage and regular Image

I think the change is necessary, because

  • we might or might not update react-native-fast-image with (full) support for web
  • it might take time or not be a priority to apply these updates to react-native-fast-image
  • so a clean interface of exactly what we use would be helpful in maintaining the cross platform App Image component

@@ -115,7 +115,7 @@ class AttachmentModal extends PureComponent {
* @param {String} sourceURL
*/
downloadAttachment(sourceURL) {
fileDownload(sourceURL, this.props.originalFileName);
fileDownload(this.props.isAuthTokenRequired ? addEncryptedAuthTokenToURL(sourceURL) : sourceURL, this.props.originalFileName);
Copy link
Contributor

Choose a reason for hiding this comment

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

@Beamanator
We can update fileDownload to also work with headers.
Not sure whether any backend handling would need to be updated

making fileDownload work with headers is not as important as images, because it's used a lot less often
on the other hand the logic would be cleaner and allow automatic cache usage

src/components/ImageWithSizeCalculation.js Show resolved Hide resolved
}

render() {
const headers = this.props.isAuthTokenRequired ? chatAttachmentTokenHeaders() : undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

IMO it might be better to forward isAuthTokenRequired to the Image and let the Image run this logic instead of having it repeat for any subtype of Image we have

Comment on lines +122 to +124
export default withOnyx({
session: {key: ONYXKEYS.SESSION},
})(ImageWithSizeCalculation);
Copy link
Contributor

Choose a reason for hiding this comment

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

As pointed out in another comment - this is unused

Comment on lines +5 to +10

let encryptedAuthToken = '';
Onyx.connect({
key: ONYXKEYS.SESSION,
callback: session => encryptedAuthToken = lodashGet(session, 'encryptedAuthToken', ''),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

@Beamanator
This is not the "React Way" to deal with global or storage state
It's a bit of an antipattern actually because it doesn't integrate with component lifecycle - only a re-render would cause users of chatAttachmentTokenHeaders to re-evaluate, but the change in ONYXKEYS.SESSION would not trigger a re-render, unless the component is also subscribed to session

The "React Way" is to capture this with withOnyx so the component is subscribed to session and re-evaluates headers the moment they change
To keep it simple the withOnyx can be applied on the core Image (FastImage) component and let it perform the logic from this file

I know we use the static Onyx.connect logic for other cases, but we should distinct the cases that really need it and the cases that can use withOnyx instead
Another bad thing about the static pattern is that it is static - we can't lazy load it and the connection stays forever, while using the "React Way" would free resources

  • the connection can be initiated only when needed
  • the connection is released when the component is no longer mounted

Copy link
Contributor

Choose a reason for hiding this comment

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

Gooooood points, I overlooked those lifecycle points & didn't think about lazy loading / freeing resources, I'll definitely keep those in mind in the future! Next round will not do this this way ;D

@OSBotify
Copy link
Contributor

🚀 Deployed to staging by @Beamanator in version: 1.2.32-0 🚀

platform result
🤖 android 🤖 success ✅
🖥 desktop 🖥 success ✅
🍎 iOS 🍎 success ✅
🕸 web 🕸 success ✅

@mountiny
Copy link
Contributor

cc @thomas-coldwell, thanks for the review @kidroca

@mananjadhav
Copy link
Collaborator

mananjadhav commented Nov 28, 2022

For the next round of QA, @Beamanator @aldo-expensify can we get a regression test suite from Testrail to ensure nothing breaks!

@OSBotify
Copy link
Contributor

🚀 Deployed to production by @luacmartins in version: 1.2.32-2 🚀

platform result
🤖 android 🤖 success ✅
🖥 desktop 🖥 success ✅
🍎 iOS 🍎 failure ❌
🕸 web 🕸 success ✅

@OSBotify
Copy link
Contributor

🚀 Deployed to production by @luacmartins in version: 1.2.32-2 🚀

platform result
🤖 android 🤖 success ✅
🖥 desktop 🖥 success ✅
🍎 iOS 🍎 success ✅
🕸 web 🕸 success ✅

@Beamanator
Copy link
Contributor

@kidroca thanks for the review, I'll also request your review on the follow-up PR when it's ready :D 🙏

Why was it decided to call App's component FastImage instead of simply Image

I thought FastImage was "ok" since we had the idea to try to use FastImage for both platforms, but you have a good point that we may or may not actually be able to / desire making that happen, so I like your idea of renaming this to something like Image or something like that.

It looks like we currently often use standard react native component names when we customize them, and if we want to directly use a react native component (like TextInput) we name it RNTextInput to make it clear it's a react native component. Maybe we can follow this convention with the new Image component

@thomas-coldwell When the follow-up PR is ready can you please tag me & request a review from @kidroca as well please? 👍

@thomas-coldwell
Copy link
Contributor Author

thomas-coldwell commented Dec 1, 2022

Hey @kidroca @Beamanator thanks for those points!

  • I agree it should be renamed and I think just Image is a good option
  • Also agree on passing the isAuthRequired prop down to the Image component and letting it handle applying the headers there and I'll move the withOnyx HoC down to this level too which should make it a lot more concise

I'll link the follow-up PR here shortly and make sure you are both set to review it :)

@thomas-coldwell
Copy link
Contributor Author

thomas-coldwell commented Dec 1, 2022

Just addressing the issue on Android that caused this PR to be reverted and hoping you might be able to provide a bit more context around this.

The issue originated from the fact that react-native-fast-image does not call its onLoad event if the height or width of its style is set to zero (original issue DylanVann/react-native-fast-image#865) and so it would not run the size calculation in the ThumbnailImage component. Additionally, the ThumbnailImage component receives a width and height from the ImageRenderer component that pulls this from some html attributes - weirdly for the PDF thumbnails the width and height here is set to zero:

{
  src: 'https://www.expensify.com/chat-attachments/2567892380284198922/w_0f4e8046ae3f4a4af1514f1eb51f71d5c7bc39f3.jpg.1024.jpg',
  'data-expensify-source': 'https://www.expensify.com/chat-attachments/2567892380284198922/w_0f4e8046ae3f4a4af1514f1eb51f71d5c7bc39f3.jpg',
  'data-name': 'tst.jpeg',
  'data-expensify-width': '4288',
  'data-expensify-height': '2848'
},
{
  src: 'https://www.expensify.com/chat-attachments/2266530483440650648/w_f04ec020386e433bdf1ee9f230b197b059b67ec3.jpg.1024.jpg',
  'data-expensify-source': 'https://www.expensify.com/chat-attachments/2266530483440650648/w_f04ec020386e433bdf1ee9f230b197b059b67ec3.pdf',
  'data-name': 'eglspec.1.5.pdf',
  'data-expensify-width': '0',
  'data-expensify-height': '0'
}

As a result the initial width/height for the thumbnail component (specifically in the case of a PDF thumbnail) style will always be zero and this is never updated or corrected by the onLoad event firing due to that aforementioned bug in fast image. This explains why we only see it happening on the PDF thumbnail images.

I will continue to see what can be solved on the react-native-fast-image side of this as the workaround suggested does have a side effect of potential crashes with very large images on Android, but if you could let me know if the data-expensify-width being zero makes sense or if this is potentially a bug then we can make this robust end to end. cc @Beamanator @kidroca @mountiny

@Beamanator
Copy link
Contributor

Beamanator commented Dec 2, 2022

Innnteresting findings @thomas-coldwell ! I just found in our back-end code, we currently use PHP's getfilesize function to try to get the file size (here, for internal employees). And because this doesn't actually work for pdfs, we just default the height & width to 0 (here).

@thomas-coldwell what I'm planning to try next is:

  1. Re-fork your old PR that we reverted, reproduce regression
  2. Test setting the height & width to something non-zero for PDFs (I don't see why this would have to be dynamic, do you?)
  3. See if everything works for me locally - If so, I'll try this change with current code to see if it breaks current stuff (I don't see how it would, but have to check).
  4. If everything goes smoothly, I'll update the serve-side code and we can hopefully not have to deal with this non-zero width / height issue!

@Beamanator
Copy link
Contributor

Cool I followed steps 1 & 2, tested with the old code, things looked good! I hard-coded to 30px by 30px in the test which is why it turned out so small, but I will tweak it so it looks fine & I'll test on other platforms to make sure it doesn't break anything while we're waiting for the next version of @thomas-coldwell 's PR

Screen.Recording.2022-12-02.at.3.35.22.PM.mov

@mountiny
Copy link
Contributor

mountiny commented Dec 2, 2022

@Beamanator the code to proceed the upload is same for OldDot as well, right. does it parse receipts too? we should probably try to avoid manipulating that flow now if that is the case.

@Beamanator
Copy link
Contributor

@vitHoracek here's the draft PR: https://github.com/Expensify/Web-Expensify/pull/35730 the code is only used when adding comments to reports, so I don't think it parses receipts 'n such, but feel free to comment on that PR if you have concerns! I'm still working on testing that it doesn't break things :D

@mountiny
Copy link
Contributor

mountiny commented Dec 2, 2022

@Beamanator no problem, I trust you :D just wanted to raise this

@thomas-coldwell
Copy link
Contributor Author

Thanks @Beamanator and @mountiny - great that this change server-side does ensure a non-zero width and height. I will get the changes in for the Fast Image onLoad event today and then these changes should work perfectly side-by-side 🙌

@Beamanator
Copy link
Contributor

That would be very helpful, thanks @thomas-coldwell !

@thomas-coldwell
Copy link
Contributor Author

Here's the draft PR @Beamanator - just travelling atm, but started doing some digging on the revert issue #13304 😃

@Beamanator
Copy link
Contributor

Thanks @thomas-coldwell ! Will respond in that new PR 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants