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"
serde = { version = "1.0.147", features = ["derive"] }
log = "0.4.17"
tokio = "1.26.0"
[dependencies.rocket_contrib]
version = "0.4.11"

View File

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

View File

@ -45,6 +45,25 @@ iframe {
.tag {
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>
</head>

View File

@ -48,6 +48,7 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
Model {
context: Context::None,
query,
refreshing_state: RefreshingState::None,
}
}
@ -64,6 +65,14 @@ enum Context {
struct Model {
query: String,
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.
enum Msg {
Noop,
Refresh,
RefreshStart,
RefreshDone(Option<FetchError>),
SearchRequest(String),
SearchResult(fetch::Result<SearchSummary>),
ShowRequest(String),
@ -87,10 +97,16 @@ enum Msg {
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::Noop => {}
Msg::Refresh => {
orders.skip().perform_cmd(async move {
refresh_request().await;
});
Msg::RefreshStart => {
model.refreshing_state = RefreshingState::Loading;
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) => {
@ -264,7 +280,9 @@ fn view_part(part: &Part) -> Node<Msg> {
"text/plain" => view_text_plain(&part.content),
"text/html" => {
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 {
div![
C!["error"],
@ -282,7 +300,12 @@ fn view_part(part: &Part) -> Node<Msg> {
for part in parts.iter().rev() {
if part.content_type == "text/html" {
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" {
@ -451,7 +474,8 @@ fn view_thread(thread_set: &ThreadSet) -> Node<Msg> {
"Original"
],
view_message(&thread_node),
pre![
div![
C!["debug"],
"Add zippy for debug dump",
view_debug_thread_set(thread_set)
] /* 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();
nav![
C!["navbar"],
@ -493,9 +524,14 @@ fn view_header(query: &str) -> Node<Msg> {
div![
C!["navbar-start"],
a![
C!["navbar-item", "button"],
span![i![C!["fa-solid", "fa-arrow-rotate-right"]]],
ev(Ev::Click, |_| Msg::Refresh),
C!["navbar-item", "button", IF![is_error => "is-danger"]],
span![i![C![
"fa-solid",
"fa-arrow-rotate-right",
"refresh",
IF![is_loading => "loading"],
]]],
ev(Ev::Click, |_| Msg::RefreshStart),
],
a![
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),
};
div![
view_header(&model.query),
view_header(&model.query, &model.refreshing_state),
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),
};
div![
view_header(&model.query),
view_header(&model.query, &model.refreshing_state),
section![C!["section"], div![C!["container"], content],]
]
}
// `view` describes what to display.
fn view(model: &Model) -> Node<Msg> {
info!("refreshing {:?}", model.refreshing_state);
let is_mobile = seed::window()
.match_media("(max-width: 768px)")
.expect("failed media query")