Compare commits

..

No commits in common. "a8d5617cf2994c40f7ea4e61941e5e5081a9912d" and "71de3ef8aef6ee58dfa52f696b20e058b7218937" have entirely different histories.

14 changed files with 213 additions and 494 deletions

View File

@ -164,7 +164,7 @@ async fn original(
#[rocket::get("/")] #[rocket::get("/")]
fn graphiql() -> content::RawHtml<String> { fn graphiql() -> content::RawHtml<String> {
content::RawHtml(GraphiQLSource::build().endpoint("/api/graphql").finish()) content::RawHtml(GraphiQLSource::build().endpoint("/graphql").finish())
} }
#[rocket::get("/graphql?<query..>")] #[rocket::get("/graphql?<query..>")]

View File

@ -32,25 +32,8 @@ pub struct ThreadSummary {
pub tags: Vec<String>, pub tags: Vec<String>,
} }
#[derive(Debug, Union)]
pub enum Thread {
Email(EmailThread),
News(NewsPost),
}
#[derive(Debug, SimpleObject)] #[derive(Debug, SimpleObject)]
pub struct NewsPost { pub struct Thread {
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)]
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>,

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, NewsPost, Tag, Thread, ThreadSummary}, graphql::{Body, Email, Html, Message, Tag, Thread, ThreadSummary},
AddOutlink, EscapeHtml, InlineStyle, SanitizeHtml, SlurpContents, StripHtml, Transformer, AddOutlink, EscapeHtml, InlineStyle, SanitizeHtml, SlurpContents, StripHtml, Transformer,
}; };
@ -144,18 +144,32 @@ pub async fn tags(pool: &PgPool, _needs_unread: bool) -> Result<Vec<Tag>, Server
} }
pub async fn thread(pool: &PgPool, thread_id: String) -> Result<Thread, ServerError> { pub async fn thread(pool: &PgPool, thread_id: String) -> Result<Thread, ServerError> {
let thread_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}'")
.to_string(); .to_string();
let r = sqlx::query_file!("sql/thread.sql", thread_id) let r = sqlx::query_file!("sql/thread.sql", id)
.fetch_one(pool) .fetch_one(pool)
.await?; .await?;
let slug = r.site.unwrap_or("no-slug".to_string()); let site = r.site.unwrap_or("NO TAG".to_string());
let site = r.name.unwrap_or("NO SITE".to_string()); let mut tags = vec![format!("{TAG_PREFIX}{site}")];
if r.is_read.unwrap_or(true) {
tags.push("unread".to_string());
};
let default_homepage = "http://no-homepage"; 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 let link = &r
.link .link
.as_ref() .as_ref()
@ -168,11 +182,23 @@ pub async fn thread(pool: &PgPool, thread_id: String) -> Result<Thread, ServerEr
}) })
.map(|h| Url::parse(&h).ok()) .map(|h| Url::parse(&h).ok())
.flatten(); .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()); let mut body = r.summary.unwrap_or("NO SUMMARY".to_string());
// TODO: add site specific cleanups. For example: // TODO: add site specific cleanups. For example:
// * 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![
@ -187,10 +213,6 @@ pub async fn thread(pool: &PgPool, thread_id: String) -> Result<Thread, ServerEr
Selector::parse("span.story-byline").unwrap(), Selector::parse("span.story-byline").unwrap(),
Selector::parse("div.p").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), Box::new(AddOutlink),
@ -206,24 +228,37 @@ pub async fn thread(pool: &PgPool, thread_id: String) -> Result<Thread, ServerEr
body = t.transform(&link, &body).await?; 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 title = clean_title(&r.title.unwrap_or("NO TITLE".to_string())).await?;
let timestamp = r let from = Some(Email {
.date name: r.name,
.expect("post missing date") addr: addr.map(|a| a.to_string()),
.assume_utc() });
.unix_timestamp(); Ok(Thread {
Ok(Thread::News(NewsPost {
thread_id, thread_id,
slug, subject: title.clone(),
site, messages: vec![Message {
title, id,
body, from,
url: link to: Vec::new(),
.as_ref() cc: Vec::new(),
.map(|url| url.to_string()) subject: Some(title),
.unwrap_or("NO URL".to_string()), timestamp: Some(
timestamp, r.date
})) .expect("post missing date")
.assume_utc()
.unix_timestamp(),
),
headers: Vec::new(),
body,
path: "".to_string(),
attachments: Vec::new(),
tags,
}],
})
} }
pub async fn set_read_status<'ctx>( pub async fn set_read_status<'ctx>(
pool: &PgPool, pool: &PgPool,

View File

@ -14,8 +14,8 @@ use crate::{
compute_offset_limit, compute_offset_limit,
error::ServerError, error::ServerError,
graphql::{ graphql::{
Attachment, Body, DispositionType, Email, EmailThread, Header, Html, Message, PlainText, Attachment, Body, DispositionType, Email, Header, Html, Message, PlainText, Tag, Thread,
Tag, Thread, ThreadSummary, UnhandledContentType, ThreadSummary, UnhandledContentType,
}, },
linkify_html, sanitize_html, linkify_html, sanitize_html,
}; };
@ -246,11 +246,11 @@ 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::Email(EmailThread { Ok(Thread {
thread_id, thread_id,
subject, subject,
messages, messages,
})) })
} }
fn email_addresses( fn email_addresses(

View File

@ -290,73 +290,6 @@
"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,
@ -858,129 +791,6 @@
"name": "Mutation", "name": "Mutation",
"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": "slug",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
{
"args": [],
"deprecationReason": null,
"description": null,
"isDeprecated": false,
"name": "site",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
{
"args": [],
"deprecationReason": null,
"description": null,
"isDeprecated": false,
"name": "title",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
{
"args": [],
"deprecationReason": null,
"description": null,
"isDeprecated": false,
"name": "body",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
{
"args": [],
"deprecationReason": null,
"description": null,
"isDeprecated": false,
"name": "url",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
{
"args": [],
"deprecationReason": null,
"description": null,
"isDeprecated": false,
"name": "timestamp",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
}
],
"inputFields": null,
"interfaces": [],
"kind": "OBJECT",
"name": "NewsPost",
"possibleTypes": null
},
{ {
"description": "Information about pagination in a connection", "description": "Information about pagination in a connection",
"enumValues": null, "enumValues": null,
@ -1246,7 +1056,7 @@
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "UNION", "kind": "OBJECT",
"name": "Thread", "name": "Thread",
"ofType": null "ofType": null
} }
@ -1347,23 +1157,69 @@
{ {
"description": null, "description": null,
"enumValues": null, "enumValues": null,
"fields": null, "fields": [
"inputFields": null,
"interfaces": null,
"kind": "UNION",
"name": "Thread",
"possibleTypes": [
{ {
"kind": "OBJECT", "args": [],
"name": "EmailThread", "deprecationReason": null,
"ofType": null "description": null,
"isDeprecated": false,
"name": "threadId",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}, },
{ {
"kind": "OBJECT", "args": [],
"name": "NewsPost", "deprecationReason": null,
"ofType": 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,

View File

@ -1,59 +1,47 @@
query ShowThreadQuery($threadId: String!) { query ShowThreadQuery($threadId: String!) {
thread(threadId: $threadId) { thread(threadId: $threadId) {
__typename ... on NewsPost{ threadId,
threadId subject
slug messages {
site id
title
body
url
timestamp
# TODO: unread
}
__typename ... on EmailThread{
threadId,
subject subject
messages { tags
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
subject idx
tags filename
from { contentType
name contentId
addr size
}
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

@ -1,4 +1,4 @@
DEV_HOST=localhost DEV_HOST=localhost
DEV_PORT=9345 DEV_PORT=9345
graphql-client introspect-schema http://${DEV_HOST:?}:${DEV_PORT:?}/api/graphql --output schema.json graphql-client introspect-schema http://${DEV_HOST:?}:${DEV_PORT:?}/graphql --output schema.json
git diff schema.json git diff schema.json

View File

@ -22,7 +22,6 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@700&display=swap" rel="stylesheet">
<link data-trunk rel="css" href="static/site-specific.css" />
</head> </head>
<body> <body>

View File

@ -341,34 +341,25 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}) })
.collect(), .collect(),
); );
match &data.thread { let mut open_messages: HashSet<_> = data
graphql::show_thread_query::ShowThreadQueryThread::EmailThread( .thread
ShowThreadQueryThreadOnEmailThread { .messages
thread_id, .iter()
subject, .filter(|msg| msg.tags.iter().any(|t| t == "unread"))
messages, .map(|msg| msg.id.clone())
}, .collect();
) => { if open_messages.is_empty() {
let mut open_messages: HashSet<_> = messages open_messages = data
.iter() .thread
.filter(|msg| msg.tags.iter().any(|t| t == "unread")) .messages
.map(|msg| msg.id.clone()) .iter()
.collect(); .map(|msg| msg.id.clone())
if open_messages.is_empty() { .collect();
open_messages = messages.iter().map(|msg| msg.id.clone()).collect();
}
model.context = Context::ThreadResult {
thread: data.thread,
open_messages,
};
}
graphql::show_thread_query::ShowThreadQueryThread::NewsPost(..) => {
model.context = Context::ThreadResult {
thread: data.thread,
open_messages: HashSet::new(),
};
}
} }
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,7 +3,6 @@ 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},
}; };
@ -16,13 +15,9 @@ 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: ShowThreadQueryThread::EmailThread(thread), thread,
open_messages, open_messages,
} => view::thread(thread, open_messages, show_icon_text), } => view::thread(thread, open_messages, show_icon_text),
Context::ThreadResult {
thread: ShowThreadQueryThread::NewsPost(post),
..
} => view::news_post(post, show_icon_text),
Context::SearchResult { Context::SearchResult {
query, query,
results, results,

View File

@ -4,7 +4,7 @@ use seed::{prelude::*, *};
use crate::{ use crate::{
api::urls, api::urls,
graphql::{front_page_query::*, show_thread_query::*}, graphql::front_page_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,
@ -13,18 +13,14 @@ use crate::{
}; };
pub(super) fn view(model: &Model) -> Node<Msg> { pub(super) fn view(model: &Model) -> Node<Msg> {
log::info!("mobile::view"); log::info!("tablet::view");
let show_icon_text = false; let show_icon_text = false;
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: ShowThreadQueryThread::EmailThread(thread), thread,
open_messages, open_messages,
} => view::thread(thread, open_messages, show_icon_text), } => view::thread(thread, open_messages, show_icon_text),
Context::ThreadResult {
thread: ShowThreadQueryThread::NewsPost(post),
..
} => view::news_post(post, show_icon_text),
Context::SearchResult { Context::SearchResult {
query, query,
results, results,

View File

@ -394,9 +394,9 @@ macro_rules! implement_email {
} }
implement_email!( implement_email!(
ShowThreadQueryThreadOnEmailThreadMessagesTo, ShowThreadQueryThreadMessagesTo,
ShowThreadQueryThreadOnEmailThreadMessagesCc, ShowThreadQueryThreadMessagesCc,
ShowThreadQueryThreadOnEmailThreadMessagesFrom ShowThreadQueryThreadMessagesFrom
); );
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: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<Msg> { fn render_open_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
let (from, from_detail) = match &msg.from { let (from, from_detail) = match &msg.from {
Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom { Some(ShowThreadQueryThreadMessagesFrom {
name: Some(name), name: Some(name),
addr, addr,
}) => (name.to_string(), addr.clone()), }) => (name.to_string(), addr.clone()),
Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom { Some(ShowThreadQueryThreadMessagesFrom {
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: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<
if i>0 { ", " }else { "" }, if i>0 { ", " }else { "" },
{ {
let to = match to { let to = match to {
ShowThreadQueryThreadOnEmailThreadMessagesTo { ShowThreadQueryThreadMessagesTo {
name: Some(name), name: Some(name),
addr:Some(addr), addr:Some(addr),
} => format!("{name} <{addr}>"), } => format!("{name} <{addr}>"),
ShowThreadQueryThreadOnEmailThreadMessagesTo { ShowThreadQueryThreadMessagesTo {
name: Some(name), name: Some(name),
addr:None addr:None
} => format!("{name}"), } => format!("{name}"),
ShowThreadQueryThreadOnEmailThreadMessagesTo { ShowThreadQueryThreadMessagesTo {
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: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<
if i>0 { ", " }else { "" }, if i>0 { ", " }else { "" },
{ {
let cc = match cc { let cc = match cc {
ShowThreadQueryThreadOnEmailThreadMessagesCc { ShowThreadQueryThreadMessagesCc {
name: Some(name), name: Some(name),
addr:Some(addr), addr:Some(addr),
} => format!("{name} <{addr}>"), } => format!("{name} <{addr}>"),
ShowThreadQueryThreadOnEmailThreadMessagesCc { ShowThreadQueryThreadMessagesCc {
name: Some(name), name: Some(name),
addr:None addr:None
} => format!("{name}"), } => format!("{name}"),
ShowThreadQueryThreadOnEmailThreadMessagesCc { ShowThreadQueryThreadMessagesCc {
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: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<
] ]
} }
fn render_closed_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<Msg> { fn render_closed_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
let from: String = match &msg.from { let from: String = match &msg.from {
Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom { Some(ShowThreadQueryThreadMessagesFrom {
name: Some(name), .. name: Some(name), ..
}) => name.to_string(), }) => name.to_string(),
Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom { Some(ShowThreadQueryThreadMessagesFrom {
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: &ShowThreadQueryThreadOnEmailThreadMessages) -> Nod
] ]
} }
fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool) -> Node<Msg> { fn message_render(msg: &ShowThreadQueryThreadMessages, 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: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool)
div![ div![
C!["body"], C!["body"],
match &msg.body { match &msg.body {
ShowThreadQueryThreadOnEmailThreadMessagesBody::UnhandledContentType( ShowThreadQueryThreadMessagesBody::UnhandledContentType(
ShowThreadQueryThreadOnEmailThreadMessagesBodyOnUnhandledContentType { contents ,content_tree}, ShowThreadQueryThreadMessagesBodyOnUnhandledContentType { 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),
] ]
], ],
ShowThreadQueryThreadOnEmailThreadMessagesBody::PlainText( ShowThreadQueryThreadMessagesBody::PlainText(
ShowThreadQueryThreadOnEmailThreadMessagesBodyOnPlainText { ShowThreadQueryThreadMessagesBodyOnPlainText {
contents, contents,
content_tree, content_tree,
}, },
@ -724,8 +724,8 @@ fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool)
raw_text_message(&contents), raw_text_message(&contents),
view_content_tree(&content_tree), view_content_tree(&content_tree),
], ],
ShowThreadQueryThreadOnEmailThreadMessagesBody::Html( ShowThreadQueryThreadMessagesBody::Html(
ShowThreadQueryThreadOnEmailThreadMessagesBodyOnHtml { ShowThreadQueryThreadMessagesBodyOnHtml {
contents, contents,
content_tree, content_tree,
}, },
@ -792,7 +792,7 @@ fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool)
#[topo::nested] #[topo::nested]
fn thread( fn thread(
thread: &ShowThreadQueryThreadOnEmailThread, thread: &ShowThreadQueryThread,
open_messages: &HashSet<String>, open_messages: &HashSet<String>,
show_icon_text: bool, show_icon_text: bool,
) -> Node<Msg> { ) -> Node<Msg> {
@ -1075,107 +1075,3 @@ pub fn view_tags(model: &Model) -> Node<Msg> {
] ]
] ]
} }
fn news_post(post: &ShowThreadQueryThreadOnNewsPost, show_icon_text: bool) -> Node<Msg> {
// TODO(wathiede): show per-message subject if it changes significantly from top-level subject
let subject = &post.title;
set_title(subject);
let read_thread_id = post.thread_id.clone();
let unread_thread_id = post.thread_id.clone();
div![
C!["thread"],
h3![C!["is-size-5"], subject],
div![
C!["level", "is-mobile"],
div![
C!["level-item"],
div![
C!["buttons", "has-addons"],
button![
C!["button", "mark-read"],
attrs! {At::Title => "Mark as read"},
span![C!["icon", "is-small"], i![C!["far", "fa-envelope-open"]]],
IF!(show_icon_text=>span!["Read"]),
ev(Ev::Click, move |_| Msg::SetUnread(read_thread_id, false)),
],
button![
C!["button", "mark-unread"],
attrs! {At::Title => "Mark as unread"},
span![C!["icon", "is-small"], i![C!["far", "fa-envelope"]]],
IF!(show_icon_text=>span!["Unread"]),
ev(Ev::Click, move |_| Msg::SetUnread(unread_thread_id, true)),
],
],
],
// This would be the holder for spam buttons on emails
div![C!["level-item"], div![]]
],
div![
C!["message"],
div![C!["header"], render_news_post_header(&post)],
div![C!["body", format!("site-{}", post.slug)], raw![&post.body]]
] /* TODO(wathiede): plumb in orignal id
a![
attrs! {At::Href=>api::original(&thread_node.0.as_ref().expect("message missing").id)},
"Original"
],
*/
]
}
fn render_news_post_header(post: &ShowThreadQueryThreadOnNewsPost) -> Node<Msg> {
let from = &post.site;
let from_detail = post.url.clone();
let avatar: Option<String> = None;
//let avatar: Option<String> = Some(String::from("https://bulma.io/images/placeholders/64x64.png"));
let id = post.thread_id.clone();
// TODO: plumb this through
//let is_unread = has_unread(&msg.tags);
let is_unread = true;
let img = render_avatar(avatar, &from);
article![
C!["media"],
figure![C!["media-left"], p![C!["image", "is-64x64"], img]],
div![
C!["media-content"],
div![
C!["content"],
p![
strong![from],
br![],
small![
&from_detail,
" ",
span![
i![C!["far", "fa-clone"]],
ev(Ev::Click, move |e| {
e.stop_propagation();
Msg::CopyToClipboard(from_detail.to_string())
})
]
],
table![tr![td![
attrs! {At::ColSpan=>2},
span![C!["header"], human_age(post.timestamp)]
]]],
],
],
],
div![
C!["media-right"],
span![
C!["read-status"],
i![C![
"far",
if is_unread {
"fa-envelope"
} else {
"fa-envelope-open"
},
]]
],
ev(Ev::Click, move |e| {
e.stop_propagation();
Msg::SetUnread(id, !is_unread)
})
]
]
}

View File

@ -1,8 +1,6 @@
use log::info;
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},
}; };
@ -14,13 +12,9 @@ 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: ShowThreadQueryThread::EmailThread(thread), thread,
open_messages, open_messages,
} => view::thread(thread, open_messages, show_icon_text), } => view::thread(thread, open_messages, show_icon_text),
Context::ThreadResult {
thread: ShowThreadQueryThread::NewsPost(post),
..
} => view::news_post(post, show_icon_text),
Context::SearchResult { Context::SearchResult {
query, query,
results, results,

View File

@ -1,14 +0,0 @@
.body.site-saturday-morning-breakfast-cereal {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.body.site-slashdot i {
display: block;
padding-left: 1em;
margin-top: 1em;
margin-bottom: 1em;
border-left: 2px solid #ddd;
}