web: add per-message unread control and display
This commit is contained in:
@@ -7,7 +7,10 @@ use chrono::{DateTime, Datelike, Duration, Local, Utc};
|
||||
use itertools::Itertools;
|
||||
use log::{error, info};
|
||||
use seed::{prelude::*, *};
|
||||
use seed_hooks::{state_access::CloneState, topo, use_state};
|
||||
use seed_hooks::{
|
||||
state_access::{CloneState, StateAccess},
|
||||
topo, use_state,
|
||||
};
|
||||
use wasm_timer::Instant;
|
||||
|
||||
use crate::{
|
||||
@@ -262,6 +265,128 @@ fn raw_text_message(contents: &str) -> Node<Msg> {
|
||||
div![C!["view-part-text-plain"], contents, truncated_msg,]
|
||||
}
|
||||
|
||||
fn has_unread(tags: &[String]) -> bool {
|
||||
for t in tags {
|
||||
if t == "unread" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn read_message_render(msg: &ShowThreadQueryThreadMessages, open: StateAccess<bool>) -> Node<Msg> {
|
||||
let id = msg.id.clone();
|
||||
let is_unread = has_unread(&msg.tags);
|
||||
div![
|
||||
C!["message"],
|
||||
div![
|
||||
C!["headers"],
|
||||
span![
|
||||
C!["read-status"],
|
||||
i![
|
||||
style! {
|
||||
St::Color => "gold"
|
||||
},
|
||||
C![if is_unread { "fa-regular" } else { "fa-solid" }, "fa-star"],
|
||||
ev(Ev::Click, move |e| {
|
||||
e.stop_propagation();
|
||||
Msg::SetUnread(format!("id:{id}"), !is_unread)
|
||||
}),
|
||||
],
|
||||
],
|
||||
" ",
|
||||
msg.from
|
||||
.as_ref()
|
||||
.map(|from| span![C!["header"], view_address(&from)]),
|
||||
" ",
|
||||
msg.timestamp.map(|ts| span![C!["header"], human_age(ts)]),
|
||||
// TODO(wathiede): add first line of message body
|
||||
],
|
||||
ev(Ev::Click, move |e| {
|
||||
open.set(!open.get());
|
||||
e.stop_propagation();
|
||||
}),
|
||||
]
|
||||
}
|
||||
fn unread_message_render(
|
||||
msg: &ShowThreadQueryThreadMessages,
|
||||
open: StateAccess<bool>,
|
||||
) -> Node<Msg> {
|
||||
let id = msg.id.clone();
|
||||
let is_unread = has_unread(&msg.tags);
|
||||
div![
|
||||
C!["message"],
|
||||
div![
|
||||
C!["headers"],
|
||||
span![
|
||||
C!["read-status"],
|
||||
i![
|
||||
style! {
|
||||
St::Color => "gold"
|
||||
},
|
||||
C![if is_unread { "fa-regular" } else { "fa-solid" }, "fa-star"],
|
||||
ev(Ev::Click, move |e| {
|
||||
e.stop_propagation();
|
||||
Msg::SetUnread(format!("id:{id}"), !is_unread)
|
||||
}),
|
||||
],
|
||||
],
|
||||
msg.from
|
||||
.as_ref()
|
||||
.map(|from| div![C!["header"], "From: ", view_address(&from)]),
|
||||
msg.timestamp
|
||||
.map(|ts| div![C!["header"], "Date: ", human_age(ts)]),
|
||||
div![C!["header"], "Message-ID: ", &msg.id],
|
||||
div![
|
||||
C!["header"],
|
||||
IF!(!msg.to.is_empty() => span!["To: ", view_addresses(&msg.to)]),
|
||||
IF!(!msg.cc.is_empty() => span!["CC: ", view_addresses(&msg.cc)])
|
||||
],
|
||||
ev(Ev::Click, move |e| {
|
||||
open.set(!open.get());
|
||||
e.stop_propagation();
|
||||
}),
|
||||
],
|
||||
div![
|
||||
C!["body"],
|
||||
match &msg.body {
|
||||
ShowThreadQueryThreadMessagesBody::UnhandledContentType(
|
||||
ShowThreadQueryThreadMessagesBodyOnUnhandledContentType { contents },
|
||||
) => pre![C!["error"], contents],
|
||||
ShowThreadQueryThreadMessagesBody::PlainText(
|
||||
ShowThreadQueryThreadMessagesBodyOnPlainText {
|
||||
contents,
|
||||
content_tree,
|
||||
},
|
||||
) => div![
|
||||
raw_text_message(&contents),
|
||||
view_content_tree(&content_tree),
|
||||
],
|
||||
ShowThreadQueryThreadMessagesBody::Html(
|
||||
ShowThreadQueryThreadMessagesBodyOnHtml {
|
||||
contents,
|
||||
content_tree,
|
||||
},
|
||||
) => div![
|
||||
C!["view-part-text-html"],
|
||||
raw![contents],
|
||||
IF!(!msg.attachments.is_empty() =>
|
||||
div![
|
||||
C!["attachments"],
|
||||
br![],
|
||||
h2!["Attachments"],
|
||||
msg.attachments
|
||||
.iter()
|
||||
.map(|a| div!["Filename: ", &a.filename, " ", &a.content_type])
|
||||
]),
|
||||
view_content_tree(&content_tree),
|
||||
],
|
||||
}
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
#[topo::nested]
|
||||
fn thread(thread: &ShowThreadQueryThread) -> Node<Msg> {
|
||||
// TODO(wathiede): show per-message subject if it changes significantly from top-level subject
|
||||
set_title(&thread.subject);
|
||||
@@ -276,65 +401,37 @@ fn thread(thread: &ShowThreadQueryThread) -> Node<Msg> {
|
||||
.collect();
|
||||
tags.sort();
|
||||
let messages = thread.messages.iter().map(|msg| {
|
||||
div![
|
||||
C!["message"],
|
||||
div![
|
||||
C!["headers"],
|
||||
/* TODO(wathiede): collect all the tags and show them here. */
|
||||
msg.from
|
||||
.as_ref()
|
||||
.map(|from| div![C!["header"], "From: ", view_address(&from)]),
|
||||
msg.timestamp
|
||||
.map(|ts| div![C!["header"], "Date: ", human_age(ts)]),
|
||||
div![C!["header"], "Message-ID: ", &msg.id],
|
||||
div![
|
||||
C!["header"],
|
||||
IF!(!msg.to.is_empty() => span!["To: ", view_addresses(&msg.to)]),
|
||||
IF!(!msg.cc.is_empty() => span!["CC: ", view_addresses(&msg.cc)])
|
||||
],
|
||||
],
|
||||
div![
|
||||
C!["body"],
|
||||
match &msg.body {
|
||||
ShowThreadQueryThreadMessagesBody::UnhandledContentType(
|
||||
ShowThreadQueryThreadMessagesBodyOnUnhandledContentType { contents },
|
||||
) => pre![C!["error"], contents],
|
||||
ShowThreadQueryThreadMessagesBody::PlainText(
|
||||
ShowThreadQueryThreadMessagesBodyOnPlainText {
|
||||
contents,
|
||||
content_tree,
|
||||
},
|
||||
) => div![
|
||||
raw_text_message(&contents),
|
||||
view_content_tree(&content_tree),
|
||||
],
|
||||
ShowThreadQueryThreadMessagesBody::Html(
|
||||
ShowThreadQueryThreadMessagesBodyOnHtml {
|
||||
contents,
|
||||
content_tree,
|
||||
},
|
||||
) => div![
|
||||
C!["view-part-text-html"],
|
||||
raw![contents],
|
||||
IF!(!msg.attachments.is_empty() =>
|
||||
div![
|
||||
C!["attachments"],
|
||||
br![],
|
||||
h2!["Attachments"],
|
||||
msg.attachments
|
||||
.iter()
|
||||
.map(|a| div!["Filename: ", &a.filename, " ", &a.content_type])
|
||||
]),
|
||||
view_content_tree(&content_tree),
|
||||
],
|
||||
}
|
||||
],
|
||||
]
|
||||
let is_unread = has_unread(&msg.tags);
|
||||
let open = use_state(|| is_unread);
|
||||
if open.get() {
|
||||
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();
|
||||
div![
|
||||
C!["thread"],
|
||||
p![
|
||||
C!["is-size-4"],
|
||||
span![
|
||||
C!["read-status"],
|
||||
i![
|
||||
style! {
|
||||
St::Color => "gold"
|
||||
},
|
||||
C![
|
||||
if any_unread { "fa-regular" } else { "fa-solid" },
|
||||
"fa-star"
|
||||
],
|
||||
ev(Ev::Click, move |_| Msg::SetUnread(
|
||||
format!("thread:{}", thread_id),
|
||||
!any_unread
|
||||
)),
|
||||
],
|
||||
" ",
|
||||
],
|
||||
&thread.subject,
|
||||
" ",
|
||||
tags_chiclet(&tags, false)
|
||||
@@ -475,9 +572,6 @@ pub fn view(model: &Model) -> Node<Msg> {
|
||||
.expect("window height")
|
||||
.as_f64()
|
||||
.expect("window height f64");
|
||||
info!("win: {w}x{h}");
|
||||
|
||||
info!("view called");
|
||||
div![
|
||||
match w {
|
||||
w if w < 800. => div![C!["mobile"], mobile::view(model)],
|
||||
|
||||
Reference in New Issue
Block a user