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.

675 lines
23KB

  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. const MAX_COLS: usize = 500;
  24. /// A list of all mail (`Envelope`s) in a `Mailbox`. On `\n` it opens the `Envelope` content in a
  25. /// `MailView`.
  26. #[derive(Debug)]
  27. pub struct ThreadListing {
  28. /// (x, y, z): x is accounts, y is folders, z is index inside a folder.
  29. cursor_pos: (usize, usize, usize),
  30. new_cursor_pos: (usize, usize, usize),
  31. length: usize,
  32. sort: (SortField, SortOrder),
  33. subsort: (SortField, SortOrder),
  34. /// Cache current view.
  35. content: CellBuffer,
  36. locations: Vec<EnvelopeHash>,
  37. /// If we must redraw on next redraw event
  38. dirty: bool,
  39. /// If `self.view` is focused or not.
  40. unfocused: bool,
  41. initialised: bool,
  42. view: Option<MailView>,
  43. movement: Option<PageMovement>,
  44. id: ComponentId,
  45. }
  46. impl ListingTrait for ThreadListing {
  47. fn coordinates(&self) -> (usize, usize, Option<EnvelopeHash>) {
  48. (
  49. self.cursor_pos.0,
  50. self.cursor_pos.1,
  51. Some(self.locations[self.cursor_pos.2]),
  52. )
  53. }
  54. fn set_coordinates(&mut self, coordinates: (usize, usize, Option<EnvelopeHash>)) {
  55. self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
  56. }
  57. }
  58. impl Default for ThreadListing {
  59. fn default() -> Self {
  60. Self::new()
  61. }
  62. }
  63. impl fmt::Display for ThreadListing {
  64. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  65. write!(f, "mail")
  66. }
  67. }
  68. impl ThreadListing {
  69. pub fn new() -> Self {
  70. let content = CellBuffer::new(0, 0, Cell::with_char(' '));
  71. ThreadListing {
  72. cursor_pos: (0, 1, 0),
  73. new_cursor_pos: (0, 0, 0),
  74. length: 0,
  75. sort: (Default::default(), Default::default()),
  76. subsort: (Default::default(), Default::default()),
  77. content,
  78. locations: Vec::new(),
  79. dirty: true,
  80. unfocused: false,
  81. view: None,
  82. initialised: false,
  83. movement: None,
  84. id: ComponentId::new_v4(),
  85. }
  86. }
  87. /// Fill the `self.content` `CellBuffer` with the contents of the account folder the user has
  88. /// chosen.
  89. fn refresh_mailbox(&mut self, context: &mut Context) {
  90. self.dirty = true;
  91. if !(self.cursor_pos.0 == self.new_cursor_pos.0
  92. && self.cursor_pos.1 == self.new_cursor_pos.1)
  93. {
  94. //TODO: store cursor_pos in each folder
  95. self.cursor_pos.2 = 0;
  96. self.new_cursor_pos.2 = 0;
  97. }
  98. self.cursor_pos.1 = self.new_cursor_pos.1;
  99. self.cursor_pos.0 = self.new_cursor_pos.0;
  100. let folder_hash = context.accounts[self.cursor_pos.0].folders_order[self.cursor_pos.1];
  101. // Inform State that we changed the current folder view.
  102. context
  103. .replies
  104. .push_back(UIEvent::RefreshMailbox((self.cursor_pos.0, folder_hash)));
  105. // Get mailbox as a reference.
  106. //
  107. match context.accounts[self.cursor_pos.0].status(folder_hash) {
  108. Ok(_) => {}
  109. Err(_) => {
  110. self.content = CellBuffer::new(MAX_COLS, 1, Cell::with_char(' '));
  111. self.length = 0;
  112. write_string_to_grid(
  113. "Loading.",
  114. &mut self.content,
  115. Color::Default,
  116. Color::Default,
  117. ((0, 0), (MAX_COLS - 1, 0)),
  118. false,
  119. );
  120. return;
  121. }
  122. }
  123. let account = &context.accounts[self.cursor_pos.0];
  124. let mailbox = account[self.cursor_pos.1].as_ref().unwrap();
  125. let threads = &account.collection.threads[&mailbox.folder.hash()];
  126. self.length = threads.len();
  127. self.content = CellBuffer::new(MAX_COLS, self.length + 1, Cell::with_char(' '));
  128. self.locations.clear();
  129. if self.length == 0 {
  130. write_string_to_grid(
  131. &format!("Folder `{}` is empty.", mailbox.folder.name()),
  132. &mut self.content,
  133. Color::Default,
  134. Color::Default,
  135. ((0, 0), (MAX_COLS - 1, 0)),
  136. true,
  137. );
  138. return;
  139. }
  140. let mut indentations: Vec<bool> = Vec::with_capacity(6);
  141. let mut thread_idx = 0; // needed for alternate thread colors
  142. /* Draw threaded view. */
  143. threads.sort_by(self.sort, self.subsort, &account.collection);
  144. let thread_nodes: &FnvHashMap<ThreadHash, ThreadNode> = &threads.thread_nodes();
  145. let mut iter = threads.threads_iter().peekable();
  146. /* This is just a desugared for loop so that we can use .peek() */
  147. let mut idx = 0;
  148. while let Some((indentation, thread_hash, has_sibling)) = iter.next() {
  149. let thread_node = &thread_nodes[&thread_hash];
  150. if indentation == 0 {
  151. thread_idx += 1;
  152. }
  153. if thread_node.has_message() {
  154. let envelope: &Envelope = &account.get_env(&thread_node.message().unwrap());
  155. self.locations.push(envelope.hash());
  156. let fg_color = if !envelope.is_seen() {
  157. Color::Byte(0)
  158. } else {
  159. Color::Default
  160. };
  161. let bg_color = if !envelope.is_seen() {
  162. Color::Byte(251)
  163. } else if thread_idx % 2 == 0 {
  164. Color::Byte(236)
  165. } else {
  166. Color::Default
  167. };
  168. let (x, _) = write_string_to_grid(
  169. &ThreadListing::make_thread_entry(
  170. envelope,
  171. idx,
  172. indentation,
  173. thread_hash,
  174. threads,
  175. &indentations,
  176. has_sibling,
  177. ),
  178. &mut self.content,
  179. fg_color,
  180. bg_color,
  181. ((0, idx), (MAX_COLS - 1, idx)),
  182. false,
  183. );
  184. for x in x..MAX_COLS {
  185. self.content[(x, idx)].set_ch(' ');
  186. self.content[(x, idx)].set_bg(bg_color);
  187. }
  188. idx += 1;
  189. } else {
  190. continue;
  191. }
  192. match iter.peek() {
  193. Some((x, _, _)) if *x > indentation => {
  194. if has_sibling {
  195. indentations.push(true);
  196. } else {
  197. indentations.push(false);
  198. }
  199. }
  200. Some((x, _, _)) if *x < indentation => {
  201. for _ in 0..(indentation - *x) {
  202. indentations.pop();
  203. }
  204. }
  205. _ => {}
  206. }
  207. }
  208. }
  209. fn highlight_line_self(&mut self, idx: usize, context: &Context) {
  210. let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1]
  211. .as_ref()
  212. .unwrap();
  213. if mailbox.is_empty() {
  214. return;
  215. }
  216. if self.locations[idx] != 0 {
  217. let envelope: &Envelope =
  218. &context.accounts[self.cursor_pos.0].get_env(&self.locations[idx]);
  219. let fg_color = if !envelope.is_seen() {
  220. Color::Byte(0)
  221. } else {
  222. Color::Default
  223. };
  224. let bg_color = if !envelope.is_seen() {
  225. Color::Byte(251)
  226. } else if idx % 2 == 0 {
  227. Color::Byte(236)
  228. } else {
  229. Color::Default
  230. };
  231. change_colors(
  232. &mut self.content,
  233. ((0, idx), (MAX_COLS - 1, idx)),
  234. fg_color,
  235. bg_color,
  236. );
  237. }
  238. }
  239. fn highlight_line(&self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context) {
  240. let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1]
  241. .as_ref()
  242. .unwrap();
  243. if mailbox.is_empty() || mailbox.len() <= idx {
  244. return;
  245. }
  246. if self.locations[idx] != 0 {
  247. let envelope: &Envelope =
  248. &context.accounts[self.cursor_pos.0].get_env(&self.locations[idx]);
  249. let fg_color = if !envelope.is_seen() {
  250. Color::Byte(0)
  251. } else {
  252. Color::Default
  253. };
  254. let bg_color = if self.cursor_pos.2 == idx {
  255. Color::Byte(246)
  256. } else if !envelope.is_seen() {
  257. Color::Byte(251)
  258. } else if idx % 2 == 0 {
  259. Color::Byte(236)
  260. } else {
  261. Color::Default
  262. };
  263. change_colors(grid, area, fg_color, bg_color);
  264. }
  265. }
  266. /// Draw the list of `Envelope`s.
  267. fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
  268. if self.cursor_pos.1 != self.new_cursor_pos.1 || self.cursor_pos.0 != self.new_cursor_pos.0
  269. {
  270. self.refresh_mailbox(context);
  271. }
  272. let upper_left = upper_left!(area);
  273. let bottom_right = bottom_right!(area);
  274. if self.length == 0 {
  275. clear_area(grid, area);
  276. copy_area(grid, &self.content, area, ((0, 0), (MAX_COLS - 1, 0)));
  277. context.dirty_areas.push_back(area);
  278. return;
  279. }
  280. let rows = get_y(bottom_right) - get_y(upper_left) + 1;
  281. let prev_page_no = (self.cursor_pos.2).wrapping_div(rows);
  282. let page_no = (self.new_cursor_pos.2).wrapping_div(rows);
  283. let top_idx = page_no * rows;
  284. if !self.initialised {
  285. self.initialised = false;
  286. copy_area(
  287. grid,
  288. &self.content,
  289. area,
  290. ((0, top_idx), (MAX_COLS - 1, self.length)),
  291. );
  292. self.highlight_line(
  293. grid,
  294. (
  295. set_y(upper_left, get_y(upper_left) + (self.cursor_pos.2 % rows)),
  296. set_y(bottom_right, get_y(upper_left) + (self.cursor_pos.2 % rows)),
  297. ),
  298. self.cursor_pos.2,
  299. context,
  300. );
  301. context.dirty_areas.push_back(area);
  302. }
  303. /* If cursor position has changed, remove the highlight from the previous position and
  304. * apply it in the new one. */
  305. if self.cursor_pos.2 != self.new_cursor_pos.2 && prev_page_no == page_no {
  306. let old_cursor_pos = self.cursor_pos;
  307. self.cursor_pos = self.new_cursor_pos;
  308. for idx in &[old_cursor_pos.2, self.new_cursor_pos.2] {
  309. if *idx >= self.length {
  310. continue; //bounds check
  311. }
  312. let new_area = (
  313. set_y(upper_left, get_y(upper_left) + (*idx % rows)),
  314. set_y(bottom_right, get_y(upper_left) + (*idx % rows)),
  315. );
  316. self.highlight_line(grid, new_area, *idx, context);
  317. context.dirty_areas.push_back(new_area);
  318. }
  319. return;
  320. } else if self.cursor_pos != self.new_cursor_pos {
  321. self.cursor_pos = self.new_cursor_pos;
  322. }
  323. /* Page_no has changed, so draw new page */
  324. copy_area(
  325. grid,
  326. &self.content,
  327. area,
  328. ((0, top_idx), (MAX_COLS - 1, self.length)),
  329. );
  330. self.highlight_line(
  331. grid,
  332. (
  333. set_y(upper_left, get_y(upper_left) + (self.cursor_pos.2 % rows)),
  334. set_y(bottom_right, get_y(upper_left) + (self.cursor_pos.2 % rows)),
  335. ),
  336. self.cursor_pos.2,
  337. context,
  338. );
  339. context.dirty_areas.push_back(area);
  340. }
  341. fn make_thread_entry(
  342. envelope: &Envelope,
  343. idx: usize,
  344. indent: usize,
  345. node_idx: ThreadHash,
  346. threads: &Threads,
  347. indentations: &[bool],
  348. has_sibling: bool,
  349. //op: Box<BackendOp>,
  350. ) -> String {
  351. let thread_node = &threads[&node_idx];
  352. let has_parent = thread_node.has_parent();
  353. let show_subject = thread_node.show_subject();
  354. let mut s = format!("{}{}{} ", idx, " ", ThreadListing::format_date(&envelope));
  355. for i in 0..indent {
  356. if indentations.len() > i && indentations[i] {
  357. s.push('β”‚');
  358. } else if indentations.len() > i {
  359. s.push(' ');
  360. }
  361. if i > 0 {
  362. s.push(' ');
  363. }
  364. }
  365. if indent > 0 && (has_sibling || has_parent) {
  366. if has_sibling && has_parent {
  367. s.push('β”œ');
  368. } else if has_sibling {
  369. s.push('┬');
  370. } else {
  371. s.push('β””');
  372. }
  373. s.push('─');
  374. s.push('>');
  375. }
  376. if show_subject {
  377. s.push_str(&format!("{:.85}", envelope.subject()));
  378. }
  379. /* TODO Very slow since we have to build all attachments
  380. let attach_count = envelope.body(op).count_attachments();
  381. if attach_count > 1 {
  382. s.push_str(&format!(" {}Γ’Λ†ΕΎ ", attach_count - 1));
  383. }
  384. */
  385. s
  386. }
  387. fn format_date(envelope: &Envelope) -> String {
  388. let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(envelope.date());
  389. let now: std::time::Duration = std::time::SystemTime::now()
  390. .duration_since(d)
  391. .unwrap_or_else(|_| std::time::Duration::new(std::u64::MAX, 0));
  392. match now.as_secs() {
  393. n if n < 10 * 60 * 60 => format!("{} hours ago{}", n / (60 * 60), " ".repeat(8)),
  394. n if n < 24 * 60 * 60 => format!("{} hours ago{}", n / (60 * 60), " ".repeat(7)),
  395. n if n < 4 * 24 * 60 * 60 => {
  396. format!("{} days ago{}", n / (24 * 60 * 60), " ".repeat(9))
  397. }
  398. _ => envelope.datetime().format("%Y-%m-%d %H:%M:%S").to_string(),
  399. }
  400. }
  401. }
  402. impl Component for ThreadListing {
  403. fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
  404. if !self.unfocused {
  405. if !self.is_dirty() {
  406. return;
  407. }
  408. self.dirty = false;
  409. /* Draw the entire list */
  410. self.draw_list(grid, area, context);
  411. } else {
  412. self.cursor_pos = self.new_cursor_pos;
  413. let upper_left = upper_left!(area);
  414. let bottom_right = bottom_right!(area);
  415. if self.length == 0 && self.dirty {
  416. clear_area(grid, area);
  417. context.dirty_areas.push_back(area);
  418. }
  419. /* Render the mail body in a pager, basically copy what HSplit does */
  420. let total_rows = get_y(bottom_right) - get_y(upper_left);
  421. let pager_ratio = context.runtime_settings.pager.pager_ratio;
  422. let bottom_entity_rows = (pager_ratio * total_rows) / 100;
  423. if bottom_entity_rows > total_rows {
  424. clear_area(grid, area);
  425. context.dirty_areas.push_back(area);
  426. return;
  427. }
  428. let idx = self.cursor_pos.2;
  429. let has_message: bool = self.locations[self.new_cursor_pos.2] > 0;
  430. if !has_message {
  431. self.dirty = false;
  432. /* Draw the entire list */
  433. return self.draw_list(grid, area, context);
  434. }
  435. /* Mark message as read */
  436. let must_highlight = {
  437. if self.length == 0 {
  438. false
  439. } else {
  440. let account = &mut context.accounts[self.cursor_pos.0];
  441. let (hash, is_seen) = {
  442. let envelope: &Envelope =
  443. &account.get_env(&self.locations[self.cursor_pos.2]);
  444. (envelope.hash(), envelope.is_seen())
  445. };
  446. if !is_seen {
  447. let op = account.operation(hash);
  448. let envelope: &mut Envelope =
  449. account.get_env_mut(&self.locations[self.cursor_pos.2]);
  450. envelope.set_seen(op).unwrap();
  451. true
  452. } else {
  453. false
  454. }
  455. }
  456. };
  457. if must_highlight {
  458. self.highlight_line_self(idx, context);
  459. }
  460. let mid = get_y(upper_left) + total_rows - bottom_entity_rows;
  461. self.draw_list(
  462. grid,
  463. (
  464. upper_left,
  465. (get_x(bottom_right), get_y(upper_left) + mid - 1),
  466. ),
  467. context,
  468. );
  469. if self.length == 0 {
  470. self.dirty = false;
  471. return;
  472. }
  473. {
  474. /* TODO: Move the box drawing business in separate functions */
  475. if get_x(upper_left) > 0 && grid[(get_x(upper_left) - 1, mid)].ch() == VERT_BOUNDARY
  476. {
  477. grid[(get_x(upper_left) - 1, mid)].set_ch(LIGHT_VERTICAL_AND_RIGHT);
  478. }
  479. for i in get_x(upper_left)..=get_x(bottom_right) {
  480. grid[(i, mid)].set_ch(HORZ_BOUNDARY);
  481. }
  482. context
  483. .dirty_areas
  484. .push_back((set_y(upper_left, mid), set_y(bottom_right, mid)));
  485. }
  486. // TODO: Make headers view configurable
  487. if !self.dirty {
  488. if let Some(v) = self.view.as_mut() {
  489. v.draw(grid, (set_y(upper_left, mid + 1), bottom_right), context);
  490. }
  491. return;
  492. }
  493. let coordinates = (
  494. self.cursor_pos.0,
  495. self.cursor_pos.1,
  496. self.locations[self.cursor_pos.2],
  497. );
  498. if let Some(ref mut v) = self.view {
  499. v.update(coordinates);
  500. } else {
  501. self.view = Some(MailView::new(coordinates, None, None));
  502. }
  503. self.view.as_mut().unwrap().draw(
  504. grid,
  505. (set_y(upper_left, mid + 1), bottom_right),
  506. context,
  507. );
  508. self.dirty = false;
  509. }
  510. }
  511. fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
  512. if let Some(ref mut v) = self.view {
  513. if v.process_event(event, context) {
  514. return true;
  515. }
  516. }
  517. match *event {
  518. UIEvent::Input(Key::Up) => {
  519. if self.cursor_pos.2 > 0 {
  520. self.new_cursor_pos.2 -= 1;
  521. self.dirty = true;
  522. }
  523. return true;
  524. }
  525. UIEvent::Input(Key::Down) => {
  526. if self.length > 0 && self.new_cursor_pos.2 < self.length - 1 {
  527. self.new_cursor_pos.2 += 1;
  528. self.dirty = true;
  529. }
  530. return true;
  531. }
  532. UIEvent::Input(ref key) if *key == Key::PageUp => {
  533. self.movement = Some(PageMovement::PageUp);
  534. self.set_dirty();
  535. }
  536. UIEvent::Input(ref key) if *key == Key::PageDown => {
  537. self.movement = Some(PageMovement::PageDown);
  538. self.set_dirty();
  539. }
  540. UIEvent::Input(ref key) if *key == Key::Home => {
  541. self.movement = Some(PageMovement::Home);
  542. self.set_dirty();
  543. }
  544. UIEvent::Input(ref key) if *key == Key::End => {
  545. self.movement = Some(PageMovement::End);
  546. self.set_dirty();
  547. }
  548. UIEvent::Input(Key::Char('\n')) if !self.unfocused => {
  549. self.unfocused = true;
  550. self.dirty = true;
  551. return true;
  552. }
  553. UIEvent::Input(Key::Char('i')) if self.unfocused => {
  554. self.unfocused = false;
  555. self.dirty = true;
  556. self.view = None;
  557. return true;
  558. }
  559. UIEvent::RefreshMailbox(_) => {
  560. self.dirty = true;
  561. self.view = None;
  562. }
  563. UIEvent::MailboxUpdate((ref idxa, ref idxf))
  564. if (*idxa, *idxf)
  565. == (
  566. self.new_cursor_pos.0,
  567. context.accounts[self.new_cursor_pos.0].folders_order
  568. [self.new_cursor_pos.1],
  569. ) =>
  570. {
  571. self.refresh_mailbox(context);
  572. self.set_dirty();
  573. }
  574. UIEvent::StartupCheck(ref f)
  575. if *f
  576. == context.accounts[self.new_cursor_pos.0].folders_order
  577. [self.new_cursor_pos.1] =>
  578. {
  579. self.refresh_mailbox(context);
  580. self.set_dirty();
  581. }
  582. UIEvent::ChangeMode(UIMode::Normal) => {
  583. self.dirty = true;
  584. }
  585. UIEvent::Resize => {
  586. self.dirty = true;
  587. }
  588. UIEvent::Action(ref action) => match action {
  589. Action::ViewMailbox(idx_m) => {
  590. self.new_cursor_pos.1 = *idx_m;
  591. self.dirty = true;
  592. self.refresh_mailbox(context);
  593. return true;
  594. }
  595. Action::SubSort(field, order) => {
  596. debug!("SubSort {:?} , {:?}", field, order);
  597. self.subsort = (*field, *order);
  598. self.dirty = true;
  599. self.refresh_mailbox(context);
  600. return true;
  601. }
  602. Action::Sort(field, order) => {
  603. debug!("Sort {:?} , {:?}", field, order);
  604. self.sort = (*field, *order);
  605. self.dirty = true;
  606. self.refresh_mailbox(context);
  607. return true;
  608. }
  609. _ => {}
  610. },
  611. _ => {}
  612. }
  613. false
  614. }
  615. fn is_dirty(&self) -> bool {
  616. self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
  617. }
  618. fn set_dirty(&mut self) {
  619. if let Some(p) = self.view.as_mut() {
  620. p.set_dirty();
  621. };
  622. self.dirty = true;
  623. }
  624. fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
  625. self.view
  626. .as_ref()
  627. .map(|p| p.get_shortcuts(context))
  628. .unwrap_or_default()
  629. }
  630. fn id(&self) -> ComponentId {
  631. self.id
  632. }
  633. fn set_id(&mut self, id: ComponentId) {
  634. self.id = id;
  635. }
  636. }