diff --git a/ElementX/Sources/AccessibilityTests/AccessibilityTestsAppCoordinator.swift b/ElementX/Sources/AccessibilityTests/AccessibilityTestsAppCoordinator.swift
index 04f37afe6..7ab95c2eb 100644
--- a/ElementX/Sources/AccessibilityTests/AccessibilityTestsAppCoordinator.swift
+++ b/ElementX/Sources/AccessibilityTests/AccessibilityTestsAppCoordinator.swift
@@ -13,10 +13,14 @@ import SwiftUI
class AccessibilityTestsAppCoordinator: AppCoordinatorProtocol {
var windowManager: any SecureWindowManagerProtocol
- 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.")
+ }
+
func handlePotentialPhishingAttempt(url: URL, openURLAction: @escaping (URL) -> Void) -> Bool {
fatalError("Not implemented")
}
diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift
index daf867484..e1f39bdb4 100644
--- a/ElementX/Sources/Application/AppCoordinator.swift
+++ b/ElementX/Sources/Application/AppCoordinator.swift
@@ -176,7 +176,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
.sink { [weak self] action in
switch action {
case .startCall(let roomID, let isVoiceCall):
- self?.handleAppRoute(.call(roomID: roomID, isVoiceCall: isVoiceCall))
+ self?.handleAppRoute(.call(roomID: roomID, isVoiceCall: isVoiceCall), windowType: nil)
case .receivedIncomingCallRequest:
// When reporting a VoIP call through the CXProvider's `reportNewIncomingVoIPPushPayload`
// the UIApplication states don't change and syncing is neither started nor ran on
@@ -233,14 +233,41 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
secondaryButton: .init(title: L10n.actionContinue) { openURLAction(confirmationParameters.internalURL) })
return true
}
+
+ 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) -> Bool {
+ func handleDeepLink(_ url: URL, isExternalURL: Bool, windowType: WindowManagerWindowType?) -> Bool {
// Parse into an AppRoute to redirect these in a type safe way.
if let route = appRouteURLParser.route(from: url) {
switch route {
case .accountProvisioningLink:
- handleAppRoute(route)
+ handleAppRoute(route,
+ windowType: windowType)
case .genericCallLink(let url):
if let userSessionFlowCoordinator {
userSessionFlowCoordinator.handleAppRoute(route, animated: true)
@@ -249,33 +276,43 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
}
case .userProfile(let userID):
if isExternalURL {
- handleAppRoute(route)
+ handleAppRoute(route,
+ windowType: windowType)
} else {
- handleAppRoute(.roomMemberDetails(userID: userID))
+ handleAppRoute(.roomMemberDetails(userID: userID),
+ windowType: windowType)
}
case .room(let roomID, let via):
if isExternalURL {
- handleAppRoute(route)
+ handleAppRoute(route,
+ windowType: windowType)
} else {
- handleAppRoute(.childRoom(roomID: roomID, via: via))
+ handleAppRoute(.childRoom(roomID: roomID, via: via),
+ windowType: windowType)
}
case .roomAlias(let alias):
if isExternalURL {
- handleAppRoute(route)
+ handleAppRoute(route,
+ windowType: windowType)
} else {
- handleAppRoute(.childRoomAlias(alias))
+ handleAppRoute(.childRoomAlias(alias),
+ windowType: windowType)
}
case .event(let eventID, let roomID, let via):
if isExternalURL {
- handleAppRoute(route)
+ handleAppRoute(route,
+ windowType: windowType)
} 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):
if isExternalURL {
- handleAppRoute(route)
+ handleAppRoute(route,
+ windowType: windowType)
} else {
- handleAppRoute(.childEventOnRoomAlias(eventID: eventID, alias: alias))
+ handleAppRoute(.childEventOnRoomAlias(eventID: eventID, alias: alias),
+ windowType: windowType)
}
case .share(let payload):
guard isExternalURL else {
@@ -284,7 +321,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
}
do {
- try handleAppRoute(.share(payload.withDefaultTemporaryDirectory()))
+ try handleAppRoute(.share(payload.withDefaultTemporaryDirectory()),
+ windowType: windowType)
} catch {
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)")
- handleAppRoute(AppRoute.call(roomID: roomIdentifier, isVoiceCall: false))
+ handleAppRoute(AppRoute.call(roomID: roomIdentifier, isVoiceCall: false), windowType: nil)
}
// MARK: - AuthenticationFlowCoordinatorDelegate
@@ -363,15 +401,15 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
} else {
storedRoomsToAwait = [roomID]
}
- handleAppRoute(.room(roomID: roomID, via: []))
+ handleAppRoute(.room(roomID: roomID, via: []), windowType: nil)
} 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 {
// Only track main timeline event deeplinking
ServiceLocator.shared.analytics.signpost.startTransaction(.notificationToMessage)
- handleAppRoute(.event(eventID: eventID, roomID: roomID, via: []))
+ handleAppRoute(.event(eventID: eventID, roomID: roomID, via: []), windowType: nil)
} else {
- handleAppRoute(.room(roomID: roomID, via: []))
+ handleAppRoute(.room(roomID: roomID, via: []), windowType: nil)
}
}
@@ -906,27 +944,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
.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() {
guard let userSession else {
fatalError("User session not setup")
diff --git a/ElementX/Sources/Application/AppCoordinatorProtocol.swift b/ElementX/Sources/Application/AppCoordinatorProtocol.swift
index 4835a4b7d..096e1ee61 100644
--- a/ElementX/Sources/Application/AppCoordinatorProtocol.swift
+++ b/ElementX/Sources/Application/AppCoordinatorProtocol.swift
@@ -12,7 +12,9 @@ import Foundation
protocol AppCoordinatorProtocol: CoordinatorProtocol {
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
diff --git a/ElementX/Sources/Application/Application.swift b/ElementX/Sources/Application/Application.swift
index 280fa3f70..f142a8842 100644
--- a/ElementX/Sources/Application/Application.swift
+++ b/ElementX/Sources/Application/Application.swift
@@ -12,6 +12,7 @@ import SwiftUI
struct Application: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@Environment(\.openURL) private var openURL
+ @Environment(\.openWindow) private var openWindow
private var appCoordinator: AppCoordinatorProtocol!
@@ -39,19 +40,7 @@ struct Application: App {
Divider().ignoresSafeArea()
}
}
- .environment(\.openURL, OpenURLAction { url in
- if appCoordinator.handleDeepLink(url, isExternalURL: false) {
- return .handled
- }
-
- if appCoordinator.handlePotentialPhishingAttempt(url: url, openURLAction: { url in
- openURL(url, isExternalURL: false)
- }) {
- return .handled
- }
-
- return .systemAction
- })
+ .environment(\.openURL, openURLAction(appCoordinator: appCoordinator, windowType: nil))
.onOpenURL { url in
openURL(url, isExternalURL: true)
}
@@ -62,14 +51,42 @@ struct Application: App {
}
.task {
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
private func openURL(_ url: URL, isExternalURL: Bool) {
- if !appCoordinator.handleDeepLink(url, isExternalURL: isExternalURL) {
+ if !appCoordinator.handleDeepLink(url, isExternalURL: isExternalURL, windowType: nil) {
openURLInSystemBrowser(url)
}
}
diff --git a/ElementX/Sources/Application/Windowing/WindowManager.swift b/ElementX/Sources/Application/Windowing/WindowManager.swift
index 2808488bc..33f0534f7 100644
--- a/ElementX/Sources/Application/Windowing/WindowManager.swift
+++ b/ElementX/Sources/Application/Windowing/WindowManager.swift
@@ -18,6 +18,8 @@ class WindowManager: SecureWindowManagerProtocol {
private(set) var overlayWindow: UIWindow!
private(set) var globalSearchWindow: UIWindow!
private(set) var alternateWindow: UIWindow!
+
+ private(set) var openWindowAction: OpenWindowAction!
var windows: [UIWindow] {
[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.
private let windowHideDelay = Duration.milliseconds(33)
+ private var coordinators: [WindowManagerWindowType: (coordinator: CoordinatorProtocol, flowCoordinator: FlowCoordinatorProtocol?)] = [:]
+
init(appDelegate: AppDelegate) {
self.appDelegate = appDelegate
}
@@ -54,6 +58,29 @@ class WindowManager: SecureWindowManagerProtocol {
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() {
mainWindow.isHidden = false
overlayWindow.isHidden = false
diff --git a/ElementX/Sources/Application/Windowing/WindowManagerProtocol.swift b/ElementX/Sources/Application/Windowing/WindowManagerProtocol.swift
index af69a265f..d53366b48 100644
--- a/ElementX/Sources/Application/Windowing/WindowManagerProtocol.swift
+++ b/ElementX/Sources/Application/Windowing/WindowManagerProtocol.swift
@@ -8,6 +8,11 @@
import SwiftUI
+enum WindowManagerWindowType: Hashable, Codable {
+ case room(roomID: String)
+ case settings
+}
+
protocol SecureWindowManagerDelegate: AnyObject {
/// The window manager has configured its windows.
func windowManagerDidConfigureWindows(_ windowManager: SecureWindowManagerProtocol)
@@ -20,6 +25,12 @@ protocol SecureWindowManagerProtocol: WindowManagerProtocol {
/// Configures the window manager to operate on the supplied scene.
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.
func switchToMain()
@@ -43,6 +54,8 @@ protocol WindowManagerProtocol: AnyObject, OrientationManagerProtocol {
/// All the windows being managed
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.
func showGlobalSearch()
diff --git a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift
index 66cab87fd..fa1214436 100644
--- a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift
+++ b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift
@@ -321,7 +321,11 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
if case .space = detailState {
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.
}
@@ -393,6 +397,8 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .presentRoom(let roomID):
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):
handleAppRoute(.roomDetails(roomID: roomID), animated: true)
case .presentReportRoom(let roomID):
@@ -516,8 +522,10 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
private func startRoomFlow(roomID: String,
via: [String],
entryPoint: RoomFlowCoordinatorEntryPoint,
+ detached: Bool,
animated: Bool) {
- let navigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
+ let navigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: detached ? nil : navigationSplitCoordinator)
+
let coordinator = RoomFlowCoordinator(roomID: roomID,
isChildFlow: false,
navigationStackCoordinator: navigationStackCoordinator,
@@ -539,9 +547,14 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
}
.store(in: &cancellables)
- roomFlowCoordinator = coordinator
-
- navigationSplitCoordinator.setDetailCoordinator(navigationStackCoordinator, animated: animated)
+ if detached {
+ flowParameters.windowManager.registerCoordinator(navigationStackCoordinator,
+ flowCoordinator: coordinator,
+ forWindowType: .room(roomID: roomID))
+ } else {
+ roomFlowCoordinator = coordinator
+ navigationSplitCoordinator.setDetailCoordinator(navigationStackCoordinator, animated: animated)
+ }
switch entryPoint {
case .room:
diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
index 791bb6132..7e96005fd 100644
--- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
+++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
@@ -21358,6 +21358,47 @@ class WindowManagerMock: WindowManagerProtocol, @unchecked Sendable {
var alternateWindow: 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
var showGlobalSearchUnderlyingCallsCount = 0
diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift
index cd588fb2d..ddaf642b4 100644
--- a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift
+++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift
@@ -21,6 +21,7 @@ struct HomeScreenCoordinatorParameters {
enum HomeScreenCoordinatorAction {
case presentRoom(roomIdentifier: String)
+ case detachRoom(roomIdentifier: String)
case presentRoomDetails(roomIdentifier: String)
case presentReportRoom(roomIdentifier: String)
case presentDeclineAndBlock(userID: String, roomID: String)
@@ -65,6 +66,8 @@ final class HomeScreenCoordinator: CoordinatorProtocol {
switch action {
case .presentRoom(let roomIdentifier):
actionsSubject.send(.presentRoom(roomIdentifier: roomIdentifier))
+ case .detachRoom(let roomIdentifier):
+ actionsSubject.send(.detachRoom(roomIdentifier: roomIdentifier))
case .presentRoomDetails(roomIdentifier: let roomIdentifier):
actionsSubject.send(.presentRoomDetails(roomIdentifier: roomIdentifier))
case .presentReportRoom(let roomIdentifier):
diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift
index 4999d09bc..4cc096150 100644
--- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift
+++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift
@@ -12,6 +12,7 @@ import UIKit
enum HomeScreenViewModelAction {
case presentRoom(roomIdentifier: String)
+ case detachRoom(roomIdentifier: String)
case presentRoomDetails(roomIdentifier: String)
case presentReportRoom(roomIdentifier: String)
case presentDeclineAndBlock(userID: String, roomID: String)
@@ -30,6 +31,7 @@ enum HomeScreenViewModelAction {
enum HomeScreenViewAction {
case selectRoom(roomIdentifier: String)
+ case detachRoom(roomIdentifier: String)
case showRoomDetails(roomIdentifier: String)
case leaveRoom(roomIdentifier: String)
case confirmLeaveRoom(roomIdentifier: String)
diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift
index 96d073df0..5e388a5ee 100644
--- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift
+++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift
@@ -173,6 +173,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
switch viewAction {
case .selectRoom(let roomIdentifier):
actionsSubject.send(.presentRoom(roomIdentifier: roomIdentifier))
+ case .detachRoom(let roomIdentifier):
+ actionsSubject.send(.detachRoom(roomIdentifier: roomIdentifier))
case .showRoomDetails(let roomIdentifier):
actionsSubject.send(.presentRoomDetails(roomIdentifier: roomIdentifier))
case .leaveRoom(let roomIdentifier):
diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomList.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomList.swift
index 5263a81c2..9a60cceb9 100644
--- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomList.swift
+++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomList.swift
@@ -34,7 +34,18 @@ struct HomeScreenRoomList: View {
let isSelected = context.viewState.selectedRoomID == room.id
HomeScreenRoomCell(room: room, isSelected: isSelected, mediaProvider: context.mediaProvider, action: context.send)
+ .simultaneousGesture(TapGesture(count: 2).onEnded {
+ context.send(viewAction: .detachRoom(roomIdentifier: room.id))
+ })
.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 {
Button {
context.send(viewAction: .markRoomAsRead(roomIdentifier: room.id))
diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift
index 922827853..421053334 100644
--- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift
+++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift
@@ -65,7 +65,11 @@ class UITestsAppCoordinator: AppCoordinatorProtocol, SecureWindowManagerDelegate
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.")
}
diff --git a/ElementX/Sources/UnitTests/UnitTestsAppCoordinator.swift b/ElementX/Sources/UnitTests/UnitTestsAppCoordinator.swift
index 0f2987085..836b45c1d 100644
--- a/ElementX/Sources/UnitTests/UnitTestsAppCoordinator.swift
+++ b/ElementX/Sources/UnitTests/UnitTestsAppCoordinator.swift
@@ -50,7 +50,11 @@ class UnitTestsAppCoordinator: AppCoordinatorProtocol {
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.")
}
diff --git a/ElementX/SupportingFiles/Info.plist b/ElementX/SupportingFiles/Info.plist
index 714655422..c790bba88 100644
--- a/ElementX/SupportingFiles/Info.plist
+++ b/ElementX/SupportingFiles/Info.plist
@@ -82,6 +82,11 @@
INSendMessageIntent
INStartCallIntent
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+
UIBackgroundModes
audio
diff --git a/ElementX/SupportingFiles/target.yml b/ElementX/SupportingFiles/target.yml
index ab118baa6..a51cfeccc 100644
--- a/ElementX/SupportingFiles/target.yml
+++ b/ElementX/SupportingFiles/target.yml
@@ -67,6 +67,9 @@ targets:
]
}
]
+ UIApplicationSceneManifest: {
+ UIApplicationSupportsMultipleScenes: true
+ }
UISupportedInterfaceOrientations: [
UIInterfaceOrientationPortrait,
UIInterfaceOrientationPortraitUpsideDown,