Skip to content

Commit a04f75e

Browse files
committed
Add the spaces feature announcement sheet.
1 parent b58d426 commit a04f75e

17 files changed

+202
-1
lines changed

AccessibilityTests/Sources/GeneratedAccessibilityTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,10 @@ extension AccessibilityTests {
615615
try await performAccessibilityAudit(named: "SpaceScreen_Previews")
616616
}
617617

618+
func testSpacesAnnouncementSheetView() async throws {
619+
try await performAccessibilityAudit(named: "SpacesAnnouncementSheetView_Previews")
620+
}
621+
618622
func testSplashScreen() async throws {
619623
try await performAccessibilityAudit(named: "SplashScreen_Previews")
620624
}

ElementX.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,7 @@
881881
9D2E03DB175A6AB14589076D /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */; };
882882
9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */; };
883883
9D9690D2FD4CD26FF670620F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75EF87651B00A176AB08E97 /* AppDelegate.swift */; };
884+
9DB4B303ECC05F0F33582594 /* SpacesAnnouncementSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7DA8FFD31B18324CC04A823 /* SpacesAnnouncementSheetView.swift */; };
884885
9DD5AA10E85137140FEA86A3 /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */; };
885886
9DD84E014ADFB2DD813022D5 /* RoomDetailsEditScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */; };
886887
9DE801D278AC34737467F937 /* VoiceMessageMediaManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 889DEDD63C68ABDA8AD29812 /* VoiceMessageMediaManagerProtocol.swift */; };
@@ -2712,6 +2713,7 @@
27122713
E7495E1119753B06FF2C2279 /* PhotoLibraryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryManager.swift; sourceTree = "<group>"; };
27132714
E76A706B3EEA32B882DA5E2D /* BlockedUsersScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenViewModelProtocol.swift; sourceTree = "<group>"; };
27142715
E78FC546F28E045A560F2963 /* EncryptionKeyProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionKeyProviderProtocol.swift; sourceTree = "<group>"; };
2716+
E7DA8FFD31B18324CC04A823 /* SpacesAnnouncementSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacesAnnouncementSheetView.swift; sourceTree = "<group>"; };
27152717
E8294DB9E95C0C0630418466 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
27162718
E8495F37D6245AD0CFA1F60B /* AppLockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTests.swift; sourceTree = "<group>"; };
27172719
E8A1F98AE670377B20679FF5 /* MediaPlayerProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerProvider.swift; sourceTree = "<group>"; };
@@ -6557,6 +6559,7 @@
65576559
isa = PBXGroup;
65586560
children = (
65596561
F52DA8CCCABA0998C8AA273C /* SpaceListScreen.swift */,
6562+
E7DA8FFD31B18324CC04A823 /* SpacesAnnouncementSheetView.swift */,
65606563
);
65616564
path = View;
65626565
sourceTree = "<group>";
@@ -8311,6 +8314,7 @@
83118314
94C2B531B96493B68B976E5F /* SpaceServiceProxy.swift in Sources */,
83128315
A2091F4B1332D9BF273B09D5 /* SpaceServiceProxyMock.swift in Sources */,
83138316
DB5200B87C4CE9DF0024AC4E /* SpaceServiceProxyProtocol.swift in Sources */,
8317+
9DB4B303ECC05F0F33582594 /* SpacesAnnouncementSheetView.swift in Sources */,
83148318
DF004A5B2EABBD0574D06A04 /* SplashScreenCoordinator.swift in Sources */,
83158319
E1C67E5D9E22135A8FEBBD60 /* StackedAvatarsView.swift in Sources */,
83168320
3DAF325D8AE461F7CDB282BD /* StartChatScreen.swift in Sources */,

