From 6f5ba297ebbcf07f9665c170940782a4c528eb5e Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:59:56 +0100 Subject: [PATCH] Add the spaces feature announcement sheet. (#4571) --- .../Sources/GeneratedAccessibilityTests.swift | 4 + ElementX.xcodeproj/project.pbxproj | 4 + .../Application/Settings/AppSettings.swift | 4 + .../SpaceExplorerFlowCoordinator.swift | 1 + .../TestablePreviewsDictionary.swift | 1 + .../SpaceListScreenCoordinator.swift | 2 + .../SpaceListScreenModels.swift | 6 +- .../SpaceListScreenViewModel.swift | 10 ++ .../View/SpaceListScreen.swift | 5 + .../View/SpacesAnnouncementSheetView.swift | 115 ++++++++++++++++++ .../UITests/UITestsAppCoordinator.swift | 1 + .../Sources/GeneratedPreviewTests.swift | 6 + ...acesAnnouncementSheetView.iPad-en-GB-0.png | 3 + ...cesAnnouncementSheetView.iPad-pseudo-0.png | 3 + ...nnouncementSheetView.iPhone-16-en-GB-0.png | 3 + ...nouncementSheetView.iPhone-16-pseudo-0.png | 3 + .../SpaceListScreenViewModelTests.swift | 32 +++++ 17 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpacesAnnouncementSheetView.swift create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/spacesAnnouncementSheetView.iPad-en-GB-0.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/spacesAnnouncementSheetView.iPad-pseudo-0.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/spacesAnnouncementSheetView.iPhone-16-en-GB-0.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/spacesAnnouncementSheetView.iPhone-16-pseudo-0.png diff --git a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift index 39cb9b440..5625279e0 100644 --- a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift +++ b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift @@ -615,6 +615,10 @@ extension AccessibilityTests { try await performAccessibilityAudit(named: "SpaceScreen_Previews") } + func testSpacesAnnouncementSheetView() async throws { + try await performAccessibilityAudit(named: "SpacesAnnouncementSheetView_Previews") + } + func testSplashScreen() async throws { try await performAccessibilityAudit(named: "SplashScreen_Previews") } diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 13a0920b0..46ec1ba8e 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -881,6 +881,7 @@ 9D2E03DB175A6AB14589076D /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */; }; 9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */; }; 9D9690D2FD4CD26FF670620F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75EF87651B00A176AB08E97 /* AppDelegate.swift */; }; + 9DB4B303ECC05F0F33582594 /* SpacesAnnouncementSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7DA8FFD31B18324CC04A823 /* SpacesAnnouncementSheetView.swift */; }; 9DD5AA10E85137140FEA86A3 /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */; }; 9DD84E014ADFB2DD813022D5 /* RoomDetailsEditScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */; }; 9DE801D278AC34737467F937 /* VoiceMessageMediaManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 889DEDD63C68ABDA8AD29812 /* VoiceMessageMediaManagerProtocol.swift */; }; @@ -2712,6 +2713,7 @@ E7495E1119753B06FF2C2279 /* PhotoLibraryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryManager.swift; sourceTree = ""; }; E76A706B3EEA32B882DA5E2D /* BlockedUsersScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenViewModelProtocol.swift; sourceTree = ""; }; E78FC546F28E045A560F2963 /* EncryptionKeyProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionKeyProviderProtocol.swift; sourceTree = ""; }; + E7DA8FFD31B18324CC04A823 /* SpacesAnnouncementSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacesAnnouncementSheetView.swift; sourceTree = ""; }; E8294DB9E95C0C0630418466 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; E8495F37D6245AD0CFA1F60B /* AppLockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTests.swift; sourceTree = ""; }; E8A1F98AE670377B20679FF5 /* MediaPlayerProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerProvider.swift; sourceTree = ""; }; @@ -6557,6 +6559,7 @@ isa = PBXGroup; children = ( F52DA8CCCABA0998C8AA273C /* SpaceListScreen.swift */, + E7DA8FFD31B18324CC04A823 /* SpacesAnnouncementSheetView.swift */, ); path = View; sourceTree = ""; @@ -8311,6 +8314,7 @@ 94C2B531B96493B68B976E5F /* SpaceServiceProxy.swift in Sources */, A2091F4B1332D9BF273B09D5 /* SpaceServiceProxyMock.swift in Sources */, DB5200B87C4CE9DF0024AC4E /* SpaceServiceProxyProtocol.swift in Sources */, + 9DB4B303ECC05F0F33582594 /* SpacesAnnouncementSheetView.swift in Sources */, DF004A5B2EABBD0574D06A04 /* SplashScreenCoordinator.swift in Sources */, E1C67E5D9E22135A8FEBBD60 /* StackedAvatarsView.swift in Sources */, 3DAF325D8AE461F7CDB282BD /* StartChatScreen.swift in Sources */, diff --git a/ElementX/Sources/Application/Settings/AppSettings.swift b/ElementX/Sources/Application/Settings/AppSettings.swift index 5323f3c2a..41b359ec7 100644 --- a/ElementX/Sources/Application/Settings/AppSettings.swift +++ b/ElementX/Sources/Application/Settings/AppSettings.swift @@ -29,6 +29,7 @@ final class AppSettings { private enum UserDefaultsKeys: String { case lastVersionLaunched case seenInvites + case hasSeenSpacesAnnouncement case hasSeenNewSoundBanner case appLockNumberOfPINAttempts case appLockNumberOfBiometricAttempts @@ -162,6 +163,9 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.seenInvites, defaultValue: [], storageType: .userDefaults(store)) var seenInvites: Set + @UserPreference(key: UserDefaultsKeys.hasSeenSpacesAnnouncement, defaultValue: false, storageType: .userDefaults(store)) + var hasSeenSpacesAnnouncement + /// Defaults to `true` for new users, and we use a migration to set it to `false` for existing users. @UserPreference(key: UserDefaultsKeys.hasSeenNewSoundBanner, defaultValue: true, storageType: .userDefaults(store)) var hasSeenNewSoundBanner diff --git a/ElementX/Sources/FlowCoordinators/SpaceExplorerFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceExplorerFlowCoordinator.swift index 250950cb5..4301efa45 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceExplorerFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceExplorerFlowCoordinator.swift @@ -117,6 +117,7 @@ class SpaceExplorerFlowCoordinator: FlowCoordinatorProtocol { private func presentSpaceList() { let parameters = SpaceListScreenCoordinatorParameters(userSession: userSession, selectedSpacePublisher: selectedSpaceSubject.asCurrentValuePublisher(), + appSettings: flowParameters.appSettings, userIndicatorController: flowParameters.userIndicatorController) let coordinator = SpaceListScreenCoordinator(parameters: parameters) coordinator.actionsPublisher diff --git a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift index 11f37b09a..8f42406e5 100644 --- a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift +++ b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift @@ -161,6 +161,7 @@ enum TestablePreviewsDictionary { "SpaceListScreen_Previews" : SpaceListScreen_Previews.self, "SpaceRoomCell_Previews" : SpaceRoomCell_Previews.self, "SpaceScreen_Previews" : SpaceScreen_Previews.self, + "SpacesAnnouncementSheetView_Previews" : SpacesAnnouncementSheetView_Previews.self, "SplashScreen_Previews" : SplashScreen_Previews.self, "StackedAvatarsView_Previews" : StackedAvatarsView_Previews.self, "StartChatScreen_Previews" : StartChatScreen_Previews.self, diff --git a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenCoordinator.swift index 878d52e43..a7c0f2f55 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenCoordinator.swift @@ -13,6 +13,7 @@ import SwiftUI struct SpaceListScreenCoordinatorParameters { let userSession: UserSessionProtocol let selectedSpacePublisher: CurrentValuePublisher + let appSettings: AppSettings let userIndicatorController: UserIndicatorControllerProtocol } @@ -37,6 +38,7 @@ final class SpaceListScreenCoordinator: CoordinatorProtocol { viewModel = SpaceListScreenViewModel(userSession: parameters.userSession, selectedSpacePublisher: parameters.selectedSpacePublisher, + appSettings: parameters.appSettings, userIndicatorController: parameters.userIndicatorController) } diff --git a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenModels.swift b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenModels.swift index bee1b2089..4abc1d54a 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenModels.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenModels.swift @@ -31,9 +31,13 @@ struct SpaceListScreenViewState: BindableState { } } -struct SpaceListScreenViewStateBindings { } +struct SpaceListScreenViewStateBindings { + var isPresentingFeatureAnnouncement = false +} enum SpaceListScreenViewAction { case spaceAction(SpaceRoomCell.Action) case showSettings + case screenAppeared + case featureAnnouncementAppeared } diff --git a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift index d711fb7b6..2878b0799 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift @@ -12,6 +12,7 @@ typealias SpaceListScreenViewModelType = StateStoreViewModelV2 = .init() @@ -21,8 +22,10 @@ class SpaceListScreenViewModel: SpaceListScreenViewModelType, SpaceListScreenVie init(userSession: UserSessionProtocol, selectedSpacePublisher: CurrentValuePublisher, + appSettings: AppSettings, userIndicatorController: UserIndicatorControllerProtocol) { spaceServiceProxy = userSession.clientProxy.spaceService + self.appSettings = appSettings self.userIndicatorController = userIndicatorController super.init(initialViewState: SpaceListScreenViewState(userID: userSession.clientProxy.userID, @@ -62,6 +65,13 @@ class SpaceListScreenViewModel: SpaceListScreenViewModelType, SpaceListScreenVie fatalError("There shouldn't be any unjoined spaces in the joined spaces list.") case .showSettings: actionsSubject.send(.showSettings) + case .screenAppeared: + if !appSettings.hasSeenSpacesAnnouncement { + // Use a task otherwise the presentation isn't animated. + Task { state.bindings.isPresentingFeatureAnnouncement = true } + } + case .featureAnnouncementAppeared: + appSettings.hasSeenSpacesAnnouncement = true } } diff --git a/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpaceListScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpaceListScreen.swift index 25bf55756..590828707 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpaceListScreen.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpaceListScreen.swift @@ -23,6 +23,10 @@ struct SpaceListScreen: View { .toolbar { toolbar } .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) .bloom() + .onAppear { context.send(viewAction: .screenAppeared) } + .sheet(isPresented: $context.isPresentingFeatureAnnouncement) { + SpacesAnnouncementSheetView(context: context) + } } var header: some View { @@ -110,6 +114,7 @@ struct SpaceListScreen_Previews: PreviewProvider, TestablePreview { let viewModel = SpaceListScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)), selectedSpacePublisher: .init(nil), + appSettings: ServiceLocator.shared.settings, userIndicatorController: UserIndicatorControllerMock()) return viewModel diff --git a/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpacesAnnouncementSheetView.swift b/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpacesAnnouncementSheetView.swift new file mode 100644 index 000000000..2a4603823 --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpacesAnnouncementSheetView.swift @@ -0,0 +1,115 @@ +// +// Copyright 2022-2025 New Vector 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 SpacesAnnouncementSheetView: View { + @Environment(\.dismiss) private var dismiss + + let context: SpaceListScreenViewModel.Context + + var body: some View { + FullscreenDialog(topPadding: 44, horizontalPadding: 24) { + content + } bottomContent: { + buttons + } + .background() + .backgroundStyle(.compound.bgCanvasDefault) + .padding(.top, 14) // For the drag indicator + .presentationDragIndicator(.visible) + .onAppear { context.send(viewAction: .featureAnnouncementAppeared) } + } + + var content: some View { + VStack(spacing: 16) { + BigIcon(icon: \.spaceSolid, style: .defaultSolid) + + VStack(spacing: 8) { + HStack(spacing: 6) { + Text(L10n.screenSpaceAnnouncementTitle) + .font(.compound.headingMDBold) + .foregroundStyle(.compound.textPrimary) + .multilineTextAlignment(.center) + Text(L10n.commonBeta) + .font(.compound.bodyXSSemibold) + .foregroundStyle(.compound.textInfoPrimary) + .textCase(.uppercase) + .padding(.horizontal, 10) + .padding(.vertical, 5) + .background { + RoundedRectangle(cornerRadius: 6) + .fill(.compound.bgInfoSubtle) + RoundedRectangle(cornerRadius: 6) + .stroke(.compound.borderInfoSubtle) + } + } + Text(L10n.screenSpaceAnnouncementSubtitle) + .font(.compound.bodyMD) + .foregroundStyle(.compound.textSecondary) + .multilineTextAlignment(.center) + } + + visualListItems + + Text(L10n.screenSpaceAnnouncementNotice) + .font(.compound.bodyMD) + .foregroundStyle(.compound.textSecondary) + .multilineTextAlignment(.center) + } + } + + var visualListItems: some View { + VStack(spacing: 4) { + VisualListItem(title: L10n.screenSpaceAnnouncementItem1, position: .top) { + CompoundIcon(\.visibilityOn) + .foregroundStyle(.compound.iconSecondary) + .alignmentGuide(.top) { _ in 2 } + } + VisualListItem(title: L10n.screenSpaceAnnouncementItem2, position: .middle) { + CompoundIcon(\.email) + .foregroundStyle(.compound.iconSecondary) + .alignmentGuide(.top) { _ in 2 } + } + VisualListItem(title: L10n.screenSpaceAnnouncementItem3, position: .middle) { + CompoundIcon(\.search) + .foregroundStyle(.compound.iconSecondary) + .alignmentGuide(.top) { _ in 2 } + } + // This isn't possible until we enabled the room directory. + // VisualListItem(title: L10n.screenSpaceAnnouncementItem4, position: .middle) { + // CompoundIcon(\.explore) + // .foregroundStyle(.compound.iconSecondary) + // .alignmentGuide(.top) { _ in 2 } + // } + VisualListItem(title: L10n.screenSpaceAnnouncementItem5, position: .bottom) { + CompoundIcon(\.leave) + .foregroundStyle(.compound.iconSecondary) + .alignmentGuide(.top) { _ in 2 } + } + } + } + + var buttons: some View { + Button(L10n.actionContinue, action: dismiss.callAsFunction) + .buttonStyle(.compound(.primary)) + } +} + +// MARK: - Previews + +struct SpacesAnnouncementSheetView_Previews: PreviewProvider, TestablePreview { + static let viewModel = SpaceListScreenViewModel(userSession: UserSessionMock(.init()), + selectedSpacePublisher: .init(nil), + appSettings: ServiceLocator.shared.settings, + userIndicatorController: UserIndicatorControllerMock()) + + static var previews: some View { + SpacesAnnouncementSheetView(context: viewModel.context) + } +} diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 1fe5d9338..9a960fa24 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -585,6 +585,7 @@ class MockScreen: Identifiable { appSettings.hasRunNotificationPermissionsOnboarding = true appSettings.analyticsConsentState = .optedOut appSettings.spacesEnabled = true + appSettings.hasSeenSpacesAnnouncement = true let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", deviceID: "MOCKCLIENT", diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index ce8474f48..86784068a 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -923,6 +923,12 @@ extension PreviewTests { } } + func testSpacesAnnouncementSheetView() async throws { + for (index, preview) in SpacesAnnouncementSheetView_Previews._allPreviews.enumerated() { + try await assertSnapshots(matching: preview, step: index) + } + } + func testSplashScreen() async throws { for (index, preview) in SplashScreen_Previews._allPreviews.enumerated() { try await assertSnapshots(matching: preview, step: index) diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesAnnouncementSheetView.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesAnnouncementSheetView.iPad-en-GB-0.png new file mode 100644 index 000000000..e6b0052c8 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesAnnouncementSheetView.iPad-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eccd2ad5a68030a787644ec2f4e08f279ba633065101ef28f6db3363a0efd3b0 +size 146453 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesAnnouncementSheetView.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesAnnouncementSheetView.iPad-pseudo-0.png new file mode 100644 index 000000000..932b303e6 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesAnnouncementSheetView.iPad-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:560872aaa26dfa28023e7f73145ad82959dcd69a0afdc1174465108182d5e3b8 +size 180990 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesAnnouncementSheetView.iPhone-16-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesAnnouncementSheetView.iPhone-16-en-GB-0.png new file mode 100644 index 000000000..7b4cdcbd2 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesAnnouncementSheetView.iPhone-16-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07d585ad557174655a0998f915ce48eb497178482b8493cc7184179f734f9d2a +size 96164 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesAnnouncementSheetView.iPhone-16-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesAnnouncementSheetView.iPhone-16-pseudo-0.png new file mode 100644 index 000000000..cb2d2999b --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/spacesAnnouncementSheetView.iPhone-16-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7f55a94cf459288c4a34c8b553038be8eb41acf7ad67268eea3ca68a4a5a9a5 +size 143969 diff --git a/UnitTests/Sources/SpaceListScreenViewModelTests.swift b/UnitTests/Sources/SpaceListScreenViewModelTests.swift index 66dae4041..8d9ab4a87 100644 --- a/UnitTests/Sources/SpaceListScreenViewModelTests.swift +++ b/UnitTests/Sources/SpaceListScreenViewModelTests.swift @@ -14,12 +14,22 @@ import XCTest class SpaceListScreenViewModelTests: XCTestCase { var joinedSpacesSubject: CurrentValueSubject<[SpaceRoomProxyProtocol], Never>! var spaceServiceProxy: SpaceServiceProxyMock! + var appSettings: AppSettings! var viewModel: SpaceListScreenViewModelProtocol! var context: SpaceListScreenViewModelType.Context { viewModel.context } + + override func setUp() { + AppSettings.resetAllSettings() + appSettings = AppSettings() + } + + override func tearDown() { + AppSettings.resetAllSettings() + } func testInitialState() { setupViewModel() @@ -59,6 +69,27 @@ class SpaceListScreenViewModelTests: XCTestCase { } } + func testFeatureAnnouncement() async throws { + setupViewModel() + XCTAssertFalse(appSettings.hasSeenSpacesAnnouncement) + XCTAssertFalse(context.isPresentingFeatureAnnouncement) + + let deferred = deferFulfillment(context.observe(\.isPresentingFeatureAnnouncement)) { $0 == true } + viewModel.context.send(viewAction: .screenAppeared) + try await deferred.fulfill() + XCTAssertTrue(context.isPresentingFeatureAnnouncement) + + viewModel.context.send(viewAction: .featureAnnouncementAppeared) + XCTAssertTrue(appSettings.hasSeenSpacesAnnouncement) + + context.isPresentingFeatureAnnouncement = false + + let deferredFailure = deferFailure(context.observe(\.isPresentingFeatureAnnouncement), timeout: 1) { $0 == true } + viewModel.context.send(viewAction: .screenAppeared) + try await deferredFailure.fulfill() + XCTAssertFalse(context.isPresentingFeatureAnnouncement) + } + // MARK: - Helpers private func setupViewModel() { @@ -80,6 +111,7 @@ class SpaceListScreenViewModelTests: XCTestCase { viewModel = SpaceListScreenViewModel(userSession: userSession, selectedSpacePublisher: .init(nil), + appSettings: ServiceLocator.shared.settings, userIndicatorController: UserIndicatorControllerMock()) } }