diff --git a/web/src/view/desktop.rs b/web/src/view/desktop.rs index bb0d4ed..0dcc9f4 100644 --- a/web/src/view/desktop.rs +++ b/web/src/view/desktop.rs @@ -6,8 +6,8 @@ use crate::{ api::urls, state::{Context, Model, Msg, Tag}, view::{ - view_header, view_search_results, view_search_results_legacy, view_thread, - view_thread_legacy, + legacy::{view_search_results_legacy, view_thread_legacy}, + view_header, view_search_results, view_thread, }, }; diff --git a/web/src/view/legacy.rs b/web/src/view/legacy.rs new file mode 100644 index 0000000..311772c --- /dev/null +++ b/web/src/view/legacy.rs @@ -0,0 +1,219 @@ +use notmuch::{Content, Part, ThreadNode, ThreadSet}; +use seed::{prelude::*, *}; + +use crate::{ + api, + api::urls, + consts::SEARCH_RESULTS_PER_PAGE, + state::Msg, + view::{human_age, pretty_authors, set_title, tags_chiclet}, +}; + +pub(super) fn view_search_results_legacy( + query: &str, + search_results: &shared::SearchResult, +) -> Node { + if query.is_empty() { + set_title("all mail"); + } else { + set_title(query); + } + let summaries = &search_results.summary.0; + let rows = summaries.iter().map(|r| { + let tid = r.thread.clone(); + let datetime = human_age(r.timestamp as i64); + tr![ + td![ + C!["from"], + pretty_authors(&r.authors), + IF!(r.total>1 => small![" ", r.total.to_string()]), + ], + td![ + C!["subject"], + tags_chiclet(&r.tags, false), + " ", + a![ + C!["has-text-light"], + attrs! { + At::Href => urls::thread(&tid) + }, + &r.subject, + ] + ], + td![C!["date"], datetime] + ] + }); + let first = search_results.page * search_results.results_per_page; + div![ + view_search_pager_legacy(first, summaries.len(), search_results.total), + table![ + C![ + "table", + "index", + "is-fullwidth", + "is-hoverable", + "is-narrow", + "is-striped", + ], + thead![tr![ + th![C!["from"], "From"], + th![C!["subject"], "Subject"], + th![C!["date"], "Date"] + ]], + tbody![rows] + ], + view_search_pager_legacy(first, summaries.len(), search_results.total) + ] +} + +pub(super) fn view_search_pager_legacy(start: usize, count: usize, total: usize) -> Node { + let is_first = start <= 0; + let is_last = (start + SEARCH_RESULTS_PER_PAGE) >= total; + nav![ + C!["pagination"], + a![ + C!["pagination-previous", "button",], + IF!(is_first => attrs!{ At::Disabled=>true }), + "<", + ev(Ev::Click, |_| Msg::PreviousPage) + ], + a![ + C!["pagination-next", "button", IF!(is_last => "is-static")], + IF!(is_last => attrs!{ At::Disabled=>true }), + ">", + ev(Ev::Click, |_| Msg::NextPage) + ], + ul![ + C!["pagination-list"], + li![format!("{} - {} of {}", start, start + count, total)], + ], + ] +} + +pub(super) fn view_thread_legacy(thread_set: &ThreadSet) -> Node { + assert_eq!(thread_set.0.len(), 1); + let thread = &thread_set.0[0]; + assert_eq!(thread.0.len(), 1); + let thread_node = &thread.0[0]; + let subject = first_subject(&thread_node).unwrap_or("".to_string()); + set_title(&subject); + div![ + C!["container"], + h1![C!["title"], subject], + view_message(&thread_node), + a![ + attrs! {At::Href=>api::original(&thread_node.0.as_ref().expect("message missing").id)}, + "Original" + ], + ] +} +fn view_message(thread: &ThreadNode) -> Node { + let message = thread.0.as_ref().expect("ThreadNode missing Message"); + let children = &thread.1; + div![ + C!["message"], + /* TODO(wathiede): collect all the tags and show them here. */ + /* TODO(wathiede): collect all the attachments from all the subparts */ + div![C!["header"], "From: ", &message.headers.from], + div![C!["header"], "Date: ", &message.headers.date], + div![C!["header"], "To: ", &message.headers.to], + div![ + C!["body"], + match &message.body { + Some(body) => view_body(body.as_slice()), + None => div![""], + }, + ], + children.iter().map(view_message) + ] +} + +fn view_body(body: &[Part]) -> Node { + div![body.iter().map(view_part)] +} + +fn view_text_plain(content: &Option) -> Node { + match &content { + Some(Content::String(content)) => p![C!["view-part-text-plain"], content], + _ => div![ + C!["error"], + format!("Unhandled content enum for text/plain"), + ], + } +} + +fn view_part(part: &Part) -> Node { + match part.content_type.as_str() { + "text/plain" => view_text_plain(&part.content), + "text/html" => { + if let Some(Content::String(html)) = &part.content { + let inliner = css_inline::CSSInliner::options() + .load_remote_stylesheets(false) + .remove_style_tags(true) + .build(); + let inlined = inliner.inline(html).expect("failed to inline CSS"); + + return div![C!["view-part-text-html"], raw![&inlined]]; + } else { + div![ + C!["error"], + format!("Unhandled content enum for multipart/mixed"), + ] + } + } + + // https://en.wikipedia.org/wiki/MIME#alternative + // RFC1341 states: In general, user agents that compose multipart/alternative entities + // should place the body parts in increasing order of preference, that is, with the + // preferred format last. + "multipart/alternative" => { + if let Some(Content::Multipart(parts)) = &part.content { + for part in parts.iter().rev() { + if part.content_type == "text/html" { + if let Some(Content::String(html)) = &part.content { + let inliner = css_inline::CSSInliner::options() + .load_remote_stylesheets(false) + .remove_style_tags(true) + .build(); + let inlined = inliner.inline(html).expect("failed to inline CSS"); + return div![Node::from_html(None, &inlined)]; + } + } + if part.content_type == "text/plain" { + return view_text_plain(&part.content); + } + } + div!["No known multipart/alternative parts"] + } else { + div![ + C!["error"], + format!("multipart/alternative with non-multipart content"), + ] + } + } + "multipart/mixed" => match &part.content { + Some(Content::Multipart(parts)) => div![parts.iter().map(view_part)], + _ => div![ + C!["error"], + format!("Unhandled content enum for multipart/mixed"), + ], + }, + _ => div![ + C!["error"], + format!("Unhandled content type: {}", part.content_type) + ], + } +} + +fn first_subject(thread: &ThreadNode) -> Option { + if let Some(msg) = &thread.0 { + return Some(msg.headers.subject.clone()); + } else { + for tn in &thread.1 { + if let Some(s) = first_subject(&tn) { + return Some(s); + } + } + } + None +} diff --git a/web/src/view/mobile.rs b/web/src/view/mobile.rs index 9753bc9..487f31e 100644 --- a/web/src/view/mobile.rs +++ b/web/src/view/mobile.rs @@ -5,8 +5,9 @@ use crate::{ graphql::front_page_query::*, state::{Context, Model, Msg}, view::{ - human_age, pretty_authors, set_title, tags_chiclet, view_header, view_search_pager, - view_search_pager_legacy, view_thread, view_thread_legacy, + human_age, + legacy::{view_search_pager_legacy, view_thread_legacy}, + pretty_authors, set_title, tags_chiclet, view_header, view_search_pager, view_thread, }, }; diff --git a/web/src/view/mod.rs b/web/src/view/mod.rs index 371101a..d9ee99b 100644 --- a/web/src/view/mod.rs +++ b/web/src/view/mod.rs @@ -6,12 +6,10 @@ use std::{ use chrono::{DateTime, Datelike, Duration, Local, Utc}; use itertools::Itertools; use log::info; -use notmuch::{Content, Part, ThreadNode, ThreadSet}; use seed::{prelude::*, *}; use wasm_timer::Instant; use crate::{ - api, api::urls, consts::{SEARCH_RESULTS_PER_PAGE, USE_GRAPHQL}, graphql::{front_page_query::*, show_thread_query::*}, @@ -19,118 +17,9 @@ use crate::{ }; mod desktop; +mod legacy; mod mobile; -fn view_message(thread: &ThreadNode) -> Node { - let message = thread.0.as_ref().expect("ThreadNode missing Message"); - let children = &thread.1; - div![ - C!["message"], - /* TODO(wathiede): collect all the tags and show them here. */ - /* TODO(wathiede): collect all the attachments from all the subparts */ - div![C!["header"], "From: ", &message.headers.from], - div![C!["header"], "Date: ", &message.headers.date], - div![C!["header"], "To: ", &message.headers.to], - div![ - C!["body"], - match &message.body { - Some(body) => view_body(body.as_slice()), - None => div![""], - }, - ], - children.iter().map(view_message) - ] -} - -fn view_body(body: &[Part]) -> Node { - div![body.iter().map(view_part)] -} - -fn view_text_plain(content: &Option) -> Node { - match &content { - Some(Content::String(content)) => p![C!["view-part-text-plain"], content], - _ => div![ - C!["error"], - format!("Unhandled content enum for text/plain"), - ], - } -} - -fn view_part(part: &Part) -> Node { - match part.content_type.as_str() { - "text/plain" => view_text_plain(&part.content), - "text/html" => { - if let Some(Content::String(html)) = &part.content { - let inliner = css_inline::CSSInliner::options() - .load_remote_stylesheets(false) - .remove_style_tags(true) - .build(); - let inlined = inliner.inline(html).expect("failed to inline CSS"); - - return div![C!["view-part-text-html"], raw![&inlined]]; - } else { - div![ - C!["error"], - format!("Unhandled content enum for multipart/mixed"), - ] - } - } - - // https://en.wikipedia.org/wiki/MIME#alternative - // RFC1341 states: In general, user agents that compose multipart/alternative entities - // should place the body parts in increasing order of preference, that is, with the - // preferred format last. - "multipart/alternative" => { - if let Some(Content::Multipart(parts)) = &part.content { - for part in parts.iter().rev() { - if part.content_type == "text/html" { - if let Some(Content::String(html)) = &part.content { - let inliner = css_inline::CSSInliner::options() - .load_remote_stylesheets(false) - .remove_style_tags(true) - .build(); - let inlined = inliner.inline(html).expect("failed to inline CSS"); - return div![Node::from_html(None, &inlined)]; - } - } - if part.content_type == "text/plain" { - return view_text_plain(&part.content); - } - } - div!["No known multipart/alternative parts"] - } else { - div![ - C!["error"], - format!("multipart/alternative with non-multipart content"), - ] - } - } - "multipart/mixed" => match &part.content { - Some(Content::Multipart(parts)) => div![parts.iter().map(view_part)], - _ => div![ - C!["error"], - format!("Unhandled content enum for multipart/mixed"), - ], - }, - _ => div![ - C!["error"], - format!("Unhandled content type: {}", part.content_type) - ], - } -} - -fn first_subject(thread: &ThreadNode) -> Option { - if let Some(msg) = &thread.0 { - return Some(msg.headers.subject.clone()); - } else { - for tn in &thread.1 { - if let Some(s) = first_subject(&tn) { - return Some(s); - } - } - } - None -} fn set_title(title: &str) { seed::document().set_title(&format!("lb: {}", title)); } @@ -272,60 +161,6 @@ fn view_search_results( ] } -fn view_search_results_legacy(query: &str, search_results: &shared::SearchResult) -> Node { - if query.is_empty() { - set_title("all mail"); - } else { - set_title(query); - } - let summaries = &search_results.summary.0; - let rows = summaries.iter().map(|r| { - let tid = r.thread.clone(); - let datetime = human_age(r.timestamp as i64); - tr![ - td![ - C!["from"], - pretty_authors(&r.authors), - IF!(r.total>1 => small![" ", r.total.to_string()]), - ], - td![ - C!["subject"], - tags_chiclet(&r.tags, false), - " ", - a![ - C!["has-text-light"], - attrs! { - At::Href => urls::thread(&tid) - }, - &r.subject, - ] - ], - td![C!["date"], datetime] - ] - }); - let first = search_results.page * search_results.results_per_page; - div![ - view_search_pager_legacy(first, summaries.len(), search_results.total), - table![ - C![ - "table", - "index", - "is-fullwidth", - "is-hoverable", - "is-narrow", - "is-striped", - ], - thead![tr![ - th![C!["from"], "From"], - th![C!["subject"], "Subject"], - th![C!["date"], "Date"] - ]], - tbody![rows] - ], - view_search_pager_legacy(first, summaries.len(), search_results.total) - ] -} - fn view_search_pager(count: usize, pager: &FrontPageQuerySearchPageInfo) -> Node { let start = pager .start_cursor @@ -366,30 +201,6 @@ fn view_search_pager(count: usize, pager: &FrontPageQuerySearchPageInfo) -> Node ] } -fn view_search_pager_legacy(start: usize, count: usize, total: usize) -> Node { - let is_first = start <= 0; - let is_last = (start + SEARCH_RESULTS_PER_PAGE) >= total; - nav![ - C!["pagination"], - a![ - C!["pagination-previous", "button",], - IF!(is_first => attrs!{ At::Disabled=>true }), - "<", - ev(Ev::Click, |_| Msg::PreviousPage) - ], - a![ - C!["pagination-next", "button", IF!(is_last => "is-static")], - IF!(is_last => attrs!{ At::Disabled=>true }), - ">", - ev(Ev::Click, |_| Msg::NextPage) - ], - ul![ - C!["pagination-list"], - li![format!("{} - {} of {}", start, start + count, total)], - ], - ] -} - trait Email { fn name(&self) -> Option<&str>; fn addr(&self) -> Option<&str>; @@ -495,24 +306,6 @@ fn view_thread(thread: &ShowThreadQueryThread) -> Node { ] } -fn view_thread_legacy(thread_set: &ThreadSet) -> Node { - assert_eq!(thread_set.0.len(), 1); - let thread = &thread_set.0[0]; - assert_eq!(thread.0.len(), 1); - let thread_node = &thread.0[0]; - let subject = first_subject(&thread_node).unwrap_or("".to_string()); - set_title(&subject); - div![ - C!["container"], - h1![C!["title"], subject], - view_message(&thread_node), - a![ - attrs! {At::Href=>api::original(&thread_node.0.as_ref().expect("message missing").id)}, - "Original" - ], - ] -} - fn view_header(query: &str, refresh_request: &RefreshingState) -> Node { let is_loading = refresh_request == &RefreshingState::Loading; let is_error = if let RefreshingState::Error(err) = refresh_request {