Compare commits
No commits in common. "a8d5617cf2994c40f7ea4e61941e5e5081a9912d" and "71de3ef8aef6ee58dfa52f696b20e058b7218937" have entirely different histories.
a8d5617cf2
...
71de3ef8ae
@ -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..>")]
|
||||||
|
|||||||
@ -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>,
|
||||||
|
|||||||
@ -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,
|
||||||
|
addr: addr.map(|a| a.to_string()),
|
||||||
|
});
|
||||||
|
Ok(Thread {
|
||||||
|
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")
|
.expect("post missing date")
|
||||||
.assume_utc()
|
.assume_utc()
|
||||||
.unix_timestamp();
|
.unix_timestamp(),
|
||||||
Ok(Thread::News(NewsPost {
|
),
|
||||||
thread_id,
|
headers: Vec::new(),
|
||||||
slug,
|
|
||||||
site,
|
|
||||||
title,
|
|
||||||
body,
|
body,
|
||||||
url: link
|
path: "".to_string(),
|
||||||
.as_ref()
|
attachments: Vec::new(),
|
||||||
.map(|url| url.to_string())
|
tags,
|
||||||
.unwrap_or("NO URL".to_string()),
|
}],
|
||||||
timestamp,
|
})
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
pub async fn set_read_status<'ctx>(
|
pub async fn set_read_status<'ctx>(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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",
|
||||||
"kind": "OBJECT",
|
"type": {
|
||||||
"name": "NewsPost",
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
"ofType": null
|
"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": "Thread",
|
||||||
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": null,
|
"description": null,
|
||||||
|
|||||||
@ -1,16 +1,5 @@
|
|||||||
query ShowThreadQuery($threadId: String!) {
|
query ShowThreadQuery($threadId: String!) {
|
||||||
thread(threadId: $threadId) {
|
thread(threadId: $threadId) {
|
||||||
__typename ... on NewsPost{
|
|
||||||
threadId
|
|
||||||
slug
|
|
||||||
site
|
|
||||||
title
|
|
||||||
body
|
|
||||||
url
|
|
||||||
timestamp
|
|
||||||
# TODO: unread
|
|
||||||
}
|
|
||||||
__typename ... on EmailThread{
|
|
||||||
threadId,
|
threadId,
|
||||||
subject
|
subject
|
||||||
messages {
|
messages {
|
||||||
@ -56,7 +45,6 @@ query ShowThreadQuery($threadId: String!) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
tags {
|
tags {
|
||||||
name
|
name
|
||||||
bgColor
|
bgColor
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -341,35 +341,26 @@ 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,
|
|
||||||
subject,
|
|
||||||
messages,
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
let mut open_messages: HashSet<_> = messages
|
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|msg| msg.tags.iter().any(|t| t == "unread"))
|
.filter(|msg| msg.tags.iter().any(|t| t == "unread"))
|
||||||
.map(|msg| msg.id.clone())
|
.map(|msg| msg.id.clone())
|
||||||
.collect();
|
.collect();
|
||||||
if open_messages.is_empty() {
|
if open_messages.is_empty() {
|
||||||
open_messages = messages.iter().map(|msg| msg.id.clone()).collect();
|
open_messages = data
|
||||||
|
.thread
|
||||||
|
.messages
|
||||||
|
.iter()
|
||||||
|
.map(|msg| msg.id.clone())
|
||||||
|
.collect();
|
||||||
}
|
}
|
||||||
model.context = Context::ThreadResult {
|
model.context = Context::ThreadResult {
|
||||||
thread: data.thread,
|
thread: data.thread,
|
||||||
open_messages,
|
open_messages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
graphql::show_thread_query::ShowThreadQueryThread::NewsPost(..) => {
|
|
||||||
model.context = Context::ThreadResult {
|
|
||||||
thread: data.thread,
|
|
||||||
open_messages: HashSet::new(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Msg::ShowThreadResult(bad) => {
|
Msg::ShowThreadResult(bad) => {
|
||||||
error!("show_thread_query error: {bad:#?}");
|
error!("show_thread_query error: {bad:#?}");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)
|
|
||||||
})
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user