mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Searching disables the filters (#2530)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Searching hides and ignores filtering now.
|
||||||
Reference in New Issue
Block a user