web: add per-message unread control and display

This commit is contained in:
2024-02-11 20:29:49 -08:00
parent ce836cd1e8
commit 516eedb086
7 changed files with 462 additions and 221 deletions

View File

@@ -20,6 +20,14 @@ pub struct FrontPageQuery;
)]
pub struct ShowThreadQuery;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "graphql/schema.json",
query_path = "graphql/mark_read.graphql",
response_derives = "Debug"
)]
pub struct MarkReadMutation;
pub async fn send_graphql<Body, Resp>(body: Body) -> Result<graphql_client::Response<Resp>, Error>
where
Body: Serialize,

View File

@@ -217,6 +217,25 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
orders.request_url(urls::search(&query, 0));
}
Msg::SetUnread(query, unread) => {
orders.skip().perform_cmd(async move {
let res: Result<
graphql_client::Response<graphql::mark_read_mutation::ResponseData>,
gloo_net::Error,
> = send_graphql(graphql::MarkReadMutation::build_query(
graphql::mark_read_mutation::Variables {
query: query.clone(),
unread,
},
))
.await;
if let Err(e) = res {
error!("Failed to set read for {query} to {unread}: {e}");
}
Msg::RefreshStart
});
}
Msg::FrontPageRequest {
query,
after,
@@ -380,6 +399,8 @@ pub enum Msg {
UpdateQuery(String),
SearchQuery(String),
SetUnread(String, bool),
FrontPageRequest {
query: String,
after: Option<String>,

View File

@@ -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)],