From f2eaa8e80d827aa89febfc32ce6e0a56363df68c Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:29:51 +0200 Subject: [PATCH] Show the multi-account announcement on the HomeScreen (#6265) * Present the multi-account announcement when nothing else is showing The user session flow owns onboarding, verification and settings, so it presents the announcement in the tab sheet slot with the lowest priority: once the verification state is known, and whenever onboarding, settings or a verification sheet is dismissed. Routes and sign-out dismiss settings without triggering it, and it is marked as seen when presented. * Show the multi-account announcement on the home screen Following review, the announcement is a best-effort sheet on the home screen instead of a UserSessionFlowCoordinator state. It's presented when the room list appears with the flag on, the announcement armed and the session verified, and counts as seen once its content appears. - Restore UserSessionFlowCoordinator to develop. - HomeScreenViewModel presents the announcement through a binding and waits for a known verification state, so it never competes with the identity confirmation onboarding. - Add account only closes the sheet until the add account flow exists. - Rename MultiAccountAnnouncementScreen to MultiAccountAnnouncementView and drop its unused coordinator and view model protocol. - Size the sheet from the measured navigation bar height and match the Figma spacing; the hard-coded 44pt pushed the content under the button. * Present the multi-account announcement from the home screen view model The announcement no longer needs its own view model: the view takes the home screen's context and presents through a boolean binding, like the home screen banners. - Drop MultiAccountAnnouncementViewModel and its models. The view sends home screen actions to close or add an account and builds its own attributed subtitle. - Check the verification state when the room list appears instead of waiting for it. Restored sessions already know it by then, unverified ones get the identity confirmation cover first and the room list appears again once it's dismissed. Nothing is left subscribed, so the sheet can no longer turn up later wherever the user is. --- .../Sources/GeneratedAccessibilityTests.swift | 4 +- ElementX.xcodeproj/project.pbxproj | 40 +-------- .../TestablePreviewsDictionary.swift | 2 +- .../Screens/HomeScreen/HomeScreenModels.swift | 5 ++ .../HomeScreen/HomeScreenViewModel.swift | 23 +++++ .../Screens/HomeScreen/View/HomeScreen.swift | 5 ++ .../View/MultiAccountAnnouncementView.swift | 85 +++++++++++++++++++ ...AccountAnnouncementScreenCoordinator.swift | 47 ---------- ...MultiAccountAnnouncementScreenModels.swift | 31 ------- ...tiAccountAnnouncementScreenViewModel.swift | 33 ------- ...tAnnouncementScreenViewModelProtocol.swift | 13 --- .../View/MultiAccountAnnouncementScreen.swift | 71 ---------------- .../Sources/GeneratedPreviewTests.swift | 4 +- ...tAnnouncementScreen.Default-iPad-en-GB.png | 3 - ...AnnouncementScreen.Default-iPad-pseudo.png | 3 - ...nnouncementScreen.Default-iPhone-en-GB.png | 3 - ...nouncementScreen.Default-iPhone-pseudo.png | 3 - ...untAnnouncementView.Default-iPad-en-GB.png | 3 + ...ntAnnouncementView.Default-iPad-pseudo.png | 3 + ...tAnnouncementView.Default-iPhone-en-GB.png | 3 + ...AnnouncementView.Default-iPhone-pseudo.png | 3 + .../Sources/HomeScreenViewModelTests.swift | 82 ++++++++++++++++++ 22 files changed, 221 insertions(+), 248 deletions(-) create mode 100644 ElementX/Sources/Screens/HomeScreen/View/MultiAccountAnnouncementView.swift delete mode 100644 ElementX/Sources/Screens/MultiAccountAnnouncementScreen/MultiAccountAnnouncementScreenCoordinator.swift delete mode 100644 ElementX/Sources/Screens/MultiAccountAnnouncementScreen/MultiAccountAnnouncementScreenModels.swift delete mode 100644 ElementX/Sources/Screens/MultiAccountAnnouncementScreen/MultiAccountAnnouncementScreenViewModel.swift delete mode 100644 ElementX/Sources/Screens/MultiAccountAnnouncementScreen/MultiAccountAnnouncementScreenViewModelProtocol.swift delete mode 100644 ElementX/Sources/Screens/MultiAccountAnnouncementScreen/View/MultiAccountAnnouncementScreen.swift delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementScreen.Default-iPad-en-GB.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementScreen.Default-iPad-pseudo.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementScreen.Default-iPhone-en-GB.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementScreen.Default-iPhone-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementView.Default-iPad-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementView.Default-iPad-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementView.Default-iPhone-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementView.Default-iPhone-pseudo.png diff --git a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift index c15edc1c2..d5d5c2822 100644 --- a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift +++ b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift @@ -403,8 +403,8 @@ extension AccessibilityTests { try await performAccessibilityAudit(named: "ModerationAndSafetySettingsScreen_Previews") } - func testMultiAccountAnnouncementScreen() async throws { - try await performAccessibilityAudit(named: "MultiAccountAnnouncementScreen_Previews") + func testMultiAccountAnnouncementView() async throws { + try await performAccessibilityAudit(named: "MultiAccountAnnouncementView_Previews") } func testNoticeRoomTimelineView() async throws { diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 3584917d0..5046a393b 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -533,7 +533,6 @@ 52BE121FC21CE88803D2D8B5 /* GalleryItemTileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7927284C6F70D60BE5949F8C /* GalleryItemTileView.swift */; }; 5341D48F833E3E30F16FA2A3 /* SeparatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2910422CB628D3B2BBE47449 /* SeparatorRoomTimelineView.swift */; }; 5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; }; - 53A182049BEEC6195B91DD79 /* MultiAccountAnnouncementScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69630272FD26D4E5974CC88E /* MultiAccountAnnouncementScreenViewModelProtocol.swift */; }; 53A59720F4729D9BBFFB7CAB /* NotificationSettingsEditScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9CB3B9DFA353AB2B7CD9F8 /* NotificationSettingsEditScreenCoordinator.swift */; }; 53C1E7F6A7D6409D89F36ED7 /* AggregatedReactionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */; }; 53DEF39F0C4DE02E3FC56D91 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 800631D7250B7F93195035F1 /* KeychainAccess */; }; @@ -1045,7 +1044,6 @@ A0646F23876D6326AD27FF52 /* LiveLocationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2FEFA393FC7D2870263012 /* LiveLocationSession.swift */; }; A07178337F3C0B208B5A77A8 /* NotificationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB6ED50FE104992419310EEB /* NotificationHandler.swift */; }; A076E0A9338FD2D950C3C4A1 /* ChatsSpaceFiltersScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E63072981793CCA84EE12798 /* ChatsSpaceFiltersScreenViewModelProtocol.swift */; }; - A0790A97DAD44E8895035704 /* MultiAccountAnnouncementScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD46D21FF370EDF6BE87F8A7 /* MultiAccountAnnouncementScreenCoordinator.swift */; }; A0861B727B273B5B3DD7FBF6 /* KnockRequestsListScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09227E671DB30795C43FFFD /* KnockRequestsListScreenViewModel.swift */; }; A0868BDE84D2140A885BE3C9 /* EncryptionResetScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8562F4D7DE073BC32902AB /* EncryptionResetScreenViewModelProtocol.swift */; }; A0D7E5BD0298A97DCBDCE40B /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 7731767AE437BA3BD2CC14A8 /* Sentry */; }; @@ -1103,6 +1101,7 @@ A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; }; A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */; }; A88328D7E17F73AB64501B51 /* SwiftOGG in Frameworks */ = {isa = PBXBuildFile; productRef = 391D11F92DFC91666AA1503F /* SwiftOGG */; }; + A88A0C11EB4231EE2989C659 /* MultiAccountAnnouncementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BE5CAC17E09EE2643D97F2 /* MultiAccountAnnouncementView.swift */; }; A8DB299C5C891EDAE74A22F4 /* UserLocationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F7482D5D1526E3399B00174 /* UserLocationCell.swift */; }; A8E324E700E596E36B0A311B /* BootDetectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F054DE7D47849687662C9D9 /* BootDetectionManager.swift */; }; A8FA7671948E3DF27F320026 /* BugReportFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7367B3B9A8CAF902220F31D1 /* BugReportFlowCoordinator.swift */; }; @@ -1289,7 +1288,6 @@ C8E11A335456FCF94A744E6E /* SpaceFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */; }; C8E1E4E06B7C7A3A8246FC9B /* MediaEventsTimelineScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8512B82404B1751D0BCC82D2 /* MediaEventsTimelineScreenCoordinator.swift */; }; C900127318820AD04D6C90B8 /* LabsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E43D8784B0054C048060FEB /* LabsScreenModels.swift */; }; - C90A5ABB868F7DDEE2DFD653 /* MultiAccountAnnouncementScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB71240A4E855DE7B9201FD /* MultiAccountAnnouncementScreenModels.swift */; }; C915347779B3C7FDD073A87A /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E1FF0DFBB3768F79FDBF6D /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */; }; C95ABC529E0009926BDE5664 /* preview_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 44688A16934827A0D1668253 /* preview_image.jpg */; }; C960BACE42A9D8C535E8CB34 /* Pagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1912062B53CE95E6F700DA60 /* Pagination.swift */; }; @@ -1422,7 +1420,6 @@ DCFE7CB3B9A104330BBB96AD /* AnalyticsPromptScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B67DF223EEB8DCAF178A1D4 /* AnalyticsPromptScreenCoordinator.swift */; }; DD21CE51DF9BD04FC8155972 /* LeaveSpaceHandleSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580BDCD23DD02481AB5FFB47 /* LeaveSpaceHandleSDKMock.swift */; }; DD4D8B5D668197D345A3ED42 /* TimelineControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F933BB25FC426D5C67E1D812 /* TimelineControllerMock.swift */; }; - DD66C863B4232BCDEBE883C8 /* MultiAccountAnnouncementScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B5FD54003D9527FE347516 /* MultiAccountAnnouncementScreen.swift */; }; DDE7B4771452300C103B1EB8 /* RoomDirectoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F6210134203BE1F2DD5C679 /* RoomDirectoryCell.swift */; }; DDFBDEE1DC32BDD5488F898C /* ClientProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */; }; DE3BF0ED68E56BF625591D49 /* LinkNewDeviceScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C4A2D279386A811BDC6DAE /* LinkNewDeviceScreen.swift */; }; @@ -1586,7 +1583,6 @@ F519DE17A3A0F760307B2E6D /* InviteUsersScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D155E09BF961BBA8F85263 /* InviteUsersScreenViewModel.swift */; }; F54E2D6CAD96E1AC15BC526F /* MessageForwardingScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */; }; F58BF3BD3233A03F013816E4 /* ClassicAppAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F4DFFFC62187D9A4D2030D /* ClassicAppAccountManager.swift */; }; - F5A74869B9899FB178EDCDF9 /* MultiAccountAnnouncementScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F6FA4E8F2A0D6947AA44C2 /* MultiAccountAnnouncementScreenViewModel.swift */; }; F5D2270B5021D521C0D22E11 /* FlowCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9FCA1CFD07B8CF9BD21266 /* FlowCoordinatorProtocol.swift */; }; F656F92A63D3DC1978D79427 /* Macros in Frameworks */ = {isa = PBXBuildFile; productRef = 401E93551F7792B0179907CF /* Macros */; }; F65F3909769E7C48B3309D9D /* RemoteSettingsHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D186A6DB8FAC5C9D0E4D61 /* RemoteSettingsHook.swift */; }; @@ -2086,7 +2082,6 @@ 2DA3DBE1A42EAFF93889FA04 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/SAS.strings; sourceTree = ""; }; 2DA4F09CB613C54FDC73AE6A /* ThreadDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDecorator.swift; sourceTree = ""; }; 2DB0E533508094156D8024C3 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = ""; }; - 2DB71240A4E855DE7B9201FD /* MultiAccountAnnouncementScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiAccountAnnouncementScreenModels.swift; sourceTree = ""; }; 2E11E7C396ED06A154CF6DF3 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/SAS.strings; sourceTree = ""; }; 2F06F70B9C433BAD4BC6B9F5 /* EncryptedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineView.swift; sourceTree = ""; }; 2F36C5D9B37E50915ECBD3EE /* RoomMemberProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxy.swift; sourceTree = ""; }; @@ -2204,7 +2199,6 @@ 42C58159916ABD7F454CE689 /* AdvancedSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenCoordinator.swift; sourceTree = ""; }; 42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenModels.swift; sourceTree = ""; }; 42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreen.swift; sourceTree = ""; }; - 42F6FA4E8F2A0D6947AA44C2 /* MultiAccountAnnouncementScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiAccountAnnouncementScreenViewModel.swift; sourceTree = ""; }; 430C73079A84654BF46A7FF5 /* FileMediaEventsTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMediaEventsTimelineView.swift; sourceTree = ""; }; 4338BB055A26094673CDC220 /* SpaceAddRoomsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreenViewModel.swift; sourceTree = ""; }; 434522ED2BDED08759048077 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; @@ -2432,7 +2426,6 @@ 68F8744C32ACA0821A771EBB /* test_audio.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = test_audio.mp3; sourceTree = ""; }; 690915157A874D1F21ADE70C /* preview_avatar_room.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = preview_avatar_room.jpg; sourceTree = ""; }; 693E16574C6F7F9FA1015A8C /* Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = ""; }; - 69630272FD26D4E5974CC88E /* MultiAccountAnnouncementScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiAccountAnnouncementScreenViewModelProtocol.swift; sourceTree = ""; }; 69A05E85E4872C3221C5C287 /* RemotePreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemotePreference.swift; sourceTree = ""; }; 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregatedReactionMock.swift; sourceTree = ""; }; 69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModelTests.swift; sourceTree = ""; }; @@ -2691,7 +2684,6 @@ 94028A227645FA880B966211 /* WaveformSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformSource.swift; sourceTree = ""; }; 9475FD81B13D50103E5290EB /* SpaceSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceSettingsScreen.swift; sourceTree = ""; }; 94A561C542596126DAE211AD /* preview_avatar_user.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = preview_avatar_user.jpg; sourceTree = ""; }; - 94B5FD54003D9527FE347516 /* MultiAccountAnnouncementScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiAccountAnnouncementScreen.swift; sourceTree = ""; }; 94D670124FC3E84F23A62CCF /* APNSPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSPayload.swift; sourceTree = ""; }; 94EE2C5F0A06F146BBE3A1B1 /* LinkNewDeviceFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceFlowCoordinator.swift; sourceTree = ""; }; 9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenModels.swift; sourceTree = ""; }; @@ -2791,6 +2783,7 @@ A4D9DF4F2DF3507F99B5B97B /* LabsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsScreenViewModel.swift; sourceTree = ""; }; A54AAF72E821B4084B7E4298 /* PinnedEventsTimelineFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventsTimelineFlowCoordinator.swift; sourceTree = ""; }; A566F8F2C27B99E1FB80C69B /* JoinRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRule.swift; sourceTree = ""; }; + A5BE5CAC17E09EE2643D97F2 /* MultiAccountAnnouncementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiAccountAnnouncementView.swift; sourceTree = ""; }; A5C45578F406698388B97C07 /* AuthenticationClassicAppAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationClassicAppAccountView.swift; sourceTree = ""; }; A63A7C1486AA8FF088B1701C /* ModerationAndSafetySettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModerationAndSafetySettingsScreenCoordinator.swift; sourceTree = ""; }; A67BCEFE35F8D00CADF108C9 /* AccountProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountProvider.swift; sourceTree = ""; }; @@ -3046,7 +3039,6 @@ CCE06F4A71FD46C9D8CD432E /* RoomThreadListServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomThreadListServiceProxy.swift; sourceTree = ""; }; CCF71646898A2F720C5BFDF5 /* RoomDirectorySearchScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenViewModel.swift; sourceTree = ""; }; CD469F7513574341181F7EAA /* ServerSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreen.swift; sourceTree = ""; }; - CD46D21FF370EDF6BE87F8A7 /* MultiAccountAnnouncementScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiAccountAnnouncementScreenCoordinator.swift; sourceTree = ""; }; CD6613DE16AD26B3A74DA1F5 /* LocationRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItemContent.swift; sourceTree = ""; }; CDA38C9B58693A49A3032552 /* DeclineAndBlockScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreenCoordinator.swift; sourceTree = ""; }; CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; @@ -4016,14 +4008,6 @@ path = RoomMember; sourceTree = ""; }; - 2C5E4DC51BC09EA05CA1D590 /* View */ = { - isa = PBXGroup; - children = ( - 94B5FD54003D9527FE347516 /* MultiAccountAnnouncementScreen.swift */, - ); - path = View; - sourceTree = ""; - }; 2D0D49B0533C4C2EB889BF3A /* ServerSelectionScreen */ = { isa = PBXGroup; children = ( @@ -4860,6 +4844,7 @@ 05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */, ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */, C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */, + A5BE5CAC17E09EE2643D97F2 /* MultiAccountAnnouncementView.swift */, 037A5661B26EC6BE068188D7 /* Filters */, ); path = View; @@ -5086,18 +5071,6 @@ path = View; sourceTree = ""; }; - 601F598160369994A6C0C85D /* MultiAccountAnnouncementScreen */ = { - isa = PBXGroup; - children = ( - CD46D21FF370EDF6BE87F8A7 /* MultiAccountAnnouncementScreenCoordinator.swift */, - 2DB71240A4E855DE7B9201FD /* MultiAccountAnnouncementScreenModels.swift */, - 42F6FA4E8F2A0D6947AA44C2 /* MultiAccountAnnouncementScreenViewModel.swift */, - 69630272FD26D4E5974CC88E /* MultiAccountAnnouncementScreenViewModelProtocol.swift */, - 2C5E4DC51BC09EA05CA1D590 /* View */, - ); - path = MultiAccountAnnouncementScreen; - sourceTree = ""; - }; 605F8221E52991786397FCC9 /* View */ = { isa = PBXGroup; children = ( @@ -7248,7 +7221,6 @@ 87E2774157D9C4894BCFF3F8 /* MediaPickerScreen */, 23605DD08620BE6558242469 /* MediaUploadPreviewScreen */, 3348D14DBDB54E72FC67E2F3 /* MessageForwardingScreen */, - 601F598160369994A6C0C85D /* MultiAccountAnnouncementScreen */, DE07A244ACE83E40D894EDF3 /* NativeCallScreen */, 8F074E22FD93E64211971845 /* Onboarding */, A448A3A8F764174C60CD0CA1 /* Other */, @@ -9287,11 +9259,7 @@ 70A4C56E4E6C60A3124631FC /* ModerationAndSafetySettingsScreenModels.swift in Sources */, 5846723185B444D52E077874 /* ModerationAndSafetySettingsScreenViewModel.swift in Sources */, 7C8F9E3AF56E7FF5B338DBFA /* ModerationAndSafetySettingsScreenViewModelProtocol.swift in Sources */, - DD66C863B4232BCDEBE883C8 /* MultiAccountAnnouncementScreen.swift in Sources */, - A0790A97DAD44E8895035704 /* MultiAccountAnnouncementScreenCoordinator.swift in Sources */, - C90A5ABB868F7DDEE2DFD653 /* MultiAccountAnnouncementScreenModels.swift in Sources */, - F5A74869B9899FB178EDCDF9 /* MultiAccountAnnouncementScreenViewModel.swift in Sources */, - 53A182049BEEC6195B91DD79 /* MultiAccountAnnouncementScreenViewModelProtocol.swift in Sources */, + A88A0C11EB4231EE2989C659 /* MultiAccountAnnouncementView.swift in Sources */, E938F7A45D6A3DBBE6789A03 /* NSEUserSession.swift in Sources */, 551E03A935A6B73158A1AB4A /* NSEUserSessionMock.swift in Sources */, F9842667B68DC6FA1F9ECCBB /* NSItemProvider.swift in Sources */, diff --git a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift index 3b97007fb..c6f389434 100644 --- a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift +++ b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift @@ -105,7 +105,7 @@ enum TestablePreviewsDictionary { "MessageForwardingScreen_Previews" : MessageForwardingScreen_Previews.self, "MessageText_Previews" : MessageText_Previews.self, "ModerationAndSafetySettingsScreen_Previews" : ModerationAndSafetySettingsScreen_Previews.self, - "MultiAccountAnnouncementScreen_Previews" : MultiAccountAnnouncementScreen_Previews.self, + "MultiAccountAnnouncementView_Previews" : MultiAccountAnnouncementView_Previews.self, "NoticeRoomTimelineView_Previews" : NoticeRoomTimelineView_Previews.self, "NotificationPermissionsScreen_Previews" : NotificationPermissionsScreen_Previews.self, "NotificationSettingsEditScreenRoomCell_Previews" : NotificationSettingsEditScreenRoomCell_Previews.self, diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index b984ad2d4..afc2ca8e6 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -42,6 +42,10 @@ enum HomeScreenViewAction { case resetEncryption case skipRecoveryKeyConfirmation case dismissNewSoundBanner + case screenAppeared + case multiAccountAnnouncementAppeared + case dismissMultiAccountAnnouncement + case addAccount case updateVisibleItemRange(Range) case spaceFilters case markRoomAsUnread(roomIdentifier: String) @@ -161,6 +165,7 @@ struct HomeScreenViewStateBindings { var leaveRoomAlertItem: LeaveRoomAlertItem? var spaceFiltersViewModel: ChatsSpaceFiltersScreenViewModel? + var isPresentingMultiAccountAnnouncement = false } enum CallBadgeType { diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 24ff5fc0a..764685b3a 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -218,6 +218,14 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol state.securityBannerMode = .dismissed case .dismissNewSoundBanner: userSettings.hasSeenNewSoundBanner = true + case .screenAppeared: + // Use a task otherwise the presentation isn't animated. + Task { presentMultiAccountAnnouncementIfNeeded() } + case .multiAccountAnnouncementAppeared: + MXLog.info("The multi-account announcement has been seen.") + userSettings.hasSeenMultiAccountAnnouncement = true + case .dismissMultiAccountAnnouncement, .addAccount: // There is no add account flow to start yet. + state.bindings.isPresentingMultiAccountAnnouncement = false case .updateVisibleItemRange(let range): roomSummaryProvider?.updateVisibleRange(range) case .startChat: @@ -476,6 +484,21 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol actionsSubject.send(.roomLeft(roomIdentifier: roomID)) } + // MARK: Multi-account announcement + + private func presentMultiAccountAnnouncementIfNeeded() { + // An unverified session gets the identity confirmation cover first, the room list appears again once it's dismissed. + guard userSettings.multiAccountEnabled, + !userSettings.hasSeenMultiAccountAnnouncement, + userSession.sessionSecurityStatePublisher.value.verificationState == .verified, + !state.bindings.isPresentingMultiAccountAnnouncement else { + return + } + + MXLog.info("Presenting the multi-account announcement.") + state.bindings.isPresentingMultiAccountAnnouncement = true + } + // MARK: Invites private func acceptInvite(roomID: String) async { diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index fbd9b0a17..e68f4c32a 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -42,6 +42,11 @@ struct HomeScreen: View { .navigationTransition(.zoom(sourceID: NavigationTransitionSourceID.spaceFilters, in: navigationTransitionNamespace)) } + .sheet(isPresented: $context.isPresentingMultiAccountAnnouncement) { + MultiAccountAnnouncementView(context: context) + .onAppear { context.send(viewAction: .multiAccountAnnouncementAppeared) } + } + .onAppear { context.send(viewAction: .screenAppeared) } } // MARK: - Private diff --git a/ElementX/Sources/Screens/HomeScreen/View/MultiAccountAnnouncementView.swift b/ElementX/Sources/Screens/HomeScreen/View/MultiAccountAnnouncementView.swift new file mode 100644 index 000000000..61296350e --- /dev/null +++ b/ElementX/Sources/Screens/HomeScreen/View/MultiAccountAnnouncementView.swift @@ -0,0 +1,85 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Compound +import SwiftUI + +struct MultiAccountAnnouncementView: View { + let context: HomeScreenViewModel.Context + + @State private var navigationBarHeight: CGFloat = .zero + @State private var contentHeight: CGFloat = .zero + @State private var buttonHeight: CGFloat = .zero + + private var subtitle: AttributedString { + let boldPlaceholder = "{bold}" + var subtitle = AttributedString(L10n.screenMultiAccountAnnouncementDescription(boldPlaceholder)) + var boldString = AttributedString("\(L10n.commonSettings) > \(L10n.screenSettingsAddAccount)") + boldString.bold() + subtitle.replace(boldPlaceholder, with: boldString) + return subtitle + } + + var body: some View { + GeometryReader { geometry in + ElementNavigationStack { + ScrollView { + content + .readHeight($contentHeight) + } + .scrollBounceBehavior(.basedOnSize) + .backportSafeAreaBar(edge: .bottom, spacing: 0) { + addAccountButton + .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16) + .readHeight($buttonHeight) + } + .onGeometryChange(for: CGFloat.self) { $0.safeAreaInsets.top } action: { navigationBarHeight = $0 } + .toolbar { + ToolbarItem(placement: .primaryAction) { + ToolbarButton(role: .close) { context.send(viewAction: .dismissMultiAccountAnnouncement) } + } + } + } + } + .presentationDetents([.height(navigationBarHeight + contentHeight + buttonHeight)]) + .presentationDragIndicator(.hidden) + .presentationBackground(.compound.bgCanvasDefault) + } + + private var content: some View { + TitleAndIcon(title: L10n.screenMultiAccountAnnouncementTitle, + subtitle: subtitle, + icon: \.userProfileSolid, + iconStyle: .defaultSolid) + .frame(maxWidth: .infinity) + .padding(.horizontal, 24) + .padding(.top, 8) + .padding(.bottom, 24) + } + + private var addAccountButton: some View { + Button { + context.send(viewAction: .addAccount) + } label: { + Label(L10n.screenMultiAccountAnnouncementAction, icon: \.plus) + } + .buttonStyle(.compound(.primary)) + .padding(.horizontal, 16) + .padding(.top, 16) + } +} + +// MARK: - Previews + +struct MultiAccountAnnouncementView_Previews: PreviewProvider, TestablePreview { + static let viewModel = HomeScreen_Previews.viewModel(.rooms) + + static var previews: some View { + MultiAccountAnnouncementView(context: viewModel.context) + .previewDisplayName("Default") + } +} diff --git a/ElementX/Sources/Screens/MultiAccountAnnouncementScreen/MultiAccountAnnouncementScreenCoordinator.swift b/ElementX/Sources/Screens/MultiAccountAnnouncementScreen/MultiAccountAnnouncementScreenCoordinator.swift deleted file mode 100644 index 3bc124d6e..000000000 --- a/ElementX/Sources/Screens/MultiAccountAnnouncementScreen/MultiAccountAnnouncementScreenCoordinator.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright 2026 Element Creations Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. -// Please see LICENSE files in the repository root for full details. -// - -import Combine -import SwiftUI - -enum MultiAccountAnnouncementScreenCoordinatorAction { - case addAccount - case dismiss -} - -final class MultiAccountAnnouncementScreenCoordinator: CoordinatorProtocol { - private let viewModel: MultiAccountAnnouncementScreenViewModelProtocol - - private var cancellables = Set() - - private let actionsSubject: PassthroughSubject = .init() - var actionsPublisher: AnyPublisher { - actionsSubject.eraseToAnyPublisher() - } - - init() { - viewModel = MultiAccountAnnouncementScreenViewModel() - } - - func start() { - viewModel.actionsPublisher.sink { [weak self] action in - guard let self else { return } - - switch action { - case .addAccount: - actionsSubject.send(.addAccount) - case .dismiss: - actionsSubject.send(.dismiss) - } - } - .store(in: &cancellables) - } - - func toPresentable() -> AnyView { - AnyView(MultiAccountAnnouncementScreen(context: viewModel.context)) - } -} diff --git a/ElementX/Sources/Screens/MultiAccountAnnouncementScreen/MultiAccountAnnouncementScreenModels.swift b/ElementX/Sources/Screens/MultiAccountAnnouncementScreen/MultiAccountAnnouncementScreenModels.swift deleted file mode 100644 index e8c55b81c..000000000 --- a/ElementX/Sources/Screens/MultiAccountAnnouncementScreen/MultiAccountAnnouncementScreenModels.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// Copyright 2026 Element Creations Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. -// Please see LICENSE files in the repository root for full details. -// - -import SwiftUI - -enum MultiAccountAnnouncementScreenViewModelAction { - case addAccount - case dismiss -} - -struct MultiAccountAnnouncementScreenViewState: BindableState { - let description: AttributedString - - init() { - let boldPlaceholder = "{bold}" - var description = AttributedString(L10n.screenMultiAccountAnnouncementDescription(boldPlaceholder)) - var boldString = AttributedString("\(L10n.commonSettings) > \(L10n.screenSettingsAddAccount)") - boldString.bold() - description.replace(boldPlaceholder, with: boldString) - self.description = description - } -} - -enum MultiAccountAnnouncementScreenViewAction { - case addAccount - case close -} diff --git a/ElementX/Sources/Screens/MultiAccountAnnouncementScreen/MultiAccountAnnouncementScreenViewModel.swift b/ElementX/Sources/Screens/MultiAccountAnnouncementScreen/MultiAccountAnnouncementScreenViewModel.swift deleted file mode 100644 index 7cac7e8ff..000000000 --- a/ElementX/Sources/Screens/MultiAccountAnnouncementScreen/MultiAccountAnnouncementScreenViewModel.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// Copyright 2026 Element Creations Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. -// Please see LICENSE files in the repository root for full details. -// - -import Combine -import SwiftUI - -typealias MultiAccountAnnouncementScreenViewModelType = StateStoreViewModelV2 - -class MultiAccountAnnouncementScreenViewModel: MultiAccountAnnouncementScreenViewModelType, MultiAccountAnnouncementScreenViewModelProtocol { - private let actionsSubject: PassthroughSubject = .init() - var actionsPublisher: AnyPublisher { - actionsSubject.eraseToAnyPublisher() - } - - init() { - super.init(initialViewState: .init()) - } - - // MARK: - Public - - override func process(viewAction: MultiAccountAnnouncementScreenViewAction) { - switch viewAction { - case .addAccount: - actionsSubject.send(.addAccount) - case .close: - actionsSubject.send(.dismiss) - } - } -} diff --git a/ElementX/Sources/Screens/MultiAccountAnnouncementScreen/MultiAccountAnnouncementScreenViewModelProtocol.swift b/ElementX/Sources/Screens/MultiAccountAnnouncementScreen/MultiAccountAnnouncementScreenViewModelProtocol.swift deleted file mode 100644 index eafeb99d2..000000000 --- a/ElementX/Sources/Screens/MultiAccountAnnouncementScreen/MultiAccountAnnouncementScreenViewModelProtocol.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Copyright 2026 Element Creations Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. -// Please see LICENSE files in the repository root for full details. -// - -import Combine - -protocol MultiAccountAnnouncementScreenViewModelProtocol { - var actionsPublisher: AnyPublisher { get } - var context: MultiAccountAnnouncementScreenViewModelType.Context { get } -} diff --git a/ElementX/Sources/Screens/MultiAccountAnnouncementScreen/View/MultiAccountAnnouncementScreen.swift b/ElementX/Sources/Screens/MultiAccountAnnouncementScreen/View/MultiAccountAnnouncementScreen.swift deleted file mode 100644 index 9b9dbc480..000000000 --- a/ElementX/Sources/Screens/MultiAccountAnnouncementScreen/View/MultiAccountAnnouncementScreen.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// Copyright 2026 Element Creations Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. -// Please see LICENSE files in the repository root for full details. -// - -import Compound -import SwiftUI - -struct MultiAccountAnnouncementScreen: View { - let context: MultiAccountAnnouncementScreenViewModel.Context - - @State private var contentHeight: CGFloat = .zero - @State private var buttonHeight: CGFloat = .zero - private let topPadding: CGFloat = 44 // For the navigation bar - - var body: some View { - ElementNavigationStack { - ScrollView { - content - .readHeight($contentHeight) - } - .scrollBounceBehavior(.basedOnSize) - .backportSafeAreaBar(edge: .bottom, spacing: 0) { - addAccountButton - .readHeight($buttonHeight) - } - .toolbar { - ToolbarItem(placement: .primaryAction) { - ToolbarButton(role: .close) { context.send(viewAction: .close) } - } - } - } - .presentationDetents([.height(contentHeight + buttonHeight + topPadding)]) - .presentationDragIndicator(.hidden) - .presentationBackground(.compound.bgCanvasDefault) - } - - private var content: some View { - TitleAndIcon(title: L10n.screenMultiAccountAnnouncementTitle, - subtitle: context.viewState.description, - icon: \.userProfileSolid, - iconStyle: .defaultSolid) - .frame(maxWidth: .infinity) - .padding(.horizontal, 24) - .padding(.top, 16) - .padding(.bottom, 24) - } - - private var addAccountButton: some View { - Button { - context.send(viewAction: .addAccount) - } label: { - Label(L10n.screenMultiAccountAnnouncementAction, icon: \.plus) - } - .buttonStyle(.compound(.primary)) - .padding(16) - } -} - -// MARK: - Previews - -struct MultiAccountAnnouncementScreen_Previews: PreviewProvider, TestablePreview { - static let viewModel = MultiAccountAnnouncementScreenViewModel() - - static var previews: some View { - MultiAccountAnnouncementScreen(context: viewModel.context) - .previewDisplayName("Default") - } -} diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index edcf34a8c..3239295f1 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -705,8 +705,8 @@ extension PreviewTests { } @Test - func multiAccountAnnouncementScreen() async throws { - for (index, preview) in MultiAccountAnnouncementScreen_Previews._allPreviews.enumerated() { + func multiAccountAnnouncementView() async throws { + for (index, preview) in MultiAccountAnnouncementView_Previews._allPreviews.enumerated() { try await assertSnapshots(matching: preview, step: index) } } diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementScreen.Default-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementScreen.Default-iPad-en-GB.png deleted file mode 100644 index b658ecf53..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementScreen.Default-iPad-en-GB.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:257f3948490a4a9ca4b9f2eefebb3f0c010bfb3fae2721e4b95cab52889fe3a5 -size 102613 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementScreen.Default-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementScreen.Default-iPad-pseudo.png deleted file mode 100644 index cf0adf860..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementScreen.Default-iPad-pseudo.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2588daf8fa9a61b445545e047799520bbd20388a5dbd3f2a71ad94438aee6149 -size 118735 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementScreen.Default-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementScreen.Default-iPhone-en-GB.png deleted file mode 100644 index 3cc32d349..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementScreen.Default-iPhone-en-GB.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:753970b5fa26d39052106d90d2f6ecc807a15eddf031f5f67c6e7457d188675a -size 63416 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementScreen.Default-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementScreen.Default-iPhone-pseudo.png deleted file mode 100644 index ddfb9f471..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementScreen.Default-iPhone-pseudo.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d987ab496eec5bfe41d3c1b8f2143da225b5740c7b79eb75d4dfaa7fa4b379c2 -size 85276 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementView.Default-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementView.Default-iPad-en-GB.png new file mode 100644 index 000000000..f45ec9b90 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementView.Default-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7967692a14b42d638ab62437ffeddb16e3256d416c601447dc0bedb13f3047f2 +size 102541 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementView.Default-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementView.Default-iPad-pseudo.png new file mode 100644 index 000000000..75e98c346 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementView.Default-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c5532a443cbd0cb308a854685d4e3a6f0025127592c11d195f6b0a0f89641c4 +size 118672 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementView.Default-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementView.Default-iPhone-en-GB.png new file mode 100644 index 000000000..88c9100b0 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementView.Default-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31d9c55229100d0b51bfeaa91fd5fb2800a36da17d4a38b794a057070bfb0ec2 +size 63289 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementView.Default-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementView.Default-iPhone-pseudo.png new file mode 100644 index 000000000..bb2a79063 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/multiAccountAnnouncementView.Default-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eae27d5df83718a448c85764713f2bea34367933885aaf484028eb6534207520 +size 85098 diff --git a/UnitTests/Sources/HomeScreenViewModelTests.swift b/UnitTests/Sources/HomeScreenViewModelTests.swift index 8ed6465df..260c0809c 100644 --- a/UnitTests/Sources/HomeScreenViewModelTests.swift +++ b/UnitTests/Sources/HomeScreenViewModelTests.swift @@ -380,10 +380,92 @@ final class HomeScreenViewModelTests { #expect(userSettings.hasSeenNewSoundBanner) } + @Test + func multiAccountAnnouncement() async throws { + userSettings.multiAccountEnabled = true + setupViewModel() + + // Arming it after the view model exists (Developer options) doesn't present it on its own. + userSettings.hasSeenMultiAccountAnnouncement = false + let deferredFailure = deferFailure(context.$viewState, timeout: .seconds(1)) { $0.bindings.isPresentingMultiAccountAnnouncement } + try await deferredFailure.fulfill() + + let deferred = deferFulfillment(context.$viewState) { $0.bindings.isPresentingMultiAccountAnnouncement } + context.send(viewAction: .screenAppeared) + try await deferred.fulfill() + #expect(!userSettings.hasSeenMultiAccountAnnouncement) + + context.send(viewAction: .multiAccountAnnouncementAppeared) + #expect(userSettings.hasSeenMultiAccountAnnouncement) + #expect(context.viewState.bindings.isPresentingMultiAccountAnnouncement) + + // A swipe down dismisses it without any of the announcement's actions. + context.isPresentingMultiAccountAnnouncement = false + let deferredReappearance = deferFailure(context.$viewState, timeout: .seconds(1)) { $0.bindings.isPresentingMultiAccountAnnouncement } + context.send(viewAction: .screenAppeared) + try await deferredReappearance.fulfill() + } + + @Test(arguments: [HomeScreenViewAction.dismissMultiAccountAnnouncement, .addAccount]) + func multiAccountAnnouncementActionsDismissIt(_ action: HomeScreenViewAction) async throws { + userSettings.multiAccountEnabled = true + userSettings.hasSeenMultiAccountAnnouncement = false + setupViewModel() + + let deferred = deferFulfillment(context.$viewState) { $0.bindings.isPresentingMultiAccountAnnouncement } + context.send(viewAction: .screenAppeared) + try await deferred.fulfill() + + context.send(viewAction: action) + #expect(!context.viewState.bindings.isPresentingMultiAccountAnnouncement) + } + + @Test(arguments: MultiAccountAnnouncementHiddenCase.allCases) + func multiAccountAnnouncementHidden(_ hiddenCase: MultiAccountAnnouncementHiddenCase) async throws { + userSettings.multiAccountEnabled = hiddenCase != .flagOff + userSettings.hasSeenMultiAccountAnnouncement = hiddenCase == .alreadySeen + let verificationState: SessionVerificationState = switch hiddenCase { + case .unverified: .unverified + case .unknownVerificationState: .unknown + case .flagOff, .alreadySeen: .verified + } + let securityStateSubject = CurrentValueSubject(.init(verificationState: verificationState, recoveryState: .enabled)) + setupViewModel(securityStatePublisher: securityStateSubject.asCurrentValuePublisher()) + + let deferred = deferFailure(context.$viewState, timeout: .seconds(1)) { $0.bindings.isPresentingMultiAccountAnnouncement } + context.send(viewAction: .screenAppeared) + try await deferred.fulfill() + #expect(userSettings.hasSeenMultiAccountAnnouncement == (hiddenCase == .alreadySeen)) + } + + @Test + func multiAccountAnnouncementPresentsOnTheNextAppearanceOnceVerified() async throws { + userSettings.multiAccountEnabled = true + userSettings.hasSeenMultiAccountAnnouncement = false + let securityStateSubject = CurrentValueSubject(.init(verificationState: .unverified, recoveryState: .enabled)) + setupViewModel(securityStatePublisher: securityStateSubject.asCurrentValuePublisher()) + + let deferredFailure = deferFailure(context.$viewState, timeout: .seconds(1)) { $0.bindings.isPresentingMultiAccountAnnouncement } + context.send(viewAction: .screenAppeared) + try await deferredFailure.fulfill() + + // Becoming verified doesn't present it on its own… + securityStateSubject.send(.init(verificationState: .verified, recoveryState: .enabled)) + let deferredVerifiedFailure = deferFailure(context.$viewState, timeout: .seconds(1)) { $0.bindings.isPresentingMultiAccountAnnouncement } + try await deferredVerifiedFailure.fulfill() + + // …the room list appearing again does, e.g. once the identity confirmation cover is dismissed. + let deferred = deferFulfillment(context.$viewState) { $0.bindings.isPresentingMultiAccountAnnouncement } + context.send(viewAction: .screenAppeared) + try await deferred.fulfill() + } + // MARK: - Helpers enum InviteType { case rooms, spaces } + enum MultiAccountAnnouncementHiddenCase: CaseIterable { case flagOff, alreadySeen, unverified, unknownVerificationState } + @Test func roomListModeIsSetSynchronouslyOnAWarmLaunch() { setupViewModel()