diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 6e252f36a..1ef5bf28c 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -29,6 +29,8 @@ final class AppSettings { case enableNotifications case enableInAppNotifications case pusherProfileTag + + // Feature flags case shouldCollapseRoomStateEvents case userSuggestionsEnabled case readReceiptsEnabled @@ -36,6 +38,7 @@ final class AppSettings { case notificationSettingsEnabled case swiftUITimelineEnabled case pollsInTimeline + case fuzzySearchEnabled } private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier @@ -225,4 +228,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..55b5b5e91 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -47,6 +47,7 @@ enum HomeScreenViewAction { case skipSessionVerification case updateVisibleItemRange(range: Range, isScrolling: Bool) case selectInvites + case updatedSearchQuery } enum HomeScreenRoomListMode: CustomStringConvertible { @@ -79,12 +80,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 9840f249f..ef9cb7ed3 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,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol .weakAssign(to: \.state.selectedRoomID, on: self) .store(in: &cancellables) + appSettings.$fuzzySearchEnabled + .weakAssign(to: \.state.fuzzySearchEnabled, on: self) + .store(in: &cancellables) + guard let roomSummaryProvider, let inviteSummaryProvider else { MXLog.error("Room summary provider unavailable") return @@ -187,6 +191,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol callback?(.presentStartChatScreen) case .selectInvites: callback?(.presentInvitesScreen) + case .updatedSearchQuery: + roomSummaryProvider?.updateFilterPattern(state.bindings.searchQuery) } } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index 6e33c9577..eedf3e0af 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -80,9 +80,13 @@ struct HomeScreen: View { updateVisibleRange() } .onChange(of: context.searchQuery) { searchQuery in - guard searchQuery.isEmpty else { return } - // Dispatch allows the view to update after changing the query - DispatchQueue.main.async { updateVisibleRange() } + if context.viewState.fuzzySearchEnabled { + context.send(viewAction: .updatedSearchQuery) + } else { + guard searchQuery.isEmpty else { return } + // Dispatch allows the view to update after changing the query + DispatchQueue.main.async { updateVisibleRange() } + } } .onReceive(scrollViewAdapter.scrollDirection) { direction in withAnimation(.elementDefault) { lastScrollDirection = direction } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index a11123ad5..6186367d8 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 } var filterNotificationsByPushRulesEnabled: Bool { get set } } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index 6b74d1323..276900c11 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -37,6 +37,13 @@ struct DeveloperOptionsScreen: View { Text("Resets on reboot") } } + + Section("Room list") { + Toggle(isOn: $context.fuzzySearchEnabled) { + Text("Fuzzy search") + Text("Requires app reboot") + } + } Section("Notifications") { Toggle(isOn: $context.notificationSettingsEnabled) { diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 4db9ace7a..406f52592 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -414,11 +414,13 @@ class ClientProxy: ClientProxyProtocol { let eventStringBuilder = RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService, eventStringBuilder: eventStringBuilder, - name: "AllRooms") + name: "AllRooms", + appSettings: appSettings) try await roomSummaryProvider?.setRoomList(roomListService.allRooms()) inviteSummaryProvider = RoomSummaryProvider(roomListService: roomListService, eventStringBuilder: eventStringBuilder, - name: "Invites") + name: "Invites", + appSettings: appSettings) self.syncService = syncService self.roomListService = roomListService diff --git a/ElementX/Sources/Services/Room/RoomSummary/MockRoomSummaryProvider.swift b/ElementX/Sources/Services/Room/RoomSummary/MockRoomSummaryProvider.swift index 1a07170a8..525a23701 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 ddd5339ad..bd8ff4b21 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift @@ -22,12 +22,14 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { private let roomListService: RoomListServiceProtocol private let eventStringBuilder: RoomEventStringBuilder private let name: String + private var appSettings: AppSettings private let serialDispatchQueue: DispatchQueue private var roomList: RoomListProtocol? private var cancellables = Set() + private var listUpdatesSubscriptionResult: RoomListEntriesWithDynamicFilterResult? private var listUpdatesTaskHandle: TaskHandle? private var stateUpdatesTaskHandle: TaskHandle? @@ -53,11 +55,13 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { init(roomListService: RoomListServiceProtocol, eventStringBuilder: RoomEventStringBuilder, - name: String) { + name: String, + appSettings: AppSettings) { self.roomListService = roomListService serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomsummaryprovider", qos: .utility) self.eventStringBuilder = eventStringBuilder self.name = name + self.appSettings = appSettings diffsPublisher .receive(on: serialDispatchQueue) @@ -73,21 +77,17 @@ 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) - } - // Manually call it here as the didSet doesn't work from constructors - roomListSubject.send(rooms) + listUpdatesTaskHandle = listUpdatesSubscriptionResult?.entriesStream + // 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 } MXLog.info("\(name): Received state update: \(state)") @@ -113,6 +113,17 @@ 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?) } diff --git a/changelog.d/pr-1483.feature b/changelog.d/pr-1483.feature new file mode 100644 index 000000000..f457e4115 --- /dev/null +++ b/changelog.d/pr-1483.feature @@ -0,0 +1 @@ +Allow fuzzy searching for room list rooms \ No newline at end of file