Skip to content

Commit 8a93640

Browse files
authored
Merge pull request #2099 from meshtastic/feat/message-search-searchable
feat(messages): move find-in-conversation to a searchable field
2 parents e5b027c + cf2ff7b commit 8a93640

4 files changed

Lines changed: 32 additions & 104 deletions

File tree

Meshtastic/Views/Messages/ChannelMessageList.swift

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ struct ChannelMessageList: View {
5454
@State private var messageToHighlight: Int64 = 0
5555
@State private var messageLimit: Int = 100
5656
@State private var messages: [MessageEntity] = []
57-
@State private var isSearching = false
5857
@State private var searchQuery = ""
5958
@State private var searchMatches: [MessageSearchMatch] = []
6059
@State private var currentMatchIndex = -1
@@ -335,7 +334,7 @@ struct ChannelMessageList: View {
335334

336335
var body: some View {
337336
VStack(spacing: 0) {
338-
if isSearching { searchBar }
337+
if !searchQuery.isEmpty { searchBar }
339338
ScrollViewReader { scrollView in
340339
ScrollView {
341340
LazyVStack {
@@ -458,9 +457,10 @@ struct ChannelMessageList: View {
458457
}
459458
}
460459
.navigationBarTitleDisplayMode(.inline)
460+
.searchable(text: $searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: "Find in conversation")
461+
.autocorrectionDisabled()
461462
.task(id: searchQuery) { await debouncedSearch() }
462463
.toolbar {
463-
ToolbarItem(placement: .navigationBarTrailing) { searchToolbarButton }
464464
ToolbarItem(placement: .principal) {
465465
HStack {
466466
CircleText(text: String(channel.index), color: .accentColor, circleSize: 44).fixedSize()
@@ -495,12 +495,10 @@ struct ChannelMessageList: View {
495495
private extension ChannelMessageList {
496496
@ViewBuilder var searchBar: some View {
497497
MessageSearchBar(
498-
query: $searchQuery,
499498
matchCount: searchMatches.count,
500499
currentIndex: currentMatchIndex,
501500
onPrevious: goToPreviousMatch,
502-
onNext: goToNextMatch,
503-
onClose: closeSearch
501+
onNext: goToNextMatch
504502
)
505503
}
506504

@@ -513,20 +511,10 @@ private extension ChannelMessageList {
513511
}
514512
}
515513

516-
@ViewBuilder var searchToolbarButton: some View {
517-
Button {
518-
if isSearching { closeSearch() } else { isSearching = true }
519-
} label: {
520-
Image(systemName: isSearching ? "magnifyingglass.circle.fill" : "magnifyingglass")
521-
}
522-
.accessibilityLabel("Find in conversation")
523-
}
524-
525514
/// Debounces search so a full-store scan doesn't run on every keystroke. Cancelled and
526515
/// restarted by `.task(id: searchQuery)` whenever the query changes.
527516
@MainActor
528517
func debouncedSearch() async {
529-
guard isSearching else { return }
530518
// Clearing the field should empty the results immediately, not after the debounce.
531519
guard !searchQuery.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
532520
await runSearch()
@@ -600,12 +588,4 @@ private extension ChannelMessageList {
600588
guard !searchMatches.isEmpty else { return }
601589
focusMatch(at: currentMatchIndex - 1 < 0 ? searchMatches.count - 1 : currentMatchIndex - 1)
602590
}
603-
604-
func closeSearch() {
605-
isSearching = false
606-
searchQuery = ""
607-
searchMatches = []
608-
currentMatchIndex = -1
609-
messageToHighlight = -1
610-
}
611591
}

Meshtastic/Views/Messages/MessageSearchBar.swift

Lines changed: 21 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,83 +2,51 @@
22
// MessageSearchBar.swift
33
// Meshtastic
44
//
5-
// Shared "find in conversation" bar for ChannelMessageList / UserMessageList.
6-
// Live match count + current index, previous/next navigation, and a close button.
5+
// Shared "find in conversation" results bar for ChannelMessageList / UserMessageList:
6+
// live match count + current index and previous/next navigation. The search field itself
7+
// is provided by each list's `.searchable` modifier; this bar sits just beneath it while
8+
// a query is active.
79
//
810

911
import SwiftUI
1012

1113
struct MessageSearchBar: View {
12-
@Binding var query: String
1314
/// Total number of matches for the current query.
1415
let matchCount: Int
1516
/// Zero-based index of the currently-focused match, or -1 when there are none.
1617
let currentIndex: Int
1718
var onPrevious: () -> Void
1819
var onNext: () -> Void
19-
var onClose: () -> Void
20-
21-
@FocusState private var focused: Bool
2220

2321
var body: some View {
24-
HStack(spacing: 10) {
25-
HStack(spacing: 6) {
26-
Image(systemName: "magnifyingglass")
27-
.foregroundStyle(.secondary)
28-
TextField("Find in conversation", text: $query)
29-
.textFieldStyle(.plain)
30-
.autocorrectionDisabled()
31-
.submitLabel(.search)
32-
.focused($focused)
33-
.onSubmit(onNext)
34-
if !query.isEmpty {
35-
Button {
36-
query = ""
37-
focused = true
38-
} label: {
39-
Image(systemName: "xmark.circle.fill")
40-
.foregroundStyle(.secondary)
41-
}
42-
.buttonStyle(.borderless)
43-
.accessibilityLabel("Clear search")
44-
}
45-
}
46-
.padding(.horizontal, 10)
47-
.padding(.vertical, 7)
48-
.background(Color(.secondarySystemBackground))
49-
.clipShape(RoundedRectangle(cornerRadius: 10))
22+
HStack(spacing: 16) {
23+
Text(positionText)
24+
.font(.caption)
25+
.monospacedDigit()
26+
.foregroundStyle(.secondary)
27+
.accessibilityLabel(matchCount == 0 ? Text("No matches") : Text("Match \(currentIndex + 1) of \(matchCount)"))
5028

51-
if !query.isEmpty {
52-
Text(positionText)
53-
.font(.caption)
54-
.monospacedDigit()
55-
.foregroundStyle(.secondary)
56-
.accessibilityLabel(matchCount == 0 ? Text("No matches") : Text("Match \(currentIndex + 1) of \(matchCount)"))
29+
Spacer()
5730

58-
Button(action: onPrevious) {
59-
Image(systemName: "chevron.up")
60-
}
61-
.disabled(matchCount == 0)
62-
.accessibilityLabel("Previous match")
63-
64-
Button(action: onNext) {
65-
Image(systemName: "chevron.down")
66-
}
67-
.disabled(matchCount == 0)
68-
.accessibilityLabel("Next match")
31+
Button(action: onPrevious) {
32+
Image(systemName: "chevron.up")
6933
}
34+
.disabled(matchCount == 0)
35+
.accessibilityLabel("Previous match")
7036

71-
Button("Done", action: onClose)
72-
.font(.callout)
37+
Button(action: onNext) {
38+
Image(systemName: "chevron.down")
39+
}
40+
.disabled(matchCount == 0)
41+
.accessibilityLabel("Next match")
7342
}
7443
.buttonStyle(.borderless)
7544
.padding(.horizontal)
7645
.padding(.vertical, 8)
7746
.background(.bar)
78-
.onAppear { focused = true }
7947
}
8048

8149
private var positionText: String {
82-
matchCount == 0 ? "0/0" : "\(currentIndex + 1)/\(matchCount)"
50+
matchCount == 0 ? "No matches" : "\(currentIndex + 1) of \(matchCount)"
8351
}
8452
}

Meshtastic/Views/Messages/UserMessageList.swift

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ struct UserMessageList: View {
5353
@AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1
5454
@State private var messageLimit: Int = 100
5555
@State private var messages: [MessageEntity] = []
56-
@State private var isSearching = false
5756
@State private var searchQuery = ""
5857
@State private var searchMatches: [MessageSearchMatch] = []
5958
@State private var currentMatchIndex = -1
@@ -411,7 +410,7 @@ struct UserMessageList: View {
411410

412411
var body: some View {
413412
VStack {
414-
if isSearching { searchBar }
413+
if !searchQuery.isEmpty { searchBar }
415414
ScrollViewReader { scrollView in
416415
ScrollView {
417416
LazyVStack {
@@ -531,9 +530,10 @@ struct UserMessageList: View {
531530
.fixedSize(horizontal: false, vertical: true)
532531
}
533532
.navigationBarTitleDisplayMode(.inline)
533+
.searchable(text: $searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: "Find in conversation")
534+
.autocorrectionDisabled()
534535
.task(id: searchQuery) { await debouncedSearch() }
535536
.toolbar {
536-
ToolbarItem(placement: .navigationBarTrailing) { searchToolbarButton }
537537
if !user.keyMatch {
538538
ToolbarItem(placement: .bottomBar) {
539539
VStack {
@@ -578,12 +578,10 @@ struct UserMessageList: View {
578578
private extension UserMessageList {
579579
@ViewBuilder var searchBar: some View {
580580
MessageSearchBar(
581-
query: $searchQuery,
582581
matchCount: searchMatches.count,
583582
currentIndex: currentMatchIndex,
584583
onPrevious: goToPreviousMatch,
585-
onNext: goToNextMatch,
586-
onClose: closeSearch
584+
onNext: goToNextMatch
587585
)
588586
}
589587

@@ -596,20 +594,10 @@ private extension UserMessageList {
596594
}
597595
}
598596

599-
@ViewBuilder var searchToolbarButton: some View {
600-
Button {
601-
if isSearching { closeSearch() } else { isSearching = true }
602-
} label: {
603-
Image(systemName: isSearching ? "magnifyingglass.circle.fill" : "magnifyingglass")
604-
}
605-
.accessibilityLabel("Find in conversation")
606-
}
607-
608597
/// Debounces search so a full-store scan doesn't run on every keystroke. Cancelled and
609598
/// restarted by `.task(id: searchQuery)` whenever the query changes.
610599
@MainActor
611600
func debouncedSearch() async {
612-
guard isSearching else { return }
613601
// Clearing the field should empty the results immediately, not after the debounce.
614602
guard !searchQuery.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
615603
await runSearch()
@@ -683,12 +671,4 @@ private extension UserMessageList {
683671
guard !searchMatches.isEmpty else { return }
684672
focusMatch(at: currentMatchIndex - 1 < 0 ? searchMatches.count - 1 : currentMatchIndex - 1)
685673
}
686-
687-
func closeSearch() {
688-
isSearching = false
689-
searchQuery = ""
690-
searchMatches = []
691-
currentMatchIndex = -1
692-
messageToHighlight = -1
693-
}
694674
}

docs/user/messages.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,11 @@ Long press any message and tap **Tapback** to send an emoji reaction.
102102

103103
## Find in Conversation
104104

105-
Tap the **magnifying glass** in the top-right of any channel or direct-message conversation to open the find bar, then type to search that conversation's message text.
105+
Tap the **Find in conversation** search field below the title of any channel or direct-message conversation, then type to search that conversation's message text.
106106

107107
- Matching is **case- and accent-insensitive**, and searches the **entire conversation history** — not just the messages currently on screen.
108-
- The bar shows your position in the results (e.g. **2/7**); use the **up/down chevrons** to jump to the previous or next match, wrapping around at the ends. The current match is highlighted and scrolled into view, loading older messages automatically if needed.
109-
- Tap **Done** (or the magnifying glass again) to close the bar and clear the search.
108+
- A results bar shows your position in the matches (e.g. **2 of 7**); use the **up/down chevrons** to jump to the previous or next match, wrapping around at the ends. The current match is highlighted and scrolled into view, loading older messages automatically if needed.
109+
- Tap **Cancel** (or clear the field) to dismiss the search and return to the conversation.
110110

111111
Search covers the text of channel broadcasts and direct messages, matching exactly the messages each conversation shows. Emoji reactions aren't matched.
112112

0 commit comments

Comments
 (0)