use graphql_client::GraphQLQuery; use log::{debug, info}; use notmuch::ThreadSet; use seed::{prelude::*, *}; use thiserror::Error; use crate::{ api, api::urls, consts::{SEARCH_RESULTS_PER_PAGE, USE_GRAPHQL}, graphql, graphql::{front_page_query::*, send_graphql, show_thread_query::*}, }; // `init` describes what should happen when your app started. pub fn init(url: Url, orders: &mut impl Orders) -> Model { if url.hash().is_none() { orders.request_url(urls::search("is:unread", 0)); } else { orders.notify(subs::UrlRequested::new(url)); }; orders.subscribe(on_url_changed); Model { context: Context::None, query: "".to_string(), refreshing_state: RefreshingState::None, ui_error: UIError::NoError, tags: None, } } fn on_url_changed(uc: subs::UrlChanged) -> Msg { let mut url = uc.0; info!( "url changed '{}', history {}", url, history().length().unwrap_or(0) ); 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()) } } ["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, } } } ["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, } } } 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, } } } } } // `update` describes how to handle each `Msg`. pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { match msg { Msg::Noop => {} Msg::RefreshStart => { model.refreshing_state = RefreshingState::Loading; orders.perform_cmd(async move { Msg::RefreshDone(api::refresh_request().await.err()) }); } Msg::RefreshDone(err) => { model.refreshing_state = if let Some(err) = err { RefreshingState::Error(format!("{:?}", err)) } else { RefreshingState::None }; orders.perform_cmd(async move { Msg::Reload }); } Msg::Reload => { orders.perform_cmd(async move { on_url_changed(subs::UrlChanged(Url::current())) }); } 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(); orders.perform_cmd(async move { Msg::FrontPageRequest { query, after, before: None, first: Some(SEARCH_RESULTS_PER_PAGE as i64), last: None, } }); } 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(); orders.perform_cmd(async move { Msg::FrontPageRequest { query, after: None, before, first: None, last: Some(SEARCH_RESULTS_PER_PAGE as i64), } }); } Context::Thread(_) => (), // do nothing (yet?) Context::ThreadResult(_) => (), // do nothing (yet?) Context::None => (), // do nothing (yet?) }; } Msg::UpdateQuery(query) => model.query = query, Msg::SearchQuery(query) => { orders.request_url(urls::search(&query, 0)); } Msg::FrontPageRequest { query, after, before, first, last, } => { info!("making FrontPageRequest: {query} after:{after:?} before:{before:?} first:{first:?} last:{last:?}"); model.query = query.clone(); orders.skip().perform_cmd(async move { Msg::FrontPageResult( send_graphql(graphql::FrontPageQuery::build_query( graphql::front_page_query::Variables { query, after, before, first, last, }, )) .await, ) }); } Msg::FrontPageResult(Err(e)) => error!("error FrontPageResult: {e:?}"), Msg::FrontPageResult(Ok(graphql_client::Response { data: None, errors: None, .. })) => { error!("FrontPageResult no data or errors, should not happen"); } Msg::FrontPageResult(Ok(graphql_client::Response { data: None, errors: Some(e), .. })) => { error!("FrontPageResult error: {e:?}"); } Msg::FrontPageResult(Ok(graphql_client::Response { data: Some(data), .. })) => { model.tags = Some( data.tags .into_iter() .map(|t| Tag { name: t.name, bg_color: t.bg_color, fg_color: t.fg_color, unread: t.unread, }) .collect(), ); model.context = Context::SearchResult { query: model.query.clone(), results: data.search.nodes, count: data.count as usize, pager: data.search.page_info, }; } Msg::ShowThreadRequest { thread_id } => { orders.skip().perform_cmd(async move { Msg::ShowThreadResult( send_graphql(graphql::ShowThreadQuery::build_query( graphql::show_thread_query::Variables { thread_id }, )) .await, ) }); } Msg::ShowThreadResult(Ok(graphql_client::Response { data: Some(data), .. })) => { model.tags = Some( data.tags .into_iter() .map(|t| Tag { name: t.name, bg_color: t.bg_color, fg_color: t.fg_color, unread: t.unread, }) .collect(), ); model.context = Context::ThreadResult(data.thread); } Msg::ShowThreadResult(bad) => { error!("show_thread_query error: {bad:?}"); } } } // `Model` describes our app state. pub struct Model { pub query: String, pub context: Context, pub refreshing_state: RefreshingState, pub ui_error: UIError, pub tags: Option>, } #[derive(Error, Debug)] #[allow(dead_code)] // Remove once the UI is showing errors pub enum UIError { #[error("No error, this should never be presented to user")] NoError, #[error("failed to fetch {0}: {1:?}")] FetchError(&'static str, FetchError), #[error("{0} error decoding: {1:?}")] FetchDecodeError(&'static str, Vec), #[error("no data or errors for {0}")] NoData(&'static str), } pub enum Context { None, Search(shared::SearchResult), SearchResult { query: String, results: Vec, count: usize, pager: FrontPageQuerySearchPageInfo, }, Thread(ThreadSet), ThreadResult(ShowThreadQueryThread), } pub struct Tag { pub name: String, pub bg_color: String, pub fg_color: String, pub unread: i64, } #[derive(Debug, PartialEq)] pub enum RefreshingState { None, Loading, Error(String), } // `Msg` describes the different events you can modify state with. pub enum Msg { Noop, // Tell the client to refresh its state Reload, // Tell the server to update state RefreshStart, RefreshDone(Option), SearchRequest { query: String, page: usize, results_per_page: usize, }, SearchResult(fetch::Result), ShowPrettyRequest(String), ShowPrettyResult(fetch::Result), NextPage, PreviousPage, UpdateQuery(String), SearchQuery(String), FrontPageRequest { query: String, after: Option, before: Option, first: Option, last: Option, }, FrontPageResult( fetch::Result>, ), ShowThreadRequest { thread_id: String, }, ShowThreadResult( fetch::Result>, ), }