From 9c3a6fe7d657ee3b66dc45bd23390c74d6b3fea8 Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Wed, 15 Jan 2025 14:21:18 -0700
Subject: [PATCH 01/22] Expose a method for getting the user channel name

---
 src/libs/PusherUtils.ts | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/libs/PusherUtils.ts b/src/libs/PusherUtils.ts
index 8a83bb012b3a..ef8d57e8bec4 100644
--- a/src/libs/PusherUtils.ts
+++ b/src/libs/PusherUtils.ts
@@ -11,6 +11,10 @@ type Callback = (data: OnyxUpdate[]) => Promise<void>;
 // Keeps track of all the callbacks that need triggered for each event type
 const multiEventCallbackMapping: Record<string, Callback> = {};
 
+function getUserChannelName(accountID: string) {
+    return `${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}${accountID}${CONFIG.PUSHER.SUFFIX}` as const;
+}
+
 function subscribeToMultiEvent(eventType: string, callback: Callback) {
     multiEventCallbackMapping[eventType] = callback;
 }
@@ -27,7 +31,7 @@ function triggerMultiEventHandler(eventType: string, data: OnyxUpdate[]): Promis
  * Abstraction around subscribing to private user channel events. Handles all logs and errors automatically.
  */
 function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string, onEvent: (pushJSON: OnyxUpdatesFromServer) => void) {
-    const pusherChannelName = `${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}${accountID}${CONFIG.PUSHER.SUFFIX}` as const;
+    const pusherChannelName = getUserChannelName(accountID);
 
     function logPusherEvent(pushJSON: OnyxUpdatesFromServer) {
         Log.info(`[Report] Handled ${eventName} event sent by Pusher`, false, pushJSON);
@@ -49,6 +53,7 @@ function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string
 }
 
 export default {
+    getUserChannelName,
     subscribeToPrivateUserChannelEvent,
     subscribeToMultiEvent,
     triggerMultiEventHandler,

From b9b946822e5964eb586aa2aa2a58c0c4988c572a Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Wed, 15 Jan 2025 14:22:07 -0700
Subject: [PATCH 02/22] Add the ping pong events

---
 src/libs/Pusher/EventType.ts | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/libs/Pusher/EventType.ts b/src/libs/Pusher/EventType.ts
index f1f0592aeac6..4b6938d7e9bc 100644
--- a/src/libs/Pusher/EventType.ts
+++ b/src/libs/Pusher/EventType.ts
@@ -8,6 +8,12 @@ export default {
     USER_IS_LEAVING_ROOM: 'client-userIsLeavingRoom',
     USER_IS_TYPING: 'client-userIsTyping',
     MULTIPLE_EVENTS: 'multipleEvents',
+
+    // An event that the client sends to the server and then listens for a returned "pong" event
+    PING: 'ping',
+
+    // An event that the server sends back to the client in response to a "ping" event
+    PONG: 'pong',
     MULTIPLE_EVENT_TYPE: {
         ONYX_API_UPDATE: 'onyxApiUpdate',
         RECONNECT_APP: 'reconnectApp',

From f5f21fa12f3ebcce3b3e143526d4d407d732efa9 Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Wed, 15 Jan 2025 14:22:19 -0700
Subject: [PATCH 03/22] Add a basic structure for the pingpong

---
 src/libs/actions/User.ts | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 80d04a4617bd..14a276b47e51 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -888,6 +888,29 @@ function playSoundForMessageType(pushJSON: OnyxServerUpdate[]) {
     });
 }
 
+function subscribeToPusherPong() {
+    // If there is no user accountID yet (because the app isn't fully setup yet), the channel can't be subscribed to so return early
+    if (currentUserAccountID === -1) {
+        return;
+    }
+
+    PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.PONG, currentUserAccountID.toString(), (pushJSON) => {});
+}
+
+let pingPongStarted = false;
+function initializePusherPingPong() {
+    // Ignore any additional calls to initialize the ping pong if it's already been started
+    if (pingPongStarted) {
+        return;
+    }
+
+    pingPongStarted = true;
+}
+
+function pingPusher() {
+    Pusher.sendEvent(privateReportChannelName, Pusher.TYPE.USER_IS_TYPING, typingStatus);
+}
+
 /**
  * Handles the newest events from Pusher where a single mega multipleEvents contains
  * an array of singular events all in one event

From 15b6ede755fba0c308a18f8da6869244258bc787 Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Wed, 15 Jan 2025 15:40:31 -0700
Subject: [PATCH 04/22] Record event data in an event map

---
 src/libs/Pusher/pusher.ts |  9 +++++++-
 src/libs/PusherUtils.ts   |  6 +++---
 src/libs/actions/User.ts  | 43 ++++++++++++++++++++++++++++++++++-----
 3 files changed, 49 insertions(+), 9 deletions(-)

diff --git a/src/libs/Pusher/pusher.ts b/src/libs/Pusher/pusher.ts
index 1dd1628e53d2..71f30e046ead 100644
--- a/src/libs/Pusher/pusher.ts
+++ b/src/libs/Pusher/pusher.ts
@@ -31,9 +31,16 @@ type UserIsLeavingRoomEvent = Record<string, boolean> & {
     userLogin?: string;
 };
 
+type PingPongEvent = Record<string, string | number> & {
+    id: string;
+    timestamp: number;
+};
+
 type PusherEventMap = {
     [TYPE.USER_IS_TYPING]: UserIsTypingEvent;
     [TYPE.USER_IS_LEAVING_ROOM]: UserIsLeavingRoomEvent;
+    [TYPE.PING]: PingPongEvent;
+    [TYPE.PONG]: PingPongEvent;
 };
 
 type EventData<EventName extends string> = {chunk?: string; id?: string; index?: number; final?: boolean} & (EventName extends keyof PusherEventMap
@@ -441,4 +448,4 @@ export {
     getPusherSocketID,
 };
 
-export type {EventCallbackError, States, UserIsTypingEvent, UserIsLeavingRoomEvent};
+export type {EventCallbackError, States, UserIsTypingEvent, UserIsLeavingRoomEvent, PingPongEvent};
diff --git a/src/libs/PusherUtils.ts b/src/libs/PusherUtils.ts
index ef8d57e8bec4..5243090063e5 100644
--- a/src/libs/PusherUtils.ts
+++ b/src/libs/PusherUtils.ts
@@ -30,10 +30,10 @@ function triggerMultiEventHandler(eventType: string, data: OnyxUpdate[]): Promis
 /**
  * Abstraction around subscribing to private user channel events. Handles all logs and errors automatically.
  */
-function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string, onEvent: (pushJSON: OnyxUpdatesFromServer) => void) {
+function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string, onEvent: (pushJSON: OnyxUpdatesFromServer | Pusher.PingPongEvent) => void) {
     const pusherChannelName = getUserChannelName(accountID);
 
-    function logPusherEvent(pushJSON: OnyxUpdatesFromServer) {
+    function logPusherEvent(pushJSON: OnyxUpdatesFromServer | Pusher.PingPongEvent) {
         Log.info(`[Report] Handled ${eventName} event sent by Pusher`, false, pushJSON);
     }
 
@@ -41,7 +41,7 @@ function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string
         NetworkConnection.triggerReconnectionCallbacks('Pusher re-subscribed to private user channel');
     }
 
-    function onEventPush(pushJSON: OnyxUpdatesFromServer) {
+    function onEventPush(pushJSON: OnyxUpdatesFromServer | Pusher.PingPongEvent) {
         logPusherEvent(pushJSON);
         onEvent(pushJSON);
     }
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 14a276b47e51..28293d404f77 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -28,6 +28,7 @@ import type Platform from '@libs/getPlatform/types';
 import Log from '@libs/Log';
 import Navigation from '@libs/Navigation/Navigation';
 import * as SequentialQueue from '@libs/Network/SequentialQueue';
+import * as NumberUtils from '@libs/NumberUtils';
 import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
 import * as Pusher from '@libs/Pusher/pusher';
 import PusherUtils from '@libs/PusherUtils';
@@ -888,15 +889,48 @@ function playSoundForMessageType(pushJSON: OnyxServerUpdate[]) {
     });
 }
 
