diff --git a/web/src/state.rs b/web/src/state.rs index 30769e7..dd5c8b0 100644 --- a/web/src/state.rs +++ b/web/src/state.rs @@ -119,8 +119,8 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { } }); } - Context::ThreadResult(_) => (), // do nothing (yet?) - Context::None => (), // do nothing (yet?) + Context::ThreadResult { .. } => (), // do nothing (yet?) + Context::None => (), // do nothing (yet?) }; } Msg::PreviousPage => { @@ -139,8 +139,8 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { }); } - Context::ThreadResult(_) => (), // do nothing (yet?) - Context::None => (), // do nothing (yet?) + Context::ThreadResult { .. } => (), // do nothing (yet?) + Context::None => (), // do nothing (yet?) }; } @@ -254,7 +254,25 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { }) .collect(), ); - model.context = Context::ThreadResult(data.thread); + let mut open_messages: HashSet<_> = data + .thread + .messages + .iter() + .filter(|msg| msg.tags.iter().any(|t| t == "unread")) + .map(|msg| msg.id.clone()) + .collect(); + if open_messages.is_empty() { + open_messages = data + .thread + .messages + .iter() + .map(|msg| msg.id.clone()) + .collect(); + } + model.context = Context::ThreadResult { + thread: data.thread, + open_messages, + }; } Msg::ShowThreadResult(bad) => { error!("show_thread_query error: {bad:#?}"); @@ -307,6 +325,18 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { info!("selected threads {selected_threads:?}"); } } + Msg::MessageCollapse(id) => { + if let Context::ThreadResult { open_messages, .. } = &mut model.context { + open_messages.remove(&id); + info!("open_messages threads {open_messages:?}"); + } + } + Msg::MessageExpand(id) => { + if let Context::ThreadResult { open_messages, .. } = &mut model.context { + open_messages.insert(id); + info!("open_messages threads {open_messages:?}"); + } + } } } // `Model` describes our app state. @@ -340,7 +370,10 @@ pub enum Context { pager: FrontPageQuerySearchPageInfo, selected_threads: HashSet, }, - ThreadResult(ShowThreadQueryThread), + ThreadResult { + thread: ShowThreadQueryThread, + open_messages: HashSet, + }, } pub struct Tag { @@ -394,4 +427,7 @@ pub enum Msg { SelectionMarkAsUnread, SelectionAddThread(String), SelectionRemoveThread(String), + + MessageCollapse(String), + MessageExpand(String), } diff --git a/web/src/view/desktop.rs b/web/src/view/desktop.rs index 2a1741a..ccdc479 100644 --- a/web/src/view/desktop.rs +++ b/web/src/view/desktop.rs @@ -12,7 +12,10 @@ 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::ThreadResult(thread) => view::thread(thread), + Context::ThreadResult { + thread, + open_messages, + } => view::thread(thread, open_messages), Context::SearchResult { query, results, diff --git a/web/src/view/mobile.rs b/web/src/view/mobile.rs index cd3065c..2c82e97 100644 --- a/web/src/view/mobile.rs +++ b/web/src/view/mobile.rs @@ -12,7 +12,10 @@ use crate::{ pub(super) fn view(model: &Model) -> Node { let content = match &model.context { Context::None => div![h1!["Loading"]], - Context::ThreadResult(thread) => view::thread(thread), + Context::ThreadResult { + thread, + open_messages, + } => view::thread(thread, open_messages), Context::SearchResult { query, results, diff --git a/web/src/view/mod.rs b/web/src/view/mod.rs index a396deb..40d0772 100644 --- a/web/src/view/mod.rs +++ b/web/src/view/mod.rs @@ -7,10 +7,7 @@ use chrono::{DateTime, Datelike, Duration, Local, Utc}; use itertools::Itertools; use log::error; use seed::{prelude::*, *}; -use seed_hooks::{ - state_access::{CloneState, StateAccess}, - topo, use_state, -}; +use seed_hooks::{state_access::CloneState, topo, use_state}; use wasm_timer::Instant; use crate::{ @@ -333,8 +330,9 @@ fn has_unread(tags: &[String]) -> bool { tags.contains(&String::from("unread")) } -fn read_message_render(msg: &ShowThreadQueryThreadMessages, open: StateAccess) -> Node { +fn read_message_render(msg: &ShowThreadQueryThreadMessages, open: bool) -> Node { let id = msg.id.clone(); + let expand_id = msg.id.clone(); let is_unread = has_unread(&msg.tags); div![ C!["message"], @@ -365,16 +363,18 @@ fn read_message_render(msg: &ShowThreadQueryThreadMessages, open: StateAccess, -) -> Node { +fn unread_message_render(msg: &ShowThreadQueryThreadMessages, open: bool) -> Node { let id = msg.id.clone(); + let expand_id = msg.id.clone(); let is_unread = has_unread(&msg.tags); div![ C!["message"], @@ -408,8 +408,12 @@ fn unread_message_render( IF!(!msg.cc.is_empty() => span!["CC: ", view_addresses(&msg.cc)]) ], ev(Ev::Click, move |e| { - open.set(!open.get()); e.stop_propagation(); + if open { + Msg::MessageCollapse(expand_id) + } else { + Msg::MessageExpand(expand_id) + } }), ], div![ @@ -452,7 +456,7 @@ fn unread_message_render( } #[topo::nested] -fn thread(thread: &ShowThreadQueryThread) -> Node { +fn thread(thread: &ShowThreadQueryThread, open_messages: &HashSet) -> Node { // TODO(wathiede): show per-message subject if it changes significantly from top-level subject set_title(&thread.subject); let mut tags: Vec<_> = thread @@ -467,40 +471,39 @@ fn thread(thread: &ShowThreadQueryThread) -> Node { tags.sort(); let messages = thread.messages.iter().map(|msg| { let is_unread = has_unread(&msg.tags); - let open = use_state(|| is_unread); - //info!("open {} {}", open.get(), msg.id); - if open.get() { + let open = open_messages.contains(&msg.id); + if open { unread_message_render(&msg, open) } else { read_message_render(&msg, open) } }); - let any_unread = thread.messages.iter().any(|msg| has_unread(&msg.tags)); - let thread_id = thread.thread_id.clone(); + let read_thread_id = thread.thread_id.clone(); + let unread_thread_id = thread.thread_id.clone(); div![ C!["thread"], - h1![ - C!["title"], - span![ - C!["read-status"], - i![ - style! { - St::Color => "gold" - }, - C![ - if any_unread { "fa-regular" } else { "fa-solid" }, - "fa-envelope" - ], - ev(Ev::Click, move |_| Msg::SetUnread( - format!("thread:{}", thread_id), - !any_unread - )), - ], - " ", + h3![C!["is-size-5"], &thread.subject,], + tags_chiclet(&tags, false), + span![ + C!["level-item", "buttons"], + button![ + C!["button"], + attrs! {At::Title => "Mark as read"}, + span![C!["icon", "is-small"], i![C!["far", "fa-envelope-open"]]], + ev(Ev::Click, move |_| Msg::SetUnread( + format!("thread:{read_thread_id}"), + false + )), + ], + button![ + C!["button"], + attrs! {At::Title => "Mark as unread"}, + span![C!["icon", "is-small"], i![C!["far", "fa-envelope"]]], + ev(Ev::Click, move |_| Msg::SetUnread( + format!("thread:{unread_thread_id}"), + true + )), ], - &thread.subject, - " ", - tags_chiclet(&tags, false) ], messages, /* TODO(wathiede): plumb in orignal id diff --git a/web/src/view/tablet.rs b/web/src/view/tablet.rs index 39ce606..38bda57 100644 --- a/web/src/view/tablet.rs +++ b/web/src/view/tablet.rs @@ -9,7 +9,10 @@ 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::ThreadResult(thread) => view::thread(thread), + Context::ThreadResult { + thread, + open_messages, + } => view::thread(thread, open_messages), Context::SearchResult { query, results,