Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/hooks/useOneTransactionThreadReportID.tsx
Original file line number Diff line number Diff line change
@@ -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;
31 changes: 26 additions & 5 deletions src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -38,13 +40,18 @@ import type {ReportAttributesDerivedValue} from '@src/types/onyx';
import {hasSeenTourSelector} from '@selectors/Onboarding';
import {useEffect, useRef} from 'react';

function initializePusher(currentUserAccountID?: number, currentUserEmail?: string, getReportAttributes?: () => ReportAttributesDerivedValue['reports'] | undefined) {
function initializePusher(
currentUserAccountID: number | undefined,
currentUserEmail: string | undefined,
getTopmostOneTransactionThreadReportID: () => string | undefined,
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 ?? '', getTopmostOneTransactionThreadReportID, getReportAttributes);
});
}

Expand Down Expand Up @@ -82,6 +89,15 @@ function AuthScreensInitHandler() {

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(() => {
Comment thread
bernhardoj marked this conversation as resolved.
topmostOneTransactionThreadReportIDRef.current = topmostOneTransactionThreadReportID;
}, [topmostOneTransactionThreadReportID]);

useEffect(() => {
registerPusherReinitializeHandler(({accountID, email}: PusherReinitializeHandlerParams = {}) => {
const currentAccountID = accountID ?? session?.accountID;
Expand All @@ -91,7 +107,12 @@ function AuthScreensInitHandler() {
return Promise.resolve();
}

return initializePusher(currentAccountID, currentEmail, () => reportAttributesRef.current);
return initializePusher(
currentAccountID,
currentEmail,
() => topmostOneTransactionThreadReportIDRef.current,
() => reportAttributesRef.current,
);
});

return () => {
Expand All @@ -104,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, () => reportAttributesRef.current);
initializePusher(session?.accountID, session?.email, () => topmostOneTransactionThreadReportIDRef.current, () => reportAttributesRef.current);
}, [session?.accountID, session?.email]);

useEffect(() => {
Expand All @@ -127,7 +148,7 @@ function AuthScreensInitHandler() {
});
PusherConnectionManager.init();

initializePusher(session?.accountID, session?.email, () => reportAttributesRef.current).finally(() => {
initializePusher(session?.accountID, session?.email, () => topmostOneTransactionThreadReportIDRef.current, () => reportAttributesRef.current).finally(() => {
endSpan(CONST.TELEMETRY.SPAN_NAVIGATION.PUSHER_INIT);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
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 {Report as ReportType, ReportActions} from '@src/types/onyx';

import type {PushPayload} from '@ua/react-native-airship';
import type {OnyxCollection} from 'react-native-onyx';

import Onyx from 'react-native-onyx';

Expand All @@ -21,6 +25,22 @@ Onyx.connectWithoutView({
},
});

// We do not depend on updates on the UI for notifications, so we can use `connectWithoutView` here.
let allReportActions: OnyxCollection<ReportActions>;
Onyx.connectWithoutView({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment to indicate why it's OK to use this instead of using useOnyx()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reuse the comment from the above one

// We do not depend on updates on the UI for notifications, so we can use connectWithoutView here.

key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
waitForCollectionCallback: true,
callback: (value) => (allReportActions = value),
});

// We do not depend on updates on the UI for notifications, so we can use `connectWithoutView` here.
let allReports: OnyxCollection<ReportType>;
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
*/
Expand All @@ -37,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), 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`);
Expand Down
20 changes: 10 additions & 10 deletions src/libs/actions/Report/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4566,7 +4566,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,
topmostOneTransactionThreadReportID: string | undefined,
currentUserAccountID: number,
action: ReportAction | null = null,
isRemote = false,
): boolean {
const tag = isRemote ? '[PushNotification]' : '[LocalNotification]';
const topmostReportID = Navigation.getTopmostReportId();

Expand Down Expand Up @@ -4602,14 +4608,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 chatTopmostReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${topmostReport?.chatReportID}`];
if (
reportID === ReportActionsUtils.getOneTransactionThreadReportID(topmostReport, chatTopmostReport, topmostReportActions, isOfflineNetwork()) &&
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;
}
Expand Down Expand Up @@ -4638,11 +4637,12 @@ function shouldShowReportActionNotification(reportID: string, currentUserAccount
function showReportActionNotification(
reportID: string,
reportAction: ReportAction,
topmostOneTransactionThreadReportID: string | undefined,
currentUserAccountID: number,
currentUserLogin: string,
reportAttributes?: ReportAttributesDerivedValue['reports'],
) {
if (!shouldShowReportActionNotification(reportID, currentUserAccountID, reportAction)) {
if (!shouldShowReportActionNotification(reportID, topmostOneTransactionThreadReportID, currentUserAccountID, reportAction)) {
return;
}

Expand Down
12 changes: 9 additions & 3 deletions src/libs/actions/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,7 @@ function triggerNotifications<TKey extends OnyxKey>(
onyxUpdates: Array<OnyxServerUpdate<TKey>>,
currentUserAccountID: number,
currentUserEmail: string,
topmostOneTransactionThreadReportID: string | undefined,
reportAttributes?: ReportAttributesDerivedValue['reports'],
) {
for (const update of onyxUpdates) {
Expand All @@ -688,7 +689,7 @@ function triggerNotifications<TKey extends OnyxKey>(
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, topmostOneTransactionThreadReportID, currentUserAccountID, currentUserEmail, reportAttributes);
}
}
}
Expand Down Expand Up @@ -915,7 +916,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,
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
if (!currentUserAccountID) {
return;
Expand Down Expand Up @@ -970,7 +976,7 @@ function subscribeToUserEvents(currentUserAccountID: number, currentUserEmail: s
}

const onyxUpdatePromise = Onyx.update(pushJSON).then(() => {
triggerNotifications(pushJSON, currentUserAccountID, currentUserEmail, getReportAttributes?.());
triggerNotifications(pushJSON, currentUserAccountID, currentUserEmail, getTopmostOneTransactionThreadReportID(), getReportAttributes?.());
});

// Return a promise when Onyx is done updating so that the OnyxUpdatesManager can properly apply all
Expand Down
2 changes: 1 addition & 1 deletion tests/actions/IOU/RequestMoneyTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1847,7 +1847,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);

Expand Down
2 changes: 1 addition & 1 deletion tests/actions/IOUTest/DeleteMoneyRequestTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,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);

Expand Down
4 changes: 2 additions & 2 deletions tests/actions/IOUTest/TrackExpenseTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2216,7 +2216,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);

Expand Down Expand Up @@ -2520,7 +2520,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);

Expand Down
12 changes: 6 additions & 6 deletions tests/actions/ReportTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,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))
Expand Down Expand Up @@ -560,7 +560,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))
Expand Down Expand Up @@ -914,7 +914,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(() => {
Expand All @@ -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);
});
});

Expand Down Expand Up @@ -972,7 +972,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))
Expand Down Expand Up @@ -1110,7 +1110,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))
Expand Down
9 changes: 5 additions & 4 deletions tests/ui/AuthScreensInitHandlerTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
},
Expand Down Expand Up @@ -156,7 +157,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 () => {
Expand All @@ -170,7 +171,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 () => {
Expand All @@ -185,7 +186,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);
});

Expand All @@ -199,7 +200,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();
});

Expand Down
Loading
Loading