call: Support voice only incoming call

This commit is contained in:
Valere
2026-03-23 14:55:40 +01:00
committed by Mauro
parent 867d3476c3
commit fec4f7d9ee
16 changed files with 73 additions and 59 deletions
@@ -175,8 +175,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] action in .sink { [weak self] action in
switch action { switch action {
case .startCall(let roomID): case .startCall(let roomID, let isVoiceCall):
self?.handleAppRoute(.call(roomID: roomID)) self?.handleAppRoute(.call(roomID: roomID, isVoiceCall: isVoiceCall))
case .receivedIncomingCallRequest: case .receivedIncomingCallRequest:
// When reporting a VoIP call through the CXProvider's `reportNewIncomingVoIPPushPayload` // When reporting a VoIP call through the CXProvider's `reportNewIncomingVoIPPushPayload`
// the UIApplication states don't change and syncing is neither started nor ran on // the UIApplication states don't change and syncing is neither started nor ran on
@@ -309,7 +309,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
} }
MXLog.info("Starting call in room: \(roomIdentifier)") MXLog.info("Starting call in room: \(roomIdentifier)")
handleAppRoute(AppRoute.call(roomID: roomIdentifier)) handleAppRoute(AppRoute.call(roomID: roomIdentifier, isVoiceCall: false))
} }
// MARK: - AuthenticationFlowCoordinatorDelegate // MARK: - AuthenticationFlowCoordinatorDelegate
@@ -40,7 +40,7 @@ enum AppRoute: Hashable {
/// The profile of a matrix user (outside of a room). /// The profile of a matrix user (outside of a room).
case userProfile(userID: String) case userProfile(userID: String)
/// An Element Call running in a particular room /// An Element Call running in a particular room
case call(roomID: String) case call(roomID: String, isVoiceCall: Bool)
/// An Element Call link generated outside of a chat room. /// An Element Call link generated outside of a chat room.
case genericCallLink(url: URL) case genericCallLink(url: URL)
/// The settings screen. /// The settings screen.
@@ -126,8 +126,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.showSettingsScreen) stateMachine.tryEvent(.showSettingsScreen)
} }
settingsFlowCoordinator?.handleAppRoute(appRoute, animated: animated) settingsFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
case .call(let roomID): case .call(let roomID, let isVoiceCall):
Task { await presentCallScreen(roomID: roomID) } Task { await presentCallScreen(roomID: roomID, isVoiceCall: isVoiceCall) }
case .genericCallLink(let url): case .genericCallLink(let url):
presentCallScreen(genericCallLink: url) presentCallScreen(genericCallLink: url)
case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias, case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias,
@@ -204,7 +204,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
case .sessionVerification(let flow): case .sessionVerification(let flow):
presentSessionVerificationScreen(flow: flow) presentSessionVerificationScreen(flow: flow)
case .showCallScreen(let roomProxy): case .showCallScreen(let roomProxy):
presentCallScreen(roomProxy: roomProxy) presentCallScreen(roomProxy: roomProxy, voiceOnly: false)
case .hideCallScreenOverlay: case .hideCallScreenOverlay:
hideCallScreenOverlay() hideCallScreenOverlay()
case .logout: case .logout:
@@ -218,7 +218,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
guard let self else { return } guard let self else { return }
switch action { switch action {
case .presentCallScreen(let roomProxy): case .presentCallScreen(let roomProxy):
presentCallScreen(roomProxy: roomProxy) presentCallScreen(roomProxy: roomProxy, voiceOnly: false)
case .verifyUser(let userID): case .verifyUser(let userID):
presentSessionVerificationScreen(flow: .userInitiator(userID: userID)) presentSessionVerificationScreen(flow: .userInitiator(userID: userID))
case .showSettings: case .showSettings:
@@ -394,21 +394,22 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
presentCallScreen(configuration: .init(genericCallLink: url)) presentCallScreen(configuration: .init(genericCallLink: url))
} }
private func presentCallScreen(roomID: String) async { private func presentCallScreen(roomID: String, isVoiceCall: Bool) async {
guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else { guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else {
return return
} }
presentCallScreen(roomProxy: roomProxy) presentCallScreen(roomProxy: roomProxy, voiceOnly: isVoiceCall)
} }
private func presentCallScreen(roomProxy: JoinedRoomProxyProtocol) { private func presentCallScreen(roomProxy: JoinedRoomProxyProtocol, voiceOnly: Bool) {
let colorScheme: ColorScheme = flowParameters.windowManager.mainWindow.traitCollection.userInterfaceStyle == .light ? .light : .dark let colorScheme: ColorScheme = flowParameters.windowManager.mainWindow.traitCollection.userInterfaceStyle == .light ? .light : .dark
presentCallScreen(configuration: .init(roomProxy: roomProxy, presentCallScreen(configuration: .init(roomProxy: roomProxy,
clientProxy: userSession.clientProxy, clientProxy: userSession.clientProxy,
clientID: InfoPlistReader.main.bundleIdentifier, clientID: InfoPlistReader.main.bundleIdentifier,
elementCallBaseURL: flowParameters.appSettings.elementCallBaseURL, elementCallBaseURL: flowParameters.appSettings.elementCallBaseURL,
elementCallBaseURLOverride: flowParameters.appSettings.elementCallBaseURLOverride, elementCallBaseURLOverride: flowParameters.appSettings.elementCallBaseURLOverride,
voiceOnly: voiceOnly,
colorScheme: colorScheme)) colorScheme: colorScheme))
} }
@@ -6453,15 +6453,15 @@ class ElementCallWidgetDriverMock: ElementCallWidgetDriverProtocol, @unchecked S
//MARK: - start //MARK: - start
var startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationUnderlyingCallsCount = 0 var startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationUnderlyingCallsCount = 0
var startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationCallsCount: Int { var startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationCallsCount: Int {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationUnderlyingCallsCount return startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationUnderlyingCallsCount
} else { } else {
var returnValue: Int? = nil var returnValue: Int? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationUnderlyingCallsCount returnValue = startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationUnderlyingCallsCount
} }
return returnValue! return returnValue!
@@ -6469,29 +6469,29 @@ class ElementCallWidgetDriverMock: ElementCallWidgetDriverProtocol, @unchecked S
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationUnderlyingCallsCount = newValue startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationUnderlyingCallsCount = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationUnderlyingCallsCount = newValue startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationUnderlyingCallsCount = newValue
} }
} }
} }
} }
var startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationCalled: Bool { var startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationCalled: Bool {
return startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationCallsCount > 0 return startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationCallsCount > 0
} }
var startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationReceivedArguments: (baseURL: URL, clientID: String, colorScheme: ColorScheme, rageshakeURL: String?, analyticsConfiguration: ElementCallAnalyticsConfiguration?)? var startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationReceivedArguments: (baseURL: URL, clientID: String, colorScheme: ColorScheme, voiceOnly: Bool, rageshakeURL: String?, analyticsConfiguration: ElementCallAnalyticsConfiguration?)?
var startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationReceivedInvocations: [(baseURL: URL, clientID: String, colorScheme: ColorScheme, rageshakeURL: String?, analyticsConfiguration: ElementCallAnalyticsConfiguration?)] = [] var startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationReceivedInvocations: [(baseURL: URL, clientID: String, colorScheme: ColorScheme, voiceOnly: Bool, rageshakeURL: String?, analyticsConfiguration: ElementCallAnalyticsConfiguration?)] = []
var startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationUnderlyingReturnValue: Result<URL, ElementCallWidgetDriverError>! var startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationUnderlyingReturnValue: Result<URL, ElementCallWidgetDriverError>!
var startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationReturnValue: Result<URL, ElementCallWidgetDriverError>! { var startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationReturnValue: Result<URL, ElementCallWidgetDriverError>! {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationUnderlyingReturnValue return startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationUnderlyingReturnValue
} else { } else {
var returnValue: Result<URL, ElementCallWidgetDriverError>? = nil var returnValue: Result<URL, ElementCallWidgetDriverError>? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationUnderlyingReturnValue returnValue = startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationUnderlyingReturnValue
} }
return returnValue! return returnValue!
@@ -6499,26 +6499,26 @@ class ElementCallWidgetDriverMock: ElementCallWidgetDriverProtocol, @unchecked S
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationUnderlyingReturnValue = newValue startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationUnderlyingReturnValue = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationUnderlyingReturnValue = newValue startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationUnderlyingReturnValue = newValue
} }
} }
} }
} }
var startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationClosure: ((URL, String, ColorScheme, String?, ElementCallAnalyticsConfiguration?) async -> Result<URL, ElementCallWidgetDriverError>)? var startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationClosure: ((URL, String, ColorScheme, Bool, String?, ElementCallAnalyticsConfiguration?) async -> Result<URL, ElementCallWidgetDriverError>)?
func start(baseURL: URL, clientID: String, colorScheme: ColorScheme, rageshakeURL: String?, analyticsConfiguration: ElementCallAnalyticsConfiguration?) async -> Result<URL, ElementCallWidgetDriverError> { func start(baseURL: URL, clientID: String, colorScheme: ColorScheme, voiceOnly: Bool, rageshakeURL: String?, analyticsConfiguration: ElementCallAnalyticsConfiguration?) async -> Result<URL, ElementCallWidgetDriverError> {
startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationCallsCount += 1 startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationCallsCount += 1
startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationReceivedArguments = (baseURL: baseURL, clientID: clientID, colorScheme: colorScheme, rageshakeURL: rageshakeURL, analyticsConfiguration: analyticsConfiguration) startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationReceivedArguments = (baseURL: baseURL, clientID: clientID, colorScheme: colorScheme, voiceOnly: voiceOnly, rageshakeURL: rageshakeURL, analyticsConfiguration: analyticsConfiguration)
DispatchQueue.main.async { DispatchQueue.main.async {
self.startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationReceivedInvocations.append((baseURL: baseURL, clientID: clientID, colorScheme: colorScheme, rageshakeURL: rageshakeURL, analyticsConfiguration: analyticsConfiguration)) self.startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationReceivedInvocations.append((baseURL: baseURL, clientID: clientID, colorScheme: colorScheme, voiceOnly: voiceOnly, rageshakeURL: rageshakeURL, analyticsConfiguration: analyticsConfiguration))
} }
if let startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationClosure = startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationClosure { if let startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationClosure = startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationClosure {
return await startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationClosure(baseURL, clientID, colorScheme, rageshakeURL, analyticsConfiguration) return await startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationClosure(baseURL, clientID, colorScheme, voiceOnly, rageshakeURL, analyticsConfiguration)
} else { } else {
return startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationReturnValue return startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationReturnValue
} }
} }
//MARK: - handleMessage //MARK: - handleMessage
@@ -137,7 +137,7 @@ extension JoinedRoomProxyMock {
fatalError() fatalError()
} }
widgetDriver.startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationReturnValue = .success(url) widgetDriver.startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationUnderlyingReturnValue = .success(url)
elementCallWidgetDriverDeviceIDReturnValue = widgetDriver elementCallWidgetDriverDeviceIDReturnValue = widgetDriver
@@ -53,7 +53,7 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
case .genericCallLink(let url): case .genericCallLink(let url):
widgetDriver = GenericCallLinkWidgetDriver(url: url) widgetDriver = GenericCallLinkWidgetDriver(url: url)
isGenericCallLink = true isGenericCallLink = true
case .roomCall(let roomProxy, let clientProxy, _, _, _, _): case .roomCall(let roomProxy, let clientProxy, _, _, _, _, _):
guard let deviceID = clientProxy.deviceID else { fatalError("Missing device ID for the call.") } guard let deviceID = clientProxy.deviceID else { fatalError("Missing device ID for the call.") }
widgetDriver = roomProxy.elementCallWidgetDriver(deviceID: deviceID) widgetDriver = roomProxy.elementCallWidgetDriver(deviceID: deviceID)
} }
@@ -167,7 +167,7 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
state.url = url state.url = url
// We need widget messaging to work before enabling CallKit, otherwise mute, hangup etc do nothing. // We need widget messaging to work before enabling CallKit, otherwise mute, hangup etc do nothing.
case .roomCall(let roomProxy, _, let clientID, let elementCallBaseURL, let elementCallBaseURLOverride, let colorScheme): case .roomCall(let roomProxy, _, let clientID, let voiceOnly, let elementCallBaseURL, let elementCallBaseURLOverride, let colorScheme):
Task { [weak self] in Task { [weak self] in
guard let self else { return } guard let self else { return }
@@ -194,6 +194,7 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
switch await widgetDriver.start(baseURL: baseURL, switch await widgetDriver.start(baseURL: baseURL,
clientID: clientID, clientID: clientID,
colorScheme: colorScheme, colorScheme: colorScheme,
voiceOnly: voiceOnly,
rageshakeURL: rageshakeURL, rageshakeURL: rageshakeURL,
analyticsConfiguration: analyticsConfiguration) { analyticsConfiguration: analyticsConfiguration) {
case .success(let url): case .success(let url):
@@ -360,7 +360,7 @@ struct CallScreen_Previews: PreviewProvider {
let widgetDriver = ElementCallWidgetDriverMock() let widgetDriver = ElementCallWidgetDriverMock()
widgetDriver.underlyingMessagePublisher = .init() widgetDriver.underlyingMessagePublisher = .init()
widgetDriver.underlyingActions = PassthroughSubject<ElementCallWidgetDriverAction, Never>().eraseToAnyPublisher() widgetDriver.underlyingActions = PassthroughSubject<ElementCallWidgetDriverAction, Never>().eraseToAnyPublisher()
widgetDriver.startBaseURLClientIDColorSchemeRageshakeURLAnalyticsConfigurationReturnValue = .success(URL.userDirectory) widgetDriver.startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationReturnValue = .success(URL.userDirectory)
roomProxy.elementCallWidgetDriverDeviceIDReturnValue = widgetDriver roomProxy.elementCallWidgetDriverDeviceIDReturnValue = widgetDriver
@@ -370,6 +370,7 @@ struct CallScreen_Previews: PreviewProvider {
clientID: "io.element.elementx", clientID: "io.element.elementx",
elementCallBaseURL: "https://call.element.io", elementCallBaseURL: "https://call.element.io",
elementCallBaseURLOverride: nil, elementCallBaseURLOverride: nil,
voiceOnly: false,
colorScheme: .light), colorScheme: .light),
allowPictureInPicture: false, allowPictureInPicture: false,
appHooks: AppHooks(), appHooks: AppHooks(),
@@ -20,6 +20,7 @@ struct ElementCallConfiguration {
case roomCall(roomProxy: JoinedRoomProxyProtocol, case roomCall(roomProxy: JoinedRoomProxyProtocol,
clientProxy: ClientProxyProtocol, clientProxy: ClientProxyProtocol,
clientID: String, clientID: String,
voiceOnly: Bool,
elementCallBaseURL: URL, elementCallBaseURL: URL,
elementCallBaseURLOverride: URL?, elementCallBaseURLOverride: URL?,
colorScheme: ColorScheme) colorScheme: ColorScheme)
@@ -59,10 +60,12 @@ struct ElementCallConfiguration {
clientID: String, clientID: String,
elementCallBaseURL: URL, elementCallBaseURL: URL,
elementCallBaseURLOverride: URL?, elementCallBaseURLOverride: URL?,
voiceOnly: Bool,
colorScheme: ColorScheme) { colorScheme: ColorScheme) {
kind = .roomCall(roomProxy: roomProxy, kind = .roomCall(roomProxy: roomProxy,
clientProxy: clientProxy, clientProxy: clientProxy,
clientID: clientID, clientID: clientID,
voiceOnly: voiceOnly,
elementCallBaseURL: elementCallBaseURL, elementCallBaseURL: elementCallBaseURL,
elementCallBaseURLOverride: elementCallBaseURLOverride, elementCallBaseURLOverride: elementCallBaseURLOverride,
colorScheme: colorScheme) colorScheme: colorScheme)
@@ -73,7 +76,7 @@ struct ElementCallConfiguration {
switch kind { switch kind {
case .genericCallLink(let url): case .genericCallLink(let url):
url.absoluteString url.absoluteString
case .roomCall(let roomProxy, _, _, _, _, _): case .roomCall(let roomProxy, _, _, _, _, _, _):
roomProxy.id roomProxy.id
} }
} }
@@ -25,6 +25,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
let callKitID: UUID let callKitID: UUID
let roomID: String let roomID: String
let rtcNotificationID: String? let rtcNotificationID: String?
let isVoiceCall: Bool
} }
private let pushRegistry: PKPushRegistry private let pushRegistry: PKPushRegistry
@@ -111,7 +112,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
let callID = if let incomingCallID, incomingCallID.roomID == roomID { let callID = if let incomingCallID, incomingCallID.roomID == roomID {
incomingCallID incomingCallID
} else { } else {
CallID(callKitID: UUID(), roomID: roomID, rtcNotificationID: nil) CallID(callKitID: UUID(), roomID: roomID, rtcNotificationID: nil, isVoiceCall: false)
} }
incomingCallID = nil incomingCallID = nil
@@ -177,7 +178,9 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
return return
} }
let callID = CallID(callKitID: UUID(), roomID: roomID, rtcNotificationID: rtcNotificationID) let isVoiceCall = payload.dictionaryPayload[ElementCallServiceNotificationKey.isVoiceCall.rawValue] as? Bool ?? false
let callID = CallID(callKitID: UUID(), roomID: roomID, rtcNotificationID: rtcNotificationID, isVoiceCall: isVoiceCall)
incomingCallID = callID incomingCallID = callID
guard let expirationDate = (payload.dictionaryPayload[ElementCallServiceNotificationKey.expirationDate.rawValue] as? Date) else { guard let expirationDate = (payload.dictionaryPayload[ElementCallServiceNotificationKey.expirationDate.rawValue] as? Date) else {
@@ -199,7 +202,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
let roomDisplayName = payload.dictionaryPayload[ElementCallServiceNotificationKey.roomDisplayName.rawValue] as? String let roomDisplayName = payload.dictionaryPayload[ElementCallServiceNotificationKey.roomDisplayName.rawValue] as? String
let update = CXCallUpdate() let update = CXCallUpdate()
update.hasVideo = true update.hasVideo = !isVoiceCall
update.localizedCallerName = roomDisplayName update.localizedCallerName = roomDisplayName
// https://stackoverflow.com/a/41230020/730924 // https://stackoverflow.com/a/41230020/730924
update.remoteHandle = .init(type: .generic, value: roomID) update.remoteHandle = .init(type: .generic, value: roomID)
@@ -271,7 +274,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
// Then end the and call rely on `setupCallSession` to create a new one // Then end the and call rely on `setupCallSession` to create a new one
provider.reportCall(with: incomingCallID.callKitID, endedAt: nil, reason: .remoteEnded) provider.reportCall(with: incomingCallID.callKitID, endedAt: nil, reason: .remoteEnded)
self.actionsSubject.send(.startCall(roomID: incomingCallID.roomID)) self.actionsSubject.send(.startCall(roomID: incomingCallID.roomID, isVoiceCall: incomingCallID.isVoiceCall))
self.endUnansweredCallTask?.cancel() self.endUnansweredCallTask?.cancel()
} }
} }
@@ -16,6 +16,7 @@ enum ElementCallServiceNotificationKey: String {
case rtcNotifyEventID case rtcNotifyEventID
/// The Date at which the incoming call should stop ringing. /// The Date at which the incoming call should stop ringing.
case expirationDate case expirationDate
case isVoiceCall
} }
let ElementCallServiceNotificationDiscardDelta = 15.0 let ElementCallServiceNotificationDiscardDelta = 15.0
@@ -10,7 +10,7 @@ import Combine
enum ElementCallServiceAction { enum ElementCallServiceAction {
case receivedIncomingCallRequest case receivedIncomingCallRequest
case startCall(roomID: String) case startCall(roomID: String, isVoiceCall: Bool)
case endCall(roomID: String) case endCall(roomID: String)
case setAudioEnabled(_ enabled: Bool, roomID: String) case setAudioEnabled(_ enabled: Bool, roomID: String)
} }
@@ -70,6 +70,7 @@ final class ElementCallWidgetDriver: WidgetCapabilitiesProvider, ElementCallWidg
func start(baseURL: URL, func start(baseURL: URL,
clientID: String, clientID: String,
colorScheme: ColorScheme, colorScheme: ColorScheme,
voiceOnly: Bool,
rageshakeURL: String?, rageshakeURL: String?,
analyticsConfiguration: ElementCallAnalyticsConfiguration?) async -> Result<URL, ElementCallWidgetDriverError> { analyticsConfiguration: ElementCallAnalyticsConfiguration?) async -> Result<URL, ElementCallWidgetDriverError> {
guard let room = room as? Room else { guard let room = room as? Room else {
@@ -77,7 +78,7 @@ final class ElementCallWidgetDriver: WidgetCapabilitiesProvider, ElementCallWidg
} }
async let useEncryption = (try? room.latestEncryptionState() == .encrypted) ?? false async let useEncryption = (try? room.latestEncryptionState() == .encrypted) ?? false
async let intent = room.joinCallIntent async let intent = room.joinCallIntent(voiceOnly: voiceOnly)
let widgetSettings: WidgetSettings let widgetSettings: WidgetSettings
do { do {
@@ -92,6 +93,7 @@ final class ElementCallWidgetDriver: WidgetCapabilitiesProvider, ElementCallWidg
posthogApiKey: analyticsConfiguration?.posthogAPIKey, posthogApiKey: analyticsConfiguration?.posthogAPIKey,
rageshakeSubmitUrl: rageshakeURL, rageshakeSubmitUrl: rageshakeURL,
sentryDsn: analyticsConfiguration?.sentryDSN, sentryDsn: analyticsConfiguration?.sentryDSN,
sentryEnvironment: nil), sentryEnvironment: nil),
config: .init(intent: intent)) config: .init(intent: intent))
} catch { } catch {
@@ -33,6 +33,7 @@ protocol ElementCallWidgetDriverProtocol {
func start(baseURL: URL, func start(baseURL: URL,
clientID: String, clientID: String,
colorScheme: ColorScheme, colorScheme: ColorScheme,
voiceOnly: Bool,
rageshakeURL: String?, rageshakeURL: String?,
analyticsConfiguration: ElementCallAnalyticsConfiguration?) async -> Result<URL, ElementCallWidgetDriverError> analyticsConfiguration: ElementCallAnalyticsConfiguration?) async -> Result<URL, ElementCallWidgetDriverError>
@@ -27,6 +27,7 @@ class GenericCallLinkWidgetDriver: ElementCallWidgetDriverProtocol {
func start(baseURL: URL, func start(baseURL: URL,
clientID: String, clientID: String,
colorScheme: ColorScheme, colorScheme: ColorScheme,
voiceOnly: Bool,
rageshakeURL: String?, rageshakeURL: String?,
analyticsConfiguration: ElementCallAnalyticsConfiguration?) async -> Result<URL, ElementCallWidgetDriverError> { analyticsConfiguration: ElementCallAnalyticsConfiguration?) async -> Result<URL, ElementCallWidgetDriverError> {
MXLog.error("Nothing to start, use the configuration's URL directly instead.") MXLog.error("Nothing to start, use the configuration's URL directly instead.")
+7 -9
View File
@@ -9,14 +9,12 @@
import MatrixRustSDK import MatrixRustSDK
extension RoomProtocol { extension RoomProtocol {
var joinCallIntent: Intent { func joinCallIntent(voiceOnly: Bool) async -> Intent {
get async { switch await (hasActiveRoomCall(), isDirect()) {
switch await (hasActiveRoomCall(), isDirect()) { case (true, true): voiceOnly ? .joinExistingDmVoice : .joinExistingDm
case (true, true): .joinExistingDm case (true, false): .joinExisting
case (true, false): .joinExisting case (false, true): voiceOnly ? .startCallDmVoice : .startCallDm
case (false, true): .startCallDm case (false, false): .startCall
case (false, false): .startCall
}
} }
} }
} }
+6 -4
View File
@@ -126,13 +126,14 @@ class NotificationHandler {
} }
return .processedShouldDiscard return .processedShouldDiscard
case .rtcNotification(let notificationType, let expirationTimestamp, _): case .rtcNotification(let notificationType, let expirationTimestamp, let callIntent):
return await handleCallNotification(notificationType: notificationType, return await handleCallNotification(notificationType: notificationType,
rtcNotifyEventID: event.eventId(), rtcNotifyEventID: event.eventId(),
timestamp: event.timestamp(), timestamp: event.timestamp(),
expirationTimestamp: expirationTimestamp, expirationTimestamp: expirationTimestamp,
roomID: itemProxy.roomID, roomID: itemProxy.roomID,
roomDisplayName: itemProxy.roomDisplayName) roomDisplayName: itemProxy.roomDisplayName,
callIntent: callIntent)
case .callAnswer, case .callAnswer,
.callInvite, .callInvite,
.callHangup, .callHangup,
@@ -161,7 +162,7 @@ class NotificationHandler {
timestamp: Timestamp, timestamp: Timestamp,
expirationTimestamp: Timestamp, expirationTimestamp: Timestamp,
roomID: String, roomID: String,
roomDisplayName: String) async -> NotificationProcessingResult { roomDisplayName: String, callIntent: RtcCallIntent?) async -> NotificationProcessingResult {
// Handle incoming VoIP calls, show the native OS call screen // Handle incoming VoIP calls, show the native OS call screen
// https://developer.apple.com/documentation/callkit/sending-end-to-end-encrypted-voip-calls // https://developer.apple.com/documentation/callkit/sending-end-to-end-encrypted-voip-calls
// //
@@ -215,7 +216,8 @@ class NotificationHandler {
let payload = [ElementCallServiceNotificationKey.roomID.rawValue: roomID, let payload = [ElementCallServiceNotificationKey.roomID.rawValue: roomID,
ElementCallServiceNotificationKey.roomDisplayName.rawValue: roomDisplayName, ElementCallServiceNotificationKey.roomDisplayName.rawValue: roomDisplayName,
ElementCallServiceNotificationKey.expirationDate.rawValue: expirationDate, ElementCallServiceNotificationKey.expirationDate.rawValue: expirationDate,
ElementCallServiceNotificationKey.rtcNotifyEventID.rawValue: rtcNotifyEventID] as [String: Any] ElementCallServiceNotificationKey.rtcNotifyEventID.rawValue: rtcNotifyEventID,
ElementCallServiceNotificationKey.isVoiceCall.rawValue: callIntent == RtcCallIntent.audio] as [String: Any]
do { do {
try await CXProvider.reportNewIncomingVoIPPushPayload(payload) try await CXProvider.reportNewIncomingVoIPPushPayload(payload)