eleven / ocho Public

ios+mac: filter picker keeps a fixed order; an empty online filter says "Nobody is online" (#552)

The funnel's view picker no longer reorders itself around the current
selection — "All chats" then "Online only", always, with the checkmark
marking where you are. And when a filter leaves the list empty, the pane
says so ("Nobody is online" for the online filter, a generic line for
the category toggles) instead of going silently blank.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell f1489146d0c281ca8efe81d29e7e8e18fd69abf0 parent c415888
2 files changed, +40 −12
  • ios/LChat/Views/ThreadListView.swift +25 −6
  • ios/LChatMac/NativeChatView.swift +15 −6
diff --git a/ios/LChat/Views/ThreadListView.swift b/ios/LChat/Views/ThreadListView.swift
index 0a7436e..65fdb92 100644
--- a/ios/LChat/Views/ThreadListView.swift
+++ b/ios/LChat/Views/ThreadListView.swift
@@ -102,6 +102,14 @@ struct ThreadListView: View {
}
.overlay { if store.loading && store.threads.isEmpty { ProgressView() } }
.overlay { if !store.loading && store.threads.isEmpty { empty } }
+ // The funnel can narrow the list to nothing — say so instead of
+ // presenting a silently blank pane.
+ .overlay {
+ if !store.loading, !store.threads.isEmpty, filtering, visibleThreads.isEmpty,
+ search.trimmingCharacters(in: .whitespaces).isEmpty {
+ filteredEmpty
+ }
+ }
.navigationTitle("Chats") // also backs the chat screen's back button
.navigationBarTitleDisplayMode(.inline)
.toolbar {
@@ -129,13 +137,12 @@ struct ThreadListView: View {
// People/Bots/Apps categories. Filled glyph while narrowing.
ToolbarItem(placement: .topBarTrailing) {
Menu {
- // The view you're already in sits first (checked), so an
- // accidental tap on the top row re-picks it — a no-op —
- // instead of instantly narrowing the list.
+ // A picker in a fixed order — the checkmark marks the view
+ // you're in, and the usual accidental tap (the top row,
+ // while unfiltered) re-picks "All chats", a no-op.
Picker("View", selection: $filterOnline) {
- ForEach(filterOnline ? [true, false] : [false, true], id: \.self) { on in
- Text(on ? "Online only" : "All chats").tag(on)
- }
+ Text("All chats").tag(false)
+ Text("Online only").tag(true)
}
.pickerStyle(.inline)
Divider()
@@ -300,6 +307,18 @@ struct ThreadListView: View {
.padding()
}
+ // The funnel emptied the list. Name the online case — it's the one people
+ // actually hit — and fall back to a generic line for the category toggles.
+ private var filteredEmpty: some View {
+ VStack(spacing: 8) {
+ Image(systemName: "line.3.horizontal.decrease.circle")
+ .font(.system(size: 36)).foregroundStyle(.secondary)
+ Text(filterOnline ? "Nobody is online" : "No chats match your filters")
+ .font(.callout).foregroundStyle(.secondary)
+ }
+ .padding()
+ }
+
// One instance-wide socket keeps the LIST live (the Mac sidebar's trick):
// the hub fans every thread's frames to any member connection, so stored-
// message activity — including a brand-new member's first message —
diff --git a/ios/LChatMac/NativeChatView.swift b/ios/LChatMac/NativeChatView.swift
index 9769469..5e48baf 100644
--- a/ios/LChatMac/NativeChatView.swift
+++ b/ios/LChatMac/NativeChatView.swift
@@ -835,13 +835,12 @@ struct NativeChatView: View {
// List filters (web's funnel beside search): online-only +
// the People/Bots/Apps categories. Filled while narrowing.
Menu {
- // The view you're already in sits first (checked), so an
- // accidental click on the top row re-picks it — a no-op —
- // instead of instantly narrowing the list.
+ // A picker in a fixed order — the checkmark marks the view
+ // you're in, and the usual accidental click (the top row,
+ // while unfiltered) re-picks "All chats", a no-op.
Picker("View", selection: $filterOnline) {
- ForEach(filterOnline ? [true, false] : [false, true], id: \.self) { on in
- Text(on ? "Online only" : "All chats").tag(on)
- }
+ Text("All chats").tag(false)
+ Text("Online only").tag(true)
}
.pickerStyle(.inline)
Divider()
@@ -1063,6 +1062,16 @@ struct NativeChatView: View {
ForEach(visibleThreads) { thread in
threadRow(thread)
}
+ // The funnel emptied the list — say so instead of presenting
+ // a silently blank pane. (Search's empty case is the panel's.)
+ if visibleThreads.isEmpty, filtering,
+ search.trimmingCharacters(in: .whitespaces).isEmpty {
+ Text(filterOnline ? "Nobody is online" : "No chats match your filters")
+ .font(.system(size: 12))
+ .foregroundStyle(.secondary)
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 14)
+ }
}
.padding(6)
}