diff --git a/web/src/view/desktop.rs b/web/src/view/desktop.rs new file mode 100644 index 0000000..bb0d4ed --- /dev/null +++ b/web/src/view/desktop.rs @@ -0,0 +1,124 @@ +use log::info; +use seed::{prelude::*, *}; +use seed_hooks::{state_access::CloneState, topo, use_state}; + +use crate::{ + api::urls, + state::{Context, Model, Msg, Tag}, + view::{ + view_header, view_search_results, view_search_results_legacy, view_thread, + view_thread_legacy, + }, +}; + +#[topo::nested] +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) => view_thread_legacy(thread_set), + Context::ThreadResult(thread) => view_thread(thread), + Context::Search(search_results) => view_search_results_legacy(&model.query, search_results), + Context::SearchResult { + query, + results, + count, + pager, + } => view_search_results(&query, results.as_slice(), *count, pager), + }; + fn view_tag_li(display_name: &str, indent: usize, t: &Tag) -> Node { + li![a![ + attrs! { + At::Href => urls::search(&format!("tag:{}", t.name), 0) + }, + (0..indent).map(|_| span![C!["tag-indent"], ""]), + i![ + C!["tag-tag", "fa-solid", "fa-tag"], + style! { + //"--fa-primary-color" => t.fg_color, + St::Color => t.bg_color, + }, + ], + display_name, + IF!(t.unread>0 => format!(" ({})", t.unread)) + ]] + } + fn matches(a: &[&str], b: &[&str]) -> usize { + std::iter::zip(a.iter(), b.iter()) + .take_while(|(a, b)| a == b) + .count() + } + fn view_tag_list<'a>(tags: impl Iterator) -> Vec> { + let mut lis = Vec::new(); + let mut last = Vec::new(); + for t in tags { + let parts: Vec<_> = t.name.split('/').collect(); + let mut n = matches(&last, &parts); + if t.name.starts_with("ZZCrap/Free") { + info!("n: {n}, parts: {parts:?} last: {last:?}"); + } + if n <= parts.len() - 2 && parts.len() > 1 { + // Synthesize fake tags for proper indenting. + for i in n..parts.len() - 1 { + let display_name = parts[n]; + lis.push(view_tag_li( + &display_name, + n, + &Tag { + name: parts[..i + 1].join("/"), + bg_color: "#fff".to_string(), + fg_color: "#000".to_string(), + unread: 0, + }, + )); + } + last = parts[..parts.len() - 1].to_vec(); + n = parts.len() - 1; + } + let display_name = parts[n]; + lis.push(view_tag_li(&display_name, n, t)); + last = parts; + } + lis + } + let unread = model + .tags + .as_ref() + .map(|tags| tags.iter().filter(|t| t.unread > 0).collect()) + .unwrap_or(Vec::new()); + let tags_open = use_state(|| false); + let force_tags_open = unread.is_empty(); + div![ + C!["desktop-main-content"], + aside![ + C!["tags-menu", "menu"], + IF!(!unread.is_empty() => p![C!["menu-label"], "Unread"]), + IF!(!unread.is_empty() => ul![C!["menu-list"], view_tag_list(unread.into_iter())]), + p![ + C!["menu-label"], + IF!(!force_tags_open => + i![C![ + "fa-solid", + if tags_open.get() { + "fa-angle-up" + } else { + "fa-angle-down" + } + ]]), + " Tags", + ev(Ev::Click, move |_| { + tags_open.set(!tags_open.get()); + }) + ], + ul![ + C!["menu-list"], + IF!(force_tags_open||tags_open.get() => model.tags.as_ref().map(|tags| view_tag_list(tags.iter()))), + ] + ], + div![ + view_header(&model.query, &model.refreshing_state), + content, + view_header(&model.query, &model.refreshing_state), + ] + ] +} diff --git a/web/src/view.rs b/web/src/view/mod.rs similarity index 84% rename from web/src/view.rs rename to web/src/view/mod.rs index fa21ac0..127ed65 100644 --- a/web/src/view.rs +++ b/web/src/view/mod.rs @@ -8,7 +8,6 @@ use itertools::Itertools; use log::info; use notmuch::{Content, Part, ThreadNode, ThreadSet}; use seed::{prelude::*, *}; -use seed_hooks::{state_access::CloneState, topo, use_state}; use wasm_timer::Instant; use crate::{ @@ -16,9 +15,11 @@ use crate::{ api::urls, consts::{SEARCH_RESULTS_PER_PAGE, USE_GRAPHQL}, graphql::{front_page_query::*, show_thread_query::*}, - state::{Context, Model, Msg, RefreshingState, Tag}, + state::{Context, Model, Msg, RefreshingState}, }; +mod desktop; + fn view_message(thread: &ThreadNode) -> Node { let message = thread.0.as_ref().expect("ThreadNode missing Message"); let children = &thread.1; @@ -687,118 +688,6 @@ fn view_footer(render_time_ms: u128) -> Node { ] } -#[topo::nested] -fn view_desktop(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) => view_thread_legacy(thread_set), - Context::ThreadResult(thread) => view_thread(thread), - Context::Search(search_results) => view_search_results_legacy(&model.query, search_results), - Context::SearchResult { - query, - results, - count, - pager, - } => view_search_results(&query, results.as_slice(), *count, pager), - }; - fn view_tag_li(display_name: &str, indent: usize, t: &Tag) -> Node { - li![a![ - attrs! { - At::Href => urls::search(&format!("tag:{}", t.name), 0) - }, - (0..indent).map(|_| span![C!["tag-indent"], ""]), - i![ - C!["tag-tag", "fa-solid", "fa-tag"], - style! { - //"--fa-primary-color" => t.fg_color, - St::Color => t.bg_color, - }, - ], - display_name, - IF!(t.unread>0 => format!(" ({})", t.unread)) - ]] - } - fn matches(a: &[&str], b: &[&str]) -> usize { - std::iter::zip(a.iter(), b.iter()) - .take_while(|(a, b)| a == b) - .count() - } - fn view_tag_list<'a>(tags: impl Iterator) -> Vec> { - let mut lis = Vec::new(); - let mut last = Vec::new(); - for t in tags { - let parts: Vec<_> = t.name.split('/').collect(); - let mut n = matches(&last, &parts); - if t.name.starts_with("ZZCrap/Free") { - info!("n: {n}, parts: {parts:?} last: {last:?}"); - } - if n <= parts.len() - 2 && parts.len() > 1 { - // Synthesize fake tags for proper indenting. - for i in n..parts.len() - 1 { - let display_name = parts[n]; - lis.push(view_tag_li( - &display_name, - n, - &Tag { - name: parts[..i + 1].join("/"), - bg_color: "#fff".to_string(), - fg_color: "#000".to_string(), - unread: 0, - }, - )); - } - last = parts[..parts.len() - 1].to_vec(); - n = parts.len() - 1; - } - let display_name = parts[n]; - lis.push(view_tag_li(&display_name, n, t)); - last = parts; - } - lis - } - let unread = model - .tags - .as_ref() - .map(|tags| tags.iter().filter(|t| t.unread > 0).collect()) - .unwrap_or(Vec::new()); - let tags_open = use_state(|| false); - let force_tags_open = unread.is_empty(); - div![ - C!["desktop-main-content"], - aside![ - C!["tags-menu", "menu"], - IF!(!unread.is_empty() => p![C!["menu-label"], "Unread"]), - IF!(!unread.is_empty() => ul![C!["menu-list"], view_tag_list(unread.into_iter())]), - p![ - C!["menu-label"], - IF!(!force_tags_open => - i![C![ - "fa-solid", - if tags_open.get() { - "fa-angle-up" - } else { - "fa-angle-down" - } - ]]), - " Tags", - ev(Ev::Click, move |_| { - tags_open.set(!tags_open.get()); - }) - ], - ul![ - C!["menu-list"], - IF!(force_tags_open||tags_open.get() => model.tags.as_ref().map(|tags| view_tag_list(tags.iter()))), - ] - ], - div![ - view_header(&model.query, &model.refreshing_state), - content, - view_header(&model.query, &model.refreshing_state), - ] - ] -} - fn view_mobile(model: &Model) -> Node { let content = match &model.context { Context::None => div![h1!["Loading"]], @@ -841,7 +730,7 @@ pub fn view(model: &Model) -> Node { if is_mobile { view_mobile(model) } else { - view_desktop(model) + desktop::view(model) }, view_footer(start.elapsed().as_millis()) ]