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

Show static skeleton UI if no reportAction exists for chat report in ONYX when the client is offline #13233

Closed
3 changes: 2 additions & 1 deletion src/pages/home/ReportScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ class ReportScreen extends React.Component {
// It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that
// is not stored locally yet. If props.report.reportID exists, then the report has been stored locally and nothing more needs to be done.
// If it doesn't exist, then we fetch the report from the API.
if (this.props.report.reportID) {
// If the client is offline, we cannot fetch the report, so let's just return.
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment explains what we are doing but not "why".

Looking at this - and I don't understand why being offline should prevent us from making this request. Here's my understanding of how our networking layer works:

  1. OpenReport is a "write" request because we update the "last read" while fetching the report
  2. If we are offline we will queue any "write" requests

App/src/libs/API.js

Lines 47 to 48 in 0ed2636

// Write commands can be saved and retried, so push it to the SequentialQueue
SequentialQueue.push(request);

// Add request to Persisted Requests so that it can be retried if it fails
PersistedRequests.save([request]);
// If we are offline we don't need to trigger the queue to empty as it will happen when we come back online
if (NetworkStore.isOffline()) {
return;
}

  1. When we come back from offline -> online those queued "write" requests will run

// Flush the queue when the connection resumes
NetworkStore.onReconnection(flush);

In this case, we are offline - so shouldn't it be fine to make the request? And when we come back online the report will be fetched?

Is there some problem with the way this is working now that needs to be addressed? Because I want to say it's an anti-pattern to prevent a "write" from happening if we are offline (as that's what the queue itself was designed to do) but maybe I am missing something.

Copy link
Contributor Author

@techievivek techievivek Dec 30, 2022

Choose a reason for hiding this comment

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

The reason I added this change was otherwise in the openReport method call

optimisticData: [{
onyxMethod: CONST.ONYX.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
value: {
isLoadingReportActions: true,
isLoadingMoreReportActions: false,

it would set isLoadingReportActions: true and because of that in the ReportScreen.js
{(this.isReportReadyForDisplay() && !isLoadingInitialReportActions) && (
<>
<ReportActionsView
reportActions={this.props.reportActions}
report={this.props.report}

we will keep showing the animating full-screen skeletonUI and the reportActionsView will never get loaded. I don't think that is expected if we want users to take optimistic actions here. What do you think about it?

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 change and one below in ReportActionsView page are mainly to prevent setting value for a report prop. Do you have any recommendations on how to handle this better?

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's stop here and consult the team on the best way to handle this.

The main question in my mind is why we should ever set any loading state before the API request actually begins? In the current design, it seems like we have a hybrid where loading states are set to true even though we are offline and not actually "loading" anything.

Taking a look at our usages elsewhere - and this isLoading is mostly only being used in a few "blocking read requests" (stuff related to payments and bank accounts) that I don't think will ever happen unless we are online. So it feels like there's an inconsistency or exception being made for the report page - and I'm not sure it's valid.

if (this.props.report.reportID || this.props.network.isOffline) {
return;
}

Expand Down
16 changes: 8 additions & 8 deletions src/pages/home/report/ReportActionsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as ReportScrollManager from '../../../libs/ReportScrollManager';
import styles from '../../../styles/styles';
import * as ReportUtils from '../../../libs/ReportUtils';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions';
import {withNetwork, withPersonalDetails} from '../../../components/OnyxProvider';
import {withPersonalDetails} from '../../../components/OnyxProvider';
import ReportActionItem from './ReportActionItem';
import ReportActionsSkeletonView from '../../../components/ReportActionsSkeletonView';
import variables from '../../../styles/variables';
Expand All @@ -19,7 +19,6 @@ import reportActionPropTypes from './reportActionPropTypes';
import CONST from '../../../CONST';
import * as StyleUtils from '../../../styles/StyleUtils';
import reportPropTypes from '../../reportPropTypes';
import networkPropTypes from '../../../components/networkPropTypes';

const propTypes = {
/** Position of the "New" line marker */
Expand Down Expand Up @@ -49,9 +48,6 @@ const propTypes = {
/** Function to load more chats */
loadMoreChats: PropTypes.func.isRequired,

/** Information about the network */
network: networkPropTypes.isRequired,

...withDrawerPropTypes,
...windowDimensionsPropTypes,
};
Expand Down Expand Up @@ -145,6 +141,10 @@ class ReportActionsList extends React.Component {
}

render() {
// Non-animating skeleton UI represents that chat history exists but is not loaded locally in Onyx.
Copy link
Contributor

Choose a reason for hiding this comment

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

Non-animating skeleton UI represents that

I'd rephrase this in this format

When [x] happens a non-animating skeleton UI is shown because...

But honestly, I'm a bit confused about how to finish that sentence.

I'm having trouble understanding the idea that "chat history exists but is not loaded locally in Onyx" so I think we need to improve this comment more generally.

Does it mean that the data exists in storage, but not yet read into memory?

Or does it mean that we know some data exists on the server, but we haven't tried to load it yet?

Let's please try to be very specific here so that people don't have to work too hard to understand why we are doing something.

Copy link
Contributor Author

@techievivek techievivek Dec 30, 2022

Choose a reason for hiding this comment

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

Or does it mean that we know some data exists on the server, but we haven't tried to load it yet?

This is the case here, I will update it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome thanks! And to make sure I understand - we know this is the case because we have some report data already stored, the report is "not optimistic", and no report actions?

Copy link
Contributor Author

@techievivek techievivek Jan 5, 2023

Choose a reason for hiding this comment

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

Yep, that's my understanding so far 😄 (This is my first App PR in like 4-5 months).

// While offline if there is at least one non-pending action or if this is a new chat then we don't need to show the non-animating skeleton UI.
Copy link
Contributor

Choose a reason for hiding this comment

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

How does being offline come into play here? This component does not seem aware of the network status anymore since we are removing withNetwork.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Aaah, I was just trying to explain in comments and used that word. I will get rid of it.

const shouldShowNonAnimatingSkeleton = !this.props.report.isOptimisticReport && _.every(this.props.sortedReportActions, action => action.pendingAction);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const shouldShowNonAnimatingSkeleton = !this.props.report.isOptimisticReport && _.every(this.props.sortedReportActions, action => action.pendingAction);
const shouldShowStaticSkeleton = !this.props.report.isOptimisticReport && _.every(this.props.sortedReportActions, action => action.pendingAction);

We generally prefer variables that describe what something is vs. what it is not.

Copy link
Contributor

Choose a reason for hiding this comment

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

Alternatively, change this to shouldAnimateSkeleton and then we don't have to invert later !shouldShowNonAnimatingSkeleton. I think I like that suggestion better.

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems unrelated to the changes here. But I'm kind of confused about why and how isOptimisticReport got introduced. I'd think that an optimistic report would have a pendingAction and not a separate flag to denote it as "optimistic".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We have decided to show the static skeleton UI for the chats that we know exist on the server but aren't loaded locally in ONYX.

Now if the chat is not loaded locally and we are offline then we can still take optimistic action but we will need to keep showing the static skeleton UI. But in the case of a new chat report, we don't have to show the static skeleton UI because we know this chat report doesn't exist on our server. But if I just check by pendingAction then static skeleton UI would show up for new chats as well so to filter them out I have added the condition to see if it is an optimistic report(which I think denotes a new chat created while offline).

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah hmm, that's good context, but your response here isn't directly addressing either of my two concerns above (though perhaps you meant to add this context to a different thread).

Would you mind asking a follow up question or restating what you think I'm asking about here? I am happy to clarify things or say them in a different way to help us get on the same page.

Copy link
Contributor Author

@techievivek techievivek Jan 5, 2023

Choose a reason for hiding this comment

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

Aaah, my bad. I see your question now. But to be honest, I don't have any explanation for this one(will have to check the blame). I used it because it was right in the report object, but this could have been easily computed as well.


// Native mobile does not render updates flatlist the changes even though component did update called.
// To notify there something changes we can use extraData prop to flatlist
const extraData = (!this.props.isDrawerOpen && this.props.isSmallScreenWidth) ? this.props.newMarkerSequenceNumber : undefined;
Expand Down Expand Up @@ -177,12 +177,13 @@ class ReportActionsList extends React.Component {
// Make sure the oldest report action loaded is not the first. This is so we do not show the
// skeleton view above the created action in a newly generated optimistic chat or one with not
// that many comments.
// We will show non-animating skeleton UI if we are offline and chat history is not loaded in ONYX.
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment does not explain "why" only the "what". Also let's try to be more consistent about capitalization. In other comments we have "Onyx" and this one "ONYX".

Suggested change
// We will show non-animating skeleton UI if we are offline and chat history is not loaded in ONYX.
// We will show a static skeleton UI if we are offline and chat history is not loaded in Onyx because...

const lastReportAction = _.last(this.props.sortedReportActions);
if (this.props.report.isLoadingReportActions && lastReportAction.sequenceNumber > 0) {
if ((this.props.report.isLoadingReportActions && lastReportAction.sequenceNumber > 0) || shouldShowNonAnimatingSkeleton) {
return (
<ReportActionsSkeletonView
containerHeight={this.state.skeletonViewHeight}
animate={!this.props.network.isOffline}
animate={!shouldShowNonAnimatingSkeleton}
/>
);
}
Expand Down Expand Up @@ -211,5 +212,4 @@ export default compose(
withDrawerState,
withWindowDimensions,
withPersonalDetails(),
withNetwork(),
)(ReportActionsList);
6 changes: 3 additions & 3 deletions src/pages/home/report/ReportActionsView.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ class ReportActionsView extends React.Component {

// If the report is optimistic (AKA not yet created) we don't need to call openReport again
openReportIfNecessary() {
if (this.props.report.isOptimisticReport) {
if (this.props.report.isOptimisticReport || this.props.network.isOffline) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The first comment applies here too.

return;
}

Expand All @@ -283,7 +283,7 @@ class ReportActionsView extends React.Component {
*/
loadMoreChats() {
// Only fetch more if we are not already fetching so that we don't initiate duplicate requests.
if (this.props.report.isLoadingMoreReportActions) {
if (this.props.report.isLoadingMoreReportActions || this.props.network.isOffline) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we preventing this request from happening?

Copy link
Contributor Author

@techievivek techievivek Dec 30, 2022

Choose a reason for hiding this comment

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

I have prevented calling this otherwise it would render an animating skeleton UI in the ReportActionList.js page.

if (this.props.report.isLoadingMoreReportActions) {
return (
<ReportActionsSkeletonView
containerHeight={CONST.CHAT_SKELETON_VIEW.AVERAGE_ROW_HEIGHT * 3}
/>

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok I think this is also related to the weird issue where we are setting "loading" states when no "loading" is actually happening and we should pause until there is agreement about how to proceed with that.

return;
}

Expand Down Expand Up @@ -352,7 +352,7 @@ class ReportActionsView extends React.Component {

render() {
// Comments have not loaded at all yet do nothing
if (!_.size(this.props.reportActions)) {
if (!_.size(this.props.reportActions) && !this.props.network.isOffline) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this needs a comment. It's not obvious why we should choose to return null when there are no report actions and we are online.

Copy link
Contributor

Choose a reason for hiding this comment

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

It also feels like maybe we should not be rendering the ReportActionsView at all if we are offline and have no actions.

Copy link
Contributor Author

@techievivek techievivek Dec 30, 2022

Choose a reason for hiding this comment

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

I guess this whole flow is a bit messed up. I am using ListFooterComponent to show the static skeleton UI so I have this additional condition that will prevent ReportActionsView from returning null.

Another thing I can explore is to show the static skeleton view right from the ReportScreen page.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess this whole flow is a bit messed up.

I agree and think it's a possible sign that we are gonna make it even more messed up than it already is.

I would propose we stop exploring code changes for a moment and instead try to pull things back into the realm of problem/solution.

Next steps:

  1. Figure out what problem we need to solve (I think we have mostly done this on the issue)
  2. Look at how things work today. Does the existing code easily support it or not? Does an obvious solution present itself or not? What solution should we ultimately go with?
  3. Summarize our best idea and take it to a few people to see what they think (either #engineering-chat or #expensify-open-source seems good)
  4. Agree on a plan forward + make sure everyone understands the general thought process behind the changes
  5. Make the changes

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this discussion happening? If yes, it's the channel I don't have access to?

Copy link
Contributor

Choose a reason for hiding this comment

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

@aimane-chnaif No, @techievivek has been OOO - so you're not missing out on anything I believe 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Catching up, I will open a thread to discuss this issue in the open-source channel by tomorrow.

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 have opened a discussion here feel free to jump to add your thoughts.

return null;
}

Expand Down