Add experimental native calls behind a developer flag (#6159)

Add the element-call-ios dependency and a developer option for native calls

The package carries the call UI and the matrix-rust-rtc media stack, pinned by
exact version the way the SDK is, with the local path kept commented out for
working against a checkout. Its media xcframework has no Intel simulator slice
and a package cannot say so for the projects that consume it, hence the
excluded architecture at the project level.

The developer option is off by default and nothing reads it yet. It is placed
next to the Element Call URL override, which only applies to the web view that
native calls replace.
This commit is contained in:
Valere Fedronic
2026-09-17 18:42:18 +02:00
committed by GitHub
parent fe18472556
commit fb49f5b83b
33 changed files with 1778 additions and 160 deletions
@@ -9,6 +9,7 @@
import AVKit
import Combine
import Compound
import ElementCall
import SwiftState
import SwiftUI
@@ -227,7 +228,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
}
}
// swiftlint:disable:next function_body_length
private func setupObservers() {
chatsTabFlowCoordinator.actionsPublisher
.sink { [weak self] action in
@@ -316,17 +316,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
}
.store(in: &cancellables)
flowParameters.elementCallService.actions
.receive(on: DispatchQueue.main)
.sink { [weak self] action in
switch action {
case .endCall:
self?.dismissCallScreenIfNeeded()
default:
break
}
}
.store(in: &cancellables)
setupCallObservers()
searchScreenCoordinator?.actionsPublisher
.sink { [weak self] action in
@@ -458,6 +448,10 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
private func presentCallScreen(roomID: String, isVoiceCall: Bool) async {
guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else {
// An answered call is left up for the native stack, so it has to be ended here rather
// than leaving the system with a call this room can no longer serve.
MXLog.error("Cannot present the call screen, \(roomID) isn't a joined room")
flowParameters.elementCallService.tearDownCallSession(roomID: roomID)
return
}
@@ -477,6 +471,12 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
private var callScreenPictureInPictureController: AVPictureInPictureController?
private func presentCallScreen(configuration: ElementCallConfiguration) {
// The service runs native calls and drives their screen through its own actions.
if flowParameters.elementCallService.handleNativeCallRequest(roomProxy: configuration.roomProxy,
isVoiceCall: configuration.voiceOnly) {
return
}
guard flowParameters.ongoingCallRoomIDPublisher.value != configuration.callRoomID else {
MXLog.info("Returning to existing call.")
callScreenPictureInPictureController?.stopPictureInPicture()
@@ -514,6 +514,11 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
}
private func hideCallScreenOverlay() {
if navigationTabCoordinator.overlayCoordinator is NativeCallScreenCoordinator {
flowParameters.elementCallService.minimizeNativeCall()
return
}
guard let callScreenPictureInPictureController else {
MXLog.warning("Picture in picture isn't available, dismissing the call screen.")
dismissCallScreenIfNeeded()
@@ -525,8 +530,66 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
navigationTabCoordinator.setOverlayPresentationMode(.minimized)
}
// MARK: - Native calls
private func setupCallObservers() {
flowParameters.elementCallService.actions
.receive(on: DispatchQueue.main)
.sink { [weak self] action in
guard let self else { return }
switch action {
case .endCall:
// A native call comes down on `.nativeCall(.dismiss)` instead, once it has
// actually left the room.
if navigationTabCoordinator.overlayCoordinator is CallScreenCoordinator {
dismissCallScreenIfNeeded()
}
case .nativeCall(let presentation):
handleNativeCall(presentation)
default:
break
}
}
.store(in: &cancellables)
}
private func handleNativeCall(_ presentation: NativeCallPresentation) {
switch presentation {
case .present:
presentNativeCallScreen()
case .restore:
restoreNativeCallScreen()
case .minimize:
navigationTabCoordinator.setOverlayPresentationMode(.minimized)
case .dismiss:
dismissCallScreenIfNeeded()
}
}
private func presentNativeCallScreen() {
guard let controller = flowParameters.elementCallService.nativeCallController else {
MXLog.error("Cannot present a native call without a call stack")
return
}
guard !(navigationTabCoordinator.overlayCoordinator is NativeCallScreenCoordinator) else {
restoreNativeCallScreen()
return
}
let coordinator = NativeCallScreenCoordinator(parameters: .init(controller: controller))
navigationTabCoordinator.setOverlayCoordinator(coordinator, animated: true)
flowParameters.analytics.track(screen: .RoomCall)
}
private func restoreNativeCallScreen() {
flowParameters.elementCallService.restoreNativeCall()
navigationTabCoordinator.setOverlayPresentationMode(.fullScreen)
}
private func dismissCallScreenIfNeeded() {
guard navigationTabCoordinator.overlayCoordinator is CallScreenCoordinator else {
guard navigationTabCoordinator.overlayCoordinator is CallScreenCoordinator
|| navigationTabCoordinator.overlayCoordinator is NativeCallScreenCoordinator else {
return
}