Compare commits
No commits in common. "39bef1ea8729d3f53a22e6f91ca2017401e3d76d" and "72622032ad7a3187e35ce244a358a8cc54263e00" have entirely different histories.
39bef1ea87
...
72622032ad
1407
Cargo.lock
generated
1407
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,4 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
|
||||||
members = [
|
members = [
|
||||||
"web",
|
"web",
|
||||||
"server",
|
"server",
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
mod error;
|
|
||||||
mod nm;
|
|
||||||
|
|
||||||
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};
|
use notmuch::{Notmuch, NotmuchError, SearchSummary, ThreadSet};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::{ContentType, Header},
|
http::{ContentType, Header},
|
||||||
request::Request,
|
request::Request,
|
||||||
@ -16,8 +14,6 @@ use rocket::{
|
|||||||
};
|
};
|
||||||
use rocket_cors::{AllowedHeaders, AllowedOrigins};
|
use rocket_cors::{AllowedHeaders, AllowedOrigins};
|
||||||
|
|
||||||
use crate::error::ServerError;
|
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
fn hello() -> &'static str {
|
fn hello() -> &'static str {
|
||||||
"Hello, world!"
|
"Hello, world!"
|
||||||
@ -55,12 +51,18 @@ async fn search(
|
|||||||
Ok(Json(res))
|
Ok(Json(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/show/<query>")]
|
#[get("/show/<query>/pretty")]
|
||||||
async fn show(
|
async fn show_pretty(
|
||||||
nm: &State<Notmuch>,
|
nm: &State<Notmuch>,
|
||||||
query: &str,
|
query: &str,
|
||||||
) -> Result<Json<Vec<shared::Message>>, Debug<ServerError>> {
|
) -> Result<Json<ThreadSet>, Debug<NotmuchError>> {
|
||||||
let res = nm::threadset_to_messages(nm.show(query).map_err(|e| -> ServerError { e.into() })?)?;
|
let res = nm.show(query)?;
|
||||||
|
Ok(Json(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/show/<query>")]
|
||||||
|
async fn show(nm: &State<Notmuch>, query: &str) -> Result<Json<ThreadSet>, Debug<NotmuchError>> {
|
||||||
|
let res = nm.show(query)?;
|
||||||
Ok(Json(res))
|
Ok(Json(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,6 +153,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
refresh,
|
refresh,
|
||||||
search_all,
|
search_all,
|
||||||
search,
|
search,
|
||||||
|
show_pretty,
|
||||||
show
|
show
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@ -9,27 +9,3 @@ pub struct SearchResult {
|
|||||||
pub results_per_page: usize,
|
pub results_per_page: usize,
|
||||||
pub total: usize,
|
pub total: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct ShowResult {
|
|
||||||
messages: Vec<Message>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type AttachementId = String;
|
|
||||||
|
|
||||||
/// # Number of seconds since the Epoch
|
|
||||||
pub type UnixTime = isize;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
|
||||||
pub struct Message {
|
|
||||||
pub from: String,
|
|
||||||
pub to: Option<String>,
|
|
||||||
pub cc: Option<String>,
|
|
||||||
pub timestamp: UnixTime, // date header as unix time
|
|
||||||
pub date_relative: String, // user-friendly timestamp
|
|
||||||
pub tags: Vec<String>,
|
|
||||||
|
|
||||||
// HTML formatted body
|
|
||||||
pub body: String,
|
|
||||||
pub attachment: Vec<AttachementId>,
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
use seed::Url;
|
|
||||||
|
|
||||||
const BASE_URL: &str = "/api";
|
|
||||||
pub fn refresh() -> String {
|
|
||||||
format!("{BASE_URL}/refresh")
|
|
||||||
}
|
|
||||||
pub fn search(query: &str, page: usize, results_per_page: usize) -> String {
|
|
||||||
let query = Url::encode_uri_component(query);
|
|
||||||
format!("{BASE_URL}/search/{query}?page={page}&results_per_page={results_per_page}")
|
|
||||||
}
|
|
||||||
pub fn show(tid: &str) -> String {
|
|
||||||
format!("{BASE_URL}/show/{tid}")
|
|
||||||
}
|
|
||||||
pub fn original(message_id: &str) -> String {
|
|
||||||
format!("{BASE_URL}/original/{message_id}")
|
|
||||||
}
|
|
||||||
288
web/src/lib.rs
288
web/src/lib.rs
@ -1,6 +1,7 @@
|
|||||||
mod api;
|
// (Lines like the one below ignore selected Clippy rules
|
||||||
mod nm;
|
// - it's useful when you want to check your code with `cargo make verify`
|
||||||
|
// but some rules are too "annoying" or are not applicable for your case.)
|
||||||
|
#![allow(clippy::wildcard_imports)]
|
||||||
use std::{
|
use std::{
|
||||||
collections::hash_map::DefaultHasher,
|
collections::hash_map::DefaultHasher,
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
@ -8,9 +9,9 @@ use std::{
|
|||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use log::{debug, error, info, Level};
|
use log::{debug, error, info, Level};
|
||||||
use notmuch::ThreadSet;
|
use notmuch::{Content, Part, Thread, ThreadNode, ThreadSet};
|
||||||
use seed::{prelude::*, *};
|
use seed::{prelude::*, *};
|
||||||
use serde::Deserialize;
|
use serde::de::Deserialize;
|
||||||
use wasm_timer::Instant;
|
use wasm_timer::Instant;
|
||||||
|
|
||||||
const SEARCH_RESULTS_PER_PAGE: usize = 20;
|
const SEARCH_RESULTS_PER_PAGE: usize = 20;
|
||||||
@ -41,7 +42,7 @@ fn on_url_changed(uc: subs::UrlChanged) -> Msg {
|
|||||||
);
|
);
|
||||||
let hpp = url.remaining_hash_path_parts();
|
let hpp = url.remaining_hash_path_parts();
|
||||||
match hpp.as_slice() {
|
match hpp.as_slice() {
|
||||||
["t", tid] => Msg::ShowRequest(tid.to_string()),
|
["t", tid] => Msg::ShowPrettyRequest(tid.to_string()),
|
||||||
["s", query] => {
|
["s", query] => {
|
||||||
let query = Url::decode_uri_component(query).unwrap_or("".to_string());
|
let query = Url::decode_uri_component(query).unwrap_or("".to_string());
|
||||||
Msg::SearchRequest {
|
Msg::SearchRequest {
|
||||||
@ -93,7 +94,7 @@ mod urls {
|
|||||||
enum Context {
|
enum Context {
|
||||||
None,
|
None,
|
||||||
Search(shared::SearchResult),
|
Search(shared::SearchResult),
|
||||||
Thread(Vec<shared::Message>),
|
Thread(ThreadSet),
|
||||||
}
|
}
|
||||||
|
|
||||||
// `Model` describes our app state.
|
// `Model` describes our app state.
|
||||||
@ -116,7 +117,7 @@ enum RefreshingState {
|
|||||||
|
|
||||||
// (Remove the line below once any of your `Msg` variants doesn't implement `Copy`.)
|
// (Remove the line below once any of your `Msg` variants doesn't implement `Copy`.)
|
||||||
// `Msg` describes the different events you can modify state with.
|
// `Msg` describes the different events you can modify state with.
|
||||||
pub enum Msg {
|
enum Msg {
|
||||||
Noop,
|
Noop,
|
||||||
RefreshStart,
|
RefreshStart,
|
||||||
RefreshDone(Option<FetchError>),
|
RefreshDone(Option<FetchError>),
|
||||||
@ -127,7 +128,9 @@ pub enum Msg {
|
|||||||
},
|
},
|
||||||
SearchResult(fetch::Result<shared::SearchResult>),
|
SearchResult(fetch::Result<shared::SearchResult>),
|
||||||
ShowRequest(String),
|
ShowRequest(String),
|
||||||
ShowResult(fetch::Result<Vec<shared::Message>>),
|
ShowResult(fetch::Result<ThreadSet>),
|
||||||
|
ShowPrettyRequest(String),
|
||||||
|
ShowPrettyResult(fetch::Result<ThreadSet>),
|
||||||
NextPage,
|
NextPage,
|
||||||
PreviousPage,
|
PreviousPage,
|
||||||
}
|
}
|
||||||
@ -144,16 +147,6 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
model.refreshing_state = if let Some(err) = err {
|
model.refreshing_state = if let Some(err) = err {
|
||||||
RefreshingState::Error(format!("{:?}", err))
|
RefreshingState::Error(format!("{:?}", err))
|
||||||
} else {
|
} else {
|
||||||
// If looking at search page, refresh the search to view update on the server side.
|
|
||||||
if let Context::Search(sr) = &model.context {
|
|
||||||
let query = sr.query.clone();
|
|
||||||
let page = sr.page;
|
|
||||||
let results_per_page = sr.results_per_page;
|
|
||||||
orders.perform_cmd(async move {
|
|
||||||
Msg::SearchResult(search_request(&query, page, results_per_page).await)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
RefreshingState::None
|
RefreshingState::None
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -189,6 +182,19 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
Msg::ShowResult(Err(fetch_error)) => {
|
Msg::ShowResult(Err(fetch_error)) => {
|
||||||
error!("fetch failed {:?}", fetch_error);
|
error!("fetch failed {:?}", fetch_error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Msg::ShowPrettyRequest(tid) => {
|
||||||
|
orders
|
||||||
|
.skip()
|
||||||
|
.perform_cmd(async move { Msg::ShowPrettyResult(show_pretty_request(&tid).await) });
|
||||||
|
}
|
||||||
|
Msg::ShowPrettyResult(Ok(response_data)) => {
|
||||||
|
debug!("fetch ok {:#?}", response_data);
|
||||||
|
model.context = Context::Thread(response_data);
|
||||||
|
}
|
||||||
|
Msg::ShowPrettyResult(Err(fetch_error)) => {
|
||||||
|
error!("fetch failed {:?}", fetch_error);
|
||||||
|
}
|
||||||
Msg::NextPage => {
|
Msg::NextPage => {
|
||||||
match &model.context {
|
match &model.context {
|
||||||
Context::Search(sr) => {
|
Context::Search(sr) => {
|
||||||
@ -210,19 +216,6 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn show_request(tid: &str) -> fetch::Result<Vec<shared::Message>> {
|
|
||||||
let b = Request::new(api::show(tid))
|
|
||||||
.method(Method::Get)
|
|
||||||
.fetch()
|
|
||||||
.await?
|
|
||||||
.check_status()?
|
|
||||||
.bytes()
|
|
||||||
.await?;
|
|
||||||
let mut deserializer = serde_json::Deserializer::from_slice(&b);
|
|
||||||
deserializer.disable_recursion_limit();
|
|
||||||
Ok(Vec::<shared::Message>::deserialize(&mut deserializer)
|
|
||||||
.map_err(|_| FetchError::JsonError(fetch::JsonError::Serde(JsValue::NULL)))?)
|
|
||||||
}
|
|
||||||
async fn search_request(
|
async fn search_request(
|
||||||
query: &str,
|
query: &str,
|
||||||
page: usize,
|
page: usize,
|
||||||
@ -237,6 +230,28 @@ async fn search_request(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod api {
|
||||||
|
use seed::Url;
|
||||||
|
|
||||||
|
const BASE_URL: &str = "/api";
|
||||||
|
pub fn refresh() -> String {
|
||||||
|
format!("{BASE_URL}/refresh")
|
||||||
|
}
|
||||||
|
pub fn search(query: &str, page: usize, results_per_page: usize) -> String {
|
||||||
|
let query = Url::encode_uri_component(query);
|
||||||
|
format!("{BASE_URL}/search/{query}?page={page}&results_per_page={results_per_page}")
|
||||||
|
}
|
||||||
|
pub fn show(tid: &str) -> String {
|
||||||
|
format!("{BASE_URL}/show/{tid}")
|
||||||
|
}
|
||||||
|
pub fn show_pretty(tid: &str) -> String {
|
||||||
|
format!("{BASE_URL}/show/{tid}/pretty")
|
||||||
|
}
|
||||||
|
pub fn original(message_id: &str) -> String {
|
||||||
|
format!("{BASE_URL}/original/{message_id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn refresh_request() -> fetch::Result<()> {
|
async fn refresh_request() -> fetch::Result<()> {
|
||||||
let t = Request::new(api::refresh())
|
let t = Request::new(api::refresh())
|
||||||
.method(Method::Get)
|
.method(Method::Get)
|
||||||
@ -249,10 +264,159 @@ async fn refresh_request() -> fetch::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn show_request(tid: &str) -> fetch::Result<ThreadSet> {
|
||||||
|
let b = Request::new(api::show(tid))
|
||||||
|
.method(Method::Get)
|
||||||
|
.fetch()
|
||||||
|
.await?
|
||||||
|
.check_status()?
|
||||||
|
.bytes()
|
||||||
|
.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)))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn show_pretty_request(tid: &str) -> fetch::Result<ThreadSet> {
|
||||||
|
Request::new(api::show_pretty(tid))
|
||||||
|
.method(Method::Get)
|
||||||
|
.fetch()
|
||||||
|
.await?
|
||||||
|
.check_status()?
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
// ------ ------
|
// ------ ------
|
||||||
// View
|
// View
|
||||||
// ------ ------
|
// ------ ------
|
||||||
|
|
||||||
|
// <subject>
|
||||||
|
// <tags>
|
||||||
|
//
|
||||||
|
// <from1> <date>
|
||||||
|
// <to1>
|
||||||
|
// <content1>
|
||||||
|
// <zippy>
|
||||||
|
// <children1>
|
||||||
|
// </zippy>
|
||||||
|
//
|
||||||
|
// <from2> <date>
|
||||||
|
// <to2>
|
||||||
|
// <body2>
|
||||||
|
fn view_message(thread: &ThreadNode) -> Node<Msg> {
|
||||||
|
let message = thread.0.as_ref().expect("ThreadNode missing Message");
|
||||||
|
let children = &thread.1;
|
||||||
|
div![
|
||||||
|
C!["message"],
|
||||||
|
/* TODO(wathiede): collect all the tags and show them here. */
|
||||||
|
/* TODO(wathiede): collect all the attachments from all the subparts */
|
||||||
|
div![C!["header"], "From: ", &message.headers.from],
|
||||||
|
div![C!["header"], "Date: ", &message.headers.date],
|
||||||
|
div![C!["header"], "To: ", &message.headers.to],
|
||||||
|
hr![],
|
||||||
|
div![
|
||||||
|
C!["body"],
|
||||||
|
match &message.body {
|
||||||
|
Some(body) => view_body(body.as_slice()),
|
||||||
|
None => div!["<no body>"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
children.iter().map(view_message)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_body(body: &[Part]) -> Node<Msg> {
|
||||||
|
div![body.iter().map(view_part)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_text_plain(content: &Option<Content>) -> Node<Msg> {
|
||||||
|
match &content {
|
||||||
|
Some(Content::String(content)) => p![C!["view-part-text-plain"], content],
|
||||||
|
_ => div![
|
||||||
|
C!["error"],
|
||||||
|
format!("Unhandled content enum for text/plain"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_part(part: &Part) -> Node<Msg> {
|
||||||
|
match part.content_type.as_str() {
|
||||||
|
"text/plain" => view_text_plain(&part.content),
|
||||||
|
"text/html" => {
|
||||||
|
if let Some(Content::String(html)) = &part.content {
|
||||||
|
let inliner = css_inline::CSSInliner::options()
|
||||||
|
.load_remote_stylesheets(false)
|
||||||
|
.remove_style_tags(true)
|
||||||
|
.build();
|
||||||
|
let inlined = inliner.inline(html).expect("failed to inline CSS");
|
||||||
|
|
||||||
|
return div![C!["view-part-text-html"], div!["TEST"], raw![&inlined]];
|
||||||
|
} else {
|
||||||
|
div![
|
||||||
|
C!["error"],
|
||||||
|
format!("Unhandled content enum for multipart/mixed"),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://en.wikipedia.org/wiki/MIME#alternative
|
||||||
|
// RFC1341 states: In general, user agents that compose multipart/alternative entities
|
||||||
|
// should place the body parts in increasing order of preference, that is, with the
|
||||||
|
// preferred format last.
|
||||||
|
"multipart/alternative" => {
|
||||||
|
if let Some(Content::Multipart(parts)) = &part.content {
|
||||||
|
for part in parts.iter().rev() {
|
||||||
|
if part.content_type == "text/html" {
|
||||||
|
if let Some(Content::String(html)) = &part.content {
|
||||||
|
let inliner = css_inline::CSSInliner::options()
|
||||||
|
.load_remote_stylesheets(false)
|
||||||
|
.remove_style_tags(true)
|
||||||
|
.build();
|
||||||
|
let inlined = inliner.inline(html).expect("failed to inline CSS");
|
||||||
|
return div![Node::from_html(None, &inlined)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if part.content_type == "text/plain" {
|
||||||
|
return view_text_plain(&part.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div!["No known multipart/alternative parts"]
|
||||||
|
} else {
|
||||||
|
div![
|
||||||
|
C!["error"],
|
||||||
|
format!("multipart/alternative with non-multipart content"),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"multipart/mixed" => match &part.content {
|
||||||
|
Some(Content::Multipart(parts)) => div![parts.iter().map(view_part)],
|
||||||
|
_ => div![
|
||||||
|
C!["error"],
|
||||||
|
format!("Unhandled content enum for multipart/mixed"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
_ => div![
|
||||||
|
C!["error"],
|
||||||
|
format!("Unhandled content type: {}", part.content_type)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first_subject(thread: &ThreadNode) -> Option<String> {
|
||||||
|
if let Some(msg) = &thread.0 {
|
||||||
|
return Some(msg.headers.subject.clone());
|
||||||
|
} else {
|
||||||
|
for tn in &thread.1 {
|
||||||
|
if let Some(s) = first_subject(&tn) {
|
||||||
|
return Some(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn set_title(title: &str) {
|
fn set_title(title: &str) {
|
||||||
seed::document().set_title(&format!("lb: {}", title));
|
seed::document().set_title(&format!("lb: {}", title));
|
||||||
}
|
}
|
||||||
@ -326,7 +490,7 @@ fn view_mobile_search_results(query: &str, search_results: &shared::SearchResult
|
|||||||
],
|
],
|
||||||
td![C!["subject"], tags_chiclet(&r.tags), " ", &r.subject],
|
td![C!["subject"], tags_chiclet(&r.tags), " ", &r.subject],
|
||||||
td![C!["date"], &r.date_relative],
|
td![C!["date"], &r.date_relative],
|
||||||
ev(Ev::Click, move |_| Msg::ShowRequest(tid)),
|
ev(Ev::Click, move |_| Msg::ShowPrettyRequest(tid)),
|
||||||
]
|
]
|
||||||
*/
|
*/
|
||||||
let tid = r.thread.clone();
|
let tid = r.thread.clone();
|
||||||
@ -334,7 +498,7 @@ fn view_mobile_search_results(query: &str, search_results: &shared::SearchResult
|
|||||||
div![
|
div![
|
||||||
C!["subject"],
|
C!["subject"],
|
||||||
&r.subject,
|
&r.subject,
|
||||||
ev(Ev::Click, move |_| Msg::ShowRequest(tid)),
|
ev(Ev::Click, move |_| Msg::ShowPrettyRequest(tid)),
|
||||||
],
|
],
|
||||||
div![
|
div![
|
||||||
span![C!["from"], pretty_authors(&r.authors)],
|
span![C!["from"], pretty_authors(&r.authors)],
|
||||||
@ -434,6 +598,55 @@ fn view_search_pager(start: usize, count: usize, total: usize) -> Node<Msg> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
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),
|
||||||
|
div![
|
||||||
|
C!["debug"],
|
||||||
|
"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, refresh_request: &RefreshingState) -> Node<Msg> {
|
fn view_header(query: &str, refresh_request: &RefreshingState) -> Node<Msg> {
|
||||||
let is_loading = refresh_request == &RefreshingState::Loading;
|
let is_loading = refresh_request == &RefreshingState::Loading;
|
||||||
let is_error = if let RefreshingState::Error(err) = refresh_request {
|
let is_error = if let RefreshingState::Error(err) = refresh_request {
|
||||||
@ -509,13 +722,6 @@ fn view_footer(render_time_ms: u128) -> Node<Msg> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_thread(messages: &[shared::Message]) -> Node<Msg> {
|
|
||||||
div![
|
|
||||||
"MESSAGES GO HERE",
|
|
||||||
ol![messages.iter().map(|msg| li![format!("{:?}", msg)])]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_desktop(model: &Model) -> Node<Msg> {
|
fn view_desktop(model: &Model) -> Node<Msg> {
|
||||||
let content = match &model.context {
|
let content = match &model.context {
|
||||||
Context::None => div![h1!["Loading"]],
|
Context::None => div![h1!["Loading"]],
|
||||||
|
|||||||
193
web/src/nm.rs
193
web/src/nm.rs
@ -1,193 +0,0 @@
|
|||||||
use notmuch::{Content, Part, Thread, ThreadNode, ThreadSet};
|
|
||||||
use seed::{prelude::*, *};
|
|
||||||
use serde::de::Deserialize;
|
|
||||||
|
|
||||||
use crate::{api, set_title, Msg};
|
|
||||||
|
|
||||||
pub async fn show_request(tid: &str) -> fetch::Result<ThreadSet> {
|
|
||||||
let b = Request::new(api::show(tid))
|
|
||||||
.method(Method::Get)
|
|
||||||
.fetch()
|
|
||||||
.await?
|
|
||||||
.check_status()?
|
|
||||||
.bytes()
|
|
||||||
.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)))?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub 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());
|
|
||||||
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),
|
|
||||||
div![
|
|
||||||
C!["debug"],
|
|
||||||
"Add zippy for debug dump",
|
|
||||||
view_debug_thread_set(thread_set)
|
|
||||||
] /* pre![format!("Thread: {:#?}", thread_set).replace(" ", " ")] */
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// <subject>
|
|
||||||
// <tags>
|
|
||||||
//
|
|
||||||
// <from1> <date>
|
|
||||||
// <to1>
|
|
||||||
// <content1>
|
|
||||||
// <zippy>
|
|
||||||
// <children1>
|
|
||||||
// </zippy>
|
|
||||||
//
|
|
||||||
// <from2> <date>
|
|
||||||
// <to2>
|
|
||||||
// <body2>
|
|
||||||
fn view_message(thread: &ThreadNode) -> Node<Msg> {
|
|
||||||
let message = thread.0.as_ref().expect("ThreadNode missing Message");
|
|
||||||
let children = &thread.1;
|
|
||||||
div![
|
|
||||||
C!["message"],
|
|
||||||
/* TODO(wathiede): collect all the tags and show them here. */
|
|
||||||
/* TODO(wathiede): collect all the attachments from all the subparts */
|
|
||||||
div![C!["header"], "From: ", &message.headers.from],
|
|
||||||
div![C!["header"], "Date: ", &message.headers.date],
|
|
||||||
div![C!["header"], "To: ", &message.headers.to],
|
|
||||||
hr![],
|
|
||||||
div![
|
|
||||||
C!["body"],
|
|
||||||
match &message.body {
|
|
||||||
Some(body) => view_body(body.as_slice()),
|
|
||||||
None => div!["<no body>"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
children.iter().map(view_message)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_body(body: &[Part]) -> Node<Msg> {
|
|
||||||
div![body.iter().map(view_part)]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_text_plain(content: &Option<Content>) -> Node<Msg> {
|
|
||||||
match &content {
|
|
||||||
Some(Content::String(content)) => p![C!["view-part-text-plain"], content],
|
|
||||||
_ => div![
|
|
||||||
C!["error"],
|
|
||||||
format!("Unhandled content enum for text/plain"),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_part(part: &Part) -> Node<Msg> {
|
|
||||||
match part.content_type.as_str() {
|
|
||||||
"text/plain" => view_text_plain(&part.content),
|
|
||||||
"text/html" => {
|
|
||||||
if let Some(Content::String(html)) = &part.content {
|
|
||||||
let inliner = css_inline::CSSInliner::options()
|
|
||||||
.load_remote_stylesheets(false)
|
|
||||||
.remove_style_tags(true)
|
|
||||||
.build();
|
|
||||||
let inlined = inliner.inline(html).expect("failed to inline CSS");
|
|
||||||
|
|
||||||
return div![C!["view-part-text-html"], div!["TEST"], raw![&inlined]];
|
|
||||||
} else {
|
|
||||||
div![
|
|
||||||
C!["error"],
|
|
||||||
format!("Unhandled content enum for multipart/mixed"),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://en.wikipedia.org/wiki/MIME#alternative
|
|
||||||
// RFC1341 states: In general, user agents that compose multipart/alternative entities
|
|
||||||
// should place the body parts in increasing order of preference, that is, with the
|
|
||||||
// preferred format last.
|
|
||||||
"multipart/alternative" => {
|
|
||||||
if let Some(Content::Multipart(parts)) = &part.content {
|
|
||||||
for part in parts.iter().rev() {
|
|
||||||
if part.content_type == "text/html" {
|
|
||||||
if let Some(Content::String(html)) = &part.content {
|
|
||||||
let inliner = css_inline::CSSInliner::options()
|
|
||||||
.load_remote_stylesheets(false)
|
|
||||||
.remove_style_tags(true)
|
|
||||||
.build();
|
|
||||||
let inlined = inliner.inline(html).expect("failed to inline CSS");
|
|
||||||
return div![Node::from_html(None, &inlined)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if part.content_type == "text/plain" {
|
|
||||||
return view_text_plain(&part.content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div!["No known multipart/alternative parts"]
|
|
||||||
} else {
|
|
||||||
div![
|
|
||||||
C!["error"],
|
|
||||||
format!("multipart/alternative with non-multipart content"),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"multipart/mixed" => match &part.content {
|
|
||||||
Some(Content::Multipart(parts)) => div![parts.iter().map(view_part)],
|
|
||||||
_ => div![
|
|
||||||
C!["error"],
|
|
||||||
format!("Unhandled content enum for multipart/mixed"),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
_ => div![
|
|
||||||
C!["error"],
|
|
||||||
format!("Unhandled content type: {}", part.content_type)
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn first_subject(thread: &ThreadNode) -> Option<String> {
|
|
||||||
if let Some(msg) = &thread.0 {
|
|
||||||
return Some(msg.headers.subject.clone());
|
|
||||||
} else {
|
|
||||||
for tn in &thread.1 {
|
|
||||||
if let Some(s) = first_subject(&tn) {
|
|
||||||
return Some(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
])
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user