Searching disables the filters (#2530)

This commit is contained in:
Mauro
2024-03-06 11:02:30 +01:00
committed by GitHub
parent 7f3451216d
commit 7754b9b8dc
18 changed files with 84 additions and 74 deletions
@@ -50,15 +50,22 @@ extension RoomSummaryProviderMock {
setFilterClosure = { [initialRooms, roomListSubject] filter in setFilterClosure = { [initialRooms, roomListSubject] filter in
switch filter { switch filter {
case let .include(predicate): case let .search(query):
var rooms = initialRooms var rooms = initialRooms
if let filter = predicate.filters.first { if !query.isEmpty {
rooms = rooms.filter { filter == .people ? $0.isDirect : !$0.isDirect } rooms = rooms.filter { $0.name?.localizedCaseInsensitiveContains(query) ?? false }
} }
if let query = predicate.query, !query.isEmpty { roomListSubject.send(rooms)
rooms = rooms.filter { $0.name?.localizedCaseInsensitiveContains(query) ?? false } case let .all(filters):
var rooms = initialRooms
if filters.count > 1 {
// for testing purpose chaining more than one filter will always return an empty state
rooms = []
} else if let filter = filters.first {
rooms = rooms.filter { filter == .people ? $0.isDirect : !$0.isDirect }
} }
roomListSubject.send(rooms) roomListSubject.send(rooms)
@@ -17,6 +17,8 @@
import SwiftUI import SwiftUI
import SwiftUIIntrospect import SwiftUIIntrospect
// MARK: - Search Controller Extensions
extension View { extension View {
/// A custom replacement for searchable that allows more precise configuration of the underlying search controller. /// A custom replacement for searchable that allows more precise configuration of the underlying search controller.
/// ///
@@ -169,3 +171,21 @@ private struct SearchController: UIViewControllerRepresentable {
} }
} }
} }
// MARK: - Searchable Extensions
struct IsSearchingModifier: ViewModifier {
@Environment(\.isSearching) private var isSearchingEnv
@Binding var isSearching: Bool
func body(content: Content) -> some View {
content
.onChange(of: isSearchingEnv) { isSearching = $0 }
}
}
extension View {
func isSearching(_ isSearching: Binding<Bool>) -> some View {
modifier(IsSearchingModifier(isSearching: isSearching))
}
}
@@ -58,7 +58,7 @@ struct EmojiPickerScreen: View {
.navigationTitle(L10n.commonReactions) .navigationTitle(L10n.commonReactions)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar } .toolbar { toolbar }
.modifier(IsSearching(isSearching: $isSearching)) .isSearching($isSearching)
.searchable(text: $searchString, placement: .navigationBarDrawer(displayMode: .always)) .searchable(text: $searchString, placement: .navigationBarDrawer(displayMode: .always))
.compoundSearchField() .compoundSearchField()
} }
@@ -87,17 +87,6 @@ struct EmojiPickerScreen: View {
} }
} }
/// A view modifier to extract whether the search field is focussed from a subview.
private struct IsSearching: ViewModifier {
@Environment(\.isSearching) private var isSearchFieldFocused
@Binding var isSearching: Bool
func body(content: Content) -> some View {
content
.onChange(of: isSearchFieldFocused) { isSearching = $0 }
}
}
// MARK: - Previews // MARK: - Previews
struct EmojiPickerScreen_Previews: PreviewProvider, TestablePreview { struct EmojiPickerScreen_Previews: PreviewProvider, TestablePreview {
@@ -45,7 +45,7 @@ class GlobalSearchScreenViewModel: GlobalSearchScreenViewModelType, GlobalSearch
.map(\.bindings.searchQuery) .map(\.bindings.searchQuery)
.removeDuplicates() .removeDuplicates()
.sink { [weak self] searchQuery in .sink { [weak self] searchQuery in
self?.roomSummaryProvider.setFilter(.include(.init(query: searchQuery))) self?.roomSummaryProvider.setFilter(.search(query: searchQuery))
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -60,7 +60,7 @@ class GlobalSearchScreenViewModel: GlobalSearchScreenViewModelType, GlobalSearch
switch viewAction { switch viewAction {
case .dismiss: case .dismiss:
actionsSubject.send(.dismiss) actionsSubject.send(.dismiss)
roomSummaryProvider.setFilter(.include(.all)) // This is a shared provider roomSummaryProvider.setFilter(.all(filters: [])) // This is a shared provider
case .select(let roomID): case .select(let roomID):
actionsSubject.send(.select(roomID: roomID)) actionsSubject.send(.select(roomID: roomID))
case .reachedTop: case .reachedTop:
@@ -89,7 +89,7 @@ struct HomeScreenViewState: BindableState {
var rooms: [HomeScreenRoom] = [] var rooms: [HomeScreenRoom] = []
var roomListMode: HomeScreenRoomListMode = .skeletons var roomListMode: HomeScreenRoomListMode = .skeletons
var shouldShowFilters = false var areFiltersEnabled = false
var markAsUnreadEnabled = false var markAsUnreadEnabled = false
var markAsFavouriteEnabled = false var markAsFavouriteEnabled = false
@@ -122,6 +122,10 @@ struct HomeScreenViewState: BindableState {
var shouldShowEmptyFilterState: Bool { var shouldShowEmptyFilterState: Bool {
shouldShowFilters && bindings.filtersState.isFiltering && visibleRooms.isEmpty shouldShowFilters && bindings.filtersState.isFiltering && visibleRooms.isEmpty
} }
var shouldShowFilters: Bool {
areFiltersEnabled && !bindings.isSearchFieldFocused
}
} }
struct HomeScreenViewStateBindings { struct HomeScreenViewStateBindings {
@@ -106,10 +106,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
return return
} }
if !value { if !value {
state.shouldShowFilters = false state.areFiltersEnabled = false
state.bindings.filtersState.clearFilters() state.bindings.filtersState.clearFilters()
} else { } else {
state.shouldShowFilters = true state.areFiltersEnabled = true
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -233,10 +233,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
roomSummaryProvider?.setFilter(.excludeAll) roomSummaryProvider?.setFilter(.excludeAll)
} else { } else {
if state.bindings.isSearchFieldFocused { if state.bindings.isSearchFieldFocused {
roomSummaryProvider?.setFilter(.include(.init(query: state.bindings.searchQuery, roomSummaryProvider?.setFilter(.search(query: state.bindings.searchQuery))
filters: state.bindings.filtersState.activeFilters.set)))
} else { } else {
roomSummaryProvider?.setFilter(.include(.init(filters: state.bindings.filtersState.activeFilters.set))) roomSummaryProvider?.setFilter(.all(filters: state.areFiltersEnabled ? state.bindings.filtersState.activeFilters.set : []))
} }
} }
} }
@@ -37,21 +37,21 @@ struct RoomListFilterView_Previews: PreviewProvider, TestablePreview {
private struct FilterToggleStyle: ToggleStyle { private struct FilterToggleStyle: ToggleStyle {
private func strokeColor(isOn: Bool) -> Color { private func strokeColor(isOn: Bool) -> Color {
isOn ? .compound.bgSubtleSecondary : .compound.borderInteractiveSecondary isOn ? .compound.bgActionPrimaryRest : .compound.borderInteractiveSecondary
} }
private func backgroundColor(isOn: Bool) -> Color { private func backgroundColor(isOn: Bool) -> Color {
isOn ? .compound.bgSubtleSecondary : .compound.bgCanvasDefault isOn ? .compound.bgActionPrimaryRest : .compound.bgCanvasDefault
} }
private func foregroundColor(isOn: Bool) -> Color { private func foregroundColor(isOn: Bool) -> Color {
isOn ? .compound.textPrimary : .compound.textSecondary isOn ? .compound.textOnSolidPrimary : .compound.textPrimary
} }
func makeBody(configuration: Configuration) -> some View { func makeBody(configuration: Configuration) -> some View {
let shape = RoundedRectangle(cornerRadius: 20) let shape = RoundedRectangle(cornerRadius: 20)
configuration.label configuration.label
.font(.compound.bodyLGSemibold) .font(.compound.bodyLG)
.foregroundColor(foregroundColor(isOn: configuration.isOn)) .foregroundColor(foregroundColor(isOn: configuration.isOn))
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.vertical, 8) .padding(.vertical, 8)
@@ -53,7 +53,7 @@ struct HomeScreenContent: View {
.layoutPriority(1) .layoutPriority(1)
} }
case .rooms: case .rooms:
if context.viewState.shouldShowFilters { if context.viewState.areFiltersEnabled {
// Showing empty views in pinned headers makes the room list spasm when reaching the top // Showing empty views in pinned headers makes the room list spasm when reaching the top
LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) { LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
Section { Section {
@@ -68,6 +68,7 @@ struct HomeScreenContent: View {
.readFrame($topSectionFrame) .readFrame($topSectionFrame)
} }
} }
.isSearching($context.isSearchFieldFocused)
.searchable(text: $context.searchQuery) .searchable(text: $context.searchQuery)
.compoundSearchField() .compoundSearchField()
.disableAutocorrection(true) .disableAutocorrection(true)
@@ -76,6 +77,7 @@ struct HomeScreenContent: View {
LazyVStack(spacing: 0) { LazyVStack(spacing: 0) {
HomeScreenRoomList(context: context) HomeScreenRoomList(context: context)
.isSearching($context.isSearchFieldFocused)
} }
.searchable(text: $context.searchQuery) .searchable(text: $context.searchQuery)
.compoundSearchField() .compoundSearchField()
@@ -17,17 +17,9 @@
import SwiftUI import SwiftUI
struct HomeScreenRoomList: View { struct HomeScreenRoomList: View {
@Environment(\.isSearching) var isSearchFieldFocused
@ObservedObject var context: HomeScreenViewModel.Context @ObservedObject var context: HomeScreenViewModel.Context
var body: some View { var body: some View {
filteredContent
.onChange(of: isSearchFieldFocused) { context.isSearchFieldFocused = $0 }
}
@ViewBuilder
private var filteredContent: some View {
// Hide the room list when the search bar is focused but the query is empty // Hide the room list when the search bar is focused but the query is empty
// This works hand in hand with the room list service layer filtering and // This works hand in hand with the room list service layer filtering and
// avoids glitches when focusing the search bar // avoids glitches when focusing the search bar
@@ -49,7 +49,7 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
.removeDuplicates() .removeDuplicates()
.sink { [weak self] searchQuery in .sink { [weak self] searchQuery in
guard let self else { return } guard let self else { return }
self.roomSummaryProvider?.setFilter(.include(.init(query: searchQuery))) self.roomSummaryProvider?.setFilter(.search(query: searchQuery))
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -60,7 +60,7 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
switch viewAction { switch viewAction {
case .cancel: case .cancel:
actionsSubject.send(.dismiss) actionsSubject.send(.dismiss)
roomSummaryProvider?.setFilter(.include(.all)) roomSummaryProvider?.setFilter(.all(filters: []))
case .send: case .send:
guard let roomID = state.selectedRoomID else { guard let roomID = state.selectedRoomID else {
fatalError() fatalError()
@@ -102,7 +102,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
}) })
// Forces the listener above to be called with the current state // Forces the listener above to be called with the current state
setFilter(.include(.all)) setFilter(.all(filters: []))
listUpdatesTaskHandle = listUpdatesSubscriptionResult?.entriesStream listUpdatesTaskHandle = listUpdatesSubscriptionResult?.entriesStream
@@ -153,12 +153,11 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
switch filter { switch filter {
case .excludeAll: case .excludeAll:
_ = listUpdatesSubscriptionResult?.controller.setFilter(kind: .none) _ = listUpdatesSubscriptionResult?.controller.setFilter(kind: .none)
case let .include(predicate): case let .search(query):
var filters = predicate.filters.map(\.rustFilter) let filters: [RoomListEntriesDynamicFilterKind] = [.normalizedMatchRoomName(pattern: query), .nonLeft]
if let query = predicate.query { _ = listUpdatesSubscriptionResult?.controller.setFilter(kind: .all(filters: filters))
filters.append(.normalizedMatchRoomName(pattern: query.lowercased())) case let .all(filters):
} var filters = filters.map(\.rustFilter)
// We never want to show left rooms.
filters.append(.nonLeft) filters.append(.nonLeft)
_ = listUpdatesSubscriptionResult?.controller.setFilter(kind: .all(filters: filters)) _ = listUpdatesSubscriptionResult?.controller.setFilter(kind: .all(filters: filters))
} }
@@ -97,27 +97,12 @@ enum RoomSummary: CustomStringConvertible, Equatable {
} }
enum RoomSummaryProviderFilter: Equatable { 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 /// Filters out everything
case excludeAll case excludeAll
/// Includes only the items that satisfy the predicate logic /// Includes only the items that satisfy the predicate logic
case include(Predicate) case search(query: String)
/// Includes only what satisfies the filters used
case all(filters: Set<RoomListFilter>)
} }
// sourcery: AutoMockable // sourcery: AutoMockable
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:28cde6aea3ba1a8dc8bf1d44359735c5cec7618ee51d4a087acd9ea0581c02bb oid sha256:3ac210fff37bb6b8023fa1527ac1eeb5fdbad9c80357b42f68248f9940db3531
size 61950 size 62409
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:07160b58b8cc326f373622079b8513322aa27c1ef44fe95c60f41ef87d747ea1 oid sha256:01003a81f1ace9c184c28728f8bee3528e326f0be88ebcbab3006af649208a10
size 60192 size 60619
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:25da13ca12e10bb1a8634fb62b9073ec02aecbfe02872f81bdd49d619a9b1ca6 oid sha256:a5972aae36b27aad2dd6c2fdddcdc99a369d566f162042b1916f77d857607134
size 69664 size 70253
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2af52fe41d2238cd6e16f759d533ad21445569270b583a2e268a7926d68e37e0 oid sha256:269445bf0caa7e3ca562e542028081fb5bf9c87d42b5509cce7f69dda472f46e
size 73653 size 73468
@@ -171,11 +171,23 @@ class HomeScreenViewModelTests: XCTestCase {
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.count, 2) XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.count, 2)
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.first?.name, "Foundation and Earth") XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.first?.name, "Foundation and Earth")
}
func testSearch() async throws {
context.isSearchFieldFocused = true context.isSearchFieldFocused = true
context.searchQuery = "lude to Found" context.searchQuery = "lude to Found"
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.first?.name, "Prelude to Foundation") XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.first?.name, "Prelude to Foundation")
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.count, 1) XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.count, 1)
XCTAssertFalse(context.viewState.shouldShowFilters)
}
func testFiltersEmptyState() async throws {
context.filtersState.activateFilter(.people)
context.filtersState.activateFilter(.favourites)
try await Task.sleep(for: .milliseconds(100))
XCTAssertTrue(context.viewState.shouldShowEmptyFilterState)
context.isSearchFieldFocused = true
XCTAssertFalse(context.viewState.shouldShowEmptyFilterState)
} }
} }
+1
View File
@@ -0,0 +1 @@
Searching hides and ignores filtering now.