From 3aa2ec99efb7872f948dc844fc8ace6a767493f4 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Tue, 24 Mar 2026 14:32:32 -0300 Subject: [PATCH 01/11] Update lib and add registered --- app/lib/services/voip/MediaSessionInstance.ts | 4 ++++ package.json | 2 +- packages/rocket.chat-media-signaling-0.1.1.tgz | Bin 62589 -> 0 bytes packages/rocket.chat-media-signaling-0.1.3.tgz | Bin 0 -> 70712 bytes yarn.lock | 6 +++--- 5 files changed, 8 insertions(+), 4 deletions(-) delete mode 100644 packages/rocket.chat-media-signaling-0.1.1.tgz create mode 100644 packages/rocket.chat-media-signaling-0.1.3.tgz diff --git a/app/lib/services/voip/MediaSessionInstance.ts b/app/lib/services/voip/MediaSessionInstance.ts index 772086438d0..17257768357 100644 --- a/app/lib/services/voip/MediaSessionInstance.ts +++ b/app/lib/services/voip/MediaSessionInstance.ts @@ -71,6 +71,10 @@ class MediaSessionInstance { } }); + this.instance?.on('registered', ({ activeCalls }) => { + console.log('[VoIP] Media session registered, activeCalls:', activeCalls); + }); + this.instance?.on('newCall', ({ call }: { call: IClientMediaCall }) => { if (call && !call.hidden) { call.emitter.on('stateChange', oldState => { diff --git a/package.json b/package.json index f263e21bcc3..3e1adde3de5 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@react-navigation/elements": "^2.6.1", "@react-navigation/native": "^7.1.16", "@react-navigation/native-stack": "^7.3.23", - "@rocket.chat/media-signaling": "file:./packages/rocket.chat-media-signaling-0.1.1.tgz", + "@rocket.chat/media-signaling": "file:./packages/rocket.chat-media-signaling-0.1.3.tgz", "@rocket.chat/message-parser": "^0.31.31", "@rocket.chat/mobile-crypto": "RocketChat/rocket.chat-mobile-crypto", "@rocket.chat/sdk": "RocketChat/Rocket.Chat.js.SDK#mobile", diff --git a/packages/rocket.chat-media-signaling-0.1.1.tgz b/packages/rocket.chat-media-signaling-0.1.1.tgz deleted file mode 100644 index 52ffe0f9308c843b85ce5f4f652b1ebbf36b929a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 62589 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%0AF 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 OKK~DB$<4$7C9UDjA|oRsBO+s&g`>~m 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 hB|}{ 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

c!ICia8q?UXRPvC@d7k#H#Hc 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 W|GIzOzwY1t{`>#Mpyet6W(WYFsFS|{ literal 0 HcmV?d00001 diff --git a/yarn.lock b/yarn.lock index 47c478e3563..132ba5a32c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5016,9 +5016,9 @@ dependencies: eslint-plugin-import "^2.17.2" -"@rocket.chat/media-signaling@file:./packages/rocket.chat-media-signaling-0.1.1.tgz": - version "0.1.1" - resolved "file:./packages/rocket.chat-media-signaling-0.1.1.tgz#b9859941d78af41e1fd82d2e0cf53d119aeaac9b" +"@rocket.chat/media-signaling@file:./packages/rocket.chat-media-signaling-0.1.3.tgz": + version "0.1.3" + resolved "file:./packages/rocket.chat-media-signaling-0.1.3.tgz#d120c37812a26c2223a53761c7936938ad05ccfb" dependencies: "@rocket.chat/emitter" "^0.32.0" ajv "^8.17.1" From 9445a1d5a98a36ad24a5101f451271ed68fc51ef Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Tue, 24 Mar 2026 15:25:06 -0300 Subject: [PATCH 02/11] Add device id to store --- app/lib/services/voip/MediaSessionStore.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/lib/services/voip/MediaSessionStore.ts b/app/lib/services/voip/MediaSessionStore.ts index 8ccc513999c..64703d778b6 100644 --- a/app/lib/services/voip/MediaSessionStore.ts +++ b/app/lib/services/voip/MediaSessionStore.ts @@ -7,6 +7,7 @@ import type { MediaCallWebRTCProcessor } from '@rocket.chat/media-signaling'; import { mediaDevices } from 'react-native-webrtc'; +import { getUniqueIdSync } from 'react-native-device-info'; import { MediaCallLogger } from './MediaCallLogger'; @@ -52,6 +53,8 @@ class MediaSessionStore extends Emitter<{ change: void }> { throw new Error('WebRTC processor factory and send signal function must be set'); } + const mobileDeviceId = getUniqueIdSync(); + console.log('[VoIP] Mobile device ID:', mobileDeviceId); this.sessionInstance = new MediaSignalingSession({ userId, transport: (signal: ClientMediaSignal) => this.sendSignal(signal), @@ -59,9 +62,11 @@ class MediaSessionStore extends Emitter<{ change: void }> { webrtc: (config: WebRTCProcessorConfig) => this.webrtcProcessorFactory(config) }, mediaStreamFactory: (constraints: any) => mediaDevices.getUserMedia(constraints) as unknown as Promise, + displayMediaFactory: (constraints: any) => mediaDevices.getUserMedia(constraints) as unknown as Promise, randomStringFactory, logger: new MediaCallLogger(), - features: ['audio'] + features: ['audio'], + mobileDeviceId }); this.change(); From e99186654480f2dadf34b40add3306f506ba22d1 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Tue, 24 Mar 2026 15:26:33 -0300 Subject: [PATCH 03/11] Able to accept the call from curl --- app/lib/services/voip/MediaSessionInstance.ts | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/app/lib/services/voip/MediaSessionInstance.ts b/app/lib/services/voip/MediaSessionInstance.ts index 17257768357..1df7a896560 100644 --- a/app/lib/services/voip/MediaSessionInstance.ts +++ b/app/lib/services/voip/MediaSessionInstance.ts @@ -8,7 +8,7 @@ import { } from '@rocket.chat/media-signaling'; import RNCallKeep from 'react-native-callkeep'; import { registerGlobals } from 'react-native-webrtc'; -import { getUniqueId } from 'react-native-device-info'; +import { getUniqueIdSync } from 'react-native-device-info'; import { mediaSessionStore } from './MediaSessionStore'; import { useCallStore } from './useCallStore'; @@ -35,8 +35,6 @@ class MediaSessionInstance { this.reset(); registerGlobals(); this.configureIceServers(); - // prevent JS and native DDP clients from interfering with each other - NativeVoipModule.stopNativeDDPClient(); mediaSessionStore.setWebRTCProcessorFactory( (config: WebRTCProcessorConfig) => @@ -65,29 +63,45 @@ class MediaSessionInstance { const signal = ddpMessage.fields.args[0]; this.instance.processSignal(signal); + console.log('🤙 [VoIP] Processed signal:', signal); + // If the call was accepted from another device, end the call - if (signal.type === 'notification' && signal.notification === 'accepted' && signal.signedContractId !== getUniqueId()) { - // TODO: pop from call view, end callkeep and remove incoming call notification + if (signal.type === 'notification' && signal.notification === 'accepted' && signal.signedContractId === getUniqueIdSync()) { + this.answerCall(signal.callId); } }); this.instance?.on('registered', ({ activeCalls }) => { + // prevent JS and native DDP clients from interfering with each other + NativeVoipModule.stopNativeDDPClient(); console.log('[VoIP] Media session registered, activeCalls:', activeCalls); + + // if (activeCalls.length === 0) { + // return; + // } + + // // fetch call by id and answer it + // const call = this.instance?.getCallData(activeCalls[0]); + // console.log('[VoIP] Registered call:', call); + // // if (call) { + // this.answerCall(activeCalls[0]); + // // } }); this.instance?.on('newCall', ({ call }: { call: IClientMediaCall }) => { if (call && !call.hidden) { call.emitter.on('stateChange', oldState => { console.log(`📊 ${oldState} → ${call.state}`); + console.log('🤙 [VoIP] New call data:', call); }); - const existingCallId = useCallStore.getState().callId; - console.log('[VoIP] Existing call Id:', existingCallId); - // // TODO: need to answer the call here? - if (existingCallId) { - this.answerCall(existingCallId); - return; - } + // const existingCallId = useCallStore.getState().callId; + // console.log('[VoIP] Existing call Id:', existingCallId); + // // // TODO: need to answer the call here? + // if (existingCallId) { + // this.answerCall(existingCallId); + // return; + // } if (call.role === 'caller') { useCallStore.getState().setCall(call); @@ -115,7 +129,7 @@ class MediaSessionInstance { Navigation.navigate('CallView'); } else { RNCallKeep.endCall(callId); - alert('Call not found'); // TODO: Show error message? + console.warn('[VoIP] Call not found:', callId); // TODO: Show error message? } }; From d5916b33fcf29ad548713e19d1a9d9c29f54958c Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Tue, 24 Mar 2026 16:17:35 -0300 Subject: [PATCH 04/11] feat: Implement VoIP call acceptance handling and notification receiver --- .../chat/rocket/reactnative/MainActivity.kt | 6 +- .../notification/NotificationIntentHandler.kt | 43 +-- .../reactnative/voip/IncomingCallActivity.kt | 12 +- .../reactnative/voip/VoipNotification.kt | 251 +++++++++++++++++- app/lib/services/voip/MediaSessionInstance.ts | 1 + 5 files changed, 246 insertions(+), 67 deletions(-) diff --git a/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt b/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt index e4ab65ceba1..dc2417f85c6 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt @@ -29,7 +29,7 @@ class MainActivity : ReactActivity() { override fun onCreate(savedInstanceState: Bundle?) { RNBootSplash.init(this, R.style.BootTheme) super.onCreate(null) - + // Handle notification intents intent?.let { NotificationIntentHandler.handleIntent(this, it) } } @@ -37,7 +37,7 @@ class MainActivity : ReactActivity() { public override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) - + // Handle notification intents when activity is already running NotificationIntentHandler.handleIntent(this, intent) } @@ -45,4 +45,4 @@ class MainActivity : ReactActivity() { override fun invokeDefaultOnBackPressed() { moveTaskToBack(true) } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt index 060d4792077..41d5921cc16 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt @@ -6,11 +6,6 @@ import android.os.Bundle import android.util.Log import com.google.gson.GsonBuilder import chat.rocket.reactnative.voip.VoipNotification -import chat.rocket.reactnative.voip.VoipModule -import chat.rocket.reactnative.voip.VoipPayload -import android.os.Build -import android.app.KeyguardManager -import android.app.Activity /** * Handles notification Intent processing from MainActivity. @@ -27,8 +22,7 @@ class NotificationIntentHandler { */ @JvmStatic fun handleIntent(context: Context, intent: Intent) { - // Handle VoIP action first - if (handleVoipIntent(context, intent)) { + if (VoipNotification.handleMainActivityVoipIntent(context, intent)) { return } @@ -41,41 +35,6 @@ class NotificationIntentHandler { handleNotificationIntent(context, intent) } - /** - * Handles VoIP call notification Intent. - * @return true if this was a VoIP intent, false otherwise - */ - @JvmStatic - private fun handleVoipIntent(context: Context, intent: Intent): Boolean { - if (!intent.getBooleanExtra("voipAction", false)) { - return false - } - val voipPayload = VoipPayload.fromBundle(intent.extras) - if (voipPayload == null || !voipPayload.isVoipIncomingCall()) { - return false - } - - Log.d(TAG, "Handling VoIP intent - voipPayload: $voipPayload") - - VoipNotification.cancelById(context, voipPayload.notificationId) - VoipNotification.cancelTimeout(voipPayload.callId) - VoipModule.storeInitialEvents(voipPayload) - - if (context is Activity) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - context.setShowWhenLocked(true) - context.setTurnScreenOn(true) - val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager - keyguardManager.requestDismissKeyguard(context, null) - } - } - - // Clear the voip flag to prevent re-processing - intent.removeExtra("voipAction") - - return true - } - /** * Handles video conference notification Intent. * @return true if this was a video conf intent, false otherwise diff --git a/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt b/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt index f74d24a06b7..1c5ee3633c8 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/voip/IncomingCallActivity.kt @@ -24,7 +24,6 @@ import android.widget.FrameLayout import android.util.Log import android.view.ViewOutlineProvider import com.bumptech.glide.Glide -import chat.rocket.reactnative.MainActivity import chat.rocket.reactnative.R import android.graphics.Typeface import chat.rocket.reactnative.notification.Ejson @@ -283,15 +282,8 @@ class IncomingCallActivity : Activity() { clearTimeout() VoipNotification.cancelTimeout(payload.callId) stopRingtone() - - // Launch MainActivity with call data - val launchIntent = Intent(this, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP - putExtras(payload.toBundle()) - } - startActivity(launchIntent) - - finish() + VoipNotification.handleAcceptAction(this, payload) + // Activity finishes when ACTION_DISMISS is broadcast from handleAcceptAction (async DDP). } private fun handleDecline(payload: VoipPayload) { diff --git a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt index 96a0d5e1bcd..099e090d5fa 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt @@ -25,6 +25,8 @@ import android.telecom.PhoneAccountHandle import android.telecom.TelecomManager import io.wazo.callkeep.VoiceConnection import io.wazo.callkeep.VoiceConnectionService +import android.app.Activity +import android.app.KeyguardManager import chat.rocket.reactnative.MainActivity import chat.rocket.reactnative.notification.Ejson import org.json.JSONArray @@ -48,12 +50,25 @@ class VoipNotification(private val context: Context) { const val ACTION_ACCEPT = "chat.rocket.reactnative.ACTION_VOIP_ACCEPT" const val ACTION_DECLINE = "chat.rocket.reactnative.ACTION_VOIP_DECLINE" + + /** + * Set on the heads-up Accept action [PendingIntent] ([PendingIntent.getActivity] → MainActivity). + * Android 12+ blocks starting an activity from a notification [BroadcastReceiver] trampoline; + * MainActivity opens first, then [handleMainActivityVoipIntent] runs accept with + * [handleAcceptAction] and `skipLaunchMainActivity = true`. + */ + const val ACTION_VOIP_ACCEPT_HEADS_UP = "chat.rocket.reactnative.ACTION_VOIP_ACCEPT_HEADS_UP" const val ACTION_TIMEOUT = "chat.rocket.reactnative.ACTION_VOIP_TIMEOUT" const val ACTION_DISMISS = "chat.rocket.reactnative.ACTION_VOIP_DISMISS" // react-native-callkeep's ConnectionService class name private const val CALLKEEP_CONNECTION_SERVICE_CLASS = "io.wazo.callkeep.VoiceConnectionService" private const val DISCONNECT_REASON_MISSED = 6 + + private data class VoipMediaCallIdentity(val userId: String, val deviceId: String) + + /** Keep in sync with MediaSessionStore features (audio-only today). */ + private val SUPPORTED_VOIP_FEATURES = JSONArray().apply { put("audio") } private val timeoutHandler = Handler(Looper.getMainLooper()) private val timeoutCallbacks = mutableMapOf() private var ddpClient: DDPClient? = null @@ -141,6 +156,135 @@ class VoipNotification(private val context: Context) { ) } + /** + * Routes VoIP-related intents delivered to [MainActivity] (cold start or [Activity.onNewIntent]). + * + * @return `true` if the intent was handled as VoIP and downstream handlers should not process it. + */ + @JvmStatic + fun handleMainActivityVoipIntent(context: Context, intent: Intent): Boolean { + val payload = VoipPayload.fromBundle(intent.extras) + if (payload == null || !payload.isVoipIncomingCall()) { + return false + } + + val headsUpAccept = intent.action == ACTION_VOIP_ACCEPT_HEADS_UP + if (headsUpAccept) { + intent.action = Intent.ACTION_MAIN + prepareMainActivityForIncomingVoip(context, payload) + handleAcceptAction(context, payload, skipLaunchMainActivity = true) + intent.removeExtra("voipAction") + return true + } + + if (intent.getBooleanExtra("voipAction", false)) { + prepareMainActivityForIncomingVoip(context, payload) + intent.removeExtra("voipAction") + return true + } + + return false + } + + /** + * Prepares MainActivity after launch with incoming-call context: cancel notification and timeout, + * stash payload for JS, and unlock/show above keyguard when [context] is an [Activity]. + */ + private fun prepareMainActivityForIncomingVoip(context: Context, payload: VoipPayload) { + Log.d(TAG, "prepareMainActivityForIncomingVoip — callId: ${payload.callId}") + cancelById(context, payload.notificationId) + cancelTimeout(payload.callId) + VoipModule.storeInitialEvents(payload) + + if (context is Activity && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + context.setShowWhenLocked(true) + context.setTurnScreenOn(true) + val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + keyguardManager.requestDismissKeyguard(context, null) + } + } + + /** + * Accept from notification or IncomingCallActivity: send accept over native DDP, sync Telecom, + * dismiss UI, then open MainActivity (unless [skipLaunchMainActivity] — already in MainActivity + * from heads-up Accept [PendingIntent.getActivity]). JS still runs answerCall afterward. + * + * The DDP call is asynchronous; [VoipModule.storeInitialEvents], notification cancel, Telecom + * answer, and [ACTION_DISMISS] run from an internal completion callback. [IncomingCallActivity] + * stays open until that broadcast is received. + */ + @JvmStatic + @JvmOverloads + fun handleAcceptAction(context: Context, payload: VoipPayload, skipLaunchMainActivity: Boolean = false) { + Log.d(TAG, "Accept action triggered for callId: ${payload.callId}") + cancelTimeout(payload.callId) + + val appCtx = context.applicationContext + fun finish(ddpSuccess: Boolean) { + if (ddpSuccess) { + answerIncomingCall(payload.callId) + } else { + Log.d(TAG, "Native accept did not succeed over DDP for ${payload.callId}; opening app for JS recovery") + } + cancelById(appCtx, payload.notificationId) + LocalBroadcastManager.getInstance(appCtx).sendBroadcast( + Intent(ACTION_DISMISS).apply { + putExtras(payload.toBundle()) + } + ) + VoipModule.storeInitialEvents(payload) + if (!skipLaunchMainActivity) { + launchMainActivityForVoip(context, payload) + } + } + + val client = ddpClient + if (client == null) { + Log.d(TAG, "Native DDP client unavailable for accept ${payload.callId}") + finish(false) + return + } + + if (isDdpLoggedIn) { + sendAcceptSignal(context, payload) { success -> + finish(success) + if (success) { + stopDDPClientInternal() + } + } + } else { + queueAcceptSignal(context, payload) { success -> + finish(success) + if (success) { + stopDDPClientInternal() + } + } + } + } + + private fun launchMainActivityForVoip(context: Context, payload: VoipPayload) { + val intent = Intent(context, MainActivity::class.java).apply { + putExtras(payload.toBundle()) + if (context is Activity) { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + } else { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_SINGLE_TOP + } + } + context.startActivity(intent) + } + + private fun answerIncomingCall(callId: String) { + val connection = VoiceConnectionService.getConnection(callId) + when (connection) { + is VoiceConnection -> connection.onAnswer() + null -> Log.d(TAG, "No active VoiceConnection found for accepted call: $callId") + else -> Log.d(TAG, "Non-VoiceConnection for accept, callId: $callId") + } + } + // TODO: unify these three functions and check VoiceConnectionService private fun disconnectTimedOutCall(callId: String) { val connection = VoiceConnectionService.getConnection(callId) @@ -206,7 +350,7 @@ class VoipNotification(private val context: Context) { Log.d(TAG, "Queued native reject signal for ${payload.callId}") } - private fun flushPendingRejectSignalIfNeeded(): Boolean { + private fun flushPendingQueuedSignalsIfNeeded(): Boolean { val client = ddpClient ?: return false if (!client.hasQueuedMethodCalls()) { return false @@ -216,33 +360,97 @@ class VoipNotification(private val context: Context) { return true } - private fun buildRejectSignalParams(context: Context, payload: VoipPayload): JSONArray? { + private fun sendAcceptSignal( + context: Context, + payload: VoipPayload, + onComplete: (Boolean) -> Unit + ) { + val client = ddpClient + if (client == null) { + Log.d(TAG, "Native DDP client unavailable, cannot send accept for ${payload.callId}") + onComplete(false) + return + } + + val params = buildAcceptSignalParams(context, payload) ?: run { + onComplete(false) + return + } + + client.callMethod("stream-notify-user", params) { success -> + Log.d(TAG, "Native accept signal result for ${payload.callId}: $success") + onComplete(success) + } + } + + private fun queueAcceptSignal( + context: Context, + payload: VoipPayload, + onComplete: (Boolean) -> Unit + ) { + val client = ddpClient + if (client == null) { + Log.d(TAG, "Native DDP client unavailable, cannot queue accept for ${payload.callId}") + onComplete(false) + return + } + + val params = buildAcceptSignalParams(context, payload) ?: run { + onComplete(false) + return + } + + client.queueMethodCall("stream-notify-user", params) { success -> + Log.d(TAG, "Queued native accept signal result for ${payload.callId}: $success") + onComplete(success) + } + Log.d(TAG, "Queued native accept signal for ${payload.callId}") + } + + private fun resolveVoipMediaCallIdentity(context: Context, payload: VoipPayload): VoipMediaCallIdentity? { val ejson = Ejson().apply { host = payload.host } val userId = ejson.userId() if (userId.isNullOrEmpty()) { - Log.d(TAG, "Missing userId, cannot send reject for ${payload.callId}") + Log.d(TAG, "Missing userId, cannot build stream-notify-user params for ${payload.callId}") stopDDPClientInternal() return null } - val deviceId = Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) if (deviceId.isNullOrEmpty()) { - Log.d(TAG, "Missing deviceId, cannot send reject for ${payload.callId}") + Log.d(TAG, "Missing deviceId, cannot build stream-notify-user params for ${payload.callId}") stopDDPClientInternal() return null } + return VoipMediaCallIdentity(userId, deviceId) + } + private fun buildAcceptSignalParams(context: Context, payload: VoipPayload): JSONArray? { + val ids = resolveVoipMediaCallIdentity(context, payload) ?: return null val signal = JSONObject().apply { put("callId", payload.callId) - put("contractId", deviceId) + put("contractId", ids.deviceId) put("type", "answer") - put("answer", "reject") + put("answer", "accept") + put("supportedFeatures", SUPPORTED_VOIP_FEATURES) + } + return JSONArray().apply { + put("${ids.userId}/media-calls") + put(signal.toString()) } + } + private fun buildRejectSignalParams(context: Context, payload: VoipPayload): JSONArray? { + val ids = resolveVoipMediaCallIdentity(context, payload) ?: return null + val signal = JSONObject().apply { + put("callId", payload.callId) + put("contractId", ids.deviceId) + put("type", "answer") + put("answer", "reject") + } return JSONArray().apply { - put("$userId/media-calls") + put("${ids.userId}/media-calls") put(signal.toString()) } } @@ -327,7 +535,7 @@ class VoipNotification(private val context: Context) { } isDdpLoggedIn = true - if (flushPendingRejectSignalIfNeeded()) { + if (flushPendingQueuedSignalsIfNeeded()) { return@login } @@ -535,12 +743,31 @@ class VoipNotification(private val context: Context) { } val fullScreenPendingIntent = createPendingIntent(notificationId, fullScreenIntent) - // Create Accept action + // Accept: must use getActivity — Android 12+ blocks starting MainActivity from a + // notification BroadcastReceiver ("trampoline"). MainActivity runs native accept with + // skipLaunchMainActivity after opening. val acceptIntent = Intent(context, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + action = ACTION_VOIP_ACCEPT_HEADS_UP + flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_SINGLE_TOP putExtras(voipPayload.toBundle()) } - val acceptPendingIntent = createPendingIntent(notificationId + 1, acceptIntent) + val acceptPendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.getActivity( + context, + notificationId + 1, + acceptIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } else { + PendingIntent.getActivity( + context, + notificationId + 1, + acceptIntent, + PendingIntent.FLAG_UPDATE_CURRENT + ) + } // Create Decline action val declineIntent = Intent(context, DeclineReceiver::class.java).apply { diff --git a/app/lib/services/voip/MediaSessionInstance.ts b/app/lib/services/voip/MediaSessionInstance.ts index 1df7a896560..a0060a7ec9c 100644 --- a/app/lib/services/voip/MediaSessionInstance.ts +++ b/app/lib/services/voip/MediaSessionInstance.ts @@ -73,6 +73,7 @@ class MediaSessionInstance { this.instance?.on('registered', ({ activeCalls }) => { // prevent JS and native DDP clients from interfering with each other + // TODO: check if this is needed NativeVoipModule.stopNativeDDPClient(); console.log('[VoIP] Media session registered, activeCalls:', activeCalls); From 9fa5b37bae099988cfebc14fb8c623dbf7c05e8e Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Tue, 24 Mar 2026 18:44:06 -0300 Subject: [PATCH 05/11] feat: Enhance VoIP call handling with native accept logic and device ID synchronization --- .cursor/skills/agent-skills | 1 + .../reactnative/voip/VoipNotification.kt | 4 + app/lib/services/voip/MediaCallEvents.ts | 18 ++-- .../voip/MediaSessionInstance.test.ts | 3 +- app/lib/services/voip/MediaSessionStore.ts | 1 + ios/Libraries/AppDelegate+Voip.swift | 6 +- ios/Libraries/VoipService.swift | 82 +++++++++++++++++-- 7 files changed, 95 insertions(+), 20 deletions(-) create mode 160000 .cursor/skills/agent-skills diff --git a/.cursor/skills/agent-skills b/.cursor/skills/agent-skills new file mode 160000 index 00000000000..a4f602ffb4a --- /dev/null +++ b/.cursor/skills/agent-skills @@ -0,0 +1 @@ +Subproject commit a4f602ffb4aeaf4199fa97b7162f9c9d1f655904 diff --git a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt index 099e090d5fa..9ab3b6854ce 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt @@ -407,6 +407,10 @@ class VoipNotification(private val context: Context) { Log.d(TAG, "Queued native accept signal for ${payload.callId}") } + /** + * Resolves user id for this host and Android [Settings.Secure.ANDROID_ID] as media-signaling contractId. + * Must match JS `getUniqueIdSync()` from react-native-device-info (iOS native code uses `DeviceUID`). + */ private fun resolveVoipMediaCallIdentity(context: Context, payload: VoipPayload): VoipMediaCallIdentity? { val ejson = Ejson().apply { host = payload.host diff --git a/app/lib/services/voip/MediaCallEvents.ts b/app/lib/services/voip/MediaCallEvents.ts index 141dcbfdd24..4f19230064a 100644 --- a/app/lib/services/voip/MediaCallEvents.ts +++ b/app/lib/services/voip/MediaCallEvents.ts @@ -32,14 +32,16 @@ export const setupMediaCallEvents = (): (() => void) => { }) ); - subscriptions.push( - RNCallKeep.addEventListener('answerCall', ({ callUUID }) => { - console.log(`${TAG} Answer call event listener:`, callUUID); - mediaSessionInstance.answerCall(callUUID); - NativeVoipModule.clearInitialEvents(); - RNCallKeep.clearInitialEvents(); - }) - ); + // Native iOS sends DDP accept from VoipService when CallKit connects; JS completes via + // MediaSessionInstance stream (accepted + signedContractId -> answerCall). Avoid duplicate answerCall. + // subscriptions.push( + // RNCallKeep.addEventListener('answerCall', ({ callUUID }) => { + // console.log(`${TAG} Answer call event listener:`, callUUID); + // mediaSessionInstance.answerCall(callUUID); + // NativeVoipModule.clearInitialEvents(); + // RNCallKeep.clearInitialEvents(); + // }) + // ); subscriptions.push( RNCallKeep.addEventListener('endCall', ({ callUUID }) => { console.log(`${TAG} End call event listener:`, callUUID); diff --git a/app/lib/services/voip/MediaSessionInstance.test.ts b/app/lib/services/voip/MediaSessionInstance.test.ts index 640d499b427..ca475453b75 100644 --- a/app/lib/services/voip/MediaSessionInstance.test.ts +++ b/app/lib/services/voip/MediaSessionInstance.test.ts @@ -45,7 +45,8 @@ jest.mock('react-native-webrtc', () => ({ jest.mock('react-native-callkeep', () => ({})); jest.mock('react-native-device-info', () => ({ - getUniqueId: jest.fn(() => 'test-device-id') + getUniqueId: jest.fn(() => 'test-device-id'), + getUniqueIdSync: jest.fn(() => 'test-device-id') })); jest.mock('../../native/NativeVoip', () => ({ diff --git a/app/lib/services/voip/MediaSessionStore.ts b/app/lib/services/voip/MediaSessionStore.ts index 64703d778b6..ede6e89063a 100644 --- a/app/lib/services/voip/MediaSessionStore.ts +++ b/app/lib/services/voip/MediaSessionStore.ts @@ -53,6 +53,7 @@ class MediaSessionStore extends Emitter<{ change: void }> { throw new Error('WebRTC processor factory and send signal function must be set'); } + // Must match native VoIP DDP contractId: iOS `DeviceUID`, Android `Settings.Secure.ANDROID_ID` (see native VoipService / VoipNotification). const mobileDeviceId = getUniqueIdSync(); console.log('[VoIP] Mobile device ID:', mobileDeviceId); this.sessionInstance = new MediaSignalingSession({ diff --git a/ios/Libraries/AppDelegate+Voip.swift b/ios/Libraries/AppDelegate+Voip.swift index 6ceb69013a0..6b3d6c7d54e 100644 --- a/ios/Libraries/AppDelegate+Voip.swift +++ b/ios/Libraries/AppDelegate+Voip.swift @@ -1,5 +1,7 @@ import PushKit +let TAG = "RocketChat.AppDelegate+Voip" + // MARK: - PKPushRegistryDelegate extension AppDelegate: PKPushRegistryDelegate { @@ -22,7 +24,7 @@ extension AppDelegate: PKPushRegistryDelegate { guard let voipPayload = VoipPayload.fromDictionary(payloadDict) else { #if DEBUG - print("[\(TAG)] Failed to parse incoming VoIP payload") + print("[\(TAG)] Failed to parse incoming VoIP payload: \(payloadDict)") #endif completion() return @@ -32,7 +34,7 @@ extension AppDelegate: PKPushRegistryDelegate { let caller = voipPayload.caller guard !voipPayload.isExpired() else { #if DEBUG - print("[\(TAG)] Skipping expired or invalid VoIP payload for callId: \(callId)") + print("[\(TAG)] Skipping expired or invalid VoIP payload for callId: \(callId): \(voipPayload)") #endif completion() return diff --git a/ios/Libraries/VoipService.swift b/ios/Libraries/VoipService.swift index 8db99f60731..dbfcaf07be2 100644 --- a/ios/Libraries/VoipService.swift +++ b/ios/Libraries/VoipService.swift @@ -42,7 +42,14 @@ public final class VoipService: NSObject { private static var isCallObserverConfigured = false private static var observedIncomingCall: ObservedIncomingCall? private static var isDdpLoggedIn = false - + /// Prevents duplicate native accepts if `hasConnected` is reported more than once for the same `callId`. + private static var nativeAcceptHandledCallIds = Set() + + private enum VoipMediaCallAnswerKind { + case accept + case reject + } + // MARK: - Static Methods (Called from VoipModule.mm and AppDelegate) /// Registers for VoIP push notifications via PushKit @@ -327,7 +334,7 @@ public final class VoipService: NSObject { } isDdpLoggedIn = true - if flushPendingRejectSignalIfNeeded() { + if flushPendingQueuedSignalsIfNeeded() { return } @@ -368,22 +375,28 @@ public final class VoipService: NSObject { ddpClient = nil } - private static func buildRejectMethodParams(payload: VoipPayload) -> [Any]? { + // MARK: - Native DDP signaling (accept / reject) + + /// `contractId` must match JS `getUniqueIdSync()` from react-native-device-info (`DeviceUID` on iOS; Android uses `Settings.Secure.ANDROID_ID` in VoipNotification). + private static func buildMediaCallAnswerParams(payload: VoipPayload, kind: VoipMediaCallAnswerKind) -> [Any]? { let credentialStorage = Storage() guard let credentials = credentialStorage.getCredentials(server: payload.host.removeTrailingSlash()) else { #if DEBUG - print("[\(TAG)] Missing credentials, cannot send reject for \(payload.callId)") + print("[\(TAG)] Missing credentials, cannot build media-call answer params for \(payload.callId)") #endif stopDDPClientInternal() return nil } - let signal: [String: Any] = [ + var signal: [String: Any] = [ "callId": payload.callId, "contractId": DeviceUID.uid(), "type": "answer", - "answer": "reject" + "answer": kind == .accept ? "accept" : "reject" ] + if kind == .accept { + signal["supportedFeatures"] = ["audio"] + } guard let signalData = try? JSONSerialization.data(withJSONObject: signal), @@ -396,6 +409,55 @@ public final class VoipService: NSObject { return ["\(credentials.userId)/media-calls", signalString] } + /// Native DDP accept when the user answers via CallKit (parity with Android `VoipNotification.handleAcceptAction`). + private static func handleNativeAccept(payload: VoipPayload) { + if nativeAcceptHandledCallIds.contains(payload.callId) { + return + } + nativeAcceptHandledCallIds.insert(payload.callId) + + cancelIncomingCallTimeout(for: payload.callId) + + let finishAccept: (Bool) -> Void = { success in + storeInitialEvents(payload) + if success { + stopDDPClientInternal() + } + } + + guard let client = ddpClient else { + #if DEBUG + print("[\(TAG)] Native DDP client unavailable for accept \(payload.callId); relying on JS") + #endif + finishAccept(false) + return + } + + guard let params = buildMediaCallAnswerParams(payload: payload, kind: .accept) else { + finishAccept(false) + return + } + + if isDdpLoggedIn { + client.callMethod("stream-notify-user", params: params) { success in + #if DEBUG + print("[\(TAG)] Native accept signal result for \(payload.callId): \(success)") + #endif + finishAccept(success) + } + } else { + client.queueMethodCall("stream-notify-user", params: params) { success in + #if DEBUG + print("[\(TAG)] Queued native accept signal result for \(payload.callId): \(success)") + #endif + finishAccept(success) + } + #if DEBUG + print("[\(TAG)] Queued native accept signal for \(payload.callId)") + #endif + } + } + private static func sendRejectSignal(payload: VoipPayload) { guard let client = ddpClient else { #if DEBUG @@ -404,7 +466,7 @@ public final class VoipService: NSObject { return } - guard let params = buildRejectMethodParams(payload: payload) else { + guard let params = buildMediaCallAnswerParams(payload: payload, kind: .reject) else { return } @@ -424,7 +486,7 @@ public final class VoipService: NSObject { return } - guard let params = buildRejectMethodParams(payload: payload) else { + guard let params = buildMediaCallAnswerParams(payload: payload, kind: .reject) else { return } @@ -436,7 +498,7 @@ public final class VoipService: NSObject { } } - private static func flushPendingRejectSignalIfNeeded() -> Bool { + private static func flushPendingQueuedSignalsIfNeeded() -> Bool { guard let client = ddpClient, client.hasQueuedMethodCalls() else { return false } @@ -489,7 +551,9 @@ public final class VoipService: NSObject { } if call.hasConnected { + let payload = observedCall.payload observedIncomingCall = nil + handleNativeAccept(payload: payload) return } From 44652cc3f937fc5ece84f6552b608cdd3db241d9 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 25 Mar 2026 13:47:49 -0300 Subject: [PATCH 06/11] feat: Add handling for VoIP accept failures and update payload structure --- .../rocket/reactnative/voip/VoipModule.kt | 25 ++++++ .../reactnative/voip/VoipNotification.kt | 23 +++--- .../rocket/reactnative/voip/VoipPayload.kt | 9 ++- app/actions/actionsTypes.ts | 2 +- app/actions/deepLinking.ts | 22 +---- app/definitions/Voip.ts | 1 + app/i18n/locales/en.json | 1 + app/lib/services/voip/MediaCallEvents.ts | 80 ++++++++++++++----- app/lib/services/voip/MediaSessionInstance.ts | 23 ------ app/sagas/deepLinking.js | 72 +++++++++++++---- ios/Libraries/AppDelegate+Voip.swift | 6 +- ios/Libraries/VoipModule.mm | 11 ++- ios/Libraries/VoipPayload.swift | 24 +++++- ios/Libraries/VoipService.swift | 48 ++++++++++- 14 files changed, 248 insertions(+), 99 deletions(-) diff --git a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt index 0862c42a9df..8e67e69fa9a 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt @@ -17,6 +17,7 @@ class VoipModule(reactContext: ReactApplicationContext) : NativeVoipSpec(reactCo companion object { private const val TAG = "RocketChat.VoipModule" private const val EVENT_INITIAL_EVENTS = "VoipPushInitialEvents" + private const val EVENT_VOIP_ACCEPT_FAILED = "VoipAcceptFailed" private var reactContextRef: WeakReference? = null private var initialEventsData: VoipPayload? = null @@ -57,6 +58,30 @@ class VoipModule(reactContext: ReactApplicationContext) : NativeVoipSpec(reactCo emitInitialEventsEvent(voipPayload) } + /** + * Stash native accept failure for cold start [getInitialEvents] and emit [EVENT_VOIP_ACCEPT_FAILED] when JS is running. + */ + @JvmStatic + fun storeAcceptFailureForJs(payload: VoipPayload) { + val failed = payload.copy(voipAcceptFailed = true) + initialEventsData = failed + emitVoipAcceptFailedEvent(failed) + } + + private fun emitVoipAcceptFailedEvent(voipPayload: VoipPayload) { + try { + reactContextRef?.get()?.let { context -> + if (context.hasActiveReactInstance()) { + context + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit(EVENT_VOIP_ACCEPT_FAILED, voipPayload.toWritableMap()) + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to emit VoipAcceptFailed", e) + } + } + @JvmStatic fun clearInitialEventsInternal() { try { diff --git a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt index 9ab3b6854ce..9ae3e47d21b 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt @@ -171,7 +171,7 @@ class VoipNotification(private val context: Context) { val headsUpAccept = intent.action == ACTION_VOIP_ACCEPT_HEADS_UP if (headsUpAccept) { intent.action = Intent.ACTION_MAIN - prepareMainActivityForIncomingVoip(context, payload) + prepareMainActivityForIncomingVoip(context, payload, storePayloadForJs = false) handleAcceptAction(context, payload, skipLaunchMainActivity = true) intent.removeExtra("voipAction") return true @@ -190,11 +190,17 @@ class VoipNotification(private val context: Context) { * Prepares MainActivity after launch with incoming-call context: cancel notification and timeout, * stash payload for JS, and unlock/show above keyguard when [context] is an [Activity]. */ - private fun prepareMainActivityForIncomingVoip(context: Context, payload: VoipPayload) { + private fun prepareMainActivityForIncomingVoip( + context: Context, + payload: VoipPayload, + storePayloadForJs: Boolean = true + ) { Log.d(TAG, "prepareMainActivityForIncomingVoip — callId: ${payload.callId}") cancelById(context, payload.notificationId) cancelTimeout(payload.callId) - VoipModule.storeInitialEvents(payload) + if (storePayloadForJs) { + VoipModule.storeInitialEvents(payload) + } if (context is Activity && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { context.setShowWhenLocked(true) @@ -221,10 +227,14 @@ class VoipNotification(private val context: Context) { val appCtx = context.applicationContext fun finish(ddpSuccess: Boolean) { + stopDDPClientInternal() if (ddpSuccess) { answerIncomingCall(payload.callId) + VoipModule.storeInitialEvents(payload) } else { Log.d(TAG, "Native accept did not succeed over DDP for ${payload.callId}; opening app for JS recovery") + disconnectIncomingCall(payload.callId, false) + VoipModule.storeAcceptFailureForJs(payload) } cancelById(appCtx, payload.notificationId) LocalBroadcastManager.getInstance(appCtx).sendBroadcast( @@ -232,7 +242,6 @@ class VoipNotification(private val context: Context) { putExtras(payload.toBundle()) } ) - VoipModule.storeInitialEvents(payload) if (!skipLaunchMainActivity) { launchMainActivityForVoip(context, payload) } @@ -248,16 +257,10 @@ class VoipNotification(private val context: Context) { if (isDdpLoggedIn) { sendAcceptSignal(context, payload) { success -> finish(success) - if (success) { - stopDDPClientInternal() - } } } else { queueAcceptSignal(context, payload) { success -> finish(success) - if (success) { - stopDDPClientInternal() - } } } } diff --git a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt index d361f711f30..beaa441871c 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipPayload.kt @@ -41,6 +41,8 @@ data class VoipPayload( @SerializedName("createdAt") val createdAt: String?, + + val voipAcceptFailed: Boolean = false, ) { val notificationId: Int = callId.hashCode() val pushType: VoipPushType? @@ -86,6 +88,9 @@ data class VoipPayload( putString("avatarUrl", avatarUrl) putString("createdAt", createdAt) putInt("notificationId", notificationId) + if (voipAcceptFailed) { + putBoolean("voipAcceptFailed", true) + } } } @@ -164,6 +169,7 @@ data class VoipPayload( hostName = hostName.orEmpty(), avatarUrl = caller?.avatarUrl, createdAt = payloadCreatedAt, + voipAcceptFailed = false, ) } } @@ -188,7 +194,8 @@ data class VoipPayload( return null } - return VoipPayload(callId, caller, username, host, type, hostName, avatarUrl, createdAt) + val voipAcceptFailed = bundle.getBoolean("voipAcceptFailed", false) + return VoipPayload(callId, caller, username, host, type, hostName, avatarUrl, createdAt, voipAcceptFailed) } private fun parseRemotePayload(data: Map): RemoteVoipPayload? { diff --git a/app/actions/actionsTypes.ts b/app/actions/actionsTypes.ts index 996edcd8a2f..938ff7b9d5d 100644 --- a/app/actions/actionsTypes.ts +++ b/app/actions/actionsTypes.ts @@ -59,7 +59,7 @@ export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DI export const LOGOUT = 'LOGOUT'; // logout is always success export const DELETE_ACCOUNT = 'DELETE_ACCOUNT'; export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']); -export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN', 'OPEN_VIDEO_CONF', 'VOIP_CALL']); +export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN', 'OPEN_VIDEO_CONF']); export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']); export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS'; export const ACTIVE_USERS = createRequestTypes('ACTIVE_USERS', ['SET', 'CLEAR']); diff --git a/app/actions/deepLinking.ts b/app/actions/deepLinking.ts index d55de045aa3..75d23cf2fe9 100644 --- a/app/actions/deepLinking.ts +++ b/app/actions/deepLinking.ts @@ -10,21 +10,15 @@ interface IParams { fullURL: string; type: string; token: string; + callId?: string; + username?: string; + voipAcceptFailed?: boolean; } interface IDeepLinkingOpen extends Action { params: Partial; } -interface IVoipCallParams { - callId: string; - host: string; -} - -interface IVoipCallOpen extends Action { - params: IVoipCallParams; -} - export function deepLinkingOpen(params: Partial): IDeepLinkingOpen { return { type: DEEP_LINKING.OPEN, @@ -39,13 +33,3 @@ export function deepLinkingClickCallPush(params: any): IDeepLinkingOpen { }; } -/** - * Action to handle VoIP call from push notification. - * Triggers server switching if needed and processes the incoming call. - */ -export function voipCallOpen(params: IVoipCallParams): IVoipCallOpen { - return { - type: DEEP_LINKING.VOIP_CALL, - params - }; -} diff --git a/app/definitions/Voip.ts b/app/definitions/Voip.ts index 72df8d1a870..e815bc8d3ed 100644 --- a/app/definitions/Voip.ts +++ b/app/definitions/Voip.ts @@ -14,4 +14,5 @@ export interface VoipPayload { readonly avatarUrl?: string | null; readonly createdAt?: string | null; readonly notificationId: number; + readonly voipAcceptFailed?: boolean; } diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 9aa2c054952..b6fc371d6de 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -984,6 +984,7 @@ "View_Original": "View original", "View_Thread": "View thread", "Voice_call": "Voice call", + "VoIP_Call_Issue": "There was an issue with the call, try again later.", "Wait_activation_warning": "Before you can login, your account must be manually activated by an administrator.", "Waiting_for_answer": "Waiting for answer", "Waiting_for_network": "Waiting for network...", diff --git a/app/lib/services/voip/MediaCallEvents.ts b/app/lib/services/voip/MediaCallEvents.ts index 4f19230064a..f8e7da09abc 100644 --- a/app/lib/services/voip/MediaCallEvents.ts +++ b/app/lib/services/voip/MediaCallEvents.ts @@ -3,7 +3,7 @@ import { DeviceEventEmitter, NativeEventEmitter } from 'react-native'; import { isIOS } from '../../methods/helpers'; import store from '../../store'; -import { voipCallOpen } from '../../../actions/deepLinking'; +import { deepLinkingOpen } from '../../../actions/deepLinking'; import { useCallStore } from './useCallStore'; import { mediaSessionInstance } from './MediaSessionInstance'; import type { VoipPayload } from '../../../definitions/Voip'; @@ -14,6 +14,30 @@ const Emitter = isIOS ? new NativeEventEmitter(NativeVoipModule) : DeviceEventEm const platform = isIOS ? 'iOS' : 'Android'; const TAG = `[MediaCallEvents][${platform}]`; +const EVENT_VOIP_ACCEPT_FAILED = 'VoipAcceptFailed'; + +/** Dedupe native emit + stash replay for the same failed accept. */ +let lastHandledVoipAcceptFailureCallId: string | null = null; + +function dispatchVoipAcceptFailureFromNative(raw: VoipPayload & { voipAcceptFailed?: boolean }) { + if (!raw.voipAcceptFailed) { + return; + } + const { callId } = raw; + if (callId && lastHandledVoipAcceptFailureCallId === callId) { + return; + } + lastHandledVoipAcceptFailureCallId = callId; + store.dispatch( + deepLinkingOpen({ + host: raw.host, + callId: raw.callId, + username: raw.username, + voipAcceptFailed: true + }) + ); +} + /** * Sets up listeners for media call events. * @returns Cleanup function to remove listeners @@ -21,7 +45,6 @@ const TAG = `[MediaCallEvents][${platform}]`; export const setupMediaCallEvents = (): (() => void) => { const subscriptions: { remove: () => void }[] = []; - // iOS listens for VoIP push token registration and CallKeep events if (isIOS) { subscriptions.push( Emitter.addListener('VoipPushTokenRegistered', ({ token }: { token: string }) => { @@ -32,16 +55,14 @@ export const setupMediaCallEvents = (): (() => void) => { }) ); - // Native iOS sends DDP accept from VoipService when CallKit connects; JS completes via - // MediaSessionInstance stream (accepted + signedContractId -> answerCall). Avoid duplicate answerCall. - // subscriptions.push( - // RNCallKeep.addEventListener('answerCall', ({ callUUID }) => { - // console.log(`${TAG} Answer call event listener:`, callUUID); - // mediaSessionInstance.answerCall(callUUID); - // NativeVoipModule.clearInitialEvents(); - // RNCallKeep.clearInitialEvents(); - // }) - // ); + subscriptions.push( + Emitter.addListener(EVENT_VOIP_ACCEPT_FAILED, (data: VoipPayload & { voipAcceptFailed?: boolean }) => { + console.log(`${TAG} VoipAcceptFailed event:`, data); + dispatchVoipAcceptFailureFromNative({ ...data, voipAcceptFailed: true }); + NativeVoipModule.clearInitialEvents(); + }) + ); + subscriptions.push( RNCallKeep.addEventListener('endCall', ({ callUUID }) => { console.log(`${TAG} End call event listener:`, callUUID); @@ -49,10 +70,15 @@ export const setupMediaCallEvents = (): (() => void) => { }) ); } else { - // Android listens for media call events from VoipModule subscriptions.push( - Emitter.addListener('VoipPushInitialEvents', async (data: VoipPayload) => { + Emitter.addListener('VoipPushInitialEvents', async (data: VoipPayload & { voipAcceptFailed?: boolean }) => { try { + if (data.voipAcceptFailed) { + console.log(`${TAG} Accept failed initial event`); + dispatchVoipAcceptFailureFromNative(data); + NativeVoipModule.clearInitialEvents(); + return; + } if (data.type !== 'incoming_call') { console.log(`${TAG} Not an incoming call`); return; @@ -61,7 +87,7 @@ export const setupMediaCallEvents = (): (() => void) => { NativeVoipModule.clearInitialEvents(); useCallStore.getState().setCallId(data.callId); store.dispatch( - voipCallOpen({ + deepLinkingOpen({ callId: data.callId, host: data.host }) @@ -72,9 +98,16 @@ export const setupMediaCallEvents = (): (() => void) => { } }) ); + + subscriptions.push( + Emitter.addListener(EVENT_VOIP_ACCEPT_FAILED, (data: VoipPayload & { voipAcceptFailed?: boolean }) => { + console.log(`${TAG} VoipAcceptFailed event:`, data); + dispatchVoipAcceptFailureFromNative({ ...data, voipAcceptFailed: true }); + NativeVoipModule.clearInitialEvents(); + }) + ); } - // Return cleanup function return () => { subscriptions.forEach(sub => sub.remove()); }; @@ -86,8 +119,7 @@ export const setupMediaCallEvents = (): (() => void) => { */ export const getInitialMediaCallEvents = async (): Promise => { try { - // Get initial events from native module - const initialEvents = NativeVoipModule.getInitialEvents() as VoipPayload | null; + const initialEvents = NativeVoipModule.getInitialEvents() as (VoipPayload & { voipAcceptFailed?: boolean }) | null; if (!initialEvents) { console.log(`${TAG} No initial events from native module`); RNCallKeep.clearInitialEvents(); @@ -95,6 +127,12 @@ export const getInitialMediaCallEvents = async (): Promise => { } console.log(`${TAG} Found initial events:`, initialEvents); + if (initialEvents.voipAcceptFailed && initialEvents.callId && initialEvents.host) { + dispatchVoipAcceptFailureFromNative(initialEvents); + RNCallKeep.clearInitialEvents(); + return false; + } + if (!initialEvents.callId || !initialEvents.host || initialEvents.type !== 'incoming_call') { console.log(`${TAG} Missing required call data`); RNCallKeep.clearInitialEvents(); @@ -108,7 +146,6 @@ export const getInitialMediaCallEvents = async (): Promise => { RNCallKeep.clearInitialEvents(); console.log(`${TAG} CallKeep initial events:`, JSON.stringify(callKeepInitialEvents, null, 2)); - // iOS loops through the events and checks if the call was already answered for (const event of callKeepInitialEvents) { const { name, data } = event; if (name === 'RNCallKeepPerformAnswerCallAction') { @@ -121,7 +158,6 @@ export const getInitialMediaCallEvents = async (): Promise => { } } } else { - // Android only sends answered event, so we can assume the call was answered wasAnswered = true; } @@ -129,12 +165,12 @@ export const getInitialMediaCallEvents = async (): Promise => { useCallStore.getState().setCallId(initialEvents.callId); store.dispatch( - voipCallOpen({ + deepLinkingOpen({ callId: initialEvents.callId, host: initialEvents.host }) ); - console.log(`${TAG} Dispatched voipCallOpen action`); + console.log(`${TAG} Dispatched deepLinkingOpen for VoIP`); } return Promise.resolve(wasAnswered); diff --git a/app/lib/services/voip/MediaSessionInstance.ts b/app/lib/services/voip/MediaSessionInstance.ts index a0060a7ec9c..3f2ef2ce6a0 100644 --- a/app/lib/services/voip/MediaSessionInstance.ts +++ b/app/lib/services/voip/MediaSessionInstance.ts @@ -16,7 +16,6 @@ import { store } from '../../store/auxStore'; import sdk from '../sdk'; import Navigation from '../../navigation/appNavigation'; import { parseStringToIceServers } from './parseStringToIceServers'; -import NativeVoipModule from '../../native/NativeVoip'; import type { IceServer } from '../../../definitions/Voip'; import type { IDDPMessage } from '../../../definitions/IDDPMessage'; import type { ISubscription, TSubscriptionModel } from '../../../definitions'; @@ -72,21 +71,7 @@ class MediaSessionInstance { }); this.instance?.on('registered', ({ activeCalls }) => { - // prevent JS and native DDP clients from interfering with each other - // TODO: check if this is needed - NativeVoipModule.stopNativeDDPClient(); console.log('[VoIP] Media session registered, activeCalls:', activeCalls); - - // if (activeCalls.length === 0) { - // return; - // } - - // // fetch call by id and answer it - // const call = this.instance?.getCallData(activeCalls[0]); - // console.log('[VoIP] Registered call:', call); - // // if (call) { - // this.answerCall(activeCalls[0]); - // // } }); this.instance?.on('newCall', ({ call }: { call: IClientMediaCall }) => { @@ -96,14 +81,6 @@ class MediaSessionInstance { console.log('🤙 [VoIP] New call data:', call); }); - // const existingCallId = useCallStore.getState().callId; - // console.log('[VoIP] Existing call Id:', existingCallId); - // // // TODO: need to answer the call here? - // if (existingCallId) { - // this.answerCall(existingCallId); - // return; - // } - if (call.role === 'caller') { useCallStore.getState().setCall(call); Navigation.navigate('CallView'); diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js index 6b0aca2235a..cd8ae76cbe7 100644 --- a/app/sagas/deepLinking.js +++ b/app/sagas/deepLinking.js @@ -1,3 +1,6 @@ +import { InteractionManager } from 'react-native'; +import RNCallKeep from 'react-native-callkeep'; +import I18n from 'i18n-js'; import { all, call, delay, put, select, take, takeLatest } from 'redux-saga/effects'; import { shareSetParams } from '../actions/share'; @@ -17,12 +20,14 @@ import EventEmitter from '../lib/methods/helpers/events'; import { goRoom, navigateToRoom } from '../lib/methods/helpers/goRoom'; import { localAuthenticate } from '../lib/methods/helpers/localAuthentication'; import log from '../lib/methods/helpers/log'; +import { showToast } from '../lib/methods/helpers/showToast'; import UserPreferences from '../lib/methods/userPreferences'; import { videoConfJoin } from '../lib/methods/videoConf'; import { loginOAuthOrSso } from '../lib/services/connect'; import { notifyUser } from '../lib/services/restApi'; import sdk from '../lib/services/sdk'; import Navigation from '../lib/navigation/appNavigation'; +import { useCallStore } from '../lib/services/voip/useCallStore'; const roomTypes = { channel: 'c', @@ -46,7 +51,7 @@ const waitForNavigation = () => { if (Navigation.navigationRef.current) { return Promise.resolve(); } - return new Promise((resolve) => { + return new Promise(resolve => { const listener = () => { emitter.off('navigationReady', listener); resolve(); @@ -87,6 +92,47 @@ const navigate = function* navigate({ params }) { yield put(appStart({ root: RootEnum.ROOT_INSIDE })); }; +/** + * After native VoIP accept fails: reset call state, end CallKit session, land inside root, + * optionally open DM via same pipeline as deep links (`direct/username`), then toast/dialog per a11y. + */ +const handleVoipAcceptFailed = function* handleVoipAcceptFailed(params) { + try { + const { callId, username } = params; + useCallStore.getState().reset(); + if (callId) { + RNCallKeep.endCall(callId); + } + + yield call(waitForNavigation); + + const navigateParams = { + ...params, + path: username ? `direct/${username}` : params.path + }; + yield navigate({ params: navigateParams }); + + yield call( + () => + new Promise(resolve => { + InteractionManager.runAfterInteractions(() => resolve()); + }) + ); + + showToast(I18n.t('VoIP_Call_Issue')); + } catch (e) { + log(e); + } +}; + +const completeDeepLinkNavigation = function* completeDeepLinkNavigation(params) { + if (params.voipAcceptFailed) { + yield call(handleVoipAcceptFailed, params); + } else { + yield navigate({ params }); + } +}; + const fallbackNavigation = function* fallbackNavigation() { const currentRoot = yield select(state => state.app.root); if (currentRoot) { @@ -140,6 +186,10 @@ const handleOpen = function* handleOpen({ params }) { // If there's no host on the deep link params and the app is opened, just call appInit() let { host } = params; if (!host) { + if (params.voipAcceptFailed) { + yield call(handleVoipAcceptFailed, params); + return; + } yield fallbackNavigation(); return; } @@ -176,7 +226,7 @@ const handleOpen = function* handleOpen({ params }) { yield put(selectServerRequest(host, serverRecord.version, true)); yield take(types.LOGIN.SUCCESS); } - yield navigate({ params }); + yield completeDeepLinkNavigation(params); } else { // search if deep link's server already exists try { @@ -184,7 +234,7 @@ const handleOpen = function* handleOpen({ params }) { yield localAuthenticate(host); yield put(selectServerRequest(host, serverRecord.version, true, true)); yield take(types.LOGIN.SUCCESS); - yield navigate({ params }); + yield completeDeepLinkNavigation(params); return; } } catch (e) { @@ -193,6 +243,10 @@ const handleOpen = function* handleOpen({ params }) { // if deep link is from a different server const result = yield getServerInfo(host); if (!result.success) { + if (params.voipAcceptFailed) { + yield call(handleVoipAcceptFailed, params); + return; + } // Fallback to prevent the app from being stuck on splash screen yield fallbackNavigation(); return; @@ -207,7 +261,7 @@ const handleOpen = function* handleOpen({ params }) { yield put(loginRequest({ resume: params.token }, true)); yield take(types.LOGIN.SUCCESS); yield put(appReady({})); - yield navigate({ params }); + yield completeDeepLinkNavigation(params); } else { yield handleInviteLink({ params, requireLogin: true }); } @@ -297,18 +351,8 @@ const handleClickCallPush = function* handleClickCallPush({ params }) { } }; -/** - * Handle VoIP call from push notification. - * Ensures the app is connected to the correct server before the call can be processed. - * The actual call handling is done by MediaSessionInstance via the pending call store. - */ -const handleVoipCall = function* handleVoipCall({ params }) { - yield handleOpen({ params }); -}; - const root = function* root() { yield takeLatest(types.DEEP_LINKING.OPEN, handleOpen); yield takeLatest(types.DEEP_LINKING.OPEN_VIDEO_CONF, handleClickCallPush); - yield takeLatest(types.DEEP_LINKING.VOIP_CALL, handleVoipCall); }; export default root; diff --git a/ios/Libraries/AppDelegate+Voip.swift b/ios/Libraries/AppDelegate+Voip.swift index 6b3d6c7d54e..81b7f7ccc3c 100644 --- a/ios/Libraries/AppDelegate+Voip.swift +++ b/ios/Libraries/AppDelegate+Voip.swift @@ -1,6 +1,6 @@ import PushKit -let TAG = "RocketChat.AppDelegate+Voip" +fileprivate let voipAppDelegateLogTag = "RocketChat.AppDelegate+Voip" // MARK: - PKPushRegistryDelegate @@ -24,7 +24,7 @@ extension AppDelegate: PKPushRegistryDelegate { guard let voipPayload = VoipPayload.fromDictionary(payloadDict) else { #if DEBUG - print("[\(TAG)] Failed to parse incoming VoIP payload: \(payloadDict)") + print("[\(voipAppDelegateLogTag)] Failed to parse incoming VoIP payload: \(payloadDict)") #endif completion() return @@ -34,7 +34,7 @@ extension AppDelegate: PKPushRegistryDelegate { let caller = voipPayload.caller guard !voipPayload.isExpired() else { #if DEBUG - print("[\(TAG)] Skipping expired or invalid VoIP payload for callId: \(callId): \(voipPayload)") + print("[\(voipAppDelegateLogTag)] Skipping expired or invalid VoIP payload for callId: \(callId): \(voipPayload)") #endif completion() return diff --git a/ios/Libraries/VoipModule.mm b/ios/Libraries/VoipModule.mm index bb2f6953305..4824c0dc240 100644 --- a/ios/Libraries/VoipModule.mm +++ b/ios/Libraries/VoipModule.mm @@ -35,7 +35,7 @@ - (instancetype)init { } - (NSArray *)supportedEvents { - return @[@"VoipPushTokenRegistered"]; + return @[@"VoipPushTokenRegistered", @"VoipAcceptFailed"]; } - (void)startObserving { @@ -46,6 +46,11 @@ - (void)startObserving { name:@"VoipPushTokenRegistered" object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleVoipAcceptFailed:) + name:@"VoipAcceptFailed" + object:nil]; + // Send any delayed events for (NSDictionary *event in _delayedEvents) { NSString *name = event[@"name"]; @@ -67,6 +72,10 @@ - (void)handleVoipTokenRegistered:(NSNotification *)notification { [self sendEventWrapper:@"VoipPushTokenRegistered" body:notification.userInfo]; } +- (void)handleVoipAcceptFailed:(NSNotification *)notification { + [self sendEventWrapper:@"VoipAcceptFailed" body:notification.userInfo]; +} + - (void)sendEventWrapper:(NSString *)name body:(id)body { if (_hasListeners) { [self sendEventWithName:name body:body]; diff --git a/ios/Libraries/VoipPayload.swift b/ios/Libraries/VoipPayload.swift index f7769e97222..c70a0bdf571 100644 --- a/ios/Libraries/VoipPayload.swift +++ b/ios/Libraries/VoipPayload.swift @@ -67,7 +67,8 @@ private struct RemoteVoipPayload { type: payloadType, hostName: payloadHostName, avatarUrl: caller?.avatarUrl, - createdAt: payloadCreatedAt + createdAt: payloadCreatedAt, + voipAcceptFailed: false ) } } @@ -87,6 +88,7 @@ public class VoipPayload: NSObject { @objc public let hostName: String @objc public let avatarUrl: String? @objc public let createdAt: String? + @objc public let voipAcceptFailed: Bool private var createdAtDate: Date? { return Self.parseCreatedAt(createdAt) @@ -124,7 +126,18 @@ public class VoipPayload: NSObject { return Int(hash) } - init(callId: String, callUUID: UUID, caller: String, username: String, host: String, type: String, hostName: String, avatarUrl: String?, createdAt: String?) { + init( + callId: String, + callUUID: UUID, + caller: String, + username: String, + host: String, + type: String, + hostName: String, + avatarUrl: String?, + createdAt: String?, + voipAcceptFailed: Bool = false + ) { self.callId = callId self.callUUID = callUUID self.caller = caller @@ -134,6 +147,7 @@ public class VoipPayload: NSObject { self.hostName = hostName self.avatarUrl = avatarUrl self.createdAt = createdAt + self.voipAcceptFailed = voipAcceptFailed super.init() } @@ -144,7 +158,7 @@ public class VoipPayload: NSObject { @objc public func toDictionary() -> [String: Any] { - return [ + var dict: [String: Any] = [ "callId": callId, "caller": caller, "username": username, @@ -155,6 +169,10 @@ public class VoipPayload: NSObject { "createdAt": createdAt ?? NSNull(), "notificationId": notificationId ] + if voipAcceptFailed { + dict["voipAcceptFailed"] = true + } + return dict } public func remainingLifetime(now: Date = Date()) -> TimeInterval? { diff --git a/ios/Libraries/VoipService.swift b/ios/Libraries/VoipService.swift index dbfcaf07be2..79506da4b02 100644 --- a/ios/Libraries/VoipService.swift +++ b/ios/Libraries/VoipService.swift @@ -221,10 +221,15 @@ public final class VoipService: NSObject { incomingCallTimeouts.removeValue(forKey: callId)?.cancel() } + private static func clearNativeAcceptDedupe(for callId: String) { + nativeAcceptHandledCallIds.remove(callId) + } + private static func handleIncomingCallTimeout(for payload: VoipPayload) { incomingCallTimeouts.removeValue(forKey: payload.callId) clearTrackedIncomingCall(for: payload.callUUID) stopDDPClientInternal() + clearNativeAcceptDedupe(for: payload.callId) let callId = payload.callId let callUUID = payload.callUUID @@ -303,6 +308,7 @@ public final class VoipService: NSObject { return } clearTrackedIncomingCall(for: payload.callUUID) + clearNativeAcceptDedupe(for: callId) RNCallKeep.endCall(withUUID: callId, reason: 3) cancelIncomingCallTimeout(for: callId) stopDDPClientInternal() @@ -419,9 +425,46 @@ public final class VoipService: NSObject { cancelIncomingCallTimeout(for: payload.callId) let finishAccept: (Bool) -> Void = { success in - storeInitialEvents(payload) + stopDDPClientInternal() if success { - stopDDPClientInternal() + storeInitialEvents(payload) + } else { + clearNativeAcceptDedupe(for: payload.callId) + RNCallKeep.endCall(withUUID: payload.callId, reason: 6) + let failedPayload = VoipPayload( + callId: payload.callId, + callUUID: payload.callUUID, + caller: payload.caller, + username: payload.username, + host: payload.host, + type: payload.type, + hostName: payload.hostName, + avatarUrl: payload.avatarUrl, + createdAt: payload.createdAt, + voipAcceptFailed: true + ) + storeInitialEvents(failedPayload) + var acceptFailedUserInfo: [String: Any] = [ + "callId": failedPayload.callId, + "caller": failedPayload.caller, + "username": failedPayload.username, + "host": failedPayload.host, + "type": failedPayload.type, + "hostName": failedPayload.hostName, + "notificationId": failedPayload.notificationId, + "voipAcceptFailed": true + ] + if let avatarUrl = failedPayload.avatarUrl { + acceptFailedUserInfo["avatarUrl"] = avatarUrl + } + if let createdAt = failedPayload.createdAt { + acceptFailedUserInfo["createdAt"] = createdAt + } + NotificationCenter.default.post( + name: NSNotification.Name("VoipAcceptFailed"), + object: nil, + userInfo: acceptFailedUserInfo + ) } } @@ -563,6 +606,7 @@ public final class VoipService: NSObject { observedIncomingCall = nil cancelIncomingCallTimeout(for: observedCall.payload.callId) + clearNativeAcceptDedupe(for: observedCall.payload.callId) if isDdpLoggedIn { sendRejectSignal(payload: observedCall.payload) From d3776e9c34e6b22d2bdb1d2ae3b89ba5a142bc76 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 25 Mar 2026 14:38:33 -0300 Subject: [PATCH 07/11] Fix accept fail on iOS --- app/index.tsx | 6 ++-- app/lib/services/voip/MediaCallEvents.ts | 32 ++++++++----------- app/lib/services/voip/MediaSessionInstance.ts | 6 ++-- ios/Libraries/VoipModule.mm | 6 ++-- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/app/index.tsx b/app/index.tsx index 87ee0fb5c82..8ff8e9d1d1b 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -153,9 +153,9 @@ export default class Root extends React.Component<{}, IState> { return; } - // TODO: change name - const handledVoipCall = await getInitialMediaCallEvents(); - if (handledVoipCall) { + const voipInitialHandled = await getInitialMediaCallEvents(); + if (voipInitialHandled) { + // VoIP path already dispatched navigation (or will via deep linking); do not call appInit() in parallel return; } diff --git a/app/lib/services/voip/MediaCallEvents.ts b/app/lib/services/voip/MediaCallEvents.ts index f8e7da09abc..5cd54072e0d 100644 --- a/app/lib/services/voip/MediaCallEvents.ts +++ b/app/lib/services/voip/MediaCallEvents.ts @@ -55,14 +55,6 @@ export const setupMediaCallEvents = (): (() => void) => { }) ); - subscriptions.push( - Emitter.addListener(EVENT_VOIP_ACCEPT_FAILED, (data: VoipPayload & { voipAcceptFailed?: boolean }) => { - console.log(`${TAG} VoipAcceptFailed event:`, data); - dispatchVoipAcceptFailureFromNative({ ...data, voipAcceptFailed: true }); - NativeVoipModule.clearInitialEvents(); - }) - ); - subscriptions.push( RNCallKeep.addEventListener('endCall', ({ callUUID }) => { console.log(`${TAG} End call event listener:`, callUUID); @@ -98,24 +90,24 @@ export const setupMediaCallEvents = (): (() => void) => { } }) ); - - subscriptions.push( - Emitter.addListener(EVENT_VOIP_ACCEPT_FAILED, (data: VoipPayload & { voipAcceptFailed?: boolean }) => { - console.log(`${TAG} VoipAcceptFailed event:`, data); - dispatchVoipAcceptFailureFromNative({ ...data, voipAcceptFailed: true }); - NativeVoipModule.clearInitialEvents(); - }) - ); } + subscriptions.push( + Emitter.addListener(EVENT_VOIP_ACCEPT_FAILED, (data: VoipPayload & { voipAcceptFailed?: boolean }) => { + console.log(`${TAG} VoipAcceptFailed event:`, data); + dispatchVoipAcceptFailureFromNative({ ...data, voipAcceptFailed: true }); + NativeVoipModule.clearInitialEvents(); + }) + ); + return () => { subscriptions.forEach(sub => sub.remove()); }; }; /** - * Handles initial media call events. - * @returns true if the call was answered, false otherwise + * Handles initial media call events (cold start). + * @returns true if startup should skip the default `appInit()` path (answered call, or accept failure handed to deep linking) */ export const getInitialMediaCallEvents = async (): Promise => { try { @@ -130,7 +122,9 @@ export const getInitialMediaCallEvents = async (): Promise => { if (initialEvents.voipAcceptFailed && initialEvents.callId && initialEvents.host) { dispatchVoipAcceptFailureFromNative(initialEvents); RNCallKeep.clearInitialEvents(); - return false; + NativeVoipModule.clearInitialEvents(); + // Avoid racing `appInit()` with the deep-linking saga that handles the failure + return true; } if (!initialEvents.callId || !initialEvents.host || initialEvents.type !== 'incoming_call') { diff --git a/app/lib/services/voip/MediaSessionInstance.ts b/app/lib/services/voip/MediaSessionInstance.ts index 3f2ef2ce6a0..0c066d037ad 100644 --- a/app/lib/services/voip/MediaSessionInstance.ts +++ b/app/lib/services/voip/MediaSessionInstance.ts @@ -64,9 +64,11 @@ class MediaSessionInstance { console.log('🤙 [VoIP] Processed signal:', signal); - // If the call was accepted from another device, end the call + // If the call was accepted from this device, answer it if (signal.type === 'notification' && signal.notification === 'accepted' && signal.signedContractId === getUniqueIdSync()) { - this.answerCall(signal.callId); + this.answerCall(signal.callId).catch(error => { + console.error('[VoIP] Error answering call :', error); + }); } }); diff --git a/ios/Libraries/VoipModule.mm b/ios/Libraries/VoipModule.mm index 4824c0dc240..4fb3a329e19 100644 --- a/ios/Libraries/VoipModule.mm +++ b/ios/Libraries/VoipModule.mm @@ -108,12 +108,14 @@ - (void)stopNativeDDPClient { [VoipService stopDDPClient]; } +// TurboModule codegen calls these on VoipModule directly. Empty implementations replaced +// RCTEventEmitter's logic, so startObserving/stopObserving never ran and no events reached JS. - (void)addListener:(NSString *)eventName { - // Required for NativeEventEmitter - starts observing + [super addListener:eventName]; } - (void)removeListeners:(double)count { - // Required for NativeEventEmitter - stops observing + [super removeListeners:count]; } #pragma mark - TurboModule From 9e9b7f705d22f7f9c33651db303de25a71fba878 Mon Sep 17 00:00:00 2001 From: diegolmello Date: Wed, 25 Mar 2026 17:49:25 +0000 Subject: [PATCH 08/11] chore: format code and fix lint issues --- app/actions/deepLinking.ts | 1 - app/sagas/deepLinking.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/actions/deepLinking.ts b/app/actions/deepLinking.ts index 75d23cf2fe9..3ff1e22fed9 100644 --- a/app/actions/deepLinking.ts +++ b/app/actions/deepLinking.ts @@ -32,4 +32,3 @@ export function deepLinkingClickCallPush(params: any): IDeepLinkingOpen { params }; } - diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js index cd8ae76cbe7..54452e83711 100644 --- a/app/sagas/deepLinking.js +++ b/app/sagas/deepLinking.js @@ -51,7 +51,7 @@ const waitForNavigation = () => { if (Navigation.navigationRef.current) { return Promise.resolve(); } - return new Promise(resolve => { + return new Promise((resolve) => { const listener = () => { emitter.off('navigationReady', listener); resolve(); @@ -114,7 +114,7 @@ const handleVoipAcceptFailed = function* handleVoipAcceptFailed(params) { yield call( () => - new Promise(resolve => { + new Promise((resolve) => { InteractionManager.runAfterInteractions(() => resolve()); }) ); From 47dc4a5d97301283ca0261090505d7d5e29ac4a7 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 25 Mar 2026 15:09:51 -0300 Subject: [PATCH 09/11] improvements --- .../rocket/reactnative/voip/VoipNotification.kt | 14 ++++++++++++++ app/lib/services/voip/MediaCallEvents.ts | 9 +++++++++ ios/Libraries/VoipService.swift | 14 +++++++++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt index 9ae3e47d21b..f2fa81ffede 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt @@ -31,6 +31,7 @@ import chat.rocket.reactnative.MainActivity import chat.rocket.reactnative.notification.Ejson import org.json.JSONArray import org.json.JSONObject +import java.util.concurrent.atomic.AtomicBoolean /** * Handles VoIP call notifications using Android's Telecom framework via CallKeep. @@ -226,7 +227,20 @@ class VoipNotification(private val context: Context) { cancelTimeout(payload.callId) val appCtx = context.applicationContext + // Guard so finish() is called at most once, whether by the DDP callback or the timeout. + val finished = AtomicBoolean(false) + val timeoutHandler = Handler(Looper.getMainLooper()) + val timeoutRunnable = Runnable { + if (finished.compareAndSet(false, true)) { + Log.w(TAG, "Native accept timed out for ${payload.callId}; falling back to JS recovery") + finish(false) + } + } + timeoutHandler.postDelayed(timeoutRunnable, 10_000L) + fun finish(ddpSuccess: Boolean) { + if (!finished.compareAndSet(false, true)) return + timeoutHandler.removeCallbacks(timeoutRunnable) stopDDPClientInternal() if (ddpSuccess) { answerIncomingCall(payload.callId) diff --git a/app/lib/services/voip/MediaCallEvents.ts b/app/lib/services/voip/MediaCallEvents.ts index 5cd54072e0d..127cbef46e6 100644 --- a/app/lib/services/voip/MediaCallEvents.ts +++ b/app/lib/services/voip/MediaCallEvents.ts @@ -45,6 +45,7 @@ function dispatchVoipAcceptFailureFromNative(raw: VoipPayload & { voipAcceptFail export const setupMediaCallEvents = (): (() => void) => { const subscriptions: { remove: () => void }[] = []; + // iOS listens for VoIP push token registration and CallKeep events if (isIOS) { subscriptions.push( Emitter.addListener('VoipPushTokenRegistered', ({ token }: { token: string }) => { @@ -61,7 +62,13 @@ export const setupMediaCallEvents = (): (() => void) => { mediaSessionInstance.endCall(callUUID); }) ); + + // Note: there is intentionally no 'answerCall' listener here. + // VoipService.swift handles accept natively: handleObservedCallChanged detects + // hasConnected = true and calls handleNativeAccept(), which sends the DDP accept + // signal before JS runs. JS only reads the stored initialEventsData payload after the fact. } else { + // Android listens for media call events from VoipModule subscriptions.push( Emitter.addListener('VoipPushInitialEvents', async (data: VoipPayload & { voipAcceptFailed?: boolean }) => { try { @@ -135,6 +142,7 @@ export const getInitialMediaCallEvents = async (): Promise => { let wasAnswered = false; + // iOS loops through the events and checks if the call was already answered if (isIOS) { const callKeepInitialEvents = await RNCallKeep.getInitialEvents(); RNCallKeep.clearInitialEvents(); @@ -152,6 +160,7 @@ export const getInitialMediaCallEvents = async (): Promise => { } } } else { + // Android only sends answered event, so we can assume the call was answered wasAnswered = true; } diff --git a/ios/Libraries/VoipService.swift b/ios/Libraries/VoipService.swift index 79506da4b02..e0903696c36 100644 --- a/ios/Libraries/VoipService.swift +++ b/ios/Libraries/VoipService.swift @@ -42,7 +42,18 @@ public final class VoipService: NSObject { private static var isCallObserverConfigured = false private static var observedIncomingCall: ObservedIncomingCall? private static var isDdpLoggedIn = false - /// Prevents duplicate native accepts if `hasConnected` is reported more than once for the same `callId`. + /// Deduplication guard: `CXCallObserver` can call `callChanged` with `hasConnected = true` + /// multiple times for the same call (e.g. observer re-registration, system race). This set + /// ensures `handleNativeAccept` sends the DDP accept signal exactly once per callId. + /// + /// Lifecycle: + /// Added: At the start of `handleNativeAccept()`, before any DDP call. + /// Removed: After native accept DDP succeeds or fails, + /// on call timeout (`handleIncomingCallTimeout`), + /// on DDP call-end signal from another device (ddp stream listener), + /// on CallKit call-ended observer event (only before connect — `observedIncomingCall` is cleared on answer). + /// + /// Memory: One entry only while a native accept is in flight; cleared when the DDP accept finishes or other exit paths run. private static var nativeAcceptHandledCallIds = Set() private enum VoipMediaCallAnswerKind { @@ -428,6 +439,7 @@ public final class VoipService: NSObject { stopDDPClientInternal() if success { storeInitialEvents(payload) + clearNativeAcceptDedupe(for: payload.callId) } else { clearNativeAcceptDedupe(for: payload.callId) RNCallKeep.endCall(withUUID: payload.callId, reason: 6) From f9c54bae3e1ab53a296b9d9dcf1bade51ad66564 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 25 Mar 2026 15:25:57 -0300 Subject: [PATCH 10/11] Add i18n --- app/i18n/locales/ar.json | 1 + app/i18n/locales/bn-IN.json | 1 + app/i18n/locales/cs.json | 1 + app/i18n/locales/de.json | 1 + app/i18n/locales/es.json | 1 + app/i18n/locales/fi.json | 1 + app/i18n/locales/fr.json | 1 + app/i18n/locales/hi-IN.json | 1 + app/i18n/locales/hu.json | 1 + app/i18n/locales/it.json | 1 + app/i18n/locales/ja.json | 1 + app/i18n/locales/nl.json | 1 + app/i18n/locales/nn.json | 1 + app/i18n/locales/no.json | 1 + app/i18n/locales/pt-BR.json | 1 + app/i18n/locales/pt-PT.json | 1 + app/i18n/locales/ru.json | 1 + app/i18n/locales/sl-SI.json | 1 + app/i18n/locales/sv.json | 1 + app/i18n/locales/ta-IN.json | 1 + app/i18n/locales/te-IN.json | 1 + app/i18n/locales/tr.json | 1 + app/i18n/locales/zh-CN.json | 1 + app/i18n/locales/zh-TW.json | 1 + 24 files changed, 24 insertions(+) diff --git a/app/i18n/locales/ar.json b/app/i18n/locales/ar.json index 21e8858388d..05ea6661682 100644 --- a/app/i18n/locales/ar.json +++ b/app/i18n/locales/ar.json @@ -621,6 +621,7 @@ "Version_no": "إصدار التطبيق: {{version}}", "View_Original": "عرض المحتوى الأصلي", "View_Thread": "عرض الموضوع", + "VoIP_Call_Issue": "حدثت مشكلة في المكالمة، حاول مرة أخرى لاحقًا.", "Wait_activation_warning": "يحب تفعيل حسابك من المشرف قبل تسجيل الدخول", "Waiting_for_network": "بانتظار توفر شبكة...", "Websocket_disabled": "تم تعطيل Websocket لهذا الخادم.\n{{contact}}", diff --git a/app/i18n/locales/bn-IN.json b/app/i18n/locales/bn-IN.json index 410bf5ee2a7..fe56fb6199d 100644 --- a/app/i18n/locales/bn-IN.json +++ b/app/i18n/locales/bn-IN.json @@ -874,6 +874,7 @@ "video-conf-provider-not-configured-header": "কনফারেন্স কল সক্ষম হয়নি", "View_Original": "মৌলিক দেখুন", "View_Thread": "থ্রেড দেখুন", + "VoIP_Call_Issue": "কলে একটি সমস্যা হয়েছে, পরে আবার চেষ্টা করুন।", "Wait_activation_warning": "আপনি লগ ইন করতে প্রারম্ভ করতে, আপনার অ্যাকাউন্টটি প্রশাসক দ্বারা ম্যানুয়ালি অ্যাক্টিভেট হতে হবে।", "Waiting_for_answer": "উত্তরের জন্য অপেক্ষা করছি", "Waiting_for_network": "নেটওয়ার্কের জন্য অপেক্ষা করছে...", diff --git a/app/i18n/locales/cs.json b/app/i18n/locales/cs.json index 6718cc41ef5..e6a450ac36e 100644 --- a/app/i18n/locales/cs.json +++ b/app/i18n/locales/cs.json @@ -945,6 +945,7 @@ "video-conf-provider-not-configured-header": "Konferenční hovor není povolen", "View_Original": "Zobrazit originál", "View_Thread": "Zobrazit vlákno", + "VoIP_Call_Issue": "Došlo k problému s hovorem, zkuste to znovu později.", "Wait_activation_warning": "Než se budete moci přihlásit, váš účet musí být ručně aktivován administrátorem.", "Waiting_for_answer": "Čekání na odpověď", "Waiting_for_network": "Čekání na síť...", diff --git a/app/i18n/locales/de.json b/app/i18n/locales/de.json index 4c2ba85c2ef..6f5fcb3cf2a 100644 --- a/app/i18n/locales/de.json +++ b/app/i18n/locales/de.json @@ -867,6 +867,7 @@ "video-conf-provider-not-configured-header": "Telefonkonferenz nicht aktiviert", "View_Original": "Original anzeigen", "View_Thread": "Thread anzeigen", + "VoIP_Call_Issue": "Bei dem Anruf ist ein Problem aufgetreten. Bitte versuchen Sie es später erneut.", "Wait_activation_warning": "Bevor Sie sich anmelden können, muss Ihr Konto durch einen Administrator freigeschaltet werden.", "Waiting_for_answer": "Warten auf Antwort", "Waiting_for_network": "Warte auf das Netzwerk …", diff --git a/app/i18n/locales/es.json b/app/i18n/locales/es.json index 72e0d6b62ff..bc9252ed24b 100644 --- a/app/i18n/locales/es.json +++ b/app/i18n/locales/es.json @@ -460,6 +460,7 @@ "Version_no": "Versión de la aplicación: {{version}}", "View_Original": "Ver original", "View_Thread": "Ver hilo", + "VoIP_Call_Issue": "Hubo un problema con la llamada, inténtelo de nuevo más tarde.", "Websocket_disabled": "Websocket está deshabilitado para este servidor.\n{{contact}}", "Whats_the_password_for_your_certificate": "¿Cuál es la contraseña de tu certificado?", "Without_Servers": "Sin servidores", diff --git a/app/i18n/locales/fi.json b/app/i18n/locales/fi.json index 377f38d5c34..74766801202 100644 --- a/app/i18n/locales/fi.json +++ b/app/i18n/locales/fi.json @@ -839,6 +839,7 @@ "Version_no": "Sovelluksen versio: {{version}}", "View_Original": "Näytä alkuperäinen", "View_Thread": "Katsele säiettä", + "VoIP_Call_Issue": "Puhelussa ilmeni ongelma, yritä myöhemmin uudelleen.", "Wait_activation_warning": "Ennen kuin voit kirjautua, järjestelmänvalvojan on aktivoitava tilisi manuaalisesti.", "Waiting_for_answer": "Odotetaan vastausta", "Waiting_for_network": "Odotetaan verkkoa...", diff --git a/app/i18n/locales/fr.json b/app/i18n/locales/fr.json index eb65e6e5ab8..b0be369a630 100644 --- a/app/i18n/locales/fr.json +++ b/app/i18n/locales/fr.json @@ -761,6 +761,7 @@ "Version_no": "Version de l'application : {{version}}", "View_Original": "Voir l'original", "View_Thread": "Afficher le fil", + "VoIP_Call_Issue": "Un problème est survenu avec l'appel, veuillez réessayer plus tard.", "Wait_activation_warning": "Avant de pouvoir vous connecter, votre compte doit être activé manuellement par un administrateur.", "Waiting_for_network": "En attente du réseau...", "Websocket_disabled": "Le Websocket est désactivé pour ce serveur.\n{{contact}}", diff --git a/app/i18n/locales/hi-IN.json b/app/i18n/locales/hi-IN.json index 78bb09a910e..110f1725d9a 100644 --- a/app/i18n/locales/hi-IN.json +++ b/app/i18n/locales/hi-IN.json @@ -874,6 +874,7 @@ "video-conf-provider-not-configured-header": "कॉन्फ़्रेंस कॉल सक्षम नहीं है", "View_Original": "मूल देखें", "View_Thread": "थ्रेड देखें", + "VoIP_Call_Issue": "कॉल में समस्या आई, कृपया बाद में पुनः प्रयास करें।", "Wait_activation_warning": "आप लॉगिन करने से पहले, आपका खाता प्रशासक द्वारा मैन्युअल रूप से सक्रिय किया जाना चाहिए।", "Waiting_for_answer": "उत्तर का इंतजार", "Waiting_for_network": "नेटवर्क के लिए प्रतीक्षा कर रहा है...", diff --git a/app/i18n/locales/hu.json b/app/i18n/locales/hu.json index 6839e2c30ba..e43c6178dad 100644 --- a/app/i18n/locales/hu.json +++ b/app/i18n/locales/hu.json @@ -876,6 +876,7 @@ "video-conf-provider-not-configured-header": "Konferenciahívás nem engedélyezett", "View_Original": "Eredeti megtekintése", "View_Thread": "Szál megtekintése", + "VoIP_Call_Issue": "Probléma történt a hívással, próbálja újra később.", "Wait_activation_warning": "Mielőtt bejelentkezhetne, a fiókját kézileg kell aktiválnia egy adminisztrátornak.", "Waiting_for_answer": "Várakozás válaszra", "Waiting_for_network": "Hálózatra várva...", diff --git a/app/i18n/locales/it.json b/app/i18n/locales/it.json index 4344347fc2f..0255e51c013 100644 --- a/app/i18n/locales/it.json +++ b/app/i18n/locales/it.json @@ -668,6 +668,7 @@ "Version_no": "Versione dell'app: {{version}}", "View_Original": "Mostra originale", "View_Thread": "Visualizza thread", + "VoIP_Call_Issue": "Si è verificato un problema con la chiamata, riprova più tardi.", "Wait_activation_warning": "Prima di poter accedere, il tuo account deve essere attivato manualmente da un amministratore.", "Waiting_for_network": "In attesa di connessione ...", "Websocket_disabled": "Websocket disabilitata per questo server.\n{{contact}}", diff --git a/app/i18n/locales/ja.json b/app/i18n/locales/ja.json index dda45aad4d9..50bf3af873e 100644 --- a/app/i18n/locales/ja.json +++ b/app/i18n/locales/ja.json @@ -551,6 +551,7 @@ "Version_no": "アプリバージョン: {{version}}", "View_Original": "オリジナルを見る", "View_Thread": "スレッドを表示します", + "VoIP_Call_Issue": "通話に問題が発生しました。しばらくしてからもう一度お試しください。", "Websocket_disabled": "Websocketはこのサーバーでは無効化されています。\n{{contact}}", "Whats_the_password_for_your_certificate": "証明書のパスワードはなんですか?", "Without_Servers": "サーバーを除く", diff --git a/app/i18n/locales/nl.json b/app/i18n/locales/nl.json index 522d23cc72d..69500f02958 100644 --- a/app/i18n/locales/nl.json +++ b/app/i18n/locales/nl.json @@ -761,6 +761,7 @@ "Version_no": "App-versie: {{version}}", "View_Original": "Bekijk origineel", "View_Thread": "Bekijk thread", + "VoIP_Call_Issue": "Er was een probleem met het gesprek, probeer het later opnieuw.", "Wait_activation_warning": "Voordat u kunt inloggen, moet uw account handmatig worden geactiveerd door een beheerder.", "Waiting_for_network": "Wachten op netwerk...", "Websocket_disabled": "Websocket is uitgeschakeld voor deze server.\n{{contact}}", diff --git a/app/i18n/locales/nn.json b/app/i18n/locales/nn.json index d171c7c1707..830c4bfc86c 100644 --- a/app/i18n/locales/nn.json +++ b/app/i18n/locales/nn.json @@ -414,6 +414,7 @@ "Verify": "Bekreft", "Verify_email_desc": "Vi har sendt deg en e-post for å bekrefte din registrering. Hvis du ikke mottar en epost, vennligst kom tilbake og prøv igjen.", "View_Thread": "Se tråden", + "VoIP_Call_Issue": "Det oppstod eit problem med samtalen, prøv igjen seinare.", "Wait_activation_warning": "Før du kan logge inn, må kontoen din aktiveres manuelt av en administrator.", "What_are_you_doing_right_now": "Hva gjør du akkurat nå?", "Why_do_you_want_to_report": "Hvorfor vil du rapportere?", diff --git a/app/i18n/locales/no.json b/app/i18n/locales/no.json index 2d5c014ce43..a520e093f8e 100644 --- a/app/i18n/locales/no.json +++ b/app/i18n/locales/no.json @@ -919,6 +919,7 @@ "video-conf-provider-not-configured-header": "Konferansesamtale ikke aktivert", "View_Original": "Se originalen", "View_Thread": "Se tråden", + "VoIP_Call_Issue": "Det oppstod et problem med samtalen, prøv igjen senere.", "Wait_activation_warning": "Før du kan logge på, må kontoen din aktiveres manuelt av en administrator.", "Waiting_for_answer": "Venter på svar", "Waiting_for_network": "Venter på nettverket ...", diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index cbd4dfb4eae..fd7286bad25 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -955,6 +955,7 @@ "video-conf-provider-not-configured-header": "Video conferência não ativada", "View_Original": "Visualizar original", "View_Thread": "Exibir thread", + "VoIP_Call_Issue": "Houve um problema com a chamada, tente novamente mais tarde.", "Wait_activation_warning": "Antes que você possa fazer o login, sua conta deve ser manualmente ativada por um administrador.", "Waiting_for_answer": "Esperando por resposta", "Waiting_for_network": "Aguardando rede...", diff --git a/app/i18n/locales/pt-PT.json b/app/i18n/locales/pt-PT.json index 4f71d5315db..f854037fcb0 100644 --- a/app/i18n/locales/pt-PT.json +++ b/app/i18n/locales/pt-PT.json @@ -523,6 +523,7 @@ "Username_or_email": "Nome de utilizador ou e-mail", "Username_required": "Nome de utilizador necessário", "View_Thread": "Exibir thread", + "VoIP_Call_Issue": "Ocorreu um problema com a chamada, tente novamente mais tarde.", "Whats_the_password_for_your_certificate": "Qual é a palavra-passe para o seu certificado?", "Workspace_URL": "URL do espaço de trabalho", "Workspace_URL_Example": "open.rocket.chat", diff --git a/app/i18n/locales/ru.json b/app/i18n/locales/ru.json index faded076a86..9a065cbcd3d 100644 --- a/app/i18n/locales/ru.json +++ b/app/i18n/locales/ru.json @@ -807,6 +807,7 @@ "Version_no": "Версия приложения: {{version}}", "View_Original": "Посмотреть оригинал", "View_Thread": "Посмотреть тему", + "VoIP_Call_Issue": "Возникла проблема со звонком, повторите попытку позже.", "Wait_activation_warning": "До того как вы сможете войти, ваш аккаунт должен быть вручную активирован администратором сервера.", "Waiting_for_answer": "Ожидание ответа", "Waiting_for_network": "Ожидание сети...", diff --git a/app/i18n/locales/sl-SI.json b/app/i18n/locales/sl-SI.json index 68e8aa5caf7..a1c6d1e7112 100644 --- a/app/i18n/locales/sl-SI.json +++ b/app/i18n/locales/sl-SI.json @@ -776,6 +776,7 @@ "Version_no": "Različica aplikacije: {{version}}", "View_Original": "Pogled original", "View_Thread": "Pogled nit", + "VoIP_Call_Issue": "Pri klicu je prišlo do težave, poskusite znova pozneje.", "Wait_activation_warning": "Preden se lahko prijavite, mora skrbnik ročno aktivirati vaš račun.", "Waiting_for_network": "Čakanje na omrežje ...", "Websocket_disabled": "WebSocket je onemogočen za ta strežnik.\n{{contact}}", diff --git a/app/i18n/locales/sv.json b/app/i18n/locales/sv.json index f22df03c7be..94945091575 100644 --- a/app/i18n/locales/sv.json +++ b/app/i18n/locales/sv.json @@ -837,6 +837,7 @@ "Version_no": "Appversion: {{version}}", "View_Original": "Visa original", "View_Thread": "Visa tråd", + "VoIP_Call_Issue": "Ett problem uppstod med samtalet, försök igen senare.", "Wait_activation_warning": "Innan du kan logga in måste ditt konto aktiveras manuellt av en administratör.", "Waiting_for_answer": "Väntar på svar", "Waiting_for_network": "Väntar på nätverket...", diff --git a/app/i18n/locales/ta-IN.json b/app/i18n/locales/ta-IN.json index a7c1dc4da6a..bcfeacc4834 100644 --- a/app/i18n/locales/ta-IN.json +++ b/app/i18n/locales/ta-IN.json @@ -874,6 +874,7 @@ "video-conf-provider-not-configured-header": "காந்ஃபரன்ஸ் கால் கொள்ளை இயக்கப்படவில்லை", "View_Original": "மூலத்தைக் காண்", "View_Thread": "நூலைக் காண்க", + "VoIP_Call_Issue": "அழைப்பில் சிக்கல் ஏற்பட்டது, பின்னர் மீண்டும் முயற்சிக்கவும்.", "Wait_activation_warning": "உள்நுழைவு செய்ய முன்வந்து, உங்கள் கணக்கு ஒரு நிர்வாகியால் கையாளப்பட வேண்டும்.", "Waiting_for_answer": "பதிலை காத்திருக்கின்றன", "Waiting_for_network": "பிணையத்தைக் காத்திருக்கின்றது...", diff --git a/app/i18n/locales/te-IN.json b/app/i18n/locales/te-IN.json index f209d932ed9..b3413bb9253 100644 --- a/app/i18n/locales/te-IN.json +++ b/app/i18n/locales/te-IN.json @@ -873,6 +873,7 @@ "video-conf-provider-not-configured-header": "కాన్ఫరెన్స్ కాల్ అనేకంగా లేదు", "View_Original": "అసలు చూడండి", "View_Thread": "థ్రెడ్‌ను వీక్షించండి", + "VoIP_Call_Issue": "కాల్‌లో సమస్య ఉంది, తర్వాత మళ్లీ ప్రయత్నించండి.", "Wait_activation_warning": "మీరు లాగిన్ చేయడానికి మొదటి స్థాయింలో మీ ఖాతాను ఒక అడ్మినిస్ట్రేటర్ మానవారం ప్రత్యామ్నాయం చేయాలి.", "Waiting_for_answer": "జవాబు కోసం ఎదురు ఉంది", "Waiting_for_network": "నెట్వర్క్ కోసం వేచి ఉండి...", diff --git a/app/i18n/locales/tr.json b/app/i18n/locales/tr.json index 051fc752a8c..20cd7c83df6 100644 --- a/app/i18n/locales/tr.json +++ b/app/i18n/locales/tr.json @@ -651,6 +651,7 @@ "Version_no": "Uygulama sürümü: {{version}}", "View_Original": "Orijinali Görüntüle", "View_Thread": "İpliği görüntüleyin", + "VoIP_Call_Issue": "Görüşmede bir sorun oluştu, daha sonra tekrar deneyin.", "Wait_activation_warning": "Giriş yapmadan önce, hesabınız bir yönetici tarafından manuel olarak etkinleştirilmelidir.", "Waiting_for_network": "Ağ bağlantısı bekleniyor ...", "Websocket_disabled": "Bu sunucu için Websocket devre dışı bırakıldı.\n{{contact}}", diff --git a/app/i18n/locales/zh-CN.json b/app/i18n/locales/zh-CN.json index 95d7ee7684c..1141065daa5 100644 --- a/app/i18n/locales/zh-CN.json +++ b/app/i18n/locales/zh-CN.json @@ -612,6 +612,7 @@ "Version_no": "应用版本: {{version}}", "View_Original": "检视原文", "View_Thread": "查看线程", + "VoIP_Call_Issue": "通话出现问题,请稍后再试。", "Wait_activation_warning": "您的帐号必须由管理员手动启用后才能登入。", "Waiting_for_network": "等待网路连接", "Websocket_disabled": "Websocket 已于此伺服器上禁用。 \\n{{contact}}", diff --git a/app/i18n/locales/zh-TW.json b/app/i18n/locales/zh-TW.json index b66d5dee1c8..6bd89bdc169 100644 --- a/app/i18n/locales/zh-TW.json +++ b/app/i18n/locales/zh-TW.json @@ -641,6 +641,7 @@ "Version_no": "應用程式版本: {{version}}", "View_Original": "檢視原文", "View_Thread": "查看線程", + "VoIP_Call_Issue": "通話發生問題,請稍後再試。", "Wait_activation_warning": "您的帳號必須由管理員手動啟用後才能登入。", "Waiting_for_network": "等待網路連線", "Websocket_disabled": "Websocket 已於此伺服器上禁用。\\n{{contact}}", From e1b547810d8e137231785341b3f69591fd1c7412 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 25 Mar 2026 16:03:48 -0300 Subject: [PATCH 11/11] Minor fixes --- android/app/src/main/AndroidManifest.xml | 5 ----- .../notification/RCFirebaseMessagingService.kt | 18 ------------------ app/lib/services/voip/useCallStore.ts | 16 ++++++++++++++++ 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ca983f7a6e1..ab1f838aa9f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -133,11 +133,6 @@ android:enabled="true" android:exported="false" /> - - void; setCall: (call: IClientMediaCall) => void; + _cleanupCallListeners: () => void; toggleMute: () => void; toggleHold: () => void; toggleSpeaker: () => void; @@ -40,6 +41,8 @@ interface CallStoreActions { export type CallStore = CallStoreState & CallStoreActions; +let callListenersCleanup: (() => void) | null = null; + const initialState: CallStoreState = { call: null, callId: null, @@ -62,7 +65,13 @@ export const useCallStore = create((set, get) => ({ set({ callId }); }, + _cleanupCallListeners: () => { + callListenersCleanup?.(); + callListenersCleanup = null; + }, + setCall: (call: IClientMediaCall) => { + get()._cleanupCallListeners(); // Update state with call info set({ call, @@ -121,6 +130,12 @@ export const useCallStore = create((set, get) => ({ call.emitter.on('stateChange', handleStateChange); call.emitter.on('trackStateChange', handleTrackStateChange); call.emitter.on('ended', handleEnded); + + callListenersCleanup = () => { + call.emitter.off('stateChange', handleStateChange); + call.emitter.off('trackStateChange', handleTrackStateChange); + call.emitter.off('ended', handleEnded); + }; }, toggleMute: () => { @@ -189,6 +204,7 @@ export const useCallStore = create((set, get) => ({ }, reset: () => { + get()._cleanupCallListeners(); try { InCallManager.stop(); } catch (error) {