+// Holds a map of all the PINGs that have been sent to the server and when they were sent
+// Once a PONG is received, the event data will be removed from this map.
+type PingPongTimestampMap = Record<string, Pusher.PingPongEvent>;
+const pingIDsAndTimestamps: PingPongTimestampMap = {};
+
 function subscribeToPusherPong() {
     // If there is no user accountID yet (because the app isn't fully setup yet), the channel can't be subscribed to so return early
     if (currentUserAccountID === -1) {
         return;
     }
 
-    PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.PONG, currentUserAccountID.toString(), (pushJSON) => {});
+    PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.PONG, currentUserAccountID.toString(), (pushJSON) => {
+        const pongEvent = pushJSON as Pusher.PingPongEvent;
+        // First, check to see if the PONG event is in the pingIDsAndTimestamps map
+        const pingEvent = pingIDsAndTimestamps[pongEvent.id];
+        if (!pingEvent) {
+            Log.info(`[Pusher PINGPONG] Received a PONG event from the server with an ID that wasn't sent by the client: ${pongEvent.id}`);
+            return;
+        }
+
+        // Calculate the latency between the client and the server
+        const latency = Date.now() - Number(pingEvent.timestamp);
+    });
 }
 
+function pingPusher() {
+    // Send a PING event to the server with a specific ID and timestamp
+    // The server will response with a PONG event with the same ID and timestamp
+    // Then we can calculate the latency between the client and the server (or if the server never replies)
+    const pingID = NumberUtils.rand64();
+    const timestamp = Date.now();
+    const eventPayload: Pusher.PingPongEvent = {
+        id: pingID,
+        timestamp,
+    };
+    Pusher.sendEvent(PusherUtils.getUserChannelName(currentUserAccountID.toString()), Pusher.TYPE.PING, eventPayload);
+    pingIDsAndTimestamps[pingID] = timestamp;
+    Log.info(`[Pusher PINGPONG] Sending a PING to the server: ${pingID} timestamp: ${timestamp}`);
+}
+
+// Specify how long between each PING event to the server
+const PING_PONG_INTERVAL_LENGTH_IN_SECONDS = 60000;
 let pingPongStarted = false;
 function initializePusherPingPong() {
     // Ignore any additional calls to initialize the ping pong if it's already been started
@@ -904,11 +938,10 @@ function initializePusherPingPong() {
         return;
     }
 
-    pingPongStarted = true;
-}
+    subscribeToPusherPong();
+    setInterval(pingPusher, PING_PONG_INTERVAL_LENGTH_IN_SECONDS);
 
