feat(voip): migrate iOS accept/reject from DDP to REST#7124
Conversation
Accept/reject now go through REST (MediaCallsAnswerRequest) in handleNativeAccept and reject(). The DDP-based buildMediaCallAnswerParams and its VoipMediaCallAnswerKind enum are no longer referenced anywhere.
PRDProblem StatementThe native iOS VoIP implementation opens a per-call WebSocket (DDP) connection solely to send two signals: accept and reject. This requires a full DDP client with connect/login/subscribe/callback-queue logic that is significantly more complex than the actual business logic. The same accept/reject signal can be sent via a single HTTP POST using the server's existing The DDP complexity also makes the code hard to reason about, test, and maintain — particularly the SolutionReplace the DDP-based accept/reject signaling from iOS native ( The DDP listener for call-end detection (hangup from remote, accepted-on-another-device) is not migrated — it requires a persistent WebSocket subscription and is a separate problem. User Stories
ModulesNew:
Modify:
Delete: AuthThe Payload shape{
"callId": "...",
"contractId": "DeviceUID",
"answer": "accept|reject",
"supportedFeatures": ["audio"] // only on accept
}ArchitectureOut of Scope
|
Implementation ProgressSlice 1 —
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughMedia call accept/reject signaling is migrated from DDP-based to REST API-based communication. The Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
ios/Shared/RocketChat/API/MediaCallsAnswerRequest.swift (1)
28-38: Silent failure inbody()could mask issues.Using
try?means ifJSONSerialization.data(withJSONObject:)fails, the request body is silentlynil. While failure is unlikely for this simple dictionary, consider logging or handling the error for debuggability during development.That said, for these basic types (
String,[String]?), serialization failure is practically impossible, so this is acceptable as-is.💡 Optional: Add debug logging for serialization failure
func body() -> Data? { var dict: [String: Any] = [ "callId": callId, "contractId": contractId, "answer": answer ] if let features = supportedFeatures { dict["supportedFeatures"] = features } - return try? JSONSerialization.data(withJSONObject: dict) + do { + return try JSONSerialization.data(withJSONObject: dict) + } catch { + `#if` DEBUG + print("[MediaCallsAnswerRequest] JSON serialization failed: \(error)") + `#endif` + return nil + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ios/Shared/RocketChat/API/MediaCallsAnswerRequest.swift` around lines 28 - 38, The body() method currently swallows JSONSerialization errors with try?, which can hide rare failures; update MediaCallsAnswerRequest.body() to use do-catch around JSONSerialization.data(withJSONObject:) (or propagate the thrown error) and in the catch log the error via the existing logging facility (or return nil only after logging) so serialization failures are visible during debugging while preserving the same return semantics for production.ios/Libraries/VoipService.swift (1)
519-521: Consider logging rejected call failures for observability.The completion handler discards the result entirely. While reject doesn't need to affect UI, logging failures would help diagnose issues where callers see "missed call" notifications despite the callee having rejected.
📊 Optional: Add debug logging
)) { _ in + `#if` DEBUG + print("[\(TAG)] Reject request completed for call \(payload.callId)") + `#endif` self.stopDDPClientInternal(callId: payload.callId) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ios/Libraries/VoipService.swift` around lines 519 - 521, The completion handler that currently just calls self.stopDDPClientInternal(callId: payload.callId) is dropping the reject result; update that closure to inspect the reject call result and log failures (including payload.callId and error details) for observability—use the app's existing logger (or NSLog/os_log if none) to emit a debug/error message when the reject operation fails before calling stopDDPClientInternal(callId:).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ios/Libraries/VoipService.swift`:
- Around line 471-485: API(server: payload.host) can be nil and currently the
fetch is skipped leaving finishAccept uncalled; update the call site to unwrap
the API(server: payload.host) result (or guard-let it) before calling fetch, and
if it is nil immediately call finishAccept(false) (optionally log the invalid
host and ensure the finishAccept call is dispatched to the main queue like the
existing completion path). Specifically, handle the nil case around the
API(server:) invocation used with fetch and MediaCallsAnswerRequest so every
execution path invokes finishAccept.
- Around line 513-522: The reject function doesn't handle API(server:) == nil so
no reject is sent and stopDDPClientInternal(callId:) may never be called; update
reject to handle the nil case by ensuring stopDDPClientInternal(callId:
payload.callId) is always invoked and, when API(server:) returns non-nil, still
send the MediaCallsAnswerRequest(callId:contractId:answer:supportedFeatures:) as
before; reference the reject(payload: VoipPayload) method, the API(server:)
initializer, MediaCallsAnswerRequest, and stopDDPClientInternal to locate and
implement the nil-guard and guaranteed cleanup.
---
Nitpick comments:
In `@ios/Libraries/VoipService.swift`:
- Around line 519-521: The completion handler that currently just calls
self.stopDDPClientInternal(callId: payload.callId) is dropping the reject
result; update that closure to inspect the reject call result and log failures
(including payload.callId and error details) for observability—use the app's
existing logger (or NSLog/os_log if none) to emit a debug/error message when the
reject operation fails before calling stopDDPClientInternal(callId:).
In `@ios/Shared/RocketChat/API/MediaCallsAnswerRequest.swift`:
- Around line 28-38: The body() method currently swallows JSONSerialization
errors with try?, which can hide rare failures; update
MediaCallsAnswerRequest.body() to use do-catch around
JSONSerialization.data(withJSONObject:) (or propagate the thrown error) and in
the catch log the error via the existing logging facility (or return nil only
after logging) so serialization failures are visible during debugging while
preserving the same return semantics for production.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3f7f3778-f428-4684-aed7-c09ecccdecd8
📒 Files selected for processing (2)
ios/Libraries/VoipService.swiftios/Shared/RocketChat/API/MediaCallsAnswerRequest.swift
📜 Review details
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/lib/services/voip/**/*.{ts,tsx} : Implement VoIP with WebRTC peer-to-peer audio calls in app/lib/services/voip/ using Zustand stores instead of Redux, with native CallKit (iOS) and Telecom (Android) integration; keep VoIP and VideoConf separate
📚 Learning: 2026-04-07T17:49:17.538Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/lib/services/voip/**/*.{ts,tsx} : Implement VoIP with WebRTC peer-to-peer audio calls in app/lib/services/voip/ using Zustand stores instead of Redux, with native CallKit (iOS) and Telecom (Android) integration; keep VoIP and VideoConf separate
Applied to files:
ios/Libraries/VoipService.swift
📚 Learning: 2026-03-05T06:06:19.755Z
Learnt from: divyanshu-patil
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6957
File: ios/RCTWatchModule.mm:19-24
Timestamp: 2026-03-05T06:06:19.755Z
Learning: In the Rocket.Chat React Native iOS app, `WCSession` (WatchConnectivity) is activated with its delegate in `ios/RocketChat Watch App/Loaders/WatchSession.swift`, not in `RCTWatchModule.mm`. Since `WCSession.defaultSession` is a singleton, activating it once in `WatchSession.swift` is sufficient; `RCTWatchModule.mm` does not need to re-activate or re-set the delegate.
Applied to files:
ios/Libraries/VoipService.swift
🔇 Additional comments (3)
ios/Shared/RocketChat/API/MediaCallsAnswerRequest.swift (1)
10-43: LGTM! Clean REST request/response implementation.The structure correctly implements the
Requestprotocol with proper HTTP method, path, and body serialization. The optionalsupportedFeatureshandling is appropriately conditional.ios/Libraries/VoipService.swift (2)
4-4: LGTM!Import added correctly for accessing
MediaCallsAnswerRequestfrom the RocketChat module.
505-507: LGTM! Clean migration to REST-based reject.The reject paths in both
rejectBusyCallandhandleObservedCallChangednow correctly route through the newreject(payload:)method, which uses the REST endpoint instead of DDP.Also applies to: 587-587
Handle the case when API(server:) returns nil by: - accept: call finishAccept(false) on the main queue with a DEBUG log - reject: call stopDDPClientInternal to clean up the DDP client with a DEBUG log Without this, a nil API silently skipped the REST call, leaving CallKit in an indeterminate state (accept) or DDP client leaking (reject).
When the user ended an incoming call before it connected, handleObservedCallChanged sent the REST reject but never notified CallKit to remove the call from the system Recents UI. Also deduplicate MediaCallsAnswerResponse with MessageResponse.
When the user ended an incoming call before it connected, handleObservedCallChanged sent the REST reject but never notified CallKit to remove the call from the system Recents UI. Also deduplicate MediaCallsAnswerResponse with MessageResponse, and remove unnecessary import RocketChat that was causing module resolution failures in the Libraries target.
…ReactNative into refactor.ddp-ios
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ios/RocketChatRN.xcodeproj/project.pbxproj`:
- Line 370: MediaCallsAnswerRequest.swift currently has only one PBXBuildFile
entry so it's only compiled into the RocketChatRN target; add a second
PBXBuildFile entry (with a new unique PBX identifier) referencing the same
fileRef (MediaCallsAnswerRequest.swift) and include that new PBXBuildFile in the
Rocket.Chat target's Sources list to mirror the dual-target pattern used by
other shared Swift files (e.g., VoipService.swift), ensuring both RocketChatRN
and Rocket.Chat build the file.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f5e0215b-90b1-4f66-8b91-8ed591bfc8e6
⛔ Files ignored due to path filters (1)
ios/Podfile.lockis excluded by!**/*.lock
📒 Files selected for processing (3)
ios/Libraries/VoipService.swiftios/RocketChatRN.xcodeproj/project.pbxprojios/Shared/RocketChat/API/Requests/MediaCallsAnswerRequest.swift
🚧 Files skipped from review as they are similar to previous changes (1)
- ios/Libraries/VoipService.swift
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: format
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/lib/services/voip/**/*.{ts,tsx} : Implement VoIP with WebRTC peer-to-peer audio calls in app/lib/services/voip/ using Zustand stores instead of Redux, with native CallKit (iOS) and Telecom (Android) integration; keep VoIP and VideoConf separate
📚 Learning: 2026-04-07T17:49:17.538Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/lib/services/voip/**/*.{ts,tsx} : Implement VoIP with WebRTC peer-to-peer audio calls in app/lib/services/voip/ using Zustand stores instead of Redux, with native CallKit (iOS) and Telecom (Android) integration; keep VoIP and VideoConf separate
Applied to files:
ios/RocketChatRN.xcodeproj/project.pbxproj
📚 Learning: 2026-03-05T06:06:19.755Z
Learnt from: divyanshu-patil
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6957
File: ios/RCTWatchModule.mm:19-24
Timestamp: 2026-03-05T06:06:19.755Z
Learning: In the Rocket.Chat React Native iOS app, `WCSession` (WatchConnectivity) is activated with its delegate in `ios/RocketChat Watch App/Loaders/WatchSession.swift`, not in `RCTWatchModule.mm`. Since `WCSession.defaultSession` is a singleton, activating it once in `WatchSession.swift` is sufficient; `RCTWatchModule.mm` does not need to re-activate or re-set the delegate.
Applied to files:
ios/RocketChatRN.xcodeproj/project.pbxproj
🔇 Additional comments (1)
ios/Shared/RocketChat/API/Requests/MediaCallsAnswerRequest.swift (1)
10-41: Nice extraction of the answer REST payload.Keeping this request isolated here makes the accept/reject migration much easier to follow, and the conditional
supportedFeaturesserialization matches the two call paths cleanly.
…RocketChat target Deleted the duplicate copy at Shared/RocketChat/API/ and kept the one at Shared/RocketChat/API/Requests/ to match SendMessage.swift layout. Added PBXBuildFile entry for the RocketChat target so VoipService.swift can reference MediaCallsAnswerRequest when compiled into that target.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
ios/RocketChatRN.xcodeproj/project.pbxproj (1)
1144-1151: Remove the orphanedDeleted Rogue API Group.This adds a second top-level
RocketChatgroup that only contains an empty placeholder group. It won’t affect the build, but it will make the navigator misleading.Cleanup diff
- 543368E8E0F7A6A03AD7C8F9 /* RocketChat */ = { - isa = PBXGroup; - children = ( - 9327C58E3506F20CD4FCE7E0 /* API */, - ); - name = RocketChat; - sourceTree = "<group>"; - }; ... - 543368E8E0F7A6A03AD7C8F9 /* RocketChat */, ... - 9327C58E3506F20CD4FCE7E0 /* API */ = { - isa = PBXGroup; - children = ( - ); - name = "Deleted Rogue API Group"; - sourceTree = "<group>"; - };Also applies to: 1214-1214, 1233-1239
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ios/RocketChatRN.xcodeproj/project.pbxproj` around lines 1144 - 1151, Remove the orphaned duplicate top-level PBXGroup block (the PBXGroup with GUID 543368E8E0F7A6A03AD7C8F9 named "RocketChat" that only contains the child GUID 9327C58E3506F20CD4FCE7E0 /* API */) and any referenced empty child group entries (e.g., 9327C58E3506F20CD4FCE7E0) from the project.pbxproj; also remove any references to these GUIDs from other PBXGroup children arrays so only the real RocketChat group remains (check the other duplicate blocks mentioned in the review and delete the redundant PBXGroup and its child placeholders).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@ios/RocketChatRN.xcodeproj/project.pbxproj`:
- Around line 1144-1151: Remove the orphaned duplicate top-level PBXGroup block
(the PBXGroup with GUID 543368E8E0F7A6A03AD7C8F9 named "RocketChat" that only
contains the child GUID 9327C58E3506F20CD4FCE7E0 /* API */) and any referenced
empty child group entries (e.g., 9327C58E3506F20CD4FCE7E0) from the
project.pbxproj; also remove any references to these GUIDs from other PBXGroup
children arrays so only the real RocketChat group remains (check the other
duplicate blocks mentioned in the review and delete the redundant PBXGroup and
its child placeholders).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 186b3c92-f02d-441c-8b04-957d42980136
📒 Files selected for processing (1)
ios/RocketChatRN.xcodeproj/project.pbxproj
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: format
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/lib/services/voip/**/*.{ts,tsx} : Implement VoIP with WebRTC peer-to-peer audio calls in app/lib/services/voip/ using Zustand stores instead of Redux, with native CallKit (iOS) and Telecom (Android) integration; keep VoIP and VideoConf separate
📚 Learning: 2026-04-07T17:49:17.538Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/lib/services/voip/**/*.{ts,tsx} : Implement VoIP with WebRTC peer-to-peer audio calls in app/lib/services/voip/ using Zustand stores instead of Redux, with native CallKit (iOS) and Telecom (Android) integration; keep VoIP and VideoConf separate
Applied to files:
ios/RocketChatRN.xcodeproj/project.pbxproj
🔇 Additional comments (1)
ios/RocketChatRN.xcodeproj/project.pbxproj (1)
370-372:MediaCallsAnswerRequest.swiftis wired into the shared native targets correctly.The file reference, group placement, and
Sourcesmemberships line up cleanly forRocketChatRN,Rocket.Chat, andNotificationService.Also applies to: 660-660, 893-893, 2157-2157, 2380-2380, 2439-2439
Conflict in ios/RocketChatRN.xcodeproj/project.pbxproj: both branches modified Pods-related entries with different UUIDs (different pod install runs). Resolution: take feat.voip-lib-new as base, inject MediaCallsAnswerRequest entries from refactor.ddp-ios, then re-run pod install to regenerate consistent UUIDs. - Added MediaCallsAnswerRequest.swift PBXBuildFile entries for RocketChatRN, NotificationService, and Rocket.Chat targets - Added PBXFileReference for MediaCallsAnswerRequest.swift - Added PBXGroup children entry for the file - Added PBXSourcesBuildPhase entries for all three targets
8e92a7a to
92045ec
Compare
|
iOS Build Available Rocket.Chat Experimental 4.72.0.108560 |
…ST state signals - Added `normalizeVoipDeepLinkHost` function to standardize incoming host URLs. - Implemented `isVoipIncomingHostCurrentWorkspace` to check if the incoming host matches the active workspace. - Updated `handleVoipAcceptSucceededFromNative` and `getInitialMediaCallEvents` to utilize the new host normalization and state synchronization logic. - Introduced `syncStateSignalsAfterNativeVoipAccept` to replay state signals when a native accept occurs. - Enhanced tests in `MediaSessionInstance.test.ts` to cover new functionality and ensure proper initialization and signal handling.
- Introduced `normalizeDeepLinkingServerHost` to standardize server URLs for deep linking. - Updated `MediaCallEvents` and `deepLinking.js` to utilize the new normalization function. - Added unit tests for `normalizeDeepLinkingServerHost` to ensure expected behavior.
- Inline syncStateSignalsAfterNativeVoipAccept wrapper; expose applyRestStateSignals - Drop stale `[VoIP] REST state signals` debug log - Drop redundant `void` before `.catch` at native-accept call sites - Return plain `true` instead of `Promise.resolve(true)` in async fn - Fix stale DDP comment in MediaCallEvents (now REST accept) - Loosen brittle error-string assertion in MediaSessionInstance.test
The new isVoipIncomingHostCurrentWorkspace gate reads `store.getState().server.server`. Tests now mock it to a workspace-a host so existing cross-server payloads (workspace-b) still take the deepLinkingOpen path. Adds a same-workspace case asserting the REST replay branch.
…/Decline (#7215) * merge feat.voip-lib * feat(voip): enhance call handling with UUID mapping and event listeners * Base call UI * feat(voip): integrate Zustand for call state management and enhance CallView UI * feat(voip): add simulateCall function for mock call handling in UI development * refactor(CallView): update button handlers and improve UI responsiveness * Add pause-shape-unfilled icon * Base CallHeader * toggleFocus * collapse buttons * Header components * Hide header when no call * Timer * Add use memo * Add voice call item on sidebar * cleanup * Temp use @rocket.chat/media-signaling from .tgz * cleanup * Check module and permissions to enable voip * Refactor stop method to use optional chaining for media signal listeners * voip push first test * Add VoIP call handling with pending call management - Implemented VoIP push notification handling in index.js, including storing call info for later processing. - Added CallKeep event handlers for answering and ending calls from a cold start. - Introduced a new CallIdUUID module to convert call IDs to deterministic UUIDs for compatibility with CallKit. - Created a pending call store to manage incoming calls when the app is not fully initialized. - Updated deep linking actions to include VoIP call handling. - Enhanced MediaSessionInstance to process pending calls and manage call states effectively. * Remove pending store and create getInitialEvents on app/index * Attempt to make iOS calls work from cold state * lint and format * Patch callkeep ios * Temp send iOS voip push token on gcm * Temp fix require cycle * chore: format code and fix lint issues [skip ci] * CallIDUUID module on android and voip push * Add setCallUUID on useCallStore to persist calls accepted on native Android * remove callkeep from notification * Android Incoming Call UI POC * Refactor VoIP handling: Migrate VoIP-related classes to a new package structure, removing deprecated modules and consolidating functionality. Update imports in MainApplication and NotificationIntentHandler to reflect changes. This cleanup enhances code organization and prepares for future VoIP feature enhancements. * Remove VoipForegroundService * cleanup and use caller instead of callerName * Cleanup and make iOS build again * Refactor VoIP handling: Remove unused event emissions for call answered and declined, switch from SharedPreferences to in-memory storage for pending VoIP call data, and update method signatures for better clarity. This cleanup enhances performance and prepares for future VoIP feature improvements. * Refactor VoIP handling: Introduce a new VoipPayload class to encapsulate call data, streamline notification processing, and enhance method signatures across the VoIP module. This update improves code clarity and prepares for future feature enhancements. * Migrate react-native-voip-push-notifications to VoipModule * Refactor VoIP module: Update package structure by moving VoipTurboPackage to the main package and removing the obsolete NativeVoipSpec class. Adjust imports in MainApplication and VoipModule to reflect these changes, enhancing code organization and maintainability. * Unify emitters * Move CallKeep listeners from MediaSessionInstance to getInitialEvents * Clear callkeep on endcall * Unify getInitialEvents logic * getInitialEvents -> MediaCallEvents * chore: format code and fix lint issues [skip ci] * feat(Android): Add full screen incoming call (#6977) * feat: Update call UI (#6990) * feat: Handle audio routing, e.g., Bluetooth headset vs. internal speaker switching (#6992) * fix: empty space when not on call (#6993) * feat: Dialpad (#7000) * action: organized translations * feat: start call (#7024) * chore: format code and fix lint issues * feat: Pre flight (#7038) * action: organized translations * feat: Receive voip push notifications from backend (#7045) * feat: Refactor media session handling and improve disconnect logic (#7065) * feat: Control incoming call from native (#7066) * feat: Voice message blocks (#7057) * feat: native accept success event (#7068) * feat(voip): call waiting, busy detection, and videoconf blocking (#7077) * action: organized translations * feat(voip): tap-to-hide call controls with animations (#7078) * feat(voip): navigate to call DM from message button and header (#7082) * feat(voip): tablet and landscape layout (#7110) * chore: develop into feat.voip-lib-new (RN 81 + Expo 54 + reanimated 4 + true-sheet + iOS 26) (#7114) * chore: format code and fix lint issues * feat(voip): android landscape layout for IncomingCallActivity (#7116) * Update agents files * feat(voip): Support a11y (#7106) * Fix content cutting on iOS on some edge cases * pods * Ignore .worktrees on jest * chore: Merge develop into feat.voip-lib-new (#7129) * fix(voip): show CallKit UI when call is active in background (#7128) * chore: Update media-signaling to 0.2.0 (#7153) * feat(voip): migrate iOS accept/reject from DDP to REST (#7124) * Fix icons * feat(voip): migrate Android accept/reject from DDP to REST (#7127) * test(voip): integration tests for CallView pipeline (#7161) * feat(voip): display video conf provider as subtitle (#7160) * fix(voip): CallView button grid and correct landscape/dialpad layouts (#7164) * fix(voip): prevent stale MMKV cache on Android first-install accept MMKVKeyManager.initialize ran in MainApplication.onCreate before the JS engine started and opened the default MMKV file via the Tencent 1.2 JAR when it was still empty. Tencent caches instances per-ID in a singleton registry, so that empty-state view was held for the rest of the process. JS later wrote credentials through react-native-mmkv (MMKV Core 2.0), which has its own separate registry. When a VoIP push arrived, Ejson.getMMKV() got the cached empty Tencent instance and reported "No userId found in MMKV for server". Closing and reopening the app cleared the cache, which is why only the very first call after install failed. Drop the open/verify block — the encryption key is already cached from SecureKeystore, so no MMKV handle is needed here. The first Tencent instance is now created inside Ejson.getMMKV() after JS has written, so it scans the file fresh. * fix(voip): prevent duplicate ringtone on Android incoming call (#7158) * fix(voip): set explicit snaps for NewMediaCall bottom sheet (#7165) * Update app/lib/services/voip/MediaSessionStore.ts Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> * fix: make startVoipFork reactive to permissions-changed (#7151) * fix(android): remove MediaProjectionService from merged manifest (#7190) * fix(voip): Phone account creation (#7170) * feat: add Enable Mobile Ringing toggle in user preferences (#7155) * fix(voip): ship blockers for PushKit, licensing, outbound calls, push tokens (#7167) * fix(android): Play Store mic discoverability, safer FCM logs, avatar auth via headers (#7171) * fix(ios): serialize VoipService bridge statics (#7169) * fix(voip): Android DDP thread safety and VoipPayload bundle parity (#7168) * chore(voip): dead-code and hygiene sweep (#7174) * refactor(voip): decouple navigateToCallRoom from Redux and backfill REST/connect tests (#7176) * test(voip): tighten ringing endCall assertion and add VideoConf VoIP-lock saga coverage (#7177) * fix(ios): harden VoIP DDP WebSocket client on receive failures and TLS (#7173) * refactor(voip): MediaCallEvents Redux adapters and resetVoipState (#7178) * refactor(voip): decouple peer autocomplete from Redux; simplify NewMediaCall (#7175) * fix(ios): add NS_SWIFT_NAME to Challenge.runChallenge for Swift 6.2 compatibility Swift 6.2 (Xcode 26.x / macos-26 runner) auto-renames the Objective-C method runChallenge:didReceiveChallenge:completionHandler: to run(_:didReceive:completionHandler:) when imported into Swift. Add NS_SWIFT_NAME to explicitly pin the Swift import name, preventing the compiler from applying its heuristics. This keeps the existing Swift call site in DDPClient.swift working without changes. * fix(ios): cancel old URLSession/webSocketTask before reconnecting in DDPClient.connect (#7197) * fix(ios): add NSLock to nativeAcceptHandledCallIds and 10s REST timeout to handleNativeAccept (#7198) * feat(android): create VoipCallService with FOREGROUND_SERVICE_MICROPHONE (#7199) * fix(android): start VoipCallService on accept, stop on hangup/timeout, install end-call listener (#7200) * fix(voip): enable DM nav for users with SIP extension (#7203) * fix(android): handle null VoiceConnection in answerIncomingCall, notify JS (#7201) * fix(voip): resolve closure capture ordering in handleNativeAccept (#7209) * fix(android): integrate VoIP modules with SSL-pinned OkHttpClient (#7208) * fix(push): gate id and voipToken behind server version checks, fix VideoConf caller extra (#7210) * fix(voip): remove sensitive data from production logs (#7207) * fix(android): remove isRunning guard + add double-tap guard on Accept/Decline - VoipCallService: remove if (!isRunning) guard, call startForeground unconditionally (idempotent on Android, fixes Android 14+ foreground service requirement) - IncomingCallActivity: add AtomicBoolean guard on handleAccept/handleDecline to prevent double-tap from triggering multiple service starts --------- Co-authored-by: diegolmello <diegolmello@users.noreply.github.com> Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com>
Proposed changes
Migrate VoIP
accept()andreject()on iOS from DDP (WebSocket) signaling to REST calls against the existingPOST /api/v1/media-calls.answerendpoint. This eliminates the per-call WebSocket lifecycle (connect → DDP connect → login → subscribe → send) from the accept/reject path, significantly simplifying the native iOS VoIP layer.What changed (iOS REST migration):
MediaCallsAnswerRequestAPI request struct (ios/Shared/RocketChat/API/MediaCallsAnswerRequest.swift)VoipService.accept(): replaced DDPclient.callMethod/queueMethodCallwithAPI.fetch(request:)VoipService.reject(): replaced DDP with REST (renamed fromsendRejectSignal)buildMediaCallAnswerParams()andVoipMediaCallAnswerKindenumDDPClient/VoipPerCallDdpRegistrykept — still used bystartListeningForCallEnd(call-end DDP listener)Follow-up commits on this PR:
normalizeDeepLinkingServerHosthelper (with unit tests) to dedupe scheme/slash handling betweenhandleOpenandhandleClickCallPushinapp/sagas/deepLinking.js.applyRestStateSignalsREST replay inMediaSessionInstanceto recovermedia-calls.stateSignalswhen native CallKit accept races ahead of the JSnativeAcceptedCallId.isVoipIncomingHostCurrentWorkspacehost gate inMediaCallEventsso REST replay only runs when the incoming VoIP host matches the active workspace; otherwise falls back todeepLinkingOpen.MediaSessionInstance.test.ts.MediaCallEventsupdated to reference the new REST accept path.What did NOT change:
startListeningForCallEndDDP subscription (separate problem — persistent WebSocket needed for real-time call-end events)Issue(s)
https://rocketchat.atlassian.net/browse/VMUX-67
How to test or reproduce
POST /api/v1/media-calls.answeris called withanswer: "accept"POST /api/v1/media-calls.answeris called withanswer: "reject"stream-notify-user)media-calls.stateSignalsand bind the call without re-prompting.deepLinkingOpento switch workspace before answering.Screenshots
Types of changes
Checklist
Further comments
Summary by CodeRabbit