From 0fac7d04ca629ec48cec8acda486c5ffa3b3341e Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 2 Jul 2026 22:52:09 +0800 Subject: [PATCH 01/10] pass reportActions as param to shouldShowReportActionNotification --- .../AppNavigator/AuthScreensInitHandler.tsx | 43 ++++++++++++++++--- .../shouldShowPushNotification.ts | 11 ++++- src/libs/actions/Report/index.ts | 13 ++++-- src/libs/actions/User.ts | 14 ++++-- tests/actions/IOU/RequestMoneyTest.ts | 2 +- .../actions/IOUTest/DeleteMoneyRequestTest.ts | 2 +- tests/actions/IOUTest/TrackExpenseTest.ts | 4 +- tests/actions/ReportTest.ts | 10 ++--- tests/ui/GroupChatNameTests.tsx | 2 +- tests/ui/PaginationTest.tsx | 2 +- tests/ui/UnreadIndicatorsTest.tsx | 2 +- .../unit/showReportActionNotificationTest.ts | 15 +++++-- 12 files changed, 90 insertions(+), 30 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx index 23b986838e78..76e278696461 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx @@ -1,5 +1,6 @@ import {hasSeenTourSelector} from '@selectors/Onboarding'; import {useEffect, useRef} from 'react'; +import type {OnyxCollection} from 'react-native-onyx'; import {useInitialURLActions, useInitialURLState} from '@components/InitialURLContextProvider'; import useActivePolicy from '@hooks/useActivePolicy'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -31,15 +32,20 @@ import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {ReportAttributesDerivedValue} from '@src/types/onyx'; - -function initializePusher(currentUserAccountID?: number, currentUserEmail?: string, getReportAttributes?: () => ReportAttributesDerivedValue['reports'] | undefined) { +import type {ReportActions, ReportAttributesDerivedValue} from '@src/types/onyx'; + +function initializePusher( + currentUserAccountID: number | undefined, + currentUserEmail: string | undefined, + getReportActions: () => OnyxCollection, + getReportAttributes: () => ReportAttributesDerivedValue['reports'] | undefined, +) { return Pusher.init({ appKey: CONFIG.PUSHER.APP_KEY, cluster: CONFIG.PUSHER.CLUSTER, authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`, }).then(() => { - User.subscribeToUserEvents(currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, currentUserEmail ?? '', getReportAttributes); + User.subscribeToUserEvents(currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, currentUserEmail ?? '', getReportActions, getReportAttributes); }); } @@ -75,8 +81,16 @@ function AuthScreensInitHandler() { const reportAttributesRef = useRef(reportAttributes); reportAttributesRef.current = reportAttributes; + const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); + // We use a ref so the Pusher callback (registered once on mount) always reads the latest value without re-subscribing. + const reportActionsRef = useRef(reportActions); + useReconcileHighContrastIntent(); + useEffect(() => { + reportActionsRef.current = reportActions; + }, [reportActions]); + useEffect(() => { registerPusherReinitializeHandler(({accountID, email}: PusherReinitializeHandlerParams = {}) => { const currentAccountID = accountID ?? session?.accountID; @@ -86,7 +100,12 @@ function AuthScreensInitHandler() { return Promise.resolve(); } - return initializePusher(currentAccountID, currentEmail, () => reportAttributesRef.current); + return initializePusher( + currentAccountID, + currentEmail, + () => reportActionsRef.current, + () => reportAttributesRef.current, + ); }); return () => { @@ -99,7 +118,12 @@ function AuthScreensInitHandler() { return; } // This means sign in in RHP was successful, so we can subscribe to user events - initializePusher(session?.accountID, session?.email, () => reportAttributesRef.current); + initializePusher( + session?.accountID, + session?.email, + () => reportActionsRef.current, + () => reportAttributesRef.current, + ); }, [session?.accountID, session?.email]); useEffect(() => { @@ -122,7 +146,12 @@ function AuthScreensInitHandler() { }); PusherConnectionManager.init(); - initializePusher(session?.accountID, session?.email, () => reportAttributesRef.current).finally(() => { + initializePusher( + session?.accountID, + session?.email, + () => reportActionsRef.current, + () => reportAttributesRef.current, + ).finally(() => { endSpan(CONST.TELEMETRY.SPAN_NAVIGATION.PUSHER_INIT); }); diff --git a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts index 31e9a3ecacdd..ef031e6e7275 100644 --- a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts +++ b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts @@ -1,10 +1,12 @@ import type {PushPayload} from '@ua/react-native-airship'; import Onyx from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; import Log from '@libs/Log'; import * as ReportActionUtils from '@libs/ReportActionsUtils'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportActions} from '@src/types/onyx'; import parsePushNotificationPayload from './parsePushNotificationPayload'; // We do not depend on updates on the UI for notifications, so we can use `connectWithoutView` here. @@ -16,6 +18,13 @@ Onyx.connectWithoutView({ }, }); +let allReportActions: OnyxCollection; +Onyx.connectWithoutView({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (value) => (allReportActions = value), +}); + /** * Returns whether the given Airship notification should be shown depending on the current state of the app */ @@ -32,7 +41,7 @@ export default function shouldShowPushNotification(pushPayload: PushPayload): bo shouldShow = true; } else { const reportAction = ReportActionUtils.getLatestReportActionFromOnyxData(data.onyxData ?? null); - shouldShow = Report.shouldShowReportActionNotification(String(data.reportID), currentUserAccountID, reportAction, true); + shouldShow = Report.shouldShowReportActionNotification(String(data.reportID), allReportActions, currentUserAccountID, reportAction, true); } Log.info(`[PushNotification] ${shouldShow ? 'Showing' : 'Not showing'} notification`); diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 35da2d71442e..c629fd6d28a6 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -4554,7 +4554,13 @@ function setIsComposerFullSize(reportID: string, isComposerFullSize: boolean) { * @param isRemote whether or not this notification is a remote push notification * @param currentUserAccountID the current user's account ID */ -function shouldShowReportActionNotification(reportID: string, currentUserAccountID: number, action: ReportAction | null = null, isRemote = false): boolean { +function shouldShowReportActionNotification( + reportID: string, + reportActions: OnyxCollection, + currentUserAccountID: number, + action: ReportAction | null = null, + isRemote = false, +): boolean { const tag = isRemote ? '[PushNotification]' : '[LocalNotification]'; const topmostReportID = Navigation.getTopmostReportId(); @@ -4591,7 +4597,7 @@ function shouldShowReportActionNotification(reportID: string, currentUserAccount // If the report is a transaction thread and we are currently viewing the associated one-transaction report do no show a notification. const topmostReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${topmostReportID}`]; - const topmostReportActions = allReportActions?.[`${topmostReport?.reportID}`]; + const topmostReportActions = reportActions?.[`${topmostReport?.reportID}`]; const chatTopmostReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${topmostReport?.chatReportID}`]; if ( reportID === ReportActionsUtils.getOneTransactionThreadReportID(topmostReport, chatTopmostReport, topmostReportActions, isOfflineNetwork()) && @@ -4626,11 +4632,12 @@ function shouldShowReportActionNotification(reportID: string, currentUserAccount function showReportActionNotification( reportID: string, reportAction: ReportAction, + reportActions: OnyxCollection, currentUserAccountID: number, currentUserLogin: string, reportAttributes?: ReportAttributesDerivedValue['reports'], ) { - if (!shouldShowReportActionNotification(reportID, currentUserAccountID, reportAction)) { + if (!shouldShowReportActionNotification(reportID, reportActions, currentUserAccountID, reportAction)) { return; } diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 4c53672d0457..2a4e9ccb225c 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -51,7 +51,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {ExpenseRuleForm, FlagForReviewRuleForm, MerchantRuleForm, MerchantTypeRuleForm, RequireFieldsRuleForm, SpendRuleForm} from '@src/types/form'; -import type {AppReview, BlockedFromConcierge, CustomStatusDraft, ExpenseRule, NewLogin, ReportAttributesDerivedValue} from '@src/types/onyx'; +import type {AppReview, BlockedFromConcierge, CustomStatusDraft, ExpenseRule, NewLogin, ReportActions, ReportAttributesDerivedValue} from '@src/types/onyx'; import type Login from '@src/types/onyx/Login'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {AnyOnyxServerUpdate, OnyxServerUpdate, OnyxUpdateEvent} from '@src/types/onyx/OnyxUpdatesFromServer'; @@ -670,6 +670,7 @@ function triggerNotifications( onyxUpdates: Array>, currentUserAccountID: number, currentUserEmail: string, + reportActionsCollection: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], ) { for (const update of onyxUpdates) { @@ -683,7 +684,7 @@ function triggerNotifications( for (const action of reportActions) { if (action) { // They aren't connected to a UI anywhere, it's OK to use currentUserEmail - showReportActionNotification(reportID, action, currentUserAccountID, currentUserEmail, reportAttributes); + showReportActionNotification(reportID, action, reportActionsCollection, currentUserAccountID, currentUserEmail, reportAttributes); } } } @@ -910,7 +911,12 @@ function initializePusherPingPong(currentUserAccountID: number) { * Handles the newest events from Pusher where a single mega multipleEvents contains * an array of singular events all in one event */ -function subscribeToUserEvents(currentUserAccountID: number, currentUserEmail: string, getReportAttributes?: () => ReportAttributesDerivedValue['reports'] | undefined) { +function subscribeToUserEvents( + currentUserAccountID: number, + currentUserEmail: string, + getReportActions: () => OnyxCollection, + getReportAttributes?: () => ReportAttributesDerivedValue['reports'] | undefined, +) { // If we don't have the user's accountID yet (because the app isn't fully setup yet) we can't subscribe so return early if (!currentUserAccountID) { return; @@ -965,7 +971,7 @@ function subscribeToUserEvents(currentUserAccountID: number, currentUserEmail: s } const onyxUpdatePromise = Onyx.update(pushJSON).then(() => { - triggerNotifications(pushJSON, currentUserAccountID, currentUserEmail, getReportAttributes?.()); + triggerNotifications(pushJSON, currentUserAccountID, currentUserEmail, getReportActions(), getReportAttributes?.()); }); // Return a promise when Onyx is done updating so that the OnyxUpdatesManager can properly apply all diff --git a/tests/actions/IOU/RequestMoneyTest.ts b/tests/actions/IOU/RequestMoneyTest.ts index 4357fcfb4176..638351ad0aa5 100644 --- a/tests/actions/IOU/RequestMoneyTest.ts +++ b/tests/actions/IOU/RequestMoneyTest.ts @@ -1841,7 +1841,7 @@ describe('actions/IOU', () => { // Given a test user is signed in with Onyx setup and some initial data await signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); - subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, () => {}, undefined); await waitForBatchedUpdates(); await setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID); diff --git a/tests/actions/IOUTest/DeleteMoneyRequestTest.ts b/tests/actions/IOUTest/DeleteMoneyRequestTest.ts index a23208082d0e..b381fed1e3ae 100644 --- a/tests/actions/IOUTest/DeleteMoneyRequestTest.ts +++ b/tests/actions/IOUTest/DeleteMoneyRequestTest.ts @@ -155,7 +155,7 @@ describe('actions/IOU/DeleteMoneyRequest', () => { // Given a test user is signed in with Onyx setup and some initial data await signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); - subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, () => {}, undefined); await waitForBatchedUpdates(); await setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID); diff --git a/tests/actions/IOUTest/TrackExpenseTest.ts b/tests/actions/IOUTest/TrackExpenseTest.ts index d8711f11652a..f12005519337 100644 --- a/tests/actions/IOUTest/TrackExpenseTest.ts +++ b/tests/actions/IOUTest/TrackExpenseTest.ts @@ -2210,7 +2210,7 @@ describe('actions/IOU/TrackExpense', () => { // Given a test user is signed in with Onyx setup and some initial data await signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); - subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, () => {}, undefined); await waitForBatchedUpdates(); await setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID); @@ -2514,7 +2514,7 @@ describe('actions/IOU/TrackExpense', () => { PusherHelper.setup(); await signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); - subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, () => {}, undefined); await waitForBatchedUpdates(); await setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID); diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 8cdd95d01f52..281f8507ce0f 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -249,7 +249,7 @@ describe('actions/Report', () => { // Set up Onyx with some test user data return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) .then(() => { - User.subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + User.subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, () => {}, undefined); return waitForBatchedUpdates(); }) .then(() => TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID)) @@ -551,7 +551,7 @@ describe('actions/Report', () => { .then(waitForNetworkPromises) .then(() => { // Given a test user that is subscribed to Pusher events - User.subscribeToUserEvents(USER_1_ACCOUNT_ID, USER_1_LOGIN, undefined); + User.subscribeToUserEvents(USER_1_ACCOUNT_ID, USER_1_LOGIN, () => {}, undefined); return waitForBatchedUpdates(); }) .then(() => TestHelper.setPersonalDetails(USER_1_LOGIN, USER_1_ACCOUNT_ID)) @@ -905,7 +905,7 @@ describe('actions/Report', () => { return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) .then(waitForBatchedUpdates) .then(() => { - User.subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + User.subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, () => {}, undefined); return waitForBatchedUpdates(); }) .then(() => { @@ -963,7 +963,7 @@ describe('actions/Report', () => { // Set up Onyx with some test user data return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) .then(() => { - User.subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + User.subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, () => {}, undefined); return waitForBatchedUpdates(); }) .then(() => TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID)) @@ -1101,7 +1101,7 @@ describe('actions/Report', () => { // Set up Onyx with some test user data return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) .then(() => { - User.subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + User.subscribeToUserEvents(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, () => {}, undefined); return waitForBatchedUpdates(); }) .then(() => TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID)) diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index c4bef2405995..1723b0aaec1b 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -187,7 +187,7 @@ function signInAndGetApp(reportName = '', participantAccountIDs?: number[]): Pro }) .then(async () => TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A')) .then(() => { - subscribeToUserEvents(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined); + subscribeToUserEvents(USER_A_ACCOUNT_ID, USER_A_EMAIL, () => {}, undefined); return waitForBatchedUpdates(); }) .then(async () => { diff --git a/tests/ui/PaginationTest.tsx b/tests/ui/PaginationTest.tsx index c364ec8f4b94..56db94419e75 100644 --- a/tests/ui/PaginationTest.tsx +++ b/tests/ui/PaginationTest.tsx @@ -226,7 +226,7 @@ async function signInAndGetApp(): Promise { await waitForBatchedUpdatesWithAct(); // Start listening for pusher events after navigation settles. - subscribeToUserEvents(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined); + subscribeToUserEvents(USER_A_ACCOUNT_ID, USER_A_EMAIL, () => {}, undefined); await waitForBatchedUpdates(); await Promise.all([ diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 0940407c5846..6b03d0b6a58e 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -183,7 +183,7 @@ async function signInAndGetAppWithUnreadChat(): Promise { renderAppOnce(); await waitForBatchedUpdatesWithAct(); - subscribeToUserEvents(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined); + subscribeToUserEvents(USER_A_ACCOUNT_ID, USER_A_EMAIL, () => {}, undefined); await waitForBatchedUpdates(); diff --git a/tests/unit/showReportActionNotificationTest.ts b/tests/unit/showReportActionNotificationTest.ts index 5366e1f287fa..34ef755b353b 100644 --- a/tests/unit/showReportActionNotificationTest.ts +++ b/tests/unit/showReportActionNotificationTest.ts @@ -88,9 +88,10 @@ describe('showReportActionNotification', () => { Report.showReportActionNotification( REPORT_ID, reportAction as Parameters[1], + undefined, CURRENT_USER_ACCOUNT_ID, CURRENT_USER_LOGIN, - REPORT_ATTRIBUTES as Parameters[4], + REPORT_ATTRIBUTES as Parameters[5], ); await waitForBatchedUpdates(); @@ -112,7 +113,14 @@ describe('showReportActionNotification', () => { person: [{type: 'TEXT', style: 'strong', text: 'Other User'}], }; - Report.showReportActionNotification(REPORT_ID, reportAction as Parameters[1], CURRENT_USER_ACCOUNT_ID, CURRENT_USER_LOGIN, undefined); + Report.showReportActionNotification( + REPORT_ID, + reportAction as Parameters[1], + undefined, + CURRENT_USER_ACCOUNT_ID, + CURRENT_USER_LOGIN, + undefined, + ); await waitForBatchedUpdates(); expect(mockShowModifiedExpenseNotification).toHaveBeenCalledTimes(1); @@ -136,9 +144,10 @@ describe('showReportActionNotification', () => { Report.showReportActionNotification( REPORT_ID, reportAction as Parameters[1], + undefined, CURRENT_USER_ACCOUNT_ID, CURRENT_USER_LOGIN, - REPORT_ATTRIBUTES as Parameters[4], + REPORT_ATTRIBUTES as Parameters[5], ); await waitForBatchedUpdates(); From 0e9cb8fc2e26e7ee4ebb76af44c9addaf1af77e6 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 4 Jul 2026 16:03:08 +0800 Subject: [PATCH 02/10] fix key --- src/libs/actions/Report/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index c629fd6d28a6..7af496154ddd 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -4597,7 +4597,7 @@ function shouldShowReportActionNotification( // If the report is a transaction thread and we are currently viewing the associated one-transaction report do no show a notification. const topmostReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${topmostReportID}`]; - const topmostReportActions = reportActions?.[`${topmostReport?.reportID}`]; + const topmostReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${topmostReport?.reportID}`]; const chatTopmostReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${topmostReport?.chatReportID}`]; if ( reportID === ReportActionsUtils.getOneTransactionThreadReportID(topmostReport, chatTopmostReport, topmostReportActions, isOfflineNetwork()) && From 3f1c61a000d639dabcd9848a31cdd207085b980e Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sun, 5 Jul 2026 15:17:18 +0800 Subject: [PATCH 03/10] fix test --- tests/actions/ReportTest.ts | 2 +- tests/ui/AuthScreensInitHandlerTest.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index d629b6ed842d..def7411b4ae1 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -933,7 +933,7 @@ describe('actions/Report', () => { }) .then(() => { // Ensure we show a notification for this new report action - expect(Report.showReportActionNotification).toBeCalledWith(REPORT_ID, REPORT_ACTION, TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); + expect(Report.showReportActionNotification).toBeCalledWith(REPORT_ID, REPORT_ACTION, undefined, TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, undefined); }); }); diff --git a/tests/ui/AuthScreensInitHandlerTest.tsx b/tests/ui/AuthScreensInitHandlerTest.tsx index 7912d19ed010..b10286ee0894 100644 --- a/tests/ui/AuthScreensInitHandlerTest.tsx +++ b/tests/ui/AuthScreensInitHandlerTest.tsx @@ -156,7 +156,7 @@ describe('AuthScreensInitHandler', () => { await waitForBatchedUpdatesWithAct(); expect(mockedPusherInit).toHaveBeenCalled(); - expect(subscribeToUserEvents).toHaveBeenCalledWith(TEST_ACCOUNT_ID, 'test@test.com', expect.any(Function)); + expect(subscribeToUserEvents).toHaveBeenCalledWith(TEST_ACCOUNT_ID, 'test@test.com', expect.any(Function), expect.any(Function)); }); it('calls subscribeToUserEvents from sign-in modal effect when SIGN_IN_MODAL is active', async () => { @@ -170,7 +170,7 @@ describe('AuthScreensInitHandler', () => { // Both mount effect AND sign-in modal effect fire → 2 calls expect(subscribeToUserEvents).toHaveBeenCalledTimes(2); - expect(subscribeToUserEvents).toHaveBeenCalledWith(TEST_ACCOUNT_ID, 'test@test.com', expect.any(Function)); + expect(subscribeToUserEvents).toHaveBeenCalledWith(TEST_ACCOUNT_ID, 'test@test.com', expect.any(Function), expect.any(Function)); }); it('getter passed to subscribeToUserEvents returns report attributes when available', async () => { @@ -185,7 +185,7 @@ describe('AuthScreensInitHandler', () => { const mockCalls = (subscribeToUserEvents as jest.Mock).mock.calls; const firstCallArgs = mockCalls.at(0) as unknown[]; - const getter = firstCallArgs.at(2) as () => unknown; + const getter = firstCallArgs.at(3) as () => unknown; expect(getter()).toEqual(mockReports); }); @@ -199,7 +199,7 @@ describe('AuthScreensInitHandler', () => { const mockCalls = (subscribeToUserEvents as jest.Mock).mock.calls; const firstCallArgs = mockCalls.at(0) as unknown[]; - const getter = firstCallArgs.at(2) as () => unknown; + const getter = firstCallArgs.at(3) as () => unknown; expect(getter()).toBeUndefined(); }); From 2da16265395a81a989ffa6ecd8a1e60c8291ad68 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 6 Jul 2026 10:05:24 +0800 Subject: [PATCH 04/10] simplify by passing the one transaction thread report only --- src/hooks/useOneTransactionThreadReportID.tsx | 19 ++++++++++++ .../AppNavigator/AuthScreensInitHandler.tsx | 29 ++++++++++--------- src/libs/actions/Report/index.ts | 16 ++++------ src/libs/actions/User.ts | 8 ++--- 4 files changed, 43 insertions(+), 29 deletions(-) create mode 100644 src/hooks/useOneTransactionThreadReportID.tsx diff --git a/src/hooks/useOneTransactionThreadReportID.tsx b/src/hooks/useOneTransactionThreadReportID.tsx new file mode 100644 index 000000000000..8feb8c35d245 --- /dev/null +++ b/src/hooks/useOneTransactionThreadReportID.tsx @@ -0,0 +1,19 @@ +import {getOneTransactionThreadReportID} from '@libs/ReportActionsUtils'; + +import ONYXKEYS from '@src/ONYXKEYS'; + +import useNetwork from './useNetwork'; +import useOnyx from './useOnyx'; + +function useOneTransactionThreadReportID(reportID: string | undefined) { + const {isOffline} = useNetwork(); + 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), + }); + + return oneTransactionThreadReportID; +} + +export default useOneTransactionThreadReportID; diff --git a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx index 55cc5544afa4..336656f31ed2 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx @@ -5,9 +5,11 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useHasActiveAdminPolicies from '@hooks/useHasActiveAdminPolicies'; import useLastWorkspaceNumber from '@hooks/useLastWorkspaceNumber'; import useLocalize from '@hooks/useLocalize'; +import useOneTransactionThreadReportID from '@hooks/useOneTransactionThreadReportID'; import useOnyx from '@hooks/useOnyx'; import useReconcileHighContrastIntent from '@hooks/useReconcileHighContrastIntent'; import useReportAttributes from '@hooks/useReportAttributes'; +import useRootNavigationState from '@hooks/useRootNavigationState'; import {init, isClientTheLeader} from '@libs/ActiveClientManager'; import Log from '@libs/Log'; @@ -33,9 +35,7 @@ import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {ReportActions, ReportAttributesDerivedValue} from '@src/types/onyx'; - -import type {OnyxCollection} from 'react-native-onyx'; +import type {ReportAttributesDerivedValue} from '@src/types/onyx'; import {hasSeenTourSelector} from '@selectors/Onboarding'; import {useEffect, useRef} from 'react'; @@ -43,7 +43,7 @@ import {useEffect, useRef} from 'react'; function initializePusher( currentUserAccountID: number | undefined, currentUserEmail: string | undefined, - getReportActions: () => OnyxCollection, + getTopmostOneTransactionThreadReportID: () => string | undefined, getReportAttributes: () => ReportAttributesDerivedValue['reports'] | undefined, ) { return Pusher.init({ @@ -51,7 +51,7 @@ function initializePusher( cluster: CONFIG.PUSHER.CLUSTER, authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`, }).then(() => { - User.subscribeToUserEvents(currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, currentUserEmail ?? '', getReportActions, getReportAttributes); + User.subscribeToUserEvents(currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, currentUserEmail ?? '', getTopmostOneTransactionThreadReportID, getReportAttributes); }); } @@ -87,15 +87,16 @@ function AuthScreensInitHandler() { const reportAttributesRef = useRef(reportAttributes); reportAttributesRef.current = reportAttributes; - const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); - // We use a ref so the Pusher callback (registered once on mount) always reads the latest value without re-subscribing. - const reportActionsRef = useRef(reportActions); - useReconcileHighContrastIntent(); + 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(() => { - reportActionsRef.current = reportActions; - }, [reportActions]); + topmostOneTransactionThreadReportIDRef.current = topmostOneTransactionThreadReportID; + }, [topmostOneTransactionThreadReportID]); useEffect(() => { registerPusherReinitializeHandler(({accountID, email}: PusherReinitializeHandlerParams = {}) => { @@ -109,7 +110,7 @@ function AuthScreensInitHandler() { return initializePusher( currentAccountID, currentEmail, - () => reportActionsRef.current, + () => topmostOneTransactionThreadReportIDRef.current, () => reportAttributesRef.current, ); }); @@ -124,7 +125,7 @@ function AuthScreensInitHandler() { return; } // This means sign in in RHP was successful, so we can subscribe to user events - initializePusher(session?.accountID, session?.email, () => reportActionsRef.current, () => reportAttributesRef.current); + initializePusher(session?.accountID, session?.email, () => topmostOneTransactionThreadReportIDRef.current, () => reportAttributesRef.current); }, [session?.accountID, session?.email]); useEffect(() => { @@ -147,7 +148,7 @@ function AuthScreensInitHandler() { }); PusherConnectionManager.init(); - initializePusher(session?.accountID, session?.email, () => reportActionsRef.current, () => reportAttributesRef.current).finally(() => { + initializePusher(session?.accountID, session?.email, () => topmostOneTransactionThreadReportIDRef.current, () => reportAttributesRef.current).finally(() => { endSpan(CONST.TELEMETRY.SPAN_NAVIGATION.PUSHER_INIT); }); diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 1159a4a6437e..0a66a2858c85 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -4568,7 +4568,7 @@ function setIsComposerFullSize(reportID: string, isComposerFullSize: boolean) { */ function shouldShowReportActionNotification( reportID: string, - reportActions: OnyxCollection, + topmostOneTransactionThreadReportID: string | undefined, currentUserAccountID: number, action: ReportAction | null = null, isRemote = false, @@ -4608,14 +4608,8 @@ function shouldShowReportActionNotification( } // If the report is a transaction thread and we are currently viewing the associated one-transaction report do no show a notification. - const topmostReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${topmostReportID}`]; - const topmostReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${topmostReport?.reportID}`]; - const chatTopmostReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${topmostReport?.chatReportID}`]; - if ( - reportID === ReportActionsUtils.getOneTransactionThreadReportID(topmostReport, chatTopmostReport, topmostReportActions, isOfflineNetwork()) && - Visibility.isVisible() && - Visibility.hasFocus() - ) { + console.log('should show notification', reportID, topmostOneTransactionThreadReportID, Visibility.isVisible(), Visibility.hasFocus()); + 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; } @@ -4644,12 +4638,12 @@ function shouldShowReportActionNotification( function showReportActionNotification( reportID: string, reportAction: ReportAction, - reportActions: OnyxCollection, + topmostOneTransactionThreadReportID: string | undefined, currentUserAccountID: number, currentUserLogin: string, reportAttributes?: ReportAttributesDerivedValue['reports'], ) { - if (!shouldShowReportActionNotification(reportID, reportActions, currentUserAccountID, reportAction)) { + if (!shouldShowReportActionNotification(reportID, topmostOneTransactionThreadReportID, currentUserAccountID, reportAction)) { return; } diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index e390ed8b440e..e1e1fc4cb14d 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -675,7 +675,7 @@ function triggerNotifications( onyxUpdates: Array>, currentUserAccountID: number, currentUserEmail: string, - reportActionsCollection: OnyxCollection, + topmostOneTransactionThreadReportID: string | undefined, reportAttributes?: ReportAttributesDerivedValue['reports'], ) { for (const update of onyxUpdates) { @@ -689,7 +689,7 @@ function triggerNotifications( for (const action of reportActions) { if (action) { // They aren't connected to a UI anywhere, it's OK to use currentUserEmail - showReportActionNotification(reportID, action, reportActionsCollection, currentUserAccountID, currentUserEmail, reportAttributes); + showReportActionNotification(reportID, action, topmostOneTransactionThreadReportID, currentUserAccountID, currentUserEmail, reportAttributes); } } } @@ -919,7 +919,7 @@ function initializePusherPingPong(currentUserAccountID: number) { function subscribeToUserEvents( currentUserAccountID: number, currentUserEmail: string, - getReportActions: () => OnyxCollection, + getTopmostOneTransactionThreadReportID: () => string | undefined, getReportAttributes?: () => ReportAttributesDerivedValue['reports'] | undefined, ) { // If we don't have the user's accountID yet (because the app isn't fully setup yet) we can't subscribe so return early @@ -976,7 +976,7 @@ function subscribeToUserEvents( } const onyxUpdatePromise = Onyx.update(pushJSON).then(() => { - triggerNotifications(pushJSON, currentUserAccountID, currentUserEmail, getReportActions(), getReportAttributes?.()); + triggerNotifications(pushJSON, currentUserAccountID, currentUserEmail, getTopmostOneTransactionThreadReportID(), getReportAttributes?.()); }); // Return a promise when Onyx is done updating so that the OnyxUpdatesManager can properly apply all From e44f344adca8144dbb82b61743638923a9d68ccb Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 6 Jul 2026 10:06:35 +0800 Subject: [PATCH 05/10] add comment --- .../Notification/PushNotification/shouldShowPushNotification.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts index 9b9a2be33d4b..469494d8b237 100644 --- a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts +++ b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts @@ -23,6 +23,7 @@ Onyx.connectWithoutView({ }, }); +// We do not depend on updates on the UI for notifications, so we can use `connectWithoutView` here. let allReportActions: OnyxCollection; Onyx.connectWithoutView({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, From b43d9206e56d81740862d7a5ffecf1607262acf0 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 6 Jul 2026 12:20:48 +0800 Subject: [PATCH 06/10] fix test --- tests/ui/AuthScreensInitHandlerTest.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ui/AuthScreensInitHandlerTest.tsx b/tests/ui/AuthScreensInitHandlerTest.tsx index b10286ee0894..980956740797 100644 --- a/tests/ui/AuthScreensInitHandlerTest.tsx +++ b/tests/ui/AuthScreensInitHandlerTest.tsx @@ -49,6 +49,7 @@ jest.mock('@libs/Navigation/Navigation', () => ({ isActiveRoute: jest.fn(() => false), navigate: jest.fn(), getActiveRouteWithoutParams: jest.fn(() => ''), + getTopmostReportId: jest.fn(() => undefined), isNavigationReady: jest.fn(() => Promise.resolve()), setNavigationActionToMicrotaskQueue: jest.fn(() => Promise.resolve()), }, From afe9bd1745117fd6254de1266f4b8cc6d67766a4 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 6 Jul 2026 13:07:23 +0800 Subject: [PATCH 07/10] add test --- .../useOneTransactionThreadReportIDTest.ts | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 tests/unit/useOneTransactionThreadReportIDTest.ts diff --git a/tests/unit/useOneTransactionThreadReportIDTest.ts b/tests/unit/useOneTransactionThreadReportIDTest.ts new file mode 100644 index 000000000000..fc16fecdc12e --- /dev/null +++ b/tests/unit/useOneTransactionThreadReportIDTest.ts @@ -0,0 +1,129 @@ +import {act, renderHook} from '@testing-library/react-native'; + +import OnyxListItemProvider from '@components/OnyxListItemProvider'; + +import useOneTransactionThreadReportID from '@hooks/useOneTransactionThreadReportID'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report, ReportAction} from '@src/types/onyx'; + +import Onyx from 'react-native-onyx'; + +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; + +const REPORT_ID = 'report1'; +const CHAT_REPORT_ID = 'chat1'; + +const report: Report = { + reportID: REPORT_ID, + chatReportID: CHAT_REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, +}; + +const chatReport: Report = { + reportID: CHAT_REPORT_ID, + type: CONST.REPORT.TYPE.CHAT, +}; + +function createIOUAction(reportActionID: string, transactionID: string, childReportID?: string): ReportAction { + return { + reportActionID, + childReportID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + created: '2025-02-14 08:12:05.165', + originalMessage: { + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + amount: 10402, + currency: CONST.CURRENCY.USD, + IOUTransactionID: transactionID, + }, + message: [ + { + type: CONST.REPORT.MESSAGE.TYPE.COMMENT, + html: '$104.02 expense', + text: '$104.02 expense', + }, + ], + }; +} + +describe('useOneTransactionThreadReportID', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${CHAT_REPORT_ID}`, chatReport); + }); + + afterEach(() => { + return Onyx.clear(); + }); + + it('returns undefined when reportID is undefined', async () => { + const {result} = renderHook(() => useOneTransactionThreadReportID(undefined), {wrapper: OnyxListItemProvider}); + await waitForBatchedUpdatesWithAct(); + expect(result.current).toBeUndefined(); + }); + + it('returns the childReportID when the report has exactly one IOU transaction action', async () => { + const action = createIOUAction('action1', 'transaction1', 'thread1'); + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {[action.reportActionID]: action}); + await waitForBatchedUpdatesWithAct(); + }); + + const {result} = renderHook(() => useOneTransactionThreadReportID(REPORT_ID), {wrapper: OnyxListItemProvider}); + await waitForBatchedUpdatesWithAct(); + expect(result.current).toBe('thread1'); + }); + + it('returns CONST.FAKE_REPORT_ID when the single IOU action has no childReportID', async () => { + const action = createIOUAction('action1', 'transaction1'); + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {[action.reportActionID]: action}); + await waitForBatchedUpdatesWithAct(); + }); + + const {result} = renderHook(() => useOneTransactionThreadReportID(REPORT_ID), {wrapper: OnyxListItemProvider}); + await waitForBatchedUpdatesWithAct(); + expect(result.current).toBe(CONST.FAKE_REPORT_ID); + }); + + it('returns undefined when the report has more than one IOU transaction action', async () => { + const action1 = createIOUAction('action1', 'transaction1', 'thread1'); + const action2 = createIOUAction('action2', 'transaction2', 'thread2'); + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + [action1.reportActionID]: action1, + [action2.reportActionID]: action2, + }); + await waitForBatchedUpdatesWithAct(); + }); + + const {result} = renderHook(() => useOneTransactionThreadReportID(REPORT_ID), {wrapper: OnyxListItemProvider}); + await waitForBatchedUpdatesWithAct(); + expect(result.current).toBeUndefined(); + }); + + it('returns undefined when there are no report actions', async () => { + const {result} = renderHook(() => useOneTransactionThreadReportID(REPORT_ID), {wrapper: OnyxListItemProvider}); + await waitForBatchedUpdatesWithAct(); + expect(result.current).toBeUndefined(); + }); + + it('returns undefined when the report is not an IOU, Expense, or Invoice report', async () => { + const action = createIOUAction('action1', 'transaction1', 'thread1'); + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, {...report, type: CONST.REPORT.TYPE.CHAT}); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {[action.reportActionID]: action}); + await waitForBatchedUpdatesWithAct(); + }); + + const {result} = renderHook(() => useOneTransactionThreadReportID(REPORT_ID), {wrapper: OnyxListItemProvider}); + await waitForBatchedUpdatesWithAct(); + expect(result.current).toBeUndefined(); + }); +}); From 59ef2814e1bf1180a922de5b0fa524a3437d5219 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 6 Jul 2026 13:07:47 +0800 Subject: [PATCH 08/10] pass the topmost one-transaction thread report id --- .../shouldShowPushNotification.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts index 469494d8b237..df6e0e34faeb 100644 --- a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts +++ b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts @@ -1,11 +1,13 @@ import Log from '@libs/Log'; +import Navigation from '@libs/Navigation/Navigation'; +import {getIsOffline} from '@libs/NetworkState'; import * as ReportActionUtils from '@libs/ReportActionsUtils'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ReportActions} from '@src/types/onyx'; +import type {Report as ReportType, ReportActions} from '@src/types/onyx'; import type {PushPayload} from '@ua/react-native-airship'; import type {OnyxCollection} from 'react-native-onyx'; @@ -31,6 +33,14 @@ Onyx.connectWithoutView({ callback: (value) => (allReportActions = value), }); +// We do not depend on updates on the UI for notifications, so we can use `connectWithoutView` here. +let allReports: OnyxCollection; +Onyx.connectWithoutView({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (value) => (allReports = value), +}); + /** * Returns whether the given Airship notification should be shown depending on the current state of the app */ @@ -47,7 +57,12 @@ export default function shouldShowPushNotification(pushPayload: PushPayload): bo shouldShow = true; } else { const reportAction = ReportActionUtils.getLatestReportActionFromOnyxData(data.onyxData ?? null); - shouldShow = Report.shouldShowReportActionNotification(String(data.reportID), allReportActions, currentUserAccountID, reportAction, true); + 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); } Log.info(`[PushNotification] ${shouldShow ? 'Showing' : 'Not showing'} notification`); From f7580141217724a9a5d3e61b0b2a9c4488d2ef72 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 6 Jul 2026 13:08:03 +0800 Subject: [PATCH 09/10] lint --- src/libs/actions/Report/index.ts | 1 - src/libs/actions/User.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 0a66a2858c85..0ab4f69c4339 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -4608,7 +4608,6 @@ function shouldShowReportActionNotification( } // If the report is a transaction thread and we are currently viewing the associated one-transaction report do no show a notification. - console.log('should show notification', reportID, topmostOneTransactionThreadReportID, Visibility.isVisible(), Visibility.hasFocus()); 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; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index e1e1fc4cb14d..8174efb6338f 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -46,7 +46,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {ExpenseRuleForm, FlagForReviewRuleForm, MerchantRuleForm, MerchantTypeRuleForm, RequireFieldsRuleForm, SpendRuleForm} from '@src/types/form'; -import type {AppReview, BlockedFromConcierge, CustomStatusDraft, ExpenseRule, NewLogin, ReportActions, ReportAttributesDerivedValue} from '@src/types/onyx'; +import type {AppReview, BlockedFromConcierge, CustomStatusDraft, ExpenseRule, NewLogin, ReportAttributesDerivedValue} from '@src/types/onyx'; import type Login from '@src/types/onyx/Login'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {AnyOnyxServerUpdate, OnyxServerUpdate, OnyxUpdateEvent} from '@src/types/onyx/OnyxUpdatesFromServer'; From 1f4ce408c8f4227447cfee4920c2df5b306e11e8 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 7 Jul 2026 23:53:02 +0800 Subject: [PATCH 10/10] update comment --- .../PushNotification/shouldShowPushNotification.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts index df6e0e34faeb..34eb3d5d6bbc 100644 --- a/src/libs/Notification/PushNotification/shouldShowPushNotification.ts +++ b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts @@ -16,7 +16,8 @@ import Onyx from 'react-native-onyx'; import parsePushNotificationPayload from './parsePushNotificationPayload'; -// We do not depend on updates on the UI for notifications, so we can use `connectWithoutView` here. +// Push notifications aren't rendered using React, so it's impossible to access Onyx data with useOnyx(), therefore, it's OK to use connectWithoutView() here. + let currentUserAccountID = -1; Onyx.connectWithoutView({ key: ONYXKEYS.SESSION, @@ -25,7 +26,6 @@ Onyx.connectWithoutView({ }, }); -// We do not depend on updates on the UI for notifications, so we can use `connectWithoutView` here. let allReportActions: OnyxCollection; Onyx.connectWithoutView({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, @@ -33,7 +33,6 @@ Onyx.connectWithoutView({ callback: (value) => (allReportActions = value), }); -// We do not depend on updates on the UI for notifications, so we can use `connectWithoutView` here. let allReports: OnyxCollection; Onyx.connectWithoutView({ key: ONYXKEYS.COLLECTION.REPORT,