-function pingPusher() {
-    Pusher.sendEvent(privateReportChannelName, Pusher.TYPE.USER_IS_TYPING, typingStatus);
+    pingPongStarted = true;
 }
 
 /**

From 0bf599366cad39b830b0b2c0f638ba24dab2c1b8 Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Tue, 21 Jan 2025 11:00:18 -0700
Subject: [PATCH 05/22] Switch to sending the ping as an API request

---
 src/libs/API/parameters/PusherPingParams.ts |  6 ++++++
 src/libs/API/parameters/index.ts            |  1 +
 src/libs/API/types.ts                       |  2 ++
 src/libs/Pusher/EventType.ts                |  5 +----
 src/libs/Pusher/pusher.ts                   |  1 -
 src/libs/actions/User.ts                    | 21 ++++++++++++---------
 6 files changed, 22 insertions(+), 14 deletions(-)
 create mode 100644 src/libs/API/parameters/PusherPingParams.ts

diff --git a/src/libs/API/parameters/PusherPingParams.ts b/src/libs/API/parameters/PusherPingParams.ts
new file mode 100644
index 000000000000..c0bd5df3b12d
--- /dev/null
+++ b/src/libs/API/parameters/PusherPingParams.ts
@@ -0,0 +1,6 @@
+type PusherPingParams = {
+    id: string;
+    timestamp: number;
+};
+
+export default PusherPingParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 4d02027b720a..5494aa50a201 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -47,6 +47,7 @@ export type {default as OpenReimbursementAccountPageParams} from './OpenReimburs
 export type {default as OpenReportParams} from './OpenReportParams';
 export type {default as OpenRoomMembersPageParams} from './OpenRoomMembersPageParams';
 export type {default as PaymentCardParams} from './PaymentCardParams';
+export type {default as PusherPingParams} from './PusherPingParams';
 export type {default as ReconnectAppParams} from './ReconnectAppParams';
 export type {default as ReferTeachersUniteVolunteerParams} from './ReferTeachersUniteVolunteerParams';
 export type {default as ReportVirtualExpensifyCardFraudParams} from './ReportVirtualExpensifyCardFraudParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 72251fd817dc..e57b65322b87 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -123,6 +123,7 @@ const WRITE_COMMANDS = {
     ADD_EMOJI_REACTION: 'AddEmojiReaction',
     REMOVE_EMOJI_REACTION: 'RemoveEmojiReaction',
     LEAVE_ROOM: 'LeaveRoom',
+    PUSHER_PING: 'PusherPing',
     LEAVE_GROUP_CHAT: 'LeaveGroupChat',
     INVITE_TO_ROOM: 'InviteToRoom',
     INVITE_TO_GROUP_CHAT: 'InviteToGroupChat',
@@ -558,6 +559,7 @@ type WriteCommandParameters = {
     [WRITE_COMMANDS.INVITE_TO_ROOM]: Parameters.InviteToRoomParams;
     [WRITE_COMMANDS.INVITE_TO_GROUP_CHAT]: Parameters.InviteToGroupChatParams;
     [WRITE_COMMANDS.UPDATE_GROUP_CHAT_AVATAR]: Parameters.UpdateGroupChatAvatarParams;
+    [WRITE_COMMANDS.PUSHER_PING]: Parameters.PusherPingParams;
     [WRITE_COMMANDS.LEAVE_GROUP_CHAT]: Parameters.LeaveGroupChatParams;
     [WRITE_COMMANDS.REMOVE_FROM_GROUP_CHAT]: Parameters.RemoveFromGroupChatParams;
     [WRITE_COMMANDS.UPDATE_GROUP_CHAT_MEMBER_ROLES]: Parameters.UpdateGroupChatMemberRolesParams;
diff --git a/src/libs/Pusher/EventType.ts b/src/libs/Pusher/EventType.ts
index 4b6938d7e9bc..b29aecac5f5b 100644
--- a/src/libs/Pusher/EventType.ts
+++ b/src/libs/Pusher/EventType.ts
@@ -9,10 +9,7 @@ export default {
     USER_IS_TYPING: 'client-userIsTyping',
     MULTIPLE_EVENTS: 'multipleEvents',
 
-    // An event that the client sends to the server and then listens for a returned "pong" event
-    PING: 'ping',
-
-    // An event that the server sends back to the client in response to a "ping" event
+    // An event that the server sends back to the client in response to a "ping" API command
     PONG: 'pong',
     MULTIPLE_EVENT_TYPE: {
         ONYX_API_UPDATE: 'onyxApiUpdate',
diff --git a/src/libs/Pusher/pusher.ts b/src/libs/Pusher/pusher.ts
index b97e2b23a420..c05bb4f1b1b4 100644
--- a/src/libs/Pusher/pusher.ts
+++ b/src/libs/Pusher/pusher.ts
@@ -39,7 +39,6 @@ type PingPongEvent = Record<string, string | number> & {
 type PusherEventMap = {
     [TYPE.USER_IS_TYPING]: UserIsTypingEvent;
     [TYPE.USER_IS_LEAVING_ROOM]: UserIsLeavingRoomEvent;
-    [TYPE.PING]: PingPongEvent;
     [TYPE.PONG]: PingPongEvent;
 };
 
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 28293d404f77..32330d30916a 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -9,6 +9,7 @@ import type {
     CloseAccountParams,
     DeleteContactMethodParams,
     GetStatementPDFParams,
+    PusherPingParams,
     RequestContactMethodValidateCodeParams,
     SetContactMethodAsDefaultParams,
     SetNameValuePairParams,
@@ -891,7 +892,7 @@ function playSoundForMessageType(pushJSON: OnyxServerUpdate[]) {
 
 // Holds a map of all the PINGs that have been sent to the server and when they were sent
 // Once a PONG is received, the event data will be removed from this map.
-type PingPongTimestampMap = Record<string, Pusher.PingPongEvent>;
+type PingPongTimestampMap = Record<string, number>;
 const pingIDsAndTimestamps: PingPongTimestampMap = {};
 
 function subscribeToPusherPong() {
@@ -903,14 +904,16 @@ function subscribeToPusherPong() {
     PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.PONG, currentUserAccountID.toString(), (pushJSON) => {
         const pongEvent = pushJSON as Pusher.PingPongEvent;
         // First, check to see if the PONG event is in the pingIDsAndTimestamps map
-        const pingEvent = pingIDsAndTimestamps[pongEvent.id];
-        if (!pingEvent) {
+        const pingEventTimestamp = pingIDsAndTimestamps[pongEvent.id];
+        if (!pingEventTimestamp) {
             Log.info(`[Pusher PINGPONG] Received a PONG event from the server with an ID that wasn't sent by the client: ${pongEvent.id}`);
             return;
         }
 
         // Calculate the latency between the client and the server
-        const latency = Date.now() - Number(pingEvent.timestamp);
+        const latency = Date.now() - Number(pingEventTimestamp);
+
+        // TODO implement a way to tell the client it's offline
     });
 }
 
@@ -920,12 +923,9 @@ function pingPusher() {
     // Then we can calculate the latency between the client and the server (or if the server never replies)
     const pingID = NumberUtils.rand64();
     const timestamp = Date.now();
-    const eventPayload: Pusher.PingPongEvent = {
-        id: pingID,
-        timestamp,
-    };
-    Pusher.sendEvent(PusherUtils.getUserChannelName(currentUserAccountID.toString()), Pusher.TYPE.PING, eventPayload);
     pingIDsAndTimestamps[pingID] = timestamp;
+    const parameters: PusherPingParams = {id: pingID, timestamp};
+    API.write(WRITE_COMMANDS.PUSHER_PING, parameters);
     Log.info(`[Pusher PINGPONG] Sending a PING to the server: ${pingID} timestamp: ${timestamp}`);
 }
 
@@ -938,6 +938,7 @@ function initializePusherPingPong() {
         return;
     }
 
+    Log.info(`[Pusher PINGPONG] Starting Pusher Ping Pong and pinging every ${PING_PONG_INTERVAL_LENGTH_IN_SECONDS} seconds`);
     subscribeToPusherPong();
     setInterval(pingPusher, PING_PONG_INTERVAL_LENGTH_IN_SECONDS);
 
@@ -1004,6 +1005,8 @@ function subscribeToUserEvents() {
         App.reconnectApp();
         return Promise.resolve();
     });
+
+    initializePusherPingPong();
 }
 
 /**

From d3ceb2e8e832cf0163386e5ae515227a83e6de91 Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Tue, 21 Jan 2025 11:45:13 -0700
Subject: [PATCH 06/22] Change parameter names

---
 src/libs/API/parameters/PusherPingParams.ts |  4 ++--
 src/libs/Pusher/pusher.ts                   |  2 +-
 src/libs/actions/User.ts                    | 18 ++++++++++--------
 3 files changed, 13 insertions(+), 11 deletions(-)

diff --git a/src/libs/API/parameters/PusherPingParams.ts b/src/libs/API/parameters/PusherPingParams.ts
index c0bd5df3b12d..b156d6fa6039 100644
--- a/src/libs/API/parameters/PusherPingParams.ts
+++ b/src/libs/API/parameters/PusherPingParams.ts
@@ -1,6 +1,6 @@
 type PusherPingParams = {
-    id: string;
-    timestamp: number;
+    pingID: string;
+    pingTimestamp: number;
 };
 
 export default PusherPingParams;
diff --git a/src/libs/Pusher/pusher.ts b/src/libs/Pusher/pusher.ts
index c05bb4f1b1b4..e4b0cee4622b 100644
--- a/src/libs/Pusher/pusher.ts
+++ b/src/libs/Pusher/pusher.ts
@@ -32,7 +32,7 @@ type UserIsLeavingRoomEvent = Record<string, boolean> & {
 };
 
 type PingPongEvent = Record<string, string | number> & {
-    id: string;
+    pingID: string;
     timestamp: number;
 };
 
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 32330d30916a..d99d5b2899fc 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -902,16 +902,18 @@ function subscribeToPusherPong() {
     }
 
     PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.PONG, currentUserAccountID.toString(), (pushJSON) => {
+        Log.info(`[Pusher PINGPONG] Received a PONG event from the server`, false, pushJSON);
         const pongEvent = pushJSON as Pusher.PingPongEvent;
         // First, check to see if the PONG event is in the pingIDsAndTimestamps map
-        const pingEventTimestamp = pingIDsAndTimestamps[pongEvent.id];
+        const pingEventTimestamp = pingIDsAndTimestamps[pongEvent.pingID];
         if (!pingEventTimestamp) {
-            Log.info(`[Pusher PINGPONG] Received a PONG event from the server with an ID that wasn't sent by the client: ${pongEvent.id}`);
+            Log.info(`[Pusher PINGPONG] Received a PONG event from the server with an ID that wasn't sent by the client: ${pongEvent.pingID}`);
             return;
         }
 
         // Calculate the latency between the client and the server
         const latency = Date.now() - Number(pingEventTimestamp);
+        Log.info(`[Pusher PINGPONG] The event took ${latency} ms`);
 
         // TODO implement a way to tell the client it's offline
     });
@@ -922,15 +924,15 @@ function pingPusher() {
     // The server will response with a PONG event with the same ID and timestamp
     // Then we can calculate the latency between the client and the server (or if the server never replies)
     const pingID = NumberUtils.rand64();
-    const timestamp = Date.now();
-    pingIDsAndTimestamps[pingID] = timestamp;
-    const parameters: PusherPingParams = {id: pingID, timestamp};
+    const pingTimestamp = Date.now();
+    pingIDsAndTimestamps[pingID] = pingTimestamp;
+    const parameters: PusherPingParams = {pingID, pingTimestamp};
     API.write(WRITE_COMMANDS.PUSHER_PING, parameters);
-    Log.info(`[Pusher PINGPONG] Sending a PING to the server: ${pingID} timestamp: ${timestamp}`);
+    Log.info(`[Pusher PINGPONG] Sending a PING to the server: ${pingID} timestamp: ${pingTimestamp}`);
 }
 
 // Specify how long between each PING event to the server
-const PING_PONG_INTERVAL_LENGTH_IN_SECONDS = 60000;
+const PING_PONG_INTERVAL_LENGTH_IN_SECONDS = 5;
 let pingPongStarted = false;
 function initializePusherPingPong() {
     // Ignore any additional calls to initialize the ping pong if it's already been started
@@ -940,7 +942,7 @@ function initializePusherPingPong() {
 
     Log.info(`[Pusher PINGPONG] Starting Pusher Ping Pong and pinging every ${PING_PONG_INTERVAL_LENGTH_IN_SECONDS} seconds`);
     subscribeToPusherPong();
-    setInterval(pingPusher, PING_PONG_INTERVAL_LENGTH_IN_SECONDS);
+    setInterval(pingPusher, PING_PONG_INTERVAL_LENGTH_IN_SECONDS * 1000);
 
     pingPongStarted = true;
 }

From a37ea8006d9fd71e3f2f053c5d75268833563110 Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Tue, 21 Jan 2025 11:59:51 -0700
Subject: [PATCH 07/22] Protect against multiple loops

---
 src/libs/actions/User.ts | 40 +++++++++++++++++++++++++++++++++-------
 1 file changed, 33 insertions(+), 7 deletions(-)

diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index d99d5b2899fc..23057df719e8 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -915,36 +915,62 @@ function subscribeToPusherPong() {
         const latency = Date.now() - Number(pingEventTimestamp);
         Log.info(`[Pusher PINGPONG] The event took ${latency} ms`);
 
-        // TODO implement a way to tell the client it's offline
+        // Remove the event from the map
+        delete pingIDsAndTimestamps[pongEvent.pingID];
     });
 }
 
+// Specify how long between each PING event to the server
+const PING_INTERVAL_LENGTH_IN_SECONDS = 5;
+
+// Specify how long between each check for missing PONG events
+const CHECK_MISSING_PONG_INTERVAL_LENGTH_IN_SECONDS = 5;
+
+let lastTimestamp = Date.now();
 function pingPusher() {
     // Send a PING event to the server with a specific ID and timestamp
-    // The server will response with a PONG event with the same ID and timestamp
+    // The server will respond with a PONG event with the same ID and timestamp
     // Then we can calculate the latency between the client and the server (or if the server never replies)
     const pingID = NumberUtils.rand64();
     const pingTimestamp = Date.now();
+
+    if (pingTimestamp - lastTimestamp < PING_INTERVAL_LENGTH_IN_SECONDS * 1000) {
+        Log.info(`[Pusher PINGPONG] Skipping PING event because the last event was sent too recently ${pingTimestamp - lastTimestamp} ${PING_INTERVAL_LENGTH_IN_SECONDS * 1000}`);
+        return;
+    }
+
     pingIDsAndTimestamps[pingID] = pingTimestamp;
     const parameters: PusherPingParams = {pingID, pingTimestamp};
     API.write(WRITE_COMMANDS.PUSHER_PING, parameters);
     Log.info(`[Pusher PINGPONG] Sending a PING to the server: ${pingID} timestamp: ${pingTimestamp}`);
+    lastTimestamp = pingTimestamp;
 }
 
-// Specify how long between each PING event to the server
-const PING_PONG_INTERVAL_LENGTH_IN_SECONDS = 5;
+function checkforMissingPongEvents() {}
+
 let pingPongStarted = false;
 function initializePusherPingPong() {
     // Ignore any additional calls to initialize the ping pong if it's already been started
     if (pingPongStarted) {
         return;
     }
+    pingPongStarted = true;
 
-    Log.info(`[Pusher PINGPONG] Starting Pusher Ping Pong and pinging every ${PING_PONG_INTERVAL_LENGTH_IN_SECONDS} seconds`);
+    Log.info(`[Pusher PINGPONG] Starting Pusher Ping Pong and pinging every ${PING_INTERVAL_LENGTH_IN_SECONDS} seconds`);
+
+    // Subscribe to the pong event from Pusher. Unfortunately, there is no way of knowing when the client is actually subscribed
+    // so there could be a little delay before the client is actually listening to this event.
     subscribeToPusherPong();
-    setInterval(pingPusher, PING_PONG_INTERVAL_LENGTH_IN_SECONDS * 1000);
 
-    pingPongStarted = true;
+    // Send a ping to pusher on a regular interval
+    setInterval(pingPusher, PING_INTERVAL_LENGTH_IN_SECONDS * 1000);
+
+    // Delay the start of this by double the length of PING_INTERVAL_LENGTH_IN_SECONDS to give a chance for the first
+    // events to be sent and received
+    setTimeout(() => {
+        // Check for any missing pong events on a regular interval
+        setInterval(checkforMissingPongEvents, CHECK_MISSING_PONG_INTERVAL_LENGTH_IN_SECONDS * 1000);
+    }, PING_INTERVAL_LENGTH_IN_SECONDS * 2);
 }
 
 /**

From af2476428fa7c05037bba75c6ff139012df2dc06 Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Tue, 21 Jan 2025 12:52:39 -0700
Subject: [PATCH 08/22] Implement offline switch

---
 src/libs/actions/User.ts | 35 +++++++++++++++++++++++++++++++----
 1 file changed, 31 insertions(+), 4 deletions(-)

diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 23057df719e8..2b291f01d07f 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -29,6 +29,7 @@ import type Platform from '@libs/getPlatform/types';
 import Log from '@libs/Log';
 import Navigation from '@libs/Navigation/Navigation';
 import * as SequentialQueue from '@libs/Network/SequentialQueue';
+import {setOfflineStatus} from '@libs/NetworkConnection';
 import * as NumberUtils from '@libs/NumberUtils';
 import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
 import * as Pusher from '@libs/Pusher/pusher';
@@ -905,9 +906,10 @@ function subscribeToPusherPong() {
         Log.info(`[Pusher PINGPONG] Received a PONG event from the server`, false, pushJSON);
         const pongEvent = pushJSON as Pusher.PingPongEvent;
         // First, check to see if the PONG event is in the pingIDsAndTimestamps map
+        // It's OK if it doesn't exist. The client was maybe refreshed while still waiting for a PONG event, in which case it might
+        // receive the PONG event but has already lost it's memory of the PING.
         const pingEventTimestamp = pingIDsAndTimestamps[pongEvent.pingID];
         if (!pingEventTimestamp) {
-            Log.info(`[Pusher PINGPONG] Received a PONG event from the server with an ID that wasn't sent by the client: ${pongEvent.pingID}`);
             return;
         }
 
@@ -926,6 +928,9 @@ const PING_INTERVAL_LENGTH_IN_SECONDS = 5;
 // Specify how long between each check for missing PONG events
 const CHECK_MISSING_PONG_INTERVAL_LENGTH_IN_SECONDS = 5;
 
+// Specifiy how long before a PING event is considered to be missing a PONG event in order to put the application in offline mode
+const NO_EVENT_RECEIVED_TO_BE_OFFLINE_THRESHOLD_IN_SECONDS = 2 * PING_INTERVAL_LENGTH_IN_SECONDS;
+
 let lastTimestamp = Date.now();
 function pingPusher() {
     // Send a PING event to the server with a specific ID and timestamp
@@ -934,22 +939,44 @@ function pingPusher() {
     const pingID = NumberUtils.rand64();
     const pingTimestamp = Date.now();
 
+    // In local development, there can end up being multiple intervals running because when JS code is replaced with hot module replacement, the old interval is not cleared
+    // and keeps running. This little bit of logic will attempt to keep multiple pings from happening.
     if (pingTimestamp - lastTimestamp < PING_INTERVAL_LENGTH_IN_SECONDS * 1000) {
-        Log.info(`[Pusher PINGPONG] Skipping PING event because the last event was sent too recently ${pingTimestamp - lastTimestamp} ${PING_INTERVAL_LENGTH_IN_SECONDS * 1000}`);
         return;
     }
+    lastTimestamp = pingTimestamp;
 
     pingIDsAndTimestamps[pingID] = pingTimestamp;
     const parameters: PusherPingParams = {pingID, pingTimestamp};
     API.write(WRITE_COMMANDS.PUSHER_PING, parameters);
     Log.info(`[Pusher PINGPONG] Sending a PING to the server: ${pingID} timestamp: ${pingTimestamp}`);
-    lastTimestamp = pingTimestamp;
 }
 
-function checkforMissingPongEvents() {}
+function checkforMissingPongEvents() {
+    Log.info(`[Pusher PINGPONG] Checking for missing PONG events`);
+    // Get the oldest PING timestamp that is left in the event map
+    const oldestPingTimestamp = Math.min(...Object.values(pingIDsAndTimestamps));
+    const ageOfEventInMS = Date.now() - oldestPingTimestamp;
+
+    // Get the eventID of that timestamp
+    const eventID = Object.keys(pingIDsAndTimestamps).find((key) => pingIDsAndTimestamps[key] === oldestPingTimestamp);
+
+    // If the oldest timestamp is older than 2 * PING_INTERVAL_LENGTH_IN_SECONDS, then log a message to the console.
+    // This means that the server never replied to the PING event.
+    if (ageOfEventInMS > NO_EVENT_RECEIVED_TO_BE_OFFLINE_THRESHOLD_IN_SECONDS * 1000) {
+        Log.info(`[Pusher PINGPONG] The server has not replied to the PING event ${eventID} in ${ageOfEventInMS} ms so going offline`);
+        setOfflineStatus(true, 'The client never got a Pusher PONG event after sending a Pusher PING event');
+    }
+}
 
 let pingPongStarted = false;
 function initializePusherPingPong() {
+    // Only run the ping pong from the leader client
+    if (!ActiveClientManager.isClientTheLeader()) {
+        Log.info("[Pusher PINGPONG] Not starting PING PONG because this instance isn't the leader client");
+        return;
+    }
+
     // Ignore any additional calls to initialize the ping pong if it's already been started
     if (pingPongStarted) {
         return;

From d2659fe723213619865b233201308a7a14c2f3fd Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Tue, 21 Jan 2025 13:04:48 -0700
Subject: [PATCH 09/22] Reset when going offline

---
 src/libs/actions/User.ts | 20 +++++++++++++++-----
 1 file changed, 15 insertions(+), 5 deletions(-)

diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 2b291f01d07f..fb79faf2d58d 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -1,4 +1,5 @@
 import {isBefore} from 'date-fns';
+import {Network} from 'expensify-common';
 import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
 import Onyx from 'react-native-onyx';
 import type {ValueOf} from 'type-fest';
@@ -28,8 +29,9 @@ import * as ErrorUtils from '@libs/ErrorUtils';
 import type Platform from '@libs/getPlatform/types';
 import Log from '@libs/Log';
 import Navigation from '@libs/Navigation/Navigation';
+import {isOffline} from '@libs/Network/NetworkStore';
 import * as SequentialQueue from '@libs/Network/SequentialQueue';
-import {setOfflineStatus} from '@libs/NetworkConnection';
+import NetworkConnection from '@libs/NetworkConnection';
 import * as NumberUtils from '@libs/NumberUtils';
 import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
 import * as Pusher from '@libs/Pusher/pusher';
@@ -894,7 +896,7 @@ function playSoundForMessageType(pushJSON: OnyxServerUpdate[]) {
 // Holds a map of all the PINGs that have been sent to the server and when they were sent
 // Once a PONG is received, the event data will be removed from this map.
 type PingPongTimestampMap = Record<string, number>;
-const pingIDsAndTimestamps: PingPongTimestampMap = {};
+let pingIDsAndTimestamps: PingPongTimestampMap = {};
 
 function subscribeToPusherPong() {
     // If there is no user accountID yet (because the app isn't fully setup yet), the channel can't be subscribed to so return early
@@ -923,16 +925,20 @@ function subscribeToPusherPong() {
 }
 
 // Specify how long between each PING event to the server
-const PING_INTERVAL_LENGTH_IN_SECONDS = 5;
+const PING_INTERVAL_LENGTH_IN_SECONDS = 30;
 
 // Specify how long between each check for missing PONG events
-const CHECK_MISSING_PONG_INTERVAL_LENGTH_IN_SECONDS = 5;
+const CHECK_MISSING_PONG_INTERVAL_LENGTH_IN_SECONDS = 60;
 
 // Specifiy how long before a PING event is considered to be missing a PONG event in order to put the application in offline mode
 const NO_EVENT_RECEIVED_TO_BE_OFFLINE_THRESHOLD_IN_SECONDS = 2 * PING_INTERVAL_LENGTH_IN_SECONDS;
 
 let lastTimestamp = Date.now();
 function pingPusher() {
+    if (isOffline()) {
+        Log.info('[Pusher PINGPONG] Skipping ping because the client is offline');
+        return;
+    }
     // Send a PING event to the server with a specific ID and timestamp
     // The server will respond with a PONG event with the same ID and timestamp
     // Then we can calculate the latency between the client and the server (or if the server never replies)
@@ -965,7 +971,11 @@ function checkforMissingPongEvents() {
     // This means that the server never replied to the PING event.
     if (ageOfEventInMS > NO_EVENT_RECEIVED_TO_BE_OFFLINE_THRESHOLD_IN_SECONDS * 1000) {
         Log.info(`[Pusher PINGPONG] The server has not replied to the PING event ${eventID} in ${ageOfEventInMS} ms so going offline`);
-        setOfflineStatus(true, 'The client never got a Pusher PONG event after sending a Pusher PING event');
+        NetworkConnection.setOfflineStatus(true, 'The client never got a Pusher PONG event after sending a Pusher PING event');
+
+        // When going offline, reset the pingpong state so that when the network reconnects, the client will start fresh
+        lastTimestamp = Date.now();
+        pingIDsAndTimestamps = {};
     }
 }
 

From 6039614e40efeac035987dd38a2956fc15d2ec9e Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Tue, 21 Jan 2025 13:05:20 -0700
Subject: [PATCH 10/22] Correct the comment

---
 src/libs/actions/User.ts | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index fb79faf2d58d..c03b7ac69b37 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -967,8 +967,7 @@ function checkforMissingPongEvents() {
     // Get the eventID of that timestamp
     const eventID = Object.keys(pingIDsAndTimestamps).find((key) => pingIDsAndTimestamps[key] === oldestPingTimestamp);
 
-    // If the oldest timestamp is older than 2 * PING_INTERVAL_LENGTH_IN_SECONDS, then log a message to the console.
-    // This means that the server never replied to the PING event.
+    // If the oldest timestamp is older than 2 * PING_INTERVAL_LENGTH_IN_SECONDS, then set the network status to offline
     if (ageOfEventInMS > NO_EVENT_RECEIVED_TO_BE_OFFLINE_THRESHOLD_IN_SECONDS * 1000) {
         Log.info(`[Pusher PINGPONG] The server has not replied to the PING event ${eventID} in ${ageOfEventInMS} ms so going offline`);
         NetworkConnection.setOfflineStatus(true, 'The client never got a Pusher PONG event after sending a Pusher PING event');

From e01d6e85ee6b1c509e2fa011d33b450f75986377 Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Tue, 21 Jan 2025 13:34:22 -0700
Subject: [PATCH 11/22] Lint and TS fixes

---
 src/CONST.ts             |  1 +
 src/libs/PusherUtils.ts  | 11 ++++++-----
 src/libs/actions/User.ts | 24 ++++++++++++------------
 3 files changed, 19 insertions(+), 17 deletions(-)

diff --git a/src/CONST.ts b/src/CONST.ts
index 8b2c173c7ca8..371ee082209d 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1326,6 +1326,7 @@ const CONST = {
         SEARCH_FILTER_OPTIONS: 'search_filter_options',
         USE_DEBOUNCED_STATE_DELAY: 300,
         LIST_SCROLLING_DEBOUNCE_TIME: 200,
+        PUSHER_PING_PONG: 'pusher_ping_pong',
     },
     PRIORITY_MODE: {
         GSD: 'gsd',
diff --git a/src/libs/PusherUtils.ts b/src/libs/PusherUtils.ts
index 5243090063e5..2c5f37a10ca2 100644
--- a/src/libs/PusherUtils.ts
+++ b/src/libs/PusherUtils.ts
@@ -4,7 +4,8 @@ import CONST from '@src/CONST';
 import type {OnyxUpdatesFromServer} from '@src/types/onyx';
 import Log from './Log';
 import NetworkConnection from './NetworkConnection';
-import * as Pusher from './Pusher/pusher';
+import {subscribe} from './Pusher/pusher';
+import type {PingPongEvent} from './Pusher/pusher';
 
 type Callback = (data: OnyxUpdate[]) => Promise<void>;
 
@@ -30,10 +31,10 @@ function triggerMultiEventHandler(eventType: string, data: OnyxUpdate[]): Promis
 /**
  * Abstraction around subscribing to private user channel events. Handles all logs and errors automatically.
  */
