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,