Skip to content

Commit f59a93d

Browse files
authored
Initial setup for the SpaceListScreen. (#4380)
1 parent d50d540 commit f59a93d

20 files changed

+356
-6
lines changed

AccessibilityTests/Sources/GeneratedAccessibilityTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,10 @@ extension AccessibilityTests {
583583
try await performAccessibilityAudit(named: "SoftLogoutScreen_Previews")
584584
}
585585

586+
func testSpaceListScreen() async throws {
587+
try await performAccessibilityAudit(named: "SpaceListScreen_Previews")
588+
}
589+
586590
func testSplashScreen() async throws {
587591
try await performAccessibilityAudit(named: "SplashScreen_Previews")
588592
}

ElementX.xcodeproj/project.pbxproj

Lines changed: 40 additions & 0 deletions
Large diffs are not rendered by default.

ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
7474
isNewLogin: isNewLogin)
7575
chatsTabDetails = .init(tag: HomeTab.chats, title: L10n.screenHomeTabChats, icon: \.chat, selectedIcon: \.chatSolid)
7676
chatsTabDetails.barVisibility = .hidden
77+
78+
// This is just temporary, it needs a flow coordinator to properly handle (amongst other things) navigation/split views.
79+
let spaceListScreenCoordinator = SpaceListScreenCoordinator(parameters: .init(userSession: userSession))
80+
let spacesNavigationCoordinator = NavigationStackCoordinator()
81+
spacesNavigationCoordinator.setRootCoordinator(spaceListScreenCoordinator)
7782
spacesTabDetails = .init(tag: HomeTab.spaces, title: L10n.screenHomeTabSpaces, icon: \.space, selectedIcon: \.spaceSolid)
7883

7984
onboardingStackCoordinator = NavigationStackCoordinator()
@@ -89,7 +94,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
8994

9095
navigationTabCoordinator.setTabs([
9196
.init(coordinator: chatsSplitCoordinator, details: chatsTabDetails),
92-
.init(coordinator: BlankFormCoordinator(), details: spacesTabDetails)
97+
.init(coordinator: spacesNavigationCoordinator, details: spacesTabDetails)
9398
])
9499

95100
setupObservers()

ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ enum TestablePreviewsDictionary {
153153
"SettingsScreen_Previews" : SettingsScreen_Previews.self,
154154
"ShimmerOverlay_Previews" : ShimmerOverlay_Previews.self,
155155
"SoftLogoutScreen_Previews" : SoftLogoutScreen_Previews.self,
156+
"SpaceListScreen_Previews" : SpaceListScreen_Previews.self,
156157
"SplashScreen_Previews" : SplashScreen_Previews.self,
157158
"StackedAvatarsView_Previews" : StackedAvatarsView_Previews.self,
158159
"StartChatScreen_Previews" : StartChatScreen_Previews.self,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//
2+
// Copyright 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+
// periphery:ignore:all - this is just a spaceList remove this comment once generating the final file
9+
10+
import Combine
11+
import SwiftUI
12+
13+
struct SpaceListScreenCoordinatorParameters {
14+
let userSession: UserSessionProtocol
15+
}
16+
17+
enum SpaceListScreenCoordinatorAction {
18+
case showSettings
19+
}
20+
21+
final class SpaceListScreenCoordinator: CoordinatorProtocol {
22+
private let parameters: SpaceListScreenCoordinatorParameters
23+
private let viewModel: SpaceListScreenViewModelProtocol
24+
25+
private var cancellables = Set<AnyCancellable>()
26+
27+
private let actionsSubject: PassthroughSubject<SpaceListScreenCoordinatorAction, Never> = .init()
28+
var actionsPublisher: AnyPublisher<SpaceListScreenCoordinatorAction, Never> {
29+
actionsSubject.eraseToAnyPublisher()
30+
}
31+
32+
init(parameters: SpaceListScreenCoordinatorParameters) {
33+
self.parameters = parameters
34+
35+
viewModel = SpaceListScreenViewModel(userSession: parameters.userSession)
36+
}
37+
38+
func start() {
39+
viewModel.actionsPublisher.sink { [weak self] action in
40+
MXLog.info("Coordinator: received view model action: \(action)")
41+
42+
guard let self else { return }
43+
switch action {
44+
case .showSettings:
45+
actionsSubject.send(.showSettings)
46+
}
47+
}
48+
.store(in: &cancellables)
49+
}
50+
51+
func toPresentable() -> AnyView {
52+
AnyView(SpaceListScreen(context: viewModel.context))
53+
}
54+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//
2+
// Copyright 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 Foundation
9+
10+
enum SpaceListScreenViewModelAction {
11+
case showSettings
12+
}
13+
14+
struct SpaceListScreenViewState: BindableState {
15+
let userID: String
16+
var userDisplayName: String?
17+
var userAvatarURL: URL?
18+
19+
var rooms: [HomeScreenRoom]
20+
var joinedRoomsCount: Int
21+
22+
var bindings: SpaceListScreenViewStateBindings
23+
24+
var subtitle: String {
25+
L10n.screenSpaceListDetails(L10n.commonSpaces(rooms.count), L10n.commonRooms(joinedRoomsCount))
26+
}
27+
}
28+
29+
struct SpaceListScreenViewStateBindings { }
30+
31+
enum SpaceListScreenViewAction {
32+
case showSettings
33+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// Copyright 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 Combine
9+
import SwiftUI
10+
11+
typealias SpaceListScreenViewModelType = StateStoreViewModelV2<SpaceListScreenViewState, SpaceListScreenViewAction>
12+
13+
class SpaceListScreenViewModel: SpaceListScreenViewModelType, SpaceListScreenViewModelProtocol {
14+
private let actionsSubject: PassthroughSubject<SpaceListScreenViewModelAction, Never> = .init()
15+
var actionsPublisher: AnyPublisher<SpaceListScreenViewModelAction, Never> {
16+
actionsSubject.eraseToAnyPublisher()
17+
}
18+
19+
init(userSession: UserSessionProtocol) {
20+
super.init(initialViewState: SpaceListScreenViewState(userID: userSession.clientProxy.userID,
21+
rooms: [],
22+
joinedRoomsCount: 0,
23+
bindings: .init()),
24+
mediaProvider: userSession.mediaProvider)
25+
26+
userSession.clientProxy.userAvatarURLPublisher
27+
.receive(on: DispatchQueue.main)
28+
.weakAssign(to: \.state.userAvatarURL, on: self)
29+
.store(in: &cancellables)
30+
31+
userSession.clientProxy.userDisplayNamePublisher
32+
.receive(on: DispatchQueue.main)
33+
.weakAssign(to: \.state.userDisplayName, on: self)
34+
.store(in: &cancellables)
35+
}
36+
37+
// MARK: - Public
38+
39+
override func process(viewAction: SpaceListScreenViewAction) {
40+
MXLog.info("View model: received view action: \(viewAction)")
41+
42+
switch viewAction {
43+
case .showSettings:
44+
actionsSubject.send(.showSettings)
45+
}
46+
}
47+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//
2+
// Copyright 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 Combine
9+
10+
@MainActor
11+
protocol SpaceListScreenViewModelProtocol {
12+
var actionsPublisher: AnyPublisher<SpaceListScreenViewModelAction, Never> { get }
13+
var context: SpaceListScreenViewModelType.Context { get }
14+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//
2+
// Copyright 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 SpaceListScreen: View {
12+
@Bindable var context: SpaceListScreenViewModel.Context
13+
14+
var body: some View {
15+
ScrollView {
16+
LazyVStack(spacing: 0) {
17+
header
18+
}
19+
}
20+
.navigationTitle(L10n.screenSpaceListTitle)
21+
.navigationBarTitleDisplayMode(.inline)
22+
.toolbar { toolbar }
23+
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
24+
.bloom()
25+
}
26+
27+
var header: some View {
28+
VStack(spacing: 16) {
29+
BigIcon(icon: \.spaceSolid)
30+
31+
VStack(spacing: 8) {
32+
Text(L10n.screenSpaceListTitle)
33+
.font(.compound.headingLGBold)
34+
.foregroundStyle(.compound.textPrimary)
35+
.multilineTextAlignment(.center)
36+
37+
Text(context.viewState.subtitle)
38+
.font(.compound.bodyLG)
39+
.foregroundStyle(.compound.textSecondary)
40+
.multilineTextAlignment(.center)
41+
}
42+
43+
Text(L10n.screenSpaceListDescription)
44+
.font(.compound.bodyMD)
45+
.foregroundStyle(.compound.textPrimary)
46+
.multilineTextAlignment(.center)
47+
}
48+
.frame(maxWidth: .infinity)
49+
.padding(.horizontal, 16)
50+
.padding(.top, 32)
51+
.padding(.bottom, 24)
52+
.overlay(alignment: .bottom) {
53+
Rectangle()
54+
.fill(Color.compound.borderDisabled)
55+
.frame(height: 1 / UIScreen.main.scale)
56+
}
57+
}
58+
59+
@ToolbarContentBuilder
60+
var toolbar: some ToolbarContent {
61+
ToolbarItem(placement: .navigationBarLeading) {
62+
Button {
63+
context.send(viewAction: .showSettings)
64+
} label: {
65+
LoadableAvatarImage(url: context.viewState.userAvatarURL,
66+
name: context.viewState.userDisplayName,
67+
contentID: context.viewState.userID,
68+
avatarSize: .user(on: .home),
69+
mediaProvider: context.mediaProvider)
70+
.accessibilityIdentifier(A11yIdentifiers.homeScreen.userAvatar)
71+
.compositingGroup()
72+
}
73+
.accessibilityLabel(L10n.commonSettings)
74+
}
75+
76+
ToolbarItem(placement: .principal) {
77+
// Hides the navigationTitle (which is set for the navigation stack label).
78+
Text("").accessibilityHidden(true)
79+
}
80+
}
81+
}
82+
83+
// MARK: - Previews
84+
85+
struct SpaceListScreen_Previews: PreviewProvider, TestablePreview {
86+
static let viewModel = makeViewModel()
87+
88+
static var previews: some View {
89+
NavigationStack {
90+
SpaceListScreen(context: viewModel.context)
91+
}
92+
}
93+
94+
static func makeViewModel(counterValue: Int = 0) -> SpaceListScreenViewModel {
95+
let clientProxy = ClientProxyMock(.init())
96+
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
97+
let viewModel = SpaceListScreenViewModel(userSession: userSession)
98+
99+
return viewModel
100+
}
101+
}

PreviewTests/Sources/GeneratedPreviewTests.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,12 @@ extension PreviewTests {
875875
}
876876
}
877877

878+
func testSpaceListScreen() async throws {
879+
for (index, preview) in SpaceListScreen_Previews._allPreviews.enumerated() {
880+
try await assertSnapshots(matching: preview, step: index)
881+
}
882+
}
883+
878884
func testSplashScreen() async throws {
879885
for (index, preview) in SplashScreen_Previews._allPreviews.enumerated() {
880886
try await assertSnapshots(matching: preview, step: index)

0 commit comments

Comments
 (0)