web: add basic graphql view thread, no body support.
This commit is contained in:
parent
447a4a3387
commit
0ae72b63d0
@ -7,8 +7,8 @@ use async_graphql::{
|
|||||||
connection::{self, Connection, Edge},
|
connection::{self, Connection, Edge},
|
||||||
Context, EmptyMutation, EmptySubscription, Error, FieldResult, Object, Schema, SimpleObject,
|
Context, EmptyMutation, EmptySubscription, Error, FieldResult, Object, Schema, SimpleObject,
|
||||||
};
|
};
|
||||||
use log::info;
|
use log::{info, warn};
|
||||||
use mailparse::{addrparse, parse_mail, MailHeaderMap, ParsedMail};
|
use mailparse::{parse_mail, MailHeaderMap, ParsedMail};
|
||||||
use memmap::MmapOptions;
|
use memmap::MmapOptions;
|
||||||
use notmuch::Notmuch;
|
use notmuch::Notmuch;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
@ -39,6 +39,7 @@ pub struct ThreadSummary {
|
|||||||
|
|
||||||
#[derive(Debug, SimpleObject)]
|
#[derive(Debug, SimpleObject)]
|
||||||
pub struct Thread {
|
pub struct Thread {
|
||||||
|
subject: String,
|
||||||
messages: Vec<Message>,
|
messages: Vec<Message>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,13 +175,18 @@ impl QueryRoot {
|
|||||||
let file = File::open(&path)?;
|
let file = File::open(&path)?;
|
||||||
let mmap = unsafe { MmapOptions::new().map(&file)? };
|
let mmap = unsafe { MmapOptions::new().map(&file)? };
|
||||||
let m = parse_mail(&mmap)?;
|
let m = parse_mail(&mmap)?;
|
||||||
let from = if let Some(from) = m.headers.get_first_value("from") {
|
let from = email_addresses(&path, &m, "from")?;
|
||||||
addrparse(&from)?.extract_single_info().map(|si| Email {
|
let from = match from.len() {
|
||||||
name: si.display_name,
|
0 => None,
|
||||||
addr: Some(si.addr),
|
1 => from.into_iter().next(),
|
||||||
})
|
_ => {
|
||||||
} else {
|
warn!(
|
||||||
None
|
"Got {} from addresses in message, truncating: {:?}",
|
||||||
|
from.len(),
|
||||||
|
from
|
||||||
|
);
|
||||||
|
from.into_iter().next()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let to = email_addresses(&path, &m, "to")?;
|
let to = email_addresses(&path, &m, "to")?;
|
||||||
let cc = email_addresses(&path, &m, "cc")?;
|
let cc = email_addresses(&path, &m, "cc")?;
|
||||||
@ -198,7 +204,15 @@ impl QueryRoot {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
messages.reverse();
|
messages.reverse();
|
||||||
Ok(Thread { messages })
|
// Find the first subject that's set. After reversing the vec, this should be the oldest
|
||||||
|
// message.
|
||||||
|
let subject: String = messages
|
||||||
|
.iter()
|
||||||
|
.skip_while(|m| m.subject.is_none())
|
||||||
|
.next()
|
||||||
|
.and_then(|m| m.subject.clone())
|
||||||
|
.unwrap_or("(NO SUBJECT)".to_string());
|
||||||
|
Ok(Thread { subject, messages })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,8 +242,8 @@ fn email_addresses(path: &str, m: &ParsedMail, header_name: &str) -> Result<Vec<
|
|||||||
if v.matches('@').count() == 1 {
|
if v.matches('@').count() == 1 {
|
||||||
if v.matches('<').count() == 1 && v.ends_with('>') {
|
if v.matches('<').count() == 1 && v.ends_with('>') {
|
||||||
let idx = v.find('<').unwrap();
|
let idx = v.find('<').unwrap();
|
||||||
let addr = &v[idx + 1..v.len() - 1];
|
let addr = &v[idx + 1..v.len() - 1].trim();
|
||||||
let name = &v[..idx];
|
let name = &v[..idx].trim();
|
||||||
addrs.push(Email {
|
addrs.push(Email {
|
||||||
name: Some(name.to_string()),
|
name: Some(name.to_string()),
|
||||||
addr: Some(addr.to_string()),
|
addr: Some(addr.to_string()),
|
||||||
|
|||||||
@ -69,6 +69,41 @@
|
|||||||
"name": "Boolean",
|
"name": "Boolean",
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": null,
|
||||||
|
"enumValues": null,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"deprecationReason": null,
|
||||||
|
"description": null,
|
||||||
|
"isDeprecated": false,
|
||||||
|
"name": "name",
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"deprecationReason": null,
|
||||||
|
"description": null,
|
||||||
|
"isDeprecated": false,
|
||||||
|
"name": "addr",
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inputFields": null,
|
||||||
|
"interfaces": [],
|
||||||
|
"kind": "OBJECT",
|
||||||
|
"name": "Email",
|
||||||
|
"possibleTypes": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).",
|
"description": "The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).",
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
@ -99,6 +134,101 @@
|
|||||||
"name": "Int",
|
"name": "Int",
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": null,
|
||||||
|
"enumValues": null,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"deprecationReason": null,
|
||||||
|
"description": null,
|
||||||
|
"isDeprecated": false,
|
||||||
|
"name": "from",
|
||||||
|
"type": {
|
||||||
|
"kind": "OBJECT",
|
||||||
|
"name": "Email",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"deprecationReason": null,
|
||||||
|
"description": null,
|
||||||
|
"isDeprecated": false,
|
||||||
|
"name": "to",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "LIST",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "OBJECT",
|
||||||
|
"name": "Email",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"deprecationReason": null,
|
||||||
|
"description": null,
|
||||||
|
"isDeprecated": false,
|
||||||
|
"name": "cc",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "LIST",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "OBJECT",
|
||||||
|
"name": "Email",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"deprecationReason": null,
|
||||||
|
"description": null,
|
||||||
|
"isDeprecated": false,
|
||||||
|
"name": "subject",
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"deprecationReason": null,
|
||||||
|
"description": null,
|
||||||
|
"isDeprecated": false,
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "Int",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inputFields": null,
|
||||||
|
"interfaces": [],
|
||||||
|
"kind": "OBJECT",
|
||||||
|
"name": "Message",
|
||||||
|
"possibleTypes": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Information about pagination in a connection",
|
"description": "Information about pagination in a connection",
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
@ -295,6 +425,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"description": null,
|
||||||
|
"name": "threadId",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"deprecationReason": null,
|
||||||
|
"description": null,
|
||||||
|
"isDeprecated": false,
|
||||||
|
"name": "thread",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "OBJECT",
|
||||||
|
"name": "Thread",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"inputFields": null,
|
"inputFields": null,
|
||||||
@ -388,6 +549,57 @@
|
|||||||
"name": "Tag",
|
"name": "Tag",
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": null,
|
||||||
|
"enumValues": null,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"deprecationReason": null,
|
||||||
|
"description": null,
|
||||||
|
"isDeprecated": false,
|
||||||
|
"name": "subject",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"deprecationReason": null,
|
||||||
|
"description": null,
|
||||||
|
"isDeprecated": false,
|
||||||
|
"name": "messages",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "LIST",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "OBJECT",
|
||||||
|
"name": "Message",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inputFields": null,
|
||||||
|
"interfaces": [],
|
||||||
|
"kind": "OBJECT",
|
||||||
|
"name": "Thread",
|
||||||
|
"possibleTypes": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": null,
|
"description": null,
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
|
|||||||
21
web/graphql/show_thread.graphql
Normal file
21
web/graphql/show_thread.graphql
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
query ShowThreadQuery($threadId: String!) {
|
||||||
|
thread(threadId: $threadId) {
|
||||||
|
subject
|
||||||
|
messages {
|
||||||
|
subject
|
||||||
|
from {
|
||||||
|
name
|
||||||
|
addr
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
name
|
||||||
|
addr
|
||||||
|
}
|
||||||
|
cc {
|
||||||
|
name
|
||||||
|
addr
|
||||||
|
}
|
||||||
|
timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ use seed::{
|
|||||||
fetch,
|
fetch,
|
||||||
fetch::{Header, Method, Request},
|
fetch::{Header, Method, Request},
|
||||||
};
|
};
|
||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
|
||||||
// The paths are relative to the directory where your `Cargo.toml` is located.
|
// The paths are relative to the directory where your `Cargo.toml` is located.
|
||||||
// Both json and the GraphQL schema language are supported as sources for the schema
|
// Both json and the GraphQL schema language are supported as sources for the schema
|
||||||
@ -15,6 +15,14 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
|||||||
)]
|
)]
|
||||||
pub struct FrontPageQuery;
|
pub struct FrontPageQuery;
|
||||||
|
|
||||||
|
#[derive(GraphQLQuery)]
|
||||||
|
#[graphql(
|
||||||
|
schema_path = "graphql/schema.json",
|
||||||
|
query_path = "graphql/show_thread.graphql",
|
||||||
|
response_derives = "Debug"
|
||||||
|
)]
|
||||||
|
pub struct ShowThreadQuery;
|
||||||
|
|
||||||
pub async fn send_graphql<Body, Resp>(body: Body) -> fetch::Result<graphql_client::Response<Resp>>
|
pub async fn send_graphql<Body, Resp>(body: Body) -> fetch::Result<graphql_client::Response<Resp>>
|
||||||
where
|
where
|
||||||
Body: Serialize,
|
Body: Serialize,
|
||||||
|
|||||||
157
web/src/lib.rs
157
web/src/lib.rs
@ -17,7 +17,7 @@ use serde::de::Deserialize;
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use wasm_timer::Instant;
|
use wasm_timer::Instant;
|
||||||
|
|
||||||
use crate::graphql::{front_page_query::*, send_graphql};
|
use crate::graphql::{front_page_query::*, send_graphql, show_thread_query::*};
|
||||||
|
|
||||||
mod graphql;
|
mod graphql;
|
||||||
|
|
||||||
@ -67,7 +67,15 @@ 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::ShowPrettyRequest(tid.to_string()),
|
["t", tid] => {
|
||||||
|
if USE_GRAPHQL {
|
||||||
|
Msg::ShowThreadRequest {
|
||||||
|
thread_id: tid.to_string(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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());
|
||||||
if USE_GRAPHQL {
|
if USE_GRAPHQL {
|
||||||
@ -156,6 +164,7 @@ enum Context {
|
|||||||
pager: FrontPageQuerySearchPageInfo,
|
pager: FrontPageQuerySearchPageInfo,
|
||||||
},
|
},
|
||||||
Thread(ThreadSet),
|
Thread(ThreadSet),
|
||||||
|
ThreadResult(ShowThreadQueryThread),
|
||||||
}
|
}
|
||||||
|
|
||||||
// `Model` describes our app state.
|
// `Model` describes our app state.
|
||||||
@ -210,6 +219,12 @@ enum Msg {
|
|||||||
FrontPageResult(
|
FrontPageResult(
|
||||||
fetch::Result<graphql_client::Response<graphql::front_page_query::ResponseData>>,
|
fetch::Result<graphql_client::Response<graphql::front_page_query::ResponseData>>,
|
||||||
),
|
),
|
||||||
|
ShowThreadRequest {
|
||||||
|
thread_id: String,
|
||||||
|
},
|
||||||
|
ShowThreadResult(
|
||||||
|
fetch::Result<graphql_client::Response<graphql::show_thread_query::ResponseData>>,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
// `update` describes how to handle each `Msg`.
|
// `update` describes how to handle each `Msg`.
|
||||||
@ -294,8 +309,9 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Context::Thread(_) => (), // do nothing (yet?)
|
Context::Thread(_) => (), // do nothing (yet?)
|
||||||
Context::None => (), // do nothing (yet?)
|
Context::ThreadResult(_) => (), // do nothing (yet?)
|
||||||
|
Context::None => (), // do nothing (yet?)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Msg::PreviousPage => {
|
Msg::PreviousPage => {
|
||||||
@ -317,8 +333,9 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Context::Thread(_) => (), // do nothing (yet?)
|
Context::Thread(_) => (), // do nothing (yet?)
|
||||||
Context::None => (), // do nothing (yet?)
|
Context::ThreadResult(_) => (), // do nothing (yet?)
|
||||||
|
Context::None => (), // do nothing (yet?)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,6 +389,25 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
pager: data.search.page_info,
|
pager: data.search.page_info,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Msg::ShowThreadRequest { thread_id } => {
|
||||||
|
orders.skip().perform_cmd(async move {
|
||||||
|
Msg::ShowThreadResult(
|
||||||
|
send_graphql(graphql::ShowThreadQuery::build_query(
|
||||||
|
graphql::show_thread_query::Variables { thread_id },
|
||||||
|
))
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Msg::ShowThreadResult(Ok(graphql_client::Response {
|
||||||
|
data: Some(data), ..
|
||||||
|
})) => {
|
||||||
|
model.context = Context::ThreadResult(data.thread);
|
||||||
|
}
|
||||||
|
Msg::ShowThreadResult(bad) => {
|
||||||
|
error!("show_thread_query error: {bad:?}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -923,7 +959,108 @@ fn view_search_pager_legacy(start: usize, count: usize, total: usize) -> Node<Ms
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_thread(thread_set: &ThreadSet) -> Node<Msg> {
|
trait Email {
|
||||||
|
fn name(&self) -> &Option<String>;
|
||||||
|
fn addr(&self) -> &Option<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Email> Email for &'_ T {
|
||||||
|
fn name(&self) -> &Option<String> {
|
||||||
|
return (*self).name();
|
||||||
|
}
|
||||||
|
fn addr(&self) -> &Option<String> {
|
||||||
|
return (*self).addr();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Email for ShowThreadQueryThreadMessagesCc {
|
||||||
|
fn name(&self) -> &Option<String> {
|
||||||
|
return &self.name;
|
||||||
|
}
|
||||||
|
fn addr(&self) -> &Option<String> {
|
||||||
|
return &self.addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Email for ShowThreadQueryThreadMessagesFrom {
|
||||||
|
fn name(&self) -> &Option<String> {
|
||||||
|
return &self.name;
|
||||||
|
}
|
||||||
|
fn addr(&self) -> &Option<String> {
|
||||||
|
return &self.addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Email for ShowThreadQueryThreadMessagesTo {
|
||||||
|
fn name(&self) -> &Option<String> {
|
||||||
|
return &self.name;
|
||||||
|
}
|
||||||
|
fn addr(&self) -> &Option<String> {
|
||||||
|
return &self.addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_addresses<E: Email>(addrs: &[E]) -> Vec<Node<Msg>> {
|
||||||
|
addrs
|
||||||
|
.into_iter()
|
||||||
|
.map(|address| {
|
||||||
|
span![
|
||||||
|
C!["tag", "is-black"],
|
||||||
|
address.addr().as_ref().map(|a| attrs! {At::Title=>a}),
|
||||||
|
address
|
||||||
|
.name()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(address.addr().as_ref().unwrap_or(&"(UNKNOWN)".to_string()))
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_thread(thread: &ShowThreadQueryThread) -> Node<Msg> {
|
||||||
|
// TODO(wathiede): show per-message subject if it changes significantly from top-level subject
|
||||||
|
set_title(&thread.subject);
|
||||||
|
let messages = thread.messages.iter().map(|msg| {
|
||||||
|
div![
|
||||||
|
C!["message"],
|
||||||
|
/* TODO(wathiede): collect all the tags and show them here. */
|
||||||
|
/* TODO(wathiede): collect all the attachments from all the subparts */
|
||||||
|
msg.from
|
||||||
|
.as_ref()
|
||||||
|
.map(|from| div![C!["header"], "From: ", view_addresses(&[from])]),
|
||||||
|
msg.timestamp
|
||||||
|
.map(|ts| div![C!["header"], "Date: ", human_age(ts)]),
|
||||||
|
IF!(!msg.to.is_empty() => div![C!["header"], "To: ", view_addresses(&msg.to)]),
|
||||||
|
IF!(!msg.cc.is_empty() => div![C!["header"], "CC: ", view_addresses(&msg.cc)]),
|
||||||
|
/*
|
||||||
|
div![
|
||||||
|
C!["body"],
|
||||||
|
match &message.body {
|
||||||
|
Some(body) => view_body(body.as_slice()),
|
||||||
|
None => div!["<no body>"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
*/
|
||||||
|
]
|
||||||
|
});
|
||||||
|
div![
|
||||||
|
C!["container"],
|
||||||
|
h1![C!["title"], &thread.subject],
|
||||||
|
messages,
|
||||||
|
/* TODO(wathiede): plumb in orignal id
|
||||||
|
a![
|
||||||
|
attrs! {At::Href=>api::original(&thread_node.0.as_ref().expect("message missing").id)},
|
||||||
|
"Original"
|
||||||
|
],
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
div![
|
||||||
|
C!["debug"],
|
||||||
|
"Add zippy for debug dump",
|
||||||
|
view_debug_thread_set(thread_set)
|
||||||
|
] /* pre![format!("Thread: {:#?}", thread_set).replace(" ", " ")] */
|
||||||
|
*/
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_thread_legacy(thread_set: &ThreadSet) -> Node<Msg> {
|
||||||
assert_eq!(thread_set.0.len(), 1);
|
assert_eq!(thread_set.0.len(), 1);
|
||||||
let thread = &thread_set.0[0];
|
let thread = &thread_set.0[0];
|
||||||
assert_eq!(thread.0.len(), 1);
|
assert_eq!(thread.0.len(), 1);
|
||||||
@ -1058,7 +1195,8 @@ fn view_desktop(model: &Model) -> Node<Msg> {
|
|||||||
// Do two queries, one without `unread` so it loads fast, then a second with unread.
|
// Do two queries, one without `unread` so it loads fast, then a second with unread.
|
||||||
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_legacy(thread_set),
|
||||||
|
Context::ThreadResult(thread) => view_thread(thread),
|
||||||
Context::Search(search_results) => view_search_results_legacy(&model.query, search_results),
|
Context::Search(search_results) => view_search_results_legacy(&model.query, search_results),
|
||||||
Context::SearchResult {
|
Context::SearchResult {
|
||||||
query,
|
query,
|
||||||
@ -1097,7 +1235,8 @@ fn view_desktop(model: &Model) -> Node<Msg> {
|
|||||||
fn view_mobile(model: &Model) -> Node<Msg> {
|
fn view_mobile(model: &Model) -> Node<Msg> {
|
||||||
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_legacy(thread_set),
|
||||||
|
Context::ThreadResult(thread) => view_thread(thread),
|
||||||
Context::Search(search_results) => {
|
Context::Search(search_results) => {
|
||||||
view_mobile_search_results_legacy(&model.query, search_results)
|
view_mobile_search_results_legacy(&model.query, search_results)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user