diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 6c3b73080..f2684d428 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -838,8 +838,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg private func startSync() { guard let userSession else { return } - // FIXME: replace this with `user_id_server_name` from https://github.com/matrix-org/matrix-rust-sdk/pull/3617 - let serverName = String(userSession.clientProxy.userID.split(separator: ":").last ?? "Unknown") + let serverName = String(userSession.clientProxy.userIDServerName ?? "Unknown") ServiceLocator.shared.analytics.signpost.beginFirstSync(serverName: serverName) userSession.clientProxy.startSync() diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index a53fe7e2f..dc4c8680b 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -5007,17 +5007,17 @@ class ElementCallServiceMock: ElementCallServiceProtocol { tearDownCallSessionCallsCount += 1 tearDownCallSessionClosure?() } - //MARK: - setCallMuted + //MARK: - setAudioEnabled - var setCallMutedRoomIDUnderlyingCallsCount = 0 - var setCallMutedRoomIDCallsCount: Int { + var setAudioEnabledRoomIDUnderlyingCallsCount = 0 + var setAudioEnabledRoomIDCallsCount: Int { get { if Thread.isMainThread { - return setCallMutedRoomIDUnderlyingCallsCount + return setAudioEnabledRoomIDUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = setCallMutedRoomIDUnderlyingCallsCount + returnValue = setAudioEnabledRoomIDUnderlyingCallsCount } return returnValue! @@ -5025,28 +5025,28 @@ class ElementCallServiceMock: ElementCallServiceProtocol { } set { if Thread.isMainThread { - setCallMutedRoomIDUnderlyingCallsCount = newValue + setAudioEnabledRoomIDUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - setCallMutedRoomIDUnderlyingCallsCount = newValue + setAudioEnabledRoomIDUnderlyingCallsCount = newValue } } } } - var setCallMutedRoomIDCalled: Bool { - return setCallMutedRoomIDCallsCount > 0 + var setAudioEnabledRoomIDCalled: Bool { + return setAudioEnabledRoomIDCallsCount > 0 } - var setCallMutedRoomIDReceivedArguments: (muted: Bool, roomID: String)? - var setCallMutedRoomIDReceivedInvocations: [(muted: Bool, roomID: String)] = [] - var setCallMutedRoomIDClosure: ((Bool, String) -> Void)? + var setAudioEnabledRoomIDReceivedArguments: (enabled: Bool, roomID: String)? + var setAudioEnabledRoomIDReceivedInvocations: [(enabled: Bool, roomID: String)] = [] + var setAudioEnabledRoomIDClosure: ((Bool, String) -> Void)? - func setCallMuted(_ muted: Bool, roomID: String) { - setCallMutedRoomIDCallsCount += 1 - setCallMutedRoomIDReceivedArguments = (muted: muted, roomID: roomID) + func setAudioEnabled(_ enabled: Bool, roomID: String) { + setAudioEnabledRoomIDCallsCount += 1 + setAudioEnabledRoomIDReceivedArguments = (enabled: enabled, roomID: roomID) DispatchQueue.main.async { - self.setCallMutedRoomIDReceivedInvocations.append((muted: muted, roomID: roomID)) + self.setAudioEnabledRoomIDReceivedInvocations.append((enabled: enabled, roomID: roomID)) } - setCallMutedRoomIDClosure?(muted, roomID) + setAudioEnabledRoomIDClosure?(enabled, roomID) } } class ElementCallWidgetDriverMock: ElementCallWidgetDriverProtocol { diff --git a/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift b/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift index 2c0175786..e563f2f3f 100644 --- a/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift +++ b/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift @@ -62,9 +62,6 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol return } - // TODO: intercept EC mute state changes and pass them over to CallKit - // elementCallService.setCallMuted(roomID: roomProxy.id, muted: muted) - Task { await self.widgetDriver.sendMessage(message) } @@ -76,14 +73,14 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol guard let self else { return } switch action { - case let .setCallMuted(muted, roomID): + case let .setAudioEnabled(enabled, roomID): guard roomID == roomProxy.id else { MXLog.error("Received mute request for a different room: \(roomID) != \(roomProxy.id)") return } Task { - await self.setMuted(muted) + await self.setAudioEnabled(enabled) } default: break @@ -97,13 +94,7 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol guard let self else { return } Task { - do { - let message = "postMessage(\(receivedMessage), '*')" - let result = try await self.state.bindings.javaScriptEvaluator?(message) - MXLog.debug("Evaluated javascript: \(message) with result: \(String(describing: result))") - } catch { - MXLog.error("Received javascript evaluation error: \(error)") - } + await self.postJSONToWidget(receivedMessage) } } .store(in: &cancellables) @@ -116,6 +107,8 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol switch action { case .callEnded: actionsSubject.send(.dismiss) + case .mediaStateChanged(let audioEnabled, _): + elementCallService.setAudioEnabled(audioEnabled, roomID: roomProxy.id) } } .store(in: &cancellables) @@ -143,8 +136,6 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol await elementCallService.setupCallSession(roomID: roomProxy.id, roomDisplayName: roomProxy.roomTitle) - // TODO: Pass over the current EC mute status to CallKit - let _ = await roomProxy.sendCallNotificationIfNeeeded() } } @@ -159,7 +150,7 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol func stop() { Task { - await hangUp() + await hangup() } elementCallService.tearDownCallSession() @@ -167,23 +158,44 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol // MARK: - Private - private func setMuted(_ muted: Bool) async { - // Not supported on EC yet + private func setAudioEnabled(_ enabled: Bool) async { + let message = ElementCallWidgetMessage(direction: .toWidget, + action: .mediaState, + data: .init(audioEnabled: enabled), + widgetId: widgetDriver.widgetID) + await postMessageToWidget(message) } - private func hangUp() async { - let hangUpMessage = """ - {"api":"fromWidget", - "widgetId":"\(widgetDriver.widgetID)", - "requestId":"widgetapi-\(UUID())", - "action":"im.vector.hangup", - "data":{}} - """ + func hangup() async { + let message = ElementCallWidgetMessage(direction: .fromWidget, + action: .hangup, + widgetId: widgetDriver.widgetID) - let result = await widgetDriver.sendMessage(hangUpMessage) - MXLog.info("Sent hangUp message with result: \(result)") + await postMessageToWidget(message) } - + + private func postMessageToWidget(_ message: ElementCallWidgetMessage) async { + do { + let data = try JSONEncoder().encode(message) + let json = String(decoding: data, as: UTF8.self) + _ = await widgetDriver.sendMessage(json) + + await postJSONToWidget(json) + } catch { + MXLog.error("Failed encoding widget message with error: \(error)") + } + } + + private func postJSONToWidget(_ json: String) async { + do { + let message = "postMessage(\(json), '*')" + let result = try await state.bindings.javaScriptEvaluator?(message) + MXLog.debug("Evaluated javascript: \(json) with result: \(String(describing: result))") + } catch { + MXLog.error("Received javascript evaluation error: \(error)") + } + } + private static let eventHandlerName = "elementx" private static var eventHandlerInjectionScript: String { diff --git a/ElementX/Sources/Services/ElementCall/ElementCallService.swift b/ElementX/Sources/Services/ElementCall/ElementCallService.swift index 35f5db741..d096a34f9 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallService.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallService.swift @@ -118,7 +118,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe tearDownCallSession(sendEndCallAction: true) } - func setCallMuted(_ muted: Bool, roomID: String) { + func setAudioEnabled(_ enabled: Bool, roomID: String) { guard let ongoingCallID else { MXLog.error("Failed toggling call microphone, no calls running") return @@ -129,7 +129,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe return } - let transaction = CXTransaction(action: CXSetMutedCallAction(call: ongoingCallID.callKitID, muted: muted)) + let transaction = CXTransaction(action: CXSetMutedCallAction(call: ongoingCallID.callKitID, muted: !enabled)) callController.request(transaction) { error in if let error { MXLog.error("Failed toggling call microphone with error: \(error)") @@ -211,16 +211,13 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe } func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { - // if let ongoingCallID { - // actionsSubject.send(.setCallMuted(action.isMuted, roomID: ongoingCallID.roomID)) - // } else { - // MXLog.error("Failed muting/unmuting call, missing ongoingCallID") - // } - // - // action.fulfill() + if let ongoingCallID { + actionsSubject.send(.setAudioEnabled(!action.isMuted, roomID: ongoingCallID.roomID)) + } else { + MXLog.error("Failed muting/unmuting call, missing ongoingCallID") + } - // TODO: EC doesn't expose controls for this yet. Fail the action for now. - action.fail() + action.fulfill() } func provider(_ provider: CXProvider, perform action: CXEndCallAction) { diff --git a/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift b/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift index d295a956d..1c47b23f0 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift @@ -19,7 +19,7 @@ import Combine enum ElementCallServiceAction { case startCall(roomID: String) case endCall(roomID: String) - case setCallMuted(_ muted: Bool, roomID: String) + case setAudioEnabled(_ enabled: Bool, roomID: String) } // sourcery: AutoMockable @@ -32,5 +32,5 @@ protocol ElementCallServiceProtocol { func tearDownCallSession() - func setCallMuted(_ muted: Bool, roomID: String) + func setAudioEnabled(_ enabled: Bool, roomID: String) } diff --git a/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriver.swift b/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriver.swift index 32ff78338..6969bb171 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriver.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriver.swift @@ -18,7 +18,7 @@ import Combine import MatrixRustSDK import SwiftUI -private struct ElementCallWidgetMessage: Codable { +struct ElementCallWidgetMessage: Codable { enum Direction: String, Codable { case fromWidget case toWidget @@ -26,14 +26,32 @@ private struct ElementCallWidgetMessage: Codable { enum Action: String, Codable { case hangup = "im.vector.hangup" + case mediaState = "io.element.device_mute" + } + + struct Data: Codable { + var audioEnabled: Bool? + var videoEnabled: Bool? + + enum CodingKeys: String, CodingKey { + case audioEnabled = "audio_enabled" + case videoEnabled = "video_enabled" + } } let direction: Direction let action: Action + var data: Data = .init() + + let widgetId: String + var requestId = "widgetapi-\(UUID())" enum CodingKeys: String, CodingKey { case direction = "api" case action + case data + case widgetId + case requestId } } @@ -151,16 +169,29 @@ class ElementCallWidgetDriver: WidgetCapabilitiesProvider, ElementCallWidgetDriv // MARK: - Private func handleMessageIfNeeded(_ message: String) { - guard let data = message.data(using: .utf8), - let widgetMessage = try? JSONDecoder().decode(ElementCallWidgetMessage.self, from: data) else { + guard let data = message.data(using: .utf8) else { return } - if widgetMessage.direction == .fromWidget { - switch widgetMessage.action { - case .hangup: - actionsSubject.send(.callEnded) + do { + let widgetMessage = try JSONDecoder().decode(ElementCallWidgetMessage.self, from: data) + if widgetMessage.direction == .fromWidget { + switch widgetMessage.action { + case .hangup: + actionsSubject.send(.callEnded) + case .mediaState: + guard let audioEnabled = widgetMessage.data.audioEnabled, + let videoEnabled = widgetMessage.data.videoEnabled else { + MXLog.error("Media state change messages should contain info data") + return + } + + actionsSubject.send(.mediaStateChanged(audioEnabled: audioEnabled, videoEnabled: videoEnabled)) + } } + } catch { + // Not all actions are supported + MXLog.verbose("Failed processing widget message with error: \(error)") } } } diff --git a/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriverProtocol.swift b/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriverProtocol.swift index 6141e9c0f..1c512a921 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriverProtocol.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriverProtocol.swift @@ -28,6 +28,7 @@ enum ElementCallWidgetDriverError: Error { enum ElementCallWidgetDriverAction { case callEnded + case mediaStateChanged(audioEnabled: Bool, videoEnabled: Bool) } // sourcery: AutoMockable