diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index bd8dd4ccd..4b19e7f00 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -37,12 +37,14 @@ final class AppSettings { case viewSourceEnabled case richTextEditorEnabled + case elementCallBaseURL + case elementCallEncryptionEnabled + // Feature flags case shouldCollapseRoomStateEvents case userSuggestionsEnabled case readReceiptsEnabled case swiftUITimelineEnabled - case elementCallEnabled case chatBackupEnabled } @@ -215,7 +217,11 @@ final class AppSettings { // MARK: - Element Call - let elementCallBaseURL: URL = "https://call.element.dev" + @UserPreference(key: UserDefaultsKeys.elementCallBaseURL, defaultValue: "https://call.element.io", storageType: .userDefaults(store)) + var elementCallBaseURL: URL + + @UserPreference(key: UserDefaultsKeys.elementCallEncryptionEnabled, defaultValue: true, storageType: .userDefaults(store)) + var elementCallUseEncryption // MARK: - Notifications @@ -267,9 +273,6 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.swiftUITimelineEnabled, defaultValue: false, storageType: .volatile) var swiftUITimelineEnabled - @UserPreference(key: UserDefaultsKeys.elementCallEnabled, defaultValue: true, storageType: .userDefaults(store)) - var elementCallEnabled - @UserPreference(key: UserDefaultsKeys.chatBackupEnabled, defaultValue: false, storageType: .userDefaults(store)) var chatBackupEnabled } diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 5e57ad98d..a7edd969d 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -510,7 +510,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { private func presentCallScreen(roomProxy: RoomProxyProtocol) { let callScreenCoordinator = CallScreenCoordinator(parameters: .init(roomProxy: roomProxy, callBaseURL: appSettings.elementCallBaseURL, - clientID: InfoPlistReader.main.bundleIdentifier)) + clientID: InfoPlistReader.main.bundleIdentifier, + useEncryption: appSettings.elementCallUseEncryption)) callScreenCoordinator.actions .sink { [weak self] action in diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 2f17a11a3..f22fc76c8 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -723,23 +723,23 @@ class ElementCallWidgetDriverMock: ElementCallWidgetDriverProtocol { //MARK: - start - var startBaseURLClientIDCallsCount = 0 - var startBaseURLClientIDCalled: Bool { - return startBaseURLClientIDCallsCount > 0 + var startBaseURLClientIDUseEncryptionCallsCount = 0 + var startBaseURLClientIDUseEncryptionCalled: Bool { + return startBaseURLClientIDUseEncryptionCallsCount > 0 } - var startBaseURLClientIDReceivedArguments: (baseURL: URL, clientID: String)? - var startBaseURLClientIDReceivedInvocations: [(baseURL: URL, clientID: String)] = [] - var startBaseURLClientIDReturnValue: Result! - var startBaseURLClientIDClosure: ((URL, String) async -> Result)? + var startBaseURLClientIDUseEncryptionReceivedArguments: (baseURL: URL, clientID: String, useEncryption: Bool)? + var startBaseURLClientIDUseEncryptionReceivedInvocations: [(baseURL: URL, clientID: String, useEncryption: Bool)] = [] + var startBaseURLClientIDUseEncryptionReturnValue: Result! + var startBaseURLClientIDUseEncryptionClosure: ((URL, String, Bool) async -> Result)? - func start(baseURL: URL, clientID: String) async -> Result { - startBaseURLClientIDCallsCount += 1 - startBaseURLClientIDReceivedArguments = (baseURL: baseURL, clientID: clientID) - startBaseURLClientIDReceivedInvocations.append((baseURL: baseURL, clientID: clientID)) - if let startBaseURLClientIDClosure = startBaseURLClientIDClosure { - return await startBaseURLClientIDClosure(baseURL, clientID) + func start(baseURL: URL, clientID: String, useEncryption: Bool) async -> Result { + startBaseURLClientIDUseEncryptionCallsCount += 1 + startBaseURLClientIDUseEncryptionReceivedArguments = (baseURL: baseURL, clientID: clientID, useEncryption: useEncryption) + startBaseURLClientIDUseEncryptionReceivedInvocations.append((baseURL: baseURL, clientID: clientID, useEncryption: useEncryption)) + if let startBaseURLClientIDUseEncryptionClosure = startBaseURLClientIDUseEncryptionClosure { + return await startBaseURLClientIDUseEncryptionClosure(baseURL, clientID, useEncryption) } else { - return startBaseURLClientIDReturnValue + return startBaseURLClientIDUseEncryptionReturnValue } } //MARK: - sendMessage diff --git a/ElementX/Sources/Screens/CallScreen/CallScreenCoordinator.swift b/ElementX/Sources/Screens/CallScreen/CallScreenCoordinator.swift index ca24f7caa..8209360ab 100644 --- a/ElementX/Sources/Screens/CallScreen/CallScreenCoordinator.swift +++ b/ElementX/Sources/Screens/CallScreen/CallScreenCoordinator.swift @@ -23,6 +23,8 @@ struct CallScreenCoordinatorParameters { let callBaseURL: URL /// A way to identify the current client against Element Call let clientID: String + /// Whether encryption should be used within the call + let useEncryption: Bool } enum CallScreenCoordinatorAction { @@ -44,7 +46,8 @@ final class CallScreenCoordinator: CoordinatorProtocol { viewModel = CallScreenViewModel(roomProxy: parameters.roomProxy, callBaseURL: parameters.callBaseURL, - clientID: parameters.clientID) + clientID: parameters.clientID, + useEncryption: parameters.useEncryption) } func start() { diff --git a/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift b/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift index f30b2125d..42feb9396 100644 --- a/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift +++ b/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift @@ -46,7 +46,10 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol /// - roomProxy: The room in which the call should be created /// - callBaseURL: Which Element Call instance should be used /// - clientID: Something to identify the current client on the Element Call side - init(roomProxy: RoomProxyProtocol, callBaseURL: URL, clientID: String) { + init(roomProxy: RoomProxyProtocol, + callBaseURL: URL, + clientID: String, + useEncryption: Bool) { self.roomProxy = roomProxy self.callBaseURL = callBaseURL self.clientID = clientID @@ -99,7 +102,7 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol .store(in: &cancellables) Task { - switch await widgetDriver.start(baseURL: callBaseURL, clientID: clientID) { + switch await widgetDriver.start(baseURL: callBaseURL, clientID: clientID, useEncryption: useEncryption) { case .success(let url): state.url = url case .failure(let error): diff --git a/ElementX/Sources/Screens/CallScreen/View/CallScreen.swift b/ElementX/Sources/Screens/CallScreen/View/CallScreen.swift index a43177fb5..5273ce8c8 100644 --- a/ElementX/Sources/Screens/CallScreen/View/CallScreen.swift +++ b/ElementX/Sources/Screens/CallScreen/View/CallScreen.swift @@ -23,8 +23,6 @@ struct CallScreen: View { var body: some View { WebView(url: context.viewState.url, viewModelContext: context) - .navigationTitle("Call") - .navigationBarTitleDisplayMode(.inline) .ignoresSafeArea(edges: .bottom) .presentationDragIndicator(.visible) .environment(\.colorScheme, .dark) @@ -164,13 +162,14 @@ struct CallScreen_Previews: PreviewProvider { let widgetDriver = ElementCallWidgetDriverMock() widgetDriver.underlyingMessagePublisher = .init() widgetDriver.underlyingActions = PassthroughSubject().eraseToAnyPublisher() - widgetDriver.startBaseURLClientIDReturnValue = .success(URL.userDirectory) + widgetDriver.startBaseURLClientIDUseEncryptionReturnValue = .success(URL.userDirectory) roomProxy.elementCallWidgetDriverReturnValue = widgetDriver return CallScreenViewModel(roomProxy: roomProxy, callBaseURL: "https://call.element.io", - clientID: "io.element.elementx") + clientID: "io.element.elementx", + useEncryption: false) }() static var previews: some View { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 531b1f044..1a6e13268 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -116,7 +116,6 @@ struct RoomScreenViewState: BindableState { var ownUserID: String - var showCallButton = false var isCallOngoing = false var bindings: RoomScreenViewStateBindings diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 08b78b83c..d872b33ca 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -264,11 +264,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol appSettings.$readReceiptsEnabled .weakAssign(to: \.state.readReceiptsEnabled, on: self) .store(in: &cancellables) - - appSettings.$elementCallEnabled - .weakAssign(to: \.state.showCallButton, on: self) - .store(in: &cancellables) - + roomProxy.members .map { members in members.reduce(into: [String: RoomMemberState]()) { dictionary, member in diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index f148433c3..4d1a9c82d 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -162,21 +162,19 @@ struct RoomScreen: View { @ViewBuilder private var callButton: some View { - if context.viewState.showCallButton { - if context.viewState.isCallOngoing { - Button { - context.send(viewAction: .presentCall) - } label: { - Label(L10n.actionJoin, icon: \.videoCallSolid) - .labelStyle(.titleAndIcon) - } - .buttonStyle(ElementCallButtonStyle()) - } else { - Button { - context.send(viewAction: .presentCall) - } label: { - CompoundIcon(\.videoCallSolid) - } + if context.viewState.isCallOngoing { + Button { + context.send(viewAction: .presentCall) + } label: { + Label(L10n.actionJoin, icon: \.videoCallSolid) + .labelStyle(.titleAndIcon) + } + .buttonStyle(ElementCallButtonStyle()) + } else { + Button { + context.send(viewAction: .presentCall) + } label: { + CompoundIcon(\.videoCallSolid) } } } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index d63694e27..3fdc4209e 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -49,8 +49,10 @@ protocol DeveloperOptionsProtocol: AnyObject { var userSuggestionsEnabled: Bool { get set } var readReceiptsEnabled: Bool { get set } var swiftUITimelineEnabled: Bool { get set } - var elementCallEnabled: Bool { get set } var chatBackupEnabled: Bool { get set } + + var elementCallBaseURL: URL { get set } + var elementCallUseEncryption: Bool { get set } } extension AppSettings: DeveloperOptionsProtocol { } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index a877ccfd0..ba5ee5643 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -19,6 +19,7 @@ import SwiftUI struct DeveloperOptionsScreen: View { @ObservedObject var context: DeveloperOptionsScreenViewModel.Context @State private var showConfetti = false + @State private var elementCallBaseURLString = "" var body: some View { Form { @@ -52,10 +53,6 @@ struct DeveloperOptionsScreen: View { Text("SwiftUI Timeline") Text("Resets on reboot") } - - Toggle(isOn: $context.elementCallEnabled) { - Text("Element Call") - } } Section("Room creation") { @@ -63,6 +60,25 @@ struct DeveloperOptionsScreen: View { Text("User suggestions") } } + + Section("Element Call") { + TextField(context.elementCallBaseURL.absoluteString, text: $elementCallBaseURLString) + .submitLabel(.done) + .onSubmit { + guard let url = URL(string: elementCallBaseURLString) else { + return + } + + context.elementCallBaseURL = url + } + .autocorrectionDisabled(true) + .autocapitalization(.none) + .foregroundColor(URL(string: elementCallBaseURLString) == nil ? .red : .primary) + + Toggle(isOn: $context.elementCallUseEncryption) { + Text("Use encryption") + } + } Section { Button { diff --git a/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriver.swift b/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriver.swift index 9fa463f69..34f709ffc 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriver.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriver.swift @@ -52,7 +52,7 @@ class ElementCallWidgetDriver: WidgetCapabilitiesProvider, ElementCallWidgetDriv self.room = room } - func start(baseURL: URL, clientID: String) async -> Result { + func start(baseURL: URL, clientID: String, useEncryption: Bool) async -> Result { guard let room = room as? Room else { return .failure(.roomInvalid) } @@ -68,7 +68,7 @@ class ElementCallWidgetDriver: WidgetCapabilitiesProvider, ElementCallWidgetDriv confineToRoom: true, font: nil, analyticsId: nil, - encryption: .unencrypted)) else { + encryption: useEncryption ? .perParticipantKeys : .unencrypted)) else { return .failure(.failedBuildingWidgetSettings) } diff --git a/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriverProtocol.swift b/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriverProtocol.swift index e44eaf56b..9bca7e0bb 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriverProtocol.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriverProtocol.swift @@ -36,7 +36,7 @@ protocol ElementCallWidgetDriverProtocol { var messagePublisher: PassthroughSubject { get } var actions: AnyPublisher { get } - func start(baseURL: URL, clientID: String) async -> Result + func start(baseURL: URL, clientID: String, useEncryption: Bool) async -> Result func sendMessage(_ message: String) async -> Result }