Compare commits
88 Commits
letterbox-
...
letterbox-
| Author | SHA1 | Date | |
|---|---|---|---|
| e01dabe6ed | |||
| ecaf0dd0fc | |||
| 3d4dcc9e6b | |||
| 28a5d9f219 | |||
| 81876d37ea | |||
| 4a6b159ddb | |||
| d84957cc8c | |||
| d53db5b49a | |||
| 0448368011 | |||
| 36754136fd | |||
| 489acccf77 | |||
| 8ef4db63ad | |||
| 9f63205ff3 | |||
| 5a0378948d | |||
| 2b4c45be74 | |||
| 147896dc80 | |||
| 1ff6ec7653 | |||
| acd590111e | |||
| b5f24ba1f2 | |||
| 79ed24135f | |||
| a4949a25b5 | |||
| f16edef124 | |||
| 2fd6479cb9 | |||
| 85a6b3a9a4 | |||
| 9ac5216d6e | |||
| 82987dbd20 | |||
| 29de7c0727 | |||
| 5f6580fa2f | |||
| 5d4732d75d | |||
| a13bac813a | |||
| 85dcc9f7bd | |||
| b696629ad9 | |||
| b9e3128718 | |||
| 88fac4c2bc | |||
| 1fad5ec536 | |||
| 8e7214d531 | |||
| 333c4a3ebb | |||
| b9ba5a3bea | |||
| 2a0989e74d | |||
| e9319dc491 | |||
| 57481a77cd | |||
| 44915cce54 | |||
| 1225483b57 | |||
| daeb8c88a1 | |||
| 8a6b3ff501 | |||
| a6fffeafdc | |||
| d791b4ce49 | |||
| 8a0e4eb441 | |||
| fc84562419 | |||
| 37ebe1ebb3 | |||
| 2d06f070ea | |||
| 527a62069a | |||
| 40afafe1a8 | |||
| e3acf9ae6d | |||
| a68d067a68 | |||
| 5547c65af0 | |||
| b622bb7d7d | |||
| 43efdf18a0 | |||
| c71ab8e9e8 | |||
| 408d6ed8ba | |||
| 1411961e36 | |||
| dfd7ef466c | |||
| 2aa3dfbd0f | |||
| fba10e27cf | |||
| 5417c74f9c | |||
| eb0b0dbe81 | |||
| 561f522658 | |||
| 32d2ffeb3d | |||
| d41946e0a5 | |||
| 61402858f4 | |||
| 17de318645 | |||
| 3aa0144e8d | |||
| f9eafff4c7 | |||
| 4c6d67901d | |||
| e9aa97a089 | |||
| a82b047f75 | |||
| 9a8b44a8df | |||
| a96693004c | |||
| ed9fe11fbf | |||
| 09fb14a796 | |||
| 58a7936bba | |||
| cd0ee361f5 | |||
| 77bd5abe0d | |||
| 450c5496b3 | |||
| 4411e45a3c | |||
| e7d20896d5 | |||
| 32a1115abd | |||
| 4982057500 |
@@ -1,4 +1,4 @@
|
|||||||
on: [push, pull_request]
|
on: [push]
|
||||||
|
|
||||||
name: Continuous integration
|
name: Continuous integration
|
||||||
|
|
||||||
|
|||||||
1360
Cargo.lock
generated
1360
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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.3"
|
version = "0.12.0"
|
||||||
repository = "https://git.z.xinu.tv/wathiede/letterbox"
|
repository = "https://git.z.xinu.tv/wathiede/letterbox"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
|
|||||||
3
Justfile
3
Justfile
@@ -1,3 +1,6 @@
|
|||||||
|
export CARGO_INCREMENTAL := "0"
|
||||||
|
export RUSTFLAGS := "-D warnings"
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@echo "Run: just patch|minor|major"
|
@echo "Run: just patch|minor|major"
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ anyhow = "1.0.79"
|
|||||||
async-graphql = { version = "7", features = ["log"] }
|
async-graphql = { version = "7", features = ["log"] }
|
||||||
async-graphql-rocket = "7"
|
async-graphql-rocket = "7"
|
||||||
async-trait = "0.1.81"
|
async-trait = "0.1.81"
|
||||||
build-info = "0.0.39"
|
build-info = "0.0.40"
|
||||||
cacher = { version = "0.1.0", registry = "xinu" }
|
cacher = { version = "0.2.0", registry = "xinu" }
|
||||||
chrono = "0.4.39"
|
chrono = "0.4.39"
|
||||||
clap = { version = "4.5.23", features = ["derive"] }
|
clap = { version = "4.5.23", features = ["derive"] }
|
||||||
css-inline = "0.14.0"
|
css-inline = "0.14.0"
|
||||||
@@ -30,7 +30,6 @@ lol_html = "2.0.0"
|
|||||||
mailparse = "0.16.0"
|
mailparse = "0.16.0"
|
||||||
maplit = "1.0.2"
|
maplit = "1.0.2"
|
||||||
memmap = "0.7.0"
|
memmap = "0.7.0"
|
||||||
opentelemetry = "0.28.0"
|
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
reqwest = { version = "0.12.7", features = ["blocking"] }
|
reqwest = { version = "0.12.7", features = ["blocking"] }
|
||||||
rocket = { version = "0.5.0-rc.2", features = ["json"] }
|
rocket = { version = "0.5.0-rc.2", features = ["json"] }
|
||||||
@@ -39,7 +38,7 @@ scraper = "0.23.0"
|
|||||||
serde = { version = "1.0.147", features = ["derive"] }
|
serde = { version = "1.0.147", features = ["derive"] }
|
||||||
serde_json = "1.0.87"
|
serde_json = "1.0.87"
|
||||||
sqlx = { version = "0.8.2", features = ["postgres", "runtime-tokio", "time"] }
|
sqlx = { version = "0.8.2", features = ["postgres", "runtime-tokio", "time"] }
|
||||||
tantivy = { version = "0.22.0", optional = true }
|
tantivy = { version = "0.24.0", optional = true }
|
||||||
thiserror = "2.0.0"
|
thiserror = "2.0.0"
|
||||||
tokio = "1.26.0"
|
tokio = "1.26.0"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
@@ -48,11 +47,11 @@ 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.3", path = "../notmuch", registry = "xinu" }
|
letterbox-notmuch = { version = "0.12.0", path = "../notmuch", registry = "xinu" }
|
||||||
letterbox-shared = { version = "0.9.3", path = "../shared", registry = "xinu" }
|
letterbox-shared = { version = "0.12.0", path = "../shared", registry = "xinu" }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
build-info-build = "0.0.39"
|
build-info-build = "0.0.40"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
#default = [ "tantivy" ]
|
#default = [ "tantivy" ]
|
||||||
|
|||||||
20
server/migrations/20250330175930_update-nzfinder-link.sql
Normal file
20
server/migrations/20250330175930_update-nzfinder-link.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
-- Bad examples:
|
||||||
|
-- https://nzbfinder.ws/getnzb/d2c3e5a08abadd985dccc6a574122892030b6a9a.nzb&i=95972&r=b55082d289937c050dedc203c9653850
|
||||||
|
-- https://nzbfinder.ws/getnzb?id=45add174-7da4-4445-bf2b-a67dbbfc07fe.nzb&r=b55082d289937c050dedc203c9653850
|
||||||
|
-- https://nzbfinder.ws/api/v1/getnzb?id=82486020-c192-4fa0-a7e7-798d7d72e973.nzb&r=b55082d289937c050dedc203c9653850
|
||||||
|
UPDATE nzb_posts
|
||||||
|
SET link =
|
||||||
|
regexp_replace(
|
||||||
|
regexp_replace(
|
||||||
|
regexp_replace(
|
||||||
|
link,
|
||||||
|
'https://nzbfinder.ws/getnzb/',
|
||||||
|
'https://nzbfinder.ws/api/v1/getnzb?id='
|
||||||
|
),
|
||||||
|
'https://nzbfinder.ws/getnzb',
|
||||||
|
'https://nzbfinder.ws/api/v1/getnzb'
|
||||||
|
),
|
||||||
|
'&r=',
|
||||||
|
'&apikey='
|
||||||
|
)
|
||||||
|
;
|
||||||
@@ -318,13 +318,16 @@ impl<'c> Transformer for SlurpContents<'c> {
|
|||||||
} else {
|
} else {
|
||||||
let resp = reqwest::get(link.as_str()).await?;
|
let resp = reqwest::get(link.as_str()).await?;
|
||||||
let status = resp.status();
|
let status = resp.status();
|
||||||
if status.is_server_error() || retryable_status.contains(&status) {
|
if status.is_server_error() {
|
||||||
return Err(TransformError::RetryableHttpStatusError(
|
error!("status error for {link}: {status}");
|
||||||
status,
|
return Ok(html.to_string());
|
||||||
link.to_string(),
|
}
|
||||||
));
|
if retryable_status.contains(&status) {
|
||||||
|
error!("retryable error for {link}: {status}");
|
||||||
|
return Ok(html.to_string());
|
||||||
}
|
}
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
|
error!("unsuccessful for {link}: {status}");
|
||||||
return Ok(html.to_string());
|
return Ok(html.to_string());
|
||||||
}
|
}
|
||||||
let body = resp.text().await?;
|
let body = resp.text().await?;
|
||||||
@@ -438,6 +441,38 @@ pub fn sanitize_html(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut element_content_handlers = vec![
|
let mut element_content_handlers = vec![
|
||||||
|
// Remove width and height attributes on elements
|
||||||
|
element!("[width],[height]", |el| {
|
||||||
|
println!("width or height {el:?}");
|
||||||
|
el.remove_attribute("width");
|
||||||
|
el.remove_attribute("height");
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
// Remove width and height values from inline styles
|
||||||
|
element!("[style]", |el| {
|
||||||
|
println!("style {el:?}");
|
||||||
|
let style = el.get_attribute("style").unwrap();
|
||||||
|
let style = style
|
||||||
|
.split(";")
|
||||||
|
.filter(|s| {
|
||||||
|
println!("s {s}");
|
||||||
|
let Some((k, _)) = s.split_once(':') else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
match k {
|
||||||
|
"width" | "max-width" | "min-width" | "height" | "max-height"
|
||||||
|
| "min-height" => false,
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(";");
|
||||||
|
println!("style: {style}");
|
||||||
|
if let Err(e) = el.set_attribute("style", &style) {
|
||||||
|
error!("Failed to set style attribute: {e}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
// Open links in new tab
|
// Open links in new tab
|
||||||
element!("a[href]", |el| {
|
element!("a[href]", |el| {
|
||||||
el.set_attribute("target", "_blank").unwrap();
|
el.set_attribute("target", "_blank").unwrap();
|
||||||
@@ -910,3 +945,21 @@ async fn clean_title(title: &str) -> Result<String, ServerError> {
|
|||||||
}
|
}
|
||||||
Ok(title)
|
Ok(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{SanitizeHtml, Transformer};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn strip_sizes() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let ss = SanitizeHtml {
|
||||||
|
cid_prefix: "",
|
||||||
|
base_url: &None,
|
||||||
|
};
|
||||||
|
let input = r#"<p width=16 height=16 style="color:blue;width:16px;height:16px;">This el has width and height attributes and inline styles</p>"#;
|
||||||
|
let want = r#"<p style="color:blue;">This el has width and height attributes and inline styles</p>"#;
|
||||||
|
let got = ss.transform(&None, input).await?;
|
||||||
|
assert_eq!(got, want);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ version.workspace = true
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
build-info = "0.0.39"
|
build-info = "0.0.40"
|
||||||
letterbox-notmuch = { version = "0.9.3", path = "../notmuch", registry = "xinu" }
|
letterbox-notmuch = { version = "0.12.0", path = "../notmuch", registry = "xinu" }
|
||||||
serde = { version = "1.0.147", features = ["derive"] }
|
serde = { version = "1.0.147", features = ["derive"] }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ repository.workspace = true
|
|||||||
version.workspace = true
|
version.workspace = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
build-info-build = "0.0.39"
|
build-info-build = "0.0.40"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
wasm-bindgen-test = "0.3.33"
|
wasm-bindgen-test = "0.3.33"
|
||||||
@@ -28,13 +28,13 @@ graphql_client = "0.14.0"
|
|||||||
thiserror = "2.0.0"
|
thiserror = "2.0.0"
|
||||||
gloo-net = { version = "0.6.0", features = ["json", "serde_json"] }
|
gloo-net = { version = "0.6.0", features = ["json", "serde_json"] }
|
||||||
human_format = "1.1.0"
|
human_format = "1.1.0"
|
||||||
build-info = "0.0.39"
|
build-info = "0.0.40"
|
||||||
wasm-bindgen = "=0.2.100"
|
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.3", path = "../shared", registry = "xinu" }
|
letterbox-shared = { version = "0.12.0", path = "../shared", registry = "xinu" }
|
||||||
letterbox-notmuch = { version = "0.9.3", path = "../notmuch", registry = "xinu" }
|
letterbox-notmuch = { version = "0.12.0", 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"
|
strum_macros = "0.27.1"
|
||||||
|
|
||||||
@@ -50,4 +50,6 @@ features = [
|
|||||||
"MediaQueryList",
|
"MediaQueryList",
|
||||||
"Navigator",
|
"Navigator",
|
||||||
"Window",
|
"Window",
|
||||||
|
"History",
|
||||||
|
"ScrollRestoration",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ fn main() {
|
|||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
let lvl = Level::Debug;
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
let lvl = Level::Info;
|
let lvl = Level::Info;
|
||||||
console_log::init_with_level(lvl).expect("failed to initialize console logging");
|
console_log::init_with_level(lvl).expect("failed to initialize console logging");
|
||||||
// Mount the `app` to the element with the `id` "app".
|
// Mount the `app` to the element with the `id` "app".
|
||||||
|
|||||||
@@ -29,16 +29,21 @@ pub fn unread_query() -> &'static str {
|
|||||||
pub fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
pub fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
||||||
let version = letterbox_shared::build_version(bi);
|
let version = letterbox_shared::build_version(bi);
|
||||||
info!("Build Info: {}", version);
|
info!("Build Info: {}", version);
|
||||||
|
// Disable restoring to scroll position when navigating
|
||||||
|
window()
|
||||||
|
.history()
|
||||||
|
.expect("couldn't get history")
|
||||||
|
.set_scroll_restoration(web_sys::ScrollRestoration::Manual)
|
||||||
|
.expect("failed to set scroll restoration to manual");
|
||||||
if url.hash().is_none() {
|
if url.hash().is_none() {
|
||||||
orders.request_url(urls::search(unread_query(), 0));
|
orders.request_url(urls::search(unread_query(), 0));
|
||||||
} else {
|
} else {
|
||||||
orders.notify(subs::UrlRequested::new(url));
|
orders.request_url(url);
|
||||||
};
|
};
|
||||||
orders.stream(streams::window_event(Ev::Resize, |_| Msg::OnResize));
|
|
||||||
// TODO(wathiede): only do this while viewing the index? Or maybe add a new message that force
|
// TODO(wathiede): only do this while viewing the index? Or maybe add a new message that force
|
||||||
// 'notmuch new' on the server periodically?
|
// 'notmuch new' on the server periodically?
|
||||||
orders.stream(streams::interval(30_000, || Msg::RefreshStart));
|
orders.stream(streams::interval(30_000, || Msg::RefreshStart));
|
||||||
orders.subscribe(on_url_changed);
|
orders.subscribe(Msg::OnUrlChanged);
|
||||||
orders.stream(streams::window_event(Ev::Scroll, |_| Msg::WindowScrolled));
|
orders.stream(streams::window_event(Ev::Scroll, |_| Msg::WindowScrolled));
|
||||||
|
|
||||||
build_info::build_info!(fn bi);
|
build_info::build_info!(fn bi);
|
||||||
@@ -54,24 +59,21 @@ pub fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
|||||||
server: None,
|
server: None,
|
||||||
},
|
},
|
||||||
catchup: None,
|
catchup: None,
|
||||||
|
last_url: Url::current(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_url_changed(uc: subs::UrlChanged) -> Msg {
|
fn on_url_changed(old: &Url, mut new: Url) -> Msg {
|
||||||
let mut url = uc.0;
|
let did_change = *old != new;
|
||||||
let href = document().location().unwrap().href().unwrap();
|
|
||||||
let origin = document().location().unwrap().origin().unwrap();
|
|
||||||
let current_url = &href[origin.len()..];
|
|
||||||
let did_change = current_url != url.to_string();
|
|
||||||
let mut messages = Vec::new();
|
let mut messages = Vec::new();
|
||||||
if did_change {
|
if did_change {
|
||||||
messages.push(Msg::ScrollToTop)
|
messages.push(Msg::ScrollToTop)
|
||||||
}
|
}
|
||||||
info!(
|
info!(
|
||||||
"url changed\nold '{current_url}'\nnew '{url}', history {}",
|
"url changed\nold '{old}'\nnew '{new}', history {}",
|
||||||
history().length().unwrap_or(0)
|
history().length().unwrap_or(0)
|
||||||
);
|
);
|
||||||
let hpp = url.remaining_hash_path_parts();
|
let hpp = new.remaining_hash_path_parts();
|
||||||
let msg = match hpp.as_slice() {
|
let msg = match hpp.as_slice() {
|
||||||
["t", tid] => Msg::ShowThreadRequest {
|
["t", tid] => Msg::ShowThreadRequest {
|
||||||
thread_id: tid.to_string(),
|
thread_id: tid.to_string(),
|
||||||
@@ -116,7 +118,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);
|
info!("update({})", msg);
|
||||||
match msg {
|
match msg {
|
||||||
Msg::Noop => {}
|
Msg::Noop => {}
|
||||||
Msg::RefreshStart => {
|
Msg::RefreshStart => {
|
||||||
@@ -142,7 +144,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
orders.perform_cmd(async move { Msg::Refresh });
|
orders.perform_cmd(async move { Msg::Refresh });
|
||||||
}
|
}
|
||||||
Msg::Refresh => {
|
Msg::Refresh => {
|
||||||
orders.perform_cmd(async move { on_url_changed(subs::UrlChanged(Url::current())) });
|
orders.request_url(Url::current());
|
||||||
}
|
}
|
||||||
Msg::Reload => {
|
Msg::Reload => {
|
||||||
window()
|
window()
|
||||||
@@ -150,7 +152,10 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
.reload()
|
.reload()
|
||||||
.expect("failed to reload window");
|
.expect("failed to reload window");
|
||||||
}
|
}
|
||||||
Msg::OnResize => (),
|
Msg::OnUrlChanged(new_url) => {
|
||||||
|
orders.send_msg(on_url_changed(&model.last_url, new_url.0.clone()));
|
||||||
|
model.last_url = new_url.0;
|
||||||
|
}
|
||||||
|
|
||||||
Msg::NextPage => {
|
Msg::NextPage => {
|
||||||
match &model.context {
|
match &model.context {
|
||||||
@@ -287,7 +292,9 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Msg::FrontPageResult(Err(e)) => error!("error FrontPageResult: {e:?}"),
|
Msg::FrontPageResult(Err(e)) => {
|
||||||
|
error!("error FrontPageResult: {e:?}");
|
||||||
|
}
|
||||||
Msg::FrontPageResult(Ok(graphql_client::Response {
|
Msg::FrontPageResult(Ok(graphql_client::Response {
|
||||||
data: None,
|
data: None,
|
||||||
errors: None,
|
errors: None,
|
||||||
@@ -547,6 +554,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
web_sys::window().unwrap().scroll_to_with_x_and_y(0., 0.);
|
web_sys::window().unwrap().scroll_to_with_x_and_y(0., 0.);
|
||||||
}
|
}
|
||||||
Msg::WindowScrolled => {
|
Msg::WindowScrolled => {
|
||||||
|
// TODO: model.content_el doesn't go to None like it should when a DOM is recreated and the refrenced element goes away
|
||||||
if let Some(el) = model.content_el.get() {
|
if let Some(el) = model.content_el.get() {
|
||||||
let ih = window()
|
let ih = window()
|
||||||
.inner_height()
|
.inner_height()
|
||||||
@@ -586,7 +594,8 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
model.read_completion_ratio = ratio;
|
model.read_completion_ratio = ratio;
|
||||||
}
|
}
|
||||||
Msg::UpdateServerVersion(version) => {
|
Msg::UpdateServerVersion(version) => {
|
||||||
if version != model.versions.client {
|
// Only git versions contain dash, don't autoreload there
|
||||||
|
if !version.contains('-') && version != model.versions.client {
|
||||||
warn!(
|
warn!(
|
||||||
"Server ({}) and client ({}) version mismatch, reloading",
|
"Server ({}) and client ({}) version mismatch, reloading",
|
||||||
version, model.versions.client
|
version, model.versions.client
|
||||||
@@ -607,9 +616,6 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
orders.send_msg(Msg::CatchupRequest { query });
|
orders.send_msg(Msg::CatchupRequest { query });
|
||||||
}
|
}
|
||||||
Msg::CatchupKeepUnread => {
|
Msg::CatchupKeepUnread => {
|
||||||
if let Some(thread_id) = current_thread_id(&model.context) {
|
|
||||||
orders.send_msg(Msg::SetUnread(thread_id, true));
|
|
||||||
};
|
|
||||||
orders.send_msg(Msg::CatchupNext);
|
orders.send_msg(Msg::CatchupNext);
|
||||||
}
|
}
|
||||||
Msg::CatchupMarkAsRead => {
|
Msg::CatchupMarkAsRead => {
|
||||||
@@ -624,7 +630,15 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
orders.send_msg(Msg::GoToSearchResults);
|
orders.send_msg(Msg::GoToSearchResults);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(idx) = catchup.items.iter().position(|i| !i.seen) else {
|
let Some(thread_id) = current_thread_id(&model.context) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(idx) = catchup
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.inspect(|i| info!("i {i:?} thread_id {thread_id}"))
|
||||||
|
.position(|i| i.id == thread_id)
|
||||||
|
else {
|
||||||
// All items have been seen
|
// All items have been seen
|
||||||
orders.send_msg(Msg::CatchupExit);
|
orders.send_msg(Msg::CatchupExit);
|
||||||
orders.send_msg(Msg::GoToSearchResults);
|
orders.send_msg(Msg::GoToSearchResults);
|
||||||
@@ -676,6 +690,7 @@ pub struct Model {
|
|||||||
pub content_el: ElRef<HtmlElement>,
|
pub content_el: ElRef<HtmlElement>,
|
||||||
pub versions: Version,
|
pub versions: Version,
|
||||||
pub catchup: Option<Catchup>,
|
pub catchup: Option<Catchup>,
|
||||||
|
pub last_url: Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -716,6 +731,7 @@ pub struct Catchup {
|
|||||||
pub items: Vec<CatchupItem>,
|
pub items: Vec<CatchupItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct CatchupItem {
|
pub struct CatchupItem {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub seen: bool,
|
pub seen: bool,
|
||||||
@@ -741,14 +757,15 @@ pub enum Msg {
|
|||||||
Refresh,
|
Refresh,
|
||||||
// Tell the client to reload whole page from server
|
// Tell the client to reload whole page from server
|
||||||
Reload,
|
Reload,
|
||||||
// Window has changed size
|
// TODO: add GoToUrl
|
||||||
OnResize,
|
OnUrlChanged(subs::UrlChanged),
|
||||||
// Tell the server to update state
|
// Tell the server to update state
|
||||||
RefreshStart,
|
RefreshStart,
|
||||||
RefreshDone(Option<gloo_net::Error>),
|
RefreshDone(Option<gloo_net::Error>),
|
||||||
NextPage,
|
NextPage,
|
||||||
PreviousPage,
|
PreviousPage,
|
||||||
GoToSearchResults,
|
GoToSearchResults,
|
||||||
|
|
||||||
UpdateQuery(String),
|
UpdateQuery(String),
|
||||||
SearchQuery(String),
|
SearchQuery(String),
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use chrono::{DateTime, Datelike, Duration, Local, Utc};
|
|||||||
use human_format::{Formatter, Scales};
|
use human_format::{Formatter, Scales};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use letterbox_shared::compute_color;
|
use letterbox_shared::compute_color;
|
||||||
use log::{debug, error};
|
use log::error;
|
||||||
use seed::{prelude::*, *};
|
use seed::{prelude::*, *};
|
||||||
use seed_hooks::{state_access::CloneState, topo, use_state, StateAccessEventHandlers};
|
use seed_hooks::{state_access::CloneState, topo, use_state, StateAccessEventHandlers};
|
||||||
use web_sys::{HtmlElement, HtmlInputElement};
|
use web_sys::{HtmlElement, HtmlInputElement};
|
||||||
@@ -12,7 +12,7 @@ use web_sys::{HtmlElement, HtmlInputElement};
|
|||||||
use crate::{
|
use crate::{
|
||||||
api::urls,
|
api::urls,
|
||||||
graphql::{front_page_query::*, show_thread_query::*},
|
graphql::{front_page_query::*, show_thread_query::*},
|
||||||
state::{unread_query, CatchupItem, Context, Model, Msg, RefreshingState, Tag, Version},
|
state::{CatchupItem, Context, Model, Msg, RefreshingState, Tag, Version},
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO(wathiede): create a QueryString enum that wraps single and multiple message ids and thread
|
// TODO(wathiede): create a QueryString enum that wraps single and multiple message ids and thread
|
||||||
@@ -93,13 +93,13 @@ pub fn view(model: &Model) -> Node<Msg> {
|
|||||||
} => {
|
} => {
|
||||||
if let Some(catchup) = &model.catchup {
|
if let Some(catchup) = &model.catchup {
|
||||||
catchup_view(
|
catchup_view(
|
||||||
thread(thread_data, open_messages, &model.content_el),
|
thread(thread_data, open_messages, &model.content_el, true),
|
||||||
&catchup.items,
|
&catchup.items,
|
||||||
model.read_completion_ratio,
|
model.read_completion_ratio,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
normal_view(
|
normal_view(
|
||||||
thread(thread_data, open_messages, &model.content_el),
|
thread(thread_data, open_messages, &model.content_el, false),
|
||||||
&model.versions,
|
&model.versions,
|
||||||
&model.query,
|
&model.query,
|
||||||
&model.refreshing_state,
|
&model.refreshing_state,
|
||||||
@@ -114,13 +114,13 @@ pub fn view(model: &Model) -> Node<Msg> {
|
|||||||
} => {
|
} => {
|
||||||
if let Some(catchup) = &model.catchup {
|
if let Some(catchup) = &model.catchup {
|
||||||
catchup_view(
|
catchup_view(
|
||||||
news_post(post, &model.content_el),
|
news_post(post, &model.content_el, true),
|
||||||
&catchup.items,
|
&catchup.items,
|
||||||
model.read_completion_ratio,
|
model.read_completion_ratio,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
normal_view(
|
normal_view(
|
||||||
news_post(post, &model.content_el),
|
news_post(post, &model.content_el, false),
|
||||||
&model.versions,
|
&model.versions,
|
||||||
&model.query,
|
&model.query,
|
||||||
&model.refreshing_state,
|
&model.refreshing_state,
|
||||||
@@ -198,10 +198,10 @@ fn catchup_view(
|
|||||||
"p-4",
|
"p-4",
|
||||||
"border-b",
|
"border-b",
|
||||||
"border-gray-500",
|
"border-gray-500",
|
||||||
"bg-black",
|
"bg-black/50",
|
||||||
],
|
],
|
||||||
div![
|
div![
|
||||||
C!["absolute", "right-4", "text-gray-500"],
|
C!["absolute", "top-0", "right-4", "text-gray-500", "p-4"],
|
||||||
span![i![C!["fas", "fa-x"]]],
|
span![i![C!["fas", "fa-x"]]],
|
||||||
ev(Ev::Click, move |_| Msg::CatchupExit)
|
ev(Ev::Click, move |_| Msg::CatchupExit)
|
||||||
],
|
],
|
||||||
@@ -210,7 +210,7 @@ fn catchup_view(
|
|||||||
format!("{} left ", items.iter().filter(|i| !i.seen).count(),)
|
format!("{} left ", items.iter().filter(|i| !i.seen).count(),)
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
div![C!["mt-12", "mb-4"], content],
|
div![C!["mt-12", "mb-20"], content],
|
||||||
div![
|
div![
|
||||||
C![
|
C![
|
||||||
"fixed",
|
"fixed",
|
||||||
@@ -223,19 +223,28 @@ fn catchup_view(
|
|||||||
"p-4",
|
"p-4",
|
||||||
"border-t",
|
"border-t",
|
||||||
"border-gray-500",
|
"border-gray-500",
|
||||||
"bg-black",
|
"bg-black/50",
|
||||||
],
|
],
|
||||||
button![
|
button![
|
||||||
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, |_| Msg::CatchupKeepUnread)
|
||||||
],
|
],
|
||||||
button![
|
button![
|
||||||
tw_classes::button_with_color("bg-green-800", "hover:bg-neutral-700"),
|
tw_classes::button(),
|
||||||
|
span![i![C!["fas", "fa-house"]]],
|
||||||
|
span![C!["pl-2"], "Go home"],
|
||||||
|
ev(Ev::Click, |_| Msg::MultiMsg(vec![
|
||||||
|
Msg::CatchupExit,
|
||||||
|
Msg::GoToSearchResults
|
||||||
|
]))
|
||||||
|
],
|
||||||
|
button![
|
||||||
|
tw_classes::button_with_color("bg-green-800", "hover:bg-green-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, |_| Msg::CatchupMarkAsRead)
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
reading_progress(read_completion_ratio)
|
reading_progress(read_completion_ratio)
|
||||||
@@ -264,6 +273,13 @@ fn search_results(
|
|||||||
tags.remove(idx);
|
tags.remove(idx);
|
||||||
};
|
};
|
||||||
let is_unread = unread_idx.is_some();
|
let is_unread = unread_idx.is_some();
|
||||||
|
let mut title_break = None;
|
||||||
|
const TITLE_LENGTH_WRAP_LIMIT: usize = 40;
|
||||||
|
for w in r.subject.split_whitespace() {
|
||||||
|
if w.len() > TITLE_LENGTH_WRAP_LIMIT {
|
||||||
|
title_break = Some(C!["break-all", "text-pretty"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
div![
|
div![
|
||||||
C![
|
C![
|
||||||
"flex",
|
"flex",
|
||||||
@@ -306,7 +322,7 @@ fn search_results(
|
|||||||
attrs! {
|
attrs! {
|
||||||
At::Href => urls::thread(&tid)
|
At::Href => urls::thread(&tid)
|
||||||
},
|
},
|
||||||
div![&r.subject],
|
div![title_break, &r.subject],
|
||||||
span![C!["text-xs"], pretty_authors(&r.authors)],
|
span![C!["text-xs"], pretty_authors(&r.authors)],
|
||||||
div![
|
div![
|
||||||
C!["flex", "flex-wrap", "justify-between"],
|
C!["flex", "flex-wrap", "justify-between"],
|
||||||
@@ -452,38 +468,42 @@ fn search_toolbar(
|
|||||||
if let Some(tri) = tri.get() {
|
if let Some(tri) = tri.get() {
|
||||||
tri.set_indeterminate(indeterminate);
|
tri.set_indeterminate(indeterminate);
|
||||||
}
|
}
|
||||||
|
let catchup = div![button![
|
||||||
|
tw_classes::button(),
|
||||||
|
attrs! {At::Title => "Catch up"},
|
||||||
|
span![i![C!["far", "fa-eye"]]],
|
||||||
|
span![C!["pl-2", "hidden", "md:inline"], "Catch-up"],
|
||||||
|
ev(Ev::Click, |_| Msg::CatchupStart)
|
||||||
|
]];
|
||||||
|
let tristate_input = div![
|
||||||
|
C!["flex", "items-center", "mr-4"],
|
||||||
|
input![
|
||||||
|
&tri,
|
||||||
|
C![&tw_classes::CHECKBOX],
|
||||||
|
attrs! {
|
||||||
|
At::Type=>"checkbox",
|
||||||
|
},
|
||||||
|
IF!(all_selected=>attrs!{At::Checked=>true})
|
||||||
|
],
|
||||||
|
ev(Ev::Input, move |_| {
|
||||||
|
if all_selected {
|
||||||
|
Msg::SelectionSetNone
|
||||||
|
} else {
|
||||||
|
Msg::SelectionSetAll
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
];
|
||||||
nav![
|
nav![
|
||||||
C!["py-4", "flex", "w-full", "justify-between"],
|
C!["py-4", "flex", "w-full", "justify-between"],
|
||||||
div![
|
div![
|
||||||
C!["gap-2", "flex", IF!(show_bulk_edit => "hidden")],
|
C!["gap-2", "flex", IF!(show_bulk_edit => "hidden")],
|
||||||
div![button![
|
&tristate_input,
|
||||||
tw_classes::button(),
|
&catchup
|
||||||
attrs! {At::Title => "Mark as read"},
|
|
||||||
span![i![C!["far", "fa-eye"]]],
|
|
||||||
span![C!["pl-2", "hidden", "md:inline"], "Catch-up"],
|
|
||||||
ev(Ev::Click, |_| Msg::CatchupStart)
|
|
||||||
]],
|
|
||||||
],
|
],
|
||||||
div![
|
div![
|
||||||
C!["gap-2", "flex", IF!(!show_bulk_edit => "hidden")],
|
C!["gap-2", "flex", IF!(!show_bulk_edit => "hidden")],
|
||||||
div![
|
&tristate_input,
|
||||||
C!["flex", "items-center", "mr-4"],
|
&catchup,
|
||||||
input![
|
|
||||||
tri,
|
|
||||||
C![&tw_classes::CHECKBOX],
|
|
||||||
attrs! {
|
|
||||||
At::Type=>"checkbox",
|
|
||||||
At::Checked=>all_selected,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
ev(Ev::Input, move |_| {
|
|
||||||
if all_selected {
|
|
||||||
Msg::SelectionSetNone
|
|
||||||
} else {
|
|
||||||
Msg::SelectionSetAll
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
div![
|
div![
|
||||||
button![
|
button![
|
||||||
tw_classes::button(),
|
tw_classes::button(),
|
||||||
@@ -925,6 +945,13 @@ 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();
|
||||||
|
let from = match &msg.from {
|
||||||
|
Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
|
||||||
|
addr: Some(addr), ..
|
||||||
|
}) => Some(addr.to_string()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let from = from.map(|f| f.replace('.', "-").replace('@', "-"));
|
||||||
div![
|
div![
|
||||||
C!["lg:mb-4"],
|
C!["lg:mb-4"],
|
||||||
div![
|
div![
|
||||||
@@ -944,7 +971,7 @@ fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool)
|
|||||||
],
|
],
|
||||||
IF!(open =>
|
IF!(open =>
|
||||||
div![
|
div![
|
||||||
C!["bg-white", "text-black", "p-4", "min-w-full", "w-0","overflow-x-auto"],
|
C!["bg-white", "text-black", "p-4", "min-w-full", "w-0","overflow-x-auto", from],
|
||||||
match &msg.body {
|
match &msg.body {
|
||||||
ShowThreadQueryThreadOnEmailThreadMessagesBody::UnhandledContentType(
|
ShowThreadQueryThreadOnEmailThreadMessagesBody::UnhandledContentType(
|
||||||
ShowThreadQueryThreadOnEmailThreadMessagesBodyOnUnhandledContentType { contents ,content_tree},
|
ShowThreadQueryThreadOnEmailThreadMessagesBodyOnUnhandledContentType { contents ,content_tree},
|
||||||
@@ -1048,11 +1075,13 @@ fn render_attachements(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: add cathup_mode:bool and hide elements when true
|
||||||
#[topo::nested]
|
#[topo::nested]
|
||||||
fn thread(
|
fn thread(
|
||||||
thread: &ShowThreadQueryThreadOnEmailThread,
|
thread: &ShowThreadQueryThreadOnEmailThread,
|
||||||
open_messages: &HashSet<String>,
|
open_messages: &HashSet<String>,
|
||||||
content_el: &ElRef<HtmlElement>,
|
content_el: &ElRef<HtmlElement>,
|
||||||
|
catchup_mode: bool,
|
||||||
) -> Node<Msg> {
|
) -> Node<Msg> {
|
||||||
// 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() {
|
||||||
@@ -1079,13 +1108,20 @@ fn thread(
|
|||||||
let unread_thread_id = thread.thread_id.clone();
|
let unread_thread_id = thread.thread_id.clone();
|
||||||
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();
|
||||||
|
let mut title_break = None;
|
||||||
|
const TITLE_LENGTH_WRAP_LIMIT: usize = 40;
|
||||||
|
for w in subject.split_whitespace() {
|
||||||
|
if w.len() > TITLE_LENGTH_WRAP_LIMIT {
|
||||||
|
title_break = Some(C!["break-all", "text-pretty"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
div![
|
div![
|
||||||
C!["lg:p-4", "max-w-4xl"],
|
C!["lg:p-4", "max-w-4xl"],
|
||||||
div![
|
div![
|
||||||
C!["p-4", "lg:p-0"],
|
C!["p-4", "lg:p-0"],
|
||||||
h3![C!["text-xl"], subject],
|
h3![C!["text-xl"], title_break, subject],
|
||||||
span![removable_tags_chiclet(&thread.thread_id, &tags)],
|
span![removable_tags_chiclet(&thread.thread_id, &tags)],
|
||||||
div![
|
IF!(!catchup_mode => div![
|
||||||
C!["pt-4", "gap-2", "flex", "justify-around"],
|
C!["pt-4", "gap-2", "flex", "justify-around"],
|
||||||
div![
|
div![
|
||||||
button![
|
button![
|
||||||
@@ -1123,13 +1159,13 @@ fn thread(
|
|||||||
Msg::GoToSearchResults
|
Msg::GoToSearchResults
|
||||||
])),
|
])),
|
||||||
]]
|
]]
|
||||||
],
|
]),
|
||||||
],
|
],
|
||||||
div![
|
div![
|
||||||
C!["lg:mt-4", "mail-thread"],
|
C!["lg:mt-4", "mail-thread"],
|
||||||
el_ref(content_el),
|
el_ref(content_el),
|
||||||
messages,
|
messages,
|
||||||
click_to_top()
|
IF!(!catchup_mode => click_to_top())
|
||||||
],
|
],
|
||||||
/* TODO(wathiede): plumb in orignal id
|
/* TODO(wathiede): plumb in orignal id
|
||||||
a![
|
a![
|
||||||
@@ -1176,47 +1212,68 @@ 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", "px-4", "pt-4", "overflow-hidden"],
|
C!["flex", "flex-col"],
|
||||||
a![
|
div![
|
||||||
C![IF![is_error => "bg-red-500"], "rounded-r-none"],
|
C!["flex-auto", "flex"],
|
||||||
tw_classes::button(),
|
button![
|
||||||
span![i![C![
|
C![IF![is_error => "bg-red-500"], "rounded-none"],
|
||||||
"fa-solid",
|
tw_classes::button(),
|
||||||
"fa-arrow-rotate-right",
|
span![i![C![
|
||||||
IF![is_loading => "animate-spin"],
|
"fa-solid",
|
||||||
]]],
|
"fa-arrow-rotate-right",
|
||||||
ev(Ev::Click, |_| Msg::RefreshStart),
|
IF![is_loading => "animate-spin"],
|
||||||
|
]]],
|
||||||
|
ev(Ev::Click, |_| Msg::RefreshStart),
|
||||||
|
],
|
||||||
|
button![
|
||||||
|
tw_classes::button(),
|
||||||
|
C!["grow", "rounded-none"],
|
||||||
|
"All",
|
||||||
|
ev(Ev::Click, |_| Msg::SearchQuery(String::new())),
|
||||||
|
],
|
||||||
|
button![
|
||||||
|
tw_classes::button(),
|
||||||
|
C!["grow", "rounded-none"],
|
||||||
|
span![i![C!["far", "fa-envelope"]]],
|
||||||
|
" Unread",
|
||||||
|
ev(Ev::Click, |_| Msg::SearchQuery("is:unread".to_string())),
|
||||||
|
],
|
||||||
|
button![
|
||||||
|
tw_classes::button(),
|
||||||
|
C!["grow", "rounded-none"],
|
||||||
|
span![i![C!["far", "fa-envelope"]]],
|
||||||
|
" News",
|
||||||
|
ev(Ev::Click, |_| Msg::SearchQuery(
|
||||||
|
"is:unread is:news".to_string()
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
button![
|
||||||
|
tw_classes::button(),
|
||||||
|
C!["grow", "rounded-none"],
|
||||||
|
span![i![C!["far", "fa-envelope"]]],
|
||||||
|
" Mail",
|
||||||
|
ev(Ev::Click, |_| Msg::SearchQuery(
|
||||||
|
"is:unread is:mail".to_string()
|
||||||
|
)),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
a![
|
div![
|
||||||
tw_classes::button(),
|
C!["flex-auto", "flex"],
|
||||||
C!["px-4", "rounded-none"],
|
input![
|
||||||
attrs! {
|
C!["grow", "text-black", "m-2", "p-1"],
|
||||||
At::Href => urls::search(unread_query(), 0)
|
attrs! {
|
||||||
},
|
At::Placeholder => "Search";
|
||||||
"Unread",
|
At::AutoFocus => auto_focus_search.as_at_value();
|
||||||
],
|
At::Value => query,
|
||||||
a![
|
},
|
||||||
tw_classes::button(),
|
input_ev(Ev::Input, |q| Msg::UpdateQuery(q)),
|
||||||
C!["px-4", "rounded-none"],
|
// Send search on enter.
|
||||||
attrs! {
|
keyboard_ev(Ev::KeyUp, move |e| if e.key_code() == 0x0d {
|
||||||
At::Href => urls::search("", 0)
|
Msg::SearchQuery(query)
|
||||||
},
|
} else {
|
||||||
"All",
|
Msg::Noop
|
||||||
],
|
}),
|
||||||
input![
|
]
|
||||||
C!["grow", "pl-2", "text-black", "rounded-r"],
|
|
||||||
attrs! {
|
|
||||||
At::Placeholder => "Search";
|
|
||||||
At::AutoFocus => auto_focus_search.as_at_value();
|
|
||||||
At::Value => query,
|
|
||||||
},
|
|
||||||
input_ev(Ev::Input, |q| Msg::UpdateQuery(q)),
|
|
||||||
// Send search on enter.
|
|
||||||
keyboard_ev(Ev::KeyUp, move |e| if e.key_code() == 0x0d {
|
|
||||||
Msg::SearchQuery(query)
|
|
||||||
} else {
|
|
||||||
Msg::Noop
|
|
||||||
}),
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1348,7 +1405,13 @@ pub fn view_tags(tags: &Option<Vec<Tag>>) -> Node<Msg> {
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
fn news_post(post: &ShowThreadQueryThreadOnNewsPost, content_el: &ElRef<HtmlElement>) -> Node<Msg> {
|
|
||||||
|
// TODO: add cathup_mode:bool and hide elements when true
|
||||||
|
fn news_post(
|
||||||
|
post: &ShowThreadQueryThreadOnNewsPost,
|
||||||
|
content_el: &ElRef<HtmlElement>,
|
||||||
|
catchup_mode: bool,
|
||||||
|
) -> Node<Msg> {
|
||||||
let subject = &post.title;
|
let subject = &post.title;
|
||||||
set_title(subject);
|
set_title(subject);
|
||||||
let read_thread_id = post.thread_id.clone();
|
let read_thread_id = post.thread_id.clone();
|
||||||
@@ -1373,13 +1436,20 @@ fn news_post(post: &ShowThreadQueryThreadOnNewsPost, content_el: &ElRef<HtmlElem
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut title_break = None;
|
||||||
|
const TITLE_LENGTH_WRAP_LIMIT: usize = 40;
|
||||||
|
for w in subject.split_whitespace() {
|
||||||
|
if w.len() > TITLE_LENGTH_WRAP_LIMIT {
|
||||||
|
title_break = Some(C!["break-all", "text-pretty"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
div![
|
div![
|
||||||
C!["lg:p-4", "max-w-4xl"],
|
C!["lg:p-4", "max-w-4xl"],
|
||||||
div![
|
div![
|
||||||
C!["p-4", "lg:p-0"],
|
C!["p-4", "lg:p-0"],
|
||||||
h3![C!["text-xl"], subject],
|
h3![C!["text-xl"], title_break, subject],
|
||||||
span![tag(format!("News/{}", post.slug))],
|
span![tag(format!("News/{}", post.slug))],
|
||||||
div![
|
IF!(!catchup_mode => div![
|
||||||
C!["pt-4", "gap-2", "flex", "justify-around"],
|
C!["pt-4", "gap-2", "flex", "justify-around"],
|
||||||
div![
|
div![
|
||||||
button![
|
button![
|
||||||
@@ -1407,7 +1477,7 @@ fn news_post(post: &ShowThreadQueryThreadOnNewsPost, content_el: &ElRef<HtmlElem
|
|||||||
],
|
],
|
||||||
// Placeholder for symmetry with email view that has Spam button
|
// Placeholder for symmetry with email view that has Spam button
|
||||||
div![],
|
div![],
|
||||||
],
|
]),
|
||||||
],
|
],
|
||||||
div![
|
div![
|
||||||
C!["lg:mt-4"],
|
C!["lg:mt-4"],
|
||||||
@@ -1427,12 +1497,11 @@ fn news_post(post: &ShowThreadQueryThreadOnNewsPost, content_el: &ElRef<HtmlElem
|
|||||||
raw![&post.body]
|
raw![&post.body]
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
click_to_top(),
|
IF!(!catchup_mode => click_to_top()),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
fn render_news_post_header(post: &ShowThreadQueryThreadOnNewsPost) -> Node<Msg> {
|
fn render_news_post_header(post: &ShowThreadQueryThreadOnNewsPost) -> Node<Msg> {
|
||||||
let from = &post.site;
|
let from = &post.site;
|
||||||
// TODO: move avatar/favicon stuff to the server side and and come up with a solution for emails
|
|
||||||
let id = post.thread_id.clone();
|
let id = post.thread_id.clone();
|
||||||
let is_unread = !post.is_read;
|
let is_unread = !post.is_read;
|
||||||
let url = &post.url;
|
let url = &post.url;
|
||||||
@@ -1511,12 +1580,12 @@ fn render_news_post_header(post: &ShowThreadQueryThreadOnNewsPost) -> Node<Msg>
|
|||||||
} else {
|
} else {
|
||||||
"fa-envelope-open"
|
"fa-envelope-open"
|
||||||
},
|
},
|
||||||
]]
|
]],
|
||||||
],
|
ev(Ev::Click, move |e| {
|
||||||
ev(Ev::Click, move |e| {
|
e.stop_propagation();
|
||||||
e.stop_propagation();
|
Msg::SetUnread(id, !is_unread)
|
||||||
Msg::SetUnread(id, !is_unread)
|
})
|
||||||
})
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
fn reading_progress(ratio: f64) -> Node<Msg> {
|
fn reading_progress(ratio: f64) -> Node<Msg> {
|
||||||
@@ -1540,7 +1609,6 @@ fn reading_progress(ratio: f64) -> Node<Msg> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
pub fn view_versions(versions: &Version) -> Node<Msg> {
|
pub fn view_versions(versions: &Version) -> Node<Msg> {
|
||||||
debug!("versions {versions:?}");
|
|
||||||
aside![
|
aside![
|
||||||
C!["p-2"],
|
C!["p-2"],
|
||||||
p![C!["uppercase", "font-bold"], "Versions"],
|
p![C!["uppercase", "font-bold"], "Versions"],
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
html {
|
html {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-thread a,
|
.mail-thread a,
|
||||||
.news-post a {
|
.news-post a {
|
||||||
color: var(--color-link) !important;
|
color: var(--color-link) !important;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-thread br,
|
.mail-thread br,
|
||||||
.news-post br {
|
.news-post br {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
content: " ";
|
content: " ";
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-thread h1,
|
.mail-thread h1,
|
||||||
@@ -23,47 +23,62 @@ html {
|
|||||||
.news-post h2,
|
.news-post h2,
|
||||||
.news-post h3,
|
.news-post h3,
|
||||||
.news-post h4 {
|
.news-post h4 {
|
||||||
margin-top: 1em !important;
|
margin-top: 1em !important;
|
||||||
margin-bottom: 1em !important;
|
margin-bottom: 1em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-thread p,
|
.mail-thread p,
|
||||||
.news-post p {
|
.news-post p {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-thread pre,
|
.mail-thread pre,
|
||||||
|
.news-post pre {
|
||||||
|
font-family: monospace;
|
||||||
|
background-color: #eee !important;
|
||||||
|
padding: 0.5em;
|
||||||
|
white-space: break-spaces;
|
||||||
|
}
|
||||||
|
|
||||||
.mail-thread code,
|
.mail-thread code,
|
||||||
.news-post pre,
|
|
||||||
.news-post code {
|
.news-post code {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
background-color: #eee !important;
|
white-space: break-spaces;
|
||||||
padding: 0.5em !important;
|
background-color: #eee !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-thread blockquote {
|
.mail-thread blockquote {
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
border-left: 2px solid #ddd;
|
border-left: 2px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-thread ol,
|
.mail-thread ol,
|
||||||
.mail-thread ul {
|
.mail-thread ul {
|
||||||
margin-left: 2em;
|
margin-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-thread .noreply-news-bloomberg-com a {
|
||||||
|
background-color: initial !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-thread .noreply-news-bloomberg-com h2 {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hackaday figures have unreadable black on dark grey */
|
/* Hackaday figures have unreadable black on dark grey */
|
||||||
.news-post figcaption.wp-caption-text {
|
.news-post figcaption.wp-caption-text {
|
||||||
background-color: initial !important;
|
background-color: initial !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.news-post.site-nautilus .article-ad,
|
.news-post.site-nautilus .article-ad,
|
||||||
.news-post.site-nautilus .primis-ad {
|
.news-post.site-nautilus .primis-ad {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.news-post.site-slashdot .story-byline {
|
.news-post.site-slashdot .story-byline {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
height: initial !important;
|
height: initial !important;
|
||||||
overflow: auto !important;
|
overflow: auto !important;
|
||||||
position: static !important;
|
position: static !important;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user