Skip to content

Commit 6a832d0

Browse files
johnsideserfclaude
andauthored
feat: filter stale groups and unresolvable contacts from sidebar (closes #256) (#286)
Hide conversations from the default sidebar view when they have no messages AND no meaningful name (empty/abandoned groups with only a raw group ID, or contacts with only a UUID hash). The active conversation is never hidden, and using the sidebar filter (/) shows all conversations including hidden ones. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a053519 commit 6a832d0

File tree

2 files changed

+78
-2
lines changed

2 files changed

+78
-2
lines changed

src/app.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,24 @@ pub struct Conversation {
258258
}
259259

260260
impl Conversation {
261+
/// Whether this conversation is stale and should be hidden from the default sidebar view.
262+
/// A conversation is stale if it has no messages AND has no meaningful name
263+
/// (e.g. empty/abandoned groups, or contacts with only a UUID hash).
264+
pub fn is_stale(&self) -> bool {
265+
if !self.messages.is_empty() {
266+
return false;
267+
}
268+
if self.is_group {
269+
// Group with no messages and no resolved name (name is the raw group ID)
270+
self.name.is_empty() || self.name == self.id
271+
} else {
272+
// 1:1 contact with no messages and no usable name:
273+
// keep if name is a phone number (+...), hide if name is just the raw ID
274+
// (a UUID hash or "..." with no real identity)
275+
!self.name.starts_with('+') && self.name == self.id
276+
}
277+
}
278+
261279
/// Binary-search for a message by timestamp (messages are sorted by `timestamp_ms`).
262280
fn find_msg_idx(&self, ts: i64) -> Option<usize> {
263281
let end = self.messages.partition_point(|m| m.timestamp_ms <= ts);
@@ -9911,4 +9929,51 @@ mod tests {
99119929
assert!(app.typing.indicators.contains_key("+1"),
99129930
"1:1 typing indicator should be keyed by sender phone");
99139931
}
9932+
9933+
#[test]
9934+
fn is_stale_filters_correctly() {
9935+
let empty_group = Conversation {
9936+
name: "abc123groupid".to_string(),
9937+
id: "abc123groupid".to_string(),
9938+
messages: vec![],
9939+
unread: 0,
9940+
is_group: true,
9941+
expiration_timer: 0,
9942+
accepted: true,
9943+
};
9944+
assert!(empty_group.is_stale(), "group with no messages and name==id is stale");
9945+
9946+
let named_group = Conversation {
9947+
name: "Book Club".to_string(),
9948+
id: "abc123groupid".to_string(),
9949+
messages: vec![],
9950+
unread: 0,
9951+
is_group: true,
9952+
expiration_timer: 0,
9953+
accepted: true,
9954+
};
9955+
assert!(!named_group.is_stale(), "group with a real name is not stale");
9956+
9957+
let phone_contact = Conversation {
9958+
name: "+15551234567".to_string(),
9959+
id: "+15551234567".to_string(),
9960+
messages: vec![],
9961+
unread: 0,
9962+
is_group: false,
9963+
expiration_timer: 0,
9964+
accepted: true,
9965+
};
9966+
assert!(!phone_contact.is_stale(), "contact with phone number is not stale");
9967+
9968+
let uuid_contact = Conversation {
9969+
name: "8eb3dbda-1234-5678".to_string(),
9970+
id: "8eb3dbda-1234-5678".to_string(),
9971+
messages: vec![],
9972+
unread: 0,
9973+
is_group: false,
9974+
expiration_timer: 0,
9975+
accepted: true,
9976+
};
9977+
assert!(uuid_contact.is_stale(), "contact with UUID-only name is stale");
9978+
}
99149979
}

src/ui.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -643,15 +643,26 @@ fn draw_sidebar(frame: &mut Frame, app: &mut App, area: Rect) {
643643
let theme = &app.theme;
644644
let max_name_width = (area.width as usize).saturating_sub(5); // "• # " + margin
645645

646-
// Use filtered list when sidebar filter is active
646+
// Use filtered list when sidebar filter is active.
647+
// When filtering, show everything (so users can find hidden conversations).
648+
// In normal view, hide stale conversations (empty groups, unresolvable contacts).
647649
let display_order: Vec<String> = if app.sidebar_filter_active {
648650
if app.sidebar_filter.is_empty() {
649651
app.conversation_order.clone()
650652
} else {
651653
app.sidebar_filtered.clone()
652654
}
653655
} else {
654-
app.conversation_order.clone()
656+
app.conversation_order
657+
.iter()
658+
.filter(|id| {
659+
app.active_conversation.as_ref() == Some(id)
660+
|| app.conversations
661+
.get(*id)
662+
.is_some_and(|c| !c.is_stale())
663+
})
664+
.cloned()
665+
.collect()
655666
};
656667

657668
let items: Vec<ListItem> = display_order

0 commit comments

Comments
 (0)