Implement the SpaceAddRoomsScreen. (#4951)

* Update proxies ready for adding rooms to a space.

- ClientProxy.recentlyVisitedRooms to take a filter and to return room proxies.
- Expose SpaceServiceProxy.addChildToSpace.

* Implement the SpaceAddRoomsScreen.

* Fix the designs of the selected carousels.

Add more snapshot tests for inviting users.

* Fix for screen using wrong avatar size.
This commit is contained in:
Doug
2026-01-14 12:07:57 +00:00
committed by GitHub
parent 530e249eee
commit cde9714a83
61 changed files with 1060 additions and 102 deletions
@@ -0,0 +1,142 @@
//
// 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 SpaceAddRoomsScreenViewModelType = StateStoreViewModelV2<SpaceAddRoomsScreenViewState, SpaceAddRoomsScreenViewAction>
class SpaceAddRoomsScreenViewModel: SpaceAddRoomsScreenViewModelType, SpaceAddRoomsScreenViewModelProtocol {
private let spaceRoomListProxy: SpaceRoomListProxyProtocol
private let spaceServiceProxy: SpaceServiceProxyProtocol
private let roomSummaryProvider: RoomSummaryProviderProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private var suggestedRooms: [SpaceAddRoomsScreenRoom] = []
private var actionsSubject: PassthroughSubject<SpaceAddRoomsScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<SpaceAddRoomsScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(spaceRoomListProxy: SpaceRoomListProxyProtocol,
userSession: UserSessionProtocol,
roomSummaryProvider: RoomSummaryProviderProtocol,
userIndicatorController: UserIndicatorControllerProtocol) {
self.spaceRoomListProxy = spaceRoomListProxy
spaceServiceProxy = userSession.clientProxy.spaceService
self.roomSummaryProvider = roomSummaryProvider
self.userIndicatorController = userIndicatorController
super.init(initialViewState: SpaceAddRoomsScreenViewState(roomsSection: .init(type: .suggestions, rooms: [])),
mediaProvider: userSession.mediaProvider)
roomSummaryProvider.roomListPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.updateRooms()
}
.store(in: &cancellables)
Task {
let existingRooms = spaceRoomListProxy.spaceRoomsPublisher.value
suggestedRooms = await userSession.clientProxy
.recentlyVisitedRooms { roomProxy in
!roomProxy.infoPublisher.value.isDirect && !existingRooms.contains { $0.id == roomProxy.id }
}
.map { .init(roomProxy: $0) }
if state.roomsSection.type == .suggestions {
state.roomsSection = .init(type: .suggestions, rooms: suggestedRooms)
}
}
}
override func process(viewAction: SpaceAddRoomsScreenViewAction) {
switch viewAction {
case .cancel:
actionsSubject.send(.dismiss)
case .reachedTop:
updateVisibleRange(edge: .top)
case .reachedBottom:
updateVisibleRange(edge: .bottom)
case .searchQueryChanged where state.bindings.searchQuery.isEmpty:
roomSummaryProvider.setFilter(.all(filters: []))
case .searchQueryChanged:
roomSummaryProvider.setFilter(.search(query: state.bindings.searchQuery))
case .toggleRoom(let room):
toggleRoom(room)
case .save:
Task { await save() }
}
}
func stop() {
// This is a shared provider so we should reset the filtering when we are done with the view
roomSummaryProvider.setFilter(.all(filters: []))
}
// MARK: - Private
private func updateRooms() {
guard !state.bindings.searchQuery.isEmpty else {
state.roomsSection = .init(type: .suggestions, rooms: suggestedRooms)
return
}
let existingRooms = spaceRoomListProxy.spaceRoomsPublisher.value
var rooms = [SpaceAddRoomsScreenRoom]()
for summary in roomSummaryProvider.roomListPublisher.value where !summary.isDirect && !existingRooms.contains(where: { $0.id == summary.id }) {
rooms.append(.init(summary: summary))
}
state.roomsSection = .init(type: .searchResults, rooms: rooms)
// rooms crap
}
/// The actual range values don't matter as long as they contain the lower
/// or upper bounds. updateVisibleRange is a hybrid API that powers both
/// sliding sync visible range update and list paginations
/// For lists other than the home screen one we don't care about visible ranges,
/// we just need the respective bounds to be there to trigger a next page load or
/// a reset to just one page
private func updateVisibleRange(edge: UIRectEdge) {
switch edge {
case .top:
roomSummaryProvider.updateVisibleRange(0..<0)
case .bottom:
let roomCount = roomSummaryProvider.roomListPublisher.value.count
roomSummaryProvider.updateVisibleRange(roomCount..<roomCount)
default:
break
}
}
private func toggleRoom(_ room: SpaceAddRoomsScreenRoom) {
if state.selectedRooms.contains(room) {
state.selectedRooms.removeAll { $0.id == room.id }
} else {
state.selectedRooms.append(room)
withElementAnimation(.easeInOut) { state.bindings.selectedRoomsPosition = room.id }
}
}
private func save() async {
for room in state.selectedRooms {
if case .failure(let error) = await spaceServiceProxy.addChild(room.id, to: spaceRoomListProxy.id) {
MXLog.error("Failed adding room to space: \(error)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
updateRooms() // Hide any rooms that were already added.
return
}
}
actionsSubject.send(.dismiss)
}
}