mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
Introduce support for multiple windows on mac and iPad OS
Once the app starts the WindowManager is configured with SwiftUI's environment OpenWindowAction. It can then be used to register coordinators (that provide the toPresentable view) and an optional flow coordinator (as most of the screens are part of a flow, especially rooms). Once a coordinator is registed, the WindowManager invokes the OpenWindowAction which in turn makes the Application call its newly introduced WindowManagerWindowType WindowGroup's block to instantiate a new visual window rooting that view. The WindowManager is also responsible for wrapping the presentable in a disappearance block and clean up the coordinator stack. # Conflicts: # ElementX/Sources/Application/AppCoordinator.swift
This commit is contained in:
committed by
Stefan Ceriu
parent
91a10f4d51
commit
f1ef968975
@@ -13,10 +13,14 @@ import SwiftUI
|
|||||||
class AccessibilityTestsAppCoordinator: AppCoordinatorProtocol {
|
class AccessibilityTestsAppCoordinator: AppCoordinatorProtocol {
|
||||||
var windowManager: any SecureWindowManagerProtocol
|
var windowManager: any SecureWindowManagerProtocol
|
||||||
|
|
||||||
func handleDeepLink(_ url: URL, isExternalURL: Bool) -> Bool {
|
func handleDeepLink(_ url: URL, isExternalURL: Bool, windowType: WindowManagerWindowType?) -> Bool {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleAppRoute(_ appRoute: AppRoute, windowType: WindowManagerWindowType?) {
|
||||||
|
fatalError("Not implemented.")
|
||||||
|
}
|
||||||
|
|
||||||
func handlePotentialPhishingAttempt(url: URL, openURLAction: @escaping (URL) -> Void) -> Bool {
|
func handlePotentialPhishingAttempt(url: URL, openURLAction: @escaping (URL) -> Void) -> Bool {
|
||||||
fatalError("Not implemented")
|
fatalError("Not implemented")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
.sink { [weak self] action in
|
.sink { [weak self] action in
|
||||||
switch action {
|
switch action {
|
||||||
case .startCall(let roomID, let isVoiceCall):
|
case .startCall(let roomID, let isVoiceCall):
|
||||||
self?.handleAppRoute(.call(roomID: roomID, isVoiceCall: isVoiceCall))
|
self?.handleAppRoute(.call(roomID: roomID, isVoiceCall: isVoiceCall), windowType: nil)
|
||||||
case .receivedIncomingCallRequest:
|
case .receivedIncomingCallRequest:
|
||||||
// When reporting a VoIP call through the CXProvider's `reportNewIncomingVoIPPushPayload`
|
// When reporting a VoIP call through the CXProvider's `reportNewIncomingVoIPPushPayload`
|
||||||
// the UIApplication states don't change and syncing is neither started nor ran on
|
// the UIApplication states don't change and syncing is neither started nor ran on
|
||||||
@@ -234,13 +234,40 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDeepLink(_ url: URL, isExternalURL: Bool) -> Bool {
|
func handleAppRoute(_ appRoute: AppRoute, windowType: WindowManagerWindowType?) {
|
||||||
|
if let windowType {
|
||||||
|
windowManager.handleRoute(appRoute, windowType: windowType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var handled = false
|
||||||
|
|
||||||
|
switch appRoute {
|
||||||
|
case .accountProvisioningLink:
|
||||||
|
if let authenticationFlowCoordinator {
|
||||||
|
authenticationFlowCoordinator.handleAppRoute(appRoute, animated: appMediator.appState == .active)
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if let userSessionFlowCoordinator {
|
||||||
|
userSessionFlowCoordinator.handleAppRoute(appRoute, animated: appMediator.appState == .active)
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !handled {
|
||||||
|
storedAppRoute = appRoute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDeepLink(_ url: URL, isExternalURL: Bool, windowType: WindowManagerWindowType?) -> Bool {
|
||||||
// Parse into an AppRoute to redirect these in a type safe way.
|
// Parse into an AppRoute to redirect these in a type safe way.
|
||||||
|
|
||||||
if let route = appRouteURLParser.route(from: url) {
|
if let route = appRouteURLParser.route(from: url) {
|
||||||
switch route {
|
switch route {
|
||||||
case .accountProvisioningLink:
|
case .accountProvisioningLink:
|
||||||
handleAppRoute(route)
|
handleAppRoute(route,
|
||||||
|
windowType: windowType)
|
||||||
case .genericCallLink(let url):
|
case .genericCallLink(let url):
|
||||||
if let userSessionFlowCoordinator {
|
if let userSessionFlowCoordinator {
|
||||||
userSessionFlowCoordinator.handleAppRoute(route, animated: true)
|
userSessionFlowCoordinator.handleAppRoute(route, animated: true)
|
||||||
@@ -249,33 +276,43 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
}
|
}
|
||||||
case .userProfile(let userID):
|
case .userProfile(let userID):
|
||||||
if isExternalURL {
|
if isExternalURL {
|
||||||
handleAppRoute(route)
|
handleAppRoute(route,
|
||||||
|
windowType: windowType)
|
||||||
} else {
|
} else {
|
||||||
handleAppRoute(.roomMemberDetails(userID: userID))
|
handleAppRoute(.roomMemberDetails(userID: userID),
|
||||||
|
windowType: windowType)
|
||||||
}
|
}
|
||||||
case .room(let roomID, let via):
|
case .room(let roomID, let via):
|
||||||
if isExternalURL {
|
if isExternalURL {
|
||||||
handleAppRoute(route)
|
handleAppRoute(route,
|
||||||
|
windowType: windowType)
|
||||||
} else {
|
} else {
|
||||||
handleAppRoute(.childRoom(roomID: roomID, via: via))
|
handleAppRoute(.childRoom(roomID: roomID, via: via),
|
||||||
|
windowType: windowType)
|
||||||
}
|
}
|
||||||
case .roomAlias(let alias):
|
case .roomAlias(let alias):
|
||||||
if isExternalURL {
|
if isExternalURL {
|
||||||
handleAppRoute(route)
|
handleAppRoute(route,
|
||||||
|
windowType: windowType)
|
||||||
} else {
|
} else {
|
||||||
handleAppRoute(.childRoomAlias(alias))
|
handleAppRoute(.childRoomAlias(alias),
|
||||||
|
windowType: windowType)
|
||||||
}
|
}
|
||||||
case .event(let eventID, let roomID, let via):
|
case .event(let eventID, let roomID, let via):
|
||||||
if isExternalURL {
|
if isExternalURL {
|
||||||
handleAppRoute(route)
|
handleAppRoute(route,
|
||||||
|
windowType: windowType)
|
||||||
} else {
|
} else {
|
||||||
handleAppRoute(.childEvent(eventID: eventID, roomID: roomID, via: via))
|
handleAppRoute(.childEvent(eventID: eventID, roomID: roomID, via: via),
|
||||||
|
windowType: windowType)
|
||||||
}
|
}
|
||||||
case .eventOnRoomAlias(let eventID, let alias):
|
case .eventOnRoomAlias(let eventID, let alias):
|
||||||
if isExternalURL {
|
if isExternalURL {
|
||||||
handleAppRoute(route)
|
handleAppRoute(route,
|
||||||
|
windowType: windowType)
|
||||||
} else {
|
} else {
|
||||||
handleAppRoute(.childEventOnRoomAlias(eventID: eventID, alias: alias))
|
handleAppRoute(.childEventOnRoomAlias(eventID: eventID, alias: alias),
|
||||||
|
windowType: windowType)
|
||||||
}
|
}
|
||||||
case .share(let payload):
|
case .share(let payload):
|
||||||
guard isExternalURL else {
|
guard isExternalURL else {
|
||||||
@@ -284,7 +321,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try handleAppRoute(.share(payload.withDefaultTemporaryDirectory()))
|
try handleAppRoute(.share(payload.withDefaultTemporaryDirectory()),
|
||||||
|
windowType: windowType)
|
||||||
} catch {
|
} catch {
|
||||||
MXLog.error("Failed moving payload out of the app group container: \(error)")
|
MXLog.error("Failed moving payload out of the app group container: \(error)")
|
||||||
}
|
}
|
||||||
@@ -309,7 +347,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
}
|
}
|
||||||
|
|
||||||
MXLog.info("Starting call in room: \(roomIdentifier)")
|
MXLog.info("Starting call in room: \(roomIdentifier)")
|
||||||
handleAppRoute(AppRoute.call(roomID: roomIdentifier, isVoiceCall: false))
|
handleAppRoute(AppRoute.call(roomID: roomIdentifier, isVoiceCall: false), windowType: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - AuthenticationFlowCoordinatorDelegate
|
// MARK: - AuthenticationFlowCoordinatorDelegate
|
||||||
@@ -363,15 +401,15 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
} else {
|
} else {
|
||||||
storedRoomsToAwait = [roomID]
|
storedRoomsToAwait = [roomID]
|
||||||
}
|
}
|
||||||
handleAppRoute(.room(roomID: roomID, via: []))
|
handleAppRoute(.room(roomID: roomID, via: []), windowType: nil)
|
||||||
} else if appSettings.threadsEnabled, let threadRootEventID = content.threadRootEventID {
|
} else if appSettings.threadsEnabled, let threadRootEventID = content.threadRootEventID {
|
||||||
handleAppRoute(.thread(roomID: roomID, threadRootEventID: threadRootEventID, focusEventID: eventID))
|
handleAppRoute(.thread(roomID: roomID, threadRootEventID: threadRootEventID, focusEventID: eventID), windowType: nil)
|
||||||
} else if let eventID {
|
} else if let eventID {
|
||||||
// Only track main timeline event deeplinking
|
// Only track main timeline event deeplinking
|
||||||
ServiceLocator.shared.analytics.signpost.startTransaction(.notificationToMessage)
|
ServiceLocator.shared.analytics.signpost.startTransaction(.notificationToMessage)
|
||||||
handleAppRoute(.event(eventID: eventID, roomID: roomID, via: []))
|
handleAppRoute(.event(eventID: eventID, roomID: roomID, via: []), windowType: nil)
|
||||||
} else {
|
} else {
|
||||||
handleAppRoute(.room(roomID: roomID, via: []))
|
handleAppRoute(.room(roomID: roomID, via: []), windowType: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -906,27 +944,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleAppRoute(_ appRoute: AppRoute) {
|
|
||||||
var handled = false
|
|
||||||
|
|
||||||
switch appRoute {
|
|
||||||
case .accountProvisioningLink:
|
|
||||||
if let authenticationFlowCoordinator {
|
|
||||||
authenticationFlowCoordinator.handleAppRoute(appRoute, animated: appMediator.appState == .active)
|
|
||||||
handled = true
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if let userSessionFlowCoordinator {
|
|
||||||
userSessionFlowCoordinator.handleAppRoute(appRoute, animated: appMediator.appState == .active)
|
|
||||||
handled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !handled {
|
|
||||||
storedAppRoute = appRoute
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func clearCache() {
|
private func clearCache() {
|
||||||
guard let userSession else {
|
guard let userSession else {
|
||||||
fatalError("User session not setup")
|
fatalError("User session not setup")
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import Foundation
|
|||||||
protocol AppCoordinatorProtocol: CoordinatorProtocol {
|
protocol AppCoordinatorProtocol: CoordinatorProtocol {
|
||||||
var windowManager: SecureWindowManagerProtocol { get }
|
var windowManager: SecureWindowManagerProtocol { get }
|
||||||
|
|
||||||
@discardableResult func handleDeepLink(_ url: URL, isExternalURL: Bool) -> Bool
|
@discardableResult func handleDeepLink(_ url: URL, isExternalURL: Bool, windowType: WindowManagerWindowType?) -> Bool
|
||||||
|
|
||||||
|
func handleAppRoute(_ appRoute: AppRoute, windowType: WindowManagerWindowType?)
|
||||||
|
|
||||||
func handlePotentialPhishingAttempt(url: URL, openURLAction: @escaping (URL) -> Void) -> Bool
|
func handlePotentialPhishingAttempt(url: URL, openURLAction: @escaping (URL) -> Void) -> Bool
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import SwiftUI
|
|||||||
struct Application: App {
|
struct Application: App {
|
||||||
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
||||||
@Environment(\.openURL) private var openURL
|
@Environment(\.openURL) private var openURL
|
||||||
|
@Environment(\.openWindow) private var openWindow
|
||||||
|
|
||||||
private var appCoordinator: AppCoordinatorProtocol!
|
private var appCoordinator: AppCoordinatorProtocol!
|
||||||
|
|
||||||
@@ -39,19 +40,7 @@ struct Application: App {
|
|||||||
Divider().ignoresSafeArea()
|
Divider().ignoresSafeArea()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.environment(\.openURL, OpenURLAction { url in
|
.environment(\.openURL, openURLAction(appCoordinator: appCoordinator, windowType: nil))
|
||||||
if appCoordinator.handleDeepLink(url, isExternalURL: false) {
|
|
||||||
return .handled
|
|
||||||
}
|
|
||||||
|
|
||||||
if appCoordinator.handlePotentialPhishingAttempt(url: url, openURLAction: { url in
|
|
||||||
openURL(url, isExternalURL: false)
|
|
||||||
}) {
|
|
||||||
return .handled
|
|
||||||
}
|
|
||||||
|
|
||||||
return .systemAction
|
|
||||||
})
|
|
||||||
.onOpenURL { url in
|
.onOpenURL { url in
|
||||||
openURL(url, isExternalURL: true)
|
openURL(url, isExternalURL: true)
|
||||||
}
|
}
|
||||||
@@ -62,14 +51,42 @@ struct Application: App {
|
|||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
appCoordinator.start()
|
appCoordinator.start()
|
||||||
|
appCoordinator.windowManager.configure(with: openWindow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.handlesExternalEvents(matching: ["*"])
|
||||||
|
|
||||||
|
// This is invoked in response of the WindowManager receiving a register
|
||||||
|
// coordinator request and invoking the `OpenWindowAction` with which
|
||||||
|
// it's configured in the task above.
|
||||||
|
WindowGroup(for: WindowManagerWindowType.self) { $type in
|
||||||
|
if let type {
|
||||||
|
appCoordinator.windowManager.windowForType(type)
|
||||||
|
.environment(\.openURL, openURLAction(appCoordinator: appCoordinator, windowType: type))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openURLAction(appCoordinator: AppCoordinatorProtocol, windowType: WindowManagerWindowType?) -> OpenURLAction {
|
||||||
|
.init { url in
|
||||||
|
if appCoordinator.handleDeepLink(url, isExternalURL: false, windowType: windowType) {
|
||||||
|
return .handled
|
||||||
|
}
|
||||||
|
|
||||||
|
if appCoordinator.handlePotentialPhishingAttempt(url: url, openURLAction: { url in
|
||||||
|
openURL(url, isExternalURL: false)
|
||||||
|
}) {
|
||||||
|
return .handled
|
||||||
|
}
|
||||||
|
|
||||||
|
return .systemAction
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func openURL(_ url: URL, isExternalURL: Bool) {
|
private func openURL(_ url: URL, isExternalURL: Bool) {
|
||||||
if !appCoordinator.handleDeepLink(url, isExternalURL: isExternalURL) {
|
if !appCoordinator.handleDeepLink(url, isExternalURL: isExternalURL, windowType: nil) {
|
||||||
openURLInSystemBrowser(url)
|
openURLInSystemBrowser(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ class WindowManager: SecureWindowManagerProtocol {
|
|||||||
private(set) var globalSearchWindow: UIWindow!
|
private(set) var globalSearchWindow: UIWindow!
|
||||||
private(set) var alternateWindow: UIWindow!
|
private(set) var alternateWindow: UIWindow!
|
||||||
|
|
||||||
|
private(set) var openWindowAction: OpenWindowAction!
|
||||||
|
|
||||||
var windows: [UIWindow] {
|
var windows: [UIWindow] {
|
||||||
[mainWindow, overlayWindow, globalSearchWindow, alternateWindow]
|
[mainWindow, overlayWindow, globalSearchWindow, alternateWindow]
|
||||||
}
|
}
|
||||||
@@ -29,6 +31,8 @@ class WindowManager: SecureWindowManagerProtocol {
|
|||||||
/// A duration that allows window switching to wait a couple of frames to avoid a transition through black.
|
/// A duration that allows window switching to wait a couple of frames to avoid a transition through black.
|
||||||
private let windowHideDelay = Duration.milliseconds(33)
|
private let windowHideDelay = Duration.milliseconds(33)
|
||||||
|
|
||||||
|
private var coordinators: [WindowManagerWindowType: (coordinator: CoordinatorProtocol, flowCoordinator: FlowCoordinatorProtocol?)] = [:]
|
||||||
|
|
||||||
init(appDelegate: AppDelegate) {
|
init(appDelegate: AppDelegate) {
|
||||||
self.appDelegate = appDelegate
|
self.appDelegate = appDelegate
|
||||||
}
|
}
|
||||||
@@ -54,6 +58,29 @@ class WindowManager: SecureWindowManagerProtocol {
|
|||||||
delegate?.windowManagerDidConfigureWindows(self)
|
delegate?.windowManagerDidConfigureWindows(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configure(with openWindowAction: OpenWindowAction) {
|
||||||
|
self.openWindowAction = openWindowAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerCoordinator(_ coordinator: CoordinatorProtocol, flowCoordinator: FlowCoordinatorProtocol?, forWindowType type: WindowManagerWindowType) {
|
||||||
|
coordinators[type] = (coordinator, flowCoordinator)
|
||||||
|
openWindowAction(value: type)
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowForType(_ type: WindowManagerWindowType) -> AnyView? {
|
||||||
|
guard let coordinator = coordinators[type]?.coordinator else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return coordinator.toPresentable()
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRoute(_ appRoute: AppRoute, windowType: WindowManagerWindowType) {
|
||||||
|
if let flowCoordinator = coordinators[windowType]?.flowCoordinator {
|
||||||
|
flowCoordinator.handleAppRoute(appRoute, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func switchToMain() {
|
func switchToMain() {
|
||||||
mainWindow.isHidden = false
|
mainWindow.isHidden = false
|
||||||
overlayWindow.isHidden = false
|
overlayWindow.isHidden = false
|
||||||
|
|||||||
@@ -8,6 +8,11 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
enum WindowManagerWindowType: Hashable, Codable {
|
||||||
|
case room(roomID: String)
|
||||||
|
case settings
|
||||||
|
}
|
||||||
|
|
||||||
protocol SecureWindowManagerDelegate: AnyObject {
|
protocol SecureWindowManagerDelegate: AnyObject {
|
||||||
/// The window manager has configured its windows.
|
/// The window manager has configured its windows.
|
||||||
func windowManagerDidConfigureWindows(_ windowManager: SecureWindowManagerProtocol)
|
func windowManagerDidConfigureWindows(_ windowManager: SecureWindowManagerProtocol)
|
||||||
@@ -20,6 +25,12 @@ protocol SecureWindowManagerProtocol: WindowManagerProtocol {
|
|||||||
/// Configures the window manager to operate on the supplied scene.
|
/// Configures the window manager to operate on the supplied scene.
|
||||||
func configure(with windowScene: UIWindowScene)
|
func configure(with windowScene: UIWindowScene)
|
||||||
|
|
||||||
|
func configure(with openWindowAction: OpenWindowAction)
|
||||||
|
|
||||||
|
func handleRoute(_ appRoute: AppRoute, windowType: WindowManagerWindowType)
|
||||||
|
|
||||||
|
func windowForType(_ type: WindowManagerWindowType) -> AnyView?
|
||||||
|
|
||||||
/// Shows the main and overlay window combo, hiding the alternate window.
|
/// Shows the main and overlay window combo, hiding the alternate window.
|
||||||
func switchToMain()
|
func switchToMain()
|
||||||
|
|
||||||
@@ -43,6 +54,8 @@ protocol WindowManagerProtocol: AnyObject, OrientationManagerProtocol {
|
|||||||
/// All the windows being managed
|
/// All the windows being managed
|
||||||
var windows: [UIWindow] { get }
|
var windows: [UIWindow] { get }
|
||||||
|
|
||||||
|
func registerCoordinator(_ coordinator: CoordinatorProtocol, flowCoordinator: FlowCoordinatorProtocol?, forWindowType type: WindowManagerWindowType)
|
||||||
|
|
||||||
/// Makes the global search window key. Used to get automatic text field focus.
|
/// Makes the global search window key. Used to get automatic text field focus.
|
||||||
func showGlobalSearch()
|
func showGlobalSearch()
|
||||||
|
|
||||||
|
|||||||
@@ -321,7 +321,11 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
if case .space = detailState {
|
if case .space = detailState {
|
||||||
dismissRoomFlow(animated: animated)
|
dismissRoomFlow(animated: animated)
|
||||||
}
|
}
|
||||||
startRoomFlow(roomID: roomID, via: via, entryPoint: entryPoint, animated: animated)
|
startRoomFlow(roomID: roomID,
|
||||||
|
via: via,
|
||||||
|
entryPoint: entryPoint,
|
||||||
|
detached: false,
|
||||||
|
animated: animated)
|
||||||
}
|
}
|
||||||
actionsSubject.send(.hideCallScreenOverlay) // Turn any active call into a PiP so that navigation from a notification is visible to the user.
|
actionsSubject.send(.hideCallScreenOverlay) // Turn any active call into a PiP so that navigation from a notification is visible to the user.
|
||||||
}
|
}
|
||||||
@@ -393,6 +397,8 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
switch action {
|
switch action {
|
||||||
case .presentRoom(let roomID):
|
case .presentRoom(let roomID):
|
||||||
handleAppRoute(.room(roomID: roomID, via: []), animated: true)
|
handleAppRoute(.room(roomID: roomID, via: []), animated: true)
|
||||||
|
case .detachRoom(let roomID):
|
||||||
|
startRoomFlow(roomID: roomID, via: [], entryPoint: .room, detached: true, animated: true)
|
||||||
case .presentRoomDetails(let roomID):
|
case .presentRoomDetails(let roomID):
|
||||||
handleAppRoute(.roomDetails(roomID: roomID), animated: true)
|
handleAppRoute(.roomDetails(roomID: roomID), animated: true)
|
||||||
case .presentReportRoom(let roomID):
|
case .presentReportRoom(let roomID):
|
||||||
@@ -516,8 +522,10 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
private func startRoomFlow(roomID: String,
|
private func startRoomFlow(roomID: String,
|
||||||
via: [String],
|
via: [String],
|
||||||
entryPoint: RoomFlowCoordinatorEntryPoint,
|
entryPoint: RoomFlowCoordinatorEntryPoint,
|
||||||
|
detached: Bool,
|
||||||
animated: Bool) {
|
animated: Bool) {
|
||||||
let navigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
let navigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: detached ? nil : navigationSplitCoordinator)
|
||||||
|
|
||||||
let coordinator = RoomFlowCoordinator(roomID: roomID,
|
let coordinator = RoomFlowCoordinator(roomID: roomID,
|
||||||
isChildFlow: false,
|
isChildFlow: false,
|
||||||
navigationStackCoordinator: navigationStackCoordinator,
|
navigationStackCoordinator: navigationStackCoordinator,
|
||||||
@@ -539,9 +547,14 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
if detached {
|
||||||
|
flowParameters.windowManager.registerCoordinator(navigationStackCoordinator,
|
||||||
|
flowCoordinator: coordinator,
|
||||||
|
forWindowType: .room(roomID: roomID))
|
||||||
|
} else {
|
||||||
roomFlowCoordinator = coordinator
|
roomFlowCoordinator = coordinator
|
||||||
|
|
||||||
navigationSplitCoordinator.setDetailCoordinator(navigationStackCoordinator, animated: animated)
|
navigationSplitCoordinator.setDetailCoordinator(navigationStackCoordinator, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
switch entryPoint {
|
switch entryPoint {
|
||||||
case .room:
|
case .room:
|
||||||
|
|||||||
@@ -21358,6 +21358,47 @@ class WindowManagerMock: WindowManagerProtocol, @unchecked Sendable {
|
|||||||
var alternateWindow: UIWindow!
|
var alternateWindow: UIWindow!
|
||||||
var windows: [UIWindow] = []
|
var windows: [UIWindow] = []
|
||||||
|
|
||||||
|
//MARK: - registerCoordinator
|
||||||
|
|
||||||
|
var registerCoordinatorFlowCoordinatorForWindowTypeUnderlyingCallsCount = 0
|
||||||
|
var registerCoordinatorFlowCoordinatorForWindowTypeCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return registerCoordinatorFlowCoordinatorForWindowTypeUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = registerCoordinatorFlowCoordinatorForWindowTypeUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
registerCoordinatorFlowCoordinatorForWindowTypeUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
registerCoordinatorFlowCoordinatorForWindowTypeUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var registerCoordinatorFlowCoordinatorForWindowTypeCalled: Bool {
|
||||||
|
return registerCoordinatorFlowCoordinatorForWindowTypeCallsCount > 0
|
||||||
|
}
|
||||||
|
var registerCoordinatorFlowCoordinatorForWindowTypeReceivedArguments: (coordinator: CoordinatorProtocol, flowCoordinator: FlowCoordinatorProtocol?, type: WindowManagerWindowType)?
|
||||||
|
var registerCoordinatorFlowCoordinatorForWindowTypeReceivedInvocations: [(coordinator: CoordinatorProtocol, flowCoordinator: FlowCoordinatorProtocol?, type: WindowManagerWindowType)] = []
|
||||||
|
var registerCoordinatorFlowCoordinatorForWindowTypeClosure: ((CoordinatorProtocol, FlowCoordinatorProtocol?, WindowManagerWindowType) -> Void)?
|
||||||
|
|
||||||
|
func registerCoordinator(_ coordinator: CoordinatorProtocol, flowCoordinator: FlowCoordinatorProtocol?, forWindowType type: WindowManagerWindowType) {
|
||||||
|
registerCoordinatorFlowCoordinatorForWindowTypeCallsCount += 1
|
||||||
|
registerCoordinatorFlowCoordinatorForWindowTypeReceivedArguments = (coordinator: coordinator, flowCoordinator: flowCoordinator, type: type)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.registerCoordinatorFlowCoordinatorForWindowTypeReceivedInvocations.append((coordinator: coordinator, flowCoordinator: flowCoordinator, type: type))
|
||||||
|
}
|
||||||
|
registerCoordinatorFlowCoordinatorForWindowTypeClosure?(coordinator, flowCoordinator, type)
|
||||||
|
}
|
||||||
//MARK: - showGlobalSearch
|
//MARK: - showGlobalSearch
|
||||||
|
|
||||||
var showGlobalSearchUnderlyingCallsCount = 0
|
var showGlobalSearchUnderlyingCallsCount = 0
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ struct HomeScreenCoordinatorParameters {
|
|||||||
|
|
||||||
enum HomeScreenCoordinatorAction {
|
enum HomeScreenCoordinatorAction {
|
||||||
case presentRoom(roomIdentifier: String)
|
case presentRoom(roomIdentifier: String)
|
||||||
|
case detachRoom(roomIdentifier: String)
|
||||||
case presentRoomDetails(roomIdentifier: String)
|
case presentRoomDetails(roomIdentifier: String)
|
||||||
case presentReportRoom(roomIdentifier: String)
|
case presentReportRoom(roomIdentifier: String)
|
||||||
case presentDeclineAndBlock(userID: String, roomID: String)
|
case presentDeclineAndBlock(userID: String, roomID: String)
|
||||||
@@ -65,6 +66,8 @@ final class HomeScreenCoordinator: CoordinatorProtocol {
|
|||||||
switch action {
|
switch action {
|
||||||
case .presentRoom(let roomIdentifier):
|
case .presentRoom(let roomIdentifier):
|
||||||
actionsSubject.send(.presentRoom(roomIdentifier: roomIdentifier))
|
actionsSubject.send(.presentRoom(roomIdentifier: roomIdentifier))
|
||||||
|
case .detachRoom(let roomIdentifier):
|
||||||
|
actionsSubject.send(.detachRoom(roomIdentifier: roomIdentifier))
|
||||||
case .presentRoomDetails(roomIdentifier: let roomIdentifier):
|
case .presentRoomDetails(roomIdentifier: let roomIdentifier):
|
||||||
actionsSubject.send(.presentRoomDetails(roomIdentifier: roomIdentifier))
|
actionsSubject.send(.presentRoomDetails(roomIdentifier: roomIdentifier))
|
||||||
case .presentReportRoom(let roomIdentifier):
|
case .presentReportRoom(let roomIdentifier):
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import UIKit
|
|||||||
|
|
||||||
enum HomeScreenViewModelAction {
|
enum HomeScreenViewModelAction {
|
||||||
case presentRoom(roomIdentifier: String)
|
case presentRoom(roomIdentifier: String)
|
||||||
|
case detachRoom(roomIdentifier: String)
|
||||||
case presentRoomDetails(roomIdentifier: String)
|
case presentRoomDetails(roomIdentifier: String)
|
||||||
case presentReportRoom(roomIdentifier: String)
|
case presentReportRoom(roomIdentifier: String)
|
||||||
case presentDeclineAndBlock(userID: String, roomID: String)
|
case presentDeclineAndBlock(userID: String, roomID: String)
|
||||||
@@ -30,6 +31,7 @@ enum HomeScreenViewModelAction {
|
|||||||
|
|
||||||
enum HomeScreenViewAction {
|
enum HomeScreenViewAction {
|
||||||
case selectRoom(roomIdentifier: String)
|
case selectRoom(roomIdentifier: String)
|
||||||
|
case detachRoom(roomIdentifier: String)
|
||||||
case showRoomDetails(roomIdentifier: String)
|
case showRoomDetails(roomIdentifier: String)
|
||||||
case leaveRoom(roomIdentifier: String)
|
case leaveRoom(roomIdentifier: String)
|
||||||
case confirmLeaveRoom(roomIdentifier: String)
|
case confirmLeaveRoom(roomIdentifier: String)
|
||||||
|
|||||||
@@ -173,6 +173,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .selectRoom(let roomIdentifier):
|
case .selectRoom(let roomIdentifier):
|
||||||
actionsSubject.send(.presentRoom(roomIdentifier: roomIdentifier))
|
actionsSubject.send(.presentRoom(roomIdentifier: roomIdentifier))
|
||||||
|
case .detachRoom(let roomIdentifier):
|
||||||
|
actionsSubject.send(.detachRoom(roomIdentifier: roomIdentifier))
|
||||||
case .showRoomDetails(let roomIdentifier):
|
case .showRoomDetails(let roomIdentifier):
|
||||||
actionsSubject.send(.presentRoomDetails(roomIdentifier: roomIdentifier))
|
actionsSubject.send(.presentRoomDetails(roomIdentifier: roomIdentifier))
|
||||||
case .leaveRoom(let roomIdentifier):
|
case .leaveRoom(let roomIdentifier):
|
||||||
|
|||||||
@@ -34,7 +34,18 @@ struct HomeScreenRoomList: View {
|
|||||||
let isSelected = context.viewState.selectedRoomID == room.id
|
let isSelected = context.viewState.selectedRoomID == room.id
|
||||||
|
|
||||||
HomeScreenRoomCell(room: room, isSelected: isSelected, mediaProvider: context.mediaProvider, action: context.send)
|
HomeScreenRoomCell(room: room, isSelected: isSelected, mediaProvider: context.mediaProvider, action: context.send)
|
||||||
|
.simultaneousGesture(TapGesture(count: 2).onEnded {
|
||||||
|
context.send(viewAction: .detachRoom(roomIdentifier: room.id))
|
||||||
|
})
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
|
if !UIDevice.current.isPhone {
|
||||||
|
Button {
|
||||||
|
context.send(viewAction: .detachRoom(roomIdentifier: room.id))
|
||||||
|
} label: {
|
||||||
|
Label("Open in new window", icon: \.popOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if room.badges.isDotShown {
|
if room.badges.isDotShown {
|
||||||
Button {
|
Button {
|
||||||
context.send(viewAction: .markRoomAsRead(roomIdentifier: room.id))
|
context.send(viewAction: .markRoomAsRead(roomIdentifier: room.id))
|
||||||
|
|||||||
@@ -65,7 +65,11 @@ class UITestsAppCoordinator: AppCoordinatorProtocol, SecureWindowManagerDelegate
|
|||||||
fatalError("Not implemented.")
|
fatalError("Not implemented.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDeepLink(_ url: URL, isExternalURL: Bool) -> Bool {
|
func handleDeepLink(_ url: URL, isExternalURL: Bool, windowType: WindowManagerWindowType?) -> Bool {
|
||||||
|
fatalError("Not implemented.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAppRoute(_ appRoute: AppRoute, windowType: WindowManagerWindowType?) {
|
||||||
fatalError("Not implemented.")
|
fatalError("Not implemented.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ class UnitTestsAppCoordinator: AppCoordinatorProtocol {
|
|||||||
fatalError("Not implemented.")
|
fatalError("Not implemented.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDeepLink(_ url: URL, isExternalURL: Bool) -> Bool {
|
func handleDeepLink(_ url: URL, isExternalURL: Bool, windowType: WindowManagerWindowType?) -> Bool {
|
||||||
|
fatalError("Not implemented.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAppRoute(_ appRoute: AppRoute, windowType: WindowManagerWindowType?) {
|
||||||
fatalError("Not implemented.")
|
fatalError("Not implemented.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,11 @@
|
|||||||
<string>INSendMessageIntent</string>
|
<string>INSendMessageIntent</string>
|
||||||
<string>INStartCallIntent</string>
|
<string>INStartCallIntent</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>audio</string>
|
<string>audio</string>
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ targets:
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
UIApplicationSceneManifest: {
|
||||||
|
UIApplicationSupportsMultipleScenes: true
|
||||||
|
}
|
||||||
UISupportedInterfaceOrientations: [
|
UISupportedInterfaceOrientations: [
|
||||||
UIInterfaceOrientationPortrait,
|
UIInterfaceOrientationPortrait,
|
||||||
UIInterfaceOrientationPortraitUpsideDown,
|
UIInterfaceOrientationPortraitUpsideDown,
|
||||||
|
|||||||
Reference in New Issue
Block a user