Refactor thread responses into an enum.

Lays ground work for different types of views, i.e. email, news, docs, etc.
This commit is contained in:
Bill Thiede 2024-08-26 21:48:53 -07:00
parent 446fcfe37f
commit 760cec01a8
10 changed files with 190 additions and 161 deletions

View File

@ -32,8 +32,13 @@ pub struct ThreadSummary {
pub tags: Vec<String>, pub tags: Vec<String>,
} }
#[derive(Debug, Union)]
pub enum Thread {
Email(EmailThread),
}
#[derive(Debug, SimpleObject)] #[derive(Debug, SimpleObject)]
pub struct Thread { pub struct EmailThread {
pub thread_id: String, pub thread_id: String,
pub subject: String, pub subject: String,
pub messages: Vec<Message>, pub messages: Vec<Message>,
@ -369,11 +374,13 @@ impl QueryRoot {
.field("contentTree") .field("contentTree")
.exists(); .exists();
// TODO: look at thread_id and conditionally load newsreader // TODO: look at thread_id and conditionally load newsreader
if newsreader::is_newsreader_thread(&thread_id) { Ok(Thread::Email(
Ok(newsreader::thread(pool, thread_id).await?) if newsreader::is_newsreader_thread(&thread_id) {
} else { newsreader::thread(pool, thread_id).await?
Ok(nm::thread(nm, thread_id, debug_content_tree).await?) } else {
} nm::thread(nm, thread_id, debug_content_tree).await?
},
))
} }
} }

View File

