Compare commits
3 Commits
19ee6f338d
...
d8275debdc
| Author | SHA1 | Date | |
|---|---|---|---|
| d8275debdc | |||
| 25541bc1ca | |||
| e5a27f82f9 |
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -975,10 +975,12 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"console_log",
|
"console_log",
|
||||||
|
"itertools",
|
||||||
"log 0.4.17",
|
"log 0.4.17",
|
||||||
"notmuch",
|
"notmuch",
|
||||||
"seed",
|
"seed",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"wasm-bindgen-test",
|
"wasm-bindgen-test",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1781,9 +1783,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.87"
|
version = "1.0.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
|
checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
|
|||||||
@ -425,14 +425,14 @@ pub struct SearchTags(pub Vec<String>);
|
|||||||
pub struct ThreadSummary {
|
pub struct ThreadSummary {
|
||||||
pub thread: ThreadId,
|
pub thread: ThreadId,
|
||||||
pub timestamp: UnixTime,
|
pub timestamp: UnixTime,
|
||||||
pub date_relative: String,
|
|
||||||
/// user-friendly timestamp
|
/// user-friendly timestamp
|
||||||
pub matched: isize,
|
pub date_relative: String,
|
||||||
/// number of matched messages
|
/// number of matched messages
|
||||||
pub total: isize,
|
pub matched: isize,
|
||||||
/// total messages in thread
|
/// total messages in thread
|
||||||
pub authors: String,
|
pub total: isize,
|
||||||
/// comma-separated names with | between matched and unmatched
|
/// comma-separated names with | between matched and unmatched
|
||||||
|
pub authors: String,
|
||||||
pub subject: String,
|
pub subject: String,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
|
|
||||||
@ -541,6 +541,8 @@ impl Notmuch {
|
|||||||
Ok(BufReader::new(child.stdout.take().unwrap()).lines())
|
Ok(BufReader::new(child.stdout.take().unwrap()).lines())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(wathiede): implement tags() based on "notmuch search --output=tags '*'"
|
||||||
|
|
||||||
fn run_notmuch<I, S>(&self, args: I) -> Result<Vec<u8>, NotmuchError>
|
fn run_notmuch<I, S>(&self, args: I) -> Result<Vec<u8>, NotmuchError>
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = S>,
|
I: IntoIterator<Item = S>,
|
||||||
|
|||||||
@ -22,6 +22,8 @@ seed = "0.9.2"
|
|||||||
console_log = {git = "http://git-private.h.xinu.tv/wathiede/console_log.git"}
|
console_log = {git = "http://git-private.h.xinu.tv/wathiede/console_log.git"}
|
||||||
serde = { version = "1.0.147", features = ["derive"] }
|
serde = { version = "1.0.147", features = ["derive"] }
|
||||||
notmuch = {path = "../notmuch"}
|
notmuch = {path = "../notmuch"}
|
||||||
|
itertools = "0.10.5"
|
||||||
|
serde_json = { version = "1.0.93", features = ["unbounded_depth"] }
|
||||||
|
|
||||||
[package.metadata.wasm-pack.profile.release]
|
[package.metadata.wasm-pack.profile.release]
|
||||||
wasm-opt = ['-Os']
|
wasm-opt = ['-Os']
|
||||||
|
|||||||
@ -6,12 +6,15 @@
|
|||||||
<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="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="preload" href="/pkg/package_bg.wasm" as="fetch" type="application/wasm" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.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" />
|
||||||
<style>
|
<style>
|
||||||
.message {
|
.message {
|
||||||
padding-left: 0.5em;
|
padding-left: 0.5em;
|
||||||
}
|
}
|
||||||
.body {
|
.body {
|
||||||
padding-bottom: 1em;
|
padding-bottom: 1em;
|
||||||
|
border: 1px red solid;
|
||||||
}
|
}
|
||||||
.error {
|
.error {
|
||||||
background-color: red;
|
background-color: red;
|
||||||
@ -23,6 +26,14 @@ iframe {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.index .from {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
.index .subject {
|
||||||
|
}
|
||||||
|
.index .date {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
135
web/src/lib.rs
135
web/src/lib.rs
@ -3,9 +3,11 @@
|
|||||||
// but some rules are too "annoying" or are not applicable for your case.)
|
// but some rules are too "annoying" or are not applicable for your case.)
|
||||||
#![allow(clippy::wildcard_imports)]
|
#![allow(clippy::wildcard_imports)]
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
use log::{debug, error, info, warn, Level};
|
use log::{debug, error, info, warn, Level};
|
||||||
use notmuch::{Content, Part, SearchSummary, Thread, ThreadNode, ThreadSet};
|
use notmuch::{Content, Part, SearchSummary, Thread, ThreadNode, ThreadSet};
|
||||||
use seed::{prelude::*, *};
|
use seed::{prelude::*, *};
|
||||||
|
use serde::de::Deserialize;
|
||||||
|
|
||||||
// ------ ------
|
// ------ ------
|
||||||
// Init
|
// Init
|
||||||
@ -112,7 +114,6 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn search_request(query: &str) -> fetch::Result<SearchSummary> {
|
async fn search_request(query: &str) -> fetch::Result<SearchSummary> {
|
||||||
info!("making search request for '{}'", query);
|
|
||||||
Request::new(api::search(query))
|
Request::new(api::search(query))
|
||||||
.method(Method::Get)
|
.method(Method::Get)
|
||||||
.fetch()
|
.fetch()
|
||||||
@ -136,13 +137,17 @@ mod api {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn show_request(tid: &str) -> fetch::Result<ThreadSet> {
|
async fn show_request(tid: &str) -> fetch::Result<ThreadSet> {
|
||||||
Request::new(api::show(tid))
|
let b = Request::new(api::show(tid))
|
||||||
.method(Method::Get)
|
.method(Method::Get)
|
||||||
.fetch()
|
.fetch()
|
||||||
.await?
|
.await?
|
||||||
.check_status()?
|
.check_status()?
|
||||||
.json()
|
.bytes()
|
||||||
.await
|
.await?;
|
||||||
|
let mut deserializer = serde_json::Deserializer::from_slice(&b);
|
||||||
|
deserializer.disable_recursion_limit();
|
||||||
|
Ok(ThreadSet::deserialize(&mut deserializer)
|
||||||
|
.map_err(|_| FetchError::JsonError(fetch::JsonError::Serde(JsValue::NULL)))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------ ------
|
// ------ ------
|
||||||
@ -267,6 +272,42 @@ fn set_title(title: &str) {
|
|||||||
seed::document().set_title(&format!("lb: {}", title));
|
seed::document().set_title(&format!("lb: {}", title));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tags_chiclet(tags: &[String]) -> impl Iterator<Item = Node<Msg>> + '_ {
|
||||||
|
tags.iter().map(|tag| match tag.as_str() {
|
||||||
|
"attachment" => span![C!["tag"], "📎"],
|
||||||
|
"replied" => span![C!["tag"], i![C!["fa-solid", "fa-reply"]]],
|
||||||
|
_ => span![C!["tag"], tag],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pretty_authors(authors: &str) -> impl Iterator<Item = Node<Msg>> + '_ {
|
||||||
|
let authors = authors.split(',');
|
||||||
|
|
||||||
|
/*
|
||||||
|
if authors.len() == 1 {
|
||||||
|
return authors.iter().filter_map(|author| {
|
||||||
|
Some(span![
|
||||||
|
attrs! {
|
||||||
|
At::Title => author.trim()},
|
||||||
|
author
|
||||||
|
])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
authors
|
||||||
|
.filter_map(|author| {
|
||||||
|
author.split_whitespace().nth(0).map(|first| {
|
||||||
|
span![
|
||||||
|
attrs! {
|
||||||
|
At::Title => author.trim()},
|
||||||
|
first
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.intersperse(span![", "])
|
||||||
|
}
|
||||||
|
|
||||||
fn view_search_results(query: &str, search_results: &SearchSummary) -> Node<Msg> {
|
fn view_search_results(query: &str, search_results: &SearchSummary) -> Node<Msg> {
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
set_title("all mail");
|
set_title("all mail");
|
||||||
@ -276,20 +317,31 @@ fn view_search_results(query: &str, search_results: &SearchSummary) -> Node<Msg>
|
|||||||
let rows = search_results.0.iter().map(|r| {
|
let rows = search_results.0.iter().map(|r| {
|
||||||
let tid = r.thread.clone();
|
let tid = r.thread.clone();
|
||||||
tr![
|
tr![
|
||||||
td![],
|
|
||||||
td![
|
td![
|
||||||
&r.authors,
|
C!["from"],
|
||||||
|
pretty_authors(&r.authors),
|
||||||
IF!(r.total>1 => small![" ", r.total.to_string()]),
|
IF!(r.total>1 => small![" ", r.total.to_string()]),
|
||||||
IF!(r.tags.contains(&"attachment".to_string()) => "📎"),
|
|
||||||
],
|
],
|
||||||
td![&r.subject],
|
td![C!["subject"], tags_chiclet(&r.tags), " ", &r.subject],
|
||||||
td![&r.date_relative],
|
td![C!["date"], &r.date_relative],
|
||||||
ev(Ev::Click, move |_| Msg::ShowRequest(tid)),
|
ev(Ev::Click, move |_| Msg::ShowRequest(tid)),
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
div![table![
|
div![table![
|
||||||
tr![th!["tid"], th!["From"], th!["Subject"], th!["Date"]],
|
C![
|
||||||
rows
|
"table",
|
||||||
|
"index",
|
||||||
|
"is-fullwidth",
|
||||||
|
"is-hoverable",
|
||||||
|
"is-narrow",
|
||||||
|
"is-striped",
|
||||||
|
],
|
||||||
|
thead![tr![
|
||||||
|
th![C!["from"], "From"],
|
||||||
|
th![C!["subject"], "Subject"],
|
||||||
|
th![C!["date"], "Date"]
|
||||||
|
]],
|
||||||
|
tbody![rows]
|
||||||
]]
|
]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,37 +395,56 @@ fn view_debug_thread_node(thread_node: &ThreadNode) -> Node<Msg> {
|
|||||||
|
|
||||||
fn view_header(query: &str) -> Node<Msg> {
|
fn view_header(query: &str) -> Node<Msg> {
|
||||||
let query = query.to_string();
|
let query = query.to_string();
|
||||||
div![
|
nav![
|
||||||
button![
|
C!["navbar"],
|
||||||
"Unread",
|
attrs! {At::Role=>"navigation"},
|
||||||
ev(Ev::Click, |_| Msg::SearchRequest("is:unread".to_string())),
|
div![
|
||||||
],
|
C!["navbar-menu"],
|
||||||
button!["All", ev(Ev::Click, |_| Msg::SearchRequest("".to_string())),],
|
div![
|
||||||
input![
|
C!["navbar-start"],
|
||||||
attrs! {
|
a![
|
||||||
At::Placeholder => "Search";
|
C!["navbar-item", "button",],
|
||||||
At::AutoFocus => true.as_at_value();
|
"Unread",
|
||||||
At::Value => query,
|
ev(Ev::Click, |_| Msg::SearchRequest("is:unread".to_string())),
|
||||||
},
|
],
|
||||||
input_ev(Ev::Input, Msg::SearchRequest),
|
a![
|
||||||
// Resend search on enter.
|
C!["navbar-item", "button"],
|
||||||
keyboard_ev(Ev::KeyUp, move |e| if e.key_code() == 0x0d {
|
"All",
|
||||||
Msg::SearchRequest(query)
|
ev(Ev::Click, |_| Msg::SearchRequest("".to_string())),
|
||||||
} else {
|
],
|
||||||
Msg::Noop
|
input![
|
||||||
}),
|
C!["navbar-item", "input"],
|
||||||
],
|
attrs! {
|
||||||
|
At::Placeholder => "Search";
|
||||||
|
At::AutoFocus => true.as_at_value();
|
||||||
|
At::Value => query,
|
||||||
|
},
|
||||||
|
input_ev(Ev::Input, Msg::SearchRequest),
|
||||||
|
// Resend search on enter.
|
||||||
|
keyboard_ev(Ev::KeyUp, move |e| if e.key_code() == 0x0d {
|
||||||
|
Msg::SearchRequest(query)
|
||||||
|
} else {
|
||||||
|
Msg::Noop
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// `view` describes what to display.
|
// `view` describes what to display.
|
||||||
fn view(model: &Model) -> Node<Msg> {
|
fn view(model: &Model) -> Node<Msg> {
|
||||||
|
info!("view called");
|
||||||
let content = match &model.context {
|
let content = match &model.context {
|
||||||
Context::None => div![h1!["Loading"]],
|
Context::None => div![h1!["Loading"]],
|
||||||
Context::Thread(thread_set) => view_thread(thread_set),
|
Context::Thread(thread_set) => view_thread(thread_set),
|
||||||
Context::Search(search_results) => view_search_results(&model.query, search_results),
|
Context::Search(search_results) => view_search_results(&model.query, search_results),
|
||||||
};
|
};
|
||||||
div![view_header(&model.query), content]
|
div![section![
|
||||||
|
C!["section"],
|
||||||
|
view_header(&model.query),
|
||||||
|
div![C!["container"], content]
|
||||||
|
]]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------ ------
|
// ------ ------
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user