mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
Allow removing individual search breadcrumbs
Each recent search now has a trailing close button to remove it from the history. The view action is logged without its payload so removed queries don't end up in the logs.
This commit is contained in:
@@ -46,14 +46,28 @@ struct SearchScreenViewStateBindings {
|
|||||||
var searchMode: SearchScreenMode = .rooms
|
var searchMode: SearchScreenMode = .rooms
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SearchScreenViewAction {
|
enum SearchScreenViewAction: CustomStringConvertible {
|
||||||
case appeared
|
case appeared
|
||||||
case submitQuery
|
case submitQuery
|
||||||
case selectRoom(roomID: String)
|
case selectRoom(roomID: String)
|
||||||
case selectMessage(roomID: String, eventID: String)
|
case selectMessage(roomID: String, eventID: String)
|
||||||
|
case removeBreadcrumb(SearchScreenBreadcrumb)
|
||||||
case reachedTop
|
case reachedTop
|
||||||
case reachedBottom
|
case reachedBottom
|
||||||
case cancel
|
case cancel
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .appeared: "appeared"
|
||||||
|
case .submitQuery: "submitQuery"
|
||||||
|
case .selectRoom(let roomID): "selectRoom(\(roomID))"
|
||||||
|
case .selectMessage(let roomID, let eventID): "selectMessage(\(roomID), \(eventID))"
|
||||||
|
case .removeBreadcrumb: "removeBreadcrumb"
|
||||||
|
case .reachedTop: "reachedTop"
|
||||||
|
case .reachedBottom: "reachedBottom"
|
||||||
|
case .cancel: "cancel"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SearchScreenRoom: Identifiable, Equatable {
|
struct SearchScreenRoom: Identifiable, Equatable {
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ class SearchScreenViewModel: SearchScreenViewModelType, SearchScreenViewModelPro
|
|||||||
case .selectMessage(let roomID, let eventID):
|
case .selectMessage(let roomID, let eventID):
|
||||||
recordBreadcrumb(.room(roomID: roomID))
|
recordBreadcrumb(.room(roomID: roomID))
|
||||||
actionsSubject.send(.presentRoom(roomID: roomID, eventID: eventID))
|
actionsSubject.send(.presentRoom(roomID: roomID, eventID: eventID))
|
||||||
|
case .removeBreadcrumb(let breadcrumb):
|
||||||
|
removeBreadcrumb(breadcrumb)
|
||||||
case .reachedTop:
|
case .reachedTop:
|
||||||
if state.bindings.searchMode == .rooms {
|
if state.bindings.searchMode == .rooms {
|
||||||
updateVisibleRange(edge: .top)
|
updateVisibleRange(edge: .top)
|
||||||
@@ -222,6 +224,14 @@ class SearchScreenViewModel: SearchScreenViewModelType, SearchScreenViewModelPro
|
|||||||
userSettings.searchBreadcrumbs = Array(breadcrumbs.prefix(Self.maximumBreadcrumbCount))
|
userSettings.searchBreadcrumbs = Array(breadcrumbs.prefix(Self.maximumBreadcrumbCount))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func removeBreadcrumb(_ breadcrumb: SearchScreenBreadcrumb) {
|
||||||
|
let storedBreadcrumb: SearchBreadcrumb = switch breadcrumb {
|
||||||
|
case .query(let query): .query(query)
|
||||||
|
case .room(let room): .room(roomID: room.id)
|
||||||
|
}
|
||||||
|
userSettings.searchBreadcrumbs.removeAll { $0 == storedBreadcrumb }
|
||||||
|
}
|
||||||
|
|
||||||
private func setActiveTabLoading(_ isLoading: Bool) {
|
private func setActiveTabLoading(_ isLoading: Bool) {
|
||||||
switch state.bindings.searchMode {
|
switch state.bindings.searchMode {
|
||||||
case .rooms:
|
case .rooms:
|
||||||
|
|||||||
@@ -199,11 +199,13 @@ struct SearchScreen: View {
|
|||||||
ForEach(context.viewState.breadcrumbs) { breadcrumb in
|
ForEach(context.viewState.breadcrumbs) { breadcrumb in
|
||||||
let isSelected = isHardwareKeyboardConnected && selectedID == breadcrumb.id
|
let isSelected = isHardwareKeyboardConnected && selectedID == breadcrumb.id
|
||||||
|
|
||||||
|
let onRemove = { context.send(viewAction: .removeBreadcrumb(breadcrumb)) }
|
||||||
|
|
||||||
switch breadcrumb {
|
switch breadcrumb {
|
||||||
case .query(let query):
|
case .query(let query):
|
||||||
SearchScreenQueryCell(query: query, isSelected: isSelected) { context.searchQuery = query }
|
SearchScreenQueryCell(query: query, isSelected: isSelected, onRemove: onRemove) { context.searchQuery = query }
|
||||||
case .room(let room):
|
case .room(let room):
|
||||||
SearchScreenRoomCell(room: room, context: context, isSelected: isSelected, avatarSize: .searchHistory)
|
SearchScreenRoomCell(room: room, context: context, isSelected: isSelected, avatarSize: .searchHistory, onRemove: onRemove)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
@@ -283,6 +285,7 @@ private struct SearchScreenRoomCell: View {
|
|||||||
let context: SearchScreenViewModel.Context
|
let context: SearchScreenViewModel.Context
|
||||||
let isSelected: Bool
|
let isSelected: Bool
|
||||||
var avatarSize: RoomAvatarSizeOnScreen = .search
|
var avatarSize: RoomAvatarSizeOnScreen = .search
|
||||||
|
var onRemove: (() -> Void)?
|
||||||
|
|
||||||
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
||||||
|
|
||||||
@@ -307,6 +310,10 @@ private struct SearchScreenRoomCell: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
|
if let onRemove {
|
||||||
|
SearchScreenRemoveBreadcrumbButton(action: onRemove)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.vertical, 12)
|
.padding(.vertical, 12)
|
||||||
@@ -360,6 +367,7 @@ private struct SearchScreenRoomCellButtonStyle: ButtonStyle {
|
|||||||
private struct SearchScreenQueryCell: View {
|
private struct SearchScreenQueryCell: View {
|
||||||
let query: String
|
let query: String
|
||||||
let isSelected: Bool
|
let isSelected: Bool
|
||||||
|
let onRemove: () -> Void
|
||||||
let action: () -> Void
|
let action: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -382,6 +390,8 @@ private struct SearchScreenQueryCell: View {
|
|||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
|
SearchScreenRemoveBreadcrumbButton(action: onRemove)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.vertical, 12)
|
.padding(.vertical, 12)
|
||||||
@@ -393,6 +403,20 @@ private struct SearchScreenQueryCell: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct SearchScreenRemoveBreadcrumbButton: View {
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: action) {
|
||||||
|
CompoundIcon(\.close)
|
||||||
|
.foregroundStyle(.compound.iconTertiary)
|
||||||
|
.accessibilityLabel(L10n.actionRemove)
|
||||||
|
}
|
||||||
|
// Borderless so the button handles its own taps instead of the row it's nested in.
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private struct SearchScreenMessageCell: View {
|
private struct SearchScreenMessageCell: View {
|
||||||
let message: SearchScreenMessage
|
let message: SearchScreenMessage
|
||||||
let context: SearchScreenViewModel.Context
|
let context: SearchScreenViewModel.Context
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:64633ca3b3c02ea5232a753cca290aec12a76541405ce5ef23735270de1bfd2b
|
oid sha256:4bc96637d784a679d6be590589758889a367355ee32bfe970a82e2f6079c3bbe
|
||||||
size 105464
|
size 107266
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9d910a4120f4e0971d5a4d78673b0ec0faeac890f02a3efb2bf66b4908e66981
|
oid sha256:37dc8e32e88ead1940ae69f7b21426e0ec93136061fef5aa217962f8a2b3a9a8
|
||||||
size 107678
|
size 109477
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2c4147f5fa913d830a6dc54c876458633b2532b9f50ca04a6287fc3253cfde65
|
oid sha256:316e278e53ff463ac5a232b4767a420b31c09cc2da2d22e302dccee9d36f6520
|
||||||
size 59617
|
size 61192
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:92654ae660070262a38777684a38894d802488f98f54f6729d230787f7bee83c
|
oid sha256:437431e2bf9b05ebedb52638034bb367ab6990ec3ecdeb4d930b8a3d9078ed11
|
||||||
size 62130
|
size 63703
|
||||||
|
|||||||
@@ -149,6 +149,19 @@ struct SearchScreenViewModelTests {
|
|||||||
#expect(userSettings.searchBreadcrumbs == [.room(roomID: "2"), .query("Second")])
|
#expect(userSettings.searchBreadcrumbs == [.room(roomID: "2"), .query("Second")])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func removingBreadcrumbs() {
|
||||||
|
userSettings.searchBreadcrumbs = [.query("Second"), .room(roomID: "2")]
|
||||||
|
|
||||||
|
context.send(viewAction: .removeBreadcrumb(.query("Second")))
|
||||||
|
|
||||||
|
#expect(userSettings.searchBreadcrumbs == [.room(roomID: "2")])
|
||||||
|
|
||||||
|
context.send(viewAction: .removeBreadcrumb(.room(SearchScreenRoom(.mock(id: "2", name: "Second")))))
|
||||||
|
|
||||||
|
#expect(userSettings.searchBreadcrumbs.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func breadcrumbsAreDisplayed() async throws {
|
func breadcrumbsAreDisplayed() async throws {
|
||||||
staticRoomListSubject.send([.mock(id: "2", name: "Second")])
|
staticRoomListSubject.send([.mock(id: "2", name: "Second")])
|
||||||
|
|||||||
Reference in New Issue
Block a user