web: handle expand/collapse of messages separate from unread status

This commit is contained in:
Bill Thiede 2024-02-20 19:58:50 -08:00
parent fe980c5468
commit 5923547159
5 changed files with 95 additions and 47 deletions

View File

@ -119,7 +119,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
}); });
} }
Context::ThreadResult(_) => (), // do nothing (yet?) Context::ThreadResult { .. } => (), // do nothing (yet?)
Context::None => (), // do nothing (yet?) Context::None => (), // do nothing (yet?)
}; };
} }
@ -139,7 +139,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}); });
} }
Context::ThreadResult(_) => (), // do nothing (yet?) Context::ThreadResult { .. } => (), // do nothing (yet?)
Context::None => (), // do nothing (yet?) Context::None => (), // do nothing (yet?)
}; };
} }
@ -254,7 +254,25 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}) })
.collect(), .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) => { Msg::ShowThreadResult(bad) => {
error!("show_thread_query error: {bad:#?}"); error!("show_thread_query error: {bad:#?}");
@ -307,6 +325,18 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
info!("selected threads {selected_threads:?}"); 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. // `Model` describes our app state.
@ -340,7 +370,10 @@ pub enum Context {
pager: FrontPageQuerySearchPageInfo, pager: FrontPageQuerySearchPageInfo,
selected_threads: HashSet<String>, selected_threads: HashSet<String>,
}, },
ThreadResult(ShowThreadQueryThread), ThreadResult {
thread: ShowThreadQueryThread,
open_messages: HashSet<String>,
},
} }
pub struct Tag { pub struct Tag {
@ -394,4 +427,7 @@ pub enum Msg {
SelectionMarkAsUnread, SelectionMarkAsUnread,
SelectionAddThread(String), SelectionAddThread(String),
SelectionRemoveThread(String), SelectionRemoveThread(String),
MessageCollapse(String),
MessageExpand(String),
} }

View File

@ -12,7 +12,10 @@ pub(super) fn view(model: &Model) -> Node<Msg> {
// Do two queries, one without `unread` so it loads fast, then a second with unread. // Do two queries, one without `unread` so it loads fast, then a second with unread.
let content = match &model.context { let content = match &model.context {
Context::None => div![h1!["Loading"]], Context::None => div![h1!["Loading"]],
Context::ThreadResult(thread) => view::thread(thread), Context::ThreadResult {
thread,
open_messages,
} => view::thread(thread, open_messages),
Context::SearchResult { Context::SearchResult {
query, query,
results, results,

View File

@ -12,7 +12,10 @@ use crate::{
pub(super) fn view(model: &Model) -> Node<Msg> { pub(super) fn view(model: &Model) -> Node<Msg> {
let content = match &model.context { let content = match &model.context {
Context::None => div![h1!["Loading"]], Context::None => div![h1!["Loading"]],
Context::ThreadResult(thread) => view::thread(thread), Context::ThreadResult {
thread,
open_messages,
} => view::thread(thread, open_messages),
Context::SearchResult { Context::SearchResult {
query, query,
results, results,

View File

@ -7,10 +7,7 @@ use chrono::{DateTime, Datelike, Duration, Local, Utc};
use itertools::Itertools; use itertools::Itertools;
use log::error; use log::error;
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use seed_hooks::{ use seed_hooks::{state_access::CloneState, topo, use_state};
state_access::{CloneState, StateAccess},
topo, use_state,
};
use wasm_timer::Instant; use wasm_timer::Instant;
use crate::{ use crate::{
@ -333,8 +330,9 @@ fn has_unread(tags: &[String]) -> bool {
tags.contains(&String::from("unread")) tags.contains(&String::from("unread"))
} }
fn read_message_render(msg: &ShowThreadQueryThreadMessages, open: StateAccess<bool>) -> Node<Msg> { fn read_message_render(msg: &ShowThreadQueryThreadMessages, open: bool) -> Node<Msg> {
let id = msg.id.clone(); let id = msg.id.clone();
let expand_id = msg.id.clone();
let is_unread = has_unread(&msg.tags); let is_unread = has_unread(&msg.tags);
div![ div![
C!["message"], C!["message"],
@ -365,16 +363,18 @@ fn read_message_render(msg: &ShowThreadQueryThreadMessages, open: StateAccess<bo
// TODO(wathiede): add first line of message body // TODO(wathiede): add first line of message body
], ],
ev(Ev::Click, move |e| { ev(Ev::Click, move |e| {
open.set(!open.get());
e.stop_propagation(); e.stop_propagation();
if open {
Msg::MessageCollapse(expand_id)
} else {
Msg::MessageExpand(expand_id)
}
}), }),
] ]
} }
fn unread_message_render( fn unread_message_render(msg: &ShowThreadQueryThreadMessages, open: bool) -> Node<Msg> {
msg: &ShowThreadQueryThreadMessages,
open: StateAccess<bool>,
) -> Node<Msg> {
let id = msg.id.clone(); let id = msg.id.clone();
let expand_id = msg.id.clone();
let is_unread = has_unread(&msg.tags); let is_unread = has_unread(&msg.tags);
div![ div![
C!["message"], C!["message"],
@ -408,8 +408,12 @@ fn unread_message_render(
IF!(!msg.cc.is_empty() => span!["CC: ", view_addresses(&msg.cc)]) IF!(!msg.cc.is_empty() => span!["CC: ", view_addresses(&msg.cc)])
], ],
ev(Ev::Click, move |e| { ev(Ev::Click, move |e| {
open.set(!open.get());
e.stop_propagation(); e.stop_propagation();
if open {
Msg::MessageCollapse(expand_id)
} else {
Msg::MessageExpand(expand_id)
}
}), }),
], ],
div![ div![
@ -452,7 +456,7 @@ fn unread_message_render(
} }
#[topo::nested] #[topo::nested]
fn thread(thread: &ShowThreadQueryThread) -> Node<Msg> { fn thread(thread: &ShowThreadQueryThread, open_messages: &HashSet<String>) -> Node<Msg> {
// TODO(wathiede): show per-message subject if it changes significantly from top-level subject // TODO(wathiede): show per-message subject if it changes significantly from top-level subject
set_title(&thread.subject); set_title(&thread.subject);
let mut tags: Vec<_> = thread let mut tags: Vec<_> = thread
@ -467,40 +471,39 @@ fn thread(thread: &ShowThreadQueryThread) -> Node<Msg> {
tags.sort(); tags.sort();
let messages = thread.messages.iter().map(|msg| { let messages = thread.messages.iter().map(|msg| {
let is_unread = has_unread(&msg.tags); let is_unread = has_unread(&msg.tags);
let open = use_state(|| is_unread); let open = open_messages.contains(&msg.id);
//info!("open {} {}", open.get(), msg.id); if open {
if open.get() {
unread_message_render(&msg, open) unread_message_render(&msg, open)
} else { } else {
read_message_render(&msg, open) read_message_render(&msg, open)
} }
}); });
let any_unread = thread.messages.iter().any(|msg| has_unread(&msg.tags)); let read_thread_id = thread.thread_id.clone();
let thread_id = thread.thread_id.clone(); let unread_thread_id = thread.thread_id.clone();
div![ div![
C!["thread"], C!["thread"],
h1![ h3![C!["is-size-5"], &thread.subject,],
C!["title"], tags_chiclet(&tags, false),
span![ span![
C!["read-status"], C!["level-item", "buttons"],
i![ button![
style! { C!["button"],
St::Color => "gold" attrs! {At::Title => "Mark as read"},
}, span![C!["icon", "is-small"], i![C!["far", "fa-envelope-open"]]],
C![
if any_unread { "fa-regular" } else { "fa-solid" },
"fa-envelope"
],
ev(Ev::Click, move |_| Msg::SetUnread( ev(Ev::Click, move |_| Msg::SetUnread(
format!("thread:{}", thread_id), format!("thread:{read_thread_id}"),
!any_unread 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, messages,
/* TODO(wathiede): plumb in orignal id /* TODO(wathiede): plumb in orignal id

View File

@ -9,7 +9,10 @@ pub(super) fn view(model: &Model) -> Node<Msg> {
// Do two queries, one without `unread` so it loads fast, then a second with unread. // Do two queries, one without `unread` so it loads fast, then a second with unread.
let content = match &model.context { let content = match &model.context {
Context::None => div![h1!["Loading"]], Context::None => div![h1!["Loading"]],
Context::ThreadResult(thread) => view::thread(thread), Context::ThreadResult {
thread,
open_messages,
} => view::thread(thread, open_messages),
Context::SearchResult { Context::SearchResult {
query, query,
results, results,