summaryrefslogtreecommitdiff
path: root/Hutch/Views/Projects/ProjectMailingListView.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-22 20:42:45 -0500
committerChristian Cleberg <[email protected]>2026-03-22 20:42:45 -0500
commit1f8b7a23529462c3c332753d33810b5c9f692bbc (patch)
tree17526dae77a7579b270e6b9cca6224c9020f772d /Hutch/Views/Projects/ProjectMailingListView.swift
parente03791d6c15ee0caac033d1b1b390c5d65a69d57 (diff)
downloadhutch-1f8b7a23529462c3c332753d33810b5c9f692bbc.tar.gz
hutch-1f8b7a23529462c3c332753d33810b5c9f692bbc.tar.bz2
hutch-1f8b7a23529462c3c332753d33810b5c9f692bbc.zip
feat: add search to builds, trackers, tickets, pastes, mailing lists, and inbox
Diffstat (limited to 'Hutch/Views/Projects/ProjectMailingListView.swift')
-rw-r--r--Hutch/Views/Projects/ProjectMailingListView.swift38
1 files changed, 37 insertions, 1 deletions
diff --git a/Hutch/Views/Projects/ProjectMailingListView.swift b/Hutch/Views/Projects/ProjectMailingListView.swift
index df0ffe1..e044efb 100644
--- a/Hutch/Views/Projects/ProjectMailingListView.swift
+++ b/Hutch/Views/Projects/ProjectMailingListView.swift
@@ -32,6 +32,7 @@ final class MailingListDetailViewModel {
private(set) var threads: [InboxThreadSummary] = []
private(set) var isLoading = false
var error: String?
+ var searchText = ""
private let mailingList: InboxMailingListReference
private let client: SRHTClient
@@ -61,6 +62,10 @@ final class MailingListDetailViewModel {
self.client = client
}
+ var filteredThreads: [InboxThreadSummary] {
+ Self.filterThreads(threads, matching: searchText)
+ }
+
func loadThreads() async {
guard !isLoading else { return }
isLoading = true
@@ -187,6 +192,30 @@ final class MailingListDetailViewModel {
return lhs.lastActivityAt > rhs.lastActivityAt
}
}
+
+ nonisolated static func filterThreads(
+ _ threads: [InboxThreadSummary],
+ matching query: String
+ ) -> [InboxThreadSummary] {
+ let q = query.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
+ guard !q.isEmpty else { return threads }
+ return threads.filter {
+ normalizedSubject(from: $0.subject).contains(q) ||
+ $0.latestSender.canonicalName.lowercased().contains(q)
+ }
+ }
+
+ private nonisolated static func normalizedSubject(from subject: String) -> String {
+ subject
+ .replacingOccurrences(of: #"\s+"#, with: " ", options: .regularExpression)
+ .trimmingCharacters(in: .whitespacesAndNewlines)
+ .replacingOccurrences(
+ of: #"^(?:(?:re|fwd?)\s*:\s*)+"#,
+ with: "",
+ options: [.regularExpression, .caseInsensitive]
+ )
+ .lowercased()
+ }
}
struct MailingListDetailView: View {
@@ -225,7 +254,7 @@ struct MailingListDetailView: View {
@Bindable var vm = viewModel
List {
- ForEach(viewModel.threads) { thread in
+ ForEach(viewModel.filteredThreads) { thread in
NavigationLink(value: MoreRoute.thread(thread)) {
InboxThreadRow(thread: thread)
}
@@ -249,6 +278,11 @@ struct MailingListDetailView: View {
}
}
.listStyle(.plain)
+ .searchable(
+ text: $vm.searchText,
+ placement: .navigationBarDrawer(displayMode: .always),
+ prompt: "Search messages"
+ )
.overlay {
if viewModel.isLoading, viewModel.threads.isEmpty {
SRHTLoadingStateView(message: "Loading mailing list…")
@@ -258,6 +292,8 @@ struct MailingListDetailView: View {
message: error,
retryAction: { await viewModel.loadThreads() }
)
+ } else if !viewModel.threads.isEmpty, viewModel.filteredThreads.isEmpty {
+ ContentUnavailableView.search(text: viewModel.searchText)
} else if viewModel.threads.isEmpty {
ContentUnavailableView(
"No Threads",