web: move legacy (pre-graphql) rendering to separate mod
This commit is contained in:
parent
e6f20e538a
commit
28d5562491
@ -6,8 +6,8 @@ use crate::{
|
|||||||
api::urls,
|
api::urls,
|
||||||
state::{Context, Model, Msg, Tag},
|
state::{Context, Model, Msg, Tag},
|
||||||
view::{
|
view::{
|
||||||
view_header, view_search_results, view_search_results_legacy, view_thread,
|
legacy::{view_search_results_legacy, view_thread_legacy},
|
||||||
view_thread_legacy,
|
view_header, view_search_results, view_thread,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
219
web/src/view/legacy.rs
Normal file
219
web/src/view/legacy.rs
Normal file
@ -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<Msg> {
|
||||||
|
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<Msg> {
|
||||||
|
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<Msg> {
|
||||||
|
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("<No subject>".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<Msg> {
|
||||||
|
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!["<no body>"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
children.iter().map(view_message)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_body(body: &[Part]) -> Node<Msg> {
|
||||||
|
div![body.iter().map(view_part)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_text_plain(content: &Option<Content>) -> Node<Msg> {
|
||||||
|
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<Msg> {
|
||||||
|
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<String> {
|
||||||
|
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
|
||||||
|
}
|
||||||
@ -5,8 +5,9 @@ use crate::{
|
|||||||
graphql::front_page_query::*,
|
graphql::front_page_query::*,
|
||||||
state::{Context, Model, Msg},
|
state::{Context, Model, Msg},
|
||||||
view::{
|
view::{
|
||||||
human_age, pretty_authors, set_title, tags_chiclet, view_header, view_search_pager,
|
human_age,
|
||||||
view_search_pager_legacy, view_thread, view_thread_legacy,
|
legacy::{view_search_pager_legacy, view_thread_legacy},
|
||||||
|
pretty_authors, set_title, tags_chiclet, view_header, view_search_pager, view_thread,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -6,12 +6,10 @@ use std::{
|
|||||||
use chrono::{DateTime, Datelike, Duration, Local, Utc};
|
use chrono::{DateTime, Datelike, Duration, Local, Utc};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use log::info;
|
use log::info;
|
||||||
use notmuch::{Content, Part, ThreadNode, ThreadSet};
|
|
||||||
use seed::{prelude::*, *};
|
use seed::{prelude::*, *};
|
||||||
use wasm_timer::Instant;
|
use wasm_timer::Instant;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api,
|
|
||||||
api::urls,
|
api::urls,
|
||||||
consts::{SEARCH_RESULTS_PER_PAGE, USE_GRAPHQL},
|
consts::{SEARCH_RESULTS_PER_PAGE, USE_GRAPHQL},
|
||||||
graphql::{front_page_query::*, show_thread_query::*},
|
graphql::{front_page_query::*, show_thread_query::*},
|
||||||
@ -19,118 +17,9 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
mod desktop;
|
mod desktop;
|
||||||
|
mod legacy;
|
||||||
mod mobile;
|
mod mobile;
|
||||||
|
|
||||||
fn view_message(thread: &ThreadNode) -> Node<Msg> {
|
|
||||||
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!["<no body>"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
children.iter().map(view_message)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_body(body: &[Part]) -> Node<Msg> {
|
|
||||||
div![body.iter().map(view_part)]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_text_plain(content: &Option<Content>) -> Node<Msg> {
|
|
||||||
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<Msg> {
|
|
||||||
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<String> {
|
|
||||||
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) {
|
fn set_title(title: &str) {
|
||||||
seed::document().set_title(&format!("lb: {}", title));
|
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<Msg> {
|
|
||||||
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<Msg> {
|
fn view_search_pager(count: usize, pager: &FrontPageQuerySearchPageInfo) -> Node<Msg> {
|
||||||
let start = pager
|
let start = pager
|
||||||
.start_cursor
|
.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<Msg> {
|
|
||||||
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 {
|
trait Email {
|
||||||
fn name(&self) -> Option<&str>;
|
fn name(&self) -> Option<&str>;
|
||||||
fn addr(&self) -> Option<&str>;
|
fn addr(&self) -> Option<&str>;
|
||||||
@ -495,24 +306,6 @@ fn view_thread(thread: &ShowThreadQueryThread) -> Node<Msg> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_thread_legacy(thread_set: &ThreadSet) -> Node<Msg> {
|
|
||||||
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("<No subject>".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<Msg> {
|
fn view_header(query: &str, refresh_request: &RefreshingState) -> Node<Msg> {
|
||||||
let is_loading = refresh_request == &RefreshingState::Loading;
|
let is_loading = refresh_request == &RefreshingState::Loading;
|
||||||
let is_error = if let RefreshingState::Error(err) = refresh_request {
|
let is_error = if let RefreshingState::Error(err) = refresh_request {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user