diff --git a/crates/api_crud/src/comment/list.rs b/crates/api_crud/src/comment/list.rs index e7632cba2..e8ff8c180 100644 --- a/crates/api_crud/src/comment/list.rs +++ b/crates/api_crud/src/comment/list.rs @@ -63,6 +63,7 @@ impl PerformCrud for GetComments { None }; + let parent_path_cloned = parent_path.to_owned(); let post_id = data.post_id; let local_user = local_user_view.map(|l| l.local_user); let mut comments = blocking(context.pool(), move |conn| { @@ -74,7 +75,7 @@ impl PerformCrud for GetComments { .saved_only(saved_only) .community_id(community_id) .community_actor_id(community_actor_id) - .parent_path(parent_path) + .parent_path(parent_path_cloned) .post_id(post_id) .local_user(local_user.as_ref()) .page(page) diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs index fbab8d671..a59e8197b 100644 --- a/crates/api_crud/src/post/read.rs +++ b/crates/api_crud/src/post/read.rs @@ -5,6 +5,7 @@ use lemmy_api_common::{ utils::{blocking, check_private_instance, get_local_user_view_from_jwt_opt, mark_post_as_read}, }; use lemmy_db_schema::{ + aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, source::comment::Comment, traits::{Crud, DeleteableOrRemoveable}, }; @@ -64,6 +65,23 @@ impl PerformCrud for GetPost { .await? .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_community"))?; + // Insert into PersonPostAggregates + // to update the read_comments count + if let Some(person_id) = person_id { + let read_comments = post_view.counts.comments; + let person_post_agg_form = PersonPostAggregatesForm { + person_id, + post_id, + read_comments, + ..PersonPostAggregatesForm::default() + }; + blocking(context.pool(), move |conn| { + PersonPostAggregates::upsert(conn, &person_post_agg_form) + }) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_post"))?; + } + // Blank out deleted or removed info for non-logged in users if person_id.is_none() { if post_view.post.deleted || post_view.post.removed { diff --git a/crates/db_schema/src/aggregates/mod.rs b/crates/db_schema/src/aggregates/mod.rs index 03ab1b89a..d55f188f3 100644 --- a/crates/db_schema/src/aggregates/mod.rs +++ b/crates/db_schema/src/aggregates/mod.rs @@ -5,6 +5,8 @@ pub mod community_aggregates; #[cfg(feature = "full")] pub mod person_aggregates; #[cfg(feature = "full")] +pub mod person_post_aggregates; +#[cfg(feature = "full")] pub mod post_aggregates; #[cfg(feature = "full")] pub mod site_aggregates; diff --git a/crates/db_schema/src/aggregates/person_post_aggregates.rs b/crates/db_schema/src/aggregates/person_post_aggregates.rs new file mode 100644 index 000000000..2d268d4f0 --- /dev/null +++ b/crates/db_schema/src/aggregates/person_post_aggregates.rs @@ -0,0 +1,27 @@ +use crate::{ + aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, + newtypes::{PersonId, PostId}, +}; +use diesel::{result::Error, *}; + +impl PersonPostAggregates { + pub fn upsert(conn: &mut PgConnection, form: &PersonPostAggregatesForm) -> Result { + use crate::schema::person_post_aggregates::dsl::*; + insert_into(person_post_aggregates) + .values(form) + .on_conflict((person_id, post_id)) + .do_update() + .set(form) + .get_result::(conn) + } + pub fn read( + conn: &mut PgConnection, + person_id_: PersonId, + post_id_: PostId, + ) -> Result { + use crate::schema::person_post_aggregates::dsl::*; + person_post_aggregates + .filter(post_id.eq(post_id_).and(person_id.eq(person_id_))) + .first::(conn) + } +} diff --git a/crates/db_schema/src/aggregates/structs.rs b/crates/db_schema/src/aggregates/structs.rs index 15fce13b2..e526b49dd 100644 --- a/crates/db_schema/src/aggregates/structs.rs +++ b/crates/db_schema/src/aggregates/structs.rs @@ -6,6 +6,7 @@ use crate::schema::{ comment_aggregates, community_aggregates, person_aggregates, + person_post_aggregates, post_aggregates, site_aggregates, }; @@ -76,6 +77,28 @@ pub struct PostAggregates { #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(Queryable, Associations, Identifiable))] +#[cfg_attr(feature = "full", diesel(table_name = person_post_aggregates))] +#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::person::Person)))] +pub struct PersonPostAggregates { + pub id: i32, + pub person_id: PersonId, + pub post_id: PostId, + pub read_comments: i64, + pub published: chrono::NaiveDateTime, +} + +#[derive(Clone, Default)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = person_post_aggregates))] +pub struct PersonPostAggregatesForm { + pub person_id: PersonId, + pub post_id: PostId, + pub read_comments: i64, + pub published: Option, +} + +#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(Queryable, Associations, Identifiable))] #[cfg_attr(feature = "full", diesel(table_name = site_aggregates))] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::site::Site)))] pub struct SiteAggregates { diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index e0d7167e7..d7f477b7f 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -380,6 +380,16 @@ table! { } } +table! { + person_post_aggregates (id) { + id -> Int4, + person_id -> Int4, + post_id -> Int4, + read_comments -> Int8, + published -> Timestamp, + } +} + table! { post_aggregates (id) { id -> Int4, @@ -667,6 +677,8 @@ joinable!(comment_reply -> comment (comment_id)); joinable!(comment_reply -> person (recipient_id)); joinable!(post -> community (community_id)); joinable!(post -> person (creator_id)); +joinable!(person_post_aggregates -> post (post_id)); +joinable!(person_post_aggregates -> person (person_id)); joinable!(post_aggregates -> post (post_id)); joinable!(post_like -> person (person_id)); joinable!(post_like -> post (post_id)); @@ -725,6 +737,7 @@ allow_tables_to_appear_in_same_query!( person_ban, person_block, person_mention, + person_post_aggregates, comment_reply, post, post_aggregates, diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index 778cf98a3..bba59ac69 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -11,6 +11,7 @@ use lemmy_db_schema::{ local_user_language, person, person_block, + person_post_aggregates, post, post_aggregates, post_like, @@ -43,8 +44,11 @@ type PostViewTuple = ( Option, Option, Option, + i64, ); +sql_function!(fn coalesce(x: sql_types::Nullable, y: sql_types::BigInt) -> sql_types::BigInt); + impl PostView { pub fn read( conn: &mut PgConnection, @@ -64,6 +68,7 @@ impl PostView { read, creator_blocked, post_like, + unread_comments, ) = post::table .find(post_id) .inner_join(person::table) @@ -116,6 +121,13 @@ impl PostView { .and(post_like::person_id.eq(person_id_join)), ), ) + .left_join( + person_post_aggregates::table.on( + post::id + .eq(person_post_aggregates::post_id) + .and(person_post_aggregates::person_id.eq(person_id_join)), + ), + ) .select(( post::all_columns, Person::safe_columns_tuple(), @@ -127,6 +139,10 @@ impl PostView { post_read::all_columns.nullable(), person_block::all_columns.nullable(), post_like::score.nullable(), + coalesce( + post_aggregates::comments.nullable() - person_post_aggregates::read_comments.nullable(), + post_aggregates::comments, + ), )) .first::(conn)?; @@ -149,6 +165,7 @@ impl PostView { read: read.is_some(), creator_blocked: creator_blocked.is_some(), my_vote, + unread_comments, }) } } @@ -237,6 +254,13 @@ impl<'a> PostQuery<'a> { .and(post_like::person_id.eq(person_id_join)), ), ) + .left_join( + person_post_aggregates::table.on( + post::id + .eq(person_post_aggregates::post_id) + .and(person_post_aggregates::person_id.eq(person_id_join)), + ), + ) .left_join( local_user_language::table.on( post::language_id @@ -255,6 +279,10 @@ impl<'a> PostQuery<'a> { post_read::all_columns.nullable(), person_block::all_columns.nullable(), post_like::score.nullable(), + coalesce( + post_aggregates::comments.nullable() - person_post_aggregates::read_comments.nullable(), + post_aggregates::comments, + ), )) .into_boxed(); @@ -412,6 +440,7 @@ impl ViewToVec for PostView { read: a.7.is_some(), creator_blocked: a.8.is_some(), my_vote: a.9, + unread_comments: a.10, }) .collect::>() } @@ -806,6 +835,7 @@ mod tests { language_id: LanguageId(47), }, my_vote: None, + unread_comments: 0, creator: PersonSafe { id: inserted_person.id, name: inserted_person.name.clone(), diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 347f01292..5b068d9ae 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -85,6 +85,7 @@ pub struct PostView { pub read: bool, // Left join to PostRead pub creator_blocked: bool, // Left join to PersonBlock pub my_vote: Option, // Left join to PostLike + pub unread_comments: i64, // Left join to PersonPostAggregates } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] diff --git a/migrations/2022-08-05-203502_add_person_post_aggregates/down.sql b/migrations/2022-08-05-203502_add_person_post_aggregates/down.sql new file mode 100644 index 000000000..f4ae52633 --- /dev/null +++ b/migrations/2022-08-05-203502_add_person_post_aggregates/down.sql @@ -0,0 +1 @@ +drop table person_post_aggregates; diff --git a/migrations/2022-08-05-203502_add_person_post_aggregates/up.sql b/migrations/2022-08-05-203502_add_person_post_aggregates/up.sql new file mode 100644 index 000000000..9a0a5fa5f --- /dev/null +++ b/migrations/2022-08-05-203502_add_person_post_aggregates/up.sql @@ -0,0 +1,11 @@ +-- This table stores the # of read comments for a person, on a post +-- It can then be joined to post_aggregates to get an unread count: +-- unread = post_aggregates.comments - person_post_aggregates.read_comments +create table person_post_aggregates( + id serial primary key, + person_id int references person on update cascade on delete cascade not null, + post_id int references post on update cascade on delete cascade not null, + read_comments bigint not null default 0, + published timestamp not null default now(), + unique(person_id, post_id) +);