Skip to content

[Due for payment 2026-08-08] [$250] Notification appears when user is focused on the report #95711

Description

@applause-bot

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.4.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: #95318
Email or phone of affected tester (no customers): gocemate+buzz1833@gmail.com
Issue reported by: Applause Internal Team
Bug source: Pull Request QA execution
Device used: windows 10/chrome
App Component: Notifications

Action Performed:

Precondition: User A has a one-single expense report

Steps:

  1. Open the one-single expense report as User A
  2. As User B, open the transaction thread (you can invite the user by mentioning them on the report)
  3. As User B, send a message on the transaction thread
  4. While the report is open as User A, verify no notification appears

NOTE: if you open both users on the same device (even with different tab/browser), on step 4, make sure the User A tab is open and focused (you can click into the app to make it focus)

Expected Result:

No notification should appear since user is focused on the report

Actual Result:

Notification appears when user is focused on the report

Workaround:

Unknown

Platforms:

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

Screenshots/Videos

Bug7203782_1783601052932.Recording__1076.mp4

View all open jobs on GitHub

Applause Internal Information
Exported by: Mita Rachim
Bug ID: 7203782

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

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Jul 9, 2026
  2. applause-bot commented on Jul 9, 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/4c61004453dacadaa5a21083ce388661647a65d0cc0abede2f65f8e42bb3b7e1

  3. MelvinBot commented on Jul 9, 2026

    @MelvinBot
    Contributor

    Issue Analysis

    This is a regression from #95318 (deployed to staging in the reported version 9.4.32-0) — QA already flagged step 4 failing on that PR. The suppression code #95318 added is in main but it doesn't actually fire in this flow.

    Root Cause: The "don't notify me for the transaction thread of the report I'm viewing" check compares the incoming action's real transaction-thread reportID against topmostOneTransactionThreadReportID at Report/index.ts:4616. For User A (a viewer who never created the expense and never opened the thread), the single IOU action has no childReportID, so getOneTransactionThreadReportID falls back to the sentinel string CONST.FAKE_REPORT_ID ('FAKE_REPORT_ID'). 'FAKE_REPORT_ID' === '<real thread id>' is always false → the check never suppresses → the notification shows.

    Recommended Fix: Don't rely on the viewer's (often-empty) childReportID. Instead, suppress by checking the incoming thread's own parent link: if the report for the incoming reportID is a transaction thread whose parentReportID equals topmostReportID (the one-transaction report being viewed), suppress. The thread report always carries parentReportID from the server, so this doesn't depend on the optimistic childReportID. At minimum, also guard the existing comparison so a FAKE_REPORT_ID value can never be treated as a match.

    Investigation details & evidence

    Data flow (local Pusher path):

    1. User A focuses the one-transaction expense report → getTopmostReportId() = expenseReportID.
    2. useOneTransactionThreadReportID(expenseReportID) finds the single IOU action correctly, but returns iouAction.childReportID ?? CONST.FAKE_REPORT_ID → ReportActionsUtils.ts:1972. On a viewer's client childReportID is unset (it's only set optimistically for the expense creator — ReportUtils.ts:8894), so it returns 'FAKE_REPORT_ID'.
    3. User B posts on the real transaction thread → triggerNotifications passes the real thread reportID.
    4. shouldShowReportActionNotification: realID !== expenseReportID (L4610 passes) and realID !== 'FAKE_REPORT_ID' (L4616 passes) → returns true → notification shown.

    Constant: CONST.FAKE_REPORT_ID: 'FAKE_REPORT_ID'.

    Why #95318's QA missed it: the tests in tests/unit/showReportActionNotificationTest.ts always pass topmostOneTransactionThreadReportID as undefined, so the L4616 branch is never exercised with a real incoming thread ID + the FAKE_REPORT_ID fallback. A regression test should set up a one-transaction report as topmost (viewer has no childReportID), an incoming action on the real thread ID with visibility/focus true, and assert no notification fires.

    Ruled out: wrong topmost report (the expense report's actions are read correctly), wrong incoming reportID (it's the correct real thread ID), and leader/staleness (leader check already passed; ref update is a tick behind at most — secondary).

    Confidence: High — deterministic code path, verified end-to-end with file:line evidence. Since this is a fresh regression from a just-merged PR, bernhardoj (PR author) may be best placed to fold the fix into a follow-up.


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

  4. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Jul 9, 2026
  5. nabi-ebrahimi commented on Jul 9, 2026

    @nabi-ebrahimi
    Contributor

    🚨 Edited by proposal-police: This proposal was edited at 2026-07-20 09:16:56 UTC.

    Proposal

    What is the root cause of that problem?

    The notification suppression path now depends on a precomputed topmostOneTransactionThreadReportID: AuthScreensInitHandler derives it from the focused report and passes it to Pusher callbacks (

    const topmostReportID = useRootNavigationState(Navigation.getTopmostReportId);
    const topmostOneTransactionThreadReportID = useOneTransactionThreadReportID(topmostReportID);
    // We use a ref so the Pusher callback (registered once on mount) always reads the latest value without re-subscribing.
    const topmostOneTransactionThreadReportIDRef = useRef(topmostOneTransactionThreadReportID);
    useEffect(() => {
    topmostOneTransactionThreadReportIDRef.current = topmostOneTransactionThreadReportID;
    }, [topmostOneTransactionThreadReportID]);
    ), then Pusher calls triggerNotifications() after applying the incoming Onyx update and forwards that cached ID into showReportActionNotification() (
    function triggerNotifications<TKey extends OnyxKey>(
    onyxUpdates: Array<OnyxServerUpdate<TKey>>,
    currentUserAccountID: number,
    currentUserEmail: string,
    topmostOneTransactionThreadReportID: string | undefined,
    reportAttributes?: ReportAttributesDerivedValue['reports'],
    ) {
    for (const update of onyxUpdates) {
    if (!update.shouldNotify && !update.shouldShowPushNotification) {
    continue;
    }
    const reportID = update.key.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, '');
    const reportActions = Object.values((update.value as OnyxCollection<ReportAction>) ?? {});
    for (const action of reportActions) {
    if (action) {
    // They aren't connected to a UI anywhere, it's OK to use currentUserEmail
    showReportActionNotification(reportID, action, topmostOneTransactionThreadReportID, currentUserAccountID, currentUserEmail, reportAttributes);
    ,
    const onyxUpdatePromise = Onyx.update(pushJSON).then(() => {
    triggerNotifications(pushJSON, currentUserAccountID, currentUserEmail, getTopmostOneTransactionThreadReportID(), getReportAttributes?.());
    });
    ). shouldShowReportActionNotification() only suppresses a transaction-thread notification when the incoming reportID exactly equals that precomputed thread ID (
    reportID: string,
    topmostOneTransactionThreadReportID: string | undefined,
    currentUserAccountID: number,
    action: ReportAction | null = null,
    isRemote = false,
    ): boolean {
    const tag = isRemote ? '[PushNotification]' : '[LocalNotification]';
    const topmostReportID = Navigation.getTopmostReportId();
    // Due to payload size constraints, some push notifications may have their report action stripped
    ).

    The problem is that the focused report ID used to compute topmostOneTransactionThreadReportID can be incorrect on wide/RHP flows. AuthScreensInitHandler currently derives the focused report from Navigation.getTopmostReportId(), but that helper only reads the report from REPORTS_SPLIT_NAVIGATOR. When a one-transaction expense report is visible in the RHP/super-wide RHP, the central pane can still hold the parent chat, so Navigation.getTopmostReportId() can return the background chat report ID instead of the expense report ID that the user is actually viewing. This matches the C+ feedback that the report ID is incorrect before reload.

    The read-only/composer-blocked case after reload looks like a separate permission issue, as discussed in the latest comments. However, the notification bug still applies to the valid expense-in-DM flow, where the user is focused on the related one-transaction report/thread context but the notification suppression still uses the wrong focused report source. Because the global notification path can start from the background split report instead of the visible expense report, it can compute undefined or an unrelated topmostOneTransactionThreadReportID, so a new message on the related transaction thread is treated as a different report and the browser notification is shown.

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

    First, derive the currently viewed report ID from the visible report context, not only from Navigation.getTopmostReportId(). When a report is open in the RHP/super-wide RHP, prefer that report ID; otherwise fall back to the current Navigation.getTopmostReportId() behavior. Then pass this corrected focused report ID into useOneTransactionThreadReportID().

    Also update the direct foreground push-notification derivation in shouldShowPushNotification() to use the corrected currently viewed report ID before deriving topmostOneTransactionThreadReportID (

    shouldShow = true;
    } else {
    const reportAction = ReportActionUtils.getLatestReportActionFromOnyxData(data.onyxData ?? null);
    const topmostReportID = Navigation.getTopmostReportId();
    const topmostReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${topmostReportID}`];
    const topmostChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${topmostReport?.chatReportID}`];
    const topmostReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${topmostReportID}`];
    ). That keeps local web notifications and native foreground push notifications aligned.

    Use the same corrected currently viewed report ID in shouldShowReportActionNotification() for the direct current-report comparison, so both notification suppression checks use the same source of truth for the report the user is actually focused on.

    What alternative solutions did you explore? (Optional)

    I considered broadening shouldShowReportActionNotification() to suppress any notification whose report is a child of the current report, but that would incorrectly hide transaction-thread notifications for multi-transaction reports where the thread message is not actually visible in the parent report. The safer fix is to make the “currently viewed report” source of truth use the correct visible report ID for notification suppression.

    I also considered changing Navigation.getTopmostReportId() globally, but that helper is used by many unrelated callers that may intentionally want the split-navigator report. Keeping the RHP-first behavior scoped to notification suppression avoids changing broader navigation semantics.

    Fixed Demo:

    Screencast.From.2026-07-10.02-22-09.mp4
  6. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on Jul 9, 2026
  7. dilshodmackbook-sketch commented on Jul 9, 2026

    @dilshodmackbook-sketch
    Contributor

    🚨 Edited by proposal-police: This proposal was edited at 2026-07-10 13:12:53 UTC.

    Proposal

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

    When User A is focused on a one-transaction expense report and User B sends a message on that report's transaction thread, a notification is shown to User A even though they are actively viewing the report.

    What is the root cause of that problem?

    shouldShowReportActionNotification decides whether the user is currently viewing the relevant report using two "topmost report" values, and both of them resolve through Navigation.getTopmostReportId():

    • directly, for the "current report" check:

    const topmostReportID = Navigation.getTopmostReportId();

    // If we are currently viewing this report do not show a notification.
    if (reportID === topmostReportID && Visibility.isVisible() && Visibility.hasFocus()) {
    Log.info(`${tag} No notification because it was a comment for the current report`);
    return false;
    }
    // If the report is a transaction thread and we are currently viewing the associated one-transaction report do no show a notification.
    if (reportID === topmostOneTransactionThreadReportID && Visibility.isVisible() && Visibility.hasFocus()) {
    Log.info(`${tag} No notification because the report is a transaction thread associated with the current one-transaction report`);
    return false;
    }

    • indirectly, for the one-transaction-thread check: AuthScreensInitHandler derives topmostOneTransactionThreadReportID from useRootNavigationState(Navigation.getTopmostReportId) and hands it to the Pusher callback

    const topmostReportID = useRootNavigationState(Navigation.getTopmostReportId);
    const topmostOneTransactionThreadReportID = useOneTransactionThreadReportID(topmostReportID);

    getTopmostReportId comes from getTopmostReportParams, which only ever inspects SCREENS.REPORT screens inside REPORTS_SPLIT_NAVIGATOR:

    function getTopmostReportParams(state: State): ReportsSplitNavigatorParamList[typeof SCREENS.REPORT] | undefined {
    if (!state) {
    return;
    }
    let topmostReportsSplitNavigator = state.routes?.findLast((route) => route.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR);
    if (!topmostReportsSplitNavigator) {
    const rootTab = state.routes?.findLast((route) => route.name === NAVIGATORS.TAB_NAVIGATOR);
    topmostReportsSplitNavigator = rootTab?.state?.routes?.findLast((route) => route.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR);
    }
    if (!topmostReportsSplitNavigator) {
    return;
    }
    const topmostReport = topmostReportsSplitNavigator.state?.routes.findLast((route) => route.name === SCREENS.REPORT);
    if (!topmostReport) {
    return;
    }
    return topmostReport?.params as ReportsSplitNavigatorParamList[typeof SCREENS.REPORT];
    }

    But a one-transaction expense report is normally not viewed on that screen:

    • On wide layouts (the repro platforms: Windows/Chrome, macOS/Chrome), opening the expense report from the chat opens it in the super-wide RHP (SCREENS.RIGHT_MODAL.EXPENSE_REPORT / SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT), while the central pane underneath still holds the parent chat. In this state getTopmostReportId() keeps returning the parent chat's reportID — a value that is wrong, not just missing.

    const WIDE_RIGHT_MODALS = new Set<string>([SCREENS.RIGHT_MODAL.SEARCH_REPORT]);
    const SUPER_WIDE_RIGHT_MODALS = new Set<string>([SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, SCREENS.RIGHT_MODAL.EXPENSE_REPORT]);
    const ALL_WIDE_RIGHT_MODALS = new Set<string>([...WIDE_RIGHT_MODALS, ...SUPER_WIDE_RIGHT_MODALS]);

    • When the report is opened from the Reports tab, it renders on SCREENS.SEARCH.MONEY_REQUEST_REPORT inside SEARCH_FULLSCREEN_NAVIGATOR, where getTopmostReportId() returns undefined.

    In both cases useOneTransactionThreadReportID(topmostReportID) is fed the wrong report (the parent chat, or undefined), so it returns undefined, the reportID === topmostOneTransactionThreadReportID check never matches, and the notification fires even though User A is focused on the report. The Airship push path has the same blind spot, since it computes topmostOneTransactionThreadReportID from Navigation.getTopmostReportId() too:

    const topmostReportID = Navigation.getTopmostReportId();
    const topmostReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${topmostReportID}`];
    const topmostChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${topmostReport?.chatReportID}`];
    const topmostReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${topmostReportID}`];
    const topmostOneTransactionThreadReportID = ReportActionUtils.getOneTransactionThreadReportID(topmostReport, topmostChatReport, topmostReportActions, getIsOffline());
    shouldShow = Report.shouldShowReportActionNotification(String(data.reportID), topmostOneTransactionThreadReportID, currentUserAccountID, reportAction, true);

    This also means the bug is not really a regression of #95318: both before and after that PR, the "which report is the user viewing" resolution only ever looked at the split navigator; viewing the report in the RHP or the Reports tab was simply never covered by this code path.

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

    Add a getCurrentlyViewedReportID helper in Navigation.ts that resolves the report in visual stacking order — the RHP first, because an open wide/super-wide RHP renders above the central pane, so checking getTopmostReportId() first would short-circuit on the chat underneath it. I deliberately keep getTopmostReportId itself untouched since it has ~24 call sites with navigation semantics we don't want to change. Navigation.ts already imports ALL_WIDE_RIGHT_MODALS, and useCurrentReportID already sets the precedent of resolving RHP report params separately from the split navigator (getFocusedRouteReportID):

    * Traverse the focused route at each level of the navigation state to find a reportID param.
    * This handles modal navigators (e.g. RightModalNavigator > ExpenseReport) that carry a reportID
    * in their screen params but are not part of the ReportsSplitNavigator hierarchy.
    */
    function getFocusedRouteReportID(state: NavigationState | PartialState<NavigationState>): string | undefined {
    const index = state.index ?? state.routes.length - 1;
    const focusedRoute = state.routes[index];
    if (!focusedRoute) {
    return;
    }
    if (focusedRoute.params && 'reportID' in focusedRoute.params && typeof focusedRoute.params.reportID === 'string') {
    return focusedRoute.params.reportID;
    }
    if (focusedRoute.state) {
    return getFocusedRouteReportID(focusedRoute.state);
    }
    }

    function getCurrentlyViewedReportID(state: NavigationState = navigationRef.getRootState()): string | undefined {
        // A report open in the wide / super-wide RHP (EXPENSE_REPORT, SEARCH_MONEY_REQUEST_REPORT, SEARCH_REPORT)
        // renders above the central pane, so it must be resolved first: while an RHP is open,
        // getTopmostReportId() still returns the central-pane chat underneath it.
        const lastRootRoute = state?.routes?.at(-1);
        if (lastRootRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
            const topmostWideRHPRoute = lastRootRoute.state?.routes?.findLast((route) => ALL_WIDE_RIGHT_MODALS.has(route.name));
            const rhpReportID = (topmostWideRHPRoute?.params as {reportID?: string} | undefined)?.reportID;
            if (rhpReportID) {
                return rhpReportID;
            }
        }
    
        // An expense report opened from the Reports tab renders on SCREENS.SEARCH.MONEY_REQUEST_REPORT inside
        // SEARCH_FULLSCREEN_NAVIGATOR (nested in TAB_NAVIGATOR), which getTopmostReportId() cannot see either.
        // Only consult it when the Search navigator is the focused full-screen tab.
        const tabState = state?.routes?.findLast((route) => route.name === NAVIGATORS.TAB_NAVIGATOR)?.state;
        const focusedTabRoute = tabState?.routes?.at(tabState.index ?? -1);
        if (focusedTabRoute?.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR) {
            const searchState = focusedTabRoute.state;
            const focusedSearchRoute = searchState?.routes?.at(searchState.index ?? -1);
            if (focusedSearchRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT) {
                return (focusedSearchRoute.params as {reportID?: string} | undefined)?.reportID;
            }
        }
    
        // Central-pane report — the existing behavior.
        return getTopmostReportId(state);
    }

    Then use it in the three places that drive the notification "am I viewing this report" logic.

    In shouldShowReportActionNotification (also fixes the plain "current report" check for messages sent to the RHP-open report itself):

    const topmostReportID = Navigation.getCurrentlyViewedReportID();

    In AuthScreensInitHandler, so the one-transaction thread lookup receives the expense report the user is actually viewing:

    const topmostReportID = useRootNavigationState(Navigation.getCurrentlyViewedReportID);

    In shouldShowPushNotification, so the native/Airship push path stays in sync:

    const topmostReportID = Navigation.getCurrentlyViewedReportID();

    With topmostReportID now resolving to the expense report, useOneTransactionThreadReportID returns the transaction thread's reportID, the one-transaction-thread guard matches the incoming notification, and it is correctly suppressed.

    What specific scenarios should we cover in automated tests to prevent this bug from reoccurring?

    • getCurrentlyViewedReportID with mocked navigation states: an expense report open in the super-wide RHP over a central-pane chat returns the expense report's reportID (not the chat's); a wide SEARCH_REPORT RHP returns its reportID; a focused SCREENS.SEARCH.MONEY_REQUEST_REPORT in the Search tab returns its reportID; a plain central-pane SCREENS.REPORT keeps the existing behavior.
    • shouldShowReportActionNotification returns false for a transaction-thread notification when the associated one-transaction expense report is currently viewed (visible + focused) in each of those navigation contexts.
    • It still returns true when the same report is not the currently viewed report, or the window is not focused, confirming we didn't over-suppress.

    What alternative solutions did you explore? (Optional)

    • Extend getTopmostReportParams itself to also look at the RHP and search screens, so every consumer of getTopmostReportId transparently benefits. I ruled this out as the primary approach because getTopmostReportId is consumed by ~24 navigation-related call sites (LHN highlighting, back navigation, unread handling), and broadening its meaning risks subtle regressions elsewhere; scoping the change to the notification path is safer.
    • Fixing useOneTransactionThreadReportID's useOnyx selector (it closes over report/chatReport without passing them as the dependencies argument, so the cached result can go stale). That is worthwhile hardening for the central-pane flow and could be included in the PR, but it cannot fix this issue on its own: whenever the report is viewed in the RHP or the Reports tab, the hook's input topmostReportID is already the wrong report (the parent chat or undefined), so its output can never be the right thread ID no matter how fresh the selector is.
  8. MobileMage commented on Jul 9, 2026

    @MobileMage
    Contributor

    🚫 Duplicated proposal withdrawn by 🤖 ProposalPolice.

  9. trasnake87 commented on Jul 9, 2026

    @trasnake87
    Contributor

    Proposal

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

    When User A has a single-expense report open and focused, and User B sends a message in that expense's transaction thread, User A still receives a notification — even though they are already viewing the report whose thread received the message.

    What is the root cause of that problem?

    This is a regression introduced by #95318 (merged 2026-07-08), which moved the local-notification path off of Onyx.connect(). Previously, shouldShowReportActionNotification read the topmost report and its actions from module-level connectWithoutView subscriptions in Report/index.ts (where allReportActions was keyed by bare reportID, old line 474) and computed getOneTransactionThreadReportID inline with always-fresh data.

    #95318 replaced that with a new hook whose result is cached in a ref (AuthScreensInitHandler.tsx:93-99) and handed to the Pusher callback (User.ts:979 → shouldShowReportActionNotification). The hook reads report and chatReport from two separate useOnyx subscriptions, then consumes them inside the REPORT_ACTIONS selector (src/hooks/useOneTransactionThreadReportID.tsx:10-14):

    const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
    const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`);
    const [oneTransactionThreadReportID] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {
        selector: (actions) => getOneTransactionThreadReportID(report, chatReport, actions, isOffline),
    });

    But Onyx memoizes a useOnyx selector against its own key's data only. In react-native-onyx/dist/useOnyx.js:60-78 the memoized selector recomputes only when lastInput !== input (the actions changed) or the dependencies array changed. No dependencies array is passed here, so when report/chatReport arrive after the selector has cached its result, the cached value goes stale — and because report is still undefined at that moment, getOneTransactionThreadReportAction early-returns at the report?.type guard (ReportActionsUtils.ts:1898), so the cached value is undefined. The ref then holds undefined, the reportID === topmostOneTransactionThreadReportID check in Report/index.ts:4616 fails, and the notification fires.

    (The Airship push path in shouldShowPushNotification.ts:59-64 computes topmostOneTransactionThreadReportID inline from connectWithoutView data, so it is unaffected — the broken path is the local one that fires when the tab is online and focused.)

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

    Pass the external closure values as the third dependencies argument — the exact mechanism Onyx provides for selectors that depend on other keys (useOnyx.js:116-134 invalidates the cache and forces a recompute when dependencies change; signature useOnyx(key, options, dependencies?) at useOnyx.d.ts:25). This matches the existing codebase idiom (usePolicyForTransaction.ts:42, useSelectionModeReportActions.ts:69, usePolicyData/index.ts:47):

    const [oneTransactionThreadReportID] = useOnyx(
        `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
        {selector: (actions) => getOneTransactionThreadReportID(report, chatReport, actions, isOffline)},
        [report, chatReport, isOffline],
    );

    That single change makes the selector recompute whenever report, chatReport, or connectivity changes, so the cached topmostOneTransactionThreadReportID always reflects the report User A is viewing and shouldShowReportActionNotification correctly suppresses the notification.

    What alternative solutions did you explore? (Optional)

    Computing getOneTransactionThreadReportID(...) via useMemo over the three useOnyx results instead of a selector. Also correct, but it discards the selector's re-render optimization (the reason #95318 introduced the hook) and re-renders AuthScreensInitHandler on every REPORT_ACTIONS update. The dependencies argument preserves #95318's intent with a one-line, idiomatic fix.

  10. samranahm commented on Jul 9, 2026

    @samranahm
    Contributor

    🚫 Duplicated proposal withdrawn by 🤖 ProposalPolice.

  11. melvin-bot commented on Jul 9, 2026

    @melvin-bot

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

  12. changed the title [-]Notification appears when user is focused on the report[/-] [+][$250] Notification appears when user is focused on the report[/+] on Jul 9, 2026
  13. 74 remaining items

  14. DylanDylann commented on Aug 7, 2026

    @DylanDylann
    Contributor

    BugZero Checklist:

    • [Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.

      Link to the comment on the PR: I don't think this is a regression from a previous PR. It's an edge case caused by mixing some changes from the navigation refactor and the report view implementation
      Link to the comment on the Issue:

    • [Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.

      Link to discussion:

    • [Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.

    • [BugZero Assignee] Create a GH issue for creating/updating the regression test once above steps have been agreed upon.

      Link to issue: https://github.com/Expensify/Expensify/issues/670283

    Regression Test Proposal

    Test:

    Precondition: User A has a one-single expense report

    1. Open the one-single expense report as User A
    2. As User B, open the transaction thread (you can invite the user by mentioning them on the report)
    3. As User B, send a message on the transaction thread
    4. While the report is open as User A, verify no notification appears

    Do we agree 👍 or 👎

    Zapier Logs Run ID: 00040eee-66d4-add8-0128-00c2cfb1ff54
  15. added
    Awaiting PaymentAuto-added when associated PR is deployed to production
    and removed on Aug 7, 2026
  16. melvin-bot commented on Aug 7, 2026

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  17. melvin-bot commented on Aug 7, 2026

    @melvin-bot

    Payment Summary

    Resolving PRs:

    BugZero Checklist (@mallenexpensify)

    • I have confirmed assignees, roles, and Upwork contracts look correct
    • I have paid out Upwork contracts / manual NewDot requests
  18. melvin-bot commented on Aug 10, 2026

    @melvin-bot

    @mallenexpensify Whoops! This issue is 2 days overdue. Let's get this updated quick!

  19. mallenexpensify commented on Aug 11, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor: @nkdengineer due $250 via NewDot
    Contributor+: @DylanDylann due $250 via NewDot

    ^ Test case created. Thx

  20. twisterdotcom commented on Aug 18, 2026

    @twisterdotcom
    Contributor

    $250 approved for @nkdengineer in Report ID: R00d83bEd3e3.

    Zapier Logs Run ID: 00040eee-410e-a2a5-9bf5-ba28da6b07a4

    SO: https://stackoverflowteams.com/c/expensify/questions/7582

  21. quinthar commented on Sep 26, 2026

    @quinthar
    Contributor

    Approved $250 to @DylanDylann

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

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething 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