393 lines
12 KiB
Rust
393 lines
12 KiB
Rust
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<Msg>) -> 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<Msg>) {
|
|
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<Vec<Tag>>,
|
|
}
|
|
|
|
#[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<graphql_client::Error>),
|
|
#[error("no data or errors for {0}")]
|
|
NoData(&'static str),
|
|
}
|
|
|
|
pub enum Context {
|
|
None,
|
|
Search(shared::SearchResult),
|
|
SearchResult {
|
|
query: String,
|
|
results: Vec<FrontPageQuerySearchNodes>,
|
|
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<FetchError>),
|
|
SearchRequest {
|
|
query: String,
|
|
page: usize,
|
|
results_per_page: usize,
|
|
},
|
|
SearchResult(fetch::Result<shared::SearchResult>),
|
|
ShowPrettyRequest(String),
|
|
ShowPrettyResult(fetch::Result<ThreadSet>),
|
|
NextPage,
|
|
PreviousPage,
|
|
UpdateQuery(String),
|
|
SearchQuery(String),
|
|
|
|
FrontPageRequest {
|
|
query: String,
|
|
after: Option<String>,
|
|
before: Option<String>,
|
|
first: Option<i64>,
|
|
last: Option<i64>,
|
|
},
|
|
FrontPageResult(
|
|
fetch::Result<graphql_client::Response<graphql::front_page_query::ResponseData>>,
|
|
),
|
|
ShowThreadRequest {
|
|
thread_id: String,
|
|
},
|
|
ShowThreadResult(
|
|
fetch::Result<graphql_client::Response<graphql::show_thread_query::ResponseData>>,
|
|
),
|
|
}
|