diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index b825dce8a..1ac8e832a 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 56; objects = { /* Begin PBXAggregateTarget section */ @@ -657,6 +657,7 @@ A722F426FD81FC67706BB1E0 /* CustomLayoutLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42236480CF0431535EBE8387 /* CustomLayoutLabelStyle.swift */; }; A743841F91B62B0E56217B04 /* SecureBackupKeyBackupScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DCB219D7B7B0299358FF81 /* SecureBackupKeyBackupScreenUITests.swift */; }; A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */; }; + A7A6452C2B5946940037ABFE /* OrientationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7A6452B2B5946940037ABFE /* OrientationManager.swift */; }; A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */; }; A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; }; @@ -1062,7 +1063,7 @@ 033DB41C51865A2E83174E87 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; 035177BCD8E8308B098AC3C2 /* WindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = ""; }; 0376C429FAB1687C3D905F3E /* MockCoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCoder.swift; sourceTree = ""; }; - 0392E3FDE372C9B56FEEED8B /* test_voice_message.m4a */ = {isa = PBXFileReference; path = test_voice_message.m4a; sourceTree = ""; }; + 0392E3FDE372C9B56FEEED8B /* test_voice_message.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_voice_message.m4a; sourceTree = ""; }; 03DD998E523D4EC93C7ED703 /* RoomNotificationSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; 03FABD73FD8086EFAB699F42 /* MediaUploadPreviewScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModelTests.swift; sourceTree = ""; }; 044E501B8331B339874D1B96 /* CompoundIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompoundIcon.swift; sourceTree = ""; }; @@ -1122,7 +1123,7 @@ 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = ""; }; 12EDAFB64FA5F6812D54F39A /* MigrationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenViewModel.swift; sourceTree = ""; }; 12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenCoordinator.swift; sourceTree = ""; }; - 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = ""; }; + 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = ""; }; 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; 1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; @@ -1553,7 +1554,7 @@ 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; - 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; 8E1BBA73B611EDEEA6E20E05 /* InvitesScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenModels.swift; sourceTree = ""; }; 8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenUITests.swift; sourceTree = ""; }; 8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = ""; }; @@ -1633,6 +1634,7 @@ A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = ""; }; A6C11AD9813045E44F950410 /* ElementCallWidgetDriverProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriverProtocol.swift; sourceTree = ""; }; A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = ""; }; + A7A6452B2B5946940037ABFE /* OrientationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationManager.swift; sourceTree = ""; }; A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = ""; }; A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenCoordinator.swift; sourceTree = ""; }; A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationProtocol.swift; sourceTree = ""; }; @@ -1691,7 +1693,7 @@ B50F03079F6B5EF9CA005F14 /* TimelineProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineProxyProtocol.swift; sourceTree = ""; }; B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = ""; }; B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; - B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = ""; }; + B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = ""; }; B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = ""; }; B63B69F9A2BC74DD40DC75C8 /* AdvancedSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModel.swift; sourceTree = ""; }; B697816AF93DA06EC58C5D70 /* WaitlistScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1795,7 +1797,7 @@ CD95B3714F806AC9CF9A557B /* ComposerToolbarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModel.swift; sourceTree = ""; }; CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; - CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; + CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = ""; }; CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxy.swift; sourceTree = ""; }; D0140615D2232612C813FD6C /* EncryptedHistoryRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedHistoryRoomTimelineItem.swift; sourceTree = ""; }; D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = ""; }; @@ -1902,7 +1904,7 @@ ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = ""; }; ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; - ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; + ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = ""; }; ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = ""; }; EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = ""; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = ""; }; @@ -1919,7 +1921,7 @@ F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenModels.swift; sourceTree = ""; }; F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = ""; }; F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineItem.swift; sourceTree = ""; }; - F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; path = portrait_test_video.mp4; sourceTree = ""; }; + F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = portrait_test_video.mp4; sourceTree = ""; }; F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = ""; }; F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = ""; }; F36C0A6D59717193F49EA986 /* UserSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionTests.swift; sourceTree = ""; }; @@ -3188,6 +3190,7 @@ 5A37E2FACFD041CE466223CD /* SceneDelegate.swift */, 035177BCD8E8308B098AC3C2 /* WindowManager.swift */, 06F27F588F9059128E17C669 /* WindowManagerProtocol.swift */, + A7A6452B2B5946940037ABFE /* OrientationManager.swift */, ); path = Windowing; sourceTree = ""; @@ -5831,6 +5834,7 @@ 94A65DD8A353DF112EBEF67A /* SessionVerificationControllerProxyProtocol.swift in Sources */, 7A0A0929556792FB19B812C5 /* SessionVerificationScreen.swift in Sources */, E9F148072F9513EC2272AA21 /* SessionVerificationScreenCoordinator.swift in Sources */, + A7A6452C2B5946940037ABFE /* OrientationManager.swift in Sources */, 5770C4906668C6D3008A2AC9 /* SessionVerificationScreenModels.swift in Sources */, B27D3190784F85916DA1C394 /* SessionVerificationScreenStateMachine.swift in Sources */, F4433EF57B4BB3C077F8B00E /* SessionVerificationScreenViewModel.swift in Sources */, @@ -6178,9 +6182,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_NSE", - ); + OTHER_SWIFT_FLAGS = "-DIS_NSE"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.nse"; PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; PRODUCT_NAME = NSE; @@ -6211,9 +6213,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_MAIN_APP", - ); + OTHER_SWIFT_FLAGS = "-DIS_MAIN_APP"; PILLS_UT_TYPE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).pills"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(APP_NAME)"; @@ -6239,9 +6239,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_MAIN_APP", - ); + OTHER_SWIFT_FLAGS = "-DIS_MAIN_APP"; PILLS_UT_TYPE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).pills"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(APP_NAME)"; @@ -6484,9 +6482,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_NSE", - ); + OTHER_SWIFT_FLAGS = "-DIS_NSE"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.nse"; PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; PRODUCT_NAME = NSE; diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index b09f30111..d53c72914 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -57,13 +57,14 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, private var clientProxyObserver: AnyCancellable? private var cancellables = Set() - let windowManager: WindowManagerProtocol = WindowManager() + let windowManager: WindowManagerProtocol let notificationManager: NotificationManagerProtocol private let appRouteURLParser: AppRouteURLParser @Consumable private var storedAppRoute: AppRoute? init(appDelegate: AppDelegate) { + windowManager = WindowManager(appDelegate: appDelegate) Self.setupEnvironmentVariables() let appSettings = AppSettings() diff --git a/ElementX/Sources/Application/AppDelegate.swift b/ElementX/Sources/Application/AppDelegate.swift index 8423a2639..628067204 100644 --- a/ElementX/Sources/Application/AppDelegate.swift +++ b/ElementX/Sources/Application/AppDelegate.swift @@ -22,8 +22,9 @@ enum AppDelegateCallback { case failedToRegisteredNotifications(error: Error) } -class AppDelegate: NSObject, UIApplicationDelegate { +final class AppDelegate: NSObject, UIApplicationDelegate { let callbacks = PassthroughSubject() + var orientationLock = UIInterfaceOrientationMask.all func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { // Add a SceneDelegate to the SwiftUI scene so that we can connect up the WindowManager. @@ -44,4 +45,8 @@ class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { callbacks.send(.failedToRegisteredNotifications(error: error)) } + + func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { + orientationLock + } } diff --git a/ElementX/Sources/Application/Application.swift b/ElementX/Sources/Application/Application.swift index bf046aa1f..5267a8776 100644 --- a/ElementX/Sources/Application/Application.swift +++ b/ElementX/Sources/Application/Application.swift @@ -25,9 +25,9 @@ struct Application: App { init() { if ProcessInfo.isRunningUITests { - appCoordinator = UITestsAppCoordinator() + appCoordinator = UITestsAppCoordinator(appDelegate: appDelegate) } else if ProcessInfo.isRunningUnitTests { - appCoordinator = UnitTestsAppCoordinator() + appCoordinator = UnitTestsAppCoordinator(appDelegate: appDelegate) } else { appCoordinator = AppCoordinator(appDelegate: appDelegate) } diff --git a/ElementX/Sources/Application/Windowing/OrientationManager.swift b/ElementX/Sources/Application/Windowing/OrientationManager.swift new file mode 100644 index 000000000..7112149d6 --- /dev/null +++ b/ElementX/Sources/Application/Windowing/OrientationManager.swift @@ -0,0 +1,27 @@ +// +// Copyright 2024 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UIKit + +// sourcery: AutoMockable +protocol OrientationManagerProtocol { + /// Forces the current orientation for the main window, works only on iOS + func setOrientation(_ orientation: UIInterfaceOrientationMask) + + /// Locks the current orientation for the main window, works only on iOS + func lockOrientation(_ orientation: UIInterfaceOrientationMask) +} diff --git a/ElementX/Sources/Application/Windowing/WindowManager.swift b/ElementX/Sources/Application/Windowing/WindowManager.swift index 20e54ec41..b4c5182c5 100644 --- a/ElementX/Sources/Application/Windowing/WindowManager.swift +++ b/ElementX/Sources/Application/Windowing/WindowManager.swift @@ -18,12 +18,14 @@ import Combine import SwiftUI class WindowManager: WindowManagerProtocol { + private let appDelegate: AppDelegate + weak var windowScene: UIWindowScene? weak var delegate: WindowManagerDelegate? private(set) var mainWindow: UIWindow! private(set) var overlayWindow: UIWindow! private(set) var alternateWindow: UIWindow! - + var windows: [UIWindow] { [mainWindow, overlayWindow, alternateWindow] } @@ -34,7 +36,12 @@ class WindowManager: WindowManagerProtocol { /// A duration that allows window switching to wait a couple of frames to avoid a transition through black. private let windowHideDelay = Duration.milliseconds(33) + init(appDelegate: AppDelegate) { + self.appDelegate = appDelegate + } + func configure(with windowScene: UIWindowScene) { + self.windowScene = windowScene mainWindow = windowScene.keyWindow mainWindow.tintColor = .compound.textActionPrimary @@ -80,6 +87,14 @@ class WindowManager: WindowManagerProtocol { mainWindow.isHidden = true } } + + func setOrientation(_ orientation: UIInterfaceOrientationMask) { + windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: orientation)) + } + + func lockOrientation(_ orientation: UIInterfaceOrientationMask) { + appDelegate.orientationLock = orientation + } } private class PassthroughWindow: UIWindow { diff --git a/ElementX/Sources/Application/Windowing/WindowManagerProtocol.swift b/ElementX/Sources/Application/Windowing/WindowManagerProtocol.swift index c0cae9e3b..6d7dd3c9c 100644 --- a/ElementX/Sources/Application/Windowing/WindowManagerProtocol.swift +++ b/ElementX/Sources/Application/Windowing/WindowManagerProtocol.swift @@ -25,7 +25,7 @@ protocol WindowManagerDelegate: AnyObject { /// A window manager that supports switching between a main app window with an overlay and /// an alternate window to switch contexts whilst also preserving the main view hierarchy. /// Heavily inspired by https://www.fivestars.blog/articles/swiftui-windows/ -protocol WindowManagerProtocol: AnyObject { +protocol WindowManagerProtocol: AnyObject, OrientationManagerProtocol { var delegate: WindowManagerDelegate? { get set } /// The app's main window (we only support a single scene). diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index f30366406..6142520a8 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -40,6 +40,7 @@ enum RoomFlowCoordinatorAction: Equatable { // swiftlint:disable file_length class RoomFlowCoordinator: FlowCoordinatorProtocol { + private let orientationManager: OrientationManagerProtocol private let userSession: UserSessionProtocol private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol private let navigationStackCoordinator: NavigationStackCoordinator @@ -73,7 +74,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { emojiProvider: EmojiProviderProtocol, appSettings: AppSettings, analytics: AnalyticsService, - userIndicatorController: UserIndicatorControllerProtocol) { + userIndicatorController: UserIndicatorControllerProtocol, + orientationManager: OrientationManagerProtocol) { self.userSession = userSession self.roomTimelineControllerFactory = roomTimelineControllerFactory self.navigationStackCoordinator = navigationStackCoordinator @@ -82,6 +84,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { self.appSettings = appSettings self.analytics = analytics self.userIndicatorController = userIndicatorController + self.orientationManager = orientationManager setupStateMachine() } @@ -606,7 +609,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { mediaProvider: userSession.mediaProvider, navigationStackCoordinator: stackCoordinator, roomProxy: roomProxy, - userIndicatorController: userIndicatorController) + userIndicatorController: userIndicatorController, + orientationManager: orientationManager) let roomDetailsEditCoordinator = RoomDetailsEditScreenCoordinator(parameters: roomDetailsEditParameters) roomDetailsEditCoordinator.actions.sink { [weak self] action in @@ -660,12 +664,17 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { private func presentMediaUploadPickerWithSource(_ source: MediaPickerScreenSource) { let stackCoordinator = NavigationStackCoordinator() - let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: userIndicatorController, source: source) { [weak self] action in + let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: userIndicatorController, + source: source, + orientationManager: orientationManager) { [weak self] action in + guard let self else { + return + } switch action { case .cancel: - self?.navigationStackCoordinator.setSheetCoordinator(nil) + navigationStackCoordinator.setSheetCoordinator(nil) case .selectMediaAtURL(let url): - self?.stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url)) + stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url)) } } diff --git a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift index 154093c00..076718d3f 100644 --- a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift @@ -152,7 +152,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { } private func presentUserDetailsEditScreen() { - let coordinator = UserDetailsEditScreenCoordinator(parameters: .init(clientProxy: parameters.userSession.clientProxy, + let coordinator = UserDetailsEditScreenCoordinator(parameters: .init(orientationManager: parameters.windowManager, + clientProxy: parameters.userSession.clientProxy, mediaProvider: parameters.userSession.mediaProvider, navigationStackCoordinator: navigationStackCoordinator, userIndicatorController: parameters.userIndicatorController)) diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 7761a13f5..88fa162fe 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -25,6 +25,7 @@ enum UserSessionFlowCoordinatorAction { } class UserSessionFlowCoordinator: FlowCoordinatorProtocol { + private let windowManager: WindowManagerProtocol private let userSession: UserSessionProtocol private let navigationSplitCoordinator: NavigationSplitCoordinator private let bugReportService: BugReportServiceProtocol @@ -60,6 +61,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { self.navigationSplitCoordinator = navigationSplitCoordinator self.bugReportService = bugReportService self.appSettings = appSettings + self.windowManager = windowManager sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator) detailNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator) @@ -73,7 +75,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { emojiProvider: EmojiProvider(), appSettings: appSettings, analytics: analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + orientationManager: windowManager) settingsFlowCoordinator = SettingsFlowCoordinator(parameters: .init(userSession: userSession, windowManager: windowManager, @@ -455,7 +458,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { let startChatNavigationStackCoordinator = NavigationStackCoordinator() let userDiscoveryService = UserDiscoveryService(clientProxy: userSession.clientProxy) - let parameters = StartChatScreenCoordinatorParameters(userSession: userSession, + let parameters = StartChatScreenCoordinatorParameters(orientationManager: windowManager, + userSession: userSession, userIndicatorController: ServiceLocator.shared.userIndicatorController, navigationStackCoordinator: startChatNavigationStackCoordinator, userDiscoveryService: userDiscoveryService) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 12b5e21a3..ecd246de5 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1632,6 +1632,41 @@ class NotificationSettingsProxyMock: NotificationSettingsProxyProtocol { } } } +class OrientationManagerMock: OrientationManagerProtocol { + + //MARK: - setOrientation + + var setOrientationCallsCount = 0 + var setOrientationCalled: Bool { + return setOrientationCallsCount > 0 + } + var setOrientationReceivedOrientation: UIInterfaceOrientationMask? + var setOrientationReceivedInvocations: [UIInterfaceOrientationMask] = [] + var setOrientationClosure: ((UIInterfaceOrientationMask) -> Void)? + + func setOrientation(_ orientation: UIInterfaceOrientationMask) { + setOrientationCallsCount += 1 + setOrientationReceivedOrientation = orientation + setOrientationReceivedInvocations.append(orientation) + setOrientationClosure?(orientation) + } + //MARK: - lockOrientation + + var lockOrientationCallsCount = 0 + var lockOrientationCalled: Bool { + return lockOrientationCallsCount > 0 + } + var lockOrientationReceivedOrientation: UIInterfaceOrientationMask? + var lockOrientationReceivedInvocations: [UIInterfaceOrientationMask] = [] + var lockOrientationClosure: ((UIInterfaceOrientationMask) -> Void)? + + func lockOrientation(_ orientation: UIInterfaceOrientationMask) { + lockOrientationCallsCount += 1 + lockOrientationReceivedOrientation = orientation + lockOrientationReceivedInvocations.append(orientation) + lockOrientationClosure?(orientation) + } +} class PollInteractionHandlerMock: PollInteractionHandlerProtocol { //MARK: - sendPollResponse diff --git a/ElementX/Sources/Screens/MediaPickerScreen/MediaPickerScreenCoordinator.swift b/ElementX/Sources/Screens/MediaPickerScreen/MediaPickerScreenCoordinator.swift index 70e7db389..f28614937 100644 --- a/ElementX/Sources/Screens/MediaPickerScreen/MediaPickerScreenCoordinator.swift +++ b/ElementX/Sources/Screens/MediaPickerScreen/MediaPickerScreenCoordinator.swift @@ -28,13 +28,18 @@ enum MediaPickerScreenCoordinatorAction { } class MediaPickerScreenCoordinator: CoordinatorProtocol { + private let orientationManager: OrientationManagerProtocol private let userIndicatorController: UserIndicatorControllerProtocol private let source: MediaPickerScreenSource - private let callback: ((MediaPickerScreenCoordinatorAction) -> Void)? + private let callback: (MediaPickerScreenCoordinatorAction) -> Void - init(userIndicatorController: UserIndicatorControllerProtocol, source: MediaPickerScreenSource, callback: @escaping (MediaPickerScreenCoordinatorAction) -> Void) { + init(userIndicatorController: UserIndicatorControllerProtocol, + source: MediaPickerScreenSource, + orientationManager: OrientationManagerProtocol, + callback: @escaping (MediaPickerScreenCoordinatorAction) -> Void) { self.userIndicatorController = userIndicatorController self.source = source + self.orientationManager = orientationManager self.callback = callback } @@ -42,6 +47,23 @@ class MediaPickerScreenCoordinator: CoordinatorProtocol { AnyView(mediaPicker) } + func start() { + guard source == .camera else { + return + } + + orientationManager.setOrientation(.portrait) + orientationManager.lockOrientation(.portrait) + } + + func stop() { + guard source == .camera else { + return + } + + orientationManager.lockOrientation(.all) + } + @ViewBuilder private var mediaPicker: some View { switch source { @@ -51,12 +73,12 @@ class MediaPickerScreenCoordinator: CoordinatorProtocol { PhotoLibraryPicker(userIndicatorController: userIndicatorController) { [weak self] action in switch action { case .cancel: - self?.callback?(.cancel) + self?.callback(.cancel) case .error(let error): MXLog.error("Failed selecting media from the photo library with error: \(error)") self?.showError() case .selectFile(let url): - self?.callback?(.selectMediaAtURL(url)) + self?.callback(.selectMediaAtURL(url)) } } case .documents: @@ -65,12 +87,12 @@ class MediaPickerScreenCoordinator: CoordinatorProtocol { DocumentPicker(userIndicatorController: userIndicatorController) { action in switch action { case .cancel: - self.callback?(.cancel) + self.callback(.cancel) case .error(let error): MXLog.error("Failed selecting media from the document picker with error: \(error)") self.showError() case .selectFile(let url): - self.callback?(.selectMediaAtURL(url)) + self.callback(.selectMediaAtURL(url)) } } } @@ -80,12 +102,12 @@ class MediaPickerScreenCoordinator: CoordinatorProtocol { CameraPicker(userIndicatorController: userIndicatorController) { [weak self] action in switch action { case .cancel: - self?.callback?(.cancel) + self?.callback(.cancel) case .error(let error): MXLog.error("Failed selecting media from the camera picker with error: \(error)") self?.showError() case .selectFile(let url): - self?.callback?(.selectMediaAtURL(url)) + self?.callback(.selectMediaAtURL(url)) } } .background(.black, ignoresSafeAreaEdges: .bottom) diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift index b0b362383..8987b861f 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift @@ -23,6 +23,7 @@ struct RoomDetailsEditScreenCoordinatorParameters { weak var navigationStackCoordinator: NavigationStackCoordinator? let roomProxy: RoomProxyProtocol let userIndicatorController: UserIndicatorControllerProtocol + let orientationManager: OrientationManagerProtocol } enum RoomDetailsEditScreenCoordinatorAction { @@ -72,7 +73,9 @@ final class RoomDetailsEditScreenCoordinator: CoordinatorProtocol { private func displayMediaPickerWithSource(_ source: MediaPickerScreenSource) { let stackCoordinator = NavigationStackCoordinator() - let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: parameters.userIndicatorController, source: source) { [weak self] action in + let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: parameters.userIndicatorController, + source: source, + orientationManager: parameters.orientationManager) { [weak self] action in guard let self else { return } switch action { case .cancel: diff --git a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenCoordinator.swift index d16d6d09d..188e3b507 100644 --- a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenCoordinator.swift @@ -18,6 +18,7 @@ import Combine import SwiftUI struct UserDetailsEditScreenCoordinatorParameters { + let orientationManager: OrientationManagerProtocol let clientProxy: ClientProxyProtocol let mediaProvider: MediaProviderProtocol weak var navigationStackCoordinator: NavigationStackCoordinator? @@ -61,7 +62,7 @@ final class UserDetailsEditScreenCoordinator: CoordinatorProtocol { private func displayMediaPickerWithSource(_ source: MediaPickerScreenSource) { let stackCoordinator = NavigationStackCoordinator() - let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: parameters.userIndicatorController, source: source) { [weak self] action in + let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: parameters.userIndicatorController, source: source, orientationManager: parameters.orientationManager) { [weak self] action in guard let self else { return } switch action { case .cancel: diff --git a/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift b/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift index 4fbe6bbfe..6ffba3f17 100644 --- a/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift +++ b/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift @@ -18,6 +18,7 @@ import Combine import SwiftUI struct StartChatScreenCoordinatorParameters { + let orientationManager: OrientationManagerProtocol let userSession: UserSessionProtocol let userIndicatorController: UserIndicatorControllerProtocol weak var navigationStackCoordinator: NavigationStackCoordinator? @@ -146,7 +147,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol { private func displayMediaPickerWithSource(_ source: MediaPickerScreenSource) { let stackCoordinator = NavigationStackCoordinator() - let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: parameters.userIndicatorController, source: source) { [weak self] action in + let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: parameters.userIndicatorController, source: source, orientationManager: parameters.orientationManager) { [weak self] action in guard let self else { return } switch action { case .cancel: diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index bdc1ffbe7..e37a84f0a 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -28,9 +28,10 @@ class UITestsAppCoordinator: AppCoordinatorProtocol, WindowManagerDelegate { // periphery:ignore - retaining purpose private var alternateWindowMockScreen: MockScreen? - let windowManager: WindowManagerProtocol = WindowManager() + let windowManager: WindowManagerProtocol - init() { + init(appDelegate: AppDelegate) { + windowManager = WindowManager(appDelegate: appDelegate) // disabling View animations UIView.setAnimationsEnabled(false) @@ -685,7 +686,8 @@ class MockScreen: Identifiable { mediaProvider: MockMediaProvider(), navigationStackCoordinator: navigationStackCoordinator, roomProxy: roomProxy, - userIndicatorController: ServiceLocator.shared.userIndicatorController)) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + orientationManager: OrientationManagerMock())) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomMembersListScreen: @@ -735,7 +737,8 @@ class MockScreen: Identifiable { userDiscoveryMock.fetchSuggestionsReturnValue = .success([.mockAlice, .mockBob, .mockCharlie]) userDiscoveryMock.searchProfilesWithReturnValue = .success([]) let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"), mediaProvider: MockMediaProvider(), voiceMessageMediaManager: VoiceMessageMediaManagerMock()) - let parameters: StartChatScreenCoordinatorParameters = .init(userSession: userSession, + let parameters: StartChatScreenCoordinatorParameters = .init(orientationManager: OrientationManagerMock(), + userSession: userSession, userIndicatorController: UserIndicatorControllerMock(), navigationStackCoordinator: navigationStackCoordinator, userDiscoveryService: userDiscoveryMock) @@ -749,7 +752,8 @@ class MockScreen: Identifiable { userDiscoveryMock.fetchSuggestionsReturnValue = .success([]) userDiscoveryMock.searchProfilesWithReturnValue = .success([.mockBob, .mockBobby]) let userSession = MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider(), voiceMessageMediaManager: VoiceMessageMediaManagerMock()) - let coordinator = StartChatScreenCoordinator(parameters: .init(userSession: userSession, + let coordinator = StartChatScreenCoordinator(parameters: .init(orientationManager: OrientationManagerMock(), + userSession: userSession, userIndicatorController: UserIndicatorControllerMock(), navigationStackCoordinator: navigationStackCoordinator, userDiscoveryService: userDiscoveryMock)) diff --git a/ElementX/Sources/UnitTests/UnitTestsAppCoordinator.swift b/ElementX/Sources/UnitTests/UnitTestsAppCoordinator.swift index 4d1f1a0b5..66df82082 100644 --- a/ElementX/Sources/UnitTests/UnitTestsAppCoordinator.swift +++ b/ElementX/Sources/UnitTests/UnitTestsAppCoordinator.swift @@ -17,9 +17,10 @@ import SwiftUI class UnitTestsAppCoordinator: AppCoordinatorProtocol { - let windowManager: WindowManagerProtocol = WindowManager() + let windowManager: WindowManagerProtocol - init() { + init(appDelegate: AppDelegate) { + windowManager = WindowManager(appDelegate: appDelegate) ServiceLocator.shared.register(userIndicatorController: UserIndicatorControllerMock.default) AppSettings.configureWithSuiteName("io.element.elementx.unittests") diff --git a/UnitTests/Sources/RoomFlowCoordinatorTests.swift b/UnitTests/Sources/RoomFlowCoordinatorTests.swift index 1d092179c..06ae70fcd 100644 --- a/UnitTests/Sources/RoomFlowCoordinatorTests.swift +++ b/UnitTests/Sources/RoomFlowCoordinatorTests.swift @@ -45,7 +45,8 @@ class RoomFlowCoordinatorTests: XCTestCase { emojiProvider: EmojiProvider(), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + orientationManager: OrientationManagerMock()) } func testRoomPresentation() async throws { diff --git a/changelog.d/1815.bugfix b/changelog.d/1815.bugfix new file mode 100644 index 000000000..e78863203 --- /dev/null +++ b/changelog.d/1815.bugfix @@ -0,0 +1 @@ +Is now possible to take photos in landscape mode. \ No newline at end of file