mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Navigation support for upcoming Element Call Picture in Picture mode. (#3174)
This commit is contained in:
@@ -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())
|
||||
}()
|
||||
|
||||
Reference in New Issue
Block a user