Room List Filters implementation (#2423)
This commit is contained in:
@@ -303,6 +303,7 @@
|
||||
4BB51476A29E7E27BC14EA22 /* UserDetailsEditScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022E6BD64CB4610B9C95FC02 /* UserDetailsEditScreenViewModel.swift */; };
|
||||
4C356F5CCB4CDC99BFA45185 /* AppLockSetupPINScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7884BD256C091EB511B2EDF /* AppLockSetupPINScreenViewModelProtocol.swift */; };
|
||||
4C5A638DAA8AF64565BA4866 /* EncryptedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5351EBD7A0B9610548E4B7B2 /* EncryptedRoomTimelineItem.swift */; };
|
||||
4C8C0C9FC10BA73AB7780534 /* RoomListFiltersStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */; };
|
||||
4E0D9E09B52CEC4C0E6211A8 /* MediaPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F49FB9EE2913234F06CE68 /* MediaPickerScreenCoordinator.swift */; };
|
||||
4E8A2A2CFEB212F14E49E1A1 /* AppLockSetupSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5484457C81B325660901B161 /* AppLockSetupSettingsScreen.swift */; };
|
||||
4E8F17EBA24FBBA6ABB62ECB /* MockBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3948D16F021DFDB2CD26EAA8 /* MockBackgroundTaskService.swift */; };
|
||||
@@ -968,6 +969,7 @@
|
||||
F421FD5979EF53C8204BDC77 /* SecureBackupLogoutConfirmationScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC09F30B0E1010951952BDC /* SecureBackupLogoutConfirmationScreenUITests.swift */; };
|
||||
F4433EF57B4BB3C077F8B00E /* SessionVerificationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD9E0FFA29EAACFF3AB9732 /* SessionVerificationScreenViewModel.swift */; };
|
||||
F4971845B5C4F270F6BC5745 /* ScaledFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D82F234B3576BD6268C7950 /* ScaledFrameModifier.swift */; };
|
||||
F4996C82A4B3A5FF0C8EDD03 /* RoomListFilterModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E06AAD6D9D3F5833E7A5A2F9 /* RoomListFilterModels.swift */; };
|
||||
F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94BCC8A9C73C1F838122C645 /* TimelineItemPlainStylerView.swift */; };
|
||||
F50A6FCE26714E27FE5495DD /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50F23B21CF15F9F4BAA0788B /* PollOptionView.swift */; };
|
||||
F519DE17A3A0F760307B2E6D /* InviteUsersScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D155E09BF961BBA8F85263 /* InviteUsersScreenViewModel.swift */; };
|
||||
@@ -1565,6 +1567,7 @@
|
||||
8977176AB534AA41630395BC /* LegalInformationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
897DF5E9A70CE05A632FC8AF /* UTType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTType.swift; sourceTree = "<group>"; };
|
||||
8AB10FA6570DD08B3966C159 /* WelcomeScreenScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreenScreenUITests.swift; sourceTree = "<group>"; };
|
||||
8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFiltersStateTests.swift; sourceTree = "<group>"; };
|
||||
8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainMentionBuilder.swift; sourceTree = "<group>"; };
|
||||
8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoader.swift; sourceTree = "<group>"; };
|
||||
8BEBF0E59F25E842EDB6FD11 /* LocationSharingScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreenModels.swift; sourceTree = "<group>"; };
|
||||
@@ -1883,6 +1886,7 @@
|
||||
DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationViewModelTests.swift; sourceTree = "<group>"; };
|
||||
DF3D25B3EDB283B5807EADCF /* ReadMarkerRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
E062C1750EFC8627DE4CAB8E /* MapTilerAuthorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerAuthorization.swift; sourceTree = "<group>"; };
|
||||
E06AAD6D9D3F5833E7A5A2F9 /* RoomListFilterModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFilterModels.swift; sourceTree = "<group>"; };
|
||||
E0FCA0957FAA0E15A9F5579D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Untranslated.stringsdict; sourceTree = "<group>"; };
|
||||
E10DA51DBC8C7E1460DBCCBD /* UserProfileListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileListRow.swift; sourceTree = "<group>"; };
|
||||
E1573D28C8A9FB6399D0EEFB /* SecureBackupLogoutConfirmationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
@@ -2130,6 +2134,7 @@
|
||||
037A5661B26EC6BE068188D7 /* Filters */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E06AAD6D9D3F5833E7A5A2F9 /* RoomListFilterModels.swift */,
|
||||
E6372DD10DED30E7AD7BCE21 /* RoomListFiltersView.swift */,
|
||||
24EC819497BB5F8C4998D760 /* RoomListFilterView.swift */,
|
||||
);
|
||||
@@ -3343,6 +3348,7 @@
|
||||
00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */,
|
||||
2EFE1922F39398ABFB36DF3F /* RoomDetailsViewModelTests.swift */,
|
||||
4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */,
|
||||
8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */,
|
||||
EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */,
|
||||
69B63F817FE305548DB4B512 /* RoomMembersListViewModelTests.swift */,
|
||||
58D295F0081084F38DB20893 /* RoomNotificationSettingsScreenViewModelTests.swift */,
|
||||
@@ -5328,6 +5334,7 @@
|
||||
9DD84E014ADFB2DD813022D5 /* RoomDetailsEditScreenViewModelTests.swift in Sources */,
|
||||
EA974337FA7D040E7C74FE6E /* RoomDetailsViewModelTests.swift in Sources */,
|
||||
095D3906CF2F940C2D2D17CC /* RoomFlowCoordinatorTests.swift in Sources */,
|
||||
4C8C0C9FC10BA73AB7780534 /* RoomListFiltersStateTests.swift in Sources */,
|
||||
6B31508C6334C617360C2EAB /* RoomMemberDetailsViewModelTests.swift in Sources */,
|
||||
CAF8755E152204F55F8D6B5B /* RoomMembersListViewModelTests.swift in Sources */,
|
||||
E49F74BD93230BDEFFE5EA51 /* RoomNotificationSettingsScreenViewModelTests.swift in Sources */,
|
||||
@@ -5796,6 +5803,7 @@
|
||||
42F1C8731166633E35A6D7E6 /* RoomEventStringBuilder.swift in Sources */,
|
||||
D55AF9B5B55FEED04771A461 /* RoomFlowCoordinator.swift in Sources */,
|
||||
04A16B45228F7678A027C079 /* RoomHeaderView.swift in Sources */,
|
||||
F4996C82A4B3A5FF0C8EDD03 /* RoomListFilterModels.swift in Sources */,
|
||||
4A9CEEE612D6D8B3DDBD28BA /* RoomListFilterView.swift in Sources */,
|
||||
33F1FB19F222BA9930AB1A00 /* RoomListFiltersView.swift in Sources */,
|
||||
FA4296218444C48BC890F46B /* RoomMemberDetails.swift in Sources */,
|
||||
@@ -6763,7 +6771,7 @@
|
||||
repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 1.1.37;
|
||||
version = 1.1.38;
|
||||
};
|
||||
};
|
||||
821C67C9A7F8CC3FD41B28B4 /* XCRemoteSwiftPackageReference "emojibase-bindings" */ = {
|
||||
|
||||
@@ -129,8 +129,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/matrix-org/matrix-rust-components-swift",
|
||||
"state" : {
|
||||
"revision" : "4d0d75004f8361530d7424ab198e363027823718",
|
||||
"version" : "1.1.37"
|
||||
"revision" : "691d8b0f0994d9669fadbd2452bef7270f3713ad",
|
||||
"version" : "1.1.38"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -45,7 +45,7 @@ class GlobalSearchScreenViewModel: GlobalSearchScreenViewModelType, GlobalSearch
|
||||
.map(\.bindings.searchQuery)
|
||||
.removeDuplicates()
|
||||
.sink { [weak self] searchQuery in
|
||||
self?.roomSummaryProvider.setFilter(.normalizedMatchRoomName(searchQuery))
|
||||
self?.roomSummaryProvider.setFilter(.include(.init(query: searchQuery)))
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -60,7 +60,7 @@ class GlobalSearchScreenViewModel: GlobalSearchScreenViewModelType, GlobalSearch
|
||||
switch viewAction {
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
roomSummaryProvider.setFilter(.all) // This is a shared provider
|
||||
roomSummaryProvider.setFilter(.include(.all)) // This is a shared provider
|
||||
case .select(let roomID):
|
||||
actionsSubject.send(.select(roomID: roomID))
|
||||
case .reachedTop:
|
||||
|
||||
@@ -212,88 +212,3 @@ extension HomeScreenRoom {
|
||||
avatarURL: details.avatarURL)
|
||||
}
|
||||
}
|
||||
|
||||
enum RoomListFilter: Int, CaseIterable, Identifiable {
|
||||
var id: Int {
|
||||
rawValue
|
||||
}
|
||||
|
||||
case people
|
||||
case rooms
|
||||
case unreads
|
||||
case favourites
|
||||
case lowPriority
|
||||
|
||||
var localizedName: String {
|
||||
switch self {
|
||||
case .people:
|
||||
return L10n.screenRoomlistFilterPeople
|
||||
case .rooms:
|
||||
return L10n.screenRoomlistFilterRooms
|
||||
case .unreads:
|
||||
return L10n.screenRoomlistFilterUnreads
|
||||
case .favourites:
|
||||
return L10n.screenRoomlistFilterFavourites
|
||||
case .lowPriority:
|
||||
return L10n.screenRoomlistFilterLowPriority
|
||||
}
|
||||
}
|
||||
|
||||
var complementaryFilter: RoomListFilter? {
|
||||
switch self {
|
||||
case .people:
|
||||
return .rooms
|
||||
case .rooms:
|
||||
return .people
|
||||
case .unreads:
|
||||
return nil
|
||||
case .favourites:
|
||||
return .lowPriority
|
||||
case .lowPriority:
|
||||
return .favourites
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class RoomListFiltersState: ObservableObject {
|
||||
@Published private var enabledFilters: Set<RoomListFilter>
|
||||
|
||||
init(enabledFilters: Set<RoomListFilter> = []) {
|
||||
self.enabledFilters = enabledFilters
|
||||
}
|
||||
|
||||
var sortedEnabledFilters: [RoomListFilter] {
|
||||
enabledFilters.sorted(by: { $0.rawValue < $1.rawValue })
|
||||
}
|
||||
|
||||
var sortedAvailableFilters: [RoomListFilter] {
|
||||
var availableFilters = Set(RoomListFilter.allCases)
|
||||
for filter in enabledFilters {
|
||||
availableFilters.remove(filter)
|
||||
if let complementaryFilter = filter.complementaryFilter {
|
||||
availableFilters.remove(complementaryFilter)
|
||||
}
|
||||
}
|
||||
return availableFilters.sorted(by: { $0.rawValue < $1.rawValue })
|
||||
}
|
||||
|
||||
var isFiltering: Bool {
|
||||
!enabledFilters.isEmpty
|
||||
}
|
||||
|
||||
func set(_ filter: RoomListFilter, isEnabled: Bool) {
|
||||
if isEnabled {
|
||||
enabledFilters.insert(filter)
|
||||
} else {
|
||||
enabledFilters.remove(filter)
|
||||
}
|
||||
}
|
||||
|
||||
func clearFilters() {
|
||||
enabledFilters.removeAll()
|
||||
}
|
||||
|
||||
func isEnabled(_ filter: RoomListFilter) -> Bool {
|
||||
enabledFilters.contains(filter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,17 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
.store(in: &cancellables)
|
||||
|
||||
appSettings.$roomListFiltersEnabled
|
||||
.weakAssign(to: \.state.shouldShowFilters, on: self)
|
||||
.sink { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !value {
|
||||
state.shouldShowFilters = false
|
||||
state.filtersState.clearFilters()
|
||||
} else {
|
||||
state.shouldShowFilters = true
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
appSettings.$markAsUnreadEnabled
|
||||
@@ -96,8 +106,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
|
||||
let isSearchFieldFocused = context.$viewState.map(\.bindings.isSearchFieldFocused)
|
||||
let searchQuery = context.$viewState.map(\.bindings.searchQuery)
|
||||
let enabledFilters = context.viewState.filtersState.$activeFilters
|
||||
isSearchFieldFocused
|
||||
.combineLatest(searchQuery)
|
||||
.combineLatest(searchQuery, enabledFilters)
|
||||
.removeDuplicates { $0 == $1 }
|
||||
.map { _ in () }
|
||||
.sink { [weak self] in
|
||||
@@ -196,12 +207,13 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
|
||||
private func updateFilter() {
|
||||
if state.shouldHideRoomList {
|
||||
roomSummaryProvider?.setFilter(.none)
|
||||
roomSummaryProvider?.setFilter(.excludeAll)
|
||||
} else {
|
||||
if state.bindings.isSearchFieldFocused {
|
||||
roomSummaryProvider?.setFilter(.normalizedMatchRoomName(state.bindings.searchQuery))
|
||||
roomSummaryProvider?.setFilter(.include(.init(query: state.bindings.searchQuery,
|
||||
filters: state.filtersState.activeFilters)))
|
||||
} else {
|
||||
roomSummaryProvider?.setFilter(.all)
|
||||
roomSummaryProvider?.setFilter(.include(.init(filters: state.filtersState.activeFilters)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
enum RoomListFilter: Int, CaseIterable, Identifiable {
|
||||
var id: Int {
|
||||
rawValue
|
||||
}
|
||||
|
||||
case people
|
||||
case rooms
|
||||
case unreads
|
||||
case favourites
|
||||
|
||||
var localizedName: String {
|
||||
switch self {
|
||||
case .people:
|
||||
return L10n.screenRoomlistFilterPeople
|
||||
case .rooms:
|
||||
return L10n.screenRoomlistFilterRooms
|
||||
case .unreads:
|
||||
return L10n.screenRoomlistFilterUnreads
|
||||
case .favourites:
|
||||
return L10n.screenRoomlistFilterFavourites
|
||||
}
|
||||
}
|
||||
|
||||
var incompatibleFilter: RoomListFilter? {
|
||||
switch self {
|
||||
case .people:
|
||||
return .rooms
|
||||
case .rooms:
|
||||
return .people
|
||||
case .unreads:
|
||||
return nil
|
||||
case .favourites:
|
||||
// When we will have Low Priority we may need to return it here
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var rustFilter: RoomListEntriesDynamicFilterKind? {
|
||||
switch self {
|
||||
case .people:
|
||||
return .category(expect: .people)
|
||||
case .rooms:
|
||||
return .category(expect: .group)
|
||||
case .unreads:
|
||||
return .unread
|
||||
case .favourites:
|
||||
// Not implemented yet
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class RoomListFiltersState: ObservableObject {
|
||||
@Published private(set) var activeFilters: Set<RoomListFilter>
|
||||
|
||||
init(activeFilters: Set<RoomListFilter> = []) {
|
||||
self.activeFilters = activeFilters
|
||||
}
|
||||
|
||||
var sortedActiveFilters: [RoomListFilter] {
|
||||
activeFilters.sorted(by: { $0.rawValue < $1.rawValue })
|
||||
}
|
||||
|
||||
var availableFilters: [RoomListFilter] {
|
||||
var availableFilters = Set(RoomListFilter.allCases)
|
||||
for filter in activeFilters {
|
||||
availableFilters.remove(filter)
|
||||
if let complementaryFilter = filter.incompatibleFilter {
|
||||
availableFilters.remove(complementaryFilter)
|
||||
}
|
||||
}
|
||||
return availableFilters.sorted(by: { $0.rawValue < $1.rawValue })
|
||||
}
|
||||
|
||||
var isFiltering: Bool {
|
||||
!activeFilters.isEmpty
|
||||
}
|
||||
|
||||
func activateFilter(_ filter: RoomListFilter) {
|
||||
if let incompatibleFilter = filter.incompatibleFilter,
|
||||
activeFilters.contains(incompatibleFilter) {
|
||||
fatalError("[RoomListFiltersState] adding mutually exclusive filters is not allowed")
|
||||
}
|
||||
activeFilters.insert(filter)
|
||||
}
|
||||
|
||||
func deactivateFilter(_ filter: RoomListFilter) {
|
||||
activeFilters.remove(filter)
|
||||
}
|
||||
|
||||
func clearFilters() {
|
||||
activeFilters.removeAll()
|
||||
}
|
||||
|
||||
func isFilterActive(_ filter: RoomListFilter) -> Bool {
|
||||
activeFilters.contains(filter)
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,9 @@ struct RoomListFilterView: View {
|
||||
|
||||
var body: some View {
|
||||
let binding = Binding<Bool>(get: {
|
||||
state.isEnabled(filter)
|
||||
state.isFilterActive(filter)
|
||||
}, set: { isEnabled, _ in
|
||||
state.set(filter, isEnabled: isEnabled)
|
||||
isEnabled ? state.activateFilter(filter) : state.deactivateFilter(filter)
|
||||
})
|
||||
Toggle(isOn: binding) {
|
||||
Text(filter.localizedName)
|
||||
@@ -36,7 +36,7 @@ struct RoomListFilterView: View {
|
||||
struct RoomListFilterView_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
RoomListFilterView(filter: .people, state: .init())
|
||||
RoomListFilterView(filter: .people, state: .init(enabledFilters: [.people]))
|
||||
RoomListFilterView(filter: .people, state: .init(activeFilters: [.people]))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,10 +31,10 @@ struct RoomListFiltersView: View {
|
||||
.hidden()
|
||||
.frame(width: 0)
|
||||
}
|
||||
ForEach(state.sortedEnabledFilters) { filter in
|
||||
ForEach(state.sortedActiveFilters) { filter in
|
||||
RoomListFilterView(filter: filter, state: state)
|
||||
}
|
||||
ForEach(state.sortedAvailableFilters) { filter in
|
||||
ForEach(state.availableFilters) { filter in
|
||||
RoomListFilterView(filter: filter, state: state)
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,6 @@ struct RoomListFiltersView: View {
|
||||
struct RoomListFiltersView_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
RoomListFiltersView(state: .init())
|
||||
RoomListFiltersView(state: .init(enabledFilters: [.rooms, .favourites]))
|
||||
RoomListFiltersView(state: .init(activeFilters: [.rooms, .favourites]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
|
||||
.removeDuplicates()
|
||||
.sink { [weak self] searchQuery in
|
||||
guard let self else { return }
|
||||
self.roomSummaryProvider?.setFilter(.normalizedMatchRoomName(searchQuery))
|
||||
self.roomSummaryProvider?.setFilter(.include(.init(query: searchQuery)))
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -60,7 +60,7 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
actionsSubject.send(.dismiss)
|
||||
roomSummaryProvider?.setFilter(.all)
|
||||
roomSummaryProvider?.setFilter(.include(.all))
|
||||
case .send:
|
||||
guard let roomID = state.selectedRoomID else {
|
||||
fatalError()
|
||||
|
||||
@@ -49,14 +49,13 @@ class RoomProxy: RoomProxyProtocol {
|
||||
var ownUserID: String {
|
||||
room.ownUserId()
|
||||
}
|
||||
|
||||
|
||||
init?(roomListItem: RoomListItemProtocol,
|
||||
room: RoomProtocol,
|
||||
backgroundTaskService: BackgroundTaskServiceProtocol) async {
|
||||
self.roomListItem = roomListItem
|
||||
self.room = room
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
|
||||
do {
|
||||
timeline = try await TimelineProxy(timeline: room.timeline(), backgroundTaskService: backgroundTaskService)
|
||||
} catch {
|
||||
@@ -130,7 +129,7 @@ class RoomProxy: RoomProxyProtocol {
|
||||
var canonicalAlias: String? {
|
||||
room.canonicalAlias()
|
||||
}
|
||||
|
||||
|
||||
var avatarURL: URL? {
|
||||
roomListItem.avatarUrl().flatMap(URL.init(string:))
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ enum MockRoomSummaryProviderState {
|
||||
|
||||
class MockRoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
private let initialRooms: [RoomSummary]
|
||||
private(set) var currentFilter: RoomSummaryProviderFilter?
|
||||
|
||||
private let roomListSubject: CurrentValueSubject<[RoomSummary], Never>
|
||||
var roomListPublisher: CurrentValuePublisher<[RoomSummary], Never> {
|
||||
@@ -60,17 +61,16 @@ class MockRoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
func updateVisibleRange(_ range: Range<Int>) { }
|
||||
|
||||
func setFilter(_ filter: RoomSummaryProviderFilter) {
|
||||
currentFilter = filter
|
||||
switch filter {
|
||||
case .all:
|
||||
roomListSubject.send(initialRooms)
|
||||
case .none:
|
||||
roomListSubject.send([])
|
||||
case .normalizedMatchRoomName(let filter):
|
||||
if filter.isEmpty {
|
||||
roomListSubject.send(initialRooms)
|
||||
case let .include(predicate):
|
||||
if let query = predicate.query, !query.isEmpty {
|
||||
roomListSubject.send(initialRooms.filter { $0.name?.localizedCaseInsensitiveContains(query) ?? false })
|
||||
} else {
|
||||
roomListSubject.send(initialRooms.filter { $0.name?.localizedCaseInsensitiveContains(filter) ?? false })
|
||||
roomListSubject.send(initialRooms)
|
||||
}
|
||||
case .excludeAll:
|
||||
roomListSubject.send([])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
})
|
||||
|
||||
// Forces the listener above to be called with the current state
|
||||
setFilter(.all)
|
||||
setFilter(.include(.all))
|
||||
|
||||
listUpdatesTaskHandle = listUpdatesSubscriptionResult?.entriesStream
|
||||
|
||||
@@ -151,12 +151,16 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
|
||||
func setFilter(_ filter: RoomSummaryProviderFilter) {
|
||||
switch filter {
|
||||
case .none:
|
||||
case .excludeAll:
|
||||
_ = listUpdatesSubscriptionResult?.controller.setFilter(kind: .none)
|
||||
case .all:
|
||||
_ = listUpdatesSubscriptionResult?.controller.setFilter(kind: .allNonLeft)
|
||||
case .normalizedMatchRoomName(let query):
|
||||
_ = listUpdatesSubscriptionResult?.controller.setFilter(kind: .normalizedMatchRoomName(pattern: query.lowercased()))
|
||||
case let .include(predicate):
|
||||
var filters = predicate.filters.compactMap(\.rustFilter)
|
||||
if let query = predicate.query {
|
||||
filters.append(.normalizedMatchRoomName(pattern: query.lowercased()))
|
||||
}
|
||||
// We never want to show left rooms.
|
||||
filters.append(.nonLeft)
|
||||
_ = listUpdatesSubscriptionResult?.controller.setFilter(kind: .all(filters: filters))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,10 +87,28 @@ enum RoomSummary: CustomStringConvertible, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
enum RoomSummaryProviderFilter {
|
||||
case none
|
||||
case all
|
||||
case normalizedMatchRoomName(String)
|
||||
enum RoomSummaryProviderFilter: Equatable {
|
||||
struct Predicate: Equatable {
|
||||
let query: String?
|
||||
let filters: Set<RoomListFilter>
|
||||
|
||||
static var all: Predicate {
|
||||
Predicate()
|
||||
}
|
||||
|
||||
/// - Parameters:
|
||||
/// - query: If provided the filter will do a normalized search, default is nil
|
||||
/// - filters: Additional filters that can be provided for further filtering the room list, default is empty which means no additional filtering is done
|
||||
init(query: String? = nil, filters: Set<RoomListFilter> = []) {
|
||||
self.query = query
|
||||
self.filters = filters
|
||||
}
|
||||
}
|
||||
|
||||
/// Filters out everything
|
||||
case excludeAll
|
||||
/// Includes only the items that satisfy the predicate logic
|
||||
case include(Predicate)
|
||||
}
|
||||
|
||||
protocol RoomSummaryProviderProtocol {
|
||||
|
||||
@@ -92,8 +92,9 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
}
|
||||
|
||||
func sendReadReceipt(for itemID: TimelineItemIdentifier) async -> Result<Void, RoomTimelineControllerError> {
|
||||
guard let eventID = itemID.eventID
|
||||
else { return .success(()) }
|
||||
guard let eventID = itemID.eventID else {
|
||||
return .failure(.generic)
|
||||
}
|
||||
|
||||
switch await roomProxy.timeline.sendReadReceipt(for: eventID,
|
||||
type: appSettings.sendReadReceiptsEnabled ? .read : .readPrivate) {
|
||||
|
||||
@@ -25,10 +25,13 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
var clientProxy: MockClientProxy!
|
||||
var context: HomeScreenViewModelType.Context! { viewModel.context }
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
var roomSummaryProvider: MockRoomSummaryProvider!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
ServiceLocator.shared.settings.roomListFiltersEnabled = true
|
||||
cancellables.removeAll()
|
||||
clientProxy = MockClientProxy(userID: "@mock:client.com")
|
||||
roomSummaryProvider = MockRoomSummaryProvider(state: .loaded(.mockRooms))
|
||||
clientProxy = MockClientProxy(userID: "@mock:client.com", roomSummaryProvider: roomSummaryProvider)
|
||||
viewModel = HomeScreenViewModel(userSession: MockUserSession(clientProxy: clientProxy,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock()),
|
||||
@@ -37,6 +40,10 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
AppSettings.reset()
|
||||
}
|
||||
|
||||
func testSelectRoom() async throws {
|
||||
let mockRoomId = "mock_room_id"
|
||||
var correctResult = false
|
||||
@@ -153,4 +160,14 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
XCTAssertNil(context.alertInfo)
|
||||
XCTAssertTrue(correctResult)
|
||||
}
|
||||
|
||||
func testFilters() async throws {
|
||||
context.viewState.filtersState.activateFilter(.people)
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertEqual(roomSummaryProvider.currentFilter, RoomSummaryProviderFilter.include(.init(filters: [.people])))
|
||||
context.isSearchFieldFocused = true
|
||||
context.searchQuery = "Test"
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertEqual(roomSummaryProvider.currentFilter, RoomSummaryProviderFilter.include(.init(query: "Test", filters: [.people])))
|
||||
}
|
||||
}
|
||||
|
||||
70
UnitTests/Sources/RoomListFiltersStateTests.swift
Normal file
70
UnitTests/Sources/RoomListFiltersStateTests.swift
Normal file
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
final class RoomListFiltersStateTests: XCTestCase {
|
||||
var state: RoomListFiltersState!
|
||||
|
||||
override func setUp() {
|
||||
state = RoomListFiltersState()
|
||||
}
|
||||
|
||||
func testInitialState() {
|
||||
XCTAssertFalse(state.isFiltering)
|
||||
XCTAssertEqual(state.activeFilters, [])
|
||||
XCTAssertEqual(state.availableFilters, RoomListFilter.allCases)
|
||||
}
|
||||
|
||||
func testSetAndUnsetFilters() {
|
||||
state.activateFilter(.unreads)
|
||||
XCTAssertTrue(state.isFiltering)
|
||||
XCTAssertEqual(state.activeFilters, [.unreads])
|
||||
XCTAssertEqual(state.availableFilters, [.people, .rooms, .favourites])
|
||||
state.deactivateFilter(.unreads)
|
||||
XCTAssertFalse(state.isFiltering)
|
||||
XCTAssertEqual(state.activeFilters, [])
|
||||
XCTAssertEqual(state.availableFilters, RoomListFilter.allCases)
|
||||
}
|
||||
|
||||
func testMutuallyExclusiveFilters() {
|
||||
state.activateFilter(.people)
|
||||
XCTAssertTrue(state.isFiltering)
|
||||
XCTAssertEqual(state.activeFilters, [.people])
|
||||
XCTAssertEqual(state.availableFilters, [.unreads, .favourites])
|
||||
state.deactivateFilter(.people)
|
||||
state.activateFilter(.rooms)
|
||||
state.activateFilter(.unreads)
|
||||
XCTAssertTrue(state.isFiltering)
|
||||
XCTAssertEqual(state.activeFilters, [.rooms, .unreads])
|
||||
XCTAssertEqual(state.availableFilters, [.favourites])
|
||||
}
|
||||
|
||||
func testClearFilters() {
|
||||
state.activateFilter(.people)
|
||||
state.activateFilter(.unreads)
|
||||
state.activateFilter(.favourites)
|
||||
XCTAssertTrue(state.isFiltering)
|
||||
XCTAssertEqual(state.activeFilters, [.people, .unreads, .favourites])
|
||||
XCTAssertEqual(state.availableFilters, [])
|
||||
state.clearFilters()
|
||||
XCTAssertFalse(state.isFiltering)
|
||||
XCTAssertEqual(state.activeFilters, [])
|
||||
XCTAssertEqual(state.availableFilters, RoomListFilter.allCases)
|
||||
}
|
||||
}
|
||||
1
changelog.d/pr-2423.wip
Normal file
1
changelog.d/pr-2423.wip
Normal file
@@ -0,0 +1 @@
|
||||
All Filters have been implemented, except for the Favourites one.
|
||||
@@ -47,7 +47,7 @@ packages:
|
||||
# Element/Matrix dependencies
|
||||
MatrixRustSDK:
|
||||
url: https://github.com/matrix-org/matrix-rust-components-swift
|
||||
exactVersion: 1.1.37
|
||||
exactVersion: 1.1.38
|
||||
# path: ../matrix-rust-sdk
|
||||
Compound:
|
||||
url: https://github.com/element-hq/compound-ios
|
||||
|
||||
Reference in New Issue
Block a user