-function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string, onEvent: (pushJSON: OnyxUpdatesFromServer | Pusher.PingPongEvent) => void) {
+function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string, onEvent: (pushJSON: OnyxUpdatesFromServer | PingPongEvent) => void) {
     const pusherChannelName = getUserChannelName(accountID);
 
-    function logPusherEvent(pushJSON: OnyxUpdatesFromServer | Pusher.PingPongEvent) {
+    function logPusherEvent(pushJSON: OnyxUpdatesFromServer | PingPongEvent) {
         Log.info(`[Report] Handled ${eventName} event sent by Pusher`, false, pushJSON);
     }
 
@@ -41,7 +42,7 @@ function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string
         NetworkConnection.triggerReconnectionCallbacks('Pusher re-subscribed to private user channel');
     }
 
-    function onEventPush(pushJSON: OnyxUpdatesFromServer | Pusher.PingPongEvent) {
+    function onEventPush(pushJSON: OnyxUpdatesFromServer | PingPongEvent) {
         logPusherEvent(pushJSON);
         onEvent(pushJSON);
     }
@@ -49,7 +50,7 @@ function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string
     function onSubscriptionFailed(error: Error) {
         Log.hmmm('Failed to subscribe to Pusher channel', {error, pusherChannelName, eventName});
     }
-    Pusher.subscribe(pusherChannelName, eventName, onEventPush, onPusherResubscribeToPrivateUserChannel).catch(onSubscriptionFailed);
+    subscribe(pusherChannelName, eventName, onEventPush, onPusherResubscribeToPrivateUserChannel).catch(onSubscriptionFailed);
 }
 
 export default {
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index c03b7ac69b37..9c0397214b04 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -1,5 +1,4 @@
 import {isBefore} from 'date-fns';
-import {Network} from 'expensify-common';
 import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
 import Onyx from 'react-native-onyx';
 import type {ValueOf} from 'type-fest';
@@ -47,16 +46,16 @@ import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
 import type {BlockedFromConcierge, CustomStatusDraft, LoginList, Policy} from '@src/types/onyx';
 import type Login from '@src/types/onyx/Login';
-import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer';
+import type {OnyxServerUpdate, OnyxUpdatesFromServer} from '@src/types/onyx/OnyxUpdatesFromServer';
 import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails';
 import type {Status} from '@src/types/onyx/PersonalDetails';
 import type ReportAction from '@src/types/onyx/ReportAction';
 import {isEmptyObject} from '@src/types/utils/EmptyObject';
-import * as App from './App';
+import {reconnectApp} from './App';
 import applyOnyxUpdatesReliably from './applyOnyxUpdatesReliably';
-import * as Link from './Link';
-import * as Report from './Report';
-import * as Session from './Session';
+import {openOldDotLink} from './Link';
+import {showReportActionNotification} from './Report';
+import {resendValidateCode as sessionResendValidateCode} from './Session';
 
 let currentUserAccountID = -1;
 let currentEmail = '';
@@ -121,7 +120,7 @@ function closeAccount(reason: string) {
  * Resends a validation link to a given login
  */
 function resendValidateCode(login: string) {
-    Session.resendValidateCode(login);
+    sessionResendValidateCode(login);
 }
 
 /**
@@ -789,7 +788,7 @@ function triggerNotifications(onyxUpdates: OnyxServerUpdate[]) {
         const reportID = update.key.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, '');
         const reportActions = Object.values((update.value as OnyxCollection<ReportAction>) ?? {});
 
-        reportActions.forEach((action) => action && ReportActionsUtils.isNotifiableReportAction(action) && Report.showReportActionNotification(reportID, action));
+        reportActions.forEach((action) => action && ReportActionsUtils.isNotifiableReportAction(action) && showReportActionNotification(reportID, action));
     });
 }
 
@@ -1022,6 +1021,7 @@ function subscribeToUserEvents() {
     // Handles the mega multipleEvents from Pusher which contains an array of single events.
     // Each single event is passed to PusherUtils in order to trigger the callbacks for that event
     PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.MULTIPLE_EVENTS, currentUserAccountID.toString(), (pushJSON) => {
+        const pushEventData = pushJSON as OnyxUpdatesFromServer;
         // If this is not the main client, we shouldn't process any data received from pusher.
         if (!ActiveClientManager.isClientTheLeader()) {
             Log.info('[Pusher] Received updates, but ignoring it since this is not the active client');
@@ -1031,8 +1031,8 @@ function subscribeToUserEvents() {
         // Example: {lastUpdateID: 1, previousUpdateID: 0, updates: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}]}
         const updates = {
             type: CONST.ONYX_UPDATE_TYPES.PUSHER,
-            lastUpdateID: Number(pushJSON.lastUpdateID ?? CONST.DEFAULT_NUMBER_ID),
-            updates: pushJSON.updates ?? [],
+            lastUpdateID: Number(pushEventData.lastUpdateID ?? CONST.DEFAULT_NUMBER_ID),
+            updates: pushEventData.updates ?? [],
             previousUpdateID: Number(pushJSON.previousUpdateID ?? CONST.DEFAULT_NUMBER_ID),
         };
         applyOnyxUpdatesReliably(updates);
@@ -1066,7 +1066,7 @@ function subscribeToUserEvents() {
     // We have an event to reconnect the App. It is triggered when we detect that the user passed updateID
     // is not in the DB
     PusherUtils.subscribeToMultiEvent(Pusher.TYPE.MULTIPLE_EVENT_TYPE.RECONNECT_APP, () => {
-        App.reconnectApp();
+        reconnectApp();
         return Promise.resolve();
     });
 
@@ -1176,7 +1176,7 @@ function clearScreenShareRequest() {
  * @param roomName Name of the screen share room to join
  */
 function joinScreenShare(accessToken: string, roomName: string) {
-    Link.openOldDotLink(`inbox?action=screenShare&accessToken=${accessToken}&name=${roomName}`);
+    openOldDotLink(`inbox?action=screenShare&accessToken=${accessToken}&name=${roomName}`);
     clearScreenShareRequest();
 }
 

From 9a3724a0302dff1a675bf8251f2dafb77f4646ce Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Tue, 21 Jan 2025 13:42:11 -0700
Subject: [PATCH 12/22] Add performance monitoring

---
 contributingGuides/PERFORMANCE_METRICS.md | 1 +
 src/libs/actions/User.ts                  | 3 +++
 2 files changed, 4 insertions(+)

diff --git a/contributingGuides/PERFORMANCE_METRICS.md b/contributingGuides/PERFORMANCE_METRICS.md
index 9e942f21d918..d8bf79970006 100644
--- a/contributingGuides/PERFORMANCE_METRICS.md
+++ b/contributingGuides/PERFORMANCE_METRICS.md
@@ -24,6 +24,7 @@ Project is using Firebase for tracking these metrics. However, not all of them a
 | `open_report_from_preview`   | ✅ | Time taken to open a report from preview.<br><br>(previously `switch_report_from_preview`)<br><br>**Platforms:** All     | Starts when the user presses the Report Preview.     | Stops when the `ReportActionsList` finishes laying out.     |
 | `open_report_thread`   | ✅ | Time taken to open a thread in a report.<br><br>**Platforms:** All      | Starts when user presses Report Action Item.     | Stops when the `ReportActionsList` finishes laying out.     |
 | `send_message`    | ✅ | Time taken to send a message.<br><br>**Platforms:** All      | Starts when the new message is sent.     | Stops when the message is being rendered in the chat.     |
+| `pusher_ping_pong`    | ✅ | The time it takes to receive a PONG event through Pusher.<br><br>**Platforms:** All      | Starts every minute and repeats on the minute.     | Stops when the event is received from the server.     |
 
 ## Documentation Maintenance
 
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 9c0397214b04..b772d229ca56 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -51,6 +51,7 @@ import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails';
 import type {Status} from '@src/types/onyx/PersonalDetails';
 import type ReportAction from '@src/types/onyx/ReportAction';
 import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import Performance from '../Performance';
 import {reconnectApp} from './App';
 import applyOnyxUpdatesReliably from './applyOnyxUpdatesReliably';
 import {openOldDotLink} from './Link';
@@ -920,6 +921,7 @@ function subscribeToPusherPong() {
 
         // Remove the event from the map
         delete pingIDsAndTimestamps[pongEvent.pingID];
+        Performance.markEnd(CONST.TIMING.PUSHER_PING_PONG);
     });
 }
 
@@ -955,6 +957,7 @@ function pingPusher() {
     const parameters: PusherPingParams = {pingID, pingTimestamp};
     API.write(WRITE_COMMANDS.PUSHER_PING, parameters);
     Log.info(`[Pusher PINGPONG] Sending a PING to the server: ${pingID} timestamp: ${pingTimestamp}`);
+    Performance.markStart(CONST.TIMING.PUSHER_PING_PONG);
 }
 
 function checkforMissingPongEvents() {

From 4081444f42ac27ef109370c7d0a4083d650cf78d Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Tue, 21 Jan 2025 13:55:16 -0700
Subject: [PATCH 13/22] Fix import

---
 src/libs/actions/User.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index b772d229ca56..ad7d974404e0 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -32,6 +32,7 @@ import {isOffline} from '@libs/Network/NetworkStore';
 import * as SequentialQueue from '@libs/Network/SequentialQueue';
 import NetworkConnection from '@libs/NetworkConnection';
 import * as NumberUtils from '@libs/NumberUtils';
+import Performance from '@libs/Performance';
 import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
 import * as Pusher from '@libs/Pusher/pusher';
 import PusherUtils from '@libs/PusherUtils';
@@ -51,7 +52,6 @@ import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails';
 import type {Status} from '@src/types/onyx/PersonalDetails';
 import type ReportAction from '@src/types/onyx/ReportAction';
 import {isEmptyObject} from '@src/types/utils/EmptyObject';
-import Performance from '../Performance';
 import {reconnectApp} from './App';
 import applyOnyxUpdatesReliably from './applyOnyxUpdatesReliably';
 import {openOldDotLink} from './Link';

From 6857762e047dbf267233fc5c700781e4a7f5b105 Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Fri, 24 Jan 2025 09:16:22 -0700
Subject: [PATCH 14/22] Use timing library

---
 src/libs/actions/User.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index ad7d974404e0..4f334681002c 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -32,7 +32,7 @@ import {isOffline} from '@libs/Network/NetworkStore';
 import * as SequentialQueue from '@libs/Network/SequentialQueue';
 import NetworkConnection from '@libs/NetworkConnection';
 import * as NumberUtils from '@libs/NumberUtils';
-import Performance from '@libs/Performance';
+import Timing from '@userActions/Timing';
 import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
 import * as Pusher from '@libs/Pusher/pusher';
 import PusherUtils from '@libs/PusherUtils';
@@ -921,7 +921,7 @@ function subscribeToPusherPong() {
 
         // Remove the event from the map
         delete pingIDsAndTimestamps[pongEvent.pingID];
-        Performance.markEnd(CONST.TIMING.PUSHER_PING_PONG);
+        Timing.end(CONST.TIMING.OPEN_REPORT);
     });
 }
 
