diff --git a/melib/src/thread.rs b/melib/src/thread.rs index 6cee67445..d761a3230 100644 --- a/melib/src/thread.rs +++ b/melib/src/thread.rs @@ -36,16 +36,18 @@ use crate::datetime::UnixTimestamp; use crate::email::parser::BytesExt; use crate::email::*; +mod iterators; +pub use iterators::*; + #[cfg(feature = "unicode_algorithms")] use text_processing::grapheme_clusters::*; use uuid::Uuid; use fnv::{FnvHashMap, FnvHashSet}; -use std::cell::{Ref, RefCell}; +use std::cell::RefCell; use std::cmp::Ordering; use std::fmt; use std::iter::FromIterator; -use std::mem; use std::ops::Index; use std::result::Result as StdResult; use std::str::FromStr; @@ -56,64 +58,39 @@ use smallvec::SmallVec; type Envelopes = Arc>>; -#[derive(PartialEq, Hash, Eq, Copy, Clone, Serialize, Deserialize, Default)] -pub struct ThreadHash(Uuid); +macro_rules! uuid_hash_type { + ($n:ident) => { + #[derive(PartialEq, Hash, Eq, Copy, Clone, Serialize, Deserialize, Default)] + pub struct $n(Uuid); -impl fmt::Debug for ThreadHash { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0.to_string()) - } + impl fmt::Debug for $n { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0.to_string()) + } + } + + impl fmt::Display for $n { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0.to_string()) + } + } + + impl $n { + fn new() -> Self { + $n(Uuid::new_v4()) + } + pub fn null() -> Self { + $n(Uuid::nil()) + } + } + }; } -impl fmt::Display for ThreadHash { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0.to_string()) - } -} - -impl ThreadHash { - fn new() -> Self { - ThreadHash(Uuid::new_v4()) - } - pub fn null() -> Self { - ThreadHash(Uuid::nil()) - } -} +uuid_hash_type!(ThreadHash); +uuid_hash_type!(ThreadGroupHash); /* Helper macros to avoid repeating ourselves */ -fn rec_change_root_parent( - b: &mut FnvHashMap, - idx: ThreadHash, - new_root: ThreadHash, -) { - let parent = { - let entry = b.entry(idx).or_default(); - entry.thread_group = new_root; - entry.parent - }; - if let Some(p) = parent { - rec_change_children(b, p, new_root); - rec_change_root_parent(b, p, new_root); - } -} -fn rec_change_children( - b: &mut FnvHashMap, - idx: ThreadHash, - new_root: ThreadHash, -) { - b.entry(idx).and_modify(|e| { - e.thread_group = new_root; - }); - - let mut ctr = 0; - while ctr < b[&idx].children.len() { - let c = b[&idx].children[ctr]; - rec_change_children(b, c, new_root); - ctr += 1; - } -} - macro_rules! remove_from_parent { ($buf:expr, $idx:expr) => {{ let mut parent: Option = None; @@ -125,34 +102,63 @@ macro_rules! remove_from_parent { e.children.remove(pos); }); } - rec_change_root_parent($buf, p, p); } $buf.entry($idx).and_modify(|e| e.parent = None); - rec_change_children($buf, $idx, $idx); - $buf.entry($idx).and_modify(|e| e.thread_group = $idx); parent }}; } macro_rules! make { (($p:expr)parent of($c:expr), $threads:expr) => {{ - if $threads.find($c) != $threads.find($p) { + let old_group_hash = $threads.find_group($threads.thread_nodes[&$c].group); + let parent_group_hash = $threads.find_group($threads.thread_nodes[&$p].group); + if old_group_hash != parent_group_hash { let prev_parent = remove_from_parent!(&mut $threads.thread_nodes, $c); if !($threads.thread_nodes[&$p]).children.contains(&$c) { /* Pruned nodes keep their children in case they show up in a later merge, so do not panic * if children exists */ $threads.thread_nodes.entry($p).and_modify(|e| e.children.push($c)); } - let child_len = $threads.thread_nodes[&$c].len; - let has_unseen = $threads.thread_nodes[&$c].has_unseen; $threads.thread_nodes.entry($c).and_modify(|e| { e.parent = Some($p); }); - $threads.thread_nodes.entry($p).and_modify(|e| { - e.len += child_len + 1; - e.has_unseen |= has_unseen; + let old_group = $threads.groups[&old_group_hash].clone(); + $threads.thread_nodes.entry($c).and_modify(|e| { + e.group = parent_group_hash; }); - $threads.union($c, $p); + $threads.thread_nodes.entry($p).and_modify(|e| { + e.group = parent_group_hash; + }); + { + let parent_group = $threads.groups.entry(parent_group_hash).or_default(); + match (parent_group, old_group) { + (ThreadGroup::Group { + ref mut date, + ref mut len, + ref mut unseen, + ref mut snoozed, + .. + }, ThreadGroup::Group { + date: old_date, + len: old_len, + unseen: old_unseen, + snoozed: old_snoozed, + .. + }) => { + *date = std::cmp::max(old_date, *date); + *len += old_len; + *unseen |= old_unseen; + *snoozed |= old_snoozed; + } + _ => unreachable!(), + } + } + { + let old_group = $threads.groups.entry(old_group_hash).or_default(); + *old_group = ThreadGroup::Node { + parent: RefCell::new(parent_group_hash), + }; + } prev_parent } else { None } }}; @@ -263,118 +269,69 @@ impl FromStr for SortOrder { } } -/* `ThreadsIterator` returns messages according to the sorted order. For example, for the following - * threads: - * - * ``` - * A_ - * |_ B - * |_C - * D - * E_ - * |_F - * ``` - * - * the iterator returns them as `A, B, C, D, E, F` - */ +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum ThreadGroup { + Group { + root: ThreadHash, + date: UnixTimestamp, + len: usize, + unseen: usize, -pub struct ThreadsIterator<'a> { - pos: usize, - stack: SmallVec<[usize; 16]>, - root_tree: Ref<'a, Vec>, - thread_nodes: &'a FnvHashMap, + snoozed: bool, + }, + Node { + parent: RefCell, + }, } -impl<'a> Iterator for ThreadsIterator<'a> { - type Item = (usize, ThreadHash, bool); - fn next(&mut self) -> Option<(usize, ThreadHash, bool)> { - { - let mut tree = &(*self.root_tree); - for i in self.stack.iter() { - tree = &self.thread_nodes[&tree[*i]].children; - } - if self.pos == tree.len() { - if let Some(p) = self.stack.pop() { - self.pos = p + 1; - } else { - return None; - } - } else { - debug_assert!(self.pos < tree.len()); - let ret = ( - self.stack.len(), - tree[self.pos], - !self.stack.is_empty() && (self.pos < (tree.len() - 1)), - ); - if !self.thread_nodes[&tree[self.pos]].children.is_empty() { - self.stack.push(self.pos); - self.pos = 0; - if self.thread_nodes[&ret.1].message.is_some() { - return Some(ret); - } else { - return self.next(); - } - } - self.pos += 1; - if self.thread_nodes[&ret.1].message.is_some() { - return Some(ret); - } - } + +impl Default for ThreadGroup { + fn default() -> Self { + ThreadGroup::Group { + root: ThreadHash::null(), + date: 0, + len: 0, + unseen: 0, + snoozed: false, } - self.next() } } -/* `ThreadIterator` returns messages of a specific thread according to the sorted order. For example, for the following - * thread: - * - * ``` - * A_ - * |_ B - * |_C - * |_D - * ``` - * - * the iterator returns them as `A, B, C, D` - */ -pub struct ThreadIterator<'a> { - init_pos: usize, - pos: usize, - stack: SmallVec<[usize; 16]>, - root_tree: Ref<'a, Vec>, - thread_nodes: &'a FnvHashMap, -} -impl<'a> Iterator for ThreadIterator<'a> { - type Item = (usize, ThreadHash); - fn next(&mut self) -> Option<(usize, ThreadHash)> { - { - let mut tree = &(*self.root_tree); - for i in self.stack.iter() { - tree = &self.thread_nodes[&tree[*i]].children; - } - if self.pos == tree.len() || (self.stack.is_empty() && self.pos > self.init_pos) { - if self.stack.is_empty() { - return None; - } - self.pos = self.stack.pop().unwrap() + 1; - } else { - debug_assert!(self.pos < tree.len()); - let ret = (self.stack.len(), tree[self.pos]); - if !self.thread_nodes[&tree[self.pos]].children.is_empty() { - self.stack.push(self.pos); - self.pos = 0; - if self.thread_nodes[&ret.1].message.is_some() { - return Some(ret); - } else { - return self.next(); - } - } - self.pos += 1; - if self.thread_nodes[&ret.1].message.is_some() { - return Some(ret); +macro_rules! property { + ($name:ident: $t:ty, $e:expr) => { + pub fn $name(&self) -> $t { + match self { + ThreadGroup::Group { $name, .. } => (*$name).into(), + _ => { + debug!( + "ThreadGroup::{}() called on a ThreadGroup::Node: {:?}", + stringify!($name), + self + ); + $e } } } - self.next() + } +} +impl ThreadGroup { + property!(root: Option, None); + property!(len: usize, 0); + property!(unseen: usize, 0); + property!(snoozed: bool, false); + property!(date: UnixTimestamp, 0); + + pub fn set_snoozed(&mut self, val: bool) { + match self { + ThreadGroup::Group { + ref mut snoozed, .. + } => *snoozed = val, + _ => { + debug!( + "ThreadGroup::set_snoozed() called on a ThreadGroup::Node: {:?}", + self + ); + } + } } } @@ -384,19 +341,12 @@ pub struct ThreadNode { parent: Option, children: Vec, date: UnixTimestamp, - indentation: usize, show_subject: bool, pruned: bool, is_root: bool, + pub group: ThreadGroupHash, - len: usize, - has_unseen: bool, - - snoozed: bool, - - /* Union/Find set fields */ - thread_group: ThreadHash, - rank: i32, + unseen: bool, } impl Default for ThreadNode { @@ -406,25 +356,19 @@ impl Default for ThreadNode { parent: None, children: Vec::new(), date: UnixTimestamp::default(), - indentation: 0, show_subject: true, pruned: false, is_root: false, + group: ThreadGroupHash::new(), - len: 0, - has_unseen: false, - snoozed: false, - - thread_group: ThreadHash::default(), - rank: 0, + unseen: false, } } } impl ThreadNode { - fn new(thread_group: ThreadHash) -> Self { + fn new() -> Self { ThreadNode { - thread_group, ..Default::default() } } @@ -432,16 +376,12 @@ impl ThreadNode { self.show_subject } - pub fn has_unseen(&self) -> bool { - self.has_unseen + pub fn unseen(&self) -> bool { + self.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 set_unseen(&mut self, new_val: bool) { + self.unseen = new_val; } pub fn date(&self) -> UnixTimestamp { @@ -472,22 +412,6 @@ impl ThreadNode { &self.children } - pub fn indentation(&self) -> usize { - self.indentation - } - - pub fn snoozed(&self) -> bool { - self.snoozed - } - - pub fn thread_group(&self) -> ThreadHash { - self.thread_group - } - - pub fn set_snoozed(&mut self, set: bool) { - self.snoozed = set; - } - fn insert_child_pos( vec: &[ThreadHash], child: ThreadHash, @@ -560,6 +484,7 @@ pub struct Threads { pub thread_dates: FnvHashMap, root_set: RefCell>, tree_index: RefCell>, + pub groups: FnvHashMap, message_ids: FnvHashMap, ThreadHash>, pub message_ids_set: FnvHashSet>, @@ -578,115 +503,26 @@ impl PartialEq for ThreadNode { } } -pub struct RootIterator<'a> { - pos: usize, - root_tree: Ref<'a, Vec>, - thread_nodes: &'a FnvHashMap, -} - -impl<'a> Iterator for RootIterator<'a> { - type Item = ThreadHash; - fn next(&mut self) -> Option { - { - if self.pos == self.root_tree.len() { - return None; - } - let mut ret = self.root_tree[self.pos]; - self.pos += 1; - let thread_node = &self.thread_nodes[&ret]; - if thread_node.message().is_none() { - ret = thread_node.children()[0]; - while self.thread_nodes[&ret].message().is_none() { - ret = self.thread_nodes[&ret].children()[0]; - } - } - Some(ret) - } - } -} - -pub fn find_root_hash(buf: &FnvHashMap, h: ThreadHash) -> ThreadHash { - if buf[&h].parent.is_none() { - return h; - } - let p = buf[&h].parent.unwrap(); - if buf[&p].message.is_none() { - return h; - } - find_root_hash(buf, p) -} - -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_thread_group(buf, p) -} -fn find(buf: &mut FnvHashMap, h: ThreadHash) -> ThreadHash { - if buf[&h].thread_group == h { - return h; - } - let p = buf[&h].thread_group; - let new_group = find(buf, p); - buf.entry(h).and_modify(|e| e.thread_group = new_group); - new_group -} - impl Threads { pub fn is_snoozed(&self, h: ThreadHash) -> bool { - let root = find_root_hash(&self.thread_nodes, h); - self.thread_nodes[&root].snoozed() + let root = &self.find_group(self.thread_nodes[&h].group); + self.groups[&root].snoozed() } - pub fn find(&mut self, i: ThreadHash) -> ThreadHash { - find(&mut self.thread_nodes, i) - } - fn union(&mut self, x: ThreadHash, y: ThreadHash) -> ThreadHash { - let mut x_root = self.find(x); - let mut y_root = self.find(y); - // x and y are already in the same set - if x_root == y_root { - let max = std::cmp::max( - *self - .thread_dates - .entry(x_root) - .or_insert(self.thread_nodes[&x_root].date), - *self - .thread_dates - .entry(y_root) - .or_insert(self.thread_nodes[&y_root].date), - ); - *self.thread_dates.entry(x_root).or_default() = max; - *self.thread_dates.entry(y_root).or_default() = max; - return x_root; + pub fn find_group(&self, h: ThreadGroupHash) -> ThreadGroupHash { + let p = match self.groups[&h] { + ThreadGroup::Group { .. } => return h, + ThreadGroup::Node { ref parent } => *parent.borrow(), + }; + + let parent_group = self.find_group(p); + match self.groups[&h] { + ThreadGroup::Node { ref parent } => { + *parent.borrow_mut() = parent_group; + } + _ => unreachable!(), } - - if self.thread_nodes[&y_root].date < self.thread_nodes[&x_root].date { - mem::swap(&mut x_root, &mut y_root); - } - - // x and y are not in same set, so we merge them - // - self.thread_nodes - .entry(y_root) - .and_modify(|e| e.thread_group = x_root); - //if self.thread_nodes[&x_root].rank == self.thread_nodes[&y_root].rank { - // self.thread_nodes.entry(x_root).and_modify(|e| e.rank += 1); - //} - let max = std::cmp::max( - *self - .thread_dates - .entry(x_root) - .or_insert(self.thread_nodes[&x_root].date), - *self - .thread_dates - .entry(y_root) - .or_insert(self.thread_nodes[&y_root].date), - ); - *self.thread_dates.entry(x_root).or_default() = max; - *self.thread_dates.entry(y_root).or_default() = max; - x_root + parent_group } pub fn new(length: usize) -> Threads { @@ -744,6 +580,15 @@ impl Threads { } } + pub fn thread_group_iter(&self, index: ThreadGroupHash) -> ThreadGroupIterator { + ThreadGroupIterator { + group: self.groups[&index].root().unwrap(), + pos: 0, + stack: SmallVec::new(), + thread_nodes: &self.thread_nodes, + } + } + pub fn update_envelope( &mut self, envelopes: &Envelopes, @@ -765,25 +610,21 @@ impl Threads { }; self.thread_nodes.get_mut(&thread_hash).unwrap().message = Some(new_hash); - self.thread_nodes.get_mut(&thread_hash).unwrap().has_unseen = - !envelopes.read().unwrap()[&new_hash].is_seen() - || self.thread_nodes[&thread_hash] - .children - .iter() - .fold(false, |acc, x| acc || self.thread_nodes[x].has_unseen); - - let mut thread_hash_iter = thread_hash; - while self.thread_nodes[&thread_hash_iter].parent.is_some() { - let parent_hash = self.thread_nodes[&thread_hash_iter].parent.unwrap(); - - self.thread_nodes.get_mut(&parent_hash).unwrap().has_unseen = self.thread_nodes - [&parent_hash] - .children - .iter() - .fold(false, |acc, x| acc || self.thread_nodes[x].has_unseen); - thread_hash_iter = parent_hash; + let was_unseen = self.thread_nodes[&thread_hash].unseen; + let is_unseen = !envelopes.read().unwrap()[&new_hash].is_seen(); + if was_unseen != is_unseen { + let thread = self.find_group(self.thread_nodes[&thread_hash].group); + self.groups.entry(thread).and_modify(|e| { + if let ThreadGroup::Group { ref mut unseen, .. } = e { + if was_unseen { + *unseen -= 1; + } else { + *unseen += 1; + } + } + }); } - + self.thread_nodes.get_mut(&thread_hash).unwrap().unseen = is_unseen; self.hash_set.remove(&old_hash); self.hash_set.insert(new_hash); Ok(()) @@ -906,16 +747,28 @@ impl Threads { { let mut node = self.thread_nodes.entry(new_id).or_default(); node.message = Some(env_hash); - if node.thread_group == ThreadHash::default() { - node.thread_group = new_id; - } if node.parent.is_none() { node.parent = reply_to_id; } node.date = envelopes_lck[&env_hash].date(); *self.thread_dates.entry(new_id).or_default() = node.date; - node.has_unseen = !envelopes_lck[&env_hash].is_seen(); + node.unseen = !envelopes_lck[&env_hash].is_seen(); } + + self.groups.insert( + self.thread_nodes[&new_id].group, + ThreadGroup::Group { + root: new_id, + date: envelopes_lck[&env_hash].date(), + len: 1, + unseen: if !envelopes_lck[&env_hash].is_seen() { + 1 + } else { + 0 + }, + snoozed: false, + }, + ); self.message_ids .insert(envelopes_lck[&env_hash].message_id().raw().to_vec(), new_id); self.message_ids_set.insert( @@ -928,9 +781,7 @@ impl Threads { self.missing_message_ids .remove(envelopes_lck[&env_hash].message_id().raw()); self.hash_set.insert(env_hash); - let mut new_root = None; if let Some(reply_to_id) = reply_to_id { - //self.union(reply_to_id, new_id); make!((reply_to_id) parent of (new_id), self); } else { if let Some(r) = envelopes_lck[&env_hash] @@ -942,45 +793,36 @@ impl Threads { reply_to_id, ThreadNode { date: envelopes_lck[&env_hash].date(), - thread_group: reply_to_id, - ..ThreadNode::new(reply_to_id) + ..ThreadNode::new() + }, + ); + + self.groups.insert( + self.thread_nodes[&reply_to_id].group, + ThreadGroup::Group { + root: reply_to_id, + date: envelopes_lck[&env_hash].date(), + len: 0, + unseen: 0, + snoozed: false, }, ); make!((reply_to_id) parent of (new_id), self); self.missing_message_ids.insert(r.to_vec()); self.message_ids.insert(r.to_vec(), reply_to_id); self.message_ids_set.insert(r.to_vec().to_vec()); - new_root = Some(reply_to_id); - } else { - new_root = Some(new_id); } } if envelopes_lck[&env_hash].references.is_some() { - let sort = *self.sort.borrow(); - let mut current_descendant = envelopes_lck[&env_hash].message_id().to_string(); - //debug!("{} references are {}", ¤t_descendant, unsafe { std::str::from_utf8_unchecked( &envelopes_lck[&env_hash].references.as_ref().unwrap().raw,) }); - //debug!( "{} in_reply_to is {:?}", ¤t_descendant, &envelopes_lck[&env_hash].in_reply_to_display()); let mut current_descendant_id = new_id; let mut references = envelopes_lck[&env_hash].references(); - if let Some(irt) = envelopes_lck[&env_hash].in_reply_to().as_ref() { - if references.first() == envelopes_lck[&env_hash].in_reply_to().as_ref() { - references.reverse(); - /* - references.remove(0); - if !references.contains(irt) { - references.push(irt); - } - */ - } + if references.first() == envelopes_lck[&env_hash].in_reply_to().as_ref() { + references.reverse(); } - //debug!( "{} references iter is {:?}", ¤t_descendant, references .iter() .map(|r| unsafe { std::str::from_utf8_unchecked(r.raw()) }) .collect::>()); - for reference in references.into_iter().rev() { - //debug!("parent of {} is {}", current_descendant, reference); if let Some(&id) = self.message_ids.get(reference.raw()) { - //self.union(id, current_descendant_id); if self.thread_nodes[&id].date > self.thread_nodes[¤t_descendant_id].date || self.thread_nodes[¤t_descendant_id].parent.is_some() { @@ -988,37 +830,6 @@ impl Threads { continue; } make!((id) parent of (current_descendant_id), self); - if self.thread_nodes[&id].message.is_some() - && (self.thread_nodes[&new_id].parent.is_none() - || self.thread_nodes - [self.thread_nodes[&new_id].parent.as_ref().unwrap()] - .message - .is_none()) - { - if self.thread_nodes[¤t_descendant_id].is_root { - let pos = ThreadNode::insert_child_pos( - &self.tree_index.borrow(), - current_descendant_id, - sort, - &mut self.thread_nodes, - &self.thread_dates, - &envelopes, - ); - if let Ok(pos) = pos { - self.tree_index.borrow_mut().remove(pos); - } - self.thread_nodes - .entry(current_descendant_id) - .and_modify(|n| { - n.is_root = false; - }); - } - if self.thread_nodes[&id].is_root { - new_root = None; - } else { - new_root = Some(id); - } - } current_descendant_id = id; } else { let id = ThreadHash::new(); @@ -1026,11 +837,19 @@ impl Threads { id, ThreadNode { date: envelopes_lck[&env_hash].date(), - thread_group: id, - ..ThreadNode::new(id) + ..ThreadNode::new() + }, + ); + self.groups.insert( + self.thread_nodes[&id].group, + ThreadGroup::Group { + root: id, + date: envelopes_lck[&env_hash].date(), + len: 0, + unseen: 0, + snoozed: false, }, ); - //self.union(id, current_descendant_id); make!((id) parent of (current_descendant_id), self); self.missing_message_ids.insert(reference.raw().to_vec()); self.message_ids.insert(reference.raw().to_vec(), id); @@ -1038,30 +857,9 @@ impl Threads { .insert(reference.raw().to_vec().to_vec()); current_descendant_id = id; } - current_descendant = reference.to_string(); } } drop(envelopes_lck); - /*{ - let mut new_tree = vec![]; - for (k, t) in self.thread_nodes.iter() { - if t.message.is_some() && t.parent.is_none() { - new_tree.push(*k); - } - } - - self.vec_inner_sort_by(&mut new_tree, *self.sort.borrow(), envelopes); - let mut tree = self.tree_index.borrow_mut(); - tree.clear(); - *tree = new_tree; - }*/ - - if let Some(id) = new_root { - self.tree_insert_root(id, envelopes); - } else { - //self.inner_sort_by(*self.sort.borrow(), envelopes); - } - self.update_show_subject(new_id, env_hash, envelopes); envelopes .write() @@ -1087,98 +885,7 @@ impl Threads { /* Insert or update */ pub fn insert_reply(&mut self, envelopes: &mut Envelopes, env_hash: EnvelopeHash) -> bool { - return self.insert_internal(envelopes, env_hash, true); - - let mut envelopes_lck = envelopes.write().unwrap(); - let reply_to_id: Option = envelopes_lck[&env_hash] - .in_reply_to() - .map(crate::email::StrBuild::raw) - .and_then(|r| self.message_ids.get(r).cloned()); - if let Some(id) = self - .message_ids - .get(envelopes_lck[&env_hash].message_id().raw()) - .cloned() - { - if self.thread_nodes[&id].message.is_some() { - return false; - } - self.thread_nodes.entry(id).and_modify(|n| { - n.message = Some(env_hash); - n.date = envelopes_lck[&env_hash].date(); - n.pruned = false; - if n.parent.is_none() { - if let Some(reply_to_id) = reply_to_id { - n.parent = Some(reply_to_id); - } - } - }); - if let Some(reply_to_id) = reply_to_id { - if !self.thread_nodes[&reply_to_id].children.contains(&id) { - make!((reply_to_id) parent of (id), self); - self.union(id, reply_to_id); - } - } - - self.message_ids - .insert(envelopes_lck[&env_hash].message_id().raw().to_vec(), id); - self.message_ids_set.insert( - envelopes_lck[&env_hash] - .message_id() - .raw() - .to_vec() - .to_vec(), - ); - self.missing_message_ids - .remove(envelopes_lck[&env_hash].message_id().raw()); - envelopes_lck.get_mut(&env_hash).unwrap().set_thread(id); - self.hash_set.insert(env_hash); - drop(envelopes_lck); - if self.thread_nodes[&id].parent.is_none() { - self.tree_insert_root(id, envelopes); - } - { - let mut tree_index = self.tree_index.borrow_mut(); - for c in &self.thread_nodes[&id].children { - if let Some(i) = tree_index.iter().position(|t| *t == *c) { - tree_index.remove(i); - } - } - } - self.update_show_subject(id, env_hash, envelopes); - true - } else if let Some(reply_to_id) = reply_to_id { - let new_id = ThreadHash::new(); - self.thread_nodes.insert( - new_id, - ThreadNode { - message: Some(env_hash), - parent: Some(reply_to_id), - date: envelopes_lck[&env_hash].date(), - has_unseen: !envelopes_lck[&env_hash].is_seen(), - ..ThreadNode::new(new_id) - }, - ); - self.message_ids - .insert(envelopes_lck[&env_hash].message_id().raw().to_vec(), new_id); - self.message_ids_set.insert( - envelopes_lck[&env_hash] - .message_id() - .raw() - .to_vec() - .to_vec(), - ); - self.missing_message_ids - .remove(envelopes_lck[&env_hash].message_id().raw()); - envelopes_lck.get_mut(&env_hash).unwrap().set_thread(new_id); - self.hash_set.insert(env_hash); - self.union(reply_to_id, new_id); - make!((reply_to_id) parent of (new_id), self); - drop(envelopes_lck); - self.update_show_subject(new_id, env_hash, envelopes); - true - } else { - false - } + self.insert_internal(envelopes, env_hash, true) } fn inner_subsort_by(&self, _subsort: (SortField, SortOrder), _envelopes: &Envelopes) { @@ -1248,22 +955,106 @@ impl Threads { */ } - pub fn vec_inner_sort_by( + pub fn group_inner_sort_by( &self, - vec: &mut Vec, + vec: &mut [ThreadGroupHash], sort: (SortField, SortOrder), envelopes: &Envelopes, ) { let envelopes = envelopes.read().unwrap(); vec.sort_by(|a, b| match sort { (SortField::Date, SortOrder::Desc) => { - let a = self.thread_dates[a]; - let b = self.thread_dates[b]; + let a = self.groups[&a].date(); + let b = self.groups[&b].date(); b.cmp(&a) } (SortField::Date, SortOrder::Asc) => { - let a = self.thread_dates[a]; - let b = self.thread_dates[b]; + let a = self.groups[&a].date(); + let b = self.groups[&b].date(); + a.cmp(&b) + } + (SortField::Subject, SortOrder::Desc) => { + let a = &self.thread_nodes[&self.groups[&a].root().unwrap()].message(); + let b = &self.thread_nodes[&self.groups[&b].root().unwrap()].message(); + + match (a, b) { + (Some(_), Some(_)) => {} + (Some(_), None) => { + return Ordering::Greater; + } + (None, Some(_)) => { + return Ordering::Less; + } + (None, None) => { + return Ordering::Equal; + } + } + let ma = &envelopes[&a.unwrap()]; + let mb = &envelopes[&b.unwrap()]; + #[cfg(feature = "unicode_algorithms")] + { + ma.subject() + .split_graphemes() + .cmp(&mb.subject().split_graphemes()) + } + #[cfg(not(feature = "unicode_algorithms"))] + { + ma.subject().cmp(&mb.subject()) + } + } + (SortField::Subject, SortOrder::Asc) => { + let a = &self.thread_nodes[&self.groups[&a].root().unwrap()].message(); + let b = &self.thread_nodes[&self.groups[&b].root().unwrap()].message(); + + match (a, b) { + (Some(_), Some(_)) => {} + (Some(_), None) => { + return Ordering::Less; + } + (None, Some(_)) => { + return Ordering::Greater; + } + (None, None) => { + return Ordering::Equal; + } + } + let ma = &envelopes[&a.unwrap()]; + let mb = &envelopes[&b.unwrap()]; + #[cfg(feature = "unicode_algorithms")] + { + mb.subject() + .as_ref() + .split_graphemes() + .cmp(&ma.subject().split_graphemes()) + } + + #[cfg(not(feature = "unicode_algorithms"))] + { + mb.subject().as_ref().cmp(&ma.subject()) + } + } + }); + } + pub fn node_inner_sort_by( + &self, + vec: &mut [ThreadHash], + sort: (SortField, SortOrder), + envelopes: &Envelopes, + ) { + let envelopes = envelopes.read().unwrap(); + vec.sort_by(|a, b| match sort { + (SortField::Date, SortOrder::Desc) => { + let a_group = self.find_group(self.thread_nodes[&a].group); + let b_group = self.find_group(self.thread_nodes[&b].group); + let a = self.groups[&a_group].date(); + let b = self.groups[&b_group].date(); + b.cmp(&a) + } + (SortField::Date, SortOrder::Asc) => { + let a_group = self.find_group(self.thread_nodes[&a].group); + let b_group = self.find_group(self.thread_nodes[&b].group); + let a = self.groups[&a_group].date(); + let b = self.groups[&b_group].date(); a.cmp(&b) } (SortField::Subject, SortOrder::Desc) => { @@ -1333,13 +1124,17 @@ impl Threads { let envelopes = envelopes.read().unwrap(); tree.sort_by(|a, b| match sort { (SortField::Date, SortOrder::Desc) => { - let a = self.thread_dates[a]; - let b = self.thread_dates[b]; + let a_group = self.find_group(self.thread_nodes[&a].group); + let b_group = self.find_group(self.thread_nodes[&b].group); + let a = self.groups[&a_group].date(); + let b = self.groups[&b_group].date(); b.cmp(&a) } (SortField::Date, SortOrder::Asc) => { - let a = self.thread_dates[a]; - let b = self.thread_dates[b]; + let a_group = self.find_group(self.thread_nodes[&a].group); + let b_group = self.find_group(self.thread_nodes[&b].group); + let a = self.groups[&a_group].date(); + let b = self.groups[&b_group].date(); a.cmp(&b) } (SortField::Subject, SortOrder::Desc) => { @@ -1442,6 +1237,14 @@ impl Threads { self.tree_index.borrow()[idx] } + pub fn roots(&self) -> SmallVec<[ThreadGroupHash; 1024]> { + //FIXME: refactor filter + self.groups + .iter() + .filter_map(|(h, g)| g.root().map(|_| *h)) + .collect::>() + } + pub fn root_iter(&self) -> RootIterator { RootIterator { pos: 0, @@ -1449,155 +1252,6 @@ impl Threads { thread_nodes: &self.thread_nodes, } } - - /* - fn link_envelope(&mut self, envelope: &mut Envelope) { - let t_idx: ThreadHash = { - let m_id = envelope.message_id().raw(); - - /* t_idx: The index of this message's ThreadNode in thread_nodes - * - * If id_table contains an empty Container for this ID: - * Store this message in the Container's message slot. - * Else: - * Create a new Container object holding this message; - * Index the Container by Message-ID in id_table. - */ - if self.message_ids.get(m_id).is_some() { - let node_idx = self.message_ids[m_id]; - /* the already existing ThreadNote should be empty, since we're - * seeing this message for the first time. otherwise it's a - * duplicate. */ - if !self.missing_message_ids.contains(m_id) { - return; - } - self.missing_message_ids.remove(m_id); - node_idx - } else { - /* Create a new ThreadNode object holding this message */ - /* The new thread node's set is just itself */ - let new_id = ThreadHash::new(); - let node = ThreadNode { - message: Some(envelope.hash()), - date: envelope.date(), - thread_group: new_id, - ..Default::default() - }; - self.thread_nodes.insert(new_id, node); - - self.message_ids.insert(m_id.to_vec(), new_id); - self.message_ids_set.insert(m_id.to_vec()); - new_id - } - }; - self.thread_nodes.entry(t_idx).and_modify(|e| { - e.date = envelope.date(); - e.message = Some(envelope.hash()); - e.has_unseen |= !envelope.is_seen(); - }); - envelope.set_thread(t_idx); - self.hash_set.insert(envelope.hash()); - - /* For each element in the message's References field: - * - * Find a ThreadNode object for the given Message-ID: - * If there's one in message_ids use that; - * Otherwise, make (and index) one with a null Message - * - * Link the References field's ThreadNode together in the order implied - * by the References header. - */ - - /* The index of the reference we are currently examining, start from current message */ - let mut ref_ptr = t_idx; - - for &refn in envelope.references().iter().rev() { - let r_id = refn.raw(); - let parent_id = if self.message_ids.contains_key(r_id) { - self.message_ids[r_id] - } else { - /* Create a new ThreadNode object holding this reference */ - let new_id = ThreadHash::new(); - self.thread_nodes.insert( - new_id, - ThreadNode { - date: envelope.date(), - thread_group: new_id, - ..Default::default() - }, - ); - self.message_ids.insert(r_id.to_vec(), new_id); - self.missing_message_ids.insert(r_id.to_vec()); - self.message_ids_set.insert(r_id.to_vec()); - new_id - }; - - /* If they are already linked, don't change the existing links. - if self.thread_nodes[&ref_ptr].has_parent() - && self.thread_nodes[&ref_ptr].parent.unwrap() != parent_id - { - ref_ptr = parent_id; - continue; - } */ - if self.thread_nodes[&ref_ptr].parent.is_some() { - if self.thread_nodes[&parent_id].parent == Some(ref_ptr) { - eprintln!("ALARM"); - remove_from_parent!(&mut self.thread_nodes, parent_id); - } - ref_ptr = parent_id; - continue; - } - - /* Do not add a link if adding that link would introduce a loop: that is, before - * asserting A->B, search down the children of B to see if A is reachable, and also - * search down the children of A to see if B is reachable. If either is already - * reachable as a child of the other, don't add the link. - */ - if self.find(ref_ptr) != self.find(parent_id) { - self.union(ref_ptr, parent_id); - make!((parent_id) parent of (ref_ptr), &mut self.thread_nodes); - } - ref_ptr = parent_id; - } - }*/ - - fn tree_insert_root(&mut self, new_id: ThreadHash, envelopes: &Envelopes) { - if !(self.thread_nodes[&new_id].parent.is_none() - || self.thread_nodes[self.thread_nodes[&new_id].parent.as_ref().unwrap()] - .message - .is_none()) - { - return; - }; - debug!("tree_insert_root {}", new_id); - /* Index of reply_to_id in self.trees */ - let Threads { - ref mut tree_index, - ref thread_nodes, - .. - } = self; - let mut tree_index = tree_index.borrow_mut(); - for c in &thread_nodes[&new_id].children { - if let Some(i) = tree_index.iter().position(|t| *t == *c) { - tree_index.remove(i); - } - } - self.thread_nodes.entry(new_id).and_modify(|n| { - n.is_root = true; - }); - - match ThreadNode::insert_child_pos( - &tree_index, - new_id, - *self.sort.borrow(), - &mut self.thread_nodes, - &self.thread_dates, - envelopes, - ) { - Err(pos) => tree_index.insert(pos, new_id), - Ok(pos) => tree_index[pos] = new_id, - } - } } impl Index<&ThreadHash> for Threads { diff --git a/melib/src/thread/iterators.rs b/melib/src/thread/iterators.rs new file mode 100644 index 000000000..2a1245043 --- /dev/null +++ b/melib/src/thread/iterators.rs @@ -0,0 +1,209 @@ +/* + * meli - melib + * + * Copyright Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use super::{ThreadGroup, ThreadHash, ThreadNode}; +use fnv::FnvHashMap; +use smallvec::SmallVec; +use std::cell::Ref; + +/* `ThreadsIterator` returns messages according to the sorted order. For example, for the following + * threads: + * + * ``` + * A_ + * |_ B + * |_C + * D + * E_ + * |_F + * ``` + * + * the iterator returns them as `A, B, C, D, E, F` + */ + +pub struct ThreadsIterator<'a> { + pub(super) pos: usize, + pub(super) stack: SmallVec<[usize; 16]>, + pub(super) root_tree: Ref<'a, Vec>, + pub(super) thread_nodes: &'a FnvHashMap, +} +impl<'a> Iterator for ThreadsIterator<'a> { + type Item = (usize, ThreadHash, bool); + fn next(&mut self) -> Option<(usize, ThreadHash, bool)> { + { + let mut tree = &(*self.root_tree); + for i in self.stack.iter() { + tree = &self.thread_nodes[&tree[*i]].children; + } + if self.pos == tree.len() { + if let Some(p) = self.stack.pop() { + self.pos = p + 1; + } else { + return None; + } + } else { + debug_assert!(self.pos < tree.len()); + let ret = ( + self.stack.len(), + tree[self.pos], + !self.stack.is_empty() && (self.pos < (tree.len() - 1)), + ); + if !self.thread_nodes[&tree[self.pos]].children.is_empty() { + self.stack.push(self.pos); + self.pos = 0; + if self.thread_nodes[&ret.1].message.is_some() { + return Some(ret); + } else { + return self.next(); + } + } + self.pos += 1; + if self.thread_nodes[&ret.1].message.is_some() { + return Some(ret); + } + } + } + self.next() + } +} +/* `ThreadIterator` returns messages of a specific thread according to the sorted order. For example, for the following + * thread: + * + * ``` + * A_ + * |_ B + * |_C + * |_D + * ``` + * + * the iterator returns them as `A, B, C, D` + */ + +pub struct ThreadIterator<'a> { + pub(super) init_pos: usize, + pub(super) pos: usize, + pub(super) stack: SmallVec<[usize; 16]>, + pub(super) root_tree: Ref<'a, Vec>, + pub(super) thread_nodes: &'a FnvHashMap, +} +impl<'a> Iterator for ThreadIterator<'a> { + type Item = (usize, ThreadHash); + fn next(&mut self) -> Option<(usize, ThreadHash)> { + { + let mut tree = &(*self.root_tree); + for i in self.stack.iter() { + tree = &self.thread_nodes[&tree[*i]].children; + } + if self.pos == tree.len() || (self.stack.is_empty() && self.pos > self.init_pos) { + if self.stack.is_empty() { + return None; + } + self.pos = self.stack.pop().unwrap() + 1; + } else { + debug_assert!(self.pos < tree.len()); + let ret = (self.stack.len(), tree[self.pos]); + if !self.thread_nodes[&tree[self.pos]].children.is_empty() { + self.stack.push(self.pos); + self.pos = 0; + if self.thread_nodes[&ret.1].message.is_some() { + return Some(ret); + } else { + return self.next(); + } + } + self.pos += 1; + if self.thread_nodes[&ret.1].message.is_some() { + return Some(ret); + } + } + } + self.next() + } +} + +pub struct RootIterator<'a> { + pub pos: usize, + pub root_tree: Ref<'a, Vec>, + pub thread_nodes: &'a FnvHashMap, +} + +impl<'a> Iterator for RootIterator<'a> { + type Item = ThreadHash; + fn next(&mut self) -> Option { + { + if self.pos == self.root_tree.len() { + return None; + } + let mut ret = self.root_tree[self.pos]; + self.pos += 1; + let thread_node = &self.thread_nodes[&ret]; + if thread_node.message().is_none() { + ret = thread_node.children()[0]; + while self.thread_nodes[&ret].message().is_none() { + ret = self.thread_nodes[&ret].children()[0]; + } + } + Some(ret) + } + } +} + +pub struct ThreadGroupIterator<'a> { + pub(super) group: ThreadHash, + pub(super) pos: usize, + pub(super) stack: SmallVec<[usize; 16]>, + pub(super) thread_nodes: &'a FnvHashMap, +} + +impl<'a> Iterator for ThreadGroupIterator<'a> { + type Item = (usize, ThreadHash); + fn next(&mut self) -> Option<(usize, ThreadHash)> { + { + let mut tree = &[self.group][..]; + for i in self.stack.iter() { + tree = self.thread_nodes[&tree[*i]].children.as_slice(); + } + if self.pos == tree.len() { + if self.stack.is_empty() { + return None; + } + self.pos = self.stack.pop().unwrap() + 1; + } else { + debug_assert!(self.pos < tree.len()); + let ret = (self.stack.len(), tree[self.pos]); + if !self.thread_nodes[&tree[self.pos]].children.is_empty() { + self.stack.push(self.pos); + self.pos = 0; + if self.thread_nodes[&ret.1].message.is_some() { + return Some(ret); + } else { + return self.next(); + } + } + self.pos += 1; + if self.thread_nodes[&ret.1].message.is_some() { + return Some(ret); + } + } + } + self.next() + } +} diff --git a/ui/src/components/mail/compose.rs b/ui/src/components/mail/compose.rs index 0c3a42ade..19790f4c5 100644 --- a/ui/src/components/mail/compose.rs +++ b/ui/src/components/mail/compose.rs @@ -63,7 +63,7 @@ impl std::ops::DerefMut for EmbedStatus { #[derive(Debug)] pub struct Composer { - reply_context: Option<(usize, usize)>, // (folder_index, thread_node_index) + reply_context: Option<(FolderHash, EnvelopeHash)>, account_cursor: usize, cursor: Cursor, @@ -148,11 +148,7 @@ impl Composer { ..Default::default() } } - /* - * coordinates: (account index, mailbox index, root set thread_node index) - * msg: index of message we reply to in thread_nodes - * context: current context - */ + pub fn edit(account_pos: usize, h: EnvelopeHash, context: &Context) -> Result { let mut ret = Composer::default(); let op = context.accounts[account_pos].operation(h); @@ -163,18 +159,15 @@ impl Composer { ret.account_cursor = account_pos; Ok(ret) } + pub fn with_context( - coordinates: (usize, usize, usize), - msg: ThreadHash, + coordinates: (usize, FolderHash), + msg: EnvelopeHash, context: &Context, ) -> Self { let account = &context.accounts[coordinates.0]; - let mailbox = &account[coordinates.1].unwrap(); - let threads = &account.collection.threads[&mailbox.folder.hash()]; - let thread_nodes = &threads.thread_nodes(); let mut ret = Composer::default(); - let p = &thread_nodes[&msg]; - let parent_message = account.collection.get_env(p.message().unwrap()); + let parent_message = account.collection.get_env(msg); /* If message is from a mailing list and we detect a List-Post header, ask user if they * want to reply to the mailing list or the submitter of the message */ if let Some(actions) = list_management::ListActions::detect(&parent_message) { @@ -202,32 +195,22 @@ impl Composer { } } - let mut op = account.operation(parent_message.hash()); + let mut op = account.operation(msg); let parent_bytes = op.as_bytes(); ret.draft = Draft::new_reply(&parent_message, parent_bytes.unwrap()); + let subject = parent_message.subject(); ret.draft.headers_mut().insert( "Subject".into(), - if p.show_subject() { - format!( - "Re: {}", - account - .collection - .get_env(p.message().unwrap()) - .subject() - .clone() - ) + if !subject.starts_with("Re: ") { + format!("Re: {}", subject) } else { - account - .collection - .get_env(p.message().unwrap()) - .subject() - .into() + subject.into() }, ); ret.account_cursor = coordinates.0; - ret.reply_context = Some((coordinates.1, coordinates.2)); + ret.reply_context = Some((coordinates.1, msg)); ret } @@ -553,13 +536,13 @@ impl Component for Composer { fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { let shortcuts = self.get_shortcuts(context); - match (&mut self.mode, &mut self.reply_context, &event) { - (ViewMode::Edit, _, _) => { + match (&mut self.mode, &event) { + (ViewMode::Edit, _) => { if self.pager.process_event(event, context) { return true; } } - (ViewMode::Send(ref mut selector), _, _) => { + (ViewMode::Send(ref mut selector), _) => { if selector.process_event(event, context) { if selector.is_done() { let s = match std::mem::replace(&mut self.mode, ViewMode::Edit) { @@ -594,7 +577,7 @@ impl Component for Composer { return true; } } - (ViewMode::SelectRecipients(ref mut selector), _, _) => { + (ViewMode::SelectRecipients(ref mut selector), _) => { if selector.process_event(event, context) { if selector.is_done() { let s = match std::mem::replace(&mut self.mode, ViewMode::Edit) { @@ -615,7 +598,7 @@ impl Component for Composer { return true; } } - (ViewMode::Discard(_, ref mut selector), _, _) => { + (ViewMode::Discard(_, ref mut selector), _) => { if selector.process_event(event, context) { if selector.is_done() { let (u, s) = match std::mem::replace(&mut self.mode, ViewMode::Edit) { diff --git a/ui/src/components/mail/listing.rs b/ui/src/components/mail/listing.rs index 67c38233c..e86636211 100644 --- a/ui/src/components/mail/listing.rs +++ b/ui/src/components/mail/listing.rs @@ -53,37 +53,18 @@ pub trait MailListingTrait: ListingTrait { fn perform_action( &mut self, context: &mut Context, - thread_hash: ThreadHash, + thread_hash: ThreadGroupHash, a: &ListingAction, ) { let account = &mut context.accounts[self.coordinates().0]; let mut envs_to_set: SmallVec<[EnvelopeHash; 8]> = SmallVec::new(); let folder_hash = account[self.coordinates().1].unwrap().folder.hash(); - { - let mut stack: SmallVec<[ThreadHash; 8]> = SmallVec::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 (_, h) in account.collection.threads[&folder_hash].thread_group_iter(thread_hash) { + envs_to_set.push( + account.collection.threads[&folder_hash].thread_nodes()[&h] + .message() + .unwrap(), + ); } for env_hash in envs_to_set { let op = account.operation(env_hash); @@ -139,9 +120,9 @@ pub trait MailListingTrait: ListingTrait { } } - fn row_updates(&mut self) -> &mut SmallVec<[ThreadHash; 8]>; - fn get_focused_items(&self, _context: &Context) -> SmallVec<[ThreadHash; 8]>; - fn redraw_list(&mut self, context: &Context, items: Box>) { + fn row_updates(&mut self) -> &mut SmallVec<[ThreadGroupHash; 8]>; + fn get_focused_items(&self, _context: &Context) -> SmallVec<[ThreadGroupHash; 8]>; + fn redraw_list(&mut self, context: &Context, items: Box>) { unimplemented!() } } diff --git a/ui/src/components/mail/listing/compact.rs b/ui/src/components/mail/listing/compact.rs index 72d7713bd..90a607e88 100644 --- a/ui/src/components/mail/listing/compact.rs +++ b/ui/src/components/mail/listing/compact.rs @@ -54,33 +54,33 @@ pub struct CompactListing { length: usize, sort: (SortField, SortOrder), subsort: (SortField, SortOrder), - all_threads: fnv::FnvHashSet, - order: FnvHashMap, + all_threads: fnv::FnvHashSet, + order: FnvHashMap, /// Cache current view. data_columns: DataColumns, filter_term: String, - filtered_selection: Vec, - filtered_order: FnvHashMap, - selection: FnvHashMap, + filtered_selection: Vec, + filtered_order: FnvHashMap, + selection: FnvHashMap, /// If we must redraw on next redraw event dirty: bool, force_draw: bool, /// If `self.view` exists or not. unfocused: bool, view: ThreadView, - row_updates: SmallVec<[ThreadHash; 8]>, + row_updates: SmallVec<[ThreadGroupHash; 8]>, movement: Option, id: ComponentId, } impl MailListingTrait for CompactListing { - fn row_updates(&mut self) -> &mut SmallVec<[ThreadHash; 8]> { + fn row_updates(&mut self) -> &mut SmallVec<[ThreadGroupHash; 8]> { &mut self.row_updates } - fn get_focused_items(&self, context: &Context) -> SmallVec<[ThreadHash; 8]> { + fn get_focused_items(&self, context: &Context) -> SmallVec<[ThreadGroupHash; 8]> { let is_selection_empty = self.selection.values().cloned().any(std::convert::identity); let i = [self.get_thread_under_cursor(self.cursor_pos.2, context)]; let cursor_iter; @@ -118,14 +118,13 @@ impl ListingTrait for CompactListing { if self.length == 0 { return; } - let i = self.get_thread_under_cursor(idx, context); + let thread = self.get_thread_under_cursor(idx, context); let account = &context.accounts[self.cursor_pos.0]; let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); let threads = &account.collection.threads[&folder_hash]; - let thread_node = &threads.thread_nodes[&i]; - let fg_color = if thread_node.has_unseen() { + let fg_color = if threads.groups[&thread].unseen() > 0 { Color::Byte(0) } else { Color::Default @@ -133,9 +132,9 @@ impl ListingTrait for CompactListing { let bg_color = if context.settings.terminal.theme == "light" { if self.cursor_pos.2 == idx { Color::Byte(244) - } else if self.selection[&i] { + } else if self.selection[&thread] { Color::Byte(210) - } else if thread_node.has_unseen() { + } else if threads.groups[&thread].unseen() > 0 { Color::Byte(251) } else if idx % 2 == 0 { Color::Byte(252) @@ -145,9 +144,9 @@ impl ListingTrait for CompactListing { } else { if self.cursor_pos.2 == idx { Color::Byte(246) - } else if self.selection[&i] { + } else if self.selection[&thread] { Color::Byte(210) - } else if thread_node.has_unseen() { + } else if threads.groups[&thread].unseen() > 0 { Color::Byte(251) } else if idx % 2 == 0 { Color::Byte(236) @@ -444,21 +443,19 @@ impl ListingTrait for CompactListing { if !threads.thread_nodes.contains_key(&env_hash_thread_hash) { continue; } - let thread_group = melib::find_root_hash( - &threads.thread_nodes, - threads.thread_nodes[&env_hash_thread_hash].thread_group(), - ); - if self.filtered_order.contains_key(&thread_group) { + let thread = + threads.find_group(threads.thread_nodes[&env_hash_thread_hash].group); + if self.filtered_order.contains_key(&thread) { continue; } - if self.all_threads.contains(&thread_group) { - self.filtered_selection.push(thread_group); + if self.all_threads.contains(&thread) { + self.filtered_selection.push(thread); self.filtered_order - .insert(thread_group, self.filtered_selection.len() - 1); + .insert(thread, self.filtered_selection.len() - 1); } } if !self.filtered_selection.is_empty() { - threads.vec_inner_sort_by( + threads.group_inner_sort_by( &mut self.filtered_selection, self.sort, &context.accounts[self.cursor_pos.0].collection.envelopes, @@ -472,7 +469,7 @@ impl ListingTrait for CompactListing { self.redraw_list( context, Box::new(self.filtered_selection.clone().into_iter()) - as Box>, + as Box>, ); } Err(e) => { @@ -550,12 +547,9 @@ impl CompactListing { e: &Envelope, context: &Context, threads: &Threads, - hash: ThreadHash, + hash: ThreadGroupHash, ) -> EntryStrings { - let is_snoozed: bool = threads.is_snoozed(hash); - let date = - threads.thread_dates[&melib::thread::find_thread_group(threads.thread_nodes(), hash)]; - let thread_node = &threads[&hash]; + let thread = &threads.groups[&hash]; let folder_hash = &context.accounts[self.cursor_pos.0][self.cursor_pos.1] .unwrap() .folder @@ -597,26 +591,26 @@ impl CompactListing { } let mut subject = e.subject().to_string(); subject.truncate_at_boundary(150); - if thread_node.len() > 0 { + if thread.len() > 1 { EntryStrings { - date: DateString(ConversationsListing::format_date(context, date)), - subject: SubjectString(format!("{} ({})", subject, thread_node.len(),)), + date: DateString(ConversationsListing::format_date(context, thread.date())), + subject: SubjectString(format!("{} ({})", subject, thread.len(),)), flag: FlagString(format!( "{}{}", if e.has_attachments() { "📎" } else { "" }, - if is_snoozed { "💤" } else { "" } + if thread.snoozed() { "💤" } else { "" } )), from: FromString(address_list!((e.from()) as comma_sep_list)), tags: TagString(tags, colors), } } else { EntryStrings { - date: DateString(ConversationsListing::format_date(context, date)), + date: DateString(ConversationsListing::format_date(context, thread.date())), subject: SubjectString(subject), flag: FlagString(format!( "{}{}", if e.has_attachments() { "📎" } else { "" }, - if is_snoozed { "💤" } else { "" } + if thread.snoozed() { "💤" } else { "" } )), from: FromString(address_list!((e.from()) as comma_sep_list)), tags: TagString(tags, colors), @@ -669,28 +663,31 @@ impl CompactListing { return; } } - if old_cursor_pos == self.new_cursor_pos { - self.view.update(context); - } else if self.unfocused { - self.view = ThreadView::new(self.new_cursor_pos, None, context); - } let threads = &context.accounts[self.cursor_pos.0].collection.threads[&folder_hash]; - threads.sort_by( + self.all_threads.clear(); + let mut roots = threads.roots(); + threads.group_inner_sort_by( + &mut roots, self.sort, - self.subsort, &context.accounts[self.cursor_pos.0].collection.envelopes, ); - self.all_threads.clear(); self.redraw_list( context, - Box::new(threads.root_iter().collect::>().into_iter()) - as Box>, + Box::new(roots.into_iter()) as Box>, ); + + if old_cursor_pos == self.new_cursor_pos { + self.view.update(context); + } else if self.unfocused { + let thread = self.get_thread_under_cursor(self.cursor_pos.2, context); + + self.view = ThreadView::new(self.new_cursor_pos, thread, None, context); + } } - fn redraw_list(&mut self, context: &Context, items: Box>) { + fn redraw_list(&mut self, context: &Context, items: Box>) { let account = &context.accounts[self.cursor_pos.0]; let mailbox = &account[self.cursor_pos.1].unwrap(); @@ -714,18 +711,19 @@ impl CompactListing { SmallVec::new(), ); - for (idx, root_idx) in items.enumerate() { + for (idx, thread) in items.enumerate() { + debug!(thread); self.length += 1; - let thread_node = &threads.thread_nodes()[&root_idx]; - let i = thread_node.message().unwrap_or_else(|| { + let thread_node = &threads.thread_nodes()[&threads.groups[&thread].root().unwrap()]; + let root_env_hash = thread_node.message().unwrap_or_else(|| { let mut iter_ptr = thread_node.children()[0]; while threads.thread_nodes()[&iter_ptr].message().is_none() { iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0]; } threads.thread_nodes()[&iter_ptr].message().unwrap() }); - if !context.accounts[self.cursor_pos.0].contains_key(i) { - debug!("key = {}", i); + if !context.accounts[self.cursor_pos.0].contains_key(root_env_hash) { + debug!("key = {}", root_env_hash); debug!( "name = {} {}", mailbox.name(), @@ -735,10 +733,11 @@ impl CompactListing { panic!(); } - let root_envelope: EnvelopeRef = - context.accounts[self.cursor_pos.0].collection.get_env(i); + let root_envelope: EnvelopeRef = context.accounts[self.cursor_pos.0] + .collection + .get_env(root_env_hash); - let entry_strings = self.make_entry_string(&root_envelope, context, threads, root_idx); + let entry_strings = self.make_entry_string(&root_envelope, context, threads, thread); row_widths.1.push( entry_strings .date @@ -772,11 +771,11 @@ impl CompactListing { min_width.4, entry_strings.subject.grapheme_width() + 1 + entry_strings.tags.grapheme_width(), ); /* subject */ - rows.push(((idx, root_idx), entry_strings)); - self.all_threads.insert(root_idx); + rows.push(((idx, (thread, root_env_hash)), entry_strings)); + self.all_threads.insert(thread); - self.order.insert(root_idx, idx); - self.selection.insert(root_idx, false); + self.order.insert(thread, idx); + self.selection.insert(thread, false); } min_width.0 = self.length.saturating_sub(1).to_string().len(); @@ -800,17 +799,9 @@ impl CompactListing { CellBuffer::new_with_context(min_width.4, rows.len(), Cell::with_char(' '), context); self.data_columns.segment_tree[4] = row_widths.4.into(); - for ((idx, root_idx), strings) in rows { - let thread_node = &threads.thread_nodes()[&root_idx]; - let i = thread_node.message().unwrap_or_else(|| { - let mut iter_ptr = thread_node.children()[0]; - while threads.thread_nodes()[&iter_ptr].message().is_none() { - iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0]; - } - threads.thread_nodes()[&iter_ptr].message().unwrap() - }); - if !context.accounts[self.cursor_pos.0].contains_key(i) { - //debug!("key = {}", i); + for ((idx, (thread, root_env_hash)), strings) in rows { + if !context.accounts[self.cursor_pos.0].contains_key(root_env_hash) { + //debug!("key = {}", root_env_hash); //debug!( // "name = {} {}", // mailbox.name(), @@ -820,13 +811,13 @@ impl CompactListing { panic!(); } - let fg_color = if thread_node.has_unseen() { + let fg_color = if threads.groups[&thread].unseen() > 0 { Color::Byte(0) } else { Color::Default }; let bg_color = if context.settings.terminal.theme == "light" { - if thread_node.has_unseen() { + if threads.groups[&thread].unseen() > 0 { Color::Byte(251) } else if idx % 2 == 0 { Color::Byte(252) @@ -834,7 +825,7 @@ impl CompactListing { Color::Default } } else { - if thread_node.has_unseen() { + if threads.groups[&thread].unseen() > 0 { Color::Byte(251) } else if idx % 2 == 0 { Color::Byte(236) @@ -930,10 +921,10 @@ impl CompactListing { self.data_columns.columns[4][(x, idx)].set_bg(bg_color); } match ( - threads.is_snoozed(root_idx), + threads.groups[&thread].snoozed(), context.accounts[self.cursor_pos.0] .collection - .get_env(i) + .get_env(root_env_hash) .has_attachments(), ) { (true, true) => { @@ -970,7 +961,7 @@ impl CompactListing { } } - fn get_thread_under_cursor(&self, cursor: usize, context: &Context) -> ThreadHash { + fn get_thread_under_cursor(&self, cursor: usize, context: &Context) -> ThreadGroupHash { //let account = &context.accounts[self.cursor_pos.0]; //let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); if self.filter_term.is_empty() { @@ -983,17 +974,18 @@ impl CompactListing { panic!(); }) .0 - //threads.root_set(cursor) } else { self.filtered_selection[cursor] } } - fn update_line(&mut self, context: &Context, thread_hash: ThreadHash) { + fn update_line(&mut self, context: &Context, thread_hash: ThreadGroupHash) { let account = &context.accounts[self.cursor_pos.0]; let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); let threads = &account.collection.threads[&folder_hash]; - if let Some(env_hash) = threads[&thread_hash].message() { + if let Some(env_hash) = + threads.thread_nodes()[&threads.groups[&thread_hash].root().unwrap()].message() + { if !account.contains_key(env_hash) { /* The envelope has been renamed or removed, so wait for the appropriate event to * arrive */ @@ -1001,14 +993,14 @@ impl CompactListing { } let envelope: EnvelopeRef = account.collection.get_env(env_hash); let has_attachments = envelope.has_attachments(); - let fg_color = if threads[&thread_hash].has_unseen() { + let fg_color = if threads.groups[&thread_hash].unseen() > 0 { Color::Byte(0) } else { Color::Default }; let idx = self.order[&thread_hash]; let bg_color = if context.settings.terminal.theme == "light" { - if threads[&thread_hash].has_unseen() { + if threads.groups[&thread_hash].unseen() > 0 { Color::Byte(251) } else if idx % 2 == 0 { Color::Byte(252) @@ -1016,7 +1008,7 @@ impl CompactListing { Color::Default } } else { - if threads[&thread_hash].has_unseen() { + if threads.groups[&thread_hash].unseen() > 0 { Color::Byte(253) } else if idx % 2 == 0 { Color::Byte(236) @@ -1125,7 +1117,7 @@ impl CompactListing { columns[4][c].set_ch(' '); columns[4][c].set_bg(bg_color); } - match (threads.is_snoozed(thread_hash), has_attachments) { + match (threads.groups[&thread_hash].snoozed(), has_attachments) { (true, true) => { columns[3][(0, idx)].set_fg(Color::Byte(103)); columns[3][(2, idx)].set_fg(Color::Red); @@ -1225,22 +1217,8 @@ impl Component for CompactListing { k == shortcuts[CompactListing::DESCRIPTION]["open_thread"] ) => { - if self.filtered_selection.is_empty() { - self.view = ThreadView::new(self.cursor_pos, None, context); - } else { - let mut temp = self.cursor_pos; - let thread_hash = self.get_thread_under_cursor(self.cursor_pos.2, context); - let account = &context.accounts[self.cursor_pos.0]; - let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); - let threads = &account.collection.threads[&folder_hash]; - let root_thread_index = threads.root_iter().position(|t| t == thread_hash); - if let Some(pos) = root_thread_index { - temp.2 = pos; - self.view = ThreadView::new(temp, Some(thread_hash), context); - } else { - return true; - } - } + let thread = self.get_thread_under_cursor(self.cursor_pos.2, context); + self.view = ThreadView::new(self.cursor_pos, thread, None, context); self.unfocused = true; self.dirty = true; return true; @@ -1269,48 +1247,34 @@ impl Component for CompactListing { self.selection.entry(thread_hash).and_modify(|e| *e = !*e); } UIEvent::Action(ref action) => match action { - Action::SubSort(field, order) if !self.unfocused => { - debug!("SubSort {:?} , {:?}", field, order); - self.subsort = (*field, *order); - //if !self.filtered_selection.is_empty() { - // let threads = &account.collection.threads[&folder_hash]; - // threads.vec_inner_sort_by(&mut self.filtered_selection, self.sort, &account.collection); - //} else { - // self.refresh_mailbox(context); - //} - return true; - } Action::Sort(field, order) if !self.unfocused => { debug!("Sort {:?} , {:?}", field, order); self.sort = (*field, *order); if !self.filtered_selection.is_empty() { - let folder_hash = context.accounts[self.cursor_pos.0] - [self.cursor_pos.1] - .unwrap() - .folder - .hash(); - let threads = &context.accounts[self.cursor_pos.0].collection.threads - [&folder_hash]; - threads.vec_inner_sort_by( - &mut self.filtered_selection, - self.sort, - &context.accounts[self.cursor_pos.0].collection.envelopes, - ); + // FIXME: perform sort self.dirty = true; } else { self.refresh_mailbox(context); } return true; } + Action::SubSort(field, order) if !self.unfocused => { + debug!("SubSort {:?} , {:?}", field, order); + self.subsort = (*field, *order); + // FIXME: perform subsort. + return true; + } Action::ToggleThreadSnooze if !self.unfocused => { - let thread_hash = self.get_thread_under_cursor(self.cursor_pos.2, context); + let thread = self.get_thread_under_cursor(self.cursor_pos.2, context); let account = &mut context.accounts[self.cursor_pos.0]; let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); let threads = account.collection.threads.entry(folder_hash).or_default(); - let root_node = threads.thread_nodes.entry(thread_hash).or_default(); - let is_snoozed = root_node.snoozed(); - root_node.set_snoozed(!is_snoozed); - self.row_updates.push(thread_hash); + let is_snoozed = threads.groups[&thread].snoozed(); + threads + .groups + .entry(thread) + .and_modify(|entry| entry.set_snoozed(!is_snoozed)); + self.row_updates.push(thread); self.refresh_mailbox(context); return true; } @@ -1352,33 +1316,10 @@ impl Component for CompactListing { if !threads.thread_nodes.contains_key(&new_env_thread_hash) { return false; } - let thread_group = melib::find_root_hash( - &threads.thread_nodes, - threads.thread_nodes[&new_env_thread_hash].thread_group(), - ); - let (&thread_hash, &row): (&ThreadHash, &usize) = self - .order - .iter() - .find(|(n, _)| { - melib::find_root_hash( - &threads.thread_nodes, - threads.thread_nodes[&n].thread_group(), - ) == thread_group - }) - .unwrap(); - - 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; - } - } + let thread: ThreadGroupHash = + threads.find_group(threads.thread_nodes()[&new_env_thread_hash].group); + if self.order.contains_key(&thread) { + self.row_updates.push(thread); } self.dirty = true; diff --git a/ui/src/components/mail/listing/conversations.rs b/ui/src/components/mail/listing/conversations.rs index 23e590a6e..b90967623 100644 --- a/ui/src/components/mail/listing/conversations.rs +++ b/ui/src/components/mail/listing/conversations.rs @@ -85,33 +85,33 @@ pub struct ConversationsListing { length: usize, sort: (SortField, SortOrder), subsort: (SortField, SortOrder), - all_threads: fnv::FnvHashSet, - order: FnvHashMap, + all_threads: fnv::FnvHashSet, + order: FnvHashMap, /// Cache current view. content: CellBuffer, filter_term: String, - filtered_selection: Vec, - filtered_order: FnvHashMap, - selection: FnvHashMap, + filtered_selection: Vec, + filtered_order: FnvHashMap, + selection: FnvHashMap, /// If we must redraw on next redraw event dirty: bool, force_draw: bool, /// If `self.view` exists or not. unfocused: bool, view: ThreadView, - row_updates: SmallVec<[ThreadHash; 8]>, + row_updates: SmallVec<[ThreadGroupHash; 8]>, movement: Option, id: ComponentId, } impl MailListingTrait for ConversationsListing { - fn row_updates(&mut self) -> &mut SmallVec<[ThreadHash; 8]> { + fn row_updates(&mut self) -> &mut SmallVec<[ThreadGroupHash; 8]> { &mut self.row_updates } - fn get_focused_items(&self, context: &Context) -> SmallVec<[ThreadHash; 8]> { + fn get_focused_items(&self, context: &Context) -> SmallVec<[ThreadGroupHash; 8]> { let is_selection_empty = self.selection.values().cloned().any(std::convert::identity); let i = [self.get_thread_under_cursor(self.cursor_pos.2, context)]; let cursor_iter; @@ -149,23 +149,22 @@ impl ListingTrait for ConversationsListing { if self.length == 0 { return; } - let i = self.get_thread_under_cursor(idx, context); + let thread = self.get_thread_under_cursor(idx, context); let account = &context.accounts[self.cursor_pos.0]; let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); let threads = &account.collection.threads[&folder_hash]; - let thread_node = &threads.thread_nodes[&i]; - let fg_color = if thread_node.has_unseen() { + let fg_color = if threads.groups[&thread].unseen() > 0 { Color::Byte(0) } else { Color::Default }; let bg_color = if self.cursor_pos.2 == idx { Color::Byte(246) - } else if self.selection[&i] { + } else if self.selection[&thread] { Color::Byte(210) - } else if thread_node.has_unseen() { + } else if threads.groups[&thread].unseen() > 0 { Color::Byte(251) } else { Color::Default @@ -187,7 +186,7 @@ impl ListingTrait for ConversationsListing { let (upper_left, bottom_right) = area; let width = self.content.size().0; let (x, y) = upper_left; - if self.cursor_pos.2 == idx || self.selection[&i] { + if self.cursor_pos.2 == idx || self.selection[&thread] { for x in x..=get_x(bottom_right) { grid[(x, y)].set_fg(fg_color); grid[(x, y)].set_bg(bg_color); @@ -416,19 +415,19 @@ impl ListingTrait for ConversationsListing { if !threads.thread_nodes.contains_key(&env_hash_thread_hash) { continue; } - let thread_group = - melib::find_root_hash(&threads.thread_nodes, env_hash_thread_hash); - if self.filtered_order.contains_key(&thread_group) { + let thread = + threads.find_group(threads.thread_nodes[&env_hash_thread_hash].group); + if self.filtered_order.contains_key(&thread) { continue; } - if self.all_threads.contains(&thread_group) { - self.filtered_selection.push(thread_group); + if self.all_threads.contains(&thread) { + self.filtered_selection.push(thread); self.filtered_order - .insert(thread_group, self.filtered_selection.len() - 1); + .insert(thread, self.filtered_selection.len() - 1); } } if !self.filtered_selection.is_empty() { - threads.vec_inner_sort_by( + threads.group_inner_sort_by( &mut self.filtered_selection, self.sort, &context.accounts[self.cursor_pos.0].collection.envelopes, @@ -439,7 +438,11 @@ impl ListingTrait for ConversationsListing { self.content = CellBuffer::new_with_context(0, 0, Cell::with_char(' '), context); } - self.redraw_list(context); + self.redraw_list( + context, + Box::new(self.filtered_selection.clone().into_iter()) + as Box>, + ); } Err(e) => { self.cursor_pos.2 = 0; @@ -516,9 +519,10 @@ impl ConversationsListing { e: &Envelope, context: &Context, from: &Vec
, - thread_node: &ThreadNode, - is_snoozed: bool, + threads: &Threads, + hash: ThreadGroupHash, ) -> EntryStrings { + let thread = &threads.groups[&hash]; let folder_hash = &context.accounts[self.cursor_pos.0][self.cursor_pos.1] .unwrap() .folder @@ -560,32 +564,26 @@ impl ConversationsListing { } let mut subject = e.subject().to_string(); subject.truncate_at_boundary(150); - if thread_node.len() > 0 { + if thread.len() > 1 { EntryStrings { - date: DateString(ConversationsListing::format_date( - context, - thread_node.date(), - )), - subject: SubjectString(format!("{} ({})", subject, thread_node.len(),)), + date: DateString(ConversationsListing::format_date(context, thread.date())), + subject: SubjectString(format!("{} ({})", subject, thread.len(),)), flag: FlagString(format!( "{}{}", if e.has_attachments() { "📎" } else { "" }, - if is_snoozed { "💤" } else { "" } + if thread.snoozed() { "💤" } else { "" } )), from: FromString(address_list!((from) as comma_sep_list)), tags: TagString(tags, colors), } } else { EntryStrings { - date: DateString(ConversationsListing::format_date( - context, - thread_node.date(), - )), + date: DateString(ConversationsListing::format_date(context, thread.date())), subject: SubjectString(subject), flag: FlagString(format!( "{}{}", if e.has_attachments() { "📎" } else { "" }, - if is_snoozed { "💤" } else { "" } + if thread.snoozed() { "💤" } else { "" } )), from: FromString(address_list!((from) as comma_sep_list)), tags: TagString(tags, colors), @@ -638,16 +636,31 @@ impl ConversationsListing { return; } } + + let threads = &context.accounts[self.cursor_pos.0].collection.threads[&folder_hash]; + self.all_threads.clear(); + let mut roots = threads.roots(); + threads.group_inner_sort_by( + &mut roots, + self.sort, + &context.accounts[self.cursor_pos.0].collection.envelopes, + ); + + self.redraw_list( + context, + Box::new(roots.into_iter()) as Box>, + ); + if old_cursor_pos == self.new_cursor_pos { self.view.update(context); } else if self.unfocused { - self.view = ThreadView::new(self.new_cursor_pos, None, context); - } + let thread_group = self.get_thread_under_cursor(self.cursor_pos.2, context); - self.redraw_list(context); + self.view = ThreadView::new(self.new_cursor_pos, thread_group, None, context); + } } - fn redraw_list(&mut self, context: &Context) { + fn redraw_list(&mut self, context: &Context, items: Box>) { let account = &context.accounts[self.cursor_pos.0]; let mailbox = &account[self.cursor_pos.1].unwrap(); @@ -658,33 +671,21 @@ impl ConversationsListing { let mut rows = Vec::with_capacity(1024); let mut max_entry_columns = 0; - threads.sort_by(self.sort, self.subsort, &account.collection.envelopes); - - let mut refresh_mailbox = false; - let threads_iter = if self.filter_term.is_empty() { - refresh_mailbox = true; - self.all_threads.clear(); - Box::new(threads.root_iter()) as Box> - } else { - Box::new(self.filtered_selection.iter().map(|h| *h)) - as Box> - }; - let mut from_address_list = Vec::new(); let mut from_address_set: std::collections::HashSet> = std::collections::HashSet::new(); - for (idx, root_idx) in threads_iter.enumerate() { + for (idx, thread) in items.enumerate() { self.length += 1; - let thread_node = &threads.thread_nodes()[&root_idx]; - let i = thread_node.message().unwrap_or_else(|| { + let thread_node = &threads.thread_nodes()[&threads.groups[&thread].root().unwrap()]; + let root_env_hash = thread_node.message().unwrap_or_else(|| { let mut iter_ptr = thread_node.children()[0]; while threads.thread_nodes()[&iter_ptr].message().is_none() { iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0]; } threads.thread_nodes()[&iter_ptr].message().unwrap() }); - if !context.accounts[self.cursor_pos.0].contains_key(i) { - debug!("key = {}", i); + if !context.accounts[self.cursor_pos.0].contains_key(root_env_hash) { + debug!("key = {}", root_env_hash); debug!( "name = {} {}", mailbox.name(), @@ -696,14 +697,8 @@ impl ConversationsListing { } from_address_list.clear(); from_address_set.clear(); - let mut stack: SmallVec<[ThreadHash; 8]> = SmallVec::new(); - stack.push(root_idx); - while let Some(h) = stack.pop() { - let env_hash = if let Some(h) = threads.thread_nodes()[&h].message() { - h - } else { - break; - }; + for (_, h) in threads.thread_group_iter(thread) { + let env_hash = threads.thread_nodes()[&h].message().unwrap(); let envelope: &EnvelopeRef = &context.accounts[self.cursor_pos.0] .collection @@ -715,21 +710,13 @@ impl ConversationsListing { from_address_set.insert(addr.raw().to_vec()); from_address_list.push(addr.clone()); } - for c in threads.thread_nodes()[&h].children() { - stack.push(*c); - } } + let root_envelope: &EnvelopeRef = &context.accounts[self.cursor_pos.0] + .collection + .get_env(root_env_hash); - let root_envelope: &EnvelopeRef = - &context.accounts[self.cursor_pos.0].collection.get_env(i); - - let strings = self.make_entry_string( - root_envelope, - context, - &from_address_list, - thread_node, - threads.is_snoozed(root_idx), - ); + let strings = + self.make_entry_string(root_envelope, context, &from_address_list, threads, thread); max_entry_columns = std::cmp::max( max_entry_columns, strings.flag.len() @@ -742,20 +729,12 @@ impl ConversationsListing { max_entry_columns, strings.date.len() + 1 + strings.from.grapheme_width(), ); - rows.push(strings); - if refresh_mailbox { - self.all_threads.insert(root_idx); - } + rows.push(((idx, (thread, root_env_hash)), strings)); + self.all_threads.insert(thread); - self.order.insert(root_idx, idx); - self.selection.insert(root_idx, false); + self.order.insert(thread, idx); + self.selection.insert(thread, false); } - let ConversationsListing { - ref mut selection, - ref order, - .. - } = self; - selection.retain(|e, _| order.contains_key(e)); let width = max_entry_columns; self.content = @@ -766,31 +745,17 @@ impl ConversationsListing { } else { Color::Byte(235) }; - let threads_iter = if self.filter_term.is_empty() { - Box::new(threads.root_iter()) as Box> - } else { - Box::new(self.filtered_selection.iter().map(|h| *h)) - as Box> - }; - for ((idx, root_idx), strings) in threads_iter.enumerate().zip(rows) { - let thread_node = &threads.thread_nodes()[&root_idx]; - let i = thread_node.message().unwrap_or_else(|| { - let mut iter_ptr = thread_node.children()[0]; - while threads.thread_nodes()[&iter_ptr].message().is_none() { - iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0]; - } - threads.thread_nodes()[&iter_ptr].message().unwrap() - }); - if !context.accounts[self.cursor_pos.0].contains_key(i) { + for ((idx, (thread, root_env_hash)), strings) in rows { + if !context.accounts[self.cursor_pos.0].contains_key(root_env_hash) { panic!(); } - let fg_color = if thread_node.has_unseen() { + let fg_color = if threads.groups[&thread].unseen() > 0 { Color::Byte(0) } else { Color::Default }; - let bg_color = if thread_node.has_unseen() { + let bg_color = if threads.groups[&thread].unseen() > 0 { Color::Byte(251) } else { Color::Default @@ -930,7 +895,7 @@ impl ConversationsListing { } } - fn get_thread_under_cursor(&self, cursor: usize, context: &Context) -> ThreadHash { + fn get_thread_under_cursor(&self, cursor: usize, context: &Context) -> ThreadGroupHash { //let account = &context.accounts[self.cursor_pos.0]; //let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); //let threads = &account.collection.threads[&folder_hash]; @@ -950,20 +915,23 @@ impl ConversationsListing { } } - fn update_line(&mut self, context: &Context, thread_hash: ThreadHash) { + fn update_line(&mut self, context: &Context, thread_hash: ThreadGroupHash) { let account = &context.accounts[self.cursor_pos.0]; let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); let threads = &account.collection.threads[&folder_hash]; - let thread_node = &threads.thread_nodes[&thread_hash]; let idx: usize = self.order[&thread_hash]; let width = self.content.size().0; - let fg_color = if thread_node.has_unseen() { + let env_hash = threads.thread_nodes()[&threads.groups[&thread_hash].root().unwrap()] + .message() + .unwrap(); + + let fg_color = if threads.groups[&thread_hash].unseen() > 0 { Color::Byte(0) } else { Color::Default }; - let bg_color = if thread_node.has_unseen() { + let bg_color = if threads.groups[&thread_hash].unseen() > 0 { Color::Byte(251) } else { Color::Default @@ -976,14 +944,8 @@ impl ConversationsListing { let mut from_address_list = Vec::new(); let mut from_address_set: std::collections::HashSet> = std::collections::HashSet::new(); - let mut stack: SmallVec<[ThreadHash; 8]> = SmallVec::new(); - stack.push(thread_hash); - while let Some(h) = stack.pop() { - let env_hash = if let Some(h) = threads.thread_nodes()[&h].message() { - h - } else { - break; - }; + for (_, h) in threads.thread_group_iter(thread_hash) { + let env_hash = threads.thread_nodes()[&h].message().unwrap(); let envelope: &EnvelopeRef = &context.accounts[self.cursor_pos.0] .collection @@ -995,19 +957,10 @@ impl ConversationsListing { from_address_set.insert(addr.raw().to_vec()); from_address_list.push(addr.clone()); } - for c in threads.thread_nodes()[&h].children() { - stack.push(*c); - } } - let env_hash = threads[&thread_hash].message().unwrap(); let envelope: EnvelopeRef = account.collection.get_env(env_hash); - let strings = self.make_entry_string( - &envelope, - context, - &from_address_list, - &threads[&thread_hash], - threads.is_snoozed(thread_hash), - ); + let strings = + self.make_entry_string(&envelope, context, &from_address_list, threads, thread_hash); drop(envelope); /* draw flags */ let (x, _) = write_string_to_grid( @@ -1215,27 +1168,8 @@ impl Component for ConversationsListing { k == shortcuts[ConversationsListing::DESCRIPTION]["open_thread"] ) => { - if self.length == 0 { - return true; - } - - if self.filter_term.is_empty() { - self.view = ThreadView::new(self.cursor_pos, None, context); - } else if !self.filtered_selection.is_empty() { - let mut temp = self.cursor_pos; - let thread_hash = self.get_thread_under_cursor(self.cursor_pos.2, context); - let account = &context.accounts[self.cursor_pos.0]; - let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); - let threads = &account.collection.threads[&folder_hash]; - let root_thread_index = threads.root_iter().position(|t| t == thread_hash); - if let Some(pos) = root_thread_index { - temp.2 = pos; - self.view = ThreadView::new(temp, Some(thread_hash), context); - } else { - return true; - } - } - + let thread = self.get_thread_under_cursor(self.cursor_pos.2, context); + self.view = ThreadView::new(self.cursor_pos, thread, None, context); self.unfocused = true; self.dirty = true; return true; @@ -1260,8 +1194,8 @@ impl Component for ConversationsListing { key == shortcuts[ConversationsListing::DESCRIPTION]["select_entry"] ) => { - let thread_hash = self.get_thread_under_cursor(self.cursor_pos.2, context); - self.selection.entry(thread_hash).and_modify(|e| *e = !*e); + let thread = self.get_thread_under_cursor(self.cursor_pos.2, context); + self.selection.entry(thread).and_modify(|e| *e = !*e); return true; } UIEvent::EnvelopeRename(ref old_hash, ref new_hash) => { @@ -1275,33 +1209,10 @@ impl Component for ConversationsListing { if !threads.thread_nodes.contains_key(&new_env_thread_hash) { return false; } - let thread_group = melib::find_root_hash( - &threads.thread_nodes, - threads.thread_nodes[&new_env_thread_hash].thread_group(), - ); - let (&thread_hash, &row): (&ThreadHash, &usize) = self - .order - .iter() - .find(|(n, _)| { - melib::find_root_hash( - &threads.thread_nodes, - threads.thread_nodes[&n].thread_group(), - ) == thread_group - }) - .unwrap(); - - 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; - } - } + let thread: ThreadGroupHash = + threads.find_group(threads.thread_nodes()[&new_env_thread_hash].group); + if self.order.contains_key(&thread) { + self.row_updates.push(thread); } self.dirty = true; @@ -1313,6 +1224,7 @@ impl Component for ConversationsListing { Action::SubSort(field, order) if !self.unfocused => { debug!("SubSort {:?} , {:?}", field, order); self.subsort = (*field, *order); + // FIXME subsort //if !self.filtered_selection.is_empty() { // let threads = &account.collection.threads[&folder_hash]; // threads.vec_inner_sort_by(&mut self.filtered_selection, self.sort, &account.collection); @@ -1323,6 +1235,8 @@ impl Component for ConversationsListing { } Action::Sort(field, order) if !self.unfocused => { debug!("Sort {:?} , {:?}", field, order); + // FIXME sort + /* self.sort = (*field, *order); if !self.filtered_selection.is_empty() { let folder_hash = context.accounts[self.cursor_pos.0] @@ -1341,17 +1255,20 @@ impl Component for ConversationsListing { } else { self.refresh_mailbox(context); } + */ return true; } Action::ToggleThreadSnooze if !self.unfocused => { - let thread_hash = self.get_thread_under_cursor(self.cursor_pos.2, context); + let thread = self.get_thread_under_cursor(self.cursor_pos.2, context); let account = &mut context.accounts[self.cursor_pos.0]; let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); let threads = account.collection.threads.entry(folder_hash).or_default(); - let root_node = threads.thread_nodes.entry(thread_hash).or_default(); - let is_snoozed = root_node.snoozed(); - root_node.set_snoozed(!is_snoozed); - self.row_updates.push(thread_hash); + let is_snoozed = threads.groups[&thread].snoozed(); + threads + .groups + .entry(thread) + .and_modify(|entry| entry.set_snoozed(!is_snoozed)); + self.row_updates.push(thread); self.refresh_mailbox(context); return true; } diff --git a/ui/src/components/mail/listing/plain.rs b/ui/src/components/mail/listing/plain.rs index c24104163..9a77cc9d9 100644 --- a/ui/src/components/mail/listing/plain.rs +++ b/ui/src/components/mail/listing/plain.rs @@ -71,18 +71,20 @@ pub struct PlainListing { unfocused: bool, view: MailView, row_updates: SmallVec<[EnvelopeHash; 8]>, - _row_updates: SmallVec<[ThreadHash; 8]>, + _row_updates: SmallVec<[ThreadGroupHash; 8]>, movement: Option, id: ComponentId, } impl MailListingTrait for PlainListing { - fn row_updates(&mut self) -> &mut SmallVec<[ThreadHash; 8]> { + fn row_updates(&mut self) -> &mut SmallVec<[ThreadGroupHash; 8]> { &mut self._row_updates } - fn get_focused_items(&self, context: &Context) -> SmallVec<[ThreadHash; 8]> { + fn get_focused_items(&self, context: &Context) -> SmallVec<[ThreadGroupHash; 8]> { + return SmallVec::new(); + /* let is_selection_empty = self.selection.values().cloned().any(std::convert::identity); if is_selection_empty { self.selection @@ -95,6 +97,7 @@ impl MailListingTrait for PlainListing { ret.push(self.get_thread_under_cursor(self.cursor_pos.2, context)); ret } + */ } } diff --git a/ui/src/components/mail/listing/thread.rs b/ui/src/components/mail/listing/thread.rs index 6a81a735c..bb841fefb 100644 --- a/ui/src/components/mail/listing/thread.rs +++ b/ui/src/components/mail/listing/thread.rs @@ -37,7 +37,7 @@ pub struct ThreadListing { /// Cache current view. content: CellBuffer, - row_updates: SmallVec<[ThreadHash; 8]>, + row_updates: SmallVec<[ThreadGroupHash; 8]>, locations: Vec, /// If we must redraw on next redraw event dirty: bool, @@ -50,11 +50,11 @@ pub struct ThreadListing { } impl MailListingTrait for ThreadListing { - fn row_updates(&mut self) -> &mut SmallVec<[ThreadHash; 8]> { + fn row_updates(&mut self) -> &mut SmallVec<[ThreadGroupHash; 8]> { &mut self.row_updates } - fn get_focused_items(&self, _context: &Context) -> SmallVec<[ThreadHash; 8]> { + fn get_focused_items(&self, _context: &Context) -> SmallVec<[ThreadGroupHash; 8]> { SmallVec::new() } } diff --git a/ui/src/components/mail/view.rs b/ui/src/components/mail/view.rs index 534fe4197..a1db5efcd 100644 --- a/ui/src/components/mail/view.rs +++ b/ui/src/components/mail/view.rs @@ -777,17 +777,9 @@ impl Component for MailView { { let account = &context.accounts[self.coordinates.0]; let folder_hash = account[self.coordinates.1].unwrap().folder.hash(); - let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2); - let thread_hash = envelope.thread(); - let threads = &account.collection.threads[&folder_hash]; - let root_thread_hash = melib::find_root_hash(&threads.thread_nodes, thread_hash); - let root_idx = threads - .root_iter() - .position(|t| t == root_thread_hash) - .unwrap(); context.replies.push_back(UIEvent::Action(Tab(Reply( - (self.coordinates.0, self.coordinates.1, root_idx), - thread_hash, + (self.coordinates.0, folder_hash), + self.coordinates.2, )))); return true; } diff --git a/ui/src/components/mail/view/thread.rs b/ui/src/components/mail/view/thread.rs index 5964e3562..467b236d0 100644 --- a/ui/src/components/mail/view/thread.rs +++ b/ui/src/components/mail/view/thread.rs @@ -52,6 +52,7 @@ pub struct ThreadView { new_expanded_pos: usize, reversed: bool, coordinates: (usize, usize, usize), + thread_group: ThreadGroupHash, mailview: MailView, show_mailview: bool, show_thread: bool, @@ -75,6 +76,7 @@ impl ThreadView { */ pub fn new( coordinates: (usize, usize, usize), + thread_group: ThreadGroupHash, expanded_hash: Option, context: &Context, ) -> Self { @@ -82,6 +84,7 @@ impl ThreadView { reversed: false, initiated: false, coordinates, + thread_group, mailview: MailView::default(), show_mailview: true, show_thread: true, @@ -163,7 +166,7 @@ impl ThreadView { let mailbox = &account[self.coordinates.1].unwrap(); let threads = &account.collection.threads[&mailbox.folder.hash()]; - let thread_iter = threads.thread_iter(self.coordinates.2); + let thread_iter = threads.thread_group_iter(self.thread_group); self.entries.clear(); for (line, (ind, thread_hash)) in thread_iter.enumerate() { let entry = if let Some(msg_hash) = threads.thread_nodes()[&thread_hash].message() { @@ -633,7 +636,12 @@ impl ThreadView { let account = &context.accounts[self.coordinates.0]; let mailbox = &account[self.coordinates.1].unwrap(); let threads = &account.collection.threads[&mailbox.folder.hash()]; - let thread_node = &threads.thread_nodes()[&threads.root_set(self.coordinates.2)]; + let thread_root = threads + .thread_group_iter(self.thread_group) + .next() + .unwrap() + .1; + let thread_node = &threads.thread_nodes()[&thread_root]; let i = thread_node.message().unwrap_or_else(|| { let mut iter_ptr = thread_node.children()[0]; while threads.thread_nodes()[&iter_ptr].message().is_none() { @@ -723,7 +731,12 @@ impl ThreadView { let account = &context.accounts[self.coordinates.0]; let mailbox = &account[self.coordinates.1].unwrap(); let threads = &account.collection.threads[&mailbox.folder.hash()]; - let thread_node = &threads.thread_nodes()[&threads.root_set(self.coordinates.2)]; + let thread_root = threads + .thread_group_iter(self.thread_group) + .next() + .unwrap() + .1; + let thread_node = &threads.thread_nodes()[&thread_root]; let i = thread_node.message().unwrap_or_else(|| { let mut iter_ptr = thread_node.children()[0]; while threads.thread_nodes()[&iter_ptr].message().is_none() { diff --git a/ui/src/conf/accounts.rs b/ui/src/conf/accounts.rs index de63ddbae..5e13c8273 100644 --- a/ui/src/conf/accounts.rs +++ b/ui/src/conf/accounts.rs @@ -594,11 +594,12 @@ impl Account { return Some(UIEvent::MailboxUpdate((self.index, folder_hash))); } - let thread_node = { - let thread_hash = &mut self.collection.get_env(env_hash).thread(); - &self.collection.threads[&folder_hash][&thread_hash] + let thread = { + let thread_hash = self.collection.get_env(env_hash).thread(); + self.collection.threads[&folder_hash] + .find_group(self.collection.threads[&folder_hash][&thread_hash].group) }; - if thread_node.snoozed() { + if self.collection.threads[&folder_hash].groups[&thread].snoozed() { return Some(UIEvent::MailboxUpdate((self.index, folder_hash))); } if is_seen || is_draft { diff --git a/ui/src/execute/actions.rs b/ui/src/execute/actions.rs index 86f1ab133..fb9922c11 100644 --- a/ui/src/execute/actions.rs +++ b/ui/src/execute/actions.rs @@ -24,8 +24,7 @@ */ use crate::components::Component; -use melib::backends::FolderOperation; -use melib::thread::ThreadHash; +use melib::backends::{FolderHash, FolderOperation}; pub use melib::thread::{SortField, SortOrder}; use melib::{Draft, EnvelopeHash}; @@ -56,7 +55,7 @@ pub enum ListingAction { pub enum TabAction { New(Option>), NewDraft(usize, Option), - Reply((usize, usize, usize), ThreadHash), // thread coordinates (account, mailbox, root_set idx) and thread hash + Reply((usize, FolderHash), EnvelopeHash), // thread coordinates (account, mailbox) and envelope Close, Edit(usize, EnvelopeHash), // account_position, envelope hash Kill(Uuid),