ElementX/Sources/Application/Settings/AppSettings.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ final class AppSettings {
2929
private enum UserDefaultsKeys: String {
3030
case lastVersionLaunched
3131
case seenInvites
32+
case hasSeenSpacesAnnouncement
3233
case hasSeenNewSoundBanner
3334
case appLockNumberOfPINAttempts
3435
case appLockNumberOfBiometricAttempts
@@ -162,6 +163,9 @@ final class AppSettings {
162163
@UserPreference(key: UserDefaultsKeys.seenInvites, defaultValue: [], storageType: .userDefaults(store))
163164
var seenInvites: Set<String>
164165

166+
@UserPreference(key: UserDefaultsKeys.hasSeenSpacesAnnouncement, defaultValue: false, storageType: .userDefaults(store))
167+
var hasSeenSpacesAnnouncement
168+
165169
/// Defaults to `true` for new users, and we use a migration to set it to `false` for existing users.
166170
@UserPreference(key: UserDefaultsKeys.hasSeenNewSoundBanner, defaultValue: true, storageType: .userDefaults(store))
167171
var hasSeenNewSoundBanner

ElementX/Sources/FlowCoordinators/SpaceExplorerFlowCoordinator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ class SpaceExplorerFlowCoordinator: FlowCoordinatorProtocol {
117117
private func presentSpaceList() {
118118
let parameters = SpaceListScreenCoordinatorParameters(userSession: userSession,
119119
selectedSpacePublisher: selectedSpaceSubject.asCurrentValuePublisher(),
120+
appSettings: flowParameters.appSettings,
120121
userIndicatorController: flowParameters.userIndicatorController)
121122
let coordinator = SpaceListScreenCoordinator(parameters: parameters)
122123
coordinator.actionsPublisher

ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ enum TestablePreviewsDictionary {
161161
"SpaceListScreen_Previews" : SpaceListScreen_Previews.self,
162162
"SpaceRoomCell_Previews" : SpaceRoomCell_Previews.self,
163163
"SpaceScreen_Previews" : SpaceScreen_Previews.self,
164+
"SpacesAnnouncementSheetView_Previews" : SpacesAnnouncementSheetView_Previews.self,
164165
"SplashScreen_Previews" : SplashScreen_Previews.self,
165166
"StackedAvatarsView_Previews" : StackedAvatarsView_Previews.self,
166167
"StartChatScreen_Previews" : StartChatScreen_Previews.self,

ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenCoordinator.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import SwiftUI
1313
struct SpaceListScreenCoordinatorParameters {
1414
let userSession: UserSessionProtocol
1515
let selectedSpacePublisher: CurrentValuePublisher<String?, Never>
16+
let appSettings: AppSettings
1617
let userIndicatorController: UserIndicatorControllerProtocol
1718
}
1819

@@ -37,6 +38,7 @@ final class SpaceListScreenCoordinator: CoordinatorProtocol {
3738

3839
viewModel = SpaceListScreenViewModel(userSession: parameters.userSession,
3940
selectedSpacePublisher: parameters.selectedSpacePublisher,
41+
appSettings: parameters.appSettings,
4042
userIndicatorController: parameters.userIndicatorController)
4143
}
4244

ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenModels.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,13 @@ struct SpaceListScreenViewState: BindableState {
3131
}
3232
}
3333

34-
struct SpaceListScreenViewStateBindings { }
34+
struct SpaceListScreenViewStateBindings {
35+
var isPresentingFeatureAnnouncement = false
36+
}
3537

3638
enum SpaceListScreenViewAction {
3739
case spaceAction(SpaceRoomCell.Action)
3840
case showSettings
41+
case screenAppeared
42+
case featureAnnouncementAppeared
3943
}

ElementX/Sources/Screens/Spaces/SpaceListScreen/SpaceListScreenViewModel.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ typealias SpaceListScreenViewModelType = StateStoreViewModelV2<SpaceListScreenVi
1212

1313
class SpaceListScreenViewModel: SpaceListScreenViewModelType, SpaceListScreenViewModelProtocol {
1414
private let spaceServiceProxy: SpaceServiceProxyProtocol
15+
private let appSettings: AppSettings
1516
private let userIndicatorController: UserIndicatorControllerProtocol
1617

1718
private let actionsSubject: PassthroughSubject<SpaceListScreenViewModelAction, Never> = .init()
@@ -21,8 +22,10 @@ class SpaceListScreenViewModel: SpaceListScreenViewModelType, SpaceListScreenVie
2122

2223
init(userSession: UserSessionProtocol,
2324
selectedSpacePublisher: CurrentValuePublisher<String?, Never>,
25+
appSettings: AppSettings,
2426
userIndicatorController: UserIndicatorControllerProtocol) {
2527
spaceServiceProxy = userSession.clientProxy.spaceService
28+
self.appSettings = appSettings
2629
self.userIndicatorController = userIndicatorController
2730

2831
super.init(initialViewState: SpaceListScreenViewState(userID: userSession.clientProxy.userID,
@@ -62,6 +65,13 @@ class SpaceListScreenViewModel: SpaceListScreenViewModelType, SpaceListScreenVie
6265
fatalError("There shouldn't be any unjoined spaces in the joined spaces list.")
6366
case .showSettings:
6467
actionsSubject.send(.showSettings)
68+
case .screenAppeared:
69+
if !appSettings.hasSeenSpacesAnnouncement {
70+
// Use a task otherwise the presentation isn't animated.
71+
Task { state.bindings.isPresentingFeatureAnnouncement = true }
72+
}
73+
case .featureAnnouncementAppeared:
74+
appSettings.hasSeenSpacesAnnouncement = true
6575
}
6676
}
6777

ElementX/Sources/Screens/Spaces/SpaceListScreen/View/SpaceListScreen.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ struct SpaceListScreen: View {
2323
.toolbar { toolbar }
2424
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
2525
.bloom()
26+
.onAppear { context.send(viewAction: .screenAppeared) }
27+
.sheet(isPresented: $context.isPresentingFeatureAnnouncement) {
28+
SpacesAnnouncementSheetView(context: context)
29+
}
2630
}
2731

2832
var header: some View {
@@ -110,6 +114,7 @@ struct SpaceListScreen_Previews: PreviewProvider, TestablePreview {
110114

111115
let viewModel = SpaceListScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
112116
selectedSpacePublisher: .init(nil),
117+
appSettings: ServiceLocator.shared.settings,
113118
userIndicatorController: UserIndicatorControllerMock())
114119

115120
return viewModel
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//
2+
// Copyright 2022-2025 New Vector Ltd.
3+
//
4+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
// Please see LICENSE files in the repository root for full details.
6+
//
7+
8+
import Compound
9+
import SwiftUI
10+
11+
struct SpacesAnnouncementSheetView: View {
12+
@Environment(\.dismiss) private var dismiss
13+
14+
let context: SpaceListScreenViewModel.Context
15+
16+
var body: some View {
17+
FullscreenDialog(topPadding: 44, horizontalPadding: 24) {
18+
content
19+
} bottomContent: {
20+
buttons
21+
}
22+
.background()
23+
.backgroundStyle(.compound.bgCanvasDefault)
24+
.padding(.top, 14) // For the drag indicator
25+
.presentationDragIndicator(.visible)
26+
.onAppear { context.send(viewAction: .featureAnnouncementAppeared) }
27+
}
28+
29+
var content: some View {
30+
VStack(spacing: 16) {
31+
BigIcon(icon: \.spaceSolid, style: .defaultSolid)
32+
33+
VStack(spacing: 8) {
34+
HStack(spacing: 6) {
35+
Text(L10n.screenSpaceAnnouncementTitle)
36+
.font(.compound.headingMDBold)
37+
.foregroundStyle(.compound.textPrimary)
38+
.multilineTextAlignment(.center)
39+
Text(L10n.commonBeta)
40+
.font(.compound.bodyXSSemibold)
41+
.foregroundStyle(.compound.textInfoPrimary)
42+
.textCase(.uppercase)
43+
.padding(.horizontal, 10)
44+
.padding(.vertical, 5)
45+
.background {
46+
RoundedRectangle(cornerRadius: 6)
47+
.fill(.compound.bgInfoSubtle)
48+
RoundedRectangle(cornerRadius: 6)
49+
.stroke(.compound.borderInfoSubtle)
50+
}
51+
}
52+
Text(L10n.screenSpaceAnnouncementSubtitle)
53+
.font(.compound.bodyMD)
54+
.foregroundStyle(.compound.textSecondary)
55+
.multilineTextAlignment(.center)
56+
}
57+
58+
visualListItems
59+
60+
Text(L10n.screenSpaceAnnouncementNotice)
61+
.font(.compound.bodyMD)
62+
.foregroundStyle(.compound.textSecondary)
63+
.multilineTextAlignment(.center)
64+
}
65+
}
66+
67+
var visualListItems: some View {
68+
VStack(spacing: 4) {
69+
VisualListItem(title: L10n.screenSpaceAnnouncementItem1, position: .top) {
70+
CompoundIcon(\.visibilityOn)
71+
.foregroundStyle(.compound.iconSecondary)
72+
.alignmentGuide(.top) { _ in 2 }
73+
}
74+
VisualListItem(title: L10n.screenSpaceAnnouncementItem2, position: .middle) {
75+
CompoundIcon(\.email)
76+
.foregroundStyle(.compound.iconSecondary)
77+
.alignmentGuide(.top) { _ in 2 }
78+
}
79+
VisualListItem(title: L10n.screenSpaceAnnouncementItem3, position: .middle) {
80+
CompoundIcon(\.search)
81+
.foregroundStyle(.compound.iconSecondary)
82+
.alignmentGuide(.top) { _ in 2 }
83+
}
84+
// This isn't possible until we enabled the room directory.
85+
// VisualListItem(title: L10n.screenSpaceAnnouncementItem4, position: .middle) {
86+
// CompoundIcon(\.explore)
87+
// .foregroundStyle(.compound.iconSecondary)
88+
// .alignmentGuide(.top) { _ in 2 }
89+
// }
90+
VisualListItem(title: L10n.screenSpaceAnnouncementItem5, position: .bottom) {
91+
CompoundIcon(\.leave)
92+
.foregroundStyle(.compound.iconSecondary)
93+
.alignmentGuide(.top) { _ in 2 }
94+
}
95+
}
96+
}
97+
98+
var buttons: some View {
99+
Button(L10n.actionContinue, action: dismiss.callAsFunction)
100+
.buttonStyle(.compound(.primary))
101+
}
102+
}
103+
104+
// MARK: - Previews
105+
106+
struct SpacesAnnouncementSheetView_Previews: PreviewProvider, TestablePreview {
107+
static let viewModel = SpaceListScreenViewModel(userSession: UserSessionMock(.init()),
108+
selectedSpacePublisher: .init(nil),
109+
appSettings: ServiceLocator.shared.settings,
110+
userIndicatorController: UserIndicatorControllerMock())
111+
112+
static var previews: some View {
113+
SpacesAnnouncementSheetView(context: viewModel.context)
114+
}
115+
}

0 commit comments

Comments
 (0)