@@ -957,7 +957,7 @@ function pingPusher() {
     const parameters: PusherPingParams = {pingID, pingTimestamp};
     API.write(WRITE_COMMANDS.PUSHER_PING, parameters);
     Log.info(`[Pusher PINGPONG] Sending a PING to the server: ${pingID} timestamp: ${pingTimestamp}`);
-    Performance.markStart(CONST.TIMING.PUSHER_PING_PONG);
+    Timing.start(CONST.TIMING.OPEN_REPORT);
 }
 
 function checkforMissingPongEvents() {

From a1782f4f52c0c683a39ee394a0396e36d93a6dd0 Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Fri, 24 Jan 2025 09:31:29 -0700
Subject: [PATCH 15/22] Fix the constant

---
 Mobile-Expensify         | 2 +-
 src/libs/actions/User.ts | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/Mobile-Expensify b/Mobile-Expensify
index e8427456032e..20be9a151000 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit e8427456032e803863da64610f17a2ea948b9b75
+Subproject commit 20be9a151000b756fcbf7c028bd59405695e41d6
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 4f334681002c..fd2f1386a4db 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -32,7 +32,6 @@ import {isOffline} from '@libs/Network/NetworkStore';
 import * as SequentialQueue from '@libs/Network/SequentialQueue';
 import NetworkConnection from '@libs/NetworkConnection';
 import * as NumberUtils from '@libs/NumberUtils';
-import Timing from '@userActions/Timing';
 import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
 import * as Pusher from '@libs/Pusher/pusher';
 import PusherUtils from '@libs/PusherUtils';
@@ -41,6 +40,7 @@ import * as ReportUtils from '@libs/ReportUtils';
 import playSound, {SOUNDS} from '@libs/Sound';
 import playSoundExcludingMobile from '@libs/Sound/playSoundExcludingMobile';
 import Visibility from '@libs/Visibility';
+import Timing from '@userActions/Timing';
 import CONFIG from '@src/CONFIG';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -921,7 +921,7 @@ function subscribeToPusherPong() {
 
         // Remove the event from the map
         delete pingIDsAndTimestamps[pongEvent.pingID];
-        Timing.end(CONST.TIMING.OPEN_REPORT);
+        Timing.end(CONST.TIMING.PUSHER_PING_PONG);
     });
 }
 
