Space filters (#4977)

* Add support for SpaceFilters on the SpaceServiceProxy level and a screen that renders them

* Add aliases to a couple of space room mocks

* Update translations and related snapshots

* Add a feature flag for space filters

* Add a new room summary provider filter that takes a list of room identifiers next to the normal state filters

This will take the SpaceFilter's descendants and combine it with the normal filters to reduce the rooms shown

* Add a new space filter button to the home screen and link it to the SpaceFiltersScreen and the actual RoomSummaryProvider room list filters

* Bump the RustSDK to v26.01.20-2 for SpaceFilter support.

* Add a cancellation button to the space filters screen

* Make the filter presentation mode liquidy, move the sheet to within the home screen.

* Address PR comments
This commit is contained in:
Stefan Ceriu
2026-01-21 22:19:39 +02:00
committed by GitHub
parent 0d34a28ec2
commit b4b9f6affa
41 changed files with 808 additions and 13 deletions
@@ -15,6 +15,7 @@ typealias HomeScreenViewModelType = StateStoreViewModel<HomeScreenViewState, Hom
class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol {
private let userSession: UserSessionProtocol
private let spaceFilterSubject: CurrentValueSubject<SpaceServiceFilter?, Never>
private let analyticsService: AnalyticsService
private let appSettings: AppSettings
private let notificationManager: NotificationManagerProtocol
@@ -39,9 +40,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
self.notificationManager = notificationManager
self.userIndicatorController = userIndicatorController
spaceFilterSubject = CurrentValueSubject<SpaceServiceFilter?, Never>(nil)
roomSummaryProvider = userSession.clientProxy.roomSummaryProvider
super.init(initialViewState: .init(userID: userSession.clientProxy.userID,
spaceFiltersEnabled: appSettings.spaceFiltersEnabled,
bindings: .init(filtersState: .init(appSettings: appSettings))),
mediaProvider: userSession.mediaProvider)
@@ -112,12 +116,22 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
}
.store(in: &cancellables)
appSettings.$spaceFiltersEnabled
.receive(on: DispatchQueue.main)
.weakAssign(to: \.state.spaceFiltersEnabled, on: self)
.store(in: &cancellables)
userSession.clientProxy.hideInviteAvatarsPublisher
.removeDuplicates()
.receive(on: DispatchQueue.main)
.weakAssign(to: \.state.hideInviteAvatars, on: self)
.store(in: &cancellables)
spaceFilterSubject
.receive(on: DispatchQueue.main)
.weakAssign(to: \.state.selectedSpaceFilter, on: self)
.store(in: &cancellables)
Task {
state.reportRoomEnabled = await userSession.clientProxy.isReportRoomSupported
}
@@ -126,9 +140,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
let searchQuery = context.$viewState.map(\.bindings.searchQuery)
let activeFilters = context.$viewState.map(\.bindings.filtersState.activeFilters)
isSearchFieldFocused
.combineLatest(searchQuery, activeFilters)
.combineLatest(searchQuery, activeFilters, spaceFilterSubject)
.removeDuplicates { $0 == $1 }
.sink { [weak self] isSearchFieldFocused, _, _ in
.sink { [weak self] isSearchFieldFocused, _, _, _ in
guard let self else { return }
// isSearchFieldFocused` is sometimes turning to true after cancelling the search. So to be extra sure we are updating the values correctly we read them directly in the next run loop, and we add a small delay if the value has changed
let delay = isSearchFieldFocused == self.context.viewState.bindings.isSearchFieldFocused ? 0.0 : 0.05
@@ -175,6 +189,26 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
actionsSubject.send(.presentStartChatScreen)
case .globalSearch:
actionsSubject.send(.presentGlobalSearch)
case .spaceFilters:
if spaceFilterSubject.value != nil {
spaceFilterSubject.send(nil)
} else {
state.bindings.spaceFiltersViewModel = ChatsSpaceFiltersScreenViewModel(spaceService: userSession.clientProxy.spaceService,
mediaProvider: userSession.mediaProvider)
state.bindings.spaceFiltersViewModel?.actionsPublisher.sink { [weak self] action in
guard let self else { return }
switch action {
case .confirm(let spaceServiceFilter):
spaceFilterSubject.send(spaceServiceFilter)
state.bindings.spaceFiltersViewModel = nil
case .cancel:
state.bindings.spaceFiltersViewModel = nil
}
}
.store(in: &cancellables)
}
case .markRoomAsUnread(let roomIdentifier):
Task {
guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomIdentifier) else {
@@ -242,7 +276,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
if state.bindings.isSearchFieldFocused {
roomSummaryProvider?.setFilter(.search(query: state.bindings.searchQuery))
} else {
roomSummaryProvider?.setFilter(.all(filters: state.bindings.filtersState.activeFilters.set))
if let spaceFilter = spaceFilterSubject.value {
roomSummaryProvider?.setFilter(.rooms(roomsIDs: spaceFilter.descendants,
filters: state.bindings.filtersState.activeFilters.set))
} else {
roomSummaryProvider?.setFilter(.all(filters: state.bindings.filtersState.activeFilters.set))
}
}
}
}