Skip to content

[Due for payment 2025-11-12] [$250] API - Duplicate ReadNewestAction API call for new messages #72774

Closed

Description

@mitarachim

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: v9.2.32-0
Reproducible in staging?: Yes
Reproducible in production?: Yes
If this was caught during regression testing, add the test name, ID and link from BrowserStack: https://test-management.browserstack.com/projects/2219752/test-runs/TR-1305/folder/13176688/52080502/955759034
Email or phone of affected tester (no customers): htad26+eriuiyui@gmail.com
Issue reported by: Applause Internal Team
Device used: Windows 10/Chrome
App Component: Other

Action Performed:

Preconditions
User A and User B already have a chat history

  1. As User A, ensure the currently opened chat screen is not the chat report with User B
  2. As User B, send a message to User A's chat report
  3. As User A, wait until the message has been received
  4. As user A, open the browser's Network tab > Filter the network traffic for the /ReadNewestAction
  5. As User A, navigate to User B's chat report

Expected Result:

Only a single instance of the /ReadNewestAction API call is initiated for the chat report

Actual Result:

Duplicate instances of the /ReadNewestAction API call are initiated for the chat report

Workaround:

Unknown

Platforms:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome / Safari
  • MacOS: Desktop

Screenshots/Videos

Bug6976526_1760614732363.2025-10-16_14_30_58.mp4
Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~021990497809920039578
  • Upwork Job ID: 1990497809920039578
  • Last Price Increase: 2025-11-17
