Most of the bulk work done, need to add tests yet.

This commit is contained in:
Dessalines 2024-12-11 15:06:08 -05:00
parent f133079f0b
commit 05f218d53c
44 changed files with 1528 additions and 741 deletions

3
Cargo.lock generated
View file

@ -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",
]

View file

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

View file

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

View file

@ -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<GetPersonPostMentions>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<GetPersonPostMentionsResponse>> {
let sort = data.sort;
let page = data.page;
let limit = data.limit;
let unread_only = data.unread_only.unwrap_or_default();
let person_id = Some(local_user_view.person.id);
let show_bot_accounts = local_user_view.local_user.show_bot_accounts;
let post_mentions = PersonPostMentionQuery {
recipient_id: person_id,
my_person_id: person_id,
sort,
unread_only,
show_bot_accounts,
page,
limit,
}
.list(&mut context.pool())
.await?;
Ok(Json(GetPersonPostMentionsResponse { post_mentions }))
}

View file

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

View file

@ -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<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<GetRepliesResponse>> {
) -> LemmyResult<Json<SuccessResponse>> {
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()))
}

View file

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

View file

@ -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<Json<GetUnreadCountResponse>> {
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 }))
}

View file

@ -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,

View file

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

View file

@ -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,10 +116,16 @@ pub async fn send_local_notifs(
(None, post_view.post, post_view.community)
}
PostOrCommentId::Comment(comment_id) => {
// 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,
local_user_view.map(|view| &view.local_user),
Some(&local_user_view.local_user),
)
.await?;
(
@ -151,6 +133,12 @@ pub async fn send_local_notifs(
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)
}
}
};

View file

@ -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<CommentSortType>,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>,
/// Get your inbox (replies, comment mentions, post mentions, and messages)
pub struct ListInbox {
#[cfg_attr(feature = "full", ts(optional))]
pub unread_only: Option<bool>,
}
#[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<CommentReplyView>,
}
#[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<CommentSortType>,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>,
pub page_cursor: Option<InboxCombinedPaginationCursor>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub unread_only: Option<bool>,
pub page_back: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The response of mentions for your user.
pub struct GetPersonCommentMentionsResponse {
pub comment_mentions: Vec<PersonCommentMentionView>,
/// Get your inbox (replies, comment mentions, post mentions, and messages)
pub struct ListInboxResponse {
pub inbox: Vec<InboxCombinedView>,
}
#[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<PostSortType>,
pub page: Option<i64>,
pub limit: Option<i64>,
pub unread_only: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The response of mentions for your user.
pub struct GetPersonPostMentionsResponse {
pub post_mentions: Vec<PersonPostMentionView>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
@ -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)]

View file

@ -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<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub page: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>,
#[cfg_attr(feature = "full", ts(optional))]
pub creator_id: Option<PersonId>,
}
#[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<PrivateMessageView>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]

View file

@ -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::{

View file

@ -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},

View file

@ -14,6 +14,7 @@ use lemmy_api_common::{
},
};
use lemmy_db_schema::{
impls::actor_language::validate_post_language,
newtypes::PostOrCommentId,
source::{
comment::{Comment, CommentUpdateForm},

View file

@ -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
}

View file

@ -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},

View file

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

View file

@ -1,4 +1,3 @@
pub mod create;
pub mod delete;
pub mod read;
pub mod update;

View file

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

View file

@ -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,

View file

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

View file

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

View file

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

View file

@ -330,6 +330,17 @@ diesel::table! {
}
}
diesel::table! {
inbox_combined (id) {
id -> Int4,
published -> Timestamptz,
comment_reply_id -> Nullable<Int4>,
person_comment_mention_id -> Nullable<Int4>,
person_post_mention_id -> Nullable<Int4>,
private_message_id -> Nullable<Int4>,
}
}
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,

View file

@ -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<Utc>,
pub comment_reply_id: Option<CommentReplyId>,
pub person_comment_mention_id: Option<PersonCommentMentionId>,
pub person_post_mention_id: Option<PersonPostMentionId>,
pub private_message_id: Option<PrivateMessageId>,
}

View file

@ -1,3 +1,4 @@
pub mod inbox;
pub mod modlog;
pub mod person_content;
pub mod person_saved;

View file

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

View file

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

View file

@ -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 }

View file

@ -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<PersonId>)>,
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

View file

