Navigation support for upcoming Element Call Picture in Picture mode. (#3174)

This commit is contained in:
Doug
2024-08-16 11:25:36 +01:00
committed by GitHub
parent 4f5b652608
commit ebf7c00eeb
14 changed files with 273 additions and 2 deletions
@@ -14,6 +14,7 @@
// limitations under the License.
//
import AVKit
import Combine
import SwiftUI
@@ -24,11 +25,17 @@ struct CallScreenCoordinatorParameters {
let clientID: String
let elementCallBaseURL: URL
let elementCallBaseURLOverride: URL?
let elementCallPictureInPictureEnabled: Bool
let colorScheme: ColorScheme
let appHooks: AppHooks
}
enum CallScreenCoordinatorAction {
/// The call is still ongoing but the user wishes to navigate around the app.
case pictureInPictureStarted(AVPictureInPictureController?)
/// The call is hidden and the user wishes to return to it.
case pictureInPictureStopped
/// The call is finished and the screen is done with.
case dismiss
}
@@ -48,6 +55,7 @@ final class CallScreenCoordinator: CoordinatorProtocol {
clientID: parameters.clientID,
elementCallBaseURL: parameters.elementCallBaseURL,
elementCallBaseURLOverride: parameters.elementCallBaseURLOverride,
elementCallPictureInPictureEnabled: parameters.elementCallPictureInPictureEnabled,
colorScheme: parameters.colorScheme,
appHooks: parameters.appHooks)
}
@@ -57,6 +65,10 @@ final class CallScreenCoordinator: CoordinatorProtocol {
guard let self else { return }
switch action {
case .pictureInPictureStarted(let controller):
actionsSubject.send(.pictureInPictureStarted(controller))
case .pictureInPictureStopped:
actionsSubject.send(.pictureInPictureStopped)
case .dismiss:
actionsSubject.send(.dismiss)
}
@@ -14,9 +14,12 @@
// limitations under the License.
//
import AVKit
import Foundation
enum CallScreenViewModelAction {
case pictureInPictureStarted(AVPictureInPictureController?)
case pictureInPictureStopped
case dismiss
}
@@ -39,4 +42,5 @@ struct Bindings {
enum CallScreenViewAction {
case urlChanged(URL?)
case navigateBack
}
@@ -14,6 +14,7 @@
// limitations under the License.
//
import AVKit
import CallKit
import Combine
import SwiftUI
@@ -23,6 +24,7 @@ typealias CallScreenViewModelType = StateStoreViewModel<CallScreenViewState, Cal
class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol {
private let elementCallService: ElementCallServiceProtocol
private let roomProxy: RoomProxyProtocol
private let isPictureInPictureEnabled: Bool
private let widgetDriver: ElementCallWidgetDriverProtocol
@@ -45,12 +47,14 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
clientID: String,
elementCallBaseURL: URL,
elementCallBaseURLOverride: URL?,
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
isPictureInPictureEnabled = elementCallPictureInPictureEnabled
widgetDriver = roomProxy.elementCallWidgetDriver(deviceID: deviceID)
@@ -151,6 +155,23 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
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
guard let self else { return }
let controller = notification.object as? AVPictureInPictureController
actionsSubject.send(.pictureInPictureStarted(controller))
}
.store(in: &cancellables)
NotificationCenter.default.publisher(for: .init("AVPictureInPictureControllerWillStopNotification"))
.sink { [weak self] _ in
guard let self else { return }
actionsSubject.send(.pictureInPictureStopped)
Task { try await self.state.bindings.javaScriptEvaluator?("controls.disableCompatPip()") }
}
.store(in: &cancellables)
}
override func process(viewAction: CallScreenViewAction) {
@@ -158,6 +179,8 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
case .urlChanged(let url):
guard let url else { return }
MXLog.info("URL changed to: \(url)")
case .navigateBack:
handleBackwardsNavigation()
}
}
@@ -171,6 +194,29 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
// MARK: - Private
private func handleBackwardsNavigation() {
#if targetEnvironment(simulator)
if UIDevice.current.isPhone {
MXLog.warning("The iPhone simulator doesn't support PiP.")
actionsSubject.send(.dismiss)
return
}
#endif
guard isPictureInPictureEnabled, state.url != nil else {
actionsSubject.send(.dismiss)
return
}
Task {
try await state.bindings.javaScriptEvaluator?("controls.enableCompatPip()")
// Enable this check when implemented on web.
// if result as? Bool != true {
// actionsSubject.send(.dismiss)
// }
}
}
private func setAudioEnabled(_ enabled: Bool) async {
let message = ElementCallWidgetMessage(direction: .toWidget,
action: .mediaState,
@@ -15,6 +15,7 @@
//
import Combine
import SFSafeSymbols
import SwiftUI
import WebKit
@@ -22,6 +23,26 @@ struct CallScreen: View {
@ObservedObject var context: CallScreenViewModel.Context
var body: some View {
NavigationStack {
content
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button { context.send(viewAction: .navigateBack) } label: {
Image(systemSymbol: .chevronBackward)
.fontWeight(.semibold)
}
.offset(y: -8)
// .padding(.leading, -8) // Fixes the button alignment, but harder to tap.
}
}
}
}
@ViewBuilder
var content: some View {
if context.viewState.url == nil {
ProgressView()
} else {
@@ -187,6 +208,7 @@ struct CallScreen_Previews: PreviewProvider {
static let viewModel = {
let clientProxy = ClientProxyMock()
clientProxy.getElementWellKnownReturnValue = .success(nil)
clientProxy.deviceID = "call-device-id"
let roomProxy = RoomProxyMock()
roomProxy.sendCallNotificationIfNeeededReturnValue = .success(())
@@ -204,6 +226,7 @@ struct CallScreen_Previews: PreviewProvider {
clientID: "io.element.elementx",
elementCallBaseURL: "https://call.element.io",
elementCallBaseURLOverride: nil,
elementCallPictureInPictureEnabled: false,
colorScheme: .light,
appHooks: AppHooks())
}()