Expensify / App

Welcome to New Expensify: a complete re-imagination of financial collaboration, centered around chat. Help us build the next generation of Expensify by sharing feedback and contributing to the code.
https://new.expensify.com
MIT License
3.48k stars 2.83k forks source link

[$250] Chat: "Unread" marker appears when the user sends a new message #50469

Open m-natarajan opened 1 week ago

m-natarajan commented 1 week ago

If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!


Version Number: 9.0.46-2 Reproducible in staging?: y Reproducible in production?: y If this was caught during regression testing, add the test name, ID and link from TestRail: Email or phone of affected tester (no customers): Logs: https://stackoverflow.com/c/expensify/questions/4856 Expensify/Expensify Issue URL: Issue reported by: @paultsimura Slack conversation: https://expensify.slack.com/archives/C049HHMV9SM/p1728390912425749

Action Performed:

  1. Open the chat with long history.
  2. Scroll up until last messages are not visible.
  3. Send a message.
  4. Immediately scroll up.

Workaround:

unknown

Platforms:

Which of our officially supported platforms is this issue occurring on?

Screenshots/Videos

https://github.com/user-attachments/assets/2b7ab579-9309-4ff1-adca-f8a60e3edf04

https://github.com/user-attachments/assets/ae7a35e5-fe3e-48c9-8959-d9382135a1dc

Add any screenshot/video evidence

View all open jobs on GitHub

Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~021844272500597248585
  • Upwork Job ID: 1844272500597248585
  • Last Price Increase: 2024-10-10
Issue OwnerCurrent Issue Owner: @ikevin127
melvin-bot[bot] commented 1 week ago

Triggered auto assignment to @lschurr (Bug), see https://stackoverflow.com/c/expensify/questions/14418 for more details. Please add this bug to a GH project, as outlined in the SO.

MonilBhavsar commented 1 week ago

I would like to curate this so assigning myself

lschurr commented 1 week ago

Do you need BugZero on this @MonilBhavsar or will it be all internal?

MonilBhavsar commented 6 days ago

We need BugZero and C+ on this. Going to export it

melvin-bot[bot] commented 6 days ago

Job added to Upwork: https://www.upwork.com/jobs/~021844272500597248585

melvin-bot[bot] commented 6 days ago

Triggered auto assignment to Contributor-plus team member for initial proposal review - @ikevin127 (External)

daledah commented 6 days ago

Edited by proposal-police: This proposal was edited at 2024-10-10 08:56:49 UTC.

Proposal

Please re-state the problem that we are trying to solve in this issue.

The "New messages" pill appears;

What is the root cause of that problem?

unreadMarkerReportActionID returns the new action when users add new comment, the reason is because we don't have the logic to exclude the message that comes from current user

What changes do you think we should make in order to solve the problem?

In

https://github.com/Expensify/App/blob/175af25b5ac2645a7d1f7edf87fd35e4529cd9be/src/pages/home/report/ReportActionsList.tsx#L483-L488

when checking hasUnreadReportAction we exclude the current user's message so we should do the same in

https://github.com/Expensify/App/blob/175af25b5ac2645a7d1f7edf87fd35e4529cd9be/src/pages/home/report/ReportActionsList.tsx#L224

            const shouldDisplay = isCurrentMessageUnread && isNextMessageRead && !ReportActionsUtils.shouldHideNewMarker(reportAction) && (
                (ReportActionsUtils.isReportPreviewAction(reportAction) ? reportAction.childLastActorAccountID : reportAction.actorAccountID) !== Report.getCurrentUserAccountID()
            );

What alternative solutions did you explore? (Optional)

  1. We can consider to create the util function to avoid duplicate code
  2. We can update isMessageUnread function to exclude the current user's message
bernhardoj commented 5 days ago

Edited by proposal-police: This proposal was edited at 2024-10-15 14:10:25 UTC.

Proposal

Please re-state the problem that we are trying to solve in this issue.

An unread marker shows every time we add a message.

What is the root cause of that problem?

This happens after https://github.com/Expensify/App/pull/49367. Previously, we ignore any message that the current user sends, unless the user mark it as unread manually.

The unread marker shows because the scroll offset is out of the threshold (250) and isMessageUnread for the new message is true. https://github.com/Expensify/App/blob/285bde8073f84dad0ca6d63ef6ec251b10e52882/src/pages/home/report/ReportActionsList.tsx#L224-L226

isMessageUnread will be true if the report action created value is bigger than the unreadMarkerTime. https://github.com/Expensify/App/blob/285bde8073f84dad0ca6d63ef6ec251b10e52882/src/pages/home/report/ReportActionsList.tsx#L125-L131

