Inline CSS on message view, and do error handling on refresh.

This commit is contained in:
Bill Thiede 2023-03-04 11:42:19 -08:00
parent 0b4cfadf88
commit 3a47385be1
5 changed files with 723 additions and 54 deletions

691
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,7 @@ serde_json = "1.0.87"
thiserror = "1.0.37" thiserror = "1.0.37"
serde = { version = "1.0.147", features = ["derive"] } serde = { version = "1.0.147", features = ["derive"] }
log = "0.4.17" log = "0.4.17"
tokio = "1.26.0"
[dependencies.rocket_contrib] [dependencies.rocket_contrib]
version = "0.4.11" version = "0.4.11"

View File

@ -25,6 +25,7 @@ notmuch = {path = "../notmuch"}
itertools = "0.10.5" itertools = "0.10.5"
serde_json = { version = "1.0.93", features = ["unbounded_depth"] } serde_json = { version = "1.0.93", features = ["unbounded_depth"] }
wasm-timer = "0.2.5" wasm-timer = "0.2.5"
css-inline = "0.8.5"
[package.metadata.wasm-pack.profile.release] [package.metadata.wasm-pack.profile.release]
wasm-opt = ['-Os'] wasm-opt = ['-Os']

View File

@ -45,6 +45,25 @@ iframe {
.tag { .tag {
margin-right: 2px; margin-right: 2px;
} }
.debug ul {
padding-left: 2em;
}
.debug li {
}
.loading {
animation-name: spin;
animation-duration: 1000ms;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
@keyframes spin {
from {
transform:rotate(0deg);
}
to {
transform:rotate(360deg);
}
}
</style> </style>
</head> </head>

View File

@ -48,6 +48,7 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
Model { Model {
context: Context::None, context: Context::None,
query, query,
refreshing_state: RefreshingState::None,
} }
} }
@ -64,6 +65,14 @@ enum Context {
struct Model { struct Model {
query: String, query: String,
context: Context, context: Context,
refreshing_state: RefreshingState,
}
#[derive(Debug, PartialEq)]
enum RefreshingState {
None,
Loading,
Error(String),
} }
// ------ ------ // ------ ------
@ -74,7 +83,8 @@ struct Model {
// `Msg` describes the different events you can modify state with. // `Msg` describes the different events you can modify state with.
enum Msg { enum Msg {
Noop, Noop,
Refresh, RefreshStart,
RefreshDone(Option<FetchError>),
SearchRequest(String), SearchRequest(String),
SearchResult(fetch::Result<SearchSummary>), SearchResult(fetch::Result<SearchSummary>),
ShowRequest(String), ShowRequest(String),
@ -87,10 +97,16 @@ enum Msg {
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) { fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg { match msg {
Msg::Noop => {} Msg::Noop => {}
Msg::Refresh => { Msg::RefreshStart => {
orders.skip().perform_cmd(async move { model.refreshing_state = RefreshingState::Loading;
refresh_request().await; orders.perform_cmd(async move { Msg::RefreshDone(refresh_request().await.err()) });
}); }
Msg::RefreshDone(err) => {
model.refreshing_state = if let Some(err) = err {
RefreshingState::Error(format!("{:?}", err))
} else {
RefreshingState::None
};
} }
Msg::SearchRequest(query) => { Msg::SearchRequest(query) => {
@ -264,7 +280,9 @@ fn view_part(part: &Part) -> Node<Msg> {
"text/plain" => view_text_plain(&part.content), "text/plain" => view_text_plain(&part.content),
"text/html" => { "text/html" => {
if let Some(Content::String(html)) = &part.content { if let Some(Content::String(html)) = &part.content {
return div![C!["view-part-text-html"], div!["TEST"], raw![&html]]; let inlined = css_inline::inline(html).expect("failed to inline CSS");
return div![C!["view-part-text-html"], div!["TEST"], raw![&inlined]];
} else { } else {
div![ div![
C!["error"], C!["error"],
@ -282,7 +300,12 @@ fn view_part(part: &Part) -> Node<Msg> {
for part in parts.iter().rev() { for part in parts.iter().rev() {
if part.content_type == "text/html" { if part.content_type == "text/html" {
if let Some(Content::String(html)) = &part.content { if let Some(Content::String(html)) = &part.content {
return div![Node::from_html(None, &html)]; 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" { if part.content_type == "text/plain" {
@ -451,7 +474,8 @@ fn view_thread(thread_set: &ThreadSet) -> Node<Msg> {
"Original" "Original"
], ],
view_message(&thread_node), view_message(&thread_node),
pre![ div![
C!["debug"],
"Add zippy for debug dump", "Add zippy for debug dump",
view_debug_thread_set(thread_set) view_debug_thread_set(thread_set)
] /* pre![format!("Thread: {:#?}", thread_set).replace(" ", " ")] */ ] /* pre![format!("Thread: {:#?}", thread_set).replace(" ", " ")] */
@ -485,7 +509,14 @@ fn view_debug_thread_node(thread_node: &ThreadNode) -> Node<Msg> {
] ]
} }
fn view_header(query: &str) -> Node<Msg> { fn view_header(query: &str, refresh_request: &RefreshingState) -> Node<Msg> {
let is_loading = refresh_request == &RefreshingState::Loading;
let is_error = if let RefreshingState::Error(err) = refresh_request {
error!("Failed to refresh: {err:?}");
true
} else {
false
};
let query = query.to_string(); let query = query.to_string();
nav![ nav![
C!["navbar"], C!["navbar"],
@ -493,9 +524,14 @@ fn view_header(query: &str) -> Node<Msg> {
div![ div![
C!["navbar-start"], C!["navbar-start"],
a![ a![
C!["navbar-item", "button"], C!["navbar-item", "button", IF![is_error => "is-danger"]],
span![i![C!["fa-solid", "fa-arrow-rotate-right"]]], span![i![C![
ev(Ev::Click, |_| Msg::Refresh), "fa-solid",
"fa-arrow-rotate-right",
"refresh",
IF![is_loading => "loading"],
]]],
ev(Ev::Click, |_| Msg::RefreshStart),
], ],
a![ a![
C!["navbar-item", "button"], C!["navbar-item", "button"],
@ -543,7 +579,7 @@ fn view_desktop(model: &Model) -> Node<Msg> {
Context::Search(search_results) => view_search_results(&model.query, search_results), Context::Search(search_results) => view_search_results(&model.query, search_results),
}; };
div![ div![
view_header(&model.query), view_header(&model.query, &model.refreshing_state),
section![C!["section"], div![C!["container"], content],] section![C!["section"], div![C!["container"], content],]
] ]
} }
@ -555,13 +591,14 @@ fn view_mobile(model: &Model) -> Node<Msg> {
Context::Search(search_results) => view_mobile_search_results(&model.query, search_results), Context::Search(search_results) => view_mobile_search_results(&model.query, search_results),
}; };
div![ div![
view_header(&model.query), view_header(&model.query, &model.refreshing_state),
section![C!["section"], div![C!["container"], content],] section![C!["section"], div![C!["container"], content],]
] ]
} }
// `view` describes what to display. // `view` describes what to display.
fn view(model: &Model) -> Node<Msg> { fn view(model: &Model) -> Node<Msg> {
info!("refreshing {:?}", model.refreshing_state);
let is_mobile = seed::window() let is_mobile = seed::window()
.match_media("(max-width: 768px)") .match_media("(max-width: 768px)")
.expect("failed media query") .expect("failed media query")