mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
call: Support voice only incoming call
This commit is contained in:
@@ -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.")
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user