web: update style for rendering emails, including attachments

This commit is contained in:
Bill Thiede 2025-01-26 15:56:08 -08:00
parent 87321fb669
commit fb73d8272e
2 changed files with 125 additions and 131 deletions

View File

@ -223,7 +223,7 @@ pub async fn thread(
} }
format!( format!(
r#"<p class="view-part-text-plain">{}</p>"#, r#"<p class="view-part-text-plain font-mono whitespace-pre">{}</p>"#,
// Trim newlines to prevent excessive white space at the beginning/end of // Trim newlines to prevent excessive white space at the beginning/end of
// presenation. Leave tabs and spaces incase plain text attempts to center a // presenation. Leave tabs and spaces incase plain text attempts to center a
// header on the first line. // header on the first line.
@ -578,7 +578,7 @@ fn flatten_body_parts(parts: &[Body]) -> Body {
.map(|p| match p { .map(|p| match p {
Body::PlainText(PlainText { text, .. }) => { Body::PlainText(PlainText { text, .. }) => {
format!( format!(
r#"<p class="view-part-text-plain">{}</p>"#, r#"<p class="view-part-text-plain font-mono whitespace-pre">{}</p>"#,
// Trim newlines to prevent excessive white space at the beginning/end of // Trim newlines to prevent excessive white space at the beginning/end of
// presenation. Leave tabs and spaces incase plain text attempts to center a // presenation. Leave tabs and spaces incase plain text attempts to center a
// header on the first line. // header on the first line.

View File

@ -30,7 +30,7 @@ mod tw_classes {
]; ];
pub const TAG_X: &[&str] = &[ pub const TAG_X: &[&str] = &[
"rounded-r", "rounded-r",
"bg-slate-800", "bg-neutral-800",
"px-2", "px-2",
"py-1", "py-1",
"mr-1", "mr-1",
@ -38,17 +38,17 @@ mod tw_classes {
"[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]", "[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]",
]; ];
pub const BUTTON: &[&str] = &[ pub const BUTTON: &[&str] = &[
"bg-slate-900", "bg-neutral-900",
"rounded-md", "rounded-md",
"p-2", "p-2",
"border", "border",
"border-slate-700", "border-neutral-700",
"text-center", "text-center",
"text-sm", "text-sm",
"transition-all", "transition-all",
"shadow-md", "shadow-md",
"hover:shadow-lg", "hover:shadow-lg",
"hover:bg-slate-700", "hover:bg-neutral-700",
"disabled:pointer-events-none", "disabled:pointer-events-none",
"disabled:opacity-50", "disabled:opacity-50",
"disabled:shadow-none", "disabled:shadow-none",
@ -62,7 +62,7 @@ mod tw_classes {
"checked:appearance-auto", "checked:appearance-auto",
"rounded", "rounded",
"border", "border",
"border-gray-500", "border-neutral-500",
]; ];
} }
@ -133,7 +133,7 @@ fn search_results(
"flex-auto", "flex-auto",
"py-4", "py-4",
"border-b", "border-b",
"border-gray-800" "border-neutral-800"
], ],
div![ div![
C!["flex", "items-center", "mr-4"], C!["flex", "items-center", "mr-4"],
@ -432,13 +432,11 @@ fn search_toolbar(
pager: &FrontPageQuerySearchPageInfo, pager: &FrontPageQuerySearchPageInfo,
show_bulk_edit: bool, show_bulk_edit: bool,
) -> Node<Msg> { ) -> Node<Msg> {
info!("pager {pager:#?}");
nav![ nav![
C!["p-4", "flex", "w-full", "justify-between"], C!["p-4", "flex", "w-full", "justify-between"],
div![ div![
C!["gap-2", "flex"], C!["gap-2", "flex"],
IF!(show_bulk_edit => IF!(show_bulk_edit =>
div![
div![ div![
button![ button![
C![&tw_classes::BUTTON, "rounded-r-none"], C![&tw_classes::BUTTON, "rounded-r-none"],
@ -454,10 +452,8 @@ fn search_toolbar(
span![C!["pl-2", "hidden", "md:inline"], "Unread"], span![C!["pl-2", "hidden", "md:inline"], "Unread"],
ev(Ev::Click, |_| Msg::SelectionMarkAsUnread) ev(Ev::Click, |_| Msg::SelectionMarkAsUnread)
] ]
]
]), ]),
IF!(show_bulk_edit => IF!(show_bulk_edit =>
div![
div![ div![
button![ button![
C![&tw_classes::BUTTON, "text-red-500"], C![&tw_classes::BUTTON, "text-red-500"],
@ -470,8 +466,7 @@ fn search_toolbar(
Msg::SelectionMarkAsRead Msg::SelectionMarkAsRead
]) ])
) )
], ]
],
]), ]),
], ],
div![ div![
@ -503,7 +498,7 @@ fn raw_text_message(contents: &str) -> Node<Msg> {
(contents, None) (contents, None)
}; };
div![ div![
C!["NOTPORTED", "view-part-text-plain"], C!["view-part-text-plain", "font-mono", "whitespace-pre"],
contents, contents,
truncated_msg, truncated_msg,
] ]
@ -582,7 +577,7 @@ fn render_open_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<
let avatar = render_avatar(photo_url, &from, true); let avatar = render_avatar(photo_url, &from, true);
let unknown = "UNKNOWN".to_string(); let unknown = "UNKNOWN".to_string();
div![ div![
C!["flex", "p-4"], C!["flex", "p-4", "bg-neutral-800"],
div![avatar], div![avatar],
div![ div![
C!["px-4", "mr-auto"], C!["px-4", "mr-auto"],
@ -679,7 +674,7 @@ fn render_closed_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Nod
let avatar = render_avatar(photo_url, &from, false); let avatar = render_avatar(photo_url, &from, false);
let unknown = "UNKNOWN".to_string(); let unknown = "UNKNOWN".to_string();
div![ div![
C!["flex", "p-4"], C!["flex", "p-4", "bg-neutral-800"],
div![avatar], div![avatar],
div![ div![
C!["px-4", "mr-auto"], C!["px-4", "mr-auto"],
@ -754,6 +749,7 @@ fn render_closed_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Nod
fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool) -> Node<Msg> { fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool) -> Node<Msg> {
let expand_id = msg.id.clone(); let expand_id = msg.id.clone();
div![ div![
C!["lg:mb-4"],
div![ div![
if open { if open {
render_open_header(&msg) render_open_header(&msg)
@ -771,13 +767,13 @@ fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool)
], ],
IF!(open => IF!(open =>
div![ div![
C![], C!["bg-white", "text-black", "p-4", "min-w-full", "w-0","overflow-x-auto"],
match &msg.body { match &msg.body {
ShowThreadQueryThreadOnEmailThreadMessagesBody::UnhandledContentType( ShowThreadQueryThreadOnEmailThreadMessagesBody::UnhandledContentType(
ShowThreadQueryThreadOnEmailThreadMessagesBodyOnUnhandledContentType { contents ,content_tree}, ShowThreadQueryThreadOnEmailThreadMessagesBodyOnUnhandledContentType { contents ,content_tree},
) => div![ ) => div![
raw_text_message(&contents), raw_text_message(&contents),
div![C!["NOTPORTED","error"], div![C!["bg-red-500","p-4", "min-w-full", "w-0", "overflow-x-auto"],
view_content_tree(&content_tree), view_content_tree(&content_tree),
] ]
], ],
@ -796,17 +792,25 @@ fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool)
content_tree, content_tree,
}, },
) => div![ ) => div![
C!["NOTPORTED","view-part-text-html"], C!["view-part-text-html"],
raw![contents], raw![contents],
IF!(!msg.attachments.is_empty() => IF!(!msg.attachments.is_empty() => render_attachements(&msg.attachments)),
view_content_tree(&content_tree),
],
}
])
]
}
fn render_attachements(
attachments: &[ShowThreadQueryThreadOnEmailThreadMessagesAttachments],
) -> Node<Msg> {
div![ div![
C!["NOTPORTED","attachments"], C!["border-t", "border-neutral-200", "mt-2", "pt-2"],
hr![], h2![C!["text-lg"], "Attachments"],
h2!["NOTPORTED","Attachments"], div![
div![C!["NOTPORTED","grid","is-col-min-6"], C!["flex", "flex-wrap", "gap-2"],
msg.attachments attachments.iter().map(|a| {
.iter()
.map(|a| {
let default = "UNKNOWN_FILE".to_string(); let default = "UNKNOWN_FILE".to_string();
let filename = a.filename.as_ref().unwrap_or(&default); let filename = a.filename.as_ref().unwrap_or(&default);
let host = seed::window().location().host().expect("couldn't get host"); let host = seed::window().location().host().expect("couldn't get host");
@ -816,43 +820,49 @@ fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool)
fmtr.with_scales(Scales::Binary()); fmtr.with_scales(Scales::Binary());
div![ div![
C!["NOTPORTED","attachment", "card"], C![
a.content_type.as_ref().map(|content_type| "flex",
IF!(content_type.starts_with("image/") => "flex-col",
div![C!["NOTPORTED","card-image","is-1by1"], "flex-none",
div![ "p-2",
C!["NOTPORTED","image","is-1by1"], "bg-neutral-200",
style!{ "border",
St::BackgroundImage=>format!(r#"url("{url}");"#), "border-neutral-400",
St::BackgroundSize=>"cover",
St::BackgroundPosition=>"center",
}
]
]
)),
div![C!["NOTPORTED","card-content"],
div![C!["NOTPORTED","content"],
&a.filename, br![],
small![ fmtr.format(a.size as f64),"B"]
]
], ],
footer![ a.content_type.as_ref().map(
C!["NOTPORTED","card-footer"], |content_type| IF!(content_type.starts_with("image/") => img![
a![C!["NOTPORTED","card-footer-item"],span![C!["NOTPORTED","icon"], i![C!["NOTPORTED","fas", "fa-download"]]], C!["w-32", "h-32", "md:w-64", "md:h-64", "object-cover"],
ev(Ev::Click, move |_| { attrs!{At::Src=>url},
seed::window().location().set_href(&url
).expect("failed to set URL");
})
]
]
]
})
]
]),
view_content_tree(&content_tree),
],
}
]) ])
),
div![
C!["py-2", "flex", "flex-nowrap", "items-center"],
div![
C!["flex", "flex-col", "grow", "w-16", "md:w-32"],
span![C!["shrink", "truncate"], &a.filename],
span![C!["text-xs"], fmtr.format(a.size as f64), "B"]
],
a![
C![
"aspect-square",
"px-2",
"pt-1",
"bg-neutral-300",
"border",
"border-neutral-400"
],
span![i![C!["fas", "fa-download"]]],
ev(Ev::Click, move |_| {
seed::window()
.location()
.set_href(&url)
.expect("failed to set URL");
})
]
]
]
})
]
] ]
} }
@ -862,8 +872,6 @@ fn thread(
open_messages: &HashSet<String>, open_messages: &HashSet<String>,
content_el: &ElRef<HtmlElement>, content_el: &ElRef<HtmlElement>,
) -> Node<Msg> { ) -> Node<Msg> {
// TODO remove and replace with CSS styling
let show_icon_text = true;
// 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
let subject = if thread.subject.is_empty() { let subject = if thread.subject.is_empty() {
"(no subject)" "(no subject)"
@ -890,68 +898,49 @@ fn thread(
let spam_add_thread_id = thread.thread_id.clone(); let spam_add_thread_id = thread.thread_id.clone();
let spam_unread_thread_id = thread.thread_id.clone(); let spam_unread_thread_id = thread.thread_id.clone();
div![ div![
C!["p-4"], C!["lg:p-4"],
div![
C!["p-4", "lg:p-0"],
h3![C!["text-xl"], subject], h3![C!["text-xl"], subject],
span![ span![removable_tags_chiclet(&thread.thread_id, &tags, false)],
C!["NOTPORTED", "tags"],
removable_tags_chiclet(&thread.thread_id, &tags, false)
],
div![ div![
C!["NOTPORTED", "level", "is-mobile"], C!["pt-4", "gap-2", "flex", "justify-around"],
div![ div![
C!["NOTPORTED", "level-item"],
div![
C!["NOTPORTED", "buttons", "has-addons"],
button![ button![
C!["NOTPORTED", "button", "mark-read"], C![&tw_classes::BUTTON, "rounded-r-none"],
attrs! {At::Title => "Mark as read"}, attrs! {At::Title => "Mark as read"},
span![ span![i![C!["far", "fa-envelope-open"]]],
C!["NOTPORTED", "icon", "is-small"], span![C!["pl-2", "hidden", "md:inline"], "Read"],
i![C!["NOTPORTED", "far", "fa-envelope-open"]]
],
IF!(show_icon_text=>span!["Read"]),
ev(Ev::Click, move |_| Msg::MultiMsg(vec![ ev(Ev::Click, move |_| Msg::MultiMsg(vec![
Msg::SetUnread(read_thread_id, false), Msg::SetUnread(read_thread_id, false),
Msg::GoToSearchResults Msg::GoToSearchResults
])), ])),
], ],
button![ button![
C!["NOTPORTED", "button", "mark-unread"], C![&tw_classes::BUTTON, "rounded-l-none"],
attrs! {At::Title => "Mark as unread"}, attrs! {At::Title => "Mark as unread"},
span![ span![i![C!["far", "fa-envelope"]]],
C!["NOTPORTED", "icon", "is-small"], span![C!["pl-2", "hidden", "md:inline"], "Unread"],
i![C!["NOTPORTED", "far", "fa-envelope"]]
],
IF!(show_icon_text=>span!["Unread"]),
ev(Ev::Click, move |_| Msg::MultiMsg(vec![ ev(Ev::Click, move |_| Msg::MultiMsg(vec![
Msg::SetUnread(unread_thread_id, true), Msg::SetUnread(unread_thread_id, true),
Msg::GoToSearchResults Msg::GoToSearchResults
])), ])),
], ],
], ],
], div![button![
div![ C![&tw_classes::BUTTON, "text-red-500"],
C!["NOTPORTED", "level-item"],
div![
C!["NOTPORTED", "buttons", "has-addons"],
button![
C!["NOTPORTED", "button", "spam"],
attrs! {At::Title => "Spam"}, attrs! {At::Title => "Spam"},
span![ span![i![C!["far", "fa-hand"]]],
C!["NOTPORTED", "icon", "is-small"], span![C!["pl-2", "hidden", "md:inline"], "Spam"],
i![C!["NOTPORTED", "far", "fa-hand"]]
],
IF!(show_icon_text=>span!["Spam"]),
ev(Ev::Click, move |_| Msg::MultiMsg(vec![ ev(Ev::Click, move |_| Msg::MultiMsg(vec![
Msg::AddTag(spam_add_thread_id, "Spam".to_string()), Msg::AddTag(spam_add_thread_id, "Spam".to_string()),
Msg::SetUnread(spam_unread_thread_id, false), Msg::SetUnread(spam_unread_thread_id, false),
Msg::GoToSearchResults Msg::GoToSearchResults
])), ])),
]]
], ],
], ],
], div![C!["lg:mt-4"], el_ref(content_el), messages, click_to_top()],
],
div![el_ref(content_el), messages, click_to_top()],
/* TODO(wathiede): plumb in orignal id /* TODO(wathiede): plumb in orignal id
a![ a![
attrs! {At::Href=>api::original(&thread_node.0.as_ref().expect("message missing").id)}, attrs! {At::Href=>api::original(&thread_node.0.as_ref().expect("message missing").id)},
@ -998,7 +987,7 @@ fn view_header(
}; };
let query = Url::decode_uri_component(query).unwrap_or("".to_string()); let query = Url::decode_uri_component(query).unwrap_or("".to_string());
nav![ nav![
C!["flex", "p-4"], C!["flex", "px-4", "pt-4"],
a![ a![
C![IF![is_error => "bg-red-500"], "rounded-r-none"], C![IF![is_error => "bg-red-500"], "rounded-r-none"],
C![&tw_classes::BUTTON], C![&tw_classes::BUTTON],
@ -1081,7 +1070,7 @@ pub fn tags(model: &Model) -> Node<Msg> {
"self-center", "self-center",
"justify-self-end", "justify-self-end",
"text-sm", "text-sm",
"text-gray-400" "text-neutral-400"
], ],
IF!(t.unread>0 => format!("{}", t.unread)) IF!(t.unread>0 => format!("{}", t.unread))
], ],
@ -1305,7 +1294,7 @@ fn render_news_post_header(post: &ShowThreadQueryThreadOnNewsPost) -> Node<Msg>
div![ div![
C!["px-4", "mr-auto"], C!["px-4", "mr-auto"],
div![ div![
span![C!["font-semibold", "text-sm"], from,], span![C!["font-semibold", "text-sm"], from],
div![ div![
C!["text-xs"], C!["text-xs"],
small![a![ small![a![
@ -1341,15 +1330,23 @@ fn render_news_post_header(post: &ShowThreadQueryThreadOnNewsPost) -> Node<Msg>
} }
fn reading_progress(ratio: f64) -> Node<Msg> { fn reading_progress(ratio: f64) -> Node<Msg> {
let percent = ratio * 100.; let percent = ratio * 100.;
// TODO: percent broken with no styles
info!("percent {percent}"); info!("percent {percent}");
progress![ div![
C!["absolute", "w-screen", IF!(percent<1. => "hidden")], C![
attrs! { "fixed",
At::Value=>percent, "top-0",
At::Max=>"100" "left-0",
}, "w-screen",
format!("{percent}%") "h-1",
"bg-gray-200",
IF!(percent<1. => "hidden")
],
div![
C!["h-1", "bg-green-500"],
style! {
St::Width => format!("{}%", percent)
}
]
] ]
} }
pub fn versions(versions: &crate::state::Version) -> Node<Msg> { pub fn versions(versions: &crate::state::Version) -> Node<Msg> {
@ -1372,12 +1369,9 @@ pub fn versions(versions: &crate::state::Version) -> Node<Msg> {
fn click_to_top() -> Node<Msg> { fn click_to_top() -> Node<Msg> {
button![ button![
C!["NOTPORTED", "button", "is-danger", "is-small"], C![&tw_classes::BUTTON, "bg-red-500", "lg:m-0", "m-4"],
span!["Top"], span!["Top"],
span![ span![i![C!["fas", "fa-arrow-turn-up"]]],
C!["NOTPORTED", "icon"],
i![C!["NOTPORTED", "fas", "fa-arrow-turn-up"]]
],
ev(Ev::Click, |_| web_sys::window() ev(Ev::Click, |_| web_sys::window()
.unwrap() .unwrap()
.scroll_to_with_x_and_y(0., 0.)) .scroll_to_with_x_and_y(0., 0.))