diff --git a/web/src/api.rs b/web/src/api.rs index ffaddf5..ff3837e 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -1,22 +1,10 @@ use gloo_net::{http::Request, Error}; use log::info; -use notmuch::ThreadSet; -use seed::Url; const BASE_URL: &str = "/api"; pub fn refresh() -> String { format!("{BASE_URL}/refresh") } -pub fn search(query: &str, page: usize, results_per_page: usize) -> String { - let query = Url::encode_uri_component(query); - format!("{BASE_URL}/search/{query}?page={page}&results_per_page={results_per_page}") -} -pub fn show_pretty(tid: &str) -> String { - format!("{BASE_URL}/show/{tid}/pretty") -} -pub fn original(message_id: &str) -> String { - format!("{BASE_URL}/original/{message_id}") -} pub mod urls { use seed::Url; pub fn search(query: &str, page: usize) -> Url { @@ -32,24 +20,8 @@ pub mod urls { } } -pub async fn search_request( - query: &str, - page: usize, - results_per_page: usize, -) -> Result { - Request::get(&search(query, page, results_per_page)) - .send() - .await? - .json() - .await -} - pub async fn refresh_request() -> Result<(), Error> { let t = Request::get(&refresh()).send().await?.text().await?; info!("refresh {t}"); Ok(()) } - -pub async fn show_pretty_request(tid: &str) -> Result { - Request::get(&show_pretty(tid)).send().await?.json().await -} diff --git a/web/src/consts.rs b/web/src/consts.rs index a5c60f8..f123454 100644 --- a/web/src/consts.rs +++ b/web/src/consts.rs @@ -1,2 +1 @@ pub const SEARCH_RESULTS_PER_PAGE: usize = 20; -pub const USE_GRAPHQL: bool = true; diff --git a/web/src/state.rs b/web/src/state.rs index 5ee8828..ee72256 100644 --- a/web/src/state.rs +++ b/web/src/state.rs @@ -1,13 +1,12 @@ use graphql_client::GraphQLQuery; -use log::{debug, error, info}; -use notmuch::ThreadSet; +use log::{error, info}; use seed::{app::subs, prelude::*, *}; use thiserror::Error; use crate::{ api, api::urls, - consts::{SEARCH_RESULTS_PER_PAGE, USE_GRAPHQL}, + consts::SEARCH_RESULTS_PER_PAGE, graphql, graphql::{front_page_query::*, send_graphql, show_thread_query::*}, }; @@ -43,70 +42,40 @@ fn on_url_changed(uc: subs::UrlChanged) -> Msg { ); let hpp = url.remaining_hash_path_parts(); match hpp.as_slice() { - ["t", tid] => { - if USE_GRAPHQL { - Msg::ShowThreadRequest { - thread_id: tid.to_string(), - } - } else { - Msg::ShowPrettyRequest(tid.to_string()) - } - } + ["t", tid] => Msg::ShowThreadRequest { + thread_id: tid.to_string(), + }, ["s", query] => { let query = Url::decode_uri_component(query).unwrap_or("".to_string()); - if USE_GRAPHQL { - Msg::FrontPageRequest { - query, - after: None, - before: None, - first: None, - last: None, - } - } else { - Msg::SearchRequest { - query, - page: 0, - results_per_page: SEARCH_RESULTS_PER_PAGE, - } + Msg::FrontPageRequest { + query, + after: None, + before: None, + first: None, + last: None, } } ["s", query, page] => { let query = Url::decode_uri_component(query).unwrap_or("".to_string()); let page = page[1..].parse().unwrap_or(0); - if USE_GRAPHQL { - Msg::FrontPageRequest { - query, - after: Some(page.to_string()), - before: None, - first: None, - last: None, - } - } else { - Msg::SearchRequest { - query, - page, - results_per_page: SEARCH_RESULTS_PER_PAGE, - } + Msg::FrontPageRequest { + query, + after: Some(page.to_string()), + before: None, + first: None, + last: None, } } p => { if !p.is_empty() { info!("Unhandled path '{p:?}'"); } - if USE_GRAPHQL { - Msg::FrontPageRequest { - query: "".to_string(), - after: None, - before: None, - first: None, - last: None, - } - } else { - Msg::SearchRequest { - query: "".to_string(), - page: 0, - results_per_page: SEARCH_RESULTS_PER_PAGE, - } + Msg::FrontPageRequest { + query: "".to_string(), + after: None, + before: None, + first: None, + last: None, } } } @@ -133,42 +102,8 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { } Msg::OnResize => (), - Msg::SearchRequest { - query, - page, - results_per_page, - } => { - info!("searching for '{query}' pg {page} # / pg {results_per_page}"); - model.query = query.clone(); - orders.skip().perform_cmd(async move { - Msg::SearchResult(api::search_request(&query, page, results_per_page).await) - }); - } - Msg::SearchResult(Ok(response_data)) => { - debug!("fetch ok {:#?}", response_data); - model.context = Context::Search(response_data); - } - Msg::SearchResult(Err(fetch_error)) => { - error!("fetch failed {:?}", fetch_error); - } - - Msg::ShowPrettyRequest(tid) => { - orders.skip().perform_cmd(async move { - Msg::ShowPrettyResult(api::show_pretty_request(&tid).await) - }); - } - Msg::ShowPrettyResult(Ok(response_data)) => { - debug!("fetch ok {:#?}", response_data); - model.context = Context::Thread(response_data); - } - Msg::ShowPrettyResult(Err(fetch_error)) => { - error!("fetch failed {:?}", fetch_error); - } Msg::NextPage => { match &model.context { - Context::Search(sr) => { - orders.request_url(urls::search(&sr.query, sr.page + 1)); - } Context::SearchResult { query, pager, .. } => { let query = query.to_string(); let after = pager.end_cursor.clone(); @@ -182,16 +117,12 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { } }); } - Context::Thread(_) => (), // do nothing (yet?) Context::ThreadResult(_) => (), // do nothing (yet?) Context::None => (), // do nothing (yet?) }; } Msg::PreviousPage => { match &model.context { - Context::Search(sr) => { - orders.request_url(urls::search(&sr.query, sr.page.saturating_sub(1))); - } Context::SearchResult { query, pager, .. } => { let query = query.to_string(); let before = pager.start_cursor.clone(); @@ -206,7 +137,6 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { }); } - Context::Thread(_) => (), // do nothing (yet?) Context::ThreadResult(_) => (), // do nothing (yet?) Context::None => (), // do nothing (yet?) }; @@ -352,14 +282,12 @@ pub enum UIError { pub enum Context { None, - Search(shared::SearchResult), SearchResult { query: String, results: Vec, count: usize, pager: FrontPageQuerySearchPageInfo, }, - Thread(ThreadSet), ThreadResult(ShowThreadQueryThread), } @@ -386,14 +314,6 @@ pub enum Msg { // Tell the server to update state RefreshStart, RefreshDone(Option), - SearchRequest { - query: String, - page: usize, - results_per_page: usize, - }, - SearchResult(Result), - ShowPrettyRequest(String), - ShowPrettyResult(Result), NextPage, PreviousPage, UpdateQuery(String), diff --git a/web/src/view/desktop.rs b/web/src/view/desktop.rs index daaeaf9..b6965d7 100644 --- a/web/src/view/desktop.rs +++ b/web/src/view/desktop.rs @@ -4,7 +4,7 @@ use seed_hooks::{state_access::CloneState, topo, use_state}; use crate::{ api::urls, state::{Context, Model, Msg, Tag}, - view::{self, legacy, view_header, view_search_results}, + view::{self, view_header, view_search_results}, }; #[topo::nested] @@ -12,9 +12,7 @@ pub(super) fn view(model: &Model) -> Node { // Do two queries, one without `unread` so it loads fast, then a second with unread. let content = match &model.context { Context::None => div![h1!["Loading"]], - Context::Thread(thread_set) => legacy::thread(thread_set), Context::ThreadResult(thread) => view::thread(thread), - Context::Search(search_results) => legacy::search_results(&model.query, search_results), Context::SearchResult { query, results, diff --git a/web/src/view/legacy.rs b/web/src/view/legacy.rs deleted file mode 100644 index 95ef71a..0000000 --- a/web/src/view/legacy.rs +++ /dev/null @@ -1,270 +0,0 @@ -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 search_results(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![ - search_pager(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] - ], - search_pager(first, summaries.len(), search_results.total) - ] -} - -fn search_pager(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 thread(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" - ], - ] -} -pub(super) fn mobile_search_results( - 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(); - 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)), - ] - */ - let tid = r.thread.clone(); - let datetime = human_age(r.timestamp as i64); - a![ - C!["has-text-light"], - attrs! { - At::Href => urls::thread(&tid) - }, - div![ - C!["row"], - div![C!["subject"], &r.subject], - span![C!["from", "is-size-7"], pretty_authors(&r.authors)], - div![ - span![C!["is-size-7"], tags_chiclet(&r.tags, true)], - span![C!["is-size-7", "float-right", "date"], datetime] - ] - ] - ] - }); - let first = search_results.page * search_results.results_per_page; - div![ - C!["search-results"], - search_pager(first, summaries.len(), search_results.total), - rows, - search_pager(first, summaries.len(), search_results.total) - ] -} -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 { - /* Build problems w/ css_inline. TODO(wathiede): move to server - 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![&html]]; - } 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, &html)]; - } - } - 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 7d73221..cd51641 100644 --- a/web/src/view/mobile.rs +++ b/web/src/view/mobile.rs @@ -5,19 +5,14 @@ use crate::{ graphql::front_page_query::*, state::{Context, Model, Msg}, view::{ - self, human_age, legacy, pretty_authors, set_title, tags_chiclet, view_header, - view_search_pager, + self, human_age, pretty_authors, set_title, tags_chiclet, view_header, view_search_pager, }, }; pub(super) fn view(model: &Model) -> Node { let content = match &model.context { Context::None => div![h1!["Loading"]], - Context::Thread(thread_set) => legacy::thread(thread_set), Context::ThreadResult(thread) => view::thread(thread), - Context::Search(search_results) => { - legacy::mobile_search_results(&model.query, search_results) - } Context::SearchResult { query, results, diff --git a/web/src/view/mod.rs b/web/src/view/mod.rs index 081dda1..973e4fc 100644 --- a/web/src/view/mod.rs +++ b/web/src/view/mod.rs @@ -15,13 +15,12 @@ use wasm_timer::Instant; use crate::{ api::urls, - consts::{SEARCH_RESULTS_PER_PAGE, USE_GRAPHQL}, + consts::SEARCH_RESULTS_PER_PAGE, graphql::{front_page_query::*, show_thread_query::*}, state::{Model, Msg, RefreshingState}, }; mod desktop; -mod legacy; mod mobile; mod tablet; @@ -47,10 +46,12 @@ fn tags_chiclet(tags: &[String], is_mobile: bool) -> impl Iterator span![classes, style, i![C!["fa-solid", "fa-reply"]]], _ => span![classes, style, &tag], }, - ev(Ev::Click, move |_| Msg::SearchRequest { + ev(Ev::Click, move |_| Msg::FrontPageRequest { query: format!("tag:{tag}"), - page: 0, - results_per_page: SEARCH_RESULTS_PER_PAGE, + after: None, + before: None, + first: None, + last: None, }) ] }) @@ -532,30 +533,10 @@ fn view_header(query: &str, refresh_request: &RefreshingState) -> Node { At::AutoFocus => true.as_at_value(); At::Value => query, }, - input_ev(Ev::Input, |q| if USE_GRAPHQL { - Msg::UpdateQuery(q) - } else { - Msg::SearchRequest { - query: Url::encode_uri_component(if q.is_empty() { - "*".to_string() - } else { - q - }), - page: 0, - results_per_page: SEARCH_RESULTS_PER_PAGE, - } - }), + input_ev(Ev::Input, |q| Msg::UpdateQuery(q)), // Send search on enter. keyboard_ev(Ev::KeyUp, move |e| if e.key_code() == 0x0d { - if USE_GRAPHQL { - Msg::SearchQuery(query) - } else { - Msg::SearchRequest { - query: Url::encode_uri_component(query), - page: 0, - results_per_page: SEARCH_RESULTS_PER_PAGE, - } - } + Msg::SearchQuery(query) } else { Msg::Noop }), @@ -584,7 +565,7 @@ pub fn view(model: &Model) -> Node { .expect("window width") .as_f64() .expect("window width f64"); - let h = win + let _h = win .inner_height() .expect("window height") .as_f64() diff --git a/web/src/view/tablet.rs b/web/src/view/tablet.rs index 24c2c6a..2e270cd 100644 --- a/web/src/view/tablet.rs +++ b/web/src/view/tablet.rs @@ -9,9 +9,7 @@ pub(super) fn view(model: &Model) -> Node { // Do two queries, one without `unread` so it loads fast, then a second with unread. let content = match &model.context { Context::None => div![h1!["Loading"]], - Context::Thread(_) => unimplemented!("tablet legacy thread view"), Context::ThreadResult(thread) => view::thread(thread), - Context::Search(_) => unimplemented!("tablet legacy search results view"), Context::SearchResult { query, results,