@ -14,7 +14,7 @@ const THREAD_PREFIX: &'static str = "news:";
use crate::{ use crate::{
compute_offset_limit, compute_offset_limit,
error::ServerError, error::ServerError,
graphql::{Body, Email, Html, Message, Tag, Thread, ThreadSummary}, graphql::{Body, Email, EmailThread, Html, Message, Tag, ThreadSummary},
AddOutlink, EscapeHtml, InlineStyle, SanitizeHtml, SlurpContents, StripHtml, Transformer, AddOutlink, EscapeHtml, InlineStyle, SanitizeHtml, SlurpContents, StripHtml, Transformer,
}; };
@ -143,7 +143,7 @@ pub async fn tags(pool: &PgPool, _needs_unread: bool) -> Result<Vec<Tag>, Server
Ok(tags) Ok(tags)
} }
pub async fn thread(pool: &PgPool, thread_id: String) -> Result<Thread, ServerError> { pub async fn thread(pool: &PgPool, thread_id: String) -> Result<EmailThread, ServerError> {
let id = thread_id let id = thread_id
.strip_prefix(THREAD_PREFIX) .strip_prefix(THREAD_PREFIX)
.expect("news thread doesn't start with '{THREAD_PREFIX}'") .expect("news thread doesn't start with '{THREAD_PREFIX}'")
@ -198,7 +198,6 @@ pub async fn thread(pool: &PgPool, thread_id: String) -> Result<Thread, ServerEr
// * Grafana does <div class="image-wrapp"><img class="lazyload>"<img src="/media/...>"</img></div> // * Grafana does <div class="image-wrapp"><img class="lazyload>"<img src="/media/...>"</img></div>
// * Some sites appear to be HTML encoded, unencode them, i.e. imperialviolent // * Some sites appear to be HTML encoded, unencode them, i.e. imperialviolent
let body_tranformers: Vec<Box<dyn Transformer>> = vec![ let body_tranformers: Vec<Box<dyn Transformer>> = vec![
// TODO: add a map of urls and selectors
Box::new(SlurpContents { Box::new(SlurpContents {
site_selectors: hashmap![ site_selectors: hashmap![
"hackaday.com".to_string() => vec![ "hackaday.com".to_string() => vec![
@ -237,7 +236,7 @@ pub async fn thread(pool: &PgPool, thread_id: String) -> Result<Thread, ServerEr
name: r.name, name: r.name,
addr: addr.map(|a| a.to_string()), addr: addr.map(|a| a.to_string()),
}); });
Ok(Thread { Ok(EmailThread {
thread_id, thread_id,
subject: title.clone(), subject: title.clone(),
messages: vec![Message { messages: vec![Message {

View File

@ -14,8 +14,8 @@ use crate::{
compute_offset_limit, compute_offset_limit,
error::ServerError, error::ServerError,
graphql::{ graphql::{
Attachment, Body, DispositionType, Email, Header, Html, Message, PlainText, Tag, Thread, Attachment, Body, DispositionType, Email, EmailThread, Header, Html, Message, PlainText,
ThreadSummary, UnhandledContentType, Tag, ThreadSummary, UnhandledContentType,
}, },
linkify_html, sanitize_html, linkify_html, sanitize_html,
}; };
@ -125,7 +125,7 @@ pub async fn thread(
nm: &Notmuch, nm: &Notmuch,
thread_id: String, thread_id: String,
debug_content_tree: bool, debug_content_tree: bool,
) -> Result<Thread, ServerError> { ) -> Result<EmailThread, ServerError> {
// TODO(wathiede): normalize all email addresses through an address book with preferred // TODO(wathiede): normalize all email addresses through an address book with preferred
// display names (that default to the most commonly seen name). // display names (that default to the most commonly seen name).
let mut messages = Vec::new(); let mut messages = Vec::new();
@ -246,7 +246,7 @@ pub async fn thread(
.next() .next()
.and_then(|m| m.subject.clone()) .and_then(|m| m.subject.clone())
.unwrap_or("(NO SUBJECT)".to_string()); .unwrap_or("(NO SUBJECT)".to_string());
Ok(Thread { Ok(EmailThread {
thread_id, thread_id,
subject, subject,
messages, messages,

View File

@ -290,6 +290,73 @@
"name": "Email", "name": "Email",
"possibleTypes": null "possibleTypes": null
}, },
{
"description": null,
"enumValues": null,
"fields": [
{
"args": [],
"deprecationReason": null,
"description": null,
"isDeprecated": false,
"name": "threadId",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
{
"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": "EmailThread",
"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,
@ -1056,7 +1123,7 @@
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "OBJECT", "kind": "UNION",
"name": "Thread", "name": "Thread",
"ofType": null "ofType": null
} }
@ -1157,69 +1224,18 @@
{ {
"description": null, "description": null,
"enumValues": null, "enumValues": null,
"fields": [ "fields": null,
{
"args": [],
"deprecationReason": null,
"description": null,
"isDeprecated": false,
"name": "threadId",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
{
"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, "inputFields": null,
"interfaces": [], "interfaces": null,
"kind": "OBJECT", "kind": "UNION",
"name": "Thread", "name": "Thread",
"possibleTypes": null "possibleTypes": [
{
"kind": "OBJECT",
"name": "EmailThread",
"ofType": null
}
]
}, },
{ {
"description": null, "description": null,

View File

@ -1,47 +1,49 @@
query ShowThreadQuery($threadId: String!) { query ShowThreadQuery($threadId: String!) {
thread(threadId: $threadId) { thread(threadId: $threadId) {
threadId, __typename ... on EmailThread{
subject threadId,
messages {
id
subject subject
tags messages {
from {
name
addr
}
to {
name
addr
}
cc {
name
addr
}
timestamp
body {
__typename
... on UnhandledContentType {
contents
contentTree
}
... on PlainText {
contents
contentTree
}
... on Html {
contents
contentTree
}
}
path
attachments {
id id
idx subject
filename tags
contentType from {
contentId name
size addr
}
to {
name
addr
}
cc {
name
addr
}
timestamp
body {
__typename
... on UnhandledContentType {
contents
contentTree
}
... on PlainText {
contents
contentTree
}
... on Html {
contents
contentTree
}
}
path
attachments {
id
idx
filename
contentType
contentId
size
}
} }
} }
} }

View File

@ -341,25 +341,28 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}) })
.collect(), .collect(),
); );
let mut open_messages: HashSet<_> = data match &data.thread {
.thread graphql::show_thread_query::ShowThreadQueryThread::EmailThread(
.messages ShowThreadQueryThreadOnEmailThread {
.iter() thread_id,
.filter(|msg| msg.tags.iter().any(|t| t == "unread")) subject,
.map(|msg| msg.id.clone()) messages,
.collect(); },
if open_messages.is_empty() { ) => {
open_messages = data let mut open_messages: HashSet<_> = messages
.thread .iter()
.messages .filter(|msg| msg.tags.iter().any(|t| t == "unread"))
.iter() .map(|msg| msg.id.clone())
.map(|msg| msg.id.clone()) .collect();
.collect(); if open_messages.is_empty() {
open_messages = messages.iter().map(|msg| msg.id.clone()).collect();
}
model.context = Context::ThreadResult {
thread: data.thread,
open_messages,
};
}
} }
model.context = Context::ThreadResult {
thread: data.thread,
open_messages,
};
} }
Msg::ShowThreadResult(bad) => { Msg::ShowThreadResult(bad) => {
error!("show_thread_query error: {bad:#?}"); error!("show_thread_query error: {bad:#?}");

View File

@ -3,6 +3,7 @@ use seed_hooks::{state_access::CloneState, topo, use_state};
use crate::{ use crate::{
api::urls, api::urls,
graphql::show_thread_query::*,
state::{Context, Model, Msg}, state::{Context, Model, Msg},
view::{self, view_header, view_search_results, view_tags}, view::{self, view_header, view_search_results, view_tags},
}; };
@ -15,7 +16,7 @@ pub(super) fn view(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::ThreadResult { Context::ThreadResult {
thread, thread: ShowThreadQueryThread::EmailThread(thread),
open_messages, open_messages,
} => view::thread(thread, open_messages, show_icon_text), } => view::thread(thread, open_messages, show_icon_text),
Context::SearchResult { Context::SearchResult {

View File

@ -4,7 +4,7 @@ use seed::{prelude::*, *};
use crate::{ use crate::{
api::urls, api::urls,
graphql::front_page_query::*, graphql::{front_page_query::*, show_thread_query::*},
state::{Context, Model, Msg}, state::{Context, Model, Msg},
view::{ view::{
self, human_age, pretty_authors, search_toolbar, set_title, tags_chiclet, view_header, self, human_age, pretty_authors, search_toolbar, set_title, tags_chiclet, view_header,
@ -18,7 +18,7 @@ pub(super) fn view(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::ThreadResult { Context::ThreadResult {
thread, thread: ShowThreadQueryThread::EmailThread(thread),
open_messages, open_messages,
} => view::thread(thread, open_messages, show_icon_text), } => view::thread(thread, open_messages, show_icon_text),
Context::SearchResult { Context::SearchResult {

View File

@ -394,9 +394,9 @@ macro_rules! implement_email {
} }
implement_email!( implement_email!(
ShowThreadQueryThreadMessagesTo, ShowThreadQueryThreadOnEmailThreadMessagesTo,
ShowThreadQueryThreadMessagesCc, ShowThreadQueryThreadOnEmailThreadMessagesCc,
ShowThreadQueryThreadMessagesFrom ShowThreadQueryThreadOnEmailThreadMessagesFrom
); );
fn raw_text_message(contents: &str) -> Node<Msg> { fn raw_text_message(contents: &str) -> Node<Msg> {
@ -467,13 +467,13 @@ fn render_avatar(avatar: Option<String>, from: &str) -> Node<Msg> {
} }
} }
fn render_open_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> { fn render_open_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<Msg> {
let (from, from_detail) = match &msg.from { let (from, from_detail) = match &msg.from {
Some(ShowThreadQueryThreadMessagesFrom { Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
name: Some(name), name: Some(name),
addr, addr,
}) => (name.to_string(), addr.clone()), }) => (name.to_string(), addr.clone()),
Some(ShowThreadQueryThreadMessagesFrom { Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
addr: Some(addr), .. addr: Some(addr), ..
}) => (addr.to_string(), None), }) => (addr.to_string(), None),
_ => (String::from("UNKNOWN"), None), _ => (String::from("UNKNOWN"), None),
@ -516,15 +516,15 @@ fn render_open_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
if i>0 { ", " }else { "" }, if i>0 { ", " }else { "" },
{ {
let to = match to { let to = match to {
ShowThreadQueryThreadMessagesTo { ShowThreadQueryThreadOnEmailThreadMessagesTo {
name: Some(name), name: Some(name),
addr:Some(addr), addr:Some(addr),
} => format!("{name} <{addr}>"), } => format!("{name} <{addr}>"),
ShowThreadQueryThreadMessagesTo { ShowThreadQueryThreadOnEmailThreadMessagesTo {
name: Some(name), name: Some(name),
addr:None addr:None
} => format!("{name}"), } => format!("{name}"),
ShowThreadQueryThreadMessagesTo { ShowThreadQueryThreadOnEmailThreadMessagesTo {
addr: Some(addr), .. addr: Some(addr), ..
} => format!("{addr}"), } => format!("{addr}"),
_ => String::from("UNKNOWN"), _ => String::from("UNKNOWN"),
@ -553,15 +553,15 @@ fn render_open_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
if i>0 { ", " }else { "" }, if i>0 { ", " }else { "" },
{ {
let cc = match cc { let cc = match cc {
ShowThreadQueryThreadMessagesCc { ShowThreadQueryThreadOnEmailThreadMessagesCc {
name: Some(name), name: Some(name),
addr:Some(addr), addr:Some(addr),
} => format!("{name} <{addr}>"), } => format!("{name} <{addr}>"),
ShowThreadQueryThreadMessagesCc { ShowThreadQueryThreadOnEmailThreadMessagesCc {
name: Some(name), name: Some(name),
addr:None addr:None
} => format!("{name}"), } => format!("{name}"),
ShowThreadQueryThreadMessagesCc { ShowThreadQueryThreadOnEmailThreadMessagesCc {
addr: Some(addr), .. addr: Some(addr), ..
} => format!("<{addr}>"), } => format!("<{addr}>"),
_ => String::from("UNKNOWN"), _ => String::from("UNKNOWN"),
@ -609,12 +609,12 @@ fn render_open_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
] ]
} }
fn render_closed_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> { fn render_closed_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<Msg> {
let from: String = match &msg.from { let from: String = match &msg.from {
Some(ShowThreadQueryThreadMessagesFrom { Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
name: Some(name), .. name: Some(name), ..
}) => name.to_string(), }) => name.to_string(),
Some(ShowThreadQueryThreadMessagesFrom { Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
addr: Some(addr), .. addr: Some(addr), ..
}) => addr.to_string(), }) => addr.to_string(),
_ => String::from("UNKNOWN"), _ => String::from("UNKNOWN"),
@ -683,7 +683,7 @@ fn render_closed_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
] ]
} }
fn message_render(msg: &ShowThreadQueryThreadMessages, open: bool) -> Node<Msg> { fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool) -> Node<Msg> {
let expand_id = msg.id.clone(); let expand_id = msg.id.clone();
div![ div![
C!["message"], C!["message"],
@ -707,16 +707,16 @@ fn message_render(msg: &ShowThreadQueryThreadMessages, open: bool) -> Node<Msg>
div![ div![
C!["body"], C!["body"],
match &msg.body { match &msg.body {
ShowThreadQueryThreadMessagesBody::UnhandledContentType( ShowThreadQueryThreadOnEmailThreadMessagesBody::UnhandledContentType(
ShowThreadQueryThreadMessagesBodyOnUnhandledContentType { contents ,content_tree}, ShowThreadQueryThreadOnEmailThreadMessagesBodyOnUnhandledContentType { contents ,content_tree},
) => div![ ) => div![
raw_text_message(&contents), raw_text_message(&contents),
div![C!["error"], div![C!["error"],
view_content_tree(&content_tree), view_content_tree(&content_tree),
] ]
], ],
ShowThreadQueryThreadMessagesBody::PlainText( ShowThreadQueryThreadOnEmailThreadMessagesBody::PlainText(
ShowThreadQueryThreadMessagesBodyOnPlainText { ShowThreadQueryThreadOnEmailThreadMessagesBodyOnPlainText {
contents, contents,
content_tree, content_tree,
}, },
@ -724,8 +724,8 @@ fn message_render(msg: &ShowThreadQueryThreadMessages, open: bool) -> Node<Msg>
raw_text_message(&contents), raw_text_message(&contents),
view_content_tree(&content_tree), view_content_tree(&content_tree),
], ],
ShowThreadQueryThreadMessagesBody::Html( ShowThreadQueryThreadOnEmailThreadMessagesBody::Html(
ShowThreadQueryThreadMessagesBodyOnHtml { ShowThreadQueryThreadOnEmailThreadMessagesBodyOnHtml {
contents, contents,
content_tree, content_tree,
}, },
@ -792,7 +792,7 @@ fn message_render(msg: &ShowThreadQueryThreadMessages, open: bool) -> Node<Msg>
#[topo::nested] #[topo::nested]
fn thread( fn thread(
thread: &ShowThreadQueryThread, thread: &ShowThreadQueryThreadOnEmailThread,
open_messages: &HashSet<String>, open_messages: &HashSet<String>,
show_icon_text: bool, show_icon_text: bool,
) -> Node<Msg> { ) -> Node<Msg> {

View File

@ -1,6 +1,7 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use crate::{ use crate::{
graphql::show_thread_query::*,
state::{Context, Model, Msg}, state::{Context, Model, Msg},
view::{self, view_header, view_search_results, view_tags}, view::{self, view_header, view_search_results, view_tags},
}; };
@ -12,7 +13,7 @@ pub(super) fn view(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::ThreadResult { Context::ThreadResult {
thread, thread: ShowThreadQueryThread::EmailThread(thread),
open_messages, open_messages,
} => view::thread(thread, open_messages, show_icon_text), } => view::thread(thread, open_messages, show_icon_text),
Context::SearchResult { Context::SearchResult {