Compare commits

..

No commits in common. "fc87fd702c164169b62b9cf45a3ba6fd30804bb3" and "42ce88d931452d73d42bb57c365b6d0b7c2d73b3" have entirely different histories.

3 changed files with 117 additions and 276 deletions

View File

@ -11,22 +11,13 @@
<link rel="icon" href="https://static.xinu.tv/favicon/letterbox.svg" /> <link rel="icon" href="https://static.xinu.tv/favicon/letterbox.svg" />
<!-- Pretty checkboxes from https://justboil.github.io/bulma-checkbox/ --> <!-- Pretty checkboxes from https://justboil.github.io/bulma-checkbox/ -->
<link data-trunk rel="css" href="static/main.css" /> <link data-trunk rel="css" href="static/main.css" />
<!-- tall thin font for user icon -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@700&display=swap" rel="stylesheet">
<style> <style>
<style>.message { .message {
display: inline-block; display: inline-block;
padding: 0.5em; padding: 0.5em;
width: 100%; width: 100%;
} }
.message .header table td {
border: 0;
padding: 0;
}
.message .headers { .message .headers {
position: relative; position: relative;
width: 100%; width: 100%;
@ -47,11 +38,11 @@
.body { .body {
background: white; background: white;
color: black; color: black;
margin-left: -0.5em;
margin-right: -0.5em;
margin-top: 0.5em; margin-top: 0.5em;
padding: 1em;
width: 0; width: 0;
min-width: 100%; min-width: 100%;
overflow-wrap: break-word;
} }
.error { .error {
@ -155,19 +146,11 @@
color: #555; color: #555;
} }
.mobile .search-results { .mobile .search-results,
.mobile .thread {
padding: 1em; padding: 1em;
} }
.mobile .thread h3 {
overflow-wrap: break-word;
padding: 1em 1em 0;
}
.mobile .thread .tags {
padding: 0 1em;
}
.search-results .row { .search-results .row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@ -150,7 +150,6 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
Msg::SetUnread(query, unread) => { Msg::SetUnread(query, unread) => {
let search_url = urls::search(&model.query, 0).to_string();
orders.skip().perform_cmd(async move { orders.skip().perform_cmd(async move {
let res: Result< let res: Result<
graphql_client::Response<graphql::mark_read_mutation::ResponseData>, graphql_client::Response<graphql::mark_read_mutation::ResponseData>,
@ -165,11 +164,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
if let Err(e) = res { if let Err(e) = res {
error!("Failed to set read for {query} to {unread}: {e}"); error!("Failed to set read for {query} to {unread}: {e}");
} }
seed::window() Msg::RefreshStart
.location()
.set_href(&search_url)
.expect("failed to change location");
Msg::Noop
}); });
} }
@ -180,6 +175,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
first, first,
last, last,
} => { } => {
info!("making FrontPageRequest: {query} after:{after:?} before:{before:?} first:{first:?} last:{last:?}");
model.query = query.clone(); model.query = query.clone();
orders.skip().perform_cmd(async move { orders.skip().perform_cmd(async move {
Msg::FrontPageResult( Msg::FrontPageResult(
@ -335,6 +331,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} = &mut model.context } = &mut model.context
{ {
selected_threads.insert(tid); selected_threads.insert(tid);
info!("selected threads {selected_threads:?}");
} }
} }
Msg::SelectionRemoveThread(tid) => { Msg::SelectionRemoveThread(tid) => {
@ -343,16 +340,19 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} = &mut model.context } = &mut model.context
{ {
selected_threads.remove(&tid); selected_threads.remove(&tid);
info!("selected threads {selected_threads:?}");
} }
} }
Msg::MessageCollapse(id) => { Msg::MessageCollapse(id) => {
if let Context::ThreadResult { open_messages, .. } = &mut model.context { if let Context::ThreadResult { open_messages, .. } = &mut model.context {
open_messages.remove(&id); open_messages.remove(&id);
info!("open_messages threads {open_messages:?}");
} }
} }
Msg::MessageExpand(id) => { Msg::MessageExpand(id) => {
if let Context::ThreadResult { open_messages, .. } = &mut model.context { if let Context::ThreadResult { open_messages, .. } = &mut model.context {
open_messages.insert(id); open_messages.insert(id);
info!("open_messages threads {open_messages:?}");
} }
} }
} }

View File

@ -5,7 +5,7 @@ use std::{
use chrono::{DateTime, Datelike, Duration, Local, Utc}; use chrono::{DateTime, Datelike, Duration, Local, Utc};
use itertools::Itertools; use itertools::Itertools;
use log::{error, info}; use log::error;
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use seed_hooks::{state_access::CloneState, topo, use_state}; use seed_hooks::{state_access::CloneState, topo, use_state};
use wasm_timer::Instant; use wasm_timer::Instant;
@ -26,15 +26,11 @@ fn set_title(title: &str) {
seed::document().set_title(&format!("lb: {}", title)); seed::document().set_title(&format!("lb: {}", title));
} }
fn compute_color(data: &str) -> String {
let mut hasher = DefaultHasher::new();
data.hash(&mut hasher);
format!("#{:06x}", hasher.finish() % (1 << 24))
}
fn tags_chiclet(tags: &[String], is_mobile: bool) -> impl Iterator<Item = Node<Msg>> + '_ { fn tags_chiclet(tags: &[String], is_mobile: bool) -> impl Iterator<Item = Node<Msg>> + '_ {
tags.iter().map(move |tag| { tags.iter().map(move |tag| {
let hex = compute_color(tag); let mut hasher = DefaultHasher::new();
tag.hash(&mut hasher);
let hex = format!("#{:06x}", hasher.finish() % (1 << 24));
let style = style! {St::BackgroundColor=>hex}; let style = style! {St::BackgroundColor=>hex};
let classes = C!["tag", IF!(is_mobile => "is-small")]; let classes = C!["tag", IF!(is_mobile => "is-small")];
let tag = tag.clone(); let tag = tag.clone();
@ -324,6 +320,21 @@ implement_email!(
ShowThreadQueryThreadMessagesFrom ShowThreadQueryThreadMessagesFrom
); );
fn view_address(email: impl Email) -> Node<Msg> {
span![
C!["tag", "is-black"],
email.addr().as_ref().map(|a| attrs! {At::Title=>a}),
email
.name()
.as_ref()
.unwrap_or(&email.addr().unwrap_or("(UNKNOWN)"))
]
}
fn view_addresses(addrs: &[impl Email]) -> Vec<Node<Msg>> {
addrs.into_iter().map(view_address).collect::<Vec<_>>()
}
fn raw_text_message(contents: &str) -> Node<Msg> { fn raw_text_message(contents: &str) -> Node<Msg> {
let (contents, truncated_msg) = if contents.len() > MAX_RAW_MESSAGE_SIZE { let (contents, truncated_msg) = if contents.len() > MAX_RAW_MESSAGE_SIZE {
( (
@ -340,247 +351,42 @@ fn has_unread(tags: &[String]) -> bool {
tags.contains(&String::from("unread")) tags.contains(&String::from("unread"))
} }
fn render_avatar(avatar: Option<String>, from: &str) -> Node<Msg> { fn read_message_render(msg: &ShowThreadQueryThreadMessages, open: bool) -> Node<Msg> {
let initials: String = from
.to_lowercase()
.split(" ")
.map(|word| word.chars().next().unwrap())
// Limit to 2 characters because more characters don't fit in the box
.take(2)
.collect();
if let Some(src) = avatar {
img![attrs! {At::Src=>src}]
} else {
let w = 64;
let h = 64;
let from_color = compute_color(from);
svg![
attrs! {At::ViewBox=>format!("0 0 {w} {h}") },
style! {
St::Display => "block",
St::FontFamily => "Poppins",
St::FontSize => pt(32),
},
g![
rect![attrs! {
At::Fill=>from_color,
At::Stroke=>"black",
At::StrokeWidth=>"1",
// Round corners
//At::Rx => px(10),
At::X => 0,
At::Y => 0,
At::Width => h,
At::Height => h,
}],
text![
attrs! {
At::Fill => "white",
At::X => percent(50),
At::Y => percent(50),
At::DominantBaseline => "middle",
At::TextAnchor => "middle"
},
initials
]
]
]
}
}
fn render_open_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
let (from, from_detail) = match &msg.from {
Some(ShowThreadQueryThreadMessagesFrom {
name: Some(name),
addr,
}) => (name.to_string(), addr.clone()),
Some(ShowThreadQueryThreadMessagesFrom {
addr: Some(addr), ..
}) => (addr.to_string(), None),
_ => (String::from("UNKNOWN"), None),
};
// TODO(wathiede): get this from server
let avatar: Option<String> = None;
//let avatar: Option<String> = Some(String::from("https://bulma.io/images/placeholders/64x64.png"));
let id = msg.id.clone(); let id = msg.id.clone();
let is_unread = has_unread(&msg.tags);
let img = render_avatar(avatar, &from);
article![
C!["media"],
figure![C!["media-left"], p![C!["image", "is-64x64"], img]],
div![
C!["media-content"],
div![
C!["content"],
p![
strong![from],
br![],
small![from_detail],
table![
IF!(!msg.to.is_empty() =>
msg.to.iter().enumerate().map(|(i, to)|
tr![
td![ if i==0 { "To" }else { "" } ],
td![
small![
match to {
ShowThreadQueryThreadMessagesTo {
name: Some(name),
addr:Some(addr),
} => format!("{name} <{addr}>"),
ShowThreadQueryThreadMessagesTo {
name: Some(name),
addr:None
} => format!("{name}"),
ShowThreadQueryThreadMessagesTo {
addr: Some(addr), ..
} => format!("<{addr}>"),
_ => String::from("UNKNOWN"),
}
]
]])),
IF!(!msg.cc.is_empty() =>
msg.cc.iter().enumerate().map(|(i, cc)|
tr![
td![ if i==0 { "CC" }else { "" } ],
td![
small![
match cc {
ShowThreadQueryThreadMessagesCc {
name: Some(name),
addr:Some(addr),
} => format!("{name} <{addr}>"),
ShowThreadQueryThreadMessagesCc {
name: Some(name),
addr:None
} => format!("{name}"),
ShowThreadQueryThreadMessagesCc {
addr: Some(addr), ..
} => format!("<{addr}>"),
_ => String::from("UNKNOWN"),
}
]
]])),
tr![
td!["Date"],
td![msg.timestamp.map(|ts| span![C!["header"], human_age(ts)])]
]
],
],
],
],
div![
C!["media-right"],
span![
C!["read-status"],
i![
C![
"far",
if is_unread {
"fa-envelope"
} else {
"fa-envelope-open"
},
],
ev(Ev::Click, move |e| {
e.stop_propagation();
Msg::SetUnread(format!("id:{id}"), !is_unread)
})
]
]
]
]
}
fn render_closed_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
let from: String = match &msg.from {
Some(ShowThreadQueryThreadMessagesFrom {
name: Some(name), ..
}) => name.to_string(),
Some(ShowThreadQueryThreadMessagesFrom {
addr: Some(addr), ..
}) => addr.to_string(),
_ => String::from("UNKNOWN"),
};
// TODO(wathiede): get this from server
let avatar: Option<String> = None;
//let avatar: Option<String> = Some(String::from("https://bulma.io/images/placeholders/64x64.png"));
let id = msg.id.clone();
let is_unread = has_unread(&msg.tags);
let img = render_avatar(avatar, &from);
article![
C!["media"],
figure![C!["media-left"], p![C!["image", "is-64x64"], img]],
div![
C!["media-content"],
div![
C!["content"],
p![
strong![from],
br![],
IF!(!msg.to.is_empty() =>
nodes![
small![" to "],
msg.to.iter().enumerate().map(|(i, to)| small![
if i > 0 { ", " } else { "" },
to.name()
.as_ref()
.unwrap_or(&to.addr().unwrap_or("(UNKNOWN)"))
]).collect::<Vec<_>>()
]),
IF!(!msg.cc.is_empty() =>
nodes![
small![" cc "],
msg.cc.iter().enumerate().map(|(i, cc)| small![
if i > 0 { ", " } else { "" },
cc.name()
.as_ref()
.unwrap_or(&cc.addr().unwrap_or("(UNKNOWN)"))
]).collect::<Vec<_>>()
]),
br![],
msg.timestamp.map(|ts| span![C!["header"], human_age(ts)]),
],
],
],
div![
C!["media-right"],
span![
C!["read-status"],
i![
C![
"far",
if is_unread {
"fa-envelope"
} else {
"fa-envelope-open"
},
],
ev(Ev::Click, move |e| {
e.stop_propagation();
Msg::SetUnread(format!("id:{id}"), !is_unread)
})
]
]
]
]
}
fn message_render(msg: &ShowThreadQueryThreadMessages, open: bool) -> Node<Msg> {
let expand_id = msg.id.clone(); let expand_id = msg.id.clone();
let is_unread = has_unread(&msg.tags);
div![ div![
C!["message"], C!["message"],
div![ div![
C!["header"], C!["headers"],
if open { span![
render_open_header(&msg) C!["read-status"],
} else { i![
render_closed_header(&msg) style! {
St::Color => "gold"
}, },
C![
"far",
if is_unread {
"fa-envelope"
} else {
"fa-envelope-open"
},
],
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| { ev(Ev::Click, move |e| {
e.stop_propagation(); e.stop_propagation();
if open { if open {
@ -588,9 +394,57 @@ fn message_render(msg: &ShowThreadQueryThreadMessages, open: bool) -> Node<Msg>
} else { } else {
Msg::MessageExpand(expand_id) Msg::MessageExpand(expand_id)
} }
}) }),
]
}
fn unread_message_render(msg: &ShowThreadQueryThreadMessages, open: bool) -> Node<Msg> {
let id = msg.id.clone();
let expand_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![
"far",
if is_unread {
"fa-envelope"
} else {
"fa-envelope-open"
},
],
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| {
e.stop_propagation();
if open {
Msg::MessageCollapse(expand_id)
} else {
Msg::MessageExpand(expand_id)
}
}),
], ],
IF!(open =>
div![ div![
C!["body"], C!["body"],
match &msg.body { match &msg.body {
@ -626,7 +480,7 @@ fn message_render(msg: &ShowThreadQueryThreadMessages, open: bool) -> Node<Msg>
view_content_tree(&content_tree), view_content_tree(&content_tree),
], ],
} }
]) ],
] ]
} }
@ -646,14 +500,18 @@ fn thread(thread: &ShowThreadQueryThread, open_messages: &HashSet<String>) -> No
tags.sort(); tags.sort();
let messages = thread.messages.iter().map(|msg| { let messages = thread.messages.iter().map(|msg| {
let open = open_messages.contains(&msg.id); let open = open_messages.contains(&msg.id);
message_render(&msg, open) if open {
unread_message_render(&msg, open)
} else {
read_message_render(&msg, open)
}
}); });
let read_thread_id = thread.thread_id.clone(); let read_thread_id = thread.thread_id.clone();
let unread_thread_id = thread.thread_id.clone(); let unread_thread_id = thread.thread_id.clone();
div![ div![
C!["thread"], C!["thread"],
h3![C!["is-size-5"], &thread.subject], h3![C!["is-size-5"], &thread.subject,],
span![C!["tags"], tags_chiclet(&tags, false)], tags_chiclet(&tags, false),
span![ span![
C!["level-item", "buttons", "has-addons"], C!["level-item", "buttons", "has-addons"],
button![ button![