2
0
Fork 0
mirror of https://github.com/LemmyNet/lemmy synced 2025-02-20 16:08:59 +00:00

Adding combined inbox ()

* Renaming person_mention to person_comment_mention.

* Finishing up post body mentions.

* Combined tables try 2

* Finishing up combined report table.

* Fix ts optionals.

* Adding tests, triggers, and history updates for report_combined.

* Adding profile.

* Add cursor pagination to report_combined view ()

* add pagination cursor

* store timestamp instead of id in cursor (partial)

* Revert "store timestamp instead of id in cursor (partial)"

This reverts commit 89359dde4b.

* use paginated query builder

* Fixing migration and paged API.

* Using dullbananas trigger procedure

* Removing pointless list routes, reorganizing tests.

* Fixing column XOR check.

* Forgot to remove list report actions.

* Cleanup.

* Use internal tagging.

* Fixing api tests.

* Adding a few indexes.

* Fixing migration name.

* Fixing unique constraints.

* Addressing PR comments.

* Start working on profile combined

* Adding views and replaceable schema.

* A few changes to profile view.

- Separating the profile fetch from its combined content fetch.
- Starting to separate saved_only into its own combined view.

* Finishing up combined person_saved and person_content.

* Fixing api tests.

* Moving to api-v4 routes.

* Fixing imports.

* Update crates/db_views/src/report_combined_view.rs

Co-authored-by: dullbananas <dull.bananas0@gmail.com>

* Update crates/db_views/src/report_combined_view.rs

Co-authored-by: dullbananas <dull.bananas0@gmail.com>

* Update crates/db_views/src/report_combined_view.rs

Co-authored-by: dullbananas <dull.bananas0@gmail.com>

* Update migrations/2024-12-02-181601_add_report_combined_table/up.sql

Co-authored-by: dullbananas <dull.bananas0@gmail.com>

* Update migrations/2024-12-02-181601_add_report_combined_table/up.sql

Co-authored-by: dullbananas <dull.bananas0@gmail.com>

* Fixing import and fmt.

* Fixing null types in postgres.

* Comment out err.

* Fixing TS issues.

* Adding types, fixing allow and blocklist crud.

* Starting to work on combined views.

* Using dullbananas trigger procedure

* Adding the full combined view queries.

* Adding tests.

* taplo fmt.

* Upgrading package.json deps.

* Updating pnpm

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

* Finishing up inbox.

* Using assert_length

* Fixing sql_format.

* Running fmt.

* Fixing cargo shear.

* Fixing clippy.

* Addressing PR comments.

* Removing serialization

* Removing serialization

* Fixing duped trigger.

* Remove saved_only test.

* Remove pointless post_tags types.

* Remove pointless index.

* Changing published to saved for person_saved_combined.

* Removing comment.

* Renaming modlog when_ columns to published.

- Fixes 

* Adding strum and simplifying imports.

* Avoiding clone in map_to_enum

* Changing modded_person to other_person.

* Update crates/db_views_moderator/src/modlog_combined_view.rs

Co-authored-by: dullbananas <dull.bananas0@gmail.com>

* Update crates/db_views_moderator/src/modlog_combined_view.rs

Co-authored-by: dullbananas <dull.bananas0@gmail.com>

* Update crates/db_views_moderator/src/modlog_combined_view.rs

Co-authored-by: dullbananas <dull.bananas0@gmail.com>

* Addressing PR comments.

* Fixing split.

* Revert "Adding strum and simplifying imports."

This reverts commit 15f1671107.

* Running fmt.

* Using assert + matches instead of filter_map.

* Adding listPersonContent check.

* Updating lemmy-js-client

* Fixing mark all as read route, changing mark read to SuccessResponse.

* Adding post body mention api test, fixing api tests.

* Fixing route locations, and api tests.

---------

Co-authored-by: dullbananas <dull.bananas0@gmail.com>
This commit is contained in:
Dessalines 2025-01-15 12:51:39 -05:00 committed by GitHub
parent 5bc3f0c4d9
commit 3f06317878
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 2193 additions and 1507 deletions

2
Cargo.lock generated
View file

