/*
* meli
*
* Copyright 2017-2018 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 std::cmp;
use melib::UnixTimestamp;
use super::*;
use crate::components::PageMovement;
#[derive(Debug)]
struct ThreadEntry {
index: (usize, ThreadNodeHash, usize),
/// (indentation, thread_node index, line number in listing)
indentation: usize,
msg_hash: EnvelopeHash,
seen: bool,
dirty: bool,
hidden: bool,
heading: String,
timestamp: UnixTimestamp,
mailview: Box,
}
#[derive(Debug, Default, Copy, Clone)]
pub enum ThreadViewFocus {
#[default]
None,
Thread,
MailView,
}
#[derive(Debug, Default)]
pub struct ThreadView {
new_cursor_pos: usize,
cursor_pos: usize,
expanded_pos: usize,
new_expanded_pos: usize,
reversed: bool,
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
thread_group: ThreadHash,
focus: ThreadViewFocus,
entries: Vec,
visible_entries: Vec>,
indentation_colors: [ThemeAttribute; 6],
use_color: bool,
horizontal: Option,
movement: Option,
dirty: bool,
content: Screen,
id: ComponentId,
}
impl ThreadView {
/*
* @coordinates: (account index, mailbox_hash, root set thread_node index)
* @expanded_hash: optional position of expanded entry when we render the
* ThreadView.
* default: expanded message is the last one.
* @context: current context
*/
pub fn new(
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
thread_group: ThreadHash,
expanded_hash: Option,
go_to_first_unread: bool,
focus: Option,
context: &mut Context,
) -> Self {
let mut view = ThreadView {
reversed: false,
coordinates,
thread_group,
focus: focus.unwrap_or_default(),
entries: Vec::new(),
cursor_pos: 1,
new_cursor_pos: 0,
dirty: true,
id: ComponentId::default(),
indentation_colors: [
crate::conf::value(context, "mail.view.thread.indentation.a"),
crate::conf::value(context, "mail.view.thread.indentation.b"),
crate::conf::value(context, "mail.view.thread.indentation.c"),
crate::conf::value(context, "mail.view.thread.indentation.d"),
crate::conf::value(context, "mail.view.thread.indentation.e"),
crate::conf::value(context, "mail.view.thread.indentation.f"),
],
use_color: context.settings.terminal.use_color(),
horizontal: None,
..Default::default()
};
view.initiate(expanded_hash, go_to_first_unread, context);
view.new_cursor_pos = view.new_expanded_pos;
view
}
pub fn update(&mut self, context: &mut Context) {
if self.entries.is_empty() {
return;
}
let old_entries = std::mem::take(&mut self.entries);
let old_focused_entry = if self.entries.len() > self.cursor_pos {
Some(self.entries.remove(self.cursor_pos))
} else {
None
};
let old_expanded_entry = if self.entries.len() > self.expanded_pos {
Some(self.entries.remove(self.expanded_pos))
} else {
None
};
let expanded_hash = old_expanded_entry.as_ref().map(|e| e.msg_hash);
self.initiate(expanded_hash, false, context);
let mut old_cursor = 0;
let mut new_cursor = 0;
loop {
if old_cursor >= old_entries.len() || new_cursor >= self.entries.len() {
break;
}
if old_entries[old_cursor].msg_hash == self.entries[new_cursor].msg_hash
|| old_entries[old_cursor].index == self.entries[new_cursor].index
|| old_entries[old_cursor].heading == self.entries[new_cursor].heading
{
self.entries[new_cursor].hidden = old_entries[old_cursor].hidden;
old_cursor += 1;
new_cursor += 1;
} else {
new_cursor += 1;
}
self.recalc_visible_entries();
}
if let Some(old_focused_entry) = old_focused_entry {
if let Some(new_entry_idx) = self.entries.iter().position(|e| {
e.msg_hash == old_focused_entry.msg_hash
|| (e.index.1 == old_focused_entry.index.1
&& e.index.2 == old_focused_entry.index.2)
}) {
self.cursor_pos = new_entry_idx;
}
}
if let Some(old_expanded_entry) = old_expanded_entry {
if let Some(new_entry_idx) = self.entries.iter().position(|e| {
e.msg_hash == old_expanded_entry.msg_hash
|| (e.index.1 == old_expanded_entry.index.1
&& e.index.2 == old_expanded_entry.index.2)
}) {
self.expanded_pos = new_entry_idx;
}
}
self.set_dirty(true);
}
fn initiate(
&mut self,
expanded_hash: Option,
go_to_first_unread: bool,
context: &mut Context,
) {
#[inline(always)]
fn make_entry(
i: (usize, ThreadNodeHash, usize),
(account_hash, mailbox_hash, msg_hash): (AccountHash, MailboxHash, EnvelopeHash),
seen: bool,
initialize_now: bool,
timestamp: UnixTimestamp,
context: &mut Context,
) -> ThreadEntry {
let (ind, _, _) = i;
ThreadEntry {
index: i,
indentation: ind,
mailview: Box::new(MailView::new(
Some((account_hash, mailbox_hash, msg_hash)),
initialize_now,
context,
)),
msg_hash,
seen,
dirty: true,
hidden: false,
heading: String::new(),
timestamp,
}
}
let collection = context.accounts[&self.coordinates.0].collection.clone();
let threads = collection.get_threads(self.coordinates.1);
if !threads.groups.contains_key(&self.thread_group) {
return;
}
let (account_hash, mailbox_hash, _) = self.coordinates;
// Find out how many entries there are going to be, and prioritize
// initialization to the open entry and the most recent ones.
//
// This helps skip initializing the whole thread at once, which will make the UI
// loading slower.
//
// This won't help at all if the latest entry is a reply to an older entry but
// oh well.
let mut total_entries = vec![];
for (_, thread_node_hash) in threads.thread_iter(self.thread_group) {
if let Some(msg_hash) = threads.thread_nodes()[&thread_node_hash].message() {
if Some(msg_hash) == expanded_hash {
continue;
}
let env_ref = collection.get_env(msg_hash);
total_entries.push((msg_hash, env_ref.timestamp));
};
}
total_entries.sort_by_key(|e| std::cmp::Reverse(e.1));
let tokens = f64::from(u32::try_from(total_entries.len()).unwrap_or(0)) * 0.29;
let tokens = tokens.ceil() as usize;
total_entries.truncate(tokens);
// Now, only the expanded envelope plus the ones that remained in total_entries
// (around 30% of the total messages in the thread) will be scheduled
// for loading immediately. The others will be lazily loaded when the
// user opens them for reading.
let thread_iter = threads.thread_iter(self.thread_group);
self.entries.clear();
let mut earliest_unread = 0;
let mut earliest_unread_entry = 0;
for (line, (ind, thread_node_hash)) in thread_iter.enumerate() {
let entry = if let Some(msg_hash) = threads.thread_nodes()[&thread_node_hash].message()
{
let (is_seen, timestamp) = {
let env_ref = collection.get_env(msg_hash);
if !env_ref.is_seen()
&& (earliest_unread == 0 || env_ref.timestamp < earliest_unread)
{
earliest_unread = env_ref.timestamp;
earliest_unread_entry = self.entries.len();
}
(env_ref.is_seen(), env_ref.timestamp)
};
let initialize_now = if total_entries.is_empty() {
false
} else {
// ExtractIf but it hasn't been stabilized yet.
// https://doc.rust-lang.org/std/vec/struct.Vec.html#method.extract_if
let mut i = 0;
let mut result = false;
while i < total_entries.len() {
if total_entries[i].0 == msg_hash {
total_entries.remove(i);
result = true;
break;
} else {
i += 1;
}
}
result
};
make_entry(
(ind, thread_node_hash, line),
(account_hash, mailbox_hash, msg_hash),
is_seen,
initialize_now || expanded_hash == Some(msg_hash),
timestamp,
context,
)
} else {
continue;
};
match expanded_hash {
Some(expanded_hash) if expanded_hash == entry.msg_hash => {
self.new_expanded_pos = self.entries.len();
self.expanded_pos = self.new_expanded_pos + 1;
}
_ => {}
}
self.entries.push(entry);
}
if expanded_hash.is_none() {
self.new_expanded_pos = self
.entries
.iter()
.enumerate()
.reduce(|a, b| if a.1.timestamp > b.1.timestamp { a } else { b })
.map(|el| el.0)
.unwrap_or(0);
self.expanded_pos = self.new_expanded_pos + 1;
}
if go_to_first_unread && earliest_unread > 0 {
self.new_expanded_pos = earliest_unread_entry;
self.expanded_pos = earliest_unread_entry + 1;
}
let height = 2 * self.entries.len() + 1;
let mut width = 0;
let mut highlight_reply_subjects: Vec