From 5b87cd815348e575b3f6c60b4e9d799351fdc741 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sat, 2 Nov 2024 13:04:31 -0400 Subject: [PATCH 1/2] Renaming person_mention to person_comment_mention. --- ...t_mentions.rs => list_comment_mentions.rs} | 14 +-- .../local_user/notifications/mark_all_read.rs | 6 +- .../mark_comment_mention_read.rs | 50 ++++++++ .../notifications/mark_mention_read.rs | 45 ------- .../api/src/local_user/notifications/mod.rs | 4 +- .../local_user/notifications/unread_count.rs | 8 +- crates/api_common/src/build_response.rs | 6 +- crates/api_common/src/person.rs | 18 +-- crates/api_crud/src/comment/create.rs | 17 +-- crates/db_schema/src/impls/mod.rs | 3 +- .../src/impls/person_comment_mention.rs | 83 +++++++++++++ crates/db_schema/src/impls/person_mention.rs | 76 ------------ crates/db_schema/src/newtypes.rs | 10 +- crates/db_schema/src/schema.rs | 39 ++++-- crates/db_schema/src/source/mod.rs | 3 +- ...n_mention.rs => person_comment_mention.rs} | 18 +-- crates/db_views/src/private_message_view.rs | 6 +- .../db_views_actor/src/comment_reply_view.rs | 8 +- crates/db_views_actor/src/lib.rs | 4 +- ...view.rs => person_comment_mention_view.rs} | 113 ++++++++++-------- crates/db_views_actor/src/structs.rs | 29 ++++- crates/routes/src/feeds.rs | 14 +-- crates/utils/src/error.rs | 2 +- .../down.sql | 5 + .../up.sql | 12 ++ src/api_routes_http.rs | 10 +- 26 files changed, 349 insertions(+), 254 deletions(-) rename crates/api/src/local_user/notifications/{list_mentions.rs => list_comment_mentions.rs} (63%) create mode 100644 crates/api/src/local_user/notifications/mark_comment_mention_read.rs delete mode 100644 crates/api/src/local_user/notifications/mark_mention_read.rs create mode 100644 crates/db_schema/src/impls/person_comment_mention.rs delete mode 100644 crates/db_schema/src/impls/person_mention.rs rename crates/db_schema/src/source/{person_mention.rs => person_comment_mention.rs} (63%) rename crates/db_views_actor/src/{person_mention_view.rs => person_comment_mention_view.rs} (81%) create mode 100644 migrations/2024-11-02-161125_add_post_body_mention/down.sql create mode 100644 migrations/2024-11-02-161125_add_post_body_mention/up.sql diff --git a/crates/api/src/local_user/notifications/list_mentions.rs b/crates/api/src/local_user/notifications/list_comment_mentions.rs similarity index 63% rename from crates/api/src/local_user/notifications/list_mentions.rs rename to crates/api/src/local_user/notifications/list_comment_mentions.rs index bf3cd8e0d..4617844ba 100644 --- a/crates/api/src/local_user/notifications/list_mentions.rs +++ b/crates/api/src/local_user/notifications/list_comment_mentions.rs @@ -1,18 +1,18 @@ use actix_web::web::{Data, Json, Query}; use lemmy_api_common::{ context::LemmyContext, - person::{GetPersonMentions, GetPersonMentionsResponse}, + person::{GetPersonCommentMentions, GetPersonCommentMentionsResponse}, }; use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::person_mention_view::PersonMentionQuery; +use lemmy_db_views_actor::person_comment_mention_view::PersonCommentMentionQuery; use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] -pub async fn list_mentions( - data: Query, +pub async fn list_comment_mentions( + data: Query, context: Data, local_user_view: LocalUserView, -) -> LemmyResult> { +) -> LemmyResult> { let sort = data.sort; let page = data.page; let limit = data.limit; @@ -20,7 +20,7 @@ pub async fn list_mentions( let person_id = Some(local_user_view.person.id); let show_bot_accounts = local_user_view.local_user.show_bot_accounts; - let mentions = PersonMentionQuery { + let comment_mentions = PersonCommentMentionQuery { recipient_id: person_id, my_person_id: person_id, sort, @@ -32,5 +32,5 @@ pub async fn list_mentions( .list(&mut context.pool()) .await?; - Ok(Json(GetPersonMentionsResponse { mentions })) + Ok(Json(GetPersonCommentMentionsResponse { comment_mentions })) } diff --git a/crates/api/src/local_user/notifications/mark_all_read.rs b/crates/api/src/local_user/notifications/mark_all_read.rs index 558d276f7..929af5ac3 100644 --- a/crates/api/src/local_user/notifications/mark_all_read.rs +++ b/crates/api/src/local_user/notifications/mark_all_read.rs @@ -2,7 +2,7 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{context::LemmyContext, person::GetRepliesResponse}; use lemmy_db_schema::source::{ comment_reply::CommentReply, - person_mention::PersonMention, + person_comment_mention::PersonCommentMention, private_message::PrivateMessage, }; use lemmy_db_views::structs::LocalUserView; @@ -20,8 +20,8 @@ pub async fn mark_all_notifications_read( .await .with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?; - // Mark all user mentions as read - PersonMention::mark_all_as_read(&mut context.pool(), person_id) + // Mark all comment mentions as read + PersonCommentMention::mark_all_as_read(&mut context.pool(), person_id) .await .with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?; diff --git a/crates/api/src/local_user/notifications/mark_comment_mention_read.rs b/crates/api/src/local_user/notifications/mark_comment_mention_read.rs new file mode 100644 index 000000000..4a9c40092 --- /dev/null +++ b/crates/api/src/local_user/notifications/mark_comment_mention_read.rs @@ -0,0 +1,50 @@ +use actix_web::web::{Data, Json}; +use lemmy_api_common::{ + context::LemmyContext, + person::{MarkPersonCommentMentionAsRead, PersonCommentMentionResponse}, +}; +use lemmy_db_schema::{ + source::person_comment_mention::{PersonCommentMention, PersonCommentMentionUpdateForm}, + traits::Crud, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::PersonCommentMentionView; +use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; + +#[tracing::instrument(skip(context))] +pub async fn mark_comment_mention_as_read( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let person_comment_mention_id = data.person_comment_mention_id; + let read_person_comment_mention = + PersonCommentMention::read(&mut context.pool(), person_comment_mention_id).await?; + + if local_user_view.person.id != read_person_comment_mention.recipient_id { + Err(LemmyErrorType::CouldntUpdateComment)? + } + + let person_comment_mention_id = read_person_comment_mention.id; + let read = Some(data.read); + PersonCommentMention::update( + &mut context.pool(), + person_comment_mention_id, + &PersonCommentMentionUpdateForm { read }, + ) + .await + .with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?; + + let person_comment_mention_id = read_person_comment_mention.id; + let person_id = local_user_view.person.id; + let person_comment_mention_view = PersonCommentMentionView::read( + &mut context.pool(), + person_comment_mention_id, + Some(person_id), + ) + .await?; + + Ok(Json(PersonCommentMentionResponse { + person_comment_mention_view, + })) +} diff --git a/crates/api/src/local_user/notifications/mark_mention_read.rs b/crates/api/src/local_user/notifications/mark_mention_read.rs deleted file mode 100644 index 9a839b2b4..000000000 --- a/crates/api/src/local_user/notifications/mark_mention_read.rs +++ /dev/null @@ -1,45 +0,0 @@ -use actix_web::web::{Data, Json}; -use lemmy_api_common::{ - context::LemmyContext, - person::{MarkPersonMentionAsRead, PersonMentionResponse}, -}; -use lemmy_db_schema::{ - source::person_mention::{PersonMention, PersonMentionUpdateForm}, - traits::Crud, -}; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::PersonMentionView; -use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; - -#[tracing::instrument(skip(context))] -pub async fn mark_person_mention_as_read( - data: Json, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - let person_mention_id = data.person_mention_id; - let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id).await?; - - if local_user_view.person.id != read_person_mention.recipient_id { - Err(LemmyErrorType::CouldntUpdateComment)? - } - - let person_mention_id = read_person_mention.id; - let read = Some(data.read); - PersonMention::update( - &mut context.pool(), - person_mention_id, - &PersonMentionUpdateForm { read }, - ) - .await - .with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?; - - let person_mention_id = read_person_mention.id; - let person_id = local_user_view.person.id; - let person_mention_view = - PersonMentionView::read(&mut context.pool(), person_mention_id, Some(person_id)).await?; - - Ok(Json(PersonMentionResponse { - person_mention_view, - })) -} diff --git a/crates/api/src/local_user/notifications/mod.rs b/crates/api/src/local_user/notifications/mod.rs index 35567afde..a3580b930 100644 --- a/crates/api/src/local_user/notifications/mod.rs +++ b/crates/api/src/local_user/notifications/mod.rs @@ -1,6 +1,6 @@ -pub mod list_mentions; +pub mod list_comment_mentions; pub mod list_replies; pub mod mark_all_read; -pub mod mark_mention_read; +pub mod mark_comment_mention_read; pub mod mark_reply_read; pub mod unread_count; diff --git a/crates/api/src/local_user/notifications/unread_count.rs b/crates/api/src/local_user/notifications/unread_count.rs index 4c6c65263..90891f21e 100644 --- a/crates/api/src/local_user/notifications/unread_count.rs +++ b/crates/api/src/local_user/notifications/unread_count.rs @@ -1,7 +1,7 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{context::LemmyContext, person::GetUnreadCountResponse}; use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; -use lemmy_db_views_actor::structs::{CommentReplyView, PersonMentionView}; +use lemmy_db_views_actor::structs::{CommentReplyView, PersonCommentMentionView}; use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] @@ -12,14 +12,14 @@ pub async fn unread_count( let person_id = local_user_view.person.id; let replies = - CommentReplyView::get_unread_replies(&mut context.pool(), &local_user_view.local_user).await?; + CommentReplyView::get_unread_count(&mut context.pool(), &local_user_view.local_user).await?; let mentions = - PersonMentionView::get_unread_mentions(&mut context.pool(), &local_user_view.local_user) + PersonCommentMentionView::get_unread_count(&mut context.pool(), &local_user_view.local_user) .await?; let private_messages = - PrivateMessageView::get_unread_messages(&mut context.pool(), person_id).await?; + PrivateMessageView::get_unread_count(&mut context.pool(), person_id).await?; Ok(Json(GetUnreadCountResponse { replies, diff --git a/crates/api_common/src/build_response.rs b/crates/api_common/src/build_response.rs index d40f4c23d..8e79867f1 100644 --- a/crates/api_common/src/build_response.rs +++ b/crates/api_common/src/build_response.rs @@ -18,7 +18,7 @@ use lemmy_db_schema::{ comment::Comment, comment_reply::{CommentReply, CommentReplyInsertForm}, person::Person, - person_mention::{PersonMention, PersonMentionInsertForm}, + person_comment_mention::{PersonCommentMention, PersonCommentMentionInsertForm}, }, traits::Crud, }; @@ -127,7 +127,7 @@ pub async fn send_local_notifs( // below by checking recipient ids recipient_ids.push(mention_user_view.local_user.id); - let user_mention_form = PersonMentionInsertForm { + let person_comment_mention_form = PersonCommentMentionInsertForm { recipient_id: mention_user_view.person.id, comment_id, read: None, @@ -135,7 +135,7 @@ pub async fn send_local_notifs( // Allow this to fail softly, since comment edits might re-update or replace it // Let the uniqueness handle this fail - PersonMention::create(&mut context.pool(), &user_mention_form) + PersonCommentMention::create(&mut context.pool(), &person_comment_mention_form) .await .ok(); diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index 6f1ddfe43..a4020f57e 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -1,5 +1,5 @@ use lemmy_db_schema::{ - newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId}, + newtypes::{CommentReplyId, CommunityId, LanguageId, PersonCommentMentionId, PersonId}, sensitive::SensitiveString, source::{login_token::LoginToken, site::Site}, CommentSortType, @@ -11,7 +11,7 @@ use lemmy_db_views::structs::{CommentView, LocalImageView, PostView}; use lemmy_db_views_actor::structs::{ CommentReplyView, CommunityModeratorView, - PersonMentionView, + PersonCommentMentionView, PersonView, }; use serde::{Deserialize, Serialize}; @@ -293,7 +293,7 @@ pub struct GetRepliesResponse { #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// Get mentions for your user. -pub struct GetPersonMentions { +pub struct GetPersonCommentMentions { pub sort: Option, pub page: Option, pub limit: Option, @@ -304,16 +304,16 @@ pub struct GetPersonMentions { #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// The response of mentions for your user. -pub struct GetPersonMentionsResponse { - pub mentions: Vec, +pub struct GetPersonCommentMentionsResponse { + pub comment_mentions: Vec, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// Mark a person mention as read. -pub struct MarkPersonMentionAsRead { - pub person_mention_id: PersonMentionId, +pub struct MarkPersonCommentMentionAsRead { + pub person_comment_mention_id: PersonCommentMentionId, pub read: bool, } @@ -321,8 +321,8 @@ pub struct MarkPersonMentionAsRead { #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// The response for a person mention action. -pub struct PersonMentionResponse { - pub person_mention_view: PersonMentionView, +pub struct PersonCommentMentionResponse { + pub person_comment_mention_view: PersonCommentMentionView, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index 2f67fa7e7..1347918c2 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -22,7 +22,7 @@ use lemmy_db_schema::{ comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm}, comment_reply::{CommentReply, CommentReplyUpdateForm}, local_site::LocalSite, - person_mention::{PersonMention, PersonMentionUpdateForm}, + person_comment_mention::{PersonCommentMention, PersonCommentMentionUpdateForm}, }, traits::{Crud, Likeable}, }; @@ -174,17 +174,18 @@ pub async fn create_comment( .with_lemmy_type(LemmyErrorType::CouldntUpdateReplies)?; } - // If the parent has PersonMentions mark them as read too - let person_mention = - PersonMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await; - if let Ok(Some(mention)) = person_mention { - PersonMention::update( + // If the parent has PersonCommentMentions mark them as read too + let person_comment_mention = + PersonCommentMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id) + .await; + if let Ok(Some(mention)) = person_comment_mention { + PersonCommentMention::update( &mut context.pool(), mention.id, - &PersonMentionUpdateForm { read: Some(true) }, + &PersonCommentMentionUpdateForm { read: Some(true) }, ) .await - .with_lemmy_type(LemmyErrorType::CouldntUpdatePersonMentions)?; + .with_lemmy_type(LemmyErrorType::CouldntUpdatePersonCommentMentions)?; } } diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index f115a101f..50cb6b6df 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -27,7 +27,8 @@ pub mod oauth_provider; pub mod password_reset_request; pub mod person; pub mod person_block; -pub mod person_mention; +pub mod person_comment_mention; +pub mod person_post_mention; pub mod post; pub mod post_report; pub mod private_message; diff --git a/crates/db_schema/src/impls/person_comment_mention.rs b/crates/db_schema/src/impls/person_comment_mention.rs new file mode 100644 index 000000000..2cc303396 --- /dev/null +++ b/crates/db_schema/src/impls/person_comment_mention.rs @@ -0,0 +1,83 @@ +use crate::{ + diesel::OptionalExtension, + newtypes::{CommentId, PersonCommentMentionId, PersonId}, + schema::person_comment_mention, + source::person_comment_mention::{ + PersonCommentMention, + PersonCommentMentionInsertForm, + PersonCommentMentionUpdateForm, + }, + traits::Crud, + utils::{get_conn, DbPool}, +}; +use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; + +#[async_trait] +impl Crud for PersonCommentMention { + type InsertForm = PersonCommentMentionInsertForm; + type UpdateForm = PersonCommentMentionUpdateForm; + type IdType = PersonCommentMentionId; + + async fn create( + pool: &mut DbPool<'_>, + person_comment_mention_form: &Self::InsertForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + // since the return here isnt utilized, we dont need to do an update + // but get_result doesn't return the existing row here + insert_into(person_comment_mention::table) + .values(person_comment_mention_form) + .on_conflict(( + person_comment_mention::recipient_id, + person_comment_mention::comment_id, + )) + .do_update() + .set(person_comment_mention_form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + person_comment_mention_id: PersonCommentMentionId, + person_comment_mention_form: &Self::UpdateForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::update(person_comment_mention::table.find(person_comment_mention_id)) + .set(person_comment_mention_form) + .get_result::(conn) + .await + } +} + +impl PersonCommentMention { + pub async fn mark_all_as_read( + pool: &mut DbPool<'_>, + for_recipient_id: PersonId, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + diesel::update( + person_comment_mention::table + .filter(person_comment_mention::recipient_id.eq(for_recipient_id)) + .filter(person_comment_mention::read.eq(false)), + ) + .set(person_comment_mention::read.eq(true)) + .get_results::(conn) + .await + } + + pub async fn read_by_comment_and_person( + pool: &mut DbPool<'_>, + for_comment_id: CommentId, + for_recipient_id: PersonId, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + person_comment_mention::table + .filter(person_comment_mention::comment_id.eq(for_comment_id)) + .filter(person_comment_mention::recipient_id.eq(for_recipient_id)) + .first(conn) + .await + .optional() + } +} diff --git a/crates/db_schema/src/impls/person_mention.rs b/crates/db_schema/src/impls/person_mention.rs deleted file mode 100644 index 433176683..000000000 --- a/crates/db_schema/src/impls/person_mention.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::{ - diesel::OptionalExtension, - newtypes::{CommentId, PersonId, PersonMentionId}, - schema::person_mention, - source::person_mention::{PersonMention, PersonMentionInsertForm, PersonMentionUpdateForm}, - traits::Crud, - utils::{get_conn, DbPool}, -}; -use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; - -#[async_trait] -impl Crud for PersonMention { - type InsertForm = PersonMentionInsertForm; - type UpdateForm = PersonMentionUpdateForm; - type IdType = PersonMentionId; - - async fn create( - pool: &mut DbPool<'_>, - person_mention_form: &Self::InsertForm, - ) -> Result { - let conn = &mut get_conn(pool).await?; - // since the return here isnt utilized, we dont need to do an update - // but get_result doesn't return the existing row here - insert_into(person_mention::table) - .values(person_mention_form) - .on_conflict((person_mention::recipient_id, person_mention::comment_id)) - .do_update() - .set(person_mention_form) - .get_result::(conn) - .await - } - - async fn update( - pool: &mut DbPool<'_>, - person_mention_id: PersonMentionId, - person_mention_form: &Self::UpdateForm, - ) -> Result { - let conn = &mut get_conn(pool).await?; - diesel::update(person_mention::table.find(person_mention_id)) - .set(person_mention_form) - .get_result::(conn) - .await - } -} - -impl PersonMention { - pub async fn mark_all_as_read( - pool: &mut DbPool<'_>, - for_recipient_id: PersonId, - ) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - diesel::update( - person_mention::table - .filter(person_mention::recipient_id.eq(for_recipient_id)) - .filter(person_mention::read.eq(false)), - ) - .set(person_mention::read.eq(true)) - .get_results::(conn) - .await - } - - pub async fn read_by_comment_and_person( - pool: &mut DbPool<'_>, - for_comment_id: CommentId, - for_recipient_id: PersonId, - ) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - person_mention::table - .filter(person_mention::comment_id.eq(for_comment_id)) - .filter(person_mention::recipient_id.eq(for_recipient_id)) - .first(conn) - .await - .optional() - } -} diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index fe1febef5..2041b0c48 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -82,8 +82,14 @@ impl fmt::Display for PrivateMessageId { #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] -/// The person mention id. -pub struct PersonMentionId(i32); +/// The person comment mention id. +pub struct PersonCommentMentionId(i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The person post mention id. +pub struct PersonPostMentionId(i32); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 9f1d00568..6dc863e48 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -719,16 +719,7 @@ diesel::table! { } diesel::table! { - person_follower (follower_id, person_id) { - person_id -> Int4, - follower_id -> Int4, - published -> Timestamptz, - pending -> Bool, - } -} - -diesel::table! { - person_mention (id) { + person_comment_mention (id) { id -> Int4, recipient_id -> Int4, comment_id -> Int4, @@ -737,6 +728,15 @@ diesel::table! { } } +diesel::table! { + person_follower (follower_id, person_id) { + person_id -> Int4, + follower_id -> Int4, + published -> Timestamptz, + pending -> Bool, + } +} + diesel::table! { person_post_aggregates (person_id, post_id) { person_id -> Int4, @@ -746,6 +746,16 @@ diesel::table! { } } +diesel::table! { + person_post_mention (id) { + id -> Int4, + recipient_id -> Int4, + post_id -> Int4, + read -> Bool, + published -> Timestamp, + } +} + diesel::table! { post (id) { id -> Int4, @@ -1049,10 +1059,12 @@ diesel::joinable!(password_reset_request -> local_user (local_user_id)); diesel::joinable!(person -> instance (instance_id)); diesel::joinable!(person_aggregates -> person (person_id)); diesel::joinable!(person_ban -> person (person_id)); -diesel::joinable!(person_mention -> comment (comment_id)); -diesel::joinable!(person_mention -> person (recipient_id)); +diesel::joinable!(person_comment_mention -> comment (comment_id)); +diesel::joinable!(person_comment_mention -> person (recipient_id)); diesel::joinable!(person_post_aggregates -> person (person_id)); diesel::joinable!(person_post_aggregates -> post (post_id)); +diesel::joinable!(person_post_mention -> person (recipient_id)); +diesel::joinable!(person_post_mention -> post (post_id)); diesel::joinable!(post -> community (community_id)); diesel::joinable!(post -> language (language_id)); diesel::joinable!(post -> person (creator_id)); @@ -1132,9 +1144,10 @@ diesel::allow_tables_to_appear_in_same_query!( person_aggregates, person_ban, person_block, + person_comment_mention, person_follower, - person_mention, person_post_aggregates, + person_post_mention, post, post_aggregates, post_hide, diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index 377c1aaef..3dac8a4f3 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -32,7 +32,8 @@ pub mod oauth_provider; pub mod password_reset_request; pub mod person; pub mod person_block; -pub mod person_mention; +pub mod person_comment_mention; +pub mod person_post_mention; pub mod post; pub mod post_report; pub mod private_message; diff --git a/crates/db_schema/src/source/person_mention.rs b/crates/db_schema/src/source/person_comment_mention.rs similarity index 63% rename from crates/db_schema/src/source/person_mention.rs rename to crates/db_schema/src/source/person_comment_mention.rs index 9c3005655..bd70af307 100644 --- a/crates/db_schema/src/source/person_mention.rs +++ b/crates/db_schema/src/source/person_comment_mention.rs @@ -1,6 +1,6 @@ -use crate::newtypes::{CommentId, PersonId, PersonMentionId}; +use crate::newtypes::{CommentId, PersonCommentMentionId, PersonId}; #[cfg(feature = "full")] -use crate::schema::person_mention; +use crate::schema::person_comment_mention; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; #[cfg(feature = "full")] @@ -12,12 +12,12 @@ use ts_rs::TS; derive(Queryable, Selectable, Associations, Identifiable, TS) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::comment::Comment)))] -#[cfg_attr(feature = "full", diesel(table_name = person_mention))] +#[cfg_attr(feature = "full", diesel(table_name = person_comment_mention))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] /// A person mention. -pub struct PersonMention { - pub id: PersonMentionId, +pub struct PersonCommentMention { + pub id: PersonCommentMentionId, pub recipient_id: PersonId, pub comment_id: CommentId, pub read: bool, @@ -25,15 +25,15 @@ pub struct PersonMention { } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = person_mention))] -pub struct PersonMentionInsertForm { +#[cfg_attr(feature = "full", diesel(table_name = person_comment_mention))] +pub struct PersonCommentMentionInsertForm { pub recipient_id: PersonId, pub comment_id: CommentId, pub read: Option, } #[cfg_attr(feature = "full", derive(AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = person_mention))] -pub struct PersonMentionUpdateForm { +#[cfg_attr(feature = "full", diesel(table_name = person_comment_mention))] +pub struct PersonCommentMentionUpdateForm { pub read: Option, } diff --git a/crates/db_views/src/private_message_view.rs b/crates/db_views/src/private_message_view.rs index 0fbc0ee16..f0bf45c3a 100644 --- a/crates/db_views/src/private_message_view.rs +++ b/crates/db_views/src/private_message_view.rs @@ -118,7 +118,7 @@ impl PrivateMessageView { } /// Gets the number of unread messages - pub async fn get_unread_messages( + pub async fn get_unread_count( pool: &mut DbPool<'_>, my_person_id: PersonId, ) -> Result { @@ -356,7 +356,7 @@ mod tests { assert_length!(1, &timmy_messages); - let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id).await?; + let timmy_unread_messages = PrivateMessageView::get_unread_count(pool, timmy.id).await?; assert_eq!(timmy_unread_messages, 1); cleanup(instance.id, pool).await @@ -398,7 +398,7 @@ mod tests { assert_length!(0, &timmy_messages); - let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id).await?; + let timmy_unread_messages = PrivateMessageView::get_unread_count(pool, timmy.id).await?; assert_eq!(timmy_unread_messages, 0); cleanup(instance.id, pool).await } diff --git a/crates/db_views_actor/src/comment_reply_view.rs b/crates/db_views_actor/src/comment_reply_view.rs index 1b657866a..22d05521b 100644 --- a/crates/db_views_actor/src/comment_reply_view.rs +++ b/crates/db_views_actor/src/comment_reply_view.rs @@ -247,7 +247,7 @@ impl CommentReplyView { } /// Gets the number of unread replies - pub async fn get_unread_replies( + pub async fn get_unread_count( pool: &mut DbPool<'_>, local_user: &LocalUser, ) -> Result { @@ -389,7 +389,7 @@ mod tests { CommentReply::update(pool, inserted_reply.id, &comment_reply_update_form).await?; // Test to make sure counts and blocks work correctly - let unread_replies = CommentReplyView::get_unread_replies(pool, &recipient_local_user).await?; + let unread_replies = CommentReplyView::get_unread_count(pool, &recipient_local_user).await?; let query = CommentReplyQuery { recipient_id: Some(recipient_id), @@ -412,7 +412,7 @@ mod tests { PersonBlock::block(pool, &block_form).await?; let unread_replies_after_block = - CommentReplyView::get_unread_replies(pool, &recipient_local_user).await?; + CommentReplyView::get_unread_count(pool, &recipient_local_user).await?; let replies_after_block = query.clone().list(pool).await?; assert_eq!(0, unread_replies_after_block); assert_eq!(0, replies_after_block.len()); @@ -440,7 +440,7 @@ mod tests { let recipient_local_user_view = LocalUserView::read(pool, recipient_local_user.id).await?; let unread_replies_after_hide_bots = - CommentReplyView::get_unread_replies(pool, &recipient_local_user_view.local_user).await?; + CommentReplyView::get_unread_count(pool, &recipient_local_user_view.local_user).await?; let mut query_without_bots = query.clone(); query_without_bots.show_bot_accounts = false; diff --git a/crates/db_views_actor/src/lib.rs b/crates/db_views_actor/src/lib.rs index 2ec9652e3..7e1b96757 100644 --- a/crates/db_views_actor/src/lib.rs +++ b/crates/db_views_actor/src/lib.rs @@ -9,7 +9,9 @@ pub mod community_person_ban_view; #[cfg(feature = "full")] pub mod community_view; #[cfg(feature = "full")] -pub mod person_mention_view; +pub mod person_comment_mention_view; +// #[cfg(feature = "full")] +// pub mod person_post_mention_view; #[cfg(feature = "full")] pub mod person_view; pub mod structs; diff --git a/crates/db_views_actor/src/person_mention_view.rs b/crates/db_views_actor/src/person_comment_mention_view.rs similarity index 81% rename from crates/db_views_actor/src/person_mention_view.rs rename to crates/db_views_actor/src/person_comment_mention_view.rs index 2478c0183..9a61ff0d9 100644 --- a/crates/db_views_actor/src/person_mention_view.rs +++ b/crates/db_views_actor/src/person_comment_mention_view.rs @@ -1,4 +1,4 @@ -use crate::structs::PersonMentionView; +use crate::structs::PersonCommentMentionView; use diesel::{ dsl::{exists, not}, pg::Pg, @@ -15,7 +15,7 @@ use diesel::{ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases, - newtypes::{PersonId, PersonMentionId}, + newtypes::{PersonCommentMentionId, PersonId}, schema::{ comment, comment_aggregates, @@ -28,7 +28,7 @@ use lemmy_db_schema::{ local_user, person, person_block, - person_mention, + person_comment_mention, post, }, source::local_user::LocalUser, @@ -37,8 +37,8 @@ use lemmy_db_schema::{ }; fn queries<'a>() -> Queries< - impl ReadFn<'a, PersonMentionView, (PersonMentionId, Option)>, - impl ListFn<'a, PersonMentionView, PersonMentionQuery>, + impl ReadFn<'a, PersonCommentMentionView, (PersonCommentMentionId, Option)>, + impl ListFn<'a, PersonCommentMentionView, PersonCommentMentionQuery>, > { let is_creator_banned_from_community = exists( community_person_ban::table.filter( @@ -116,7 +116,7 @@ fn queries<'a>() -> Queries< ), ); - let all_joins = move |query: person_mention::BoxedQuery<'a, Pg>, + let all_joins = move |query: person_comment_mention::BoxedQuery<'a, Pg>, my_person_id: Option| { let is_local_user_banned_from_community_selection: Box< dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>, @@ -163,7 +163,7 @@ fn queries<'a>() -> Queries< .inner_join(aliases::person1) .inner_join(comment_aggregates::table.on(comment::id.eq(comment_aggregates::comment_id))) .select(( - person_mention::all_columns, + person_comment_mention::all_columns, comment::all_columns, person::all_columns, post::all_columns, @@ -181,28 +181,35 @@ fn queries<'a>() -> Queries< )) }; - let read = - move |mut conn: DbConn<'a>, - (person_mention_id, my_person_id): (PersonMentionId, Option)| async move { - all_joins( - person_mention::table.find(person_mention_id).into_boxed(), - my_person_id, - ) - .first(&mut conn) - .await - }; + let read = move |mut conn: DbConn<'a>, + (person_comment_mention_id, my_person_id): ( + PersonCommentMentionId, + Option, + )| async move { + all_joins( + person_comment_mention::table + .find(person_comment_mention_id) + .into_boxed(), + my_person_id, + ) + .first(&mut conn) + .await + }; - let list = move |mut conn: DbConn<'a>, options: PersonMentionQuery| async move { + let list = move |mut conn: DbConn<'a>, options: PersonCommentMentionQuery| async move { // These filters need to be kept in sync with the filters in - // PersonMentionView::get_unread_mentions() - let mut query = all_joins(person_mention::table.into_boxed(), options.my_person_id); + // PersonCommentMentionView::get_unread_mentions() + let mut query = all_joins( + person_comment_mention::table.into_boxed(), + options.my_person_id, + ); if let Some(recipient_id) = options.recipient_id { - query = query.filter(person_mention::recipient_id.eq(recipient_id)); + query = query.filter(person_comment_mention::recipient_id.eq(recipient_id)); } if options.unread_only { - query = query.filter(person_mention::read.eq(false)); + query = query.filter(person_comment_mention::read.eq(false)); } if !options.show_bot_accounts { @@ -229,33 +236,33 @@ fn queries<'a>() -> Queries< query .limit(limit) .offset(offset) - .load::(&mut conn) + .load::(&mut conn) .await }; Queries::new(read, list) } -impl PersonMentionView { +impl PersonCommentMentionView { pub async fn read( pool: &mut DbPool<'_>, - person_mention_id: PersonMentionId, + person_comment_mention_id: PersonCommentMentionId, my_person_id: Option, ) -> Result { queries() - .read(pool, (person_mention_id, my_person_id)) + .read(pool, (person_comment_mention_id, my_person_id)) .await } /// Gets the number of unread mentions - pub async fn get_unread_mentions( + pub async fn get_unread_count( pool: &mut DbPool<'_>, local_user: &LocalUser, ) -> Result { use diesel::dsl::count; let conn = &mut get_conn(pool).await?; - let mut query = person_mention::table + let mut query = person_comment_mention::table .inner_join(comment::table) .left_join( person_block::table.on( @@ -275,18 +282,18 @@ impl PersonMentionView { query // Don't count replies from blocked users .filter(person_block::person_id.is_null()) - .filter(person_mention::recipient_id.eq(local_user.person_id)) - .filter(person_mention::read.eq(false)) + .filter(person_comment_mention::recipient_id.eq(local_user.person_id)) + .filter(person_comment_mention::read.eq(false)) .filter(comment::deleted.eq(false)) .filter(comment::removed.eq(false)) - .select(count(person_mention::id)) + .select(count(person_comment_mention::id)) .first::(conn) .await } } #[derive(Default, Clone)] -pub struct PersonMentionQuery { +pub struct PersonCommentMentionQuery { pub my_person_id: Option, pub recipient_id: Option, pub sort: Option, @@ -296,8 +303,8 @@ pub struct PersonMentionQuery { pub limit: Option, } -impl PersonMentionQuery { - pub async fn list(self, pool: &mut DbPool<'_>) -> Result, Error> { +impl PersonCommentMentionQuery { + pub async fn list(self, pool: &mut DbPool<'_>) -> Result, Error> { queries().list(pool, self).await } } @@ -305,7 +312,10 @@ impl PersonMentionQuery { #[cfg(test)] mod tests { - use crate::{person_mention_view::PersonMentionQuery, structs::PersonMentionView}; + use crate::{ + person_comment_mention_view::PersonCommentMentionQuery, + structs::PersonCommentMentionView, + }; use lemmy_db_schema::{ source::{ comment::{Comment, CommentInsertForm}, @@ -314,7 +324,11 @@ mod tests { local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, person::{Person, PersonInsertForm, PersonUpdateForm}, person_block::{PersonBlock, PersonBlockForm}, - person_mention::{PersonMention, PersonMentionInsertForm, PersonMentionUpdateForm}, + person_comment_mention::{ + PersonCommentMention, + PersonCommentMentionInsertForm, + PersonCommentMentionUpdateForm, + }, post::{Post, PostInsertForm}, }, traits::{Blockable, Crud}, @@ -367,15 +381,15 @@ mod tests { ); let inserted_comment = Comment::create(pool, &comment_form, None).await?; - let person_mention_form = PersonMentionInsertForm { + let person_comment_mention_form = PersonCommentMentionInsertForm { recipient_id: inserted_recipient.id, comment_id: inserted_comment.id, read: None, }; - let inserted_mention = PersonMention::create(pool, &person_mention_form).await?; + let inserted_mention = PersonCommentMention::create(pool, &person_comment_mention_form).await?; - let expected_mention = PersonMention { + let expected_mention = PersonCommentMention { id: inserted_mention.id, recipient_id: inserted_mention.recipient_id, comment_id: inserted_mention.comment_id, @@ -383,17 +397,21 @@ mod tests { published: inserted_mention.published, }; - let read_mention = PersonMention::read(pool, inserted_mention.id).await?; + let read_mention = PersonCommentMention::read(pool, inserted_mention.id).await?; - let person_mention_update_form = PersonMentionUpdateForm { read: Some(false) }; - let updated_mention = - PersonMention::update(pool, inserted_mention.id, &person_mention_update_form).await?; + let person_comment_mention_update_form = PersonCommentMentionUpdateForm { read: Some(false) }; + let updated_mention = PersonCommentMention::update( + pool, + inserted_mention.id, + &person_comment_mention_update_form, + ) + .await?; // Test to make sure counts and blocks work correctly let unread_mentions = - PersonMentionView::get_unread_mentions(pool, &recipient_local_user).await?; + PersonCommentMentionView::get_unread_count(pool, &recipient_local_user).await?; - let query = PersonMentionQuery { + let query = PersonCommentMentionQuery { recipient_id: Some(recipient_id), my_person_id: Some(recipient_id), sort: None, @@ -414,7 +432,7 @@ mod tests { PersonBlock::block(pool, &block_form).await?; let unread_mentions_after_block = - PersonMentionView::get_unread_mentions(pool, &recipient_local_user).await?; + PersonCommentMentionView::get_unread_count(pool, &recipient_local_user).await?; let mentions_after_block = query.clone().list(pool).await?; assert_eq!(0, unread_mentions_after_block); assert_eq!(0, mentions_after_block.len()); @@ -442,7 +460,8 @@ mod tests { let recipient_local_user_view = LocalUserView::read(pool, recipient_local_user.id).await?; let unread_mentions_after_hide_bots = - PersonMentionView::get_unread_mentions(pool, &recipient_local_user_view.local_user).await?; + PersonCommentMentionView::get_unread_count(pool, &recipient_local_user_view.local_user) + .await?; let mut query_without_bots = query.clone(); query_without_bots.show_bot_accounts = false; diff --git a/crates/db_views_actor/src/structs.rs b/crates/db_views_actor/src/structs.rs index ecf9ba11d..96013a8c7 100644 --- a/crates/db_views_actor/src/structs.rs +++ b/crates/db_views_actor/src/structs.rs @@ -7,7 +7,7 @@ use lemmy_db_schema::{ comment_reply::CommentReply, community::Community, person::Person, - person_mention::PersonMention, + person_comment_mention::PersonCommentMention, post::Post, }, SubscribedType, @@ -94,8 +94,8 @@ pub enum CommunitySortType { #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] /// A person mention view. -pub struct PersonMentionView { - pub person_mention: PersonMention, +pub struct PersonCommentMentionView { + pub person_comment_mention: PersonCommentMention, pub comment: Comment, pub creator: Person, pub post: Post, @@ -112,6 +112,29 @@ pub struct PersonMentionView { pub my_vote: Option, } +// #[skip_serializing_none] +// #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +// #[cfg_attr(feature = "full", derive(TS, Queryable))] +// #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +// #[cfg_attr(feature = "full", ts(export))] +// /// A person mention view. +// pub struct PersonPostMentionView { +// pub person_post_mention: PersonPostMention, +// pub creator: Person, +// pub post: Post, +// pub community: Community, +// pub recipient: Person, +// pub counts: CommentAggregates, +// pub creator_banned_from_community: bool, +// pub banned_from_community: bool, +// pub creator_is_moderator: bool, +// pub creator_is_admin: bool, +// pub subscribed: SubscribedType, +// pub saved: bool, +// pub creator_blocked: bool, +// pub my_vote: Option, +// } + #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 00518032d..5aa55663f 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -17,8 +17,8 @@ use lemmy_db_views::{ }; use lemmy_db_views_actor::{ comment_reply_view::CommentReplyQuery, - person_mention_view::PersonMentionQuery, - structs::{CommentReplyView, PersonMentionView}, + person_comment_mention_view::PersonCommentMentionQuery, + structs::{CommentReplyView, PersonCommentMentionView}, }; use lemmy_utils::{ cache_header::cache_1hour, @@ -395,7 +395,7 @@ async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> LemmyResult LemmyResult LemmyResult, - mentions: Vec, + comment_mentions: Vec, protocol_and_hostname: &str, ) -> LemmyResult> { let mut reply_items: Vec = replies @@ -444,7 +444,7 @@ fn create_reply_and_mention_items( }) .collect::>>()?; - let mut mention_items: Vec = mentions + let mut comment_mention_items: Vec = comment_mentions .iter() .map(|m| { let mention_url = format!("{}/comment/{}", protocol_and_hostname, m.comment.id); @@ -458,7 +458,7 @@ fn create_reply_and_mention_items( }) .collect::>>()?; - reply_items.append(&mut mention_items); + reply_items.append(&mut comment_mention_items); Ok(reply_items) } diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index c95af03e2..4e5b589bb 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -105,7 +105,7 @@ pub enum LemmyErrorType { CouldntHidePost, CouldntUpdateCommunity, CouldntUpdateReplies, - CouldntUpdatePersonMentions, + CouldntUpdatePersonCommentMentions, CouldntCreatePost, CouldntCreatePrivateMessage, CouldntUpdatePrivate, diff --git a/migrations/2024-11-02-161125_add_post_body_mention/down.sql b/migrations/2024-11-02-161125_add_post_body_mention/down.sql new file mode 100644 index 000000000..37bb14e3c --- /dev/null +++ b/migrations/2024-11-02-161125_add_post_body_mention/down.sql @@ -0,0 +1,5 @@ +-- Rename the person_mention table to person_comment_mention +ALTER TABLE person_comment_mention RENAME TO person_mention; + +-- Drop the new table +DROP TABLE person_post_mention; diff --git a/migrations/2024-11-02-161125_add_post_body_mention/up.sql b/migrations/2024-11-02-161125_add_post_body_mention/up.sql new file mode 100644 index 000000000..dffecf9c2 --- /dev/null +++ b/migrations/2024-11-02-161125_add_post_body_mention/up.sql @@ -0,0 +1,12 @@ +-- Rename the person_mention table to person_comment_mention +ALTER TABLE person_mention RENAME TO person_comment_mention; + +-- Create the new post_mention table +CREATE TABLE person_post_mention ( + id serial PRIMARY KEY, + recipient_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 boolean DEFAULT FALSE NOT NULL, + published timestamp NOT NULL DEFAULT now(), + UNIQUE (recipient_id, post_id) +); diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index df1aebf84..d24b907a8 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -34,10 +34,10 @@ use lemmy_api::{ login::login, logout::logout, notifications::{ - list_mentions::list_mentions, + list_comment_mentions::list_comment_mentions, list_replies::list_replies, mark_all_read::mark_all_notifications_read, - mark_mention_read::mark_person_mention_as_read, + mark_comment_mention_read::mark_comment_mention_as_read, mark_reply_read::mark_reply_as_read, unread_count::unread_count, }, @@ -329,10 +329,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { web::scope("/user") .wrap(rate_limit.message()) .route("", web::get().to(read_person)) - .route("/mention", web::get().to(list_mentions)) + .route("/comment_mention", web::get().to(list_comment_mentions)) .route( - "/mention/mark_as_read", - web::post().to(mark_person_mention_as_read), + "/comment_mention/mark_as_read", + web::post().to(mark_comment_mention_as_read), ) .route("/replies", web::get().to(list_replies)) // Admin action. I don't like that it's in /user From 6a7b1d417f01699ec049ffa9f251f18c078ae9c9 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 4 Nov 2024 08:48:29 -0500 Subject: [PATCH 2/2] Finishing up post body mentions. --- crates/api/src/comment/like.rs | 6 +- .../notifications/list_post_mentions.rs | 36 ++ .../notifications/mark_post_mention_read.rs | 47 ++ .../api/src/local_user/notifications/mod.rs | 2 + .../local_user/notifications/unread_count.rs | 15 +- crates/api/src/post/like.rs | 4 +- crates/api_common/src/build_response.rs | 240 +++++---- crates/api_common/src/person.rs | 50 +- crates/api_common/src/utils.rs | 24 +- crates/api_crud/src/comment/create.rs | 3 +- crates/api_crud/src/comment/delete.rs | 3 +- crates/api_crud/src/comment/remove.rs | 3 +- crates/api_crud/src/comment/update.rs | 3 +- crates/api_crud/src/post/create.rs | 16 +- crates/api_crud/src/post/update.rs | 16 +- .../activities/create_or_update/comment.rs | 12 +- .../src/activities/create_or_update/post.rs | 24 +- .../src/impls/person_post_mention.rs | 83 +++ crates/db_schema/src/newtypes.rs | 5 + crates/db_schema/src/schema.rs | 2 +- .../src/source/person_post_mention.rs | 39 ++ crates/db_views_actor/src/lib.rs | 4 +- .../src/person_comment_mention_view.rs | 2 +- .../src/person_post_mention_view.rs | 501 ++++++++++++++++++ crates/db_views_actor/src/structs.rs | 49 +- crates/routes/src/feeds.rs | 65 ++- .../up.sql | 2 +- src/api_routes_http.rs | 7 + 28 files changed, 1082 insertions(+), 181 deletions(-) create mode 100644 crates/api/src/local_user/notifications/list_post_mentions.rs create mode 100644 crates/api/src/local_user/notifications/mark_post_mention_read.rs create mode 100644 crates/db_schema/src/impls/person_post_mention.rs create mode 100644 crates/db_schema/src/source/person_post_mention.rs create mode 100644 crates/db_views_actor/src/person_post_mention_view.rs diff --git a/crates/api/src/comment/like.rs b/crates/api/src/comment/like.rs index e93b8513f..39e2e5338 100644 --- a/crates/api/src/comment/like.rs +++ b/crates/api/src/comment/like.rs @@ -5,10 +5,10 @@ use lemmy_api_common::{ comment::{CommentResponse, CreateCommentLike}, context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, - utils::{check_bot_account, check_community_user_action, check_local_vote_mode, VoteItem}, + utils::{check_bot_account, check_community_user_action, check_local_vote_mode}, }; use lemmy_db_schema::{ - newtypes::LocalUserId, + newtypes::{LocalUserId, PostOrCommentId}, source::{ comment::{CommentLike, CommentLikeForm}, comment_reply::CommentReply, @@ -33,7 +33,7 @@ pub async fn like_comment( check_local_vote_mode( data.score, - VoteItem::Comment(comment_id), + PostOrCommentId::Comment(comment_id), &local_site, local_user_view.person.id, &mut context.pool(), diff --git a/crates/api/src/local_user/notifications/list_post_mentions.rs b/crates/api/src/local_user/notifications/list_post_mentions.rs new file mode 100644 index 000000000..e39dc59af --- /dev/null +++ b/crates/api/src/local_user/notifications/list_post_mentions.rs @@ -0,0 +1,36 @@ +use actix_web::web::{Data, Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + person::{GetPersonPostMentions, GetPersonPostMentionsResponse}, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::person_post_mention_view::PersonPostMentionQuery; +use lemmy_utils::error::LemmyResult; + +#[tracing::instrument(skip(context))] +pub async fn list_post_mentions( + data: Query, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let sort = data.sort; + let page = data.page; + let limit = data.limit; + let unread_only = data.unread_only.unwrap_or_default(); + let person_id = Some(local_user_view.person.id); + let show_bot_accounts = local_user_view.local_user.show_bot_accounts; + + let post_mentions = PersonPostMentionQuery { + recipient_id: person_id, + my_person_id: person_id, + sort, + unread_only, + show_bot_accounts, + page, + limit, + } + .list(&mut context.pool()) + .await?; + + Ok(Json(GetPersonPostMentionsResponse { post_mentions })) +} diff --git a/crates/api/src/local_user/notifications/mark_post_mention_read.rs b/crates/api/src/local_user/notifications/mark_post_mention_read.rs new file mode 100644 index 000000000..35a12c65c --- /dev/null +++ b/crates/api/src/local_user/notifications/mark_post_mention_read.rs @@ -0,0 +1,47 @@ +use actix_web::web::{Data, Json}; +use lemmy_api_common::{ + context::LemmyContext, + person::{MarkPersonPostMentionAsRead, PersonPostMentionResponse}, +}; +use lemmy_db_schema::{ + source::person_post_mention::{PersonPostMention, PersonPostMentionUpdateForm}, + traits::Crud, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::PersonPostMentionView; +use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; + +#[tracing::instrument(skip(context))] +pub async fn mark_post_mention_as_read( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let person_post_mention_id = data.person_post_mention_id; + let read_person_post_mention = + PersonPostMention::read(&mut context.pool(), person_post_mention_id).await?; + + if local_user_view.person.id != read_person_post_mention.recipient_id { + Err(LemmyErrorType::CouldntUpdatePost)? + } + + let person_post_mention_id = read_person_post_mention.id; + let read = Some(data.read); + PersonPostMention::update( + &mut context.pool(), + person_post_mention_id, + &PersonPostMentionUpdateForm { read }, + ) + .await + .with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?; + + let person_post_mention_id = read_person_post_mention.id; + let person_id = local_user_view.person.id; + let person_post_mention_view = + PersonPostMentionView::read(&mut context.pool(), person_post_mention_id, Some(person_id)) + .await?; + + Ok(Json(PersonPostMentionResponse { + person_post_mention_view, + })) +} diff --git a/crates/api/src/local_user/notifications/mod.rs b/crates/api/src/local_user/notifications/mod.rs index a3580b930..e62905109 100644 --- a/crates/api/src/local_user/notifications/mod.rs +++ b/crates/api/src/local_user/notifications/mod.rs @@ -1,6 +1,8 @@ pub mod list_comment_mentions; +pub mod list_post_mentions; pub mod list_replies; pub mod mark_all_read; pub mod mark_comment_mention_read; +pub mod mark_post_mention_read; pub mod mark_reply_read; pub mod unread_count; diff --git a/crates/api/src/local_user/notifications/unread_count.rs b/crates/api/src/local_user/notifications/unread_count.rs index 90891f21e..62bf7a0c9 100644 --- a/crates/api/src/local_user/notifications/unread_count.rs +++ b/crates/api/src/local_user/notifications/unread_count.rs @@ -1,7 +1,11 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{context::LemmyContext, person::GetUnreadCountResponse}; use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; -use lemmy_db_views_actor::structs::{CommentReplyView, PersonCommentMentionView}; +use lemmy_db_views_actor::structs::{ + CommentReplyView, + PersonCommentMentionView, + PersonPostMentionView, +}; use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] @@ -14,16 +18,21 @@ pub async fn unread_count( let replies = CommentReplyView::get_unread_count(&mut context.pool(), &local_user_view.local_user).await?; - let mentions = + let comment_mentions = PersonCommentMentionView::get_unread_count(&mut context.pool(), &local_user_view.local_user) .await?; + let post_mentions = + PersonPostMentionView::get_unread_count(&mut context.pool(), &local_user_view.local_user) + .await?; + let private_messages = PrivateMessageView::get_unread_count(&mut context.pool(), person_id).await?; Ok(Json(GetUnreadCountResponse { replies, - mentions, + comment_mentions, + post_mentions, private_messages, })) } diff --git a/crates/api/src/post/like.rs b/crates/api/src/post/like.rs index c81d9630a..e85523175 100644 --- a/crates/api/src/post/like.rs +++ b/crates/api/src/post/like.rs @@ -10,10 +10,10 @@ use lemmy_api_common::{ check_community_user_action, check_local_vote_mode, mark_post_as_read, - VoteItem, }, }; use lemmy_db_schema::{ + newtypes::PostOrCommentId, source::{ community::Community, local_site::LocalSite, @@ -36,7 +36,7 @@ pub async fn like_post( check_local_vote_mode( data.score, - VoteItem::Post(post_id), + PostOrCommentId::Post(post_id), &local_site, local_user_view.person.id, &mut context.pool(), diff --git a/crates/api_common/src/build_response.rs b/crates/api_common/src/build_response.rs index 8e79867f1..e5f5f2024 100644 --- a/crates/api_common/src/build_response.rs +++ b/crates/api_common/src/build_response.rs @@ -12,13 +12,14 @@ use crate::{ }; use actix_web::web::Json; use lemmy_db_schema::{ - newtypes::{CommentId, CommunityId, LocalUserId, PostId}, + newtypes::{CommentId, CommunityId, LocalUserId, PostId, PostOrCommentId}, source::{ actor_language::CommunityLanguage, comment::Comment, comment_reply::{CommentReply, CommentReplyInsertForm}, person::Person, person_comment_mention::{PersonCommentMention, PersonCommentMentionInsertForm}, + person_post_mention::{PersonPostMention, PersonPostMentionInsertForm}, }, traits::Crud, }; @@ -92,7 +93,7 @@ pub async fn build_post_response( #[tracing::instrument(skip_all)] pub async fn send_local_notifs( mentions: Vec, - comment_id: CommentId, + post_or_comment_id: PostOrCommentId, person: &Person, do_send_email: bool, context: &LemmyContext, @@ -103,15 +104,32 @@ pub async fn send_local_notifs( // let person = my_local_user.person; // Read the comment view to get extra info - let comment_view = CommentView::read( - &mut context.pool(), - comment_id, - local_user_view.map(|view| &view.local_user), - ) - .await?; - let comment = comment_view.comment; - let post = comment_view.post; - let community = comment_view.community; + + let (comment_opt, post, community) = match post_or_comment_id { + PostOrCommentId::Post(post_id) => { + let post_view = PostView::read( + &mut context.pool(), + post_id, + local_user_view.map(|view| &view.local_user), + false, + ) + .await?; + (None, post_view.post, post_view.community) + } + PostOrCommentId::Comment(comment_id) => { + let comment_view = CommentView::read( + &mut context.pool(), + comment_id, + local_user_view.map(|view| &view.local_user), + ) + .await?; + ( + Some(comment_view.comment), + comment_view.post, + comment_view.community, + ) + } + }; // Send the local mentions for mention in mentions @@ -127,22 +145,38 @@ pub async fn send_local_notifs( // below by checking recipient ids recipient_ids.push(mention_user_view.local_user.id); - let person_comment_mention_form = PersonCommentMentionInsertForm { - recipient_id: mention_user_view.person.id, - comment_id, - read: None, - }; + // Make the correct reply form depending on whether its a post or comment mention + let comment_content_or_post_body = if let Some(comment) = &comment_opt { + let person_comment_mention_form = PersonCommentMentionInsertForm { + recipient_id: mention_user_view.person.id, + comment_id: comment.id, + read: None, + }; - // Allow this to fail softly, since comment edits might re-update or replace it - // Let the uniqueness handle this fail - PersonCommentMention::create(&mut context.pool(), &person_comment_mention_form) - .await - .ok(); + // Allow this to fail softly, since comment edits might re-update or replace it + // Let the uniqueness handle this fail + PersonCommentMention::create(&mut context.pool(), &person_comment_mention_form) + .await + .ok(); + comment.content.clone() + } else { + let person_post_mention_form = PersonPostMentionInsertForm { + recipient_id: mention_user_view.person.id, + post_id: post.id, + read: None, + }; + + // Allow this to fail softly, since edits might re-update or replace it + PersonPostMention::create(&mut context.pool(), &person_post_mention_form) + .await + .ok(); + post.body.clone().unwrap_or_default() + }; // Send an email to those local users that have notifications on if do_send_email { let lang = get_interface_language(&mention_user_view); - let content = markdown_to_html(&comment.content); + let content = markdown_to_html(&comment_content_or_post_body); send_email_to_user( &mention_user_view, &lang.notification_mentioned_by_subject(&person.name), @@ -155,99 +189,101 @@ pub async fn send_local_notifs( } // Send comment_reply to the parent commenter / poster - if let Some(parent_comment_id) = comment.parent_comment_id() { - let parent_comment = Comment::read(&mut context.pool(), parent_comment_id).await?; + if let Some(comment) = &comment_opt { + if let Some(parent_comment_id) = comment.parent_comment_id() { + let parent_comment = Comment::read(&mut context.pool(), parent_comment_id).await?; - // Get the parent commenter local_user - let parent_creator_id = parent_comment.creator_id; + // Get the parent commenter local_user + let parent_creator_id = parent_comment.creator_id; - let check_blocks = check_person_instance_community_block( - person.id, - parent_creator_id, - // Only block from the community's instance_id - community.instance_id, - community.id, - &mut context.pool(), - ) - .await - .is_err(); + let check_blocks = check_person_instance_community_block( + person.id, + parent_creator_id, + // Only block from the community's instance_id + community.instance_id, + community.id, + &mut context.pool(), + ) + .await + .is_err(); - // Don't send a notif to yourself - if parent_comment.creator_id != person.id && !check_blocks { - let user_view = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await; - if let Ok(parent_user_view) = user_view { - // Don't duplicate notif if already mentioned by checking recipient ids - if !recipient_ids.contains(&parent_user_view.local_user.id) { - recipient_ids.push(parent_user_view.local_user.id); + // Don't send a notif to yourself + if parent_comment.creator_id != person.id && !check_blocks { + let user_view = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await; + if let Ok(parent_user_view) = user_view { + // Don't duplicate notif if already mentioned by checking recipient ids + if !recipient_ids.contains(&parent_user_view.local_user.id) { + recipient_ids.push(parent_user_view.local_user.id); - let comment_reply_form = CommentReplyInsertForm { - recipient_id: parent_user_view.person.id, - comment_id: comment.id, - read: None, - }; + let comment_reply_form = CommentReplyInsertForm { + recipient_id: parent_user_view.person.id, + comment_id: comment.id, + read: None, + }; - // Allow this to fail softly, since comment edits might re-update or replace it - // Let the uniqueness handle this fail - CommentReply::create(&mut context.pool(), &comment_reply_form) - .await - .ok(); + // Allow this to fail softly, since comment edits might re-update or replace it + // Let the uniqueness handle this fail + CommentReply::create(&mut context.pool(), &comment_reply_form) + .await + .ok(); - if do_send_email { - let lang = get_interface_language(&parent_user_view); - let content = markdown_to_html(&comment.content); - send_email_to_user( - &parent_user_view, - &lang.notification_comment_reply_subject(&person.name), - &lang.notification_comment_reply_body(&content, &inbox_link, &person.name), - context.settings(), - ) - .await + if do_send_email { + let lang = get_interface_language(&parent_user_view); + let content = markdown_to_html(&comment.content); + send_email_to_user( + &parent_user_view, + &lang.notification_comment_reply_subject(&person.name), + &lang.notification_comment_reply_body(&content, &inbox_link, &person.name), + context.settings(), + ) + .await + } } } } - } - } else { - // Use the post creator to check blocks - let check_blocks = check_person_instance_community_block( - person.id, - post.creator_id, - // Only block from the community's instance_id - community.instance_id, - community.id, - &mut context.pool(), - ) - .await - .is_err(); + } else { + // Use the post creator to check blocks + let check_blocks = check_person_instance_community_block( + person.id, + post.creator_id, + // Only block from the community's instance_id + community.instance_id, + community.id, + &mut context.pool(), + ) + .await + .is_err(); - if post.creator_id != person.id && !check_blocks { - let creator_id = post.creator_id; - let parent_user = LocalUserView::read_person(&mut context.pool(), creator_id).await; - if let Ok(parent_user_view) = parent_user { - if !recipient_ids.contains(&parent_user_view.local_user.id) { - recipient_ids.push(parent_user_view.local_user.id); + if post.creator_id != person.id && !check_blocks { + let creator_id = post.creator_id; + let parent_user = LocalUserView::read_person(&mut context.pool(), creator_id).await; + if let Ok(parent_user_view) = parent_user { + if !recipient_ids.contains(&parent_user_view.local_user.id) { + recipient_ids.push(parent_user_view.local_user.id); - let comment_reply_form = CommentReplyInsertForm { - recipient_id: parent_user_view.person.id, - comment_id: comment.id, - read: None, - }; + let comment_reply_form = CommentReplyInsertForm { + recipient_id: parent_user_view.person.id, + comment_id: comment.id, + read: None, + }; - // Allow this to fail softly, since comment edits might re-update or replace it - // Let the uniqueness handle this fail - CommentReply::create(&mut context.pool(), &comment_reply_form) - .await - .ok(); + // Allow this to fail softly, since comment edits might re-update or replace it + // Let the uniqueness handle this fail + CommentReply::create(&mut context.pool(), &comment_reply_form) + .await + .ok(); - if do_send_email { - let lang = get_interface_language(&parent_user_view); - let content = markdown_to_html(&comment.content); - send_email_to_user( - &parent_user_view, - &lang.notification_post_reply_subject(&person.name), - &lang.notification_post_reply_body(&content, &inbox_link, &person.name), - context.settings(), - ) - .await + if do_send_email { + let lang = get_interface_language(&parent_user_view); + let content = markdown_to_html(&comment.content); + send_email_to_user( + &parent_user_view, + &lang.notification_post_reply_subject(&person.name), + &lang.notification_post_reply_body(&content, &inbox_link, &person.name), + context.settings(), + ) + .await + } } } } diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index a4020f57e..cbe510e3c 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -1,5 +1,12 @@ use lemmy_db_schema::{ - newtypes::{CommentReplyId, CommunityId, LanguageId, PersonCommentMentionId, PersonId}, + newtypes::{ + CommentReplyId, + CommunityId, + LanguageId, + PersonCommentMentionId, + PersonId, + PersonPostMentionId, + }, sensitive::SensitiveString, source::{login_token::LoginToken, site::Site}, CommentSortType, @@ -12,6 +19,7 @@ use lemmy_db_views_actor::structs::{ CommentReplyView, CommunityModeratorView, PersonCommentMentionView, + PersonPostMentionView, PersonView, }; use serde::{Deserialize, Serialize}; @@ -325,6 +333,43 @@ pub struct PersonCommentMentionResponse { pub person_comment_mention_view: PersonCommentMentionView, } +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Get mentions for your user. +pub struct GetPersonPostMentions { + pub sort: Option, + pub page: Option, + pub limit: Option, + pub unread_only: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The response of mentions for your user. +pub struct GetPersonPostMentionsResponse { + pub post_mentions: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Mark a person mention as read. +pub struct MarkPersonPostMentionAsRead { + pub person_post_mention_id: PersonPostMentionId, + pub read: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The response for a person mention action. +pub struct PersonPostMentionResponse { + pub person_post_mention_view: PersonPostMentionView, +} + #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] @@ -396,7 +441,8 @@ pub struct GetReportCountResponse { /// A response containing counts for your notifications. pub struct GetUnreadCountResponse { pub replies: i64, - pub mentions: i64, + pub comment_mentions: i64, + pub post_mentions: i64, pub private_messages: i64, } diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index e358d483b..069523533 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -11,7 +11,7 @@ use chrono::{DateTime, Days, Local, TimeZone, Utc}; use enum_map::{enum_map, EnumMap}; use lemmy_db_schema::{ aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, - newtypes::{CommentId, CommunityId, DbUrl, InstanceId, PersonId, PostId}, + newtypes::{CommentId, CommunityId, DbUrl, InstanceId, PersonId, PostId, PostOrCommentId}, source::{ comment::{Comment, CommentLike, CommentUpdateForm}, community::{Community, CommunityModerator, CommunityUpdateForm}, @@ -301,23 +301,17 @@ pub async fn check_person_instance_community_block( Ok(()) } -/// A vote item type used to check the vote mode. -pub enum VoteItem { - Post(PostId), - Comment(CommentId), -} - #[tracing::instrument(skip_all)] pub async fn check_local_vote_mode( score: i16, - vote_item: VoteItem, + post_or_comment_id: PostOrCommentId, local_site: &LocalSite, person_id: PersonId, pool: &mut DbPool<'_>, ) -> LemmyResult<()> { - let (downvote_setting, upvote_setting) = match vote_item { - VoteItem::Post(_) => (local_site.post_downvotes, local_site.post_upvotes), - VoteItem::Comment(_) => (local_site.comment_downvotes, local_site.comment_upvotes), + let (downvote_setting, upvote_setting) = match post_or_comment_id { + PostOrCommentId::Post(_) => (local_site.post_downvotes, local_site.post_upvotes), + PostOrCommentId::Comment(_) => (local_site.comment_downvotes, local_site.comment_upvotes), }; let downvote_fail = score == -1 && downvote_setting == FederationMode::Disable; @@ -325,9 +319,11 @@ pub async fn check_local_vote_mode( // Undo previous vote for item if new vote fails if downvote_fail || upvote_fail { - match vote_item { - VoteItem::Post(post_id) => PostLike::remove(pool, person_id, post_id).await?, - VoteItem::Comment(comment_id) => CommentLike::remove(pool, person_id, comment_id).await?, + match post_or_comment_id { + PostOrCommentId::Post(post_id) => PostLike::remove(pool, person_id, post_id).await?, + PostOrCommentId::Comment(comment_id) => { + CommentLike::remove(pool, person_id, comment_id).await? + } }; } Ok(()) diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index 1347918c2..b0d3f4d13 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -17,6 +17,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ impls::actor_language::default_post_language, + newtypes::PostOrCommentId, source::{ actor_language::CommunityLanguage, comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm}, @@ -121,7 +122,7 @@ pub async fn create_comment( let mentions = scrape_text_for_mentions(&content); let recipient_ids = send_local_notifs( mentions, - inserted_comment_id, + PostOrCommentId::Comment(inserted_comment_id), &local_user_view.person, true, &context, diff --git a/crates/api_crud/src/comment/delete.rs b/crates/api_crud/src/comment/delete.rs index 2b5f35827..7a864b4e7 100644 --- a/crates/api_crud/src/comment/delete.rs +++ b/crates/api_crud/src/comment/delete.rs @@ -8,6 +8,7 @@ use lemmy_api_common::{ utils::check_community_user_action, }; use lemmy_db_schema::{ + newtypes::PostOrCommentId, source::comment::{Comment, CommentUpdateForm}, traits::Crud, }; @@ -60,7 +61,7 @@ pub async fn delete_comment( let recipient_ids = send_local_notifs( vec![], - comment_id, + PostOrCommentId::Comment(comment_id), &local_user_view.person, false, &context, diff --git a/crates/api_crud/src/comment/remove.rs b/crates/api_crud/src/comment/remove.rs index 3c137a984..ae4386bdd 100644 --- a/crates/api_crud/src/comment/remove.rs +++ b/crates/api_crud/src/comment/remove.rs @@ -8,6 +8,7 @@ use lemmy_api_common::{ utils::check_community_mod_action, }; use lemmy_db_schema::{ + newtypes::PostOrCommentId, source::{ comment::{Comment, CommentUpdateForm}, comment_report::CommentReport, @@ -82,7 +83,7 @@ pub async fn remove_comment( let recipient_ids = send_local_notifs( vec![], - comment_id, + PostOrCommentId::Comment(comment_id), &local_user_view.person, false, &context, diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index 51f65aa67..ae9e1b99b 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -13,6 +13,7 @@ use lemmy_api_common::{ }, }; use lemmy_db_schema::{ + newtypes::PostOrCommentId, source::{ actor_language::CommunityLanguage, comment::{Comment, CommentUpdateForm}, @@ -87,7 +88,7 @@ pub async fn update_comment( let mentions = scrape_text_for_mentions(&updated_comment_content); let recipient_ids = send_local_notifs( mentions, - comment_id, + PostOrCommentId::Comment(comment_id), &local_user_view.person, false, &context, diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index 90c68bdbd..d9ee9cb7a 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -2,7 +2,7 @@ use super::convert_published_time; use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ - build_response::build_post_response, + build_response::{build_post_response, send_local_notifs}, context::LemmyContext, post::{CreatePost, PostResponse}, request::generate_post_link_metadata, @@ -18,6 +18,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ impls::actor_language::default_post_language, + newtypes::PostOrCommentId, source::{ actor_language::CommunityLanguage, community::Community, @@ -34,6 +35,7 @@ use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, spawn_try_task, utils::{ + mention::scrape_text_for_mentions, slurs::check_slurs, validation::{ is_url_blocked, @@ -169,6 +171,18 @@ pub async fn create_post( .await .with_lemmy_type(LemmyErrorType::CouldntLikePost)?; + // Scan the post body for user mentions, add those rows + let mentions = scrape_text_for_mentions(&inserted_post.body.clone().unwrap_or_default()); + send_local_notifs( + mentions, + PostOrCommentId::Post(inserted_post.id), + &local_user_view.person, + true, + &context, + Some(&local_user_view), + ) + .await?; + mark_post_as_read(person_id, post_id, &mut context.pool()).await?; build_post_response(&context, community_id, local_user_view, post_id).await diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs index cef8bfea8..7b1cb7629 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -2,7 +2,7 @@ use super::{convert_published_time, create::send_webmention}; use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ - build_response::build_post_response, + build_response::{build_post_response, send_local_notifs}, context::LemmyContext, post::{EditPost, PostResponse}, request::generate_post_link_metadata, @@ -15,6 +15,7 @@ use lemmy_api_common::{ }, }; use lemmy_db_schema::{ + newtypes::PostOrCommentId, source::{ actor_language::CommunityLanguage, community::Community, @@ -28,6 +29,7 @@ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::{ + mention::scrape_text_for_mentions, slurs::check_slurs, validation::{ is_url_blocked, @@ -142,6 +144,18 @@ pub async fn update_post( .await .with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?; + // Scan the post body for user mentions, add those rows + let mentions = scrape_text_for_mentions(&updated_post.body.clone().unwrap_or_default()); + send_local_notifs( + mentions, + PostOrCommentId::Post(updated_post.id), + &local_user_view.person, + false, + &context, + Some(&local_user_view), + ) + .await?; + // send out federation/webmention if necessary match ( orig_post.scheduled_publish_time, diff --git a/crates/apub/src/activities/create_or_update/comment.rs b/crates/apub/src/activities/create_or_update/comment.rs index 0a0737151..777b94f54 100644 --- a/crates/apub/src/activities/create_or_update/comment.rs +++ b/crates/apub/src/activities/create_or_update/comment.rs @@ -29,7 +29,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ aggregates::structs::CommentAggregates, - newtypes::PersonId, + newtypes::{PersonId, PostOrCommentId}, source::{ activity::ActivitySendTargets, comment::{Comment, CommentLike, CommentLikeForm}, @@ -171,7 +171,15 @@ impl ActivityHandler for CreateOrUpdateNote { // TODO: for compatibility with other projects, it would be much better to read this from cc or // tags let mentions = scrape_text_for_mentions(&comment.content); - send_local_notifs(mentions, comment.id, &actor, do_send_email, context, None).await?; + send_local_notifs( + mentions, + PostOrCommentId::Comment(comment.id), + &actor, + do_send_email, + context, + None, + ) + .await?; Ok(()) } } diff --git a/crates/apub/src/activities/create_or_update/post.rs b/crates/apub/src/activities/create_or_update/post.rs index fb53100f6..39fd6573c 100644 --- a/crates/apub/src/activities/create_or_update/post.rs +++ b/crates/apub/src/activities/create_or_update/post.rs @@ -20,10 +20,10 @@ use activitypub_federation::{ protocol::verification::{verify_domains_match, verify_urls_match}, traits::{ActivityHandler, Actor, Object}, }; -use lemmy_api_common::context::LemmyContext; +use lemmy_api_common::{build_response::send_local_notifs, context::LemmyContext}; use lemmy_db_schema::{ aggregates::structs::PostAggregates, - newtypes::PersonId, + newtypes::{PersonId, PostOrCommentId}, source::{ activity::ActivitySendTargets, community::Community, @@ -32,7 +32,10 @@ use lemmy_db_schema::{ }, traits::{Crud, Likeable}, }; -use lemmy_utils::error::{LemmyError, LemmyResult}; +use lemmy_utils::{ + error::{LemmyError, LemmyResult}, + utils::mention::scrape_text_for_mentions, +}; use url::Url; impl CreateOrUpdatePage { @@ -128,6 +131,21 @@ impl ActivityHandler for CreateOrUpdatePage { // Calculate initial hot_rank for post PostAggregates::update_ranks(&mut context.pool(), post.id).await?; + let do_send_email = self.kind == CreateOrUpdateType::Create; + let actor = self.actor.dereference(context).await?; + + // Send the post body mentions + let mentions = scrape_text_for_mentions(&post.body.clone().unwrap_or_default()); + send_local_notifs( + mentions, + PostOrCommentId::Post(post.id), + &actor, + do_send_email, + context, + None, + ) + .await?; + Ok(()) } } diff --git a/crates/db_schema/src/impls/person_post_mention.rs b/crates/db_schema/src/impls/person_post_mention.rs new file mode 100644 index 000000000..ef59b60e1 --- /dev/null +++ b/crates/db_schema/src/impls/person_post_mention.rs @@ -0,0 +1,83 @@ +use crate::{ + diesel::OptionalExtension, + newtypes::{PersonId, PersonPostMentionId, PostId}, + schema::person_post_mention, + source::person_post_mention::{ + PersonPostMention, + PersonPostMentionInsertForm, + PersonPostMentionUpdateForm, + }, + traits::Crud, + utils::{get_conn, DbPool}, +}; +use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; + +#[async_trait] +impl Crud for PersonPostMention { + type InsertForm = PersonPostMentionInsertForm; + type UpdateForm = PersonPostMentionUpdateForm; + type IdType = PersonPostMentionId; + + async fn create( + pool: &mut DbPool<'_>, + person_post_mention_form: &Self::InsertForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + // since the return here isnt utilized, we dont need to do an update + // but get_result doesn't return the existing row here + insert_into(person_post_mention::table) + .values(person_post_mention_form) + .on_conflict(( + person_post_mention::recipient_id, + person_post_mention::post_id, + )) + .do_update() + .set(person_post_mention_form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + person_post_mention_id: PersonPostMentionId, + person_post_mention_form: &Self::UpdateForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::update(person_post_mention::table.find(person_post_mention_id)) + .set(person_post_mention_form) + .get_result::(conn) + .await + } +} + +impl PersonPostMention { + pub async fn mark_all_as_read( + pool: &mut DbPool<'_>, + for_recipient_id: PersonId, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + diesel::update( + person_post_mention::table + .filter(person_post_mention::recipient_id.eq(for_recipient_id)) + .filter(person_post_mention::read.eq(false)), + ) + .set(person_post_mention::read.eq(true)) + .get_results::(conn) + .await + } + + pub async fn read_by_post_and_person( + pool: &mut DbPool<'_>, + for_post_id: PostId, + for_recipient_id: PersonId, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + person_post_mention::table + .filter(person_post_mention::post_id.eq(for_post_id)) + .filter(person_post_mention::recipient_id.eq(for_recipient_id)) + .first(conn) + .await + .optional() + } +} diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index 2041b0c48..c372aa255 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -55,6 +55,11 @@ impl fmt::Display for CommentId { } } +pub enum PostOrCommentId { + Post(PostId), + Comment(CommentId), +} + #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Default, Serialize, Deserialize)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 6dc863e48..65e013af6 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -752,7 +752,7 @@ diesel::table! { recipient_id -> Int4, post_id -> Int4, read -> Bool, - published -> Timestamp, + published -> Timestamptz, } } diff --git a/crates/db_schema/src/source/person_post_mention.rs b/crates/db_schema/src/source/person_post_mention.rs new file mode 100644 index 000000000..b1c00febf --- /dev/null +++ b/crates/db_schema/src/source/person_post_mention.rs @@ -0,0 +1,39 @@ +use crate::newtypes::{PersonId, PersonPostMentionId, PostId}; +#[cfg(feature = "full")] +use crate::schema::person_post_mention; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr( + feature = "full", + derive(Queryable, Selectable, Associations, Identifiable, TS) +)] +#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))] +#[cfg_attr(feature = "full", diesel(table_name = person_post_mention))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A person mention. +pub struct PersonPostMention { + pub id: PersonPostMentionId, + pub recipient_id: PersonId, + pub post_id: PostId, + pub read: bool, + pub published: DateTime, +} + +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = person_post_mention))] +pub struct PersonPostMentionInsertForm { + pub recipient_id: PersonId, + pub post_id: PostId, + pub read: Option, +} + +#[cfg_attr(feature = "full", derive(AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = person_post_mention))] +pub struct PersonPostMentionUpdateForm { + pub read: Option, +} diff --git a/crates/db_views_actor/src/lib.rs b/crates/db_views_actor/src/lib.rs index 7e1b96757..6a955aacb 100644 --- a/crates/db_views_actor/src/lib.rs +++ b/crates/db_views_actor/src/lib.rs @@ -10,8 +10,8 @@ pub mod community_person_ban_view; pub mod community_view; #[cfg(feature = "full")] pub mod person_comment_mention_view; -// #[cfg(feature = "full")] -// pub mod person_post_mention_view; +#[cfg(feature = "full")] +pub mod person_post_mention_view; #[cfg(feature = "full")] pub mod person_view; pub mod structs; diff --git a/crates/db_views_actor/src/person_comment_mention_view.rs b/crates/db_views_actor/src/person_comment_mention_view.rs index 9a61ff0d9..a4f7b4e85 100644 --- a/crates/db_views_actor/src/person_comment_mention_view.rs +++ b/crates/db_views_actor/src/person_comment_mention_view.rs @@ -216,7 +216,7 @@ fn queries<'a>() -> Queries< query = query.filter(not(person::bot_account)); }; - query = match options.sort.unwrap_or(CommentSortType::Hot) { + query = match options.sort.unwrap_or(CommentSortType::New) { CommentSortType::Hot => query.then_order_by(comment_aggregates::hot_rank.desc()), CommentSortType::Controversial => { query.then_order_by(comment_aggregates::controversy_rank.desc()) diff --git a/crates/db_views_actor/src/person_post_mention_view.rs b/crates/db_views_actor/src/person_post_mention_view.rs new file mode 100644 index 000000000..c7ab89d51 --- /dev/null +++ b/crates/db_views_actor/src/person_post_mention_view.rs @@ -0,0 +1,501 @@ +use crate::structs::PersonPostMentionView; +use diesel::{ + dsl::{exists, not, IntervalDsl}, + pg::Pg, + result::Error, + sql_types, + BoolExpressionMethods, + BoxableExpression, + ExpressionMethods, + IntoSql, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, +}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + aliases, + newtypes::{PersonId, PersonPostMentionId}, + schema::{ + community, + community_follower, + community_moderator, + community_person_ban, + local_user, + person, + person_block, + person_post_mention, + post, + post_aggregates, + post_like, + post_saved, + }, + source::local_user::LocalUser, + utils::{get_conn, limit_and_offset, now, DbConn, DbPool, ListFn, Queries, ReadFn}, + PostSortType, +}; + +fn queries<'a>() -> Queries< + impl ReadFn<'a, PersonPostMentionView, (PersonPostMentionId, Option)>, + impl ListFn<'a, PersonPostMentionView, PersonPostMentionQuery>, +> { + let is_creator_banned_from_community = exists( + community_person_ban::table.filter( + community::id + .eq(community_person_ban::community_id) + .and(community_person_ban::person_id.eq(post::creator_id)), + ), + ); + + let is_local_user_banned_from_community = |person_id| { + exists( + community_person_ban::table.filter( + community::id + .eq(community_person_ban::community_id) + .and(community_person_ban::person_id.eq(person_id)), + ), + ) + }; + + let is_saved = |person_id| { + exists( + post_saved::table.filter( + post::id + .eq(post_saved::post_id) + .and(post_saved::person_id.eq(person_id)), + ), + ) + }; + + let is_community_followed = |person_id| { + community_follower::table + .filter( + post::community_id + .eq(community_follower::community_id) + .and(community_follower::person_id.eq(person_id)), + ) + .select(community_follower::pending.nullable()) + .single_value() + }; + + let is_creator_blocked = |person_id| { + exists( + person_block::table.filter( + post::creator_id + .eq(person_block::target_id) + .and(person_block::person_id.eq(person_id)), + ), + ) + }; + + let score = |person_id| { + post_like::table + .filter( + post::id + .eq(post_like::post_id) + .and(post_like::person_id.eq(person_id)), + ) + .select(post_like::score.nullable()) + .single_value() + }; + + let creator_is_moderator = exists( + community_moderator::table.filter( + community::id + .eq(community_moderator::community_id) + .and(community_moderator::person_id.eq(post::creator_id)), + ), + ); + + let creator_is_admin = exists( + local_user::table.filter( + post::creator_id + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ), + ); + + let all_joins = move |query: person_post_mention::BoxedQuery<'a, Pg>, + my_person_id: Option| { + let is_local_user_banned_from_community_selection: Box< + dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>, + > = if let Some(person_id) = my_person_id { + Box::new(is_local_user_banned_from_community(person_id)) + } else { + Box::new(false.into_sql::()) + }; + let score_selection: Box< + dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, + > = if let Some(person_id) = my_person_id { + Box::new(score(person_id)) + } else { + Box::new(None::.into_sql::>()) + }; + + let subscribed_type_selection: Box< + dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, + > = if let Some(person_id) = my_person_id { + Box::new(is_community_followed(person_id)) + } else { + Box::new(None::.into_sql::>()) + }; + + let is_saved_selection: Box> = + if let Some(person_id) = my_person_id { + Box::new(is_saved(person_id)) + } else { + Box::new(false.into_sql::()) + }; + + let is_creator_blocked_selection: Box> = + if let Some(person_id) = my_person_id { + Box::new(is_creator_blocked(person_id)) + } else { + Box::new(false.into_sql::()) + }; + + query + .inner_join(post::table) + .inner_join(person::table.on(post::creator_id.eq(person::id))) + .inner_join(community::table.on(post::community_id.eq(community::id))) + .inner_join(aliases::person1) + .inner_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) + .select(( + person_post_mention::all_columns, + post::all_columns, + person::all_columns, + community::all_columns, + aliases::person1.fields(person::all_columns), + post_aggregates::all_columns, + is_creator_banned_from_community, + is_local_user_banned_from_community_selection, + creator_is_moderator, + creator_is_admin, + subscribed_type_selection, + is_saved_selection, + is_creator_blocked_selection, + score_selection, + )) + }; + + let read = move |mut conn: DbConn<'a>, + (person_post_mention_id, my_person_id): ( + PersonPostMentionId, + Option, + )| async move { + all_joins( + person_post_mention::table + .find(person_post_mention_id) + .into_boxed(), + my_person_id, + ) + .first(&mut conn) + .await + }; + + let list = move |mut conn: DbConn<'a>, options: PersonPostMentionQuery| async move { + // These filters need to be kept in sync with the filters in + // PersonPostMentionView::get_unread_mentions() + let mut query = all_joins( + person_post_mention::table.into_boxed(), + options.my_person_id, + ); + + if let Some(recipient_id) = options.recipient_id { + query = query.filter(person_post_mention::recipient_id.eq(recipient_id)); + } + + if options.unread_only { + query = query.filter(person_post_mention::read.eq(false)); + } + + if !options.show_bot_accounts { + query = query.filter(not(person::bot_account)); + }; + + let time = |interval| post_aggregates::published.gt(now() - interval); + + query = match options.sort.unwrap_or(PostSortType::New) { + PostSortType::Active => query.then_order_by(post_aggregates::hot_rank_active.desc()), + PostSortType::Hot => query.then_order_by(post_aggregates::hot_rank.desc()), + PostSortType::Controversial => query.then_order_by(post_aggregates::controversy_rank.desc()), + PostSortType::New => query.then_order_by(post_aggregates::published.desc()), + PostSortType::Old => query.then_order_by(post_aggregates::published.asc()), + PostSortType::TopAll => query.order_by(post_aggregates::score.desc()), + PostSortType::Scaled => query.then_order_by(post_aggregates::scaled_rank.desc()), + PostSortType::NewComments => query.then_order_by(post_aggregates::newest_comment_time.desc()), + PostSortType::MostComments => query.then_order_by(post_aggregates::comments.desc()), + PostSortType::TopYear => query + .then_order_by(post_aggregates::score.desc()) + .filter(time(1.years())), + PostSortType::TopMonth => query + .then_order_by(post_aggregates::score.desc()) + .filter(time(1.months())), + PostSortType::TopWeek => query + .then_order_by(post_aggregates::score.desc()) + .filter(time(1.weeks())), + PostSortType::TopDay => query + .then_order_by(post_aggregates::score.desc()) + .filter(time(1.days())), + PostSortType::TopHour => query + .then_order_by(post_aggregates::score.desc()) + .filter(time(1.hours())), + PostSortType::TopSixHour => query + .then_order_by(post_aggregates::score.desc()) + .filter(time(6.hours())), + PostSortType::TopTwelveHour => query + .then_order_by(post_aggregates::score.desc()) + .filter(time(12.hours())), + PostSortType::TopThreeMonths => query + .then_order_by(post_aggregates::score.desc()) + .filter(time(3.months())), + PostSortType::TopSixMonths => query + .then_order_by(post_aggregates::score.desc()) + .filter(time(6.months())), + PostSortType::TopNineMonths => query + .then_order_by(post_aggregates::score.desc()) + .filter(time(9.months())), + }; + + // Don't show mentions from blocked persons + if let Some(my_person_id) = options.my_person_id { + query = query.filter(not(is_creator_blocked(my_person_id))); + } + + let (limit, offset) = limit_and_offset(options.page, options.limit)?; + + query + .limit(limit) + .offset(offset) + .load::(&mut conn) + .await + }; + + Queries::new(read, list) +} + +impl PersonPostMentionView { + pub async fn read( + pool: &mut DbPool<'_>, + person_post_mention_id: PersonPostMentionId, + my_person_id: Option, + ) -> Result { + queries() + .read(pool, (person_post_mention_id, my_person_id)) + .await + } + + /// Gets the number of unread mentions + pub async fn get_unread_count( + pool: &mut DbPool<'_>, + local_user: &LocalUser, + ) -> Result { + use diesel::dsl::count; + let conn = &mut get_conn(pool).await?; + + let mut query = person_post_mention::table + .inner_join(post::table) + .left_join( + person_block::table.on( + post::creator_id + .eq(person_block::target_id) + .and(person_block::person_id.eq(local_user.person_id)), + ), + ) + .inner_join(person::table.on(post::creator_id.eq(person::id))) + .into_boxed(); + + // These filters need to be kept in sync with the filters in queries().list() + if !local_user.show_bot_accounts { + query = query.filter(not(person::bot_account)); + } + + query + // Don't count replies from blocked users + .filter(person_block::person_id.is_null()) + .filter(person_post_mention::recipient_id.eq(local_user.person_id)) + .filter(person_post_mention::read.eq(false)) + .filter(post::deleted.eq(false)) + .filter(post::removed.eq(false)) + .select(count(person_post_mention::id)) + .first::(conn) + .await + } +} + +#[derive(Default, Clone)] +pub struct PersonPostMentionQuery { + pub my_person_id: Option, + pub recipient_id: Option, + pub sort: Option, + pub unread_only: bool, + pub show_bot_accounts: bool, + pub page: Option, + pub limit: Option, +} + +impl PersonPostMentionQuery { + pub async fn list(self, pool: &mut DbPool<'_>) -> Result, Error> { + queries().list(pool, self).await + } +} + +#[cfg(test)] +mod tests { + + use crate::{person_post_mention_view::PersonPostMentionQuery, structs::PersonPostMentionView}; + use lemmy_db_schema::{ + source::{ + community::{Community, CommunityInsertForm}, + instance::Instance, + local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, + person::{Person, PersonInsertForm, PersonUpdateForm}, + person_block::{PersonBlock, PersonBlockForm}, + person_post_mention::{ + PersonPostMention, + PersonPostMentionInsertForm, + PersonPostMentionUpdateForm, + }, + post::{Post, PostInsertForm}, + }, + traits::{Blockable, Crud}, + utils::build_db_pool_for_tests, + }; + use lemmy_db_views::structs::LocalUserView; + use lemmy_utils::error::LemmyResult; + use pretty_assertions::assert_eq; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn test_crud() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests().await; + let pool = &mut pool.into(); + + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let new_person = PersonInsertForm::test_form(inserted_instance.id, "terrylake"); + + let inserted_person = Person::create(pool, &new_person).await?; + + let recipient_form = PersonInsertForm::test_form(inserted_instance.id, "terrylakes recipient"); + + let inserted_recipient = Person::create(pool, &recipient_form).await?; + let recipient_id = inserted_recipient.id; + + let recipient_local_user = + LocalUser::create(pool, &LocalUserInsertForm::test_form(recipient_id), vec![]).await?; + + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "test community lake".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; + + let new_post = PostInsertForm::new( + "A test post".into(), + inserted_person.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &new_post).await?; + + let person_post_mention_form = PersonPostMentionInsertForm { + recipient_id: inserted_recipient.id, + post_id: inserted_post.id, + read: None, + }; + + let inserted_mention = PersonPostMention::create(pool, &person_post_mention_form).await?; + + let expected_mention = PersonPostMention { + id: inserted_mention.id, + recipient_id: inserted_mention.recipient_id, + post_id: inserted_mention.post_id, + read: false, + published: inserted_mention.published, + }; + + let read_mention = PersonPostMention::read(pool, inserted_mention.id).await?; + + let person_post_mention_update_form = PersonPostMentionUpdateForm { read: Some(false) }; + let updated_mention = + PersonPostMention::update(pool, inserted_mention.id, &person_post_mention_update_form) + .await?; + + // Test to make sure counts and blocks work correctly + let unread_mentions = + PersonPostMentionView::get_unread_count(pool, &recipient_local_user).await?; + + let query = PersonPostMentionQuery { + recipient_id: Some(recipient_id), + my_person_id: Some(recipient_id), + sort: None, + unread_only: false, + show_bot_accounts: true, + page: None, + limit: None, + }; + let mentions = query.clone().list(pool).await?; + assert_eq!(1, unread_mentions); + assert_eq!(1, mentions.len()); + + // Block the person, and make sure these counts are now empty + let block_form = PersonBlockForm { + person_id: recipient_id, + target_id: inserted_person.id, + }; + PersonBlock::block(pool, &block_form).await?; + + let unread_mentions_after_block = + PersonPostMentionView::get_unread_count(pool, &recipient_local_user).await?; + let mentions_after_block = query.clone().list(pool).await?; + assert_eq!(0, unread_mentions_after_block); + assert_eq!(0, mentions_after_block.len()); + + // Unblock user so we can reuse the same person + PersonBlock::unblock(pool, &block_form).await?; + + // Turn Terry into a bot account + let person_update_form = PersonUpdateForm { + bot_account: Some(true), + ..Default::default() + }; + Person::update(pool, inserted_person.id, &person_update_form).await?; + + let recipient_local_user_update_form = LocalUserUpdateForm { + show_bot_accounts: Some(false), + ..Default::default() + }; + LocalUser::update( + pool, + recipient_local_user.id, + &recipient_local_user_update_form, + ) + .await?; + let recipient_local_user_view = LocalUserView::read(pool, recipient_local_user.id).await?; + + let unread_mentions_after_hide_bots = + PersonPostMentionView::get_unread_count(pool, &recipient_local_user_view.local_user).await?; + + let mut query_without_bots = query.clone(); + query_without_bots.show_bot_accounts = false; + let replies_after_hide_bots = query_without_bots.list(pool).await?; + assert_eq!(0, unread_mentions_after_hide_bots); + assert_eq!(0, replies_after_hide_bots.len()); + + Post::delete(pool, inserted_post.id).await?; + Post::delete(pool, inserted_post.id).await?; + Community::delete(pool, inserted_community.id).await?; + Person::delete(pool, inserted_person.id).await?; + Person::delete(pool, inserted_recipient.id).await?; + Instance::delete(pool, inserted_instance.id).await?; + + assert_eq!(expected_mention, read_mention); + assert_eq!(expected_mention, inserted_mention); + assert_eq!(expected_mention, updated_mention); + + Ok(()) + } +} diff --git a/crates/db_views_actor/src/structs.rs b/crates/db_views_actor/src/structs.rs index 96013a8c7..a2f95695f 100644 --- a/crates/db_views_actor/src/structs.rs +++ b/crates/db_views_actor/src/structs.rs @@ -1,13 +1,14 @@ #[cfg(feature = "full")] use diesel::Queryable; use lemmy_db_schema::{ - aggregates::structs::{CommentAggregates, CommunityAggregates, PersonAggregates}, + aggregates::structs::{CommentAggregates, CommunityAggregates, PersonAggregates, PostAggregates}, source::{ comment::Comment, comment_reply::CommentReply, community::Community, person::Person, person_comment_mention::PersonCommentMention, + person_post_mention::PersonPostMention, post::Post, }, SubscribedType, @@ -93,7 +94,7 @@ pub enum CommunitySortType { #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] -/// A person mention view. +/// A person comment mention view. pub struct PersonCommentMentionView { pub person_comment_mention: PersonCommentMention, pub comment: Comment, @@ -112,28 +113,28 @@ pub struct PersonCommentMentionView { pub my_vote: Option, } -// #[skip_serializing_none] -// #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] -// #[cfg_attr(feature = "full", derive(TS, Queryable))] -// #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -// #[cfg_attr(feature = "full", ts(export))] -// /// A person mention view. -// pub struct PersonPostMentionView { -// pub person_post_mention: PersonPostMention, -// pub creator: Person, -// pub post: Post, -// pub community: Community, -// pub recipient: Person, -// pub counts: CommentAggregates, -// pub creator_banned_from_community: bool, -// pub banned_from_community: bool, -// pub creator_is_moderator: bool, -// pub creator_is_admin: bool, -// pub subscribed: SubscribedType, -// pub saved: bool, -// pub creator_blocked: bool, -// pub my_vote: Option, -// } +#[skip_serializing_none] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A person post mention view. +pub struct PersonPostMentionView { + pub person_post_mention: PersonPostMention, + pub post: Post, + pub creator: Person, + pub community: Community, + pub recipient: Person, + pub counts: PostAggregates, + pub creator_banned_from_community: bool, + pub banned_from_community: bool, + pub creator_is_moderator: bool, + pub creator_is_admin: bool, + pub subscribed: SubscribedType, + pub saved: bool, + pub creator_blocked: bool, + pub my_vote: Option, +} #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 5aa55663f..211a459bf 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -18,7 +18,8 @@ use lemmy_db_views::{ use lemmy_db_views_actor::{ comment_reply_view::CommentReplyQuery, person_comment_mention_view::PersonCommentMentionQuery, - structs::{CommentReplyView, PersonCommentMentionView}, + person_post_mention_view::PersonPostMentionQuery, + structs::{CommentReplyView, PersonCommentMentionView, PersonPostMentionView}, }; use lemmy_utils::{ cache_header::cache_1hour, @@ -377,37 +378,53 @@ async fn get_feed_front( async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> LemmyResult { let site_view = SiteView::read_local(&mut context.pool()).await?; let local_user = local_user_view_from_jwt(jwt, context).await?; - let person_id = local_user.local_user.person_id; + let my_person_id = Some(local_user.person.id); + let recipient_id = Some(local_user.local_user.person_id); let show_bot_accounts = local_user.local_user.show_bot_accounts; - - let sort = CommentSortType::New; + let limit = Some(RSS_FETCH_LIMIT); check_private_instance(&Some(local_user.clone()), &site_view.local_site)?; let replies = CommentReplyQuery { - recipient_id: (Some(person_id)), - my_person_id: (Some(person_id)), - show_bot_accounts: (show_bot_accounts), - sort: (Some(sort)), - limit: (Some(RSS_FETCH_LIMIT)), + recipient_id, + my_person_id, + show_bot_accounts, + sort: Some(CommentSortType::New), + limit, ..Default::default() } .list(&mut context.pool()) .await?; let comment_mentions = PersonCommentMentionQuery { - recipient_id: (Some(person_id)), - my_person_id: (Some(person_id)), - show_bot_accounts: (show_bot_accounts), - sort: (Some(sort)), - limit: (Some(RSS_FETCH_LIMIT)), + recipient_id, + my_person_id, + show_bot_accounts, + sort: Some(CommentSortType::New), + limit, + ..Default::default() + } + .list(&mut context.pool()) + .await?; + + let post_mentions = PersonPostMentionQuery { + recipient_id, + my_person_id, + show_bot_accounts, + sort: Some(PostSortType::New), + limit, ..Default::default() } .list(&mut context.pool()) .await?; let protocol_and_hostname = context.settings().get_protocol_and_hostname(); - let items = create_reply_and_mention_items(replies, comment_mentions, &protocol_and_hostname)?; + let items = create_reply_and_mention_items( + replies, + comment_mentions, + post_mentions, + &protocol_and_hostname, + )?; let mut channel = Channel { namespaces: RSS_NAMESPACE.clone(), @@ -428,6 +445,7 @@ async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> LemmyResult, comment_mentions: Vec, + post_mentions: Vec, protocol_and_hostname: &str, ) -> LemmyResult> { let mut reply_items: Vec = replies @@ -459,6 +477,23 @@ fn create_reply_and_mention_items( .collect::>>()?; reply_items.append(&mut comment_mention_items); + + let mut post_mention_items: Vec = post_mentions + .iter() + .map(|m| { + let mention_url = format!("{}/post/{}", protocol_and_hostname, m.post.id); + build_item( + &m.creator.name, + &m.post.published, + &mention_url, + &m.post.body.clone().unwrap_or_default(), + protocol_and_hostname, + ) + }) + .collect::>>()?; + + reply_items.append(&mut post_mention_items); + Ok(reply_items) } diff --git a/migrations/2024-11-02-161125_add_post_body_mention/up.sql b/migrations/2024-11-02-161125_add_post_body_mention/up.sql index dffecf9c2..ae8e0bcad 100644 --- a/migrations/2024-11-02-161125_add_post_body_mention/up.sql +++ b/migrations/2024-11-02-161125_add_post_body_mention/up.sql @@ -7,6 +7,6 @@ CREATE TABLE person_post_mention ( recipient_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 boolean DEFAULT FALSE NOT NULL, - published timestamp NOT NULL DEFAULT now(), + published timestamptz NOT NULL DEFAULT now(), UNIQUE (recipient_id, post_id) ); diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index d24b907a8..18cf5bad3 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -35,9 +35,11 @@ use lemmy_api::{ logout::logout, notifications::{ list_comment_mentions::list_comment_mentions, + list_post_mentions::list_post_mentions, list_replies::list_replies, mark_all_read::mark_all_notifications_read, mark_comment_mention_read::mark_comment_mention_as_read, + mark_post_mention_read::mark_post_mention_as_read, mark_reply_read::mark_reply_as_read, unread_count::unread_count, }, @@ -334,6 +336,11 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { "/comment_mention/mark_as_read", web::post().to(mark_comment_mention_as_read), ) + .route("/post_mention", web::get().to(list_post_mentions)) + .route( + "/post_mention/mark_as_read", + web::post().to(mark_post_mention_as_read), + ) .route("/replies", web::get().to(list_replies)) // Admin action. I don't like that it's in /user .route("/ban", web::post().to(ban_from_site))