Skip to content

[$250] Inbox - Marking all as read does not clear unread chats and unread markers #101684

Open

Description

@applause-bot

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


Version Number: 9.4.90-0
Reproducible in staging?: Yes
Reproducible in production?: No
If this was caught during regression testing, add the test name, ID and link from BrowserStack: #100693
Email or phone of affected tester (no customers): applausetester+bm2244a@applause.expensifail.com
Issue reported by: Applause Internal Team
Bug source: Pull Request QA execution
Device used: android
App Component: Left Hand Navigation (LHN)

Action Performed:

Preconditions:
Log in to an account that has unread messages

  1. Go to staging.new.expensify.com
  2. Go to Inbox page, switch to Unread tab
  3. Long-press on Unread tab, select "Mark all as read"

Expected Result:

App should clear all unread chats and unread markers

Actual Result:

Unread chats and unread markers are not cleared

Workaround:

Unknown

Platforms:

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

Screenshots/Videos

Bug7263667_1789776019296.1.mp4

View all open jobs on GitHub

Applause Internal Information
Exported by: Rizky Gunawan
Bug ID: 7263667

Issue OwnerCurrent Issue Owner: @eVoloshchak
Upwork Automation - Do Not Edit

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Sep 21, 2026
  2. applause-bot commented on Sep 21, 2026

    @applause-bot
    Author

    While Applause doesn't embed attachments for this bug, here are the links for quick view:
    https://platform.applause.com/services/links/v1/external/c329b51fb7afe7ab201e84c73e9afdd567d11ca1817f62d37f25bb746871e11f

  3. MelvinBot commented on Sep 21, 2026

    @MelvinBot
    Contributor

    Proposal

    This is two separate defects that together produce the reported symptom. "Unread chats not cleared" is a regression from [Phase 4] - Remove module-level reportAttributesDerivedValue in isEmptyReport(). "Unread markers not cleared" is a pre-existing gap that PR QA surfaced for the first time.

    What is the root cause of that problem?

    A. Unread chats aren't cleared — regression from PR #100693

    That PR rewrote isEmptyReport() to take the emptiness flag as a parameter, and in doing so changed what an absent value means:

    • Before (ReportUtils.ts:9695-9707) — read the derived attributes live; if the entry existed, use its isEmpty, even when false.
    • After (ReportUtils.ts:9689-9694) — derivedIsEmptyReport ?? generateIsEmptyReport(...). If the caller's value is undefined, recompute.

    The fallback is not equivalent. generateIsEmptyReport() returns true for any report with no lastMessageText whose actions aren't in memory — the normal state on a freshly launched mobile app. The derived flag is persisted to disk, so it can hold false where a fresh recompute says true.

    The same PR also changed where markAllMessagesAsRead gets that data. It used to come from ReportUtils' own live Onyx.connect, independent of React. Now it's a render snapshot captured in InboxTabSelector.tsx:47 and passed down at :101.

    When that snapshot is missing or unhydrated, every report fails the isUnread() filter, reportIDList stays empty, and the action returns before API.write — no optimistic write, no network call, no error. Silently nothing happens, which is exactly what the tester saw.

    Note the same PR kept the live read in UnreadIndicatorUpdater/index.ts:68-75. So the badge and the action can now disagree about what's unread.

    B. Unread markers can't be cleared by this action at all — pre-existing

    Even with a successful write, markAllMessagesAsRead cannot remove the in-chat "New" line, for two independent reasons:

    1. unreadMarkerTime is seeded once at mount (useUnreadMarker.ts:82) and only advanced by the unreadAction_* / readNewestAction_* DeviceEventEmitter events. markAllMessagesAsRead emits neither — it only merges lastReadTime.
    2. shouldDisplayNewMarkerOnReportAction.ts:65-66 hard-pins the marker whenever manuallyMarkedUnreadReportActionID is set, ignoring lastReadTime entirely. Only openReport ever clears it; markAllMessagesAsRead never does.

    This matters for this exact repro: if the tester created the unread state with "Mark as unread" (the usual way to satisfy the "account with unread messages" precondition), the marker survives even a fully successful mark-all-as-read.

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

    1. Make markAllMessagesAsRead read REPORT_ATTRIBUTES live via Onyx.connectWithoutView, matching what UnreadIndicatorUpdater already does, instead of accepting a render snapshot. This restores pre-PR behavior and lets the reportAttributesDerived plumbing come back out of InboxTabSelector and MarkAllMessagesAsReadHandler.
    2. Restore the old contract in isEmptyReport so "no derived entry" is distinguishable from "entry says false". An absent caller value must never silently trigger a recompute that can disagree with the cached flag.
    3. Clear the manual-unread state in markAllMessagesAsRead: include manuallyMarkedUnreadReportActionID: null in the optimistic merge and emit readNewestAction_<reportID> for each marked report, so mounted chats drop the "New" line.
    4. Close the test gap. The only coverage is tests/actions/ReportTest.ts:3592, which calls markAllMessagesAsRead(undefined, undefined, undefined) — it never exercises the derived-isEmpty path. Add a test with REPORT_ATTRIBUTES populated, and one asserting the Unread tab's membership and what the action marks stay in agreement.

    What alternative solutions did you explore?

    • Backend rejection + failureData rollback. There's no successData, and failureData restores the previous lastReadTime for every report, so a rejected MarkAllMessagesAsRead would look identical to "nothing cleared". I could not rule this out — VictoriaLogs was unavailable in this run, so there is no production confirmation of whether the request was sent. One log query settles it: if no MarkAllMessagesAsRead request exists for the tester's session, the client bailed and the diagnosis above is confirmed; if one exists and failed, it's the backend instead.
    • Frozen LHN snapshot. Ruled out — updateReportsToDisplayInLHN correctly rewrites the entry when unread flags clear (SidebarUtils.ts:404-421), so the optimistic merge is sufficient to clear LHN bolding and the tab badge.
    • Wrong or misordered arguments in the PR. Ruled out — every changed isUnread() / isEmptyReport() call site was audited and threads the correct value in the correct position.
    • A different staging-only PR. Ruled out at blob level, see below.
    Supporting evidence

    Release-window fit is exact. Comparing production 9.4.89-0 against staging 9.4.90-0:

    Also verified and ruled out: shape mismatch in useReportAttributes() (it correctly returns .reports); a different archived input in the derived isEmpty computation; partial derived entries missing isEmpty; stale closures in PopoverMenu (byte-identical prod↔staging).

    Separate pre-existing asymmetry, worth noting but not the cause here: the Unread tab calls isUnread(report, undefined, ...) while the action passes the one-transaction thread report. Since isUnread takes max(report.lastReadTime, thread.lastReadTime), a one-transaction expense report whose thread was already read looks unread to the tab but read to the action. That's present verbatim in production, and would cause a partial failure, not a total one.

    Confidence: high that PR #100693 is the cause (blob-level proof); medium on the precise runtime trigger, which needs the log query above or a device log of reportIDList.length at the call site.

    Reproduction note: I attempted to verify on web, but agent-device in this environment supports neither right-click nor long-press, and right-click is the only entry point to this menu on desktop web. The repro was not reproduced first-hand.

    Related issues

    💡 Suggested AI code review rule

    The refactor that caused this is being applied repeatedly (this PR is "Phase 4", citing Phase 2's pattern, under refactor issue #66412), so the same hazard can recur in each phase. Proposed for Expensify/App:

    • Rule: CONSISTENCY-18 — Threading a cached Onyx value into a parameter must not make undefined mean "recompute"
    • Condition: Flag when a refactor replaces a module-level derived-Onyx read with a caller-passed parameter and the absent case falls back to a recompute (return derivedX ?? computeX(...)). The recompute is rarely equivalent to the persisted cached value. Make the parameter required, or keep the entry-exists check so an absent caller value can't silently flip the result. Separately, flag imperative one-shot actions in src/libs/actions/ that receive derived Onyx data as a React render snapshot instead of reading it live via Onyx.connectWithoutView.
    • Why: A wrong emptiness answer made isUnread() false for every report, so markAllMessagesAsRead early-returned with no API call and no optimistic write — a completely silent no-op.
    • Search pattern: return\s+\w*[Dd]erived\w*\s*\?\?\s*(generate|compute|get)\w+\( across src/libs/, plus Derived(Value)?\b appearing in a parameter list under src/libs/actions/.
    • Offending code (❌): src/libs/ReportUtils.ts:9689-9694
    • Fix (✅): src/libs/ReportUtils.ts:9695-9707 as it was before the PR

    The full rule file, with the ❌/✅ code inline in the app-coding-standards format, would go in App/.claude/skills/app-coding-standards/rules/. If it's worth adding, a maintainer can open it there as a draft PR.


    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/101684) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run · no recording available

  4. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Sep 21, 2026
  5. melvin-bot commented on Sep 21, 2026

    @melvin-bot

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

  6. melvin-bot commented on Sep 21, 2026

    @melvin-bot

    Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.

  7. melvin-bot commented on Sep 24, 2026

    @melvin-bot

    @eVoloshchak Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!

  8. M00rish commented on Sep 25, 2026

    @M00rish
    Contributor

    Reproduced on web. The cause is not a missing isEmpty snapshot. In my run the derived value was present and correct, and a normal unread chat was marked read as expected.

    What gets stuck is a one-expense report whose expense (thread) was already read. The Unread tab checks it without the thread, so it's listed as unread:

    isUnread(report, undefined, isReportArchived, derivedIsEmptyReport) &&

    The action checks it with the thread, so it's treated as read and skipped:

    if (!isUnread(report, oneTransactionThreadReport, isReportArchived, reportAttributesDerived?.[report.reportID]?.isEmpty)) {

    Values logged inside the action for a report the Unread tab was showing:

    6133559146143422 {"derived":{"isEmpty":false,"oneTransactionThreadReportID":"2055341732240261"},"unread":false}
    reportIDList []
    

    No API call and no optimistic update. The report stays in the Unread tab until it's opened.

    Fix: pass the one-transaction thread to getIsUnreadReportForInboxTab, so the tab, the LHN bold state and the action use the same check. In both getReportsToDisplayInLHN and updateReportsToDisplayInLHN:

    const oneTransactionThreadReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportAttributes?.[report?.reportID]?.oneTransactionThreadReportID}`];
    const isUnreadReport = getIsUnreadReportForInboxTab(report, oneTransactionThreadReport, isReportArchived, reportAttributes?.[report?.reportID]?.isEmpty);

    and pass it through to isUnread in getIsUnreadReportForInboxTab. I tested this locally: with the same state, the Unread tab shows "No unread chats".

  9. ashusatyarthi-wq commented on Sep 27, 2026

    @ashusatyarthi-wq

    Proposal

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

    When using the "Mark all as read" action on the Inbox > Unread tab, certain one-expense reports remain stuck in the Unread tab with bold unread markers and do not clear until manually opened.


    What is the root cause of that problem?

    There is a discrepancy in how unread state is calculated between the Inbox tab filter and the MarkAllMessageAsRead action:

    1. In src/libs/SidebarUtils.ts:987-995, getIsUnreadReportForInboxTab checks whether a report should appear in the "Unread" tab by calling isUnread with undefined for oneTransactionThreadReport:
    function getIsUnreadReportForInboxTab(report: Report, isReportArchived: boolean, derivedIsEmptyReport: boolean | undefined): boolean {
        return (
            isUnread(report, undefined, isReportArchived, derivedIsEmptyReport) &&
            !!report.lastActorAccountID &&
            getReportNotificationPreference(report) !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE
        );
    }

    Because oneTransactionThreadReport is undefined, for a one-expense report whose expense thread was already read, isUnread falls back to evaluating the parent report in isolation and returns true. Consequently, isUnreadReport is set to true, and the report is displayed in the "Unread" tab.

    1. In src/libs/actions/Report/MarkAllMessageAsRead.tsx:67, markAllMessagesAsRead checks whether the report needs to be marked read with the thread report:
    isUnread(report, oneTransactionThreadReport)

    Because the one-transaction thread is already read, isUnread evaluates to false.

    1. As a result, MarkAllMessageAsRead determines that the report is already read and excludes it from the reportIDList payload. No optimistic update and no API call are made. The report remains permanently trapped in the Unread tab until opened.

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

    Pass the oneTransactionThreadReport to getIsUnreadReportForInboxTab so that the Unread tab, the LHN bold indicator, and MarkAllMessageAsRead all use the identical, unified unread check.

    1. In src/libs/SidebarUtils.ts:

    Update getIsUnreadReportForInboxTab to accept oneTransactionThreadReport:

    -function getIsUnreadReportForInboxTab(report: Report, isReportArchived: boolean, derivedIsEmptyReport: boolean | undefined): boolean {
    +function getIsUnreadReportForInboxTab(
    +    report: Report,
    +    oneTransactionThreadReport: OnyxEntry<Report>,
    +    isReportArchived: boolean,
    +    derivedIsEmptyReport: boolean | undefined,
    +): boolean {
         return (
    -        isUnread(report, undefined, isReportArchived, derivedIsEmptyReport) &&
    +        isUnread(report, oneTransactionThreadReport, isReportArchived, derivedIsEmptyReport) &&
             !!report.lastActorAccountID &&
             getReportNotificationPreference(report) !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE
         );
     }

    2. Pass oneTransactionThreadReport at call sites in SidebarUtils.ts:

    In both getReportsToDisplayInLHN (line 316) and updateReportsToDisplayInLHN (line 405):

    +const oneTransactionThreadReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportAttributes?.[report?.reportID]?.oneTransactionThreadReportID}`];
     const isUnreadReport = getIsUnreadReportForInboxTab(
         report,
    +    oneTransactionThreadReport,
         isReportArchived,
         reportAttributes?.[report?.reportID]?.isEmpty,
     );

    What specific scenarios should be covered in tests?

    Add unit tests in tests/unit/SidebarUtilsTest.ts:

    1. One-transaction expense report where thread is read:
      • Given a one-transaction expense report with an associated oneTransactionThreadReport whose lastReadTime >= lastVisibleActionCreated.
      • Assert getIsUnreadReportForInboxTab returns false.
      • Assert the report does not appear in CONST.INBOX_TAB.UNREAD.
    2. One-transaction expense report where thread is unread:
      • Given a one-transaction expense report with an unread oneTransactionThreadReport.
      • Assert getIsUnreadReportForInboxTab returns true.
      • Execute markAllMessagesAsRead and assert report is included and unread state clears.

    What alternative solutions did you explore? (Optional)

    1. Removing oneTransactionThreadReport from MarkAllMessageAsRead:
      • Rejected because MarkAllMessageAsRead's check is the correct business logic (an expense report whose single transaction thread has been read should not be treated as unread). Aligning SidebarUtils ensures consistent UI behavior across the entire app.
  10. melvin-bot commented on Sep 28, 2026

    @melvin-bot

    @eVoloshchak Still overdue 6 days?! Let's take care of this!

  11. melvin-bot commented on Sep 30, 2026

    @melvin-bot

    @eVoloshchak 8 days overdue is a lot. Should this be a Weekly issue? If so, feel free to change it!

  12. 93 remaining items

  13. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    @eVoloshchak Huh... This is 4 days overdue. Who can take care of this?

  14. herdlevar commented on Oct 20, 2026

    @herdlevar

    Proposal

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

    In the Inbox tab with the Unread filter selected, clicking Mark all as read leaves some chats in the Unread list and does not clear their unread status.

    What is the root cause of that problem?

    There is a mismatch between how SidebarUtils.ts determines whether a report is unread for the Inbox Unread tab and how markAllMessagesAsRead in src/libs/actions/Report/MarkAllMessageAsRead.tsx determines whether to mark a report as read:

    1. In src/libs/SidebarUtils.ts (getIsUnreadReportForInboxTab, lines 1023-1026, called at lines 352 and 444), isUnread(report, undefined, reportAttributes) is called with undefined as the second parameter (oneTransactionThreadReport).
    2. In src/libs/actions/Report/MarkAllMessageAsRead.tsx (lines 63-70), markAllMessagesAsRead resolves oneTransactionThreadReport and calls isUnread(report, oneTransactionThreadReport, reportAttributes). For a single-expense report where the user previously viewed the transaction thread (oneTransactionThreadReport.lastReadTime >= lastVisibleActionCreated) while the parent report report.lastReadTime is older, isUnread(report, oneTransactionThreadReport, ...) returns false (so markAllMessagesAsRead skips it), whereas getIsUnreadReportForInboxTab passes undefined for oneTransactionThreadReport and returns true (keeping it stuck in the Unread tab).
    3. Furthermore, when oneTransactionThreadReport itself is unread, markAllMessagesAsRead calls readNewestAction(report.reportID) on the parent report without calling readNewestAction(oneTransactionThreadReport.reportID).

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

    1. In src/libs/SidebarUtils.ts, update getIsUnreadReportForInboxTab to accept oneTransactionThreadReport: OnyxEntry<Report> and pass it through to isUnread(report, oneTransactionThreadReport, reportAttributes) at both call sites (lines 352 and 444).
    2. In src/libs/actions/Report/MarkAllMessageAsRead.tsx, when a report has a oneTransactionThreadReport that is unread (isUnread(oneTransactionThreadReport, undefined, reportAttributes)), also call readNewestAction(oneTransactionThreadReport.reportID, !!oneTransactionThreadReport.hasDraftComment).
  15. stacklancer commented on Oct 20, 2026

    @stacklancer

    @eVoloshchak friendly bump when you get a chance. My proposal covers both parts (the opened chat staying in the Unread list and the New line not clearing), and there's a test branch if that helps the review. Happy to answer any questions.

  16. melvin-bot commented on Oct 21, 2026

    @melvin-bot

    @eVoloshchak 6 days overdue. This is scarier than being forced to listen to Vogon poetry!

  17. bconnnnn commented on Oct 23, 2026

    @bconnnnn

    Proposal

    What is the root cause of that problem?

    The bulk read action may target only rendered items or update a different read-state field from the unread list and badges; the actual handler needs inspection.

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

    Trace the Unread-tab context menu through bulk mark-read and its optimistic/server state. Target all unread report IDs and reconcile the same fields used by markers, handling failures.

    What alternative solutions did you explore? (Optional)

    Avoid a UI-only workaround that masks the symptom without fixing the state or lifecycle problem.

    Validation: Test multiple offscreen unread chats, counts, offline sync, Android and web.

    @eVoloshchak I'm available if you need a contributor for this issue. I'll wait for approval before opening a PR.

  18. melvin-bot commented on Oct 23, 2026

    @melvin-bot

    📣 It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? 💸

  19. melvin-bot commented on Oct 23, 2026

    @melvin-bot

    @eVoloshchak 8 days overdue is a lot. Should this be a Weekly issue? If so, feel free to change it!

  20. melvin-bot commented on Oct 24, 2026

    @melvin-bot

    📣 @Hatem-tmi! 📣
    Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
    Please follow these steps:

    1. Make sure you've read and understood the contributing guidelines.
    2. Get the email address used to login to your Expensify account. If you don't already have an Expensify account, create one here. If you have multiple accounts (e.g. one for testing), please use your main account email.
    3. Get the link to your Upwork profile. It's necessary because we only pay via Upwork. You can access it by logging in, and then clicking on your name. It'll look like this. If you don't already have an account, sign up for one here.
    4. Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
      Screen Shot 2022-11-16 at 4 42 54 PM
      Format:
    Contributor details
    Your Expensify account email: <REPLACE EMAIL HERE>
    Upwork Profile Link: <REPLACE LINK HERE>
    
  21. Hatem-tmi commented on Oct 24, 2026

    @Hatem-tmi

    Proposal

    What is the root cause of that problem?

    The Unread list does clear for a moment, then comes back. A chat the action skips never gets that flash, so this is not the Unread-tab predicate disagreeing with the one-transaction thread.

    Mark all as read sends one lastReadTime for every selected chat. That time comes from getDBTimeWithSkew. That helper only moves the clock forward when the client is behind the server. When the client is ahead, it sends the client clock unchanged. The server rejects a time in its future, the write fails, and failureData puts the old lastReadTime back. The Unread tab and the badge return together.

    The in-chat New marker is a separate gap. It is local state on an open chat. Mark all as read never tells that chat it was read, so the marker can stay after the list is fixed.

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

    Stop using "now" for this command.

    For the batch, send the newest lastVisibleActionCreated among the chats being marked. Those times were already stored by the server, so they are not in the future, and a chat is read once lastReadTime is at least that value. Use the same value for the optimistic update and the request.

    For an open chat, also emit the same read event the single "Mark as read" action already emits, so the New marker moves.

    Do not change getDBTimeWithSkew. Other writes rely on it never moving backward.

    What alternative solutions did you explore?

    Use getServerAnchoredDBTime instead. That still means "now". Before the skew is known it falls back to the client clock, which is the value the server is already rejecting.

    Make the Unread tab and the action share one isUnread call. That only changes which chats are selected. It does not explain the list emptying and then returning.

    Contributor details
    Your Expensify account email: hatem.tmi@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~0103158c5f0816abbe

  22. melvin-bot commented on Oct 24, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  23. mvtglobally commented on Oct 24, 2026

    @mvtglobally

    Issue not reproducible during KI retests. (Third week)

  24. neerajbachani commented on Oct 24, 2026

    @neerajbachani
    Contributor

    Proposal

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

    On staging Android, long-pressing the Inbox Unread tab and choosing Mark all as read does nothing visible: chats stay in the Unread tab with bold/unread styling, and in-chat New markers remain. Production (9.4.89-0) does not show this; staging (9.4.90-0) does. QA tied it to PR #100693.

    What is the root cause of that problem?

    This is two separate defects that together produce the reported symptom.

    A. Unread chats are not cleared — regression from PR #100693

    That PR changed how markAllMessagesAsRead decides which reports are unread.

    Before the PR, markAllMessagesAsRead called isUnread() without a derived-attributes parameter, and isEmptyReport() read REPORT_ATTRIBUTES live via a module-level Onyx.connect inside ReportUtils:

    App/src/libs/actions/Report/MarkAllMessageAsRead.tsx
    Lines 63 to 67 in 3caa681

             const isReportArchived = isArchivedReport(reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]);
             if (!isUnread(report, oneTransactionThreadReport, isReportArchived)) {
                 continue;
             }
    

    if (!isUnread(report, oneTransactionThreadReport, isReportArchived)) {
    continue;
    }
    const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`;

    App/src/libs/ReportUtils.ts
    Lines 9695 to 9707 in 3caa681

     function isEmptyReport(report: OnyxEntry<Report>, isReportArchived: boolean | undefined): boolean {
         if (!report) {
             return true;
         }
     
         // Get the `isEmpty` state from cached report attributes
         const attributes = reportAttributesDerivedValue?.[report.reportID];
         if (attributes) {
             return attributes.isEmpty;
         }
     
         return generateIsEmptyReport(report, isReportArchived);
     }
    

    App/src/libs/ReportUtils.ts

    Lines 9695 to 9707 in 3caa681

    function isEmptyReport(report: OnyxEntry<Report>, isReportArchived: boolean | undefined): boolean {
    if (!report) {
    return true;
    }
    // Get the `isEmpty` state from cached report attributes
    const attributes = reportAttributesDerivedValue?.[report.reportID];
    if (attributes) {
    return attributes.isEmpty;
    }
    return generateIsEmptyReport(report, isReportArchived);
    }

    After the PR, isEmptyReport() takes a caller-supplied flag and falls back with ??:

    App/src/libs/ReportUtils.ts
    Lines 9689 to 9694 in 2402291

     function isEmptyReport(report: OnyxEntry<Report>, isReportArchived: boolean | undefined, derivedIsEmptyReport: boolean | undefined): boolean {
         if (!report) {
             return true;
         }
         return derivedIsEmptyReport ?? generateIsEmptyReport(report, isReportArchived);
     }
    

    App/src/libs/ReportUtils.ts

    Lines 9689 to 9694 in 2402291

    function isEmptyReport(report: OnyxEntry<Report>, isReportArchived: boolean | undefined, derivedIsEmptyReport: boolean | undefined): boolean {
    if (!report) {
    return true;
    }
    return derivedIsEmptyReport ?? generateIsEmptyReport(report, isReportArchived);
    }

    That fallback is not equivalent on mobile. generateIsEmptyReport() returns true when a report has no lastMessageText and its actions are not in memory — the common state on a freshly launched Android app:

    App/src/libs/ReportUtils.ts
    Lines 9779 to 9790 in 2402291

     function generateIsEmptyReport(report: OnyxEntry<Report>, isReportArchived: boolean | undefined): boolean {
         if (!report) {
             return true;
         }
     
         if (report.lastMessageText) {
             return false;
         }
     
         const lastVisibleMessage = getLastVisibleMessage(report.reportID, isReportArchived);
         return !lastVisibleMessage.lastMessageText;
     }
    

    App/src/libs/ReportUtils.ts

    Lines 9779 to 9790 in 2402291

    function generateIsEmptyReport(report: OnyxEntry<Report>, isReportArchived: boolean | undefined): boolean {
    if (!report) {
    return true;
    }
    if (report.lastMessageText) {
    return false;
    }
    const lastVisibleMessage = getLastVisibleMessage(report.reportID, isReportArchived);
    return !lastVisibleMessage.lastMessageText;
    }

    The persisted derived flag can hold isEmpty: false while a fresh recompute says true. When the caller passes undefined (missing snapshot entry), isUnread() returns false, the report is skipped, and the action exits before any write:

    App/src/libs/actions/Report/MarkAllMessageAsRead.tsx
    Lines 67 to 79 in 2402291

             if (!isUnread(report, oneTransactionThreadReport, isReportArchived, reportAttributesDerived?.[report.reportID]?.isEmpty)) {
                 continue;
             }
     
             const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`;
             optimisticReports[reportKey] = {lastReadTime: newLastReadTime};
             failureReports[reportKey] = {lastReadTime: report.lastReadTime ?? null};
             reportIDList.push(report.reportID);
         }
     
         if (reportIDList.length === 0) {
             return;
         }
    

    if (!isUnread(report, oneTransactionThreadReport, isReportArchived, reportAttributesDerived?.[report.reportID]?.isEmpty)) {
    continue;
    }
    const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`;
    optimisticReports[reportKey] = {lastReadTime: newLastReadTime};
    failureReports[reportKey] = {lastReadTime: report.lastReadTime ?? null};
    reportIDList.push(report.reportID);
    }
    if (reportIDList.length === 0) {
    return;
    }

    The PR moved derived data to a React render snapshot captured in InboxTabSelector and passed at confirm time:

    App/src/pages/inbox/sidebar/InboxTabSelector.tsx
    Lines 47 to 47 in 2402291

         const reportAttributesDerived = useReportAttributes();
    

    const reportAttributesDerived = useReportAttributes();

    App/src/pages/inbox/sidebar/InboxTabSelector.tsx
    Lines 99 to 101 in 2402291

                 // From the To-dos tab only the chats listed there are marked read. The All and Unread tabs both cover every
                 // unread chat, so they mark all of them.
                 markAllMessagesAsRead(reportNameValuePairs, isTodoTab ? getReportIDsForTab(CONST.INBOX_TAB.TODO) : undefined, reportAttributesDerived);
    

    // From the To-dos tab only the chats listed there are marked read. The All and Unread tabs both cover every
    // unread chat, so they mark all of them.
    markAllMessagesAsRead(reportNameValuePairs, isTodoTab ? getReportIDsForTab(CONST.INBOX_TAB.TODO) : undefined, reportAttributesDerived);

    Imperative one-shot actions should not depend on a render snapshot. UnreadIndicatorUpdater still reads REPORT_ATTRIBUTES live via Onyx.connectWithoutView, so the badge and the action can disagree about what's unread:

    App/src/libs/UnreadIndicatorUpdater/index.ts
    Lines 68 to 75 in 2402291

     let reportAttributesDerived: ReportAttributesDerivedValue['reports'] | undefined;
     // This subscription is used to update the unread indicators count which is not linked to UI and it does not update any UI state.
     Onyx.connectWithoutView({
         key: ONYXKEYS.DERIVED.REPORT_ATTRIBUTES,
         callback: (value) => {
             reportAttributesDerived = value?.reports;
         },
     });
    

    let reportAttributesDerived: ReportAttributesDerivedValue['reports'] | undefined;
    // This subscription is used to update the unread indicators count which is not linked to UI and it does not update any UI state.
    Onyx.connectWithoutView({
    key: ONYXKEYS.DERIVED.REPORT_ATTRIBUTES,
    callback: (value) => {
    reportAttributesDerived = value?.reports;
    },
    });

    Meanwhile the Unread tab itself correctly lists chats using the same derived isEmpty flag via SidebarUtils — so the user sees unread chats the action refuses to mark:

    App/src/libs/SidebarUtils.ts
    Lines 987 to 994 in 2402291

     function getIsUnreadReportForInboxTab(report: Report, isReportArchived: boolean, derivedIsEmptyReport: boolean | undefined): boolean {
         // The `lastActorAccountID` guard matches getOptionData: it keeps chats whose only visible message was
         // deleted out of the Unread tab even though isUnread() can still be true (lastVisibleActionCreated isn't reset).
         return (
             isUnread(report, undefined, isReportArchived, derivedIsEmptyReport) &&
             !!report.lastActorAccountID &&
             getReportNotificationPreference(report) !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE
         );
     }
    

    function getIsUnreadReportForInboxTab(report: Report, isReportArchived: boolean, derivedIsEmptyReport: boolean | undefined): boolean {
    // The `lastActorAccountID` guard matches getOptionData: it keeps chats whose only visible message was
    // deleted out of the Unread tab even though isUnread() can still be true (lastVisibleActionCreated isn't reset).
    return (
    isUnread(report, undefined, isReportArchived, derivedIsEmptyReport) &&
    !!report.lastActorAccountID &&
    getReportNotificationPreference(report) !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE
    );

    Why Android / staging only: PR #100693 is the only change to MarkAllMessageAsRead.tsx between production 9.4.89-0 and staging 9.4.90-0. Android HybridApp cold-starts with fewer report actions hydrated in memory, which makes the generateIsEmptyReport fallback falsely classify real unread chats as empty. Other platforms were not checked in QA; the regression is not Android-specific in code.

    B. Unread markers are not cleared — pre-existing gap

    Even when markAllMessagesAsRead succeeds, it only merges lastReadTime. It never clears manual-unread state or notifies mounted report views.

    1. unreadMarkerTime in useUnreadMarker is seeded once at mount and only advanced by unreadAction_* / readNewestAction_* events — markAllMessagesAsRead emits neither:

    App/src/hooks/useUnreadMarker.ts
    Lines 82 to 94 in 2402291

         const [unreadMarkerTime, setUnreadMarkerTime] = useState(reportLastReadTime);
     
         useEffect(() => {
             if (isAnonymousUser) {
                 return;
             }
     
             const unreadActionSubscription = DeviceEventEmitter.addListener(`unreadAction_${reportID}`, (newLastReadTime: string) => {
                 setUnreadMarkerTime(newLastReadTime);
             });
             const readNewestActionSubscription = DeviceEventEmitter.addListener(`readNewestAction_${reportID}`, (newLastReadTime: string) => {
                 setUnreadMarkerTime(newLastReadTime);
             });
    

    const [unreadMarkerTime, setUnreadMarkerTime] = useState(reportLastReadTime);
    useEffect(() => {
    if (isAnonymousUser) {
    return;
    }
    const unreadActionSubscription = DeviceEventEmitter.addListener(`unreadAction_${reportID}`, (newLastReadTime: string) => {
    setUnreadMarkerTime(newLastReadTime);
    });
    const readNewestActionSubscription = DeviceEventEmitter.addListener(`readNewestAction_${reportID}`, (newLastReadTime: string) => {
    setUnreadMarkerTime(newLastReadTime);
    });

    1. When manuallyMarkedUnreadReportActionID is set (typical for the "account with unread messages" precondition), the New line is hard-pinned and ignores lastReadTime:

    App/src/pages/inbox/report/shouldDisplayNewMarkerOnReportAction.ts
    Lines 65 to 67 in 2402291

         if (manuallyMarkedUnreadReportActionID) {
             return message.reportActionID === manuallyMarkedUnreadReportActionID && !shouldHideNewMarker(message, isOffline);
         }
    

    if (manuallyMarkedUnreadReportActionID) {
    return message.reportActionID === manuallyMarkedUnreadReportActionID && !shouldHideNewMarker(message, isOffline);
    }

    Only openReport clears manuallyMarkedUnreadReportActionID; readNewestAction deliberately leaves it alone:

    App/src/libs/actions/Report/index.ts
    Lines 3236 to 3244 in 2402291

         // Deliberately leaves `manuallyMarkedUnreadReportActionID` alone so an auto-read doesn't wipe a marker the
         // user created. `openReport` clears it on a return trip or a refresh.
         const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT>> = [
             {
                 onyxMethod: Onyx.METHOD.MERGE,
                 key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
                 value: {
                     lastReadTime,
                 },
    

    // Deliberately leaves `manuallyMarkedUnreadReportActionID` alone so an auto-read doesn't wipe a marker the
    // user created. `openReport` clears it on a return trip or a refresh.
    const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT>> = [
    {
    onyxMethod: Onyx.METHOD.MERGE,
    key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
    value: {
    lastReadTime,
    },

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

    1. Make markAllMessagesAsRead read REPORT_ATTRIBUTES live via Onyx.connectWithoutView, matching UnreadIndicatorUpdater. Drop the third reportAttributesDerived parameter and remove the plumbing from InboxTabSelector and MarkAllMessagesAsReadHandler.

    In src/libs/actions/Report/MarkAllMessageAsRead.tsx, add a live subscription (same pattern as UnreadIndicatorUpdater):

    + import {DeviceEventEmitter} from 'react-native';
    + let reportAttributesDerived: ReportAttributesDerivedValue['reports'] | undefined;
    + Onyx.connectWithoutView({key: ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, callback: (value) => { reportAttributesDerived = value?.reports; }});
    

    Drop the third parameter from the function signature:

    - function markAllMessagesAsRead(reportNameValuePairs, reportIDs, reportAttributesDerived)
    + function markAllMessagesAsRead(reportNameValuePairs, reportIDs)
    

    Read derived isEmpty from the live subscription inside the loop:

    - reportAttributesDerived?.[report.reportID]?.isEmpty
    + reportAttributesDerived?.[report.reportID]?.isEmpty  // read from module-level subscription, not caller snapshot
    

    Clear manual-unread state in the optimistic merge and roll it back on failure:

    - optimisticReports[reportKey] = {lastReadTime: newLastReadTime};
    + optimisticReports[reportKey] = {lastReadTime: newLastReadTime, manuallyMarkedUnreadReportActionID: null};
    - failureReports[reportKey] = {lastReadTime: report.lastReadTime ?? null};
    + failureReports[reportKey] = {lastReadTime: report.lastReadTime ?? null, manuallyMarkedUnreadReportActionID: report.manuallyMarkedUnreadReportActionID ?? null};
    

    Notify mounted report views after building reportIDList:

    + for (const reportID of reportIDList) { DeviceEventEmitter.emit(`readNewestAction_${reportID}`, newLastReadTime); }
    

    In src/pages/inbox/sidebar/InboxTabSelector.tsx, revert to the two-argument call and remove the unused useReportAttributes() hook:

    - markAllMessagesAsRead(reportNameValuePairs, isTodoTab ? getReportIDsForTab(CONST.INBOX_TAB.TODO) : undefined, reportAttributesDerived);
    + markAllMessagesAsRead(reportNameValuePairs, isTodoTab ? getReportIDsForTab(CONST.INBOX_TAB.TODO) : undefined);
    

    Apply the same signature change in MarkAllMessagesAsReadHandler.tsx (remove the ref and third argument).

    2. Restore the pre-PR isEmptyReport contract in src/libs/ReportUtils.ts so a missing caller value never silently triggers a disagreeing recompute:

    - return derivedIsEmptyReport ?? generateIsEmptyReport(report, isReportArchived);
    + if (derivedIsEmptyReport !== undefined) { return derivedIsEmptyReport; }
    + return generateIsEmptyReport(report, isReportArchived);
    

    This mirrors the pre-PR "use cached flag when present" semantics and protects every other Phase-4 callsite that threads derivedIsEmptyReport.

    3. Clear manual-unread marker state (changes in item 1 above): merge manuallyMarkedUnreadReportActionID: null in the optimistic update and emit readNewestAction_<reportID> for each marked report so mounted chats drop the New line immediately. Roll back both fields in failureData.

    Blast radius: isEmptyReport semantics change affects all unread/empty checks but restores pre-PR behavior. LHN clearing itself is already correct once lastReadTime is written — SidebarUtils.updateReportsToDisplayInLHN recomputes isUnreadReport on every report change (lines 404–418). The manual-marker clear is scoped to explicit bulk mark-as-read; single-chat auto-read paths remain unchanged.

    What alternative solutions did you explore? (Optional)

    • Use refs in InboxTabSelector (like MarkAllMessagesAsReadHandler) — still a render snapshot; does not fix the case where the snapshot entry is missing while Onyx has isEmpty: false. Rejected.
    • Pass getReportIDsForTab(UNREAD) instead of scanning all reports — would mark only what the tab shows, but defect A still makes reportIDList empty when isUnread() misfires. Does not fix markers. Rejected as primary fix.
    • Backend-only fix — the client bails before API.write when reportIDList is empty; no request is sent. A backend change cannot help the silent no-op path.
    • Frozen LHN cache — ruled out; updateReportsToDisplayInLHN already rewrites isUnreadReport when unread flags change.

    What specific scenarios should we cover in automated tests?

    • markAllMessagesAsRead with REPORT_ATTRIBUTES populated where isEmpty: false but report actions are not in memory — asserts API.write is called and all targeted reports get an updated lastReadTime.
    • Same setup but derived entry absent — asserts the action still marks reports that have lastMessageText as read (does not false-negative via generateIsEmptyReport).
    • Report with manuallyMarkedUnreadReportActionID set — asserts optimistic merge clears it and readNewestAction_<reportID> is emitted.
    • Existing test at tests/actions/ReportTest.ts:3592 passes undefined for all three args today; update signature and add the derived-attributes cases above so this regression cannot recur.
  25. stacklancer commented on Oct 24, 2026

    @stacklancer

    @mvtglobally @eVoloshchak I was able to reproduce this on web (main, Oct 17). It only happens to a chat that was opened from the Unread tab, so a plain "Mark all as read" won't show it. Steps:

    1. As user A, have at least 2 unread chats (send 2 messages from user B in different chats).
    2. As user A, go to Inbox > Unread and click one of the unread chats to open it.
    3. Right click (or long press) the Unread tab > Mark all as read > confirm.
    4. The other chat is cleared, but the chat you opened stays in the Unread list and its "New" line is still there. The "No unread chats" empty state doesn't show until you switch to All and back.

    Root cause and screenshots are in my proposal, and there's a test branch with the fix.

  26. samithreddychinni commented on Oct 25, 2026

    @samithreddychinni

    Proposal

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

    When a user switches to the Unread tab in the Inbox, opens or views unread chats, long-presses the Unread tab, and selects Mark all as read, the opened chat remains stuck in the Unread tab list and unread markers remain visible instead of clearing.

    What is the root cause of that problem?

    There are three distinct causes in the current Inbox tab and markAllMessagesAsRead implementation:

    1. stickyReport in useSidebarOrderedReports.tsx is never cleared on "Mark all as read" (src/hooks/useSidebarOrderedReports.tsx:354-378 and src/pages/inbox/sidebar/InboxTabSelector.tsx:100-113):
      When a user clicks a chat while on the Unread or To-do tab, setStickyReportID(reportID) stores stickyReport = {reportID, tab: activeTab}. In computedFilteredReportIDs (src/hooks/useSidebarOrderedReports.tsx:360-377), stickyReportID is forcibly kept in filteredReportIDs even after baseFilteredReportIDs no longer includes it. When the user triggers Mark all as read in InboxTabSelector.tsx, stickyReport is never reset, so the active chat stays pinned in the Unread tab list.

    2. markAllMessagesAsRead does not mark oneTransactionThreadReport as read and does not guard against lastVisibleActionCreated > newLastReadTime (src/libs/actions/Report/MarkAllMessageAsRead.tsx:64-77):
      In MarkAllMessageAsRead.tsx:

      const oneTransactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`], isOffline);
      const oneTransactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${oneTransactionThreadReportID}`];
      const isReportArchived = isArchivedReport(reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]);
      if (!isUnread(report, oneTransactionThreadReport, isReportArchived, reportAttributesDerived?.[report.reportID]?.isEmpty)) {
          continue;
      }
      
      const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`;
      optimisticReports[reportKey] = {lastReadTime: newLastReadTime};
      • When reportIDs is passed (for example from the To-do tab), reportsToMark only iterates over the parent report IDs in the LHN, not oneTransactionThreadReport. If isUnread(report, oneTransactionThreadReport, ...) returned true because oneTransactionThreadReport was unread, only report.reportID gets lastReadTime updated while oneTransactionThreadReport stays unread.
      • If report.lastVisibleActionCreated (or oneTransactionThreadReport.lastVisibleActionCreated) is slightly ahead of getDBTimeWithSkew(), setting lastReadTime: newLastReadTime still leaves lastReadTime < lastVisibleActionCreated, so isUnread continues to return true.
    3. The open report's unread marker ("New" line) remains pinned in ReportActionsList.tsx:
      In src/pages/inbox/report/ReportActionsList.tsx, currentUnreadMarkerReportActionID stays latched for the currently open report while the user views it so the "New" divider does not vanish immediately on open. Because markAllMessagesAsRead updates report.lastReadTime without resetting the open report's latched currentUnreadMarkerReportActionID, the green "New" line remains visible inside the open chat.

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

    1. Expose clearStickyReportID from useSidebarOrderedReportsActions (src/hooks/useSidebarOrderedReports.tsx) and call it in confirmMarkAllAsRead (src/pages/inbox/sidebar/InboxTabSelector.tsx):

      • In useSidebarOrderedReports.tsx, add clearStickyReportID:
        const clearStickyReportID = useCallback(() => {
            setStickyReport(undefined);
        }, []);
      • In InboxTabSelector.tsx (confirmMarkAllAsRead), call clearStickyReportID() when the user confirms Mark all as read on the active tab (or when menuTab === activeTab or menuTab !== CONST.INBOX_TAB.TODO).
    2. Update oneTransactionThreadReport and clamp lastReadTime in src/libs/actions/Report/MarkAllMessageAsRead.tsx:

      const effectiveLastReadTime = [
          newLastReadTime,
          report.lastVisibleActionCreated ?? '',
          oneTransactionThreadReport?.lastVisibleActionCreated ?? '',
      ].reduce((latest, current) => (current > latest ? current : latest), newLastReadTime);
      
      const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`;
      optimisticReports[reportKey] = {lastReadTime: effectiveLastReadTime};
      failureReports[reportKey] = {lastReadTime: report.lastReadTime ?? null};
      reportIDList.push(report.reportID);
      
      if (oneTransactionThreadReport && isUnread(oneTransactionThreadReport, undefined, isArchivedReport(reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${oneTransactionThreadReport.reportID}`]), reportAttributesDerived?.[oneTransactionThreadReport.reportID]?.isEmpty)) {
          const threadKey = `${ONYXKEYS.COLLECTION.REPORT}${oneTransactionThreadReport.reportID}`;
          optimisticReports[threadKey] = {lastReadTime: effectiveLastReadTime};
          failureReports[threadKey] = {lastReadTime: oneTransactionThreadReport.lastReadTime ?? null};
          reportIDList.push(oneTransactionThreadReport.reportID);
      }
    3. Clear currentUnreadMarkerReportActionID in ReportActionsList.tsx when report.lastReadTime advances past report.lastVisibleActionCreated while the report is already mounted:
      In src/pages/inbox/report/ReportActionsList.tsx, when report.lastReadTime updates from a previous value to >= (report.lastVisibleActionCreated ?? '') and !unreadMarkerTime (no manual "Mark as unread" timestamp), clear currentUnreadMarkerReportActionID so "Mark all as read" removes the "New" line in the open chat as well.

    What alternative solutions did you explore? (Optional)

    I considered clearing stickyReport inside useSidebarOrderedReports.tsx whenever inboxTabCounts[CONST.INBOX_TAB.UNREAD] drops to 0. However, if a user opens the only unread chat on the Unread tab, reading that single chat also drops the unread count to 0, and stickyReport is intentionally designed to keep that chat visible until the user switches tabs or triggers a bulk "Mark all as read". Clearing stickyReport explicitly in confirmMarkAllAsRead preserves normal single-chat sticky behavior.

  27. Kabeer099 commented on Oct 25, 2026

    @Kabeer099

    Proposal

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

    When switching to the Unread tab in the Inbox and selecting Mark all as read from the long-press menu, the currently open chat remains visible in the Unread list and unread markers/states on single-transaction thread reports are not completely cleared.

    What is the root cause of that problem?

    1. In src/hooks/useSidebarOrderedReports.tsx, the sidebar maintains a stickyReport state ({reportID, tab}) so the currently viewed chat stays visible in the filtered list while the user is reading it. When confirmMarkAllAsRead in src/pages/inbox/sidebar/InboxTabSelector.tsx calls markAllMessagesAsRead(...), it never clears stickyReport, so the currently active chat stays pinned inside the Unread tab even after being marked read.
    2. In src/libs/actions/Report/MarkAllMessageAsRead.tsx (lines 63-75), markAllMessagesAsRead checks isUnread(report, oneTransactionThreadReport, isReportArchived, reportAttributesDerived?.[report.reportID]?.isEmpty). When a single-transaction parent report appears unread because its oneTransactionThreadReport is unread (or when reportIDs is passed for a specific tab), optimisticReports only writes {lastReadTime: newLastReadTime} to report.reportID without also updating oneTransactionThreadReport when oneTransactionThreadReportID is resolved.

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

    1. In src/hooks/useSidebarOrderedReports.tsx and src/pages/inbox/sidebar/InboxTabSelector.tsx, expose a clearStickyReport action from useSidebarOrderedReportsActions() and call it inside confirmMarkAllAsRead when markAllMessagesAsRead runs so the active report does not remain stuck in the Unread tab.
    2. In src/libs/actions/Report/MarkAllMessageAsRead.tsx, when oneTransactionThreadReport exists and is unread (isUnread(oneTransactionThreadReport, undefined, ...)), include ${ONYXKEYS.COLLECTION.REPORT}${oneTransactionThreadReport.reportID} in optimisticReports, failureReports, and reportIDList alongside the parent report.

    What alternative solutions did you explore? (Optional)

    Automatically clearing stickyReport inside a useEffect whenever isUnread flips to false was considered, but that would break the intended sticky behavior when a user simply clicks into an unread chat to read it normally. Clearing stickyReport specifically on the explicit Mark all as read action preserves normal single-chat sticky behavior.

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

Metadata

Metadata

Assignees

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorHelp WantedApply this label when an issue is open to proposals by contributorsOverdue

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions