Add a SpaceScreen for listing rooms and subspaces within a space. (#4412)

This commit is contained in:
Doug
2025-08-14 17:24:20 +01:00
committed by GitHub
parent de626186bd
commit 8000dbe29f
34 changed files with 756 additions and 93 deletions
+4
View File
@@ -624,6 +624,8 @@ internal enum L10n {
internal static var commonSomethingWentWrong: String { return L10n.tr("Localizable", "common_something_went_wrong") }
/// We encountered an issue. Please try again.
internal static var commonSomethingWentWrongMessage: String { return L10n.tr("Localizable", "common_something_went_wrong_message") }
/// Space
internal static var commonSpace: String { return L10n.tr("Localizable", "common_space") }
/// Plural format key: "%#@COUNT@"
internal static func commonSpaces(_ p1: Int) -> String {
return L10n.tr("Localizable", "common_spaces", p1)
@@ -2581,6 +2583,8 @@ internal enum L10n {
internal static var screenRoomlistFilterInvitesEmptyStateTitle: String { return L10n.tr("Localizable", "screen_roomlist_filter_invites_empty_state_title") }
/// Low Priority
internal static var screenRoomlistFilterLowPriority: String { return L10n.tr("Localizable", "screen_roomlist_filter_low_priority") }
/// You don’t have any low priority chats yet
internal static var screenRoomlistFilterLowPriorityEmptyStateTitle: String { return L10n.tr("Localizable", "screen_roomlist_filter_low_priority_empty_state_title") }
/// You can deselect filters in order to see your other chats
internal static var screenRoomlistFilterMixedEmptyStateSubtitle: String { return L10n.tr("Localizable", "screen_roomlist_filter_mixed_empty_state_subtitle") }
/// You don’t have chats for this selection
@@ -5,19 +5,43 @@
// Please see LICENSE files in the repository root for full details.
//
import Combine
import Foundation
import MatrixRustSDK
extension SpaceRoomListProxyMock {
struct Configuration {
class Configuration {
var spaceRoomProxy: SpaceRoomProxyProtocol
var spaceRooms: [SpaceRoomProxyProtocol] = []
var initialSpaceRooms: [SpaceRoomProxyProtocol]
var paginationStateSubject: CurrentValueSubject<SpaceRoomListProxyPaginationState, Never>
var paginationResponses: [[SpaceRoomProxyProtocol]]
init(spaceRoomProxy: SpaceRoomProxyProtocol,
initialSpaceRooms: [SpaceRoomProxyProtocol] = [],
paginationStateSubject: CurrentValueSubject<SpaceRoomListProxyPaginationState, Never> = .init(.idle(endReached: true)),
paginationResponses: [[SpaceRoomProxyProtocol]] = []) {
self.spaceRoomProxy = spaceRoomProxy
self.initialSpaceRooms = initialSpaceRooms
self.paginationStateSubject = paginationStateSubject
self.paginationResponses = paginationResponses
}
}
convenience init(_ configuration: Configuration) {
self.init()
let spaceRoomsSubject: CurrentValueSubject<[SpaceRoomProxyProtocol], Never> = .init(configuration.initialSpaceRooms)
spaceRoom = configuration.spaceRoomProxy
spaceRoomsPublisher = .init(configuration.spaceRooms)
spaceRoomsPublisher = spaceRoomsSubject.asCurrentValuePublisher()
paginationStatePublisher = configuration.paginationStateSubject.asCurrentValuePublisher()
paginateClosure = {
configuration.paginationStateSubject.send(.loading)
try? await Task.sleep(for: .milliseconds(100))
let newRooms = configuration.paginationResponses.removeFirst()
spaceRoomsSubject.send(spaceRoomsSubject.value + newRooms)
configuration.paginationStateSubject.send(.idle(endReached: configuration.paginationResponses.isEmpty))
}
}
}
@@ -46,3 +46,79 @@ extension SpaceRoomProxyMock {
state = configuration.state
}
}
extension [SpaceRoomProxyProtocol] {
static var mockJoinedSpaces: [SpaceRoomProxyMock] {
[
SpaceRoomProxyMock(.init(id: "space1",
name: "The Foundation",
isSpace: true,
childrenCount: 1,
joinedMembersCount: 500,
state: .joined)),
SpaceRoomProxyMock(.init(id: "space2",
name: "The Second Foundation",
isSpace: true,
childrenCount: 1,
joinedMembersCount: 100,
state: .joined)),
SpaceRoomProxyMock(.init(id: "space3",
name: "The Galactic Empire",
isSpace: true,
childrenCount: 25000,
joinedMembersCount: 1_000_000_000,
state: .joined)),
SpaceRoomProxyMock(.init(id: "space4",
name: "The Korellians",
isSpace: true,
childrenCount: 27,
joinedMembersCount: 2_000_000,
state: .joined)),
SpaceRoomProxyMock(.init(id: "space5",
name: "The Luminists",
isSpace: true,
childrenCount: 1,
joinedMembersCount: 100_000,
state: .joined)),
SpaceRoomProxyMock(.init(id: "space6",
name: "The Anacreons",
isSpace: true,
childrenCount: 25,
joinedMembersCount: 400_000,
state: .joined)),
SpaceRoomProxyMock(.init(id: "space7",
name: "The Thespians",
isSpace: true,
childrenCount: 15,
joinedMembersCount: 300_000,
state: .joined))
]
}
static var mockSpaceList: [SpaceRoomProxyProtocol] {
makeSpaceRooms(isSpace: true) + makeSpaceRooms(isSpace: false)
}
private static func makeSpaceRooms(isSpace: Bool) -> [SpaceRoomProxyMock] {
let typeName = isSpace ? "Space" : "Room"
return [
SpaceRoomProxyMock(.init(id: "!\(typeName.lowercased())1:matrix.org",
name: "Company \(typeName)",
isSpace: isSpace)),
SpaceRoomProxyMock(.init(id: "!\(typeName.lowercased())2:matrix.org",
name: "Public \(typeName)",
avatarURL: .mockMXCAvatar,
isSpace: isSpace,
joinedMembersCount: 78,
topic: "Discussion on specific topic goes here.",
joinRule: .public)),
SpaceRoomProxyMock(.init(id: "!\(typeName.lowercased())3:matrix.org",
name: "Joined \(typeName)",
isSpace: isSpace,
joinedMembersCount: 123,
topic: "Discussion on specific topic goes here.",
state: .joined))
]
}
}
@@ -25,6 +25,10 @@ extension UserProfileProxy {
.init(userID: "@charlie:matrix.org", displayName: "Charlie", avatarURL: nil)
}
static var mockDan: UserProfileProxy {
.init(userID: "@dan:matrix.org", displayName: "Dan", avatarURL: .mockMXCUserAvatar)
}
static var mockVerbose: UserProfileProxy {
.init(userID: "@charlie:matrix.org", displayName: "Charlie is the best display name", avatarURL: nil)
}
+3
View File
@@ -71,6 +71,7 @@ enum UserAvatarSizeOnScreen {
case readReceipt
case readReceiptSheet
case editUserDetails
case spaceHeader
case completionSuggestions
case blockedUsers
case knockingUsersBannerStack
@@ -91,6 +92,8 @@ enum UserAvatarSizeOnScreen {
return 16
case .readReceiptSheet:
return 32
case .spaceHeader:
return 20
case .completionSuggestions:
return 32
case .blockedUsers:
@@ -153,8 +153,10 @@ enum TestablePreviewsDictionary {
"SettingsScreen_Previews" : SettingsScreen_Previews.self,
"ShimmerOverlay_Previews" : ShimmerOverlay_Previews.self,
"SoftLogoutScreen_Previews" : SoftLogoutScreen_Previews.self,
"SpaceHeaderView_Previews" : SpaceHeaderView_Previews.self,
"SpaceListScreen_Previews" : SpaceListScreen_Previews.self,
"SpaceRoomCell_Previews" : SpaceRoomCell_Previews.self,
"SpaceScreen_Previews" : SpaceScreen_Previews.self,
"SplashScreen_Previews" : SplashScreen_Previews.self,
"StackedAvatarsView_Previews" : StackedAvatarsView_Previews.self,
"StartChatScreen_Previews" : StartChatScreen_Previews.self,
@@ -0,0 +1,195 @@
//
// Copyright 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 SpaceHeaderView: View {
let spaceRoomProxy: SpaceRoomProxyProtocol
let mediaProvider: MediaProviderProtocol?
var title: String { spaceRoomProxy.name ?? "" }
var body: some View {
VStack(spacing: 16) {
RoomAvatarImage(avatar: spaceRoomProxy.avatar,
avatarSize: .room(on: .spaceHeader),
mediaProvider: mediaProvider)
VStack(spacing: 8) {
Text(title)
.font(.compound.headingLGBold)
.foregroundStyle(.compound.textPrimary)
.multilineTextAlignment(.center)
spaceDetails
SpaceHeaderMembersView(heroes: spaceRoomProxy.heroes,
joinedCount: spaceRoomProxy.joinedMembersCount,
mediaProvider: mediaProvider)
}
if let topic = spaceRoomProxy.topic {
Text(topic)
.font(.compound.bodyMD)
.foregroundStyle(.compound.textPrimary)
.multilineTextAlignment(.center)
.lineLimit(2)
}
}
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity)
.padding(.horizontal, 16)
.padding(.top, 32)
.padding(.bottom, 24)
.overlay(alignment: .bottom) {
Rectangle()
.fill(Color.compound.borderDisabled)
.frame(height: 1 / UIScreen.main.scale)
}
}
var spaceDetails: some View {
Label {
Text(L10n.screenSpaceListDetails(spaceDetailsVisibilityTitle, L10n.commonRooms(spaceRoomProxy.childrenCount)))
.font(.compound.bodyLG)
.foregroundStyle(.compound.textSecondary)
.multilineTextAlignment(.center)
} icon: {
CompoundIcon(spaceDetailsVisibilityIcon, size: .small, relativeTo: .compound.bodyLG)
.foregroundStyle(.compound.iconTertiary)
}
}
var spaceDetailsVisibilityTitle: String {
switch spaceRoomProxy.joinRule {
case .public:
L10n.commonPublicSpace
case .restricted(let rules), .knockRestricted(let rules):
// FIXME: Get this from the rule (falling back to a passed in parent??)
"<Parent name> space"
case .invite, .knock, .private, .custom, .none:
L10n.commonPrivateSpace
}
}
var spaceDetailsVisibilityIcon: KeyPath<CompoundIcons, Image> {
switch spaceRoomProxy.joinRule {
case .public:
\.public
case .restricted, .knockRestricted:
\.space
case .invite, .knock, .private, .custom, .none:
\.lock
}
}
}
import MatrixRustSDK
struct SpaceHeaderMembersView: View {
let heroes: [UserProfileProxy]
let joinedCount: Int
let mediaProvider: MediaProviderProtocol?
var body: some View {
if heroes.isEmpty {
Label(title: title) {
CompoundIcon(\.userProfile, size: .small, relativeTo: .compound.bodyMD)
.foregroundStyle(.compound.textSecondary)
}
.font(.compound.bodyMD)
.foregroundStyle(.compound.textSecondary)
.labelStyle(.custom(spacing: 4))
.padding(.trailing, 8)
.background(.compound.bgSubtleSecondary, in: Capsule())
} else {
Label(title: title) {
heroesFacePile
}
.font(.compound.bodyMD)
.foregroundStyle(.compound.textSecondary)
.labelStyle(.custom(spacing: 6))
}
}
func title() -> Text {
Text("\(joinedCount)")
}
var heroesFacePile: some View {
HStack(spacing: -8) {
ForEach(heroes.prefix(3).reversed()) { hero in
LoadableAvatarImage(url: hero.avatarURL,
name: hero.displayName,
contentID: hero.userID,
avatarSize: .user(on: .spaceHeader),
mediaProvider: mediaProvider)
.mask {
Circle()
.fill(Color.white)
.overlay {
if hero != heroes.first {
Circle()
.inset(by: -2)
.fill(Color.black)
.offset(x: 12)
}
}
.compositingGroup()
.luminanceToAlpha()
}
}
}
}
}
struct SpaceHeaderView_Previews: PreviewProvider, TestablePreview {
static let mediaProvider = MediaProviderMock(configuration: .init())
static let spaces = makeSpaceRooms()
static var previews: some View {
VStack(spacing: 0) {
ForEach(spaces, id: \.id) { space in
SpaceHeaderView(spaceRoomProxy: space, mediaProvider: mediaProvider)
}
}
}
static func makeSpaceRooms() -> [SpaceRoomProxyMock] {
[
SpaceRoomProxyMock(.init(id: "!space1:matrix.org",
name: "Company Space",
isSpace: true,
childrenCount: 10,
joinedMembersCount: 50)),
SpaceRoomProxyMock(.init(id: "!space2:matrix.org",
name: "Community Space",
avatarURL: .mockMXCAvatar,
isSpace: true,
childrenCount: 20,
joinedMembersCount: 78,
topic: "Description of the space goes right here.",
joinRule: .public)),
SpaceRoomProxyMock(.init(id: "!space3:matrix.org",
name: "Subspace",
isSpace: true,
childrenCount: 30,
joinedMembersCount: 123,
heroes: [.mockDan, .mockBob, .mockCharlie, .mockVerbose],
topic: ["Description of the space goes right here.",
"Lorem ipsum dolor sit amet consectetur.",
"Leo viverra morbi habitant in.",
"Sem amet enim habitant nibh augue mauris.",
"Interdum mauris ultrices tincidunt proin morbi erat aenean risus nibh.",
"Diam amet sit fermentum vulputate faucibus."].joined(separator: " "),
joinRule: .knockRestricted(rules: [.roomMembership(roomId: "")])))
]
}
}
@@ -136,8 +136,7 @@ struct SpaceRoomCellButtonStyle: ButtonStyle {
struct SpaceRoomCell_Previews: PreviewProvider, TestablePreview {
static let mediaProvider = MediaProviderMock(configuration: .init())
static let spaces = makeSpaceRooms(isSpace: true)
static let rooms = makeSpaceRooms(isSpace: false)
static let spaces = [SpaceRoomProxyProtocol].mockSpaceList
static var previews: some View {
VStack(spacing: 0) {
@@ -146,34 +145,6 @@ struct SpaceRoomCell_Previews: PreviewProvider, TestablePreview {
isSelected: false,
mediaProvider: mediaProvider) { _ in }
}
ForEach(rooms, id: \.id) { room in
SpaceRoomCell(spaceRoom: room,
isSelected: false,
mediaProvider: mediaProvider) { _ in }
}
}
}
static func makeSpaceRooms(isSpace: Bool) -> [SpaceRoomProxyMock] {
let name = isSpace ? "Space" : "Room"
return [
SpaceRoomProxyMock(.init(id: "!space1:matrix.org",
name: "Company \(name)",
isSpace: isSpace)),
SpaceRoomProxyMock(.init(id: "!space2:matrix.org",
name: "Public \(name)",
avatarURL: .mockMXCAvatar,
isSpace: isSpace,
joinedMembersCount: 78,
topic: "Discussion on specific topic goes here.",
joinRule: .public)),
SpaceRoomProxyMock(.init(id: "!space3:matrix.org",
name: "Joined \(name)",
isSpace: isSpace,
joinedMembersCount: 123,
topic: "Discussion on specific topic goes here.",
state: .joined))
]
}
}
@@ -28,6 +28,7 @@ class SpaceListScreenViewModel: SpaceListScreenViewModelType, SpaceListScreenVie
mediaProvider: userSession.mediaProvider)
spaceServiceProxy.joinedSpacesPublisher
.receive(on: DispatchQueue.main)
.weakAssign(to: \.state.joinedSpaces, on: self)
.store(in: &cancellables)
@@ -105,56 +105,9 @@ struct SpaceListScreen_Previews: PreviewProvider, TestablePreview {
static func makeViewModel(counterValue: Int = 0) -> SpaceListScreenViewModel {
let clientProxy = ClientProxyMock(.init())
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
let spaceService = SpaceServiceProxyMock(
.init(
joinedSpaces: [
SpaceRoomProxyMock(.init(id: "space1",
name: "The Foundation",
isSpace: true,
childrenCount: 1,
joinedMembersCount: 500,
state: .joined)),
SpaceRoomProxyMock(.init(id: "space2",
name: "The Second Foundation",
isSpace: true,
childrenCount: 1,
joinedMembersCount: 100,
state: .joined)),
SpaceRoomProxyMock(.init(id: "space3",
name: "The Galactic Empire",
isSpace: true,
childrenCount: 25000,
joinedMembersCount: 1_000_000_000,
state: .joined)),
SpaceRoomProxyMock(.init(id: "space4",
name: "The Korellians",
isSpace: true,
childrenCount: 27,
joinedMembersCount: 2_000_000,
state: .joined)),
SpaceRoomProxyMock(.init(id: "space5",
name: "The Luminists",
isSpace: true,
childrenCount: 1,
joinedMembersCount: 100_000,
state: .joined)),
SpaceRoomProxyMock(.init(id: "space6",
name: "The Anacreons",
isSpace: true,
childrenCount: 25,
joinedMembersCount: 400_000,
state: .joined)),
SpaceRoomProxyMock(.init(id: "space7",
name: "The Thespians",
isSpace: true,
childrenCount: 15,
joinedMembersCount: 300_000,
state: .joined))
]
)
)
let viewModel = SpaceListScreenViewModel(userSession: userSession,
spaceServiceProxy: spaceService)
let spaceService = SpaceServiceProxyMock(.init(joinedSpaces: .mockJoinedSpaces))
let viewModel = SpaceListScreenViewModel(userSession: userSession, spaceServiceProxy: spaceService)
return viewModel
}
@@ -0,0 +1,55 @@
//
// Copyright 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.
//
// periphery:ignore:all - this is just a space remove this comment once generating the final file
import Combine
import SwiftUI
struct SpaceScreenCoordinatorParameters {
let spaceRoomListProxy: SpaceRoomListProxyProtocol
let mediaProvider: MediaProviderProtocol
}
enum SpaceScreenCoordinatorAction {
case selectSpace(SpaceRoomProxyProtocol)
}
final class SpaceScreenCoordinator: CoordinatorProtocol {
private let parameters: SpaceScreenCoordinatorParameters
private let viewModel: SpaceScreenViewModelProtocol
private var cancellables = Set<AnyCancellable>()
private let actionsSubject: PassthroughSubject<SpaceScreenCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<SpaceScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: SpaceScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = SpaceScreenViewModel(spaceRoomList: parameters.spaceRoomListProxy, mediaProvider: parameters.mediaProvider)
}
func start() {
viewModel.actionsPublisher.sink { [weak self] action in
MXLog.info("Coordinator: received view model action: \(action)")
guard let self else { return }
switch action {
case .selectSpace(let spaceRoom):
actionsSubject.send(.selectSpace(spaceRoom))
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(SpaceScreen(context: viewModel.context))
}
}
@@ -0,0 +1,27 @@
//
// Copyright 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 Foundation
enum SpaceScreenViewModelAction {
case selectSpace(SpaceRoomProxyProtocol)
}
struct SpaceScreenViewState: BindableState {
let space: SpaceRoomProxyProtocol
var isPaginating = false
var rooms: [SpaceRoomProxyProtocol]
var bindings = SpaceScreenViewStateBindings()
}
struct SpaceScreenViewStateBindings { }
enum SpaceScreenViewAction {
case spaceAction(SpaceRoomCell.Action)
}
@@ -0,0 +1,60 @@
//
// Copyright 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 Combine
import SwiftUI
typealias SpaceScreenViewModelType = StateStoreViewModelV2<SpaceScreenViewState, SpaceScreenViewAction>
class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtocol {
private let actionsSubject: PassthroughSubject<SpaceScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<SpaceScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(spaceRoomList: SpaceRoomListProxyProtocol, mediaProvider: MediaProviderProtocol) {
super.init(initialViewState: SpaceScreenViewState(space: spaceRoomList.spaceRoom,
rooms: spaceRoomList.spaceRoomsPublisher.value),
mediaProvider: mediaProvider)
spaceRoomList.spaceRoomsPublisher
.receive(on: DispatchQueue.main)
.weakAssign(to: \.state.rooms, on: self)
.store(in: &cancellables)
spaceRoomList.paginationStatePublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] paginationState in
switch paginationState {
case .idle(let endReached):
self?.state.isPaginating = false
guard !endReached else { return }
Task { await spaceRoomList.paginate() }
case .loading:
self?.state.isPaginating = true
}
}
.store(in: &cancellables)
}
// MARK: - Public
override func process(viewAction: SpaceScreenViewAction) {
MXLog.info("View model: received view action: \(viewAction)")
switch viewAction {
case .spaceAction(.select(let spaceRoom)):
if spaceRoom.isSpace {
actionsSubject.send(.selectSpace(spaceRoom))
} else {
#warning("Implement joining")
}
case .spaceAction(.join(let spaceID)):
#warning("Implement joining.")
}
}
}
@@ -0,0 +1,14 @@
//
// Copyright 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 Combine
@MainActor
protocol SpaceScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<SpaceScreenViewModelAction, Never> { get }
var context: SpaceScreenViewModelType.Context { get }
}
@@ -0,0 +1,71 @@
//
// Copyright 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 SpaceScreen: View {
@Bindable var context: SpaceScreenViewModel.Context
var body: some View {
ScrollView {
LazyVStack(spacing: 0) {
SpaceHeaderView(spaceRoomProxy: context.viewState.space,
mediaProvider: context.mediaProvider)
rooms
}
}
.navigationTitle(context.viewState.space.name ?? L10n.commonSpace)
.navigationBarTitleDisplayMode(.inline)
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
}
@ViewBuilder
var rooms: some View {
ForEach(context.viewState.rooms, id: \.id) { spaceRoom in
SpaceRoomCell(spaceRoom: spaceRoom,
isSelected: false,
mediaProvider: context.mediaProvider) { action in
context.send(viewAction: .spaceAction(action))
}
}
if context.viewState.isPaginating {
ProgressView()
.padding()
}
}
}
// MARK: - Previews
struct SpaceScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = makeViewModel()
static var previews: some View {
NavigationStack {
SpaceScreen(context: viewModel.context)
}
}
static func makeViewModel() -> SpaceScreenViewModel {
let spaceRoomProxy = SpaceRoomProxyMock(.init(id: "!eng-space:matrix.org",
name: "Engineering Team",
isSpace: true,
childrenCount: 30,
joinedMembersCount: 76,
heroes: [.mockDan, .mockBob, .mockCharlie, .mockVerbose],
topic: "Description of the space goes right here. Lorem ipsum dolor sit amet consectetur. Leo viverra morbi habitant in.",
joinRule: .knockRestricted(rules: [.roomMembership(roomId: "")])))
let spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceRoomProxy: spaceRoomProxy,
initialSpaceRooms: .mockSpaceList))
let viewModel = SpaceScreenViewModel(spaceRoomList: spaceRoomListProxy,
mediaProvider: MediaProviderMock(configuration: .init()))
return viewModel
}
}
@@ -9,7 +9,7 @@ import Combine
import MatrixRustSDK
// Temporary until the SDK is updated.
enum SpaceRoomListProxyPaginationState {
enum SpaceRoomListProxyPaginationState: Equatable {
case idle(endReached: Bool)
case loading
}