Compare commits
No commits in common. "de2b79fa2a490a46329d896666d51b2cc05ed3d8" and "ce3c027e9a4f46df7eaa1d151fff187c91291b67" have entirely different histories.
de2b79fa2a
...
ce3c027e9a
2
dev.sh
2
dev.sh
@ -1,6 +1,6 @@
|
|||||||
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 --address 0.0.0.0 --port 6758 --proxy-backend http://localhost:9345/ --proxy-rewrite=/api/" C-m
|
tmux send-keys "cd web; trunk serve --address 0.0.0.0 --port 6758" C-m
|
||||||
tmux new-window -n server
|
tmux new-window -n server
|
||||||
tmux send-keys "cd server; cargo watch -x run" C-m
|
tmux send-keys "cd server; cargo watch -x run" C-m
|
||||||
tmux attach -d -t letterbox-dev
|
tmux attach -d -t letterbox-dev
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
.PHONY: all
|
|
||||||
|
|
||||||
# Build in release mode and push to minio for serving.
|
|
||||||
all:
|
|
||||||
trunk build --release
|
|
||||||
mc mirror --overwrite --remove dist/ m/letterbox/
|
|
||||||
@ -16,13 +16,9 @@
|
|||||||
.error {
|
.error {
|
||||||
background-color: red;
|
background-color: red;
|
||||||
}
|
}
|
||||||
.view-part-text-plain {
|
.text_plain {
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
iframe {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
210
web/src/lib.rs
210
web/src/lib.rs
@ -3,8 +3,8 @@
|
|||||||
// 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 log::{debug, error, info, warn, Level};
|
use log::{debug, error, info, Level};
|
||||||
use notmuch::{Content, Part, SearchSummary, Thread, ThreadNode, ThreadSet};
|
use notmuch::{Content, Part, SearchSummary, ThreadNode, ThreadSet};
|
||||||
use seed::{prelude::*, *};
|
use seed::{prelude::*, *};
|
||||||
|
|
||||||
// ------ ------
|
// ------ ------
|
||||||
@ -13,50 +13,27 @@ use seed::{prelude::*, *};
|
|||||||
|
|
||||||
// `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 {
|
||||||
warn!("init called");
|
|
||||||
log!(url);
|
log!(url);
|
||||||
let mut url = url.clone();
|
orders.subscribe(|_: subs::UrlChanged| info!("url changed!"));
|
||||||
let mut query = "".to_string();
|
orders
|
||||||
let hpp = url.next_hash_path_part();
|
.skip()
|
||||||
log!(hpp);
|
.perform_cmd(async { Msg::SearchResult(search_request("*").await) });
|
||||||
match hpp {
|
|
||||||
Some("t") => {
|
|
||||||
let tid = url.next_hash_path_part().unwrap_or("").to_string();
|
|
||||||
orders.send_msg(Msg::ShowRequest(tid));
|
|
||||||
}
|
|
||||||
Some("s") => {
|
|
||||||
query = url.next_hash_path_part().unwrap_or("").to_string();
|
|
||||||
orders.send_msg(Msg::SearchRequest(query.clone()));
|
|
||||||
}
|
|
||||||
p => {
|
|
||||||
log!(p);
|
|
||||||
orders.send_msg(Msg::SearchRequest("".to_string()));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
orders.subscribe(|uc: subs::UrlChanged| {
|
|
||||||
info!("uc {:#?}", uc);
|
|
||||||
});
|
|
||||||
|
|
||||||
info!("init query '{}'", query);
|
|
||||||
Model {
|
Model {
|
||||||
context: Context::None,
|
search_results: None,
|
||||||
query,
|
show_results: None,
|
||||||
|
query: "".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------ ------
|
// ------ ------
|
||||||
// Model
|
// Model
|
||||||
// ------ ------
|
// ------ ------
|
||||||
enum Context {
|
|
||||||
None,
|
|
||||||
Search(SearchSummary),
|
|
||||||
Thread(ThreadSet),
|
|
||||||
}
|
|
||||||
|
|
||||||
// `Model` describes our app state.
|
// `Model` describes our app state.
|
||||||
struct Model {
|
struct Model {
|
||||||
|
search_results: Option<SearchSummary>,
|
||||||
|
show_results: Option<ThreadSet>,
|
||||||
query: String,
|
query: String,
|
||||||
context: Context,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------ ------
|
// ------ ------
|
||||||
@ -77,34 +54,34 @@ enum Msg {
|
|||||||
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||||
match msg {
|
match msg {
|
||||||
Msg::Noop => {}
|
Msg::Noop => {}
|
||||||
|
|
||||||
Msg::SearchRequest(query) => {
|
Msg::SearchRequest(query) => {
|
||||||
|
model.show_results = None;
|
||||||
model.query = query.clone();
|
model.query = query.clone();
|
||||||
let url = Url::new().set_hash_path(["s", &query]);
|
|
||||||
orders.request_url(url);
|
|
||||||
orders
|
orders
|
||||||
.skip()
|
.skip()
|
||||||
.perform_cmd(async move { Msg::SearchResult(search_request(&query).await) });
|
.perform_cmd(async move { Msg::SearchResult(search_request(&query).await) });
|
||||||
}
|
}
|
||||||
|
|
||||||
Msg::SearchResult(Ok(response_data)) => {
|
Msg::SearchResult(Ok(response_data)) => {
|
||||||
debug!("fetch ok {:#?}", response_data);
|
debug!("fetch ok {:#?}", response_data);
|
||||||
model.context = Context::Search(response_data);
|
model.search_results = Some(response_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
Msg::SearchResult(Err(fetch_error)) => {
|
Msg::SearchResult(Err(fetch_error)) => {
|
||||||
error!("fetch failed {:?}", fetch_error);
|
error!("fetch failed {:?}", fetch_error);
|
||||||
}
|
}
|
||||||
|
Msg::ShowRequest(query) => {
|
||||||
Msg::ShowRequest(tid) => {
|
model.show_results = None;
|
||||||
let url = Url::new().set_hash_path(["t", &tid]);
|
|
||||||
orders.request_url(url);
|
|
||||||
orders
|
orders
|
||||||
.skip()
|
.skip()
|
||||||
.perform_cmd(async move { Msg::ShowResult(show_request(&tid).await) });
|
.perform_cmd(async move { Msg::ShowResult(show_request(&query).await) });
|
||||||
}
|
}
|
||||||
|
|
||||||
Msg::ShowResult(Ok(response_data)) => {
|
Msg::ShowResult(Ok(response_data)) => {
|
||||||
debug!("fetch ok {:#?}", response_data);
|
debug!("fetch ok {:#?}", response_data);
|
||||||
model.context = Context::Thread(response_data);
|
model.show_results = Some(response_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
Msg::ShowResult(Err(fetch_error)) => {
|
Msg::ShowResult(Err(fetch_error)) => {
|
||||||
error!("fetch failed {:?}", fetch_error);
|
error!("fetch failed {:?}", fetch_error);
|
||||||
}
|
}
|
||||||
@ -112,7 +89,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()
|
||||||
@ -123,20 +99,20 @@ async fn search_request(query: &str) -> fetch::Result<SearchSummary> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod api {
|
mod api {
|
||||||
const BASE_URL: &str = "/api";
|
const BASE_URL: &str = "http://nixos-07:9345";
|
||||||
pub fn search(query: &str) -> String {
|
pub fn search(query: &str) -> String {
|
||||||
format!("{}/search/{}", BASE_URL, query)
|
format!("{}/search/{}", BASE_URL, query)
|
||||||
}
|
}
|
||||||
pub fn show(tid: &str) -> String {
|
pub fn show(query: &str) -> String {
|
||||||
format!("{}/show/{}", BASE_URL, tid)
|
format!("{}/show/{}", BASE_URL, query)
|
||||||
}
|
}
|
||||||
pub fn original(message_id: &str) -> String {
|
pub fn original(message_id: &str) -> String {
|
||||||
format!("{}/original/{}", BASE_URL, message_id)
|
format!("{}/original/{}", BASE_URL, message_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn show_request(tid: &str) -> fetch::Result<ThreadSet> {
|
async fn show_request(query: &str) -> fetch::Result<ThreadSet> {
|
||||||
Request::new(api::show(tid))
|
Request::new(api::show(query))
|
||||||
.method(Method::Get)
|
.method(Method::Get)
|
||||||
.fetch()
|
.fetch()
|
||||||
.await?
|
.await?
|
||||||
@ -203,7 +179,11 @@ fn view_part(part: &Part) -> Node<Msg> {
|
|||||||
"text/plain" => view_text_plain(&part.content),
|
"text/plain" => view_text_plain(&part.content),
|
||||||
"text/html" => {
|
"text/html" => {
|
||||||
if let Some(Content::String(html)) = &part.content {
|
if let Some(Content::String(html)) = &part.content {
|
||||||
return div![C!["view-part-text-html"], div!["TEST"], raw![&html]];
|
return div![
|
||||||
|
C!["view-part-text-html"],
|
||||||
|
div!["TEST"],
|
||||||
|
iframe![Node::from_html(None, &html)]
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
div![
|
div![
|
||||||
C!["error"],
|
C!["error"],
|
||||||
@ -262,13 +242,61 @@ fn first_subject(thread: &ThreadNode) -> Option<String> {
|
|||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
let msg = thread.0.as_ref();
|
||||||
|
if let Some(msg) = msg {
|
||||||
|
div![
|
||||||
|
a![attrs! {At::Href=>api::original(&msg.id)}, "Original"],
|
||||||
|
table![
|
||||||
|
tr![th!["Subject"], td![&msg.headers.subject],],
|
||||||
|
tr![th!["From"], td![&msg.headers.from],],
|
||||||
|
tr![th!["To"], td![&msg.headers.to],],
|
||||||
|
tr![th!["CC"], td![&msg.headers.cc],],
|
||||||
|
tr![th!["BCC"], td![&msg.headers.bcc],],
|
||||||
|
tr![th!["Reply-To"], td![&msg.headers.reply_to],],
|
||||||
|
tr![th!["Date"], td![&msg.headers.date],],
|
||||||
|
],
|
||||||
|
table![
|
||||||
|
tr![th!["MessageId"], td![&msg.id],],
|
||||||
|
tr![
|
||||||
|
th!["Match"],
|
||||||
|
td![if msg.r#match { "true" } else { "false" }],
|
||||||
|
],
|
||||||
|
tr![
|
||||||
|
th!["Excluded"],
|
||||||
|
td![if msg.excluded { "true" } else { "false" }],
|
||||||
|
],
|
||||||
|
tr![th!["Filename"], td![&msg.filename],],
|
||||||
|
tr![th!["Timestamp"], td![msg.timestamp.to_string()],],
|
||||||
|
tr![th!["Date"], td![&msg.date_relative],],
|
||||||
|
tr![th!["Tags"], td![format!("{:?}", msg.tags)],],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
div![h2!["No message"]]
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
fn view_search_results(query: &str, search_results: &SearchSummary) -> Node<Msg> {
|
// `view` describes what to display.
|
||||||
seed::document().set_title(&format!("lb: {}", query));
|
fn view(model: &Model) -> Node<Msg> {
|
||||||
|
let content = if let Some(show_results) = &model.show_results {
|
||||||
|
assert_eq!(show_results.0.len(), 1);
|
||||||
|
let thread = &show_results.0[0];
|
||||||
|
assert_eq!(thread.0.len(), 1);
|
||||||
|
let thread_node = &thread.0[0];
|
||||||
|
div![
|
||||||
|
h1![first_subject(&thread_node)],
|
||||||
|
a![
|
||||||
|
attrs! {At::Href=>api::original(&thread_node.0.as_ref().expect("message missing").id)},
|
||||||
|
"Original"
|
||||||
|
],
|
||||||
|
view_message(&thread_node),
|
||||||
|
pre!["Add zippy for debug dump"] /* pre![format!("Thread: {:#?}", show_results).replace(" ", " ")] */
|
||||||
|
]
|
||||||
|
} else if let Some(search_results) = &model.search_results {
|
||||||
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,
|
&r.authors,
|
||||||
IF!(r.total>1 => small![" ", r.total.to_string()]),
|
IF!(r.total>1 => small![" ", r.total.to_string()]),
|
||||||
@ -279,62 +307,11 @@ fn view_search_results(query: &str, search_results: &SearchSummary) -> Node<Msg>
|
|||||||
ev(Ev::Click, move |_| Msg::ShowRequest(tid)),
|
ev(Ev::Click, move |_| Msg::ShowRequest(tid)),
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
div![table![
|
div![table![tr![th!["From"], th!["Subject"], th!["Date"]], rows]]
|
||||||
tr![th!["tid"], th!["From"], th!["Subject"], th!["Date"]],
|
} else {
|
||||||
rows
|
div![h1!["Loading"]]
|
||||||
]]
|
};
|
||||||
}
|
let query = model.query.clone();
|
||||||
|
|
||||||
fn view_thread(thread_set: &ThreadSet) -> Node<Msg> {
|
|
||||||
assert_eq!(thread_set.0.len(), 1);
|
|
||||||
let thread = &thread_set.0[0];
|
|
||||||
assert_eq!(thread.0.len(), 1);
|
|
||||||
let thread_node = &thread.0[0];
|
|
||||||
let subject = first_subject(&thread_node).unwrap_or("<No subject>".to_string());
|
|
||||||
seed::document().set_title(&subject);
|
|
||||||
div![
|
|
||||||
h1![subject],
|
|
||||||
a![
|
|
||||||
attrs! {At::Href=>api::original(&thread_node.0.as_ref().expect("message missing").id)},
|
|
||||||
"Original"
|
|
||||||
],
|
|
||||||
view_message(&thread_node),
|
|
||||||
pre![
|
|
||||||
"Add zippy for debug dump",
|
|
||||||
view_debug_thread_set(thread_set)
|
|
||||||
] /* pre![format!("Thread: {:#?}", thread_set).replace(" ", " ")] */
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_debug_thread_set(thread_set: &ThreadSet) -> Node<Msg> {
|
|
||||||
ul![thread_set
|
|
||||||
.0
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, t)| { li!["t", i, ": ", view_debug_thread(t),] })]
|
|
||||||
}
|
|
||||||
fn view_debug_thread(thread: &Thread) -> Node<Msg> {
|
|
||||||
ul![thread
|
|
||||||
.0
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, tn)| { li!["tn", i, ": ", view_debug_thread_node(tn),] })]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_debug_thread_node(thread_node: &ThreadNode) -> Node<Msg> {
|
|
||||||
ul![
|
|
||||||
IF!(thread_node.0.is_some()=>li!["tn id:", &thread_node.0.as_ref().unwrap().id]),
|
|
||||||
thread_node.1.iter().enumerate().map(|(i, tn)| li![
|
|
||||||
"tn",
|
|
||||||
i,
|
|
||||||
": ",
|
|
||||||
view_debug_thread_node(tn)
|
|
||||||
])
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_header(query: &str) -> Node<Msg> {
|
|
||||||
let query = query.to_string();
|
|
||||||
div![
|
div![
|
||||||
button![
|
button![
|
||||||
"Unread",
|
"Unread",
|
||||||
@ -349,25 +326,16 @@ fn view_header(query: &str) -> Node<Msg> {
|
|||||||
},
|
},
|
||||||
input_ev(Ev::Input, Msg::SearchRequest),
|
input_ev(Ev::Input, Msg::SearchRequest),
|
||||||
// Resend search on enter.
|
// Resend search on enter.
|
||||||
keyboard_ev(Ev::KeyUp, move |e| if e.key_code() == 0x0d {
|
keyboard_ev(Ev::KeyUp, |e| if e.key_code() == 0x0d {
|
||||||
Msg::SearchRequest(query)
|
Msg::SearchRequest(query)
|
||||||
} else {
|
} else {
|
||||||
Msg::Noop
|
Msg::Noop
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
content
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// `view` describes what to display.
|
|
||||||
fn view(model: &Model) -> Node<Msg> {
|
|
||||||
let content = match &model.context {
|
|
||||||
Context::None => div![h1!["Loading"]],
|
|
||||||
Context::Thread(thread_set) => view_thread(thread_set),
|
|
||||||
Context::Search(search_results) => view_search_results(&model.query, search_results),
|
|
||||||
};
|
|
||||||
div![view_header(&model.query), content]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------ ------
|
// ------ ------
|
||||||
// Start
|
// Start
|
||||||
// ------ ------
|
// ------ ------
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user