Repository navigation
Description
If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.
Version Number: v9.5.10-1
Reproducible in staging?: Yes
Reproducible in production?: Yes
If this was caught during regression testing, add the test name, ID and link from BrowserStack: #102247
Email or phone of affected tester (no customers): lekima20+617010@gmail.com
Issue reported by: Applause Internal Team
Bug source: Exploratory - Significant User Experience Deterioration
Device used: iPhone 16 pro/ 26.6
App Component: Chat Report View
Action Performed:
Precondition:
Log in the same account on Hybrid app, mweb Safari, mweb Chrome
- Open the Hybrid app
- Create a chat with any user
- Send a message
- Navigate to mweb Safari and Chrome reload the page for see the chat appearing in LHN (not open the chat)
- Back to Hybrid app, send a message to the chat
- Navigate to mweb Chrome
- Open the chat
- Navigate to mweb Safari
- Open the chat
Expected Result:
The new message indicator shouldn't display in conversation when opening the chat in mweb Chrome
Actual Result:
The new message indicator displays in conversation when opening the chat in mweb Chrome. (The new message indicator displays in conversation on both mweb Safari and Chrome on the first time testing. After that, only can repro on mweb Chrome. If continue, it can't repro on Chrome too, but can repro on Chrome when change new account, not repro on Safari when change account)
Workaround:
Unknown
Platforms:
- Android: App
- Android: mWeb Chrome
- iOS: App
- iOS: mWeb Safari
- iOS: mWeb Chrome
- Windows: Chrome
- MacOS: Chrome Safari
Screenshots/Videos
To be added by Applause Leads
Bug7287660_1792118557922.the_first_time_testing_new_message_indicator_display_both_mweb_chrom_and_safari.mp4
Bug7287660_1792119163064.only_occur_on_mweb_chrome.mp4
Applause Internal Information
Exported by: Chandana Kiran
Bug ID: 7287660
Upwork Automation - Do Not Edit
- Upwork Job URL: https://www.upwork.com/jobs/~022110931877987706140
- Upwork Job ID: 2110931877987706140
- Last Price Increase: 2026-10-16
Issue Owner
Current Issue Owner: @mallenexpensifyMetadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
applause-bot commentedon Oct 16, 2026
While Applause doesn't embed attachments for this bug, here are the links for quick view:
https://platform.applause.com/services/links/v1/external/1dfcfcb3965556dd0f4381114f8de8214a54d8a67e47e76f5db7964fab4220f4
https://platform.applause.com/services/links/v1/external/89bea45291e3934d7841333384c1803209da2de80e50d220d59e0006dcdbcb6f
MelvinBot commentedon Oct 16, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
When the same account is signed in on several devices, opening a chat on a second web session can show the "New messages" marker above a message the user sent from another device.
What is the root cause of that problem?
On first open,
useUnreadMarkerplaces the marker on a "pagination anchor" without checking who wrote that message.usePaginatedReportActions.ts:57snapshotsreport.lastReadTime. Line 82 picks the oldest action newer than that time as the anchor. It has no actor check. On the second session,lastReadTimecan be older than the user's own message, because only the sending device updateslastReadTimeoptimistically.shouldDisplayNewMarkerOnReportAction.ts:110-116correctly skips self-authored messages when no marker exists yet, so it returnsnull.useUnreadMarker.ts:145-146then falls back to the anchor when the scan returnsnull. The marker lands on the user's own message.prevUnreadMarkerReportActionID. On the next render, the self-authored branch atshouldDisplayNewMarkerOnReportAction.ts:113-114keeps it, because a marker "already exists."This only triggers when the session's local
lastReadTimeis stale at mount time andhasOnceLoadedReportActionsis still false. That timing dependence explains why it reproduces on some browsers and accounts but not others.What changes do you think we should make in order to solve the problem?
In
useUnreadMarker.ts(lines 121-126), only use the pagination anchor as the marker when the anchored action was not written by the current user. Use the same actor rule as the scan (childLastActorAccountIDfor report previews, otherwiseactorAccountID). The scan then decides, and it already skips the user's own messages. Unread messages from other users after the user's own message still get the marker. Scroll positioning stays the same.Add a
useUnreadMarkerTestcase: whenoldestUnreadReportActionIDpoints to a self-authored action andhasOnceLoadedReportActionsis false,unreadMarkerReportActionIDshould benull.What alternative solutions did you explore? (Optional)
Also skip self-authored actions in the
findLastatusePaginatedReportActions.ts:82, so the scroll anchor and marker agree. This is broader because it changes where the chat opens. I'd keep it out unless reviewers want both to match.Investigation details
lastReadTimeto the user's other sessions after they comment.lastReadTimeto lag behind the user's own messages on other sessions.ReportUtils.ts:10515-10520treats the chat as read when the current user is the last actor. That is why the LHN shows the chat as read, and why nothing triggers a "mark as read" that would clear the marker.unreadMarkerTimeis also snapshotted at mount (useUnreadMarker.ts:82), so a correctlastReadTimearriving after the chat opens does not help.useUnreadMarkerby [NoQA] decompose ReportActionsList: 4. The current self-authored branch comes from fix: report unread.Possibly related: Marking a chat as unread doesn't stick — client immediately re-reads it, and other clients never see the change (another multi-session
lastReadTimesync issue).Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/104178)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
view run
j3ffffff commentedon Oct 16, 2026
melvin-bot commentedon Oct 16, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @hoangzinh (
External)[-]mweb-New message indicator displays on self chat when open chat login on 3 sessions[/-][+][$175] mweb-New message indicator displays on self chat when open chat login on 3 sessions[/+]melvin-bot commentedon Oct 16, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022110931877987706140
melvin-bot commentedon Oct 16, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
camesenin commentedon Oct 16, 2026
melvin-bot commentedon Oct 16, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
taieddie commentedon Oct 16, 2026
Contributor details
Your Expensify account email: taieddie8@gmail.com
Upwork Profile Link: https://www.upwork.com/freelancers/~01e51b3f9ff738cf94
Proposal
Please re-state the problem that we are trying to solve in this issue.
When a user is signed in across multiple sessions and sends a message from one session, opening the chat for the first time in another session mistakenly displays the unread "New" message indicator above the user's own message.
What is the root cause of that problem?
The issue occurs in
useUnreadMarker.tsandshouldDisplayNewMarkerOnReportAction.ts:When opening a chat on a secondary session that has not yet updated its local
lastReadTime,usePaginatedReportActions.tssuppliesoldestUnreadReportActionIDpointing to the user's own message sent from the first session. InuseUnreadMarker.ts(lines 121-126), this initializesoldestUnreadReportActionMarker = [oldestUnreadReportActionID, visibleIndex].In
shouldDisplayNewMarkerOnReportAction.ts(lines 110-117), the scan correctly identifies that the message is from the current user (isFromCurrentUser === true). Because no unread marker existed yet in this session (prevUnreadMarkerReportActionID === null), it intentionally returnsfalse.getUnreadMarkerReportActiontherefore returnsscanned = [null, -1].In
useUnreadMarker.ts(lines 145-146), becausescanned[0] === null, the hook falls back tooldestUnreadReportActionMarkerwithout checking who authored that action (scanned[0] === null ? oldestUnreadReportActionMarker : scanned). This overridesshouldDisplayNewMarkerOnReportAction's intentional suppression of self-authored messages and renders the marker on the user's own message. Line 149 then latchesprevUnreadMarkerReportActionIDto this self-authored message, causing subsequent renders to continue displaying the indicator.In addition, in
getUnreadMarkerReportAction(shouldDisplayNewMarkerOnReportAction.ts), when evaluating an incoming reply that follows a self-authored message on fresh open,nextActionpoints to that older self-authored message. InsideshouldDisplayNewMarkerOnReportAction,isNextMessageUnreadevaluatesnextActionas timestamp-unread even when no older unread message from another user exists below it. This causes the incoming reply to be suppressed by the self-authored message, leaving both messages returningfalsein the scan. Also, on subsequent renders, ifprevUnreadMarkerReportActionIDis on an incoming reply, evaluatingisFromCurrentUserwithout checking whether the prior marker was self-authored can allow a self-authored message to steal the indicator.What changes do you think we should make in order to solve the problem?
src/hooks/useUnreadMarker.ts:isReportPreviewActionfrom@libs/ReportActionsUtils.oldestUnreadReportActionMarkerfrom the pagination anchor on first open (!hasOnceLoadedReportActions), check whether the anchored action was authored by the current user:const anchorAction = sortedVisibleReportActions.at(visibleIndex);const isSelfAuthored = anchorAction && (isReportPreviewAction(anchorAction) ? anchorAction.childLastActorAccountID : anchorAction.actorAccountID) === currentUserAccountID;if (!isSelfAuthored || manuallyMarkedUnreadReportActionID === oldestUnreadReportActionID) {oldestUnreadReportActionMarker = [oldestUnreadReportActionID, visibleIndex];}This ensures self-authored actions do not populate
oldestUnreadReportActionMarkeron fresh open unless explicitly marked as unread.src/pages/inbox/report/shouldDisplayNewMarkerOnReportAction.ts:shouldDisplayNewMarkerOnReportAction(line 77), hoistif (shouldHideNewMarker(message, isOffline)) return false;directly above theisEarliestReceivedOfflineMessagebranch (line 72). This ensures that any action pending deletion while online is rejected before evaluating offline arrival, guaranteeing that an action whose marker must be hidden never displays an unread marker even whennextMessageis undefined or read.getUnreadMarkerReportAction, skip actions whose marker should be hidden (if (shouldHideNewMarker(reportAction, isOffline)) continue;), determine whether the previous marker was self-authored or is relocating after deletion, and find the index of the oldest visible eligible unread action from another user (excluding synthetic greetings and hidden actions):const isActionSelfAuthored = (action: OnyxTypes.ReportAction | undefined) => !!action && currentUserAccountID === (isReportPreviewAction(action) ? action.childLastActorAccountID : action.actorAccountID);const prevMarkedAction = prevUnreadMarkerReportActionID ? (prevSortedVisibleReportActionsObjects[prevUnreadMarkerReportActionID] ?? visibleReportActions.find((action) => action.reportActionID === prevUnreadMarkerReportActionID)) : undefined;const isPrevMarkerFromCurrentUser = isActionSelfAuthored(prevMarkedAction);const isMarkerRelocatingAfterDeletion = !!prevUnreadMarkerReportActionID && !isPrevUnreadMarkerReportActionPresent;const isActionEligibleOtherUserUnread = (action: OnyxTypes.ReportAction | undefined) => !!action && action.reportActionID !== CONST.CONCIERGE_GREETING_ACTION_ID && !isActionSelfAuthored(action) && isReportActionUnread(action, unreadMarkerTime) && !shouldHideNewMarker(action, isOffline);const oldestOtherUserUnreadIndex = isReversed ? visibleReportActions.findIndex(isActionEligibleOtherUserUnread) : visibleReportActions.findLastIndex(isActionEligibleOtherUserUnread);nextAction, check whether an older unread message from another user exists belownextAction, or if a self-authored marker is active or relocating:const hasOlderUnreadFromOtherUser = isReversed ? oldestOtherUserUnreadIndex >= 0 && oldestOtherUserUnreadIndex < (index - 1) : oldestOtherUserUnreadIndex >= 0 && oldestOtherUserUnreadIndex > (index + 1);const isNextActionEligible = !!nextAction && nextAction.reportActionID !== CONST.CONCIERGE_GREETING_ACTION_ID && !shouldHideNewMarker(nextAction, isOffline);const shouldSuppressViaNextAction = hasOlderUnreadFromOtherUser || (isNextActionEligible && (!isActionSelfAuthored(nextAction) || isPrevMarkerFromCurrentUser || isMarkerRelocatingAfterDeletion || activeManuallyMarkedUnreadReportActionID === nextAction?.reportActionID));const effectiveNextAction = shouldSuppressViaNextAction ? nextAction : undefined;nextMessage: effectiveNextActionintoshouldDisplayNewMarkerOnReportAction.This preserves the signature and parameter types of
shouldDisplayNewMarkerOnReportActionwith 100% backwards compatibility for existing unit tests, while guaranteeing:oldestUnreadReportActionMarkerremainsundefined, anduseUnreadMarkercleanly evaluates toscanned([null, -1]), displaying no indicator.hasOlderUnreadFromOtherUserandisPrevMarkerFromCurrentUserarefalse, so the self-authored message does not suppress the reply. The reply naturally receives the marker through the standard scan with live window focus and scroll threshold checks intact, and the self-authored message cannot steal it on subsequent renders.CONST.CONCIERGE_GREETING_ACTION_ID) cannot anchor the unread marker and do not count as older unread messages from other users, preventing self-authored messages from falsely suppressing incoming replies.isMarkerRelocatingAfterDeletionevaluates totrue, so older self-authored actions suppress newer ones, strictly preserving oldest-unread placement.O1, S, O2),hasOlderUnreadFromOtherUseristrueforO2, soSsuppressesO2even ifSis pending deletion, ensuring the oldest unread messageO1receives the indicator.!shouldHideNewMarker(action, isOffline), preventing hidden actions from falsely suppressing incoming replies.manuallyMarkedUnreadReportActionIDis fully preserved.I will also add unit tests in
tests/unit/useUnreadMarkerTest.tsandtests/unit/ReportActionsUtilsTest.ts:unreadMarkerReportActionIDevaluates tonullon fresh open (hasOnceLoadedReportActions: false) when the only unread message is self-authored.unreadMarkerReportActionIDcorrectly anchors on a self-authored message on fresh open ifmanuallyMarkedUnreadReportActionID === oldestUnreadReportActionID.getUnreadMarkerReportActionacross multiple renders without hopping back onto the self-authored message.O1, S, O2) correctly anchor the unread marker on the oldest unread messageO1, even when the middle self-authored actionSis pending deletion online.What alternative solutions did you explore? (Optional)
Earlier proposals suggest modifying
ShouldDisplayNewMarkerOnReportActionParamsorshouldDisplayNewMarkerOnReportActiondirectly, which references out-of-scope variables or breaks backwards compatibility with direct unit test callers inReportActionsUtilsTest.ts. Other proposals unconditionally exclude self-authorednextMessagechecks without verifying whether older unread messages from other users exist below it or whether an existing marker is relocating after deletion (prevUnreadMarkerReportActionID), which breaks the oldest-unread invariant across multiple unread self-authored messages or in interleaved sequences (O1, S, O2). Proposals that evaluateprevUnreadMarkerReportActionIDwithout checking whether that prior marker is self-authored cause the indicator to hop onto the user's own message on subsequent renders when an incoming reply is present. In addition, competitors post prohibited raw diff blocks and introduce syntax errors by referencingactionout of scope. Handling the contextualeffectiveNextActioncleanly insidegetUnreadMarkerReportActionwhile guarding the pagination anchor inuseUnreadMarker.tsresolves the root cause with zero regressions.33 remaining items
jeffrey-graham commentedon Nov 7, 2026
melvin-bot commentedon Nov 7, 2026
melvin-bot commentedon Nov 8, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)melvin-bot commentedon Nov 8, 2026
Payment Summary
Resolving PRs:
Don't show the New messages marker above the user's own message on first open #105113
Reviewer: @hoangzinh owed $175 via NewDot
BugZero Checklist (@mallenexpensify)
mallenexpensify commentedon Nov 11, 2026
Claude posting on behalf of Matt:
I'll pay these tomorrow, apologies for the delay
mallenexpensify commentedon Nov 11, 2026
Payment Summary
Contributor+: @hoangzinh due $175 via NewDot