Finishing up post body mentions.

This commit is contained in:
Dessalines 2024-11-04 08:48:29 -05:00
parent 5b87cd8153
commit 6a7b1d417f
28 changed files with 1082 additions and 181 deletions

View file

@ -5,10 +5,10 @@ use lemmy_api_common::{
comment::{CommentResponse, CreateCommentLike}, comment::{CommentResponse, CreateCommentLike},
context::LemmyContext, context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData}, 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::{ use lemmy_db_schema::{
newtypes::LocalUserId, newtypes::{LocalUserId, PostOrCommentId},
source::{ source::{
comment::{CommentLike, CommentLikeForm}, comment::{CommentLike, CommentLikeForm},
comment_reply::CommentReply, comment_reply::CommentReply,
@ -33,7 +33,7 @@ pub async fn like_comment(
check_local_vote_mode( check_local_vote_mode(
data.score, data.score,
VoteItem::Comment(comment_id), PostOrCommentId::Comment(comment_id),
&local_site, &local_site,
local_user_view.person.id, local_user_view.person.id,
&mut context.pool(), &mut context.pool(),

View file

@ -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<GetPersonPostMentions>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<GetPersonPostMentionsResponse>> {
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 }))
}

View file

@ -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<MarkPersonPostMentionAsRead>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<PersonPostMentionResponse>> {
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,
}))
}

View file

@ -1,6 +1,8 @@
pub mod list_comment_mentions; pub mod list_comment_mentions;
pub mod list_post_mentions;
pub mod list_replies; pub mod list_replies;
pub mod mark_all_read; pub mod mark_all_read;
pub mod mark_comment_mention_read; pub mod mark_comment_mention_read;
pub mod mark_post_mention_read;
pub mod mark_reply_read; pub mod mark_reply_read;
pub mod unread_count; pub mod unread_count;

View file

@ -1,7 +1,11 @@
use actix_web::web::{Data, Json}; use actix_web::web::{Data, Json};
use lemmy_api_common::{context::LemmyContext, person::GetUnreadCountResponse}; use lemmy_api_common::{context::LemmyContext, person::GetUnreadCountResponse};
use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; 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; use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -14,16 +18,21 @@ pub async fn unread_count(
let replies = let replies =
CommentReplyView::get_unread_count(&mut context.pool(), &local_user_view.local_user).await?; 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) PersonCommentMentionView::get_unread_count(&mut context.pool(), &local_user_view.local_user)
.await?; .await?;
let post_mentions =
PersonPostMentionView::get_unread_count(&mut context.pool(), &local_user_view.local_user)
.await?;
let private_messages = let private_messages =
PrivateMessageView::get_unread_count(&mut context.pool(), person_id).await?; PrivateMessageView::get_unread_count(&mut context.pool(), person_id).await?;
Ok(Json(GetUnreadCountResponse { Ok(Json(GetUnreadCountResponse {
replies, replies,
mentions, comment_mentions,
post_mentions,
private_messages, private_messages,
})) }))
} }

View file