@ -2752,8 +2752,8 @@ dependencies = [
"chrono",
"diesel",
"diesel-async",
"i-love-jesus",
"lemmy_db_schema",
"lemmy_db_views",
"lemmy_utils",
"pretty_assertions",
"serde",

View file

@ -28,7 +28,7 @@
"eslint": "^9.18.0",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.5.0",
"lemmy-js-client": "0.20.0-no-delete-token.2",
"lemmy-js-client": "0.20.0-inbox-combined.1",
"prettier": "^3.4.2",
"ts-jest": "^29.1.0",
"typescript": "^5.7.3",

View file

@ -30,8 +30,8 @@ importers:
specifier: ^29.5.0
version: 29.7.0(@types/node@22.10.6)
lemmy-js-client:
specifier: 0.20.0-no-delete-token.2
version: 0.20.0-no-delete-token.2
specifier: 0.20.0-inbox-combined.1
version: 0.20.0-inbox-combined.1
prettier:
specifier: ^3.4.2
version: 3.4.2
@ -1157,8 +1157,8 @@ packages:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
lemmy-js-client@0.20.0-no-delete-token.2:
resolution: {integrity: sha512-3ra3DpD8XR6RRwCeUDLI/ztFgVuF1IoUoft+xKVDALyupwRWUsA3JcHXRIcFd1a2Qt+pHJtWbc5Iwvybakxwdg==}
lemmy-js-client@0.20.0-inbox-combined.1:
resolution: {integrity: sha512-sFJJePXdMHIVQwCa3fN+nIcIvfD7ZbBEZn08fmITXEA6/qbJLvZGWG/rEcRNkZM+lRKnhfrZihWKx1AHZE9wqA==}
leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
@ -3060,7 +3060,7 @@ snapshots:
kleur@3.0.3: {}
lemmy-js-client@0.20.0-no-delete-token.2: {}
lemmy-js-client@0.20.0-inbox-combined.1: {}
leven@3.1.0: {}

View file

@ -16,7 +16,6 @@ import {
editComment,
deleteComment,
removeComment,
getMentions,
resolvePost,
unfollowRemotes,
createCommunity,
@ -27,7 +26,6 @@ import {
getComments,
getCommentParentId,
resolveCommunity,
getReplies,
getUnreadCount,
waitUntil,
waitForPost,
@ -38,11 +36,14 @@ import {
saveUserSettings,
listReports,
listPersonContent,
listInbox,
} from "./shared";
import {
CommentReplyView,
CommentReportView,
CommentView,
CommunityView,
PersonCommentMentionView,
ReportCombinedView,
SaveUserSettings,
} from "lemmy-js-client";
@ -356,7 +357,7 @@ test("Federated comment like", async () => {
});
test("Reply to a comment from another instance, get notification", async () => {
await alpha.markAllAsRead();
await alpha.markAllNotificationsAsRead();
let betaCommunity = (
await waitUntil(
@ -423,18 +424,18 @@ test("Reply to a comment from another instance, get notification", async () => {
// Did alpha get notified of the reply from beta?
let alphaUnreadCountRes = await waitUntil(
() => getUnreadCount(alpha),
e => e.replies >= 1,
e => e.count >= 1,
);
expect(alphaUnreadCountRes.replies).toBeGreaterThanOrEqual(1);
expect(alphaUnreadCountRes.count).toBeGreaterThanOrEqual(1);
// check inbox of replies on alpha, fetching read/unread both
let alphaRepliesRes = await waitUntil(
() => getReplies(alpha),
r => r.replies.length > 0,
);
const alphaReply = alphaRepliesRes.replies.find(
r => r.comment.id === alphaComment.comment.id,
() => listInbox(alpha, "CommentReply"),
r => r.inbox.length > 0,
);
const alphaReply = alphaRepliesRes.inbox.find(
r => r.type_ == "CommentReply" && r.comment.id === alphaComment.comment.id,
) as CommentReplyView | undefined;
expect(alphaReply).toBeDefined();
if (!alphaReply) throw Error();
expect(alphaReply.comment.content).toBeDefined();
@ -463,7 +464,7 @@ test("Bot reply notifications are filtered when bots are hidden", async () => {
throw "Missing alpha community";
}
await alpha.markAllAsRead();
await alpha.markAllNotificationsAsRead();
form = {
show_bot_accounts: false,
};
@ -478,10 +479,7 @@ test("Bot reply notifications are filtered when bots are hidden", async () => {
expect(commentRes).toBeDefined();
let alphaUnreadCountRes = await getUnreadCount(alpha);
expect(alphaUnreadCountRes.replies).toBe(0);
let alphaUnreadRepliesRes = await getReplies(alpha, true);
expect(alphaUnreadRepliesRes.replies.length).toBe(0);
expect(alphaUnreadCountRes.count).toBe(0);
// This both restores the original state that may be expected by other tests
// implicitly and is used by the next steps to ensure replies are still
@ -492,16 +490,16 @@ test("Bot reply notifications are filtered when bots are hidden", async () => {
await saveUserSettings(alpha, form);
alphaUnreadCountRes = await getUnreadCount(alpha);
expect(alphaUnreadCountRes.replies).toBe(1);
expect(alphaUnreadCountRes.count).toBe(1);
alphaUnreadRepliesRes = await getReplies(alpha, true);
expect(alphaUnreadRepliesRes.replies.length).toBe(1);
expect(alphaUnreadRepliesRes.replies[0].comment.id).toBe(
let alphaUnreadRepliesRes = await listInbox(alpha, "CommentReply", true);
expect(alphaUnreadRepliesRes.inbox.length).toBe(1);
expect((alphaUnreadRepliesRes.inbox[0] as CommentReplyView).comment.id).toBe(
commentRes.comment_view.comment.id,
);
});
test("Mention beta from alpha", async () => {
test("Mention beta from alpha comment", async () => {
if (!betaCommunity) throw Error("no community");
const postOnAlphaRes = await createPost(alpha, betaCommunity.community.id);
// Create a new branch, trunk-level comment branch, from alpha instance
@ -548,15 +546,17 @@ test("Mention beta from alpha", async () => {
assertCommentFederation(betaRootComment, commentRes.comment_view);
let mentionsRes = await waitUntil(
() => getMentions(beta),
m => !!m.mentions[0],
() => listInbox(beta, "CommentMention"),
m => !!m.inbox[0],
);
expect(mentionsRes.mentions[0].comment.content).toBeDefined();
expect(mentionsRes.mentions[0].community.local).toBe(true);
expect(mentionsRes.mentions[0].creator.local).toBe(false);
expect(mentionsRes.mentions[0].counts.score).toBe(1);
const firstMention = mentionsRes.inbox[0] as PersonCommentMentionView;
expect(firstMention.comment.content).toBeDefined();
expect(firstMention.community.local).toBe(true);
expect(firstMention.creator.local).toBe(false);
expect(firstMention.counts.score).toBe(1);
// the reply comment with mention should be the most fresh, newest, index 0
expect(mentionsRes.mentions[0].person_mention.comment_id).toBe(
expect(firstMention.person_comment_mention.comment_id).toBe(
betaPostComments.comments[0].comment.id,
);
});
@ -623,15 +623,17 @@ test("A and G subscribe to B (center) A posts, G mentions B, it gets announced t
);
// Make sure beta has mentions
let relevantMention = await waitUntil(
let relevantMention = (await waitUntil(
() =>
getMentions(beta).then(m =>
m.mentions.find(
m => m.comment.ap_id === commentRes.comment_view.comment.ap_id,
listInbox(beta, "CommentMention").then(m =>
m.inbox.find(
m =>
m.type_ == "CommentMention" &&
m.comment.ap_id === commentRes.comment_view.comment.ap_id,
),
),
e => !!e,
);
)) as PersonCommentMentionView | undefined;
if (!relevantMention) throw Error("could not find mention");
expect(relevantMention.comment.content).toBe(commentContent);
expect(relevantMention.community.local).toBe(false);
@ -824,6 +826,7 @@ test("Report a comment", async () => {
});
test("Dont send a comment reply to a blocked community", async () => {
await beta.markAllNotificationsAsRead();
let newCommunity = await createCommunity(beta);
let newCommunityId = newCommunity.community_view.community.id;
@ -837,7 +840,7 @@ test("Dont send a comment reply to a blocked community", async () => {
// Check beta's inbox count
let unreadCount = await getUnreadCount(beta);
expect(unreadCount.replies).toBe(1);
expect(unreadCount.count).toBe(0);
// Beta blocks the new beta community
let blockRes = await blockCommunity(beta, newCommunityId, true);
@ -857,10 +860,10 @@ test("Dont send a comment reply to a blocked community", async () => {
// Check beta's inbox count, make sure it stays the same
unreadCount = await getUnreadCount(beta);
expect(unreadCount.replies).toBe(1);
expect(unreadCount.count).toBe(0);
let replies = await getReplies(beta);
expect(replies.replies.length).toBe(1);
let replies = await listInbox(beta, "CommentReply", true);
expect(replies.inbox.length).toBe(0);
// Unblock the community
blockRes = await blockCommunity(beta, newCommunityId, false);

View file

@ -38,11 +38,13 @@ import {
createCommunity,
listReports,
getMyUser,
listInbox,
} from "./shared";
import { PostView } from "lemmy-js-client/dist/types/PostView";
import { AdminBlockInstanceParams } from "lemmy-js-client/dist/types/AdminBlockInstanceParams";
import {
EditSite,
PersonPostMentionView,
PostReport,
PostReportView,
ReportCombinedView,
@ -799,6 +801,44 @@ test("Fetch post with redirect", async () => {
expect(gammaPost2.post).toBeDefined();
});
test("Mention beta from alpha post body", async () => {
if (!betaCommunity) throw Error("no community");
let mentionContent = "A test mention of @lemmy_beta@lemmy-beta:8551";
const postOnAlphaRes = await createPost(
alpha,
betaCommunity.community.id,
undefined,
mentionContent,
);
expect(postOnAlphaRes.post_view.post.body).toBeDefined();
expect(postOnAlphaRes.post_view.community.local).toBe(false);
expect(postOnAlphaRes.post_view.creator.local).toBe(true);
expect(postOnAlphaRes.post_view.counts.score).toBe(1);
// get beta's localized copy of the alpha post
let betaPost = await waitForPost(beta, postOnAlphaRes.post_view.post);
if (!betaPost) {
throw "unable to locate post on beta";
}
expect(betaPost.post.ap_id).toBe(postOnAlphaRes.post_view.post.ap_id);
expect(betaPost.post.name).toBe(postOnAlphaRes.post_view.post.name);
await assertPostFederation(betaPost, postOnAlphaRes.post_view);
let mentionsRes = await waitUntil(
() => listInbox(beta, "PostMention"),
m => !!m.inbox[0],
);
const firstMention = mentionsRes.inbox[0] as PersonPostMentionView;
expect(firstMention.post.body).toBeDefined();
expect(firstMention.community.local).toBe(true);
expect(firstMention.creator.local).toBe(false);
expect(firstMention.counts.score).toBe(1);
expect(firstMention.person_post_mention.post_id).toBe(betaPost.post.id);
});
test("Rewrite markdown links", async () => {
const community = (await resolveBetaCommunity(beta)).community!;

View file

@ -1,4 +1,5 @@
jest.setTimeout(120000);
import { PrivateMessageView } from "lemmy-js-client";
import {
alpha,
beta,
@ -6,11 +7,11 @@ import {
followBeta,
createPrivateMessage,
editPrivateMessage,
listPrivateMessages,
deletePrivateMessage,
waitUntil,
reportPrivateMessage,
unfollows,
listInbox,
} from "./shared";
let recipient_id: number;
@ -31,13 +32,14 @@ test("Create a private message", async () => {
expect(pmRes.private_message_view.recipient.local).toBe(false);
let betaPms = await waitUntil(
() => listPrivateMessages(beta),
e => !!e.private_messages[0],
() => listInbox(beta, "PrivateMessage"),
e => !!e.inbox[0],
);
expect(betaPms.private_messages[0].private_message.content).toBeDefined();
expect(betaPms.private_messages[0].private_message.local).toBe(false);
expect(betaPms.private_messages[0].creator.local).toBe(false);
expect(betaPms.private_messages[0].recipient.local).toBe(true);
const firstPm = betaPms.inbox[0] as PrivateMessageView;
expect(firstPm.private_message.content).toBeDefined();
expect(firstPm.private_message.local).toBe(false);
expect(firstPm.creator.local).toBe(false);
expect(firstPm.recipient.local).toBe(true);
});
test("Update a private message", async () => {
@ -53,10 +55,12 @@ test("Update a private message", async () => {
);
let betaPms = await waitUntil(
() => listPrivateMessages(beta),
p => p.private_messages[0].private_message.content === updatedContent,
() => listInbox(beta, "PrivateMessage"),
p =>
p.inbox[0].type_ == "PrivateMessage" &&
p.inbox[0].private_message.content === updatedContent,
);
expect(betaPms.private_messages[0].private_message.content).toBe(
expect((betaPms.inbox[0] as PrivateMessageView).private_message.content).toBe(
updatedContent,
);
});
@ -64,12 +68,13 @@ test("Update a private message", async () => {
test("Delete a private message", async () => {
let pmRes = await createPrivateMessage(alpha, recipient_id);
let betaPms1 = await waitUntil(
() => listPrivateMessages(beta),
() => listInbox(beta, "PrivateMessage"),
m =>
!!m.private_messages.find(
!!m.inbox.find(
e =>
e.type_ == "PrivateMessage" &&
e.private_message.ap_id ===
pmRes.private_message_view.private_message.ap_id,
pmRes.private_message_view.private_message.ap_id,
),
);
let deletedPmRes = await deletePrivateMessage(
@ -83,12 +88,10 @@ test("Delete a private message", async () => {
// even though they are in the actual database.
// no reason to show them
let betaPms2 = await waitUntil(
() => listPrivateMessages(beta),
p => p.private_messages.length === betaPms1.private_messages.length - 1,
);
expect(betaPms2.private_messages.length).toBe(
betaPms1.private_messages.length - 1,
() => listInbox(beta, "PrivateMessage"),
p => p.inbox.length === betaPms1.inbox.length - 1,
);
expect(betaPms2.inbox.length).toBe(betaPms1.inbox.length - 1);
// Undelete
let undeletedPmRes = await deletePrivateMessage(
@ -101,26 +104,25 @@ test("Delete a private message", async () => {
);
let betaPms3 = await waitUntil(
() => listPrivateMessages(beta),
p => p.private_messages.length === betaPms1.private_messages.length,
);
expect(betaPms3.private_messages.length).toBe(
betaPms1.private_messages.length,
() => listInbox(beta, "PrivateMessage"),
p => p.inbox.length === betaPms1.inbox.length,
);
expect(betaPms3.inbox.length).toBe(betaPms1.inbox.length);
});
test("Create a private message report", async () => {
let pmRes = await createPrivateMessage(alpha, recipient_id);
let betaPms1 = await waitUntil(
() => listPrivateMessages(beta),
() => listInbox(beta, "PrivateMessage"),
m =>
!!m.private_messages.find(
!!m.inbox.find(
e =>
e.type_ == "PrivateMessage" &&
e.private_message.ap_id ===
pmRes.private_message_view.private_message.ap_id,
pmRes.private_message_view.private_message.ap_id,
),
);
let betaPm = betaPms1.private_messages[0];
let betaPm = betaPms1.inbox[0] as PrivateMessageView;
expect(betaPm).toBeDefined();
// Make sure that only the recipient can report it, so this should fail

View file

@ -7,8 +7,6 @@ import {
CreatePrivateMessageReport,
EditCommunity,
GetCommunityPendingFollowsCountResponse,
GetReplies,
GetRepliesResponse,
GetUnreadCountResponse,
InstanceId,
LemmyHttp,
@ -26,6 +24,9 @@ import {
ListPersonContentResponse,
ListPersonContent,
PersonContentType,
ListInboxResponse,
ListInbox,
InboxDataType,
} from "lemmy-js-client";
import { CreatePost } from "lemmy-js-client/dist/types/CreatePost";
import { DeletePost } from "lemmy-js-client/dist/types/DeletePost";
@ -59,8 +60,6 @@ import { CreateComment } from "lemmy-js-client/dist/types/CreateComment";
import { EditComment } from "lemmy-js-client/dist/types/EditComment";
import { DeleteComment } from "lemmy-js-client/dist/types/DeleteComment";
import { RemoveComment } from "lemmy-js-client/dist/types/RemoveComment";
import { GetPersonMentionsResponse } from "lemmy-js-client/dist/types/GetPersonMentionsResponse";
import { GetPersonMentions } from "lemmy-js-client/dist/types/GetPersonMentions";
import { CreateCommentLike } from "lemmy-js-client/dist/types/CreateCommentLike";
import { CreateCommunity } from "lemmy-js-client/dist/types/CreateCommunity";
import { GetCommunity } from "lemmy-js-client/dist/types/GetCommunity";
@ -75,8 +74,6 @@ import { Register } from "lemmy-js-client/dist/types/Register";
import { SaveUserSettings } from "lemmy-js-client/dist/types/SaveUserSettings";
import { DeleteAccount } from "lemmy-js-client/dist/types/DeleteAccount";
import { GetSiteResponse } from "lemmy-js-client/dist/types/GetSiteResponse";
import { PrivateMessagesResponse } from "lemmy-js-client/dist/types/PrivateMessagesResponse";
import { GetPrivateMessages } from "lemmy-js-client/dist/types/GetPrivateMessages";
import { PostReportResponse } from "lemmy-js-client/dist/types/PostReportResponse";
import { CreatePostReport } from "lemmy-js-client/dist/types/CreatePostReport";
import { CommentReportResponse } from "lemmy-js-client/dist/types/CommentReportResponse";
@ -377,15 +374,16 @@ export async function getUnreadCount(
return api.getUnreadCount();
}
export async function getReplies(
export async function listInbox(
api: LemmyHttp,
type_?: InboxDataType,
unread_only: boolean = false,
): Promise<GetRepliesResponse> {
let form: GetReplies = {
sort: "New",
): Promise<ListInboxResponse> {
let form: ListInbox = {
unread_only,
type_,
};
return api.getReplies(form);
return api.listInbox(form);
}
export async function resolveComment(
@ -542,16 +540,6 @@ export async function removeComment(
return api.removeComment(form);
}
export async function getMentions(
api: LemmyHttp,
): Promise<GetPersonMentionsResponse> {
let form: GetPersonMentions = {
sort: "New",
unread_only: false,
};
return api.getPersonMentions(form);
}
export async function likeComment(
api: LemmyHttp,
score: number,
@ -777,15 +765,6 @@ export async function getMyUser(api: LemmyHttp): Promise<MyUserInfo> {
return api.getMyUser();
}
export async function listPrivateMessages(
api: LemmyHttp,
): Promise<PrivateMessagesResponse> {
let form: GetPrivateMessages = {
unread_only: false,
};
return api.getPrivateMessages(form);
}
export async function unfollowRemotes(api: LemmyHttp): Promise<MyUserInfo> {
// Unfollow all remote communities
let my_user = await getMyUser(api);

View file

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

View file

@ -0,0 +1,40 @@
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 type_ = data.type_;
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 {
type_,
unread_only,
show_bot_accounts,
page_after,
page_back,
}
.list(&mut context.pool(), person_id)
.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::{GetPersonMentions, GetPersonMentionsResponse},
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::person_mention_view::PersonMentionQuery;
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn list_mentions(
data: Query<GetPersonMentions>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<GetPersonMentionsResponse>> {
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 mentions = PersonMentionQuery {
recipient_id: person_id,
my_person_id: person_id,
sort,
unread_only,
show_bot_accounts,
page,
limit,
}
.list(&mut context.pool())
.await?;
Ok(Json(GetPersonMentionsResponse { 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_mention::PersonMention,
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
@ -20,15 +21,20 @@ pub async fn mark_all_notifications_read(
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
// Mark all user mentions as read
PersonMention::mark_all_as_read(&mut context.pool(), person_id)
// Mark all comment mentions as read
PersonCommentMention::mark_all_as_read(&mut context.pool(), person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
// 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

@ -0,0 +1,39 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
person::MarkPersonCommentMentionAsRead,
SuccessResponse,
};
use lemmy_db_schema::{
source::person_comment_mention::{PersonCommentMention, PersonCommentMentionUpdateForm},
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))]
pub async fn mark_comment_mention_as_read(
data: Json<MarkPersonCommentMentionAsRead>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
let person_comment_mention_id = data.person_comment_mention_id;
let read_person_comment_mention =
PersonCommentMention::read(&mut context.pool(), person_comment_mention_id).await?;
if local_user_view.person.id != read_person_comment_mention.recipient_id {
Err(LemmyErrorType::CouldntUpdateComment)?
}
let person_comment_mention_id = read_person_comment_mention.id;
let read = Some(data.read);
PersonCommentMention::update(
&mut context.pool(),
person_comment_mention_id,
&PersonCommentMentionUpdateForm { read },
)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -1,45 +0,0 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
person::{MarkPersonMentionAsRead, PersonMentionResponse},
};
use lemmy_db_schema::{
source::person_mention::{PersonMention, PersonMentionUpdateForm},
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::PersonMentionView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))]
pub async fn mark_person_mention_as_read(
data: Json<MarkPersonMentionAsRead>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<PersonMentionResponse>> {
let person_mention_id = data.person_mention_id;
let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id).await?;
if local_user_view.person.id != read_person_mention.recipient_id {
Err(LemmyErrorType::CouldntUpdateComment)?
}
let person_mention_id = read_person_mention.id;
let read = Some(data.read);
PersonMention::update(
&mut context.pool(),
person_mention_id,
&PersonMentionUpdateForm { read },
)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
let person_mention_id = read_person_mention.id;
let person_id = local_user_view.person.id;
let person_mention_view =
PersonMentionView::read(&mut context.pool(), person_mention_id, Some(person_id)).await?;
Ok(Json(PersonMentionResponse {
person_mention_view,
}))
}

View file

@ -0,0 +1,39 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
person::MarkPersonPostMentionAsRead,
SuccessResponse,
};
use lemmy_db_schema::{
source::person_post_mention::{PersonPostMention, PersonPostMentionUpdateForm},
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))]
pub async fn mark_post_mention_as_read(
data: Json<MarkPersonPostMentionAsRead>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
let person_post_mention_id = data.person_post_mention_id;
let read_person_post_mention =
PersonPostMention::read(&mut context.pool(), person_post_mention_id).await?;
if local_user_view.person.id != read_person_post_mention.recipient_id {
Err(LemmyErrorType::CouldntUpdatePost)?
}
let person_post_mention_id = read_person_post_mention.id;
let read = Some(data.read);
PersonPostMention::update(
&mut context.pool(),
person_post_mention_id,
&PersonPostMentionUpdateForm { read },
)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -1,14 +1,10 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
person::{CommentReplyResponse, MarkCommentReplyAsRead},
};
use lemmy_api_common::{context::LemmyContext, person::MarkCommentReplyAsRead, SuccessResponse};
use lemmy_db_schema::{
source::comment_reply::{CommentReply, CommentReplyUpdateForm},
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommentReplyView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))]
@ -16,7 +12,7 @@ pub async fn mark_reply_as_read(
data: Json<MarkCommentReplyAsRead>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentReplyResponse>> {
) -> LemmyResult<Json<SuccessResponse>> {
let comment_reply_id = data.comment_reply_id;
let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id).await?;
@ -35,10 +31,5 @@ pub async fn mark_reply_as_read(
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
let comment_reply_id = read_comment_reply.id;
let person_id = local_user_view.person.id;
let comment_reply_view =
CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id)).await?;
Ok(Json(CommentReplyResponse { comment_reply_view }))
Ok(Json(SuccessResponse::default()))
}

View file

@ -1,6 +1,6 @@
pub mod list_mentions;
pub mod list_replies;
pub mod list_inbox;
pub mod mark_all_read;
pub mod mark_mention_read;
pub mod mark_comment_mention_read;
pub mod mark_post_mention_read;
pub mod mark_reply_read;
pub mod unread_count;

View file

@ -1,7 +1,7 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{context::LemmyContext, person::GetUnreadCountResponse};
use lemmy_db_views::structs::{LocalUserView, PrivateMessageView};
use lemmy_db_views_actor::structs::{CommentReplyView, PersonMentionView};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::InboxCombinedViewInternal;
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
@ -10,20 +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_replies(&mut context.pool(), &local_user_view.local_user).await?;
let mentions =
PersonMentionView::get_unread_mentions(&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 private_messages =
PrivateMessageView::get_unread_messages(&mut context.pool(), person_id).await?;
Ok(Json(GetUnreadCountResponse {
replies,
mentions,
private_messages,
}))
Ok(Json(GetUnreadCountResponse { count }))
}

View file

@ -5,9 +5,10 @@ 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, VoteItem},
utils::{check_bot_account, check_community_user_action, check_local_vote_mode},
};
use lemmy_db_schema::{
newtypes::PostOrCommentId,
source::{
local_site::LocalSite,
post::{PostLike, PostLikeForm, PostRead, PostReadForm},
@ -29,7 +30,7 @@ pub async fn like_post(
check_local_vote_mode(
data.score,
VoteItem::Post(post_id),
PostOrCommentId::Post(post_id),
&local_site,
local_user_view.person.id,
&mut context.pool(),

View file

@ -1,13 +1,14 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
private_message::{MarkPrivateMessageAsRead, PrivateMessageResponse},
private_message::MarkPrivateMessageAsRead,
SuccessResponse,
};
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_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))]
@ -15,7 +16,7 @@ pub async fn mark_pm_as_read(
data: Json<MarkPrivateMessageAsRead>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<PrivateMessageResponse>> {
) -> LemmyResult<Json<SuccessResponse>> {
// Checking permissions
let private_message_id = data.private_message_id;
let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?;
@ -37,8 +38,5 @@ pub async fn mark_pm_as_read(
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?;
let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?;
Ok(Json(PrivateMessageResponse {
private_message_view: view,
}))
Ok(Json(SuccessResponse::default()))
}

View file

@ -12,14 +12,15 @@ use crate::{
};
use actix_web::web::Json;
use lemmy_db_schema::{
newtypes::{CommentId, CommunityId, LocalUserId, PostId},
newtypes::{CommentId, CommunityId, LocalUserId, PostId, PostOrCommentId},
source::{
actor_language::CommunityLanguage,
comment::Comment,
comment_reply::{CommentReply, CommentReplyInsertForm},
community::Community,
person::Person,
person_mention::{PersonMention, PersonMentionInsertForm},
person_comment_mention::{PersonCommentMention, PersonCommentMentionInsertForm},
person_post_mention::{PersonPostMention, PersonPostMentionInsertForm},
post::Post,
},
traits::Crud,
@ -94,7 +95,7 @@ pub async fn build_post_response(
#[tracing::instrument(skip_all)]
pub async fn send_local_notifs(
mentions: Vec<MentionData>,
comment_id: CommentId,
post_or_comment_id: PostOrCommentId,
person: &Person,
do_send_email: bool,
context: &LemmyContext,
@ -103,27 +104,42 @@ 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 (comment_opt, post, community) = match post_or_comment_id {
PostOrCommentId::Post(post_id) => {
let post_view = PostView::read(
&mut context.pool(),
post_id,
local_user_view.map(|view| &view.local_user),
false,
)
.await?;
(None, post_view.post, post_view.community)
}
PostOrCommentId::Comment(comment_id) => {
// 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)
}
}
};
// Send the local mentions
@ -140,22 +156,38 @@ pub async fn send_local_notifs(
// below by checking recipient ids
recipient_ids.push(mention_user_view.local_user.id);
let user_mention_form = PersonMentionInsertForm {
recipient_id: mention_user_view.person.id,
comment_id,
read: None,
};
// Make the correct reply form depending on whether its a post or comment mention
let comment_content_or_post_body = if let Some(comment) = &comment_opt {
let person_comment_mention_form = PersonCommentMentionInsertForm {
recipient_id: mention_user_view.person.id,
comment_id: comment.id,
read: None,
};
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
PersonMention::create(&mut context.pool(), &user_mention_form)
.await
.ok();
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
PersonCommentMention::create(&mut context.pool(), &person_comment_mention_form)
.await
.ok();
comment.content.clone()
} else {
let person_post_mention_form = PersonPostMentionInsertForm {
recipient_id: mention_user_view.person.id,
post_id: post.id,
read: None,
};
// Allow this to fail softly, since edits might re-update or replace it
PersonPostMention::create(&mut context.pool(), &person_post_mention_form)
.await
.ok();
post.body.clone().unwrap_or_default()
};
// Send an email to those local users that have notifications on
if do_send_email {
let lang = get_interface_language(&mention_user_view);
let content = markdown_to_html(&comment.content);
let content = markdown_to_html(&comment_content_or_post_body);
send_email_to_user(
&mention_user_view,
&lang.notification_mentioned_by_subject(&person.name),
@ -168,99 +200,101 @@ pub async fn send_local_notifs(
}
// Send comment_reply to the parent commenter / poster
if let Some(parent_comment_id) = comment.parent_comment_id() {
let parent_comment = Comment::read(&mut context.pool(), parent_comment_id).await?;
if let Some(comment) = &comment_opt {
if let Some(parent_comment_id) = comment.parent_comment_id() {
let parent_comment = Comment::read(&mut context.pool(), parent_comment_id).await?;
// Get the parent commenter local_user
let parent_creator_id = parent_comment.creator_id;
// Get the parent commenter local_user
let parent_creator_id = parent_comment.creator_id;
let check_blocks = check_person_instance_community_block(
person.id,
parent_creator_id,
// Only block from the community's instance_id
community.instance_id,
community.id,
&mut context.pool(),
)
.await
.is_err();
let check_blocks = check_person_instance_community_block(
person.id,
parent_creator_id,
// Only block from the community's instance_id
community.instance_id,
community.id,
&mut context.pool(),
)
.await
.is_err();
// Don't send a notif to yourself
if parent_comment.creator_id != person.id && !check_blocks {
let user_view = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await;
if let Ok(parent_user_view) = user_view {
// Don't duplicate notif if already mentioned by checking recipient ids
if !recipient_ids.contains(&parent_user_view.local_user.id) {
recipient_ids.push(parent_user_view.local_user.id);
// Don't send a notif to yourself
if parent_comment.creator_id != person.id && !check_blocks {
let user_view = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await;
if let Ok(parent_user_view) = user_view {
// Don't duplicate notif if already mentioned by checking recipient ids
if !recipient_ids.contains(&parent_user_view.local_user.id) {
recipient_ids.push(parent_user_view.local_user.id);
let comment_reply_form = CommentReplyInsertForm {
recipient_id: parent_user_view.person.id,
comment_id: comment.id,
read: None,
};
let comment_reply_form = CommentReplyInsertForm {
recipient_id: parent_user_view.person.id,
comment_id: comment.id,
read: None,
};
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
CommentReply::create(&mut context.pool(), &comment_reply_form)
.await
.ok();
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
CommentReply::create(&mut context.pool(), &comment_reply_form)
.await
.ok();
if do_send_email {
let lang = get_interface_language(&parent_user_view);
let content = markdown_to_html(&comment.content);
send_email_to_user(
&parent_user_view,
&lang.notification_comment_reply_subject(&person.name),
&lang.notification_comment_reply_body(&content, &inbox_link, &person.name),
context.settings(),
)
.await
if do_send_email {
let lang = get_interface_language(&parent_user_view);
let content = markdown_to_html(&comment.content);
send_email_to_user(
&parent_user_view,
&lang.notification_comment_reply_subject(&person.name),
&lang.notification_comment_reply_body(&content, &inbox_link, &person.name),
context.settings(),
)
.await
}
}
}
}
}
} else {
// Use the post creator to check blocks
let check_blocks = check_person_instance_community_block(
person.id,
post.creator_id,
// Only block from the community's instance_id
community.instance_id,
community.id,
&mut context.pool(),
)
.await
.is_err();
} else {
// Use the post creator to check blocks
let check_blocks = check_person_instance_community_block(
person.id,
post.creator_id,
// Only block from the community's instance_id
community.instance_id,
community.id,
&mut context.pool(),
)
.await
.is_err();
if post.creator_id != person.id && !check_blocks {
let creator_id = post.creator_id;
let parent_user = LocalUserView::read_person(&mut context.pool(), creator_id).await;
if let Ok(parent_user_view) = parent_user {
if !recipient_ids.contains(&parent_user_view.local_user.id) {
recipient_ids.push(parent_user_view.local_user.id);
if post.creator_id != person.id && !check_blocks {
let creator_id = post.creator_id;
let parent_user = LocalUserView::read_person(&mut context.pool(), creator_id).await;
if let Ok(parent_user_view) = parent_user {
if !recipient_ids.contains(&parent_user_view.local_user.id) {
recipient_ids.push(parent_user_view.local_user.id);
let comment_reply_form = CommentReplyInsertForm {
recipient_id: parent_user_view.person.id,
comment_id: comment.id,
read: None,
};
let comment_reply_form = CommentReplyInsertForm {
recipient_id: parent_user_view.person.id,
comment_id: comment.id,
read: None,
};
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
CommentReply::create(&mut context.pool(), &comment_reply_form)
.await
.ok();
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
CommentReply::create(&mut context.pool(), &comment_reply_form)
.await
.ok();
if do_send_email {
let lang = get_interface_language(&parent_user_view);
let content = markdown_to_html(&comment.content);
send_email_to_user(
&parent_user_view,
&lang.notification_post_reply_subject(&person.name),
&lang.notification_post_reply_body(&content, &inbox_link, &person.name),
context.settings(),
)
.await
if do_send_email {
let lang = get_interface_language(&parent_user_view);
let content = markdown_to_html(&comment.content);
send_email_to_user(
&parent_user_view,
&lang.notification_post_reply_subject(&person.name),
&lang.notification_post_reply_body(&content, &inbox_link, &person.name),
context.settings(),
)
.await
}
}
}
}

View file

@ -1,8 +1,16 @@
use lemmy_db_schema::{
newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId},
newtypes::{
CommentReplyId,
CommunityId,
LanguageId,
PersonCommentMentionId,
PersonId,
PersonPostMentionId,
},
sensitive::SensitiveString,
source::{login_token::LoginToken, site::Site},
CommentSortType,
InboxDataType,
ListingType,
PersonContentType,
PostListingMode,
@ -15,9 +23,9 @@ use lemmy_db_views::structs::{
PersonSavedCombinedPaginationCursor,
};
use lemmy_db_views_actor::structs::{
CommentReplyView,
CommunityModeratorView,
PersonMentionView,
InboxCombinedPaginationCursor,
InboxCombinedView,
PersonView,
};
use serde::{Deserialize, Serialize};
@ -364,69 +372,45 @@ 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 {
/// Get your inbox (replies, comment mentions, post mentions, and messages)
pub struct ListInbox {
#[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>,
pub type_: Option<InboxDataType>,
#[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 GetPersonMentions {
#[cfg_attr(feature = "full", ts(optional))]
pub sort: Option<CommentSortType>,
pub page_cursor: Option<InboxCombinedPaginationCursor>,
#[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 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 GetPersonMentionsResponse {
pub mentions: Vec<PersonMentionView>,
/// 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)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Mark a person mention as read.
pub struct MarkPersonMentionAsRead {
pub person_mention_id: PersonMentionId,
pub struct MarkPersonCommentMentionAsRead {
pub person_comment_mention_id: PersonCommentMentionId,
pub read: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The response for a person mention action.
pub struct PersonMentionResponse {
pub person_mention_view: PersonMentionView,
/// Mark a person mention as read.
pub struct MarkPersonPostMentionAsRead {
pub person_post_mention_id: PersonPostMentionId,
pub read: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
@ -438,14 +422,6 @@ pub struct MarkCommentReplyAsRead {
pub read: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The response for a comment reply action.
pub struct CommentReplyResponse {
pub comment_reply_view: CommentReplyView,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
@ -495,11 +471,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 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

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

View file

@ -17,11 +17,12 @@ use lemmy_api_common::{
};
use lemmy_db_schema::{
impls::actor_language::validate_post_language,
newtypes::PostOrCommentId,
source::{
comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm},
comment_reply::{CommentReply, CommentReplyUpdateForm},
local_site::LocalSite,
person_mention::{PersonMention, PersonMentionUpdateForm},
person_comment_mention::{PersonCommentMention, PersonCommentMentionUpdateForm},
},
traits::{Crud, Likeable},
};
@ -117,7 +118,7 @@ pub async fn create_comment(
let mentions = scrape_text_for_mentions(&content);
let recipient_ids = send_local_notifs(
mentions,
inserted_comment_id,
PostOrCommentId::Comment(inserted_comment_id),
&local_user_view.person,
true,
&context,
@ -169,17 +170,18 @@ pub async fn create_comment(
.with_lemmy_type(LemmyErrorType::CouldntUpdateReplies)?;
}
// If the parent has PersonMentions mark them as read too
let person_mention =
PersonMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await;
if let Ok(Some(mention)) = person_mention {
PersonMention::update(
// If the parent has PersonCommentMentions mark them as read too
let person_comment_mention =
PersonCommentMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id)
.await;
if let Ok(Some(mention)) = person_comment_mention {
PersonCommentMention::update(
&mut context.pool(),
mention.id,
&PersonMentionUpdateForm { read: Some(true) },
&PersonCommentMentionUpdateForm { read: Some(true) },
)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePersonMentions)?;
.with_lemmy_type(LemmyErrorType::CouldntUpdatePersonCommentMentions)?;
}
}

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ use super::convert_published_time;
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
build_response::build_post_response,
build_response::{build_post_response, send_local_notifs},
context::LemmyContext,
post::{CreatePost, PostResponse},
request::generate_post_link_metadata,
@ -17,6 +17,7 @@ use lemmy_api_common::{
};
use lemmy_db_schema::{
impls::actor_language::validate_post_language,
newtypes::PostOrCommentId,
source::{
community::Community,
local_site::LocalSite,
@ -32,6 +33,7 @@ use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
spawn_try_task,
utils::{
mention::scrape_text_for_mentions,
slurs::check_slurs,
validation::{
is_url_blocked,
@ -151,6 +153,18 @@ pub async fn create_post(
.await
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
// Scan the post body for user mentions, add those rows
let mentions = scrape_text_for_mentions(&inserted_post.body.clone().unwrap_or_default());
send_local_notifs(
mentions,
PostOrCommentId::Post(inserted_post.id),
&local_user_view.person,
true,
&context,
Some(&local_user_view),
)
.await?;
let read_form = PostReadForm::new(post_id, person_id);
PostRead::mark_as_read(&mut context.pool(), &read_form).await?;

View file

@ -3,7 +3,7 @@ use activitypub_federation::config::Data;
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{
build_response::build_post_response,
build_response::{build_post_response, send_local_notifs},
context::LemmyContext,
post::{EditPost, PostResponse},
request::generate_post_link_metadata,
@ -17,6 +17,7 @@ use lemmy_api_common::{
};
use lemmy_db_schema::{
impls::actor_language::validate_post_language,
newtypes::PostOrCommentId,
source::{
community::Community,
local_site::LocalSite,
@ -29,6 +30,7 @@ use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
utils::{
mention::scrape_text_for_mentions,
slurs::check_slurs,
validation::{
is_url_blocked,
@ -142,6 +144,18 @@ pub async fn update_post(
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?;
// Scan the post body for user mentions, add those rows
let mentions = scrape_text_for_mentions(&updated_post.body.clone().unwrap_or_default());
send_local_notifs(
mentions,
PostOrCommentId::Post(updated_post.id),
&local_user_view.person,
false,
&context,
Some(&local_user_view),
)
.await?;
// send out federation/webmention if necessary
match (
orig_post.post.scheduled_publish_time,

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

@ -29,7 +29,7 @@ use lemmy_api_common::{
};
use lemmy_db_schema::{
aggregates::structs::CommentAggregates,
newtypes::PersonId,
newtypes::{PersonId, PostOrCommentId},
source::{
activity::ActivitySendTargets,
comment::{Comment, CommentLike, CommentLikeForm},
@ -175,10 +175,17 @@ impl ActivityHandler for CreateOrUpdateNote {
// TODO: for compatibility with other projects, it would be much better to read this from cc or
// tags
let mentions = scrape_text_for_mentions(&comment.content);
// TODO: this fails in local community comment as CommentView::read() returns nothing
// without passing LocalUser
send_local_notifs(mentions, comment.id, &actor, do_send_email, context, None).await?;
send_local_notifs(
mentions,
PostOrCommentId::Comment(comment.id),
&actor,
do_send_email,
context,
None,
)
.await?;
Ok(())
}
}

View file

@ -20,10 +20,10 @@ use activitypub_federation::{
protocol::verification::{verify_domains_match, verify_urls_match},
traits::{ActivityHandler, Actor, Object},
};
use lemmy_api_common::context::LemmyContext;
use lemmy_api_common::{build_response::send_local_notifs, context::LemmyContext};
use lemmy_db_schema::{
aggregates::structs::PostAggregates,
newtypes::PersonId,
newtypes::{PersonId, PostOrCommentId},
source::{
activity::ActivitySendTargets,
community::Community,
@ -32,7 +32,10 @@ use lemmy_db_schema::{
},
traits::{Crud, Likeable},
};
use lemmy_utils::error::{LemmyError, LemmyResult};
use lemmy_utils::{
error::{LemmyError, LemmyResult},
utils::mention::scrape_text_for_mentions,
};
use url::Url;
impl CreateOrUpdatePage {
@ -123,6 +126,21 @@ impl ActivityHandler for CreateOrUpdatePage {
// Calculate initial hot_rank for post
PostAggregates::update_ranks(&mut context.pool(), post.id).await?;
let do_send_email = self.kind == CreateOrUpdateType::Create;
let actor = self.actor.dereference(context).await?;
// Send the post body mentions
let mentions = scrape_text_for_mentions(&post.body.clone().unwrap_or_default());
send_local_notifs(
mentions,
PostOrCommentId::Post(post.id),
&actor,
do_send_email,
context,
None,
)
.await?;
Ok(())
}
}

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

@ -857,3 +857,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

@ -28,7 +28,8 @@ pub mod oauth_provider;
pub mod password_reset_request;
pub mod person;
pub mod person_block;
pub mod person_mention;
pub mod person_comment_mention;
pub mod person_post_mention;
pub mod post;
pub mod post_report;
pub mod private_message;

View file

@ -0,0 +1,83 @@
use crate::{
diesel::OptionalExtension,
newtypes::{CommentId, PersonCommentMentionId, PersonId},
schema::person_comment_mention,
source::person_comment_mention::{
PersonCommentMention,
PersonCommentMentionInsertForm,
PersonCommentMentionUpdateForm,
},
traits::Crud,
utils::{get_conn, DbPool},
};
use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
#[async_trait]
impl Crud for PersonCommentMention {
type InsertForm = PersonCommentMentionInsertForm;
type UpdateForm = PersonCommentMentionUpdateForm;
type IdType = PersonCommentMentionId;
async fn create(
pool: &mut DbPool<'_>,
person_comment_mention_form: &Self::InsertForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
// since the return here isnt utilized, we dont need to do an update
// but get_result doesn't return the existing row here
insert_into(person_comment_mention::table)
.values(person_comment_mention_form)
.on_conflict((
person_comment_mention::recipient_id,
person_comment_mention::comment_id,
))
.do_update()
.set(person_comment_mention_form)
.get_result::<Self>(conn)
.await
}
async fn update(
pool: &mut DbPool<'_>,
person_comment_mention_id: PersonCommentMentionId,
person_comment_mention_form: &Self::UpdateForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(person_comment_mention::table.find(person_comment_mention_id))
.set(person_comment_mention_form)
.get_result::<Self>(conn)
.await
}
}
impl PersonCommentMention {
pub async fn mark_all_as_read(
pool: &mut DbPool<'_>,
for_recipient_id: PersonId,
) -> Result<Vec<PersonCommentMention>, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(
person_comment_mention::table
.filter(person_comment_mention::recipient_id.eq(for_recipient_id))
.filter(person_comment_mention::read.eq(false)),
)
.set(person_comment_mention::read.eq(true))
.get_results::<Self>(conn)
.await
}
pub async fn read_by_comment_and_person(
pool: &mut DbPool<'_>,
for_comment_id: CommentId,
for_recipient_id: PersonId,
) -> Result<Option<Self>, Error> {
let conn = &mut get_conn(pool).await?;
person_comment_mention::table
.filter(person_comment_mention::comment_id.eq(for_comment_id))
.filter(person_comment_mention::recipient_id.eq(for_recipient_id))
.first(conn)
.await
.optional()
}
}

View file

@ -1,76 +0,0 @@
use crate::{
diesel::OptionalExtension,
newtypes::{CommentId, PersonId, PersonMentionId},
schema::person_mention,
source::person_mention::{PersonMention, PersonMentionInsertForm, PersonMentionUpdateForm},
traits::Crud,
utils::{get_conn, DbPool},
};
use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
#[async_trait]
impl Crud for PersonMention {
type InsertForm = PersonMentionInsertForm;
type UpdateForm = PersonMentionUpdateForm;
type IdType = PersonMentionId;
async fn create(
pool: &mut DbPool<'_>,
person_mention_form: &Self::InsertForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
// since the return here isnt utilized, we dont need to do an update
// but get_result doesn't return the existing row here
insert_into(person_mention::table)
.values(person_mention_form)
.on_conflict((person_mention::recipient_id, person_mention::comment_id))
.do_update()
.set(person_mention_form)
.get_result::<Self>(conn)
.await
}
async fn update(
pool: &mut DbPool<'_>,
person_mention_id: PersonMentionId,
person_mention_form: &Self::UpdateForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(person_mention::table.find(person_mention_id))
.set(person_mention_form)
.get_result::<Self>(conn)
.await
}
}
impl PersonMention {
pub async fn mark_all_as_read(
pool: &mut DbPool<'_>,
for_recipient_id: PersonId,
) -> Result<Vec<PersonMention>, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(
person_mention::table
.filter(person_mention::recipient_id.eq(for_recipient_id))
.filter(person_mention::read.eq(false)),
)
.set(person_mention::read.eq(true))
.get_results::<Self>(conn)
.await
}
pub async fn read_by_comment_and_person(
pool: &mut DbPool<'_>,
for_comment_id: CommentId,
for_recipient_id: PersonId,
) -> Result<Option<Self>, Error> {
let conn = &mut get_conn(pool).await?;
person_mention::table
.filter(person_mention::comment_id.eq(for_comment_id))
.filter(person_mention::recipient_id.eq(for_recipient_id))
.first(conn)
.await
.optional()
}
}

View file

@ -0,0 +1,83 @@
use crate::{
diesel::OptionalExtension,
newtypes::{PersonId, PersonPostMentionId, PostId},
schema::person_post_mention,
source::person_post_mention::{
PersonPostMention,
PersonPostMentionInsertForm,
PersonPostMentionUpdateForm,
},
traits::Crud,
utils::{get_conn, DbPool},
};
use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
#[async_trait]
impl Crud for PersonPostMention {
type InsertForm = PersonPostMentionInsertForm;
type UpdateForm = PersonPostMentionUpdateForm;
type IdType = PersonPostMentionId;
async fn create(
pool: &mut DbPool<'_>,
person_post_mention_form: &Self::InsertForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
// since the return here isnt utilized, we dont need to do an update
// but get_result doesn't return the existing row here
insert_into(person_post_mention::table)
.values(person_post_mention_form)
.on_conflict((
person_post_mention::recipient_id,
person_post_mention::post_id,
))
.do_update()
.set(person_post_mention_form)
.get_result::<Self>(conn)
.await
}
async fn update(
pool: &mut DbPool<'_>,
person_post_mention_id: PersonPostMentionId,
person_post_mention_form: &Self::UpdateForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(person_post_mention::table.find(person_post_mention_id))
.set(person_post_mention_form)
.get_result::<Self>(conn)
.await
}
}
impl PersonPostMention {
pub async fn mark_all_as_read(
pool: &mut DbPool<'_>,
for_recipient_id: PersonId,
) -> Result<Vec<PersonPostMention>, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(
person_post_mention::table
.filter(person_post_mention::recipient_id.eq(for_recipient_id))
.filter(person_post_mention::read.eq(false)),
)
.set(person_post_mention::read.eq(true))
.get_results::<Self>(conn)
.await
}
pub async fn read_by_post_and_person(
pool: &mut DbPool<'_>,
for_post_id: PostId,
for_recipient_id: PersonId,
) -> Result<Option<Self>, Error> {
let conn = &mut get_conn(pool).await?;
person_post_mention::table
.filter(person_post_mention::post_id.eq(for_post_id))
.filter(person_post_mention::recipient_id.eq(for_recipient_id))
.first(conn)
.await
.optional()
}
}

View file

@ -214,6 +214,18 @@ pub enum ModlogActionType {
AdminAllowInstance,
}
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// A list of possible types for the inbox.
pub enum InboxDataType {
All,
CommentReply,
CommentMention,
PostMention,
PrivateMessage,
}
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]

View file

@ -55,6 +55,11 @@ impl fmt::Display for CommentId {
}
}
pub enum PostOrCommentId {
Post(PostId),
Comment(CommentId),
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "full", derive(DieselNewType, TS))]
#[cfg_attr(feature = "full", ts(export))]
@ -71,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 {
@ -82,8 +87,14 @@ impl fmt::Display for PrivateMessageId {
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "full", derive(DieselNewType, TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The person mention id.
pub struct PersonMentionId(i32);
/// The person comment mention id.
pub struct PersonCommentMentionId(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(pub i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "full", derive(DieselNewType, TS))]
@ -125,7 +136,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,
@ -204,6 +215,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

@ -351,6 +351,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,
@ -773,6 +784,16 @@ diesel::table! {
}
}
diesel::table! {
person_comment_mention (id) {
id -> Int4,
recipient_id -> Int4,
comment_id -> Int4,
read -> Bool,
published -> Timestamptz,
}
}
diesel::table! {
person_content_combined (id) {
id -> Int4,
@ -783,10 +804,10 @@ diesel::table! {
}
diesel::table! {
person_mention (id) {
person_post_mention (id) {
id -> Int4,
recipient_id -> Int4,
comment_id -> Int4,
post_id -> Int4,
read -> Bool,
published -> Timestamptz,
}
@ -1091,6 +1112,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));
@ -1139,10 +1164,12 @@ diesel::joinable!(password_reset_request -> local_user (local_user_id));
diesel::joinable!(person -> instance (instance_id));
diesel::joinable!(person_aggregates -> person (person_id));
diesel::joinable!(person_ban -> person (person_id));
diesel::joinable!(person_comment_mention -> comment (comment_id));
diesel::joinable!(person_comment_mention -> person (recipient_id));
diesel::joinable!(person_content_combined -> comment (comment_id));
diesel::joinable!(person_content_combined -> post (post_id));
diesel::joinable!(person_mention -> comment (comment_id));
diesel::joinable!(person_mention -> person (recipient_id));
diesel::joinable!(person_post_mention -> person (recipient_id));
diesel::joinable!(person_post_mention -> post (post_id));
diesel::joinable!(person_saved_combined -> comment (comment_id));
diesel::joinable!(person_saved_combined -> person (person_id));
diesel::joinable!(person_saved_combined -> post (post_id));
@ -1196,6 +1223,7 @@ diesel::allow_tables_to_appear_in_same_query!(
federation_blocklist,
federation_queue_state,
image_details,
inbox_combined,
instance,
instance_actions,
language,
@ -1226,8 +1254,9 @@ diesel::allow_tables_to_appear_in_same_query!(
person_actions,
person_aggregates,
person_ban,
person_comment_mention,
person_content_combined,
person_mention,
person_post_mention,
person_saved_combined,
post,
post_actions,

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

@ -34,7 +34,8 @@ pub mod oauth_provider;
pub mod password_reset_request;
pub mod person;
pub mod person_block;
pub mod person_mention;
pub mod person_comment_mention;
pub mod person_post_mention;
pub mod post;
pub mod post_report;
pub mod private_message;

View file

@ -1,6 +1,6 @@
use crate::newtypes::{CommentId, PersonId, PersonMentionId};
use crate::newtypes::{CommentId, PersonCommentMentionId, PersonId};
#[cfg(feature = "full")]
use crate::schema::person_mention;
use crate::schema::person_comment_mention;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[cfg(feature = "full")]
@ -12,12 +12,12 @@ use ts_rs::TS;
derive(Queryable, Selectable, Associations, Identifiable, TS)
)]
#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::comment::Comment)))]
#[cfg_attr(feature = "full", diesel(table_name = person_mention))]
#[cfg_attr(feature = "full", diesel(table_name = person_comment_mention))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))]
/// A person mention.
pub struct PersonMention {
pub id: PersonMentionId,
pub struct PersonCommentMention {
pub id: PersonCommentMentionId,
pub recipient_id: PersonId,
pub comment_id: CommentId,
pub read: bool,
@ -25,15 +25,15 @@ pub struct PersonMention {
}
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = person_mention))]
pub struct PersonMentionInsertForm {
#[cfg_attr(feature = "full", diesel(table_name = person_comment_mention))]
pub struct PersonCommentMentionInsertForm {
pub recipient_id: PersonId,
pub comment_id: CommentId,
pub read: Option<bool>,
}
#[cfg_attr(feature = "full", derive(AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = person_mention))]
pub struct PersonMentionUpdateForm {
#[cfg_attr(feature = "full", diesel(table_name = person_comment_mention))]
pub struct PersonCommentMentionUpdateForm {
pub read: Option<bool>,
}

View file

@ -0,0 +1,39 @@
use crate::newtypes::{PersonId, PersonPostMentionId, PostId};
#[cfg(feature = "full")]
use crate::schema::person_post_mention;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[cfg(feature = "full")]
use ts_rs::TS;
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[cfg_attr(
feature = "full",
derive(Queryable, Selectable, Associations, Identifiable, TS)
)]
#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))]
#[cfg_attr(feature = "full", diesel(table_name = person_post_mention))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))]
/// A person mention.
pub struct PersonPostMention {
pub id: PersonPostMentionId,
pub recipient_id: PersonId,
pub post_id: PostId,
pub read: bool,
pub published: DateTime<Utc>,
}
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = person_post_mention))]
pub struct PersonPostMentionInsertForm {
pub recipient_id: PersonId,
pub post_id: PostId,
pub read: Option<bool>,
}
#[cfg_attr(feature = "full", derive(AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = person_post_mention))]
pub struct PersonPostMentionUpdateForm {
pub read: Option<bool>,
}

View file

@ -24,8 +24,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

@ -198,17 +198,6 @@ pub struct PostView {
pub tags: PostTags,
}
#[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,7 @@ workspace = true
full = [
"lemmy_db_schema/full",
"lemmy_utils/full",
"i-love-jesus",
"diesel",
"diesel-async",
"ts-rs",
@ -40,10 +41,10 @@ ts-rs = { workspace = true, optional = true }
chrono.workspace = true
strum = { workspace = true }
lemmy_utils = { workspace = true, optional = true }
i-love-jesus = { workspace = true, optional = true }
[dev-dependencies]
serial_test = { workspace = true }
tokio = { workspace = true }
pretty_assertions = { workspace = true }
url.workspace = true
lemmy_db_views = { workspace = true, features = ["full"] }

View file

@ -1,379 +0,0 @@
use crate::structs::CommentReplyView;
use diesel::{
dsl::{exists, not},
pg::Pg,
result::Error,
BoolExpressionMethods,
ExpressionMethods,
JoinOnDsl,
NullableExpressionMethods,
QueryDsl,
};
use diesel_async::RunQueryDsl;
use lemmy_db_schema::{
aliases::{self, creator_community_actions},
newtypes::{CommentReplyId, PersonId},
schema::{
comment,
comment_actions,
comment_aggregates,
comment_reply,
community,
community_actions,
local_user,
person,
person_actions,
post,
},
source::{community::CommunityFollower, local_user::LocalUser},
utils::{
actions,
actions_alias,
get_conn,
limit_and_offset,
DbConn,
DbPool,
ListFn,
Queries,
ReadFn,
},
CommentSortType,
};
fn queries<'a>() -> Queries<
impl ReadFn<'a, CommentReplyView, (CommentReplyId, Option<PersonId>)>,
impl ListFn<'a, CommentReplyView, CommentReplyQuery>,
> {
let creator_is_admin = exists(
local_user::table.filter(
comment::creator_id
.eq(local_user::person_id)
.and(local_user::admin.eq(true)),
),
);
let all_joins = move |query: comment_reply::BoxedQuery<'a, Pg>,
my_person_id: Option<PersonId>| {
query
.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)))
.inner_join(community::table.on(post::community_id.eq(community::id)))
.inner_join(aliases::person1)
.inner_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(actions(
community_actions::table,
my_person_id,
post::community_id,
))
.left_join(actions(
person_actions::table,
my_person_id,
comment::creator_id,
))
.left_join(actions_alias(
creator_community_actions,
comment::creator_id,
post::community_id,
))
.select((
comment_reply::all_columns,
comment::all_columns,
person::all_columns,
post::all_columns,
community::all_columns,
aliases::person1.fields(person::all_columns),
comment_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(),
comment_actions::saved.nullable().is_not_null(),
person_actions::blocked.nullable().is_not_null(),
comment_actions::like_score.nullable(),
))
};
let read =
move |mut conn: DbConn<'a>,
(comment_reply_id, my_person_id): (CommentReplyId, Option<PersonId>)| async move {
all_joins(
comment_reply::table.find(comment_reply_id).into_boxed(),
my_person_id,
)
.first(&mut conn)
.await
};
let list = move |mut conn: DbConn<'a>, o: CommentReplyQuery| async move {
// These filters need to be kept in sync with the filters in
// CommentReplyView::get_unread_replies()
let mut query = all_joins(comment_reply::table.into_boxed(), o.my_person_id);
if let Some(recipient_id) = o.recipient_id {
query = query.filter(comment_reply::recipient_id.eq(recipient_id));
}
if o.unread_only {
query = query.filter(comment_reply::read.eq(false));
}
if !o.show_bot_accounts {
query = query.filter(not(person::bot_account));
};
query = match o.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_reply::published.desc()),
CommentSortType::Old => query.then_order_by(comment_reply::published.asc()),
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(o.page, o.limit)?;
query
.limit(limit)
.offset(offset)
.load::<CommentReplyView>(&mut conn)
.await
};
Queries::new(read, list)
}
impl CommentReplyView {
pub async fn read(
pool: &mut DbPool<'_>,
comment_reply_id: CommentReplyId,
my_person_id: Option<PersonId>,
) -> Result<Self, Error> {
queries().read(pool, (comment_reply_id, my_person_id)).await
}
/// Gets the number of unread replies
pub async fn get_unread_replies(
pool: &mut DbPool<'_>,
local_user: &LocalUser,
) -> Result<i64, Error> {
use diesel::dsl::count;
let conn = &mut get_conn(pool).await?;
let mut query = comment_reply::table
.inner_join(comment::table)
.left_join(actions(
person_actions::table,
Some(local_user.person_id),
comment::creator_id,
))
.inner_join(person::table.on(comment::creator_id.eq(person::id)))
.into_boxed();
// These filters need to be kept in sync with the filters in queries().list()
if !local_user.show_bot_accounts {
query = query.filter(not(person::bot_account));
}
query
// Don't count replies from blocked users
.filter(person_actions::blocked.is_null())
.filter(comment_reply::recipient_id.eq(local_user.person_id))
.filter(comment_reply::read.eq(false))
.filter(comment::deleted.eq(false))
.filter(comment::removed.eq(false))
.select(count(comment_reply::id))
.first::<i64>(conn)
.await
}
}
#[derive(Default, Clone)]
pub struct CommentReplyQuery {
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 CommentReplyQuery {
pub async fn list(self, pool: &mut DbPool<'_>) -> Result<Vec<CommentReplyView>, Error> {
queries().list(pool, self).await
}
}
#[cfg(test)]
mod tests {
use crate::{comment_reply_view::CommentReplyQuery, structs::CommentReplyView};
use lemmy_db_schema::{
source::{
comment::{Comment, CommentInsertForm},
comment_reply::{CommentReply, CommentReplyInsertForm, CommentReplyUpdateForm},
community::{Community, CommunityInsertForm},
instance::Instance,
local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm},
person::{Person, PersonInsertForm, PersonUpdateForm},
person_block::{PersonBlock, PersonBlockForm},
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 terry_form = PersonInsertForm::test_form(inserted_instance.id, "terrylake");
let inserted_terry = Person::create(pool, &terry_form).await?;
let recipient_form = PersonInsertForm {
local: Some(true),
..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_terry.id,
inserted_community.id,
);
let inserted_post = Post::create(pool, &new_post).await?;
let comment_form =
CommentInsertForm::new(inserted_terry.id, inserted_post.id, "A test comment".into());
let inserted_comment = Comment::create(pool, &comment_form, None).await?;
let comment_reply_form = CommentReplyInsertForm {
recipient_id: inserted_recipient.id,
comment_id: inserted_comment.id,
read: None,
};
let inserted_reply = CommentReply::create(pool, &comment_reply_form).await?;
let expected_reply = CommentReply {
id: inserted_reply.id,
recipient_id: inserted_reply.recipient_id,
comment_id: inserted_reply.comment_id,
read: false,
published: inserted_reply.published,
};
let read_reply = CommentReply::read(pool, inserted_reply.id).await?;
let comment_reply_update_form = CommentReplyUpdateForm { read: Some(false) };
let updated_reply =
CommentReply::update(pool, inserted_reply.id, &comment_reply_update_form).await?;
// Test to make sure counts and blocks work correctly
let unread_replies = CommentReplyView::get_unread_replies(pool, &recipient_local_user).await?;
let query = CommentReplyQuery {
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 replies = query.clone().list(pool).await?;
assert_eq!(1, unread_replies);
assert_eq!(1, replies.len());
// Block the person, and make sure these counts are now empty
let block_form = PersonBlockForm {
person_id: recipient_id,
target_id: inserted_terry.id,
};
PersonBlock::block(pool, &block_form).await?;
let unread_replies_after_block =
CommentReplyView::get_unread_replies(pool, &recipient_local_user).await?;
let replies_after_block = query.clone().list(pool).await?;
assert_eq!(0, unread_replies_after_block);
assert_eq!(0, replies_after_block.len());
// 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_terry.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_replies_after_hide_bots =
CommentReplyView::get_unread_replies(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_replies_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_terry.id).await?;
Person::delete(pool, inserted_recipient.id).await?;
Instance::delete(pool, inserted_instance.id).await?;
assert_eq!(expected_reply, read_reply);
assert_eq!(expected_reply, inserted_reply);
assert_eq!(expected_reply, updated_reply);
Ok(())
}
}

View file

@ -0,0 +1,991 @@
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},
InboxDataType,
InternalToCombinedView,
};
use lemmy_utils::error::LemmyResult;
impl InboxCombinedViewInternal {
/// Gets the number of unread mentions
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))
.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))
// Filter out the deleted / removed
.and(not(comment::deleted))
.and(not(comment::removed));
let post_join = person_post_mention::post_id
.eq(post::id)
.or(comment::post_id.eq(post::id))
// Filter out the deleted / removed
.and(not(post::deleted))
.and(not(post::removed));
// This could be a simple join, but you need to check for deleted here
let private_message_join = inbox_combined::private_message_id
.eq(private_message::id.nullable())
.and(not(private_message::deleted));
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.on(private_message_join))
.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())
.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(Default)]
pub struct InboxCombinedQuery {
pub type_: Option<InboxDataType>,
pub unread_only: Option<bool>,
pub show_bot_accounts: Option<bool>,
pub page_after: Option<PaginationCursorData>,
pub page_back: Option<bool>,
}
impl InboxCombinedQuery {
pub async fn list(
self,
pool: &mut DbPool<'_>,
my_person_id: PersonId,
) -> LemmyResult<Vec<InboxCombinedView>> {
let conn = &mut get_conn(pool).await?;
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))
.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))
// Filter out the deleted / removed
.and(not(comment::deleted))
.and(not(comment::removed));
let post_join = person_post_mention::post_id
.eq(post::id)
.or(comment::post_id.eq(post::id))
// Filter out the deleted / removed
.and(not(post::deleted))
.and(not(post::removed));
// This could be a simple join, but you need to check for deleted here
let private_message_join = inbox_combined::private_message_id
.eq(private_message::id.nullable())
.and(not(private_message::deleted));
let community_join = post::community_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.on(private_message_join))
.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,
Some(my_person_id),
post::community_id,
))
.left_join(actions(
instance_actions::table,
Some(my_person_id),
person::instance_id,
))
.left_join(actions(post_actions::table, Some(my_person_id), post::id))
.left_join(actions(
person_actions::table,
Some(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,
Some(my_person_id),
comment::id,
))
.left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable())))
.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
// The recipient filter (IE only show replies to you)
.filter(recipient_person.eq(my_person_id))
.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)),
);
} else {
// A special case for private messages: show messages FROM you also.
// Use a not-null checks to catch the others
query = query.filter(
inbox_combined::comment_reply_id
.is_not_null()
.and(recipient_person.eq(my_person_id))
.or(
inbox_combined::person_comment_mention_id
.is_not_null()
.and(recipient_person.eq(my_person_id)),
)
.or(
inbox_combined::person_post_mention_id
.is_not_null()
.and(recipient_person.eq(my_person_id)),
)
.or(
inbox_combined::private_message_id.is_not_null().and(
recipient_person
.eq(my_person_id)
.or(item_creator.eq(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());
if let Some(type_) = self.type_ {
query = match type_ {
InboxDataType::All => query,
InboxDataType::CommentReply => query.filter(inbox_combined::comment_reply_id.is_not_null()),
InboxDataType::CommentMention => {
query.filter(inbox_combined::person_comment_mention_id.is_not_null())
}
InboxDataType::PostMention => {
query.filter(inbox_combined::person_post_mention_id.is_not_null())
}
InboxDataType::PrivateMessage => {
query.filter(inbox_combined::private_message_id.is_not_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(InternalToCombinedView::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;
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
}
}
}
#[cfg(test)]
#[expect(clippy::indexing_slicing)]
mod tests {
use crate::{
inbox_combined_view::InboxCombinedQuery,
structs::{InboxCombinedView, InboxCombinedViewInternal, PrivateMessageView},
};
use lemmy_db_schema::{
assert_length,
source::{
comment::{Comment, CommentInsertForm},
comment_reply::{CommentReply, CommentReplyInsertForm, CommentReplyUpdateForm},
community::{Community, CommunityInsertForm},
instance::Instance,
instance_block::{InstanceBlock, InstanceBlockForm},
person::{Person, PersonInsertForm, PersonUpdateForm},
person_block::{PersonBlock, PersonBlockForm},
person_comment_mention::{PersonCommentMention, PersonCommentMentionInsertForm},
person_post_mention::{PersonPostMention, PersonPostMentionInsertForm},
post::{Post, PostInsertForm},
private_message::{PrivateMessage, PrivateMessageInsertForm},
},
traits::{Blockable, Crud},
utils::{build_db_pool_for_tests, DbPool},
InboxDataType,
};
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
struct Data {
instance: Instance,
timmy: Person,
sara: Person,
jessica: Person,
timmy_post: Post,
jessica_post: Post,
timmy_comment: Comment,
sara_comment: 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 jessica_form = PersonInsertForm::test_form(instance.id, "jessica_mrv");
let jessica = Person::create(pool, &jessica_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 jessica_post_form =
PostInsertForm::new("jessica post prv".into(), jessica.id, community.id);
let jessica_post = Post::create(pool, &jessica_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, Some(&timmy_comment.path)).await?;
Ok(Data {
instance,
timmy,
sara,
jessica,
timmy_post,
jessica_post,
timmy_comment,
sara_comment,
})
}
async fn setup_private_messages(data: &Data, pool: &mut DbPool<'_>) -> LemmyResult<()> {
let sara_timmy_message_form =
PrivateMessageInsertForm::new(data.sara.id, data.timmy.id, "sara to timmy".into());
PrivateMessage::create(pool, &sara_timmy_message_form).await?;
let sara_jessica_message_form =
PrivateMessageInsertForm::new(data.sara.id, data.jessica.id, "sara to jessica".into());
PrivateMessage::create(pool, &sara_jessica_message_form).await?;
let timmy_sara_message_form =
PrivateMessageInsertForm::new(data.timmy.id, data.sara.id, "timmy to sara".into());
PrivateMessage::create(pool, &timmy_sara_message_form).await?;
let jessica_timmy_message_form =
PrivateMessageInsertForm::new(data.jessica.id, data.timmy.id, "jessica to timmy".into());
PrivateMessage::create(pool, &jessica_timmy_message_form).await?;
Ok(())
}
async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> {
Instance::delete(pool, data.instance.id).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn replies() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let data = init_data(pool).await?;
// Sara replied to timmys comment, but lets create the row now
let form = CommentReplyInsertForm {
recipient_id: data.timmy.id,
comment_id: data.sara_comment.id,
read: None,
};
let reply = CommentReply::create(pool, &form).await?;
let timmy_unread_replies =
InboxCombinedViewInternal::get_unread_count(pool, data.timmy.id, true).await?;
assert_eq!(1, timmy_unread_replies);
let timmy_inbox = InboxCombinedQuery::default()
.list(pool, data.timmy.id)
.await?;
assert_length!(1, timmy_inbox);
if let InboxCombinedView::CommentReply(v) = &timmy_inbox[0] {
assert_eq!(data.sara_comment.id, v.comment_reply.comment_id);
assert_eq!(data.sara_comment.id, v.comment.id);
assert_eq!(data.timmy_post.id, v.post.id);
assert_eq!(data.sara.id, v.creator.id);
assert_eq!(data.timmy.id, v.recipient.id);
} else {
panic!("wrong type");
}
// Mark it as read
let form = CommentReplyUpdateForm { read: Some(true) };
CommentReply::update(pool, reply.id, &form).await?;
let timmy_unread_replies =
InboxCombinedViewInternal::get_unread_count(pool, data.timmy.id, true).await?;
assert_eq!(0, timmy_unread_replies);
let timmy_inbox_unread = InboxCombinedQuery {
unread_only: Some(true),
..Default::default()
}
.list(pool, data.timmy.id)
.await?;
assert_length!(0, timmy_inbox_unread);
cleanup(data, pool).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn mentions() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let data = init_data(pool).await?;
// Timmy mentions sara in a comment
let timmy_mention_sara_comment_form = PersonCommentMentionInsertForm {
recipient_id: data.sara.id,
comment_id: data.timmy_comment.id,
read: None,
};
PersonCommentMention::create(pool, &timmy_mention_sara_comment_form).await?;
// Jessica mentions sara in a post
let jessica_mention_sara_post_form = PersonPostMentionInsertForm {
recipient_id: data.sara.id,
post_id: data.jessica_post.id,
read: None,
};
PersonPostMention::create(pool, &jessica_mention_sara_post_form).await?;
// Test to make sure counts and blocks work correctly
let sara_unread_mentions =
InboxCombinedViewInternal::get_unread_count(pool, data.sara.id, true).await?;
assert_eq!(2, sara_unread_mentions);
let sara_inbox = InboxCombinedQuery::default()
.list(pool, data.sara.id)
.await?;
assert_length!(2, sara_inbox);
if let InboxCombinedView::PostMention(v) = &sara_inbox[0] {
assert_eq!(data.jessica_post.id, v.person_post_mention.post_id);
assert_eq!(data.jessica_post.id, v.post.id);
assert_eq!(data.jessica.id, v.creator.id);
assert_eq!(data.sara.id, v.recipient.id);
} else {
panic!("wrong type");
}
if let InboxCombinedView::CommentMention(v) = &sara_inbox[1] {
assert_eq!(data.timmy_comment.id, v.person_comment_mention.comment_id);
assert_eq!(data.timmy_comment.id, v.comment.id);
assert_eq!(data.timmy_post.id, v.post.id);
assert_eq!(data.timmy.id, v.creator.id);
assert_eq!(data.sara.id, v.recipient.id);
} else {
panic!("wrong type");
}
// Sara blocks timmy, and make sure these counts are now empty
let sara_blocks_timmy_form = PersonBlockForm {
person_id: data.sara.id,
target_id: data.timmy.id,
};
PersonBlock::block(pool, &sara_blocks_timmy_form).await?;
let sara_unread_mentions_after_block =
InboxCombinedViewInternal::get_unread_count(pool, data.sara.id, true).await?;
assert_eq!(1, sara_unread_mentions_after_block);
let sara_inbox_after_block = InboxCombinedQuery::default()
.list(pool, data.sara.id)
.await?;
assert_length!(1, sara_inbox_after_block);
// Make sure the comment mention which timmy made is the hidden one
assert!(matches!(
sara_inbox_after_block[0],
InboxCombinedView::PostMention(_)
));
// Unblock user so we can reuse the same person
PersonBlock::unblock(pool, &sara_blocks_timmy_form).await?;
// Test the type filter
let sara_inbox_post_mentions_only = InboxCombinedQuery {
type_: Some(InboxDataType::PostMention),
..Default::default()
}
.list(pool, data.sara.id)
.await?;
assert_length!(1, sara_inbox_post_mentions_only);
assert!(matches!(
sara_inbox_post_mentions_only[0],
InboxCombinedView::PostMention(_)
));
// Turn Jessica into a bot account
let person_update_form = PersonUpdateForm {
bot_account: Some(true),
..Default::default()
};
Person::update(pool, data.jessica.id, &person_update_form).await?;
// Make sure sara hides bots
let sara_unread_mentions_after_hide_bots =
InboxCombinedViewInternal::get_unread_count(pool, data.sara.id, false).await?;
assert_eq!(1, sara_unread_mentions_after_hide_bots);
let sara_inbox_after_hide_bots = InboxCombinedQuery::default()
.list(pool, data.sara.id)
.await?;
assert_length!(1, sara_inbox_after_hide_bots);
// Make sure the post mention which jessica made is the hidden one
assert!(matches!(
sara_inbox_after_hide_bots[0],
InboxCombinedView::CommentMention(_)
));
// Mark them all as read
PersonPostMention::mark_all_as_read(pool, data.sara.id).await?;
PersonCommentMention::mark_all_as_read(pool, data.sara.id).await?;
// Make sure none come back
let sara_unread_mentions =
InboxCombinedViewInternal::get_unread_count(pool, data.sara.id, false).await?;
assert_eq!(0, sara_unread_mentions);
let sara_inbox_unread = InboxCombinedQuery {
unread_only: Some(true),
..Default::default()
}
.list(pool, data.sara.id)
.await?;
assert_length!(0, sara_inbox_unread);
cleanup(data, pool).await?;
Ok(())
}
/// A helper function to coerce to a private message type for tests
fn map_to_pm(inbox: &[InboxCombinedView]) -> Vec<PrivateMessageView> {
inbox
.iter()
// Filter map to collect private messages
.filter_map(|f| {
if let InboxCombinedView::PrivateMessage(v) = f {
Some(v)
} else {
None
}
})
.cloned()
.collect::<Vec<PrivateMessageView>>()
}
#[tokio::test]
#[serial]
async fn read_private_messages() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let data = init_data(pool).await?;
setup_private_messages(&data, pool).await?;
let timmy_messages = map_to_pm(
&InboxCombinedQuery::default()
.list(pool, data.timmy.id)
.await?,
);
// The read even shows timmy's sent messages
assert_length!(3, &timmy_messages);
assert_eq!(timmy_messages[0].creator.id, data.jessica.id);
assert_eq!(timmy_messages[0].recipient.id, data.timmy.id);
assert_eq!(timmy_messages[1].creator.id, data.timmy.id);
assert_eq!(timmy_messages[1].recipient.id, data.sara.id);
assert_eq!(timmy_messages[2].creator.id, data.sara.id);
assert_eq!(timmy_messages[2].recipient.id, data.timmy.id);
let timmy_unread =
InboxCombinedViewInternal::get_unread_count(pool, data.timmy.id, false).await?;
assert_eq!(2, timmy_unread);
let timmy_unread_messages = map_to_pm(
&InboxCombinedQuery {
unread_only: Some(true),
..Default::default()
}
.list(pool, data.timmy.id)
.await?,
);
// The unread hides timmy's sent messages
assert_length!(2, &timmy_unread_messages);
assert_eq!(timmy_unread_messages[0].creator.id, data.jessica.id);
assert_eq!(timmy_unread_messages[0].recipient.id, data.timmy.id);
assert_eq!(timmy_unread_messages[1].creator.id, data.sara.id);
assert_eq!(timmy_unread_messages[1].recipient.id, data.timmy.id);
cleanup(data, pool).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn ensure_private_message_person_block() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let data = init_data(pool).await?;
setup_private_messages(&data, pool).await?;
// Make sure blocks are working
let timmy_blocks_sara_form = PersonBlockForm {
person_id: data.timmy.id,
target_id: data.sara.id,
};
let inserted_block = PersonBlock::block(pool, &timmy_blocks_sara_form).await?;
let expected_block = PersonBlock {
person_id: data.timmy.id,
target_id: data.sara.id,
published: inserted_block.published,
};
assert_eq!(expected_block, inserted_block);
let timmy_messages = map_to_pm(
&InboxCombinedQuery {
unread_only: Some(true),
..Default::default()
}
.list(pool, data.timmy.id)
.await?,
);
assert_length!(1, &timmy_messages);
let timmy_unread =
InboxCombinedViewInternal::get_unread_count(pool, data.timmy.id, false).await?;
assert_eq!(1, timmy_unread);
cleanup(data, pool).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn ensure_private_message_instance_block() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let data = init_data(pool).await?;
setup_private_messages(&data, pool).await?;
// Make sure instance_blocks are working
let timmy_blocks_instance_form = InstanceBlockForm {
person_id: data.timmy.id,
instance_id: data.sara.instance_id,
};
let inserted_instance_block = InstanceBlock::block(pool, &timmy_blocks_instance_form).await?;
let expected_instance_block = InstanceBlock {
person_id: data.timmy.id,
instance_id: data.sara.instance_id,
published: inserted_instance_block.published,
};
assert_eq!(expected_instance_block, inserted_instance_block);
let timmy_messages = map_to_pm(
&InboxCombinedQuery {
unread_only: Some(true),
..Default::default()
}
.list(pool, data.timmy.id)
.await?,
);
assert_length!(0, &timmy_messages);
let timmy_unread =
InboxCombinedViewInternal::get_unread_count(pool, data.timmy.id, false).await?;
assert_eq!(0, timmy_unread);
cleanup(data, pool).await?;
Ok(())
}
}

View file

@ -1,6 +1,4 @@
#[cfg(feature = "full")]
pub mod comment_reply_view;
#[cfg(feature = "full")]
pub mod community_follower_view;
#[cfg(feature = "full")]
pub mod community_moderator_view;
@ -9,7 +7,9 @@ pub mod community_person_ban_view;
#[cfg(feature = "full")]
pub mod community_view;
#[cfg(feature = "full")]
pub mod person_mention_view;
pub mod inbox_combined_view;
#[cfg(feature = "full")]
pub mod person_view;
#[cfg(feature = "full")]
pub mod private_message_view;
pub mod structs;

View file

@ -1,383 +0,0 @@
use crate::structs::PersonMentionView;
use diesel::{
dsl::{exists, not},
pg::Pg,
result::Error,
BoolExpressionMethods,
ExpressionMethods,
JoinOnDsl,
NullableExpressionMethods,
QueryDsl,
};
use diesel_async::RunQueryDsl;
use lemmy_db_schema::{
aliases::{self, creator_community_actions},
newtypes::{PersonId, PersonMentionId},
schema::{
comment,
comment_actions,
comment_aggregates,
community,
community_actions,
local_user,
person,
person_actions,
person_mention,
post,
},
source::{community::CommunityFollower, local_user::LocalUser},
utils::{
actions,
actions_alias,
get_conn,
limit_and_offset,
DbConn,
DbPool,
ListFn,
Queries,
ReadFn,
},
CommentSortType,
};
fn queries<'a>() -> Queries<
impl ReadFn<'a, PersonMentionView, (PersonMentionId, Option<PersonId>)>,
impl ListFn<'a, PersonMentionView, PersonMentionQuery>,
> {
let creator_is_admin = exists(
local_user::table.filter(
comment::creator_id
.eq(local_user::person_id)
.and(local_user::admin.eq(true)),
),
);
let all_joins = move |query: person_mention::BoxedQuery<'a, Pg>,
my_person_id: Option<PersonId>| {
query
.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)))
.inner_join(community::table.on(post::community_id.eq(community::id)))
.inner_join(aliases::person1)
.inner_join(comment_aggregates::table.on(comment::id.eq(comment_aggregates::comment_id)))
.left_join(actions(
community_actions::table,
my_person_id,
post::community_id,
))
.left_join(actions(comment_actions::table, my_person_id, comment::id))
.left_join(actions(
person_actions::table,
my_person_id,
comment::creator_id,
))
.left_join(actions_alias(
creator_community_actions,
comment::creator_id,
post::community_id,
))
.select((
person_mention::all_columns,
comment::all_columns,
person::all_columns,
post::all_columns,
community::all_columns,
aliases::person1.fields(person::all_columns),
comment_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(),
comment_actions::saved.nullable().is_not_null(),
person_actions::blocked.nullable().is_not_null(),
comment_actions::like_score.nullable(),
))
};
let read =
move |mut conn: DbConn<'a>,
(person_mention_id, my_person_id): (PersonMentionId, Option<PersonId>)| async move {
all_joins(
person_mention::table.find(person_mention_id).into_boxed(),
my_person_id,
)
.first(&mut conn)
.await
};
let list = move |mut conn: DbConn<'a>, o: PersonMentionQuery| async move {
// These filters need to be kept in sync with the filters in
// PersonMentionView::get_unread_mentions()
let mut query = all_joins(person_mention::table.into_boxed(), o.my_person_id);
if let Some(recipient_id) = o.recipient_id {
query = query.filter(person_mention::recipient_id.eq(recipient_id));
}
if o.unread_only {
query = query.filter(person_mention::read.eq(false));
}
if !o.show_bot_accounts {
query = query.filter(not(person::bot_account));
};
query = match o.sort.unwrap_or(CommentSortType::Hot) {
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(o.page, o.limit)?;
query
.limit(limit)
.offset(offset)
.load::<PersonMentionView>(&mut conn)
.await
};
Queries::new(read, list)
}
impl PersonMentionView {
pub async fn read(
pool: &mut DbPool<'_>,
person_mention_id: PersonMentionId,
my_person_id: Option<PersonId>,
) -> Result<Self, Error> {
queries()
.read(pool, (person_mention_id, my_person_id))
.await
}
/// Gets the number of unread mentions
pub async fn get_unread_mentions(
pool: &mut DbPool<'_>,
local_user: &LocalUser,
) -> Result<i64, Error> {
use diesel::dsl::count;
let conn = &mut get_conn(pool).await?;
let mut query = person_mention::table
.inner_join(comment::table)
.left_join(actions(
person_actions::table,
Some(local_user.person_id),
comment::creator_id,
))
.inner_join(person::table.on(comment::creator_id.eq(person::id)))
.into_boxed();
// These filters need to be kept in sync with the filters in queries().list()
if !local_user.show_bot_accounts {
query = query.filter(not(person::bot_account));
}
query
// Don't count replies from blocked users
.filter(person_actions::blocked.is_null())
.filter(person_mention::recipient_id.eq(local_user.person_id))
.filter(person_mention::read.eq(false))
.filter(comment::deleted.eq(false))
.filter(comment::removed.eq(false))
.select(count(person_mention::id))
.first::<i64>(conn)
.await
}
}
#[derive(Default, Clone)]
pub struct PersonMentionQuery {
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 PersonMentionQuery {
pub async fn list(self, pool: &mut DbPool<'_>) -> Result<Vec<PersonMentionView>, Error> {
queries().list(pool, self).await
}
}
#[cfg(test)]
mod tests {
use crate::{person_mention_view::PersonMentionQuery, structs::PersonMentionView};
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_mention::{PersonMention, PersonMentionInsertForm, PersonMentionUpdateForm},
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_mention_form = PersonMentionInsertForm {
recipient_id: inserted_recipient.id,
comment_id: inserted_comment.id,
read: None,
};
let inserted_mention = PersonMention::create(pool, &person_mention_form).await?;
let expected_mention = PersonMention {
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 = PersonMention::read(pool, inserted_mention.id).await?;
let person_mention_update_form = PersonMentionUpdateForm { read: Some(false) };
let updated_mention =
PersonMention::update(pool, inserted_mention.id, &person_mention_update_form).await?;
// Test to make sure counts and blocks work correctly
let unread_mentions =
PersonMentionView::get_unread_mentions(pool, &recipient_local_user).await?;
let query = PersonMentionQuery {
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 =
PersonMentionView::get_unread_mentions(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 =
PersonMentionView::get_unread_mentions(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,42 @@
use crate::structs::PrivateMessageView;
use diesel::{result::Error, ExpressionMethods, JoinOnDsl, QueryDsl};
use diesel_async::RunQueryDsl;
use lemmy_db_schema::{
aliases,
newtypes::PrivateMessageId,
schema::{instance_actions, person, person_actions, private_message},
utils::{actions, get_conn, DbPool},
};
impl PrivateMessageView {
pub async fn read(
pool: &mut DbPool<'_>,
private_message_id: PrivateMessageId,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
private_message::table
.find(private_message_id)
.inner_join(person::table.on(private_message::creator_id.eq(person::id)))
.inner_join(
aliases::person1.on(private_message::recipient_id.eq(aliases::person1.field(person::id))),
)
.left_join(actions(
person_actions::table,
Some(aliases::person1.field(person::id)),
private_message::creator_id,
))
.left_join(actions(
instance_actions::table,
Some(aliases::person1.field(person::id)),
person::instance_id,
))
.select((
private_message::all_columns,
person::all_columns,
aliases::person1.fields(person::all_columns),
))
.first(conn)
.await
}
}

View file

@ -1,14 +1,17 @@
#[cfg(feature = "full")]
use diesel::Queryable;
use lemmy_db_schema::{
aggregates::structs::{CommentAggregates, CommunityAggregates, PersonAggregates},
aggregates::structs::{CommentAggregates, CommunityAggregates, PersonAggregates, PostAggregates},
source::{
comment::Comment,
comment_reply::CommentReply,
community::Community,
images::ImageDetails,
person::Person,
person_mention::PersonMention,
person_comment_mention::PersonCommentMention,
person_post_mention::PersonPostMention,
post::Post,
private_message::PrivateMessage,
},
SubscribedType,
};
@ -93,9 +96,9 @@ pub enum CommunitySortType {
#[cfg_attr(feature = "full", derive(TS, Queryable))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))]
/// A person mention view.
pub struct PersonMentionView {
pub person_mention: PersonMention,
/// A person comment mention view.
pub struct PersonCommentMentionView {
pub person_comment_mention: PersonCommentMention,
pub comment: Comment,
pub creator: Person,
pub post: Post,
@ -113,6 +116,35 @@ pub struct PersonMentionView {
pub my_vote: Option<i16>,
}
#[skip_serializing_none]
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS, Queryable))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))]
/// A person post mention view.
pub struct PersonPostMentionView {
pub person_post_mention: PersonPostMention,
pub post: Post,
pub creator: Person,
pub community: Community,
#[cfg_attr(feature = "full", ts(optional))]
pub image_details: Option<ImageDetails>,
pub recipient: Person,
pub counts: PostAggregates,
pub creator_banned_from_community: bool,
pub banned_from_community: bool,
pub creator_is_moderator: bool,
pub creator_is_admin: bool,
pub subscribed: SubscribedType,
pub saved: bool,
pub 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]
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS, Queryable))]
@ -159,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,11 +14,7 @@ use lemmy_db_views::{
post_view::PostQuery,
structs::{PostView, SiteView},
};
use lemmy_db_views_actor::{
comment_reply_view::CommentReplyQuery,
person_mention_view::PersonMentionQuery,
structs::{CommentReplyView, PersonMentionView},
};
use lemmy_db_views_actor::{inbox_combined_view::InboxCombinedQuery, structs::InboxCombinedView};
use lemmy_utils::{
cache_header::cache_1hour,
error::{LemmyError, LemmyErrorType, LemmyResult},
@ -360,37 +355,20 @@ 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 person_id = local_user.local_user.person_id;
let show_bot_accounts = local_user.local_user.show_bot_accounts;
let sort = CommentSortType::New;
let my_person_id = local_user.person.id;
let show_bot_accounts = Some(local_user.local_user.show_bot_accounts);
check_private_instance(&Some(local_user.clone()), &site_view.local_site)?;
let replies = CommentReplyQuery {
recipient_id: (Some(person_id)),
my_person_id: (Some(person_id)),
show_bot_accounts: (show_bot_accounts),
sort: (Some(sort)),
limit: (Some(RSS_FETCH_LIMIT)),
let inbox = InboxCombinedQuery {
show_bot_accounts,
..Default::default()
}
.list(&mut context.pool())
.await?;
let mentions = PersonMentionQuery {
recipient_id: (Some(person_id)),
my_person_id: (Some(person_id)),
show_bot_accounts: (show_bot_accounts),
sort: (Some(sort)),
limit: (Some(RSS_FETCH_LIMIT)),
..Default::default()
}
.list(&mut context.pool())
.list(&mut context.pool(), my_person_id)
.await?;
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
let items = create_reply_and_mention_items(replies, mentions, &protocol_and_hostname)?;
let items = create_reply_and_mention_items(inbox, &protocol_and_hostname)?;
let mut channel = Channel {
namespaces: RSS_NAMESPACE.clone(),
@ -409,39 +387,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>,
mentions: Vec<PersonMentionView>,
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);
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::<LemmyResult<Vec<Item>>>()?;
let mut mention_items: Vec<Item> = 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::<LemmyResult<Vec<Item>>>()?;
reply_items.append(&mut mention_items);
Ok(reply_items)
}

View file

@ -107,7 +107,7 @@ pub enum LemmyErrorType {
CouldntHidePost,
CouldntUpdateCommunity,
CouldntUpdateReplies,
CouldntUpdatePersonMentions,
CouldntUpdatePersonCommentMentions,
CouldntCreatePost,
CouldntCreatePrivateMessage,
CouldntUpdatePrivate,

View file

@ -33,12 +33,8 @@ CREATE TABLE person_saved_combined (
id serial PRIMARY KEY,
saved timestamptz NOT NULL,
person_id int NOT NULL REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE,
post_id int REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE,
comment_id int REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE,
-- Unique constraints are different here, check for person AND item.
-- Otherwise you won't be able to add multiple posts
UNIQUE (person_id, post_id),
UNIQUE (person_id, comment_id),
post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE,
comment_id int UNIQUE REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE,
-- Make sure only one of the columns is not null
CHECK (num_nonnulls (post_id, comment_id) = 1)
);

View file

@ -0,0 +1,6 @@
-- Rename the person_mention table to person_comment_mention
ALTER TABLE person_comment_mention RENAME TO person_mention;
-- Drop the new tables
DROP TABLE person_post_mention, inbox_combined;

View file

@ -0,0 +1,69 @@
-- 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,8 @@ 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_reply_read::mark_reply_as_read,
unread_count::unread_count,
},
@ -108,7 +106,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},
@ -254,7 +251,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))
@ -315,12 +311,10 @@ 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),
post().to(mark_comment_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,
},
@ -125,7 +125,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},
@ -283,7 +282,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))
@ -314,28 +312,21 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) {
.route("/verify_email", post().to(verify_email))
.route("/saved", get().to(list_person_saved)),
)
.route("/account/settings/save", put().to(save_user_settings))
.service(
scope("/account/settings")
.wrap(rate_limit.import_user_settings())
.route("/export", get().to(export_settings))
.route("/import", post().to(import_settings)),
)
.service(
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),
)
.route(
"/mention/mark_as_read/all",
post().to(mark_all_notifications_read),
.service(
scope("/mention")
.route(
"/comment/mark_as_read",
post().to(mark_comment_mention_as_read),
)
.route("/post/mark_as_read", post().to(mark_post_mention_as_read)),
)
.route("/mark_as_read/all", post().to(mark_all_notifications_read))
.route("/report_count", get().to(report_count))
.route("/unread_count", get().to(unread_count))
.route("/list_logins", get().to(list_logins))
@ -349,6 +340,14 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) {
.route("/person", post().to(user_block_person))
.route("/community", post().to(user_block_community))
.route("/instance", post().to(user_block_instance)),
)
.route("/settings/save", put().to(save_user_settings))
// Account settings import / export have a strict rate limit
.service(
scope("/settings")
.wrap(rate_limit.import_user_settings())
.route("/export", get().to(export_settings))
.route("/import", post().to(import_settings)),
),
)
// User actions