Issue OwnerCurrent Issue Owner: @

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 16, 2025
  2. melvin-bot commented on Oct 16, 2025

    @melvin-bot

    Triggered auto assignment to @garrettmknight (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.

  3. dmkt9 commented on Oct 16, 2025

    @dmkt9
    Contributor

    Proposal

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

    Duplicate instances of the /ReadNewestAction API call are initiated for the chat report

    What is the root cause of that problem?

    readNewestAction is called sequentially in two useEffect blocks here:

    useEffect(() => {
    if (report.reportID !== prevReportID) {
    return;
    }
    if (isUnread(report, transactionThreadReport, isReportArchived) || (lastAction && isCurrentActionUnread(report, lastAction, sortedVisibleReportActions))) {
    // On desktop, when the notification center is displayed, isVisible will return false.
    // Currently, there's no programmatic way to dismiss the notification center panel.
    // To handle this, we use the 'referrer' parameter to check if the current navigation is triggered from a notification.
    const isFromNotification = route?.params?.referrer === CONST.REFERRER.NOTIFICATION;
    if ((isVisible || isFromNotification) && scrollingVerticalOffset.current < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD) {
    readNewestAction(report.reportID);
    if (isFromNotification) {
    Navigation.setParams({referrer: undefined});
    }
    } else {
    readActionSkipped.current = true;
    }
    }
    // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
    }, [report.lastVisibleActionCreated, transactionThreadReport?.lastVisibleActionCreated, report.reportID, isVisible]);

    useEffect(() => {
    if (report.reportID !== prevReportID) {
    return;
    }
    if (!isVisible || !isFocused) {
    if (!lastMessageTime.current) {
    lastMessageTime.current = lastAction?.created ?? '';
    }
    return;
    }
    // In case the user read new messages (after being inactive) with other device we should
    // show marker based on report.lastReadTime
    const newMessageTimeReference = lastMessageTime.current && report.lastReadTime && lastMessageTime.current > report.lastReadTime ? userActiveSince.current : report.lastReadTime;
    lastMessageTime.current = null;
    const isArchivedReport = isArchivedNonExpenseReport(report, isReportArchived);
    const hasNewMessagesInView = scrollingVerticalOffset.current < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD;
    const hasUnreadReportAction = sortedVisibleReportActions.some(
    (reportAction) =>
    newMessageTimeReference &&
    newMessageTimeReference < reportAction.created &&
    (isReportPreviewAction(reportAction) ? reportAction.childLastActorAccountID : reportAction.actorAccountID) !== getCurrentUserAccountID(),
    );
    if (!isArchivedReport && (!hasNewMessagesInView || !hasUnreadReportAction)) {
    return;
    }
    readNewestAction(report.reportID);
    userActiveSince.current = DateUtils.getDBTime();
    // This effect logic to `mark as read` will only run when the report focused has new messages and the App visibility
    // is changed to visible(meaning user switched to app/web, while user was previously using different tab or application).
    // We will mark the report as read in the above case which marks the LHN report item as read while showing the new message
    // marker for the chat messages received while the user wasn't focused on the report or on another browser tab for web.
    // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
    }, [isFocused, isVisible]);

    Each useEffect serves a different purpose. One calls readNewestAction whenever there are unread messages, while the other triggers it when the ReportScreen is re-focused or when the app returns from the background to the foreground.

    // This effect logic to `mark as read` will only run when the report focused has new messages and the App visibility
    // is changed to visible(meaning user switched to app/web, while user was previously using different tab or application).
    // We will mark the report as read in the above case which marks the LHN report item as read while showing the new message
    // marker for the chat messages received while the user wasn't focused on the report or on another browser tab for web.

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

    There are two solutions:

    Solution 1:
    We can merge the two useEffect blocks by adding preIsVisible and preIsFocused to ensure the second effect’s behavior is preserved.

    Solution 2:
    We can create a ref to store the state that determines whether readNewestAction should be called in the second useEffect.
    If readNewestAction has already been triggered in the first useEffect, the state will be set to false.
    Then, it can be reset to true in the second useEffect.

    Or more simply, we could use useDebounce for readNewestAction with a reasonable delay.

  4. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 16, 2025
  5. changed the title [-]API - Duplicate ReadNewestAction API call for new messages[/-] [+][$250] API - Duplicate ReadNewestAction API call for new messages[/+] on Oct 16, 2025
  6. melvin-bot commented on Oct 16, 2025

    @melvin-bot
  7. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on Oct 16, 2025
  8. melvin-bot commented on Oct 16, 2025

    @melvin-bot

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

  9. dukenv0307 commented on Oct 18, 2025

    @dukenv0307
    Contributor

    @dmkt9 I don't think your RCA is correct, that's fine to trigger readNewestAction many times, but in readNewestAction because we already used writeWithNoDuplicatesConflictAction which prevents the duplication. Can you please check again?

  10. dmkt9 commented on Oct 19, 2025

    @dmkt9
    Contributor

    @dukenv0307 Yes. I just double-checked. We indeed used writeWithNoDuplicatesConflictAction, but because readNewestAction is inside callbacks of two different useEffects, and those callbacks are triggered separately and asynchronously, usually after the first callback, the whole request queue gets resolved successfully, so when the second callback gets triggered, the queue is already empty.

  11. ShridharGoel commented on Oct 19, 2025

    @ShridharGoel
    Contributor

    Proposal

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

    Opening a chat report that just received a new message fires the /ReadNewestAction API twice, which shows up as duplicate network calls and adds unnecessary traffic.

    What is the root cause of that problem?

    When the report gains focus, the visibility effect in ReportActionsList calls readNewestAction even if the earlier logic already handled the unread state. There is no guard that checks whether the read was previously skipped, so the second effect retriggers the write request every time the report becomes visible.

    useEffect(() => {
    if (report.reportID !== prevReportID) {
    return;
    }
    if (!isVisible || !isFocused) {
    if (!lastMessageTime.current) {
    lastMessageTime.current = lastAction?.created ?? '';
    }
    return;
    }
    // In case the user read new messages (after being inactive) with other device we should
    // show marker based on report.lastReadTime
    const newMessageTimeReference = lastMessageTime.current && report.lastReadTime && lastMessageTime.current > report.lastReadTime ? userActiveSince.current : report.lastReadTime;
    lastMessageTime.current = null;
    const isArchivedReport = isArchivedNonExpenseReport(report, isReportArchived);
    const hasNewMessagesInView = scrollingVerticalOffset.current < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD;
    const hasUnreadReportAction = sortedVisibleReportActions.some(
    (reportAction) =>
    newMessageTimeReference &&
    newMessageTimeReference < reportAction.created &&
    (isReportPreviewAction(reportAction) ? reportAction.childLastActorAccountID : reportAction.actorAccountID) !== getCurrentUserAccountID(),
    );
    if (!isArchivedReport && (!hasNewMessagesInView || !hasUnreadReportAction)) {
    return;
    }
    readNewestAction(report.reportID);
    userActiveSince.current = DateUtils.getDBTime();

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

    Add a guard that only lets the visibility effect call readNewestAction when readActionSkipped is true, and reset the flag after the call so the API runs once per unread batch.

    useEffect(() => {
        // .... existing code
    
        // Existing
        if (!isVisible || !isFocused) {
            if (!lastMessageTime.current) {
                lastMessageTime.current = lastAction?.created ?? '';
            }
            return;
        }
    
        // New
        if (!readActionSkipped.current) {
            return;
        }
    
        // .... existing code
    
        readNewestAction(report.reportID);
        readActionSkipped.current = false; // New
        userActiveSince.current = DateUtils.getDBTime();
    }, [isFocused, isVisible]);
  12. 65 remaining items

  13. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Nov 17, 2025
  14. melvin-bot commented on Nov 17, 2025

    @melvin-bot
  15. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on Nov 17, 2025
  16. melvin-bot commented on Nov 17, 2025

    @melvin-bot

    Current assignee @dukenv0307 is eligible for the External assigner, not assigning anyone new.

  17. removed
    Help WantedApply this label when an issue is open to proposals by contributors
    on Nov 17, 2025
  18. melvin-bot commented on Nov 17, 2025

    @melvin-bot

    📣 @dmkt9 You have been assigned to this job!
    Please apply to the Upwork job and leave a comment on the Github issue letting us know when we can expect a PR to be ready for review 🧑‍💻
    Once you apply to this job, your Upwork ID will be stored and you will be automatically hired for future jobs!
    Keep in mind: Code of Conduct | Contributing 📖

  19. garrettmknight commented on Nov 17, 2025

    @garrettmknight
    Contributor

    @dmkt9 please apply to the new upwork job for payment.

    @dukenv0307 request when you're ready.

  20. dmkt9 commented on Nov 18, 2025

    @dmkt9
    Contributor

    @garrettmknight Thanks. I just applied for this job.

  21. garrettmknight commented on Nov 18, 2025

    @garrettmknight
    Contributor

    @dmkt9 offer out to you.

  22. dmkt9 commented on Nov 18, 2025

    @dmkt9
    Contributor

    @garrettmknight Thanks. I've just requested payment.

  23. garrettmknight commented on Nov 19, 2025

    @garrettmknight
    Contributor

    All paid up!

  24. quinthar commented on Dec 19, 2025

    @quinthar
    Contributor

    Approved $250 to @dukenv0307

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions