Compare commits

..

3 Commits

Author SHA1 Message Date
831466ddda Add mark read/unread support for news 2024-07-22 14:43:05 -07:00
4ee34444ae Move thread: and id: prefixing to server side.
This paves way for better news: support
2024-07-22 14:26:48 -07:00
879ddb112e Remove some logging and fix a comment 2024-07-22 14:26:24 -07:00
7 changed files with 77 additions and 32 deletions

View File

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE\n post\nSET\n is_read = $1\nWHERE\n uid = $2\n",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Bool",
"Text"
]
},
"nullable": []
},
"hash": "b39147b9d06171cb742141eda4675688cb702fb284758b1224ed3aa2d7f3b3d9"
}

View File

@ -0,0 +1,6 @@
UPDATE
post
SET
is_read = $1
WHERE
uid = $2

View File

@ -1,7 +1,6 @@
use async_graphql::{ use async_graphql::{
connection::{Connection}, connection::Connection, Context, EmptySubscription, Enum, Error, FieldResult, Object, Schema,
Context, EmptySubscription, Enum, Error, FieldResult, Object, Schema, SimpleObject, Union, SimpleObject, Union,
}; };
use log::info; use log::info;
use notmuch::Notmuch; use notmuch::Notmuch;
@ -273,11 +272,14 @@ impl Mutation {
unread: bool, unread: bool,
) -> Result<bool, Error> { ) -> Result<bool, Error> {
let nm = ctx.data_unchecked::<Notmuch>(); let nm = ctx.data_unchecked::<Notmuch>();
info!("set_read_status({unread})"); let pool = ctx.data_unchecked::<PgPool>();
if unread {
nm.tag_add("unread", &format!("{query}"))?; for q in query.split_whitespace() {
} else { if newsreader::is_newsreader_thread(&q) {
nm.tag_remove("unread", &format!("{query}"))?; newsreader::set_read_status(pool, &q, unread).await?;
} else {
nm::set_read_status(nm, q, unread).await?;
}
} }
Ok(true) Ok(true)
} }

View File

@ -44,7 +44,6 @@ pub async fn search(
query: String, query: String,
) -> Result<Connection<usize, ThreadSummary>, async_graphql::Error> { ) -> Result<Connection<usize, ThreadSummary>, async_graphql::Error> {
let query: Query = query.parse()?; let query: Query = query.parse()?;
info!("news search query {query:?}");
let site = query.site.expect("search has no site"); let site = query.site.expect("search has no site");
connection::query( connection::query(
after, after,
@ -52,7 +51,6 @@ pub async fn search(
first, first,
last, last,
|after: Option<usize>, before: Option<usize>, first, last| async move { |after: Option<usize>, before: Option<usize>, first, last| async move {
info!("search page info {after:#?}, {before:#?}, {first:#?}, {last:#?}");
let default_page_size = 100; let default_page_size = 100;
let (offset, limit) = match (after, before, first, last) { let (offset, limit) = match (after, before, first, last) {
// Reasonable defaults // Reasonable defaults
@ -86,7 +84,6 @@ pub async fn search(
// The +1 is to see if there are more pages of data available. // The +1 is to see if there are more pages of data available.
let limit = limit + 1; let limit = limit + 1;
info!("search page offset {offset} limit {limit}");
let rows = sqlx::query_file!( let rows = sqlx::query_file!(
"sql/threads.sql", "sql/threads.sql",
site, site,
@ -214,7 +211,7 @@ pub async fn thread(pool: &PgPool, thread_id: String) -> Result<Thread, ServerEr
let html = r.summary.unwrap_or("NO SUMMARY".to_string()); let html = 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. imperialviolet // * Some sites appear to be HTML encoded, unencode them, i.e. imperialviolent
let html = sanitize_html(&html, "", &link)?; let html = sanitize_html(&html, "", &link)?;
let body = Body::Html(Html { let body = Body::Html(Html {
html, html,
@ -253,6 +250,7 @@ pub async fn thread(pool: &PgPool, thread_id: String) -> Result<Thread, ServerEr
struct Query { struct Query {
unread_only: bool, unread_only: bool,
site: Option<String>, site: Option<String>,
uid: Option<String>,
remainder: Vec<String>, remainder: Vec<String>,
} }
@ -261,6 +259,7 @@ impl FromStr for Query {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut unread_only = false; let mut unread_only = false;
let mut site = None; let mut site = None;
let mut uid = None;
let mut remainder = Vec::new(); let mut remainder = Vec::new();
let site_prefix = format!("tag:{TAG_PREFIX}"); let site_prefix = format!("tag:{TAG_PREFIX}");
for word in s.split_whitespace() { for word in s.split_whitespace() {
@ -268,6 +267,8 @@ impl FromStr for Query {
unread_only = true unread_only = true
} else if word.starts_with(&site_prefix) { } else if word.starts_with(&site_prefix) {
site = Some(word[site_prefix.len()..].to_string()) site = Some(word[site_prefix.len()..].to_string())
} else if word.starts_with(THREAD_PREFIX) {
uid = Some(word[THREAD_PREFIX.len()..].to_string())
} else { } else {
remainder.push(word.to_string()); remainder.push(word.to_string());
} }
@ -275,7 +276,20 @@ impl FromStr for Query {
Ok(Query { Ok(Query {
unread_only, unread_only,
site, site,
uid,
remainder, remainder,
}) })
} }
} }
pub async fn set_read_status<'ctx>(
pool: &PgPool,
query: &str,
unread: bool,
) -> Result<bool, ServerError> {
let query: Query = query.parse()?;
sqlx::query_file!("sql/set_unread.sql", !unread, query.uid)
.execute(pool)
.await?;
Ok(true)
}

View File

@ -80,7 +80,7 @@ pub async fn search(
.0 .0
.into_iter() .into_iter()
.map(|ts| ThreadSummary { .map(|ts| ThreadSummary {
thread: ts.thread, thread: format!("thread:{}", ts.thread),
timestamp: ts.timestamp, timestamp: ts.timestamp,
date_relative: ts.date_relative, date_relative: ts.date_relative,
matched: ts.matched, matched: ts.matched,
@ -248,7 +248,7 @@ pub async fn thread(
// TODO(wathiede): parse message and fill out attachments // TODO(wathiede): parse message and fill out attachments
let attachments = extract_attachments(&m, &id)?; let attachments = extract_attachments(&m, &id)?;
messages.push(Message { messages.push(Message {
id, id: format!("id:{id}"),
from, from,
to, to,
cc, cc,
@ -752,3 +752,16 @@ fn render_content_type_tree(m: &ParsedMail) -> String {
SKIP_HEADERS.join("\n ") SKIP_HEADERS.join("\n ")
) )
} }
pub async fn set_read_status<'ctx>(
nm: &Notmuch,
query: &str,
unread: bool,
) -> Result<bool, ServerError> {
if unread {
nm.tag_add("unread", &format!("{query}"))?;
} else {
nm.tag_remove("unread", &format!("{query}"))?;
}
Ok(true)
}

View File

@ -372,7 +372,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
{ {
let threads = selected_threads let threads = selected_threads
.iter() .iter()
.map(|tid| format!("thread:{tid}")) .map(|tid| tid.to_string())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" "); .join(" ");
orders orders
@ -387,7 +387,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
{ {
let threads = selected_threads let threads = selected_threads
.iter() .iter()
.map(|tid| format!("thread:{tid}")) .map(|tid| tid.to_string())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" "); .join(" ");
orders orders
@ -402,7 +402,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
{ {
let threads = selected_threads let threads = selected_threads
.iter() .iter()
.map(|tid| format!("thread:{tid}")) .map(|tid| tid.to_string())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" "); .join(" ");
orders orders
@ -417,7 +417,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
{ {
let threads = selected_threads let threads = selected_threads
.iter() .iter()
.map(|tid| format!("thread:{tid}")) .map(|tid| tid.to_string())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" "); .join(" ");
orders orders

View File

@ -73,6 +73,7 @@ fn removable_tags_chiclet<'a>(
"is-grouped-multiline" "is-grouped-multiline"
], ],
tags.iter().map(move |tag| { tags.iter().map(move |tag| {
let thread_id = thread_id.to_string();
let hex = compute_color(tag); let hex = compute_color(tag);
let style = style! {St::BackgroundColor=>hex}; let style = style! {St::BackgroundColor=>hex};
let classes = C!["tag", IF!(is_mobile => "is-small")]; let classes = C!["tag", IF!(is_mobile => "is-small")];
@ -81,7 +82,6 @@ fn removable_tags_chiclet<'a>(
}; };
let tag = tag.clone(); let tag = tag.clone();
let rm_tag = tag.clone(); let rm_tag = tag.clone();
let thread_id = format!("thread:{thread_id}");
div![ div![
C!["control"], C!["control"],
div![ div![
@ -592,7 +592,7 @@ fn render_open_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
], ],
ev(Ev::Click, move |e| { ev(Ev::Click, move |e| {
e.stop_propagation(); e.stop_propagation();
Msg::SetUnread(format!("id:{id}"), !is_unread) Msg::SetUnread(id, !is_unread)
}) })
] ]
] ]
@ -664,7 +664,7 @@ fn render_closed_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
], ],
ev(Ev::Click, move |e| { ev(Ev::Click, move |e| {
e.stop_propagation(); e.stop_propagation();
Msg::SetUnread(format!("id:{id}"), !is_unread) Msg::SetUnread(id, !is_unread)
}) })
] ]
] ]
@ -808,7 +808,8 @@ fn thread(
}); });
let read_thread_id = thread.thread_id.clone(); let read_thread_id = thread.thread_id.clone();
let unread_thread_id = thread.thread_id.clone(); let unread_thread_id = thread.thread_id.clone();
let spam_thread_id = thread.thread_id.clone(); let spam_add_thread_id = thread.thread_id.clone();
let spam_unread_thread_id = thread.thread_id.clone();
div![ div![
C!["thread"], C!["thread"],
h3![C!["is-size-5"], subject], h3![C!["is-size-5"], subject],
@ -827,20 +828,14 @@ fn thread(
attrs! {At::Title => "Mark as read"}, attrs! {At::Title => "Mark as read"},
span![C!["icon", "is-small"], i![C!["far", "fa-envelope-open"]]], span![C!["icon", "is-small"], i![C!["far", "fa-envelope-open"]]],
IF!(show_icon_text=>span!["Read"]), IF!(show_icon_text=>span!["Read"]),
ev(Ev::Click, move |_| Msg::SetUnread( ev(Ev::Click, move |_| Msg::SetUnread(read_thread_id, false)),
format!("thread:{read_thread_id}"),
false
)),
], ],
button![ button![
C!["button", "mark-unread"], C!["button", "mark-unread"],
attrs! {At::Title => "Mark as unread"}, attrs! {At::Title => "Mark as unread"},
span![C!["icon", "is-small"], i![C!["far", "fa-envelope"]]], span![C!["icon", "is-small"], i![C!["far", "fa-envelope"]]],
IF!(show_icon_text=>span!["Unread"]), IF!(show_icon_text=>span!["Unread"]),
ev(Ev::Click, move |_| Msg::SetUnread( ev(Ev::Click, move |_| Msg::SetUnread(unread_thread_id, true)),
format!("thread:{unread_thread_id}"),
true
)),
], ],
], ],
], ],
@ -854,8 +849,8 @@ fn thread(
span![C!["icon", "is-small"], i![C!["far", "fa-hand"]]], span![C!["icon", "is-small"], i![C!["far", "fa-hand"]]],
IF!(show_icon_text=>span!["Spam"]), IF!(show_icon_text=>span!["Spam"]),
ev(Ev::Click, move |_| Msg::MultiMsg(vec![ ev(Ev::Click, move |_| Msg::MultiMsg(vec![
Msg::AddTag(format!("thread:{spam_thread_id}"), "Spam".to_string()), Msg::AddTag(spam_add_thread_id, "Spam".to_string()),
Msg::SetUnread(format!("thread:{spam_thread_id}"), false) Msg::SetUnread(spam_unread_thread_id, false)
])), ])),
], ],
], ],