You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1085 lines
37KB

  1. /*
  2. * meli - ui crate.
  3. *
  4. * Copyright 2017-2018 Manos Pitsidianakis
  5. *
  6. * This file is part of meli.
  7. *
  8. * meli is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License as published by
  10. * the Free Software Foundation, either version 3 of the License, or
  11. * (at your option) any later version.
  12. *
  13. * meli is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License
  19. * along with meli. If not, see <http://www.gnu.org/licenses/>.
  20. */
  21. use super::*;
  22. use crate::components::utilities::PageMovement;
  23. use std::cmp;
  24. use std::ops::{Deref, DerefMut};
  25. //use melib::mailbox::backends::BackendOp;
  26. const MAX_COLS: usize = 500;
  27. #[derive(Debug)]
  28. struct MailboxView {
  29. /// (x, y, z): x is accounts, y is folders, z is index inside a folder.
  30. cursor_pos: (usize, usize, usize),
  31. new_cursor_pos: (usize, usize, usize),
  32. length: usize,
  33. sort: (SortField, SortOrder),
  34. subsort: (SortField, SortOrder),
  35. order: FnvHashMap<EnvelopeHash, usize>,
  36. /// Cache current view.
  37. content: CellBuffer,
  38. columns: [CellBuffer; 5],
  39. widths: [usize; 5], // widths of columns calculated in first draw and after size changes
  40. /// If we must redraw on next redraw event
  41. dirty: bool,
  42. /// If `self.view` exists or not.
  43. unfocused: bool,
  44. view: ThreadView,
  45. row_updates: StackVec<EnvelopeHash>,
  46. movement: Option<PageMovement>,
  47. id: ComponentId,
  48. }
  49. macro_rules! address_list {
  50. (($name:expr) as comma_sep_list) => {{
  51. let mut ret: String =
  52. $name
  53. .into_iter()
  54. .fold(String::new(), |mut s: String, n: &Address| {
  55. s.extend(n.to_string().chars());
  56. s.push_str(", ");
  57. s
  58. });
  59. ret.pop();
  60. ret.pop();
  61. ret
  62. }};
  63. }
  64. macro_rules! column_str {
  65. (
  66. struct $name:ident(String)) => {
  67. pub struct $name(String);
  68. impl Deref for $name {
  69. type Target = String;
  70. fn deref(&self) -> &String {
  71. &self.0
  72. }
  73. }
  74. impl DerefMut for $name {
  75. fn deref_mut(&mut self) -> &mut String {
  76. &mut self.0
  77. }
  78. }
  79. };
  80. }
  81. column_str!(struct IndexNoString(String));
  82. column_str!(struct DateString(String));
  83. column_str!(struct FromString(String));
  84. column_str!(struct SubjectString(String));
  85. column_str!(struct FlagString(String));
  86. impl fmt::Display for MailboxView {
  87. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  88. write!(f, "{}", MailboxView::DESCRIPTION)
  89. }
  90. }
  91. impl MailboxView {
  92. const DESCRIPTION: &'static str = "";
  93. /// Helper function to format entry strings for CompactListing */
  94. /* TODO: Make this configurable */
  95. fn make_entry_string(
  96. e: &Envelope,
  97. len: usize,
  98. idx: usize,
  99. is_snoozed: bool,
  100. ) -> (
  101. IndexNoString,
  102. DateString,
  103. FromString,
  104. FlagString,
  105. SubjectString,
  106. ) {
  107. if len > 0 {
  108. (
  109. IndexNoString(idx.to_string()),
  110. DateString(MailboxView::format_date(e)),
  111. FromString(address_list!((e.from()) as comma_sep_list)),
  112. FlagString(format!(
  113. "{}{}",
  114. if e.has_attachments() { "📎" } else { "" },
  115. if is_snoozed { "💤" } else { "" }
  116. )),
  117. SubjectString(format!("{} ({})", e.subject(), len,)),
  118. )
  119. } else {
  120. (
  121. IndexNoString(idx.to_string()),
  122. DateString(MailboxView::format_date(e)),
  123. FromString(address_list!((e.from()) as comma_sep_list)),
  124. FlagString(format!(
  125. "{}{}",
  126. if e.has_attachments() { "📎" } else { "" },
  127. if is_snoozed { "💤" } else { "" }
  128. )),
  129. SubjectString(e.subject().to_string()),
  130. )
  131. }
  132. }
  133. fn new() -> Self {
  134. let content = CellBuffer::new(0, 0, Cell::with_char(' '));
  135. MailboxView {
  136. cursor_pos: (0, 1, 0),
  137. new_cursor_pos: (0, 0, 0),
  138. length: 0,
  139. sort: (Default::default(), Default::default()),
  140. subsort: (SortField::Date, SortOrder::Desc),
  141. order: FnvHashMap::default(),
  142. row_updates: StackVec::new(),
  143. columns: [
  144. CellBuffer::default(),
  145. CellBuffer::default(),
  146. CellBuffer::default(),
  147. CellBuffer::default(),
  148. CellBuffer::default(),
  149. ],
  150. widths: [0; 5],
  151. content,
  152. dirty: true,
  153. unfocused: false,
  154. view: ThreadView::default(),
  155. movement: None,
  156. id: ComponentId::new_v4(),
  157. }
  158. }
  159. /// Fill the `self.content` `CellBuffer` with the contents of the account folder the user has
  160. /// chosen.
  161. fn refresh_mailbox(&mut self, context: &mut Context) {
  162. self.dirty = true;
  163. let old_cursor_pos = self.cursor_pos;
  164. if !(self.cursor_pos.0 == self.new_cursor_pos.0
  165. && self.cursor_pos.1 == self.new_cursor_pos.1)
  166. {
  167. self.cursor_pos.2 = 0;
  168. self.new_cursor_pos.2 = 0;
  169. }
  170. self.cursor_pos.1 = self.new_cursor_pos.1;
  171. self.cursor_pos.0 = self.new_cursor_pos.0;
  172. let folder_hash = if let Some(h) = context.accounts[self.cursor_pos.0]
  173. .folders_order
  174. .get(self.cursor_pos.1)
  175. {
  176. *h
  177. } else {
  178. self.cursor_pos.1 = old_cursor_pos.1;
  179. self.dirty = false;
  180. return;
  181. };
  182. context
  183. .replies
  184. .push_back(UIEvent::RefreshMailbox((self.cursor_pos.0, folder_hash)));
  185. // Get mailbox as a reference.
  186. //
  187. match context.accounts[self.cursor_pos.0].status(folder_hash) {
  188. Ok(_) => {}
  189. Err(_) => {
  190. self.content = CellBuffer::new(MAX_COLS, 1, Cell::with_char(' '));
  191. self.length = 0;
  192. write_string_to_grid(
  193. "Loading.",
  194. &mut self.content,
  195. Color::Default,
  196. Color::Default,
  197. ((0, 0), (MAX_COLS - 1, 0)),
  198. false,
  199. );
  200. return;
  201. }
  202. }
  203. if old_cursor_pos == self.new_cursor_pos {
  204. self.view.update(context);
  205. } else if self.unfocused {
  206. self.view = ThreadView::new(self.new_cursor_pos, None, context);
  207. }
  208. let account = &context.accounts[self.cursor_pos.0];
  209. let mailbox = account[self.cursor_pos.1].as_ref().unwrap();
  210. let threads = &account.collection.threads[&mailbox.folder.hash()];
  211. self.order.clear();
  212. self.length = 0;
  213. let mut rows = Vec::with_capacity(1024);
  214. let mut min_width = (0, 0, 0, 0, 0);
  215. threads.sort_by(self.sort, self.subsort, &account.collection);
  216. for (idx, root_idx) in threads.root_iter().enumerate() {
  217. self.length += 1;
  218. let thread_node = &threads.thread_nodes()[&root_idx];
  219. let i = if let Some(i) = thread_node.message() {
  220. i
  221. } else {
  222. let mut iter_ptr = thread_node.children()[0];
  223. while threads.thread_nodes()[&iter_ptr].message().is_none() {
  224. iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0];
  225. }
  226. threads.thread_nodes()[&iter_ptr].message().unwrap()
  227. };
  228. if !context.accounts[self.cursor_pos.0].contains_key(i) {
  229. debug!("key = {}", i);
  230. debug!(
  231. "name = {} {}",
  232. mailbox.name(),
  233. context.accounts[self.cursor_pos.0].name()
  234. );
  235. debug!("{:#?}", context.accounts);
  236. panic!();
  237. }
  238. let root_envelope: &Envelope = &context.accounts[self.cursor_pos.0].get_env(&i);
  239. let strings = MailboxView::make_entry_string(
  240. root_envelope,
  241. thread_node.len(),
  242. idx,
  243. threads.is_snoozed(root_idx),
  244. );
  245. min_width.0 = cmp::max(min_width.0, strings.0.grapheme_width()); /* index */
  246. min_width.1 = cmp::max(min_width.1, strings.1.grapheme_width()); /* date */
  247. min_width.2 = cmp::max(min_width.2, strings.2.grapheme_width()); /* from */
  248. min_width.3 = cmp::max(min_width.3, strings.3.grapheme_width()); /* flags */
  249. min_width.4 = cmp::max(min_width.4, strings.4.grapheme_width()); /* subject */
  250. rows.push(strings);
  251. self.order.insert(i, idx);
  252. }
  253. /* index column */
  254. self.columns[0] = CellBuffer::new(min_width.0, rows.len(), Cell::with_char(' '));
  255. /* date column */
  256. self.columns[1] = CellBuffer::new(min_width.1, rows.len(), Cell::with_char(' '));
  257. /* from column */
  258. self.columns[2] = CellBuffer::new(min_width.2, rows.len(), Cell::with_char(' '));
  259. /* flags column */
  260. self.columns[3] = CellBuffer::new(min_width.3, rows.len(), Cell::with_char(' '));
  261. /* subject column */
  262. self.columns[4] = CellBuffer::new(min_width.4, rows.len(), Cell::with_char(' '));
  263. for ((idx, root_idx), strings) in threads.root_iter().enumerate().zip(rows) {
  264. let thread_node = &threads.thread_nodes()[&root_idx];
  265. let i = if let Some(i) = thread_node.message() {
  266. i
  267. } else {
  268. let mut iter_ptr = thread_node.children()[0];
  269. while threads.thread_nodes()[&iter_ptr].message().is_none() {
  270. iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0];
  271. }
  272. threads.thread_nodes()[&iter_ptr].message().unwrap()
  273. };
  274. if !context.accounts[self.cursor_pos.0].contains_key(i) {
  275. //debug!("key = {}", i);
  276. //debug!(
  277. // "name = {} {}",
  278. // mailbox.name(),
  279. // context.accounts[self.cursor_pos.0].name()
  280. //);
  281. //debug!("{:#?}", context.accounts);
  282. panic!();
  283. }
  284. let fg_color = if thread_node.has_unseen() {
  285. Color::Byte(0)
  286. } else {
  287. Color::Default
  288. };
  289. let bg_color = if thread_node.has_unseen() {
  290. Color::Byte(251)
  291. } else if idx % 2 == 0 {
  292. Color::Byte(236)
  293. } else {
  294. Color::Default
  295. };
  296. let (x, _) = write_string_to_grid(
  297. &strings.0,
  298. &mut self.columns[0],
  299. fg_color,
  300. bg_color,
  301. ((0, idx), (min_width.0, idx)),
  302. false,
  303. );
  304. for x in x..min_width.0 {
  305. self.columns[0][(x, idx)].set_bg(bg_color);
  306. }
  307. let (x, _) = write_string_to_grid(
  308. &strings.1,
  309. &mut self.columns[1],
  310. fg_color,
  311. bg_color,
  312. ((0, idx), (min_width.1, idx)),
  313. false,
  314. );
  315. for x in x..min_width.1 {
  316. self.columns[1][(x, idx)].set_bg(bg_color);
  317. }
  318. let (x, _) = write_string_to_grid(
  319. &strings.2,
  320. &mut self.columns[2],
  321. fg_color,
  322. bg_color,
  323. ((0, idx), (min_width.2, idx)),
  324. false,
  325. );
  326. for x in x..min_width.2 {
  327. self.columns[2][(x, idx)].set_bg(bg_color);
  328. }
  329. let (x, _) = write_string_to_grid(
  330. &strings.3,
  331. &mut self.columns[3],
  332. fg_color,
  333. bg_color,
  334. ((0, idx), (min_width.3, idx)),
  335. false,
  336. );
  337. for x in x..min_width.3 {
  338. self.columns[3][(x, idx)].set_bg(bg_color);
  339. }
  340. let (x, _) = write_string_to_grid(
  341. &strings.4,
  342. &mut self.columns[4],
  343. fg_color,
  344. bg_color,
  345. ((0, idx), (min_width.4, idx)),
  346. false,
  347. );
  348. for x in x..min_width.4 {
  349. self.columns[4][(x, idx)].set_bg(bg_color);
  350. }
  351. match (
  352. threads.is_snoozed(root_idx),
  353. &context.accounts[self.cursor_pos.0]
  354. .get_env(&i)
  355. .has_attachments(),
  356. ) {
  357. (true, true) => {
  358. self.columns[3][(0, idx)].set_fg(Color::Red);
  359. self.columns[3][(1, idx)].set_fg(Color::Byte(103));
  360. }
  361. (true, false) => {
  362. self.columns[3][(0, idx)].set_fg(Color::Red);
  363. }
  364. (false, true) => {
  365. self.columns[3][(0, idx)].set_fg(Color::Byte(103));
  366. }
  367. (false, false) => {}
  368. }
  369. self.order.insert(i, idx);
  370. }
  371. self.content = CellBuffer::new(MAX_COLS, self.length + 1, Cell::with_char(' '));
  372. if self.length == 0 {
  373. write_string_to_grid(
  374. &format!("Folder `{}` is empty.", mailbox.folder.name()),
  375. &mut self.content,
  376. Color::Default,
  377. Color::Default,
  378. ((0, 0), (MAX_COLS - 1, 0)),
  379. false,
  380. );
  381. return;
  382. }
  383. }
  384. fn highlight_line(
  385. &mut self,
  386. grid: Option<&mut CellBuffer>,
  387. area: Area,
  388. idx: usize,
  389. context: &Context,
  390. ) {
  391. let is_seen = {
  392. if self.length == 0 {
  393. return;
  394. }
  395. let account = &context.accounts[self.cursor_pos.0];
  396. let mailbox = account[self.cursor_pos.1].as_ref().unwrap();
  397. let threads = &account.collection.threads[&mailbox.folder.hash()];
  398. let thread_node = threads.root_set(idx);
  399. let thread_node = &threads.thread_nodes()[&thread_node];
  400. let i = if let Some(i) = thread_node.message() {
  401. i
  402. } else {
  403. let mut iter_ptr = thread_node.children()[0];
  404. while threads.thread_nodes()[&iter_ptr].message().is_none() {
  405. iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0];
  406. }
  407. threads.thread_nodes()[&iter_ptr].message().unwrap()
  408. };
  409. let root_envelope: &Envelope = &account.get_env(&i);
  410. root_envelope.is_seen()
  411. };
  412. let fg_color = if !is_seen {
  413. Color::Byte(0)
  414. } else {
  415. Color::Default
  416. };
  417. let bg_color = if self.cursor_pos.2 == idx {
  418. Color::Byte(246)
  419. } else if !is_seen {
  420. Color::Byte(251)
  421. } else if idx % 2 == 0 {
  422. Color::Byte(236)
  423. } else {
  424. Color::Default
  425. };
  426. if idx == self.cursor_pos.2 || grid.is_none() {
  427. if let Some(grid) = grid {
  428. change_colors(grid, area, fg_color, bg_color);
  429. } else {
  430. change_colors(&mut self.content, area, fg_color, bg_color);
  431. }
  432. return;
  433. }
  434. let (upper_left, bottom_right) = area;
  435. let grid = grid.unwrap();
  436. let (mut x, _y) = upper_left;
  437. for i in 0..self.columns.len() {
  438. let (width, height) = self.columns[i].size();
  439. if self.widths[i] == 0 {
  440. continue;
  441. }
  442. copy_area(
  443. grid,
  444. &self.columns[i],
  445. (
  446. set_x(upper_left, x),
  447. set_x(
  448. bottom_right,
  449. std::cmp::min(get_x(bottom_right), x + (self.widths[i])),
  450. ),
  451. ),
  452. ((0, idx), (width.saturating_sub(1), height - 1)),
  453. );
  454. if i != self.columns.len() - 1 {
  455. change_colors(
  456. grid,
  457. (
  458. set_x(upper_left, x + self.widths[i].saturating_sub(1)),
  459. set_x(bottom_right, x + self.widths[i] + 1),
  460. ),
  461. fg_color,
  462. bg_color,
  463. );
  464. } else {
  465. change_colors(
  466. grid,
  467. (
  468. set_x(
  469. upper_left,
  470. std::cmp::min(get_x(bottom_right), x + (self.widths[i])),
  471. ),
  472. bottom_right,
  473. ),
  474. fg_color,
  475. bg_color,
  476. );
  477. }
  478. x += self.widths[i] + 2; // + SEPARATOR
  479. if x > get_x(bottom_right) {
  480. break;
  481. }
  482. }
  483. }
  484. /// Draw the list of `Envelope`s.
  485. fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
  486. if self.cursor_pos.1 != self.new_cursor_pos.1 || self.cursor_pos.0 != self.new_cursor_pos.0
  487. {
  488. self.refresh_mailbox(context);
  489. }
  490. let upper_left = upper_left!(area);
  491. let bottom_right = bottom_right!(area);
  492. if self.length == 0 {
  493. clear_area(grid, area);
  494. copy_area(
  495. grid,
  496. &self.content,
  497. area,
  498. ((0, 0), (MAX_COLS - 1, self.length)),
  499. );
  500. context.dirty_areas.push_back(area);
  501. return;
  502. }
  503. let rows = get_y(bottom_right) - get_y(upper_left) + 1;
  504. if let Some(mvm) = self.movement.take() {
  505. match mvm {
  506. PageMovement::PageUp => {
  507. self.new_cursor_pos.2 = self.new_cursor_pos.2.saturating_sub(rows);
  508. }
  509. PageMovement::PageDown => {
  510. if self.new_cursor_pos.2 + rows + 1 < self.length {
  511. self.new_cursor_pos.2 += rows;
  512. } else {
  513. self.new_cursor_pos.2 = (self.length / rows) * rows;
  514. }
  515. }
  516. PageMovement::Home => {
  517. self.new_cursor_pos.2 = 0;
  518. }
  519. PageMovement::End => {
  520. self.new_cursor_pos.2 = (self.length / rows) * rows;
  521. }
  522. }
  523. }
  524. let prev_page_no = (self.cursor_pos.2).wrapping_div(rows);
  525. let page_no = (self.new_cursor_pos.2).wrapping_div(rows);
  526. let top_idx = page_no * rows;
  527. /* If cursor position has changed, remove the highlight from the previous position and
  528. * apply it in the new one. */
  529. if self.cursor_pos.2 != self.new_cursor_pos.2 && prev_page_no == page_no {
  530. let old_cursor_pos = self.cursor_pos;
  531. self.cursor_pos = self.new_cursor_pos;
  532. for idx in &[old_cursor_pos.2, self.new_cursor_pos.2] {
  533. if *idx >= self.length {
  534. continue; //bounds check
  535. }
  536. let new_area = (
  537. set_y(upper_left, get_y(upper_left) + (*idx % rows)),
  538. set_y(bottom_right, get_y(upper_left) + (*idx % rows)),
  539. );
  540. self.highlight_line(Some(grid), new_area, *idx, context);
  541. context.dirty_areas.push_back(new_area);
  542. }
  543. return;
  544. } else if self.cursor_pos != self.new_cursor_pos {
  545. self.cursor_pos = self.new_cursor_pos;
  546. }
  547. if self.new_cursor_pos.2 >= self.length {
  548. self.new_cursor_pos.2 = self.length - 1;
  549. self.cursor_pos.2 = self.new_cursor_pos.2;
  550. }
  551. let width = width!(area);
  552. self.widths = [
  553. self.columns[0].size().0,
  554. self.columns[1].size().0, /* date*/
  555. self.columns[2].size().0, /* from */
  556. self.columns[3].size().0, /* flags */
  557. self.columns[4].size().0, /* subject */
  558. ];
  559. let min_col_width = std::cmp::min(15, std::cmp::min(self.widths[4], self.widths[2]));
  560. if self.widths[0] + self.widths[1] + 3 * min_col_width + 8 > width {
  561. let remainder = width
  562. .saturating_sub(self.widths[0])
  563. .saturating_sub(self.widths[1])
  564. - 4;
  565. self.widths[2] = remainder / 6;
  566. self.widths[4] = (2 * remainder) / 3 - self.widths[3];
  567. } else {
  568. let remainder = width
  569. .saturating_sub(self.widths[0])
  570. .saturating_sub(self.widths[1])
  571. .saturating_sub(8);
  572. if min_col_width + self.widths[4] > remainder {
  573. self.widths[4] = remainder - min_col_width - self.widths[3];
  574. self.widths[2] = min_col_width;
  575. }
  576. }
  577. clear_area(grid, area);
  578. /* Page_no has changed, so draw new page */
  579. let mut x = get_x(upper_left);
  580. let mut flag_x = 0;
  581. for i in 0..self.columns.len() {
  582. let column_width = self.columns[i].size().0;
  583. if i == 3 {
  584. flag_x = x;
  585. }
  586. if self.widths[i] == 0 {
  587. continue;
  588. }
  589. copy_area(
  590. grid,
  591. &self.columns[i],
  592. (
  593. set_x(upper_left, x),
  594. set_x(
  595. bottom_right,
  596. std::cmp::min(get_x(bottom_right), x + (self.widths[i])),
  597. ),
  598. ),
  599. (
  600. (0, top_idx),
  601. (column_width.saturating_sub(1), self.length - 1),
  602. ),
  603. );
  604. x += self.widths[i] + 2; // + SEPARATOR
  605. if x > get_x(bottom_right) {
  606. break;
  607. }
  608. }
  609. for r in 0..cmp::min(self.length - top_idx, rows) {
  610. let (fg_color, bg_color) = {
  611. let c = &self.columns[0][(0, r + top_idx)];
  612. (c.fg(), c.bg())
  613. };
  614. change_colors(
  615. grid,
  616. (
  617. pos_inc(upper_left, (0, r)),
  618. (flag_x - 1, get_y(upper_left) + r),
  619. ),
  620. fg_color,
  621. bg_color,
  622. );
  623. for x in flag_x..(flag_x + 2 + self.widths[3]) {
  624. grid[(x, get_y(upper_left) + r)].set_bg(bg_color);
  625. }
  626. change_colors(
  627. grid,
  628. (
  629. (flag_x + 2 + self.widths[3], get_y(upper_left) + r),
  630. (get_x(bottom_right), get_y(upper_left) + r),
  631. ),
  632. fg_color,
  633. bg_color,
  634. );
  635. }
  636. let temp_copy_because_of_nll = self.cursor_pos.2; // FIXME
  637. self.highlight_line(
  638. Some(grid),
  639. (
  640. set_y(
  641. upper_left,
  642. get_y(upper_left) + (temp_copy_because_of_nll % rows),
  643. ),
  644. set_y(
  645. bottom_right,
  646. get_y(upper_left) + (temp_copy_because_of_nll % rows),
  647. ),
  648. ),
  649. temp_copy_because_of_nll,
  650. context,
  651. );
  652. if top_idx + rows > self.length {
  653. clear_area(
  654. grid,
  655. (
  656. pos_inc(upper_left, (0, self.length - top_idx)),
  657. bottom_right,
  658. ),
  659. );
  660. }
  661. context.dirty_areas.push_back(area);
  662. }
  663. fn format_date(envelope: &Envelope) -> String {
  664. let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(envelope.date());
  665. let now: std::time::Duration = std::time::SystemTime::now()
  666. .duration_since(d)
  667. .unwrap_or_else(|_| std::time::Duration::new(std::u64::MAX, 0));
  668. match now.as_secs() {
  669. n if n < 10 * 60 * 60 => format!("{} hours ago{}", n / (60 * 60), " ".repeat(8)),
  670. n if n < 24 * 60 * 60 => format!("{} hours ago{}", n / (60 * 60), " ".repeat(7)),
  671. n if n < 4 * 24 * 60 * 60 => {
  672. format!("{} days ago{}", n / (24 * 60 * 60), " ".repeat(9))
  673. }
  674. _ => envelope.datetime().format("%Y-%m-%d %H:%M:%S").to_string(),
  675. }
  676. }
  677. }
  678. impl Component for MailboxView {
  679. fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
  680. if !self.unfocused {
  681. if !self.is_dirty() {
  682. return;
  683. }
  684. if !self.row_updates.is_empty() {
  685. let (upper_left, bottom_right) = area;
  686. while let Some(row) = self.row_updates.pop() {
  687. let row: usize = self.order[&row];
  688. let rows = get_y(bottom_right) - get_y(upper_left) + 1;
  689. let page_no = (self.new_cursor_pos.2).wrapping_div(rows);
  690. let top_idx = page_no * rows;
  691. if row >= top_idx && row <= top_idx + rows {
  692. self.highlight_line(
  693. Some(grid),
  694. (
  695. set_y(upper_left, get_y(upper_left) + (row % rows)),
  696. set_y(bottom_right, get_y(upper_left) + (row % rows)),
  697. ),
  698. row,
  699. context,
  700. );
  701. }
  702. }
  703. } else {
  704. /* Draw the entire list */
  705. self.draw_list(grid, area, context);
  706. }
  707. } else {
  708. if self.length == 0 && self.dirty {
  709. clear_area(grid, area);
  710. context.dirty_areas.push_back(area);
  711. return;
  712. }
  713. self.view.draw(grid, area, context);
  714. }
  715. self.dirty = false;
  716. }
  717. fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
  718. if self.unfocused && self.view.process_event(event, context) {
  719. return true;
  720. }
  721. let shortcuts = &self.get_shortcuts(context)[CompactListing::DESCRIPTION];
  722. match *event {
  723. UIEvent::Input(Key::Up) => {
  724. if self.cursor_pos.2 > 0 {
  725. self.new_cursor_pos.2 = self.new_cursor_pos.2.saturating_sub(1);
  726. self.dirty = true;
  727. }
  728. return true;
  729. }
  730. UIEvent::Input(Key::Down) => {
  731. if self.length > 0 && self.new_cursor_pos.2 < self.length - 1 {
  732. self.new_cursor_pos.2 += 1;
  733. self.dirty = true;
  734. }
  735. return true;
  736. }
  737. UIEvent::Input(ref k) if !self.unfocused && *k == shortcuts["open_thread"] => {
  738. self.view = ThreadView::new(self.cursor_pos, None, context);
  739. self.unfocused = true;
  740. self.dirty = true;
  741. return true;
  742. }
  743. UIEvent::Input(ref key) if *key == shortcuts["prev_page"] => {
  744. self.movement = Some(PageMovement::PageUp);
  745. self.set_dirty();
  746. }
  747. UIEvent::Input(ref key) if *key == shortcuts["next_page"] => {
  748. self.movement = Some(PageMovement::PageDown);
  749. self.set_dirty();
  750. }
  751. UIEvent::Input(ref key) if *key == Key::Home => {
  752. self.movement = Some(PageMovement::Home);
  753. self.set_dirty();
  754. }
  755. UIEvent::Input(ref key) if *key == Key::End => {
  756. self.movement = Some(PageMovement::End);
  757. self.set_dirty();
  758. }
  759. UIEvent::Input(ref k) if self.unfocused && *k == shortcuts["exit_thread"] => {
  760. self.unfocused = false;
  761. self.dirty = true;
  762. return true;
  763. }
  764. UIEvent::MailboxUpdate((ref idxa, ref idxf))
  765. if (*idxa, *idxf)
  766. == (
  767. self.new_cursor_pos.0,
  768. context.accounts[self.new_cursor_pos.0].folders_order
  769. [self.new_cursor_pos.1],
  770. ) =>
  771. {
  772. self.refresh_mailbox(context);
  773. self.set_dirty();
  774. }
  775. UIEvent::StartupCheck(ref f)
  776. if *f
  777. == context.accounts[self.cursor_pos.0].folders_order[self.new_cursor_pos.1] =>
  778. {
  779. self.refresh_mailbox(context);
  780. self.set_dirty();
  781. }
  782. UIEvent::EnvelopeRename(ref old_hash, ref new_hash) => {
  783. if let Some(row) = self.order.remove(old_hash) {
  784. self.order.insert(*new_hash, row);
  785. self.highlight_line(None, ((0, row), (MAX_COLS - 1, row)), row, context);
  786. self.row_updates.push(*new_hash);
  787. self.dirty = true;
  788. } else {
  789. /* Listing has was updated in time before the event */
  790. }
  791. self.view
  792. .process_event(&mut UIEvent::EnvelopeRename(*old_hash, *new_hash), context);
  793. }
  794. UIEvent::ChangeMode(UIMode::Normal) => {
  795. self.dirty = true;
  796. }
  797. UIEvent::Resize => {
  798. self.dirty = true;
  799. }
  800. UIEvent::Action(ref action) => match action {
  801. Action::ViewMailbox(idx) => {
  802. if context.accounts[self.cursor_pos.0]
  803. .folders_order
  804. .get(self.cursor_pos.1)
  805. .is_none()
  806. {
  807. return true;
  808. }
  809. self.new_cursor_pos.1 = *idx;
  810. self.refresh_mailbox(context);
  811. return true;
  812. }
  813. Action::SubSort(field, order) => {
  814. debug!("SubSort {:?} , {:?}", field, order);
  815. self.subsort = (*field, *order);
  816. self.refresh_mailbox(context);
  817. return true;
  818. }
  819. Action::Sort(field, order) => {
  820. debug!("Sort {:?} , {:?}", field, order);
  821. self.sort = (*field, *order);
  822. self.refresh_mailbox(context);
  823. return true;
  824. }
  825. Action::ToggleThreadSnooze => {
  826. let account = &mut context.accounts[self.cursor_pos.0];
  827. let folder_hash = account[self.cursor_pos.1]
  828. .as_ref()
  829. .map(|m| m.folder.hash())
  830. .unwrap();
  831. let threads = account.collection.threads.entry(folder_hash).or_default();
  832. let thread_group =
  833. threads.thread_nodes()[&threads.root_set(self.cursor_pos.2)].thread_group();
  834. let thread_group = threads.find(thread_group);
  835. /*let i = if let Some(i) = threads.thread_nodes[&thread_group].message() {
  836. i
  837. } else {
  838. let mut iter_ptr = threads.thread_nodes[&thread_group].children()[0];
  839. while threads.thread_nodes()[&iter_ptr].message().is_none() {
  840. iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0];
  841. }
  842. threads.thread_nodes()[&iter_ptr].message().unwrap()
  843. };*/
  844. let root_node = threads.thread_nodes.entry(thread_group).or_default();
  845. let is_snoozed = root_node.snoozed();
  846. root_node.set_snoozed(!is_snoozed);
  847. //self.row_updates.push(i);
  848. self.refresh_mailbox(context);
  849. return true;
  850. }
  851. _ => {}
  852. },
  853. _ => {}
  854. }
  855. false
  856. }
  857. fn is_dirty(&self) -> bool {
  858. self.dirty
  859. || if self.unfocused {
  860. self.view.is_dirty()
  861. } else {
  862. false
  863. }
  864. }
  865. fn set_dirty(&mut self) {
  866. if self.unfocused {
  867. self.view.set_dirty();
  868. }
  869. self.dirty = true;
  870. }
  871. fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
  872. let mut map = if self.unfocused {
  873. self.view.get_shortcuts(context)
  874. } else {
  875. ShortcutMaps::default()
  876. };
  877. let config_map = context.settings.shortcuts.compact_listing.key_values();
  878. map.insert(
  879. CompactListing::DESCRIPTION.to_string(),
  880. [
  881. (
  882. "open_thread",
  883. if let Some(key) = config_map.get("open_thread") {
  884. (*key).clone()
  885. } else {
  886. Key::Char('\n')
  887. },
  888. ),
  889. (
  890. "prev_page",
  891. if let Some(key) = config_map.get("prev_page") {
  892. (*key).clone()
  893. } else {
  894. Key::PageUp
  895. },
  896. ),
  897. (
  898. "next_page",
  899. if let Some(key) = config_map.get("next_page") {
  900. (*key).clone()
  901. } else {
  902. Key::PageDown
  903. },
  904. ),
  905. (
  906. "exit_thread",
  907. if let Some(key) = config_map.get("exit_thread") {
  908. (*key).clone()
  909. } else {
  910. Key::Char('i')
  911. },
  912. ),
  913. ]
  914. .iter()
  915. .cloned()
  916. .collect(),
  917. );
  918. map
  919. }
  920. fn id(&self) -> ComponentId {
  921. self.id
  922. }
  923. fn set_id(&mut self, id: ComponentId) {
  924. self.id = id;
  925. }
  926. }
  927. /// A list of all mail (`Envelope`s) in a `Mailbox`. On `\n` it opens the `Envelope` content in a
  928. /// `ThreadView`.
  929. #[derive(Debug)]
  930. pub struct CompactListing {
  931. views: Vec<MailboxView>,
  932. cursor: usize,
  933. dirty: bool,
  934. populated: bool,
  935. id: ComponentId,
  936. }
  937. impl ListingTrait for CompactListing {
  938. fn coordinates(&self) -> (usize, usize, Option<EnvelopeHash>) {
  939. (self.cursor, self.views[self.cursor].cursor_pos.1, None)
  940. }
  941. fn set_coordinates(&mut self, coordinates: (usize, usize, Option<EnvelopeHash>)) {
  942. self.views
  943. .get_mut(self.cursor)
  944. .map(|v| v.new_cursor_pos = (coordinates.0, coordinates.1, 0));
  945. self.views.get_mut(self.cursor).map(|v| v.unfocused = false);
  946. }
  947. }
  948. impl fmt::Display for CompactListing {
  949. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  950. write!(f, "mail")
  951. }
  952. }
  953. impl Default for CompactListing {
  954. fn default() -> Self {
  955. CompactListing::new()
  956. }
  957. }
  958. impl CompactListing {
  959. const DESCRIPTION: &'static str = "compact listing";
  960. pub fn new() -> Self {
  961. CompactListing {
  962. views: Vec::with_capacity(8),
  963. cursor: 0,
  964. dirty: true,
  965. populated: false,
  966. id: ComponentId::new_v4(),
  967. }
  968. }
  969. }
  970. impl Component for CompactListing {
  971. fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
  972. if !self.populated {
  973. debug!("populating");
  974. for (idx, a) in context.accounts.iter().enumerate() {
  975. for (fidx, _) in a.iter_mailboxes().enumerate() {
  976. let mut m = MailboxView::new();
  977. m.new_cursor_pos = (idx, fidx, 0);
  978. self.views.push(m);
  979. }
  980. }
  981. self.populated = true;
  982. }
  983. self.dirty = false;
  984. if self.views.is_empty() {
  985. return;
  986. }
  987. self.views[self.cursor].draw(grid, area, context);
  988. }
  989. fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
  990. if self.views.is_empty() {
  991. return false;
  992. }
  993. match *event {
  994. UIEvent::Resize
  995. | UIEvent::MailboxUpdate(_)
  996. | UIEvent::ComponentKill(_)
  997. | UIEvent::StartupCheck(_)
  998. | UIEvent::EnvelopeUpdate(_)
  999. | UIEvent::EnvelopeRename(_, _)
  1000. | UIEvent::EnvelopeRemove(_) => {
  1001. return self.views[self.cursor].process_event(event, context)
  1002. }
  1003. _ => return self.views[self.cursor].process_event(event, context),
  1004. }
  1005. }
  1006. fn is_dirty(&self) -> bool {
  1007. if self.views.is_empty() {
  1008. return self.dirty;
  1009. }
  1010. self.dirty || self.views[self.cursor].is_dirty()
  1011. }
  1012. fn set_dirty(&mut self) {
  1013. if self.views.is_empty() {
  1014. return;
  1015. }
  1016. self.views[self.cursor].set_dirty();
  1017. self.dirty = true;
  1018. }
  1019. fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
  1020. if self.views.is_empty() {
  1021. return Default::default();
  1022. }
  1023. self.views[self.cursor].get_shortcuts(context)
  1024. }
  1025. fn id(&self) -> ComponentId {
  1026. self.id
  1027. }
  1028. fn set_id(&mut self, id: ComponentId) {
  1029. self.id = id;
  1030. }
  1031. }