Showing # of unread comments for posts. Fixes #2134 (#2393)

* Showing # of unread comments for posts. Fixes #2134

* Fix lint.

* Forgot to remove comment list update.

* Fix clippy
This commit is contained in:
Dessalines 2022-09-27 12:45:46 -04:00 committed by GitHub
parent f2537ba7db
commit 0aeb78b8f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 128 additions and 1 deletions

View file

@ -63,6 +63,7 @@ impl PerformCrud for GetComments {
None None
}; };
let parent_path_cloned = parent_path.to_owned();
let post_id = data.post_id; let post_id = data.post_id;
let local_user = local_user_view.map(|l| l.local_user); let local_user = local_user_view.map(|l| l.local_user);
let mut comments = blocking(context.pool(), move |conn| { let mut comments = blocking(context.pool(), move |conn| {
@ -74,7 +75,7 @@ impl PerformCrud for GetComments {
.saved_only(saved_only) .saved_only(saved_only)
.community_id(community_id) .community_id(community_id)
.community_actor_id(community_actor_id) .community_actor_id(community_actor_id)
.parent_path(parent_path) .parent_path(parent_path_cloned)
.post_id(post_id) .post_id(post_id)
.local_user(local_user.as_ref()) .local_user(local_user.as_ref())
.page(page) .page(page)

View file

@ -5,6 +5,7 @@ use lemmy_api_common::{
utils::{blocking, check_private_instance, get_local_user_view_from_jwt_opt, mark_post_as_read}, utils::{blocking, check_private_instance, get_local_user_view_from_jwt_opt, mark_post_as_read},
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm},
source::comment::Comment, source::comment::Comment,
traits::{Crud, DeleteableOrRemoveable}, traits::{Crud, DeleteableOrRemoveable},
}; };
@ -64,6 +65,23 @@ impl PerformCrud for GetPost {
.await? .await?
.map_err(|e| LemmyError::from_error_message(e, "couldnt_find_community"))?; .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 // Blank out deleted or removed info for non-logged in users
if person_id.is_none() { if person_id.is_none() {
if post_view.post.deleted || post_view.post.removed { if post_view.post.deleted || post_view.post.removed {

View file

@ -5,6 +5,8 @@ pub mod community_aggregates;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod person_aggregates; pub mod person_aggregates;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod person_post_aggregates;
#[cfg(feature = "full")]
pub mod post_aggregates; pub mod post_aggregates;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod site_aggregates; pub mod site_aggregates;

View file

@ -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<Self, Error> {
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::<Self>(conn)
}
pub fn read(
conn: &mut PgConnection,
person_id_: PersonId,
post_id_: PostId,
) -> Result<Self, Error> {
use crate::schema::person_post_aggregates::dsl::*;
person_post_aggregates
.filter(post_id.eq(post_id_).and(person_id.eq(person_id_)))
.first::<Self>(conn)
}
}

View file

@ -6,6 +6,7 @@ use crate::schema::{
comment_aggregates, comment_aggregates,
community_aggregates, community_aggregates,
person_aggregates, person_aggregates,
person_post_aggregates,
post_aggregates, post_aggregates,
site_aggregates, site_aggregates,
}; };
@ -76,6 +77,28 @@ pub struct PostAggregates {
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(Queryable, Associations, Identifiable))] #[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<chrono::NaiveDateTime>,
}
#[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(table_name = site_aggregates))]
#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::site::Site)))] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::site::Site)))]
pub struct SiteAggregates { pub struct SiteAggregates {

View file

@ -380,6 +380,16 @@ table! {
} }
} }
table! {
person_post_aggregates (id) {
id -> Int4,
person_id -> Int4,
post_id -> Int4,
read_comments -> Int8,
published -> Timestamp,
}
}
table! { table! {
post_aggregates (id) { post_aggregates (id) {
id -> Int4, id -> Int4,
@ -667,6 +677,8 @@ joinable!(comment_reply -> comment (comment_id));
joinable!(comment_reply -> person (recipient_id)); joinable!(comment_reply -> person (recipient_id));
joinable!(post -> community (community_id)); joinable!(post -> community (community_id));
joinable!(post -> person (creator_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_aggregates -> post (post_id));
joinable!(post_like -> person (person_id)); joinable!(post_like -> person (person_id));
joinable!(post_like -> post (post_id)); joinable!(post_like -> post (post_id));
@ -725,6 +737,7 @@ allow_tables_to_appear_in_same_query!(
person_ban, person_ban,
person_block, person_block,
person_mention, person_mention,
person_post_aggregates,
comment_reply, comment_reply,
post, post,
post_aggregates, post_aggregates,

View file

@ -11,6 +11,7 @@ use lemmy_db_schema::{
local_user_language, local_user_language,
person, person,
person_block, person_block,
person_post_aggregates,
post, post,
post_aggregates, post_aggregates,
post_like, post_like,
@ -43,8 +44,11 @@ type PostViewTuple = (
Option<PostRead>, Option<PostRead>,
Option<PersonBlock>, Option<PersonBlock>,
Option<i16>, Option<i16>,
i64,
); );
sql_function!(fn coalesce(x: sql_types::Nullable<sql_types::BigInt>, y: sql_types::BigInt) -> sql_types::BigInt);
impl PostView { impl PostView {
pub fn read( pub fn read(
conn: &mut PgConnection, conn: &mut PgConnection,
@ -64,6 +68,7 @@ impl PostView {
read, read,
creator_blocked, creator_blocked,
post_like, post_like,
unread_comments,
) = post::table ) = post::table
.find(post_id) .find(post_id)
.inner_join(person::table) .inner_join(person::table)
@ -116,6 +121,13 @@ impl PostView {
.and(post_like::person_id.eq(person_id_join)), .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(( .select((
post::all_columns, post::all_columns,
Person::safe_columns_tuple(), Person::safe_columns_tuple(),
@ -127,6 +139,10 @@ impl PostView {
post_read::all_columns.nullable(), post_read::all_columns.nullable(),
person_block::all_columns.nullable(), person_block::all_columns.nullable(),
post_like::score.nullable(), post_like::score.nullable(),
coalesce(
post_aggregates::comments.nullable() - person_post_aggregates::read_comments.nullable(),
post_aggregates::comments,
),
)) ))
.first::<PostViewTuple>(conn)?; .first::<PostViewTuple>(conn)?;
@ -149,6 +165,7 @@ impl PostView {
read: read.is_some(), read: read.is_some(),
creator_blocked: creator_blocked.is_some(), creator_blocked: creator_blocked.is_some(),
my_vote, my_vote,
unread_comments,
}) })
} }
} }
@ -237,6 +254,13 @@ impl<'a> PostQuery<'a> {
.and(post_like::person_id.eq(person_id_join)), .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( .left_join(
local_user_language::table.on( local_user_language::table.on(
post::language_id post::language_id
@ -255,6 +279,10 @@ impl<'a> PostQuery<'a> {
post_read::all_columns.nullable(), post_read::all_columns.nullable(),
person_block::all_columns.nullable(), person_block::all_columns.nullable(),
post_like::score.nullable(), post_like::score.nullable(),
coalesce(
post_aggregates::comments.nullable() - person_post_aggregates::read_comments.nullable(),
post_aggregates::comments,
),
)) ))
.into_boxed(); .into_boxed();
@ -412,6 +440,7 @@ impl ViewToVec for PostView {
read: a.7.is_some(), read: a.7.is_some(),
creator_blocked: a.8.is_some(), creator_blocked: a.8.is_some(),
my_vote: a.9, my_vote: a.9,
unread_comments: a.10,
}) })
.collect::<Vec<Self>>() .collect::<Vec<Self>>()
} }
@ -806,6 +835,7 @@ mod tests {
language_id: LanguageId(47), language_id: LanguageId(47),
}, },
my_vote: None, my_vote: None,
unread_comments: 0,
creator: PersonSafe { creator: PersonSafe {
id: inserted_person.id, id: inserted_person.id,
name: inserted_person.name.clone(), name: inserted_person.name.clone(),

View file

@ -85,6 +85,7 @@ pub struct PostView {
pub read: bool, // Left join to PostRead pub read: bool, // Left join to PostRead
pub creator_blocked: bool, // Left join to PersonBlock pub creator_blocked: bool, // Left join to PersonBlock
pub my_vote: Option<i16>, // Left join to PostLike pub my_vote: Option<i16>, // Left join to PostLike
pub unread_comments: i64, // Left join to PersonPostAggregates
} }
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]

View file

@ -0,0 +1 @@
drop table person_post_aggregates;

View file

@ -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)
);