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

Report Attachments Modal Route #20167

Merged

Conversation

kidroca
Copy link
Contributor

@kidroca kidroca commented Jun 5, 2023

Details

In this PR, I have made several enhancements to the existing code to improve the handling of report attachments. Key changes include the addition of a new modal screen src/pages/home/report/ReportAttachments.js, modifying AttachmentModal to make the passing of children optional, and the addition of a defaultOpen prop to AttachmentModal. A new route, REPORT_ATTACHMENT, has also been introduced.

Changes:

  1. src/ROUTES.js:
    A new route named REPORT_ATTACHMENT has been added. It is structured as r/:reportID/attachment?source=... and has an associated getter function for generating this URL with the required parameters.

  2. src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js:
    The AttachmentModal component usage has been replaced with navigation to the new REPORT_ATTACHMENT route.

  3. src/libs/Navigation/AppNavigator/AuthScreens.js:
    The new Report_Attachments screen has been registered in the application's authenticated screens.

  4. src/libs/Navigation/linkingConfig.js:
    The Report_Attachments screen has been added to the linking configuration, enabling it to be opened via URL schemes or deep linking.

  5. src/components/AttachmentCarousel/index.js:
    Use onNavigate to update the parent modal's state with additional information about the selected attachment - file name and auth requirements

  6. src/components/AttachmentModal.js:
    The children prop is now optional, and a new defaultOpen prop has been added, which determines whether the modal should be open by default.

  7. src/pages/home/report/ReportAttachments.js:
    This new file creates the ReportAttachments component, which returns an AttachmentModal that's set to defaultOpen. It takes a reportID and source as parameters from the route, and on modal hide, it dismisses the modal.

Fixed Issues

$ #18451
PROPOSAL: #18451 (comment)

Tests

Test 1: Verify Existing Functionality is Unaltered

Objective: To ensure that the implemented changes haven't affected the existing functionality.

  1. Navigate to a chat with multiple attachments.
  2. Open different types of attachments (e.g., images, documents), and verify that they are correctly displayed in the carousel.
  3. Download a variety of attachments and check that the downloading functionality remains intact.

Expected Outcomes:

  • Each attachment should open and display correctly when selected from the chat.
  • Navigation between attachments in the carousel should be smooth, with attachments appearing in the correct order.
  • All downloads should proceed as before, with no new issues introduced.