The unreadMarkerTime is updated in 3 cases as written on the comment below. https://github.com/Expensify/App/blob/285bde8073f84dad0ca6d63ef6ec251b10e52882/src/pages/home/report/ReportActionsList.tsx#L202-L214

For the 3rd case, we have this useEffect that updates the unreadMarkerTime, but only if unreadMarkerReportActionID doesn't exist, which exist in our case because we are out of the scroll threshold.

https://github.com/Expensify/App/blob/285bde8073f84dad0ca6d63ef6ec251b10e52882/src/pages/home/report/ReportActionsList.tsx#L265-L278

What changes do you think we should make in order to solve the problem?

We will have a new useEffect that is similar to the 3rd case, but in this case, we will only update the unreadMarkerTime if there is a new last action created value from the current user and there is previously no unread marker.

When user adds a new message and there is currently an unread marker, the prevUnreadMarkerReportActionID will always have a value.

For example, the current user mark A as unread: prevUnreadMarkerReportActionID: undefined unreadMarkerReportActionID: A

then, the current user sends a message. prevUnreadMarkerReportActionID will now contain A.

const prevUnreadMarkerReportActionID = usePrevious(unreadMarkerReportActionID);
const lastAction = sortedVisibleReportActions.at(0);
useEffect(() => {
    if (prevUnreadMarkerReportActionID || !lastAction) {
        return;
    }

    const isFromCurrentUser = accountID === (ReportActionsUtils.isReportPreviewAction(lastAction) ? !lastAction.childLastActorAccountID : lastAction.actorAccountID);

    if (!isFromCurrentUser || lastAction.created <= unreadMarkerTime) {
        return;
    }

    setUnreadMarkerTime(lastAction.created);
}, [prevUnreadMarkerReportActionID, lastAction?.created]);

What alternative solutions did you explore? (Optional)

We need to add a new case for updating unreadMarkerTime if a new message is added by the current user, even when the user scroll offset is out of threshold.

Luckily, we already have an observer to detect a new message from the current user. https://github.com/Expensify/App/blob/285bde8073f84dad0ca6d63ef6ec251b10e52882/src/pages/home/report/ReportActionsList.tsx#L371 https://github.com/Expensify/App/blob/285bde8073f84dad0ca6d63ef6ec251b10e52882/src/pages/home/report/ReportActionsList.tsx#L341-L353

We can update the unreadMarkerTime inside the callback, scrollToBottomForCurrentUserAction, but that only covers for optimistic case. After the AddComment API success, the BE will send us a new action created which is bigger from the optimistic created, so we still need to update the unreadMarkerTime when receiving the API response.

To do that, we can have a new ref called lastActionAddedByCurrentUser. We set the ref every time we send a message. reportActionID is from the 2nd parameter of the callback (scrollToBottomForCurrentUserAction).

if (!unreadMarkerReportActionID) {
    lastActionAddedByCurrentUser.current = reportActionID;
}

It's important to check for existing unread marker to not remove it when adding a new message, which is the current behavior. If we want to remove the marker when adding a new message, then we can remove the if. Make sure to add unreadMarkerReportActionID to the useCallback deps and add scrollToBottomForCurrentUserAction to this useEffect where it's being registered. https://github.com/Expensify/App/blob/285bde8073f84dad0ca6d63ef6ec251b10e52882/src/pages/home/report/ReportActionsList.tsx#L371-L385

Next, adds a useEffect that updates unreadMarkerTime whenever the last action is updated. If the last action from props is the same as the last action saved to the ref, then we update the unreadMarkerTime to that last action created, so unreadMarkerTime will equal to the last action created.

const lastAction = sortedVisibleReportActions.at(0);
useEffect(() => {
    if (!lastActionAddedByCurrentUser.current || lastAction?.reportActionID !== lastActionAddedByCurrentUser.current) {
        return;
    }
    setUnreadMarkerTime(lastAction.created);

    if (lastAction.isOptimisticAction) {
        return;
    }

    lastActionAddedByCurrentUser.current = undefined;
}, [lastAction]);

lastActionAddedByCurrentUser should be cleared after being used, but we only want to clear that if the last action is not an optimistic action anymore because we still want to update the unreadMarkerTime when receiving the new action from the API response.

Next, return false here if the report action is the same as the last action added by user. https://github.com/Expensify/App/blob/285bde8073f84dad0ca6d63ef6ec251b10e52882/src/pages/home/report/ReportActionsList.tsx#L220-L227

return shouldDisplay && isWithinVisibleThreshold && reportAction.reportActionID !== lastActionAddedByCurrentUser.current;

