diff --git a/melib/src/thread.rs b/melib/src/thread.rs index ed2c8adbc..0daa2ddf3 100644 --- a/melib/src/thread.rs +++ b/melib/src/thread.rs @@ -417,10 +417,25 @@ impl ThreadNode { self.has_unseen } + pub fn set_has_unseen(&mut self, new_val: bool) { + self.has_unseen = new_val; + } + pub fn len(&self) -> usize { self.len } + pub fn date(&self) -> UnixTimestamp { + self.date + } + + pub fn datetime(&self) -> chrono::DateTime { + use chrono::{TimeZone, Utc}; + use std::convert::TryInto; + + Utc.timestamp(self.date.try_into().unwrap_or(0), 0) + } + pub fn is_empty(&self) -> bool { self.parent.is_none() && self.message.is_none() && self.children.is_empty() } @@ -575,12 +590,12 @@ impl<'a> Iterator for RootIterator<'a> { } } -fn find_ref(buf: &FnvHashMap, h: ThreadHash) -> ThreadHash { +pub fn find_thread_group(buf: &FnvHashMap, h: ThreadHash) -> ThreadHash { if buf[&h].thread_group == h { return h; } let p = buf[&h].thread_group; - find_ref(buf, p) + find_thread_group(buf, p) } fn find(buf: &mut FnvHashMap, h: ThreadHash) -> ThreadHash { if buf[&h].thread_group == h { @@ -617,7 +632,7 @@ fn union(buf: &mut FnvHashMap, x: ThreadHash, y: ThreadH impl Threads { pub fn is_snoozed(&self, h: ThreadHash) -> bool { - let root = find_ref(&self.thread_nodes, h); + let root = find_thread_group(&self.thread_nodes, h); self.thread_nodes[&root].snoozed() } pub fn find(&mut self, i: ThreadHash) -> ThreadHash { diff --git a/ui/src/components/mail/listing/conversations.rs b/ui/src/components/mail/listing/conversations.rs index 5c8c311c4..716f0882d 100644 --- a/ui/src/components/mail/listing/conversations.rs +++ b/ui/src/components/mail/listing/conversations.rs @@ -21,6 +21,7 @@ use super::*; use crate::components::utilities::PageMovement; +use std::iter::FromIterator; use std::ops::{Deref, DerefMut}; const MAX_COLS: usize = 500; @@ -892,6 +893,71 @@ impl ConversationsListing { self.filtered_selection[cursor] } } + + fn perform_action( + &mut self, + context: &mut Context, + thread_hash: ThreadHash, + a: &ListingAction, + ) { + let account = &mut context.accounts[self.cursor_pos.0]; + let mut envs_to_set: StackVec = StackVec::new(); + { + let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); + let mut stack = StackVec::new(); + stack.push(thread_hash); + while let Some(thread_iter) = stack.pop() { + { + let threads = account.collection.threads.get_mut(&folder_hash).unwrap(); + threads + .thread_nodes + .entry(thread_iter) + .and_modify(|t| t.set_has_unseen(false)); + } + let threads = &account.collection.threads[&folder_hash]; + if let Some(env_hash) = threads[&thread_iter].message() { + if !account.contains_key(env_hash) { + /* The envelope has been renamed or removed, so wait for the appropriate event to + * arrive */ + continue; + } + envs_to_set.push(env_hash); + } + for c in 0..threads[&thread_iter].children().len() { + let c = threads[&thread_iter].children()[c]; + stack.push(c); + } + } + } + for env_hash in envs_to_set { + match a { + ListingAction::SetSeen => { + let hash = account.get_env(&env_hash).hash(); + let op = account.operation(hash); + let envelope: &mut Envelope = &mut account.get_env_mut(&env_hash); + if let Err(e) = envelope.set_seen(op) { + context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(e.to_string()), + )); + } + self.row_updates.push(thread_hash); + } + ListingAction::SetUnseen => { + let hash = account.get_env(&env_hash).hash(); + let op = account.operation(hash); + let envelope: &mut Envelope = &mut account.get_env_mut(&env_hash); + if let Err(e) = envelope.set_unseen(op) { + context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(e.to_string()), + )); + } + self.row_updates.push(thread_hash); + } + ListingAction::Delete => { /* do nothing */ } + _ => unreachable!(), + } + } + } } impl Component for ConversationsListing { @@ -1074,6 +1140,12 @@ impl Component for ConversationsListing { self.movement = Some(PageMovement::End); self.set_dirty(); } + UIEvent::Input(ref key) if *key == shortcuts["set_seen"] => { + let thread_hash = self.get_thread_under_cursor(self.cursor_pos.2, context); + self.perform_action(context, thread_hash, &ListingAction::SetSeen); + self.row_updates.push(thread_hash); + self.set_dirty(); + } UIEvent::Input(ref k) if self.unfocused && *k == shortcuts["exit_thread"] => { self.unfocused = false; self.dirty = true; @@ -1116,14 +1188,35 @@ impl Component for ConversationsListing { if !threads.thread_nodes.contains_key(&new_env_thread_hash) { return false; } - let thread_group = threads.thread_nodes[&new_env_thread_hash].thread_group(); - let (&thread_hash, _): (&ThreadHash, &usize) = self + let thread_group = melib::find_thread_group( + &threads.thread_nodes, + threads.thread_nodes[&new_env_thread_hash].thread_group(), + ); + let (&thread_hash, &row): (&ThreadHash, &usize) = self .order .iter() - .find(|(n, _)| threads.thread_nodes[&n].thread_group() == thread_group) + .find(|(n, _)| { + melib::find_thread_group( + &threads.thread_nodes, + threads.thread_nodes[&n].thread_group(), + ) == thread_group + }) .unwrap(); - self.row_updates.push(thread_hash); + let new_thread_hash = threads.root_set(row); + self.row_updates.push(new_thread_hash); + if let Some(row) = self.order.remove(&thread_hash) { + self.order.insert(new_thread_hash, row); + let selection_status = self.selection.remove(&thread_hash).unwrap(); + self.selection.insert(new_thread_hash, selection_status); + for h in self.filtered_selection.iter_mut() { + if *h == thread_hash { + *h = new_thread_hash; + break; + } + } + } + self.dirty = true; self.view @@ -1182,10 +1275,6 @@ impl Component for ConversationsListing { | Action::Listing(a @ ListingAction::Delete) if !self.unfocused => { - /* Iterate over selection if exists, else only over the envelope under the - * cursor. Using two iterators allows chaining them which results into a Chain - * type. We can't conditonally select either a slice iterator or a Map iterator - * because of the type system */ let is_selection_empty = self.selection.values().cloned().any(std::convert::identity); let i = [self.get_thread_under_cursor(self.cursor_pos.2, context)]; @@ -1200,60 +1289,11 @@ impl Component for ConversationsListing { let iter = sel_iter .into_iter() .flatten() - .chain(cursor_iter.into_iter().flatten()); - for &i in iter { - let account = &mut context.accounts[self.cursor_pos.0]; - let mut envs_to_set: StackVec = StackVec::new(); - { - let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); - let threads = &account.collection.threads[&folder_hash]; - let mut stack = StackVec::new(); - stack.push(i); - while let Some(thread_iter) = stack.pop() { - if let Some(env_hash) = threads[&thread_iter].message() { - if !account.contains_key(env_hash) { - /* The envelope has been renamed or removed, so wait for the appropriate event to - * arrive */ - continue; - } - envs_to_set.push(env_hash); - } - for c in 0..threads[&thread_iter].children().len() { - let c = threads[&thread_iter].children()[c]; - stack.push(c); - } - } - } - for env_hash in envs_to_set { - match a { - ListingAction::SetSeen => { - let hash = account.get_env(&env_hash).hash(); - let op = account.operation(hash); - let envelope: &mut Envelope = - &mut account.get_env_mut(&env_hash); - if let Err(e) = envelope.set_seen(op) { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage(e.to_string()), - )); - } - self.row_updates.push(i); - } - ListingAction::SetUnseen => { - let hash = account.get_env(&env_hash).hash(); - let op = account.operation(hash); - let envelope: &mut Envelope = - &mut account.get_env_mut(&env_hash); - if let Err(e) = envelope.set_unseen(op) { - context.replies.push_back(UIEvent::StatusEvent( - StatusEvent::DisplayMessage(e.to_string()), - )); - } - self.row_updates.push(i); - } - ListingAction::Delete => { /* do nothing */ } - _ => unreachable!(), - } - } + .chain(cursor_iter.into_iter().flatten()) + .cloned(); + let stack = StackVec::from_iter(iter.into_iter()); + for i in stack { + self.perform_action(context, i, a); } self.dirty = true; for v in self.selection.values_mut() { @@ -1344,6 +1384,14 @@ impl Component for ConversationsListing { Key::Char('v') }, ), + ( + "set_seen", + if let Some(key) = config_map.get("set_seen") { + (*key).clone() + } else { + Key::Char('n') + }, + ), ] .iter() .cloned() diff --git a/ui/src/conf/shortcuts.rs b/ui/src/conf/shortcuts.rs index 6ff569cb2..370cc79fd 100644 --- a/ui/src/conf/shortcuts.rs +++ b/ui/src/conf/shortcuts.rs @@ -51,7 +51,8 @@ shortcut_key_values! { "compact_listing", /// Shortcut listing for a mail listing in compact mode. pub struct CompactListingShortcuts { open_thread: Key |> "Open thread.", - exit_thread: Key |> "Exit thread view." + exit_thread: Key |> "Exit thread view.", + set_seen: Key |> "Set thread as seen." } } @@ -60,6 +61,7 @@ impl Default for CompactListingShortcuts { CompactListingShortcuts { open_thread: Key::Char('\n'), exit_thread: Key::Char('i'), + set_seen: Key::Char('n'), } } }