web: add per-message unread control and display
This commit is contained in:
parent
ce836cd1e8
commit
516eedb086
3
web/graphql/mark_read.graphql
Normal file
3
web/graphql/mark_read.graphql
Normal file
@ -0,0 +1,3 @@
|
||||
mutation MarkReadMutation($query: String!, $unread: Boolean!) {
|
||||
setReadStatus(query:$query, unread:$unread)
|
||||
}
|
||||
@ -53,7 +53,9 @@
|
||||
"name": "skip"
|
||||
}
|
||||
],
|
||||
"mutationType": null,
|
||||
"mutationType": {
|
||||
"name": "Mutation"
|
||||
},
|
||||
"queryType": {
|
||||
"name": "QueryRoot"
|
||||
},
|
||||
@ -536,6 +538,62 @@
|
||||
"name": "Message",
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"description": null,
|
||||
"enumValues": null,
|
||||
"fields": [
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"description": null,
|
||||
"name": "query",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"defaultValue": null,
|
||||
"description": null,
|
||||
"name": "unread",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"deprecationReason": null,
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"name": "setReadStatus",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"kind": "OBJECT",
|
||||
"name": "Mutation",
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"description": "Information about pagination in a connection",
|
||||
"enumValues": null,
|
||||
@ -903,6 +961,22 @@
|
||||
"description": null,
|
||||
"enumValues": null,
|
||||
"fields": [
|
||||
{
|
||||
"args": [],
|
||||
"deprecationReason": null,
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"name": "threadId",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"deprecationReason": null,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
query ShowThreadQuery($threadId: String!) {
|
||||
thread(threadId: $threadId) {
|
||||
threadId,
|
||||
subject
|
||||
messages {
|
||||
id
|
||||
|
||||
364
web/index.html
364
web/index.html
@ -2,142 +2,175 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet", href="https://jenil.github.io/bulmaswatch/cyborg/bulmaswatch.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css" integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<link rel="icon" href="https://static.xinu.tv/favicon/letterbox.svg" />
|
||||
<style>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet" , href="https://jenil.github.io/bulmaswatch/cyborg/bulmaswatch.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css"
|
||||
integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<link rel="icon" href="https://static.xinu.tv/favicon/letterbox.svg" />
|
||||
<style>
|
||||
.message {
|
||||
display: inline-block;
|
||||
padding: 0.5em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: inline-block;
|
||||
padding: 0.5em;
|
||||
width: 100%;
|
||||
}
|
||||
.message .headers {
|
||||
width: 100%;
|
||||
}
|
||||
.message .headers .header {
|
||||
overflow: clip;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.body {
|
||||
background: white;
|
||||
color: black;
|
||||
margin-left: -0.5em;
|
||||
margin-right: -0.5em;
|
||||
margin-top: 0.5em;
|
||||
width:0;
|
||||
min-width:100%;
|
||||
}
|
||||
.error {
|
||||
background-color: red;
|
||||
}
|
||||
.view-part-text-plain {
|
||||
padding: 0.5em;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-line;
|
||||
}
|
||||
iframe {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.message .headers {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.index {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
.index .from {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 10em;
|
||||
}
|
||||
.index .subject {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.index .date {
|
||||
width: 10em;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
.lb-footer {
|
||||
background-color: #eee;
|
||||
color: #222;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3em;
|
||||
padding: 1em;
|
||||
}
|
||||
.tag {
|
||||
margin-right: 2px;
|
||||
}
|
||||
.debug ul {
|
||||
padding-left: 2em;
|
||||
}
|
||||
.debug li {
|
||||
}
|
||||
.loading {
|
||||
animation-name: spin;
|
||||
animation-duration: 1000ms;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform:rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform:rotate(360deg);
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.section {
|
||||
padding: 1.5em;
|
||||
}
|
||||
}
|
||||
input, .input {
|
||||
color: #000;
|
||||
}
|
||||
input::placeholder, .input::placeholder{
|
||||
color: #555;
|
||||
}
|
||||
.message .headers .read-status {
|
||||
position: absolute;
|
||||
right: 1em;
|
||||
top: 0em;
|
||||
}
|
||||
|
||||
.mobile .search-results,
|
||||
.mobile .thread {
|
||||
padding: 1em;
|
||||
}
|
||||
.message .headers .header {
|
||||
overflow: clip;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.search-results .row {
|
||||
border-bottom: 1px #444 solid;
|
||||
padding-bottom: .5em;
|
||||
padding-top: .5em;
|
||||
}
|
||||
.search-results .row .subject {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.search-results .row .from {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.search-results .row .tag {
|
||||
height: 1.5em;
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
}
|
||||
.float-right {
|
||||
float: right;
|
||||
}
|
||||
/* Hide quoted emails */
|
||||
/*
|
||||
.body {
|
||||
background: white;
|
||||
color: black;
|
||||
margin-left: -0.5em;
|
||||
margin-right: -0.5em;
|
||||
margin-top: 0.5em;
|
||||
width: 0;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.view-part-text-plain {
|
||||
padding: 0.5em;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
iframe {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.index {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.index .from {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
.index .subject {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.index .date {
|
||||
width: 10em;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.lb-footer {
|
||||
background-color: #eee;
|
||||
color: #222;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.debug ul {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.debug li {}
|
||||
|
||||
.loading {
|
||||
animation-name: spin;
|
||||
animation-duration: 1000ms;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.section {
|
||||
padding: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
.input {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
.input::placeholder {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.mobile .search-results,
|
||||
.mobile .thread {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.search-results .row {
|
||||
border-bottom: 1px #444 solid;
|
||||
padding-bottom: .5em;
|
||||
padding-top: .5em;
|
||||
}
|
||||
|
||||
.search-results .row .subject {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.search-results .row .from {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.search-results .row .tag {
|
||||
height: 1.5em;
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* Hide quoted emails */
|
||||
/*
|
||||
div[name="quote"],
|
||||
blockquote[type="cite"],
|
||||
.gmail_quote {
|
||||
@ -146,39 +179,46 @@ blockquote[type="cite"],
|
||||
}
|
||||
*/
|
||||
|
||||
.desktop .main-content {
|
||||
display: grid;
|
||||
grid-template-columns: 12rem 1fr;
|
||||
}
|
||||
.tags-menu {
|
||||
padding: 1rem;
|
||||
}
|
||||
.tags-menu .menu-list a {
|
||||
padding: 0.25em 0.5em;
|
||||
}
|
||||
.tags-menu .tag-indent {
|
||||
padding-left: .5em;
|
||||
}
|
||||
.tags-menu .tag-tag {
|
||||
margin-left: -1em;
|
||||
padding-right: .25em;
|
||||
}
|
||||
.navbar {
|
||||
border: none;
|
||||
}
|
||||
.desktop nav.pagination,
|
||||
.tablet nav.pagination {
|
||||
margin-left: .5em;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.content-tree {
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
.desktop .main-content {
|
||||
display: grid;
|
||||
grid-template-columns: 12rem 1fr;
|
||||
}
|
||||
|
||||
.tags-menu {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.tags-menu .menu-list a {
|
||||
padding: 0.25em 0.5em;
|
||||
}
|
||||
|
||||
.tags-menu .tag-indent {
|
||||
padding-left: .5em;
|
||||
}
|
||||
|
||||
.tags-menu .tag-tag {
|
||||
margin-left: -1em;
|
||||
padding-right: .25em;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.desktop nav.pagination,
|
||||
.tablet nav.pagination {
|
||||
margin-left: .5em;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.content-tree {
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section id="app"></section>
|
||||
<section id="app"></section>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@ -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)],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user