diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 9b464e275..a9a645149 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -38,6 +38,7 @@ final class AppSettings { case notificationSettingsEnabled case swiftUITimelineEnabled case pollsInTimeline + case fuzzySearchEnabled } private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier @@ -223,4 +224,7 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.pollsInTimeline, defaultValue: false, storageType: .userDefaults(store)) var pollsInTimelineEnabled + + @UserPreference(key: UserDefaultsKeys.fuzzySearchEnabled, defaultValue: true, storageType: .userDefaults(store)) + var fuzzySearchEnabled } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index b6361c47b..19aee6ffb 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -79,12 +79,14 @@ struct HomeScreenViewState: BindableState { var selectedRoomID: String? + var fuzzySearchEnabled: Bool + var visibleRooms: [HomeScreenRoom] { if roomListMode == .skeletons { return placeholderRooms } - if bindings.searchQuery.isEmpty { + if fuzzySearchEnabled || bindings.searchQuery.isEmpty { return rooms } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 54b9058cd..1f776aa66 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -49,7 +49,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol roomSummaryProvider = userSession.clientProxy.roomSummaryProvider inviteSummaryProvider = userSession.clientProxy.inviteSummaryProvider - super.init(initialViewState: HomeScreenViewState(userID: userSession.userID), + super.init(initialViewState: HomeScreenViewState(userID: userSession.userID, fuzzySearchEnabled: appSettings.fuzzySearchEnabled), imageProvider: userSession.mediaProvider) userSession.callbacks @@ -74,6 +74,19 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol .weakAssign(to: \.state.selectedRoomID, on: self) .store(in: &cancellables) + appSettings.$fuzzySearchEnabled + .weakAssign(to: \.state.fuzzySearchEnabled, on: self) + .store(in: &cancellables) + + context.$viewState + .filter { _ in appSettings.fuzzySearchEnabled } + .map(\.bindings.searchQuery) + .debounceAndRemoveDuplicates() + .sink { [weak self] searchQuery in + self?.roomSummaryProvider?.updateFilterPattern(searchQuery) + } + .store(in: &cancellables) + setupRoomSummaryProviderSubscriptions() updateRooms() diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index a3255f19d..928113a49 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -49,6 +49,7 @@ protocol DeveloperOptionsProtocol: AnyObject { var notificationSettingsEnabled: Bool { get set } var swiftUITimelineEnabled: Bool { get set } var pollsInTimelineEnabled: Bool { get set } + var fuzzySearchEnabled: Bool { get set } } extension AppSettings: DeveloperOptionsProtocol { } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index 7ec796678..aa5172029 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -38,6 +38,13 @@ struct DeveloperOptionsScreen: View { } } + Section("Room list") { + Toggle(isOn: $context.fuzzySearchEnabled) { + Text("Fuzzy search") + Text("Requires app reboot") + } + } + Section("Notifications") { Toggle(isOn: $context.notificationSettingsEnabled) { Text("Show notification settings") diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 45e0ef49c..933e4dd3d 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -399,11 +399,13 @@ class ClientProxy: ClientProxyProtocol { roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService, eventStringBuilder: eventStringBuilder, name: "AllRooms", + appSettings: appSettings, backgroundTaskService: backgroundTaskService) try await roomSummaryProvider?.setRoomList(roomListService.allRooms()) inviteSummaryProvider = RoomSummaryProvider(roomListService: roomListService, eventStringBuilder: eventStringBuilder, name: "Invites", + appSettings: appSettings, backgroundTaskService: backgroundTaskService) self.syncService = syncService diff --git a/ElementX/Sources/Services/Room/RoomSummary/MockRoomSummaryProvider.swift b/ElementX/Sources/Services/Room/RoomSummary/MockRoomSummaryProvider.swift index b516fceec..ba974ec69 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/MockRoomSummaryProvider.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/MockRoomSummaryProvider.swift @@ -45,6 +45,8 @@ class MockRoomSummaryProvider: RoomSummaryProviderProtocol { func setRoomList(_ roomList: RoomList) { } func updateVisibleRange(_ range: Range) { } + + func updateFilterPattern(_ pattern: String?) { } } extension Array where Element == RoomSummary { diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift index 909ef31b9..63157c1cf 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift @@ -22,6 +22,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { private let roomListService: RoomListServiceProtocol private let eventStringBuilder: RoomEventStringBuilder private let name: String + private var appSettings: AppSettings private let backgroundTaskService: BackgroundTaskServiceProtocol private let serialDispatchQueue: DispatchQueue @@ -29,6 +30,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { private var roomList: RoomListProtocol? private var cancellables = Set() + private var listUpdatesSubscriptionResult: RoomListEntriesWithDynamicFilterResult? private var listUpdatesTaskHandle: TaskHandle? private var stateUpdatesTaskHandle: TaskHandle? @@ -55,11 +57,13 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { init(roomListService: RoomListServiceProtocol, eventStringBuilder: RoomEventStringBuilder, name: String, + appSettings: AppSettings, backgroundTaskService: BackgroundTaskServiceProtocol) { self.roomListService = roomListService serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomsummaryprovider", qos: .utility) self.eventStringBuilder = eventStringBuilder self.name = name + self.appSettings = appSettings self.backgroundTaskService = backgroundTaskService diffsPublisher @@ -76,20 +80,16 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { self.roomList = roomList do { - let listUpdatesSubscriptionResult = roomList.entries(listener: RoomListEntriesListenerProxy { [weak self] updates in + listUpdatesSubscriptionResult = roomList.entriesWithDynamicFilter(listener: RoomListEntriesListenerProxy { [weak self] updates in guard let self else { return } MXLog.info("\(name): Received list update") diffsPublisher.send(updates) }) - listUpdatesTaskHandle = listUpdatesSubscriptionResult.entriesStream - - rooms = listUpdatesSubscriptionResult.entries.map { roomListEntry in - buildSummaryForRoomListEntry(roomListEntry) - } + listUpdatesTaskHandle = listUpdatesSubscriptionResult?.entriesStream - // Manually call it here as the didSet doesn't work from constructors - roomListSubject.send(rooms) + // Forces the listener above to be called with the current state + updateFilterPattern(nil) let stateUpdatesSubscriptionResult = try roomList.loadingState(listener: RoomListStateObserver { [weak self] state in guard let self else { return } @@ -100,7 +100,6 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { stateUpdatesTaskHandle = stateUpdatesSubscriptionResult.stateStream stateSubject.send(RoomSummaryProviderState(roomListState: stateUpdatesSubscriptionResult.state)) - } catch { MXLog.error("Failed setting up room list entry listener with error: \(error)") } @@ -116,7 +115,18 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { } } } + + func updateFilterPattern(_ pattern: String?) { + guard let pattern, !pattern.isEmpty else { + _ = listUpdatesSubscriptionResult?.dynamicFilter.set(kind: .all) + return + } + guard appSettings.fuzzySearchEnabled else { return } + + _ = listUpdatesSubscriptionResult?.dynamicFilter.set(kind: .fuzzyMatchRoomName(pattern: pattern.lowercased())) + } + // MARK: - Private fileprivate func updateRoomsWithDiffs(_ diffs: [RoomListEntriesUpdate]) { diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProviderProtocol.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProviderProtocol.swift index a804163fd..545bd2fdd 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProviderProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProviderProtocol.swift @@ -99,4 +99,6 @@ protocol RoomSummaryProviderProtocol { func setRoomList(_ roomList: RoomList) func updateVisibleRange(_ range: Range) + + func updateFilterPattern(_ pattern: String?) }