web: only use one view function, desktop/tablet/mobile handled in CSS

This commit is contained in:
Bill Thiede 2025-01-26 09:31:44 -08:00
parent ea280dd366
commit 25d31a6ce7
5 changed files with 131 additions and 249 deletions

View File

@ -507,6 +507,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}); });
} }
Msg::WindowScrolled => { Msg::WindowScrolled => {
info!("WindowScrolled");
if let Some(el) = model.content_el.get() { if let Some(el) = model.content_el.get() {
let ih = window() let ih = window()
.inner_height() .inner_height()
@ -515,6 +516,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
.value_of(); .value_of();
let r = el.get_bounding_client_rect(); let r = el.get_bounding_client_rect();
info!("r {r:?} ih {ih}");
if r.height() < ih { if r.height() < ih {
// The whole content fits in the window, no scrollbar // The whole content fits in the window, no scrollbar
orders.send_msg(Msg::SetProgress(0.)); orders.send_msg(Msg::SetProgress(0.));

View File

@ -1,49 +0,0 @@
use seed::{prelude::*, *};
use seed_hooks::topo;
use crate::{
graphql::show_thread_query::*,
state::{Context, Model, Msg},
view::{self, reading_progress, view_header, view_search_results},
};
#[topo::nested]
pub(super) fn view(model: &Model) -> Node<Msg> {
let show_icon_text = true;
// 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::ThreadResult {
thread: ShowThreadQueryThread::EmailThread(thread),
open_messages,
} => view::thread(thread, open_messages, show_icon_text, &model.content_el),
Context::ThreadResult {
thread: ShowThreadQueryThread::NewsPost(post),
..
} => view::news_post(post, show_icon_text, &model.content_el),
Context::SearchResult {
query,
results,
count,
pager,
selected_threads,
} => view_search_results(
&query,
results.as_slice(),
*count,
pager,
selected_threads,
show_icon_text,
),
};
div![
C!["flex"],
reading_progress(model.read_completion_ratio),
div![view::tags(model), view::versions(&model.versions)],
div![
view_header(&model.query, &model.refreshing_state),
content,
view_header(&model.query, &model.refreshing_state),
]
]
}

View File

@ -1,127 +0,0 @@
use std::collections::HashSet;
use seed::{prelude::*, *};
use crate::{
api::urls,
graphql::{front_page_query::*, show_thread_query::*},
state::{Context, Model, Msg},
view::{
self, human_age, pretty_authors, reading_progress, search_toolbar, set_title, tags_chiclet,
view_header,
},
};
pub(super) fn view(model: &Model) -> Node<Msg> {
let show_icon_text = false;
let content = match &model.context {
Context::None => div![h1!["Loading"]],
Context::ThreadResult {
thread: ShowThreadQueryThread::EmailThread(thread),
open_messages,
} => view::thread(thread, open_messages, show_icon_text, &model.content_el),
Context::ThreadResult {
thread: ShowThreadQueryThread::NewsPost(post),
..
} => view::news_post(post, show_icon_text, &model.content_el),
Context::SearchResult {
query,
results,
count,
pager,
selected_threads,
} => search_results(
&query,
results.as_slice(),
*count,
pager,
selected_threads,
show_icon_text,
),
};
div![
reading_progress(model.read_completion_ratio),
view_header(&model.query, &model.refreshing_state),
content,
view_header(&model.query, &model.refreshing_state),
div![view::tags(model), view::versions(&model.versions)]
]
}
fn search_results(
query: &str,
results: &[FrontPageQuerySearchNodes],
count: usize,
pager: &FrontPageQuerySearchPageInfo,
selected_threads: &HashSet<String>,
show_icon_text: bool,
) -> Node<Msg> {
if query.is_empty() {
set_title("all mail");
} else {
set_title(query);
}
let rows = results.iter().map(|r| {
let tid = r.thread.clone();
let check_tid = r.thread.clone();
let datetime = human_age(r.timestamp as i64);
let unread_idx = r.tags.iter().position(|e| e == &"unread");
let mut tags = r.tags.clone();
if let Some(idx) = unread_idx {
tags.remove(idx);
};
div![
C!["NOTPORTED", "row"],
label![
C!["NOTPORTED", "b-checkbox", "checkbox", "is-large"],
input![attrs! {
At::Type=>"checkbox",
At::Checked=>selected_threads.contains(&tid).as_at_value(),
}],
span![C!["NOTPORTED", "check"]],
ev(Ev::Input, move |e| {
if let Some(input) = e
.target()
.as_ref()
.expect("failed to get reference to target")
.dyn_ref::<web_sys::HtmlInputElement>()
{
if input.checked() {
Msg::SelectionAddThread(check_tid)
} else {
Msg::SelectionRemoveThread(check_tid)
}
} else {
Msg::Noop
}
}),
],
a![
C!["NOTPORTED", "has-text-light", "summary"],
IF!(unread_idx.is_some() => C!["NOTPORTED","unread"]),
attrs! {
At::Href => urls::thread(&tid)
},
div![C!["NOTPORTED", "subject"], &r.subject],
span![
C!["NOTPORTED", "from", "is-size-7"],
pretty_authors(&r.authors)
],
div![
span![C!["NOTPORTED", "is-size-7"], tags_chiclet(&tags, true)],
span![
C!["NOTPORTED", "is-size-7", "float-right", "date"],
datetime
]
]
]
]
});
let show_bulk_edit = !selected_threads.is_empty();
div![
C!["NOTPORTED", "search-results"],
search_toolbar(count, pager, show_bulk_edit, show_icon_text),
div![C!["NOTPORTED", "index"], rows],
search_toolbar(count, pager, show_bulk_edit, show_icon_text),
]
}

View File

@ -12,17 +12,140 @@ use web_sys::HtmlElement;
use crate::{ use crate::{
api::urls, api::urls,
graphql::{front_page_query::*, show_thread_query::*}, graphql::{front_page_query::*, show_thread_query::*},
state::{unread_query, Model, Msg, RefreshingState, Tag}, state::{unread_query, Context, Model, Msg, RefreshingState, Tag},
}; };
mod desktop;
mod mobile;
mod tablet;
// TODO(wathiede): create a QueryString enum that wraps single and multiple message ids and thread // TODO(wathiede): create a QueryString enum that wraps single and multiple message ids and thread
// ids, and has a to_query_string() that knows notmuch's syntax. Then remove the smattering of // ids, and has a to_query_string() that knows notmuch's syntax. Then remove the smattering of
// format!() calls all over with magic strings representing notmuch specific syntax. // format!() calls all over with magic strings representing notmuch specific syntax.
const MAX_RAW_MESSAGE_SIZE: usize = 100_000; const MAX_RAW_MESSAGE_SIZE: usize = 100_000;
pub fn view(model: &Model) -> Node<Msg> {
let show_icon_text = true;
let content = match &model.context {
Context::None => div![h1!["Loading"]],
Context::ThreadResult {
thread: ShowThreadQueryThread::EmailThread(thread_data),
open_messages,
} => thread(
thread_data,
open_messages,
show_icon_text,
&model.content_el,
),
Context::ThreadResult {
thread: ShowThreadQueryThread::NewsPost(post),
..
} => news_post(post, show_icon_text, &model.content_el),
Context::SearchResult {
query,
results,
count,
pager,
selected_threads,
} => search_results(
&query,
results.as_slice(),
*count,
pager,
selected_threads,
show_icon_text,
),
};
div![
C!["flex", "flex-wrap-reverse"],
div![
C!["w-48", "flex-none", "flex", "flex-col"],
tags(model),
versions(&model.versions)
],
reading_progress(model.read_completion_ratio),
div![
C!["flex-auto"],
view_header(&model.query, &model.refreshing_state),
content,
view_header(&model.query, &model.refreshing_state),
]
]
}
fn search_results(
query: &str,
results: &[FrontPageQuerySearchNodes],
count: usize,
pager: &FrontPageQuerySearchPageInfo,
selected_threads: &HashSet<String>,
show_icon_text: bool,
) -> Node<Msg> {
if query.is_empty() {
set_title("all mail");
} else {
set_title(query);
}
let rows = results.iter().map(|r| {
let tid = r.thread.clone();
let check_tid = r.thread.clone();
let datetime = human_age(r.timestamp as i64);
let unread_idx = r.tags.iter().position(|e| e == &"unread");
let mut tags = r.tags.clone();
if let Some(idx) = unread_idx {
tags.remove(idx);
};
div![
C!["NOTPORTED", "row"],
label![
C!["NOTPORTED", "b-checkbox", "checkbox", "is-large"],
input![attrs! {
At::Type=>"checkbox",
At::Checked=>selected_threads.contains(&tid).as_at_value(),
}],
span![C!["NOTPORTED", "check"]],
ev(Ev::Input, move |e| {
if let Some(input) = e
.target()
.as_ref()
.expect("failed to get reference to target")
.dyn_ref::<web_sys::HtmlInputElement>()
{
if input.checked() {
Msg::SelectionAddThread(check_tid)
} else {
Msg::SelectionRemoveThread(check_tid)
}
} else {
Msg::Noop
}
}),
],
a![
C!["NOTPORTED", "has-text-light", "summary"],
IF!(unread_idx.is_some() => C!["NOTPORTED","unread"]),
attrs! {
At::Href => urls::thread(&tid)
},
div![C!["NOTPORTED", "subject"], &r.subject],
span![
C!["NOTPORTED", "from", "is-size-7"],
pretty_authors(&r.authors)
],
div![
span![C!["NOTPORTED", "is-size-7"], tags_chiclet(&tags, true)],
span![
C!["NOTPORTED", "is-size-7", "float-right", "date"],
datetime
]
]
]
]
});
let show_bulk_edit = !selected_threads.is_empty();
div![
C!["NOTPORTED", "search-results"],
search_toolbar(count, pager, show_bulk_edit, show_icon_text),
div![C!["NOTPORTED", "index"], rows],
search_toolbar(count, pager, show_bulk_edit, show_icon_text),
]
}
fn set_title(title: &str) { fn set_title(title: &str) {
seed::document().set_title(&format!("lb: {}", title)); seed::document().set_title(&format!("lb: {}", title));
} }
@ -916,25 +1039,6 @@ fn view_header(query: &str, refresh_request: &RefreshingState) -> Node<Msg> {
] ]
} }
// `view` describes what to display.
pub fn view(model: &Model) -> Node<Msg> {
let win = seed::window();
let w = win
.inner_width()
.expect("window width")
.as_f64()
.expect("window width f64");
let _h = win
.inner_height()
.expect("window height")
.as_f64()
.expect("window height f64");
div![match w {
w if w < 800. => div![C!["NOTPORTED", "mobile"], mobile::view(model)],
w if w < 1024. => div![C!["NOTPORTED", "tablet"], tablet::view(model)],
_ => div![C!["NOTPORTED", "desktop"], desktop::view(model)],
},]
}
pub fn tags(model: &Model) -> Node<Msg> { pub fn tags(model: &Model) -> Node<Msg> {
fn view_tag_li(display_name: &str, indent: usize, t: &Tag, search_unread: bool) -> Node<Msg> { fn view_tag_li(display_name: &str, indent: usize, t: &Tag, search_unread: bool) -> Node<Msg> {
let href = if search_unread { let href = if search_unread {
@ -977,7 +1081,7 @@ pub fn tags(model: &Model) -> Node<Msg> {
for t in tags { for t in tags {
let parts: Vec<_> = t.name.split('/').collect(); let parts: Vec<_> = t.name.split('/').collect();
let mut n = matches(&last, &parts); let mut n = matches(&last, &parts);
if n <= parts.len() - 2 && parts.len() > 1 { if n + 2 <= parts.len() && parts.len() > 1 {
// Synthesize fake tags for proper indenting. // Synthesize fake tags for proper indenting.
for i in n..parts.len() - 1 { for i in n..parts.len() - 1 {
let display_name = parts[n]; let display_name = parts[n];

View File

@ -1,48 +0,0 @@
use seed::{prelude::*, *};
use crate::{
graphql::show_thread_query::*,
state::{Context, Model, Msg},
view::{self, reading_progress, view_header, view_search_results},
};
pub(super) fn view(model: &Model) -> Node<Msg> {
let show_icon_text = false;
// 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::ThreadResult {
thread: ShowThreadQueryThread::EmailThread(thread),
open_messages,
} => view::thread(thread, open_messages, show_icon_text, &model.content_el),
Context::ThreadResult {
thread: ShowThreadQueryThread::NewsPost(post),
..
} => view::news_post(post, show_icon_text, &model.content_el),
Context::SearchResult {
query,
results,
count,
pager,
selected_threads,
} => view_search_results(
&query,
results.as_slice(),
*count,
pager,
selected_threads,
show_icon_text,
),
};
div![
C!["NOTPORTED", "main-content"],
div![
reading_progress(model.read_completion_ratio),
view_header(&model.query, &model.refreshing_state),
content,
view_header(&model.query, &model.refreshing_state),
view::tags(model),
view::versions(&model.versions)
]
]
}