Last, we need to clear lastActionAddedByCurrentUser when mark as unread/read manually. https://github.com/Expensify/App/blob/285bde8073f84dad0ca6d63ef6ec251b10e52882/src/pages/home/report/ReportActionsList.tsx#L245-L251

ikevin127 commented 5 days ago

♻️ Reviewing proposals...

ikevin127 commented 5 days ago

From my experience with the ReportActionsList.tsx unread marker functionality I know that there's always more to consider and whenever a straight-forward fix was suggested in the past, we always had regressions because there's a lot of functionality which can be disrupted even with small changes.

Given this I'm inclined to go with bernhardoj's proposal since it mentioned the offending PR which caused the issue and also both RCA and solution proposed are more detailed and consider multiple aspects of the current functionality - hence it shows better understanding of the issue at hand.

However, before moving on with assignment here I want test the proposed solution thoroughly and because I don't want to miss any part of the solution, @bernhardoj can you please share a test branch with the complete solution so I can test it ?

♻️ Will get back on this once I get the chance to test the proposed solution.

bernhardoj commented 4 days ago

@ikevin127 here is the test branch

ikevin127 commented 2 days ago

@bernhardoj Thanks for putting together the test branch, I got to test the fix implementation and while it does seem to work compared to staging:

Compare Videos | Staging | Test Branch | | ------------- | ------------- | |

⚠️ However I found an edge case in which the fix doen't work, allow me to explain the steps and show you a video demonstrating the issue:

[!tip]

Steps to reproduce

  1. Login into the same account on 2 separate browser windows, one on local dev (with testing branch) and one on current staging (https://staging.new.expensify.com).
  2. On local dev (with testing branch), verify that you can NOT reproduce the issue by following the steps in issue's description.
  3. On local dev (with testing branch), make sure there is no new message marker or pill showing and scroll up such that you cannot see the last message.
  4. On the same account / report on Staging, verify that you CAN reproduce the issue by following the steps in issue's description.
  5. Now returning to the local dev test branc, observe that the new message marker and pill are showing even though you posted them with the same user account from Staging.
  6. On local dev (with testing branch), mark as read to ensure there is no new message marker or pill showing then repeat step (2.) and notice that the fix does not work anymore after doing the Staging / Dev back n' forth.

[!note] The same thing happens if the steps are followed on 2 different sessions of the local dev (with testing branch) for example if logging in with the same account and following the above steps on 2 different browsers.

Here's a video for better visualisation:

Video https://github.com/user-attachments/assets/5b34bd19-112f-4c45-9789-5a14594b2f86

Given this, from my side I see two issues here:

  1. Regarding step (5.) and according to the issue's expected result, we should not see the new message marker and pill showing-up because the message was posted by the same user, even though it was posted from a different (Staging) session.
  2. Regarding step (6.) which seems to somehow be caused and linked to step (5.), it looks like when the edge case steps are performed, something happens which invalidates the solution implementation and causes it to not work anymore.
bernhardoj commented 1 day ago

I updated my proposal with a different solution to cover that (and moved the previous main solution as the alternative).

ikevin127 commented 1 day ago

Thanks for the update, I'll review again today.

ikevin127 commented 5 hours ago

I reviewed @bernhardoj's updated proposal's main solution again and this time it passed the tests. Given this I think we're good to go with their proposal as they show a deep understanding of the issue / functionality, the RCA is valid and the updated solution fixes the issue as per the expected result, without altering current working functionality.

For reference, here's how I implemented the solution locally:

Solution Code ```tsx const [session] = useOnyx(ONYXKEYS.SESSION); const prevUnreadMarkerReportActionID = usePrevious(unreadMarkerReportActionID); const currentUserAccountID = Number(session?.accountID); const lastAction = sortedVisibleReportActions.at(0); useEffect(() => { if (prevUnreadMarkerReportActionID || !lastAction) { return; } const isFromCurrentUser = currentUserAccountID === (ReportActionsUtils.isReportPreviewAction(lastAction) ? !lastAction.childLastActorAccountID : lastAction.actorAccountID); if (!isFromCurrentUser || lastAction.created <= unreadMarkerTime) { return; } setUnreadMarkerTime(lastAction.created); }, [prevUnreadMarkerReportActionID, lastAction?.created]); ```

[!tip]

Notes for PR

  1. Make sure to comment the new useEffect in detail for others to understand why we need it and what it does.
  2. Ensure thorough testing on narrow layout devices (native / mweb) since on these devices the unread marker / pill can work slightly different because of the DeviceEventEmitter listeners and LHN / Central Pane not being mounted both at the same time.

🎀👀🎀 C+ reviewed

melvin-bot[bot] commented 5 hours ago

Current assignee @MonilBhavsar is eligible for the choreEngineerContributorManagement assigner, not assigning anyone new.