Note: This PR does not address existing known issues such as the carousel occasionally displaying the wrong image (as detailed in #18150 (comment)).

Test 2: Verify the issue with deleting an image is resolved

Objective: To ensure that the main issue with deleting attachments is resolved

  1. Have a chat with 2 users: User A and User B.
  2. Share a few attachments between them. Share one last attachment with User B.
  3. With User A, open the last attachment User B shared.
  4. With User B, delete the chat message which shared the last attachment (long press or 2nd mouse click on the chat message and select "Delete Comment").

Expected Outcomes:

  • From the point of view of User A, nothing happens, they are still inside the carousel and can see the deleted image until they close the carousel.
  • From the point of view of User B, the image is deleted.
  • When User A exits the carousel / attachment view, the image is no longer available in the chat and they can't open it again (it no longer appears if they open the carousel again).

Offline tests

N/A

Opening an attachment while offline should have the same behavior as it did before - the attachment appears to load indefinitely. We can close the attachment view and return to the chat.

QA Steps

Test 1: Verify Existing Functionality is Unaltered

Objective: To ensure that the implemented changes haven't affected the existing functionality.

  1. Navigate to a chat with multiple attachments.
  2. Open different types of attachments (e.g., images, documents), and verify that they are correctly displayed in the carousel.
  3. Download a variety of attachments and check that the downloading functionality remains intact.

Expected Outcomes:

  • Each attachment should open and display correctly when selected from the chat.
  • Navigation between attachments in the carousel should be smooth, with attachments appearing in the correct order.
  • All downloads should proceed as before, with no new issues introduced.

Note: This PR does not address existing known issues such as the carousel occasionally displaying the wrong image (as detailed in #18150 (comment)).

Test 2: Verify the issue with deleting an image is resolved

Objective: To ensure that the main issue with deleting attachments is resolved

  1. Have a chat with 2 users: User A and User B.
  2. Share a few attachments between them. Share one last attachment with User B.
  3. With User A, open the last attachment User B shared.
  4. With User B, delete the chat message which shared the last attachment (long press or 2nd mouse click on the chat message and select "Delete Comment").

Expected Outcomes:

  • From the point of view of User A, nothing happens, they are still inside the carousel and can see the deleted image until they close the carousel.
  • From the point of view of User B, the image is deleted.
  • When User A exits the carousel / attachment view, the image is no longer available in the chat and they can't open it again (it no longer appears if they open the carousel again).

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 the expected offline behavior in the Offline steps 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 tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android / native
    • Android / Chrome
    • iOS / native
    • iOS / Safari
    • MacOS / Chrome / Safari
    • 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 the left part of a conditional rendering a React component is a boolean and NOT a string, e.g. myBool && <MyComponent />.
    • 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 is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I verified the translation was requested/reviewed in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is 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
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • 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 any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • 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 code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • 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.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR author checklist, including those that don't apply to this PR.

Screenshots/Videos

Web
Screen.Recording.2023-06-06.at.21.23.53.mov
Mobile Web - Chrome
2023-06-21_22-06-18.mp4
Mobile Web - Safari
Screen.Recording.2023-06-06.at.21.27.54.mov
Desktop
Screen.Recording.2023-06-06.at.21.27.54.mov
iOS
Screen.Recording.2023-06-06.at.21.23.53.mov
Android
2023-06-21_22-10-18.mp4

Copy link
Contributor Author

@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.

This PR is ready for code review

I'll be able to continue and add testing steps and check all platforms tomorrow

src/ROUTES.js Outdated
Comment on lines 71 to 72
REPORT_ATTACHMENT: 'r/:reportID/attachment',
getReportAttachmentRoute: (reportID, source) => `r/${reportID}/attachment?source=${encodeURI(source)}`,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I can remove the changes here and in linking which would disable deep linking, but also prevent me from using the global Navigation object in ImageRenderer (I'll post a similar note there)

Right now if we refresh the page or copy paste the URL while we have the attachment carousel open, it would load the page with the attachment carousel open

The question is, do we want to deliberately not allow this? It can be used as a feature linking to specific attachments

src/components/AttachmentCarousel/index.js Show resolved Hide resolved
Comment on lines +57 to +58
const route = ROUTES.getReportAttachmentRoute(report.reportID, source);
Navigation.navigate(route);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This can be replaced by a call to useNavigation and then navigation.navigate('Report_Attachment', {.reportID, source })

Everywhere else we use Navigation.navigate and ROUTES so I've opted for the same approach here, but this allows deep linking and link sharing and this feature might be unwanted

@kidroca kidroca marked this pull request as ready for review June 5, 2023 13:58
@kidroca kidroca requested a review from a team as a code owner June 5, 2023 13:58
@melvin-bot melvin-bot bot requested review from NikkiWines and rushatgabhane and removed request for a team June 5, 2023 13:58
@melvin-bot
Copy link

melvin-bot bot commented Jun 5, 2023

@NikkiWines @rushatgabhane One of you needs to 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]

kidroca added 6 commits June 8, 2023 00:53
The Carousel already updates the parent component this way, when attachments change
Updating it here allows the parent component to not need `isAuthenticated` and
`originalFileName` for the initial image source provided, since they are
obtained here as well
@rushatgabhane
Copy link
Member

rushatgabhane commented Jun 11, 2023

@kidroca @NikkiWines sorry i was out sick and couldn't review the PR in time.
Someone else from the team should take over, thank you!

@allroundexperts
Copy link
Contributor

@NikkiWines I'll be taking this over. Please assign this to me.

@NikkiWines NikkiWines requested review from allroundexperts and removed request for rushatgabhane June 12, 2023 15:09
@allroundexperts
Copy link
Contributor

@kidroca Can you please resolve conflicts?

@kidroca
Copy link
Contributor Author

kidroca commented Jun 13, 2023

Yes, I'll look into it tonight

kidroca added 3 commits June 14, 2023 13:55
Otherwise, when closing the modal there's a blank screen displayed briefly
It seems the previous screen does not stay underneath the modal unless it's on the root
@kidroca kidroca force-pushed the kidroca/feat/attachment-modal-route branch from ff20435 to 3d5bc85 Compare June 14, 2023 13:13
Copy link
Contributor Author

@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.

Please be aware that a significant overhaul to the navigation structure in the main branch, introduced some merge conflicts. I've managed to resolve these conflicts, but in the process, I found it necessary to implement some additional changes to ensure seamless integration and functionality.

All merge conflicts have been successfully resolved ✅

Comment on lines +253 to +264
<RootStack.Screen
name={SCREENS.REPORT_ATTACHMENTS}
options={{
headerShown: false,
presentation: 'transparentModal',
}}
getComponent={() => {
const ReportAttachments = require('../../../pages/home/report/ReportAttachments').default;
return ReportAttachments;
}}
listeners={modalScreenListeners}
/>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Utilizing RootStack.Screen proved to be the only viable solution for overlaying the modal over the opened chat. I tested alternative navigators such as CENTRAL_PANE_NAVIGATOR, FULL_SCREEN_NAVIGATOR, and RIGHT_MODAL_NAVIGATOR. However, these alternatives resulted in a blank screen beneath the modal. This undesired outcome was visible for at least half a second during the modal's closing transition.

Sample Behavior When Not Using RootStack

Please refer to the following link for a demonstration of this behavior without the use of RootStack:

Android.Emulator.-.Pixel_2_API_29_5554.2023-06-14.16-28-43.mp4

Sample Behavior When Using RootStack

For comparison, the following link shows the preferred outcome achieved when utilizing RootStack:

Android.Emulator.-.Pixel_2_API_29_5554.2023-06-14.16-31-27.mp4

Comment on lines 135 to 152
if (lastRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR || lastRoute.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR) {
// if we are not in the target report, we need to navigate to it after dismissing the modal
if (targetReportID && targetReportID !== getTopmostReportId(rootState)) {
const state = getStateFromPath(ROUTES.getReportRoute(targetReportID));

const action = getActionFromState(state, linkingConfig.config);
action.type = 'REPLACE';
navigationRef.current.dispatch(action);
} else {
navigationRef.current.dispatch(StackActions.pop());
switch (lastRoute.name) {
case NAVIGATORS.RIGHT_MODAL_NAVIGATOR:
case NAVIGATORS.FULL_SCREEN_NAVIGATOR:
case SCREENS.REPORT_ATTACHMENTS:
// if we are not in the target report, we need to navigate to it after dismissing the modal
if (targetReportID && targetReportID !== getTopmostReportId(rootState)) {
const state = getStateFromPath(ROUTES.getReportRoute(targetReportID));

const action = getActionFromState(state, linkingConfig.config);
action.type = 'REPLACE';
navigationRef.current.dispatch(action);
} else {
navigationRef.current.dispatch(StackActions.pop());
}
break;
default: {
Log.hmmm('[Navigation] dismissModal failed because there is no modal stack to dismiss');
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I found it necessary to introduce an extra OR condition in our logic. For better readability and understanding of the flow, I decided to convert this structure into a switch statement.

@allroundexperts
Copy link
Contributor

@kidroca Can you please resolve the conflicts again?

@rushatgabhane
Copy link
Member

rushatgabhane commented Jun 19, 2023

@allroundexperts just dropping a friendly note that you should review the code without waiting for @kidroca to resolve the conflicts. Especially if the conflicts are something trivial.

image

@allroundexperts
Copy link
Contributor

@allroundexperts just dropping a friendly note that you should review the code without waiting for @kidroca to resolve the conflicts. Especially if the conflicts are something trivial.

Thanks @rushatgabhane!
I'll just continue with the review then.

# Conflicts:
#	src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js
#	src/libs/Navigation/Navigation.js
@kidroca
Copy link
Contributor Author

kidroca commented Jun 19, 2023

@allroundexperts @rushatgabhane

The updates introduced in PR #20338 have led to a new merge conflict that isn't straightforward to resolve. In these changes, the AttachmentModal now expects a full report object via the report prop. However, my solution—where the modal functions as a route—relied on simple, serializable values such as reportID.

The approach from my PR, which depended on receiving only the report ID as a string, is no longer compatible:

const route = ROUTES.getReportAttachmentRoute(report.reportID, source);
Navigation.navigate(route);

To address this, I propose adjusting the logic so that we continue to pass only the reportID as a route parameter, with the screen subsequently obtaining and passing down the report object. This approach necessitates some modifications to my current logic and a partial reversion of the changes that introduced the merge conflict.

@kidroca
Copy link
Contributor Author

kidroca commented Jun 21, 2023

Conflicts resolved ✅

Screenshots / videos updated

Copy link
Contributor

@allroundexperts allroundexperts left a comment

Choose a reason for hiding this comment

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

This is looking good. All yours @NikkiWines!

Copy link
Contributor

@NikkiWines NikkiWines left a comment

Choose a reason for hiding this comment

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

Looks good and tests well - Going to add @chiragsalian as a reviewer for a third set of eyes since he helped review the proposal

Comment on lines +22 to +24
const reportID = _.get(props, ['route', 'params', 'reportID']);
const report = ReportUtils.getReport(reportID);
const source = decodeURI(_.get(props, ['route', 'params', 'source']));
Copy link
Contributor

Choose a reason for hiding this comment

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

NAB: these can all be used inline instead of declared separately

@NikkiWines NikkiWines requested a review from chiragsalian June 22, 2023 00:40
Copy link
Contributor

@chiragsalian chiragsalian left a comment

Choose a reason for hiding this comment

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

Code LGTM and seems to work well. There is a feature freeze atm so i won't be merging this PR but i had one question, shouldn't we update the attachment URL when the user switches between attachments? I'm a little concerned that it could be confusing for a user seeing the same attachment URL while switching between multiple attachments

@allroundexperts
Copy link
Contributor

Code LGTM and seems to work well. There is a feature freeze atm so i won't be merging this PR but i had one question, shouldn't we update the attachment URL when the user switches between attachments? I'm a little concerned that it could be confusing for a user seeing the same attachment URL while switching between multiple attachments

I had the same question @chiragsalian. I agree that it is confusing for the user.

@kidroca
Copy link
Contributor Author

kidroca commented Jun 27, 2023

I appreciate the thought of changing the URL to provide feedback to the user when they switch attachments. This could indeed be helpful, especially in scenarios where the same attachment might be uploaded multiple times consecutively.

However, I am unsure about the overall benefit of this change. Only a small portion of the URL (around 5-10 digits we see) would vary between different attachments, while the majority of the URL would remain the same. This might not provide a significant difference in feedback to the user.

If we decide to proceed with this change, I would suggest implementing it as a separate pull request. This way, it would be easier to review and test independently.

@NikkiWines
Copy link
Contributor

Feature freeze is over! @kidroca might be good just to pull main into this just to be safe, but we should be good to merge this 🚀

@chiragsalian
Copy link
Contributor

Hmm sure i am fine with a separate pull request to make things easier to review/test.

Only a small portion of the URL (around 5-10 digits we see) would vary between different attachments, while the majority of the URL would remain the same.

Yeah no matter how small the change i like URLs to match what the user sees. Its always possible one chat report to another has just 1 different digit, but it is still valuable to keep the URL up to date imo.

@chiragsalian
Copy link
Contributor

I'll go ahead and merge this PR to get it out the door soon. If something is wrong with it QA should hopefully catch problems. I'll mention in the issue that we'll be updating the URL realtime as a follow up.

@chiragsalian chiragsalian merged commit 206f107 into Expensify:main Jun 30, 2023
@OSBotify
Copy link
Contributor

OSBotify commented Jul 1, 2023

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify
Copy link
Contributor

OSBotify commented Jul 3, 2023

🚀 Deployed to staging by https://github.com/chiragsalian in version: 1.3.36-0 🚀

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

@Julesssss
Copy link
Contributor

Hey @kidroca, is it possible that changing how our attachments work triggered this issue? #22133

If so I don't think it's necessarily a App regression, but knowing this might help us to fix the issue.

@kidroca kidroca deleted the kidroca/feat/attachment-modal-route branch July 4, 2023 13:03
@kidroca
Copy link
Contributor Author

kidroca commented Jul 4, 2023

@Julesssss
I don't think so, I've explicitly included a test step regarding downloading various attachments in the PR description:

Download a variety of attachments and check that the downloading functionality remains intact.

If this PR did break downloads it should have been caught
I've also tested downloading myself and it worked

I'll take a look for a potential cause as I'm already working on another carousel update

@Julesssss
Copy link
Contributor

Julesssss commented Jul 4, 2023

Thanks for looking, what you said makes sense 👍 This is just occuring on staging, so it might not have shown up anyway during testing.

@deetergp
Copy link
Contributor

deetergp commented Jul 4, 2023

We have a blocker (a relatively trivial one, IMO) that was introduced in this last release and since this PR is the only one that has to do with attachments, I'm wonder if didn't introduce this bug.

@kidroca
Copy link
Contributor Author

kidroca commented Jul 5, 2023

@deetergp, you've made a valid point.

Prior to this, I was unaware of the existing functionality that relied upon a specific hierarchy to restrict drag and drop actions over attachments. It's plausible that by altering this hierarchy, I unintentionally introduced the issue at hand.

@OSBotify
Copy link
Contributor

OSBotify commented Jul 5, 2023

🚀 Deployed to production by https://github.com/Julesssss in version: 1.3.36-5 🚀

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

function ReportAttachments(props) {
const reportID = _.get(props, ['route', 'params', 'reportID']);
const report = ReportUtils.getReport(reportID);
const source = decodeURI(_.get(props, ['route', 'params', 'source']));
Copy link
Contributor

Choose a reason for hiding this comment

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

The source can be a number (viewing assets in native) but the decoded uri is always a string which makes the image fail to load. We needed to preserve the type of the passed source. (Coming from #31138)

@parasharrajat
Copy link
Member

We missed handling a case for inaccessible report images. When the user opens a link to attachment where the user does not have access to the report, the attachment modal keeps on loading. #23374

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.

10 participants