From f99d55c3729c018bf9c09042e7c679f178a71705 Mon Sep 17 00:00:00 2001 From: Ramon Candel Segura Date: Tue, 10 Feb 2026 10:07:29 +0100 Subject: [PATCH 1/7] Handle listeners correctly when reconnect to a conference and fix memory leaks --- package.json | 2 +- .../connection-notifications/index.ts | 12 +- .../listener-setup.ts | 104 ++++++++++++++--- .../connection-notifications/types.ts | 30 +++++ .../middleware.poor-connection.ts | 16 ++- react/features/e2ee/middleware.ts | 105 ++++++++++++------ yarn.lock | 6 +- 7 files changed, 219 insertions(+), 56 deletions(-) diff --git a/package.json b/package.json index b629cbdea321..ce7bf80a03ef 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "js-md5": "0.6.1", "js-sha512": "0.8.0", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/internxt/lib-jitsi-meet/releases/download/v.0.0.20/lib-jitsi-meet-0.0.20.tgz", + "lib-jitsi-meet": "https://github.com/internxt/lib-jitsi-meet/releases/download/v.0.0.20-debug/lib-jitsi-meet-0.0.20-debug.tgz", "lodash-es": "4.17.23", "moment": "2.29.4", "moment-duration-format": "2.2.2", diff --git a/react/features/base/meet/middlewares/connection-stability/connection-notifications/index.ts b/react/features/base/meet/middlewares/connection-stability/connection-notifications/index.ts index b464d8ddede8..61d14f4efd85 100644 --- a/react/features/base/meet/middlewares/connection-stability/connection-notifications/index.ts +++ b/react/features/base/meet/middlewares/connection-stability/connection-notifications/index.ts @@ -4,7 +4,12 @@ import { CONFERENCE_JOINED, CONFERENCE_WILL_LEAVE } from '../../../../conference import { setLeaveConferenceManually } from '../../../general/utils/conferenceState'; import { CONNECTION_WILL_CONNECT } from '../../../../connection/actionTypes'; import MiddlewareRegistry from '../../../../redux/MiddlewareRegistry'; -import { setupConferenceMediaListeners, setupXMPPConnectionListeners } from './listener-setup'; +import { + removeConferenceMediaListeners, + removeXMPPConnectionListeners, + setupConferenceMediaListeners, + setupXMPPConnectionListeners +} from './listener-setup'; import { createConnectionState } from './state'; /** @@ -41,9 +46,10 @@ MiddlewareRegistry.register(({ dispatch }: IStore) => { } case CONFERENCE_WILL_LEAVE: { - // User clicked hangup button - don't show reconnection notifications + // User clicked hangup button - cleanup listeners to prevent memory leaks + removeConferenceMediaListeners(connectionState); + removeXMPPConnectionListeners(connectionState); setLeaveConferenceManually(true); - connectionState.hasConferenceListeners = false; connectionState.wasMediaConnectionInterrupted = false; break; } diff --git a/react/features/base/meet/middlewares/connection-stability/connection-notifications/listener-setup.ts b/react/features/base/meet/middlewares/connection-stability/connection-notifications/listener-setup.ts index e133dcc815aa..5af09f8211da 100644 --- a/react/features/base/meet/middlewares/connection-stability/connection-notifications/listener-setup.ts +++ b/react/features/base/meet/middlewares/connection-stability/connection-notifications/listener-setup.ts @@ -29,19 +29,56 @@ export const setupConferenceMediaListeners = ( return; } - conference.addEventListener(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => - handleMediaConnectionInterrupted(dispatch, state) - ); - - conference.addEventListener(JitsiConferenceEvents.CONNECTION_RESTORED, () => - handleMediaConnectionRestored(dispatch, state) - ); + // Create named handler functions for proper cleanup + const interruptedHandler = () => handleMediaConnectionInterrupted(dispatch, state); + const restoredHandler = () => handleMediaConnectionRestored(dispatch, state); + const suspendHandler = () => handleDeviceSuspended(dispatch); - conference.addEventListener(JitsiConferenceEvents.SUSPEND_DETECTED, () => handleDeviceSuspended(dispatch)); + conference.addEventListener(JitsiConferenceEvents.CONNECTION_INTERRUPTED, interruptedHandler); + conference.addEventListener(JitsiConferenceEvents.CONNECTION_RESTORED, restoredHandler); + conference.addEventListener(JitsiConferenceEvents.SUSPEND_DETECTED, suspendHandler); + // Store handlers and conference reference for cleanup + state.conferenceHandlers = { + interruptedHandler, + restoredHandler, + suspendHandler + }; + state.conferenceRef = conference; state.hasConferenceListeners = true; }; +/** + * Removes event listeners for conference media connection events + * + * @param state - Connection state containing handler references + */ +export const removeConferenceMediaListeners = (state: ConnectionState) => { + if (!state.conferenceRef || !state.conferenceHandlers) { + return; + } + + const { conferenceRef, conferenceHandlers } = state; + + conferenceRef.removeEventListener( + JitsiConferenceEvents.CONNECTION_INTERRUPTED, + conferenceHandlers.interruptedHandler + ); + conferenceRef.removeEventListener( + JitsiConferenceEvents.CONNECTION_RESTORED, + conferenceHandlers.restoredHandler + ); + conferenceRef.removeEventListener( + JitsiConferenceEvents.SUSPEND_DETECTED, + conferenceHandlers.suspendHandler + ); + + // Clear references to prevent memory leaks + state.conferenceHandlers = undefined; + state.conferenceRef = undefined; + state.hasConferenceListeners = false; +}; + /** * Attaches event listeners for XMPP connection events * These events track the signaling connection (WebSocket to XMPP server) @@ -55,15 +92,52 @@ export const setupXMPPConnectionListeners = (connection: any, dispatch: IStore[" return; } - connection.addEventListener(JitsiConnectionEvents.CONNECTION_ESTABLISHED, () => handleXMPPConnected()); + // Create named handler functions for proper cleanup + const connectedHandler = () => handleXMPPConnected(); + const disconnectedHandler = (message: string) => handleXMPPDisconnected(dispatch, message); + const failedHandler = (error: any, message: string) => handleXMPPConnectionFailed(dispatch, error, message); - connection.addEventListener(JitsiConnectionEvents.CONNECTION_DISCONNECTED, (message: string) => - handleXMPPDisconnected(dispatch, message) - ); + connection.addEventListener(JitsiConnectionEvents.CONNECTION_ESTABLISHED, connectedHandler); + connection.addEventListener(JitsiConnectionEvents.CONNECTION_DISCONNECTED, disconnectedHandler); + connection.addEventListener(JitsiConnectionEvents.CONNECTION_FAILED, failedHandler); + + // Store handlers and connection reference for cleanup + state.connectionHandlers = { + connectedHandler, + disconnectedHandler, + failedHandler + }; + state.connectionRef = connection; + state.hasConnectionListeners = true; +}; - connection.addEventListener(JitsiConnectionEvents.CONNECTION_FAILED, (error: any, message: string) => - handleXMPPConnectionFailed(dispatch, error, message) +/** + * Removes event listeners for XMPP connection events + * + * @param state - Connection state containing handler references + */ +export const removeXMPPConnectionListeners = (state: ConnectionState) => { + if (!state.connectionRef || !state.connectionHandlers) { + return; + } + + const { connectionRef, connectionHandlers } = state; + + connectionRef.removeEventListener( + JitsiConnectionEvents.CONNECTION_ESTABLISHED, + connectionHandlers.connectedHandler + ); + connectionRef.removeEventListener( + JitsiConnectionEvents.CONNECTION_DISCONNECTED, + connectionHandlers.disconnectedHandler + ); + connectionRef.removeEventListener( + JitsiConnectionEvents.CONNECTION_FAILED, + connectionHandlers.failedHandler ); - state.hasConnectionListeners = true; + // Clear references to prevent memory leaks + state.connectionHandlers = undefined; + state.connectionRef = undefined; + state.hasConnectionListeners = false; }; diff --git a/react/features/base/meet/middlewares/connection-stability/connection-notifications/types.ts b/react/features/base/meet/middlewares/connection-stability/connection-notifications/types.ts index 48a615197a3f..eca491eebab3 100644 --- a/react/features/base/meet/middlewares/connection-stability/connection-notifications/types.ts +++ b/react/features/base/meet/middlewares/connection-stability/connection-notifications/types.ts @@ -19,4 +19,34 @@ export interface ConnectionState { * Used to only show "connection restored" notification if there was a previous interruption */ wasMediaConnectionInterrupted: boolean; + + /** + * Stored handler references for conference media listeners + * Required for proper cleanup via removeEventListener + */ + conferenceHandlers?: { + interruptedHandler: () => void; + restoredHandler: () => void; + suspendHandler: () => void; + }; + + /** + * Stored handler references for XMPP connection listeners + * Required for proper cleanup via removeEventListener + */ + connectionHandlers?: { + connectedHandler: () => void; + disconnectedHandler: (message: string) => void; + failedHandler: (error: any, message: string) => void; + }; + + /** + * Reference to the conference object for listener removal + */ + conferenceRef?: any; + + /** + * Reference to the connection object for listener removal + */ + connectionRef?: any; } diff --git a/react/features/base/meet/middlewares/connection-stability/middleware.poor-connection.ts b/react/features/base/meet/middlewares/connection-stability/middleware.poor-connection.ts index 5f8f0f395638..258ee95f46d1 100644 --- a/react/features/base/meet/middlewares/connection-stability/middleware.poor-connection.ts +++ b/react/features/base/meet/middlewares/connection-stability/middleware.poor-connection.ts @@ -15,6 +15,8 @@ let conferenceJoinTime: number | null = null; let lastWarningTime: number | null = null; let isNotificationCurrentlyShown = false; let isSubscribedToStats = false; +let subscribedParticipantId: string | null = null; +let subscribedCallback: ((stats: IConnectionStats) => void) | null = null; interface IConnectionStats { connectionQuality?: number; @@ -100,7 +102,10 @@ MiddlewareRegistry.register((store: IStore) => (next) => (action: AnyAction) => isNotificationCurrentlyShown = false; if (localParticipant.id && !isSubscribedToStats) { - statsEmitter.subscribeToClientStats(localParticipant.id, onStatsUpdated(store)); + const callback = onStatsUpdated(store); + subscribedCallback = callback; + subscribedParticipantId = localParticipant.id; + statsEmitter.subscribeToClientStats(localParticipant.id, callback); isSubscribedToStats = true; } @@ -110,9 +115,18 @@ MiddlewareRegistry.register((store: IStore) => (next) => (action: AnyAction) => case CONFERENCE_WILL_LEAVE: { // User manually hung up - hide notification and reset state hidePoorConnectionWarning(store); + + // Unsubscribe from stats emitter to prevent memory leaks + if (isSubscribedToStats && subscribedParticipantId && subscribedCallback) { + statsEmitter.unsubscribeToClientStats(subscribedParticipantId, subscribedCallback); + } + conferenceJoinTime = null; lastWarningTime = null; isNotificationCurrentlyShown = false; + isSubscribedToStats = false; + subscribedParticipantId = null; + subscribedCallback = null; break; } } diff --git a/react/features/e2ee/middleware.ts b/react/features/e2ee/middleware.ts index ac4e8760ba48..bfd8522d92fd 100644 --- a/react/features/e2ee/middleware.ts +++ b/react/features/e2ee/middleware.ts @@ -143,6 +143,14 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { return next(action); }); +/** + * Stored E2EE event handler references for cleanup. + */ +let e2eeHandlerRefs: { + conference: any; + handlers: Map; +} | null = null; + /** * Set up state change listener to perform maintenance tasks when the conference * is left or failed. @@ -150,42 +158,73 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { StateListenerRegistry.register( (state) => getCurrentConference(state), (conference, { dispatch }, previousConference) => { - if (previousConference) { - dispatch(toggleE2EE(false)); + if (previousConference && e2eeHandlerRefs?.conference === previousConference) { + // CRITICAL: Call E2EE cleanup from lib-jitsi-meet + if (typeof previousConference.cleanUpWebWorkers === "function") { + previousConference.cleanUpWebWorkers(); + } + + // Remove all stored event handlers to prevent memory leaks + if (e2eeHandlerRefs) { + for (const [event, handler] of e2eeHandlerRefs.handlers) { + previousConference.off(event, handler); + } + } + + e2eeHandlerRefs = null; } if (conference) { - conference.on(JitsiConferenceEvents.E2EE_SAS_AVAILABLE, (sas: object) => { - if (ConfigService.instance.isDevelopment()) { - dispatch(openDialog('ParticipantVerificationDialog', ParticipantVerificationSASDialog, { sas })); - } - }); - - conference.on(JitsiConferenceEvents.E2EE_KEY_SYNC_FAILED, () => { - dispatch(showWarningNotification({ - titleKey: 'notify.encryptionKeySyncFailedTitle', - descriptionKey: 'notify.encryptionKeySyncFailed' - }, NOTIFICATION_TIMEOUT_TYPE.STICKY)); - }); - conference.on(JitsiConferenceEvents.E2EE_VERIFICATION_READY, (pId: string, sas: object) => { - // Added our ParticipantVerificationSASDialog - dispatch(openDialog("ParticipantVerificationDialog", ParticipantVerificationSASDialog, { pId, sas })); - }); - - conference.on(JitsiConferenceEvents.E2EE_CRYPTO_FAILED, () => { - logger.debug(`E2EE: crypto failure detected`); - dispatch(showWarningNotification({ - titleKey: 'notify.cryptoFailedTitle', - descriptionKey: 'notify.cryptoFailed' - }, NOTIFICATION_TIMEOUT_TYPE.STICKY)); - }); - - conference.on(JitsiConferenceEvents.E2EE_KEY_SYNC_AFTER_TIMEOUT, () => { - dispatch(showNotification({ - titleKey: 'notify.encryptionKeySyncRestoredTitle', - descriptionKey: 'notify.encryptionKeySyncRestored' - }, NOTIFICATION_TIMEOUT_TYPE.STICKY)); - }); + const handlers = new Map(); + + // E2EE_SAS_AVAILABLE handler + const sasAvailableHandler = (sas: object) => { + if (ConfigService.instance.isDevelopment()) { + dispatch(openDialog('ParticipantVerificationDialog', ParticipantVerificationSASDialog, { sas })); + } + }; + handlers.set(JitsiConferenceEvents.E2EE_SAS_AVAILABLE, sasAvailableHandler); + conference.on(JitsiConferenceEvents.E2EE_SAS_AVAILABLE, sasAvailableHandler); + + // E2EE_KEY_SYNC_FAILED handler + const keySyncFailedHandler = () => { + dispatch(showWarningNotification({ + titleKey: 'notify.encryptionKeySyncFailedTitle', + descriptionKey: 'notify.encryptionKeySyncFailed' + }, NOTIFICATION_TIMEOUT_TYPE.STICKY)); + }; + handlers.set(JitsiConferenceEvents.E2EE_KEY_SYNC_FAILED, keySyncFailedHandler); + conference.on(JitsiConferenceEvents.E2EE_KEY_SYNC_FAILED, keySyncFailedHandler); + + // E2EE_VERIFICATION_READY handler + const verificationReadyHandler = (pId: string, sas: object) => { + dispatch(openDialog('ParticipantVerificationDialog', ParticipantVerificationSASDialog, { pId, sas })); + }; + handlers.set(JitsiConferenceEvents.E2EE_VERIFICATION_READY, verificationReadyHandler); + conference.on(JitsiConferenceEvents.E2EE_VERIFICATION_READY, verificationReadyHandler); + + // E2EE_CRYPTO_FAILED handler + const cryptoFailedHandler = () => { + dispatch(showWarningNotification({ + titleKey: 'notify.cryptoFailedTitle', + descriptionKey: 'notify.cryptoFailed' + }, NOTIFICATION_TIMEOUT_TYPE.STICKY)); + }; + handlers.set(JitsiConferenceEvents.E2EE_CRYPTO_FAILED, cryptoFailedHandler); + conference.on(JitsiConferenceEvents.E2EE_CRYPTO_FAILED, cryptoFailedHandler); + + // E2EE_KEY_SYNC_AFTER_TIMEOUT handler + const keySyncAfterTimeoutHandler = () => { + dispatch(showNotification({ + titleKey: 'notify.encryptionKeySyncRestoredTitle', + descriptionKey: 'notify.encryptionKeySyncRestored' + }, NOTIFICATION_TIMEOUT_TYPE.STICKY)); + }; + handlers.set(JitsiConferenceEvents.E2EE_KEY_SYNC_AFTER_TIMEOUT, keySyncAfterTimeoutHandler); + conference.on(JitsiConferenceEvents.E2EE_KEY_SYNC_AFTER_TIMEOUT, keySyncAfterTimeoutHandler); + + // Store references for cleanup + e2eeHandlerRefs = { conference, handlers }; } } ); diff --git a/yarn.lock b/yarn.lock index ce266a416176..b3b32f055029 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11081,9 +11081,9 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -"lib-jitsi-meet@https://github.com/internxt/lib-jitsi-meet/releases/download/v.0.0.20/lib-jitsi-meet-0.0.20.tgz": - version "0.0.20" - resolved "https://github.com/internxt/lib-jitsi-meet/releases/download/v.0.0.20/lib-jitsi-meet-0.0.20.tgz#5048eba36fa1f6b1884c00d6d5a21e847c9d2c46" +"lib-jitsi-meet@https://github.com/internxt/lib-jitsi-meet/releases/download/v.0.0.20-debug/lib-jitsi-meet-0.0.20-debug.tgz": + version "0.0.20-debug" + resolved "https://github.com/internxt/lib-jitsi-meet/releases/download/v.0.0.20-debug/lib-jitsi-meet-0.0.20-debug.tgz#3e5a6b196a215d2dc449c588c23ad32bded2b1de" dependencies: "@hexagon/base64" "^2.0.4" "@jitsi/js-utils" "^2.6.7" From 1c7d4be5f5fcd302c765c05c621f8a31d6e68e29 Mon Sep 17 00:00:00 2001 From: Ramon Candel Segura Date: Tue, 10 Feb 2026 10:48:30 +0100 Subject: [PATCH 2/7] Added logs in auto reconnect middleware --- .../middleware.auto-reconnect.ts | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts b/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts index 8f4bd01c70bf..e8d157b24b49 100644 --- a/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts +++ b/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts @@ -58,6 +58,7 @@ const clearRemoteTracks = (store: IStore) => { }; const triggerReconnection = (store: IStore) => { + console.log("[AUTO_RECONNECT] Triggering reconnection via connect()"); store.dispatch(connect()); }; @@ -80,14 +81,20 @@ const handleMaxAttemptsReached = (store: IStore) => { * If max attempts reached, reloads the page. */ const attemptReconnection = async (store: IStore) => { - if (isLeavingConferenceManually()) return; + console.log("[AUTO_RECONNECT] attemptReconnection called"); + if (isLeavingConferenceManually()) { + console.log("[AUTO_RECONNECT] Aborting: User is leaving manually"); + return; + } if (reconnectionAttempts >= MAX_RECONNECTION_ATTEMPTS) { + console.log("[AUTO_RECONNECT] Max attempts reached, will reload page"); handleMaxAttemptsReached(store); return; } reconnectionAttempts++; + console.log(`[AUTO_RECONNECT] Attempt #${reconnectionAttempts}/${MAX_RECONNECTION_ATTEMPTS}`); isReconnecting = true; showReconnectionLoader(store, reconnectionAttempts); @@ -111,6 +118,7 @@ const clearTimer = () => { }; const resetReconnectionState = () => { + console.log("[AUTO_RECONNECT] Resetting reconnection state"); clearTimer(); reconnectionAttempts = 0; isReconnecting = false; @@ -124,6 +132,7 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA switch (action.type) { case CONFERENCE_WILL_LEAVE: { + console.log("[AUTO_RECONNECT] CONFERENCE_WILL_LEAVE - User leaving conference"); setLeaveConferenceManually(true); resetReconnectionState(); hideReconnectionNotification(store); @@ -132,11 +141,18 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA } case CONNECTION_DISCONNECTED: { - if (isLeavingConferenceManually()) break; + console.log("[AUTO_RECONNECT] CONNECTION_DISCONNECTED", { + isLeavingManually: isLeavingConferenceManually() + }); + if (isLeavingConferenceManually()) { + console.log("[AUTO_RECONNECT] Skipping reconnection - user leaving manually"); + break; + } clearTimer(); reconnectionAttempts = 0; isReconnecting = true; + console.log(`[AUTO_RECONNECT] Will attempt reconnection in ${RECONNECTION_WAIT_TIME_MS}ms`); reconnectionTimer = window.setTimeout(() => { if (!isLeavingConferenceManually() && isReconnecting) { @@ -148,7 +164,11 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA } case CONNECTION_ESTABLISHED: { + console.log("[AUTO_RECONNECT] CONNECTION_ESTABLISHED", { + wasReconnecting: isReconnecting + }); if (isReconnecting) { + console.log("[AUTO_RECONNECT] Reconnection successful!"); hideReconnectionNotification(store); hideReconnectionLoader(store); } @@ -160,8 +180,14 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA case CONNECTION_FAILED: { const { error } = action; - console.log("[AUTO_RECONNECT] Connection failed with error:", error); + console.log("[AUTO_RECONNECT] CONNECTION_FAILED", { + error, + errorName: error?.name, + isLeavingManually: isLeavingConferenceManually(), + isReconnecting + }); if (error?.name === JWT_EXPIRED_ERROR && !isLeavingConferenceManually() && !isReconnecting) { + console.log("[AUTO_RECONNECT] JWT expired, starting reconnection"); attemptReconnection(store); } From 57473779f61883facec08588a81dc4ec3d4152d4 Mon Sep 17 00:00:00 2001 From: Ramon Candel Segura Date: Tue, 10 Feb 2026 11:31:50 +0100 Subject: [PATCH 3/7] Disable auto-reconnect middleware --- .../connection-stability/middleware.auto-reconnect.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts b/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts index e8d157b24b49..00a55f418958 100644 --- a/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts +++ b/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts @@ -3,12 +3,12 @@ import { AnyAction } from "redux"; import { IStore } from "../../../../app/types"; import { hideNotification } from "../../../../notifications/actions"; import { CONFERENCE_WILL_LEAVE } from "../../../conference/actionTypes"; -import { isLeavingConferenceManually, setLeaveConferenceManually } from "../../general/utils/conferenceState"; import { CONNECTION_DISCONNECTED, CONNECTION_ESTABLISHED, CONNECTION_FAILED } from "../../../connection/actionTypes"; import { connect } from "../../../connection/actions.web"; import { setJWT } from "../../../jwt/actions"; import MiddlewareRegistry from "../../../redux/MiddlewareRegistry"; import { trackRemoved } from "../../../tracks/actions.any"; +import { isLeavingConferenceManually, setLeaveConferenceManually } from "../../general/utils/conferenceState"; import { hideLoader, showLoader } from "../../loader"; const RECONNECTION_NOTIFICATION_ID = "connection.reconnecting"; @@ -130,6 +130,8 @@ const resetReconnectionState = () => { MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyAction) => { const result = next(action); + return result; + switch (action.type) { case CONFERENCE_WILL_LEAVE: { console.log("[AUTO_RECONNECT] CONFERENCE_WILL_LEAVE - User leaving conference"); From 2f8af949a16674317c96177b3560f76a0178825f Mon Sep 17 00:00:00 2001 From: Ramon Candel Segura Date: Tue, 10 Feb 2026 11:48:38 +0100 Subject: [PATCH 4/7] Added logs in different middlewares to check e2ee workers cleanup, connection events and listeners setup --- .../connection-notifications/index.ts | 3 +++ .../listener-setup.ts | 22 +++++++++++++++++++ react/features/e2ee/middleware.ts | 8 +++++++ 3 files changed, 33 insertions(+) diff --git a/react/features/base/meet/middlewares/connection-stability/connection-notifications/index.ts b/react/features/base/meet/middlewares/connection-stability/connection-notifications/index.ts index 61d14f4efd85..a2731f04f2e2 100644 --- a/react/features/base/meet/middlewares/connection-stability/connection-notifications/index.ts +++ b/react/features/base/meet/middlewares/connection-stability/connection-notifications/index.ts @@ -29,6 +29,7 @@ MiddlewareRegistry.register(({ dispatch }: IStore) => { switch (action.type) { case CONNECTION_WILL_CONNECT: { + console.log("[CONNECTION_NOTIFICATIONS] CONNECTION_WILL_CONNECT - Setting up XMPP listeners"); setLeaveConferenceManually(false); connectionState.hasConnectionListeners = false; @@ -39,6 +40,7 @@ MiddlewareRegistry.register(({ dispatch }: IStore) => { } case CONFERENCE_JOINED: { + console.log("[CONNECTION_NOTIFICATIONS] CONFERENCE_JOINED - Setting up media listeners"); const { conference } = action; setupConferenceMediaListeners(conference, dispatch, connectionState); @@ -46,6 +48,7 @@ MiddlewareRegistry.register(({ dispatch }: IStore) => { } case CONFERENCE_WILL_LEAVE: { + console.log("[CONNECTION_NOTIFICATIONS] CONFERENCE_WILL_LEAVE - Cleaning up all listeners"); // User clicked hangup button - cleanup listeners to prevent memory leaks removeConferenceMediaListeners(connectionState); removeXMPPConnectionListeners(connectionState); diff --git a/react/features/base/meet/middlewares/connection-stability/connection-notifications/listener-setup.ts b/react/features/base/meet/middlewares/connection-stability/connection-notifications/listener-setup.ts index 5af09f8211da..652f3fd28098 100644 --- a/react/features/base/meet/middlewares/connection-stability/connection-notifications/listener-setup.ts +++ b/react/features/base/meet/middlewares/connection-stability/connection-notifications/listener-setup.ts @@ -26,9 +26,15 @@ export const setupConferenceMediaListeners = ( state: ConnectionState ) => { if (state.hasConferenceListeners || !conference) { + console.log("[LISTENER_SETUP] Skipping conference media listeners setup", { + alreadyHasListeners: state.hasConferenceListeners, + noConference: !conference + }); return; } + console.log("[LISTENER_SETUP] Setting up conference media listeners (ICE events)"); + // Create named handler functions for proper cleanup const interruptedHandler = () => handleMediaConnectionInterrupted(dispatch, state); const restoredHandler = () => handleMediaConnectionRestored(dispatch, state); @@ -46,6 +52,7 @@ export const setupConferenceMediaListeners = ( }; state.conferenceRef = conference; state.hasConferenceListeners = true; + console.log("[LISTENER_SETUP] Conference media listeners registered successfully"); }; /** @@ -55,9 +62,12 @@ export const setupConferenceMediaListeners = ( */ export const removeConferenceMediaListeners = (state: ConnectionState) => { if (!state.conferenceRef || !state.conferenceHandlers) { + console.log("[LISTENER_SETUP] Skipping conference media listeners removal - no refs"); return; } + console.log("[LISTENER_SETUP] Removing conference media listeners"); + const { conferenceRef, conferenceHandlers } = state; conferenceRef.removeEventListener( @@ -77,6 +87,7 @@ export const removeConferenceMediaListeners = (state: ConnectionState) => { state.conferenceHandlers = undefined; state.conferenceRef = undefined; state.hasConferenceListeners = false; + console.log("[LISTENER_SETUP] Conference media listeners removed successfully"); }; /** @@ -89,9 +100,15 @@ export const removeConferenceMediaListeners = (state: ConnectionState) => { */ export const setupXMPPConnectionListeners = (connection: any, dispatch: IStore["dispatch"], state: ConnectionState) => { if (!connection || state.hasConnectionListeners) { + console.log("[LISTENER_SETUP] Skipping XMPP listeners setup", { + noConnection: !connection, + alreadyHasListeners: state.hasConnectionListeners + }); return; } + console.log("[LISTENER_SETUP] Setting up XMPP connection listeners"); + // Create named handler functions for proper cleanup const connectedHandler = () => handleXMPPConnected(); const disconnectedHandler = (message: string) => handleXMPPDisconnected(dispatch, message); @@ -109,6 +126,7 @@ export const setupXMPPConnectionListeners = (connection: any, dispatch: IStore[" }; state.connectionRef = connection; state.hasConnectionListeners = true; + console.log("[LISTENER_SETUP] XMPP connection listeners registered successfully"); }; /** @@ -118,9 +136,12 @@ export const setupXMPPConnectionListeners = (connection: any, dispatch: IStore[" */ export const removeXMPPConnectionListeners = (state: ConnectionState) => { if (!state.connectionRef || !state.connectionHandlers) { + console.log("[LISTENER_SETUP] Skipping XMPP listeners removal - no refs"); return; } + console.log("[LISTENER_SETUP] Removing XMPP connection listeners"); + const { connectionRef, connectionHandlers } = state; connectionRef.removeEventListener( @@ -140,4 +161,5 @@ export const removeXMPPConnectionListeners = (state: ConnectionState) => { state.connectionHandlers = undefined; state.connectionRef = undefined; state.hasConnectionListeners = false; + console.log("[LISTENER_SETUP] XMPP connection listeners removed successfully"); }; diff --git a/react/features/e2ee/middleware.ts b/react/features/e2ee/middleware.ts index bfd8522d92fd..10e2663ab4ba 100644 --- a/react/features/e2ee/middleware.ts +++ b/react/features/e2ee/middleware.ts @@ -159,13 +159,19 @@ StateListenerRegistry.register( (state) => getCurrentConference(state), (conference, { dispatch }, previousConference) => { if (previousConference && e2eeHandlerRefs?.conference === previousConference) { + console.log("[E2EE] Conference changed - cleaning up previous conference"); + // CRITICAL: Call E2EE cleanup from lib-jitsi-meet if (typeof previousConference.cleanUpWebWorkers === "function") { + console.log("[E2EE] Calling cleanUpWebWorkers() to terminate E2EE worker"); previousConference.cleanUpWebWorkers(); + } else { + console.warn("[E2EE] cleanUpWebWorkers() not available on previous conference"); } // Remove all stored event handlers to prevent memory leaks if (e2eeHandlerRefs) { + console.log(`[E2EE] Removing ${e2eeHandlerRefs.handlers.size} event handlers`); for (const [event, handler] of e2eeHandlerRefs.handlers) { previousConference.off(event, handler); } @@ -175,6 +181,7 @@ StateListenerRegistry.register( } if (conference) { + console.log("[E2EE] Setting up E2EE handlers for new conference"); const handlers = new Map(); // E2EE_SAS_AVAILABLE handler @@ -225,6 +232,7 @@ StateListenerRegistry.register( // Store references for cleanup e2eeHandlerRefs = { conference, handlers }; + console.log(`[E2EE] Successfully registered ${handlers.size} E2EE event handlers`); } } ); From fc6c6808f12aa22a56a771f4d9466e89c78767f5 Mon Sep 17 00:00:00 2001 From: Ramon Candel Segura Date: Tue, 10 Feb 2026 12:13:20 +0100 Subject: [PATCH 5/7] Enabled auto-reconnect middleware --- .../connection-stability/middleware.auto-reconnect.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts b/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts index 00a55f418958..decd392f50d2 100644 --- a/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts +++ b/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts @@ -130,8 +130,6 @@ const resetReconnectionState = () => { MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyAction) => { const result = next(action); - return result; - switch (action.type) { case CONFERENCE_WILL_LEAVE: { console.log("[AUTO_RECONNECT] CONFERENCE_WILL_LEAVE - User leaving conference"); From 2a1e3035ac16e888295fdef748b318b7939549e3 Mon Sep 17 00:00:00 2001 From: Ramon Candel Segura Date: Tue, 10 Feb 2026 12:44:18 +0100 Subject: [PATCH 6/7] Removed auto reconnect middleware --- .../event-handlers.connection.ts | 3 - .../middlewares/connection-stability/index.ts | 1 - .../middleware.auto-reconnect.ts | 201 ------------------ 3 files changed, 205 deletions(-) delete mode 100644 react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts diff --git a/react/features/base/meet/middlewares/connection-stability/connection-notifications/event-handlers.connection.ts b/react/features/base/meet/middlewares/connection-stability/connection-notifications/event-handlers.connection.ts index 7bff10440763..16110c011ce5 100644 --- a/react/features/base/meet/middlewares/connection-stability/connection-notifications/event-handlers.connection.ts +++ b/react/features/base/meet/middlewares/connection-stability/connection-notifications/event-handlers.connection.ts @@ -1,6 +1,5 @@ import { IStore } from '../../../../../app/types'; import { isLeavingConferenceManually } from "../../../general/utils/conferenceState"; -import { isAutoReconnecting } from "../middleware.auto-reconnect"; import { showConnectionFailedNotification, showConnectionLostNotification } from "./notification-helpers"; /** @@ -22,7 +21,6 @@ export const handleXMPPDisconnected = (dispatch: IStore["dispatch"], message: st console.log("[CONNECTION_NOTIFICATIONS] XMPP disconnected:", message); if (isLeavingConferenceManually()) return; - if (isAutoReconnecting()) return; showConnectionLostNotification(dispatch); }; @@ -37,7 +35,6 @@ export const handleXMPPDisconnected = (dispatch: IStore["dispatch"], message: st */ export const handleXMPPConnectionFailed = (dispatch: IStore["dispatch"], error: any, message: string) => { console.error("[CONNECTION_NOTIFICATIONS] XMPP connection failed:", error, message); - if (isAutoReconnecting()) return; showConnectionFailedNotification(dispatch, message); }; diff --git a/react/features/base/meet/middlewares/connection-stability/index.ts b/react/features/base/meet/middlewares/connection-stability/index.ts index bacad5c7140c..c260460526a7 100644 --- a/react/features/base/meet/middlewares/connection-stability/index.ts +++ b/react/features/base/meet/middlewares/connection-stability/index.ts @@ -13,7 +13,6 @@ import './connection-notifications'; import './middleware.datachannel'; import './middleware.error-handling'; import './middleware.poor-connection'; -import './middleware.auto-reconnect'; export { }; diff --git a/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts b/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts deleted file mode 100644 index decd392f50d2..000000000000 --- a/react/features/base/meet/middlewares/connection-stability/middleware.auto-reconnect.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { batch } from "react-redux"; -import { AnyAction } from "redux"; -import { IStore } from "../../../../app/types"; -import { hideNotification } from "../../../../notifications/actions"; -import { CONFERENCE_WILL_LEAVE } from "../../../conference/actionTypes"; -import { CONNECTION_DISCONNECTED, CONNECTION_ESTABLISHED, CONNECTION_FAILED } from "../../../connection/actionTypes"; -import { connect } from "../../../connection/actions.web"; -import { setJWT } from "../../../jwt/actions"; -import MiddlewareRegistry from "../../../redux/MiddlewareRegistry"; -import { trackRemoved } from "../../../tracks/actions.any"; -import { isLeavingConferenceManually, setLeaveConferenceManually } from "../../general/utils/conferenceState"; -import { hideLoader, showLoader } from "../../loader"; - -const RECONNECTION_NOTIFICATION_ID = "connection.reconnecting"; -const RECONNECTION_LOADER_ID = "auto-reconnect"; -const RECONNECTION_WAIT_TIME_MS = 15000; -const MAX_RECONNECTION_ATTEMPTS = 2; -const RECONNECTION_DELAY_MS = 3000; -const JWT_EXPIRED_ERROR = "connection.passwordRequired"; - -let reconnectionTimer: number | null = null; -let isReconnecting = false; -let reconnectionAttempts = 0; - -export const isAutoReconnecting = () => isReconnecting; - -const hideReconnectionNotification = (store: IStore) => { - store.dispatch(hideNotification(RECONNECTION_NOTIFICATION_ID)); -}; - -const showReconnectionLoader = (store: IStore, attempt: number) => { - const textKey = attempt <= MAX_RECONNECTION_ATTEMPTS ? "loader.reconnecting" : "loader.reloading"; - - store.dispatch(showLoader(undefined, textKey, RECONNECTION_LOADER_ID)); -}; - -const hideReconnectionLoader = (store: IStore) => { - store.dispatch(hideLoader(RECONNECTION_LOADER_ID)); -}; - -const reloadPage = () => { - window.location.reload(); -}; - -const clearExpiredJWT = (store: IStore) => { - store.dispatch(setJWT(undefined)); -}; - -const clearRemoteTracks = (store: IStore) => { - const state = store.getState(); - const remoteTracks = state["features/base/tracks"].filter((t) => !t.local); - - batch(() => { - for (const track of remoteTracks) { - store.dispatch(trackRemoved(track.jitsiTrack)); - } - }); -}; - -const triggerReconnection = (store: IStore) => { - console.log("[AUTO_RECONNECT] Triggering reconnection via connect()"); - store.dispatch(connect()); -}; - -const scheduleRetry = (store: IStore) => { - reconnectionTimer = window.setTimeout(() => { - if (!isLeavingConferenceManually() && isReconnecting) { - attemptReconnection(store); - } - }, RECONNECTION_DELAY_MS); -}; - -const handleMaxAttemptsReached = (store: IStore) => { - isReconnecting = true; - showReconnectionLoader(store, reconnectionAttempts + 1); - reconnectionTimer = window.setTimeout(reloadPage, 2000); -}; - -/** - * Attempts to reconnect by clearing JWT and connecting to conference again. - * If max attempts reached, reloads the page. - */ -const attemptReconnection = async (store: IStore) => { - console.log("[AUTO_RECONNECT] attemptReconnection called"); - if (isLeavingConferenceManually()) { - console.log("[AUTO_RECONNECT] Aborting: User is leaving manually"); - return; - } - - if (reconnectionAttempts >= MAX_RECONNECTION_ATTEMPTS) { - console.log("[AUTO_RECONNECT] Max attempts reached, will reload page"); - handleMaxAttemptsReached(store); - return; - } - - reconnectionAttempts++; - console.log(`[AUTO_RECONNECT] Attempt #${reconnectionAttempts}/${MAX_RECONNECTION_ATTEMPTS}`); - isReconnecting = true; - showReconnectionLoader(store, reconnectionAttempts); - - try { - clearRemoteTracks(store); - clearExpiredJWT(store); - await new Promise((resolve) => setTimeout(resolve, 100)); - triggerReconnection(store); - scheduleRetry(store); - } catch (error) { - console.error("[AUTO_RECONNECT] Reconnection error:", error); - scheduleRetry(store); - } -}; - -const clearTimer = () => { - if (reconnectionTimer !== null) { - clearTimeout(reconnectionTimer); - reconnectionTimer = null; - } -}; - -const resetReconnectionState = () => { - console.log("[AUTO_RECONNECT] Resetting reconnection state"); - clearTimer(); - reconnectionAttempts = 0; - isReconnecting = false; -}; - -/** - * Middleware that handles automatic reconnection when JWT expires or connection is lost. - */ -MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyAction) => { - const result = next(action); - - switch (action.type) { - case CONFERENCE_WILL_LEAVE: { - console.log("[AUTO_RECONNECT] CONFERENCE_WILL_LEAVE - User leaving conference"); - setLeaveConferenceManually(true); - resetReconnectionState(); - hideReconnectionNotification(store); - hideReconnectionLoader(store); - break; - } - - case CONNECTION_DISCONNECTED: { - console.log("[AUTO_RECONNECT] CONNECTION_DISCONNECTED", { - isLeavingManually: isLeavingConferenceManually() - }); - if (isLeavingConferenceManually()) { - console.log("[AUTO_RECONNECT] Skipping reconnection - user leaving manually"); - break; - } - - clearTimer(); - reconnectionAttempts = 0; - isReconnecting = true; - console.log(`[AUTO_RECONNECT] Will attempt reconnection in ${RECONNECTION_WAIT_TIME_MS}ms`); - - reconnectionTimer = window.setTimeout(() => { - if (!isLeavingConferenceManually() && isReconnecting) { - attemptReconnection(store); - } - }, RECONNECTION_WAIT_TIME_MS); - - break; - } - - case CONNECTION_ESTABLISHED: { - console.log("[AUTO_RECONNECT] CONNECTION_ESTABLISHED", { - wasReconnecting: isReconnecting - }); - if (isReconnecting) { - console.log("[AUTO_RECONNECT] Reconnection successful!"); - hideReconnectionNotification(store); - hideReconnectionLoader(store); - } - - resetReconnectionState(); - setLeaveConferenceManually(false); - break; - } - - case CONNECTION_FAILED: { - const { error } = action; - console.log("[AUTO_RECONNECT] CONNECTION_FAILED", { - error, - errorName: error?.name, - isLeavingManually: isLeavingConferenceManually(), - isReconnecting - }); - if (error?.name === JWT_EXPIRED_ERROR && !isLeavingConferenceManually() && !isReconnecting) { - console.log("[AUTO_RECONNECT] JWT expired, starting reconnection"); - attemptReconnection(store); - } - - break; - } - } - - return result; -}); - -export default {}; From 84cbf92460a4ecec8ab0938bae2e327fcfc64022 Mon Sep 17 00:00:00 2001 From: Ramon Candel Segura Date: Tue, 10 Feb 2026 16:09:03 +0100 Subject: [PATCH 7/7] Remove unnecessary comments and change condition --- .../connection-notifications/listener-setup.ts | 6 ------ react/features/e2ee/middleware.ts | 9 +-------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/react/features/base/meet/middlewares/connection-stability/connection-notifications/listener-setup.ts b/react/features/base/meet/middlewares/connection-stability/connection-notifications/listener-setup.ts index 652f3fd28098..efbd99fd7092 100644 --- a/react/features/base/meet/middlewares/connection-stability/connection-notifications/listener-setup.ts +++ b/react/features/base/meet/middlewares/connection-stability/connection-notifications/listener-setup.ts @@ -35,7 +35,6 @@ export const setupConferenceMediaListeners = ( console.log("[LISTENER_SETUP] Setting up conference media listeners (ICE events)"); - // Create named handler functions for proper cleanup const interruptedHandler = () => handleMediaConnectionInterrupted(dispatch, state); const restoredHandler = () => handleMediaConnectionRestored(dispatch, state); const suspendHandler = () => handleDeviceSuspended(dispatch); @@ -44,7 +43,6 @@ export const setupConferenceMediaListeners = ( conference.addEventListener(JitsiConferenceEvents.CONNECTION_RESTORED, restoredHandler); conference.addEventListener(JitsiConferenceEvents.SUSPEND_DETECTED, suspendHandler); - // Store handlers and conference reference for cleanup state.conferenceHandlers = { interruptedHandler, restoredHandler, @@ -83,7 +81,6 @@ export const removeConferenceMediaListeners = (state: ConnectionState) => { conferenceHandlers.suspendHandler ); - // Clear references to prevent memory leaks state.conferenceHandlers = undefined; state.conferenceRef = undefined; state.hasConferenceListeners = false; @@ -109,7 +106,6 @@ export const setupXMPPConnectionListeners = (connection: any, dispatch: IStore[" console.log("[LISTENER_SETUP] Setting up XMPP connection listeners"); - // Create named handler functions for proper cleanup const connectedHandler = () => handleXMPPConnected(); const disconnectedHandler = (message: string) => handleXMPPDisconnected(dispatch, message); const failedHandler = (error: any, message: string) => handleXMPPConnectionFailed(dispatch, error, message); @@ -118,7 +114,6 @@ export const setupXMPPConnectionListeners = (connection: any, dispatch: IStore[" connection.addEventListener(JitsiConnectionEvents.CONNECTION_DISCONNECTED, disconnectedHandler); connection.addEventListener(JitsiConnectionEvents.CONNECTION_FAILED, failedHandler); - // Store handlers and connection reference for cleanup state.connectionHandlers = { connectedHandler, disconnectedHandler, @@ -157,7 +152,6 @@ export const removeXMPPConnectionListeners = (state: ConnectionState) => { connectionHandlers.failedHandler ); - // Clear references to prevent memory leaks state.connectionHandlers = undefined; state.connectionRef = undefined; state.hasConnectionListeners = false; diff --git a/react/features/e2ee/middleware.ts b/react/features/e2ee/middleware.ts index 10e2663ab4ba..8722718c29dc 100644 --- a/react/features/e2ee/middleware.ts +++ b/react/features/e2ee/middleware.ts @@ -161,15 +161,13 @@ StateListenerRegistry.register( if (previousConference && e2eeHandlerRefs?.conference === previousConference) { console.log("[E2EE] Conference changed - cleaning up previous conference"); - // CRITICAL: Call E2EE cleanup from lib-jitsi-meet - if (typeof previousConference.cleanUpWebWorkers === "function") { + if (previousConference?.cleanUpWebWorkers) { console.log("[E2EE] Calling cleanUpWebWorkers() to terminate E2EE worker"); previousConference.cleanUpWebWorkers(); } else { console.warn("[E2EE] cleanUpWebWorkers() not available on previous conference"); } - // Remove all stored event handlers to prevent memory leaks if (e2eeHandlerRefs) { console.log(`[E2EE] Removing ${e2eeHandlerRefs.handlers.size} event handlers`); for (const [event, handler] of e2eeHandlerRefs.handlers) { @@ -184,7 +182,6 @@ StateListenerRegistry.register( console.log("[E2EE] Setting up E2EE handlers for new conference"); const handlers = new Map(); - // E2EE_SAS_AVAILABLE handler const sasAvailableHandler = (sas: object) => { if (ConfigService.instance.isDevelopment()) { dispatch(openDialog('ParticipantVerificationDialog', ParticipantVerificationSASDialog, { sas })); @@ -193,7 +190,6 @@ StateListenerRegistry.register( handlers.set(JitsiConferenceEvents.E2EE_SAS_AVAILABLE, sasAvailableHandler); conference.on(JitsiConferenceEvents.E2EE_SAS_AVAILABLE, sasAvailableHandler); - // E2EE_KEY_SYNC_FAILED handler const keySyncFailedHandler = () => { dispatch(showWarningNotification({ titleKey: 'notify.encryptionKeySyncFailedTitle', @@ -203,14 +199,12 @@ StateListenerRegistry.register( handlers.set(JitsiConferenceEvents.E2EE_KEY_SYNC_FAILED, keySyncFailedHandler); conference.on(JitsiConferenceEvents.E2EE_KEY_SYNC_FAILED, keySyncFailedHandler); - // E2EE_VERIFICATION_READY handler const verificationReadyHandler = (pId: string, sas: object) => { dispatch(openDialog('ParticipantVerificationDialog', ParticipantVerificationSASDialog, { pId, sas })); }; handlers.set(JitsiConferenceEvents.E2EE_VERIFICATION_READY, verificationReadyHandler); conference.on(JitsiConferenceEvents.E2EE_VERIFICATION_READY, verificationReadyHandler); - // E2EE_CRYPTO_FAILED handler const cryptoFailedHandler = () => { dispatch(showWarningNotification({ titleKey: 'notify.cryptoFailedTitle', @@ -220,7 +214,6 @@ StateListenerRegistry.register( handlers.set(JitsiConferenceEvents.E2EE_CRYPTO_FAILED, cryptoFailedHandler); conference.on(JitsiConferenceEvents.E2EE_CRYPTO_FAILED, cryptoFailedHandler); - // E2EE_KEY_SYNC_AFTER_TIMEOUT handler const keySyncAfterTimeoutHandler = () => { dispatch(showNotification({ titleKey: 'notify.encryptionKeySyncRestoredTitle',