Add refresh button and mobile view.

This commit is contained in:
Bill Thiede 2023-03-04 09:00:18 -08:00
parent c33f901f48
commit 0b4cfadf88
4 changed files with 111 additions and 14 deletions

1
Cargo.lock generated
View File

@ -983,6 +983,7 @@ dependencies = [
"serde_json", "serde_json",
"wasm-bindgen-test", "wasm-bindgen-test",
"wasm-timer", "wasm-timer",
"web-sys",
] ]
[[package]] [[package]]

View File

@ -18,6 +18,10 @@ fn hello() -> &'static str {
"Hello, world!" "Hello, world!"
} }
#[get("/refresh")]
async fn refresh(nm: &State<Notmuch>) -> Result<Json<String>, Debug<NotmuchError>> {
Ok(Json(String::from_utf8_lossy(&nm.new()?).to_string()))
}
#[get("/search")] #[get("/search")]
async fn search_all(nm: &State<Notmuch>) -> Result<Json<SearchSummary>, Debug<NotmuchError>> { async fn search_all(nm: &State<Notmuch>) -> Result<Json<SearchSummary>, Debug<NotmuchError>> {
search(nm, "*").await search(nm, "*").await
@ -123,6 +127,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
original_part, original_part,
original, original,
hello, hello,
refresh,
search_all, search_all,
search, search,
show_pretty, show_pretty,

View File

@ -28,3 +28,10 @@ wasm-timer = "0.2.5"
[package.metadata.wasm-pack.profile.release] [package.metadata.wasm-pack.profile.release]
wasm-opt = ['-Os'] wasm-opt = ['-Os']
[dependencies.web-sys]
version = "0.3.58"
features = [
"MediaQueryList",
"Window"
]

View File

@ -74,6 +74,7 @@ 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,
SearchRequest(String), SearchRequest(String),
SearchResult(fetch::Result<SearchSummary>), SearchResult(fetch::Result<SearchSummary>),
ShowRequest(String), ShowRequest(String),
@ -86,6 +87,11 @@ 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 => {
orders.skip().perform_cmd(async move {
refresh_request().await;
});
}
Msg::SearchRequest(query) => { Msg::SearchRequest(query) => {
model.query = query.clone(); model.query = query.clone();
@ -147,20 +153,35 @@ async fn search_request(query: &str) -> fetch::Result<SearchSummary> {
mod api { mod api {
const BASE_URL: &str = "/api"; const BASE_URL: &str = "/api";
pub fn refresh() -> String {
format!("{BASE_URL}/refresh")
}
pub fn search(query: &str) -> String { pub fn search(query: &str) -> String {
format!("{}/search/{}", BASE_URL, query) format!("{BASE_URL}/search/{query}")
} }
pub fn show(tid: &str) -> String { pub fn show(tid: &str) -> String {
format!("{}/show/{}", BASE_URL, tid) format!("{BASE_URL}/show/{tid}")
} }
pub fn show_pretty(tid: &str) -> String { pub fn show_pretty(tid: &str) -> String {
format!("{}/show/{}/pretty", BASE_URL, tid) format!("{BASE_URL}/show/{tid}/pretty")
} }
pub fn original(message_id: &str) -> String { pub fn original(message_id: &str) -> String {
format!("{}/original/{}", BASE_URL, message_id) format!("{BASE_URL}/original/{message_id}")
} }
} }
async fn refresh_request() -> fetch::Result<()> {
let t = Request::new(api::refresh())
.method(Method::Get)
.fetch()
.await?
.check_status()?
.text()
.await?;
info!("refresh {t}");
Ok(())
}
async fn show_request(tid: &str) -> fetch::Result<ThreadSet> { async fn show_request(tid: &str) -> fetch::Result<ThreadSet> {
let b = Request::new(api::show(tid)) let b = Request::new(api::show(tid))
.method(Method::Get) .method(Method::Get)
@ -346,6 +367,39 @@ fn pretty_authors(authors: &str) -> impl Iterator<Item = Node<Msg>> + '_ {
) )
} }
fn view_mobile_search_results(query: &str, search_results: &SearchSummary) -> Node<Msg> {
if query.is_empty() {
set_title("all mail");
} else {
set_title(query);
}
let rows = search_results.0.iter().map(|r| {
/*
let tid = r.thread.clone();
tr![
td![
C!["from"],
pretty_authors(&r.authors),
IF!(r.total>1 => small![" ", r.total.to_string()]),
],
td![C!["subject"], tags_chiclet(&r.tags), " ", &r.subject],
td![C!["date"], &r.date_relative],
ev(Ev::Click, move |_| Msg::ShowPrettyRequest(tid)),
]
*/
div![
p![C!["subject"], &r.subject],
div![
span![C!["from"], pretty_authors(&r.authors)],
span![C!["tags"], tags_chiclet(&r.tags)],
],
span![C!["date"], &r.date_relative],
hr![],
]
});
div![h1!["Search results"], rows]
}
fn view_search_results(query: &str, search_results: &SearchSummary) -> Node<Msg> { fn view_search_results(query: &str, search_results: &SearchSummary) -> Node<Msg> {
if query.is_empty() { if query.is_empty() {
set_title("all mail"); set_title("all mail");
@ -439,7 +493,12 @@ 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"],
span![i![C!["fa-solid", "fa-arrow-rotate-right"]]],
ev(Ev::Click, |_| Msg::Refresh),
],
a![
C!["navbar-item", "button"],
"Unread", "Unread",
ev(Ev::Click, |_| Msg::SearchRequest("is:unread".to_string())), ev(Ev::Click, |_| Msg::SearchRequest("is:unread".to_string())),
], ],
@ -477,10 +536,7 @@ fn view_footer(render_time_ms: u128) -> Node<Msg> {
] ]
} }
// `view` describes what to display. fn view_desktop(model: &Model) -> Node<Msg> {
fn view(model: &Model) -> Node<Msg> {
let start = Instant::now();
info!("view called");
let content = match &model.context { let content = match &model.context {
Context::None => div![h1!["Loading"]], Context::None => div![h1!["Loading"]],
Context::Thread(thread_set) => view_thread(thread_set), Context::Thread(thread_set) => view_thread(thread_set),
@ -488,11 +544,39 @@ fn view(model: &Model) -> Node<Msg> {
}; };
div![ div![
view_header(&model.query), view_header(&model.query),
section![ section![C!["section"], div![C!["container"], content],]
C!["section"],
div![C!["container"], content],
view_footer(start.elapsed().as_millis())
] ]
}
fn view_mobile(model: &Model) -> Node<Msg> {
let content = match &model.context {
Context::None => div![h1!["Loading"]],
Context::Thread(thread_set) => view_thread(thread_set),
Context::Search(search_results) => view_mobile_search_results(&model.query, search_results),
};
div![
view_header(&model.query),
section![C!["section"], div![C!["container"], content],]
]
}
// `view` describes what to display.
fn view(model: &Model) -> Node<Msg> {
let is_mobile = seed::window()
.match_media("(max-width: 768px)")
.expect("failed media query")
.map(|mql| mql.matches())
.unwrap_or(false);
let start = Instant::now();
info!("view called");
div![
if is_mobile {
view_mobile(model)
} else {
view_desktop(model)
},
view_footer(start.elapsed().as_millis())
] ]
} }