Treat email and news posts as distinct types on the frontend and backend

This commit is contained in:
2024-08-31 11:40:06 -07:00
parent 760cec01a8
commit a8d5617cf2
12 changed files with 324 additions and 72 deletions

View File

@@ -35,6 +35,18 @@ pub struct ThreadSummary {
#[derive(Debug, Union)]
pub enum Thread {
Email(EmailThread),
News(NewsPost),
}
#[derive(Debug, SimpleObject)]
pub struct NewsPost {
pub thread_id: String,
pub slug: String,
pub site: String,
pub title: String,
pub body: String,
pub url: String,
pub timestamp: i64,
}
#[derive(Debug, SimpleObject)]
@@ -374,13 +386,11 @@ impl QueryRoot {
.field("contentTree")
.exists();
// TODO: look at thread_id and conditionally load newsreader
Ok(Thread::Email(
if newsreader::is_newsreader_thread(&thread_id) {
newsreader::thread(pool, thread_id).await?
} else {
nm::thread(nm, thread_id, debug_content_tree).await?
},
))
if newsreader::is_newsreader_thread(&thread_id) {
Ok(newsreader::thread(pool, thread_id).await?)
} else {
Ok(nm::thread(nm, thread_id, debug_content_tree).await?)
}
}
}

View File

@@ -14,7 +14,7 @@ const THREAD_PREFIX: &'static str = "news:";
use crate::{
compute_offset_limit,
error::ServerError,
graphql::{Body, Email, EmailThread, Html, Message, Tag, ThreadSummary},
graphql::{Body, Email, Html, Message, NewsPost, Tag, Thread, ThreadSummary},
AddOutlink, EscapeHtml, InlineStyle, SanitizeHtml, SlurpContents, StripHtml, Transformer,
};
@@ -143,33 +143,19 @@ pub async fn tags(pool: &PgPool, _needs_unread: bool) -> Result<Vec<Tag>, Server
Ok(tags)
}
pub async fn thread(pool: &PgPool, thread_id: String) -> Result<EmailThread, ServerError> {
let id = thread_id
pub async fn thread(pool: &PgPool, thread_id: String) -> Result<Thread, ServerError> {
let thread_id = thread_id
.strip_prefix(THREAD_PREFIX)
.expect("news thread doesn't start with '{THREAD_PREFIX}'")
.to_string();
let r = sqlx::query_file!("sql/thread.sql", id)
let r = sqlx::query_file!("sql/thread.sql", thread_id)
.fetch_one(pool)
.await?;
let site = r.site.unwrap_or("NO TAG".to_string());
let mut tags = vec![format!("{TAG_PREFIX}{site}")];
if r.is_read.unwrap_or(true) {
tags.push("unread".to_string());
};
let slug = r.site.unwrap_or("no-slug".to_string());
let site = r.name.unwrap_or("NO SITE".to_string());
let default_homepage = "http://no-homepage";
let homepage = Url::parse(
&r.homepage
.map(|h| {
if h.is_empty() {
default_homepage.to_string()
} else {
h
}
})
.unwrap_or(default_homepage.to_string()),
)?;
let link = &r
.link
.as_ref()
@@ -182,17 +168,6 @@ pub async fn thread(pool: &PgPool, thread_id: String) -> Result<EmailThread, Ser
})
.map(|h| Url::parse(&h).ok())
.flatten();
let addr = r.link.as_ref().map(|link| {
if link.contains('@') {
link.clone()
} else {
if let Ok(url) = homepage.join(&link) {
url.to_string()
} else {
link.clone()
}
}
});
let mut body = r.summary.unwrap_or("NO SUMMARY".to_string());
// TODO: add site specific cleanups. For example:
// * Grafana does <div class="image-wrapp"><img class="lazyload>"<img src="/media/...>"</img></div>
@@ -212,6 +187,10 @@ pub async fn thread(pool: &PgPool, thread_id: String) -> Result<EmailThread, Ser
Selector::parse("span.story-byline").unwrap(),
Selector::parse("div.p").unwrap(),
],
"www.smbc-comics.com".to_string() => vec![
Selector::parse("img#cc-comic").unwrap(),
Selector::parse("div#aftercomic img").unwrap(),
],
],
}),
Box::new(AddOutlink),
@@ -227,37 +206,24 @@ pub async fn thread(pool: &PgPool, thread_id: String) -> Result<EmailThread, Ser
body = t.transform(&link, &body).await?;
}
}
let body = Body::Html(Html {
html: body,
content_tree: "".to_string(),
});
let title = clean_title(&r.title.unwrap_or("NO TITLE".to_string())).await?;
let from = Some(Email {
name: r.name,
addr: addr.map(|a| a.to_string()),
});
Ok(EmailThread {
let timestamp = r
.date
.expect("post missing date")
.assume_utc()
.unix_timestamp();
Ok(Thread::News(NewsPost {
thread_id,
subject: title.clone(),
messages: vec![Message {
id,
from,
to: Vec::new(),
cc: Vec::new(),
subject: Some(title),
timestamp: Some(
r.date
.expect("post missing date")
.assume_utc()
.unix_timestamp(),
),
headers: Vec::new(),
body,
path: "".to_string(),
attachments: Vec::new(),
tags,
}],
})
slug,
site,
title,
body,
url: link
.as_ref()
.map(|url| url.to_string())
.unwrap_or("NO URL".to_string()),
timestamp,
}))
}
pub async fn set_read_status<'ctx>(
pool: &PgPool,

View File

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