@ -10,10 +10,10 @@ use lemmy_api_common::{
check_community_user_action, check_community_user_action,
check_local_vote_mode, check_local_vote_mode,
mark_post_as_read, mark_post_as_read,
VoteItem,
}, },
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::PostOrCommentId,
source::{ source::{
community::Community, community::Community,
local_site::LocalSite, local_site::LocalSite,
@ -36,7 +36,7 @@ pub async fn like_post(
check_local_vote_mode( check_local_vote_mode(
data.score, data.score,
VoteItem::Post(post_id), PostOrCommentId::Post(post_id),
&local_site, &local_site,
local_user_view.person.id, local_user_view.person.id,
&mut context.pool(), &mut context.pool(),

View file

@ -12,13 +12,14 @@ use crate::{
}; };
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::{CommentId, CommunityId, LocalUserId, PostId}, newtypes::{CommentId, CommunityId, LocalUserId, PostId, PostOrCommentId},
source::{ source::{
actor_language::CommunityLanguage, actor_language::CommunityLanguage,
comment::Comment, comment::Comment,
comment_reply::{CommentReply, CommentReplyInsertForm}, comment_reply::{CommentReply, CommentReplyInsertForm},
person::Person, person::Person,
person_comment_mention::{PersonCommentMention, PersonCommentMentionInsertForm}, person_comment_mention::{PersonCommentMention, PersonCommentMentionInsertForm},
person_post_mention::{PersonPostMention, PersonPostMentionInsertForm},
}, },
traits::Crud, traits::Crud,
}; };
@ -92,7 +93,7 @@ pub async fn build_post_response(
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn send_local_notifs( pub async fn send_local_notifs(
mentions: Vec<MentionData>, mentions: Vec<MentionData>,
comment_id: CommentId, post_or_comment_id: PostOrCommentId,
person: &Person, person: &Person,
do_send_email: bool, do_send_email: bool,
context: &LemmyContext, context: &LemmyContext,
@ -103,15 +104,32 @@ pub async fn send_local_notifs(
// let person = my_local_user.person; // let person = my_local_user.person;
// Read the comment view to get extra info // Read the comment view to get extra info
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( let comment_view = CommentView::read(
&mut context.pool(), &mut context.pool(),
comment_id, comment_id,
local_user_view.map(|view| &view.local_user), local_user_view.map(|view| &view.local_user),
) )
.await?; .await?;
let comment = comment_view.comment; (
let post = comment_view.post; Some(comment_view.comment),
let community = comment_view.community; comment_view.post,
comment_view.community,
)
}
};
// Send the local mentions // Send the local mentions
for mention in mentions for mention in mentions
@ -127,9 +145,11 @@ pub async fn send_local_notifs(
// below by checking recipient ids // below by checking recipient ids
recipient_ids.push(mention_user_view.local_user.id); recipient_ids.push(mention_user_view.local_user.id);
// 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 { let person_comment_mention_form = PersonCommentMentionInsertForm {
recipient_id: mention_user_view.person.id, recipient_id: mention_user_view.person.id,
comment_id, comment_id: comment.id,
read: None, read: None,
}; };
@ -138,11 +158,25 @@ pub async fn send_local_notifs(
PersonCommentMention::create(&mut context.pool(), &person_comment_mention_form) PersonCommentMention::create(&mut context.pool(), &person_comment_mention_form)
.await .await
.ok(); .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 // Send an email to those local users that have notifications on
if do_send_email { if do_send_email {
let lang = get_interface_language(&mention_user_view); 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( send_email_to_user(
&mention_user_view, &mention_user_view,
&lang.notification_mentioned_by_subject(&person.name), &lang.notification_mentioned_by_subject(&person.name),
@ -155,6 +189,7 @@ pub async fn send_local_notifs(
} }
// Send comment_reply to the parent commenter / poster // Send comment_reply to the parent commenter / poster
if let Some(comment) = &comment_opt {
if let Some(parent_comment_id) = comment.parent_comment_id() { if let Some(parent_comment_id) = comment.parent_comment_id() {
let parent_comment = Comment::read(&mut context.pool(), parent_comment_id).await?; let parent_comment = Comment::read(&mut context.pool(), parent_comment_id).await?;
@ -253,6 +288,7 @@ pub async fn send_local_notifs(
} }
} }
} }
}
Ok(recipient_ids) Ok(recipient_ids)
} }

View file

@ -1,5 +1,12 @@
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::{CommentReplyId, CommunityId, LanguageId, PersonCommentMentionId, PersonId}, newtypes::{
CommentReplyId,
CommunityId,
LanguageId,
PersonCommentMentionId,
PersonId,
PersonPostMentionId,
},
sensitive::SensitiveString, sensitive::SensitiveString,
source::{login_token::LoginToken, site::Site}, source::{login_token::LoginToken, site::Site},
CommentSortType, CommentSortType,
@ -12,6 +19,7 @@ use lemmy_db_views_actor::structs::{
CommentReplyView, CommentReplyView,
CommunityModeratorView, CommunityModeratorView,
PersonCommentMentionView, PersonCommentMentionView,
PersonPostMentionView,
PersonView, PersonView,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -325,6 +333,43 @@ pub struct PersonCommentMentionResponse {
pub person_comment_mention_view: PersonCommentMentionView, 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<PostSortType>,
pub page: Option<i64>,
pub limit: Option<i64>,
pub unread_only: Option<bool>,
}
#[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<PersonPostMentionView>,
}
#[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)] #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
@ -396,7 +441,8 @@ pub struct GetReportCountResponse {
/// A response containing counts for your notifications. /// A response containing counts for your notifications.
pub struct GetUnreadCountResponse { pub struct GetUnreadCountResponse {
pub replies: i64, pub replies: i64,
pub mentions: i64, pub comment_mentions: i64,
pub post_mentions: i64,
pub private_messages: i64, pub private_messages: i64,
} }

View file

@ -11,7 +11,7 @@ use chrono::{DateTime, Days, Local, TimeZone, Utc};
use enum_map::{enum_map, EnumMap}; use enum_map::{enum_map, EnumMap};
use lemmy_db_schema::{ use lemmy_db_schema::{
aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm},
newtypes::{CommentId, CommunityId, DbUrl, InstanceId, PersonId, PostId}, newtypes::{CommentId, CommunityId, DbUrl, InstanceId, PersonId, PostId, PostOrCommentId},
source::{ source::{
comment::{Comment, CommentLike, CommentUpdateForm}, comment::{Comment, CommentLike, CommentUpdateForm},
community::{Community, CommunityModerator, CommunityUpdateForm}, community::{Community, CommunityModerator, CommunityUpdateForm},
@ -301,23 +301,17 @@ pub async fn check_person_instance_community_block(
Ok(()) Ok(())
} }
/// A vote item type used to check the vote mode.
pub enum VoteItem {
Post(PostId),
Comment(CommentId),
}
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn check_local_vote_mode( pub async fn check_local_vote_mode(
score: i16, score: i16,
vote_item: VoteItem, post_or_comment_id: PostOrCommentId,
local_site: &LocalSite, local_site: &LocalSite,
person_id: PersonId, person_id: PersonId,
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
let (downvote_setting, upvote_setting) = match vote_item { let (downvote_setting, upvote_setting) = match post_or_comment_id {
VoteItem::Post(_) => (local_site.post_downvotes, local_site.post_upvotes), PostOrCommentId::Post(_) => (local_site.post_downvotes, local_site.post_upvotes),
VoteItem::Comment(_) => (local_site.comment_downvotes, local_site.comment_upvotes), PostOrCommentId::Comment(_) => (local_site.comment_downvotes, local_site.comment_upvotes),
}; };
let downvote_fail = score == -1 && downvote_setting == FederationMode::Disable; 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 // Undo previous vote for item if new vote fails
if downvote_fail || upvote_fail { if downvote_fail || upvote_fail {
match vote_item { match post_or_comment_id {
VoteItem::Post(post_id) => PostLike::remove(pool, person_id, post_id).await?, PostOrCommentId::Post(post_id) => PostLike::remove(pool, person_id, post_id).await?,
VoteItem::Comment(comment_id) => CommentLike::remove(pool, person_id, comment_id).await?, PostOrCommentId::Comment(comment_id) => {
CommentLike::remove(pool, person_id, comment_id).await?
}
}; };
} }
Ok(()) Ok(())

View file

@ -17,6 +17,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
impls::actor_language::default_post_language, impls::actor_language::default_post_language,
newtypes::PostOrCommentId,
source::{ source::{
actor_language::CommunityLanguage, actor_language::CommunityLanguage,
comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm}, comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm},
@ -121,7 +122,7 @@ pub async fn create_comment(
let mentions = scrape_text_for_mentions(&content); let mentions = scrape_text_for_mentions(&content);
let recipient_ids = send_local_notifs( let recipient_ids = send_local_notifs(
mentions, mentions,
inserted_comment_id, PostOrCommentId::Comment(inserted_comment_id),
&local_user_view.person, &local_user_view.person,
true, true,
&context, &context,

View file

@ -8,6 +8,7 @@ use lemmy_api_common::{
utils::check_community_user_action, utils::check_community_user_action,
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::PostOrCommentId,
source::comment::{Comment, CommentUpdateForm}, source::comment::{Comment, CommentUpdateForm},
traits::Crud, traits::Crud,
}; };
@ -60,7 +61,7 @@ pub async fn delete_comment(
let recipient_ids = send_local_notifs( let recipient_ids = send_local_notifs(
vec![], vec![],
comment_id, PostOrCommentId::Comment(comment_id),
&local_user_view.person, &local_user_view.person,
false, false,
&context, &context,

View file

@ -8,6 +8,7 @@ use lemmy_api_common::{
utils::check_community_mod_action, utils::check_community_mod_action,
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::PostOrCommentId,
source::{ source::{
comment::{Comment, CommentUpdateForm}, comment::{Comment, CommentUpdateForm},
comment_report::CommentReport, comment_report::CommentReport,
@ -82,7 +83,7 @@ pub async fn remove_comment(
let recipient_ids = send_local_notifs( let recipient_ids = send_local_notifs(
vec![], vec![],
comment_id, PostOrCommentId::Comment(comment_id),
&local_user_view.person, &local_user_view.person,
false, false,
&context, &context,

View file

@ -13,6 +13,7 @@ use lemmy_api_common::{
}, },
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::PostOrCommentId,
source::{ source::{
actor_language::CommunityLanguage, actor_language::CommunityLanguage,
comment::{Comment, CommentUpdateForm}, comment::{Comment, CommentUpdateForm},
@ -87,7 +88,7 @@ pub async fn update_comment(
let mentions = scrape_text_for_mentions(&updated_comment_content); let mentions = scrape_text_for_mentions(&updated_comment_content);
let recipient_ids = send_local_notifs( let recipient_ids = send_local_notifs(
mentions, mentions,
comment_id, PostOrCommentId::Comment(comment_id),
&local_user_view.person, &local_user_view.person,
false, false,
&context, &context,

View file

@ -2,7 +2,7 @@ use super::convert_published_time;
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_api_common::{ use lemmy_api_common::{
build_response::build_post_response, build_response::{build_post_response, send_local_notifs},
context::LemmyContext, context::LemmyContext,
post::{CreatePost, PostResponse}, post::{CreatePost, PostResponse},
request::generate_post_link_metadata, request::generate_post_link_metadata,
@ -18,6 +18,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
impls::actor_language::default_post_language, impls::actor_language::default_post_language,
newtypes::PostOrCommentId,
source::{ source::{
actor_language::CommunityLanguage, actor_language::CommunityLanguage,
community::Community, community::Community,
@ -34,6 +35,7 @@ use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
spawn_try_task, spawn_try_task,
utils::{ utils::{
mention::scrape_text_for_mentions,
slurs::check_slurs, slurs::check_slurs,
validation::{ validation::{
is_url_blocked, is_url_blocked,
@ -169,6 +171,18 @@ pub async fn create_post(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?; .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?; mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
build_post_response(&context, community_id, local_user_view, post_id).await build_post_response(&context, community_id, local_user_view, post_id).await

View file

@ -2,7 +2,7 @@ use super::{convert_published_time, create::send_webmention};
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_api_common::{ use lemmy_api_common::{
build_response::build_post_response, build_response::{build_post_response, send_local_notifs},
context::LemmyContext, context::LemmyContext,
post::{EditPost, PostResponse}, post::{EditPost, PostResponse},
request::generate_post_link_metadata, request::generate_post_link_metadata,
@ -15,6 +15,7 @@ use lemmy_api_common::{
}, },
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::PostOrCommentId,
source::{ source::{
actor_language::CommunityLanguage, actor_language::CommunityLanguage,
community::Community, community::Community,
@ -28,6 +29,7 @@ use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
utils::{ utils::{
mention::scrape_text_for_mentions,
slurs::check_slurs, slurs::check_slurs,
validation::{ validation::{
is_url_blocked, is_url_blocked,
@ -142,6 +144,18 @@ pub async fn update_post(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?; .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 // send out federation/webmention if necessary
match ( match (
orig_post.scheduled_publish_time, orig_post.scheduled_publish_time,

View file

@ -29,7 +29,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
aggregates::structs::CommentAggregates, aggregates::structs::CommentAggregates,
newtypes::PersonId, newtypes::{PersonId, PostOrCommentId},
source::{ source::{
activity::ActivitySendTargets, activity::ActivitySendTargets,
comment::{Comment, CommentLike, CommentLikeForm}, 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 // TODO: for compatibility with other projects, it would be much better to read this from cc or
// tags // tags
let mentions = scrape_text_for_mentions(&comment.content); 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(()) Ok(())
} }
} }

View file

@ -20,10 +20,10 @@ use activitypub_federation::{
protocol::verification::{verify_domains_match, verify_urls_match}, protocol::verification::{verify_domains_match, verify_urls_match},
traits::{ActivityHandler, Actor, Object}, 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::{ use lemmy_db_schema::{
aggregates::structs::PostAggregates, aggregates::structs::PostAggregates,
newtypes::PersonId, newtypes::{PersonId, PostOrCommentId},
source::{ source::{
activity::ActivitySendTargets, activity::ActivitySendTargets,
community::Community, community::Community,
@ -32,7 +32,10 @@ use lemmy_db_schema::{
}, },
traits::{Crud, Likeable}, traits::{Crud, Likeable},
}; };
use lemmy_utils::error::{LemmyError, LemmyResult}; use lemmy_utils::{
error::{LemmyError, LemmyResult},
utils::mention::scrape_text_for_mentions,
};
use url::Url; use url::Url;
impl CreateOrUpdatePage { impl CreateOrUpdatePage {
@ -128,6 +131,21 @@ impl ActivityHandler for CreateOrUpdatePage {
// Calculate initial hot_rank for post // Calculate initial hot_rank for post
PostAggregates::update_ranks(&mut context.pool(), post.id).await?; 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(()) Ok(())
} }
} }

View file

@ -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<Self, Error> {
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::<Self>(conn)
.await
}
async fn update(
pool: &mut DbPool<'_>,
person_post_mention_id: PersonPostMentionId,
person_post_mention_form: &Self::UpdateForm,
) -> Result<Self, Error> {
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::<Self>(conn)
.await
}
}
impl PersonPostMention {
pub async fn mark_all_as_read(
pool: &mut DbPool<'_>,
for_recipient_id: PersonId,
) -> Result<Vec<PersonPostMention>, 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::<Self>(conn)
.await
}
pub async fn read_by_post_and_person(
pool: &mut DbPool<'_>,
for_post_id: PostId,
for_recipient_id: PersonId,
) -> Result<Option<Self>, 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()
}
}

View file

@ -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)] #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", derive(DieselNewType, TS))]
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]

View file

@ -752,7 +752,7 @@ diesel::table! {
recipient_id -> Int4, recipient_id -> Int4,
post_id -> Int4, post_id -> Int4,
read -> Bool, read -> Bool,
published -> Timestamp, published -> Timestamptz,
} }
} }

View file

@ -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<Utc>,
}
#[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<bool>,
}
#[cfg_attr(feature = "full", derive(AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = person_post_mention))]
pub struct PersonPostMentionUpdateForm {
pub read: Option<bool>,
}

View file

@ -10,8 +10,8 @@ pub mod community_person_ban_view;
pub mod community_view; pub mod community_view;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod person_comment_mention_view; pub mod person_comment_mention_view;
// #[cfg(feature = "full")] #[cfg(feature = "full")]
// pub mod person_post_mention_view; pub mod person_post_mention_view;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod person_view; pub mod person_view;
pub mod structs; pub mod structs;

View file

@ -216,7 +216,7 @@ fn queries<'a>() -> Queries<
query = query.filter(not(person::bot_account)); 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::Hot => query.then_order_by(comment_aggregates::hot_rank.desc()),
CommentSortType::Controversial => { CommentSortType::Controversial => {
query.then_order_by(comment_aggregates::controversy_rank.desc()) query.then_order_by(comment_aggregates::controversy_rank.desc())

View file

@ -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<PersonId>)>,
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<PersonId>| {
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::<sql_types::Bool>())
};
let score_selection: Box<
dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable<sql_types::SmallInt>>,
> = if let Some(person_id) = my_person_id {
Box::new(score(person_id))
} else {
Box::new(None::<i16>.into_sql::<sql_types::Nullable<sql_types::SmallInt>>())
};
let subscribed_type_selection: Box<
dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable<sql_types::Bool>>,
> = if let Some(person_id) = my_person_id {
Box::new(is_community_followed(person_id))
} else {
Box::new(None::<bool>.into_sql::<sql_types::Nullable<sql_types::Bool>>())
};
let is_saved_selection: Box<dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>> =
if let Some(person_id) = my_person_id {
Box::new(is_saved(person_id))
} else {
Box::new(false.into_sql::<sql_types::Bool>())
};
let is_creator_blocked_selection: Box<dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>> =
if let Some(person_id) = my_person_id {
Box::new(is_creator_blocked(person_id))
} else {
Box::new(false.into_sql::<sql_types::Bool>())
};
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<PersonId>,
)| 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::<PersonPostMentionView>(&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<PersonId>,
) -> Result<Self, Error> {
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<i64, Error> {
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::<i64>(conn)
.await
}
}
#[derive(Default, Clone)]
pub struct PersonPostMentionQuery {
pub my_person_id: Option<PersonId>,
pub recipient_id: Option<PersonId>,
pub sort: Option<PostSortType>,
pub unread_only: bool,
pub show_bot_accounts: bool,
pub page: Option<i64>,
pub limit: Option<i64>,
}
impl PersonPostMentionQuery {
pub async fn list(self, pool: &mut DbPool<'_>) -> Result<Vec<PersonPostMentionView>, 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(())
}
}

View file

@ -1,13 +1,14 @@
#[cfg(feature = "full")] #[cfg(feature = "full")]
use diesel::Queryable; use diesel::Queryable;
use lemmy_db_schema::{ use lemmy_db_schema::{
aggregates::structs::{CommentAggregates, CommunityAggregates, PersonAggregates}, aggregates::structs::{CommentAggregates, CommunityAggregates, PersonAggregates, PostAggregates},
source::{ source::{
comment::Comment, comment::Comment,
comment_reply::CommentReply, comment_reply::CommentReply,
community::Community, community::Community,
person::Person, person::Person,
person_comment_mention::PersonCommentMention, person_comment_mention::PersonCommentMention,
person_post_mention::PersonPostMention,
post::Post, post::Post,
}, },
SubscribedType, SubscribedType,
@ -93,7 +94,7 @@ pub enum CommunitySortType {
#[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", derive(TS, Queryable))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// A person mention view. /// A person comment mention view.
pub struct PersonCommentMentionView { pub struct PersonCommentMentionView {
pub person_comment_mention: PersonCommentMention, pub person_comment_mention: PersonCommentMention,
pub comment: Comment, pub comment: Comment,
@ -112,28 +113,28 @@ pub struct PersonCommentMentionView {
pub my_vote: Option<i16>, pub my_vote: Option<i16>,
} }
// #[skip_serializing_none] #[skip_serializing_none]
// #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
// #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", derive(TS, Queryable))]
// #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
// #[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
// /// A person mention view. /// A person post mention view.
// pub struct PersonPostMentionView { pub struct PersonPostMentionView {
// pub person_post_mention: PersonPostMention, pub person_post_mention: PersonPostMention,
// pub creator: Person, pub post: Post,
// pub post: Post, pub creator: Person,
// pub community: Community, pub community: Community,
// pub recipient: Person, pub recipient: Person,
// pub counts: CommentAggregates, pub counts: PostAggregates,
// pub creator_banned_from_community: bool, pub creator_banned_from_community: bool,
// pub banned_from_community: bool, pub banned_from_community: bool,
// pub creator_is_moderator: bool, pub creator_is_moderator: bool,
// pub creator_is_admin: bool, pub creator_is_admin: bool,
// pub subscribed: SubscribedType, pub subscribed: SubscribedType,
// pub saved: bool, pub saved: bool,
// pub creator_blocked: bool, pub creator_blocked: bool,
// pub my_vote: Option<i16>, pub my_vote: Option<i16>,
// } }
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]

View file

@ -18,7 +18,8 @@ use lemmy_db_views::{
use lemmy_db_views_actor::{ use lemmy_db_views_actor::{
comment_reply_view::CommentReplyQuery, comment_reply_view::CommentReplyQuery,
person_comment_mention_view::PersonCommentMentionQuery, person_comment_mention_view::PersonCommentMentionQuery,
structs::{CommentReplyView, PersonCommentMentionView}, person_post_mention_view::PersonPostMentionQuery,
structs::{CommentReplyView, PersonCommentMentionView, PersonPostMentionView},
}; };
use lemmy_utils::{ use lemmy_utils::{
cache_header::cache_1hour, cache_header::cache_1hour,
@ -377,37 +378,53 @@ async fn get_feed_front(
async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> LemmyResult<Channel> { async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> LemmyResult<Channel> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool()).await?;
let local_user = local_user_view_from_jwt(jwt, context).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 show_bot_accounts = local_user.local_user.show_bot_accounts;
let limit = Some(RSS_FETCH_LIMIT);
let sort = CommentSortType::New;
check_private_instance(&Some(local_user.clone()), &site_view.local_site)?; check_private_instance(&Some(local_user.clone()), &site_view.local_site)?;
let replies = CommentReplyQuery { let replies = CommentReplyQuery {
recipient_id: (Some(person_id)), recipient_id,
my_person_id: (Some(person_id)), my_person_id,
show_bot_accounts: (show_bot_accounts), show_bot_accounts,
sort: (Some(sort)), sort: Some(CommentSortType::New),
limit: (Some(RSS_FETCH_LIMIT)), limit,
..Default::default() ..Default::default()
} }
.list(&mut context.pool()) .list(&mut context.pool())
.await?; .await?;
let comment_mentions = PersonCommentMentionQuery { let comment_mentions = PersonCommentMentionQuery {
recipient_id: (Some(person_id)), recipient_id,
my_person_id: (Some(person_id)), my_person_id,
show_bot_accounts: (show_bot_accounts), show_bot_accounts,
sort: (Some(sort)), sort: Some(CommentSortType::New),
limit: (Some(RSS_FETCH_LIMIT)), 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() ..Default::default()
} }
.list(&mut context.pool()) .list(&mut context.pool())
.await?; .await?;
let protocol_and_hostname = context.settings().get_protocol_and_hostname(); 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 { let mut channel = Channel {
namespaces: RSS_NAMESPACE.clone(), namespaces: RSS_NAMESPACE.clone(),
@ -428,6 +445,7 @@ async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> LemmyResult<Channe
fn create_reply_and_mention_items( fn create_reply_and_mention_items(
replies: Vec<CommentReplyView>, replies: Vec<CommentReplyView>,
comment_mentions: Vec<PersonCommentMentionView>, comment_mentions: Vec<PersonCommentMentionView>,
post_mentions: Vec<PersonPostMentionView>,
protocol_and_hostname: &str, protocol_and_hostname: &str,
) -> LemmyResult<Vec<Item>> { ) -> LemmyResult<Vec<Item>> {
let mut reply_items: Vec<Item> = replies let mut reply_items: Vec<Item> = replies
@ -459,6 +477,23 @@ fn create_reply_and_mention_items(
.collect::<LemmyResult<Vec<Item>>>()?; .collect::<LemmyResult<Vec<Item>>>()?;
reply_items.append(&mut comment_mention_items); reply_items.append(&mut comment_mention_items);
let mut post_mention_items: Vec<Item> = 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::<LemmyResult<Vec<Item>>>()?;
reply_items.append(&mut post_mention_items);
Ok(reply_items) Ok(reply_items)
} }

View file

@ -7,6 +7,6 @@ CREATE TABLE person_post_mention (
recipient_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, 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, post_id int REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
read boolean DEFAULT FALSE 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) UNIQUE (recipient_id, post_id)
); );

View file

@ -35,9 +35,11 @@ use lemmy_api::{
logout::logout, logout::logout,
notifications::{ notifications::{
list_comment_mentions::list_comment_mentions, list_comment_mentions::list_comment_mentions,
list_post_mentions::list_post_mentions,
list_replies::list_replies, list_replies::list_replies,
mark_all_read::mark_all_notifications_read, mark_all_read::mark_all_notifications_read,
mark_comment_mention_read::mark_comment_mention_as_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, mark_reply_read::mark_reply_as_read,
unread_count::unread_count, unread_count::unread_count,
}, },
@ -334,6 +336,11 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
"/comment_mention/mark_as_read", "/comment_mention/mark_as_read",
web::post().to(mark_comment_mention_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)) .route("/replies", web::get().to(list_replies))
// Admin action. I don't like that it's in /user // Admin action. I don't like that it's in /user
.route("/ban", web::post().to(ban_from_site)) .route("/ban", web::post().to(ban_from_site))