From 05f218d53cf2aacf4c7e7d650b62017dea526a99 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 11 Dec 2024 15:06:08 -0500 Subject: [PATCH] Most of the bulk work done, need to add tests yet. --- Cargo.lock | 3 + .../notifications/list_comment_mentions.rs | 36 - .../local_user/notifications/list_inbox.rs | 39 + .../notifications/list_post_mentions.rs | 36 - .../local_user/notifications/list_replies.rs | 36 - .../local_user/notifications/mark_all_read.rs | 12 +- .../api/src/local_user/notifications/mod.rs | 4 +- .../local_user/notifications/unread_count.rs | 31 +- crates/api/src/post/like.rs | 7 +- crates/api/src/private_message/mark_read.rs | 3 +- crates/api_common/src/build_response.rs | 60 +- crates/api_common/src/person.rs | 71 +- crates/api_common/src/private_message.rs | 27 +- crates/api_common/src/send_activity.rs | 2 +- crates/api_crud/src/comment/create.rs | 2 +- crates/api_crud/src/comment/update.rs | 1 + crates/api_crud/src/post/create.rs | 7 +- crates/api_crud/src/private_message/create.rs | 3 +- crates/api_crud/src/private_message/delete.rs | 3 +- crates/api_crud/src/private_message/mod.rs | 1 - crates/api_crud/src/private_message/read.rs | 33 - crates/api_crud/src/private_message/update.rs | 3 +- .../create_or_update/private_message.rs | 2 +- .../db_schema/replaceable_schema/triggers.sql | 32 + crates/db_schema/src/newtypes.rs | 13 +- crates/db_schema/src/schema.rs | 16 + crates/db_schema/src/source/combined/inbox.rs | 33 + crates/db_schema/src/source/combined/mod.rs | 1 + crates/db_views/src/lib.rs | 2 - crates/db_views/src/structs.rs | 11 - crates/db_views_actor/Cargo.toml | 5 + .../db_views_actor/src/comment_reply_view.rs | 7 +- .../db_views_actor/src/inbox_combined_view.rs | 971 ++++++++++++++++++ crates/db_views_actor/src/lib.rs | 6 + .../src/person_comment_mention_view.rs | 309 +----- .../src/person_post_mention_view.rs | 103 ++ .../src/private_message_view.rs | 0 crates/db_views_actor/src/structs.rs | 74 ++ crates/routes/src/feeds.rs | 141 +-- .../up.sql | 12 - .../down.sql | 4 +- .../up.sql | 71 ++ src/api_routes_v3.rs | 17 +- src/api_routes_v4.rs | 19 +- 44 files changed, 1528 insertions(+), 741 deletions(-) delete mode 100644 crates/api/src/local_user/notifications/list_comment_mentions.rs create mode 100644 crates/api/src/local_user/notifications/list_inbox.rs delete mode 100644 crates/api/src/local_user/notifications/list_post_mentions.rs delete mode 100644 crates/api/src/local_user/notifications/list_replies.rs delete mode 100644 crates/api_crud/src/private_message/read.rs create mode 100644 crates/db_schema/src/source/combined/inbox.rs create mode 100644 crates/db_views_actor/src/inbox_combined_view.rs create mode 100644 crates/db_views_actor/src/person_post_mention_view.rs rename crates/{db_views => db_views_actor}/src/private_message_view.rs (100%) delete mode 100644 migrations/2024-11-02-161125_add_post_body_mention/up.sql rename migrations/{2024-11-02-161125_add_post_body_mention => 2024-12-10-193418_add_inbox_combined_table}/down.sql (63%) create mode 100644 migrations/2024-12-10-193418_add_inbox_combined_table/up.sql diff --git a/Cargo.lock b/Cargo.lock index a9eecc636..3468709ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2699,8 +2699,10 @@ name = "lemmy_db_views_actor" version = "0.19.6-beta.7" dependencies = [ "chrono", + "derive-new", "diesel", "diesel-async", + "i-love-jesus", "lemmy_db_schema", "lemmy_db_views", "lemmy_utils", @@ -2710,6 +2712,7 @@ dependencies = [ "serial_test", "strum", "tokio", + "tracing", "ts-rs", "url", ] diff --git a/crates/api/src/local_user/notifications/list_comment_mentions.rs b/crates/api/src/local_user/notifications/list_comment_mentions.rs deleted file mode 100644 index 4617844ba..000000000 --- a/crates/api/src/local_user/notifications/list_comment_mentions.rs +++ /dev/null @@ -1,36 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - context::LemmyContext, - person::{GetPersonCommentMentions, GetPersonCommentMentionsResponse}, -}; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::person_comment_mention_view::PersonCommentMentionQuery; -use lemmy_utils::error::LemmyResult; - -#[tracing::instrument(skip(context))] -pub async fn list_comment_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 comment_mentions = PersonCommentMentionQuery { - recipient_id: person_id, - my_person_id: person_id, - sort, - unread_only, - show_bot_accounts, - page, - limit, - } - .list(&mut context.pool()) - .await?; - - Ok(Json(GetPersonCommentMentionsResponse { comment_mentions })) -} diff --git a/crates/api/src/local_user/notifications/list_inbox.rs b/crates/api/src/local_user/notifications/list_inbox.rs new file mode 100644 index 000000000..259a3b778 --- /dev/null +++ b/crates/api/src/local_user/notifications/list_inbox.rs @@ -0,0 +1,39 @@ +use actix_web::web::{Data, Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + person::{ListInbox, ListInboxResponse}, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::inbox_combined_view::InboxCombinedQuery; +use lemmy_utils::error::LemmyResult; + +#[tracing::instrument(skip(context))] +pub async fn list_inbox( + data: Query, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let unread_only = data.unread_only; + let person_id = local_user_view.person.id; + let show_bot_accounts = Some(local_user_view.local_user.show_bot_accounts); + + // parse pagination token + let page_after = if let Some(pa) = &data.page_cursor { + Some(pa.read(&mut context.pool()).await?) + } else { + None + }; + let page_back = data.page_back; + + let inbox = InboxCombinedQuery { + my_person_id: person_id, + unread_only, + show_bot_accounts, + page_after, + page_back, + } + .list(&mut context.pool()) + .await?; + + Ok(Json(ListInboxResponse { inbox })) +} diff --git a/crates/api/src/local_user/notifications/list_post_mentions.rs b/crates/api/src/local_user/notifications/list_post_mentions.rs deleted file mode 100644 index e39dc59af..000000000 --- a/crates/api/src/local_user/notifications/list_post_mentions.rs +++ /dev/null @@ -1,36 +0,0 @@ -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/list_replies.rs b/crates/api/src/local_user/notifications/list_replies.rs deleted file mode 100644 index d88595d96..000000000 --- a/crates/api/src/local_user/notifications/list_replies.rs +++ /dev/null @@ -1,36 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - context::LemmyContext, - person::{GetReplies, GetRepliesResponse}, -}; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::comment_reply_view::CommentReplyQuery; -use lemmy_utils::error::LemmyResult; - -#[tracing::instrument(skip(context))] -pub async fn list_replies( - 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 replies = CommentReplyQuery { - recipient_id: person_id, - my_person_id: person_id, - sort, - unread_only, - show_bot_accounts, - page, - limit, - } - .list(&mut context.pool()) - .await?; - - Ok(Json(GetRepliesResponse { replies })) -} 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 929af5ac3..9ba0916f8 100644 --- a/crates/api/src/local_user/notifications/mark_all_read.rs +++ b/crates/api/src/local_user/notifications/mark_all_read.rs @@ -1,8 +1,9 @@ use actix_web::web::{Data, Json}; -use lemmy_api_common::{context::LemmyContext, person::GetRepliesResponse}; +use lemmy_api_common::{context::LemmyContext, SuccessResponse}; use lemmy_db_schema::source::{ comment_reply::CommentReply, person_comment_mention::PersonCommentMention, + person_post_mention::PersonPostMention, private_message::PrivateMessage, }; use lemmy_db_views::structs::LocalUserView; @@ -12,7 +13,7 @@ use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; pub async fn mark_all_notifications_read( context: Data, local_user_view: LocalUserView, -) -> LemmyResult> { +) -> LemmyResult> { let person_id = local_user_view.person.id; // Mark all comment_replies as read @@ -25,10 +26,15 @@ pub async fn mark_all_notifications_read( .await .with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?; + // Mark all post mentions as read + PersonPostMention::mark_all_as_read(&mut context.pool(), person_id) + .await + .with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?; + // Mark all private_messages as read PrivateMessage::mark_all_as_read(&mut context.pool(), person_id) .await .with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?; - Ok(Json(GetRepliesResponse { replies: vec![] })) + Ok(Json(SuccessResponse::default())) } diff --git a/crates/api/src/local_user/notifications/mod.rs b/crates/api/src/local_user/notifications/mod.rs index e62905109..9f2048d90 100644 --- a/crates/api/src/local_user/notifications/mod.rs +++ b/crates/api/src/local_user/notifications/mod.rs @@ -1,6 +1,4 @@ -pub mod list_comment_mentions; -pub mod list_post_mentions; -pub mod list_replies; +pub mod list_inbox; pub mod mark_all_read; pub mod mark_comment_mention_read; pub mod mark_post_mention_read; diff --git a/crates/api/src/local_user/notifications/unread_count.rs b/crates/api/src/local_user/notifications/unread_count.rs index 62bf7a0c9..4fa959329 100644 --- a/crates/api/src/local_user/notifications/unread_count.rs +++ b/crates/api/src/local_user/notifications/unread_count.rs @@ -1,11 +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, - PersonCommentMentionView, - PersonPostMentionView, -}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::InboxCombinedViewInternal; use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] @@ -14,25 +10,10 @@ pub async fn unread_count( local_user_view: LocalUserView, ) -> LemmyResult> { let person_id = local_user_view.person.id; - - let replies = - CommentReplyView::get_unread_count(&mut context.pool(), &local_user_view.local_user).await?; - - let comment_mentions = - PersonCommentMentionView::get_unread_count(&mut context.pool(), &local_user_view.local_user) + let show_bot_accounts = local_user_view.local_user.show_bot_accounts; + let count = + InboxCombinedViewInternal::get_unread_count(&mut context.pool(), person_id, show_bot_accounts) .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, - comment_mentions, - post_mentions, - private_messages, - })) + Ok(Json(GetUnreadCountResponse { count })) } diff --git a/crates/api/src/post/like.rs b/crates/api/src/post/like.rs index acf55b60e..6555228e9 100644 --- a/crates/api/src/post/like.rs +++ b/crates/api/src/post/like.rs @@ -5,12 +5,7 @@ use lemmy_api_common::{ context::LemmyContext, post::{CreatePostLike, PostResponse}, send_activity::{ActivityChannel, SendActivityData}, - utils::{ - check_bot_account, - check_community_user_action, - check_local_vote_mode, - mark_post_as_read, - }, + utils::{check_bot_account, check_community_user_action, check_local_vote_mode}, }; use lemmy_db_schema::{ newtypes::PostOrCommentId, diff --git a/crates/api/src/private_message/mark_read.rs b/crates/api/src/private_message/mark_read.rs index 7c213464b..26655caef 100644 --- a/crates/api/src/private_message/mark_read.rs +++ b/crates/api/src/private_message/mark_read.rs @@ -7,7 +7,8 @@ use lemmy_db_schema::{ source::private_message::{PrivateMessage, PrivateMessageUpdateForm}, traits::Crud, }; -use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::PrivateMessageView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] diff --git a/crates/api_common/src/build_response.rs b/crates/api_common/src/build_response.rs index 9683de9cf..0245a0459 100644 --- a/crates/api_common/src/build_response.rs +++ b/crates/api_common/src/build_response.rs @@ -21,6 +21,7 @@ use lemmy_db_schema::{ person::Person, person_comment_mention::{PersonCommentMention, PersonCommentMentionInsertForm}, person_post_mention::{PersonPostMention, PersonPostMentionInsertForm}, + post::Post, }, traits::Crud, }; @@ -103,31 +104,6 @@ pub async fn send_local_notifs( let mut recipient_ids = Vec::new(); let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); - // When called from api code, we have local user view and can read with CommentView - // to reduce db queries. But when receiving a federated comment the user view is None, - // which means that comments inside private communities cant be read. As a workaround - // we need to read the items manually to bypass this check. - let (comment, post, community) = if let Some(local_user_view) = local_user_view { - let comment_view = CommentView::read( - &mut context.pool(), - comment_id, - Some(&local_user_view.local_user), - ) - .await?; - ( - comment_view.comment, - comment_view.post, - comment_view.community, - ) - } else { - let comment = Comment::read(&mut context.pool(), comment_id).await?; - let post = Post::read(&mut context.pool(), comment.post_id).await?; - let community = Community::read(&mut context.pool(), post.community_id).await?; - (comment, post, community) - }; - // let person = my_local_user.person; - // 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( @@ -140,17 +116,29 @@ pub async fn send_local_notifs( (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, - ) + // When called from api code, we have local user view and can read with CommentView + // to reduce db queries. But when receiving a federated comment the user view is None, + // which means that comments inside private communities cant be read. As a workaround + // we need to read the items manually to bypass this check. + if let Some(local_user_view) = local_user_view { + // Read the comment view to get extra info + let comment_view = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + ) + .await?; + ( + Some(comment_view.comment), + comment_view.post, + comment_view.community, + ) + } else { + let comment = Comment::read(&mut context.pool(), comment_id).await?; + let post = Post::read(&mut context.pool(), comment.post_id).await?; + let community = Community::read(&mut context.pool(), post.community_id).await?; + (Some(comment), post, community) + } } }; diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index cb2a6f85b..bde6b59b1 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -23,6 +23,8 @@ use lemmy_db_views::structs::{ use lemmy_db_views_actor::structs::{ CommentReplyView, CommunityModeratorView, + InboxCombinedPaginationCursor, + InboxCombinedView, PersonCommentMentionView, PersonPostMentionView, PersonView, @@ -373,51 +375,25 @@ pub struct BlockPersonResponse { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] -/// Get comment replies. -pub struct GetReplies { - #[cfg_attr(feature = "full", ts(optional))] - pub sort: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, +/// Get your inbox (replies, comment mentions, post mentions, and messages) +pub struct ListInbox { #[cfg_attr(feature = "full", ts(optional))] pub unread_only: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Fetches your replies. -// TODO, replies and mentions below should be redone as tagged enums. -pub struct GetRepliesResponse { - pub replies: Vec, -} - -#[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 GetPersonCommentMentions { - pub sort: Option, #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, + pub page_cursor: Option, #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub unread_only: Option, + pub page_back: 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 GetPersonCommentMentionsResponse { - pub comment_mentions: Vec, +/// Get your inbox (replies, comment mentions, post mentions, and messages) +pub struct ListInboxResponse { + pub inbox: Vec, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] @@ -437,26 +413,6 @@ 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))] @@ -540,12 +496,9 @@ pub struct GetReportCountResponse { #[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] -/// A response containing counts for your notifications. +/// A response containing a count of unread notifications. pub struct GetUnreadCountResponse { - pub replies: i64, - pub comment_mentions: i64, - pub post_mentions: i64, - pub private_messages: i64, + pub count: i64, } #[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, Hash)] diff --git a/crates/api_common/src/private_message.rs b/crates/api_common/src/private_message.rs index 8bd417a8e..f8134ea27 100644 --- a/crates/api_common/src/private_message.rs +++ b/crates/api_common/src/private_message.rs @@ -1,7 +1,6 @@ use lemmy_db_schema::newtypes::{PersonId, PrivateMessageId}; -use lemmy_db_views::structs::PrivateMessageView; +use lemmy_db_views_actor::structs::PrivateMessageView; use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; @@ -41,30 +40,6 @@ pub struct MarkPrivateMessageAsRead { pub read: bool, } -#[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 your private messages. -pub struct GetPrivateMessages { - #[cfg_attr(feature = "full", ts(optional))] - pub unread_only: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub creator_id: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The private messages response. -pub struct PrivateMessagesResponse { - pub private_messages: Vec, -} - #[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] diff --git a/crates/api_common/src/send_activity.rs b/crates/api_common/src/send_activity.rs index b606c9a90..07203ffe4 100644 --- a/crates/api_common/src/send_activity.rs +++ b/crates/api_common/src/send_activity.rs @@ -11,7 +11,7 @@ use lemmy_db_schema::{ private_message::PrivateMessage, }, }; -use lemmy_db_views::structs::PrivateMessageView; +use lemmy_db_views_actor::structs::PrivateMessageView; use lemmy_utils::error::LemmyResult; use std::sync::{LazyLock, OnceLock}; use tokio::{ diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index ed3f7402c..692b85c17 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -16,7 +16,7 @@ use lemmy_api_common::{ }, }; use lemmy_db_schema::{ - impls::actor_language::default_post_language, + impls::actor_language::validate_post_language, newtypes::PostOrCommentId, source::{ comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm}, diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index 8a3cf35b0..3cb1a3a4e 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -14,6 +14,7 @@ use lemmy_api_common::{ }, }; use lemmy_db_schema::{ + impls::actor_language::validate_post_language, newtypes::PostOrCommentId, source::{ comment::{Comment, CommentUpdateForm}, diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index 75c1021c4..860e82f44 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -16,7 +16,7 @@ use lemmy_api_common::{ }, }; use lemmy_db_schema::{ - impls::actor_language::default_post_language, + impls::actor_language::validate_post_language, newtypes::PostOrCommentId, source::{ community::Community, @@ -162,9 +162,8 @@ pub async fn create_post( ) .await?; - // TODO -PostRead::mark_as_read(&mut context.pool(), &read_form).await?; - mark_post_as_read(person_id, post_id, &mut context.pool()).await?; + let read_form = PostReadForm::new(post_id, person_id); + PostRead::mark_as_read(&mut context.pool(), &read_form).await?; build_post_response(&context, community_id, local_user_view, post_id).await } diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs index 1a6a78d00..fd95a2b9e 100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@ -21,7 +21,8 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::PrivateMessageView; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::{markdown::markdown_to_html, validation::is_valid_body_field}, diff --git a/crates/api_crud/src/private_message/delete.rs b/crates/api_crud/src/private_message/delete.rs index 30efc020c..d06c8bc04 100644 --- a/crates/api_crud/src/private_message/delete.rs +++ b/crates/api_crud/src/private_message/delete.rs @@ -9,7 +9,8 @@ use lemmy_db_schema::{ source::private_message::{PrivateMessage, PrivateMessageUpdateForm}, traits::Crud, }; -use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::PrivateMessageView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] diff --git a/crates/api_crud/src/private_message/mod.rs b/crates/api_crud/src/private_message/mod.rs index ab7fa4390..fdb2f5561 100644 --- a/crates/api_crud/src/private_message/mod.rs +++ b/crates/api_crud/src/private_message/mod.rs @@ -1,4 +1,3 @@ pub mod create; pub mod delete; -pub mod read; pub mod update; diff --git a/crates/api_crud/src/private_message/read.rs b/crates/api_crud/src/private_message/read.rs deleted file mode 100644 index 7558b97fc..000000000 --- a/crates/api_crud/src/private_message/read.rs +++ /dev/null @@ -1,33 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - context::LemmyContext, - private_message::{GetPrivateMessages, PrivateMessagesResponse}, -}; -use lemmy_db_views::{private_message_view::PrivateMessageQuery, structs::LocalUserView}; -use lemmy_utils::error::LemmyResult; - -#[tracing::instrument(skip(context))] -pub async fn get_private_message( - data: Query, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - let person_id = local_user_view.person.id; - - let page = data.page; - let limit = data.limit; - let unread_only = data.unread_only.unwrap_or_default(); - let creator_id = data.creator_id; - let messages = PrivateMessageQuery { - page, - limit, - unread_only, - creator_id, - } - .list(&mut context.pool(), person_id) - .await?; - - Ok(Json(PrivateMessagesResponse { - private_messages: messages, - })) -} diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs index b9e4785ef..22c1da4a2 100644 --- a/crates/api_crud/src/private_message/update.rs +++ b/crates/api_crud/src/private_message/update.rs @@ -14,7 +14,8 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::PrivateMessageView; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::validation::is_valid_body_field, diff --git a/crates/apub/src/activities/create_or_update/private_message.rs b/crates/apub/src/activities/create_or_update/private_message.rs index b6e7478ef..ce04a9330 100644 --- a/crates/apub/src/activities/create_or_update/private_message.rs +++ b/crates/apub/src/activities/create_or_update/private_message.rs @@ -14,7 +14,7 @@ use activitypub_federation::{ }; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::source::activity::ActivitySendTargets; -use lemmy_db_views::structs::PrivateMessageView; +use lemmy_db_views_actor::structs::PrivateMessageView; use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index b7eb0209b..19355846f 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -836,3 +836,35 @@ CALL r.create_modlog_combined_trigger ('mod_remove_post'); CALL r.create_modlog_combined_trigger ('mod_transfer_community'); + +-- Inbox: (replies, comment mentions, post mentions, and private_messages) +CREATE PROCEDURE r.create_inbox_combined_trigger (table_name text) +LANGUAGE plpgsql +AS $a$ +BEGIN + EXECUTE replace($b$ CREATE FUNCTION r.inbox_combined_thing_insert ( ) + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ + BEGIN + INSERT INTO inbox_combined (published, thing_id) + VALUES (NEW.published, NEW.id); + RETURN NEW; + END $$; + CREATE TRIGGER inbox_combined + AFTER INSERT ON thing + FOR EACH ROW + EXECUTE FUNCTION r.inbox_combined_thing_insert ( ); + $b$, + 'thing', + table_name); +END; +$a$; + +CALL r.create_inbox_combined_trigger ('comment_reply'); + +CALL r.create_inbox_combined_trigger ('person_comment_mention'); + +CALL r.create_inbox_combined_trigger ('person_post_mention'); + +CALL r.create_inbox_combined_trigger ('private_message'); diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index 8184cda5a..711d83684 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -76,7 +76,7 @@ pub struct LocalUserId(pub i32); #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The private message id. -pub struct PrivateMessageId(i32); +pub struct PrivateMessageId(pub i32); impl fmt::Display for PrivateMessageId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -88,13 +88,13 @@ impl fmt::Display for PrivateMessageId { #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The person comment mention id. -pub struct PersonCommentMentionId(i32); +pub struct PersonCommentMentionId(pub 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); +pub struct PersonPostMentionId(pub i32); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] @@ -130,7 +130,7 @@ pub struct LanguageId(pub i32); #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The comment reply id. -pub struct CommentReplyId(i32); +pub struct CommentReplyId(pub i32); #[derive( Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default, Ord, PartialOrd, @@ -211,6 +211,11 @@ pub struct PersonSavedCombinedId(i32); #[cfg_attr(feature = "full", derive(DieselNewType))] pub struct ModlogCombinedId(i32); +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType))] +/// The inbox combined id +pub struct InboxCombinedId(i32); + #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[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 15f0cd0f9..cb430a3fe 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -330,6 +330,17 @@ diesel::table! { } } +diesel::table! { + inbox_combined (id) { + id -> Int4, + published -> Timestamptz, + comment_reply_id -> Nullable, + person_comment_mention_id -> Nullable, + person_post_mention_id -> Nullable, + private_message_id -> Nullable, + } +} + diesel::table! { instance (id) { id -> Int4, @@ -1052,6 +1063,10 @@ diesel::joinable!(email_verification -> local_user (local_user_id)); diesel::joinable!(federation_allowlist -> instance (instance_id)); diesel::joinable!(federation_blocklist -> instance (instance_id)); diesel::joinable!(federation_queue_state -> instance (instance_id)); +diesel::joinable!(inbox_combined -> comment_reply (comment_reply_id)); +diesel::joinable!(inbox_combined -> person_comment_mention (person_comment_mention_id)); +diesel::joinable!(inbox_combined -> person_post_mention (person_post_mention_id)); +diesel::joinable!(inbox_combined -> private_message (private_message_id)); diesel::joinable!(instance_actions -> instance (instance_id)); diesel::joinable!(instance_actions -> person (person_id)); diesel::joinable!(local_image -> local_user (local_user_id)); @@ -1154,6 +1169,7 @@ diesel::allow_tables_to_appear_in_same_query!( federation_blocklist, federation_queue_state, image_details, + inbox_combined, instance, instance_actions, language, diff --git a/crates/db_schema/src/source/combined/inbox.rs b/crates/db_schema/src/source/combined/inbox.rs new file mode 100644 index 000000000..523dd5040 --- /dev/null +++ b/crates/db_schema/src/source/combined/inbox.rs @@ -0,0 +1,33 @@ +use crate::newtypes::{ + CommentReplyId, + InboxCombinedId, + PersonCommentMentionId, + PersonPostMentionId, + PrivateMessageId, +}; +#[cfg(feature = "full")] +use crate::schema::inbox_combined; +use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use i_love_jesus::CursorKeysModule; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +#[skip_serializing_none] +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[cfg_attr( + feature = "full", + derive(Identifiable, Queryable, Selectable, CursorKeysModule) +)] +#[cfg_attr(feature = "full", diesel(table_name = inbox_combined))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", cursor_keys_module(name = inbox_combined_keys))] +/// A combined inbox table. +pub struct InboxCombined { + pub id: InboxCombinedId, + pub published: DateTime, + pub comment_reply_id: Option, + pub person_comment_mention_id: Option, + pub person_post_mention_id: Option, + pub private_message_id: Option, +} diff --git a/crates/db_schema/src/source/combined/mod.rs b/crates/db_schema/src/source/combined/mod.rs index 6beec3921..2555ef5be 100644 --- a/crates/db_schema/src/source/combined/mod.rs +++ b/crates/db_schema/src/source/combined/mod.rs @@ -1,3 +1,4 @@ +pub mod inbox; pub mod modlog; pub mod person_content; pub mod person_saved; diff --git a/crates/db_views/src/lib.rs b/crates/db_views/src/lib.rs index 6b95b9efe..06411a0cb 100644 --- a/crates/db_views/src/lib.rs +++ b/crates/db_views/src/lib.rs @@ -22,8 +22,6 @@ pub mod post_view; #[cfg(feature = "full")] pub mod private_message_report_view; #[cfg(feature = "full")] -pub mod private_message_view; -#[cfg(feature = "full")] pub mod registration_application_view; #[cfg(feature = "full")] pub mod report_combined_view; diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 709615672..6aad4af21 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -171,17 +171,6 @@ pub struct PostView { pub unread_comments: i64, } -#[derive(Debug, PartialEq, Eq, 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 private message view. -pub struct PrivateMessageView { - pub private_message: PrivateMessage, - pub creator: Person, - pub recipient: Person, -} - #[skip_serializing_none] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] diff --git a/crates/db_views_actor/Cargo.toml b/crates/db_views_actor/Cargo.toml index 18a79826b..1637304bb 100644 --- a/crates/db_views_actor/Cargo.toml +++ b/crates/db_views_actor/Cargo.toml @@ -18,6 +18,8 @@ workspace = true full = [ "lemmy_db_schema/full", "lemmy_utils/full", + "tracing", + "i-love-jesus", "diesel", "diesel-async", "ts-rs", @@ -40,6 +42,9 @@ ts-rs = { workspace = true, optional = true } chrono.workspace = true strum = { workspace = true } lemmy_utils = { workspace = true, optional = true } +tracing = { workspace = true, optional = true } +i-love-jesus = { workspace = true, optional = true } +derive-new.workspace = true [dev-dependencies] serial_test = { workspace = true } diff --git a/crates/db_views_actor/src/comment_reply_view.rs b/crates/db_views_actor/src/comment_reply_view.rs index 97d719370..f90cac9e1 100644 --- a/crates/db_views_actor/src/comment_reply_view.rs +++ b/crates/db_views_actor/src/comment_reply_view.rs @@ -40,6 +40,7 @@ use lemmy_db_schema::{ CommentSortType, }; +// TODO get rid of all this fn queries<'a>() -> Queries< impl ReadFn<'a, CommentReplyView, (CommentReplyId, Option)>, impl ListFn<'a, CommentReplyView, CommentReplyQuery>, @@ -130,6 +131,9 @@ fn queries<'a>() -> Queries< query = query.filter(not(person::bot_account)); }; + // Don't show replies from blocked persons + query = query.filter(person_actions::blocked.is_null()); + query = match options.sort.unwrap_or(CommentSortType::New) { CommentSortType::Hot => query.then_order_by(comment_aggregates::hot_rank.desc()), CommentSortType::Controversial => { @@ -140,9 +144,6 @@ fn queries<'a>() -> Queries< CommentSortType::Top => query.order_by(comment_aggregates::score.desc()), }; - // Don't show replies from blocked persons - query = query.filter(person_actions::blocked.is_null()); - let (limit, offset) = limit_and_offset(options.page, options.limit)?; query diff --git a/crates/db_views_actor/src/inbox_combined_view.rs b/crates/db_views_actor/src/inbox_combined_view.rs new file mode 100644 index 000000000..aa9ced41f --- /dev/null +++ b/crates/db_views_actor/src/inbox_combined_view.rs @@ -0,0 +1,971 @@ +use crate::structs::{ + CommentReplyView, + InboxCombinedPaginationCursor, + InboxCombinedView, + InboxCombinedViewInternal, + PersonCommentMentionView, + PersonPostMentionView, + PrivateMessageView, +}; +use diesel::{ + dsl::not, + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, + SelectableHelper, +}; +use diesel_async::RunQueryDsl; +use i_love_jesus::PaginatedQueryBuilder; +use lemmy_db_schema::{ + aliases::{self, creator_community_actions}, + newtypes::PersonId, + schema::{ + comment, + comment_actions, + comment_aggregates, + comment_reply, + community, + community_actions, + image_details, + inbox_combined, + instance_actions, + local_user, + person, + person_actions, + person_comment_mention, + person_post_mention, + post, + post_actions, + post_aggregates, + private_message, + }, + source::{ + combined::inbox::{inbox_combined_keys as key, InboxCombined}, + community::CommunityFollower, + }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, + InternalToCombinedView, +}; +use lemmy_utils::error::LemmyResult; + +impl InboxCombinedViewInternal { + /// Gets the number of unread mentions + // TODO need to test this + pub async fn get_unread_count( + pool: &mut DbPool<'_>, + my_person_id: PersonId, + show_bot_accounts: bool, + ) -> Result { + use diesel::dsl::count; + let conn = &mut get_conn(pool).await?; + + let item_creator = person::id; + let recipient_person = aliases::person1.field(person::id); + + let unread_filter = comment_reply::read + .eq(false) + .or(person_comment_mention::read.eq(false)) + .or(person_post_mention::read.eq(false)) + // If its unread, I only want the messages to me + .or( + private_message::read + .eq(false) + .and(private_message::recipient_id.eq(my_person_id)), + ); + + let item_creator_join = comment::creator_id + .eq(item_creator) + .or( + inbox_combined::person_post_mention_id + .is_not_null() + .and(post::creator_id.eq(item_creator)), + ) + .or(private_message::creator_id.eq(item_creator)); + + let recipient_join = comment_reply::recipient_id + .eq(recipient_person) + .or(person_comment_mention::recipient_id.eq(recipient_person)) + .or(person_post_mention::recipient_id.eq(recipient_person)); + + let comment_join = comment_reply::comment_id + .eq(comment::id) + .or(person_comment_mention::comment_id.eq(comment::id)); + + let post_join = person_post_mention::post_id + .eq(post::id) + .or(comment::post_id.eq(post::id)); + + let mut query = inbox_combined::table + .left_join(comment_reply::table) + .left_join(person_comment_mention::table) + .left_join(person_post_mention::table) + .left_join(private_message::table) + .left_join(comment::table.on(comment_join)) + .left_join(post::table.on(post_join)) + // The item creator + .inner_join(person::table.on(item_creator_join)) + // The recipient + .inner_join(aliases::person1.on(recipient_join)) + .left_join(actions( + instance_actions::table, + Some(my_person_id), + person::instance_id, + )) + .left_join(actions( + person_actions::table, + Some(my_person_id), + item_creator, + )) + // Filter for your user + .filter(recipient_person.eq(my_person_id)) + // Filter unreads + .filter(unread_filter) + // Don't count replies from blocked users + .filter(person_actions::blocked.is_null()) + .filter(instance_actions::blocked.is_null()) + .filter(comment::deleted.eq(false)) + .filter(comment::removed.eq(false)) + .filter(post::deleted.eq(false)) + .filter(post::removed.eq(false)) + .filter(private_message::deleted.eq(false)) + .into_boxed(); + + // These filters need to be kept in sync with the filters in queries().list() + if !show_bot_accounts { + query = query.filter(not(person::bot_account)); + } + + query + .select(count(inbox_combined::id)) + .first::(conn) + .await + } +} + +impl InboxCombinedPaginationCursor { + // get cursor for page that starts immediately after the given post + pub fn after_post(view: &InboxCombinedView) -> InboxCombinedPaginationCursor { + let (prefix, id) = match view { + InboxCombinedView::CommentReply(v) => ('R', v.comment_reply.id.0), + InboxCombinedView::CommentMention(v) => ('C', v.person_comment_mention.id.0), + InboxCombinedView::PostMention(v) => ('P', v.person_post_mention.id.0), + InboxCombinedView::PrivateMessage(v) => ('M', v.private_message.id.0), + }; + // hex encoding to prevent ossification + InboxCombinedPaginationCursor(format!("{prefix}{id:x}")) + } + + pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { + let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); + let mut query = inbox_combined::table + .select(InboxCombined::as_select()) + .into_boxed(); + let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; + let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; + query = match prefix { + "R" => query.filter(inbox_combined::comment_reply_id.eq(id)), + "C" => query.filter(inbox_combined::person_comment_mention_id.eq(id)), + "P" => query.filter(inbox_combined::person_post_mention_id.eq(id)), + "M" => query.filter(inbox_combined::private_message_id.eq(id)), + _ => return Err(err_msg()), + }; + let token = query.first(&mut get_conn(pool).await?).await?; + + Ok(PaginationCursorData(token)) + } +} + +#[derive(Clone)] +pub struct PaginationCursorData(InboxCombined); + +#[derive(derive_new::new)] +pub struct InboxCombinedQuery { + pub my_person_id: PersonId, + #[new(default)] + pub unread_only: Option, + #[new(default)] + pub show_bot_accounts: Option, + #[new(default)] + pub page_after: Option, + #[new(default)] + pub page_back: Option, +} + +impl InboxCombinedQuery { + pub async fn list(self, pool: &mut DbPool<'_>) -> LemmyResult> { + let conn = &mut get_conn(pool).await?; + + let my_person_id = Some(self.my_person_id); + let item_creator = person::id; + let recipient_person = aliases::person1.field(person::id); + + let item_creator_join = comment::creator_id + .eq(item_creator) + .or( + inbox_combined::person_post_mention_id + .is_not_null() + .and(post::creator_id.eq(item_creator)), + ) + .or(private_message::creator_id.eq(item_creator)); + + let recipient_join = comment_reply::recipient_id + .eq(recipient_person) + .or(person_comment_mention::recipient_id.eq(recipient_person)) + .or(person_post_mention::recipient_id.eq(recipient_person)); + // TODO this might need fixing, because if its not unread, you want all pms, even the ones you + // sent + // .or(private_message::recipient_id.eq(recipient_person)); + + let comment_join = comment_reply::comment_id + .eq(comment::id) + .or(person_comment_mention::comment_id.eq(comment::id)); + + let post_join = person_post_mention::post_id + .eq(post::id) + .or(comment::post_id.eq(post::id)); + + let community_join = post::id.eq(community::id); + + let mut query = inbox_combined::table + .left_join(comment_reply::table) + .left_join(person_comment_mention::table) + .left_join(person_post_mention::table) + .left_join(private_message::table) + .left_join(comment::table.on(comment_join)) + .left_join(post::table.on(post_join)) + .left_join(community::table.on(community_join)) + // The item creator + .inner_join(person::table.on(item_creator_join)) + // The recipient + .inner_join(aliases::person1.on(recipient_join)) + .left_join(actions_alias( + creator_community_actions, + item_creator, + post::community_id, + )) + .left_join( + local_user::table.on( + item_creator + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ), + ) + .left_join(actions( + community_actions::table, + my_person_id, + post::community_id, + )) + .left_join(actions( + instance_actions::table, + my_person_id, + person::instance_id, + )) + .left_join(actions(post_actions::table, my_person_id, post::id)) + .left_join(actions(person_actions::table, my_person_id, item_creator)) + .left_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) + .left_join(comment_aggregates::table.on(comment::id.eq(comment_aggregates::comment_id))) + .left_join(actions(comment_actions::table, my_person_id, comment::id)) + .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) + // The recipient filter (IE only show replies to you) + .filter(recipient_person.eq(self.my_person_id)) + .select(( + // Specific + comment_reply::all_columns.nullable(), + person_comment_mention::all_columns.nullable(), + person_post_mention::all_columns.nullable(), + post_aggregates::all_columns.nullable(), + coalesce( + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), + post_aggregates::comments, + ) + .nullable(), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + post_actions::like_score.nullable(), + image_details::all_columns.nullable(), + private_message::all_columns.nullable(), + // Shared + post::all_columns.nullable(), + community::all_columns.nullable(), + comment::all_columns.nullable(), + comment_aggregates::all_columns.nullable(), + comment_actions::saved.nullable().is_not_null(), + comment_actions::like_score.nullable(), + CommunityFollower::select_subscribed_type(), + person::all_columns, + aliases::person1.fields(person::all_columns), + local_user::admin.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + person_actions::blocked.nullable().is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + )) + .into_boxed(); + + // Filters + if self.unread_only.unwrap_or_default() { + query = query.filter( + comment_reply::read + .eq(false) + .or(person_comment_mention::read.eq(false)) + .or(person_post_mention::read.eq(false)) + // If its unread, I only want the messages to me + .or( + private_message::read + .eq(false) + .and(private_message::recipient_id.eq(self.my_person_id)), + ), + ); + } + + if !(self.show_bot_accounts.unwrap_or_default()) { + query = query.filter(not(person::bot_account)); + }; + + // Dont show replies from blocked users or instances + query = query + .filter(person_actions::blocked.is_null()) + .filter(instance_actions::blocked.is_null()); + + let mut query = PaginatedQueryBuilder::new(query); + + let page_after = self.page_after.map(|c| c.0); + + if self.page_back.unwrap_or_default() { + query = query.before(page_after).limit_and_offset_from_end(); + } else { + query = query.after(page_after); + } + + // Sorting by published + query = query + .then_desc(key::published) + // Tie breaker + .then_desc(key::id); + + let res = query.load::(conn).await?; + + // Map the query results to the enum + let out = res.into_iter().filter_map(|u| u.map_to_enum()).collect(); + + Ok(out) + } +} + +impl InternalToCombinedView for InboxCombinedViewInternal { + type CombinedView = InboxCombinedView; + + fn map_to_enum(&self) -> Option { + // Use for a short alias + let v = self.clone(); + + if let (Some(comment_reply), Some(comment), Some(counts), Some(post), Some(community)) = ( + v.comment_reply, + v.comment.clone(), + v.comment_counts.clone(), + v.post.clone(), + v.community.clone(), + ) { + Some(InboxCombinedView::CommentReply(CommentReplyView { + comment_reply, + comment, + counts, + recipient: v.item_recipient, + post, + community, + creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.comment_saved, + my_vote: v.my_comment_vote, + banned_from_community: v.banned_from_community, + })) + } else if let ( + Some(person_comment_mention), + Some(comment), + Some(counts), + Some(post), + Some(community), + ) = ( + v.person_comment_mention, + v.comment, + v.comment_counts, + v.post.clone(), + v.community.clone(), + ) { + Some(InboxCombinedView::CommentMention( + PersonCommentMentionView { + person_comment_mention, + comment, + counts, + recipient: v.item_recipient, + post, + community, + creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.comment_saved, + my_vote: v.my_comment_vote, + banned_from_community: v.banned_from_community, + }, + )) + } else if let ( + Some(person_post_mention), + Some(post), + Some(counts), + Some(unread_comments), + Some(community), + ) = ( + v.person_post_mention, + v.post, + v.post_counts, + v.post_unread_comments, + v.community, + ) { + Some(InboxCombinedView::PostMention(PersonPostMentionView { + person_post_mention, + counts, + post, + community, + recipient: v.item_recipient, + unread_comments, + creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.post_saved, + read: v.post_read, + hidden: v.post_hidden, + my_vote: v.my_post_vote, + image_details: v.image_details, + banned_from_community: v.banned_from_community, + })) + } else if let Some(private_message) = v.private_message { + Some(InboxCombinedView::PrivateMessage(PrivateMessageView { + private_message, + creator: v.item_creator, + recipient: v.item_recipient, + })) + } else { + None + } + } +} + +// TODO Dont delete these +// #[cfg(test)] +// #[expect(clippy::indexing_slicing)] +// mod tests { + +// use crate::{inbox_combined_view::InboxCombinedQuery, structs::InboxCombinedView}; +// use lemmy_db_schema::{ +// source::{ +// comment::{Comment, CommentInsertForm}, +// community::{Community, CommunityInsertForm}, +// instance::Instance, +// person::{Person, PersonInsertForm}, +// post::{Post, PostInsertForm}, +// }, +// traits::Crud, +// utils::{build_db_pool_for_tests, DbPool}, +// }; +// use lemmy_utils::error::LemmyResult; +// use pretty_assertions::assert_eq; +// use serial_test::serial; + +// struct Data { +// instance: Instance, +// timmy: Person, +// sara: Person, +// timmy_post: Post, +// timmy_post_2: Post, +// sara_post: Post, +// timmy_comment: Comment, +// sara_comment: Comment, +// sara_comment_2: Comment, +// } + +// async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { +// let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + +// let timmy_form = PersonInsertForm::test_form(instance.id, "timmy_pcv"); +// let timmy = Person::create(pool, &timmy_form).await?; + +// let sara_form = PersonInsertForm::test_form(instance.id, "sara_pcv"); +// let sara = Person::create(pool, &sara_form).await?; + +// let community_form = CommunityInsertForm::new( +// instance.id, +// "test community pcv".to_string(), +// "nada".to_owned(), +// "pubkey".to_string(), +// ); +// let community = Community::create(pool, &community_form).await?; + +// let timmy_post_form = PostInsertForm::new("timmy post prv".into(), timmy.id, community.id); +// let timmy_post = Post::create(pool, &timmy_post_form).await?; + +// let timmy_post_form_2 = PostInsertForm::new("timmy post prv 2".into(), timmy.id, +// community.id); let timmy_post_2 = Post::create(pool, &timmy_post_form_2).await?; + +// let sara_post_form = PostInsertForm::new("sara post prv".into(), sara.id, community.id); +// let sara_post = Post::create(pool, &sara_post_form).await?; + +// let timmy_comment_form = +// CommentInsertForm::new(timmy.id, timmy_post.id, "timmy comment prv".into()); +// let timmy_comment = Comment::create(pool, &timmy_comment_form, None).await?; + +// let sara_comment_form = +// CommentInsertForm::new(sara.id, timmy_post.id, "sara comment prv".into()); +// let sara_comment = Comment::create(pool, &sara_comment_form, None).await?; + +// let sara_comment_form_2 = +// CommentInsertForm::new(sara.id, timmy_post_2.id, "sara comment prv 2".into()); +// let sara_comment_2 = Comment::create(pool, &sara_comment_form_2, None).await?; + +// Ok(Data { +// instance, +// timmy, +// sara, +// timmy_post, +// timmy_post_2, +// sara_post, +// timmy_comment, +// sara_comment, +// sara_comment_2, +// }) +// } + +// async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { +// Instance::delete(pool, data.instance.id).await?; + +// Ok(()) +// } + +// #[tokio::test] +// #[serial] +// async fn test_combined() -> LemmyResult<()> { +// let pool = &build_db_pool_for_tests(); +// let pool = &mut pool.into(); +// let data = init_data(pool).await?; + +// // Do a batch read of timmy +// let timmy_content = InboxCombinedQuery::new(data.timmy.id) +// .list(pool, &None) +// .await?; +// assert_eq!(3, timmy_content.len()); + +// // Make sure the types are correct +// if let InboxCombinedView::Comment(v) = &timmy_content[0] { +// assert_eq!(data.timmy_comment.id, v.comment.id); +// assert_eq!(data.timmy.id, v.creator.id); +// } else { +// panic!("wrong type"); +// } +// if let InboxCombinedView::Post(v) = &timmy_content[1] { +// assert_eq!(data.timmy_post_2.id, v.post.id); +// assert_eq!(data.timmy.id, v.post.creator_id); +// } else { +// panic!("wrong type"); +// } +// if let InboxCombinedView::Post(v) = &timmy_content[2] { +// assert_eq!(data.timmy_post.id, v.post.id); +// assert_eq!(data.timmy.id, v.post.creator_id); +// } else { +// panic!("wrong type"); +// } + +// // Do a batch read of sara +// let sara_content = InboxCombinedQuery::new(data.sara.id) +// .list(pool, &None) +// .await?; +// assert_eq!(3, sara_content.len()); + +// // Make sure the report types are correct +// if let InboxCombinedView::Comment(v) = &sara_content[0] { +// assert_eq!(data.sara_comment_2.id, v.comment.id); +// assert_eq!(data.sara.id, v.creator.id); +// // This one was to timmy_post_2 +// assert_eq!(data.timmy_post_2.id, v.post.id); +// assert_eq!(data.timmy.id, v.post.creator_id); +// } else { +// panic!("wrong type"); +// } +// if let InboxCombinedView::Comment(v) = &sara_content[1] { +// assert_eq!(data.sara_comment.id, v.comment.id); +// assert_eq!(data.sara.id, v.creator.id); +// assert_eq!(data.timmy_post.id, v.post.id); +// assert_eq!(data.timmy.id, v.post.creator_id); +// } else { +// panic!("wrong type"); +// } +// if let InboxCombinedView::Post(v) = &sara_content[2] { +// assert_eq!(data.sara_post.id, v.post.id); +// assert_eq!(data.sara.id, v.post.creator_id); +// } else { +// panic!("wrong type"); +// } + +// cleanup(data, pool).await?; + +// Ok(()) +// } +// } + +// + +// #[cfg(test)] +// mod tests { + +// use crate::{ +// person_comment_mention_view::PersonCommentMentionQuery, +// structs::PersonCommentMentionView, +// }; +// use lemmy_db_schema::{ +// source::{ +// comment::{Comment, CommentInsertForm}, +// community::{Community, CommunityInsertForm}, +// instance::Instance, +// local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, +// person::{Person, PersonInsertForm, PersonUpdateForm}, +// person_block::{PersonBlock, PersonBlockForm}, +// person_comment_mention::{ +// PersonCommentMention, +// PersonCommentMentionInsertForm, +// PersonCommentMentionUpdateForm, +// }, +// 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(); +// 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 comment_form = CommentInsertForm::new( +// inserted_person.id, +// inserted_post.id, +// "A test comment".into(), +// ); +// let inserted_comment = Comment::create(pool, &comment_form, None).await?; + +// let person_comment_mention_form = PersonCommentMentionInsertForm { +// recipient_id: inserted_recipient.id, +// comment_id: inserted_comment.id, +// read: None, +// }; + +// let inserted_mention = PersonCommentMention::create(pool, +// &person_comment_mention_form).await?; + +// let expected_mention = PersonCommentMention { +// id: inserted_mention.id, +// recipient_id: inserted_mention.recipient_id, +// comment_id: inserted_mention.comment_id, +// read: false, +// published: inserted_mention.published, +// }; + +// let read_mention = PersonCommentMention::read(pool, inserted_mention.id).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 = +// PersonCommentMentionView::get_unread_count(pool, &recipient_local_user).await?; + +// let query = PersonCommentMentionQuery { +// 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 = +// 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()); + +// // 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 = +// 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; +// 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()); + +// Comment::delete(pool, inserted_comment.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(()) +// } +// } +// #[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/lib.rs b/crates/db_views_actor/src/lib.rs index 04c56a8c4..d982c4a1f 100644 --- a/crates/db_views_actor/src/lib.rs +++ b/crates/db_views_actor/src/lib.rs @@ -9,7 +9,13 @@ pub mod community_person_ban_view; #[cfg(feature = "full")] pub mod community_view; #[cfg(feature = "full")] +pub mod inbox_combined_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_view; +#[cfg(feature = "full")] +pub mod private_message_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 d447fea0a..ecef5c777 100644 --- a/crates/db_views_actor/src/person_comment_mention_view.rs +++ b/crates/db_views_actor/src/person_comment_mention_view.rs @@ -1,7 +1,6 @@ use crate::structs::PersonCommentMentionView; use diesel::{ dsl::{exists, not}, - pg::Pg, result::Error, BoolExpressionMethods, ExpressionMethods, @@ -26,35 +25,27 @@ use lemmy_db_schema::{ post, }, source::{community::CommunityFollower, local_user::LocalUser}, - utils::{ - actions, - actions_alias, - get_conn, - limit_and_offset, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, - CommentSortType, + utils::{actions, actions_alias, get_conn, DbPool}, }; -fn queries<'a>() -> Queries< - impl ReadFn<'a, PersonCommentMentionView, (PersonCommentMentionId, Option)>, - impl ListFn<'a, PersonCommentMentionView, PersonCommentMentionQuery>, -> { - let creator_is_admin = exists( - local_user::table.filter( - comment::creator_id - .eq(local_user::person_id) - .and(local_user::admin.eq(true)), - ), - ); +impl PersonCommentMentionView { + pub async fn read( + pool: &mut DbPool<'_>, + person_comment_mention_id: PersonCommentMentionId, + my_person_id: Option, + ) -> Result { + let conn = &mut get_conn(pool).await?; - let all_joins = move |query: person_comment_mention::BoxedQuery<'a, Pg>, - my_person_id: Option| { - query + let creator_is_admin = exists( + local_user::table.filter( + comment::creator_id + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ), + ); + + person_comment_mention::table + .find(person_comment_mention_id) .inner_join(comment::table) .inner_join(person::table.on(comment::creator_id.eq(person::id))) .inner_join(post::table.on(comment::post_id.eq(post::id))) @@ -100,80 +91,12 @@ fn queries<'a>() -> Queries< person_actions::blocked.nullable().is_not_null(), comment_actions::like_score.nullable(), )) - }; - - 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: PersonCommentMentionQuery| async move { - // These filters need to be kept in sync with the filters in - // 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_comment_mention::recipient_id.eq(recipient_id)); - } - - if options.unread_only { - query = query.filter(person_comment_mention::read.eq(false)); - } - - if !options.show_bot_accounts { - query = query.filter(not(person::bot_account)); - }; - - 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()) - } - CommentSortType::New => query.then_order_by(comment::published.desc()), - CommentSortType::Old => query.then_order_by(comment::published.asc()), - CommentSortType::Top => query.order_by(comment_aggregates::score.desc()), - }; - - // Don't show mentions from blocked persons - query = query.filter(person_actions::blocked.is_null()); - - let (limit, offset) = limit_and_offset(options.page, options.limit)?; - - query - .limit(limit) - .offset(offset) - .load::(&mut conn) - .await - }; - - Queries::new(read, list) -} - -impl PersonCommentMentionView { - pub async fn read( - pool: &mut DbPool<'_>, - person_comment_mention_id: PersonCommentMentionId, - my_person_id: Option, - ) -> Result { - queries() - .read(pool, (person_comment_mention_id, my_person_id)) + .first(conn) .await } /// Gets the number of unread mentions + // TODO get rid of this pub async fn get_unread_count( pool: &mut DbPool<'_>, local_user: &LocalUser, @@ -208,195 +131,3 @@ impl PersonCommentMentionView { .await } } - -#[derive(Default, Clone)] -pub struct PersonCommentMentionQuery { - 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 PersonCommentMentionQuery { - pub async fn list(self, pool: &mut DbPool<'_>) -> Result, Error> { - queries().list(pool, self).await - } -} - -#[cfg(test)] -mod tests { - - use crate::{ - person_comment_mention_view::PersonCommentMentionQuery, - structs::PersonCommentMentionView, - }; - use lemmy_db_schema::{ - source::{ - comment::{Comment, CommentInsertForm}, - community::{Community, CommunityInsertForm}, - instance::Instance, - local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, - person::{Person, PersonInsertForm, PersonUpdateForm}, - person_block::{PersonBlock, PersonBlockForm}, - person_comment_mention::{ - PersonCommentMention, - PersonCommentMentionInsertForm, - PersonCommentMentionUpdateForm, - }, - 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(); - 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 comment_form = CommentInsertForm::new( - inserted_person.id, - inserted_post.id, - "A test comment".into(), - ); - let inserted_comment = Comment::create(pool, &comment_form, None).await?; - - let person_comment_mention_form = PersonCommentMentionInsertForm { - recipient_id: inserted_recipient.id, - comment_id: inserted_comment.id, - read: None, - }; - - let inserted_mention = PersonCommentMention::create(pool, &person_comment_mention_form).await?; - - let expected_mention = PersonCommentMention { - id: inserted_mention.id, - recipient_id: inserted_mention.recipient_id, - comment_id: inserted_mention.comment_id, - read: false, - published: inserted_mention.published, - }; - - let read_mention = PersonCommentMention::read(pool, inserted_mention.id).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 = - PersonCommentMentionView::get_unread_count(pool, &recipient_local_user).await?; - - let query = PersonCommentMentionQuery { - 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 = - 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()); - - // 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 = - 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; - 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()); - - Comment::delete(pool, inserted_comment.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/person_post_mention_view.rs b/crates/db_views_actor/src/person_post_mention_view.rs new file mode 100644 index 000000000..fd16a0619 --- /dev/null +++ b/crates/db_views_actor/src/person_post_mention_view.rs @@ -0,0 +1,103 @@ +use crate::structs::PersonPostMentionView; +use diesel::{ + dsl::exists, + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, +}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + aliases::{self, creator_community_actions}, + newtypes::{PersonId, PersonPostMentionId}, + schema::{ + community, + community_actions, + image_details, + local_user, + person, + person_actions, + person_post_mention, + post, + post_actions, + post_aggregates, + }, + source::community::CommunityFollower, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, +}; + +impl PersonPostMentionView { + pub async fn read( + pool: &mut DbPool<'_>, + person_post_mention_id: PersonPostMentionId, + my_person_id: Option, + ) -> Result { + let conn = &mut get_conn(pool).await?; + + let creator_is_admin = exists( + local_user::table.filter( + post::creator_id + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ), + ); + + person_post_mention::table + .find(person_post_mention_id) + .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))) + .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) + .left_join(actions( + community_actions::table, + my_person_id, + post::community_id, + )) + .left_join(actions(post_actions::table, my_person_id, post::id)) + .left_join(actions( + person_actions::table, + my_person_id, + post::creator_id, + )) + .left_join(actions_alias( + creator_community_actions, + post::creator_id, + post::community_id, + )) + .select(( + person_post_mention::all_columns, + post::all_columns, + person::all_columns, + community::all_columns, + image_details::all_columns.nullable(), + aliases::person1.fields(person::all_columns), + post_aggregates::all_columns, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), + creator_is_admin, + CommunityFollower::select_subscribed_type(), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + post_actions::like_score.nullable(), + coalesce( + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), + post_aggregates::comments, + ), + )) + .first(conn) + .await + } +} diff --git a/crates/db_views/src/private_message_view.rs b/crates/db_views_actor/src/private_message_view.rs similarity index 100% rename from crates/db_views/src/private_message_view.rs rename to crates/db_views_actor/src/private_message_view.rs diff --git a/crates/db_views_actor/src/structs.rs b/crates/db_views_actor/src/structs.rs index d997f31d4..b1f75c86d 100644 --- a/crates/db_views_actor/src/structs.rs +++ b/crates/db_views_actor/src/structs.rs @@ -6,10 +6,12 @@ use lemmy_db_schema::{ comment::Comment, comment_reply::CommentReply, community::Community, + images::ImageDetails, person::Person, person_comment_mention::PersonCommentMention, person_post_mention::PersonPostMention, post::Post, + private_message::PrivateMessage, }, SubscribedType, }; @@ -125,6 +127,8 @@ pub struct PersonPostMentionView { pub post: Post, pub creator: Person, pub community: Community, + #[cfg_attr(feature = "full", ts(optional))] + pub image_details: Option, pub recipient: Person, pub counts: PostAggregates, pub creator_banned_from_community: bool, @@ -133,8 +137,12 @@ pub struct PersonPostMentionView { pub creator_is_admin: bool, pub subscribed: SubscribedType, pub saved: bool, + pub read: bool, + pub hidden: bool, pub creator_blocked: bool, + #[cfg_attr(feature = "full", ts(optional))] pub my_vote: Option, + pub unread_comments: i64, } #[skip_serializing_none] @@ -183,3 +191,69 @@ pub struct PendingFollow { pub is_new_instance: bool, pub subscribed: SubscribedType, } + +#[derive(Debug, PartialEq, Eq, 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 private message view. +pub struct PrivateMessageView { + pub private_message: PrivateMessage, + pub creator: Person, + pub recipient: Person, +} + +/// like PaginationCursor but for the report_combined table +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct InboxCombinedPaginationCursor(pub String); + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +/// A combined inbox view +pub struct InboxCombinedViewInternal { + // Comment reply + pub comment_reply: Option, + // Person comment mention + pub person_comment_mention: Option, + // Person post mention + pub person_post_mention: Option, + pub post_counts: Option, + pub post_unread_comments: Option, + pub post_saved: bool, + pub post_read: bool, + pub post_hidden: bool, + pub my_post_vote: Option, + pub image_details: Option, + // Private message + pub private_message: Option, + // Shared + pub post: Option, + pub community: Option, + pub comment: Option, + pub comment_counts: Option, + pub comment_saved: bool, + pub my_comment_vote: Option, + pub subscribed: SubscribedType, + pub item_creator: Person, + pub item_recipient: Person, + pub item_creator_is_admin: bool, + pub item_creator_is_moderator: bool, + pub item_creator_banned_from_community: bool, + pub item_creator_blocked: bool, + pub banned_from_community: bool, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +// Use serde's internal tagging, to work easier with javascript libraries +#[serde(tag = "type_")] +pub enum InboxCombinedView { + CommentReply(CommentReplyView), + CommentMention(PersonCommentMentionView), + PostMention(PersonPostMentionView), + PrivateMessage(PrivateMessageView), +} diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 31a565aa3..a10a3f46e 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -6,7 +6,6 @@ use lemmy_api_common::{context::LemmyContext, utils::check_private_instance}; use lemmy_db_schema::{ source::{community::Community, person::Person}, traits::ApubActor, - CommentSortType, CommunityVisibility, ListingType, PostSortType, @@ -15,12 +14,7 @@ use lemmy_db_views::{ post_view::PostQuery, structs::{PostView, SiteView}, }; -use lemmy_db_views_actor::{ - comment_reply_view::CommentReplyQuery, - person_comment_mention_view::PersonCommentMentionQuery, - person_post_mention_view::PersonPostMentionQuery, - structs::{CommentReplyView, PersonCommentMentionView, PersonPostMentionView}, -}; +use lemmy_db_views_actor::{inbox_combined_view::InboxCombinedQuery, structs::InboxCombinedView}; use lemmy_utils::{ cache_header::cache_1hour, error::{LemmyError, LemmyErrorType, LemmyResult}, @@ -361,53 +355,24 @@ 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 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 limit = Some(RSS_FETCH_LIMIT); + let my_person_id = local_user.person.id; + let show_bot_accounts = Some(local_user.local_user.show_bot_accounts); + let unread_only = Some(false); check_private_instance(&Some(local_user.clone()), &site_view.local_site)?; - let replies = CommentReplyQuery { - recipient_id, + let inbox = InboxCombinedQuery { my_person_id, + unread_only, show_bot_accounts, - sort: Some(CommentSortType::New), - limit, - ..Default::default() - } - .list(&mut context.pool()) - .await?; - - let comment_mentions = PersonCommentMentionQuery { - 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() + page_after: None, + page_back: None, } .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, - post_mentions, - &protocol_and_hostname, - )?; + let items = create_reply_and_mention_items(inbox, &protocol_and_hostname)?; let mut channel = Channel { namespaces: RSS_NAMESPACE.clone(), @@ -426,57 +391,55 @@ async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> LemmyResult, - comment_mentions: Vec, - post_mentions: Vec, + inbox: Vec, protocol_and_hostname: &str, ) -> LemmyResult> { - let mut reply_items: Vec = replies + let reply_items: Vec = inbox .iter() - .map(|r| { - let reply_url = format!("{}/comment/{}", protocol_and_hostname, r.comment.id); - build_item( - &r.creator.name, - &r.comment.published, - &reply_url, - &r.comment.content, - protocol_and_hostname, - ) + .map(|r| match r { + InboxCombinedView::CommentReply(v) => { + let reply_url = format!("{}/comment/{}", protocol_and_hostname, v.comment.id); + build_item( + &v.creator.name, + &v.comment.published, + &reply_url, + &v.comment.content, + protocol_and_hostname, + ) + } + InboxCombinedView::CommentMention(v) => { + let mention_url = format!("{}/comment/{}", protocol_and_hostname, v.comment.id); + build_item( + &v.creator.name, + &v.comment.published, + &mention_url, + &v.comment.content, + protocol_and_hostname, + ) + } + InboxCombinedView::PostMention(v) => { + let mention_url = format!("{}/post/{}", protocol_and_hostname, v.post.id); + build_item( + &v.creator.name, + &v.post.published, + &mention_url, + &v.post.body.clone().unwrap_or_default(), + protocol_and_hostname, + ) + } + InboxCombinedView::PrivateMessage(v) => { + let inbox_url = format!("{}/inbox", protocol_and_hostname); + build_item( + &v.creator.name, + &v.private_message.published, + &inbox_url, + &v.private_message.content, + protocol_and_hostname, + ) + } }) .collect::>>()?; - let mut comment_mention_items: Vec = comment_mentions - .iter() - .map(|m| { - let mention_url = format!("{}/comment/{}", protocol_and_hostname, m.comment.id); - build_item( - &m.creator.name, - &m.comment.published, - &mention_url, - &m.comment.content, - protocol_and_hostname, - ) - }) - .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 deleted file mode 100644 index ae8e0bcad..000000000 --- a/migrations/2024-11-02-161125_add_post_body_mention/up.sql +++ /dev/null @@ -1,12 +0,0 @@ --- 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 timestamptz NOT NULL DEFAULT now(), - UNIQUE (recipient_id, post_id) -); diff --git a/migrations/2024-11-02-161125_add_post_body_mention/down.sql b/migrations/2024-12-10-193418_add_inbox_combined_table/down.sql similarity index 63% rename from migrations/2024-11-02-161125_add_post_body_mention/down.sql rename to migrations/2024-12-10-193418_add_inbox_combined_table/down.sql index 37bb14e3c..a355f49f4 100644 --- a/migrations/2024-11-02-161125_add_post_body_mention/down.sql +++ b/migrations/2024-12-10-193418_add_inbox_combined_table/down.sql @@ -1,5 +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; +-- Drop the new tables +DROP TABLE person_post_mention, inbox_combined; diff --git a/migrations/2024-12-10-193418_add_inbox_combined_table/up.sql b/migrations/2024-12-10-193418_add_inbox_combined_table/up.sql new file mode 100644 index 000000000..2cc0246d2 --- /dev/null +++ b/migrations/2024-12-10-193418_add_inbox_combined_table/up.sql @@ -0,0 +1,71 @@ +-- Creates combined tables for +-- Inbox: (replies, comment mentions, post mentions, and private_messages) + +-- Also add post mentions, since these didn't exist before. + +-- 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 timestamptz NOT NULL DEFAULT now(), + UNIQUE (recipient_id, post_id) +); + +CREATE TABLE inbox_combined ( + id serial PRIMARY KEY, + published timestamptz NOT NULL, + comment_reply_id int UNIQUE REFERENCES comment_reply ON UPDATE CASCADE ON DELETE CASCADE, + person_comment_mention_id int UNIQUE REFERENCES person_comment_mention ON UPDATE CASCADE ON DELETE CASCADE, + person_post_mention_id int UNIQUE REFERENCES person_post_mention ON UPDATE CASCADE ON DELETE CASCADE, + private_message_id int UNIQUE REFERENCES private_message ON UPDATE CASCADE ON DELETE CASCADE, + -- Make sure only one of the columns is not null + CHECK (num_nonnulls (comment_reply_id, person_comment_mention_id, person_post_mention_id, private_message_id) = 1) +); + +CREATE INDEX idx_inbox_combined_published ON inbox_combined (published DESC, id DESC); + +CREATE INDEX idx_inbox_combined_published_asc ON inbox_combined (reverse_timestamp_sort (published) DESC, id DESC); + +-- Updating the history +INSERT INTO inbox_combined (published, comment_reply_id, person_comment_mention_id, person_post_mention_id, private_message_id) +SELECT + published, + id, + NULL::int, + NULL::int, + NULL::int +FROM + comment_reply +UNION ALL +SELECT + published, + NULL::int, + id, + NULL::int, + NULL::int +FROM + person_comment_mention +UNION ALL +SELECT + published, + NULL::int, + NULL::int, + id, + NULL::int +FROM + person_post_mention +UNION ALL +SELECT + published, + NULL::int, + NULL::int, + NULL::int, + id +FROM + private_message; + diff --git a/src/api_routes_v3.rs b/src/api_routes_v3.rs index 5e8fb741d..d9e225c25 100644 --- a/src/api_routes_v3.rs +++ b/src/api_routes_v3.rs @@ -28,10 +28,9 @@ use lemmy_api::{ login::login, logout::logout, notifications::{ - list_mentions::list_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_post_mention_read::mark_post_mention_as_read, mark_reply_read::mark_reply_as_read, unread_count::unread_count, }, @@ -109,7 +108,6 @@ use lemmy_api_crud::{ private_message::{ create::create_private_message, delete::delete_private_message, - read::get_private_message, update::update_private_message, }, site::{create::create_site, read::get_site_v3, update::update_site}, @@ -242,7 +240,6 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .service( scope("/private_message") .wrap(rate_limit.message()) - .route("/list", get().to(get_private_message)) .route("", post().to(create_private_message)) .route("", put().to(update_private_message)) .route("/delete", post().to(delete_private_message)) @@ -302,12 +299,14 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { scope("/user") .wrap(rate_limit.message()) .route("", get().to(read_person)) - .route("/mention", get().to(list_mentions)) .route( - "/mention/mark_as_read", - post().to(mark_person_mention_as_read), + "/mention/comment/mark_as_read", + post().to(mark_comment_mention_as_read), + ) + .route( + "/mention/post/mark_as_read", + post().to(mark_post_mention_as_read), ) - .route("/replies", get().to(list_replies)) // Admin action. I don't like that it's in /user .route("/ban", post().to(ban_from_site)) .route("/banned", get().to(list_banned_users)) diff --git a/src/api_routes_v4.rs b/src/api_routes_v4.rs index 9f2b8d289..604552192 100644 --- a/src/api_routes_v4.rs +++ b/src/api_routes_v4.rs @@ -35,10 +35,10 @@ use lemmy_api::{ login::login, logout::logout, notifications::{ - list_mentions::list_mentions, - list_replies::list_replies, + list_inbox::list_inbox, 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_post_mention_read::mark_post_mention_as_read, mark_reply_read::mark_reply_as_read, unread_count::unread_count, }, @@ -126,7 +126,6 @@ use lemmy_api_crud::{ private_message::{ create::create_private_message, delete::delete_private_message, - read::get_private_message, update::update_private_message, }, site::{create::create_site, read::get_site_v4, update::update_site}, @@ -256,7 +255,6 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { // Private Message .service( scope("/private_message") - .route("/list", get().to(get_private_message)) .route("", post().to(create_private_message)) .route("", put().to(update_private_message)) .route("/delete", post().to(delete_private_message)) @@ -298,12 +296,15 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { scope("/account") .route("", get().to(get_my_user)) .route("/list_media", get().to(list_media)) - .route("/mention", get().to(list_mentions)) - .route("/replies", get().to(list_replies)) + .route("/inbox", get().to(list_inbox)) .route("/delete", post().to(delete_account)) .route( - "/mention/mark_as_read", - post().to(mark_person_mention_as_read), + "/mention/comment/mark_as_read", + post().to(mark_comment_mention_as_read), + ) + .route( + "/mention/post/mark_as_read", + post().to(mark_post_mention_as_read), ) .route( "/mention/mark_as_read/all",