Searching disables the filters (#2530)
This commit is contained in:
@@ -50,15 +50,22 @@ extension RoomSummaryProviderMock {
|
||||
|
||||
setFilterClosure = { [initialRooms, roomListSubject] filter in
|
||||
switch filter {
|
||||
case let .include(predicate):
|
||||
case let .search(query):
|
||||
var rooms = initialRooms
|
||||
|
||||
if let filter = predicate.filters.first {
|
||||
rooms = rooms.filter { filter == .people ? $0.isDirect : !$0.isDirect }
|
||||
if !query.isEmpty {
|
||||
rooms = rooms.filter { $0.name?.localizedCaseInsensitiveContains(query) ?? false }
|
||||
}
|
||||
|
||||
if let query = predicate.query, !query.isEmpty {
|
||||
rooms = rooms.filter { $0.name?.localizedCaseInsensitiveContains(query) ?? false }
|
||||
roomListSubject.send(rooms)
|
||||
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)
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
import SwiftUI
|
||||
import SwiftUIIntrospect
|
||||
|
||||
// MARK: - Search Controller Extensions
|
||||
|
||||
extension View {
|
||||
/// 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)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { toolbar }
|
||||
.modifier(IsSearching(isSearching: $isSearching))
|
||||
.isSearching($isSearching)
|
||||
.searchable(text: $searchString, placement: .navigationBarDrawer(displayMode: .always))
|
||||
.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
|
||||
|
||||
struct EmojiPickerScreen_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
@@ -45,7 +45,7 @@ class GlobalSearchScreenViewModel: GlobalSearchScreenViewModelType, GlobalSearch
|
||||
.map(\.bindings.searchQuery)
|
||||
.removeDuplicates()
|
||||
.sink { [weak self] searchQuery in
|
||||
self?.roomSummaryProvider.setFilter(.include(.init(query: searchQuery)))
|
||||
self?.roomSummaryProvider.setFilter(.search(query: searchQuery))
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -60,7 +60,7 @@ class GlobalSearchScreenViewModel: GlobalSearchScreenViewModelType, GlobalSearch
|
||||
switch viewAction {
|
||||
case .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):
|
||||
actionsSubject.send(.select(roomID: roomID))
|
||||
case .reachedTop:
|
||||
|
||||
@@ -89,7 +89,7 @@ struct HomeScreenViewState: BindableState {
|
||||
var rooms: [HomeScreenRoom] = []
|
||||
var roomListMode: HomeScreenRoomListMode = .skeletons
|
||||
|
||||
var shouldShowFilters = false
|
||||
var areFiltersEnabled = false
|
||||
var markAsUnreadEnabled = false
|
||||
var markAsFavouriteEnabled = false
|
||||
|
||||
@@ -122,6 +122,10 @@ struct HomeScreenViewState: BindableState {
|
||||
var shouldShowEmptyFilterState: Bool {
|
||||
shouldShowFilters && bindings.filtersState.isFiltering && visibleRooms.isEmpty
|
||||
}
|
||||
|
||||
var shouldShowFilters: Bool {
|
||||
areFiltersEnabled && !bindings.isSearchFieldFocused
|
||||
}
|
||||
}
|
||||
|
||||
struct HomeScreenViewStateBindings {
|
||||
|
||||
@@ -106,10 +106,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
return
|
||||
}
|
||||
if !value {
|
||||
state.shouldShowFilters = false
|
||||
state.areFiltersEnabled = false
|
||||
state.bindings.filtersState.clearFilters()
|
||||
} else {
|
||||
state.shouldShowFilters = true
|
||||
state.areFiltersEnabled = true
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -233,10 +233,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
roomSummaryProvider?.setFilter(.excludeAll)
|
||||
} else {
|
||||
if state.bindings.isSearchFieldFocused {
|
||||
roomSummaryProvider?.setFilter(.include(.init(query: state.bindings.searchQuery,
|
||||
filters: state.bindings.filtersState.activeFilters.set)))
|
||||
roomSummaryProvider?.setFilter(.search(query: state.bindings.searchQuery))
|
||||
} 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 func strokeColor(isOn: Bool) -> Color {
|
||||
isOn ? .compound.bgSubtleSecondary : .compound.borderInteractiveSecondary
|
||||
isOn ? .compound.bgActionPrimaryRest : .compound.borderInteractiveSecondary
|
||||
}
|
||||
|
||||
private func backgroundColor(isOn: Bool) -> Color {
|
||||
isOn ? .compound.bgSubtleSecondary : .compound.bgCanvasDefault
|
||||
isOn ? .compound.bgActionPrimaryRest : .compound.bgCanvasDefault
|
||||
}
|
||||
|
||||
private func foregroundColor(isOn: Bool) -> Color {
|
||||
isOn ? .compound.textPrimary : .compound.textSecondary
|
||||
isOn ? .compound.textOnSolidPrimary : .compound.textPrimary
|
||||
}
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
let shape = RoundedRectangle(cornerRadius: 20)
|
||||
configuration.label
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundColor(foregroundColor(isOn: configuration.isOn))
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
|
||||
@@ -53,7 +53,7 @@ struct HomeScreenContent: View {
|
||||
.layoutPriority(1)
|
||||
}
|
||||
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
|
||||
LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
|
||||
Section {
|
||||
@@ -68,6 +68,7 @@ struct HomeScreenContent: View {
|
||||
.readFrame($topSectionFrame)
|
||||
}
|
||||
}
|
||||
.isSearching($context.isSearchFieldFocused)
|
||||
.searchable(text: $context.searchQuery)
|
||||
.compoundSearchField()
|
||||
.disableAutocorrection(true)
|
||||
@@ -76,6 +77,7 @@ struct HomeScreenContent: View {
|
||||
|
||||
LazyVStack(spacing: 0) {
|
||||
HomeScreenRoomList(context: context)
|
||||
.isSearching($context.isSearchFieldFocused)
|
||||
}
|
||||
.searchable(text: $context.searchQuery)
|
||||
.compoundSearchField()
|
||||
|
||||
@@ -17,17 +17,9 @@
|
||||
import SwiftUI
|
||||
|
||||
struct HomeScreenRoomList: View {
|
||||
@Environment(\.isSearching) var isSearchFieldFocused
|
||||
|
||||
@ObservedObject var context: HomeScreenViewModel.Context
|
||||
|
||||
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
|
||||
// This works hand in hand with the room list service layer filtering and
|
||||
// avoids glitches when focusing the search bar
|
||||
|
||||
@@ -49,7 +49,7 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
|
||||
.removeDuplicates()
|
||||
.sink { [weak self] searchQuery in
|
||||
guard let self else { return }
|
||||
self.roomSummaryProvider?.setFilter(.include(.init(query: searchQuery)))
|
||||
self.roomSummaryProvider?.setFilter(.search(query: searchQuery))
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -60,7 +60,7 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
actionsSubject.send(.dismiss)
|
||||
roomSummaryProvider?.setFilter(.include(.all))
|
||||
roomSummaryProvider?.setFilter(.all(filters: []))
|
||||
case .send:
|
||||
guard let roomID = state.selectedRoomID else {
|
||||
fatalError()
|
||||
|
||||
@@ -102,7 +102,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
})
|
||||
|
||||
// Forces the listener above to be called with the current state
|
||||
setFilter(.include(.all))
|
||||
setFilter(.all(filters: []))
|
||||
|
||||
listUpdatesTaskHandle = listUpdatesSubscriptionResult?.entriesStream
|
||||
|
||||
@@ -153,12 +153,11 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
switch filter {
|
||||
case .excludeAll:
|
||||
_ = listUpdatesSubscriptionResult?.controller.setFilter(kind: .none)
|
||||
case let .include(predicate):
|
||||
var filters = predicate.filters.map(\.rustFilter)
|
||||
if let query = predicate.query {
|
||||
filters.append(.normalizedMatchRoomName(pattern: query.lowercased()))
|
||||
}
|
||||
// We never want to show left rooms.
|
||||
case let .search(query):
|
||||
let filters: [RoomListEntriesDynamicFilterKind] = [.normalizedMatchRoomName(pattern: query), .nonLeft]
|
||||
_ = listUpdatesSubscriptionResult?.controller.setFilter(kind: .all(filters: filters))
|
||||
case let .all(filters):
|
||||
var filters = filters.map(\.rustFilter)
|
||||
filters.append(.nonLeft)
|
||||
_ = listUpdatesSubscriptionResult?.controller.setFilter(kind: .all(filters: filters))
|
||||
}
|
||||
|
||||
@@ -97,27 +97,12 @@ enum RoomSummary: CustomStringConvertible, 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
|
||||
case excludeAll
|
||||
/// 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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:28cde6aea3ba1a8dc8bf1d44359735c5cec7618ee51d4a087acd9ea0581c02bb
|
||||
size 61950
|
||||
oid sha256:3ac210fff37bb6b8023fa1527ac1eeb5fdbad9c80357b42f68248f9940db3531
|
||||
size 62409
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:07160b58b8cc326f373622079b8513322aa27c1ef44fe95c60f41ef87d747ea1
|
||||
size 60192
|
||||
oid sha256:01003a81f1ace9c184c28728f8bee3528e326f0be88ebcbab3006af649208a10
|
||||
size 60619
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:25da13ca12e10bb1a8634fb62b9073ec02aecbfe02872f81bdd49d619a9b1ca6
|
||||
size 69664
|
||||
oid sha256:a5972aae36b27aad2dd6c2fdddcdc99a369d566f162042b1916f77d857607134
|
||||
size 70253
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2af52fe41d2238cd6e16f759d533ad21445569270b583a2e268a7926d68e37e0
|
||||
size 73653
|
||||
oid sha256:269445bf0caa7e3ca562e542028081fb5bf9c87d42b5509cce7f69dda472f46e
|
||||
size 73468
|
||||
|
||||
@@ -171,11 +171,23 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.count, 2)
|
||||
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.first?.name, "Foundation and Earth")
|
||||
|
||||
}
|
||||
|
||||
func testSearch() async throws {
|
||||
context.isSearchFieldFocused = true
|
||||
context.searchQuery = "lude to Found"
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.first?.name, "Prelude to Foundation")
|
||||
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
changelog.d/pr-2530.wip
Normal file
1
changelog.d/pr-2530.wip
Normal file
@@ -0,0 +1 @@
|
||||
Searching hides and ignores filtering now.
|
||||
Reference in New Issue
Block a user