@@ -957,7 +957,7 @@ function pingPusher() {
     const parameters: PusherPingParams = {pingID, pingTimestamp};
     API.write(WRITE_COMMANDS.PUSHER_PING, parameters);
     Log.info(`[Pusher PINGPONG] Sending a PING to the server: ${pingID} timestamp: ${pingTimestamp}`);
-    Timing.start(CONST.TIMING.OPEN_REPORT);
+    Timing.start(CONST.TIMING.PUSHER_PING_PONG);
 }
 
 function checkforMissingPongEvents() {

From 1f2fa1f2d8ba0971aec79387a283ca15eadb92d2 Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Mon, 27 Jan 2025 10:09:15 -0700
Subject: [PATCH 16/22] Remove export

---
 src/libs/PusherUtils.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/libs/PusherUtils.ts b/src/libs/PusherUtils.ts
index 2c5f37a10ca2..547aa06e770e 100644
--- a/src/libs/PusherUtils.ts
+++ b/src/libs/PusherUtils.ts
@@ -54,7 +54,6 @@ function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string
 }
 
 export default {
-    getUserChannelName,
     subscribeToPrivateUserChannelEvent,
     subscribeToMultiEvent,
     triggerMultiEventHandler,

From c8cd3f682cce06e3e7854b7b0e98634d32a12f1b Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Mon, 27 Jan 2025 10:20:35 -0700
Subject: [PATCH 17/22] Update Mobile-Expensify

---
 Mobile-Expensify | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Mobile-Expensify b/Mobile-Expensify
index 20be9a151000..10beb38304d3 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit 20be9a151000b756fcbf7c028bd59405695e41d6
+Subproject commit 10beb38304d35ee86b948f64afcdf49d51ab3d4a

From f92d8cc7149b73eea127f32d1c54b9c81df90ea0 Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Mon, 27 Jan 2025 10:31:07 -0700
Subject: [PATCH 18/22] Reset submodule

---
 Mobile-Expensify | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Mobile-Expensify b/Mobile-Expensify
index 10beb38304d3..e8427456032e 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit 10beb38304d35ee86b948f64afcdf49d51ab3d4a
+Subproject commit e8427456032e803863da64610f17a2ea948b9b75

From f222260d4314081921ea8430e0295ace1a86165b Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Mon, 27 Jan 2025 14:09:27 -0700
Subject: [PATCH 19/22] Fix lint

---
 src/libs/actions/User.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index fd2f1386a4db..dcc4dc319d83 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -40,7 +40,7 @@ import * as ReportUtils from '@libs/ReportUtils';
 import playSound, {SOUNDS} from '@libs/Sound';
 import playSoundExcludingMobile from '@libs/Sound/playSoundExcludingMobile';
 import Visibility from '@libs/Visibility';
-import Timing from '@userActions/Timing';
+import Timing from './Timing';
 import CONFIG from '@src/CONFIG';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';

From d745da0496505695d207aa8b9ce127171b01b4af Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Mon, 27 Jan 2025 14:26:05 -0700
Subject: [PATCH 20/22] Move import

---
 src/libs/actions/User.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index dcc4dc319d83..10111c9b55b0 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -40,7 +40,6 @@ import * as ReportUtils from '@libs/ReportUtils';
 import playSound, {SOUNDS} from '@libs/Sound';
 import playSoundExcludingMobile from '@libs/Sound/playSoundExcludingMobile';
 import Visibility from '@libs/Visibility';
-import Timing from './Timing';
 import CONFIG from '@src/CONFIG';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -52,6 +51,7 @@ import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails';
 import type {Status} from '@src/types/onyx/PersonalDetails';
 import type ReportAction from '@src/types/onyx/ReportAction';
 import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import Timing from './Timing';
 import {reconnectApp} from './App';
 import applyOnyxUpdatesReliably from './applyOnyxUpdatesReliably';
 import {openOldDotLink} from './Link';

From 46449aa86a892a22cf9b786708ddd496b12c1a05 Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@expensify.com>
Date: Mon, 27 Jan 2025 14:29:34 -0700
Subject: [PATCH 21/22] Srsly

---
 src/libs/actions/User.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 10111c9b55b0..c779c7ed6774 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -51,12 +51,12 @@ import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails';
 import type {Status} from '@src/types/onyx/PersonalDetails';
 import type ReportAction from '@src/types/onyx/ReportAction';
 import {isEmptyObject} from '@src/types/utils/EmptyObject';
-import Timing from './Timing';
 import {reconnectApp} from './App';
 import applyOnyxUpdatesReliably from './applyOnyxUpdatesReliably';
 import {openOldDotLink} from './Link';
 import {showReportActionNotification} from './Report';
 import {resendValidateCode as sessionResendValidateCode} from './Session';
+import Timing from './Timing';
 
 let currentUserAccountID = -1;
 let currentEmail = '';

From 600c9f02abc70fbb6e7db5b194488177bbe74a5f Mon Sep 17 00:00:00 2001
From: Tim Golen <tgolen@gmail.com>
Date: Wed, 29 Jan 2025 10:43:13 -0700
Subject: [PATCH 22/22] Update src/libs/actions/User.ts

Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com>
---
 src/libs/actions/User.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index c779c7ed6774..9764296c3a9a 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -931,7 +931,7 @@ const PING_INTERVAL_LENGTH_IN_SECONDS = 30;
 // Specify how long between each check for missing PONG events
 const CHECK_MISSING_PONG_INTERVAL_LENGTH_IN_SECONDS = 60;
 
-// Specifiy how long before a PING event is considered to be missing a PONG event in order to put the application in offline mode
+// Specify how long before a PING event is considered to be missing a PONG event in order to put the application in offline mode
 const NO_EVENT_RECEIVED_TO_BE_OFFLINE_THRESHOLD_IN_SECONDS = 2 * PING_INTERVAL_LENGTH_IN_SECONDS;
 
 let lastTimestamp = Date.now();
