Remove GenericCallLinkCoordinator, merging it into CallScreen. (#3181)

* Remove incorrect message send call and fix typo.

* Add overlay coordinator presentation to the NavigationRootCoordinator.

* Remove GenericCallLinkCoordinator, merging it into CallScreen.

This will allow for picture in picture on call links when available.
This commit is contained in:
Doug
2024-08-19 17:21:25 +01:00
committed by GitHub
parent 6616d1799f
commit e0ba9925e7
18 changed files with 418 additions and 294 deletions
@@ -20,13 +20,8 @@ import SwiftUI
struct CallScreenCoordinatorParameters {
let elementCallService: ElementCallServiceProtocol
let clientProxy: ClientProxyProtocol
let roomProxy: RoomProxyProtocol
let clientID: String
let elementCallBaseURL: URL
let elementCallBaseURLOverride: URL?
let configuration: ElementCallConfiguration
let elementCallPictureInPictureEnabled: Bool
let colorScheme: ColorScheme
let appHooks: AppHooks
}
@@ -50,13 +45,8 @@ final class CallScreenCoordinator: CoordinatorProtocol {
init(parameters: CallScreenCoordinatorParameters) {
viewModel = CallScreenViewModel(elementCallService: parameters.elementCallService,
clientProxy: parameters.clientProxy,
roomProxy: parameters.roomProxy,
clientID: parameters.clientID,
elementCallBaseURL: parameters.elementCallBaseURL,
elementCallBaseURLOverride: parameters.elementCallBaseURLOverride,
configuration: parameters.configuration,
elementCallPictureInPictureEnabled: parameters.elementCallPictureInPictureEnabled,
colorScheme: parameters.colorScheme,
appHooks: parameters.appHooks)
}
@@ -23,7 +23,7 @@ typealias CallScreenViewModelType = StateStoreViewModel<CallScreenViewState, Cal
class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol {
private let elementCallService: ElementCallServiceProtocol
private let roomProxy: RoomProxyProtocol
private let configuration: ElementCallConfiguration
private let isPictureInPictureEnabled: Bool
private let widgetDriver: ElementCallWidgetDriverProtocol
@@ -42,35 +42,28 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
/// - callBaseURL: Which Element Call instance should be used
/// - clientID: Something to identify the current client on the Element Call side
init(elementCallService: ElementCallServiceProtocol,
clientProxy: ClientProxyProtocol,
roomProxy: RoomProxyProtocol,
clientID: String,
elementCallBaseURL: URL,
elementCallBaseURLOverride: URL?,
configuration: ElementCallConfiguration,
elementCallPictureInPictureEnabled: Bool,
colorScheme: ColorScheme,
appHooks: AppHooks) {
guard let deviceID = clientProxy.deviceID else { fatalError("Missing device ID for the call.") }
self.elementCallService = elementCallService
self.roomProxy = roomProxy
self.configuration = configuration
isPictureInPictureEnabled = elementCallPictureInPictureEnabled
widgetDriver = roomProxy.elementCallWidgetDriver(deviceID: deviceID)
switch configuration.kind {
case .genericCallLink(let url):
widgetDriver = GenericCallLinkWidgetDriver(url: url)
case .roomCall(let roomProxy, let clientProxy, _, _, _, _):
guard let deviceID = clientProxy.deviceID else { fatalError("Missing device ID for the call.") }
widgetDriver = roomProxy.elementCallWidgetDriver(deviceID: deviceID)
}
super.init(initialViewState: CallScreenViewState(messageHandler: Self.eventHandlerName,
script: Self.eventHandlerInjectionScript,
certificateValidator: appHooks.certificateValidatorHook))
state.bindings.javaScriptMessageHandler = { [weak self] message in
guard let self,
let message = message as? String else {
return
}
Task {
await self.widgetDriver.sendMessage(message)
}
guard let self, let message = message as? String else { return }
Task { await self.widgetDriver.handleMessage(message) }
}
elementCallService.actions
@@ -80,8 +73,8 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
switch action {
case let .setAudioEnabled(enabled, roomID):
guard roomID == roomProxy.id else {
MXLog.error("Received mute request for a different room: \(roomID) != \(roomProxy.id)")
guard roomID == configuration.callID else {
MXLog.error("Received mute request for a different room: \(roomID) != \(configuration.callID)")
return
}
@@ -114,48 +107,11 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
case .callEnded:
actionsSubject.send(.dismiss)
case .mediaStateChanged(let audioEnabled, _):
elementCallService.setAudioEnabled(audioEnabled, roomID: roomProxy.id)
elementCallService.setAudioEnabled(audioEnabled, roomID: configuration.callID)
}
}
.store(in: &cancellables)
// Wait for room states to be up to date before starting the call and notifying others
syncUpdateCancellable = clientProxy.actionsPublisher
.filter(\.isSyncUpdate)
.timeout(.seconds(5), scheduler: DispatchQueue.main)
.first() // Timeout will make the publisher complete, use first to handle both branches in the same place
.sink(receiveCompletion: { [weak self] _ in
Task { [weak self] in
guard let self else { return }
let baseURL = if let elementCallBaseURLOverride {
elementCallBaseURLOverride
} else if case .success(let wellKnown) = await clientProxy.getElementWellKnown(), let wellKnownCall = wellKnown?.call {
wellKnownCall.widgetURL
} else {
elementCallBaseURL
}
switch await widgetDriver.start(baseURL: baseURL, clientID: clientID, colorScheme: colorScheme) {
case .success(let url):
state.url = url
case .failure(let error):
MXLog.error("Failed starting ElementCall Widget Driver with error: \(error)")
state.bindings.alertInfo = .init(id: UUID(), title: L10n.errorUnknown, primaryButton: .init(title: L10n.actionOk, action: { [weak self] in
self?.actionsSubject.send(.dismiss)
}))
return
}
await elementCallService.setupCallSession(roomID: roomProxy.id, roomDisplayName: roomProxy.roomTitle)
_ = await roomProxy.sendCallNotificationIfNeeeded()
syncUpdateCancellable = nil
}
}, receiveValue: { _ in })
// Use did start otherwise there's a black box left on the screen during the pip controller animation.
NotificationCenter.default.publisher(for: .init("AVPictureInPictureControllerDidStartNotification"))
.sink { [weak self] notification in
@@ -172,6 +128,8 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
Task { try await self.state.bindings.javaScriptEvaluator?("controls.disableCompatPip()") }
}
.store(in: &cancellables)
setupCall()
}
override func process(viewAction: CallScreenViewAction) {
@@ -194,6 +152,54 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
// MARK: - Private
private func setupCall() {
switch configuration.kind {
case .genericCallLink(let url):
state.url = url
// We need widget messaging to work before enabling CallKit, otherwise mute, hangup etc do nothing.
case .roomCall(let roomProxy, let clientProxy, let clientID, let elementCallBaseURL, let elementCallBaseURLOverride, let colorScheme):
// Wait for room states to be up to date before starting the call and notifying others
syncUpdateCancellable = clientProxy.actionsPublisher
.filter(\.isSyncUpdate)
.timeout(.seconds(5), scheduler: DispatchQueue.main)
.first() // Timeout will make the publisher complete, use first to handle both branches in the same place
.sink(receiveCompletion: { [weak self] _ in
Task { [weak self] in
guard let self else { return }
let baseURL = if let elementCallBaseURLOverride {
elementCallBaseURLOverride
} else if case .success(let wellKnown) = await clientProxy.getElementWellKnown(), let wellKnownCall = wellKnown?.call {
wellKnownCall.widgetURL
} else {
elementCallBaseURL
}
switch await widgetDriver.start(baseURL: baseURL, clientID: clientID, colorScheme: colorScheme) {
case .success(let url):
state.url = url
case .failure(let error):
MXLog.error("Failed starting ElementCall Widget Driver with error: \(error)")
state.bindings.alertInfo = .init(id: UUID(),
title: L10n.errorUnknown,
primaryButton: .init(title: L10n.actionOk) {
self.actionsSubject.send(.dismiss)
})
return
}
await elementCallService.setupCallSession(roomID: roomProxy.id, roomDisplayName: roomProxy.roomTitle)
_ = await roomProxy.sendCallNotificationIfNeeded()
syncUpdateCancellable = nil
}
}, receiveValue: { _ in })
}
}
private func handleBackwardsNavigation() {
#if targetEnvironment(simulator)
if UIDevice.current.isPhone {
@@ -237,7 +243,6 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
do {
let data = try JSONEncoder().encode(message)
let json = String(decoding: data, as: UTF8.self)
_ = await widgetDriver.sendMessage(json)
await postJSONToWidget(json)
} catch {
@@ -34,11 +34,11 @@ struct CallScreen: View {
Image(systemSymbol: .chevronBackward)
.fontWeight(.semibold)
}
.offset(y: -8)
// .padding(.leading, -8) // Fixes the button alignment, but harder to tap.
}
}
}
.alert(item: $context.alertInfo)
}
@ViewBuilder
@@ -50,8 +50,6 @@ struct CallScreen: View {
// This URL is stable, forces view reloads if this representable is ever reused for another url
.id(context.viewState.url)
.ignoresSafeArea(edges: .bottom)
.presentationDragIndicator(.visible)
.alert(item: $context.alertInfo)
}
}
}
@@ -211,7 +209,7 @@ struct CallScreen_Previews: PreviewProvider {
clientProxy.deviceID = "call-device-id"
let roomProxy = RoomProxyMock()
roomProxy.sendCallNotificationIfNeeededReturnValue = .success(())
roomProxy.sendCallNotificationIfNeededReturnValue = .success(())
let widgetDriver = ElementCallWidgetDriverMock()
widgetDriver.underlyingMessagePublisher = .init()
@@ -221,13 +219,13 @@ struct CallScreen_Previews: PreviewProvider {
roomProxy.elementCallWidgetDriverDeviceIDReturnValue = widgetDriver
return CallScreenViewModel(elementCallService: ElementCallServiceMock(.init()),
clientProxy: clientProxy,
roomProxy: roomProxy,
clientID: "io.element.elementx",
elementCallBaseURL: "https://call.element.io",
elementCallBaseURLOverride: nil,
configuration: .init(roomProxy: roomProxy,
clientProxy: clientProxy,
clientID: "io.element.elementx",
elementCallBaseURL: "https://call.element.io",
elementCallBaseURLOverride: nil,
colorScheme: .light),
elementCallPictureInPictureEnabled: false,
colorScheme: .light,
appHooks: AppHooks())
}()