@ -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<i64, Error> {
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::<i64>(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<PaginationCursorData, Error> {
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<bool>,
#[new(default)]
pub show_bot_accounts: Option<bool>,
#[new(default)]
pub page_after: Option<PaginationCursorData>,
#[new(default)]
pub page_back: Option<bool>,
}
impl InboxCombinedQuery {
pub async fn list(self, pool: &mut DbPool<'_>) -> LemmyResult<Vec<InboxCombinedView>> {
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::<InboxCombinedViewInternal>(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<Self::CombinedView> {
// 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<Data> {
// 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(())
// }
// }

View file

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

View file

@ -1,7 +1,6 @@
use crate::structs::PersonCommentMentionView;
use diesel::{
dsl::{exists, not},
pg::Pg,
result::Error,
BoolExpressionMethods,
ExpressionMethods,
@ -26,24 +25,17 @@ 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<PersonId>)>,
impl ListFn<'a, PersonCommentMentionView, PersonCommentMentionQuery>,
> {
impl PersonCommentMentionView {
pub async fn read(
pool: &mut DbPool<'_>,
person_comment_mention_id: PersonCommentMentionId,
my_person_id: Option<PersonId>,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
let creator_is_admin = exists(
local_user::table.filter(
comment::creator_id
@ -52,9 +44,8 @@ fn queries<'a>() -> Queries<
),
);
let all_joins = move |query: person_comment_mention::BoxedQuery<'a, Pg>,
my_person_id: Option<PersonId>| {
query
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<PersonId>,
)| 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::<PersonCommentMentionView>(&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<PersonId>,
) -> Result<Self, Error> {
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<PersonId>,
pub recipient_id: Option<PersonId>,
pub sort: Option<CommentSortType>,
pub unread_only: bool,
pub show_bot_accounts: bool,
pub page: Option<i64>,
pub limit: Option<i64>,
}
impl PersonCommentMentionQuery {
pub async fn list(self, pool: &mut DbPool<'_>) -> Result<Vec<PersonCommentMentionView>, 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(())
}
}

View file

@ -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<PersonId>,
) -> Result<Self, Error> {
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
}
}

View file

@ -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<ImageDetails>,
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<i16>,
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<CommentReply>,
// Person comment mention
pub person_comment_mention: Option<PersonCommentMention>,
// Person post mention
pub person_post_mention: Option<PersonPostMention>,
pub post_counts: Option<PostAggregates>,
pub post_unread_comments: Option<i64>,
pub post_saved: bool,
pub post_read: bool,
pub post_hidden: bool,
pub my_post_vote: Option<i16>,
pub image_details: Option<ImageDetails>,
// Private message
pub private_message: Option<PrivateMessage>,
// Shared
pub post: Option<Post>,
pub community: Option<Community>,
pub comment: Option<Comment>,
pub comment_counts: Option<CommentAggregates>,
pub comment_saved: bool,
pub my_comment_vote: Option<i16>,
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),
}

View file

@ -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<Channel> {
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<Channe
#[tracing::instrument(skip_all)]
fn create_reply_and_mention_items(
replies: Vec<CommentReplyView>,
comment_mentions: Vec<PersonCommentMentionView>,
post_mentions: Vec<PersonPostMentionView>,
inbox: Vec<InboxCombinedView>,
protocol_and_hostname: &str,
) -> LemmyResult<Vec<Item>> {
let mut reply_items: Vec<Item> = replies
let reply_items: Vec<Item> = inbox
.iter()
.map(|r| {
let reply_url = format!("{}/comment/{}", protocol_and_hostname, r.comment.id);
.map(|r| match r {
InboxCombinedView::CommentReply(v) => {
let reply_url = format!("{}/comment/{}", protocol_and_hostname, v.comment.id);
build_item(
&r.creator.name,
&r.comment.published,
&v.creator.name,
&v.comment.published,
&reply_url,
&r.comment.content,
&v.comment.content,
protocol_and_hostname,
)
})
.collect::<LemmyResult<Vec<Item>>>()?;
let mut comment_mention_items: Vec<Item> = comment_mentions
.iter()
.map(|m| {
let mention_url = format!("{}/comment/{}", protocol_and_hostname, m.comment.id);
}
InboxCombinedView::CommentMention(v) => {
let mention_url = format!("{}/comment/{}", protocol_and_hostname, v.comment.id);
build_item(
&m.creator.name,
&m.comment.published,
&v.creator.name,
&v.comment.published,
&mention_url,
&m.comment.content,
&v.comment.content,
protocol_and_hostname,
)
})
.collect::<LemmyResult<Vec<Item>>>()?;
reply_items.append(&mut comment_mention_items);
let mut post_mention_items: Vec<Item> = post_mentions
.iter()
.map(|m| {
let mention_url = format!("{}/post/{}", protocol_and_hostname, m.post.id);
}
InboxCombinedView::PostMention(v) => {
let mention_url = format!("{}/post/{}", protocol_and_hostname, v.post.id);
build_item(
&m.creator.name,
&m.post.published,
&v.creator.name,
&v.post.published,
&mention_url,
&m.post.body.clone().unwrap_or_default(),
&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::<LemmyResult<Vec<Item>>>()?;
reply_items.append(&mut post_mention_items);
Ok(reply_items)
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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",