Compare commits

...

4 Commits

Author SHA1 Message Date
8977f8bab5 chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 40s
Continuous integration / Test Suite (push) Successful in 43s
Continuous integration / Trunk (push) Successful in 38s
Continuous integration / Rustfmt (push) Successful in 30s
Continuous integration / build (push) Successful in 49s
Continuous integration / Disallow unused dependencies (push) Successful in 2m39s
2025-02-25 13:51:38 -08:00
0962a6b3cf web: improve scroll-to-top behavior 2025-02-25 13:51:11 -08:00
3c72929a4f web: enable properly styled buttons 2025-02-25 10:26:16 -08:00
e4eb495a70 web: properly exit catchup mode when done 2025-02-25 10:25:28 -08:00
7 changed files with 97 additions and 58 deletions

38
Cargo.lock generated
View File

@ -1552,7 +1552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -2886,7 +2886,7 @@ checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37"
dependencies = [ dependencies = [
"hermit-abi 0.4.0", "hermit-abi 0.4.0",
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -2965,7 +2965,7 @@ dependencies = [
[[package]] [[package]]
name = "letterbox-notmuch" name = "letterbox-notmuch"
version = "0.9.2" version = "0.9.3"
dependencies = [ dependencies = [
"itertools 0.14.0", "itertools 0.14.0",
"log", "log",
@ -2980,14 +2980,14 @@ dependencies = [
[[package]] [[package]]
name = "letterbox-procmail2notmuch" name = "letterbox-procmail2notmuch"
version = "0.9.2" version = "0.9.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
] ]
[[package]] [[package]]
name = "letterbox-server" name = "letterbox-server"
version = "0.9.2" version = "0.9.3"
dependencies = [ dependencies = [
"ammonia", "ammonia",
"anyhow", "anyhow",
@ -3030,7 +3030,7 @@ dependencies = [
[[package]] [[package]]
name = "letterbox-shared" name = "letterbox-shared"
version = "0.9.2" version = "0.9.3"
dependencies = [ dependencies = [
"build-info", "build-info",
"letterbox-notmuch", "letterbox-notmuch",
@ -3039,7 +3039,7 @@ dependencies = [
[[package]] [[package]]
name = "letterbox-web" name = "letterbox-web"
version = "0.9.2" version = "0.9.3"
dependencies = [ dependencies = [
"build-info", "build-info",
"build-info-build", "build-info-build",
@ -3057,6 +3057,7 @@ dependencies = [
"seed_hooks", "seed_hooks",
"serde", "serde",
"serde_json", "serde_json",
"strum_macros 0.27.1",
"thiserror 2.0.11", "thiserror 2.0.11",
"uuid", "uuid",
"wasm-bindgen", "wasm-bindgen",
@ -4446,7 +4447,7 @@ dependencies = [
"once_cell", "once_cell",
"socket2", "socket2",
"tracing", "tracing",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -5129,7 +5130,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -5962,7 +5963,7 @@ version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [ dependencies = [
"strum_macros", "strum_macros 0.26.4",
] ]
[[package]] [[package]]
@ -5978,6 +5979,19 @@ dependencies = [
"syn 2.0.98", "syn 2.0.98",
] ]
[[package]]
name = "strum_macros"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.98",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "1.0.0" version = "1.0.0"
@ -6205,7 +6219,7 @@ dependencies = [
"getrandom 0.3.1", "getrandom 0.3.1",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -7281,7 +7295,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]

View File

@ -8,7 +8,7 @@ authors = ["Bill Thiede <git@xinu.tv>"]
edition = "2021" edition = "2021"
license = "UNLICENSED" license = "UNLICENSED"
publish = ["xinu"] publish = ["xinu"]
version = "0.9.2" version = "0.9.3"
repository = "https://git.z.xinu.tv/wathiede/letterbox" repository = "https://git.z.xinu.tv/wathiede/letterbox"
[profile.dev] [profile.dev]

View File

@ -48,8 +48,8 @@ urlencoding = "2.1.3"
#xtracing = { path = "../../xtracing" } #xtracing = { path = "../../xtracing" }
#xtracing = { git = "http://git-private.h.xinu.tv/wathiede/xtracing.git" } #xtracing = { git = "http://git-private.h.xinu.tv/wathiede/xtracing.git" }
xtracing = { version = "0.3.0", registry = "xinu" } xtracing = { version = "0.3.0", registry = "xinu" }
letterbox-notmuch = { version = "0.9.2", path = "../notmuch", registry = "xinu" } letterbox-notmuch = { version = "0.9.3", path = "../notmuch", registry = "xinu" }
letterbox-shared = { version = "0.9.2", path = "../shared", registry = "xinu" } letterbox-shared = { version = "0.9.3", path = "../shared", registry = "xinu" }
[build-dependencies] [build-dependencies]
build-info-build = "0.0.39" build-info-build = "0.0.39"

View File

@ -12,5 +12,5 @@ version.workspace = true
[dependencies] [dependencies]
build-info = "0.0.39" build-info = "0.0.39"
letterbox-notmuch = { version = "0.9.2", path = "../notmuch", registry = "xinu" } letterbox-notmuch = { version = "0.9.3", path = "../notmuch", registry = "xinu" }
serde = { version = "1.0.147", features = ["derive"] } serde = { version = "1.0.147", features = ["derive"] }

View File

@ -33,9 +33,10 @@ wasm-bindgen = "=0.2.100"
uuid = { version = "1.13.1", features = [ uuid = { version = "1.13.1", features = [
"js", "js",
] } # direct dep to set js feature, prevents Rng issues ] } # direct dep to set js feature, prevents Rng issues
letterbox-shared = { version = "0.9.2", path = "../shared", registry = "xinu" } letterbox-shared = { version = "0.9.3", path = "../shared", registry = "xinu" }
letterbox-notmuch = { version = "0.9.2", path = "../notmuch", registry = "xinu" } letterbox-notmuch = { version = "0.9.3", path = "../notmuch", registry = "xinu" }
seed_hooks = { version = "0.4.0", registry = "xinu" } seed_hooks = { version = "0.4.0", registry = "xinu" }
strum_macros = "0.27.1"
[package.metadata.wasm-pack.profile.release] [package.metadata.wasm-pack.profile.release]
wasm-opt = ['-Os'] wasm-opt = ['-Os']

View File

@ -116,6 +116,7 @@ fn on_url_changed(uc: subs::UrlChanged) -> Msg {
// `update` describes how to handle each `Msg`. // `update` describes how to handle each `Msg`.
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
debug!("update({})", msg);
match msg { match msg {
Msg::Noop => {} Msg::Noop => {}
Msg::RefreshStart => { Msg::RefreshStart => {
@ -191,10 +192,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}; };
} }
Msg::GoToSearchResults => { Msg::GoToSearchResults => {
let url = urls::search(&model.query, 0); orders.send_msg(Msg::SearchQuery(model.query.clone()));
info!("GoToSearchResults Start");
orders.request_url(url);
info!("GoToSearchResults End");
} }
Msg::UpdateQuery(query) => model.query = query, Msg::UpdateQuery(query) => model.query = query,
@ -605,6 +603,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
format!("{} is:unread", model.query) format!("{} is:unread", model.query)
}; };
info!("starting catchup mode w/ {}", query); info!("starting catchup mode w/ {}", query);
orders.send_msg(Msg::ScrollToTop);
orders.send_msg(Msg::CatchupRequest { query }); orders.send_msg(Msg::CatchupRequest { query });
} }
Msg::CatchupKeepUnread => { Msg::CatchupKeepUnread => {
@ -626,19 +625,24 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
return; return;
}; };
let Some(idx) = catchup.items.iter().position(|i| !i.seen) else { let Some(idx) = catchup.items.iter().position(|i| !i.seen) else {
// All items have been seen
orders.send_msg(Msg::CatchupExit);
orders.send_msg(Msg::GoToSearchResults); orders.send_msg(Msg::GoToSearchResults);
return; return;
}; };
catchup.items[idx].seen = true; catchup.items[idx].seen = true;
if idx < catchup.items.len() - 1 { if idx < catchup.items.len() - 1 {
// Reached last item
orders.request_url(urls::thread(&catchup.items[idx + 1].id)); orders.request_url(urls::thread(&catchup.items[idx + 1].id));
return; return;
} else { } else {
orders.send_msg(Msg::CatchupExit);
orders.send_msg(Msg::GoToSearchResults); orders.send_msg(Msg::GoToSearchResults);
return; return;
}; };
} }
Msg::CatchupExit => { Msg::CatchupExit => {
orders.send_msg(Msg::ScrollToTop);
model.catchup = None; model.catchup = None;
} }
} }
@ -730,6 +734,7 @@ pub enum RefreshingState {
Error(String), Error(String),
} }
// `Msg` describes the different events you can modify state with. // `Msg` describes the different events you can modify state with.
#[derive(strum_macros::Display)]
pub enum Msg { pub enum Msg {
Noop, Noop,
// Tell the client to refresh its state // Tell the client to refresh its state

View File

@ -21,6 +21,8 @@ use crate::{
const MAX_RAW_MESSAGE_SIZE: usize = 100_000; const MAX_RAW_MESSAGE_SIZE: usize = 100_000;
mod tw_classes { mod tw_classes {
use seed::{prelude::*, *};
pub const TAG: &[&str] = &[ pub const TAG: &[&str] = &[
"rounded-md", "rounded-md",
"px-2", "px-2",
@ -37,22 +39,30 @@ mod tw_classes {
"text-xs", "text-xs",
"[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]", "[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]",
]; ];
pub const BUTTON: &[&str] = &[
"bg-neutral-900", // TODO: should this be a builder pattern?
"rounded-md", pub fn button() -> seed::Attrs {
"p-2", button_with_color("bg-neutral-900", "hover:bg-neutral-700")
"border", }
"border-neutral-700",
"text-center", pub fn button_with_color<T: ToClasses>(bg: T, hover: T) -> seed::Attrs {
"text-sm", C![
"transition-all", "rounded-md",
"shadow-md", "p-2",
"hover:shadow-lg", "border",
"hover:bg-neutral-700", "border-neutral-700",
"disabled:pointer-events-none", "text-center",
"disabled:opacity-50", "text-sm",
"disabled:shadow-none", "transition-all",
]; "shadow-md",
"hover:shadow-lg",
"disabled:pointer-events-none",
"disabled:opacity-50",
"disabled:shadow-none",
bg,
hover,
]
}
pub const CHECKBOX: &[&str] = &[ pub const CHECKBOX: &[&str] = &[
"w-8", "w-8",
@ -216,13 +226,13 @@ fn catchup_view(
"bg-black", "bg-black",
], ],
button![ button![
C![&tw_classes::BUTTON], tw_classes::button(),
span![i![C!["far", "fa-envelope"]]], span![i![C!["far", "fa-envelope"]]],
span![C!["pl-2"], "Keep unread"], span![C!["pl-2"], "Keep unread"],
ev(Ev::Click, move |_| Msg::CatchupKeepUnread) ev(Ev::Click, move |_| Msg::CatchupKeepUnread)
], ],
button![ button![
C![&tw_classes::BUTTON, "bg-green-500"], tw_classes::button_with_color("bg-green-800", "hover:bg-neutral-700"),
span![i![C!["far", "fa-envelope-open"]]], span![i![C!["far", "fa-envelope-open"]]],
span![C!["pl-2"], "Mark as read"], span![C!["pl-2"], "Mark as read"],
ev(Ev::Click, move |_| Msg::CatchupMarkAsRead) ev(Ev::Click, move |_| Msg::CatchupMarkAsRead)
@ -447,7 +457,7 @@ fn search_toolbar(
div![ div![
C!["gap-2", "flex", IF!(show_bulk_edit => "hidden")], C!["gap-2", "flex", IF!(show_bulk_edit => "hidden")],
div![button![ div![button![
C![&tw_classes::BUTTON], tw_classes::button(),
attrs! {At::Title => "Mark as read"}, attrs! {At::Title => "Mark as read"},
span![i![C!["far", "fa-eye"]]], span![i![C!["far", "fa-eye"]]],
span![C!["pl-2", "hidden", "md:inline"], "Catch-up"], span![C!["pl-2", "hidden", "md:inline"], "Catch-up"],
@ -476,14 +486,16 @@ fn search_toolbar(
], ],
div![ div![
button![ button![
C![&tw_classes::BUTTON, "rounded-r-none"], tw_classes::button(),
C!["rounded-r-none"],
attrs! {At::Title => "Mark as read"}, attrs! {At::Title => "Mark as read"},
span![i![C!["far", "fa-envelope-open"]]], span![i![C!["far", "fa-envelope-open"]]],
span![C!["pl-2", "hidden", "md:inline"], "Read"], span![C!["pl-2", "hidden", "md:inline"], "Read"],
ev(Ev::Click, |_| Msg::SelectionMarkAsRead) ev(Ev::Click, |_| Msg::SelectionMarkAsRead)
], ],
button![ button![
C![&tw_classes::BUTTON, "rounded-l-none"], tw_classes::button(),
C!["rounded-l-none"],
attrs! {At::Title => "Mark as unread"}, attrs! {At::Title => "Mark as unread"},
span![i![C!["far", "fa-envelope"]]], span![i![C!["far", "fa-envelope"]]],
span![C!["pl-2", "hidden", "md:inline"], "Unread"], span![C!["pl-2", "hidden", "md:inline"], "Unread"],
@ -491,7 +503,8 @@ fn search_toolbar(
] ]
], ],
div![button![ div![button![
C![&tw_classes::BUTTON, "text-red-500"], tw_classes::button(),
C!["text-red-500"],
attrs! {At::Title => "Mark as spam"}, attrs! {At::Title => "Mark as spam"},
span![i![C!["far", "fa-hand"]]], span![i![C!["far", "fa-hand"]]],
span![C!["pl-2", "hidden", "md:inline"], "Spam"], span![C!["pl-2", "hidden", "md:inline"], "Spam"],
@ -505,13 +518,13 @@ fn search_toolbar(
C!["flex", "gap-2", "items-center"], C!["flex", "gap-2", "items-center"],
p![format!("{count} results")], p![format!("{count} results")],
button![ button![
C![&tw_classes::BUTTON], tw_classes::button(),
IF!(!pager.has_previous_page => attrs!{ At::Disabled=>true }), IF!(!pager.has_previous_page => attrs!{ At::Disabled=>true }),
"<", "<",
IF!(pager.has_previous_page => ev(Ev::Click, |_| Msg::PreviousPage)), IF!(pager.has_previous_page => ev(Ev::Click, |_| Msg::PreviousPage)),
], ],
button![ button![
C![&tw_classes::BUTTON], tw_classes::button(),
IF!(!pager.has_next_page => attrs!{ At::Disabled=>true }), IF!(!pager.has_next_page => attrs!{ At::Disabled=>true }),
">", ">",
IF!(pager.has_next_page => ev(Ev::Click, |_| Msg::NextPage)) IF!(pager.has_next_page => ev(Ev::Click, |_| Msg::NextPage))
@ -1076,7 +1089,8 @@ fn thread(
C!["pt-4", "gap-2", "flex", "justify-around"], C!["pt-4", "gap-2", "flex", "justify-around"],
div![ div![
button![ button![
C![&tw_classes::BUTTON, "rounded-r-none"], tw_classes::button(),
C!["rounded-r-none"],
attrs! {At::Title => "Mark as read"}, attrs! {At::Title => "Mark as read"},
span![i![C!["far", "fa-envelope-open"]]], span![i![C!["far", "fa-envelope-open"]]],
span![C!["pl-2"], "Read"], span![C!["pl-2"], "Read"],
@ -1086,7 +1100,8 @@ fn thread(
])), ])),
], ],
button![ button![
C![&tw_classes::BUTTON, "rounded-l-none"], tw_classes::button(),
C!["rounded-l-none"],
attrs! {At::Title => "Mark as unread"}, attrs! {At::Title => "Mark as unread"},
span![i![C!["far", "fa-envelope"]]], span![i![C!["far", "fa-envelope"]]],
span![C!["pl-2"], "Unread"], span![C!["pl-2"], "Unread"],
@ -1097,7 +1112,8 @@ fn thread(
], ],
], ],
div![button![ div![button![
C![&tw_classes::BUTTON, "text-red-500"], tw_classes::button(),
C!["text-red-500"],
attrs! {At::Title => "Spam"}, attrs! {At::Title => "Spam"},
span![i![C!["far", "fa-hand"]]], span![i![C!["far", "fa-hand"]]],
span![C!["pl-2"], "Spam"], span![C!["pl-2"], "Spam"],
@ -1163,7 +1179,7 @@ fn view_header(
C!["flex", "px-4", "pt-4", "overflow-hidden"], C!["flex", "px-4", "pt-4", "overflow-hidden"],
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], tw_classes::button(),
span![i![C![ span![i![C![
"fa-solid", "fa-solid",
"fa-arrow-rotate-right", "fa-arrow-rotate-right",
@ -1172,7 +1188,7 @@ fn view_header(
ev(Ev::Click, |_| Msg::RefreshStart), ev(Ev::Click, |_| Msg::RefreshStart),
], ],
a![ a![
C![&tw_classes::BUTTON], tw_classes::button(),
C!["px-4", "rounded-none"], C!["px-4", "rounded-none"],
attrs! { attrs! {
At::Href => urls::search(unread_query(), 0) At::Href => urls::search(unread_query(), 0)
@ -1180,7 +1196,7 @@ fn view_header(
"Unread", "Unread",
], ],
a![ a![
C![&tw_classes::BUTTON], tw_classes::button(),
C!["px-4", "rounded-none"], C!["px-4", "rounded-none"],
attrs! { attrs! {
At::Href => urls::search("", 0) At::Href => urls::search("", 0)
@ -1367,7 +1383,8 @@ fn news_post(post: &ShowThreadQueryThreadOnNewsPost, content_el: &ElRef<HtmlElem
C!["pt-4", "gap-2", "flex", "justify-around"], C!["pt-4", "gap-2", "flex", "justify-around"],
div![ div![
button![ button![
C![&tw_classes::BUTTON, "rounded-r-none"], tw_classes::button(),
C!["rounded-r-none"],
attrs! {At::Title => "Mark as read"}, attrs! {At::Title => "Mark as read"},
span![i![C!["far", "fa-envelope-open"]]], span![i![C!["far", "fa-envelope-open"]]],
span![C!["pl-2"], "Read"], span![C!["pl-2"], "Read"],
@ -1377,7 +1394,8 @@ fn news_post(post: &ShowThreadQueryThreadOnNewsPost, content_el: &ElRef<HtmlElem
])), ])),
], ],
button![ button![
C![&tw_classes::BUTTON, "rounded-l-none"], tw_classes::button(),
C!["rounded-l-none"],
attrs! {At::Title => "Mark as unread"}, attrs! {At::Title => "Mark as unread"},
span![i![C!["far", "fa-envelope"]]], span![i![C!["far", "fa-envelope"]]],
span![C!["pl-2"], "Unread"], span![C!["pl-2"], "Unread"],
@ -1452,7 +1470,7 @@ fn render_news_post_header(post: &ShowThreadQueryThreadOnNewsPost) -> Node<Msg>
div![ div![
C!["flex", "gap-2", "pt-2", "text-sm"], C!["flex", "gap-2", "pt-2", "text-sm"],
a![ a![
C![&tw_classes::BUTTON], tw_classes::button(),
attrs! { attrs! {
At::Href => post.url, At::Href => post.url,
At::Target => "_blank", At::Target => "_blank",
@ -1461,7 +1479,7 @@ fn render_news_post_header(post: &ShowThreadQueryThreadOnNewsPost) -> Node<Msg>
i![C!["fas", "fa-up-right-from-square"]], i![C!["fas", "fa-up-right-from-square"]],
], ],
a![ a![
C![&tw_classes::BUTTON], tw_classes::button(),
attrs! { attrs! {
At::Href => add_archive_url, At::Href => add_archive_url,
At::Target => "_blank", At::Target => "_blank",
@ -1470,7 +1488,7 @@ fn render_news_post_header(post: &ShowThreadQueryThreadOnNewsPost) -> Node<Msg>
i![C!["fas", "fa-plus"]], i![C!["fas", "fa-plus"]],
], ],
a![ a![
C![&tw_classes::BUTTON], tw_classes::button(),
attrs! { attrs! {
At::Href => view_archive_url, At::Href => view_archive_url,
At::Target => "_blank", At::Target => "_blank",
@ -1537,7 +1555,8 @@ pub fn view_versions(versions: &Version) -> Node<Msg> {
fn click_to_top() -> Node<Msg> { fn click_to_top() -> Node<Msg> {
button![ button![
C![&tw_classes::BUTTON, "bg-red-500", "lg:m-0", "m-4"], tw_classes::button_with_color("bg-red-500", "hover:bg-neutral-700"),
C!["lg:m-0", "m-4"],
span!["Top"], span!["Top"],
span![i![C!["fas", "fa-arrow-turn-up"]]], span![i![C!["fas", "fa-arrow-turn-up"]]],
ev(Ev::Click, |_| Msg::ScrollToTop) ev(Ev::Click, |_| Msg::ScrollToTop)