Compare commits

...

9 Commits

Author SHA1 Message Date
01e5ea14ab URL decode queries. 2023-09-05 09:49:59 -07:00
042d475c75 Style tweaks 2023-09-02 13:10:44 -07:00
dd0af52feb cargo update 2023-09-02 13:10:34 -07:00
130f9bbeba Use Trunk.toml for trunk config. 2023-09-02 13:10:17 -07:00
0a05b32a7a Remove 'TEST' text when viewing certain email types 2023-09-02 09:00:36 -07:00
c3f897c61a Fix pagination and default homepage to unread search. 2023-08-11 16:51:41 -07:00
c62bac037f Reload page on refresh 2023-08-11 14:06:47 -07:00
79a57f3082 Address workspace lint 2023-08-11 13:55:39 -07:00
c33de9d754 cargo update 2023-07-15 16:58:40 -07:00
9 changed files with 689 additions and 858 deletions

1472
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
[workspace] [workspace]
resolver = "2"
members = [ members = [
"web", "web",
"server", "server",

2
dev.sh
View File

@@ -1,7 +1,7 @@
cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" cd -- "$( dirname -- "${BASH_SOURCE[0]}" )"
tmux new-session -d -s letterbox-dev tmux new-session -d -s letterbox-dev
tmux rename-window web tmux rename-window web
tmux send-keys "cd web; trunk serve --release --address 0.0.0.0 --port 6758 --proxy-backend http://localhost:9345/ --proxy-rewrite=/api/ -w ../shared -w ../notmuch -w ./" C-m tmux send-keys "cd web; trunk serve -w ../shared -w ../notmuch -w ./" C-m
tmux new-window -n server tmux new-window -n server
tmux send-keys "cd server; cargo watch -x run -w ../shared -w ../notmuch -w ./" C-m tmux send-keys "cd server; cargo watch -x run -w ../shared -w ../notmuch -w ./" C-m
tmux attach -d -t letterbox-dev tmux attach -d -t letterbox-dev

View File

@@ -454,6 +454,8 @@ pub enum NotmuchError {
SerdeJson(#[from] serde_json::Error), SerdeJson(#[from] serde_json::Error),
#[error("failed to parse bytes as str")] #[error("failed to parse bytes as str")]
Utf8Error(#[from] std::str::Utf8Error), Utf8Error(#[from] std::str::Utf8Error),
#[error("failed to parse bytes as String")]
StringUtf8Error(#[from] std::string::FromUtf8Error),
#[error("failed to parse str as int")] #[error("failed to parse str as int")]
ParseIntError(#[from] std::num::ParseIntError), ParseIntError(#[from] std::num::ParseIntError),
} }

View File

@@ -16,6 +16,7 @@ serde = { version = "1.0.147", features = ["derive"] }
log = "0.4.17" log = "0.4.17"
tokio = "1.26.0" tokio = "1.26.0"
glog = "0.1.0" glog = "0.1.0"
urlencoding = "2.1.3"
[dependencies.rocket_contrib] [dependencies.rocket_contrib]
version = "0.4.11" version = "0.4.11"

View File

@@ -4,7 +4,7 @@ extern crate rocket;
use std::{error::Error, io::Cursor, str::FromStr}; use std::{error::Error, io::Cursor, str::FromStr};
use glog::Flags; use glog::Flags;
use notmuch::{Notmuch, NotmuchError, SearchSummary, ThreadSet}; use notmuch::{Notmuch, NotmuchError, ThreadSet};
use rocket::{ use rocket::{
http::{ContentType, Header}, http::{ContentType, Header},
request::Request, request::Request,
@@ -40,13 +40,14 @@ async fn search(
) -> Result<Json<shared::SearchResult>, Debug<NotmuchError>> { ) -> Result<Json<shared::SearchResult>, Debug<NotmuchError>> {
let page = page.unwrap_or(0); let page = page.unwrap_or(0);
let results_per_page = results_per_page.unwrap_or(10); let results_per_page = results_per_page.unwrap_or(10);
let query = urlencoding::decode(query).map_err(NotmuchError::from)?;
info!(" search '{query}'"); info!(" search '{query}'");
let res = shared::SearchResult { let res = shared::SearchResult {
summary: nm.search(query, page * results_per_page, results_per_page)?, summary: nm.search(&query, page * results_per_page, results_per_page)?,
query: query.to_string(), query: query.to_string(),
page, page,
results_per_page, results_per_page,
total: nm.count(query)?, total: nm.count(&query)?,
}; };
Ok(Json(res)) Ok(Json(res))
} }
@@ -56,13 +57,15 @@ async fn show_pretty(
nm: &State<Notmuch>, nm: &State<Notmuch>,
query: &str, query: &str,
) -> Result<Json<ThreadSet>, Debug<NotmuchError>> { ) -> Result<Json<ThreadSet>, Debug<NotmuchError>> {
let res = nm.show(query)?; let query = urlencoding::decode(query).map_err(NotmuchError::from)?;
let res = nm.show(&query)?;
Ok(Json(res)) Ok(Json(res))
} }
#[get("/show/<query>")] #[get("/show/<query>")]
async fn show(nm: &State<Notmuch>, query: &str) -> Result<Json<ThreadSet>, Debug<NotmuchError>> { async fn show(nm: &State<Notmuch>, query: &str) -> Result<Json<ThreadSet>, Debug<NotmuchError>> {
let res = nm.show(query)?; let query = urlencoding::decode(query).map_err(NotmuchError::from)?;
let res = nm.show(&query)?;
Ok(Json(res)) Ok(Json(res))
} }

11
web/Trunk.toml Normal file
View File

@@ -0,0 +1,11 @@
[build]
release = true
[serve]
# The address to serve on.
address = "0.0.0.0"
port = 6758
[[proxy]]
backend = "http://localhost:9345/"
rewrite= "/api/"

View File

@@ -4,18 +4,19 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="modulepreload" href="/pkg/package.js" as="script" type="text/javascript">
<link rel="preload" href="/pkg/package_bg.wasm" as="fetch" type="application/wasm" crossorigin="anonymous">
<link rel="stylesheet", href="https://jenil.github.io/bulmaswatch/cyborg/bulmaswatch.min.css"> <link rel="stylesheet", href="https://jenil.github.io/bulmaswatch/cyborg/bulmaswatch.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css" integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ==" crossorigin="anonymous" referrerpolicy="no-referrer" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css" integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style> <style>
.message { .message {
padding-left: 0.5em; padding: 0.5em;*/
} }
.body { .body {
background: white; background: white;
color: black; color: black;
padding-bottom: 1em; padding: 0.5em;
margin-left: -0.5em;
margin-right: -0.5em;
margin-top: 0.5em;
} }
.error { .error {
background-color: red; background-color: red;
@@ -83,10 +84,6 @@ input::placeholder, .input::placeholder{
<body> <body>
<section id="app"></section> <section id="app"></section>
<script type="module">
import init from '/pkg/package.js';
init('/pkg/package_bg.wasm');
</script>
</body> </body>
</html> </html>

View File

@@ -22,9 +22,10 @@ const SEARCH_RESULTS_PER_PAGE: usize = 20;
// `init` describes what should happen when your app started. // `init` describes what should happen when your app started.
fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model { fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
orders if url.hash().is_none() {
.subscribe(on_url_changed) orders.request_url(urls::search("is:unread", 0));
.notify(subs::UrlChanged(url.clone())); };
orders.subscribe(on_url_changed);
Model { Model {
context: Context::None, context: Context::None,
@@ -119,6 +120,9 @@ enum RefreshingState {
// `Msg` describes the different events you can modify state with. // `Msg` describes the different events you can modify state with.
enum Msg { enum Msg {
Noop, Noop,
// Tell the client to refresh its state
Reload,
// Tell the server to update state
RefreshStart, RefreshStart,
RefreshDone(Option<FetchError>), RefreshDone(Option<FetchError>),
SearchRequest { SearchRequest {
@@ -149,6 +153,10 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} else { } else {
RefreshingState::None RefreshingState::None
}; };
orders.perform_cmd(async move { Msg::Reload });
}
Msg::Reload => {
orders.perform_cmd(async move { on_url_changed(subs::UrlChanged(Url::current())) });
} }
Msg::SearchRequest { Msg::SearchRequest {
@@ -315,7 +323,6 @@ fn view_message(thread: &ThreadNode) -> Node<Msg> {
div![C!["header"], "From: ", &message.headers.from], div![C!["header"], "From: ", &message.headers.from],
div![C!["header"], "Date: ", &message.headers.date], div![C!["header"], "Date: ", &message.headers.date],
div![C!["header"], "To: ", &message.headers.to], div![C!["header"], "To: ", &message.headers.to],
hr![],
div![ div![
C!["body"], C!["body"],
match &message.body { match &message.body {
@@ -352,7 +359,7 @@ fn view_part(part: &Part) -> Node<Msg> {
.build(); .build();
let inlined = inliner.inline(html).expect("failed to inline CSS"); let inlined = inliner.inline(html).expect("failed to inline CSS");
return div![C!["view-part-text-html"], div!["TEST"], raw![&inlined]]; return div![C!["view-part-text-html"], raw![&inlined]];
} else { } else {
div![ div![
C!["error"], C!["error"],
@@ -505,7 +512,6 @@ fn view_mobile_search_results(query: &str, search_results: &shared::SearchResult
span![C!["tags"], tags_chiclet(&r.tags, true)], span![C!["tags"], tags_chiclet(&r.tags, true)],
], ],
span![C!["date"], &r.date_relative], span![C!["date"], &r.date_relative],
hr![],
] ]
}); });
let first = search_results.page * search_results.results_per_page; let first = search_results.page * search_results.results_per_page;
@@ -576,12 +582,8 @@ fn view_search_pager(start: usize, count: usize, total: usize) -> Node<Msg> {
nav![ nav![
C!["pagination"], C!["pagination"],
a![ a![
C![ C!["pagination-previous", "button",],
"pagination-previous", IF!(is_first => attrs!{ At::Disabled=>true }),
"button",
IF!(is_first => "is-static"),
IF!(is_first => "is-info"),
],
"<", "<",
ev(Ev::Click, |_| Msg::PreviousPage) ev(Ev::Click, |_| Msg::PreviousPage)
], ],
@@ -606,17 +608,19 @@ fn view_thread(thread_set: &ThreadSet) -> Node<Msg> {
let subject = first_subject(&thread_node).unwrap_or("<No subject>".to_string()); let subject = first_subject(&thread_node).unwrap_or("<No subject>".to_string());
set_title(&subject); set_title(&subject);
div![ div![
h1![subject], h1![C!["title"], subject],
view_message(&thread_node),
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)},
"Original" "Original"
], ],
view_message(&thread_node), /*
div![ div![
C!["debug"], C!["debug"],
"Add zippy for debug dump", "Add zippy for debug dump",
view_debug_thread_set(thread_set) view_debug_thread_set(thread_set)
] /* pre![format!("Thread: {:#?}", thread_set).replace(" ", " ")] */ ] /* pre![format!("Thread: {:#?}", thread_set).replace(" ", " ")] */
*/
] ]
} }