mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
Navigation support for upcoming Element Call Picture in Picture mode. (#3174)
This commit is contained in:
@@ -288,6 +288,9 @@ final class AppSettings {
|
|||||||
|
|
||||||
@UserPreference(key: UserDefaultsKeys.timelineItemAuthenticityEnabled, defaultValue: false, storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.timelineItemAuthenticityEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||||
var timelineItemAuthenticityEnabled
|
var timelineItemAuthenticityEnabled
|
||||||
|
|
||||||
|
// Not user configurable as it depends on work in EC too.
|
||||||
|
let elementCallPictureInPictureEnabled = false
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,28 @@ class NavigationSplitCoordinator: CoordinatorProtocol, ObservableObject, CustomS
|
|||||||
fullScreenCoverModule?.coordinator
|
fullScreenCoverModule?.coordinator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Published fileprivate var overlayModule: NavigationModule? {
|
||||||
|
didSet {
|
||||||
|
if let oldValue {
|
||||||
|
logPresentationChange("Remove overlay", oldValue)
|
||||||
|
oldValue.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let overlayModule {
|
||||||
|
logPresentationChange("Set overlay", overlayModule)
|
||||||
|
overlayModule.coordinator?.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The currently displayed overlay coordinator
|
||||||
|
var overlayCoordinator: (any CoordinatorProtocol)? {
|
||||||
|
overlayModule?.coordinator
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OverlayPresentationMode { case fullScreen, minimized }
|
||||||
|
@Published fileprivate var overlayPresentationMode: OverlayPresentationMode = .minimized
|
||||||
|
|
||||||
fileprivate var compactLayoutRootModule: NavigationModule? {
|
fileprivate var compactLayoutRootModule: NavigationModule? {
|
||||||
if let sidebarNavigationStackCoordinator = sidebarModule?.coordinator as? NavigationStackCoordinator {
|
if let sidebarNavigationStackCoordinator = sidebarModule?.coordinator as? NavigationStackCoordinator {
|
||||||
if let sidebarRootModule = sidebarNavigationStackCoordinator.rootModule {
|
if let sidebarRootModule = sidebarNavigationStackCoordinator.rootModule {
|
||||||
@@ -282,6 +304,47 @@ class NavigationSplitCoordinator: CoordinatorProtocol, ObservableObject, CustomS
|
|||||||
fullScreenCoverModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
|
fullScreenCoverModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Present an overlay on top of the split view
|
||||||
|
/// - Parameters:
|
||||||
|
/// - coordinator: the coordinator to display
|
||||||
|
/// - presentationMode: how the coordinator should be presented
|
||||||
|
/// - animated: whether the transition should be animated
|
||||||
|
/// - dismissalCallback: called when the overlay has been dismissed, programatically or otherwise
|
||||||
|
func setOverlayCoordinator(_ coordinator: (any CoordinatorProtocol)?,
|
||||||
|
presentationMode: OverlayPresentationMode = .fullScreen,
|
||||||
|
animated: Bool = true,
|
||||||
|
dismissalCallback: (() -> Void)? = nil) {
|
||||||
|
guard let coordinator else {
|
||||||
|
overlayModule = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if overlayModule?.coordinator === coordinator {
|
||||||
|
fatalError("Cannot use the same coordinator more than once")
|
||||||
|
}
|
||||||
|
|
||||||
|
var transaction = Transaction()
|
||||||
|
transaction.disablesAnimations = !animated
|
||||||
|
|
||||||
|
withTransaction(transaction) {
|
||||||
|
overlayPresentationMode = presentationMode
|
||||||
|
overlayModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the presentation of the overlay coordinator.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - mode: The type of presentation to use.
|
||||||
|
/// - animated: whether the transition should be animated
|
||||||
|
func setOverlayPresentationMode(_ mode: OverlayPresentationMode, animated: Bool = true) {
|
||||||
|
var transaction = Transaction()
|
||||||
|
transaction.disablesAnimations = !animated
|
||||||
|
|
||||||
|
withTransaction(transaction) {
|
||||||
|
overlayPresentationMode = mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - CoordinatorProtocol
|
// MARK: - CoordinatorProtocol
|
||||||
|
|
||||||
@@ -385,6 +448,16 @@ private struct NavigationSplitCoordinatorView: View {
|
|||||||
module.coordinator?.toPresentable()
|
module.coordinator?.toPresentable()
|
||||||
.id(module.id)
|
.id(module.id)
|
||||||
}
|
}
|
||||||
|
.overlay {
|
||||||
|
Group {
|
||||||
|
if let coordinator = navigationSplitCoordinator.overlayModule?.coordinator {
|
||||||
|
coordinator.toPresentable()
|
||||||
|
.opacity(navigationSplitCoordinator.overlayPresentationMode == .minimized ? 0 : 1)
|
||||||
|
.transition(.opacity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animation(.elementDefault, value: navigationSplitCoordinator.overlayPresentationMode)
|
||||||
|
}
|
||||||
// Handle `horizontalSizeClass` changes breaking the navigation bar
|
// Handle `horizontalSizeClass` changes breaking the navigation bar
|
||||||
// https://github.com/element-hq/element-x-ios/issues/617
|
// https://github.com/element-hq/element-x-ios/issues/617
|
||||||
.onChange(of: horizontalSizeClass) { value in
|
.onChange(of: horizontalSizeClass) { value in
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AVKit
|
||||||
import Combine
|
import Combine
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@@ -557,7 +558,14 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
// MARK: Calls
|
// MARK: Calls
|
||||||
|
|
||||||
|
private var callScreenPictureInPictureController: AVPictureInPictureController?
|
||||||
private func presentCallScreen(roomProxy: RoomProxyProtocol) {
|
private func presentCallScreen(roomProxy: RoomProxyProtocol) {
|
||||||
|
guard elementCallService.ongoingCallRoomID != roomProxy.id else {
|
||||||
|
MXLog.info("Returning to existing call.")
|
||||||
|
callScreenPictureInPictureController?.stopPictureInPicture()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let colorScheme: ColorScheme = appMediator.windowManager.mainWindow.traitCollection.userInterfaceStyle == .light ? .light : .dark
|
let colorScheme: ColorScheme = appMediator.windowManager.mainWindow.traitCollection.userInterfaceStyle == .light ? .light : .dark
|
||||||
let callScreenCoordinator = CallScreenCoordinator(parameters: .init(elementCallService: elementCallService,
|
let callScreenCoordinator = CallScreenCoordinator(parameters: .init(elementCallService: elementCallService,
|
||||||
clientProxy: userSession.clientProxy,
|
clientProxy: userSession.clientProxy,
|
||||||
@@ -565,19 +573,29 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
clientID: InfoPlistReader.main.bundleIdentifier,
|
clientID: InfoPlistReader.main.bundleIdentifier,
|
||||||
elementCallBaseURL: appSettings.elementCallBaseURL,
|
elementCallBaseURL: appSettings.elementCallBaseURL,
|
||||||
elementCallBaseURLOverride: appSettings.elementCallBaseURLOverride,
|
elementCallBaseURLOverride: appSettings.elementCallBaseURLOverride,
|
||||||
|
elementCallPictureInPictureEnabled: appSettings.elementCallPictureInPictureEnabled,
|
||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
appHooks: appHooks))
|
appHooks: appHooks))
|
||||||
|
|
||||||
callScreenCoordinator.actions
|
callScreenCoordinator.actions
|
||||||
.sink { [weak self] action in
|
.sink { [weak self] action in
|
||||||
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
|
case .pictureInPictureStarted(let controller):
|
||||||
|
MXLog.info("Hiding call for PiP presentation.")
|
||||||
|
callScreenPictureInPictureController = controller
|
||||||
|
navigationSplitCoordinator.setOverlayPresentationMode(.minimized)
|
||||||
|
case .pictureInPictureStopped:
|
||||||
|
MXLog.info("Restoring call after PiP presentation.")
|
||||||
|
navigationSplitCoordinator.setOverlayPresentationMode(.fullScreen)
|
||||||
|
callScreenPictureInPictureController = nil
|
||||||
case .dismiss:
|
case .dismiss:
|
||||||
self?.navigationSplitCoordinator.setSheetCoordinator(nil)
|
navigationSplitCoordinator.setOverlayCoordinator(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
navigationSplitCoordinator.setSheetCoordinator(callScreenCoordinator, animated: true)
|
navigationSplitCoordinator.setOverlayCoordinator(callScreenCoordinator, animated: true)
|
||||||
|
|
||||||
analytics.track(screen: .RoomCall)
|
analytics.track(screen: .RoomCall)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4894,6 +4894,7 @@ class ElementCallServiceMock: ElementCallServiceProtocol {
|
|||||||
set(value) { underlyingActions = value }
|
set(value) { underlyingActions = value }
|
||||||
}
|
}
|
||||||
var underlyingActions: AnyPublisher<ElementCallServiceAction, Never>!
|
var underlyingActions: AnyPublisher<ElementCallServiceAction, Never>!
|
||||||
|
var ongoingCallRoomID: String?
|
||||||
|
|
||||||
//MARK: - setClientProxy
|
//MARK: - setClientProxy
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AVKit
|
||||||
import Combine
|
import Combine
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@@ -24,11 +25,17 @@ struct CallScreenCoordinatorParameters {
|
|||||||
let clientID: String
|
let clientID: String
|
||||||
let elementCallBaseURL: URL
|
let elementCallBaseURL: URL
|
||||||
let elementCallBaseURLOverride: URL?
|
let elementCallBaseURLOverride: URL?
|
||||||
|
let elementCallPictureInPictureEnabled: Bool
|
||||||
let colorScheme: ColorScheme
|
let colorScheme: ColorScheme
|
||||||
let appHooks: AppHooks
|
let appHooks: AppHooks
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CallScreenCoordinatorAction {
|
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
|
case dismiss
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +55,7 @@ final class CallScreenCoordinator: CoordinatorProtocol {
|
|||||||
clientID: parameters.clientID,
|
clientID: parameters.clientID,
|
||||||
elementCallBaseURL: parameters.elementCallBaseURL,
|
elementCallBaseURL: parameters.elementCallBaseURL,
|
||||||
elementCallBaseURLOverride: parameters.elementCallBaseURLOverride,
|
elementCallBaseURLOverride: parameters.elementCallBaseURLOverride,
|
||||||
|
elementCallPictureInPictureEnabled: parameters.elementCallPictureInPictureEnabled,
|
||||||
colorScheme: parameters.colorScheme,
|
colorScheme: parameters.colorScheme,
|
||||||
appHooks: parameters.appHooks)
|
appHooks: parameters.appHooks)
|
||||||
}
|
}
|
||||||
@@ -57,6 +65,10 @@ final class CallScreenCoordinator: CoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
|
case .pictureInPictureStarted(let controller):
|
||||||
|
actionsSubject.send(.pictureInPictureStarted(controller))
|
||||||
|
case .pictureInPictureStopped:
|
||||||
|
actionsSubject.send(.pictureInPictureStopped)
|
||||||
case .dismiss:
|
case .dismiss:
|
||||||
actionsSubject.send(.dismiss)
|
actionsSubject.send(.dismiss)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,12 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AVKit
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum CallScreenViewModelAction {
|
enum CallScreenViewModelAction {
|
||||||
|
case pictureInPictureStarted(AVPictureInPictureController?)
|
||||||
|
case pictureInPictureStopped
|
||||||
case dismiss
|
case dismiss
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,4 +42,5 @@ struct Bindings {
|
|||||||
|
|
||||||
enum CallScreenViewAction {
|
enum CallScreenViewAction {
|
||||||
case urlChanged(URL?)
|
case urlChanged(URL?)
|
||||||
|
case navigateBack
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AVKit
|
||||||
import CallKit
|
import CallKit
|
||||||
import Combine
|
import Combine
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
@@ -23,6 +24,7 @@ typealias CallScreenViewModelType = StateStoreViewModel<CallScreenViewState, Cal
|
|||||||
class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol {
|
class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol {
|
||||||
private let elementCallService: ElementCallServiceProtocol
|
private let elementCallService: ElementCallServiceProtocol
|
||||||
private let roomProxy: RoomProxyProtocol
|
private let roomProxy: RoomProxyProtocol
|
||||||
|
private let isPictureInPictureEnabled: Bool
|
||||||
|
|
||||||
private let widgetDriver: ElementCallWidgetDriverProtocol
|
private let widgetDriver: ElementCallWidgetDriverProtocol
|
||||||
|
|
||||||
@@ -45,12 +47,14 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
|
|||||||
clientID: String,
|
clientID: String,
|
||||||
elementCallBaseURL: URL,
|
elementCallBaseURL: URL,
|
||||||
elementCallBaseURLOverride: URL?,
|
elementCallBaseURLOverride: URL?,
|
||||||
|
elementCallPictureInPictureEnabled: Bool,
|
||||||
colorScheme: ColorScheme,
|
colorScheme: ColorScheme,
|
||||||
appHooks: AppHooks) {
|
appHooks: AppHooks) {
|
||||||
guard let deviceID = clientProxy.deviceID else { fatalError("Missing device ID for the call.") }
|
guard let deviceID = clientProxy.deviceID else { fatalError("Missing device ID for the call.") }
|
||||||
|
|
||||||
self.elementCallService = elementCallService
|
self.elementCallService = elementCallService
|
||||||
self.roomProxy = roomProxy
|
self.roomProxy = roomProxy
|
||||||
|
isPictureInPictureEnabled = elementCallPictureInPictureEnabled
|
||||||
|
|
||||||
widgetDriver = roomProxy.elementCallWidgetDriver(deviceID: deviceID)
|
widgetDriver = roomProxy.elementCallWidgetDriver(deviceID: deviceID)
|
||||||
|
|
||||||
@@ -151,6 +155,23 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
|
|||||||
syncUpdateCancellable = nil
|
syncUpdateCancellable = nil
|
||||||
}
|
}
|
||||||
}, receiveValue: { _ in })
|
}, 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) {
|
override func process(viewAction: CallScreenViewAction) {
|
||||||
@@ -158,6 +179,8 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
|
|||||||
case .urlChanged(let url):
|
case .urlChanged(let url):
|
||||||
guard let url else { return }
|
guard let url else { return }
|
||||||
MXLog.info("URL changed to: \(url)")
|
MXLog.info("URL changed to: \(url)")
|
||||||
|
case .navigateBack:
|
||||||
|
handleBackwardsNavigation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +194,29 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
|
|||||||
|
|
||||||
// MARK: - Private
|
// 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 {
|
private func setAudioEnabled(_ enabled: Bool) async {
|
||||||
let message = ElementCallWidgetMessage(direction: .toWidget,
|
let message = ElementCallWidgetMessage(direction: .toWidget,
|
||||||
action: .mediaState,
|
action: .mediaState,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import SFSafeSymbols
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WebKit
|
import WebKit
|
||||||
|
|
||||||
@@ -22,6 +23,26 @@ struct CallScreen: View {
|
|||||||
@ObservedObject var context: CallScreenViewModel.Context
|
@ObservedObject var context: CallScreenViewModel.Context
|
||||||
|
|
||||||
var body: some View {
|
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 {
|
if context.viewState.url == nil {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
} else {
|
} else {
|
||||||
@@ -187,6 +208,7 @@ struct CallScreen_Previews: PreviewProvider {
|
|||||||
static let viewModel = {
|
static let viewModel = {
|
||||||
let clientProxy = ClientProxyMock()
|
let clientProxy = ClientProxyMock()
|
||||||
clientProxy.getElementWellKnownReturnValue = .success(nil)
|
clientProxy.getElementWellKnownReturnValue = .success(nil)
|
||||||
|
clientProxy.deviceID = "call-device-id"
|
||||||
|
|
||||||
let roomProxy = RoomProxyMock()
|
let roomProxy = RoomProxyMock()
|
||||||
roomProxy.sendCallNotificationIfNeeededReturnValue = .success(())
|
roomProxy.sendCallNotificationIfNeeededReturnValue = .success(())
|
||||||
@@ -204,6 +226,7 @@ struct CallScreen_Previews: PreviewProvider {
|
|||||||
clientID: "io.element.elementx",
|
clientID: "io.element.elementx",
|
||||||
elementCallBaseURL: "https://call.element.io",
|
elementCallBaseURL: "https://call.element.io",
|
||||||
elementCallBaseURLOverride: nil,
|
elementCallBaseURLOverride: nil,
|
||||||
|
elementCallPictureInPictureEnabled: false,
|
||||||
colorScheme: .light,
|
colorScheme: .light,
|
||||||
appHooks: AppHooks())
|
appHooks: AppHooks())
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -156,6 +156,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
actionsSubject.send(.presentRoomDetails)
|
actionsSubject.send(.presentRoomDetails)
|
||||||
case .displayCall:
|
case .displayCall:
|
||||||
actionsSubject.send(.presentCallScreen)
|
actionsSubject.send(.presentCallScreen)
|
||||||
|
case .removeComposerFocus:
|
||||||
|
composerViewModel.process(timelineAction: .removeFocus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ enum RoomScreenViewModelAction {
|
|||||||
case displayPinnedEventsTimeline
|
case displayPinnedEventsTimeline
|
||||||
case displayRoomDetails
|
case displayRoomDetails
|
||||||
case displayCall
|
case displayCall
|
||||||
|
case removeComposerFocus
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomScreenViewAction {
|
enum RoomScreenViewAction {
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
actionsSubject.send(.displayRoomDetails)
|
actionsSubject.send(.displayRoomDetails)
|
||||||
case .displayCall:
|
case .displayCall:
|
||||||
actionsSubject.send(.displayCall)
|
actionsSubject.send(.displayCall)
|
||||||
|
actionsSubject.send(.removeComposerFocus)
|
||||||
analyticsService.trackInteraction(name: .MobileRoomCallButton)
|
analyticsService.trackInteraction(name: .MobileRoomCallButton)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
|||||||
|
|
||||||
private var ongoingCallID: CallID?
|
private var ongoingCallID: CallID?
|
||||||
|
|
||||||
|
var ongoingCallRoomID: String? { ongoingCallID?.roomID }
|
||||||
|
|
||||||
private let actionsSubject: PassthroughSubject<ElementCallServiceAction, Never> = .init()
|
private let actionsSubject: PassthroughSubject<ElementCallServiceAction, Never> = .init()
|
||||||
var actions: AnyPublisher<ElementCallServiceAction, Never> {
|
var actions: AnyPublisher<ElementCallServiceAction, Never> {
|
||||||
actionsSubject.eraseToAnyPublisher()
|
actionsSubject.eraseToAnyPublisher()
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ enum ElementCallServiceAction {
|
|||||||
protocol ElementCallServiceProtocol {
|
protocol ElementCallServiceProtocol {
|
||||||
var actions: AnyPublisher<ElementCallServiceAction, Never> { get }
|
var actions: AnyPublisher<ElementCallServiceAction, Never> { get }
|
||||||
|
|
||||||
|
var ongoingCallRoomID: String? { get }
|
||||||
|
|
||||||
func setClientProxy(_ clientProxy: ClientProxyProtocol)
|
func setClientProxy(_ clientProxy: ClientProxyProtocol)
|
||||||
|
|
||||||
func setupCallSession(roomID: String, roomDisplayName: String) async
|
func setupCallSession(roomID: String, roomDisplayName: String) async
|
||||||
|
|||||||
@@ -99,6 +99,52 @@ class NavigationSplitCoordinatorTests: XCTestCase {
|
|||||||
assertCoordinatorsEqual(someOtherSheetCoordinator, navigationSplitCoordinator.sheetCoordinator)
|
assertCoordinatorsEqual(someOtherSheetCoordinator, navigationSplitCoordinator.sheetCoordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testFullScreenCover() {
|
||||||
|
let sidebarCoordinator = SomeTestCoordinator()
|
||||||
|
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator)
|
||||||
|
let detailCoordinator = SomeTestCoordinator()
|
||||||
|
navigationSplitCoordinator.setDetailCoordinator(detailCoordinator)
|
||||||
|
|
||||||
|
let fullScreenCoordinator = SomeTestCoordinator()
|
||||||
|
navigationSplitCoordinator.setFullScreenCoverCoordinator(fullScreenCoordinator)
|
||||||
|
|
||||||
|
assertCoordinatorsEqual(sidebarCoordinator, navigationSplitCoordinator.sidebarCoordinator)
|
||||||
|
assertCoordinatorsEqual(detailCoordinator, navigationSplitCoordinator.detailCoordinator)
|
||||||
|
assertCoordinatorsEqual(fullScreenCoordinator, navigationSplitCoordinator.fullScreenCoverCoordinator)
|
||||||
|
|
||||||
|
navigationSplitCoordinator.setFullScreenCoverCoordinator(nil)
|
||||||
|
|
||||||
|
assertCoordinatorsEqual(sidebarCoordinator, navigationSplitCoordinator.sidebarCoordinator)
|
||||||
|
assertCoordinatorsEqual(detailCoordinator, navigationSplitCoordinator.detailCoordinator)
|
||||||
|
XCTAssertNil(navigationSplitCoordinator.fullScreenCoverCoordinator)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOverlay() {
|
||||||
|
let sidebarCoordinator = SomeTestCoordinator()
|
||||||
|
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator)
|
||||||
|
let detailCoordinator = SomeTestCoordinator()
|
||||||
|
navigationSplitCoordinator.setDetailCoordinator(detailCoordinator)
|
||||||
|
|
||||||
|
let overlayCoordinator = SomeTestCoordinator()
|
||||||
|
navigationSplitCoordinator.setOverlayCoordinator(overlayCoordinator)
|
||||||
|
|
||||||
|
assertCoordinatorsEqual(sidebarCoordinator, navigationSplitCoordinator.sidebarCoordinator)
|
||||||
|
assertCoordinatorsEqual(detailCoordinator, navigationSplitCoordinator.detailCoordinator)
|
||||||
|
assertCoordinatorsEqual(overlayCoordinator, navigationSplitCoordinator.overlayCoordinator)
|
||||||
|
|
||||||
|
// The coordinator should still be retained when changing the presentation mode.
|
||||||
|
navigationSplitCoordinator.setOverlayPresentationMode(.minimized)
|
||||||
|
assertCoordinatorsEqual(overlayCoordinator, navigationSplitCoordinator.overlayCoordinator)
|
||||||
|
navigationSplitCoordinator.setOverlayPresentationMode(.fullScreen)
|
||||||
|
assertCoordinatorsEqual(overlayCoordinator, navigationSplitCoordinator.overlayCoordinator)
|
||||||
|
|
||||||
|
navigationSplitCoordinator.setOverlayCoordinator(nil)
|
||||||
|
|
||||||
|
assertCoordinatorsEqual(sidebarCoordinator, navigationSplitCoordinator.sidebarCoordinator)
|
||||||
|
assertCoordinatorsEqual(detailCoordinator, navigationSplitCoordinator.detailCoordinator)
|
||||||
|
XCTAssertNil(navigationSplitCoordinator.overlayCoordinator)
|
||||||
|
}
|
||||||
|
|
||||||
func testSidebarReplacementCallbacks() {
|
func testSidebarReplacementCallbacks() {
|
||||||
let sidebarCoordinator = SomeTestCoordinator()
|
let sidebarCoordinator = SomeTestCoordinator()
|
||||||
|
|
||||||
@@ -135,6 +181,43 @@ class NavigationSplitCoordinatorTests: XCTestCase {
|
|||||||
waitForExpectations(timeout: 1.0)
|
waitForExpectations(timeout: 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testFullScreenCoverDismissalCallback() {
|
||||||
|
let fullScreenCoordinator = SomeTestCoordinator()
|
||||||
|
|
||||||
|
let expectation = expectation(description: "Wait for callback")
|
||||||
|
navigationSplitCoordinator.setFullScreenCoverCoordinator(fullScreenCoordinator) {
|
||||||
|
expectation.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
navigationSplitCoordinator.setFullScreenCoverCoordinator(nil)
|
||||||
|
waitForExpectations(timeout: 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOverlayDismissalCallback() {
|
||||||
|
let overlayCoordinator = SomeTestCoordinator()
|
||||||
|
|
||||||
|
let expectation = expectation(description: "Wait for callback")
|
||||||
|
navigationSplitCoordinator.setOverlayCoordinator(overlayCoordinator) {
|
||||||
|
expectation.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
navigationSplitCoordinator.setOverlayCoordinator(nil)
|
||||||
|
waitForExpectations(timeout: 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOverlayDismissalCallbackWhenChangingMode() {
|
||||||
|
let overlayCoordinator = SomeTestCoordinator()
|
||||||
|
|
||||||
|
let expectation = expectation(description: "Wait for callback")
|
||||||
|
expectation.isInverted = true
|
||||||
|
navigationSplitCoordinator.setOverlayCoordinator(overlayCoordinator) {
|
||||||
|
expectation.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
navigationSplitCoordinator.setOverlayPresentationMode(.minimized)
|
||||||
|
waitForExpectations(timeout: 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
func testEmbeddedStackPresentsSheetThroughSplit() {
|
func testEmbeddedStackPresentsSheetThroughSplit() {
|
||||||
let sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
let sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||||
sidebarNavigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
sidebarNavigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||||
|
|||||||
Reference in New Issue
Block a user