Browse Source

melib: add ThreadGroup

Instead of using Union/Find to gather mail that belongs in the same
e-mail thread together, add a new entity ThreadGroup that ThreadNodes
point to. ThreadGroup represents an actual Thread: A thread root
ThreadGroup::Group or a reply ThreadGroup::Node.

To make semantics more accurate:

- ThreadNode hash should be renamed to ThreadNodeHash
- ThreadGroupHash should be renamed to ThreadHash
- ThreadGroup::Group should be a struct named Thread instead
- move ThreadGroup::Node logic to ThreadNode akin to Union/Find
- rename ThreaddGroup::Group to Thread
tags/alpha-0.6.0
Manos Pitsidianakis 2 years ago
parent
commit
47a69f8eb9
Signed by untrusted user: epilys GPG Key ID: 73627C2F690DF710
  1. 982
      melib/src/thread.rs
  2. 209
      melib/src/thread/iterators.rs
  3. 51
      ui/src/components/mail/compose.rs
  4. 39
      ui/src/components/mail/listing.rs
  5. 249
      ui/src/components/mail/listing/compact.rs
  6. 291
      ui/src/components/mail/listing/conversations.rs
  7. 9
      ui/src/components/mail/listing/plain.rs
  8. 6
      ui/src/components/mail/listing/thread.rs
  9. 12
      ui/src/components/mail/view.rs
  10. 19
      ui/src/components/mail/view/thread.rs
  11. 9
      ui/src/conf/accounts.rs
  12. 5
      ui/src/execute/actions.rs

982
melib/src/thread.rs
File diff suppressed because it is too large
View File

209
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 <http://www.gnu.org/licenses/>.
*/
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<ThreadHash>>,
pub(super) thread_nodes: &'a FnvHashMap<ThreadHash, ThreadNode>,
}
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<ThreadHash>>,
pub(super) thread_nodes: &'a FnvHashMap<ThreadHash, ThreadNode>,
}
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<ThreadHash>>,
pub thread_nodes: &'a FnvHashMap<ThreadHash, ThreadNode>,
}
impl<'a> Iterator for RootIterator<'a> {
type Item = ThreadHash;
fn next(&mut self) -> Option<ThreadHash> {
{
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<ThreadHash, ThreadNode>,
}
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()
}
}

51
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<Self> {
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) {

39
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<dyn Iterator<Item = ThreadHash>>) {
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<dyn Iterator<Item = ThreadGroupHash>>) {
unimplemented!()
}
}

249
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<ThreadHash>,
order: FnvHashMap<ThreadHash, usize>,
all_threads: fnv::FnvHashSet<ThreadGroupHash>,
order: FnvHashMap<ThreadGroupHash, usize>,
/// Cache current view.
data_columns: DataColumns,
filter_term: String,
filtered_selection: Vec<ThreadHash>,
filtered_order: FnvHashMap<ThreadHash, usize>,
selection: FnvHashMap<ThreadHash, bool>,
filtered_selection: Vec<ThreadGroupHash>,
filtered_order: FnvHashMap<ThreadGroupHash, usize>,
selection: FnvHashMap<ThreadGroupHash, bool>,
/// 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<PageMovement>,
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<dyn Iterator<Item = ThreadHash>>,
as Box<dyn Iterator<Item = ThreadGroupHash>>,
);
}
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::<Vec<ThreadHash>>().into_iter())
as Box<dyn Iterator<Item = ThreadHash>>,
Box::new(roots.into_iter()) as Box<dyn Iterator<Item = ThreadGroupHash>>,
);
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<dyn Iterator<Item = ThreadHash>>) {
fn redraw_list(&mut self, context: &Context, items: Box<dyn Iterator<Item = ThreadGroupHash>>) {
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;

291
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<ThreadHash>,
order: FnvHashMap<ThreadHash, usize>,
all_threads: fnv::FnvHashSet<ThreadGroupHash>,
order: FnvHashMap<ThreadGroupHash, usize>,
/// Cache current view.
content: CellBuffer,
filter_term: String,
filtered_selection: Vec<ThreadHash>,
filtered_order: FnvHashMap<ThreadHash, usize>,
selection: FnvHashMap<ThreadHash, bool>,
filtered_selection: Vec<ThreadGroupHash>,
filtered_order: FnvHashMap<ThreadGroupHash, usize>,
selection: FnvHashMap<ThreadGroupHash, bool>,
/// 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<PageMovement>,
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<dyn Iterator<Item = ThreadGroupHash>>,
);
}
Err(e) => {
self.cursor_pos.2 = 0;
@ -516,9 +519,10 @@ impl ConversationsListing {
e: &Envelope,
context: &Context,
from: &Vec<Address>,
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<dyn Iterator<Item = ThreadGroupHash>>,
);
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<dyn Iterator<Item = ThreadGroupHash>>) {
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<dyn Iterator<Item = ThreadHash>>
} else {
Box::new(self.filtered_selection.iter().map(|h| *h))
as Box<dyn Iterator<Item = ThreadHash>>
};
let mut from_address_list = Vec::new();
let mut from_address_set: std::collections::HashSet<Vec<u8>> =
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