From 8d2e30c0b6be3f31cea071edc82feac85862b94d Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 16 Nov 2022 15:37:34 +0200 Subject: [PATCH] SwiftUI NavigationController and UserNotificationControllers (#309) * Fixes #286 - Adopted the new SwiftUI NavigationStack based NavigationController throughout the application * Fixes #315 - Implemented new user notification components on top of SwiftUI and the new navigation flows * Add home screen fade animation between skeletons and real rooms * Bump the danger-swift version used on the CI and swiftlint with it * Renamed Splash to Onboarding, Empty to Splash --- .github/workflows/danger.yml | 2 +- ElementX.xcodeproj/project.pbxproj | 352 ++++++---------- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Contents.json | 0 .../OnboardingSplashScreenPage1-Dark.pdf | 0 .../OnboardingSplashScreenPage1.pdf | 0 .../Contents.json | 0 .../OnboardingSplashScreenPage2-Dark.pdf | Bin .../OnboardingSplashScreenPage2.pdf | Bin .../Contents.json | 0 .../OnboardingSplashScreenPage3-Dark.pdf | 0 .../OnboardingSplashScreenPage3.pdf | 0 .../Contents.json | 0 .../OnboardingSplashScreenPage4-Dark.pdf | Bin .../OnboardingSplashScreenPage4.pdf | Bin .../Sources/Application/AppCoordinator.swift | 88 ++-- .../Sources/Application/AppDelegate.swift | 44 +- .../Application/CoordinatorProtocol.swift | 34 ++ .../Application/NavigationController.swift | 186 ++++++++ ElementX/Sources/Generated/Assets.swift | 8 +- ElementX/Sources/Other/Coordinator.swift | 54 --- .../Other/Routers/NavigationRouter.swift | 397 ------------------ .../Other/Routers/NavigationRouterStore.swift | 93 ---- .../NavigationRouterStoreProtocol.swift | 25 -- .../Other/Routers/NavigationRouterType.swift | 143 ------- .../Sources/Other/Routers/Presentable.swift | 37 -- .../Sources/Other/Routers/RootRouter.swift | 85 ---- .../Other/Routers/RootRouterType.swift | 49 --- .../SwiftUI/Animation/ElementAnimations.swift | 2 - .../SwiftUI/ErrorHandling/AlertInfo.swift | 11 +- .../ActivityIndicatorPresenter.swift | 146 ------- .../ActivityIndicatorPresenterType.swift | 34 -- .../ActivityIndicatorView.swift | 111 ----- .../UserIndicators/ActivityIndicatorView.xib | 55 --- .../FullscreenLoadingViewPresenter.swift | 75 ---- .../LabelledActivityIndicatorView.swift | 90 ---- .../UserIndicators/RectangleToastView.swift | 87 ---- .../UserIndicators/RoundedToastView.swift | 124 ------ .../UserIndicators/ToastViewPresenter.swift | 71 ---- .../Other/UserIndicators/UserIndicator.swift | 103 ----- .../UserIndicatorDismissal.swift | 25 -- .../UserIndicatorPresentationContext.swift | 42 -- .../UserIndicatorPresenter.swift | 133 ------ .../UserIndicators/UserIndicatorQueue.swift | 63 --- .../UserIndicators/UserIndicatorStore.swift | 63 --- .../UserIndicatorViewPresentable.swift | 25 -- .../MockUserNotificationController.swift | 13 +- .../UserNotifications/UserNotification.swift | 30 ++ .../UserNotificationController.swift | 80 ++++ .../UserNotificationControllerProtocol.swift} | 11 +- .../UserNotificationModalView.swift | 70 +++ .../UserNotificationPresenter.swift | 44 ++ .../UserNotificationToastView.swift | 63 +++ .../AnalyticsPromptCoordinator.swift | 31 +- .../AuthenticationCoordinator.swift | 83 ++-- .../LoginScreen/LoginCoordinator.swift | 112 +++-- .../MockServerSelectionScreenState.swift | 8 +- .../ServerSelectionCoordinator.swift | 66 +-- .../ServerSelectionModels.swift | 4 +- .../ServerSelectionViewModel.swift | 8 +- .../View/ServerSelectionScreen.swift | 3 +- .../SoftLogout/SoftLogoutCoordinator.swift | 63 ++- .../BugReport/BugReportCoordinator.swift | 83 ++-- .../Screens/BugReport/BugReportModels.swift | 3 + .../BugReport/BugReportViewModel.swift | 64 ++- .../BugReportViewModelProtocol.swift | 1 - .../BugReport/View/BugReportScreen.swift | 12 +- .../FilePreview/FilePreviewCoordinator.swift | 51 +-- .../HomeScreen/HomeScreenCoordinator.swift | 45 +- .../Screens/HomeScreen/HomeScreenModels.swift | 9 +- .../HomeScreen/HomeScreenViewModel.swift | 4 + .../HomeScreenViewModelProtocol.swift | 2 + .../Screens/HomeScreen/View/HomeScreen.swift | 19 +- .../MediaPlayer/MediaPlayerCoordinator.swift | 50 +-- .../OnboardingCoordinator.swift | 44 ++ .../OnboardingModels.swift} | 44 +- .../OnboardingViewModel.swift} | 10 +- .../OnboardingViewModelProtocol.swift} | 6 +- .../View/OnboardingPageIndicator.swift} | 2 +- .../View/OnboardingPageView.swift} | 10 +- .../View/OnboardingScreen.swift} | 18 +- .../Other/InviteFriendsCoordinator.swift} | 26 +- .../Other/SplashScreenCoordinator.swift} | 15 +- .../UIActivityViewControllerWrapper.swift} | 16 +- .../RoomScreen/RoomScreenCoordinator.swift | 74 +--- .../SessionVerificationCoordinator.swift | 29 +- .../Settings/SettingsCoordinator.swift | 86 +--- .../Screens/Settings/SettingsViewModel.swift | 6 +- .../Settings/SettingsViewModelProtocol.swift | 1 - .../Screens/Splash/SplashViewController.swift | 19 - .../Screens/Splash/SplashViewController.xib | 44 -- .../SplashScreenCoordinator.swift | 81 ---- .../VideoPlayer/VideoPlayerCoordinator.swift | 52 +-- .../UserSessionFlowCoordinator.swift | 174 +++----- .../UITests/UITestsAppCoordinator.swift | 94 ++--- ...iew.swift => UITestsRootCoordinator.swift} | 20 +- Package.resolved | 4 +- .../ElementX/TemplateCoordinator.swift | 53 +-- ...nUITests.swift => OnboardingUITests.swift} | 2 +- .../Sources/BugReportViewModelTests.swift | 6 +- .../Sources/NavigationControllerTests.swift | 221 ++++++++++ ...s.swift => OnboardingViewModelTests.swift} | 2 +- .../ServerSelectionViewModelTests.swift | 2 +- .../UserIndicatorQueueTests.swift | 56 --- .../UserIndicators/UserIndicatorTests.swift | 128 ------ .../UserNotificationControllerTests.swift | 107 +++++ changelog.d/286.feature | 1 + changelog.d/315.feature | 1 + 108 files changed, 1627 insertions(+), 3720 deletions(-) rename ElementX/Resources/Assets.xcassets/Images/Authentication/{Splash Screen Page 1.imageset => Onboarding Screen Page 1.imageset}/Contents.json (100%) rename ElementX/Resources/Assets.xcassets/Images/Authentication/{Splash Screen Page 1.imageset => Onboarding Screen Page 1.imageset}/OnboardingSplashScreenPage1-Dark.pdf (100%) rename ElementX/Resources/Assets.xcassets/Images/Authentication/{Splash Screen Page 1.imageset => Onboarding Screen Page 1.imageset}/OnboardingSplashScreenPage1.pdf (100%) rename ElementX/Resources/Assets.xcassets/Images/Authentication/{Splash Screen Page 2.imageset => Onboarding Screen Page 2.imageset}/Contents.json (100%) rename ElementX/Resources/Assets.xcassets/Images/Authentication/{Splash Screen Page 2.imageset => Onboarding Screen Page 2.imageset}/OnboardingSplashScreenPage2-Dark.pdf (100%) rename ElementX/Resources/Assets.xcassets/Images/Authentication/{Splash Screen Page 2.imageset => Onboarding Screen Page 2.imageset}/OnboardingSplashScreenPage2.pdf (100%) rename ElementX/Resources/Assets.xcassets/Images/Authentication/{Splash Screen Page 3.imageset => Onboarding Screen Page 3.imageset}/Contents.json (100%) rename ElementX/Resources/Assets.xcassets/Images/Authentication/{Splash Screen Page 3.imageset => Onboarding Screen Page 3.imageset}/OnboardingSplashScreenPage3-Dark.pdf (100%) rename ElementX/Resources/Assets.xcassets/Images/Authentication/{Splash Screen Page 3.imageset => Onboarding Screen Page 3.imageset}/OnboardingSplashScreenPage3.pdf (100%) rename ElementX/Resources/Assets.xcassets/Images/Authentication/{Splash Screen Page 4.imageset => Onboarding Screen Page 4.imageset}/Contents.json (100%) rename ElementX/Resources/Assets.xcassets/Images/Authentication/{Splash Screen Page 4.imageset => Onboarding Screen Page 4.imageset}/OnboardingSplashScreenPage4-Dark.pdf (100%) rename ElementX/Resources/Assets.xcassets/Images/Authentication/{Splash Screen Page 4.imageset => Onboarding Screen Page 4.imageset}/OnboardingSplashScreenPage4.pdf (100%) create mode 100644 ElementX/Sources/Application/CoordinatorProtocol.swift create mode 100644 ElementX/Sources/Application/NavigationController.swift delete mode 100755 ElementX/Sources/Other/Coordinator.swift delete mode 100755 ElementX/Sources/Other/Routers/NavigationRouter.swift delete mode 100644 ElementX/Sources/Other/Routers/NavigationRouterStore.swift delete mode 100644 ElementX/Sources/Other/Routers/NavigationRouterStoreProtocol.swift delete mode 100755 ElementX/Sources/Other/Routers/NavigationRouterType.swift delete mode 100755 ElementX/Sources/Other/Routers/Presentable.swift delete mode 100755 ElementX/Sources/Other/Routers/RootRouter.swift delete mode 100755 ElementX/Sources/Other/Routers/RootRouterType.swift delete mode 100755 ElementX/Sources/Other/UserIndicators/ActivityIndicatorPresenter.swift delete mode 100644 ElementX/Sources/Other/UserIndicators/ActivityIndicatorPresenterType.swift delete mode 100755 ElementX/Sources/Other/UserIndicators/ActivityIndicatorView.swift delete mode 100755 ElementX/Sources/Other/UserIndicators/ActivityIndicatorView.xib delete mode 100644 ElementX/Sources/Other/UserIndicators/FullscreenLoadingViewPresenter.swift delete mode 100644 ElementX/Sources/Other/UserIndicators/LabelledActivityIndicatorView.swift delete mode 100644 ElementX/Sources/Other/UserIndicators/RectangleToastView.swift delete mode 100644 ElementX/Sources/Other/UserIndicators/RoundedToastView.swift delete mode 100644 ElementX/Sources/Other/UserIndicators/ToastViewPresenter.swift delete mode 100644 ElementX/Sources/Other/UserIndicators/UserIndicator.swift delete mode 100644 ElementX/Sources/Other/UserIndicators/UserIndicatorDismissal.swift delete mode 100644 ElementX/Sources/Other/UserIndicators/UserIndicatorPresentationContext.swift delete mode 100644 ElementX/Sources/Other/UserIndicators/UserIndicatorPresenter.swift delete mode 100644 ElementX/Sources/Other/UserIndicators/UserIndicatorQueue.swift delete mode 100644 ElementX/Sources/Other/UserIndicators/UserIndicatorStore.swift delete mode 100644 ElementX/Sources/Other/UserIndicators/UserIndicatorViewPresentable.swift rename UnitTests/Sources/UserIndicators/UserIndicatorPresenterSpy.swift => ElementX/Sources/Other/UserNotifications/MockUserNotificationController.swift (72%) create mode 100644 ElementX/Sources/Other/UserNotifications/UserNotification.swift create mode 100644 ElementX/Sources/Other/UserNotifications/UserNotificationController.swift rename ElementX/Sources/Other/{ElementNavigationController.swift => UserNotifications/UserNotificationControllerProtocol.swift} (72%) create mode 100644 ElementX/Sources/Other/UserNotifications/UserNotificationModalView.swift create mode 100644 ElementX/Sources/Other/UserNotifications/UserNotificationPresenter.swift create mode 100644 ElementX/Sources/Other/UserNotifications/UserNotificationToastView.swift create mode 100644 ElementX/Sources/Screens/OnboardingScreen/OnboardingCoordinator.swift rename ElementX/Sources/Screens/{SplashScreen/SplashScreenModels.swift => OnboardingScreen/OnboardingModels.swift} (60%) rename ElementX/Sources/Screens/{SplashScreen/SplashScreenViewModel.swift => OnboardingScreen/OnboardingViewModel.swift} (69%) rename ElementX/Sources/Screens/{SplashScreen/SplashScreenViewModelProtocol.swift => OnboardingScreen/OnboardingViewModelProtocol.swift} (78%) rename ElementX/Sources/Screens/{SplashScreen/View/SplashScreenPageIndicator.swift => OnboardingScreen/View/OnboardingPageIndicator.swift} (97%) rename ElementX/Sources/Screens/{SplashScreen/View/SplashScreenPageView.swift => OnboardingScreen/View/OnboardingPageView.swift} (88%) rename ElementX/Sources/Screens/{SplashScreen/View/SplashScreen.swift => OnboardingScreen/View/OnboardingScreen.swift} (92%) rename ElementX/Sources/{Other/Routers/NavigationModule.swift => Screens/Other/InviteFriendsCoordinator.swift} (51%) rename ElementX/Sources/{Other/UserIndicators/ToastViewState.swift => Screens/Other/SplashScreenCoordinator.swift} (76%) rename ElementX/Sources/{Other/UserIndicators/UserIndicatorRequest.swift => Screens/Other/UIActivityViewControllerWrapper.swift} (51%) delete mode 100644 ElementX/Sources/Screens/Splash/SplashViewController.swift delete mode 100644 ElementX/Sources/Screens/Splash/SplashViewController.xib delete mode 100644 ElementX/Sources/Screens/SplashScreen/SplashScreenCoordinator.swift rename ElementX/Sources/UITests/{UITestsRootView.swift => UITestsRootCoordinator.swift} (67%) rename UITests/Sources/{SplashScreenUITests.swift => OnboardingUITests.swift} (98%) create mode 100644 UnitTests/Sources/NavigationControllerTests.swift rename UnitTests/Sources/{SplashScreenViewModelTests.swift => OnboardingViewModelTests.swift} (93%) delete mode 100644 UnitTests/Sources/UserIndicators/UserIndicatorQueueTests.swift delete mode 100644 UnitTests/Sources/UserIndicators/UserIndicatorTests.swift create mode 100644 UnitTests/Sources/UserNotificationControllerTests.swift create mode 100644 changelog.d/286.feature create mode 100644 changelog.d/315.feature diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 2d2b7d0cb..38cf40a22 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -16,6 +16,6 @@ jobs: key: danger-swift-cache-key - name: Danger - uses: docker://ghcr.io/danger/danger-swift-with-swiftlint:3.12.3 + uses: docker://ghcr.io/danger/danger-swift-with-swiftlint:3.14.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 92e13939e..f9e1fffec 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -16,12 +16,11 @@ 04A16B45228F7678A027C079 /* RoomHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 422724361B6555364C43281E /* RoomHeaderView.swift */; }; 059173B3C77056C406906B6D /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = D4DA544B2520BFA65D6DB4BB /* target.yml */; }; 05EC896A4B9AF4A56670C0BB /* SessionVerificationUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */; }; - 0602FA07557F580086782A9E /* UserIndicatorPresentationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA072E995316CD18BC29313 /* UserIndicatorPresentationContext.swift */; }; 066A1E9B94723EE9F3038044 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; - 06E93B2E3B32740B40F47CC5 /* ElementNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF4B39D52CAE7D21D276ABEE /* ElementNavigationController.swift */; }; 071A017E415AD378F2961B11 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; }; 07240B7159A3990C4C2E8FFC /* LoginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D256FEE2F1AF1E51D39B622 /* LoginTests.swift */; }; 072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */; }; + 086C2FA7750378EB2BFD0BEE /* UITestsRootCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D751BB69BB7C38FD247517B4 /* UITestsRootCoordinator.swift */; }; 095C0ACFC234E0550A6404C5 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */; }; 09AAF04B27732046C755D914 /* SoftLogoutViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */; }; 0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */; }; @@ -34,19 +33,16 @@ 0ED951768EC443A8728DE1D7 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; }; 0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; }; 0F9E38A75337D0146652ACAB /* BackgroundTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DFCAA239095A116976E32C4 /* BackgroundTaskTests.swift */; }; - 10866439ABA58CCDB5D1459D /* UserIndicatorQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91A6BC1A54CDB598EE2A81B /* UserIndicatorQueue.swift */; }; - 1151DCC5EC2C6585826545EC /* UserIndicatorPresenterSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B695D0D12086158BAD1D9859 /* UserIndicatorPresenterSpy.swift */; }; 1281625B25371BE53D36CB3A /* SeparatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1ED7E89865201EE7D53E6DA /* SeparatorRoomTimelineItem.swift */; }; - 12F70C493FB69F4D7E9A37EA /* NavigationRouterStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29EBCBFEC6FD0941749404D /* NavigationRouterStore.swift */; }; 132D241B09F9044711FD70A5 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; }; 13853973A5E24374FCEDE8A3 /* RedactedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */; }; 13C77FDF17C4C6627CFFC205 /* RoomTimelineItemFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */; }; + 14132418A748C988B85B025E /* OnboardingPageIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09199C43BAB209C0BD89A836 /* OnboardingPageIndicator.swift */; }; 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */; }; 1504CE9A609A348D90B69E47 /* VideoPlayerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3004DFA1B10951962787D90 /* VideoPlayerViewModelTests.swift */; }; 152AE2B8650FB23AFD2E28B9 /* MockAuthenticationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */; }; 1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */; }; 157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A2932515EA11D3DD8A3506 /* TimelineItemBubbledStylerView.swift */; }; - 15D1F9C415D9C921643BA82E /* UserIndicatorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B73D5E21F524A9BE44448D /* UserIndicatorRequest.swift */; }; 15D867E638BFD0E5E71DB1EF /* List.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AFEF3AC64B1358083F76B8B /* List.swift */; }; 165A883C29998EC779465068 /* SoftLogoutViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC38904A9663F7FAFD47457 /* SoftLogoutViewModelProtocol.swift */; }; 1702981A8085BE4FB0EC001B /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33116993D54FADC0C721C1F /* Application.swift */; }; @@ -66,7 +62,6 @@ 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */; }; 214C6B416609E58CCBF6DCEE /* SoftLogoutModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC77FC5C4F2000133047AA27 /* SoftLogoutModels.swift */; }; 2276870A19F34B3FFFDA690F /* SoftLogoutCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AEA20A6B4883E60469ACF8F /* SoftLogoutCoordinator.swift */; }; - 22DADD537401E79D66132134 /* NavigationRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4173A48FD8542CD4AD3645C /* NavigationRouter.swift */; }; 2352C541AF857241489756FF /* MockRoomSummaryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */; }; 237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */; }; 23B2CD5A06B16055BDDD0994 /* ApplicationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44D8C8431416EB8DFEC7E235 /* ApplicationTests.swift */; }; @@ -83,19 +78,17 @@ 2B9AEEC12B1BBE5BD61D0F5E /* UserSessionFlowCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3429142FE11930422E7CC1A0 /* UserSessionFlowCoordinatorStateMachine.swift */; }; 2BA59D0AEFB4B82A2EC2A326 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 78A5A8DE1E2B09C978C7F3B0 /* KeychainAccess */; }; 2BAA5B222856068158D0B3C6 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = B1E8B697DF78FE7F61FC6CA4 /* MatrixRustSDK */; }; - 2C0CE61E5DC177938618E0B1 /* RootRouterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90733775209F4D4D366A268F /* RootRouterType.swift */; }; + 2CB6787E25B11711518E9588 /* OnboardingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6281B199D8A8F0892490C2E /* OnboardingCoordinator.swift */; }; 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */; }; 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086B997409328F091EBA43CE /* RoomScreenUITests.swift */; }; 2F30EFEB7BD39242D1AD96F3 /* LoginViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1FB768A24FDD2A5CA16E3C /* LoginViewModelProtocol.swift */; }; 2F94054F50E312AF30BE07F3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B21E611DADDEF00307E7AC /* String.swift */; }; - 30122AB3484AC6C3A7F6A717 /* ActivityIndicatorView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B64F3A3D0DF86ED5A241AB05 /* ActivityIndicatorView.xib */; }; 308BD9343B95657FAA583FB7 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = AD2AC190E55B2BD4D0F1D4A7 /* SwiftyBeaver */; }; 3097A0A867D2B19CE32DAE58 /* UIKitBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */; }; 3274219F7F26A5C6C2C55630 /* FilePreviewViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F652E88106B855A2A55ADE /* FilePreviewViewModelProtocol.swift */; }; 32BA37B01B05261FCF2D4B45 /* WeakDictionaryKeyReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090CA61A835C151CEDF8F372 /* WeakDictionaryKeyReference.swift */; }; 33CAC1226DFB8B5D8447D286 /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; }; 33D630461FC4562CC767EE9F /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B0B1226DA8DB55918B34CD /* FileCache.swift */; }; - 344AF4CBB6D8786214878642 /* NavigationRouterStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */; }; 34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */; }; 352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */; }; 3588F34D05B4D731A73214C6 /* BugReportScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED59F9EFF273BFA2055FFDF /* BugReportScreen.swift */; }; @@ -117,15 +110,14 @@ 3F2148F11164C7C5609984EB /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 19CD5B074D7DD44AF4C58BB6 /* SwiftState */; }; 407DCE030E0F9B7C9861D38A /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 997C7385E1A07E061D7E2100 /* GZIP */; }; 41DFDD212D1BE57CA50D783B /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = FD43A50D9B75C9D6D30F006B /* SwiftyBeaver */; }; + 41E16904B30C529373B4E1A4 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 495D3EC4972639C1A87DDF8E /* NavigationController.swift */; }; 438FB9BC535BC95948AA5F34 /* SettingsViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */; }; 43FD77998F33C32718C51450 /* TemplateCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */; }; 440123E29E2F9B001A775BBE /* TimelineItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */; }; 447E8580A0A2569E32529E17 /* MockRoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */; }; 44AE0752E001D1D10605CD88 /* Swipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FDA5344F7C4C6E4E863E13 /* Swipe.swift */; }; 457465EC436703E8C76133A4 /* WeakDictionaryReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7955B20E2E6DA68E5BC0AB9 /* WeakDictionaryReference.swift */; }; - 462813B93C39DF93B1249403 /* RoundedToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFABDF2E19D349DAAAC18C65 /* RoundedToastView.swift */; }; 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */; }; - 4669804D0369FBED4E8625D1 /* ToastViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4470B8CB654B097D807AA713 /* ToastViewPresenter.swift */; }; 46F8817A235DC41228128BE7 /* MediaPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92B7BF5D0705F3CB70E7B2D7 /* MediaPlayerViewModel.swift */; }; 483507026FDCA2E16E5197A6 /* MediaPlayerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C444092DB0E4AB393067AC36 /* MediaPlayerViewModelTests.swift */; }; 485A7A97076C7D19104BDC1D /* MediaPlayerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCBE603A7EB2C93E81BA6415 /* MediaPlayerModels.swift */; }; @@ -134,28 +126,23 @@ 49E9B99CB6A275C7744351F0 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D58333B377888012740101 /* LoginViewModel.swift */; }; 49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */; }; 4A2E0DBB63919AC8309B6D40 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A191D3FDB995309C7E2DE7D /* SettingsViewModel.swift */; }; - 4B8A2C45FF906ADBB1F5C3B4 /* UserIndicatorQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E1273CC3BC3E471AF87BE5 /* UserIndicatorQueueTests.swift */; }; - 4D23C56053013437C35E511E /* ActivityIndicatorPresenterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C2318DF4C0E601EEE31F84 /* ActivityIndicatorPresenterType.swift */; }; 4D970CB606276717B43E2332 /* TimelineItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804F9B0FABE093C7284CD09B /* TimelineItemList.swift */; }; 4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */; }; - 4ED453A61AF45EBE18D8BC69 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F77E8010D41AA3F5F9A1FCA /* NavigationModule.swift */; }; 4FC1EFE4968A259CBBACFAFB /* RoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */; }; 4FF90E2242DBD596E1ED2E27 /* AppCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */; }; 500CB65ED116B81DA52FDAEE /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874A1842477895F199567BD7 /* TimelineView.swift */; }; 51DB67C5B5BC68B0A6FF54D4 /* MockRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBDC1D28EFB7789EB467E0 /* MockRoomProxy.swift */; }; 524C9C31EF8D58C2249F8A10 /* sample_screenshot.png in Resources */ = {isa = PBXBuildFile; fileRef = 9414DCADBDF9D6C4B806F61E /* sample_screenshot.png */; }; - 53504DF61DBC81ACC9B4D275 /* SplashScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF847B3C1873B8E81CEE7FAC /* SplashScreenViewModel.swift */; }; 5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; }; - 53B9C2240C2F5533246EE230 /* RectangleToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */; }; 541374590CA7E8318BD480FD /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; }; 54C774874BED4A8FAD1F22FE /* AnalyticsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77B3D4950F1707E66E4A45A /* AnalyticsConfiguration.swift */; }; 563A05B43207D00A6B698211 /* OIDCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9010EE0CC913D095887EF36E /* OIDCService.swift */; }; 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; }; - 5B2C4C17888FC095ED6880B2 /* SplashViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 48971F1FFD7FC5C466889FC7 /* SplashViewController.xib */; }; 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */; }; 5D430CDE11EAC3E8E6B80A66 /* RoomTimelineViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */; }; + 5D7960B32C350FA93F48D02B /* OnboardingModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB33A751BFDA223BDD106EC0 /* OnboardingModels.swift */; }; + 5D9F0695DC6C0057F85C12B6 /* UserNotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1113CA0A67B4AA227AAFB63B /* UserNotificationController.swift */; }; 5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */; }; - 5E1FCC43B738941D5A5F1794 /* SplashScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B73A8C3118EAC7BF3F3EE7A /* SplashScreenViewModelProtocol.swift */; }; 5E540CAEF764D7FBD8D80776 /* VideoPlayerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A3FC45B7643298BF361CEB1 /* VideoPlayerModels.swift */; }; 5F1FDE49DFD0C680386E48F9 /* TemplateViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B80895CE021B49847BD7D74 /* TemplateViewModelProtocol.swift */; }; 5F5488FBC9CFEB6F433D74A4 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7109E709A7738E6BCC4553E6 /* Localizable.strings */; }; @@ -173,15 +160,15 @@ 67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; }; 67E391A2E00709FB41903B36 /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6920A4869821BF72FFC58842 /* MockMediaProvider.swift */; }; 6832733838C57A7D3FE8FEB5 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 36B7FC232711031AA2B0D188 /* DTCoreText */; }; - 684BDE198AE5AA1392288A73 /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32CE6D4FF64C9A3C18619224 /* SplashScreen.swift */; }; 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; }; 690ED5315B401238A3249DCB /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 3FDFF4C1153D263BAB93C1F3 /* README.md */; }; 69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; }; 6A0E7551E0D1793245F34CDD /* ClientError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09A267106B9585D3D0CFC0D /* ClientError.swift */; }; - 6A367F3D7A437A79B7D9A31C /* FullscreenLoadingViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4112D04077F6709C5CA0A13E /* FullscreenLoadingViewPresenter.swift */; }; 6AC1DC1EAD9F7568360DA1BA /* ServerSelectionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A30A1758E2B73EF38E7C42F8 /* ServerSelectionModels.swift */; }; + 6B15FF984906AAFCF9DC4F58 /* OnboardingUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C88046D6A070D9827181C4D /* OnboardingUITests.swift */; }; 6C67774E8387D44426718BD9 /* FilePreviewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB3A7BCE745626EC61EF3C3 /* FilePreviewCoordinator.swift */; }; 6C9F6C7F2B35288C4230EF3F /* FilePreviewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55EA4B03F92F31EAA83B3F7B /* FilePreviewModels.swift */; }; + 6CA81428F0970785CDCC5E86 /* UserNotificationToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31A4E5941ACBA4BB9FEF94C /* UserNotificationToastView.swift */; }; 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */; }; 6DF37000571B1BC6D134CC9E /* WeakDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 304FFD608DB6E612075AB1B4 /* WeakDictionary.swift */; }; 6EA61FCA55D950BDE326A1A7 /* ImageAnonymizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */; }; @@ -190,20 +177,19 @@ 7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */; }; 706F79A39BDB32F592B8C2C7 /* UIKitBackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92FCD9116ADDE820E4E30F92 /* UIKitBackgroundTask.swift */; }; 7096FA3AC218D914E88BFB70 /* AggregratedReaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15BE37BE2FB86E00C8D150A /* AggregratedReaction.swift */; }; - 72F6E890820FF606A7E276C8 /* SplashScreenPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A534A4619D8FEFB6439FCC /* SplashScreenPageView.swift */; }; 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; }; 744C029EB6C43429926A0499 /* AnalyticsPromptViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A86C95340248A8B7BA9A43 /* AnalyticsPromptViewModelProtocol.swift */; }; 74604ACFDBE7F54260E7B617 /* ApplicationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */; }; 755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */; }; 758BF44CA565AB0AB84F2185 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7109E709A7738E6BCC4553E6 /* Localizable.strings */; }; 75EA4ABBFAA810AFF289D6F4 /* TemplateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB6E40BAD4504D899FAAC9A /* TemplateViewModel.swift */; }; + 7732B2F635626BE1C1CD92A4 /* UIActivityViewControllerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A57A4AFA6A068668AFBD070 /* UIActivityViewControllerWrapper.swift */; }; 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */; }; 77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */; }; 77FACC29F98FE2E65BBB6A5F /* ServerSelectionUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */; }; 78B71D53C1FC55FB7A9B75F0 /* RoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24B0C97D2F560BCB72BE73B1 /* RoomTimelineController.swift */; }; 78BF60C696FFED63AAF58D10 /* SoftLogoutViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22D46DB0CC6C55EBA7AE67A3 /* SoftLogoutViewModel.swift */; }; 7963F98CDFDEAC75E072BD81 /* TextRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */; }; - 7A54700193DC1F264368746A /* UserIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E077F76026C85ED96FEBB810 /* UserIndicatorPresenter.swift */; }; 7A71AEF419904209BB8C2833 /* UserAgentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */; }; 7AE1FFB132F2B84EB8A2AEBC /* TemplateViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3340ABAE3A4647E80163AE18 /* TemplateViewModelTests.swift */; }; 7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */; }; @@ -212,14 +198,14 @@ 7E7DF1867F98B0D10A6C0A63 /* FileCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3648F2FADEF2672D6A0D489 /* FileCacheTests.swift */; }; 7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8BDC092D817B68CD9040C5 /* UserSessionStore.swift */; }; 7F08F4BC1312075E2B5EAEFA /* AuthenticationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */; }; - 7F19E97E7985F518C9018B83 /* RootRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF47564C584F614B7287F3EB /* RootRouter.swift */; }; 7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */; }; - 7FA4227B2BAAA71560252866 /* UserIndicatorDismissal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D1532B5D9FB0C8461A1453 /* UserIndicatorDismissal.swift */; }; + 7F64FA937B95924B3A44EC12 /* OnboardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB8E75B9CB6C78BE8D09B1AF /* OnboardingScreen.swift */; }; 7FED310F6AB7A70CBFB7C8A3 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C483956FA3D665E3842E319A /* SettingsScreen.swift */; }; 8024BE37156FF0A95A7A3465 /* AnalyticsPromptUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF11DD57D9FACF2A757AB024 /* AnalyticsPromptUITests.swift */; }; 80997E933A5B2C0868D80B45 /* MediaPlayerViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6410F8C03DC4AA46991A6B02 /* MediaPlayerViewModelProtocol.swift */; }; 80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */; }; - 80E04BE80A89A78FBB4863BB /* UserIndicatorViewPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193FB285430D3956B6E61E4D /* UserIndicatorViewPresentable.swift */; }; + 8196A2E71ACC902DD69F24EE /* UserNotificationControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DE6C5C756E1393202BA95CD /* UserNotificationControllerTests.swift */; }; + 834DD9E41FC42A509BAD52E3 /* NavigationControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 109361C96BFFBE2FD89BF15C /* NavigationControllerTests.swift */; }; 83E5054739949181CA981193 /* LoginCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD667C4BB98CF4F3FE2CE3B0 /* LoginCoordinator.swift */; }; 841172E1576A863F4450132D /* WeakKeyDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ADFDC712027931F2216668 /* WeakKeyDictionary.swift */; }; 85AFBB433AD56704A880F8A0 /* FramePreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */; }; @@ -227,7 +213,7 @@ 86C2E93920FD15AD17E193A9 /* BugReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E532D95330139D118A9BF88 /* BugReportViewModel.swift */; }; 872A6457DF573AF8CEAE927A /* LoginHomeserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9349F590E35CE514A71E6764 /* LoginHomeserver.swift */; }; 87756CA950ED55870A1AAE8F /* ServerSelectionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7D706FFF438CAF16F44D8C /* ServerSelectionCoordinator.swift */; }; - 8775F46AE3234A5A5688C19D /* UserIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FAAA4A76CE4A1F3014D9 /* UserIndicator.swift */; }; + 87BD4F95F9D603C309837378 /* UserNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA6B262D7584C65BC5B79A0E /* UserNotification.swift */; }; 8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BC7CA1BC1041E93077BBA1 /* HomeScreenModels.swift */; }; 890F0D453FE388756479AC97 /* AnalyticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C687844F60BFF532D49A994C /* AnalyticsTests.swift */; }; 8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */; }; @@ -246,7 +232,6 @@ 93BA4A81B6D893271101F9F0 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 531CE4334AC5CA8DFF6AEB84 /* DTCoreText */; }; 94A65DD8A353DF112EBEF67A /* SessionVerificationControllerProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D56469A9EE0CFA2B7BA9760 /* SessionVerificationControllerProxyProtocol.swift */; }; 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; - 94E062D08E27B0387658E364 /* SplashScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5CF94E124616FD89424B73 /* SplashScreenViewModelTests.swift */; }; 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B902EA6CD3296B0E10EE432B /* HomeScreen.swift */; }; 97189E495F0E47805D1868DB /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 527578916BD388A09F5A8036 /* DTCoreText */; }; 9738F894DB1BD383BE05767A /* ElementSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1027BB9A852F445B7623897F /* ElementSettings.swift */; }; @@ -254,6 +239,7 @@ 97CECF91D68235F1D13598D7 /* AnalyticsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77B3D4950F1707E66E4A45A /* AnalyticsConfiguration.swift */; }; 983896D611ABF52A5C37498D /* RoomSummaryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */; }; 989029A28C9E2F828AD6658A /* AppIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */; }; + 992477AB8E3F3C36D627D32E /* OnboardingViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC4437C107D52ED19357DFC /* OnboardingViewModelProtocol.swift */; }; 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; }; 99ED42B8F8D6BFB1DBCF4C45 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = D661CAB418C075A94306A792 /* AnalyticsEvents */; }; 9A47B7EFE3793760EEF68FFE /* UITestScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6FE34A0A47D010BBB4D4D4 /* UITestScreenIdentifier.swift */; }; @@ -263,14 +249,12 @@ 9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */; }; 9BE7A9CF6C593251D734B461 /* MockServerSelectionScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A20AE75FF4FF35B1FF6CA7 /* MockServerSelectionScreenState.swift */; }; 9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */; }; - 9C9E48A627C7C166084E3F5B /* LabelledActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F01DD1BBD4450E18115916 /* LabelledActivityIndicatorView.swift */; }; - 9CB5129C83F75921E5E28028 /* ToastViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C82DAE0B8EB28234E84E6CF /* ToastViewState.swift */; }; 9CCC77C31CB399661A034739 /* UserProperties+Element.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A6C4BE591FE5C38CE9C7EF3 /* UserProperties+Element.swift */; }; 9D2E03DB175A6AB14589076D /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = AA4E1BEB4E9BC2467006E12B /* AppAuth */; }; 9D9690D2FD4CD26FF670620F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75EF87651B00A176AB08E97 /* AppDelegate.swift */; }; 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */; }; 9E8AE387FD03E4F1C1B8815A /* SessionVerificationStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06FCD42EEFEFC220F14EAC5 /* SessionVerificationStateMachine.swift */; }; - A00DFC1DD3567B1EDC9F8D16 /* SplashScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 325A2B3278875554DDEB8A9B /* SplashScreenUITests.swift */; }; + 9F41FF9C53F7A6EAEA6259C9 /* InviteFriendsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7AB0A148FCCAC28681C190 /* InviteFriendsCoordinator.swift */; }; A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */; }; A32517FB1CA0BBCE2BC75249 /* BugReportCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6C07DA7D3FF193F7419F55 /* BugReportCoordinator.swift */; }; A371629728E597C5FCA3C2B2 /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73FC861755C6388F62B9280A /* Analytics.swift */; }; @@ -285,11 +269,13 @@ A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */; }; A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */; }; A8EC7C9D886244DAE9433E37 /* SessionVerificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C18FAAD59AE7F1462D817E /* SessionVerificationViewModel.swift */; }; + A9D23B78F42BCDD896531436 /* UserNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649759084B0C9FE1F8DF8D17 /* UserNotificationPresenter.swift */; }; AA050DF4AEE54A641BA7CA22 /* RoomSummaryProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */; }; AAF0BBED840DF4A53EE85E77 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = C2C69B8BA5A9702E7A8BC08F /* MatrixRustSDK */; }; AB34401E4E1CAD5D2EC3072B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9760103CF316DF68698BCFE6 /* LaunchScreen.storyboard */; }; AB4C5D62A21AD712811CE8CD /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68232D336E2B546AD95B78B5 /* XCUIElement.swift */; }; ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */; }; + AC5CC8250CEAE57B73900C57 /* UserNotificationModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD80F22830C2360F3F39DDCE /* UserNotificationModalView.swift */; }; AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */; }; ACF094CF3BF02DBFA6DFDE60 /* AuthenticationCoordinatorUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */; }; B037C365CF8A58A0D149A2DB /* AuthenticationIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97755C01C3971474EFAD5367 /* AuthenticationIconImage.swift */; }; @@ -312,11 +298,8 @@ BB6B0B91CE11E06330017000 /* SessionVerificationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AC1A01C3A745BDF1D3697D3 /* SessionVerificationScreen.swift */; }; BCC3EDB7AD0902797CB4BBC2 /* MXLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = EF188681D6B6068CFAEAFC3F /* MXLogger.m */; }; BCEC41FB1F2BB663183863E4 /* LoginServerInfoSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D379E13DD9D987470A3C70C /* LoginServerInfoSection.swift */; }; - BEEC06EFD30BFCA02F0FD559 /* UserIndicatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D8EA85D4F10D7445BB6368A /* UserIndicatorTests.swift */; }; - BF35062D06888FA80BD139FF /* Presentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB7F9D6FC121204D59E18DF /* Presentable.swift */; }; BFD1AC03B6F8C5F5897D5B55 /* ReversedScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DE30233B57761F8AFEB415 /* ReversedScrollView.swift */; }; - C052A8CDC7A8E7A2D906674F /* UserIndicatorStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FAA6438B00FDB130F404E31 /* UserIndicatorStore.swift */; }; - C2CF93B067FD935E4F82FE44 /* SplashScreenPageIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850064FF8D7DB9C875E7AA1A /* SplashScreenPageIndicator.swift */; }; + C3522917C0C367C403429EEC /* CoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B251F5B4511D1CA0BA8361FE /* CoordinatorProtocol.swift */; }; C35CF4DAB1467FE1BBDC204B /* MessageTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAF1C75771D9DC75877F4B4 /* MessageTimelineItem.swift */; }; C4180F418235DAD9DD173951 /* TemplateScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9873076F224E4CE09D8BD47D /* TemplateScreenUITests.swift */; }; C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */; }; @@ -336,7 +319,7 @@ CC736DA1AA8F8B9FD8785009 /* ScreenshotDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */; }; CD6A72B65D3B6076F4045C30 /* PHGPostHogConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */; }; CE1694C7BB93C3311524EF28 /* Untranslated.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2F7194F440375338F8E2487 /* Untranslated.strings */; }; - CE7A715947ABAB1DEB5C21D7 /* SplashScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F7A812F160E75B69A9181A2 /* SplashScreenCoordinator.swift */; }; + CE7148E80F09B7305E026AC6 /* OnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1198B925F4A88DA74083662 /* OnboardingViewModel.swift */; }; CEB8FB1269DE20536608B957 /* LoginMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41FABA2B0AEF4389986495 /* LoginMode.swift */; }; CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */; }; D034A195A3494E38BF060485 /* MockSessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A9CCCF53495CF3D7B19FCE /* MockSessionVerificationControllerProxy.swift */; }; @@ -345,15 +328,15 @@ D3E603A5E9D529CF293E1BF9 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1651A532305027D3F605E2B /* VideoPlayerCoordinator.swift */; }; D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */; }; D6417E5A799C3C7F14F9EC0A /* SessionVerificationViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3069ADED46D063202FE7698 /* SessionVerificationViewModelProtocol.swift */; }; + D79F0F852C6A4255D5E616D2 /* UserNotificationControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ED2D2F6A137A95EA50413BE /* UserNotificationControllerProtocol.swift */; }; D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */; }; D85D4FA590305180B4A41795 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3073CCD77D906B330BC1D6 /* Tests.swift */; }; D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; }; D8CFF02C2730EE5BC4F17ABF /* ElementToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */; }; - D94F664677C380A3CAB8D7F6 /* ActivityIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */; }; - DCB781BD227CA958809AFADF /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CC95CD75B688E946438165 /* Coordinator.swift */; }; DD9B70DE54B24E0694A35D8A /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; DDB80FD2753FEAAE43CC2AAE /* ImageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A63815AD6A5C306453342F2 /* ImageRoomTimelineItem.swift */; }; DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */; }; + DF004A5B2EABBD0574D06A04 /* SplashScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */; }; DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */; }; DF790EF2E4D41D1091AEB263 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317F02B15921BF5CC8486990 /* KeychainController.swift */; }; DFF7D6A6C26DDD40D00AE579 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = F012CB5EE3F2B67359F6CC52 /* target.yml */; }; @@ -375,24 +358,22 @@ EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F40F48279322E504153AB0D /* MockClientProxy.swift */; }; EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; }; EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; }; - EF99A92701E401C4CD5ADC50 /* SplashScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE978A6118C131D7F2A04B3 /* SplashScreenModels.swift */; }; F040ABFEB0A2B142D948BA12 /* Untranslated.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = F75DF9500D69A3AAF8339E69 /* Untranslated.stringsdict */; }; F0F82C3C848C865C3098AA52 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 67E7A6F388D3BF85767609D9 /* Sentry */; }; + F257F964493A9CD02A6F720C /* OnboardingPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF2717AB91060260E5F4781 /* OnboardingPageView.swift */; }; F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94BCC8A9C73C1F838122C645 /* TimelineItemPlainStylerView.swift */; }; - F56261126E368C831B3DE976 /* NavigationRouterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752DEC02D93AFF46BC13313A /* NavigationRouterType.swift */; }; F656F92A63D3DC1978D79427 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */; }; F6E860FF7B18B81DF43B30B8 /* EncryptedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FA7C8D4EF2B1873C180ED7 /* EncryptedRoomTimelineItem.swift */; }; F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; }; F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */; }; - F75C4222D52B643214D5E623 /* UITestsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81740EEAFDF0D34C5E10D0DF /* UITestsRootView.swift */; }; F99FB21EFC6D99D247FE7CBE /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DE8DC9B3FBA402117DC4C49F /* Kingfisher */; }; + F9F6D2883BBEBB9A3789A137 /* OnboardingViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */; }; FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; }; - FC6B7436C3A5B3D0565227D5 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF05352F28D4E7336228E9F4 /* ActivityIndicatorView.swift */; }; - FCB640C576292BEAF7FA3B2E /* SplashViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F395A2E917115C7AAF7F34 /* SplashViewController.swift */; }; FCD3F2B82CAB29A07887A127 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 2B43F2AF7456567FE37270A7 /* KeychainAccess */; }; FD4706DC752744A0C91ED6FE /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B275C686F8253E655E42BA3 /* FileManager.swift */; }; FE4593FC2A02AAF92E089565 /* ElementAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF1593DD87F974F8509BB619 /* ElementAnimations.swift */; }; FE79E2BCCF69E8BF4D21E15A /* RoomMessageFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA154570F693D93513E584C1 /* RoomMessageFactory.swift */; }; + FE8D76708280968F7A670852 /* MockUserNotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9080CDD3881D0D1B2F280A7C /* MockUserNotificationController.swift */; }; FFD3E4FF948E06C7585317FC /* TimelineStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892E29C98C4E8182C9037F84 /* TimelineStyler.swift */; }; /* End PBXBuildFile section */ @@ -421,10 +402,10 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModelTests.swift; sourceTree = ""; }; 01C4C7DB37597D7D8379511A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 02A07FF019724B6ACEA73076 /* szl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = szl; path = szl.lproj/Localizable.strings; sourceTree = ""; }; 04BBC9E08250EF92ADE89CFD /* sr-Latn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sr-Latn"; path = "sr-Latn.lproj/Localizable.strings"; sourceTree = ""; }; - 04E1273CC3BC3E471AF87BE5 /* UserIndicatorQueueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorQueueTests.swift; sourceTree = ""; }; 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionUITests.swift; sourceTree = ""; }; 057B747CF045D3C6C30EAB2C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fi; path = fi.lproj/Localizable.stringsdict; sourceTree = ""; }; 0776771332259AB1C9661430 /* MXLog.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MXLog.h; sourceTree = ""; }; @@ -432,6 +413,7 @@ 086B997409328F091EBA43CE /* RoomScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenUITests.swift; sourceTree = ""; }; 08F64963396A6A23538EFCEC /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = is; path = is.lproj/Localizable.stringsdict; sourceTree = ""; }; 090CA61A835C151CEDF8F372 /* WeakDictionaryKeyReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakDictionaryKeyReference.swift; sourceTree = ""; }; + 09199C43BAB209C0BD89A836 /* OnboardingPageIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPageIndicator.swift; sourceTree = ""; }; 0950733DD4BA83EEE752E259 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = ""; }; 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProviderProtocol.swift; sourceTree = ""; }; 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementToggleStyle.swift; sourceTree = ""; }; @@ -440,19 +422,22 @@ 0B869438A1B52836F912A702 /* MockSoftLogoutScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSoftLogoutScreenState.swift; sourceTree = ""; }; 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSenderAvatarView.swift; sourceTree = ""; }; 0C13A92C1E9C79F055B8133D /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = ""; }; + 0C88046D6A070D9827181C4D /* OnboardingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingUITests.swift; sourceTree = ""; }; 0CB569EAA5017B5B23970655 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = ""; }; 0DB634B42CFE667112369D57 /* VideoPlayerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerScreen.swift; sourceTree = ""; }; 0DD16CE9A66C9040B066AD60 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = vi; path = vi.lproj/Localizable.stringsdict; sourceTree = ""; }; + 0DE6C5C756E1393202BA95CD /* UserNotificationControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationControllerTests.swift; sourceTree = ""; }; 0E7062F88E9D5F79C8A80524 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = th; path = th.lproj/Localizable.stringsdict; sourceTree = ""; }; 0E8BDC092D817B68CD9040C5 /* UserSessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStore.swift; sourceTree = ""; }; 0EE9EAF0309A2A1D67D8FAF5 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sv; path = sv.lproj/Localizable.stringsdict; sourceTree = ""; }; 0F52BF30D12BA3BD3D3DBB8F /* ServerSelectionViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelProtocol.swift; sourceTree = ""; }; - 0F7A812F160E75B69A9181A2 /* SplashScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenCoordinator.swift; sourceTree = ""; }; 1027BB9A852F445B7623897F /* ElementSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementSettings.swift; sourceTree = ""; }; 1059E2AE7878CF7820592637 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFactory.swift; sourceTree = ""; }; 105D16E7DB0CCE9526612BDD /* bn-IN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "bn-IN"; path = "bn-IN.lproj/Localizable.strings"; sourceTree = ""; }; + 109361C96BFFBE2FD89BF15C /* NavigationControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationControllerTests.swift; sourceTree = ""; }; 10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProviderProtocol.swift; sourceTree = ""; }; + 1113CA0A67B4AA227AAFB63B /* UserNotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationController.swift; sourceTree = ""; }; 111B698739E3410E2CDB7144 /* MXLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = ""; }; 113356152C099951A6D17D85 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; 1215A4FC53D2319E81AE8970 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -466,17 +451,19 @@ 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = ""; }; 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = ""; }; 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = ""; }; - 193FB285430D3956B6E61E4D /* UserIndicatorViewPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorViewPresentable.swift; sourceTree = ""; }; 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = ""; }; 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = ""; }; 1A3FC45B7643298BF361CEB1 /* VideoPlayerModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerModels.swift; sourceTree = ""; }; 1A63815AD6A5C306453342F2 /* ImageRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineItem.swift; sourceTree = ""; }; + 1BC4437C107D52ED19357DFC /* OnboardingViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModelProtocol.swift; sourceTree = ""; }; 1C429043E986008B97736636 /* ab */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ab; path = ab.lproj/Localizable.strings; sourceTree = ""; }; 1D56469A9EE0CFA2B7BA9760 /* SessionVerificationControllerProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxyProtocol.swift; sourceTree = ""; }; 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenUITests.swift; sourceTree = ""; }; + 1DF2717AB91060260E5F4781 /* OnboardingPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPageView.swift; sourceTree = ""; }; 1E1FB768A24FDD2A5CA16E3C /* LoginViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModelProtocol.swift; sourceTree = ""; }; 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DTHTMLElement+AttributedStringBuilder.swift"; sourceTree = ""; }; 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentBuilder.swift; sourceTree = ""; }; + 1F7AB0A148FCCAC28681C190 /* InviteFriendsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteFriendsCoordinator.swift; sourceTree = ""; }; 2069C264213B9F381DF9F876 /* ta */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ta; path = ta.lproj/Localizable.stringsdict; sourceTree = ""; }; 2112A6CFEA46E672D90EBF54 /* kab */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kab; path = kab.lproj/Localizable.strings; sourceTree = ""; }; 218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedTimelineItemProtocol.swift; sourceTree = ""; }; @@ -485,7 +472,6 @@ 22B384D54464FA39C6C7F6E7 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ca; path = ca.lproj/Localizable.stringsdict; sourceTree = ""; }; 22D46DB0CC6C55EBA7AE67A3 /* SoftLogoutViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModel.swift; sourceTree = ""; }; 233D5F7E5E9F49ABF3413291 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hr; path = hr.lproj/Localizable.stringsdict; sourceTree = ""; }; - 24A534A4619D8FEFB6439FCC /* SplashScreenPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenPageView.swift; sourceTree = ""; }; 24B0C97D2F560BCB72BE73B1 /* RoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineController.swift; sourceTree = ""; }; 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = ""; }; 2583416C8974272ADBADDBE1 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-TW"; path = "zh-TW.lproj/Localizable.stringsdict"; sourceTree = ""; }; @@ -513,9 +499,7 @@ 317F02B15921BF5CC8486990 /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = ""; }; 31B01468022EC826CB2FD2C0 /* LoginModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginModels.swift; sourceTree = ""; }; 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModel.swift; sourceTree = ""; }; - 325A2B3278875554DDEB8A9B /* SplashScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenUITests.swift; sourceTree = ""; }; 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModelTests.swift; sourceTree = ""; }; - 32CE6D4FF64C9A3C18619224 /* SplashScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreen.swift; sourceTree = ""; }; 3340ABAE3A4647E80163AE18 /* TemplateViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewModelTests.swift; sourceTree = ""; }; 33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = ""; }; 3429142FE11930422E7CC1A0 /* UserSessionFlowCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinatorStateMachine.swift; sourceTree = ""; }; @@ -540,15 +524,12 @@ 3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTaskService.swift; sourceTree = ""; }; 3F40F48279322E504153AB0D /* MockClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockClientProxy.swift; sourceTree = ""; }; 3F9E67AAB66638C69626866C /* UserSessionFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinator.swift; sourceTree = ""; }; - 3FAA6438B00FDB130F404E31 /* UserIndicatorStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorStore.swift; sourceTree = ""; }; 3FDFF4C1153D263BAB93C1F3 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactory.swift; sourceTree = ""; }; 40B21E611DADDEF00307E7AC /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; - 4112D04077F6709C5CA0A13E /* FullscreenLoadingViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenLoadingViewPresenter.swift; sourceTree = ""; }; 41C2348F84A80F682E3A68D0 /* MediaPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerCoordinator.swift; sourceTree = ""; }; 422724361B6555364C43281E /* RoomHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomHeaderView.swift; sourceTree = ""; }; 434522ED2BDED08759048077 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; - 4470B8CB654B097D807AA713 /* ToastViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewPresenter.swift; sourceTree = ""; }; 447A6399BC5EDE7AF7713267 /* MediaPlayerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerScreen.swift; sourceTree = ""; }; 4488F5F92A64A137665C96CD /* pa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pa; path = pa.lproj/Localizable.strings; sourceTree = ""; }; 44AEEE13AC1BF303AE48CBF8 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = ""; }; @@ -561,16 +542,16 @@ 475EB595D7527E9A8A14043E /* uz */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uz; path = uz.lproj/Localizable.strings; sourceTree = ""; }; 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = ""; }; 47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; - 48971F1FFD7FC5C466889FC7 /* SplashViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SplashViewController.xib; sourceTree = ""; }; 48CE6BF18E542B32FA52CE06 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = ""; }; 49193CB0C248D621A96FB2AA /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; + 495D3EC4972639C1A87DDF8E /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = ""; }; 4990FDBDA96B88E214F92F48 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = ""; }; 49D2C8E66E83EA578A7F318A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Benchmark.swift; sourceTree = ""; }; + 4A57A4AFA6A068668AFBD070 /* UIActivityViewControllerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIActivityViewControllerWrapper.swift; sourceTree = ""; }; 4B362E695A7103C11F64B185 /* AnalyticsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettings.swift; sourceTree = ""; }; 4B40B7F6FCCE2D8C242492D9 /* ga */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ga; path = ga.lproj/Localizable.strings; sourceTree = ""; }; 4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = ""; }; - 4C82DAE0B8EB28234E84E6CF /* ToastViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewState.swift; sourceTree = ""; }; 4C8D988E82A8DFA13BE46F7C /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = pl; path = pl.lproj/Localizable.stringsdict; sourceTree = ""; }; 4CD6AC7546E8D7E5C73CEA48 /* ElementX.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = ElementX.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = ""; }; @@ -590,50 +571,41 @@ 541542F5AC323709D8563458 /* AnalyticsPrompt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPrompt.swift; sourceTree = ""; }; 5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = ""; }; 54E438DBCBDC7A41B95DDDD9 /* MXLogObjcWrapper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MXLogObjcWrapper.m; sourceTree = ""; }; - 551DAED7F623AA5366E79927 /* repository */ = {isa = PBXFileReference; lastKnownFileType = folder; name = repository; path = .; sourceTree = SOURCE_ROOT; }; 55BC11560C8A2598964FFA4C /* bs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bs; path = bs.lproj/Localizable.strings; sourceTree = ""; }; 55D7187F6B0C0A651AC3DFFA /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = in; path = in.lproj/Localizable.strings; sourceTree = ""; }; 55EA4B03F92F31EAA83B3F7B /* FilePreviewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewModels.swift; sourceTree = ""; }; 55F30E764BED111C81739844 /* SoftLogoutUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutUITests.swift; sourceTree = ""; }; 56C1BCB9E83B09A45387FCA2 /* EncryptedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineView.swift; sourceTree = ""; }; - 56F01DD1BBD4450E18115916 /* LabelledActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelledActivityIndicatorView.swift; sourceTree = ""; }; 5773C86AF04AEF26515AD00C /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Localizable.strings; sourceTree = ""; }; 5872785B9C7934940146BFBA /* MXLogger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MXLogger.h; sourceTree = ""; }; 5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelProtocol.swift; sourceTree = ""; }; - 5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterStoreProtocol.swift; sourceTree = ""; }; - 5CB7F9D6FC121204D59E18DF /* Presentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presentable.swift; sourceTree = ""; }; 5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = ""; }; 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinatorUITests.swift; sourceTree = ""; }; - 5D8EA85D4F10D7445BB6368A /* UserIndicatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorTests.swift; sourceTree = ""; }; 5F12E996BFBEB43815189ABF /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = ""; }; 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = ""; }; - 5F77E8010D41AA3F5F9A1FCA /* NavigationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = ""; }; 5FF214969B25BFCBF87B908B /* bn-BD */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "bn-BD"; path = "bn-BD.lproj/Localizable.stringsdict"; sourceTree = ""; }; 6033779EB37259F27F938937 /* ClientProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyProtocol.swift; sourceTree = ""; }; 6045E825AE900A92D61FEFF0 /* ImageAnonymizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAnonymizerTests.swift; sourceTree = ""; }; 607974D08BD2AF83725D817A /* RoomMessageProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageProtocol.swift; sourceTree = ""; }; 616197D81103330BF2ADD559 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = gl.lproj/Localizable.strings; sourceTree = ""; }; 61ADFB893DEF81E58DF3FAB9 /* MockRoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineController.swift; sourceTree = ""; }; - 61B73D5E21F524A9BE44448D /* UserIndicatorRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorRequest.swift; sourceTree = ""; }; - 6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RectangleToastView.swift; sourceTree = ""; }; 624244C398804ADC885239AA /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; 62BDF0FF4F59AF6EA858B70B /* FilePreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewViewModel.swift; sourceTree = ""; }; 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = ""; }; 6410F8C03DC4AA46991A6B02 /* MediaPlayerViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerViewModelProtocol.swift; sourceTree = ""; }; + 649759084B0C9FE1F8DF8D17 /* UserNotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationPresenter.swift; sourceTree = ""; }; 653610CB5F9776EAAAB98155 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = ""; }; 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationServiceProxy.swift; sourceTree = ""; }; 6654859746B0BE9611459391 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = ""; }; 667DD3A9D932D7D9EB380CAA /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sk; path = sk.lproj/Localizable.stringsdict; sourceTree = ""; }; 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = ""; }; 68232D336E2B546AD95B78B5 /* XCUIElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIElement.swift; sourceTree = ""; }; - 68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorPresenter.swift; sourceTree = ""; }; 6920A4869821BF72FFC58842 /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = ""; }; 6A1AAC8EB2992918D01874AC /* rue */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = rue; path = rue.lproj/Localizable.strings; sourceTree = ""; }; 6A6C4BE591FE5C38CE9C7EF3 /* UserProperties+Element.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserProperties+Element.swift"; sourceTree = ""; }; 6A901D95158B02CA96C79C7F /* InfoPlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPlist.swift; sourceTree = ""; }; 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegex.swift; sourceTree = ""; }; 6B275C686F8253E655E42BA3 /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; - 6B73A8C3118EAC7BF3F3EE7A /* SplashScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenViewModelProtocol.swift; sourceTree = ""; }; 6BC38904A9663F7FAFD47457 /* SoftLogoutViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModelProtocol.swift; sourceTree = ""; }; 6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationUITests.swift; sourceTree = ""; }; 6DB53055CB130F0651C70763 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; @@ -641,7 +613,6 @@ 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = ""; }; 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindableState.swift; sourceTree = ""; }; 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateStoreViewModel.swift; sourceTree = ""; }; - 6FA072E995316CD18BC29313 /* UserIndicatorPresentationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorPresentationContext.swift; sourceTree = ""; }; 6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermalinkBuilderTests.swift; sourceTree = ""; }; 6FC5015B9634698BDB8701AF /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Localizable.stringsdict; sourceTree = ""; }; 71BC7CA1BC1041E93077BBA1 /* HomeScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenModels.swift; sourceTree = ""; }; @@ -649,11 +620,9 @@ 72D03D36422177EF01905D20 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = ""; }; 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderProtocol.swift; sourceTree = ""; }; 73FC861755C6388F62B9280A /* Analytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Analytics.swift; sourceTree = ""; }; - 752DEC02D93AFF46BC13313A /* NavigationRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterType.swift; sourceTree = ""; }; 799A3A11C434296ED28F87C8 /* iw */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = iw; path = iw.lproj/Localizable.strings; sourceTree = ""; }; 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModelTests.swift; sourceTree = ""; }; 7B04BD3874D736127A8156B8 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; - 7B5CF94E124616FD89424B73 /* SplashScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenViewModelTests.swift; sourceTree = ""; }; 7BDF6A69C2BB99535193E554 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = ""; }; 7D0CBC76C80E04345E11F2DB /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFactoryProtocol.swift; sourceTree = ""; }; @@ -666,14 +635,12 @@ 804F9B0FABE093C7284CD09B /* TimelineItemList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemList.swift; sourceTree = ""; }; 8140010A796DB2C7977B6643 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; 8166F121C79C7B62BF01D508 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = pt; path = pt.lproj/Localizable.stringsdict; sourceTree = ""; }; - 81740EEAFDF0D34C5E10D0DF /* UITestsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsRootView.swift; sourceTree = ""; }; 81B17DB1BC3B0C62AF84D230 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 8210612D17A39369480FC183 /* MediaSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSource.swift; sourceTree = ""; }; 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCornerShape.swift; sourceTree = ""; }; 84E92FF38EBC12EC2452C79C /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/Localizable.strings"; sourceTree = ""; }; - 850064FF8D7DB9C875E7AA1A /* SplashScreenPageIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenPageIndicator.swift; sourceTree = ""; }; + 854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenCoordinator.swift; sourceTree = ""; }; 858F8D0B0D51CC41BAA18E24 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; - 85C2318DF4C0E601EEE31F84 /* ActivityIndicatorPresenterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorPresenterType.swift; sourceTree = ""; }; 85CB1DDCEE53B946D09DF4F6 /* bn-BD */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "bn-BD"; path = "bn-BD.lproj/Localizable.strings"; sourceTree = ""; }; 873718F8BD17B778C5141C45 /* ta */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ta; path = ta.lproj/Localizable.strings; sourceTree = ""; }; 874A1842477895F199567BD7 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = ""; }; @@ -690,10 +657,11 @@ 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 = ""; }; + 8ED2D2F6A137A95EA50413BE /* UserNotificationControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationControllerProtocol.swift; sourceTree = ""; }; 8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummaryProvider.swift; sourceTree = ""; }; 8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 9010EE0CC913D095887EF36E /* OIDCService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCService.swift; sourceTree = ""; }; - 90733775209F4D4D366A268F /* RootRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouterType.swift; sourceTree = ""; }; + 9080CDD3881D0D1B2F280A7C /* MockUserNotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserNotificationController.swift; sourceTree = ""; }; 9238D3A3A00F45E841FE4EFF /* DebugScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugScreen.swift; sourceTree = ""; }; 92B7BF5D0705F3CB70E7B2D7 /* MediaPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerViewModel.swift; sourceTree = ""; }; 92FCD9116ADDE820E4E30F92 /* UIKitBackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTask.swift; sourceTree = ""; }; @@ -703,7 +671,6 @@ 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelTests.swift; sourceTree = ""; }; 9414DCADBDF9D6C4B806F61E /* sample_screenshot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = sample_screenshot.png; sourceTree = ""; }; 94BCC8A9C73C1F838122C645 /* TimelineItemPlainStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemPlainStylerView.swift; sourceTree = ""; }; - 95CC95CD75B688E946438165 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; 96F37AB24AF5A006521D38D1 /* RoomMessageFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageFactoryProtocol.swift; sourceTree = ""; }; 9772C1D2223108EB3131AEE4 /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = ""; }; 97755C01C3971474EFAD5367 /* AuthenticationIconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationIconImage.swift; sourceTree = ""; }; @@ -750,6 +717,7 @@ AAC9344689121887B74877AF /* UnitTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AACE9B8E1A4AE79A7E2914F6 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = es; path = es.lproj/Localizable.stringsdict; sourceTree = ""; }; AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTaskServiceProtocol.swift; sourceTree = ""; }; + AB8E75B9CB6C78BE8D09B1AF /* OnboardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = ""; }; ACA11F7F50A4A3887A18CA5A /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewProvider.swift; sourceTree = ""; }; AD378D580A41E42560C60E9C /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; @@ -758,24 +726,19 @@ ADCB8A232D3A8FB3E16A7303 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; AE225C66978648AA4AF37B45 /* te */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = te; path = te.lproj/Localizable.strings; sourceTree = ""; }; AE5DDBEBBA17973ED4638823 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = ""; }; - AF05352F28D4E7336228E9F4 /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = ""; }; AF11DD57D9FACF2A757AB024 /* AnalyticsPromptUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptUITests.swift; sourceTree = ""; }; AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderTests.swift; sourceTree = ""; }; - AFABDF2E19D349DAAAC18C65 /* RoundedToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedToastView.swift; sourceTree = ""; }; B08CBE1E670690ECF11C2C6A /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = eu; path = eu.lproj/Localizable.stringsdict; sourceTree = ""; }; B0C5E5931A668B18D8C09028 /* cy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cy; path = cy.lproj/Localizable.strings; sourceTree = ""; }; B1183B55FF4B01022DA721CB /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/Localizable.strings"; sourceTree = ""; }; - B1D1532B5D9FB0C8461A1453 /* UserIndicatorDismissal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorDismissal.swift; sourceTree = ""; }; + B251F5B4511D1CA0BA8361FE /* CoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatorProtocol.swift; sourceTree = ""; }; B3069ADED46D063202FE7698 /* SessionVerificationViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationViewModelProtocol.swift; sourceTree = ""; }; B3FA7C8D4EF2B1873C180ED7 /* EncryptedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineItem.swift; sourceTree = ""; }; - B4173A48FD8542CD4AD3645C /* NavigationRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouter.swift; sourceTree = ""; }; B43AF03660F5FD4FFFA7F1CE /* TimelineItemContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemContextMenu.swift; sourceTree = ""; }; B4C18FAAD59AE7F1462D817E /* SessionVerificationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationViewModel.swift; sourceTree = ""; }; B4DE1CF8F5EFD353B1A5E36F /* AnalyticsPromptCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptCoordinator.swift; sourceTree = ""; }; B516212D9FE785DDD5E490D1 /* BugReportModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportModels.swift; sourceTree = ""; }; B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; - B64F3A3D0DF86ED5A241AB05 /* ActivityIndicatorView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ActivityIndicatorView.xib; sourceTree = ""; }; - B695D0D12086158BAD1D9859 /* UserIndicatorPresenterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorPresenterSpy.swift; sourceTree = ""; }; B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; B7E035C6AC137C9392D98814 /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lv; path = lv.lproj/Localizable.strings; sourceTree = ""; }; B80D1901BA0B095E27793EDE /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; @@ -786,6 +749,7 @@ B902EA6CD3296B0E10EE432B /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; BA7B2E9CC5DC3B76ADC35A43 /* AnalyticsPromptCheckmarkItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptCheckmarkItem.swift; sourceTree = ""; }; BB3073CCD77D906B330BC1D6 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = ""; }; + BB33A751BFDA223BDD106EC0 /* OnboardingModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingModels.swift; sourceTree = ""; }; BC9B05D6B293A039EB963CA7 /* az */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az; path = az.lproj/Localizable.strings; sourceTree = ""; }; BCBE603A7EB2C93E81BA6415 /* MediaPlayerModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerModels.swift; sourceTree = ""; }; BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerTextField.swift; sourceTree = ""; }; @@ -795,6 +759,7 @@ C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXAttributeScope.swift; sourceTree = ""; }; C06FCD42EEFEFC220F14EAC5 /* SessionVerificationStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationStateMachine.swift; sourceTree = ""; }; C070FD43DC6BF4E50217965A /* LocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = ""; }; + C1198B925F4A88DA74083662 /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = ""; }; C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = ""; }; C2DE30233B57761F8AFEB415 /* ReversedScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReversedScrollView.swift; sourceTree = ""; }; C3F652E88106B855A2A55ADE /* FilePreviewViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewViewModelProtocol.swift; sourceTree = ""; }; @@ -808,10 +773,8 @@ C88508B6F7974CFABEC4B261 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = ""; }; C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineView.swift; sourceTree = ""; }; - C91A6BC1A54CDB598EE2A81B /* UserIndicatorQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorQueue.swift; sourceTree = ""; }; C95ADE8D9527523572532219 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Localizable.stringsdict; sourceTree = ""; }; C9A86C95340248A8B7BA9A43 /* AnalyticsPromptViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptViewModelProtocol.swift; sourceTree = ""; }; - C9F395A2E917115C7AAF7F34 /* SplashViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewController.swift; sourceTree = ""; }; CAAE4A709C0A2144C103AA0F /* ang */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ang; path = ang.lproj/Localizable.strings; sourceTree = ""; }; CACA846B3E3E9A521D98B178 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; CBA95E52C4C6EE8769A63E57 /* eo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eo; path = eo.lproj/Localizable.strings; sourceTree = ""; }; @@ -821,21 +784,19 @@ CC6FE34A0A47D010BBB4D4D4 /* UITestScreenIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestScreenIdentifier.swift; sourceTree = ""; }; CC7CCC6DE5FA623E31BA8546 /* RoomTimelineControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerProtocol.swift; sourceTree = ""; }; CCF86010A0A719A9A50EEC59 /* SessionVerificationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationCoordinator.swift; sourceTree = ""; }; + CD80F22830C2360F3F39DDCE /* UserNotificationModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationModalView.swift; sourceTree = ""; }; CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; CDE3F3911FF7CC639BDE5844 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; CED34C87277BA3CCC6B6EC7A /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = ""; }; - CF47564C584F614B7287F3EB /* RootRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouter.swift; sourceTree = ""; }; CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxy.swift; sourceTree = ""; }; - CF4B39D52CAE7D21D276ABEE /* ElementNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementNavigationController.swift; sourceTree = ""; }; - CF847B3C1873B8E81CEE7FAC /* SplashScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenViewModel.swift; sourceTree = ""; }; D06DFD894157A4C93A02D8B5 /* lo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lo; path = lo.lproj/Localizable.strings; sourceTree = ""; }; D09A267106B9585D3D0CFC0D /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = ""; }; D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineView.swift; sourceTree = ""; }; D0ADFDC712027931F2216668 /* WeakKeyDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakKeyDictionary.swift; sourceTree = ""; }; D1651A532305027D3F605E2B /* VideoPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerCoordinator.swift; sourceTree = ""; }; D1A9CCCF53495CF3D7B19FCE /* MockSessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSessionVerificationControllerProxy.swift; sourceTree = ""; }; - D29EBCBFEC6FD0941749404D /* NavigationRouterStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterStore.swift; sourceTree = ""; }; D2D783758EAE6A88C93564EB /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = ""; }; + D31DC8105C6233E5FFD9B84C /* element-x-ios */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "element-x-ios"; path = .; sourceTree = SOURCE_ROOT; }; D33116993D54FADC0C721C1F /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; D4DA544B2520BFA65D6DB4BB /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenCoordinator.swift; sourceTree = ""; }; @@ -843,17 +804,16 @@ D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = ""; }; D6D094C15E8DB424F1C6FC94 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = ""; }; D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = ""; }; + D751BB69BB7C38FD247517B4 /* UITestsRootCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsRootCoordinator.swift; sourceTree = ""; }; D77B3D4950F1707E66E4A45A /* AnalyticsConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsConfiguration.swift; sourceTree = ""; }; + DA6B262D7584C65BC5B79A0E /* UserNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotification.swift; sourceTree = ""; }; DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateCoordinator.swift; sourceTree = ""; }; DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = ""; }; DC77FC5C4F2000133047AA27 /* SoftLogoutModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutModels.swift; sourceTree = ""; }; - DCE978A6118C131D7F2A04B3 /* SplashScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenModels.swift; sourceTree = ""; }; DD667C4BB98CF4F3FE2CE3B0 /* LoginCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginCoordinator.swift; sourceTree = ""; }; - DD73FAAA4A76CE4A1F3014D9 /* UserIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicator.swift; sourceTree = ""; }; DED59F9EFF273BFA2055FFDF /* BugReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreen.swift; sourceTree = ""; }; DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationViewModelTests.swift; sourceTree = ""; }; DF38B69D2C331A499276F400 /* FilePreviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewViewModelTests.swift; sourceTree = ""; }; - E077F76026C85ED96FEBB810 /* UserIndicatorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorPresenter.swift; sourceTree = ""; }; E0FCA0957FAA0E15A9F5579D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Untranslated.stringsdict; sourceTree = ""; }; E157152B11E347F735C3FD6E /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = tr; path = tr.lproj/Localizable.stringsdict; sourceTree = ""; }; E18CF12478983A5EB390FB26 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = ""; }; @@ -868,6 +828,7 @@ E579A0DA01F488C97B771EF6 /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = lv; path = lv.lproj/Localizable.stringsdict; sourceTree = ""; }; E5D2C0950F8196232D88045C /* ServerSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreen.swift; sourceTree = ""; }; E5F2B6443D1ED8602F328539 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = ""; }; + E6281B199D8A8F0892490C2E /* OnboardingCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingCoordinator.swift; sourceTree = ""; }; E8294DB9E95C0C0630418466 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIFont+AttributedStringBuilder.m"; sourceTree = ""; }; E9D059BFE329BE09B6D96A9F /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ro; path = ro.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -890,6 +851,7 @@ F15BE37BE2FB86E00C8D150A /* AggregratedReaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregratedReaction.swift; sourceTree = ""; }; F23BA6D4842D53C5AC9B7584 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nn; path = nn.lproj/Localizable.stringsdict; sourceTree = ""; }; F2D58333B377888012740101 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; + F31A4E5941ACBA4BB9FEF94C /* UserNotificationToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationToastView.swift; sourceTree = ""; }; F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = ""; }; F3648F2FADEF2672D6A0D489 /* FileCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCacheTests.swift; sourceTree = ""; }; F506C6ADB1E1DA6638078E11 /* UITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -964,15 +926,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 02175C9269C4632DB6D12C25 /* Splash */ = { - isa = PBXGroup; - children = ( - C9F395A2E917115C7AAF7F34 /* SplashViewController.swift */, - 48971F1FFD7FC5C466889FC7 /* SplashViewController.xib */, - ); - path = Splash; - sourceTree = ""; - }; 052CC920F473C10B509F9FC1 /* SwiftUI */ = { isa = PBXGroup; children = ( @@ -1123,7 +1076,7 @@ children = ( 46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */, CC6FE34A0A47D010BBB4D4D4 /* UITestScreenIdentifier.swift */, - 81740EEAFDF0D34C5E10D0DF /* UITestsRootView.swift */, + D751BB69BB7C38FD247517B4 /* UITestsRootCoordinator.swift */, ); path = UITests; sourceTree = ""; @@ -1170,6 +1123,18 @@ path = Helpers; sourceTree = ""; }; + 3F38EAC92E2281990E65DAF2 /* OnboardingScreen */ = { + isa = PBXGroup; + children = ( + E6281B199D8A8F0892490C2E /* OnboardingCoordinator.swift */, + BB33A751BFDA223BDD106EC0 /* OnboardingModels.swift */, + C1198B925F4A88DA74083662 /* OnboardingViewModel.swift */, + 1BC4437C107D52ED19357DFC /* OnboardingViewModelProtocol.swift */, + 7B14834450AE76EEFDDBCBB8 /* View */, + ); + path = OnboardingScreen; + sourceTree = ""; + }; 4009BE2E791C16AC6EE39A7E /* BugReport */ = { isa = PBXGroup; children = ( @@ -1447,6 +1412,8 @@ A05707BF550D770168A406DB /* LoginViewModelTests.swift */, F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */, C444092DB0E4AB393067AC36 /* MediaPlayerViewModelTests.swift */, + 109361C96BFFBE2FD89BF15C /* NavigationControllerTests.swift */, + 00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */, 6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */, 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */, F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */, @@ -1455,11 +1422,10 @@ DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */, 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */, 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */, - 7B5CF94E124616FD89424B73 /* SplashScreenViewModelTests.swift */, 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */, EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */, + 0DE6C5C756E1393202BA95CD /* UserNotificationControllerTests.swift */, A3004DFA1B10951962787D90 /* VideoPlayerViewModelTests.swift */, - AF552BB969DC98A4BB8CF8D5 /* UserIndicators */, ); path = Sources; sourceTree = ""; @@ -1533,6 +1499,16 @@ path = Media; sourceTree = ""; }; + 7B14834450AE76EEFDDBCBB8 /* View */ = { + isa = PBXGroup; + children = ( + 09199C43BAB209C0BD89A836 /* OnboardingPageIndicator.swift */, + 1DF2717AB91060260E5F4781 /* OnboardingPageView.swift */, + AB8E75B9CB6C78BE8D09B1AF /* OnboardingScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 8039515BAA53B7C3275AC64A /* Client */ = { isa = PBXGroup; children = ( @@ -1601,7 +1577,7 @@ 9413F680ECDFB2B0DDB0DEF2 /* Packages */ = { isa = PBXGroup; children = ( - 551DAED7F623AA5366E79927 /* repository */, + D31DC8105C6233E5FFD9B84C /* element-x-ios */, ); name = Packages; sourceTree = SOURCE_ROOT; @@ -1615,12 +1591,12 @@ C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */, 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */, 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */, + 0C88046D6A070D9827181C4D /* OnboardingUITests.swift */, 086B997409328F091EBA43CE /* RoomScreenUITests.swift */, 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */, 6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */, E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */, 55F30E764BED111C81739844 /* SoftLogoutUITests.swift */, - 325A2B3278875554DDEB8A9B /* SplashScreenUITests.swift */, ); path = Sources; sourceTree = ""; @@ -1679,16 +1655,6 @@ path = Localizations; sourceTree = ""; }; - A19A5C94C2B8DC42606C1B4F /* View */ = { - isa = PBXGroup; - children = ( - 32CE6D4FF64C9A3C18619224 /* SplashScreen.swift */, - 850064FF8D7DB9C875E7AA1A /* SplashScreenPageIndicator.swift */, - 24A534A4619D8FEFB6439FCC /* SplashScreenPageView.swift */, - ); - path = View; - sourceTree = ""; - }; A253B36CAD2059B6D8C130CD /* View */ = { isa = PBXGroup; children = ( @@ -1708,6 +1674,16 @@ path = Style; sourceTree = ""; }; + A448A3A8F764174C60CD0CA1 /* Other */ = { + isa = PBXGroup; + children = ( + 1F7AB0A148FCCAC28681C190 /* InviteFriendsCoordinator.swift */, + 854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */, + 4A57A4AFA6A068668AFBD070 /* UIActivityViewControllerWrapper.swift */, + ); + path = Other; + sourceTree = ""; + }; A4852B57D55D71EEBFCD931D /* UnitTests */ = { isa = PBXGroup; children = ( @@ -1725,6 +1701,8 @@ 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */, C75EF87651B00A176AB08E97 /* AppDelegate.swift */, 263B3B811C2B900F12C6F695 /* BuildSettings.swift */, + B251F5B4511D1CA0BA8361FE /* CoordinatorProtocol.swift */, + 495D3EC4972639C1A87DDF8E /* NavigationController.swift */, ); path = Application; sourceTree = ""; @@ -1748,28 +1726,6 @@ path = UI; sourceTree = ""; }; - AF552BB969DC98A4BB8CF8D5 /* UserIndicators */ = { - isa = PBXGroup; - children = ( - B695D0D12086158BAD1D9859 /* UserIndicatorPresenterSpy.swift */, - 04E1273CC3BC3E471AF87BE5 /* UserIndicatorQueueTests.swift */, - 5D8EA85D4F10D7445BB6368A /* UserIndicatorTests.swift */, - ); - path = UserIndicators; - sourceTree = ""; - }; - B1A847595434E3DD177F5143 /* SplashScreen */ = { - isa = PBXGroup; - children = ( - 0F7A812F160E75B69A9181A2 /* SplashScreenCoordinator.swift */, - DCE978A6118C131D7F2A04B3 /* SplashScreenModels.swift */, - CF847B3C1873B8E81CEE7FAC /* SplashScreenViewModel.swift */, - 6B73A8C3118EAC7BF3F3EE7A /* SplashScreenViewModelProtocol.swift */, - A19A5C94C2B8DC42606C1B4F /* View */, - ); - path = SplashScreen; - sourceTree = ""; - }; B442FCF47E0A6F28D7D50A4D /* FilePreview */ = { isa = PBXGroup; children = ( @@ -1831,8 +1787,6 @@ E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */, 49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */, E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */, - 95CC95CD75B688E946438165 /* Coordinator.swift */, - CF4B39D52CAE7D21D276ABEE /* ElementNavigationController.swift */, 1027BB9A852F445B7623897F /* ElementSettings.swift */, 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */, 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */, @@ -1843,9 +1797,8 @@ 44BBB96FAA2F0D53C507396B /* Extensions */, 8F9A844EB44B6AD7CA18FD96 /* HTMLParsing */, 06501F0E978B2D5C92771DC7 /* Logging */, - FE50232944F9E67ADD7A2D21 /* Routers */, 052CC920F473C10B509F9FC1 /* SwiftUI */, - F8474EB69289112888B65518 /* UserIndicators */, + DA5F79E290EB586FC98AAC63 /* UserNotifications */, ); path = Other; sourceTree = ""; @@ -1934,6 +1887,20 @@ path = SessionVerification; sourceTree = ""; }; + DA5F79E290EB586FC98AAC63 /* UserNotifications */ = { + isa = PBXGroup; + children = ( + 9080CDD3881D0D1B2F280A7C /* MockUserNotificationController.swift */, + DA6B262D7584C65BC5B79A0E /* UserNotification.swift */, + 1113CA0A67B4AA227AAFB63B /* UserNotificationController.swift */, + 8ED2D2F6A137A95EA50413BE /* UserNotificationControllerProtocol.swift */, + CD80F22830C2360F3F39DDCE /* UserNotificationModalView.swift */, + 649759084B0C9FE1F8DF8D17 /* UserNotificationPresenter.swift */, + F31A4E5941ACBA4BB9FEF94C /* UserNotificationToastView.swift */, + ); + path = UserNotifications; + sourceTree = ""; + }; DBF3259D9A7092A49E0FE642 /* View */ = { isa = PBXGroup; children = ( @@ -1971,11 +1938,11 @@ B442FCF47E0A6F28D7D50A4D /* FilePreview */, B53CA9BECD3F97805E1432D0 /* HomeScreen */, D3E07C2F92EC8C5659601744 /* MediaPlayer */, + 3F38EAC92E2281990E65DAF2 /* OnboardingScreen */, + A448A3A8F764174C60CD0CA1 /* Other */, 679E9837ECA8D6776079D16E /* RoomScreen */, D958761758AA1110476DE6A3 /* SessionVerification */, 70B74A432C241E56A7ACE610 /* Settings */, - 02175C9269C4632DB6D12C25 /* Splash */, - B1A847595434E3DD177F5143 /* SplashScreen */, 285079C24A5189C48284CC47 /* VideoPlayer */, ); path = Screens; @@ -2028,31 +1995,6 @@ path = Background; sourceTree = ""; }; - F8474EB69289112888B65518 /* UserIndicators */ = { - isa = PBXGroup; - children = ( - 68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */, - 85C2318DF4C0E601EEE31F84 /* ActivityIndicatorPresenterType.swift */, - AF05352F28D4E7336228E9F4 /* ActivityIndicatorView.swift */, - B64F3A3D0DF86ED5A241AB05 /* ActivityIndicatorView.xib */, - 4112D04077F6709C5CA0A13E /* FullscreenLoadingViewPresenter.swift */, - 56F01DD1BBD4450E18115916 /* LabelledActivityIndicatorView.swift */, - 6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */, - AFABDF2E19D349DAAAC18C65 /* RoundedToastView.swift */, - 4470B8CB654B097D807AA713 /* ToastViewPresenter.swift */, - 4C82DAE0B8EB28234E84E6CF /* ToastViewState.swift */, - DD73FAAA4A76CE4A1F3014D9 /* UserIndicator.swift */, - B1D1532B5D9FB0C8461A1453 /* UserIndicatorDismissal.swift */, - 6FA072E995316CD18BC29313 /* UserIndicatorPresentationContext.swift */, - E077F76026C85ED96FEBB810 /* UserIndicatorPresenter.swift */, - C91A6BC1A54CDB598EE2A81B /* UserIndicatorQueue.swift */, - 61B73D5E21F524A9BE44448D /* UserIndicatorRequest.swift */, - 3FAA6438B00FDB130F404E31 /* UserIndicatorStore.swift */, - 193FB285430D3956B6E61E4D /* UserIndicatorViewPresentable.swift */, - ); - path = UserIndicators; - sourceTree = ""; - }; FCDF06BDB123505F0334B4F9 /* Timeline */ = { isa = PBXGroup; children = ( @@ -2069,21 +2011,6 @@ path = Timeline; sourceTree = ""; }; - FE50232944F9E67ADD7A2D21 /* Routers */ = { - isa = PBXGroup; - children = ( - 5F77E8010D41AA3F5F9A1FCA /* NavigationModule.swift */, - B4173A48FD8542CD4AD3645C /* NavigationRouter.swift */, - D29EBCBFEC6FD0941749404D /* NavigationRouterStore.swift */, - 5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */, - 752DEC02D93AFF46BC13313A /* NavigationRouterType.swift */, - 5CB7F9D6FC121204D59E18DF /* Presentable.swift */, - CF47564C584F614B7287F3EB /* RootRouter.swift */, - 90733775209F4D4D366A268F /* RootRouterType.swift */, - ); - path = Routers; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -2338,7 +2265,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 30122AB3484AC6C3A7F6A717 /* ActivityIndicatorView.xib in Resources */, B80C4FABB5529DF12436FFDA /* AppIcon.pdf in Resources */, 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */, B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */, @@ -2346,7 +2272,6 @@ 5F5488FBC9CFEB6F433D74A4 /* Localizable.strings in Resources */, 0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */, 690ED5315B401238A3249DCB /* README.md in Resources */, - 5B2C4C17888FC095ED6880B2 /* SplashViewController.xib in Resources */, CE1694C7BB93C3311524EF28 /* Untranslated.strings in Resources */, 2797C9D9BA642370F1C85D78 /* Untranslated.stringsdict in Resources */, DFF7D6A6C26DDD40D00AE579 /* target.yml in Resources */, @@ -2487,6 +2412,8 @@ 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */, 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */, 483507026FDCA2E16E5197A6 /* MediaPlayerViewModelTests.swift in Sources */, + 834DD9E41FC42A509BAD52E3 /* NavigationControllerTests.swift in Sources */, + F9F6D2883BBEBB9A3789A137 /* OnboardingViewModelTests.swift in Sources */, 27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */, 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */, EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */, @@ -2495,13 +2422,10 @@ 755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */, 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */, 09AAF04B27732046C755D914 /* SoftLogoutViewModelTests.swift in Sources */, - 94E062D08E27B0387658E364 /* SplashScreenViewModelTests.swift in Sources */, 1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */, 7AE1FFB132F2B84EB8A2AEBC /* TemplateViewModelTests.swift in Sources */, 8D3E1FADD78E72504DE0E402 /* UserAgentBuilderTests.swift in Sources */, - 1151DCC5EC2C6585826545EC /* UserIndicatorPresenterSpy.swift in Sources */, - 4B8A2C45FF906ADBB1F5C3B4 /* UserIndicatorQueueTests.swift in Sources */, - BEEC06EFD30BFCA02F0FD559 /* UserIndicatorTests.swift in Sources */, + 8196A2E71ACC902DD69F24EE /* UserNotificationControllerTests.swift in Sources */, 1504CE9A609A348D90B69E47 /* VideoPlayerViewModelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2510,9 +2434,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D94F664677C380A3CAB8D7F6 /* ActivityIndicatorPresenter.swift in Sources */, - 4D23C56053013437C35E511E /* ActivityIndicatorPresenterType.swift in Sources */, - FC6B7436C3A5B3D0565227D5 /* ActivityIndicatorView.swift in Sources */, 7096FA3AC218D914E88BFB70 /* AggregratedReaction.swift in Sources */, A50849766F056FD1DB942DEA /* AlertInfo.swift in Sources */, 39929D29B265C3F6606047DE /* AlignedScrollView.swift in Sources */, @@ -2557,12 +2478,11 @@ 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */, 24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */, 663E198678778F7426A9B27D /* Collection.swift in Sources */, - DCB781BD227CA958809AFADF /* Coordinator.swift in Sources */, + C3522917C0C367C403429EEC /* CoordinatorProtocol.swift in Sources */, C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */, 1CF18DE71D5D23C61BD88852 /* DebugScreen.swift in Sources */, EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */, FE4593FC2A02AAF92E089565 /* ElementAnimations.swift in Sources */, - 06E93B2E3B32740B40F47CC5 /* ElementNavigationController.swift in Sources */, 9738F894DB1BD383BE05767A /* ElementSettings.swift in Sources */, D8CFF02C2730EE5BC4F17ABF /* ElementToggleStyle.swift in Sources */, 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */, @@ -2582,7 +2502,6 @@ 1F04C63D4FA95948E3F52147 /* FileRoomTimelineView.swift in Sources */, A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */, 85AFBB433AD56704A880F8A0 /* FramePreferenceKey.swift in Sources */, - 6A367F3D7A437A79B7D9A31C /* FullscreenLoadingViewPresenter.swift in Sources */, 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */, 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */, 8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */, @@ -2595,10 +2514,10 @@ DDB80FD2753FEAAE43CC2AAE /* ImageRoomTimelineItem.swift in Sources */, D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */, A5C8F013ED9FB8AA6FEE18A7 /* InfoPlist.swift in Sources */, + 9F41FF9C53F7A6EAEA6259C9 /* InviteFriendsCoordinator.swift in Sources */, E3CA565A4B9704F191B191F0 /* JoinedRoomSize+MemberCount.swift in Sources */, DF790EF2E4D41D1091AEB263 /* KeychainController.swift in Sources */, 0C601923A872A87C775B889A /* KeychainControllerProtocol.swift in Sources */, - 9C9E48A627C7C166084E3F5B /* LabelledActivityIndicatorView.swift in Sources */, 15D867E638BFD0E5E71DB1EF /* List.swift in Sources */, 83E5054739949181CA981193 /* LoginCoordinator.swift in Sources */, 872A6457DF573AF8CEAE927A /* LoginHomeserver.swift in Sources */, @@ -2634,22 +2553,24 @@ 9BE7A9CF6C593251D734B461 /* MockServerSelectionScreenState.swift in Sources */, D034A195A3494E38BF060485 /* MockSessionVerificationControllerProxy.swift in Sources */, C74EE50257ED925C2B8EFCE6 /* MockSoftLogoutScreenState.swift in Sources */, + FE8D76708280968F7A670852 /* MockUserNotificationController.swift in Sources */, D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */, - 4ED453A61AF45EBE18D8BC69 /* NavigationModule.swift in Sources */, - 22DADD537401E79D66132134 /* NavigationRouter.swift in Sources */, - 12F70C493FB69F4D7E9A37EA /* NavigationRouterStore.swift in Sources */, - 344AF4CBB6D8786214878642 /* NavigationRouterStoreProtocol.swift in Sources */, - F56261126E368C831B3DE976 /* NavigationRouterType.swift in Sources */, + 41E16904B30C529373B4E1A4 /* NavigationController.swift in Sources */, 8BBD3AA589DEE02A1B0923B2 /* NoticeRoomTimelineItem.swift in Sources */, 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */, 563A05B43207D00A6B698211 /* OIDCService.swift in Sources */, + 2CB6787E25B11711518E9588 /* OnboardingCoordinator.swift in Sources */, + 5D7960B32C350FA93F48D02B /* OnboardingModels.swift in Sources */, + 14132418A748C988B85B025E /* OnboardingPageIndicator.swift in Sources */, + F257F964493A9CD02A6F720C /* OnboardingPageView.swift in Sources */, + 7F64FA937B95924B3A44EC12 /* OnboardingScreen.swift in Sources */, + CE7148E80F09B7305E026AC6 /* OnboardingViewModel.swift in Sources */, + 992477AB8E3F3C36D627D32E /* OnboardingViewModelProtocol.swift in Sources */, CD6A72B65D3B6076F4045C30 /* PHGPostHogConfiguration.swift in Sources */, 80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */, 7D1DAAA364A9A29D554BD24E /* PlaceholderAvatarImage.swift in Sources */, DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */, - BF35062D06888FA80BD139FF /* Presentable.swift in Sources */, C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */, - 53B9C2240C2F5533246EE230 /* RectangleToastView.swift in Sources */, 00EA14F62DCEF62CDE4808D6 /* RedactedRoomTimelineItem.swift in Sources */, 13853973A5E24374FCEDE8A3 /* RedactedRoomTimelineView.swift in Sources */, A494741843F087881299ACF0 /* RestorationToken.swift in Sources */, @@ -2679,10 +2600,7 @@ 5D430CDE11EAC3E8E6B80A66 /* RoomTimelineViewFactory.swift in Sources */, 297CD0A27C87B0C50FF192EE /* RoomTimelineViewFactoryProtocol.swift in Sources */, CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */, - 7F19E97E7985F518C9018B83 /* RootRouter.swift in Sources */, - 2C0CE61E5DC177938618E0B1 /* RootRouterType.swift in Sources */, B2F8E01ABA1BA30265B4ECBE /* RoundedCornerShape.swift in Sources */, - 462813B93C39DF93B1249403 /* RoundedToastView.swift in Sources */, CC736DA1AA8F8B9FD8785009 /* ScreenshotDetector.swift in Sources */, 1281625B25371BE53D36CB3A /* SeparatorRoomTimelineItem.swift in Sources */, 49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */, @@ -2709,14 +2627,7 @@ B09514A0A3EB3C19A4FD0B71 /* SoftLogoutScreen.swift in Sources */, 78BF60C696FFED63AAF58D10 /* SoftLogoutViewModel.swift in Sources */, 165A883C29998EC779465068 /* SoftLogoutViewModelProtocol.swift in Sources */, - 684BDE198AE5AA1392288A73 /* SplashScreen.swift in Sources */, - CE7A715947ABAB1DEB5C21D7 /* SplashScreenCoordinator.swift in Sources */, - EF99A92701E401C4CD5ADC50 /* SplashScreenModels.swift in Sources */, - C2CF93B067FD935E4F82FE44 /* SplashScreenPageIndicator.swift in Sources */, - 72F6E890820FF606A7E276C8 /* SplashScreenPageView.swift in Sources */, - 53504DF61DBC81ACC9B4D275 /* SplashScreenViewModel.swift in Sources */, - 5E1FCC43B738941D5A5F1794 /* SplashScreenViewModelProtocol.swift in Sources */, - FCB640C576292BEAF7FA3B2E /* SplashViewController.swift in Sources */, + DF004A5B2EABBD0574D06A04 /* SplashScreenCoordinator.swift in Sources */, B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */, 2F94054F50E312AF30BE07F3 /* String.swift in Sources */, A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */, @@ -2741,8 +2652,7 @@ 69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */, FFD3E4FF948E06C7585317FC /* TimelineStyler.swift in Sources */, 500CB65ED116B81DA52FDAEE /* TimelineView.swift in Sources */, - 4669804D0369FBED4E8625D1 /* ToastViewPresenter.swift in Sources */, - 9CB5129C83F75921E5E28028 /* ToastViewState.swift in Sources */, + 7732B2F635626BE1C1CD92A4 /* UIActivityViewControllerWrapper.swift in Sources */, 36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */, A37EED79941AD3B7140B3822 /* UIDevice.swift in Sources */, 0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */, @@ -2750,17 +2660,15 @@ 3097A0A867D2B19CE32DAE58 /* UIKitBackgroundTaskService.swift in Sources */, D05A193AE63030F2CFCE2E9C /* UITestScreenIdentifier.swift in Sources */, E96005321849DBD7C72A28F2 /* UITestsAppCoordinator.swift in Sources */, - F75C4222D52B643214D5E623 /* UITestsRootView.swift in Sources */, + 086C2FA7750378EB2BFD0BEE /* UITestsRootCoordinator.swift in Sources */, 071A017E415AD378F2961B11 /* URL.swift in Sources */, 7A71AEF419904209BB8C2833 /* UserAgentBuilder.swift in Sources */, - 8775F46AE3234A5A5688C19D /* UserIndicator.swift in Sources */, - 7FA4227B2BAAA71560252866 /* UserIndicatorDismissal.swift in Sources */, - 0602FA07557F580086782A9E /* UserIndicatorPresentationContext.swift in Sources */, - 7A54700193DC1F264368746A /* UserIndicatorPresenter.swift in Sources */, - 10866439ABA58CCDB5D1459D /* UserIndicatorQueue.swift in Sources */, - 15D1F9C415D9C921643BA82E /* UserIndicatorRequest.swift in Sources */, - C052A8CDC7A8E7A2D906674F /* UserIndicatorStore.swift in Sources */, - 80E04BE80A89A78FBB4863BB /* UserIndicatorViewPresentable.swift in Sources */, + 87BD4F95F9D603C309837378 /* UserNotification.swift in Sources */, + 5D9F0695DC6C0057F85C12B6 /* UserNotificationController.swift in Sources */, + D79F0F852C6A4255D5E616D2 /* UserNotificationControllerProtocol.swift in Sources */, + AC5CC8250CEAE57B73900C57 /* UserNotificationModalView.swift in Sources */, + A9D23B78F42BCDD896531436 /* UserNotificationPresenter.swift in Sources */, + 6CA81428F0970785CDCC5E86 /* UserNotificationToastView.swift in Sources */, 9CCC77C31CB399661A034739 /* UserProperties+Element.swift in Sources */, 8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */, 8B807DC963D1D4155A241BCC /* UserSessionFlowCoordinator.swift in Sources */, @@ -2797,12 +2705,12 @@ 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */, BB4C6F362F75933DDDE30F3E /* InfoPlist.swift in Sources */, 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */, + 6B15FF984906AAFCF9DC4F58 /* OnboardingUITests.swift in Sources */, 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */, 77FACC29F98FE2E65BBB6A5F /* ServerSelectionUITests.swift in Sources */, 05EC896A4B9AF4A56670C0BB /* SessionVerificationUITests.swift in Sources */, 490E606044B18985055FF690 /* SettingsUITests.swift in Sources */, B064D42BA087649ACAE462E8 /* SoftLogoutUITests.swift in Sources */, - A00DFC1DD3567B1EDC9F8D16 /* SplashScreenUITests.swift in Sources */, DD9B70DE54B24E0694A35D8A /* Strings+Untranslated.swift in Sources */, B3357B00F1AA930E54F76609 /* Strings.swift in Sources */, C4180F418235DAD9DD173951 /* TemplateScreenUITests.swift in Sources */, diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 685f6638f..a58950d37 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -111,7 +111,7 @@ { "identity" : "swift-snapshot-testing", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-snapshot-testing", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing.git", "state" : { "revision" : "f29e2014f6230cf7d5138fc899da51c7f513d467", "version" : "1.10.0" @@ -129,7 +129,7 @@ { "identity" : "swiftui-introspect", "kind" : "remoteSourceControl", - "location" : "https://github.com/siteline/SwiftUI-Introspect", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", "state" : { "revision" : "f2616860a41f9d9932da412a8978fec79c06fe24", "version" : "0.1.4" diff --git a/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 1.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 1.imageset/Contents.json similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 1.imageset/Contents.json rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 1.imageset/Contents.json diff --git a/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 1.imageset/OnboardingSplashScreenPage1-Dark.pdf b/ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 1.imageset/OnboardingSplashScreenPage1-Dark.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 1.imageset/OnboardingSplashScreenPage1-Dark.pdf rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 1.imageset/OnboardingSplashScreenPage1-Dark.pdf diff --git a/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 1.imageset/OnboardingSplashScreenPage1.pdf b/ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 1.imageset/OnboardingSplashScreenPage1.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 1.imageset/OnboardingSplashScreenPage1.pdf rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 1.imageset/OnboardingSplashScreenPage1.pdf diff --git a/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 2.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 2.imageset/Contents.json similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 2.imageset/Contents.json rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 2.imageset/Contents.json diff --git a/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 2.imageset/OnboardingSplashScreenPage2-Dark.pdf b/ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 2.imageset/OnboardingSplashScreenPage2-Dark.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 2.imageset/OnboardingSplashScreenPage2-Dark.pdf rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 2.imageset/OnboardingSplashScreenPage2-Dark.pdf diff --git a/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 2.imageset/OnboardingSplashScreenPage2.pdf b/ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 2.imageset/OnboardingSplashScreenPage2.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 2.imageset/OnboardingSplashScreenPage2.pdf rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 2.imageset/OnboardingSplashScreenPage2.pdf diff --git a/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 3.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 3.imageset/Contents.json similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 3.imageset/Contents.json rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 3.imageset/Contents.json diff --git a/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 3.imageset/OnboardingSplashScreenPage3-Dark.pdf b/ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 3.imageset/OnboardingSplashScreenPage3-Dark.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 3.imageset/OnboardingSplashScreenPage3-Dark.pdf rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 3.imageset/OnboardingSplashScreenPage3-Dark.pdf diff --git a/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 3.imageset/OnboardingSplashScreenPage3.pdf b/ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 3.imageset/OnboardingSplashScreenPage3.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 3.imageset/OnboardingSplashScreenPage3.pdf rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 3.imageset/OnboardingSplashScreenPage3.pdf diff --git a/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 4.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 4.imageset/Contents.json similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 4.imageset/Contents.json rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 4.imageset/Contents.json diff --git a/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 4.imageset/OnboardingSplashScreenPage4-Dark.pdf b/ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 4.imageset/OnboardingSplashScreenPage4-Dark.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 4.imageset/OnboardingSplashScreenPage4-Dark.pdf rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 4.imageset/OnboardingSplashScreenPage4-Dark.pdf diff --git a/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 4.imageset/OnboardingSplashScreenPage4.pdf b/ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 4.imageset/OnboardingSplashScreenPage4.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 4.imageset/OnboardingSplashScreenPage4.pdf rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Onboarding Screen Page 4.imageset/OnboardingSplashScreenPage4.pdf diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index a6e4ce160..a43659848 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -16,7 +16,7 @@ import Combine import MatrixRustSDK -import UIKit +import SwiftUI struct ServiceLocator { fileprivate static var serviceLocator: ServiceLocator? @@ -28,19 +28,12 @@ struct ServiceLocator { return serviceLocator } - let userIndicatorPresenter: UserIndicatorTypePresenter + let userNotificationController: UserNotificationControllerProtocol } -class AppCoordinator: Coordinator { - private let window: UIWindow - +class AppCoordinator: CoordinatorProtocol { private let stateMachine: AppCoordinatorStateMachine - - private let mainNavigationController: UINavigationController - private let splashViewController: UIViewController - - private let navigationRouter: NavigationRouter - + private let navigationController: NavigationController private let userSessionStore: UserSessionStoreProtocol private var userSession: UserSessionProtocol! { @@ -53,34 +46,22 @@ class AppCoordinator: Coordinator { } private var userSessionFlowCoordinator: UserSessionFlowCoordinator? + private var authenticationCoordinator: AuthenticationCoordinator? private let bugReportService: BugReportServiceProtocol private let backgroundTaskService: BackgroundTaskServiceProtocol - private var loadingIndicator: UserIndicator? - private var statusIndicator: UserIndicator? - private var cancellables = Set() - - var childCoordinators: [Coordinator] = [] - + init() { + navigationController = NavigationController() stateMachine = AppCoordinatorStateMachine() bugReportService = BugReportService(withBaseURL: BuildSettings.bugReportServiceBaseURL, sentryURL: BuildSettings.bugReportSentryURL) - splashViewController = SplashViewController() + navigationController.setRootCoordinator(SplashScreenCoordinator()) - mainNavigationController = ElementNavigationController(rootViewController: splashViewController) - mainNavigationController.navigationBar.prefersLargeTitles = true - - window = UIWindow(frame: UIScreen.main.bounds) - window.rootViewController = mainNavigationController - window.tintColor = .element.accent - - navigationRouter = NavigationRouter(navigationController: mainNavigationController) - - ServiceLocator.serviceLocator = ServiceLocator(userIndicatorPresenter: UserIndicatorTypePresenter(presentingViewController: mainNavigationController)) + ServiceLocator.serviceLocator = ServiceLocator(userNotificationController: UserNotificationController(rootCoordinator: navigationController)) guard let bundleIdentifier = Bundle.main.bundleIdentifier else { fatalError("Should have a valid bundle identifier at this point") @@ -95,11 +76,12 @@ class AppCoordinator: Coordinator { setupLogging() + Bundle.elementFallbackLanguage = "en" + // Benchmark.trackingEnabled = true } func start() { - window.makeKeyAndVisible() stateMachine.processEvent(userSessionStore.hasSessions ? .startWithExistingSession : .startWithAuthentication) } @@ -107,6 +89,10 @@ class AppCoordinator: Coordinator { hideLoadingIndicator() } + func toPresentable() -> AnyView { + ServiceLocator.shared.userNotificationController.toPresentable() + } + // MARK: - Private private func setupLogging() { @@ -192,12 +178,11 @@ class AppCoordinator: Coordinator { private func startAuthentication() { let authenticationService = AuthenticationServiceProxy(userSessionStore: userSessionStore) - let coordinator = AuthenticationCoordinator(authenticationService: authenticationService, - navigationRouter: navigationRouter) - coordinator.delegate = self + authenticationCoordinator = AuthenticationCoordinator(authenticationService: authenticationService, + navigationController: navigationController) + authenticationCoordinator?.delegate = self - add(childCoordinator: coordinator) - coordinator.start() + authenticationCoordinator?.start() } private func startAuthenticationSoftLogout() { @@ -206,12 +191,12 @@ class AppCoordinator: Coordinator { if case .success(let name) = await userSession.clientProxy.loadUserDisplayName() { displayName = name } - + let credentials = SoftLogoutCredentials(userId: userSession.userID, homeserverName: userSession.homeserver, userDisplayName: displayName, deviceId: userSession.deviceId) - + let authenticationService = AuthenticationServiceProxy(userSessionStore: userSessionStore) _ = await authenticationService.configure(for: userSession.homeserver) @@ -223,27 +208,22 @@ class AppCoordinator: Coordinator { switch result { case .signedIn(let session): self.userSession = session - self.remove(childCoordinator: coordinator) self.stateMachine.processEvent(.succeededSigningIn) case .clearAllData: // clear user data self.userSessionStore.logout(userSession: self.userSession) self.userSession = nil - self.remove(childCoordinator: coordinator) self.startAuthentication() } } - - add(childCoordinator: coordinator) - coordinator.start() - - navigationRouter.setRootModule(coordinator) + + navigationController.setRootCoordinator(coordinator) } } private func setupUserSession() { let userSessionFlowCoordinator = UserSessionFlowCoordinator(userSession: userSession, - navigationRouter: navigationRouter, + navigationController: navigationController, bugReportService: bugReportService) userSessionFlowCoordinator.callback = { [weak self] action in @@ -280,12 +260,8 @@ class AppCoordinator: Coordinator { } private func presentSplashScreen(isSoftLogout: Bool = false) { - if let presentedCoordinator = childCoordinators.first { - remove(childCoordinator: presentedCoordinator) - } - - mainNavigationController.setViewControllers([splashViewController], animated: false) - + navigationController.setRootCoordinator(SplashScreenCoordinator()) + if isSoftLogout { startAuthenticationSoftLogout() } else { @@ -319,16 +295,21 @@ class AppCoordinator: Coordinator { // MARK: Toasts and loading indicators + static let loadingIndicatorIdentifier = "AppCoordinatorLoading" + private func showLoadingIndicator() { - loadingIndicator = ServiceLocator.shared.userIndicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true)) + ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(id: Self.loadingIndicatorIdentifier, + type: .modal, + title: ElementL10n.loading, + persistent: true)) } private func hideLoadingIndicator() { - loadingIndicator = nil + ServiceLocator.shared.userNotificationController.retractNotificationWithId(Self.loadingIndicatorIdentifier) } private func showLoginErrorToast() { - statusIndicator = ServiceLocator.shared.userIndicatorPresenter.present(.error(label: "Failed logging in")) + ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(title: "Failed logging in")) } } @@ -337,7 +318,6 @@ class AppCoordinator: Coordinator { extension AppCoordinator: AuthenticationCoordinatorDelegate { func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didLoginWithSession userSession: UserSessionProtocol) { self.userSession = userSession - remove(childCoordinator: authenticationCoordinator) stateMachine.processEvent(.succeededSigningIn) } } diff --git a/ElementX/Sources/Application/AppDelegate.swift b/ElementX/Sources/Application/AppDelegate.swift index 65429d293..b5dd71a4a 100644 --- a/ElementX/Sources/Application/AppDelegate.swift +++ b/ElementX/Sources/Application/AppDelegate.swift @@ -14,26 +14,38 @@ // limitations under the License. // -import UIKit +import SwiftUI @main -class AppDelegate: UIResponder, UIApplicationDelegate { - private lazy var appCoordinator: Coordinator = Tests.isRunningUITests ? UITestsAppCoordinator() : AppCoordinator() - - func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { - // use `en` as fallback language - Bundle.elementFallbackLanguage = "en" - - return true +struct Application: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) private var applicationDelegate + private let applicationCoordinator: CoordinatorProtocol + + init() { + if Tests.isRunningUITests { + applicationCoordinator = UITestsAppCoordinator() + } else { + applicationCoordinator = AppCoordinator() + } } - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { - if Tests.isRunningUnitTests { - return true + var body: some Scene { + WindowGroup { + if Tests.isRunningUnitTests { + EmptyView() + } else { + applicationCoordinator.toPresentable() + .tint(.element.accent) + .task { + applicationCoordinator.start() + } + } } - - appCoordinator.start() - - return true + } +} + +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + true } } diff --git a/ElementX/Sources/Application/CoordinatorProtocol.swift b/ElementX/Sources/Application/CoordinatorProtocol.swift new file mode 100644 index 000000000..23cf2378a --- /dev/null +++ b/ElementX/Sources/Application/CoordinatorProtocol.swift @@ -0,0 +1,34 @@ +// +// Copyright 2022 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 SwiftUI + +@MainActor +protocol CoordinatorProtocol { + func start() + func stop() + func toPresentable() -> AnyView +} + +extension CoordinatorProtocol { + func start() { } + + func stop() { } + + func toPresentable() -> AnyView { + AnyView(Text("View not configured")) + } +} diff --git a/ElementX/Sources/Application/NavigationController.swift b/ElementX/Sources/Application/NavigationController.swift new file mode 100644 index 000000000..0b79a1472 --- /dev/null +++ b/ElementX/Sources/Application/NavigationController.swift @@ -0,0 +1,186 @@ +// +// Copyright 2022 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 SwiftUI + +class NavigationController: ObservableObject, CoordinatorProtocol { + private var dismissalCallbacks = [UUID: () -> Void]() + + @Published fileprivate var internalRootCoordinator: AnyCoordinator? { + didSet { + if let oldValue { + oldValue.coordinator.stop() + } + + if let internalRootCoordinator { + logPresentationChange("Set root", internalRootCoordinator) + internalRootCoordinator.coordinator.start() + } + } + } + + @Published fileprivate var internalSheetCoordinator: AnyCoordinator? { + didSet { + if let oldValue { + logPresentationChange("Dismiss", oldValue) + oldValue.coordinator.stop() + dismissalCallbacks[oldValue.id]?() + dismissalCallbacks.removeValue(forKey: oldValue.id) + } + + if let internalSheetCoordinator { + logPresentationChange("Present", internalSheetCoordinator) + internalSheetCoordinator.coordinator.start() + } + } + } + + @Published fileprivate var internalNavigationStack = [AnyCoordinator]() { + didSet { + let diffs = internalNavigationStack.difference(from: oldValue) + diffs.forEach { change in + switch change { + case .insert(_, let anyCoordinator, _): + logPresentationChange("Push", anyCoordinator) + anyCoordinator.coordinator.start() + case .remove(_, let anyCoordinator, _): + logPresentationChange("Pop", anyCoordinator) + anyCoordinator.coordinator.stop() + + dismissalCallbacks[anyCoordinator.id]?() + dismissalCallbacks.removeValue(forKey: anyCoordinator.id) + } + } + } + } + + var rootCoordinator: CoordinatorProtocol? { + internalRootCoordinator?.coordinator + } + + var coordinators: [CoordinatorProtocol] { + internalNavigationStack.map(\.coordinator) + } + + var sheetCoordinator: CoordinatorProtocol? { + internalSheetCoordinator?.coordinator + } + + func setRootCoordinator(_ coordinator: any CoordinatorProtocol) { + popToRoot(animated: false) + internalRootCoordinator = AnyCoordinator(coordinator) + } + + func push(_ coordinator: any CoordinatorProtocol, dismissalCallback: (() -> Void)? = nil) { + let anyCoordinator = AnyCoordinator(coordinator) + + if let dismissalCallback { + dismissalCallbacks[anyCoordinator.id] = dismissalCallback + } + + internalNavigationStack.append(anyCoordinator) + } + + func popToRoot(animated: Bool = true) { + dismissSheet() + + guard !internalNavigationStack.isEmpty else { + return + } + + if !animated { + // Disabling animations doesn't work through normal Transactions + // https://stackoverflow.com/questions/72832243 + UIView.setAnimationsEnabled(false) + } + + internalNavigationStack.removeAll() + + if !animated { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { + UIView.setAnimationsEnabled(true) + } + } + } + + func pop() { + dismissSheet() + internalNavigationStack.removeLast() + } + + func presentSheet(_ coordinator: any CoordinatorProtocol, dismissalCallback: (() -> Void)? = nil) { + let anyCoordinator = AnyCoordinator(coordinator) + + if let dismissalCallback { + dismissalCallbacks[anyCoordinator.id] = dismissalCallback + } + + internalSheetCoordinator = anyCoordinator + } + + func dismissSheet() { + internalSheetCoordinator = nil + } + + // MARK: - CoordinatorProtocol + + func toPresentable() -> AnyView { + AnyView(NavigationControllerView(navigationController: self)) + } + + // MARK: - Private + + private func logPresentationChange(_ change: String, _ anyCoordinator: AnyCoordinator) { + if let navigationCoordinator = anyCoordinator.coordinator as? NavigationController, let rootCoordinator = navigationCoordinator.rootCoordinator { + MXLog.info("\(change): NavigationController(\(anyCoordinator.id)) - \(rootCoordinator)") + } else { + MXLog.info("\(change): \(anyCoordinator.coordinator)(\(anyCoordinator.id))") + } + } +} + +private struct NavigationControllerView: View { + @ObservedObject var navigationController: NavigationController + + var body: some View { + NavigationStack(path: $navigationController.internalNavigationStack) { + navigationController.internalRootCoordinator?.coordinator.toPresentable() + .navigationDestination(for: AnyCoordinator.self) { anyCoordinator in + anyCoordinator.coordinator.toPresentable() + } + } + .sheet(item: $navigationController.internalSheetCoordinator) { anyCoordinator in + anyCoordinator.coordinator.toPresentable() + } + } +} + +private struct AnyCoordinator: Identifiable, Hashable { + let id = UUID() + let coordinator: any CoordinatorProtocol + + init(_ coordinator: any CoordinatorProtocol) { + self.coordinator = coordinator + } + + static func == (lhs: AnyCoordinator, rhs: AnyCoordinator) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/ElementX/Sources/Generated/Assets.swift b/ElementX/Sources/Generated/Assets.swift index 3fa5ac4a2..f9e231a4e 100644 --- a/ElementX/Sources/Generated/Assets.swift +++ b/ElementX/Sources/Generated/Assets.swift @@ -25,11 +25,11 @@ internal enum Asset { internal enum Images { internal static let analyticsCheckmark = ImageAsset(name: "Images/AnalyticsCheckmark") internal static let analyticsLogo = ImageAsset(name: "Images/AnalyticsLogo") + internal static let onboardingScreenPage1 = ImageAsset(name: "Images/Onboarding Screen Page 1") + internal static let onboardingScreenPage2 = ImageAsset(name: "Images/Onboarding Screen Page 2") + internal static let onboardingScreenPage3 = ImageAsset(name: "Images/Onboarding Screen Page 3") + internal static let onboardingScreenPage4 = ImageAsset(name: "Images/Onboarding Screen Page 4") internal static let serverSelectionIcon = ImageAsset(name: "Images/Server Selection Icon") - internal static let splashScreenPage1 = ImageAsset(name: "Images/Splash Screen Page 1") - internal static let splashScreenPage2 = ImageAsset(name: "Images/Splash Screen Page 2") - internal static let splashScreenPage3 = ImageAsset(name: "Images/Splash Screen Page 3") - internal static let splashScreenPage4 = ImageAsset(name: "Images/Splash Screen Page 4") internal static let encryptionNormal = ImageAsset(name: "Images/encryption_normal") internal static let encryptionTrusted = ImageAsset(name: "Images/encryption_trusted") internal static let encryptionWarning = ImageAsset(name: "Images/encryption_warning") diff --git a/ElementX/Sources/Other/Coordinator.swift b/ElementX/Sources/Other/Coordinator.swift deleted file mode 100755 index 4a57bc034..000000000 --- a/ElementX/Sources/Other/Coordinator.swift +++ /dev/null @@ -1,54 +0,0 @@ -/* - Copyright 2019 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 UIKit - -/// Protocol describing a [Coordinator](http://khanlou.com/2015/10/coordinators-redux/). -/// Coordinators are the objects which control the navigation flow of the application. -/// It helps to isolate and reuse view controllers and pass dependencies down the navigation hierarchy. -@MainActor -protocol Coordinator: AnyObject { - /// Starts job of the coordinator. - func start() - - /// Child coordinators to retain. Prevent them from getting deallocated. - var childCoordinators: [Coordinator] { get set } - - /// Stores coordinator to the `childCoordinators` array. - /// - /// - Parameter childCoordinator: Child coordinator to store. - func add(childCoordinator: Coordinator) - - /// Remove coordinator from the `childCoordinators` array. - /// - /// - Parameter childCoordinator: Child coordinator to remove. - func remove(childCoordinator: Coordinator) - - /// Stops job of the coordinator. Can be used to clear some resources. Will be automatically called when the coordinator removed. - func stop() -} - -// `Coordinator` default implementation -extension Coordinator { - func add(childCoordinator coordinator: Coordinator) { - childCoordinators.append(coordinator) - } - - func remove(childCoordinator: Coordinator) { - childCoordinator.stop() - childCoordinators = childCoordinators.filter { $0 !== childCoordinator } - } -} diff --git a/ElementX/Sources/Other/Routers/NavigationRouter.swift b/ElementX/Sources/Other/Routers/NavigationRouter.swift deleted file mode 100755 index 46a6b61b5..000000000 --- a/ElementX/Sources/Other/Routers/NavigationRouter.swift +++ /dev/null @@ -1,397 +0,0 @@ -/* - Copyright 2019 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 UIKit - -/// `NavigationRouter` is a concrete implementation of NavigationRouterType. -final class NavigationRouter: NSObject, NavigationRouterType { - // MARK: - Properties - - // MARK: Private - - private var completions: [UIViewController: () -> Void] - private let navigationController: UINavigationController - - /// Stores the association between the added Presentable and his view controller. - /// They can be the same if the controller is not added via his Coordinator or it is a simple UIViewController. - private var storedModules = WeakDictionary() - - // MARK: Public - - /// Returns the presentables associated to each view controller - var modules: [Presentable] { - viewControllers.map { viewController -> Presentable in - self.module(for: viewController) - } - } - - /// Return the view controllers stack - var viewControllers: [UIViewController] { - navigationController.viewControllers - } - - // MARK: - Setup - - init(navigationController: UINavigationController) { - self.navigationController = navigationController - completions = [:] - super.init() - self.navigationController.delegate = self - - // Post local notification on NavigationRouter creation - let userInfo: [String: Any] = [NavigationRouter.NotificationUserInfoKey.navigationRouter: self, - NavigationRouter.NotificationUserInfoKey.navigationController: navigationController] - NotificationCenter.default.post(name: NavigationRouter.didCreate, object: self, userInfo: userInfo) - } - - deinit { - // Post local notification on NavigationRouter deinit - let userInfo: [String: Any] = [NavigationRouter.NotificationUserInfoKey.navigationRouter: self, - NavigationRouter.NotificationUserInfoKey.navigationController: navigationController] - NotificationCenter.default.post(name: NavigationRouter.willDestroy, object: self, userInfo: userInfo) - } - - // MARK: - Public - - func present(_ module: Presentable, animated: Bool = true) { - MXLog.debug("Present \(module)") - navigationController.present(module.toPresentable(), animated: animated, completion: nil) - } - - func dismissModule(animated: Bool = true, completion: (() -> Void)? = nil) { - MXLog.debug("Dismiss presented module") - navigationController.dismiss(animated: animated, completion: completion) - } - - func setRootModule(_ module: Presentable, hideNavigationBar: Bool = false, animated: Bool = false, popCompletion: (() -> Void)? = nil) { - MXLog.debug("Set root module \(module)") - - let controller = module.toPresentable() - - // Avoid setting a UINavigationController onto stack - guard controller is UINavigationController == false else { - MXLog.error("Cannot add a UINavigationController to NavigationRouter") - return - } - - addModule(module, for: controller) - - let controllersToPop = navigationController.viewControllers.reversed() - - controllersToPop.forEach { - self.willPopViewController($0) - } - - if let popCompletion { - completions[controller] = popCompletion - } - - willPushViewController(controller) - - navigationController.setViewControllers([controller], animated: animated) - navigationController.isNavigationBarHidden = hideNavigationBar - - // Pop old view controllers - controllersToPop.forEach { - self.didPopViewController($0) - } - - // Add again controller to module association, in case same module instance is added back - addModule(module, for: controller) - - didPushViewController(controller) - } - - func setModules(_ modules: [NavigationModule], hideNavigationBar: Bool, animated: Bool) { - MXLog.debug("Set modules \(modules)") - - let controllers = modules.map { module -> UIViewController in - let controller = module.presentable.toPresentable() - self.addModule(module.presentable, for: controller) - return controller - } - - let controllersToPop = navigationController.viewControllers.reversed() - - controllersToPop.forEach { - self.willPopViewController($0) - } - - controllers.forEach { - self.willPushViewController($0) - } - - // Set new view controllers - navigationController.setViewControllers(controllers, animated: animated) - navigationController.isNavigationBarHidden = hideNavigationBar - - // Pop old view controllers - controllersToPop.forEach { - self.didPopViewController($0) - } - - // Add again controller to module association, in case same modules instance are added back - modules.forEach { module in - self.addModule(module.presentable, for: module.presentable.toPresentable()) - } - - controllers.forEach { - self.didPushViewController($0) - } - } - - func popToRootModule(animated: Bool) { - MXLog.debug("Pop to root module") - - let controllers = navigationController.viewControllers - - if controllers.count > 1 { - let controllersToPop = controllers[1.. Void)? = nil) { - MXLog.debug("Push module \(module)") - - let controller = module.toPresentable() - - // Avoid pushing UINavigationController onto stack - guard controller is UINavigationController == false else { - MXLog.error("Cannot push a UINavigationController to NavigationRouter") - return - } - - addModule(module, for: controller) - - if let completion = popCompletion { - completions[controller] = completion - } - - willPushViewController(controller) - - navigationController.pushViewController(controller, animated: animated) - - didPushViewController(controller) - } - - func push(_ modules: [NavigationModule], animated: Bool) { - MXLog.debug("Push modules \(modules)") - - // Avoid pushing any UINavigationController onto stack - guard modules.first(where: { $0.presentable.toPresentable() is UINavigationController }) == nil else { - MXLog.error("Cannot push a UINavigationController to NavigationRouter") - return - } - - for module in modules { - let controller = module.presentable.toPresentable() - addModule(module.presentable, for: controller) - - if let completion = module.popCompletion { - completions[controller] = completion - } - - willPushViewController(controller) - } - - var viewControllers = navigationController.viewControllers - viewControllers.append(contentsOf: modules.map { $0.presentable.toPresentable() }) - navigationController.setViewControllers(viewControllers, animated: animated) - - for module in modules { - let controller = module.presentable.toPresentable() - didPushViewController(controller) - } - } - - func popModule(animated: Bool = true) { - MXLog.debug("Pop module") - - if let lastController = navigationController.viewControllers.last { - willPopViewController(lastController) - } - - if let controller = navigationController.popViewController(animated: animated) { - didPopViewController(controller) - } - } - - func popAllModules(animated: Bool) { - MXLog.debug("Pop all modules") - - let controllersToPop = navigationController.viewControllers.reversed() - - controllersToPop.forEach { - self.willPopViewController($0) - } - - navigationController.setViewControllers([], animated: animated) - - controllersToPop.forEach { - self.didPopViewController($0) - } - } - - func contains(_ module: Presentable) -> Bool { - let controller = module.toPresentable() - return navigationController.viewControllers.contains(controller) - } - - // MARK: Presentable - - func toPresentable() -> UIViewController { - navigationController - } - - // MARK: - Private - - private func module(for viewController: UIViewController) -> Presentable { - guard let module = storedModules[viewController] as? Presentable else { - return viewController - } - return module - } - - private func addModule(_ module: Presentable, for viewController: UIViewController) { - storedModules[viewController] = module as AnyObject - } - - private func removeModule(for viewController: UIViewController) { - storedModules[viewController] = nil - } - - private func runCompletion(for controller: UIViewController) { - guard let completion = completions[controller] else { - return - } - completion() - completions.removeValue(forKey: controller) - } - - private func willPushViewController(_ viewController: UIViewController) { - postNotification(withName: NavigationRouter.willPushModule, for: viewController) - } - - private func didPushViewController(_ viewController: UIViewController) { - postNotification(withName: NavigationRouter.didPushModule, for: viewController) - } - - private func willPopViewController(_ viewController: UIViewController) { - postNotification(withName: NavigationRouter.willPopModule, for: viewController) - } - - private func didPopViewController(_ viewController: UIViewController) { - postNotification(withName: NavigationRouter.didPopModule, for: viewController) - - // Call completion closure associated to the view controller - // So associated coordinator can be deallocated - runCompletion(for: viewController) - - removeModule(for: viewController) - } - - private func postNotification(withName name: Notification.Name, for viewController: UIViewController) { - let module = module(for: viewController) - - let userInfo: [String: Any] = [ - NotificationUserInfoKey.navigationRouter: self, - NotificationUserInfoKey.module: module, - NotificationUserInfoKey.viewController: viewController - ] - NotificationCenter.default.post(name: name, object: self, userInfo: userInfo) - } -} - -// MARK: - UINavigationControllerDelegate - -extension NavigationRouter: UINavigationControllerDelegate { - func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { - // Try to post `NavigationRouter.willPopModule` notification here - } - - func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { - // Ensure the view controller is popping - guard let poppedViewController = navigationController.transitionCoordinator?.viewController(forKey: .from), - !navigationController.viewControllers.contains(poppedViewController) else { - return - } - - MXLog.debug("Popped module: \(poppedViewController)") - - didPopViewController(poppedViewController) - } -} - -// MARK: - NavigationRouter notification constants - -extension NavigationRouter { - // MARK: Notification names - - public static let willPushModule = Notification.Name("NavigationRouterWillPushModule") - public static let didPushModule = Notification.Name("NavigationRouterDidPushModule") - public static let willPopModule = Notification.Name("NavigationRouterWillPopModule") - public static let didPopModule = Notification.Name("NavigationRouterDidPopModule") - - public static let didCreate = Notification.Name("NavigationRouterDidCreate") - public static let willDestroy = Notification.Name("NavigationRouterWillDestroy") - - // MARK: Notification keys - - public enum NotificationUserInfoKey { - /// The associated view controller (UIViewController). - static let viewController = "viewController" - - /// The associated module (Presentable), can the view controller itself or is Coordinator - static let module = "module" - - /// The navigation router that send the notification (NavigationRouterType) - static let navigationRouter = "navigationRouter" - - /// The navigation controller (UINavigationController) associated to the navigation router - static let navigationController = "navigationController" - } -} diff --git a/ElementX/Sources/Other/Routers/NavigationRouterStore.swift b/ElementX/Sources/Other/Routers/NavigationRouterStore.swift deleted file mode 100644 index fcb97a0f9..000000000 --- a/ElementX/Sources/Other/Routers/NavigationRouterStore.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// Copyright 2022 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 UIKit - -/// `NavigationRouterStore` enables to get a NavigationRouter from a UINavigationController instance. -class NavigationRouterStore: NavigationRouterStoreProtocol { - // MARK: - Constants - - static let shared = NavigationRouterStore() - - // MARK: - Properties - - // WeakDictionary does not work with protocol - // Find a way to use NavigationRouterType as value - private var navigationRouters = WeakDictionary() - - // MARK: - Setup - - /// As we are ensuring that there is only one navigation controller per NavigationRouter, the class here should be used as a singleton. - private init() { - registerNavigationRouterNotifications() - } - - // MARK: - Public - - func navigationRouter(for navigationController: UINavigationController) -> NavigationRouterType { - if let existingNavigationRouter = findNavigationRouter(for: navigationController) { - return existingNavigationRouter - } - - let navigationRouter = NavigationRouter(navigationController: ElementNavigationController()) - return navigationRouter - } - - // MARK: - Private - - private func findNavigationRouter(for navigationController: UINavigationController) -> NavigationRouterType? { - navigationRouters[navigationController] - } - - private func removeNavigationRouter(for navigationController: UINavigationController) { - navigationRouters[navigationController] = nil - } - - private func registerNavigationRouterNotifications() { - NotificationCenter.default.addObserver(self, selector: #selector(navigationRouterDidCreate(_:)), name: NavigationRouter.didCreate, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(navigationRouterWillDestroy(_:)), name: NavigationRouter.willDestroy, object: nil) - } - - @objc private func navigationRouterDidCreate(_ notification: Notification) { - guard let userInfo = notification.userInfo, - let navigationRouter = userInfo[NavigationRouter.NotificationUserInfoKey.navigationRouter] as? NavigationRouterType, - let navigationController = userInfo[NavigationRouter.NotificationUserInfoKey.navigationController] as? UINavigationController else { - return - } - - if let existingNavigationRouter = findNavigationRouter(for: navigationController) { - fatalError("\(existingNavigationRouter) is already tied to the same navigation controller as \(navigationRouter). We should have only one NavigationRouter per navigation controller") - } else { - // WeakDictionary does not work with protocol - // Find a way to avoid this cast - navigationRouters[navigationController] = navigationRouter as? NavigationRouter - } - } - - @objc private func navigationRouterWillDestroy(_ notification: Notification) { - guard let userInfo = notification.userInfo, - let navigationRouter = userInfo[NavigationRouter.NotificationUserInfoKey.navigationRouter] as? NavigationRouterType, - let navigationController = userInfo[NavigationRouter.NotificationUserInfoKey.navigationController] as? UINavigationController else { - return - } - - if let existingNavigationRouter = findNavigationRouter(for: navigationController), existingNavigationRouter !== navigationRouter { - fatalError("\(existingNavigationRouter) is already tied to the same navigation controller as \(navigationRouter). We should have only one NavigationRouter per navigation controller") - } - - removeNavigationRouter(for: navigationController) - } -} diff --git a/ElementX/Sources/Other/Routers/NavigationRouterStoreProtocol.swift b/ElementX/Sources/Other/Routers/NavigationRouterStoreProtocol.swift deleted file mode 100644 index 93509a3a2..000000000 --- a/ElementX/Sources/Other/Routers/NavigationRouterStoreProtocol.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright 2022 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 UIKit - -/// `NavigationRouterStoreProtocol` describes a structure that enables to get a NavigationRouter from a UINavigationController instance. -@MainActor -protocol NavigationRouterStoreProtocol { - /// Gets the existing navigation router for the supplied controller, creating a new one if it doesn't yet exist. - /// Note: The store only holds a weak reference to the returned router. It is the caller's responsibility to retain it. - func navigationRouter(for navigationController: UINavigationController) -> NavigationRouterType -} diff --git a/ElementX/Sources/Other/Routers/NavigationRouterType.swift b/ElementX/Sources/Other/Routers/NavigationRouterType.swift deleted file mode 100755 index bf63b8d99..000000000 --- a/ElementX/Sources/Other/Routers/NavigationRouterType.swift +++ /dev/null @@ -1,143 +0,0 @@ -/* - Copyright 2019 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 UIKit - -/// Protocol describing a router that wraps a UINavigationController and add convenient completion handlers. Completions are called when a Presentable is removed. -/// Routers are used to be passed between coordinators. They handles only `physical` navigation. -@MainActor -protocol NavigationRouterType: AnyObject, Presentable { - /// Present modally a view controller on the navigation controller - /// - /// - Parameter module: The Presentable to present. - /// - Parameter animated: Specify true to animate the transition. - func present(_ module: Presentable, animated: Bool) - - /// Dismiss presented view controller from navigation controller - /// - /// - Parameter animated: Specify true to animate the transition. - /// - Parameter completion: Animation completion (not the pop completion). - func dismissModule(animated: Bool, completion: (() -> Void)?) - - /// Set root view controller of navigation controller - /// - /// - Parameter module: The Presentable to set as root. - /// - Parameter hideNavigationBar: Specify true to hide the UINavigationBar. - /// - Parameter animated: Specify true to animate the transition. - /// - Parameter popCompletion: Completion called when `module` is removed from the navigation stack. - func setRootModule(_ module: Presentable, hideNavigationBar: Bool, animated: Bool, popCompletion: (() -> Void)?) - - /// Set view controllers stack of navigation controller - /// - Parameters: - /// - modules: The modules stack to set. - /// - hideNavigationBar: Specify true to hide the UINavigationBar. - /// - animated: Specify true to animate the transition. - func setModules(_ modules: [NavigationModule], hideNavigationBar: Bool, animated: Bool) - - /// Pop to root view controller of navigation controller and remove all others - /// - /// - Parameter animated: Specify true to animate the transition. - func popToRootModule(animated: Bool) - - /// Pops view controllers until the specified view controller is at the top of the navigation stack - /// - /// - Parameter module: The Presentable that should to be at the top of the stack. - /// - Parameter animated: Specify true to animate the transition. - func popToModule(_ module: Presentable, animated: Bool) - - /// Push a view controller on navigation controller stack - /// - /// - Parameter animated: Specify true to animate the transition. - /// - Parameter popCompletion: Completion called when `module` is removed from the navigation stack. - func push(_ module: Presentable, animated: Bool, popCompletion: (() -> Void)?) - - /// Push some view controllers on navigation controller stack - /// - /// - Parameter modules: Modules to push - /// - Parameter animated: Specify true to animate the transition. - func push(_ modules: [NavigationModule], animated: Bool) - - /// Pop last view controller from navigation controller stack - /// - /// - Parameter animated: Specify true to animate the transition. - func popModule(animated: Bool) - - /// Pops all view controllers - /// - /// - Parameter animated: Specify true to animate the transition. - func popAllModules(animated: Bool) - - /// Returns the modules that are currently in the navigation stack - var modules: [Presentable] { get } - - /// Check if the navigation controller contains the given presentable. - /// - Parameter module: The presentable for which to check the existence. - func contains(_ module: Presentable) -> Bool -} - -// `NavigationRouterType` default implementation -extension NavigationRouterType { - func setRootModule(_ module: Presentable) { - setRootModule(module, hideNavigationBar: false, animated: false, popCompletion: nil) - } - - func setRootModule(_ module: Presentable, popCompletion: (() -> Void)?) { - setRootModule(module, hideNavigationBar: false, animated: false, popCompletion: popCompletion) - } - - func setModules(_ modules: [NavigationModule], animated: Bool) { - setModules(modules, hideNavigationBar: false, animated: animated) - } - - func setModules(_ modules: [Presentable], animated: Bool) { - setModules(modules, hideNavigationBar: false, animated: animated) - } -} - -// MARK: - Presentable <--> NavigationModule Transitive Methods - -extension NavigationRouterType { - func setRootModule(_ module: NavigationModule) { - setRootModule(module.presentable, popCompletion: module.popCompletion) - } - - func push(_ module: NavigationModule, animated: Bool) { - push(module.presentable, animated: animated, popCompletion: module.popCompletion) - } - - func setModules(_ modules: [Presentable], hideNavigationBar: Bool, animated: Bool) { - setModules(modules.map { $0.toModule() }, - hideNavigationBar: hideNavigationBar, - animated: animated) - } - - func push(_ modules: [Presentable], animated: Bool) { - push(modules.map { $0.toModule() }, - animated: animated) - } - - func dismissModule(animated: Bool = true, completion: (() -> Void)? = nil) { - dismissModule(animated: animated, completion: completion) - } - - func push(_ module: Presentable, animated: Bool = true, popCompletion: (() -> Void)? = nil) { - push(module, animated: animated, popCompletion: popCompletion) - } - - func present(_ module: Presentable, animated: Bool = true) { - present(module, animated: animated) - } -} diff --git a/ElementX/Sources/Other/Routers/Presentable.swift b/ElementX/Sources/Other/Routers/Presentable.swift deleted file mode 100755 index 9ca9dd8b8..000000000 --- a/ElementX/Sources/Other/Routers/Presentable.swift +++ /dev/null @@ -1,37 +0,0 @@ -/* - Copyright 2019 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 UIKit - -/// Protocol used to pass UIViewControllers to routers -@MainActor -protocol Presentable { - func toPresentable() -> UIViewController -} - -extension UIViewController: Presentable { - public func toPresentable() -> UIViewController { - self - } -} - -extension Presentable { - /// Returns a new module from the presentable without a pop completion block - /// - Returns: Module - func toModule() -> NavigationModule { - NavigationModule(presentable: self, popCompletion: nil) - } -} diff --git a/ElementX/Sources/Other/Routers/RootRouter.swift b/ElementX/Sources/Other/Routers/RootRouter.swift deleted file mode 100755 index aad4342f9..000000000 --- a/ElementX/Sources/Other/Routers/RootRouter.swift +++ /dev/null @@ -1,85 +0,0 @@ -/* - Copyright 2020 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 UIKit - -/// `RootRouter` is a concrete implementation of RootRouterType. -final class RootRouter: RootRouterType { - // MARK: - Constants - - // `rootViewController` animation constants - private enum RootViewControllerUpdateAnimation { - static let duration: TimeInterval = 0.3 - static let options: UIView.AnimationOptions = .transitionCrossDissolve - } - - // MARK: - Properties - - private var presentedModule: Presentable? - - let window: UIWindow - - /// The root view controller currently presented - var rootViewController: UIViewController? { - window.rootViewController - } - - // MARK: - Setup - - init(window: UIWindow) { - self.window = window - } - - // MARK: - Public methods - - func setRootModule(_ module: Presentable) { - updateRootViewController(rootViewController: module.toPresentable(), animated: false, completion: nil) - window.makeKeyAndVisible() - } - - func dismissRootModule(animated: Bool, completion: (() -> Void)?) { - updateRootViewController(rootViewController: nil, animated: animated, completion: completion) - } - - func presentModule(_ module: Presentable, animated: Bool, completion: (() -> Void)?) { - let viewControllerPresenter = rootViewController?.presentedViewController ?? rootViewController - - viewControllerPresenter?.present(module.toPresentable(), animated: animated, completion: completion) - presentedModule = module - } - - func dismissModule(animated: Bool, completion: (() -> Void)?) { - presentedModule?.toPresentable().dismiss(animated: animated, completion: completion) - } - - // MARK: - Private methods - - private func updateRootViewController(rootViewController: UIViewController?, animated: Bool, completion: (() -> Void)?) { - if animated { - UIView.transition(with: window, duration: RootViewControllerUpdateAnimation.duration, options: RootViewControllerUpdateAnimation.options, animations: { - let oldState: Bool = UIView.areAnimationsEnabled - UIView.setAnimationsEnabled(false) - self.window.rootViewController = rootViewController - UIView.setAnimationsEnabled(oldState) - }, completion: { _ in - completion?() - }) - } else { - window.rootViewController = rootViewController - completion?() - } - } -} diff --git a/ElementX/Sources/Other/Routers/RootRouterType.swift b/ElementX/Sources/Other/Routers/RootRouterType.swift deleted file mode 100755 index 8d6d63473..000000000 --- a/ElementX/Sources/Other/Routers/RootRouterType.swift +++ /dev/null @@ -1,49 +0,0 @@ -/* - Copyright 2020 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 UIKit - -/// Protocol describing a router that wraps the root navigation of the application. -/// Routers are used to be passed between coordinators. They handles only `physical` navigation. -@MainActor -protocol RootRouterType: AnyObject { - /// Update the root view controller - /// - /// - Parameter module: The new root view controller to set - func setRootModule(_ module: Presentable) - - /// Dismiss the root view controller - /// - /// - Parameters: - /// - animated: Specify true to animate the transition. - /// - completion: The closure executed after the view controller is dismissed. - func dismissRootModule(animated: Bool, completion: (() -> Void)?) - - /// Present modally a view controller on the root view controller - /// - /// - Parameters: - /// - module: Specify true to animate the transition. - /// - animated: Specify true to animate the transition. - /// - completion: Animation completion. - func presentModule(_ module: Presentable, animated: Bool, completion: (() -> Void)?) - - /// Dismiss modally presented view controller from root view controller - /// - /// - Parameters: - /// - animated: Specify true to animate the transition. - /// - completion: Animation completion. - func dismissModule(animated: Bool, completion: (() -> Void)?) -} diff --git a/ElementX/Sources/Other/SwiftUI/Animation/ElementAnimations.swift b/ElementX/Sources/Other/SwiftUI/Animation/ElementAnimations.swift index d9e7437fe..e78ec1bac 100644 --- a/ElementX/Sources/Other/SwiftUI/Animation/ElementAnimations.swift +++ b/ElementX/Sources/Other/SwiftUI/Animation/ElementAnimations.swift @@ -17,7 +17,6 @@ import Foundation import SwiftUI -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public extension Animation { /// Animation to be used to disable animations. static let noAnimation: Animation = .linear(duration: 0) @@ -28,7 +27,6 @@ public extension Animation { } } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) /// Returns the result of recomputing the view's body with the provided /// animation. /// - Parameters: diff --git a/ElementX/Sources/Other/SwiftUI/ErrorHandling/AlertInfo.swift b/ElementX/Sources/Other/SwiftUI/ErrorHandling/AlertInfo.swift index ea6545310..46219710d 100644 --- a/ElementX/Sources/Other/SwiftUI/ErrorHandling/AlertInfo.swift +++ b/ElementX/Sources/Other/SwiftUI/ErrorHandling/AlertInfo.swift @@ -31,9 +31,14 @@ struct AlertInfo: Identifiable { /// The alert's message (optional). var message: String? /// The alert's primary button title and action. Defaults to an Ok button with no action. - var primaryButton: (title: String, action: (() -> Void)?) = (ElementL10n.ok, nil) + var primaryButton = AlertButton(title: ElementL10n.ok, action: nil) /// The alert's secondary button title and action. - var secondaryButton: (title: String, action: (() -> Void)?)? + var secondaryButton: AlertButton? +} + +struct AlertButton { + let title: String + let action: (() -> Void)? } extension AlertInfo { @@ -79,7 +84,7 @@ extension AlertInfo { } } - private func alertButton(for buttonParameters: (title: String, action: (() -> Void)?)) -> Alert.Button { + private func alertButton(for buttonParameters: AlertButton) -> Alert.Button { guard let action = buttonParameters.action else { return .default(Text(buttonParameters.title)) } diff --git a/ElementX/Sources/Other/UserIndicators/ActivityIndicatorPresenter.swift b/ElementX/Sources/Other/UserIndicators/ActivityIndicatorPresenter.swift deleted file mode 100755 index a3968c530..000000000 --- a/ElementX/Sources/Other/UserIndicators/ActivityIndicatorPresenter.swift +++ /dev/null @@ -1,146 +0,0 @@ -/* - Copyright 2019 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 - -/// Used to present activity indicator on a view -final class ActivityIndicatorPresenter: ActivityIndicatorPresenterType { - // MARK: - Constants - - private enum Constants { - static let animationDuration: TimeInterval = 0.3 - static let backgroundOverlayColor = UIColor.clear - static let backgroundOverlayAlpha: CGFloat = 1.0 - } - - // MARK: - Properties - - private weak var backgroundOverlayView: UIView? - private weak var activityIndicatorView: ActivityIndicatorView? - private weak var presentingView: UIView? - - var isPresenting: Bool { - activityIndicatorView != nil - } - - // MARK: - Public - - func presentActivityIndicator(on view: UIView, animated: Bool, completion: (() -> Void)? = nil) { - if presentingView != nil { - if let completion { - completion() - } - return - } - - presentingView = view - - view.isUserInteractionEnabled = false - - let backgroundOverlayView = createBackgroundOverlayView(with: view.frame) - - let activityIndicatorView = ActivityIndicatorView() - - // Add activityIndicatorView on backgroundOverlayView centered - backgroundOverlayView.addSubview(activityIndicatorView) - activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false - activityIndicatorView.centerXAnchor.constraint(equalTo: backgroundOverlayView.centerXAnchor).isActive = true - activityIndicatorView.centerYAnchor.constraint(equalTo: backgroundOverlayView.centerYAnchor).isActive = true - - activityIndicatorView.startAnimating() - - backgroundOverlayView.alpha = 0 - backgroundOverlayView.isHidden = false - - view.vc_addSubViewMatchingParent(backgroundOverlayView) - - self.backgroundOverlayView = backgroundOverlayView - self.activityIndicatorView = activityIndicatorView - - let animationInstructions = { - backgroundOverlayView.alpha = Constants.backgroundOverlayAlpha - } - - if animated { - UIView.animate(withDuration: Constants.animationDuration) { - animationInstructions() - } completion: { _ in - completion?() - } - } else { - animationInstructions() - completion?() - } - } - - func removeCurrentActivityIndicator(animated: Bool, completion: (() -> Void)? = nil) { - guard let presentingView, - let backgroundOverlayView = backgroundOverlayView, - let activityIndicatorView = activityIndicatorView else { - return - } - - presentingView.isUserInteractionEnabled = true - self.presentingView = nil - - let animationInstructions = { - activityIndicatorView.alpha = 0 - } - - let animationCompletionInstructions = { - activityIndicatorView.stopAnimating() - backgroundOverlayView.isHidden = true - backgroundOverlayView.removeFromSuperview() - } - - if animated { - UIView.animate(withDuration: Constants.animationDuration) { - animationInstructions() - } completion: { _ in - animationCompletionInstructions() - } - } else { - animationInstructions() - animationCompletionInstructions() - } - } - - // MARK: - Private - - private func createBackgroundOverlayView(with frame: CGRect = CGRect.zero) -> UIView { - let backgroundOverlayView = UIView(frame: frame) - backgroundOverlayView.backgroundColor = Constants.backgroundOverlayColor - backgroundOverlayView.alpha = Constants.backgroundOverlayAlpha - return backgroundOverlayView - } -} - -private extension UIView { - /// Add a subview matching parent view using autolayout - @objc func vc_addSubViewMatchingParent(_ subView: UIView) { - addSubview(subView) - subView.translatesAutoresizingMaskIntoConstraints = false - let views = ["view": subView] - ["H:|[view]|", "V:|[view]|"].forEach { vfl in - let constraints = NSLayoutConstraint.constraints(withVisualFormat: vfl, - options: [], - metrics: nil, - views: views) - constraints.forEach { $0.isActive = true } - } - } -} diff --git a/ElementX/Sources/Other/UserIndicators/ActivityIndicatorPresenterType.swift b/ElementX/Sources/Other/UserIndicators/ActivityIndicatorPresenterType.swift deleted file mode 100644 index 7ff6d7284..000000000 --- a/ElementX/Sources/Other/UserIndicators/ActivityIndicatorPresenterType.swift +++ /dev/null @@ -1,34 +0,0 @@ -/* - Copyright 2019 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 UIKit - -/// Protocol used to present activity indicator on a view -protocol ActivityIndicatorPresenterType { - func presentActivityIndicator(on view: UIView, animated: Bool, completion: (() -> Void)?) - func removeCurrentActivityIndicator(animated: Bool, completion: (() -> Void)?) -} - -// `ActivityIndicatorPresenterType` default implementation -extension ActivityIndicatorPresenterType { - func presentActivityIndicator(on view: UIView, animated: Bool) { - presentActivityIndicator(on: view, animated: animated, completion: nil) - } - - func removeCurrentActivityIndicator(animated: Bool) { - removeCurrentActivityIndicator(animated: animated, completion: nil) - } -} diff --git a/ElementX/Sources/Other/UserIndicators/ActivityIndicatorView.swift b/ElementX/Sources/Other/UserIndicators/ActivityIndicatorView.swift deleted file mode 100755 index c1622d5ce..000000000 --- a/ElementX/Sources/Other/UserIndicators/ActivityIndicatorView.swift +++ /dev/null @@ -1,111 +0,0 @@ -/* - Copyright 2019 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 UIKit - -final class ActivityIndicatorView: UIView { - // MARK: - Constants - - private enum Constants { - static let cornerRadius: CGFloat = 5.0 - static let activityIndicatorMargin = CGSize(width: 30.0, height: 30.0) - } - - // MARK: - Properties - - // MARK: Outlets - - @IBOutlet private var activityIndicatorView: UIActivityIndicatorView! - @IBOutlet private var activityIndicatorBackgroundView: UIView! - - // MARK: Public - - var color: UIColor? { - get { - activityIndicatorView.color - } - set { - activityIndicatorView.color = newValue - } - } - - // MARK: - Setup - - private func commonInit() { - activityIndicatorBackgroundView.layer.masksToBounds = true - } - - convenience init() { - self.init(frame: CGRect.zero) - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - loadNibContent() - commonInit() - } - - override init(frame: CGRect) { - super.init(frame: frame) - loadNibContent() - commonInit() - } - - // MARK: - Overrides - - override var intrinsicContentSize: CGSize { - CGSize(width: activityIndicatorView.intrinsicContentSize.width + Constants.activityIndicatorMargin.width, - height: activityIndicatorView.intrinsicContentSize.height + Constants.activityIndicatorMargin.height) - } - - override func layoutSubviews() { - super.layoutSubviews() - - activityIndicatorBackgroundView.layer.cornerRadius = Constants.cornerRadius - } - - // MARK: - Public - - func startAnimating() { - activityIndicatorView.startAnimating() - } - - func stopAnimating() { - activityIndicatorView.stopAnimating() - } -} - -private extension UIView { - static var nib: UINib { - UINib(nibName: String(describing: self), bundle: Bundle(for: self)) - } - - func loadNibContent() { - let layoutAttributes: [NSLayoutConstraint.Attribute] = [.top, .leading, .bottom, .trailing] - for case let view as UIView in type(of: self).nib.instantiate(withOwner: self, options: nil) { - view.translatesAutoresizingMaskIntoConstraints = false - addSubview(view) - NSLayoutConstraint.activate(layoutAttributes.map { attribute in - NSLayoutConstraint( - item: view, attribute: attribute, - relatedBy: .equal, - toItem: self, attribute: attribute, - multiplier: 1, constant: 0.0 - ) - }) - } - } -} diff --git a/ElementX/Sources/Other/UserIndicators/ActivityIndicatorView.xib b/ElementX/Sources/Other/UserIndicators/ActivityIndicatorView.xib deleted file mode 100755 index 874a4e665..000000000 --- a/ElementX/Sources/Other/UserIndicators/ActivityIndicatorView.xib +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ElementX/Sources/Other/UserIndicators/FullscreenLoadingViewPresenter.swift b/ElementX/Sources/Other/UserIndicators/FullscreenLoadingViewPresenter.swift deleted file mode 100644 index b633f4d17..000000000 --- a/ElementX/Sources/Other/UserIndicators/FullscreenLoadingViewPresenter.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// Copyright 2022 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 UIKit - -/// A presenter responsible for showing / hiding a full-screen loading view that obscures (and thus disables) all other controls. -/// It is managed by a `UserIndicator`, meaning the `present` and `dismiss` methods will be called when the parent `UserIndicator` starts or completes. -class FullscreenLoadingViewPresenter: UserIndicatorViewPresentable { - private let label: String - private let presentationContext: UserIndicatorPresentationContext - private weak var view: UIView? - private var animator: UIViewPropertyAnimator? - - init(label: String, presentationContext: UserIndicatorPresentationContext) { - self.label = label - self.presentationContext = presentationContext - } - - func present() { - // Find the current top navigation controller - var presentingController: UIViewController? = presentationContext.indicatorPresentingViewController - while presentingController?.navigationController != nil { - presentingController = presentingController?.navigationController - } - guard let presentingController else { - return - } - - let view = LabelledActivityIndicatorView(text: label) - self.view = view - - view.translatesAutoresizingMaskIntoConstraints = false - presentingController.view.addSubview(view) - NSLayoutConstraint.activate([ - view.topAnchor.constraint(equalTo: presentingController.view.topAnchor), - view.bottomAnchor.constraint(equalTo: presentingController.view.bottomAnchor), - view.leadingAnchor.constraint(equalTo: presentingController.view.leadingAnchor), - view.trailingAnchor.constraint(equalTo: presentingController.view.trailingAnchor) - ]) - - view.alpha = 0 - animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeOut) { - view.alpha = 1 - } - animator?.startAnimation() - } - - func dismiss() { - guard let view, view.superview != nil else { - return - } - - animator?.stopAnimation(true) - animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeIn) { - view.alpha = 0 - } - animator?.addCompletion { _ in - view.removeFromSuperview() - } - animator?.startAnimation() - } -} diff --git a/ElementX/Sources/Other/UserIndicators/LabelledActivityIndicatorView.swift b/ElementX/Sources/Other/UserIndicators/LabelledActivityIndicatorView.swift deleted file mode 100644 index 06102f9f1..000000000 --- a/ElementX/Sources/Other/UserIndicators/LabelledActivityIndicatorView.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// Copyright 2022 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 - -final class LabelledActivityIndicatorView: UIView { - private enum Constants { - static let padding = UIEdgeInsets(top: 20, left: 40, bottom: 15, right: 40) - static let activityIndicatorScale = CGFloat(1.5) - static let cornerRadius: CGFloat = 12.0 - static let stackSpacing: CGFloat = 15 - static let backgroundOpacity: CGFloat = 0.5 - } - - private let stackBackgroundView: UIView = { - let view = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial)) - view.layer.cornerRadius = Constants.cornerRadius - view.clipsToBounds = true - return view - }() - - private let stackView: UIStackView = { - let stack = UIStackView() - stack.axis = .vertical - stack.distribution = .fill - stack.alignment = .center - stack.spacing = Constants.stackSpacing - return stack - }() - - private let activityIndicator: UIActivityIndicatorView = { - let view = UIActivityIndicatorView() - view.transform = .init(scaleX: Constants.activityIndicatorScale, y: Constants.activityIndicatorScale) - view.startAnimating() - return view - }() - - private let label = UILabel() - - init(text: String) { - super.init(frame: .zero) - setup(text: text) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setup(text: String) { - setupStackView() - label.text = text - label.textColor = .element.primaryContent - } - - private func setupStackView() { - addSubview(stackView) - stackView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - stackView.centerXAnchor.constraint(equalTo: centerXAnchor), - stackView.centerYAnchor.constraint(equalTo: centerYAnchor) - ]) - - stackView.addArrangedSubview(activityIndicator) - stackView.addArrangedSubview(label) - - insertSubview(stackBackgroundView, belowSubview: stackView) - stackBackgroundView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - stackBackgroundView.topAnchor.constraint(equalTo: stackView.topAnchor, constant: -Constants.padding.top), - stackBackgroundView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: Constants.padding.bottom), - stackBackgroundView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: -Constants.padding.left), - stackBackgroundView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: Constants.padding.right) - ]) - } -} diff --git a/ElementX/Sources/Other/UserIndicators/RectangleToastView.swift b/ElementX/Sources/Other/UserIndicators/RectangleToastView.swift deleted file mode 100644 index e7192e1d6..000000000 --- a/ElementX/Sources/Other/UserIndicators/RectangleToastView.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// Copyright 2022 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 - -class RectangleToastView: UIView { - private enum Constants { - static let padding = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) - static let cornerRadius: CGFloat = 8.0 - } - - private lazy var imageView: UIImageView = { - let view = UIImageView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - return view - }() - - private lazy var messageLabel: UILabel = { - let label = UILabel() - label.backgroundColor = .clear - label.numberOfLines = 0 - label.textAlignment = .left - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private lazy var stackView: UIStackView = { - let result = UIStackView() - result.axis = .horizontal - result.distribution = .fill - result.alignment = .center - result.spacing = 8.0 - result.backgroundColor = .clear - - addSubview(result) - result.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - result.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.padding.left), - result.topAnchor.constraint(equalTo: topAnchor, constant: Constants.padding.top), - result.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.padding.right), - result.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Constants.padding.bottom) - ]) - - return result - }() - - init(withMessage message: String?, - image: UIImage? = nil) { - super.init(frame: .zero) - - if let image { - imageView.image = image - NSLayoutConstraint.activate([ - imageView.widthAnchor.constraint(equalToConstant: image.size.width), - imageView.heightAnchor.constraint(equalToConstant: image.size.height) - ]) - stackView.addArrangedSubview(imageView) - } - - messageLabel.text = message - stackView.addArrangedSubview(messageLabel) - - stackView.layoutIfNeeded() - layer.cornerRadius = Constants.cornerRadius - layer.masksToBounds = true - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/ElementX/Sources/Other/UserIndicators/RoundedToastView.swift b/ElementX/Sources/Other/UserIndicators/RoundedToastView.swift deleted file mode 100644 index fe0c042e1..000000000 --- a/ElementX/Sources/Other/UserIndicators/RoundedToastView.swift +++ /dev/null @@ -1,124 +0,0 @@ -// -// Copyright 2022 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 UIKit - -class RoundedToastView: UIView { - private struct ShadowStyle { - let offset: CGSize - let radius: CGFloat - let opacity: Float - } - - private enum Constants { - static let padding = UIEdgeInsets(top: 10, left: 12, bottom: 10, right: 12) - static let activityIndicatorScale = CGFloat(0.75) - static let imageViewSize = CGFloat(15) - static let lightShadow = ShadowStyle(offset: .init(width: 0, height: 4), radius: 12, opacity: 0.1) - static let darkShadow = ShadowStyle(offset: .init(width: 0, height: 4), radius: 4, opacity: 0.2) - } - - private lazy var activityIndicator: UIActivityIndicatorView = { - let indicator = UIActivityIndicatorView() - indicator.transform = .init(scaleX: Constants.activityIndicatorScale, y: Constants.activityIndicatorScale) - indicator.startAnimating() - return indicator - }() - - private lazy var imageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFit - imageView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - imageView.widthAnchor.constraint(equalToConstant: Constants.imageViewSize), - imageView.heightAnchor.constraint(equalToConstant: Constants.imageViewSize) - ]) - return imageView - }() - - private let stackView: UIStackView = { - let stack = UIStackView() - stack.axis = .horizontal - stack.alignment = .center - stack.spacing = 5 - return stack - }() - - private let label = UILabel() - - init(viewState: ToastViewState) { - super.init(frame: .zero) - setup(viewState: viewState) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setup(viewState: ToastViewState) { - backgroundColor = .clear - clipsToBounds = true - - setupBackgroundMaterial() - setupStackView() - stackView.addArrangedSubview(toastView(for: viewState.style)) - stackView.addArrangedSubview(label) - label.text = viewState.label - label.textColor = .element.primaryContent - } - - private func setupBackgroundMaterial() { - let material = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial)) - addSubview(material) - material.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - material.topAnchor.constraint(equalTo: topAnchor), - material.bottomAnchor.constraint(equalTo: bottomAnchor), - material.leadingAnchor.constraint(equalTo: leadingAnchor), - material.trailingAnchor.constraint(equalTo: trailingAnchor) - ]) - } - - private func setupStackView() { - addSubview(stackView) - stackView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - stackView.topAnchor.constraint(equalTo: topAnchor, constant: Constants.padding.top), - stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Constants.padding.bottom), - stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.padding.left), - stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.padding.right) - ]) - } - - override func layoutSubviews() { - super.layoutSubviews() - layer.cornerRadius = layer.frame.height / 2 - } - - private func toastView(for style: ToastViewState.Style) -> UIView { - switch style { - case .loading: - return activityIndicator - case .success: - imageView.image = UIImage(systemName: "checkmark") - return imageView - case .error: - imageView.image = UIImage(systemName: "xmark") - return imageView - } - } -} diff --git a/ElementX/Sources/Other/UserIndicators/ToastViewPresenter.swift b/ElementX/Sources/Other/UserIndicators/ToastViewPresenter.swift deleted file mode 100644 index b8e005138..000000000 --- a/ElementX/Sources/Other/UserIndicators/ToastViewPresenter.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// Copyright 2022 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 UIKit - -/// A presenter responsible for showing / hiding a toast view for loading spinners or success messages. -/// It is managed by an `UserIndicator`, meaning the `present` and `dismiss` methods will be called when the parent `UserIndicator` starts or completes. -class ToastViewPresenter: UserIndicatorViewPresentable { - private let viewState: ToastViewState - private let presentationContext: UserIndicatorPresentationContext - private weak var view: UIView? - private var animator: UIViewPropertyAnimator? - - init(viewState: ToastViewState, presentationContext: UserIndicatorPresentationContext) { - self.viewState = viewState - self.presentationContext = presentationContext - } - - func present() { - guard let viewController = presentationContext.indicatorPresentingViewController else { - return - } - - let view = RoundedToastView(viewState: viewState) - self.view = view - - view.translatesAutoresizingMaskIntoConstraints = false - viewController.view.addSubview(view) - NSLayoutConstraint.activate([ - view.centerXAnchor.constraint(equalTo: viewController.view.centerXAnchor), - view.topAnchor.constraint(equalTo: viewController.view.safeAreaLayoutGuide.topAnchor) - ]) - - view.alpha = 0 - view.transform = .init(translationX: 0, y: 5) - animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeOut) { - view.alpha = 1 - view.transform = .identity - } - animator?.startAnimation() - } - - func dismiss() { - guard let view, view.superview != nil else { - return - } - - animator?.stopAnimation(true) - animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeIn) { - view.alpha = 0 - view.transform = .init(translationX: 0, y: -5) - } - animator?.addCompletion { _ in - view.removeFromSuperview() - } - animator?.startAnimation() - } -} diff --git a/ElementX/Sources/Other/UserIndicators/UserIndicator.swift b/ElementX/Sources/Other/UserIndicators/UserIndicator.swift deleted file mode 100644 index ce9d4786e..000000000 --- a/ElementX/Sources/Other/UserIndicators/UserIndicator.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// Copyright 2022 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 - -/// A `UserIndicator` represents the state of a temporary visual indicator, such as loading spinner, success notification or an error message. It does not directly manage the UI, instead it delegates to a `presenter` -/// whenever the UI should be shown or hidden. -/// -/// More than one `UserIndicator` may be requested by the system at the same time (e.g. global syncing vs local refresh), -/// and the `UserIndicatorQueue` will ensure that only one indicator is shown at a given time, putting the other in a pending queue. -/// -/// A client that requests an indicator can specify a default timeout after which the indicator is dismissed, or it has to be manually -/// responsible for dismissing it via `cancel` method, or by deallocating itself. -public class UserIndicator { - public enum State { - case pending - case executing - case completed - } - - private let request: UserIndicatorRequest - private let completion: () -> Void - - public private(set) var state: State - - public init(request: UserIndicatorRequest, completion: @escaping () -> Void) { - self.request = request - self.completion = completion - - state = .pending - } - - deinit { - complete() - } - - internal func start() { - guard state == .pending else { - return - } - - state = .executing - request.presenter.present() - - switch request.dismissal { - case .manual: - break - case .timeout(let interval): - Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in - self?.complete() - } - } - } - - /// Cancel the indicator, triggering any dismissal action / animation - /// - /// Note: clients can call this method directly, if they have access to the `UserIndicator`. Alternatively - /// deallocating the `UserIndicator` will call `cancel` automatically. - /// Once cancelled, `UserIndicatorQueue` will automatically start the next `UserIndicator` in the queue. - public func cancel() { - complete() - } - - private func complete() { - guard state != .completed else { - return - } - if state == .executing { - request.presenter.dismiss() - } - - state = .completed - completion() - } -} - -public extension UserIndicator { - func store(in collection: inout C) where C: RangeReplaceableCollection, C.Element == UserIndicator { - collection.append(self) - } -} - -public extension Collection where Element == UserIndicator { - func cancelAll() { - forEach { - $0.cancel() - } - } -} diff --git a/ElementX/Sources/Other/UserIndicators/UserIndicatorDismissal.swift b/ElementX/Sources/Other/UserIndicators/UserIndicatorDismissal.swift deleted file mode 100644 index abbb06812..000000000 --- a/ElementX/Sources/Other/UserIndicators/UserIndicatorDismissal.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright 2022 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 - -/// Different ways in which a `UserIndicator` can be dismissed -public enum UserIndicatorDismissal { - /// The `UserIndicator` will not manage the dismissal, but will expect the calling client to do so manually - case manual - /// The `UserIndicator` will be automatically dismissed after `TimeInterval` - case timeout(TimeInterval) -} diff --git a/ElementX/Sources/Other/UserIndicators/UserIndicatorPresentationContext.swift b/ElementX/Sources/Other/UserIndicators/UserIndicatorPresentationContext.swift deleted file mode 100644 index 4f3477b7d..000000000 --- a/ElementX/Sources/Other/UserIndicators/UserIndicatorPresentationContext.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// Copyright 2022 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 - -/// The presentation context is used by `UserIndicatorViewPresentable`s to display content -/// on the screen and it serves two primary purposes: -/// -/// - abstraction on top of UIKit (passing context instead of view controllers) -/// - immutable context passed at init with variable presenting view controller -/// (e.g. depending on collapsed / uncollapsed iPad presentation that changes -/// at runtime) -public protocol UserIndicatorPresentationContext { - var indicatorPresentingViewController: UIViewController? { get } -} - -/// A simple implementation of `UserIndicatorPresentationContext` that uses a weak reference -/// to the passed-in view controller as the presentation context. -public class StaticUserIndicatorPresentationContext: UserIndicatorPresentationContext { - // The presenting view controller will be the parent of the user indicator, - // and the indicator holds a strong reference to the context, so the view controller - // must be decleared `weak` to avoid a retain cycle - public private(set) weak var indicatorPresentingViewController: UIViewController? - - public init(viewController: UIViewController) { - indicatorPresentingViewController = viewController - } -} diff --git a/ElementX/Sources/Other/UserIndicators/UserIndicatorPresenter.swift b/ElementX/Sources/Other/UserIndicators/UserIndicatorPresenter.swift deleted file mode 100644 index 8df335b46..000000000 --- a/ElementX/Sources/Other/UserIndicators/UserIndicatorPresenter.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// Copyright 2022 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 UIKit - -/// A set of user interactors commonly used across the app -enum UserIndicatorType { - case loading(label: String, isInteractionBlocking: Bool) - case success(label: String) - case error(label: String) -} - -/// A presenter which can handle `UserIndicatorType` by creating the underlying `UserIndicator` -/// and adding it to its `UserIndicatorQueue` -protocol UserIndicatorTypePresenterProtocol { - /// Present a new type of user indicator, such as loading spinner or success message. - /// - /// The presenter will internally convert the type into a `UserIndicator` and add it to its internal queue - /// of other indicators. - /// - /// If the queue is empty, the indicator will be displayed immediately, otherwise it will be pending - /// until the previously added indicators have completed / been cancelled. - /// - /// To remove an indicator, `cancel` or deallocate the returned `UserIndicator` - func present(_ type: UserIndicatorType) -> UserIndicator - - /// The queue of user indicators owned by the presenter - /// - /// Clients can access the queue to add custom `UserIndicatorRequest`s - /// above and beyond those defined by `UserIndicatorType` - var queue: UserIndicatorQueue { get } -} - -class UserIndicatorTypePresenter: UserIndicatorTypePresenterProtocol { - private let presentationContext: UserIndicatorPresentationContext - let queue: UserIndicatorQueue - - init(presentationContext: UserIndicatorPresentationContext) { - self.presentationContext = presentationContext - queue = UserIndicatorQueue() - } - - convenience init(presentingViewController: UIViewController) { - let context = StaticUserIndicatorPresentationContext(viewController: presentingViewController) - self.init(presentationContext: context) - } - - func present(_ type: UserIndicatorType) -> UserIndicator { - let request = userIndicatorRequest(for: type) - return queue.add(request) - } - - private func userIndicatorRequest(for type: UserIndicatorType) -> UserIndicatorRequest { - switch type { - case .loading(let label, let isInteractionBlocking): - if isInteractionBlocking { - return fullScreenLoadingRequest(label: label) - } else { - return loadingRequest(label: label) - } - case .success(let label): - return successRequest(label: label) - case .error(let label): - return errorRequest(label: label) - } - } - - private func loadingRequest(label: String) -> UserIndicatorRequest { - let presenter = ToastViewPresenter( - viewState: .init( - style: .loading, - label: label - ), - presentationContext: presentationContext - ) - return UserIndicatorRequest( - presenter: presenter, - dismissal: .manual - ) - } - - private func fullScreenLoadingRequest(label: String) -> UserIndicatorRequest { - let presenter = FullscreenLoadingViewPresenter( - label: label, - presentationContext: presentationContext - ) - return UserIndicatorRequest( - presenter: presenter, - dismissal: .manual - ) - } - - private func successRequest(label: String) -> UserIndicatorRequest { - let presenter = ToastViewPresenter( - viewState: .init( - style: .success, - label: label - ), - presentationContext: presentationContext - ) - return UserIndicatorRequest( - presenter: presenter, - dismissal: .timeout(1.5) - ) - } - - private func errorRequest(label: String) -> UserIndicatorRequest { - let presenter = ToastViewPresenter( - viewState: .init( - style: .error, - label: label - ), - presentationContext: presentationContext - ) - return UserIndicatorRequest( - presenter: presenter, - dismissal: .timeout(1.5) - ) - } -} diff --git a/ElementX/Sources/Other/UserIndicators/UserIndicatorQueue.swift b/ElementX/Sources/Other/UserIndicators/UserIndicatorQueue.swift deleted file mode 100644 index d2328b26f..000000000 --- a/ElementX/Sources/Other/UserIndicators/UserIndicatorQueue.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// Copyright 2022 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 - -/// A FIFO queue which will ensure only one user indicator is shown at a given time. -/// -/// `UserIndicatorQueue` offers a `shared` queue that can be used by any clients app-wide, but clients are also allowed -/// to create local `UserIndicatorQueue` if the context requres multiple simultaneous indicators. -public class UserIndicatorQueue { - private class Weak { - weak var element: T? - init(_ element: T) { - self.element = element - } - } - - private var queue: [Weak] - - public init() { - queue = [] - } - - /// Add a new indicator to the queue by providing a request. - /// - /// The queue will start the indicator right away, if there are no currently running indicators, - /// otherwise the indicator will be put on hold. - public func add(_ request: UserIndicatorRequest) -> UserIndicator { - let indicator = UserIndicator(request: request) { [weak self] in - self?.startNextIfIdle() - } - - queue.append(Weak(indicator)) - startNextIfIdle() - return indicator - } - - private func startNextIfIdle() { - cleanup() - if let indicator = queue.first?.element, indicator.state == .pending { - indicator.start() - } - } - - private func cleanup() { - queue.removeAll { - $0.element == nil || $0.element?.state == .completed - } - } -} diff --git a/ElementX/Sources/Other/UserIndicators/UserIndicatorStore.swift b/ElementX/Sources/Other/UserIndicators/UserIndicatorStore.swift deleted file mode 100644 index 7a44a6b02..000000000 --- a/ElementX/Sources/Other/UserIndicators/UserIndicatorStore.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// Copyright 2022 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 - -typealias UserIndicatorCancel = () -> Void - -/// An abstraction on top of `UserIndicatorTypePresenterProtocol` which manages and stores the individual user indicators. -/// When used to present an indicator the `UserIndicatorStore` will instead returns a simple callback function to the clients -/// letting them cancel the indicators without worrying about memory. -@objc final class UserIndicatorStore: NSObject { - private let presenter: UserIndicatorTypePresenterProtocol - private var indicators: [UserIndicator] - - init(presenter: UserIndicatorTypePresenterProtocol) { - self.presenter = presenter - indicators = [] - } - - /// Present a new type of user indicator, such as loading spinner or success message. - /// To remove an indicator, call the returned `UserIndicatorCancel` function - func present(type: UserIndicatorType) -> UserIndicatorCancel { - let indicator = presenter.present(type) - indicators.append(indicator) - return { - indicator.cancel() - } - } - - /// Present a loading indicator. - /// To remove the indicator call the returned `UserIndicatorCancel` function - /// - /// Note: This is a convenience function callable by objective-c code - @objc func presentLoading(label: String, isInteractionBlocking: Bool) -> UserIndicatorCancel { - present( - type: .loading( - label: label, - isInteractionBlocking: isInteractionBlocking - ) - ) - } - - /// Present a success message that will be automatically dismissed after a few seconds. - /// - /// Note: This is a convenience function callable by objective-c code - @objc func presentSuccess(label: String) { - let indicator = presenter.present(.success(label: label)) - indicators.append(indicator) - } -} diff --git a/ElementX/Sources/Other/UserIndicators/UserIndicatorViewPresentable.swift b/ElementX/Sources/Other/UserIndicators/UserIndicatorViewPresentable.swift deleted file mode 100644 index 0a6c86d95..000000000 --- a/ElementX/Sources/Other/UserIndicators/UserIndicatorViewPresentable.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright 2022 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 - -/// A presenter associated with and called by a `UserIndicator`, and responsible for the underlying view shown on the screen. -public protocol UserIndicatorViewPresentable { - /// Called when the `UserIndicator` is started (manually or by the `UserIndicatorQueue`) - func present() - /// Called when the `UserIndicator` is manually cancelled or completed - func dismiss() -} diff --git a/UnitTests/Sources/UserIndicators/UserIndicatorPresenterSpy.swift b/ElementX/Sources/Other/UserNotifications/MockUserNotificationController.swift similarity index 72% rename from UnitTests/Sources/UserIndicators/UserIndicatorPresenterSpy.swift rename to ElementX/Sources/Other/UserNotifications/MockUserNotificationController.swift index 0aefb88d5..faff544a1 100644 --- a/UnitTests/Sources/UserIndicators/UserIndicatorPresenterSpy.swift +++ b/ElementX/Sources/Other/UserNotifications/MockUserNotificationController.swift @@ -14,17 +14,12 @@ // limitations under the License. // -@testable import ElementX import Foundation -class UserIndicatorPresenterSpy: UserIndicatorViewPresentable { - var intel = [String]() +struct MockUserNotificationController: UserNotificationControllerProtocol { + func submitNotification(_ notification: UserNotification) { } - func present() { - intel.append(#function) - } + func retractNotificationWithId(_ id: String) { } - func dismiss() { - intel.append(#function) - } + func retractAllNotifications() { } } diff --git a/ElementX/Sources/Other/UserNotifications/UserNotification.swift b/ElementX/Sources/Other/UserNotifications/UserNotification.swift new file mode 100644 index 000000000..fda9d2a06 --- /dev/null +++ b/ElementX/Sources/Other/UserNotifications/UserNotification.swift @@ -0,0 +1,30 @@ +// +// Copyright 2022 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 SwiftUI + +enum UserNotificationType { + case toast + case modal +} + +struct UserNotification: Equatable, Identifiable { + var id: String = UUID().uuidString + var type = UserNotificationType.toast + var title: String + var iconName: String? + var persistent = false +} diff --git a/ElementX/Sources/Other/UserNotifications/UserNotificationController.swift b/ElementX/Sources/Other/UserNotifications/UserNotificationController.swift new file mode 100644 index 000000000..d10d88564 --- /dev/null +++ b/ElementX/Sources/Other/UserNotifications/UserNotificationController.swift @@ -0,0 +1,80 @@ +// +// Copyright 2022 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 SwiftUI + +class UserNotificationController: ObservableObject, UserNotificationControllerProtocol { + private let rootCoordinator: CoordinatorProtocol + + private var dismisalTimer: Timer? + private var displayTimes = [String: Date]() + + var nonPersistentDisplayDuration = 2.5 + var minimumDisplayDuration = 0.5 + + @Published private(set) var activeNotification: UserNotification? + private(set) var notificationQueue = [UserNotification]() { + didSet { + activeNotification = notificationQueue.last + + if let activeNotification, !activeNotification.persistent { + dismisalTimer?.invalidate() + dismisalTimer = Timer.scheduledTimer(withTimeInterval: nonPersistentDisplayDuration, repeats: false) { [weak self] _ in + self?.retractNotificationWithId(activeNotification.id) + } + } + } + } + + init(rootCoordinator: CoordinatorProtocol) { + self.rootCoordinator = rootCoordinator + } + + func toPresentable() -> AnyView { + AnyView( + UserNotificationPresenter(userNotificationController: self, rootView: rootCoordinator.toPresentable()) + ) + } + + func submitNotification(_ notification: UserNotification) { + if let index = notificationQueue.firstIndex(where: { $0.id == notification.id }) { + notificationQueue[index] = notification + } else { + retractNotificationWithId(notification.id) + notificationQueue.append(notification) + } + + displayTimes[notification.id] = .now + } + + func retractAllNotifications() { + for notification in notificationQueue { + retractNotificationWithId(notification.id) + } + } + + func retractNotificationWithId(_ id: String) { + guard let displayTime = displayTimes[id], abs(displayTime.timeIntervalSinceNow) <= minimumDisplayDuration else { + notificationQueue.removeAll { $0.id == id } + return + } + + Timer.scheduledTimer(withTimeInterval: minimumDisplayDuration, repeats: false) { [weak self] _ in + self?.notificationQueue.removeAll { $0.id == id } + self?.displayTimes[id] = nil + } + } +} diff --git a/ElementX/Sources/Other/ElementNavigationController.swift b/ElementX/Sources/Other/UserNotifications/UserNotificationControllerProtocol.swift similarity index 72% rename from ElementX/Sources/Other/ElementNavigationController.swift rename to ElementX/Sources/Other/UserNotifications/UserNotificationControllerProtocol.swift index 8a282b382..ba0ef6c1f 100644 --- a/ElementX/Sources/Other/ElementNavigationController.swift +++ b/ElementX/Sources/Other/UserNotifications/UserNotificationControllerProtocol.swift @@ -14,11 +14,10 @@ // limitations under the License. // -import UIKit +import Foundation -class ElementNavigationController: UINavigationController { - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - navigationBar.topItem?.backButtonDisplayMode = .minimal - } +protocol UserNotificationControllerProtocol: CoordinatorProtocol { + func submitNotification(_ notification: UserNotification) + func retractNotificationWithId(_ id: String) + func retractAllNotifications() } diff --git a/ElementX/Sources/Other/UserNotifications/UserNotificationModalView.swift b/ElementX/Sources/Other/UserNotifications/UserNotificationModalView.swift new file mode 100644 index 000000000..ec56f1dd1 --- /dev/null +++ b/ElementX/Sources/Other/UserNotifications/UserNotificationModalView.swift @@ -0,0 +1,70 @@ +// +// Copyright 2022 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 SwiftUI + +struct UserNotificationModalView: View { + let notification: UserNotification + + var body: some View { + ZStack { + VStack(spacing: 12.0) { + ProgressView() + + HStack { + if let iconName = notification.iconName { + Image(systemName: iconName) + } + Text(notification.title) + .font(.element.body) + .foregroundColor(.element.primaryContent) + } + } + .padding() + .frame(minWidth: 150.0) + .background(Color.element.quinaryContent) + .clipShape(RoundedCornerShape(radius: 12.0, corners: .allCorners)) + .shadow(color: .black.opacity(0.1), radius: 10.0, y: 4.0) + .transition(.opacity) + } + .id(notification.id) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(.black.opacity(0.1)) + .ignoresSafeArea() + } + + private var toastTransition: AnyTransition { + AnyTransition + .asymmetric(insertion: .move(edge: .top), + removal: .move(edge: .bottom)) + .combined(with: .opacity) + } +} + +struct UserNotificationModalView_Previews: PreviewProvider { + static var previews: some View { + body.preferredColorScheme(.light) + body.preferredColorScheme(.dark) + } + + private static var body: some View { + VStack { + UserNotificationModalView(notification: UserNotification(type: .modal, + title: "Successfully logged in", + iconName: "checkmark")) + } + } +} diff --git a/ElementX/Sources/Other/UserNotifications/UserNotificationPresenter.swift b/ElementX/Sources/Other/UserNotifications/UserNotificationPresenter.swift new file mode 100644 index 000000000..5856ba918 --- /dev/null +++ b/ElementX/Sources/Other/UserNotifications/UserNotificationPresenter.swift @@ -0,0 +1,44 @@ +// +// Copyright 2022 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 SwiftUI + +struct UserNotificationPresenter: View { + @ObservedObject var userNotificationController: UserNotificationController + let rootView: AnyView + + var body: some View { + ZStack(alignment: .top) { + rootView + notificationViewFor(notification: userNotificationController.activeNotification) + } + .animation(.elementDefault, value: userNotificationController.activeNotification) + } + + @ViewBuilder + private func notificationViewFor(notification: UserNotification?) -> some View { + ZStack { // Need a container to properly animate transitions + if let notification { + switch notification.type { + case .toast: + UserNotificationToastView(notification: notification) + case .modal: + UserNotificationModalView(notification: notification) + } + } + } + } +} diff --git a/ElementX/Sources/Other/UserNotifications/UserNotificationToastView.swift b/ElementX/Sources/Other/UserNotifications/UserNotificationToastView.swift new file mode 100644 index 000000000..83ef3bceb --- /dev/null +++ b/ElementX/Sources/Other/UserNotifications/UserNotificationToastView.swift @@ -0,0 +1,63 @@ +// +// Copyright 2022 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 SwiftUI + +struct UserNotificationToastView: View { + let notification: UserNotification + + var body: some View { + HStack { + if let iconName = notification.iconName { + Image(systemName: iconName) + } + Text(notification.title) + .font(.element.footnote) + .foregroundColor(.element.primaryContent) + } + .id(notification.id) + .padding(.horizontal, 12.0) + .padding(.vertical, 10.0) + .frame(minWidth: 150.0) + .background(Color.element.quaternaryContent) + .clipShape(RoundedCornerShape(radius: 24.0, corners: .allCorners)) + .shadow(color: .black.opacity(0.1), radius: 10.0, y: 4.0) + .transition(toastTransition) + } + + private var toastTransition: AnyTransition { + AnyTransition + .asymmetric(insertion: .move(edge: .top), + removal: .move(edge: .bottom)) + .combined(with: .opacity) + } +} + +struct UserNotificationToastView_Previews: PreviewProvider { + static var previews: some View { + body.preferredColorScheme(.light) + body.preferredColorScheme(.dark) + } + + private static var body: some View { + VStack { + UserNotificationToastView(notification: UserNotification(title: "Successfully logged in", + iconName: "checkmark")) + + UserNotificationToastView(notification: UserNotification(title: "Toast without icon")) + } + } +} diff --git a/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptCoordinator.swift b/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptCoordinator.swift index 716e4e4c5..db5e479a3 100644 --- a/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptCoordinator.swift +++ b/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptCoordinator.swift @@ -21,39 +21,22 @@ struct AnalyticsPromptCoordinatorParameters { let userSession: UserSessionProtocol } -final class AnalyticsPromptCoordinator: Coordinator, Presentable { - // MARK: - Properties - - // MARK: Private - +final class AnalyticsPromptCoordinator: CoordinatorProtocol { private let parameters: AnalyticsPromptCoordinatorParameters - private let analyticsPromptHostingController: UIViewController - private var analyticsPromptViewModel: AnalyticsPromptViewModel - - // MARK: Public + private var viewModel: AnalyticsPromptViewModel - // Must be used only internally - var childCoordinators: [Coordinator] = [] var callback: (@MainActor () -> Void)? - // MARK: - Setup - init(parameters: AnalyticsPromptCoordinatorParameters) { self.parameters = parameters - let viewModel = AnalyticsPromptViewModel(termsURL: BuildSettings.analyticsConfiguration.termsURL) - - let view = AnalyticsPrompt(context: viewModel.context) - analyticsPromptViewModel = viewModel - analyticsPromptHostingController = UIHostingController(rootView: view) + viewModel = AnalyticsPromptViewModel(termsURL: BuildSettings.analyticsConfiguration.termsURL) } // MARK: - Public func start() { - MXLog.debug("Did start.") - - analyticsPromptViewModel.callback = { [weak self] result in + viewModel.callback = { [weak self] result in MXLog.debug("AnalyticsPromptViewModel did complete with result: \(result).") guard let self else { return } @@ -69,7 +52,7 @@ final class AnalyticsPromptCoordinator: Coordinator, Presentable { } } - func toPresentable() -> UIViewController { analyticsPromptHostingController } - - func stop() { } + func toPresentable() -> AnyView { + AnyView(AnalyticsPrompt(context: viewModel.context)) + } } diff --git a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift index 21279004d..2c788c646 100644 --- a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift @@ -14,7 +14,7 @@ // limitations under the License. // -import UIKit +import SwiftUI @MainActor protocol AuthenticationCoordinatorDelegate: AnyObject { @@ -22,42 +22,31 @@ protocol AuthenticationCoordinatorDelegate: AnyObject { didLoginWithSession userSession: UserSessionProtocol) } -class AuthenticationCoordinator: Coordinator, Presentable { +class AuthenticationCoordinator: CoordinatorProtocol { private let authenticationService: AuthenticationServiceProxyProtocol - private let navigationRouter: NavigationRouter - private var indicatorPresenter: UserIndicatorTypePresenterProtocol - private var activityIndicator: UserIndicator? - - var childCoordinators: [Coordinator] = [] + private let navigationController: NavigationController weak var delegate: AuthenticationCoordinatorDelegate? init(authenticationService: AuthenticationServiceProxyProtocol, - navigationRouter: NavigationRouter) { + navigationController: NavigationController) { self.authenticationService = authenticationService - self.navigationRouter = navigationRouter - - indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: navigationRouter.toPresentable()) + self.navigationController = navigationController } func start() { - showSplashScreen() + showOnboarding() } - func toPresentable() -> UIViewController { - navigationRouter.toPresentable() - } - func stop() { stopLoading() } - + // MARK: - Private - /// Shows the splash screen as the root view in the navigation stack. - private func showSplashScreen() { - let coordinator = SplashScreenCoordinator() - + private func showOnboarding() { + let coordinator = OnboardingCoordinator() + coordinator.callback = { [weak self] action in guard let self else { return } switch action { @@ -66,12 +55,7 @@ class AuthenticationCoordinator: Coordinator, Presentable { } } - coordinator.start() - add(childCoordinator: coordinator) - - navigationRouter.setRootModule(coordinator) { [weak self] in - self?.remove(childCoordinator: coordinator) - } + navigationController.setRootCoordinator(coordinator) } private func startAuthentication() async { @@ -89,7 +73,8 @@ class AuthenticationCoordinator: Coordinator, Presentable { private func showServerSelectionScreen() { let parameters = ServerSelectionCoordinatorParameters(authenticationService: authenticationService, - hasModalPresentation: false) + userNotificationController: ServiceLocator.shared.userNotificationController, + isModallyPresented: false) let coordinator = ServerSelectionCoordinator(parameters: parameters) coordinator.callback = { [weak self] action in @@ -103,34 +88,24 @@ class AuthenticationCoordinator: Coordinator, Presentable { } } - coordinator.start() - add(childCoordinator: coordinator) - - navigationRouter.push(coordinator) { [weak self] in - self?.remove(childCoordinator: coordinator) - } + navigationController.push(coordinator) } private func showLoginScreen() { let parameters = LoginCoordinatorParameters(authenticationService: authenticationService, - navigationRouter: navigationRouter) + navigationController: navigationController) let coordinator = LoginCoordinator(parameters: parameters) - + coordinator.callback = { [weak self] action in guard let self else { return } - + switch action { case .signedIn(let userSession): self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession) } } - - coordinator.start() - add(childCoordinator: coordinator) - - navigationRouter.push(coordinator) { [weak self] in - self?.remove(childCoordinator: coordinator) - } + + navigationController.push(coordinator) } private func showAnalyticsPrompt(with userSession: UserSessionProtocol) { @@ -141,22 +116,20 @@ class AuthenticationCoordinator: Coordinator, Presentable { guard let self else { return } self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession) } - - coordinator.start() - add(childCoordinator: coordinator) - - navigationRouter.setRootModule(coordinator, hideNavigationBar: true, animated: true) { [weak self] in - self?.remove(childCoordinator: coordinator) - } + + navigationController.setRootCoordinator(coordinator) } - /// Show a blocking activity indicator. + static let loadingIndicatorIdentifier = "AuthenticationCoordinatorLoading" + private func startLoading() { - activityIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true)) + ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(id: Self.loadingIndicatorIdentifier, + type: .modal, + title: ElementL10n.loading, + persistent: true)) } - /// Hide the currently displayed activity indicator. private func stopLoading() { - activityIndicator = nil + ServiceLocator.shared.userNotificationController.retractNotificationWithId(Self.loadingIndicatorIdentifier) } } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift index 062583851..1639589ad 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift @@ -21,7 +21,7 @@ struct LoginCoordinatorParameters { /// The service used to authenticate the user. let authenticationService: AuthenticationServiceProxyProtocol /// The navigation router used to present the server selection screen. - let navigationRouter: NavigationRouterType + let navigationController: NavigationController } enum LoginCoordinatorAction { @@ -29,14 +29,10 @@ enum LoginCoordinatorAction { case signedIn(UserSessionProtocol) } -final class LoginCoordinator: Coordinator, Presentable { - // MARK: - Properties - - // MARK: Private - +final class LoginCoordinator: CoordinatorProtocol { private let parameters: LoginCoordinatorParameters - private let loginHostingController: UIViewController - private var loginViewModel: LoginViewModelProtocol + private var viewModel: LoginViewModelProtocol + private let hostingController: UIViewController /// Passed to the OIDC service to provide a view controller from which to present the authentication session. private let oidcUserAgent: OIDExternalUserAgentIOS? @@ -47,14 +43,8 @@ final class LoginCoordinator: Coordinator, Presentable { } private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService } - private var navigationRouter: NavigationRouterType { parameters.navigationRouter } - private var indicatorPresenter: UserIndicatorTypePresenterProtocol - private var activityIndicator: UserIndicator? - - // MARK: Public + private var navigationController: NavigationController { parameters.navigationController } - // Must be used only internally - var childCoordinators: [Coordinator] = [] var callback: (@MainActor (LoginCoordinatorAction) -> Void)? // MARK: - Setup @@ -62,22 +52,16 @@ final class LoginCoordinator: Coordinator, Presentable { init(parameters: LoginCoordinatorParameters) { self.parameters = parameters - let viewModel = LoginViewModel(homeserver: parameters.authenticationService.homeserver) - loginViewModel = viewModel + viewModel = LoginViewModel(homeserver: parameters.authenticationService.homeserver) - let view = LoginScreen(context: viewModel.context) - loginHostingController = UIHostingController(rootView: view) - - indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: loginHostingController) - oidcUserAgent = OIDExternalUserAgentIOS(presenting: loginHostingController) + hostingController = UIHostingController(rootView: LoginScreen(context: viewModel.context)) + oidcUserAgent = OIDExternalUserAgentIOS(presenting: hostingController) } // MARK: - Public func start() { - MXLog.debug("Did start.") - - loginViewModel.callback = { [weak self] action in + viewModel.callback = { [weak self] action in guard let self else { return } MXLog.debug("LoginViewModel did callback with result: \(action).") @@ -95,51 +79,52 @@ final class LoginCoordinator: Coordinator, Presentable { } } } - - func toPresentable() -> UIViewController { - loginHostingController - } func stop() { stopLoading() } + func toPresentable() -> AnyView { + AnyView(LoginScreen(context: viewModel.context)) + } + // MARK: - Private - /// Show a blocking activity indicator whilst saving. + static let loadingIndicatorIdentifier = "LoginCoordinatorLoading" + private func startLoading(isInteractionBlocking: Bool) { - activityIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: isInteractionBlocking)) + ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(id: Self.loadingIndicatorIdentifier, + type: .modal, + title: ElementL10n.loading, + persistent: true)) if !isInteractionBlocking { - loginViewModel.update(isLoading: true) + viewModel.update(isLoading: true) } } - /// Show a non-blocking indicator that an operation was successful. - private func indicateSuccess() { - activityIndicator = indicatorPresenter.present(.success(label: ElementL10n.dialogTitleSuccess)) - } - - /// Show a non-blocking indicator that an operation failed. - private func indicateFailure() { - activityIndicator = indicatorPresenter.present(.error(label: ElementL10n.dialogTitleError)) - } - - /// Hide the currently displayed activity indicator. private func stopLoading() { - loginViewModel.update(isLoading: false) - activityIndicator = nil + viewModel.update(isLoading: false) + ServiceLocator.shared.userNotificationController.retractNotificationWithId(Self.loadingIndicatorIdentifier) + } + + private func indicateSuccess() { + ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(title: ElementL10n.dialogTitleSuccess)) + } + + private func indicateFailure() { + ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(title: ElementL10n.dialogTitleError)) } /// Processes an error to either update the flow or display it to the user. private func handleError(_ error: AuthenticationServiceError) { switch error { case .invalidCredentials: - loginViewModel.displayError(.alert(ElementL10n.authInvalidLoginParam)) + viewModel.displayError(.alert(ElementL10n.authInvalidLoginParam)) case .accountDeactivated: - loginViewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount)) + viewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount)) default: - loginViewModel.displayError(.alert(ElementL10n.unknownError)) + viewModel.displayError(.alert(ElementL10n.unknownError)) } } @@ -204,44 +189,43 @@ final class LoginCoordinator: Coordinator, Presentable { /// Updates the view model with a different homeserver. private func updateViewModel() { - loginViewModel.update(homeserver: authenticationService.homeserver) + viewModel.update(homeserver: authenticationService.homeserver) indicateSuccess() } /// Presents the server selection screen as a modal. private func presentServerSelectionScreen() { - MXLog.debug("PresentServerSelectionScreen") + let serverSelectionNavigationController = NavigationController() + + let userNotificationController = UserNotificationController(rootCoordinator: serverSelectionNavigationController) + let parameters = ServerSelectionCoordinatorParameters(authenticationService: authenticationService, - hasModalPresentation: true) + userNotificationController: userNotificationController, + isModallyPresented: true) + let coordinator = ServerSelectionCoordinator(parameters: parameters) coordinator.callback = { [weak self, weak coordinator] action in guard let self, let coordinator = coordinator else { return } self.serverSelectionCoordinator(coordinator, didCompleteWith: action) } - coordinator.start() - add(childCoordinator: coordinator) + serverSelectionNavigationController.setRootCoordinator(coordinator) - let modalRouter = NavigationRouter(navigationController: ElementNavigationController()) - modalRouter.setRootModule(coordinator) - - navigationRouter.present(modalRouter, animated: true) + navigationController.presentSheet(userNotificationController) } /// Handles the result from the server selection modal, dismissing it after updating the view. private func serverSelectionCoordinator(_ coordinator: ServerSelectionCoordinator, didCompleteWith action: ServerSelectionCoordinatorAction) { - navigationRouter.dismissModule(animated: true) { [weak self] in - if action == .updated { - self?.updateViewModel() - } - - self?.remove(childCoordinator: coordinator) + if action == .updated { + updateViewModel() } + + navigationController.dismissSheet() } /// Shows the forgot password screen. private func showForgotPasswordScreen() { - loginViewModel.displayError(.alert("Not implemented.")) + viewModel.displayError(.alert("Not implemented.")) } } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/MockServerSelectionScreenState.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/MockServerSelectionScreenState.swift index 2198da6af..5221c2a81 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelection/MockServerSelectionScreenState.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/MockServerSelectionScreenState.swift @@ -27,18 +27,18 @@ enum MockServerSelectionScreenState: CaseIterable { switch self { case .matrix: return ServerSelectionViewModel(homeserverAddress: "https://matrix.org", - hasModalPresentation: true) + isModallyPresented: true) case .emptyAddress: return ServerSelectionViewModel(homeserverAddress: "", - hasModalPresentation: true) + isModallyPresented: true) case .invalidAddress: let viewModel = ServerSelectionViewModel(homeserverAddress: "thisisbad", - hasModalPresentation: true) + isModallyPresented: true) viewModel.displayError(.footerMessage(ElementL10n.unknownError)) return viewModel case .nonModal: return ServerSelectionViewModel(homeserverAddress: "https://matrix.org", - hasModalPresentation: false) + isModallyPresented: false) } } } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift index aeac36595..2efc96497 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift @@ -19,8 +19,9 @@ import SwiftUI struct ServerSelectionCoordinatorParameters { /// The service used to authenticate the user. let authenticationService: AuthenticationServiceProxyProtocol + let userNotificationController: UserNotificationControllerProtocol /// Whether the screen is presented modally or within a navigation stack. - let hasModalPresentation: Bool + let isModallyPresented: Bool } enum ServerSelectionCoordinatorAction { @@ -28,45 +29,25 @@ enum ServerSelectionCoordinatorAction { case dismiss } -final class ServerSelectionCoordinator: Coordinator, Presentable { - // MARK: - Properties - - // MARK: Private - +final class ServerSelectionCoordinator: CoordinatorProtocol { private let parameters: ServerSelectionCoordinatorParameters - private let serverSelectionHostingController: UIViewController - private var serverSelectionViewModel: ServerSelectionViewModelProtocol - + private let userNotificationController: UserNotificationControllerProtocol + private var viewModel: ServerSelectionViewModelProtocol private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService } - private var indicatorPresenter: UserIndicatorTypePresenterProtocol - private var loadingIndicator: UserIndicator? - - // MARK: Public - // Must be used only internally - var childCoordinators: [Coordinator] = [] var callback: (@MainActor (ServerSelectionCoordinatorAction) -> Void)? - // MARK: - Setup - init(parameters: ServerSelectionCoordinatorParameters) { self.parameters = parameters - - let viewModel = ServerSelectionViewModel(homeserverAddress: parameters.authenticationService.homeserver.address, - hasModalPresentation: parameters.hasModalPresentation) - let view = ServerSelectionScreen(context: viewModel.context) - serverSelectionViewModel = viewModel - serverSelectionHostingController = UIHostingController(rootView: view) - - indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: serverSelectionHostingController) + viewModel = ServerSelectionViewModel(homeserverAddress: parameters.authenticationService.homeserver.address, + isModallyPresented: parameters.isModallyPresented) + userNotificationController = parameters.userNotificationController } // MARK: - Public func start() { - MXLog.debug("Did start.") - - serverSelectionViewModel.callback = { [weak self] action in + viewModel.callback = { [weak self] action in guard let self else { return } MXLog.debug("ServerSelectionViewModel did callback with action: \(action).") @@ -79,27 +60,24 @@ final class ServerSelectionCoordinator: Coordinator, Presentable { } } - func toPresentable() -> UIViewController { - serverSelectionHostingController - } - func stop() { stopLoading() } - // MARK: - Private - - /// Show an activity indicator whilst loading. - /// - Parameters: - /// - label: The label to show on the indicator. - /// - isInteractionBlocking: Whether the indicator should block any user interaction. - private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) { - loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking)) + func toPresentable() -> AnyView { + AnyView(ServerSelectionScreen(context: viewModel.context)) + } + + // MARK: - Private + + private func startLoading(label: String = ElementL10n.loading) { + userNotificationController.submitNotification(UserNotification(type: .modal, + title: label, + persistent: true)) } - /// Hide the currently displayed activity indicator. private func stopLoading() { - loadingIndicator = nil + userNotificationController.retractAllNotifications() } /// Updates the login flow using the supplied homeserver address, or shows an error when this isn't possible. @@ -122,9 +100,9 @@ final class ServerSelectionCoordinator: Coordinator, Presentable { private func handleError(_ error: AuthenticationServiceError) { switch error { case .invalidServer, .invalidHomeserverAddress: - serverSelectionViewModel.displayError(.footerMessage(ElementL10n.loginErrorHomeserverNotFound)) + viewModel.displayError(.footerMessage(ElementL10n.loginErrorHomeserverNotFound)) default: - serverSelectionViewModel.displayError(.footerMessage(ElementL10n.unknownError)) + viewModel.displayError(.footerMessage(ElementL10n.unknownError)) } } } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionModels.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionModels.swift index 6d3ef5268..1c3c9f8e4 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionModels.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionModels.swift @@ -33,7 +33,7 @@ struct ServerSelectionViewState: BindableState { /// An error message to be shown in the text field footer. var footerErrorMessage: String? /// Whether the screen is presented modally or within a navigation stack. - var hasModalPresentation: Bool + var isModallyPresented: Bool /// The message to show in the text field footer. var footerMessage: String { @@ -42,7 +42,7 @@ struct ServerSelectionViewState: BindableState { /// The title shown on the confirm button. var buttonTitle: String { - hasModalPresentation ? ElementL10n.actionConfirm : ElementL10n.actionNext + isModallyPresented ? ElementL10n.actionConfirm : ElementL10n.actionNext } /// The text field is showing an error. diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModel.swift index cba6929e8..2c9cc7d6a 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModel.swift @@ -28,13 +28,13 @@ class ServerSelectionViewModel: ServerSelectionViewModelType, ServerSelectionVie var callback: (@MainActor (ServerSelectionViewModelAction) -> Void)? // MARK: - Setup - - init(homeserverAddress: String, hasModalPresentation: Bool) { + + init(homeserverAddress: String, isModallyPresented: Bool) { let bindings = ServerSelectionBindings(homeserverAddress: homeserverAddress) super.init(initialViewState: ServerSelectionViewState(bindings: bindings, - hasModalPresentation: hasModalPresentation)) + isModallyPresented: isModallyPresented)) } - + // MARK: - Public override func process(viewAction: ServerSelectionViewAction) async { diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/View/ServerSelectionScreen.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/View/ServerSelectionScreen.swift index 07d24dc3c..d3e2e8fec 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelection/View/ServerSelectionScreen.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/View/ServerSelectionScreen.swift @@ -44,6 +44,7 @@ struct ServerSelectionScreen: View { .background(Color.element.background, ignoresSafeAreaEdges: .all) .toolbar { toolbar } .alert(item: $context.alertInfo) { $0.alert } + .interactiveDismissDisabled() } /// The title, message and icon at the top of the screen. @@ -91,7 +92,7 @@ struct ServerSelectionScreen: View { @ToolbarContentBuilder var toolbar: some ToolbarContent { ToolbarItem(placement: .cancellationAction) { - if context.viewState.hasModalPresentation { + if context.viewState.isModallyPresented { Button { context.send(viewAction: .dismiss) } label: { Text(ElementL10n.actionCancel) } diff --git a/ElementX/Sources/Screens/Authentication/SoftLogout/SoftLogoutCoordinator.swift b/ElementX/Sources/Screens/Authentication/SoftLogout/SoftLogoutCoordinator.swift index 05fa745dd..889cae7a3 100644 --- a/ElementX/Sources/Screens/Authentication/SoftLogout/SoftLogoutCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/SoftLogout/SoftLogoutCoordinator.swift @@ -40,55 +40,35 @@ enum SoftLogoutCoordinatorResult: CustomStringConvertible { } } -final class SoftLogoutCoordinator: Coordinator, Presentable { - // MARK: - Properties - - // MARK: Private - +final class SoftLogoutCoordinator: CoordinatorProtocol { private let parameters: SoftLogoutCoordinatorParameters - private let softLogoutHostingController: UIViewController - private var softLogoutViewModel: SoftLogoutViewModelProtocol + private var viewModel: SoftLogoutViewModelProtocol + private let hostingController: UIViewController /// Passed to the OIDC service to provide a view controller from which to present the authentication session. private let oidcUserAgent: OIDExternalUserAgentIOS? - private var indicatorPresenter: UserIndicatorTypePresenterProtocol - private var loadingIndicator: UserIndicator? - private var successIndicator: UserIndicator? - /// The wizard used to handle the registration flow. private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService } - - // MARK: Public - // Must be used only internally - var childCoordinators: [Coordinator] = [] var callback: (@MainActor (SoftLogoutCoordinatorResult) -> Void)? - // MARK: - Setup - @MainActor init(parameters: SoftLogoutCoordinatorParameters) { self.parameters = parameters let homeserver = parameters.authenticationService.homeserver - let viewModel = SoftLogoutViewModel(credentials: parameters.credentials, - homeserver: homeserver, - keyBackupNeeded: parameters.keyBackupNeeded) - softLogoutViewModel = viewModel - - let view = SoftLogoutScreen(context: viewModel.context) - softLogoutHostingController = UIHostingController(rootView: view) + viewModel = SoftLogoutViewModel(credentials: parameters.credentials, + homeserver: homeserver, + keyBackupNeeded: parameters.keyBackupNeeded) - indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: softLogoutHostingController) - oidcUserAgent = OIDExternalUserAgentIOS(presenting: softLogoutHostingController) + hostingController = UIHostingController(rootView: SoftLogoutScreen(context: viewModel.context)) + oidcUserAgent = OIDExternalUserAgentIOS(presenting: hostingController) } // MARK: - Public func start() { - MXLog.debug("[SoftLogoutCoordinator] did start.") - - softLogoutViewModel.callback = { [weak self] result in + viewModel.callback = { [weak self] result in guard let self else { return } MXLog.debug("[SoftLogoutCoordinator] SoftLogoutViewModel did complete with result: \(result).") @@ -105,31 +85,36 @@ final class SoftLogoutCoordinator: Coordinator, Presentable { } } - func toPresentable() -> UIViewController { - softLogoutHostingController - } - func stop() { stopLoading() } + func toPresentable() -> AnyView { + AnyView(SoftLogoutScreen(context: viewModel.context)) + } + // MARK: - Private + static let loadingIndicatorIdentifier = "SoftLogoutLoading" + /// Show an activity indicator whilst loading. @MainActor private func startLoading() { - loadingIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true)) + ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(id: Self.loadingIndicatorIdentifier, + type: .modal, + title: ElementL10n.loading, + persistent: true)) } /// Hide the currently displayed activity indicator. @MainActor private func stopLoading() { - loadingIndicator = nil + ServiceLocator.shared.userNotificationController.retractNotificationWithId(Self.loadingIndicatorIdentifier) } /// Shows the forgot password screen. @MainActor private func showForgotPasswordScreen() { MXLog.debug("[SoftLogoutCoordinator] showForgotPasswordScreen") - softLogoutViewModel.displayError(.alert("Not implemented.")) + viewModel.displayError(.alert("Not implemented.")) } /// Login with the supplied username and password. @@ -177,11 +162,11 @@ final class SoftLogoutCoordinator: Coordinator, Presentable { private func handleError(_ error: AuthenticationServiceError) { switch error { case .invalidCredentials: - softLogoutViewModel.displayError(.alert(ElementL10n.authInvalidLoginParam)) + viewModel.displayError(.alert(ElementL10n.authInvalidLoginParam)) case .accountDeactivated: - softLogoutViewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount)) + viewModel.displayError(.alert(ElementL10n.authInvalidLoginDeactivatedAccount)) default: - softLogoutViewModel.displayError(.alert(ElementL10n.unknownError)) + viewModel.displayError(.alert(ElementL10n.unknownError)) } } } diff --git a/ElementX/Sources/Screens/BugReport/BugReportCoordinator.swift b/ElementX/Sources/Screens/BugReport/BugReportCoordinator.swift index 2ce56a880..fa2e2bff6 100644 --- a/ElementX/Sources/Screens/BugReport/BugReportCoordinator.swift +++ b/ElementX/Sources/Screens/BugReport/BugReportCoordinator.swift @@ -16,90 +16,77 @@ import SwiftUI -struct BugReportCoordinatorParameters { - let bugReportService: BugReportServiceProtocol - let screenshot: UIImage? +enum BugReportCoordinatorResult { + case cancel + case finish } -final class BugReportCoordinator: Coordinator, Presentable { - // MARK: - Properties - - // MARK: Private - - private let parameters: BugReportCoordinatorParameters - private let bugReportHostingController: UIViewController - private var bugReportViewModel: BugReportViewModelProtocol - - private var indicatorPresenter: UserIndicatorTypePresenterProtocol - private var loadingIndicator: UserIndicator? - private var statusIndicator: UserIndicator? - - // MARK: Public +struct BugReportCoordinatorParameters { + let bugReportService: BugReportServiceProtocol + let userNotificationController: UserNotificationControllerProtocol + let screenshot: UIImage? + let isModallyPresented: Bool +} - // Must be used only internally - var childCoordinators: [Coordinator] = [] - var completion: (() -> Void)? - - // MARK: - Setup +final class BugReportCoordinator: CoordinatorProtocol { + private let parameters: BugReportCoordinatorParameters + private var viewModel: BugReportViewModelProtocol + + var completion: ((BugReportCoordinatorResult) -> Void)? init(parameters: BugReportCoordinatorParameters) { self.parameters = parameters - let viewModel = BugReportViewModel(bugReportService: parameters.bugReportService, - screenshot: parameters.screenshot) - let view = BugReportScreen(context: viewModel.context) - bugReportViewModel = viewModel - bugReportHostingController = UIHostingController(rootView: view) - - indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: bugReportHostingController) + viewModel = BugReportViewModel(bugReportService: parameters.bugReportService, + screenshot: parameters.screenshot, + isModallyPresented: parameters.isModallyPresented) } // MARK: - Public func start() { - MXLog.debug("Did start.") - bugReportViewModel.callback = { [weak self] result in + viewModel.callback = { [weak self] result in guard let self else { return } MXLog.debug("BugReportViewModel did complete with result: \(result).") switch result { + case .cancel: + self.completion?(.cancel) case .submitStarted: self.startLoading() case .submitFinished: self.stopLoading() - self.completion?() + self.completion?(.finish) case .submitFailed(let error): self.stopLoading() self.showError(label: error.localizedDescription) } } } - - func toPresentable() -> UIViewController { - bugReportHostingController - } func stop() { stopLoading() } + func toPresentable() -> AnyView { + AnyView(BugReportScreen(context: viewModel.context)) + } + // MARK: - Private - /// Show an activity indicator whilst loading. - /// - Parameters: - /// - label: The label to show on the indicator. - /// - isInteractionBlocking: Whether the indicator should block any user interaction. - private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) { - loadingIndicator = indicatorPresenter.present(.loading(label: label, - isInteractionBlocking: isInteractionBlocking)) + static let loadingIndicatorIdentifier = "BugReportLoading" + + private func startLoading(label: String = ElementL10n.loading) { + parameters.userNotificationController.submitNotification(UserNotification(id: Self.loadingIndicatorIdentifier, + type: .modal, + title: label, + persistent: true)) } - /// Hide the currently displayed activity indicator. private func stopLoading() { - loadingIndicator = nil + parameters.userNotificationController.retractNotificationWithId(Self.loadingIndicatorIdentifier) } - - /// Show error indicator + private func showError(label: String) { - statusIndicator = indicatorPresenter.present(.error(label: label)) + parameters.userNotificationController.submitNotification(UserNotification(title: label)) } } diff --git a/ElementX/Sources/Screens/BugReport/BugReportModels.swift b/ElementX/Sources/Screens/BugReport/BugReportModels.swift index b68aaebf4..0477ba227 100644 --- a/ElementX/Sources/Screens/BugReport/BugReportModels.swift +++ b/ElementX/Sources/Screens/BugReport/BugReportModels.swift @@ -22,6 +22,7 @@ import UIKit // MARK: View model enum BugReportViewModelAction { + case cancel case submitStarted case submitFinished case submitFailed(error: Error) @@ -32,6 +33,7 @@ enum BugReportViewModelAction { struct BugReportViewState: BindableState { var screenshot: UIImage? var bindings: BugReportViewStateBindings + let isModallyPresented: Bool } struct BugReportViewStateBindings { @@ -40,6 +42,7 @@ struct BugReportViewStateBindings { } enum BugReportViewAction { + case cancel case submit case toggleSendLogs case removeScreenshot diff --git a/ElementX/Sources/Screens/BugReport/BugReportViewModel.swift b/ElementX/Sources/Screens/BugReport/BugReportViewModel.swift index 883965796..e2e05d9b0 100644 --- a/ElementX/Sources/Screens/BugReport/BugReportViewModel.swift +++ b/ElementX/Sources/Screens/BugReport/BugReportViewModel.swift @@ -16,18 +16,41 @@ import SwiftUI -@available(iOS 14, *) -typealias BugReportViewModelType = StateStoreViewModel -@available(iOS 14, *) -class BugReportViewModel: BugReportViewModelType, BugReportViewModelProtocol { - // MARK: - Properties +typealias BugReportViewModelType = StateStoreViewModel +class BugReportViewModel: BugReportViewModelType, BugReportViewModelProtocol { let bugReportService: BugReportServiceProtocol + var callback: ((BugReportViewModelAction) -> Void)? + + init(bugReportService: BugReportServiceProtocol, + screenshot: UIImage?, + isModallyPresented: Bool) { + self.bugReportService = bugReportService + let bindings = BugReportViewStateBindings(reportText: "", sendingLogsEnabled: true) + super.init(initialViewState: BugReportViewState(screenshot: screenshot, + bindings: bindings, + isModallyPresented: isModallyPresented)) + } + + // MARK: - Public + + override func process(viewAction: BugReportViewAction) async { + switch viewAction { + case .cancel: + callback?(.cancel) + case .submit: + await submitBugReport() + case .toggleSendLogs: + context.sendingLogsEnabled.toggle() + case .removeScreenshot: + state.screenshot = nil + } + } + // MARK: Private - func submitBugReport() async { + private func submitBugReport() async { callback?(.submitStarted) do { var files: [URL] = [] @@ -54,31 +77,4 @@ class BugReportViewModel: BugReportViewModelType, BugReportViewModelProtocol { callback?(.submitFailed(error: error)) } } - - // MARK: Public - - var callback: ((BugReportViewModelAction) -> Void)? - - // MARK: - Setup - - init(bugReportService: BugReportServiceProtocol, - screenshot: UIImage?) { - self.bugReportService = bugReportService - let bindings = BugReportViewStateBindings(reportText: "", sendingLogsEnabled: true) - super.init(initialViewState: BugReportViewState(screenshot: screenshot, - bindings: bindings)) - } - - // MARK: - Public - - override func process(viewAction: BugReportViewAction) async { - switch viewAction { - case .submit: - await submitBugReport() - case .toggleSendLogs: - context.sendingLogsEnabled.toggle() - case .removeScreenshot: - state.screenshot = nil - } - } } diff --git a/ElementX/Sources/Screens/BugReport/BugReportViewModelProtocol.swift b/ElementX/Sources/Screens/BugReport/BugReportViewModelProtocol.swift index ea6d5964b..3910d6cfb 100644 --- a/ElementX/Sources/Screens/BugReport/BugReportViewModelProtocol.swift +++ b/ElementX/Sources/Screens/BugReport/BugReportViewModelProtocol.swift @@ -19,6 +19,5 @@ import Foundation @MainActor protocol BugReportViewModelProtocol { var callback: ((BugReportViewModelAction) -> Void)? { get set } - @available(iOS 14, *) var context: BugReportViewModelType.Context { get } } diff --git a/ElementX/Sources/Screens/BugReport/View/BugReportScreen.swift b/ElementX/Sources/Screens/BugReport/View/BugReportScreen.swift index 3be02fac6..b1462636d 100644 --- a/ElementX/Sources/Screens/BugReport/View/BugReportScreen.swift +++ b/ElementX/Sources/Screens/BugReport/View/BugReportScreen.swift @@ -48,6 +48,16 @@ struct BugReportScreen: View { .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16) } .navigationTitle(ElementL10n.titleActivityBugReport) + .toolbar { + if context.viewState.isModallyPresented { + ToolbarItem(placement: .cancellationAction) { + Button(ElementL10n.actionCancel) { + context.send(viewAction: .cancel) + } + } + } + } + .interactiveDismissDisabled() } } @@ -136,7 +146,7 @@ struct BugReport_Previews: PreviewProvider { @ViewBuilder static var body: some View { - let viewModel = BugReportViewModel(bugReportService: MockBugReportService(), screenshot: Asset.Images.appLogo.image) + let viewModel = BugReportViewModel(bugReportService: MockBugReportService(), screenshot: Asset.Images.appLogo.image, isModallyPresented: false) BugReportScreen(context: viewModel.context) .previewInterfaceOrientation(.portrait) } diff --git a/ElementX/Sources/Screens/FilePreview/FilePreviewCoordinator.swift b/ElementX/Sources/Screens/FilePreview/FilePreviewCoordinator.swift index cc8f053d1..f7f621a7b 100644 --- a/ElementX/Sources/Screens/FilePreview/FilePreviewCoordinator.swift +++ b/ElementX/Sources/Screens/FilePreview/FilePreviewCoordinator.swift @@ -25,42 +25,22 @@ enum FilePreviewCoordinatorAction { case cancel } -final class FilePreviewCoordinator: Coordinator, Presentable { - // MARK: - Properties - - // MARK: Private - +final class FilePreviewCoordinator: CoordinatorProtocol { private let parameters: FilePreviewCoordinatorParameters - private let filePreviewHostingController: UIViewController - private var filePreviewViewModel: FilePreviewViewModelProtocol - - private var indicatorPresenter: UserIndicatorTypePresenterProtocol - private var activityIndicator: UserIndicator? - - // MARK: Public + private var viewModel: FilePreviewViewModelProtocol - // Must be used only internally - var childCoordinators: [Coordinator] = [] var callback: ((FilePreviewCoordinatorAction) -> Void)? - // MARK: - Setup - init(parameters: FilePreviewCoordinatorParameters) { self.parameters = parameters - let viewModel = FilePreviewViewModel(fileURL: parameters.fileURL, title: parameters.title) - let view = FilePreviewScreen(context: viewModel.context) - filePreviewViewModel = viewModel - filePreviewHostingController = UIHostingController(rootView: view) - - indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: filePreviewHostingController) + viewModel = FilePreviewViewModel(fileURL: parameters.fileURL, title: parameters.title) } // MARK: - Public func start() { - MXLog.debug("Did start.") - filePreviewViewModel.callback = { [weak self] action in + viewModel.callback = { [weak self] action in guard let self else { return } MXLog.debug("FilePreviewViewModel did complete with result: \(action).") switch action { @@ -70,26 +50,7 @@ final class FilePreviewCoordinator: Coordinator, Presentable { } } - func toPresentable() -> UIViewController { - filePreviewHostingController - } - - func stop() { - stopLoading() - } - - // MARK: - Private - - /// Show an activity indicator whilst loading. - /// - Parameters: - /// - label: The label to show on the indicator. - /// - isInteractionBlocking: Whether the indicator should block any user interaction. - private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) { - activityIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking)) - } - - /// Hide the currently displayed activity indicator. - private func stopLoading() { - activityIndicator = nil + func toPresentable() -> AnyView { + AnyView(FilePreviewScreen(context: viewModel.context)) } } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift index 0ba64460f..d65a92567 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift @@ -20,6 +20,8 @@ import SwiftUI struct HomeScreenCoordinatorParameters { let userSession: UserSessionProtocol let attributedStringBuilder: AttributedStringBuilderProtocol + let bugReportService: BugReportServiceProtocol + let navigationController: NavigationController } enum HomeScreenCoordinatorAction { @@ -30,34 +32,20 @@ enum HomeScreenCoordinatorAction { case signOut } -final class HomeScreenCoordinator: Coordinator, Presentable { - // MARK: - Properties - - // MARK: Private - +final class HomeScreenCoordinator: CoordinatorProtocol { private let parameters: HomeScreenCoordinatorParameters - private let hostingController: UIViewController private var viewModel: HomeScreenViewModelProtocol private var cancellables = Set() - // MARK: Public - - // Must be used only internally - var childCoordinators: [Coordinator] = [] var callback: ((HomeScreenCoordinatorAction) -> Void)? - // MARK: - Setup - init(parameters: HomeScreenCoordinatorParameters) { self.parameters = parameters viewModel = HomeScreenViewModel(userSession: parameters.userSession, attributedStringBuilder: parameters.attributedStringBuilder) - let view = HomeScreen(context: viewModel.context) - hostingController = UIHostingController(rootView: view) - viewModel.callback = { [weak self] action in guard let self else { return } @@ -74,13 +62,21 @@ final class HomeScreenCoordinator: Coordinator, Presentable { // MARK: - Public - func start() { } - - func toPresentable() -> UIViewController { - hostingController + func start() { + if parameters.bugReportService.crashedLastRun { + viewModel.presentAlert( + AlertInfo(id: UUID(), + title: ElementL10n.sendBugReportAppCrashed, + primaryButton: .init(title: ElementL10n.no, action: nil), + secondaryButton: .init(title: ElementL10n.yes) { [weak self] in + self?.callback?(.presentFeedbackScreen) + })) + } + } + + func toPresentable() -> AnyView { + AnyView(HomeScreen(context: viewModel.context)) } - - func stop() { } // MARK: - Private @@ -98,11 +94,6 @@ final class HomeScreenCoordinator: Coordinator, Presentable { } private func presentInviteFriends() { - guard let permalink = try? PermalinkBuilder.permalinkTo(userIdentifier: parameters.userSession.userID).absoluteString else { - return - } - let shareText = ElementL10n.inviteFriendsText(ElementInfoPlist.cfBundleDisplayName, permalink) - let vc = UIActivityViewController(activityItems: [shareText], applicationActivities: nil) - hostingController.present(vc, animated: true) + parameters.navigationController.presentSheet(InviteFriendsCoordinator(userId: parameters.userSession.userID)) } } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index 00d45459e..0e498c434 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -52,7 +52,7 @@ struct HomeScreenViewState: BindableState { var rooms: [HomeScreenRoom] = [] - var roomListMode: HomeScreenRoomListMode = .rooms + var roomListMode: HomeScreenRoomListMode = .skeletons var visibleRooms: [HomeScreenRoom] { if roomListMode == .skeletons { @@ -77,6 +77,8 @@ struct HomeScreenViewState: BindableState { struct HomeScreenViewStateBindings { var searchQuery = "" + + var alertInfo: AlertInfo? } struct HomeScreenRoom: Identifiable, Equatable { @@ -92,15 +94,12 @@ struct HomeScreenRoom: Identifiable, Equatable { var avatar: UIImage? - var isPlaceholder = false - static func placeholder(id: String) -> HomeScreenRoom { HomeScreenRoom(id: id, name: "Placeholder room name", hasUnreads: false, timestamp: "Now", lastMessage: AttributedString("Last message"), - avatar: UIImage(systemName: "photo"), - isPlaceholder: true) + avatar: UIImage(systemName: "photo")) } } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 57e1776d7..2de3baeb7 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -111,6 +111,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol } } + func presentAlert(_ alertInfo: AlertInfo) { + state.bindings.alertInfo = alertInfo + } + // MARK: - Private private func loadDataForRoomIdentifier(_ identifier: String) { diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModelProtocol.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModelProtocol.swift index f8ea2a024..54d15d17b 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModelProtocol.swift @@ -22,4 +22,6 @@ protocol HomeScreenViewModelProtocol { var callback: ((HomeScreenViewModelAction) -> Void)? { get set } var context: HomeScreenViewModelType.Context { get } + + func presentAlert(_ alert: AlertInfo) } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index 866dee679..ab73c6701 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -28,23 +28,30 @@ struct HomeScreen: View { sessionVerificationBanner } - LazyVStack { - ForEach(context.viewState.visibleRooms) { room in - if room.isPlaceholder { + if context.viewState.roomListMode == .skeletons { + LazyVStack { + ForEach(context.viewState.visibleRooms) { room in HomeScreenRoomCell(room: room, context: context) .redacted(reason: .placeholder) .disabled(true) - } else { + } + } + .padding(.horizontal) + } else { + LazyVStack { + ForEach(context.viewState.visibleRooms) { room in HomeScreenRoomCell(room: room, context: context) } } + .padding(.horizontal) + .searchable(text: $context.searchQuery) } - .padding(.horizontal) - .searchable(text: $context.searchQuery) } .disabled(context.viewState.roomListMode == .skeletons) .animation(.elementDefault, value: context.viewState.showSessionVerificationBanner) + .animation(.elementDefault, value: context.viewState.roomListMode) .ignoresSafeArea(.all, edges: .bottom) + .alert(item: $context.alertInfo) { $0.alert } .navigationTitle(ElementL10n.allChats) .toolbar { ToolbarItem(placement: .navigationBarLeading) { diff --git a/ElementX/Sources/Screens/MediaPlayer/MediaPlayerCoordinator.swift b/ElementX/Sources/Screens/MediaPlayer/MediaPlayerCoordinator.swift index 425ae9443..5ed6b3275 100644 --- a/ElementX/Sources/Screens/MediaPlayer/MediaPlayerCoordinator.swift +++ b/ElementX/Sources/Screens/MediaPlayer/MediaPlayerCoordinator.swift @@ -24,42 +24,23 @@ enum MediaPlayerCoordinatorAction { case cancel } -final class MediaPlayerCoordinator: Coordinator, Presentable { - // MARK: - Properties - - // MARK: Private - +final class MediaPlayerCoordinator: CoordinatorProtocol { private let parameters: MediaPlayerCoordinatorParameters - private let mediaPlayerHostingController: UIViewController - private var mediaPlayerViewModel: MediaPlayerViewModelProtocol + private var viewModel: MediaPlayerViewModelProtocol - private var indicatorPresenter: UserIndicatorTypePresenterProtocol - private var activityIndicator: UserIndicator? - - // MARK: Public - - // Must be used only internally - var childCoordinators: [Coordinator] = [] var callback: ((MediaPlayerCoordinatorAction) -> Void)? - // MARK: - Setup - init(parameters: MediaPlayerCoordinatorParameters) { self.parameters = parameters - let viewModel = MediaPlayerViewModel(mediaURL: parameters.mediaURL) - let view = MediaPlayerScreen(context: viewModel.context) - mediaPlayerViewModel = viewModel - mediaPlayerHostingController = UIHostingController(rootView: view) - - indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: mediaPlayerHostingController) + viewModel = MediaPlayerViewModel(mediaURL: parameters.mediaURL) } // MARK: - Public func start() { MXLog.debug("Did start.") - mediaPlayerViewModel.callback = { [weak self] action in + viewModel.callback = { [weak self] action in guard let self else { return } MXLog.debug("MediaPlayerViewModel did complete with result: \(action).") switch action { @@ -69,26 +50,7 @@ final class MediaPlayerCoordinator: Coordinator, Presentable { } } - func toPresentable() -> UIViewController { - mediaPlayerHostingController - } - - func stop() { - stopLoading() - } - - // MARK: - Private - - /// Show an activity indicator whilst loading. - /// - Parameters: - /// - label: The label to show on the indicator. - /// - isInteractionBlocking: Whether the indicator should block any user interaction. - private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) { - activityIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking)) - } - - /// Hide the currently displayed activity indicator. - private func stopLoading() { - activityIndicator = nil + func toPresentable() -> AnyView { + AnyView(MediaPlayerScreen(context: viewModel.context)) } } diff --git a/ElementX/Sources/Screens/OnboardingScreen/OnboardingCoordinator.swift b/ElementX/Sources/Screens/OnboardingScreen/OnboardingCoordinator.swift new file mode 100644 index 000000000..8af7dddfa --- /dev/null +++ b/ElementX/Sources/Screens/OnboardingScreen/OnboardingCoordinator.swift @@ -0,0 +1,44 @@ +// +// Copyright 2022 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 SwiftUI + +final class OnboardingCoordinator: CoordinatorProtocol { + private var viewModel: OnboardingViewModelProtocol + + var callback: ((OnboardingCoordinatorAction) -> Void)? + + init() { + viewModel = OnboardingViewModel() + } + + // MARK: - Public + + func start() { + viewModel.callback = { [weak self] action in + MXLog.debug("OnboardingViewModel did complete with result: \(action).") + guard let self else { return } + switch action { + case .login: + self.callback?(.login) + } + } + } + + func toPresentable() -> AnyView { + AnyView(OnboardingScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/SplashScreen/SplashScreenModels.swift b/ElementX/Sources/Screens/OnboardingScreen/OnboardingModels.swift similarity index 60% rename from ElementX/Sources/Screens/SplashScreen/SplashScreenModels.swift rename to ElementX/Sources/Screens/OnboardingScreen/OnboardingModels.swift index 8ad831804..65c4ef8a3 100644 --- a/ElementX/Sources/Screens/SplashScreen/SplashScreenModels.swift +++ b/ElementX/Sources/Screens/OnboardingScreen/OnboardingModels.swift @@ -18,12 +18,12 @@ import SwiftUI // MARK: - Coordinator -enum SplashScreenCoordinatorAction { +enum OnboardingCoordinatorAction { case login } -/// The content displayed in a single splash screen page. -struct SplashScreenPageContent { +/// The content displayed in a single screen page. +struct OnboardingPageContent { let title: AttributedString let message: String let image: ImageAsset @@ -31,13 +31,13 @@ struct SplashScreenPageContent { // MARK: View model -enum SplashScreenViewModelAction { +enum OnboardingViewModelAction { case login } // MARK: View -struct SplashScreenViewState: BindableState { +struct OnboardingViewState: BindableState { /// The colours of the background gradient shown behind the 4 pages. private let gradientColors = [ Color(red: 0.95, green: 0.98, blue: 0.96), @@ -48,8 +48,8 @@ struct SplashScreenViewState: BindableState { ] /// An array containing all content of the carousel pages - let content: [SplashScreenPageContent] - var bindings: SplashScreenBindings + let content: [OnboardingPageContent] + var bindings: OnboardingBindings /// The background gradient for all 4 pages and the hidden page at the start of the carousel. var backgroundGradient: Gradient { @@ -69,27 +69,27 @@ struct SplashScreenViewState: BindableState { let page4Title = locale.identifier.hasPrefix("en") ? "Cut the slack from teams." : ElementL10n.ftueAuthCarouselWorkplaceTitle content = [ - SplashScreenPageContent(title: ElementL10n.ftueAuthCarouselSecureTitle.tinting("."), - message: ElementL10n.ftueAuthCarouselSecureBody, - image: Asset.Images.splashScreenPage1), - SplashScreenPageContent(title: ElementL10n.ftueAuthCarouselControlTitle.tinting("."), - message: ElementL10n.ftueAuthCarouselControlBody, - image: Asset.Images.splashScreenPage2), - SplashScreenPageContent(title: ElementL10n.ftueAuthCarouselEncryptedTitle.tinting("."), - message: ElementL10n.ftueAuthCarouselEncryptedBody, - image: Asset.Images.splashScreenPage3), - SplashScreenPageContent(title: page4Title.tinting("."), - message: ElementL10n.ftueAuthCarouselWorkplaceBody(ElementInfoPlist.cfBundleDisplayName), - image: Asset.Images.splashScreenPage4) + OnboardingPageContent(title: ElementL10n.ftueAuthCarouselSecureTitle.tinting("."), + message: ElementL10n.ftueAuthCarouselSecureBody, + image: Asset.Images.onboardingScreenPage1), + OnboardingPageContent(title: ElementL10n.ftueAuthCarouselControlTitle.tinting("."), + message: ElementL10n.ftueAuthCarouselControlBody, + image: Asset.Images.onboardingScreenPage2), + OnboardingPageContent(title: ElementL10n.ftueAuthCarouselEncryptedTitle.tinting("."), + message: ElementL10n.ftueAuthCarouselEncryptedBody, + image: Asset.Images.onboardingScreenPage3), + OnboardingPageContent(title: page4Title.tinting("."), + message: ElementL10n.ftueAuthCarouselWorkplaceBody(ElementInfoPlist.cfBundleDisplayName), + image: Asset.Images.onboardingScreenPage4) ] - bindings = SplashScreenBindings() + bindings = OnboardingBindings() } } -struct SplashScreenBindings { +struct OnboardingBindings { var pageIndex = 0 } -enum SplashScreenViewAction { +enum OnboardingViewAction { case login } diff --git a/ElementX/Sources/Screens/SplashScreen/SplashScreenViewModel.swift b/ElementX/Sources/Screens/OnboardingScreen/OnboardingViewModel.swift similarity index 69% rename from ElementX/Sources/Screens/SplashScreen/SplashScreenViewModel.swift rename to ElementX/Sources/Screens/OnboardingScreen/OnboardingViewModel.swift index f0abe5ab7..55812dec5 100644 --- a/ElementX/Sources/Screens/SplashScreen/SplashScreenViewModel.swift +++ b/ElementX/Sources/Screens/OnboardingScreen/OnboardingViewModel.swift @@ -17,26 +17,26 @@ import Combine import SwiftUI -typealias SplashScreenViewModelType = StateStoreViewModel +typealias OnboardingViewModelType = StateStoreViewModel -class SplashScreenViewModel: SplashScreenViewModelType, SplashScreenViewModelProtocol { +class OnboardingViewModel: OnboardingViewModelType, OnboardingViewModelProtocol { // MARK: - Properties // MARK: Private // MARK: Public - var callback: ((SplashScreenViewModelAction) -> Void)? + var callback: ((OnboardingViewModelAction) -> Void)? // MARK: - Setup init() { - super.init(initialViewState: SplashScreenViewState()) + super.init(initialViewState: OnboardingViewState()) } // MARK: - Public - override func process(viewAction: SplashScreenViewAction) async { + override func process(viewAction: OnboardingViewAction) async { switch viewAction { case .login: callback?(.login) diff --git a/ElementX/Sources/Screens/SplashScreen/SplashScreenViewModelProtocol.swift b/ElementX/Sources/Screens/OnboardingScreen/OnboardingViewModelProtocol.swift similarity index 78% rename from ElementX/Sources/Screens/SplashScreen/SplashScreenViewModelProtocol.swift rename to ElementX/Sources/Screens/OnboardingScreen/OnboardingViewModelProtocol.swift index 64cc86f0d..a876a2156 100644 --- a/ElementX/Sources/Screens/SplashScreen/SplashScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/OnboardingScreen/OnboardingViewModelProtocol.swift @@ -17,7 +17,7 @@ import Foundation @MainActor -protocol SplashScreenViewModelProtocol { - var callback: ((SplashScreenViewModelAction) -> Void)? { get set } - var context: SplashScreenViewModelType.Context { get } +protocol OnboardingViewModelProtocol { + var callback: ((OnboardingViewModelAction) -> Void)? { get set } + var context: OnboardingViewModelType.Context { get } } diff --git a/ElementX/Sources/Screens/SplashScreen/View/SplashScreenPageIndicator.swift b/ElementX/Sources/Screens/OnboardingScreen/View/OnboardingPageIndicator.swift similarity index 97% rename from ElementX/Sources/Screens/SplashScreen/View/SplashScreenPageIndicator.swift rename to ElementX/Sources/Screens/OnboardingScreen/View/OnboardingPageIndicator.swift index 6f557ad70..522ea2582 100644 --- a/ElementX/Sources/Screens/SplashScreen/View/SplashScreenPageIndicator.swift +++ b/ElementX/Sources/Screens/OnboardingScreen/View/OnboardingPageIndicator.swift @@ -16,7 +16,7 @@ import SwiftUI -struct SplashScreenPageIndicator: View { +struct OnboardingPageIndicator: View { // MARK: - Properties // MARK: Public diff --git a/ElementX/Sources/Screens/SplashScreen/View/SplashScreenPageView.swift b/ElementX/Sources/Screens/OnboardingScreen/View/OnboardingPageView.swift similarity index 88% rename from ElementX/Sources/Screens/SplashScreen/View/SplashScreenPageView.swift rename to ElementX/Sources/Screens/OnboardingScreen/View/OnboardingPageView.swift index 890a50a5b..0c460baa7 100644 --- a/ElementX/Sources/Screens/SplashScreen/View/SplashScreenPageView.swift +++ b/ElementX/Sources/Screens/OnboardingScreen/View/OnboardingPageView.swift @@ -16,13 +16,13 @@ import SwiftUI -struct SplashScreenPageView: View { +struct OnboardingPageView: View { // MARK: - Properties // MARK: Public /// The content that this page should display. - let content: SplashScreenPageContent + let content: OnboardingPageContent // MARK: - Views @@ -54,11 +54,11 @@ struct SplashScreenPageView: View { } } -struct SplashScreenPage_Previews: PreviewProvider { - static let content = SplashScreenViewState().content +struct OnboardingPage_Previews: PreviewProvider { + static let content = OnboardingViewState().content static var previews: some View { ForEach(0..