web: add per-message unread control and display
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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