// // 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 class SpaceAddRoomsScreenViewModel: SpaceAddRoomsScreenViewModelType, SpaceAddRoomsScreenViewModelProtocol { private let spaceRoomListProxy: SpaceRoomListProxyProtocol private let spaceServiceProxy: SpaceServiceProxyProtocol private let roomSummaryProvider: RoomSummaryProviderProtocol private let userIndicatorController: UserIndicatorControllerProtocol private var suggestedRooms: [SpaceAddRoomsScreenRoom] = [] /// Keep known children excluded while the space list resets and reloads after saving. private var existingRoomIDs: Set private var actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } init(spaceRoomListProxy: SpaceRoomListProxyProtocol, userSession: UserSessionProtocol, roomSummaryProvider: RoomSummaryProviderProtocol, userIndicatorController: UserIndicatorControllerProtocol) { self.spaceRoomListProxy = spaceRoomListProxy existingRoomIDs = Set(spaceRoomListProxy.spaceRoomsPublisher.value.map(\.id)) spaceServiceProxy = userSession.clientProxy.spaceService self.roomSummaryProvider = roomSummaryProvider self.userIndicatorController = userIndicatorController super.init(initialViewState: SpaceAddRoomsScreenViewState(roomsSection: .init(type: .suggestions, rooms: [])), mediaProvider: userSession.mediaProvider) spaceRoomListProxy.spaceRoomsPublisher .receive(on: DispatchQueue.main) .sink { [weak self] spaceRooms in guard let self else { return } existingRoomIDs.formUnion(spaceRooms.map(\.id)) updateRooms() } .store(in: &cancellables) roomSummaryProvider.roomListPublisher .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.updateRooms() } .store(in: &cancellables) Task { [existingRoomIDs] in // Filter before the recent-room limit so existing children do not displace suggestions. suggestedRooms = await userSession.clientProxy .recentlyVisitedRooms { @MainActor roomProxy in !roomProxy.infoPublisher.value.isDirect && !roomProxy.infoPublisher.value.isSpace && roomProxy.infoPublisher.value.membership == .joined && !existingRoomIDs.contains(roomProxy.id) } .map { .init(roomProxy: $0) } updateRooms() } } 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.filter { !existingRoomIDs.contains($0.id) }) return } let rooms = roomSummaryProvider.roomListPublisher.value .lazy .filter { [existingRoomIDs] summary in !summary.isDirect && !existingRoomIDs.contains(summary.id) } .map(SpaceAddRoomsScreenRoom.init) state.roomsSection = .init(type: .searchResults, rooms: Array(rooms)) } /// 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..