Notifications rewrite and post following (fixes #3069) (#5604)

* Add post_actions.disable_notifications (fixes #3042)

* Split up logic for send_local_notifs()

* refactor

* fmt

* add api endpoint, check

* dont call send_local_notifs from comment delete/remove

* move scrape_text_for_mentions() inside send_local_notifs()

* nullable

* simplify

* handle parent notification first

* cleanup

* remove `CommentResponse.recipient_ids`

* post notifications enum

* Follow posts (fixes #3069)

* use references

* cleanup

* new file

* db migration to merge mention, reply tables

* code adjustments

* adjust test code

* adjust enum case

* wip: get rid of inbox_combined table

* add table local_user_notification

* tests compiling

* get rid of inbox_combined, only use notification table

* rename view

* notify private messages

* clippy

* copy existing data

* wip: tests

* move tests

* clippy

* notify post subscribers

* community subscribe

* test fixes

* migration fix

* rename routes

* separate struct for api

* rename api params

* merge migrations

* separate notification modes for post/community

* fix

* down migration copy data

* test fix

* update api tests

* only single notification table

* clippy

* use local user id for recipient

* fix comments

* rename table

* recipient local user

* add indices

* keep local user id

* renames and cleanup

* NotificationDataType

* change notification response

* fix api tests

* test fix

* remove private_message.read

* fixes

* test fix

* fix
This commit is contained in:
Nutomic 2025-07-17 23:04:09 +00:00 committed by GitHub
parent 31dd9719b0
commit 72d254b4db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
98 changed files with 2336 additions and 2686 deletions

46
Cargo.lock generated
View file

@ -3107,10 +3107,10 @@ dependencies = [
"lemmy_db_views_community_follower", "lemmy_db_views_community_follower",
"lemmy_db_views_community_moderator", "lemmy_db_views_community_moderator",
"lemmy_db_views_community_person_ban", "lemmy_db_views_community_person_ban",
"lemmy_db_views_inbox_combined",
"lemmy_db_views_local_image", "lemmy_db_views_local_image",
"lemmy_db_views_local_user", "lemmy_db_views_local_user",
"lemmy_db_views_modlog_combined", "lemmy_db_views_modlog_combined",
"lemmy_db_views_notification",
"lemmy_db_views_person", "lemmy_db_views_person",
"lemmy_db_views_person_content_combined", "lemmy_db_views_person_content_combined",
"lemmy_db_views_person_liked_combined", "lemmy_db_views_person_liked_combined",
@ -3143,10 +3143,10 @@ dependencies = [
"lemmy_db_views_community_follower", "lemmy_db_views_community_follower",
"lemmy_db_views_community_moderator", "lemmy_db_views_community_moderator",
"lemmy_db_views_custom_emoji", "lemmy_db_views_custom_emoji",
"lemmy_db_views_inbox_combined",
"lemmy_db_views_local_image", "lemmy_db_views_local_image",
"lemmy_db_views_local_user", "lemmy_db_views_local_user",
"lemmy_db_views_modlog_combined", "lemmy_db_views_modlog_combined",
"lemmy_db_views_notification",
"lemmy_db_views_person", "lemmy_db_views_person",
"lemmy_db_views_person_content_combined", "lemmy_db_views_person_content_combined",
"lemmy_db_views_person_liked_combined", "lemmy_db_views_person_liked_combined",
@ -3209,6 +3209,7 @@ dependencies = [
"actix-web-httpauth", "actix-web-httpauth",
"anyhow", "anyhow",
"chrono", "chrono",
"derive-new",
"diesel_ltree", "diesel_ltree",
"either", "either",
"encoding_rs", "encoding_rs",
@ -3227,6 +3228,7 @@ dependencies = [
"lemmy_db_views_community_person_ban", "lemmy_db_views_community_person_ban",
"lemmy_db_views_local_image", "lemmy_db_views_local_image",
"lemmy_db_views_local_user", "lemmy_db_views_local_user",
"lemmy_db_views_notification",
"lemmy_db_views_person", "lemmy_db_views_person",
"lemmy_db_views_post", "lemmy_db_views_post",
"lemmy_db_views_private_message", "lemmy_db_views_private_message",
@ -3316,6 +3318,7 @@ dependencies = [
"lemmy_db_views_community_moderator", "lemmy_db_views_community_moderator",
"lemmy_db_views_community_person_ban", "lemmy_db_views_community_person_ban",
"lemmy_db_views_local_user", "lemmy_db_views_local_user",
"lemmy_db_views_private_message",
"lemmy_db_views_site", "lemmy_db_views_site",
"lemmy_utils", "lemmy_utils",
"moka", "moka",
@ -3500,25 +3503,6 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "lemmy_db_views_inbox_combined"
version = "1.0.0-alpha.5"
dependencies = [
"diesel",
"diesel-async",
"i-love-jesus",
"lemmy_db_schema",
"lemmy_db_schema_file",
"lemmy_db_views_private_message",
"lemmy_utils",
"pretty_assertions",
"serde",
"serde_with",
"serial_test",
"tokio",
"ts-rs",
]
[[package]] [[package]]
name = "lemmy_db_views_local_image" name = "lemmy_db_views_local_image"
version = "1.0.0-alpha.5" version = "1.0.0-alpha.5"
@ -3572,6 +3556,24 @@ dependencies = [
"ts-rs", "ts-rs",
] ]
[[package]]
name = "lemmy_db_views_notification"
version = "1.0.0-alpha.5"
dependencies = [
"diesel",
"diesel-async",
"i-love-jesus",
"lemmy_db_schema",
"lemmy_db_schema_file",
"lemmy_db_views_comment",
"lemmy_db_views_post",
"lemmy_db_views_private_message",
"lemmy_utils",
"serde",
"serde_with",
"ts-rs",
]
[[package]] [[package]]
name = "lemmy_db_views_person" name = "lemmy_db_views_person"
version = "1.0.0-alpha.5" version = "1.0.0-alpha.5"
@ -3882,10 +3884,10 @@ dependencies = [
"lemmy_db_schema", "lemmy_db_schema",
"lemmy_db_schema_file", "lemmy_db_schema_file",
"lemmy_db_views_community", "lemmy_db_views_community",
"lemmy_db_views_inbox_combined",
"lemmy_db_views_local_image", "lemmy_db_views_local_image",
"lemmy_db_views_local_user", "lemmy_db_views_local_user",
"lemmy_db_views_modlog_combined", "lemmy_db_views_modlog_combined",
"lemmy_db_views_notification",
"lemmy_db_views_person_content_combined", "lemmy_db_views_person_content_combined",
"lemmy_db_views_post", "lemmy_db_views_post",
"lemmy_db_views_site", "lemmy_db_views_site",

View file

@ -65,7 +65,7 @@ members = [
"crates/db_views/community_follower", "crates/db_views/community_follower",
"crates/db_views/community_person_ban", "crates/db_views/community_person_ban",
"crates/db_views/custom_emoji", "crates/db_views/custom_emoji",
"crates/db_views/inbox_combined", "crates/db_views/notification",
"crates/db_views/modlog_combined", "crates/db_views/modlog_combined",
"crates/db_views/person_content_combined", "crates/db_views/person_content_combined",
"crates/db_views/person_saved_combined", "crates/db_views/person_saved_combined",
@ -124,7 +124,7 @@ lemmy_db_views_community_follower = { version = "=1.0.0-alpha.5", path = "./crat
lemmy_db_views_community_moderator = { version = "=1.0.0-alpha.5", path = "./crates/db_views/community_moderator" } lemmy_db_views_community_moderator = { version = "=1.0.0-alpha.5", path = "./crates/db_views/community_moderator" }
lemmy_db_views_community_person_ban = { version = "=1.0.0-alpha.5", path = "./crates/db_views/community_person_ban" } lemmy_db_views_community_person_ban = { version = "=1.0.0-alpha.5", path = "./crates/db_views/community_person_ban" }
lemmy_db_views_custom_emoji = { version = "=1.0.0-alpha.5", path = "./crates/db_views/custom_emoji" } lemmy_db_views_custom_emoji = { version = "=1.0.0-alpha.5", path = "./crates/db_views/custom_emoji" }
lemmy_db_views_inbox_combined = { version = "=1.0.0-alpha.5", path = "./crates/db_views/inbox_combined" } lemmy_db_views_notification = { version = "=1.0.0-alpha.5", path = "./crates/db_views/notification" }
lemmy_db_views_local_image = { version = "=1.0.0-alpha.5", path = "./crates/db_views/local_image" } lemmy_db_views_local_image = { version = "=1.0.0-alpha.5", path = "./crates/db_views/local_image" }
lemmy_db_views_local_user = { version = "=1.0.0-alpha.5", path = "./crates/db_views/local_user" } lemmy_db_views_local_user = { version = "=1.0.0-alpha.5", path = "./crates/db_views/local_user" }
lemmy_db_views_modlog_combined = { version = "=1.0.0-alpha.5", path = "./crates/db_views/modlog_combined" } lemmy_db_views_modlog_combined = { version = "=1.0.0-alpha.5", path = "./crates/db_views/modlog_combined" }

View file

@ -31,7 +31,7 @@
"eslint-plugin-prettier": "^5.4.0", "eslint-plugin-prettier": "^5.4.0",
"jest": "^29.5.0", "jest": "^29.5.0",
"joi": "^17.13.3", "joi": "^17.13.3",
"lemmy-js-client": "1.0.0-rename-rate-limit-columns.1", "lemmy-js-client": "1.0.0-post-notifications.5",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"ts-jest": "^29.3.2", "ts-jest": "^29.3.2",
"tsoa": "^6.6.0", "tsoa": "^6.6.0",

View file

@ -36,8 +36,8 @@ importers:
specifier: ^17.13.3 specifier: ^17.13.3
version: 17.13.3 version: 17.13.3
lemmy-js-client: lemmy-js-client:
specifier: 1.0.0-rename-rate-limit-columns.1 specifier: 1.0.0-post-notifications.5
version: 1.0.0-rename-rate-limit-columns.1 version: 1.0.0-post-notifications.5
prettier: prettier:
specifier: ^3.5.3 specifier: ^3.5.3
version: 3.5.3 version: 3.5.3
@ -1594,8 +1594,8 @@ packages:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'} engines: {node: '>=6'}
lemmy-js-client@1.0.0-rename-rate-limit-columns.1: lemmy-js-client@1.0.0-post-notifications.5:
resolution: {integrity: sha512-zlVJ4zkoI/7hNm6x7vr+Su2cRjAr8PQCA9j0GeK1UCMEIBLLSltknuRPC79VJY2sUhRAuR2JwTR0JtZ75SH2XQ==} resolution: {integrity: sha512-2P0KPCordLRfuGTcgsU3pHSFJlVN5t91e04yhpUf5fZT7iTdlEctFQFtURsvfYPNYK/sdvsucqYbnpbbHJUCTA==}
leven@3.1.0: leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
@ -4404,7 +4404,7 @@ snapshots:
kleur@3.0.3: {} kleur@3.0.3: {}
lemmy-js-client@1.0.0-rename-rate-limit-columns.1: lemmy-js-client@1.0.0-post-notifications.5:
dependencies: dependencies:
'@tsoa/runtime': 6.6.0 '@tsoa/runtime': 6.6.0
transitivePeerDependencies: transitivePeerDependencies:

View file

@ -36,16 +36,14 @@ import {
saveUserSettings, saveUserSettings,
listReports, listReports,
listPersonContent, listPersonContent,
listInbox, listNotifications,
} from "./shared"; } from "./shared";
import { import {
CommentReplyView,
CommentReportView, CommentReportView,
CommentView, CommentView,
CommunityView, CommunityView,
DistinguishComment, DistinguishComment,
LemmyError, LemmyError,
PersonCommentMentionView,
ReportCombinedView, ReportCombinedView,
SaveUserSettings, SaveUserSettings,
} from "lemmy-js-client"; } from "lemmy-js-client";
@ -409,23 +407,25 @@ test("Reply to a comment from another instance, get notification", async () => {
// check inbox of replies on alpha, fetching read/unread both // check inbox of replies on alpha, fetching read/unread both
let alphaRepliesRes = await waitUntil( let alphaRepliesRes = await waitUntil(
() => listInbox(alpha, "CommentReply"), () => listNotifications(alpha, "Reply"),
r => r.inbox.length > 0, r => r.notifications.length > 0,
);
const alphaReply = alphaRepliesRes.notifications.find(
r =>
r.data.type_ == "Comment" &&
r.data.comment.id === alphaComment.comment.id,
); );
const alphaReply = alphaRepliesRes.inbox.find(
r => r.type_ == "CommentReply" && r.comment.id === alphaComment.comment.id,
) as CommentReplyView | undefined;
expect(alphaReply).toBeDefined(); expect(alphaReply).toBeDefined();
if (!alphaReply) throw Error(); if (!alphaReply) throw Error();
expect(alphaReply.comment.content).toBeDefined(); const alphaReplyData = alphaReply.data as CommentView;
expect(alphaReply.community.local).toBe(false); expect(alphaReplyData.comment!.content).toBeDefined();
expect(alphaReply.creator.local).toBe(false); expect(alphaReplyData.community!.local).toBe(false);
expect(alphaReply.comment.score).toBe(1); expect(alphaReplyData.creator.local).toBe(false);
expect(alphaReplyData.comment!.score).toBe(1);
// ToDo: interesting alphaRepliesRes.replies[0].comment_reply.id is 1, meaning? how did that come about? // ToDo: interesting alphaRepliesRes.replies[0].comment_reply.id is 1, meaning? how did that come about?
expect(alphaReply.comment.id).toBe(alphaComment.comment.id); expect(alphaReplyData.comment!.id).toBe(alphaComment.comment.id);
// this is a new notification, getReplies fetch was for read/unread both, confirm it is unread. // this is a new notification, getReplies fetch was for read/unread both, confirm it is unread.
expect(alphaReply.comment_reply.read).toBe(false); expect(alphaReply.notification.read).toBe(false);
assertCommentFederation(alphaReply, replyRes.comment_view);
}); });
test("Bot reply notifications are filtered when bots are hidden", async () => { test("Bot reply notifications are filtered when bots are hidden", async () => {
@ -472,9 +472,9 @@ test("Bot reply notifications are filtered when bots are hidden", async () => {
alphaUnreadCountRes = await getUnreadCount(alpha); alphaUnreadCountRes = await getUnreadCount(alpha);
expect(alphaUnreadCountRes.count).toBe(1); expect(alphaUnreadCountRes.count).toBe(1);
let alphaUnreadRepliesRes = await listInbox(alpha, "CommentReply", true); let alphaUnreadRepliesRes = await listNotifications(alpha, "Reply", true);
expect(alphaUnreadRepliesRes.inbox.length).toBe(1); expect(alphaUnreadRepliesRes.notifications.length).toBe(1);
expect((alphaUnreadRepliesRes.inbox[0] as CommentReplyView).comment.id).toBe( expect(alphaUnreadRepliesRes.notifications[0].notification.comment_id).toBe(
commentRes.comment_view.comment.id, commentRes.comment_view.comment.id,
); );
}); });
@ -526,17 +526,18 @@ test("Mention beta from alpha comment", async () => {
assertCommentFederation(betaRootComment, commentRes.comment_view); assertCommentFederation(betaRootComment, commentRes.comment_view);
let mentionsRes = await waitUntil( let mentionsRes = await waitUntil(
() => listInbox(beta, "CommentMention"), () => listNotifications(beta, "Mention"),
m => !!m.inbox[0], m => !!m.notifications[0],
); );
const firstMention = mentionsRes.inbox[0] as PersonCommentMentionView; const firstMention = mentionsRes.notifications[0];
expect(firstMention.comment.content).toBeDefined(); let firstMentionData = firstMention.data as CommentView;
expect(firstMention.community.local).toBe(true); expect(firstMentionData.comment!.content).toBeDefined();
expect(firstMention.creator.local).toBe(false); expect(firstMentionData.community!.local).toBe(true);
expect(firstMention.comment.score).toBe(1); expect(firstMentionData.creator.local).toBe(false);
expect(firstMentionData.comment!.score).toBe(1);
// the reply comment with mention should be the most fresh, newest, index 0 // the reply comment with mention should be the most fresh, newest, index 0
expect(firstMention.person_comment_mention.comment_id).toBe( expect(firstMentionData.comment!.id).toBe(
betaPostComments.comments[0].comment.id, betaPostComments.comments[0].comment.id,
); );
}); });
@ -599,21 +600,24 @@ test("A and G subscribe to B (center) A posts, G mentions B, it gets announced t
); );
// Make sure beta has mentions // Make sure beta has mentions
let relevantMention = (await waitUntil( let relevantMention = await waitUntil(
() => () =>
listInbox(beta, "CommentMention").then(m => listNotifications(beta, "Mention").then(m =>
m.inbox.find( m.notifications.find(m => {
m => let data = m.data as CommentView;
m.type_ == "CommentMention" && return (
m.comment.ap_id === commentRes.comment_view.comment.ap_id, m.notification.kind == "Mention" &&
), data.comment.ap_id === commentRes.comment_view.comment.ap_id
);
}),
), ),
e => !!e, e => !!e,
)) as PersonCommentMentionView | undefined; );
if (!relevantMention) throw Error("could not find mention"); if (!relevantMention) throw Error("could not find mention");
expect(relevantMention.comment.content).toBe(commentContent); let relevantMentionData = relevantMention.data as CommentView;
expect(relevantMention.community.local).toBe(false); expect(relevantMentionData.comment!.content).toBe(commentContent);
expect(relevantMention.creator.local).toBe(false); expect(relevantMentionData.community!.local).toBe(false);
expect(relevantMentionData.creator.local).toBe(false);
// TODO this is failing because fetchInReplyTos aren't getting score // TODO this is failing because fetchInReplyTos aren't getting score
// expect(mentionsRes.mentions[0].score).toBe(1); // expect(mentionsRes.mentions[0].score).toBe(1);
}); });
@ -839,8 +843,8 @@ test("Dont send a comment reply to a blocked community", async () => {
unreadCount = await getUnreadCount(beta); unreadCount = await getUnreadCount(beta);
expect(unreadCount.count).toBe(0); expect(unreadCount.count).toBe(0);
let replies = await listInbox(beta, "CommentReply", true); let replies = await listNotifications(beta, "Reply", true);
expect(replies.inbox.length).toBe(0); expect(replies.notifications.length).toBe(0);
// Unblock the community // Unblock the community
blockRes = await blockCommunity(beta, newCommunityId, false); blockRes = await blockCommunity(beta, newCommunityId, false);

View file

@ -45,7 +45,6 @@ import {
FollowMultiCommunity, FollowMultiCommunity,
GetPosts, GetPosts,
LemmyError, LemmyError,
MultiCommunity,
MultiCommunityView, MultiCommunityView,
ReportCombinedView, ReportCombinedView,
ResolveCommunityReport, ResolveCommunityReport,

View file

@ -38,7 +38,7 @@ import {
createCommunity, createCommunity,
listReports, listReports,
getMyUser, getMyUser,
listInbox, listNotifications,
getModlog, getModlog,
getCommunity, getCommunity,
} from "./shared"; } from "./shared";
@ -48,7 +48,6 @@ import {
AddModToCommunity, AddModToCommunity,
EditSite, EditSite,
EditPost, EditPost,
PersonPostMentionView,
PostReport, PostReport,
PostReportView, PostReportView,
ReportCombinedView, ReportCombinedView,
@ -942,16 +941,15 @@ test("Mention beta from alpha post body", async () => {
await assertPostFederation(betaPost, postOnAlphaRes.post_view); await assertPostFederation(betaPost, postOnAlphaRes.post_view);
let mentionsRes = await waitUntil( let mentionsRes = await waitUntil(
() => listInbox(beta, "PostMention"), () => listNotifications(beta, "Mention"),
m => !!m.inbox[0], m => !!m.notifications[0],
); );
const firstMention = mentionsRes.inbox[0] as PersonPostMentionView; const firstMention = mentionsRes.notifications[0].data as PostView;
expect(firstMention.post.body).toBeDefined(); expect(firstMention.post!.body).toBeDefined();
expect(firstMention.community.local).toBe(true); expect(firstMention.community!.local).toBe(true);
expect(firstMention.creator.local).toBe(false); expect(firstMention.creator.local).toBe(false);
expect(firstMention.post.score).toBe(1); expect(firstMention.post!.score).toBe(1);
expect(firstMention.person_post_mention.post_id).toBe(betaPost.post.id);
}); });
test("Rewrite markdown links", async () => { test("Rewrite markdown links", async () => {

View file

@ -4,14 +4,13 @@ import {
alpha, alpha,
beta, beta,
setupLogins, setupLogins,
followBeta,
createPrivateMessage, createPrivateMessage,
editPrivateMessage, editPrivateMessage,
deletePrivateMessage, deletePrivateMessage,
waitUntil, waitUntil,
reportPrivateMessage, reportPrivateMessage,
unfollows, unfollows,
listInbox, listNotifications,
resolvePerson, resolvePerson,
} from "./shared"; } from "./shared";
@ -37,10 +36,10 @@ test("Create a private message", async () => {
expect(pmRes.private_message_view.recipient.local).toBe(false); expect(pmRes.private_message_view.recipient.local).toBe(false);
let betaPms = await waitUntil( let betaPms = await waitUntil(
() => listInbox(beta, "PrivateMessage"), () => listNotifications(beta, "PrivateMessage"),
e => !!e.inbox[0], e => !!e.notifications[0],
); );
const firstPm = betaPms.inbox[0] as PrivateMessageView; const firstPm = betaPms.notifications[0].data as PrivateMessageView;
expect(firstPm.private_message.content).toBeDefined(); expect(firstPm.private_message.content).toBeDefined();
expect(firstPm.private_message.local).toBe(false); expect(firstPm.private_message.local).toBe(false);
expect(firstPm.creator.local).toBe(false); expect(firstPm.creator.local).toBe(false);
@ -60,25 +59,24 @@ test("Update a private message", async () => {
); );
let betaPms = await waitUntil( let betaPms = await waitUntil(
() => listInbox(beta, "PrivateMessage"), () => listNotifications(beta, "PrivateMessage"),
p => p =>
p.inbox[0].type_ == "PrivateMessage" && p.notifications[0].data.type_ == "PrivateMessage" &&
p.inbox[0].private_message.content === updatedContent, p.notifications[0].data.private_message.content === updatedContent,
);
expect((betaPms.inbox[0] as PrivateMessageView).private_message.content).toBe(
updatedContent,
); );
let pm = betaPms.notifications[0].data as PrivateMessageView;
expect(pm.private_message.content).toBe(updatedContent);
}); });
test("Delete a private message", async () => { test("Delete a private message", async () => {
let pmRes = await createPrivateMessage(alpha, recipient_id); let pmRes = await createPrivateMessage(alpha, recipient_id);
let betaPms1 = await waitUntil( let betaPms1 = await waitUntil(
() => listInbox(beta, "PrivateMessage"), () => listNotifications(beta, "PrivateMessage"),
m => m =>
!!m.inbox.find( !!m.notifications.find(
e => e =>
e.type_ == "PrivateMessage" && e.data.type_ == "PrivateMessage" &&
e.private_message.ap_id === e.data.private_message.ap_id ===
pmRes.private_message_view.private_message.ap_id, pmRes.private_message_view.private_message.ap_id,
), ),
); );
@ -93,10 +91,10 @@ test("Delete a private message", async () => {
// even though they are in the actual database. // even though they are in the actual database.
// no reason to show them // no reason to show them
let betaPms2 = await waitUntil( let betaPms2 = await waitUntil(
() => listInbox(beta, "PrivateMessage"), () => listNotifications(beta, "PrivateMessage"),
p => p.inbox.length === betaPms1.inbox.length - 1, p => p.notifications.length === betaPms1.notifications.length - 1,
); );
expect(betaPms2.inbox.length).toBe(betaPms1.inbox.length - 1); expect(betaPms2.notifications.length).toBe(betaPms1.notifications.length - 1);
// Undelete // Undelete
let undeletedPmRes = await deletePrivateMessage( let undeletedPmRes = await deletePrivateMessage(
@ -109,25 +107,25 @@ test("Delete a private message", async () => {
); );
let betaPms3 = await waitUntil( let betaPms3 = await waitUntil(
() => listInbox(beta, "PrivateMessage"), () => listNotifications(beta, "PrivateMessage"),
p => p.inbox.length === betaPms1.inbox.length, p => p.notifications.length === betaPms1.notifications.length,
); );
expect(betaPms3.inbox.length).toBe(betaPms1.inbox.length); expect(betaPms3.notifications.length).toBe(betaPms1.notifications.length);
}); });
test("Create a private message report", async () => { test("Create a private message report", async () => {
let pmRes = await createPrivateMessage(alpha, recipient_id); let pmRes = await createPrivateMessage(alpha, recipient_id);
let betaPms1 = await waitUntil( let betaPms1 = await waitUntil(
() => listInbox(beta, "PrivateMessage"), () => listNotifications(beta, "PrivateMessage"),
m => m =>
!!m.inbox.find( !!m.notifications.find(
e => e =>
e.type_ == "PrivateMessage" && e.data.type_ == "PrivateMessage" &&
e.private_message.ap_id === e.data.private_message.ap_id ===
pmRes.private_message_view.private_message.ap_id, pmRes.private_message_view.private_message.ap_id,
), ),
); );
let betaPm = betaPms1.inbox[0] as PrivateMessageView; let betaPm = betaPms1.notifications[0].data as PrivateMessageView;
expect(betaPm).toBeDefined(); expect(betaPm).toBeDefined();
// Make sure that only the recipient can report it, so this should fail // Make sure that only the recipient can report it, so this should fail

View file

@ -24,14 +24,14 @@ import {
ListPersonContentResponse, ListPersonContentResponse,
ListPersonContent, ListPersonContent,
PersonContentType, PersonContentType,
ListInboxResponse,
ListInbox,
InboxDataType, InboxDataType,
GetModlogResponse, GetModlogResponse,
GetModlog, GetModlog,
CommunityView, CommunityView,
CommentView, CommentView,
PersonView, PersonView,
ListNotifications,
ListNotificationsResponse,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { CreatePost } from "lemmy-js-client/dist/types/CreatePost"; import { CreatePost } from "lemmy-js-client/dist/types/CreatePost";
import { DeletePost } from "lemmy-js-client/dist/types/DeletePost"; import { DeletePost } from "lemmy-js-client/dist/types/DeletePost";
@ -384,16 +384,16 @@ export async function getUnreadCount(
return api.getUnreadCount(); return api.getUnreadCount();
} }
export async function listInbox( export async function listNotifications(
api: LemmyHttp, api: LemmyHttp,
type_?: InboxDataType, type_?: InboxDataType,
unread_only: boolean = false, unread_only: boolean = false,
): Promise<ListInboxResponse> { ): Promise<ListNotificationsResponse> {
let form: ListInbox = { let form: ListNotifications = {
unread_only, unread_only,
type_, type_,
}; };
return api.listInbox(form); return api.listNotifications(form);
} }
export async function resolveComment( export async function resolveComment(

View file

@ -29,7 +29,7 @@ lemmy_db_views_vote = { workspace = true, features = ["full"] }
lemmy_db_views_local_user = { workspace = true, features = ["full"] } lemmy_db_views_local_user = { workspace = true, features = ["full"] }
lemmy_db_views_person = { workspace = true, features = ["full"] } lemmy_db_views_person = { workspace = true, features = ["full"] }
lemmy_db_views_local_image = { workspace = true, features = ["full"] } lemmy_db_views_local_image = { workspace = true, features = ["full"] }
lemmy_db_views_inbox_combined = { workspace = true, features = ["full"] } lemmy_db_views_notification = { workspace = true, features = ["full"] }
lemmy_db_views_modlog_combined = { workspace = true, features = ["full"] } lemmy_db_views_modlog_combined = { workspace = true, features = ["full"] }
lemmy_db_views_person_saved_combined = { workspace = true, features = ["full"] } lemmy_db_views_person_saved_combined = { workspace = true, features = ["full"] }
lemmy_db_views_person_liked_combined = { workspace = true, features = ["full"] } lemmy_db_views_person_liked_combined = { workspace = true, features = ["full"] }

View file

@ -69,8 +69,5 @@ pub async fn distinguish_comment(
) )
.await?; .await?;
Ok(Json(CommentResponse { Ok(Json(CommentResponse { comment_view }))
comment_view,
recipient_ids: Vec::new(),
}))
} }

View file

@ -8,10 +8,9 @@ use lemmy_api_utils::{
utils::{check_bot_account, check_community_user_action, check_local_vote_mode}, utils::{check_bot_account, check_community_user_action, check_local_vote_mode},
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::{LocalUserId, PostOrCommentId}, newtypes::PostOrCommentId,
source::{ source::{
comment::{CommentActions, CommentLikeForm}, comment::{CommentActions, CommentLikeForm},
comment_reply::CommentReply,
person::PersonActions, person::PersonActions,
}, },
traits::Likeable, traits::Likeable,
@ -35,8 +34,6 @@ pub async fn like_comment(
let comment_id = data.comment_id; let comment_id = data.comment_id;
let my_person_id = local_user_view.person.id; let my_person_id = local_user_view.person.id;
let mut recipient_ids = Vec::<LocalUserId>::new();
check_local_vote_mode( check_local_vote_mode(
data.score, data.score,
PostOrCommentId::Comment(comment_id), PostOrCommentId::Comment(comment_id),
@ -63,16 +60,6 @@ pub async fn like_comment(
) )
.await?; .await?;
// Add parent poster or commenter to recipients
let comment_reply = CommentReply::read_by_comment(&mut context.pool(), comment_id).await;
if let Ok(Some(reply)) = comment_reply {
let recipient_id = reply.recipient_id;
if let Ok(local_recipient) = LocalUserView::read_person(&mut context.pool(), recipient_id).await
{
recipient_ids.push(local_recipient.local_user.id);
}
}
let mut like_form = CommentLikeForm::new(my_person_id, data.comment_id, data.score); let mut like_form = CommentLikeForm::new(my_person_id, data.comment_id, data.score);
// Remove any likes first // Remove any likes first
@ -119,7 +106,6 @@ pub async fn like_comment(
context.deref(), context.deref(),
comment_id, comment_id,
Some(local_user_view), Some(local_user_view),
recipient_ids,
local_instance_id, local_instance_id,
) )
.await?, .await?,

View file

@ -34,8 +34,5 @@ pub async fn save_comment(
) )
.await?; .await?;
Ok(Json(CommentResponse { Ok(Json(CommentResponse { comment_view }))
comment_view,
recipient_ids: Vec::new(),
}))
} }

View file

@ -7,3 +7,4 @@ pub mod pending_follows;
pub mod random; pub mod random;
pub mod tag; pub mod tag;
pub mod transfer; pub mod transfer;
pub mod update_notifications;

View file

@ -0,0 +1,23 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_utils::context::LemmyContext;
use lemmy_db_schema::source::community::CommunityActions;
use lemmy_db_views_community::api::UpdateCommunityNotifications;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_site::api::SuccessResponse;
use lemmy_utils::error::LemmyResult;
pub async fn update_community_notifications(
data: Json<UpdateCommunityNotifications>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
CommunityActions::update_notification_state(
data.community_id,
local_user_view.person.id,
data.mode,
&mut context.pool(),
)
.await?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -13,7 +13,6 @@ pub mod comment;
pub mod community; pub mod community;
pub mod local_user; pub mod local_user;
pub mod post; pub mod post;
pub mod private_message;
pub mod reports; pub mod reports;
pub mod site; pub mod site;
pub mod sitemap; pub mod sitemap;

View file

@ -3,8 +3,8 @@ use actix_web::web::Json;
use lemmy_api_utils::context::LemmyContext; use lemmy_api_utils::context::LemmyContext;
use lemmy_db_schema::source::local_user::LocalUser; use lemmy_db_schema::source::local_user::LocalUser;
use lemmy_db_views_community_moderator::CommunityModeratorView; use lemmy_db_views_community_moderator::CommunityModeratorView;
use lemmy_db_views_inbox_combined::{impls::InboxCombinedQuery, InboxCombinedView};
use lemmy_db_views_local_user::LocalUserView; use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_notification::{impls::NotificationQuery, NotificationData};
use lemmy_db_views_person_content_combined::{ use lemmy_db_views_person_content_combined::{
impls::PersonContentCombinedQuery, impls::PersonContentCombinedQuery,
PersonContentCombinedView, PersonContentCombinedView,
@ -46,18 +46,18 @@ pub async fn export_data(
}) })
.collect(); .collect();
let inbox = InboxCombinedQuery { let notifications = NotificationQuery {
no_limit: Some(true), no_limit: Some(true),
..InboxCombinedQuery::default() show_bot_accounts: Some(local_user_view.local_user.show_bot_accounts),
..NotificationQuery::default()
} }
.list(pool, my_person_id, local_instance_id) .list(pool, &local_user_view.person)
.await? .await?
.into_iter() .into_iter()
.map(|u| match u { .map(|u| match u.data {
InboxCombinedView::CommentReply(cr) => Comment(cr.comment), NotificationData::Post(p) => Post(p.post),
InboxCombinedView::CommentMention(cm) => Comment(cm.comment), NotificationData::Comment(c) => Comment(c.comment),
InboxCombinedView::PostMention(pm) => Post(pm.post), NotificationData::PrivateMessage(pm) => PrivateMessage(pm.private_message),
InboxCombinedView::PrivateMessage(pm) => PrivateMessage(pm.private_message),
}) })
.collect(); .collect();
@ -93,7 +93,7 @@ pub async fn export_data(
let settings = user_backup_list_to_user_settings_backup(local_user_view, lists); let settings = user_backup_list_to_user_settings_backup(local_user_view, lists);
Ok(Json(ExportDataResponse { Ok(Json(ExportDataResponse {
inbox, notifications,
content, content,
liked, liked,
read_posts, read_posts,

View file

@ -0,0 +1,46 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_utils::context::LemmyContext;
use lemmy_db_schema::traits::PaginationCursorBuilder;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_notification::{
impls::NotificationQuery,
ListNotifications,
ListNotificationsResponse,
NotificationView,
};
use lemmy_utils::error::LemmyResult;
pub async fn list_notifications(
data: Query<ListNotifications>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<ListNotificationsResponse>> {
let cursor_data = if let Some(cursor) = &data.page_cursor {
Some(NotificationView::from_cursor(cursor, &mut context.pool()).await?)
} else {
None
};
let notifications = NotificationQuery {
type_: data.type_,
unread_only: data.unread_only,
show_bot_accounts: Some(local_user_view.local_user.show_bot_accounts),
cursor_data,
page_back: data.page_back,
limit: data.limit,
no_limit: None,
}
.list(&mut context.pool(), &local_user_view.person)
.await?;
let next_page = notifications.last().map(PaginationCursorBuilder::to_cursor);
let prev_page = notifications
.first()
.map(PaginationCursorBuilder::to_cursor);
Ok(Json(ListNotificationsResponse {
notifications,
next_page,
prev_page,
}))
}

View file

@ -1,47 +0,0 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_utils::context::LemmyContext;
use lemmy_db_schema::traits::PaginationCursorBuilder;
use lemmy_db_views_inbox_combined::{
impls::InboxCombinedQuery,
InboxCombinedView,
ListInbox,
ListInboxResponse,
};
use lemmy_db_views_local_user::LocalUserView;
use lemmy_utils::error::LemmyResult;
pub async fn list_inbox(
data: Query<ListInbox>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<ListInboxResponse>> {
let person_id = local_user_view.person.id;
let local_instance_id = local_user_view.person.instance_id;
let cursor_data = if let Some(cursor) = &data.page_cursor {
Some(InboxCombinedView::from_cursor(cursor, &mut context.pool()).await?)
} else {
None
};
let inbox = InboxCombinedQuery {
type_: data.type_,
unread_only: data.unread_only,
show_bot_accounts: Some(local_user_view.local_user.show_bot_accounts),
cursor_data,
page_back: data.page_back,
limit: data.limit,
no_limit: None,
}
.list(&mut context.pool(), person_id, local_instance_id)
.await?;
let next_page = inbox.last().map(PaginationCursorBuilder::to_cursor);
let prev_page = inbox.first().map(PaginationCursorBuilder::to_cursor);
Ok(Json(ListInboxResponse {
inbox,
next_page,
prev_page,
}))
}

View file

@ -1,11 +1,6 @@
use actix_web::web::{Data, Json}; use actix_web::web::{Data, Json};
use lemmy_api_utils::context::LemmyContext; use lemmy_api_utils::context::LemmyContext;
use lemmy_db_schema::source::{ use lemmy_db_schema::source::notification::Notification;
comment_reply::CommentReply,
person_comment_mention::PersonCommentMention,
person_post_mention::PersonPostMention,
private_message::PrivateMessage,
};
use lemmy_db_views_local_user::LocalUserView; use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_site::api::SuccessResponse; use lemmy_db_views_site::api::SuccessResponse;
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyResult;
@ -14,19 +9,7 @@ pub async fn mark_all_notifications_read(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
let person_id = local_user_view.person.id; Notification::mark_all_as_read(&mut context.pool(), local_user_view.person.id).await?;
// Mark all comment_replies as read
CommentReply::mark_all_as_read(&mut context.pool(), person_id).await?;
// Mark all comment mentions as read
PersonCommentMention::mark_all_as_read(&mut context.pool(), person_id).await?;
// Mark all post mentions as read
PersonPostMention::mark_all_as_read(&mut context.pool(), person_id).await?;
// Mark all private_messages as read
PrivateMessage::mark_all_as_read(&mut context.pool(), person_id).await?;
Ok(Json(SuccessResponse::default())) Ok(Json(SuccessResponse::default()))
} }

View file

@ -1,35 +0,0 @@
use actix_web::web::{Data, Json};
use lemmy_api_utils::context::LemmyContext;
use lemmy_db_schema::{
source::person_comment_mention::{PersonCommentMention, PersonCommentMentionUpdateForm},
traits::Crud,
};
use lemmy_db_views_inbox_combined::api::MarkPersonCommentMentionAsRead;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_site::api::SuccessResponse;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
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?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -0,0 +1,22 @@
use actix_web::web::{Data, Json};
use lemmy_api_utils::context::LemmyContext;
use lemmy_db_schema::source::notification::Notification;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_notification::api::MarkNotificationAsRead;
use lemmy_db_views_site::api::SuccessResponse;
use lemmy_utils::error::LemmyResult;
pub async fn mark_notification_as_read(
data: Json<MarkNotificationAsRead>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
Notification::mark_read_by_id_and_person(
&mut context.pool(),
data.notification_id,
local_user_view.person.id,
)
.await?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -1,35 +0,0 @@
use actix_web::web::{Data, Json};
use lemmy_api_utils::context::LemmyContext;
use lemmy_db_schema::{
source::person_post_mention::{PersonPostMention, PersonPostMentionUpdateForm},
traits::Crud,
};
use lemmy_db_views_inbox_combined::api::MarkPersonPostMentionAsRead;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_site::api::SuccessResponse;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
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?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -1,35 +0,0 @@
use actix_web::web::{Data, Json};
use lemmy_api_utils::context::LemmyContext;
use lemmy_db_schema::{
source::comment_reply::{CommentReply, CommentReplyUpdateForm},
traits::Crud,
};
use lemmy_db_views_inbox_combined::api::MarkCommentReplyAsRead;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_site::api::SuccessResponse;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
pub async fn mark_reply_as_read(
data: Json<MarkCommentReplyAsRead>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
let comment_reply_id = data.comment_reply_id;
let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id).await?;
if local_user_view.person.id != read_comment_reply.recipient_id {
Err(LemmyErrorType::CouldntUpdateComment)?
}
let comment_reply_id = read_comment_reply.id;
let read = Some(data.read);
CommentReply::update(
&mut context.pool(),
comment_reply_id,
&CommentReplyUpdateForm { read },
)
.await?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -1,6 +1,4 @@
pub mod list_inbox; pub mod list;
pub mod mark_all_read; pub mod mark_all_read;
pub mod mark_comment_mention_read; pub mod mark_notification_read;
pub mod mark_post_mention_read;
pub mod mark_reply_read;
pub mod unread_count; pub mod unread_count;

View file

@ -1,21 +1,17 @@
use actix_web::web::{Data, Json}; use actix_web::web::{Data, Json};
use lemmy_api_utils::context::LemmyContext; use lemmy_api_utils::context::LemmyContext;
use lemmy_db_views_inbox_combined::{api::GetUnreadCountResponse, InboxCombinedViewInternal};
use lemmy_db_views_local_user::LocalUserView; use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_notification::{api::GetUnreadCountResponse, NotificationView};
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyResult;
pub async fn unread_count( pub async fn unread_count(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<GetUnreadCountResponse>> { ) -> LemmyResult<Json<GetUnreadCountResponse>> {
let person_id = local_user_view.person.id;
let local_instance_id = local_user_view.person.instance_id;
let show_bot_accounts = local_user_view.local_user.show_bot_accounts; let show_bot_accounts = local_user_view.local_user.show_bot_accounts;
let count = NotificationView::get_unread_count(
let count = InboxCombinedViewInternal::get_unread_count(
&mut context.pool(), &mut context.pool(),
person_id, &local_user_view.person,
local_instance_id,
show_bot_accounts, show_bot_accounts,
) )
.await?; .await?;

View file

@ -7,3 +7,4 @@ pub mod lock;
pub mod mark_many_read; pub mod mark_many_read;
pub mod mark_read; pub mod mark_read;
pub mod save; pub mod save;
pub mod update_notifications;

View file

@ -0,0 +1,23 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_utils::context::LemmyContext;
use lemmy_db_schema::source::post::PostActions;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_post::api::UpdatePostNotifications;
use lemmy_db_views_site::api::SuccessResponse;
use lemmy_utils::error::LemmyResult;
pub async fn update_post_notifications(
data: Json<UpdatePostNotifications>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
PostActions::update_notification_state(
data.post_id,
local_user_view.person.id,
data.mode,
&mut context.pool(),
)
.await?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -1,38 +0,0 @@
use actix_web::web::{Data, Json};
use lemmy_api_utils::context::LemmyContext;
use lemmy_db_schema::{
source::private_message::{PrivateMessage, PrivateMessageUpdateForm},
traits::Crud,
};
use lemmy_db_views_inbox_combined::api::MarkPrivateMessageAsRead;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_site::api::SuccessResponse;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
pub async fn mark_pm_as_read(
data: Json<MarkPrivateMessageAsRead>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> 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?;
if local_user_view.person.id != orig_private_message.recipient_id {
Err(LemmyErrorType::CouldntUpdatePrivateMessage)?
}
// Doing the update
let private_message_id = data.private_message_id;
let read = data.read;
PrivateMessage::update(
&mut context.pool(),
private_message_id,
&PrivateMessageUpdateForm {
read: Some(read),
..Default::default()
},
)
.await?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -1 +0,0 @@
pub mod mark_read;

View file

@ -20,8 +20,8 @@ use lemmy_db_schema::{
utils::DbPool, utils::DbPool,
}; };
use lemmy_db_schema_file::enums::RegistrationMode; use lemmy_db_schema_file::enums::RegistrationMode;
use lemmy_db_views_inbox_combined::api::GetUnreadRegistrationApplicationCountResponse;
use lemmy_db_views_local_user::LocalUserView; use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_notification::api::GetUnreadRegistrationApplicationCountResponse;
use lemmy_db_views_registration_applications::api::{ use lemmy_db_views_registration_applications::api::{
ApproveRegistrationApplication, ApproveRegistrationApplication,
ListRegistrationApplicationsResponse, ListRegistrationApplicationsResponse,

View file

@ -1,8 +1,8 @@
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_api_utils::{context::LemmyContext, utils::is_admin}; use lemmy_api_utils::{context::LemmyContext, utils::is_admin};
use lemmy_db_views_inbox_combined::api::GetUnreadRegistrationApplicationCountResponse;
use lemmy_db_views_local_user::LocalUserView; use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_notification::api::GetUnreadRegistrationApplicationCountResponse;
use lemmy_db_views_registration_applications::RegistrationApplicationView; use lemmy_db_views_registration_applications::RegistrationApplicationView;
use lemmy_db_views_site::SiteView; use lemmy_db_views_site::SiteView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyResult;

View file

@ -27,7 +27,7 @@ ts-rs = [
"lemmy_db_views_community_follower/ts-rs", "lemmy_db_views_community_follower/ts-rs",
"lemmy_db_views_community_moderator/ts-rs", "lemmy_db_views_community_moderator/ts-rs",
"lemmy_db_views_custom_emoji/ts-rs", "lemmy_db_views_custom_emoji/ts-rs",
"lemmy_db_views_inbox_combined/ts-rs", "lemmy_db_views_notification/ts-rs",
"lemmy_db_views_local_image/ts-rs", "lemmy_db_views_local_image/ts-rs",
"lemmy_db_views_local_user/ts-rs", "lemmy_db_views_local_user/ts-rs",
"lemmy_db_views_modlog_combined/ts-rs", "lemmy_db_views_modlog_combined/ts-rs",
@ -54,7 +54,7 @@ lemmy_db_views_community.workspace = true
lemmy_db_views_community_follower.workspace = true lemmy_db_views_community_follower.workspace = true
lemmy_db_views_community_moderator.workspace = true lemmy_db_views_community_moderator.workspace = true
lemmy_db_views_custom_emoji.workspace = true lemmy_db_views_custom_emoji.workspace = true
lemmy_db_views_inbox_combined.workspace = true lemmy_db_views_notification.workspace = true
lemmy_db_views_local_image.workspace = true lemmy_db_views_local_image.workspace = true
lemmy_db_views_local_user.workspace = true lemmy_db_views_local_user.workspace = true
lemmy_db_views_modlog_combined.workspace = true lemmy_db_views_modlog_combined.workspace = true

View file

@ -1,24 +0,0 @@
pub use lemmy_db_schema::{
newtypes::{CommentReplyId, PersonCommentMentionId, PersonPostMentionId},
source::{
comment_reply::CommentReply,
person_comment_mention::PersonCommentMention,
person_post_mention::PersonPostMention,
},
InboxDataType,
};
pub use lemmy_db_views_inbox_combined::{
api::{
GetUnreadCountResponse,
MarkCommentReplyAsRead,
MarkPersonCommentMentionAsRead,
MarkPersonPostMentionAsRead,
MarkPrivateMessageAsRead,
},
CommentReplyView,
InboxCombinedView,
ListInbox,
ListInboxResponse,
PersonCommentMentionView,
PersonPostMentionView,
};

View file

@ -4,10 +4,10 @@ pub mod community;
pub mod custom_emoji; pub mod custom_emoji;
pub mod error; pub mod error;
pub mod federation; pub mod federation;
pub mod inbox;
pub mod language; pub mod language;
pub mod media; pub mod media;
pub mod modlog; pub mod modlog;
pub mod notification;
pub mod oauth; pub mod oauth;
pub mod person; pub mod person;
pub mod plugin; pub mod plugin;

View file

@ -0,0 +1,11 @@
pub use lemmy_db_schema::{
newtypes::NotificationId,
source::notification::Notification,
NotificationDataType,
};
pub use lemmy_db_views_notification::{
api::{GetUnreadCountResponse, MarkNotificationAsRead, MarkPrivateMessageAsRead},
ListNotifications,
ListNotificationsResponse,
NotificationView,
};

View file

@ -14,8 +14,8 @@ pub use lemmy_db_views_site::{
}; };
pub mod administration { pub mod administration {
pub use lemmy_db_views_inbox_combined::api::GetUnreadRegistrationApplicationCountResponse;
pub use lemmy_db_views_local_user::api::{AdminListUsers, AdminListUsersResponse}; pub use lemmy_db_views_local_user::api::{AdminListUsers, AdminListUsersResponse};
pub use lemmy_db_views_notification::api::GetUnreadRegistrationApplicationCountResponse;
pub use lemmy_db_views_person::api::{AddAdmin, AddAdminResponse}; pub use lemmy_db_views_person::api::{AddAdmin, AddAdminResponse};
pub use lemmy_db_views_registration_applications::api::{ pub use lemmy_db_views_registration_applications::api::{
ApproveRegistrationApplication, ApproveRegistrationApplication,

View file

@ -2,8 +2,9 @@ use crate::community_use_pending;
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_api_utils::{ use lemmy_api_utils::{
build_response::{build_comment_response, send_local_notifs}, build_response::build_comment_response,
context::LemmyContext, context::LemmyContext,
notify::NotifyData,
plugins::{plugin_hook_after, plugin_hook_before}, plugins::{plugin_hook_after, plugin_hook_before},
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::{ utils::{
@ -19,11 +20,9 @@ use lemmy_api_utils::{
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
impls::actor_language::validate_post_language, impls::actor_language::validate_post_language,
newtypes::PostOrCommentId,
source::{ source::{
comment::{Comment, CommentActions, CommentInsertForm, CommentLikeForm}, comment::{Comment, CommentActions, CommentInsertForm, CommentLikeForm},
comment_reply::{CommentReply, CommentReplyUpdateForm}, notification::Notification,
person_comment_mention::{PersonCommentMention, PersonCommentMentionUpdateForm},
}, },
traits::{Crud, Likeable}, traits::{Crud, Likeable},
}; };
@ -33,7 +32,7 @@ use lemmy_db_views_post::PostView;
use lemmy_db_views_site::SiteView; use lemmy_db_views_site::SiteView;
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyErrorType, LemmyResult}, error::{LemmyErrorType, LemmyResult},
utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field}, utils::validation::is_valid_body_field,
}; };
pub async fn create_comment( pub async fn create_comment(
@ -113,20 +112,14 @@ pub async fn create_comment(
Comment::create(&mut context.pool(), &comment_form, parent_path.as_ref()).await?; Comment::create(&mut context.pool(), &comment_form, parent_path.as_ref()).await?;
plugin_hook_after("after_create_local_comment", &inserted_comment)?; plugin_hook_after("after_create_local_comment", &inserted_comment)?;
let inserted_comment_id = inserted_comment.id; NotifyData::new(
&post,
// Scan the comment for user mentions, add those rows Some(&inserted_comment),
let mentions = scrape_text_for_mentions(&content);
let do_send_email = !local_site.disable_email_notifications;
let recipient_ids = send_local_notifs(
mentions,
PostOrCommentId::Comment(inserted_comment_id),
&local_user_view.person, &local_user_view.person,
do_send_email, &post_view.community,
&context, !local_site.disable_email_notifications,
Some(&local_user_view),
local_instance_id,
) )
.send(&context)
.await?; .await?;
// You like your own comment by default // You like your own comment by default
@ -153,30 +146,10 @@ pub async fn create_comment(
// then mark the parent as read. // then mark the parent as read.
// Then we don't have to do it manually after we respond to a comment. // Then we don't have to do it manually after we respond to a comment.
if let Some(parent) = parent_opt { if let Some(parent) = parent_opt {
let person_id = local_user_view.person.id; let notif = Notification::read_by_comment_id(&mut context.pool(), parent.id).await;
let parent_id = parent.id; if let Ok(notif) = notif {
let comment_reply = let person_id = local_user_view.person.id;
CommentReply::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await; Notification::mark_read_by_id_and_person(&mut context.pool(), notif.id, person_id).await?;
if let Ok(Some(reply)) = comment_reply {
CommentReply::update(
&mut context.pool(),
reply.id,
&CommentReplyUpdateForm { read: Some(true) },
)
.await?;
}
// 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,
&PersonCommentMentionUpdateForm { read: Some(true) },
)
.await?;
} }
} }
@ -185,7 +158,6 @@ pub async fn create_comment(
&context, &context,
inserted_comment.id, inserted_comment.id,
Some(local_user_view), Some(local_user_view),
recipient_ids,
local_instance_id, local_instance_id,
) )
.await?, .await?,

View file

@ -1,13 +1,12 @@
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_api_utils::{ use lemmy_api_utils::{
build_response::{build_comment_response, send_local_notifs}, build_response::build_comment_response,
context::LemmyContext, context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::check_community_user_action, utils::check_community_user_action,
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::PostOrCommentId,
source::comment::{Comment, CommentUpdateForm}, source::comment::{Comment, CommentUpdateForm},
traits::Crud, traits::Crud,
}; };
@ -62,16 +61,6 @@ pub async fn delete_comment(
) )
.await?; .await?;
let recipient_ids = send_local_notifs(
vec![],
PostOrCommentId::Comment(comment_id),
&local_user_view.person,
false,
&context,
Some(&local_user_view),
local_instance_id,
)
.await?;
let updated_comment_id = updated_comment.id; let updated_comment_id = updated_comment.id;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
@ -88,7 +77,6 @@ pub async fn delete_comment(
&context, &context,
updated_comment_id, updated_comment_id,
Some(local_user_view), Some(local_user_view),
recipient_ids,
local_instance_id, local_instance_id,
) )
.await?, .await?,

View file

@ -21,13 +21,6 @@ pub async fn get_comment(
check_private_instance(&local_user_view, &local_site)?; check_private_instance(&local_user_view, &local_site)?;
Ok(Json( Ok(Json(
build_comment_response( build_comment_response(&context, data.id, local_user_view, local_instance_id).await?,
&context,
data.id,
local_user_view,
vec![],
local_instance_id,
)
.await?,
)) ))
} }

View file

@ -1,13 +1,12 @@
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_api_utils::{ use lemmy_api_utils::{
build_response::{build_comment_response, send_local_notifs}, build_response::build_comment_response,
context::LemmyContext, context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::check_community_mod_action, utils::check_community_mod_action,
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::PostOrCommentId,
source::{ source::{
comment::{Comment, CommentUpdateForm}, comment::{Comment, CommentUpdateForm},
comment_report::CommentReport, comment_report::CommentReport,
@ -84,16 +83,6 @@ pub async fn remove_comment(
}; };
ModRemoveComment::create(&mut context.pool(), &form).await?; ModRemoveComment::create(&mut context.pool(), &form).await?;
let recipient_ids = send_local_notifs(
vec![],
PostOrCommentId::Comment(comment_id),
&local_user_view.person,
false,
&context,
Some(&local_user_view),
local_instance_id,
)
.await?;
let updated_comment_id = updated_comment.id; let updated_comment_id = updated_comment.id;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
@ -111,7 +100,6 @@ pub async fn remove_comment(
&context, &context,
updated_comment_id, updated_comment_id,
Some(local_user_view), Some(local_user_view),
recipient_ids,
local_instance_id, local_instance_id,
) )
.await?, .await?,

View file

@ -2,15 +2,15 @@ use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use chrono::Utc; use chrono::Utc;
use lemmy_api_utils::{ use lemmy_api_utils::{
build_response::{build_comment_response, send_local_notifs}, build_response::build_comment_response,
context::LemmyContext, context::LemmyContext,
notify::NotifyData,
plugins::{plugin_hook_after, plugin_hook_before}, plugins::{plugin_hook_after, plugin_hook_before},
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_user_action, get_url_blocklist, process_markdown_opt, slur_regex}, utils::{check_community_user_action, get_url_blocklist, process_markdown_opt, slur_regex},
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
impls::actor_language::validate_post_language, impls::actor_language::validate_post_language,
newtypes::PostOrCommentId,
source::comment::{Comment, CommentUpdateForm}, source::comment::{Comment, CommentUpdateForm},
traits::Crud, traits::Crud,
}; };
@ -21,7 +21,7 @@ use lemmy_db_views_comment::{
use lemmy_db_views_local_user::LocalUserView; use lemmy_db_views_local_user::LocalUserView;
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyErrorType, LemmyResult}, error::{LemmyErrorType, LemmyResult},
utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field}, utils::validation::is_valid_body_field,
}; };
pub async fn update_comment( pub async fn update_comment(
@ -79,17 +79,14 @@ pub async fn update_comment(
plugin_hook_after("after_update_local_comment", &updated_comment)?; plugin_hook_after("after_update_local_comment", &updated_comment)?;
// Do the mentions / recipients // Do the mentions / recipients
let updated_comment_content = updated_comment.content.clone(); NotifyData::new(
let mentions = scrape_text_for_mentions(&updated_comment_content); &orig_comment.post,
let recipient_ids = send_local_notifs( Some(&updated_comment),
mentions,
PostOrCommentId::Comment(comment_id),
&local_user_view.person, &local_user_view.person,
&orig_comment.community,
false, false,
&context,
Some(&local_user_view),
local_instance_id,
) )
.send(&context)
.await?; .await?;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
@ -102,7 +99,6 @@ pub async fn update_comment(
&context, &context,
updated_comment.id, updated_comment.id,
Some(local_user_view), Some(local_user_view),
recipient_ids,
local_instance_id, local_instance_id,
) )
.await?, .await?,

View file

@ -3,8 +3,9 @@ use crate::community_use_pending;
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_api_utils::{ use lemmy_api_utils::{
build_response::{build_post_response, send_local_notifs}, build_response::build_post_response,
context::LemmyContext, context::LemmyContext,
notify::NotifyData,
plugins::{plugin_hook_after, plugin_hook_before}, plugins::{plugin_hook_after, plugin_hook_before},
request::generate_post_link_metadata, request::generate_post_link_metadata,
send_activity::SendActivityData, send_activity::SendActivityData,
@ -21,7 +22,6 @@ use lemmy_api_utils::{
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
impls::actor_language::validate_post_language, impls::actor_language::validate_post_language,
newtypes::PostOrCommentId,
source::post::{Post, PostActions, PostInsertForm, PostLikeForm, PostReadForm}, source::post::{Post, PostActions, PostInsertForm, PostLikeForm, PostReadForm},
traits::{Crud, Likeable}, traits::{Crud, Likeable},
utils::diesel_url_create, utils::diesel_url_create,
@ -34,7 +34,6 @@ use lemmy_db_views_site::SiteView;
use lemmy_utils::{ use lemmy_utils::{
error::LemmyResult, error::LemmyResult,
utils::{ utils::{
mention::scrape_text_for_mentions,
slurs::check_slurs, slurs::check_slurs,
validation::{ validation::{
is_url_blocked, is_url_blocked,
@ -169,23 +168,18 @@ pub async fn create_post(
// They like their own post by default // They like their own post by default
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let post_id = inserted_post.id; let post_id = inserted_post.id;
let local_instance_id = local_user_view.person.instance_id;
let like_form = PostLikeForm::new(post_id, person_id, 1); let like_form = PostLikeForm::new(post_id, person_id, 1);
PostActions::like(&mut context.pool(), &like_form).await?; PostActions::like(&mut context.pool(), &like_form).await?;
// Scan the post body for user mentions, add those rows NotifyData::new(
let mentions = scrape_text_for_mentions(&inserted_post.body.clone().unwrap_or_default()); &inserted_post,
let do_send_email = !local_site.disable_email_notifications; None,
send_local_notifs(
mentions,
PostOrCommentId::Post(inserted_post.id),
&local_user_view.person, &local_user_view.person,
do_send_email, community,
&context, !local_site.disable_email_notifications,
Some(&local_user_view),
local_instance_id,
) )
.send(&context)
.await?; .await?;
let read_form = PostReadForm::new(post_id, person_id); let read_form = PostReadForm::new(post_id, person_id);

View file

@ -3,8 +3,9 @@ use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use chrono::Utc; use chrono::Utc;
use lemmy_api_utils::{ use lemmy_api_utils::{
build_response::{build_post_response, send_local_notifs}, build_response::build_post_response,
context::LemmyContext, context::LemmyContext,
notify::NotifyData,
plugins::{plugin_hook_after, plugin_hook_before}, plugins::{plugin_hook_after, plugin_hook_before},
request::generate_post_link_metadata, request::generate_post_link_metadata,
send_activity::SendActivityData, send_activity::SendActivityData,
@ -20,7 +21,6 @@ use lemmy_api_utils::{
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
impls::actor_language::validate_post_language, impls::actor_language::validate_post_language,
newtypes::PostOrCommentId,
source::{ source::{
community::Community, community::Community,
post::{Post, PostUpdateForm}, post::{Post, PostUpdateForm},
@ -38,7 +38,6 @@ use lemmy_db_views_site::SiteView;
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyErrorType, LemmyResult}, error::{LemmyErrorType, LemmyResult},
utils::{ utils::{
mention::scrape_text_for_mentions,
slurs::check_slurs, slurs::check_slurs,
validation::{ validation::{
is_url_blocked, is_url_blocked,
@ -163,17 +162,14 @@ pub async fn update_post(
let updated_post = Post::update(&mut context.pool(), post_id, &post_form).await?; let updated_post = Post::update(&mut context.pool(), post_id, &post_form).await?;
plugin_hook_after("after_update_local_post", &post_form)?; plugin_hook_after("after_update_local_post", &post_form)?;
// Scan the post body for user mentions, add those rows NotifyData::new(
let mentions = scrape_text_for_mentions(&updated_post.body.clone().unwrap_or_default()); &updated_post,
send_local_notifs( None,
mentions,
PostOrCommentId::Post(updated_post.id),
&local_user_view.person, &local_user_view.person,
&orig_post.community,
false, false,
&context,
Some(&local_user_view),
local_instance_id,
) )
.send(&context)
.await?; .await?;
// send out federation/webmention if necessary // send out federation/webmention if necessary

View file

@ -2,6 +2,7 @@ use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_api_utils::{ use lemmy_api_utils::{
context::LemmyContext, context::LemmyContext,
notify::notify_private_message,
plugins::{plugin_hook_after, plugin_hook_before}, plugins::{plugin_hook_after, plugin_hook_before},
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::{check_private_messages_enabled, get_url_blocklist, process_markdown, slur_regex}, utils::{check_private_messages_enabled, get_url_blocklist, process_markdown, slur_regex},
@ -18,7 +19,6 @@ use lemmy_db_views_private_message::{
api::{CreatePrivateMessage, PrivateMessageResponse}, api::{CreatePrivateMessage, PrivateMessageResponse},
PrivateMessageView, PrivateMessageView,
}; };
use lemmy_email::notifications::send_private_message_email;
use lemmy_utils::{error::LemmyResult, utils::validation::is_valid_body_field}; use lemmy_utils::{error::LemmyResult, utils::validation::is_valid_body_field};
pub async fn create_private_message( pub async fn create_private_message(
@ -63,18 +63,7 @@ pub async fn create_private_message(
let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id).await?; let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id).await?;
// Send email to the local recipient, if one exists notify_private_message(&view, true, &context).await?;
if view.recipient.local {
let local_recipient =
LocalUserView::read_person(&mut context.pool(), data.recipient_id).await?;
send_private_message_email(
&local_user_view,
&local_recipient,
&content,
context.settings(),
)
.await;
}
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::CreatePrivateMessage(view.clone()), SendActivityData::CreatePrivateMessage(view.clone()),

View file

@ -3,6 +3,7 @@ use actix_web::web::Json;
use chrono::Utc; use chrono::Utc;
use lemmy_api_utils::{ use lemmy_api_utils::{
context::LemmyContext, context::LemmyContext,
notify::notify_private_message,
plugins::{plugin_hook_after, plugin_hook_before}, plugins::{plugin_hook_after, plugin_hook_before},
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::{get_url_blocklist, process_markdown, slur_regex}, utils::{get_url_blocklist, process_markdown, slur_regex},
@ -52,6 +53,8 @@ pub async fn update_private_message(
let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?; let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?;
notify_private_message(&view, false, &context).await?;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::UpdatePrivateMessage(view.clone()), SendActivityData::UpdatePrivateMessage(view.clone()),
&context, &context,

View file

@ -77,8 +77,10 @@ webpage = { version = "2.0", default-features = false, features = ["serde"] }
regex = { workspace = true } regex = { workspace = true }
jsonwebtoken = { version = "9.3.1" } jsonwebtoken = { version = "9.3.1" }
either.workspace = true either.workspace = true
derive-new.workspace = true
[dev-dependencies] [dev-dependencies]
serial_test = { workspace = true } serial_test = { workspace = true }
pretty_assertions = { workspace = true } pretty_assertions = { workspace = true }
lemmy_db_views_notification = { workspace = true, features = ["full"] }
diesel_ltree = { workspace = true } diesel_ltree = { workspace = true }

View file

@ -1,38 +1,19 @@
use crate::{ use crate::{context::LemmyContext, utils::is_mod_or_admin};
context::LemmyContext,
utils::{check_person_instance_community_block, is_mod_or_admin},
};
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::{CommentId, CommunityId, InstanceId, LocalUserId, PostId, PostOrCommentId}, newtypes::{CommentId, CommunityId, InstanceId, PostId},
source::{ source::actor_language::CommunityLanguage,
actor_language::CommunityLanguage,
comment::Comment,
comment_reply::{CommentReply, CommentReplyInsertForm},
community::Community,
person::Person,
person_comment_mention::{PersonCommentMention, PersonCommentMentionInsertForm},
person_post_mention::{PersonPostMention, PersonPostMentionInsertForm},
post::Post,
},
traits::Crud,
}; };
use lemmy_db_views_comment::{api::CommentResponse, CommentView}; use lemmy_db_views_comment::{api::CommentResponse, CommentView};
use lemmy_db_views_community::{api::CommunityResponse, CommunityView}; use lemmy_db_views_community::{api::CommunityResponse, CommunityView};
use lemmy_db_views_local_user::LocalUserView; use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_post::{api::PostResponse, PostView}; use lemmy_db_views_post::{api::PostResponse, PostView};
use lemmy_email::notifications::{ use lemmy_utils::error::LemmyResult;
send_comment_reply_email,
send_mention_email,
send_post_reply_email,
};
use lemmy_utils::{error::LemmyResult, utils::mention::MentionData};
pub async fn build_comment_response( pub async fn build_comment_response(
context: &LemmyContext, context: &LemmyContext,
comment_id: CommentId, comment_id: CommentId,
local_user_view: Option<LocalUserView>, local_user_view: Option<LocalUserView>,
recipient_ids: Vec<LocalUserId>,
local_instance_id: InstanceId, local_instance_id: InstanceId,
) -> LemmyResult<CommentResponse> { ) -> LemmyResult<CommentResponse> {
let local_user = local_user_view.map(|l| l.local_user); let local_user = local_user_view.map(|l| l.local_user);
@ -43,10 +24,7 @@ pub async fn build_comment_response(
local_instance_id, local_instance_id,
) )
.await?; .await?;
Ok(CommentResponse { Ok(CommentResponse { comment_view })
comment_view,
recipient_ids,
})
} }
pub async fn build_community_response( pub async fn build_community_response(
@ -93,221 +71,3 @@ pub async fn build_post_response(
.await?; .await?;
Ok(Json(PostResponse { post_view })) Ok(Json(PostResponse { post_view }))
} }
// TODO: this function is a mess and should be split up to handle email separately
pub async fn send_local_notifs(
mentions: Vec<MentionData>,
post_or_comment_id: PostOrCommentId,
person: &Person,
do_send_email: bool,
context: &LemmyContext,
local_user_view: Option<&LocalUserView>,
local_instance_id: InstanceId,
) -> LemmyResult<Vec<LocalUserId>> {
let mut recipient_ids = Vec::new();
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),
local_instance_id,
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),
local_instance_id,
)
.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
for mention in mentions
.iter()
.filter(|m| m.is_local(&context.settings().hostname) && m.name.ne(&person.name))
{
let mention_name = mention.name.clone();
let user_view = LocalUserView::read_from_name(&mut context.pool(), &mention_name).await;
if let Ok(mention_user_view) = user_view {
// TODO
// At some point, make it so you can't tag the parent creator either
// Potential duplication of notifications, one for reply and the other for mention, is handled
// below by checking recipient ids
recipient_ids.push(mention_user_view.local_user.id);
// Make the correct reply form depending on whether its a post or comment mention
let (link, comment_content_or_post_body) = if let Some(comment) = &comment_opt {
let person_comment_mention_form = PersonCommentMentionInsertForm {
recipient_id: mention_user_view.person.id,
comment_id: comment.id,
read: None,
};
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
PersonCommentMention::create(&mut context.pool(), &person_comment_mention_form)
.await
.ok();
(
comment.local_url(context.settings())?,
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.local_url(context.settings())?,
post.body.clone().unwrap_or_default(),
)
};
// Send an email to those local users that have notifications on
if do_send_email {
send_mention_email(
&mention_user_view,
&comment_content_or_post_body,
person,
link.into(),
context.settings(),
)
.await;
}
}
}
// Send comment_reply to the parent commenter / poster
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;
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);
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();
if do_send_email {
send_comment_reply_email(
&parent_user_view,
comment,
person,
&parent_comment,
&post,
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();
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,
};
// 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 {
send_post_reply_email(
&parent_user_view,
comment,
person,
&post,
context.settings(),
)
.await?;
}
}
}
}
}
}
Ok(recipient_ids)
}

View file

@ -1,6 +1,7 @@
pub mod build_response; pub mod build_response;
pub mod claims; pub mod claims;
pub mod context; pub mod context;
pub mod notify;
pub mod plugins; pub mod plugins;
pub mod request; pub mod request;
pub mod send_activity; pub mod send_activity;

View file

@ -0,0 +1,765 @@
use crate::context::LemmyContext;
use lemmy_db_schema::{
newtypes::PersonId,
source::{
comment::Comment,
community::{Community, CommunityActions},
instance::InstanceActions,
notification::{Notification, NotificationInsertForm},
person::{Person, PersonActions},
post::{Post, PostActions},
},
traits::{Blockable, Crud},
};
use lemmy_db_schema_file::enums::{
CommunityNotificationsMode,
NotificationTypes,
PostNotificationsMode,
};
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_private_message::PrivateMessageView;
use lemmy_db_views_site::SiteView;
use lemmy_email::notifications::{
send_community_subscribed_email,
send_mention_email,
send_post_subscribed_email,
send_private_message_email,
send_reply_email,
};
use lemmy_utils::{
error::{LemmyErrorType, LemmyResult},
utils::mention::scrape_text_for_mentions,
};
use url::Url;
#[derive(derive_new::new, Debug)]
pub struct NotifyData<'a> {
post: &'a Post,
comment_opt: Option<&'a Comment>,
creator: &'a Person,
community: &'a Community,
do_send_email: bool,
}
impl NotifyData<'_> {
/// Scans the post/comment content for mentions, then sends notifications via db and email
/// to mentioned users and parent creator.
pub async fn send(self, context: &LemmyContext) -> LemmyResult<()> {
notify_parent_creator(&self, context).await?;
notify_mentions(&self, context).await?;
notify_subscribers(&self, context).await?;
Ok(())
}
async fn check_notifications_allowed(
&self,
potential_blocker_id: PersonId,
context: &LemmyContext,
) -> LemmyResult<()> {
let pool = &mut context.pool();
// TODO: this needs too many queries for each user
PersonActions::read_block(pool, potential_blocker_id, self.post.creator_id).await?;
InstanceActions::read_block(pool, potential_blocker_id, self.community.instance_id).await?;
CommunityActions::read_block(pool, potential_blocker_id, self.post.community_id).await?;
let post_notifications = PostActions::read(pool, self.post.id, potential_blocker_id)
.await
.ok()
.and_then(|a| a.notifications)
.unwrap_or_default();
let community_notifications =
CommunityActions::read(pool, self.community.id, potential_blocker_id)
.await
.ok()
.and_then(|a| a.notifications)
.unwrap_or_default();
if post_notifications == PostNotificationsMode::Mute
|| community_notifications == CommunityNotificationsMode::Mute
{
// The specific error type is irrelevant
return Err(LemmyErrorType::NotFound.into());
}
Ok(())
}
fn content(&self) -> String {
if let Some(comment) = self.comment_opt.as_ref() {
comment.content.clone()
} else {
self.post.body.clone().unwrap_or_default()
}
}
fn link(&self, context: &LemmyContext) -> LemmyResult<Url> {
if let Some(comment) = self.comment_opt.as_ref() {
Ok(comment.local_url(context.settings())?)
} else {
Ok(self.post.local_url(context.settings())?)
}
}
async fn insert(
&self,
recipient_id: PersonId,
kind: NotificationTypes,
context: &LemmyContext,
) -> LemmyResult<Notification> {
let form = if let Some(comment) = self.comment_opt {
NotificationInsertForm::new_comment(comment.id, recipient_id, kind)
} else {
NotificationInsertForm::new_post(self.post.id, recipient_id, kind)
};
Notification::create(&mut context.pool(), &form).await
}
}
pub async fn notify_private_message(
view: &PrivateMessageView,
is_create: bool,
context: &LemmyContext,
) -> LemmyResult<()> {
let Ok(local_recipient) =
LocalUserView::read_person(&mut context.pool(), view.recipient.id).await
else {
return Ok(());
};
let form = NotificationInsertForm::new_private_message(
view.private_message.id,
local_recipient.person.id,
NotificationTypes::PrivateMessage,
);
Notification::create(&mut context.pool(), &form).await?;
if is_create {
let site_view = SiteView::read_local(&mut context.pool()).await?;
if !site_view.local_site.disable_email_notifications {
send_private_message_email(
&view.creator,
&local_recipient,
&view.private_message.content,
context.settings(),
)
.await;
}
}
Ok(())
}
async fn notify_parent_creator(data: &NotifyData<'_>, context: &LemmyContext) -> LemmyResult<()> {
let Some(comment) = data.comment_opt.as_ref() else {
return Ok(());
};
// Get the parent data
let (parent_creator_id, parent_comment) =
if let Some(parent_comment_id) = comment.parent_comment_id() {
let parent_comment = Comment::read(&mut context.pool(), parent_comment_id).await?;
(parent_comment.creator_id, Some(parent_comment))
} else {
(data.post.creator_id, None)
};
// Dont send notification to yourself
if parent_creator_id == data.creator.id {
return Ok(());
}
let is_blocked = data
.check_notifications_allowed(parent_creator_id, context)
.await
.is_err();
if is_blocked {
return Ok(());
}
let Ok(user_view) = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await
else {
return Ok(());
};
data
.insert(user_view.person.id, NotificationTypes::Reply, context)
.await?;
if data.do_send_email {
send_reply_email(
&user_view,
comment,
data.creator,
&parent_comment,
data.post,
context.settings(),
)
.await?;
}
Ok(())
}
async fn notify_mentions(data: &NotifyData<'_>, context: &LemmyContext) -> LemmyResult<()> {
let mentions = scrape_text_for_mentions(&data.content())
.into_iter()
.filter(|m| m.is_local(&context.settings().hostname) && m.name.ne(&data.creator.name));
for mention in mentions {
// Ignore error if user is remote
let Ok(user_view) = LocalUserView::read_from_name(&mut context.pool(), &mention.name).await
else {
continue;
};
let is_blocked = data
.check_notifications_allowed(user_view.person.id, context)
.await
.is_err();
if is_blocked {
continue;
};
data
.insert(user_view.person.id, NotificationTypes::Mention, context)
.await?;
// Send an email to those local users that have notifications on
if data.do_send_email {
send_mention_email(
&user_view,
&data.content(),
data.creator,
data.link(context)?.into(),
context.settings(),
)
.await;
}
}
Ok(())
}
async fn notify_subscribers(data: &NotifyData<'_>, context: &LemmyContext) -> LemmyResult<()> {
let is_post = data.comment_opt.is_none();
let subscribers = vec![
PostActions::list_subscribers(data.post.id, &mut context.pool()).await?,
CommunityActions::list_subscribers(data.post.community_id, is_post, &mut context.pool())
.await?,
]
.into_iter()
.flatten()
.collect::<Vec<_>>();
for person_id in subscribers {
if data
.check_notifications_allowed(person_id, context)
.await
.is_err()
{
continue;
};
data
.insert(person_id, NotificationTypes::Subscribed, context)
.await?;
if data.do_send_email {
let user_view = LocalUserView::read_person(&mut context.pool(), person_id).await?;
if let Some(comment) = data.comment_opt {
send_post_subscribed_email(
&user_view,
data.post,
comment,
data.link(context)?.into(),
context.settings(),
)
.await;
} else {
send_community_subscribed_email(
&user_view,
data.post,
data.community,
data.link(context)?.into(),
context.settings(),
)
.await;
}
}
}
Ok(())
}
#[cfg(test)]
#[expect(clippy::indexing_slicing)]
mod tests {
use crate::{
context::LemmyContext,
notify::{notify_private_message, NotifyData},
};
use lemmy_db_schema::{
assert_length,
source::{
comment::{Comment, CommentInsertForm},
community::{Community, CommunityInsertForm},
instance::{Instance, InstanceActions, InstanceBlockForm},
notification::{Notification, NotificationInsertForm},
person::{Person, PersonActions, PersonBlockForm, PersonInsertForm, PersonUpdateForm},
post::{Post, PostInsertForm},
private_message::{PrivateMessage, PrivateMessageInsertForm},
},
traits::{Blockable, Crud},
utils::{build_db_pool_for_tests, DbPool},
NotificationDataType,
};
use lemmy_db_schema_file::enums::NotificationTypes;
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_notification::{impls::NotificationQuery, NotificationData, NotificationView};
use lemmy_db_views_private_message::PrivateMessageView;
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
struct Data {
instance: Instance,
timmy: LocalUserView,
sara: LocalUserView,
jessica: Person,
community: Community,
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, "example.com".to_string()).await?;
let timmy = LocalUserView::create_test_user(pool, "timmy_pcv", "", false).await?;
let sara = LocalUserView::create_test_user(pool, "sara_pcv", "", false).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.person.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.person.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.person.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,
community,
timmy_post,
jessica_post,
timmy_comment,
sara_comment,
})
}
async fn insert_private_message(
form: PrivateMessageInsertForm,
context: &LemmyContext,
) -> LemmyResult<()> {
let pool = &mut context.pool();
let pm = PrivateMessage::create(pool, &form).await?;
let view = PrivateMessageView::read(pool, pm.id).await?;
notify_private_message(&view, false, context).await?;
Ok(())
}
async fn setup_private_messages(data: &Data, context: &LemmyContext) -> LemmyResult<()> {
let sara_timmy_message_form = PrivateMessageInsertForm::new(
data.sara.person.id,
data.timmy.person.id,
"sara to timmy".into(),
);
insert_private_message(sara_timmy_message_form, context).await?;
let sara_jessica_message_form = PrivateMessageInsertForm::new(
data.sara.person.id,
data.jessica.id,
"sara to jessica".into(),
);
insert_private_message(sara_jessica_message_form, context).await?;
let timmy_sara_message_form = PrivateMessageInsertForm::new(
data.timmy.person.id,
data.sara.person.id,
"timmy to sara".into(),
);
insert_private_message(timmy_sara_message_form, context).await?;
let jessica_timmy_message_form = PrivateMessageInsertForm::new(
data.jessica.id,
data.timmy.person.id,
"jessica to timmy".into(),
);
insert_private_message(jessica_timmy_message_form, context).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 context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let data = init_data(pool).await?;
// Sara replied to timmys comment
NotifyData {
post: &data.timmy_post,
comment_opt: Some(&data.sara_comment),
creator: &data.sara.person,
community: &data.community,
do_send_email: false,
}
.send(&context)
.await?;
let timmy_unread_replies =
NotificationView::get_unread_count(pool, &data.timmy.person, true).await?;
assert_eq!(1, timmy_unread_replies);
let timmy_inbox = NotificationQuery::default()
.list(pool, &data.timmy.person)
.await?;
assert_length!(1, timmy_inbox);
if let NotificationData::Comment(comment) = &timmy_inbox[0].data {
assert_eq!(data.sara_comment.id, comment.comment.id);
assert_eq!(data.timmy_post.id, comment.post.id);
assert_eq!(data.sara.person.id, comment.creator.id);
assert_eq!(
data.timmy.person.id,
timmy_inbox[0].notification.recipient_id
);
assert_eq!(NotificationTypes::Reply, timmy_inbox[0].notification.kind);
} else {
panic!("wrong type")
};
// Mark it as read
Notification::mark_read_by_id_and_person(
pool,
timmy_inbox[0].notification.id,
data.timmy.person.id,
)
.await?;
let timmy_unread_replies =
NotificationView::get_unread_count(pool, &data.timmy.person, true).await?;
assert_eq!(0, timmy_unread_replies);
let timmy_inbox_unread = NotificationQuery {
unread_only: Some(true),
..Default::default()
}
.list(pool, &data.timmy.person)
.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 = NotificationInsertForm::new_comment(
data.timmy_comment.id,
data.sara.person.id,
NotificationTypes::Mention,
);
Notification::create(pool, &timmy_mention_sara_comment_form).await?;
// Jessica mentions sara in a post
let jessica_mention_sara_post_form = NotificationInsertForm::new_post(
data.jessica_post.id,
data.sara.person.id,
NotificationTypes::Mention,
);
Notification::create(pool, &jessica_mention_sara_post_form).await?;
// Test to make sure counts and blocks work correctly
let sara_unread_mentions =
NotificationView::get_unread_count(pool, &data.sara.person, true).await?;
assert_eq!(2, sara_unread_mentions);
let sara_inbox = NotificationQuery::default()
.list(pool, &data.sara.person)
.await?;
assert_length!(2, sara_inbox);
if let NotificationData::Post(post) = &sara_inbox[0].data {
assert_eq!(data.jessica_post.id, post.post.id);
assert_eq!(data.jessica.id, post.creator.id);
} else {
panic!("wrong type")
}
assert_eq!(data.sara.person.id, sara_inbox[0].notification.recipient_id);
assert_eq!(NotificationTypes::Mention, sara_inbox[0].notification.kind);
if let NotificationData::Comment(comment) = &sara_inbox[1].data {
assert_eq!(data.timmy_comment.id, comment.comment.id);
assert_eq!(data.timmy_post.id, comment.post.id);
assert_eq!(data.timmy.person.id, comment.creator.id);
} else {
panic!("wrong type");
}
assert_eq!(data.sara.person.id, sara_inbox[1].notification.recipient_id);
assert_eq!(NotificationTypes::Mention, sara_inbox[1].notification.kind);
// Sara blocks timmy, and make sure these counts are now empty
let sara_blocks_timmy_form = PersonBlockForm::new(data.sara.person.id, data.timmy.person.id);
PersonActions::block(pool, &sara_blocks_timmy_form).await?;
let sara_unread_mentions_after_block =
NotificationView::get_unread_count(pool, &data.sara.person, true).await?;
assert_eq!(1, sara_unread_mentions_after_block);
let sara_inbox_after_block = NotificationQuery::default()
.list(pool, &data.sara.person)
.await?;
assert_length!(1, sara_inbox_after_block);
// Make sure the comment mention which timmy made is the hidden one
assert_eq!(
NotificationTypes::Mention,
sara_inbox_after_block[0].notification.kind
);
// Unblock user so we can reuse the same person
PersonActions::unblock(pool, &sara_blocks_timmy_form).await?;
// Test the type filter
let sara_inbox_mentions_only = NotificationQuery {
type_: Some(NotificationDataType::Mention),
..Default::default()
}
.list(pool, &data.sara.person)
.await?;
assert_length!(2, sara_inbox_mentions_only);
assert_eq!(
NotificationTypes::Mention,
sara_inbox_mentions_only[0].notification.kind
);
// 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 bot
let sara_unread_mentions_after_hide_bots =
NotificationView::get_unread_count(pool, &data.sara.person, false).await?;
assert_eq!(1, sara_unread_mentions_after_hide_bots);
let sara_inbox_after_hide_bots = NotificationQuery::default()
.list(pool, &data.sara.person)
.await?;
assert_length!(1, sara_inbox_after_hide_bots);
// Make sure the post mention which jessica made is the hidden one
assert_eq!(
NotificationTypes::Mention,
sara_inbox_after_hide_bots[0].notification.kind
);
// Mark them all as read
Notification::mark_all_as_read(pool, data.sara.person.id).await?;
// Make sure none come back
let sara_unread_mentions =
NotificationView::get_unread_count(pool, &data.sara.person, true).await?;
assert_eq!(0, sara_unread_mentions);
let sara_inbox_unread = NotificationQuery {
unread_only: Some(true),
..Default::default()
}
.list(pool, &data.sara.person)
.await?;
assert_length!(0, sara_inbox_unread);
cleanup(data, pool).await?;
Ok(())
}
/// Useful in combination with filter_map
fn to_pm(x: NotificationView) -> Option<PrivateMessageView> {
if let NotificationData::PrivateMessage(v) = x.data {
Some(v)
} else {
None
}
}
#[tokio::test]
#[serial]
async fn read_private_messages() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let data = init_data(pool).await?;
setup_private_messages(&data, &context).await?;
let timmy_messages: Vec<_> = NotificationQuery::default()
.list(pool, &data.timmy.person)
.await?
.into_iter()
.filter_map(to_pm)
.collect();
// 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.person.id);
assert_eq!(timmy_messages[1].creator.id, data.timmy.person.id);
assert_eq!(timmy_messages[1].recipient.id, data.sara.person.id);
assert_eq!(timmy_messages[2].creator.id, data.sara.person.id);
assert_eq!(timmy_messages[2].recipient.id, data.timmy.person.id);
let timmy_unread = NotificationView::get_unread_count(pool, &data.timmy.person, true).await?;
assert_eq!(2, timmy_unread);
let timmy_unread_messages: Vec<_> = NotificationQuery {
unread_only: Some(true),
..Default::default()
}
.list(pool, &data.timmy.person)
.await?
.into_iter()
.filter_map(to_pm)
.collect();
// 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.person.id);
assert_eq!(timmy_unread_messages[1].creator.id, data.sara.person.id);
assert_eq!(timmy_unread_messages[1].recipient.id, data.timmy.person.id);
cleanup(data, pool).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn ensure_private_message_person_block() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let data = init_data(pool).await?;
setup_private_messages(&data, &context).await?;
// Make sure blocks are working
let timmy_blocks_sara_form = PersonBlockForm::new(data.timmy.person.id, data.sara.person.id);
let inserted_block = PersonActions::block(pool, &timmy_blocks_sara_form).await?;
assert_eq!(
(data.timmy.person.id, data.sara.person.id, true),
(
inserted_block.person_id,
inserted_block.target_id,
inserted_block.blocked_at.is_some()
)
);
let timmy_messages: Vec<_> = NotificationQuery {
unread_only: Some(true),
..Default::default()
}
.list(pool, &data.timmy.person)
.await?
.into_iter()
.filter_map(to_pm)
.collect();
assert_length!(1, &timmy_messages);
let timmy_unread = NotificationView::get_unread_count(pool, &data.timmy.person, true).await?;
assert_eq!(1, timmy_unread);
cleanup(data, pool).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn ensure_private_message_instance_block() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let data = init_data(pool).await?;
setup_private_messages(&data, &context).await?;
// Make sure instance_blocks are working
let timmy_blocks_instance_form =
InstanceBlockForm::new(data.timmy.person.id, data.sara.person.instance_id);
let inserted_instance_block = InstanceActions::block(pool, &timmy_blocks_instance_form).await?;
assert_eq!(data.timmy.person.id, inserted_instance_block.person_id);
assert_eq!(
data.sara.person.instance_id,
inserted_instance_block.instance_id
);
assert!(inserted_instance_block.blocked_at.is_some());
let timmy_messages: Vec<_> = NotificationQuery {
unread_only: Some(true),
..Default::default()
}
.list(pool, &data.timmy.person)
.await?
.into_iter()
.filter_map(to_pm)
.collect();
assert_length!(0, &timmy_messages);
let timmy_unread = NotificationView::get_unread_count(pool, &data.timmy.person, true).await?;
assert_eq!(0, timmy_unread);
cleanup(data, pool).await?;
Ok(())
}
}

View file

@ -24,13 +24,13 @@ use lemmy_db_schema::{
ModRemovePostForm, ModRemovePostForm,
}, },
oauth_account::OAuthAccount, oauth_account::OAuthAccount,
person::{Person, PersonActions, PersonUpdateForm}, person::{Person, PersonUpdateForm},
post::{Post, PostActions, PostReadCommentsForm}, post::{Post, PostActions, PostReadCommentsForm},
private_message::PrivateMessage, private_message::PrivateMessage,
registration_application::RegistrationApplication, registration_application::RegistrationApplication,
site::Site, site::Site,
}, },
traits::{Blockable, Crud, Likeable}, traits::{Crud, Likeable},
utils::DbPool, utils::DbPool,
}; };
use lemmy_db_schema_file::enums::{FederationMode, RegistrationMode}; use lemmy_db_schema_file::enums::{FederationMode, RegistrationMode};
@ -321,19 +321,6 @@ pub fn check_comment_deleted_or_removed(comment: &Comment) -> LemmyResult<()> {
} }
} }
pub async fn check_person_instance_community_block(
my_id: PersonId,
potential_blocker_id: PersonId,
community_instance_id: InstanceId,
community_id: CommunityId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
PersonActions::read_block(pool, potential_blocker_id, my_id).await?;
InstanceActions::read_block(pool, potential_blocker_id, community_instance_id).await?;
CommunityActions::read_block(pool, potential_blocker_id, community_id).await?;
Ok(())
}
pub async fn check_local_vote_mode( pub async fn check_local_vote_mode(
score: i16, score: i16,
post_or_comment_id: PostOrCommentId, post_or_comment_id: PostOrCommentId,

View file

@ -15,12 +15,12 @@ name = "lemmy_apub"
path = "src/lib.rs" path = "src/lib.rs"
doctest = false doctest = false
[lints]
workspace = true
[features] [features]
full = [] full = []
[lints]
workspace = true
[dependencies] [dependencies]
lemmy_db_views_comment = { workspace = true, features = ["full"] } lemmy_db_views_comment = { workspace = true, features = ["full"] }
lemmy_db_views_community = { workspace = true, features = ["full"] } lemmy_db_views_community = { workspace = true, features = ["full"] }

View file

@ -14,8 +14,8 @@ use activitypub_federation::{
traits::{Activity, Actor, Object}, traits::{Activity, Actor, Object},
}; };
use lemmy_api_utils::{ use lemmy_api_utils::{
build_response::send_local_notifs,
context::LemmyContext, context::LemmyContext,
notify::NotifyData,
utils::{check_is_mod_or_admin, check_post_deleted_or_removed}, utils::{check_is_mod_or_admin, check_post_deleted_or_removed},
}; };
use lemmy_apub_objects::{ use lemmy_apub_objects::{
@ -27,7 +27,7 @@ use lemmy_apub_objects::{
}, },
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::{PersonId, PostOrCommentId}, newtypes::PersonId,
source::{ source::{
activity::ActivitySendTargets, activity::ActivitySendTargets,
comment::{Comment, CommentActions, CommentLikeForm}, comment::{Comment, CommentActions, CommentLikeForm},
@ -38,10 +38,7 @@ use lemmy_db_schema::{
traits::{Crud, Likeable}, traits::{Crud, Likeable},
}; };
use lemmy_db_views_site::SiteView; use lemmy_db_views_site::SiteView;
use lemmy_utils::{ use lemmy_utils::error::{LemmyError, LemmyResult};
error::{LemmyError, LemmyResult},
utils::mention::scrape_text_for_mentions,
};
use serde_json::{from_value, to_value}; use serde_json::{from_value, to_value};
use url::Url; use url::Url;
@ -137,12 +134,12 @@ impl Activity for CreateOrUpdateNote {
// Need to do this check here instead of Note::from_json because we need the person who // Need to do this check here instead of Note::from_json because we need the person who
// send the activity, not the comment author. // send the activity, not the comment author.
let existing_comment = self.object.id.dereference_local(context).await.ok(); let existing_comment = self.object.id.dereference_local(context).await.ok();
let (post, _) = self.object.get_parents(context).await?;
if let (Some(distinguished), Some(existing_comment)) = if let (Some(distinguished), Some(existing_comment)) =
(self.object.distinguished, existing_comment) (self.object.distinguished, existing_comment)
{ {
if distinguished != existing_comment.distinguished { if distinguished != existing_comment.distinguished {
let creator = self.actor.dereference(context).await?; let creator = self.actor.dereference(context).await?;
let (post, _) = self.object.get_parents(context).await?;
check_is_mod_or_admin( check_is_mod_or_admin(
&mut context.pool(), &mut context.pool(),
creator.id, creator.id,
@ -172,19 +169,10 @@ impl Activity for CreateOrUpdateNote {
// anyway. // anyway.
// TODO: for compatibility with other projects, it would be much better to read this from cc or // TODO: for compatibility with other projects, it would be much better to read this from cc or
// tags // tags
let mentions = scrape_text_for_mentions(&comment.content); let community = Community::read(&mut context.pool(), post.community_id).await?;
// TODO: this fails in local community comment as CommentView::read() returns nothing NotifyData::new(&post.0, Some(&comment.0), &actor, &community, do_send_email)
// without passing LocalUser .send(context)
send_local_notifs( .await?;
mentions,
PostOrCommentId::Comment(comment.id),
&actor,
do_send_email,
context,
None,
local_instance_id,
)
.await?;
Ok(()) Ok(())
} }
} }

View file

@ -12,7 +12,7 @@ use activitypub_federation::{
protocol::verification::{verify_domains_match, verify_urls_match}, protocol::verification::{verify_domains_match, verify_urls_match},
traits::{Activity, Object}, traits::{Activity, Object},
}; };
use lemmy_api_utils::{build_response::send_local_notifs, context::LemmyContext}; use lemmy_api_utils::{context::LemmyContext, notify::NotifyData};
use lemmy_apub_objects::{ use lemmy_apub_objects::{
objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost}, objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
utils::{ utils::{
@ -21,7 +21,7 @@ use lemmy_apub_objects::{
}, },
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::{PersonId, PostOrCommentId}, newtypes::PersonId,
source::{ source::{
activity::ActivitySendTargets, activity::ActivitySendTargets,
community::Community, community::Community,
@ -31,10 +31,7 @@ use lemmy_db_schema::{
traits::{Crud, Likeable}, traits::{Crud, Likeable},
}; };
use lemmy_db_views_site::SiteView; use lemmy_db_views_site::SiteView;
use lemmy_utils::{ use lemmy_utils::error::{LemmyError, LemmyResult};
error::{LemmyError, LemmyResult},
utils::mention::scrape_text_for_mentions,
};
use url::Url; use url::Url;
impl CreateOrUpdatePage { impl CreateOrUpdatePage {
@ -110,7 +107,6 @@ impl Activity for CreateOrUpdatePage {
async fn receive(self, context: &Data<LemmyContext>) -> LemmyResult<()> { async fn receive(self, context: &Data<LemmyContext>) -> LemmyResult<()> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool()).await?;
let local_instance_id = site_view.site.instance_id;
let post = ApubPost::from_json(self.object, context).await?; let post = ApubPost::from_json(self.object, context).await?;
@ -125,18 +121,10 @@ impl Activity for CreateOrUpdatePage {
self.kind == CreateOrUpdateType::Create && !site_view.local_site.disable_email_notifications; self.kind == CreateOrUpdateType::Create && !site_view.local_site.disable_email_notifications;
let actor = self.actor.dereference(context).await?; let actor = self.actor.dereference(context).await?;
// Send the post body mentions let community = Community::read(&mut context.pool(), post.community_id).await?;
let mentions = scrape_text_for_mentions(&post.body.clone().unwrap_or_default()); NotifyData::new(&post.0, None, &actor, &community, do_send_email)
send_local_notifs( .send(context)
mentions, .await?;
PostOrCommentId::Post(post.id),
&actor,
do_send_email,
context,
None,
local_instance_id,
)
.await?;
Ok(()) Ok(())
} }

View file

@ -15,17 +15,18 @@ name = "lemmy_apub_objects"
path = "src/lib.rs" path = "src/lib.rs"
doctest = false doctest = false
[lints]
workspace = true
[features] [features]
full = [] full = []
[lints]
workspace = true
[dependencies] [dependencies]
lemmy_db_views_community_moderator = { workspace = true, features = ["full"] } lemmy_db_views_community_moderator = { workspace = true, features = ["full"] }
lemmy_db_views_community_person_ban = { workspace = true, features = ["full"] } lemmy_db_views_community_person_ban = { workspace = true, features = ["full"] }
lemmy_db_views_local_user = { workspace = true, features = ["full"] } lemmy_db_views_local_user = { workspace = true, features = ["full"] }
lemmy_db_views_site = { workspace = true, features = ["full"] } lemmy_db_views_site = { workspace = true, features = ["full"] }
lemmy_db_views_private_message = { workspace = true, features = ["full"] }
lemmy_utils = { workspace = true, features = ["full"] } lemmy_utils = { workspace = true, features = ["full"] }
lemmy_db_schema = { workspace = true, features = ["full"] } lemmy_db_schema = { workspace = true, features = ["full"] }
lemmy_api_utils = { workspace = true, features = ["full"] } lemmy_api_utils = { workspace = true, features = ["full"] }

View file

@ -17,6 +17,7 @@ use activitypub_federation::{
use chrono::Utc; use chrono::Utc;
use lemmy_api_utils::{ use lemmy_api_utils::{
context::LemmyContext, context::LemmyContext,
notify::notify_private_message,
plugins::{plugin_hook_after, plugin_hook_before}, plugins::{plugin_hook_after, plugin_hook_before},
utils::{check_private_messages_enabled, get_url_blocklist, process_markdown, slur_regex}, utils::{check_private_messages_enabled, get_url_blocklist, process_markdown, slur_regex},
}; };
@ -29,6 +30,7 @@ use lemmy_db_schema::{
traits::{Blockable, Crud}, traits::{Blockable, Crud},
}; };
use lemmy_db_views_local_user::LocalUserView; use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_private_message::PrivateMessageView;
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyError, LemmyErrorType, LemmyResult}, error::{LemmyError, LemmyErrorType, LemmyResult},
utils::markdown::markdown_to_html, utils::markdown::markdown_to_html,
@ -158,7 +160,6 @@ impl Object for ApubPrivateMessage {
published_at: note.published, published_at: note.published,
updated_at: note.updated, updated_at: note.updated,
deleted: Some(false), deleted: Some(false),
read: None,
ap_id: Some(note.id.into()), ap_id: Some(note.id.into()),
local: Some(false), local: Some(false),
}; };
@ -166,6 +167,8 @@ impl Object for ApubPrivateMessage {
let timestamp = note.updated.or(note.published).unwrap_or_else(Utc::now); let timestamp = note.updated.or(note.published).unwrap_or_else(Utc::now);
let pm = DbPrivateMessage::insert_apub(&mut context.pool(), timestamp, &form).await?; let pm = DbPrivateMessage::insert_apub(&mut context.pool(), timestamp, &form).await?;
plugin_hook_after("after_receive_federated_private_message", &pm)?; plugin_hook_after("after_receive_federated_private_message", &pm)?;
let view = PrivateMessageView::read(&mut context.pool(), pm.id).await?;
notify_private_message(&view, pm.updated_at.is_none(), context).await?;
Ok(pm.into()) Ok(pm.into())
} }
} }

View file

@ -1,94 +0,0 @@
use crate::{
diesel::OptionalExtension,
newtypes::{CommentId, CommentReplyId, PersonId},
source::comment_reply::{CommentReply, CommentReplyInsertForm, CommentReplyUpdateForm},
traits::Crud,
utils::{get_conn, DbPool},
};
use diesel::{dsl::insert_into, ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
use lemmy_db_schema_file::schema::comment_reply;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
impl Crud for CommentReply {
type InsertForm = CommentReplyInsertForm;
type UpdateForm = CommentReplyUpdateForm;
type IdType = CommentReplyId;
async fn create(
pool: &mut DbPool<'_>,
comment_reply_form: &Self::InsertForm,
) -> LemmyResult<Self> {
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(comment_reply::table)
.values(comment_reply_form)
.on_conflict((comment_reply::recipient_id, comment_reply::comment_id))
.do_update()
.set(comment_reply_form)
.get_result::<Self>(conn)
.await
.with_lemmy_type(LemmyErrorType::CouldntCreateCommentReply)
}
async fn update(
pool: &mut DbPool<'_>,
comment_reply_id: CommentReplyId,
comment_reply_form: &Self::UpdateForm,
) -> LemmyResult<Self> {
let conn = &mut get_conn(pool).await?;
diesel::update(comment_reply::table.find(comment_reply_id))
.set(comment_reply_form)
.get_result::<Self>(conn)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateCommentReply)
}
}
impl CommentReply {
pub async fn mark_all_as_read(
pool: &mut DbPool<'_>,
for_recipient_id: PersonId,
) -> LemmyResult<Vec<CommentReply>> {
let conn = &mut get_conn(pool).await?;
diesel::update(
comment_reply::table
.filter(comment_reply::recipient_id.eq(for_recipient_id))
.filter(comment_reply::read.eq(false)),
)
.set(comment_reply::read.eq(true))
.get_results::<Self>(conn)
.await
.with_lemmy_type(LemmyErrorType::CouldntMarkCommentReplyAsRead)
}
pub async fn read_by_comment(
pool: &mut DbPool<'_>,
for_comment_id: CommentId,
) -> LemmyResult<Option<Self>> {
let conn = &mut get_conn(pool).await?;
comment_reply::table
.filter(comment_reply::comment_id.eq(for_comment_id))
.first(conn)
.await
.optional()
.with_lemmy_type(LemmyErrorType::NotFound)
}
pub async fn read_by_comment_and_person(
pool: &mut DbPool<'_>,
for_comment_id: CommentId,
for_recipient_id: PersonId,
) -> LemmyResult<Option<Self>> {
let conn = &mut get_conn(pool).await?;
comment_reply::table
.filter(comment_reply::comment_id.eq(for_comment_id))
.filter(comment_reply::recipient_id.eq(for_recipient_id))
.first(conn)
.await
.optional()
.with_lemmy_type(LemmyErrorType::NotFound)
}
}

View file

@ -37,8 +37,8 @@ use diesel::{
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use diesel_uplete::{uplete, UpleteCount}; use diesel_uplete::{uplete, UpleteCount};
use lemmy_db_schema_file::{ use lemmy_db_schema_file::{
enums::{CommunityFollowerState, CommunityVisibility, ListingType}, enums::{CommunityFollowerState, CommunityNotificationsMode, CommunityVisibility, ListingType},
schema::{comment, community, community_actions, instance, post}, schema::{comment, community, community_actions, instance, local_user, post},
}; };
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
@ -455,6 +455,61 @@ impl CommunityActions {
.await .await
.map_err(|_e: Arc<LemmyError>| LemmyErrorType::NotFound.into()) .map_err(|_e: Arc<LemmyError>| LemmyErrorType::NotFound.into())
} }
pub async fn update_notification_state(
community_id: CommunityId,
person_id: PersonId,
new_state: CommunityNotificationsMode,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?;
let form = (
community_actions::person_id.eq(person_id),
community_actions::community_id.eq(community_id),
community_actions::notifications.eq(new_state),
);
insert_into(community_actions::table)
.values(form.clone())
.on_conflict((
community_actions::person_id,
community_actions::community_id,
))
.do_update()
.set(form)
.execute(conn)
.await?;
Ok(())
}
pub async fn list_subscribers(
community_id: CommunityId,
is_post: bool,
pool: &mut DbPool<'_>,
) -> LemmyResult<Vec<PersonId>> {
let conn = &mut get_conn(pool).await?;
let mut query = community_actions::table
.inner_join(local_user::table.on(community_actions::person_id.eq(local_user::person_id)))
.filter(community_actions::community_id.eq(community_id))
.select(local_user::person_id)
.into_boxed();
if is_post {
query = query.filter(
community_actions::notifications
.eq(CommunityNotificationsMode::AllPosts)
.or(community_actions::notifications.eq(CommunityNotificationsMode::AllPostsAndComments)),
);
} else {
query = query.filter(
community_actions::notifications.eq(CommunityNotificationsMode::AllPostsAndComments),
);
}
query
.get_results(conn)
.await
.with_lemmy_type(LemmyErrorType::NotFound)
}
} }
impl Bannable for CommunityActions { impl Bannable for CommunityActions {

View file

@ -2,7 +2,6 @@ pub mod activity;
pub mod actor_language; pub mod actor_language;
pub mod captcha_answer; pub mod captcha_answer;
pub mod comment; pub mod comment;
pub mod comment_reply;
pub mod comment_report; pub mod comment_report;
pub mod community; pub mod community;
pub mod community_report; pub mod community_report;
@ -22,12 +21,11 @@ pub mod local_user;
pub mod login_token; pub mod login_token;
pub mod mod_log; pub mod mod_log;
pub mod multi_community; pub mod multi_community;
pub mod notification;
pub mod oauth_account; pub mod oauth_account;
pub mod oauth_provider; pub mod oauth_provider;
pub mod password_reset_request; pub mod password_reset_request;
pub mod person; pub mod person;
pub mod person_comment_mention;
pub mod person_post_mention;
pub mod post; pub mod post;
pub mod post_report; pub mod post_report;
pub mod post_tag; pub mod post_tag;

View file

@ -0,0 +1,69 @@
use crate::{
newtypes::{CommentId, NotificationId, PersonId},
source::notification::{Notification, NotificationInsertForm},
utils::{get_conn, DbPool},
};
use diesel::{
dsl::{insert_into, update},
ExpressionMethods,
QueryDsl,
};
use diesel_async::RunQueryDsl;
use lemmy_db_schema_file::schema::notification;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
impl Notification {
pub async fn create(pool: &mut DbPool<'_>, form: &NotificationInsertForm) -> LemmyResult<Self> {
let conn = &mut get_conn(pool).await?;
insert_into(notification::table)
.values(form)
.get_result::<Self>(conn)
.await
.with_lemmy_type(LemmyErrorType::CouldntCreateNotification)
}
pub async fn read_by_comment_id(
pool: &mut DbPool<'_>,
comment_id: CommentId,
) -> LemmyResult<Self> {
let conn = &mut get_conn(pool).await?;
notification::table
.filter(notification::comment_id.eq(comment_id))
.get_result::<Self>(conn)
.await
.with_lemmy_type(LemmyErrorType::NotFound)
}
pub async fn mark_all_as_read(
pool: &mut DbPool<'_>,
for_recipient_id: PersonId,
) -> LemmyResult<usize> {
let conn = &mut get_conn(pool).await?;
diesel::update(
notification::table
.filter(notification::recipient_id.eq(for_recipient_id))
.filter(notification::read.eq(false)),
)
.set(notification::read.eq(true))
.execute(conn)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateNotification)
}
pub async fn mark_read_by_id_and_person(
pool: &mut DbPool<'_>,
notification_id: NotificationId,
for_recipient_id: PersonId,
) -> LemmyResult<usize> {
let conn = &mut get_conn(pool).await?;
update(
notification::table
.filter(notification::id.eq(notification_id))
.filter(notification::recipient_id.eq(for_recipient_id)),
)
.set(notification::read.eq(true))
.execute(conn)
.await
.with_lemmy_type(LemmyErrorType::NotFound)
}
}

View file

@ -1,84 +0,0 @@
use crate::{
diesel::OptionalExtension,
newtypes::{CommentId, PersonCommentMentionId, PersonId},
source::person_comment_mention::{
PersonCommentMention,
PersonCommentMentionInsertForm,
PersonCommentMentionUpdateForm,
},
traits::Crud,
utils::{get_conn, DbPool},
};
use diesel::{dsl::insert_into, ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
use lemmy_db_schema_file::schema::person_comment_mention;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
impl Crud for PersonCommentMention {
type InsertForm = PersonCommentMentionInsertForm;
type UpdateForm = PersonCommentMentionUpdateForm;
type IdType = PersonCommentMentionId;
async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> LemmyResult<Self> {
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(form)
.on_conflict((
person_comment_mention::recipient_id,
person_comment_mention::comment_id,
))
.do_update()
.set(form)
.get_result::<Self>(conn)
.await
.with_lemmy_type(LemmyErrorType::CouldntCreatePersonCommentMention)
}
async fn update(
pool: &mut DbPool<'_>,
person_comment_mention_id: PersonCommentMentionId,
person_comment_mention_form: &Self::UpdateForm,
) -> LemmyResult<Self> {
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
.with_lemmy_type(LemmyErrorType::CouldntUpdatePersonCommentMention)
}
}
impl PersonCommentMention {
pub async fn mark_all_as_read(
pool: &mut DbPool<'_>,
for_recipient_id: PersonId,
) -> LemmyResult<Vec<PersonCommentMention>> {
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
.with_lemmy_type(LemmyErrorType::CouldntUpdatePersonCommentMention)
}
pub async fn read_by_comment_and_person(
pool: &mut DbPool<'_>,
for_comment_id: CommentId,
for_recipient_id: PersonId,
) -> LemmyResult<Option<Self>> {
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()
.with_lemmy_type(LemmyErrorType::NotFound)
}
}

View file

@ -1,87 +0,0 @@
use crate::{
diesel::OptionalExtension,
newtypes::{PersonId, PersonPostMentionId, PostId},
source::person_post_mention::{
PersonPostMention,
PersonPostMentionInsertForm,
PersonPostMentionUpdateForm,
},
traits::Crud,
utils::{get_conn, DbPool},
};
use diesel::{dsl::insert_into, ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
use lemmy_db_schema_file::schema::person_post_mention;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
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,
) -> LemmyResult<Self> {
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
.with_lemmy_type(LemmyErrorType::CouldntCreatePersonPostMention)
}
async fn update(
pool: &mut DbPool<'_>,
person_post_mention_id: PersonPostMentionId,
person_post_mention_form: &Self::UpdateForm,
) -> LemmyResult<Self> {
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
.with_lemmy_type(LemmyErrorType::CouldntUpdatePersonPostMention)
}
}
impl PersonPostMention {
pub async fn mark_all_as_read(
pool: &mut DbPool<'_>,
for_recipient_id: PersonId,
) -> LemmyResult<Vec<Self>> {
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
.with_lemmy_type(LemmyErrorType::CouldntUpdatePersonPostMention)
}
pub async fn read_by_post_and_person(
pool: &mut DbPool<'_>,
for_post_id: PostId,
for_recipient_id: PersonId,
) -> LemmyResult<Option<Self>> {
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()
.with_lemmy_type(LemmyErrorType::NotFound)
}
}

View file

@ -24,7 +24,6 @@ use crate::{
SITEMAP_LIMIT, SITEMAP_LIMIT,
}, },
}; };
use ::url::Url;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use diesel::{ use diesel::{
dsl::{count, insert_into, not, update}, dsl::{count, insert_into, not, update},
@ -39,11 +38,15 @@ use diesel::{
}; };
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use diesel_uplete::{uplete, UpleteCount}; use diesel_uplete::{uplete, UpleteCount};
use lemmy_db_schema_file::schema::{community, person, post, post_actions}; use lemmy_db_schema_file::{
enums::PostNotificationsMode,
schema::{community, local_user, person, post, post_actions},
};
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult}, error::{LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult},
settings::structs::Settings, settings::structs::Settings,
}; };
use url::Url;
impl Crud for Post { impl Crud for Post {
type InsertForm = PostInsertForm; type InsertForm = PostInsertForm;
@ -542,9 +545,7 @@ impl PostActions {
.map(|post_id| (PostReadForm::new(*post_id, person_id))) .map(|post_id| (PostReadForm::new(*post_id, person_id)))
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
}
impl PostActions {
pub async fn read( pub async fn read(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
post_id: PostId, post_id: PostId,
@ -570,6 +571,45 @@ impl PostActions {
.ok_or(LemmyErrorType::CouldntParsePaginationToken)?; .ok_or(LemmyErrorType::CouldntParsePaginationToken)?;
Self::read(pool, PostId(*post_id), PersonId(*person_id)).await Self::read(pool, PostId(*post_id), PersonId(*person_id)).await
} }
pub async fn update_notification_state(
post_id: PostId,
person_id: PersonId,
new_state: PostNotificationsMode,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?;
let form = (
post_actions::person_id.eq(person_id),
post_actions::post_id.eq(post_id),
post_actions::notifications.eq(new_state),
);
insert_into(post_actions::table)
.values(form.clone())
.on_conflict((post_actions::person_id, post_actions::post_id))
.do_update()
.set(form)
.execute(conn)
.await?;
Ok(())
}
pub async fn list_subscribers(
post_id: PostId,
pool: &mut DbPool<'_>,
) -> LemmyResult<Vec<PersonId>> {
let conn = &mut get_conn(pool).await?;
post_actions::table
.inner_join(local_user::table.on(post_actions::person_id.eq(local_user::person_id)))
.filter(post_actions::post_id.eq(post_id))
.filter(post_actions::notifications.eq(PostNotificationsMode::AllComments))
.select(local_user::person_id)
.get_results(conn)
.await
.with_lemmy_type(LemmyErrorType::NotFound)
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -63,22 +63,6 @@ impl PrivateMessage {
.with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage) .with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage)
} }
pub async fn mark_all_as_read(
pool: &mut DbPool<'_>,
for_recipient_id: PersonId,
) -> LemmyResult<Vec<Self>> {
let conn = &mut get_conn(pool).await?;
diesel::update(
private_message::table
.filter(private_message::recipient_id.eq(for_recipient_id))
.filter(private_message::read.eq(false)),
)
.set(private_message::read.eq(true))
.get_results::<Self>(conn)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)
}
pub async fn read_from_apub_id( pub async fn read_from_apub_id(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
object_id: Url, object_id: Url,
@ -161,7 +145,6 @@ mod tests {
creator_id: inserted_creator.id, creator_id: inserted_creator.id,
recipient_id: inserted_recipient.id, recipient_id: inserted_recipient.id,
deleted: false, deleted: false,
read: false,
updated_at: None, updated_at: None,
published_at: inserted_private_message.published_at, published_at: inserted_private_message.published_at,
ap_id: Url::parse(&format!( ap_id: Url::parse(&format!(
@ -195,15 +178,6 @@ mod tests {
}, },
) )
.await?; .await?;
let marked_read_private_message = PrivateMessage::update(
pool,
inserted_private_message.id,
&PrivateMessageUpdateForm {
read: Some(true),
..Default::default()
},
)
.await?;
Person::delete(pool, inserted_creator.id).await?; Person::delete(pool, inserted_creator.id).await?;
Person::delete(pool, inserted_recipient.id).await?; Person::delete(pool, inserted_recipient.id).await?;
Instance::delete(pool, inserted_instance.id).await?; Instance::delete(pool, inserted_instance.id).await?;
@ -212,7 +186,6 @@ mod tests {
assert_eq!(expected_private_message, updated_private_message); assert_eq!(expected_private_message, updated_private_message);
assert_eq!(expected_private_message, inserted_private_message); assert_eq!(expected_private_message, inserted_private_message);
assert!(deleted_private_message.deleted); assert!(deleted_private_message.deleted);
assert!(marked_read_private_message.read);
Ok(()) Ok(())
} }

View file

@ -117,12 +117,12 @@ pub enum ModlogActionType {
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] #[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export))] #[cfg_attr(feature = "ts-rs", ts(export))]
/// A list of possible types for the inbox. /// A list of possible types for the inbox.
pub enum InboxDataType { pub enum NotificationDataType {
All, All,
CommentReply, Reply,
CommentMention, Mention,
PostMention,
PrivateMessage, PrivateMessage,
Subscribed,
} }
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] #[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]

View file

@ -93,15 +93,7 @@ impl fmt::Display for PrivateMessageId {
#[cfg_attr(feature = "full", derive(DieselNewType))] #[cfg_attr(feature = "full", derive(DieselNewType))]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] #[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))] #[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// The person comment mention id. pub struct NotificationId(pub i32);
pub struct PersonCommentMentionId(pub i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "full", derive(DieselNewType))]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// The person post mention id.
pub struct PersonPostMentionId(pub i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "full", derive(DieselNewType))] #[cfg_attr(feature = "full", derive(DieselNewType))]
@ -145,13 +137,6 @@ pub struct SiteId(pub i32);
/// The language id. /// The language id.
pub struct LanguageId(pub i32); pub struct LanguageId(pub i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "full", derive(DieselNewType))]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// The comment reply id.
pub struct CommentReplyId(pub i32);
#[derive( #[derive(
Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default, Ord, PartialOrd, Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default, Ord, PartialOrd,
)] )]
@ -242,13 +227,6 @@ pub struct PersonLikedCombinedId(i32);
#[cfg_attr(feature = "full", derive(DieselNewType))] #[cfg_attr(feature = "full", derive(DieselNewType))]
pub struct ModlogCombinedId(i32); pub struct ModlogCombinedId(i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "full", derive(DieselNewType))]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export))]
/// The inbox combined id
pub struct InboxCombinedId(i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "full", derive(DieselNewType))] #[cfg_attr(feature = "full", derive(DieselNewType))]
/// The search combined id /// The search combined id

View file

@ -1,35 +0,0 @@
use crate::newtypes::{
CommentReplyId,
InboxCombinedId,
PersonCommentMentionId,
PersonPostMentionId,
PrivateMessageId,
};
use chrono::{DateTime, Utc};
#[cfg(feature = "full")]
use i_love_jesus::CursorKeysModule;
#[cfg(feature = "full")]
use lemmy_db_schema_file::schema::inbox_combined;
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))]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// A combined inbox table.
pub struct InboxCombined {
pub id: InboxCombinedId,
pub published_at: 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,4 +1,3 @@
pub mod inbox;
pub mod modlog; pub mod modlog;
pub mod person_content; pub mod person_content;
pub mod person_liked; pub mod person_liked;

View file

@ -1,38 +0,0 @@
use crate::newtypes::{CommentId, CommentReplyId, PersonId};
use chrono::{DateTime, Utc};
#[cfg(feature = "full")]
use lemmy_db_schema_file::schema::comment_reply;
use serde::{Deserialize, Serialize};
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[cfg_attr(
feature = "full",
derive(Queryable, Selectable, Associations, Identifiable)
)]
#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::comment::Comment)))]
#[cfg_attr(feature = "full", diesel(table_name = comment_reply))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// A comment reply.
pub struct CommentReply {
pub id: CommentReplyId,
pub recipient_id: PersonId,
pub comment_id: CommentId,
pub read: bool,
pub published_at: DateTime<Utc>,
}
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = comment_reply))]
pub struct CommentReplyInsertForm {
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 = comment_reply))]
pub struct CommentReplyUpdateForm {
pub read: Option<bool>,
}

View file

@ -4,7 +4,11 @@ use crate::{
source::placeholder_apub_url, source::placeholder_apub_url,
}; };
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use lemmy_db_schema_file::enums::{CommunityFollowerState, CommunityVisibility}; use lemmy_db_schema_file::enums::{
CommunityFollowerState,
CommunityNotificationsMode,
CommunityVisibility,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
#[cfg(feature = "full")] #[cfg(feature = "full")]
@ -210,6 +214,7 @@ pub struct CommunityActions {
pub received_ban_at: Option<DateTime<Utc>>, pub received_ban_at: Option<DateTime<Utc>>,
/// When their ban expires. /// When their ban expires.
pub ban_expires_at: Option<DateTime<Utc>>, pub ban_expires_at: Option<DateTime<Utc>>,
pub notifications: Option<CommunityNotificationsMode>,
} }
#[derive(Clone, derive_new::new)] #[derive(Clone, derive_new::new)]

View file

@ -7,7 +7,6 @@ pub mod actor_language;
pub mod captcha_answer; pub mod captcha_answer;
pub mod combined; pub mod combined;
pub mod comment; pub mod comment;
pub mod comment_reply;
pub mod comment_report; pub mod comment_report;
pub mod community; pub mod community;
pub mod community_report; pub mod community_report;
@ -28,12 +27,11 @@ pub mod local_user;
pub mod login_token; pub mod login_token;
pub mod mod_log; pub mod mod_log;
pub mod multi_community; pub mod multi_community;
pub mod notification;
pub mod oauth_account; pub mod oauth_account;
pub mod oauth_provider; pub mod oauth_provider;
pub mod password_reset_request; pub mod password_reset_request;
pub mod person; pub mod person;
pub mod person_comment_mention;
pub mod person_post_mention;
pub mod post; pub mod post;
pub mod post_report; pub mod post_report;
pub mod post_tag; pub mod post_tag;

View file

@ -0,0 +1,77 @@
use crate::newtypes::{CommentId, NotificationId, PersonId, PostId, PrivateMessageId};
use chrono::{DateTime, Utc};
#[cfg(feature = "full")]
use i_love_jesus::CursorKeysModule;
use lemmy_db_schema_file::enums::NotificationTypes;
#[cfg(feature = "full")]
use lemmy_db_schema_file::schema::notification;
use serde::{Deserialize, Serialize};
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[cfg_attr(
feature = "full",
derive(Queryable, Selectable, Identifiable, CursorKeysModule)
)]
#[cfg_attr(feature = "full", diesel(table_name = notification))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
#[cfg_attr(feature = "full", cursor_keys_module(name = notification_keys))]
pub struct Notification {
pub id: NotificationId,
pub recipient_id: PersonId,
pub comment_id: Option<CommentId>,
pub read: bool,
pub published_at: DateTime<Utc>,
pub kind: NotificationTypes,
pub post_id: Option<PostId>,
pub private_message_id: Option<PrivateMessageId>,
}
#[cfg_attr(feature = "full", derive(Insertable))]
#[cfg_attr(feature = "full", diesel(table_name = notification))]
pub struct NotificationInsertForm {
pub recipient_id: PersonId,
pub comment_id: Option<CommentId>,
pub kind: NotificationTypes,
pub post_id: Option<PostId>,
pub private_message_id: Option<PrivateMessageId>,
}
impl NotificationInsertForm {
pub fn new_post(post_id: PostId, recipient_id: PersonId, kind: NotificationTypes) -> Self {
Self {
post_id: Some(post_id),
comment_id: None,
private_message_id: None,
recipient_id,
kind,
}
}
pub fn new_comment(
comment_id: CommentId,
recipient_id: PersonId,
kind: NotificationTypes,
) -> Self {
Self {
post_id: None,
comment_id: Some(comment_id),
private_message_id: None,
recipient_id,
kind,
}
}
pub fn new_private_message(
private_message_id: PrivateMessageId,
recipient_id: PersonId,
kind: NotificationTypes,
) -> Self {
Self {
post_id: None,
comment_id: None,
private_message_id: Some(private_message_id),
recipient_id,
kind,
}
}
}

View file

@ -1,38 +0,0 @@
use crate::newtypes::{CommentId, PersonCommentMentionId, PersonId};
use chrono::{DateTime, Utc};
#[cfg(feature = "full")]
use lemmy_db_schema_file::schema::person_comment_mention;
use serde::{Deserialize, Serialize};
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[cfg_attr(
feature = "full",
derive(Queryable, Selectable, Associations, Identifiable)
)]
#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::comment::Comment)))]
#[cfg_attr(feature = "full", diesel(table_name = person_comment_mention))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// A person mention.
pub struct PersonCommentMention {
pub id: PersonCommentMentionId,
pub recipient_id: PersonId,
pub comment_id: CommentId,
pub read: bool,
pub published_at: DateTime<Utc>,
}
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[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_comment_mention))]
pub struct PersonCommentMentionUpdateForm {
pub read: Option<bool>,
}

View file

@ -1,38 +0,0 @@
use crate::newtypes::{PersonId, PersonPostMentionId, PostId};
use chrono::{DateTime, Utc};
#[cfg(feature = "full")]
use lemmy_db_schema_file::schema::person_post_mention;
use serde::{Deserialize, Serialize};
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[cfg_attr(
feature = "full",
derive(Queryable, Selectable, Associations, Identifiable)
)]
#[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 = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// A person mention.
pub struct PersonPostMention {
pub id: PersonPostMentionId,
pub recipient_id: PersonId,
pub post_id: PostId,
pub read: bool,
pub published_at: 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

@ -1,5 +1,6 @@
use crate::newtypes::{CommunityId, DbUrl, LanguageId, PersonId, PostId}; use crate::newtypes::{CommunityId, DbUrl, LanguageId, PersonId, PostId};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use lemmy_db_schema_file::enums::PostNotificationsMode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
#[cfg(feature = "full")] #[cfg(feature = "full")]
@ -201,6 +202,7 @@ pub struct PostActions {
pub like_score: Option<i16>, pub like_score: Option<i16>,
/// When the post was hidden. /// When the post was hidden.
pub hidden_at: Option<DateTime<Utc>>, pub hidden_at: Option<DateTime<Utc>>,
pub notifications: Option<PostNotificationsMode>,
} }
#[derive(Clone, derive_new::new)] #[derive(Clone, derive_new::new)]

View file

@ -26,7 +26,6 @@ pub struct PrivateMessage {
pub recipient_id: PersonId, pub recipient_id: PersonId,
pub content: String, pub content: String,
pub deleted: bool, pub deleted: bool,
pub read: bool,
pub published_at: DateTime<Utc>, pub published_at: DateTime<Utc>,
pub updated_at: Option<DateTime<Utc>>, pub updated_at: Option<DateTime<Utc>>,
pub ap_id: DbUrl, pub ap_id: DbUrl,
@ -47,8 +46,6 @@ pub struct PrivateMessageInsertForm {
#[new(default)] #[new(default)]
pub deleted: Option<bool>, pub deleted: Option<bool>,
#[new(default)] #[new(default)]
pub read: Option<bool>,
#[new(default)]
pub published_at: Option<DateTime<Utc>>, pub published_at: Option<DateTime<Utc>>,
#[new(default)] #[new(default)]
pub updated_at: Option<DateTime<Utc>>, pub updated_at: Option<DateTime<Utc>>,
@ -64,7 +61,6 @@ pub struct PrivateMessageInsertForm {
pub struct PrivateMessageUpdateForm { pub struct PrivateMessageUpdateForm {
pub content: Option<String>, pub content: Option<String>,
pub deleted: Option<bool>, pub deleted: Option<bool>,
pub read: Option<bool>,
pub published_at: Option<DateTime<Utc>>, pub published_at: Option<DateTime<Utc>>,
pub updated_at: Option<Option<DateTime<Utc>>>, pub updated_at: Option<Option<DateTime<Utc>>>,
pub ap_id: Option<DbUrl>, pub ap_id: Option<DbUrl>,

View file

@ -226,3 +226,59 @@ pub enum VoteShow {
ShowForOthers, ShowForOthers,
Hide, Hide,
} }
#[derive(
EnumString, Display, Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Hash,
)]
#[cfg_attr(feature = "full", derive(DbEnum))]
#[cfg_attr(
feature = "full",
ExistingTypePath = "crate::schema::sql_types::PostNotificationsModeEnum"
)]
#[cfg_attr(feature = "full", DbValueStyle = "verbatim")]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export))]
/// Available settings for post notifications
pub enum PostNotificationsMode {
AllComments,
#[default]
RepliesAndMentions,
Mute,
}
#[derive(
EnumString, Display, Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Hash,
)]
#[cfg_attr(feature = "full", derive(DbEnum))]
#[cfg_attr(
feature = "full",
ExistingTypePath = "crate::schema::sql_types::CommunityNotificationsModeEnum"
)]
#[cfg_attr(feature = "full", DbValueStyle = "verbatim")]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export))]
/// Available settings for community notifications
pub enum CommunityNotificationsMode {
AllPostsAndComments,
AllPosts,
#[default]
RepliesAndMentions,
Mute,
}
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(DbEnum))]
#[cfg_attr(
feature = "full",
ExistingTypePath = "crate::schema::sql_types::NotificationTypeEnum"
)]
#[cfg_attr(feature = "full", DbValueStyle = "verbatim")]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export))]
/// Types of notifications which can be received in inbox
pub enum NotificationTypes {
Mention,
Reply,
Subscribed,
PrivateMessage,
}

View file

@ -13,6 +13,10 @@ pub mod sql_types {
#[diesel(postgres_type(name = "community_follower_state"))] #[diesel(postgres_type(name = "community_follower_state"))]
pub struct CommunityFollowerState; pub struct CommunityFollowerState;
#[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "community_notifications_mode_enum"))]
pub struct CommunityNotificationsModeEnum;
#[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "community_visibility"))] #[diesel(postgres_type(name = "community_visibility"))]
pub struct CommunityVisibility; pub struct CommunityVisibility;
@ -29,10 +33,18 @@ pub mod sql_types {
#[diesel(postgres_type(name = "ltree"))] #[diesel(postgres_type(name = "ltree"))]
pub struct Ltree; pub struct Ltree;
#[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "notification_type_enum"))]
pub struct NotificationTypeEnum;
#[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "post_listing_mode_enum"))] #[diesel(postgres_type(name = "post_listing_mode_enum"))]
pub struct PostListingModeEnum; pub struct PostListingModeEnum;
#[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "post_notifications_mode_enum"))]
pub struct PostNotificationsModeEnum;
#[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "post_sort_type_enum"))] #[diesel(postgres_type(name = "post_sort_type_enum"))]
pub struct PostSortTypeEnum; pub struct PostSortTypeEnum;
@ -156,16 +168,6 @@ diesel::table! {
} }
} }
diesel::table! {
comment_reply (id) {
id -> Int4,
recipient_id -> Int4,
comment_id -> Int4,
read -> Bool,
published_at -> Timestamptz,
}
}
diesel::table! { diesel::table! {
comment_report (id) { comment_report (id) {
id -> Int4, id -> Int4,
@ -238,6 +240,7 @@ diesel::table! {
diesel::table! { diesel::table! {
use diesel::sql_types::*; use diesel::sql_types::*;
use super::sql_types::CommunityFollowerState; use super::sql_types::CommunityFollowerState;
use super::sql_types::CommunityNotificationsModeEnum;
community_actions (person_id, community_id) { community_actions (person_id, community_id) {
community_id -> Int4, community_id -> Int4,
@ -249,6 +252,7 @@ diesel::table! {
became_moderator_at -> Nullable<Timestamptz>, became_moderator_at -> Nullable<Timestamptz>,
received_ban_at -> Nullable<Timestamptz>, received_ban_at -> Nullable<Timestamptz>,
ban_expires_at -> Nullable<Timestamptz>, ban_expires_at -> Nullable<Timestamptz>,
notifications -> Nullable<CommunityNotificationsModeEnum>,
} }
} }
@ -347,17 +351,6 @@ diesel::table! {
} }
} }
diesel::table! {
inbox_combined (id) {
id -> Int4,
published_at -> 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! { diesel::table! {
instance (id) { instance (id) {
id -> Int4, id -> Int4,
@ -752,6 +745,22 @@ diesel::table! {
} }
} }
diesel::table! {
use diesel::sql_types::*;
use super::sql_types::NotificationTypeEnum;
notification (id) {
id -> Int4,
recipient_id -> Int4,
comment_id -> Nullable<Int4>,
read -> Bool,
published_at -> Timestamptz,
kind -> NotificationTypeEnum,
post_id -> Nullable<Int4>,
private_message_id -> Nullable<Int4>,
}
}
diesel::table! { diesel::table! {
oauth_account (oauth_provider_id, local_user_id) { oauth_account (oauth_provider_id, local_user_id) {
local_user_id -> Int4, local_user_id -> Int4,
@ -838,16 +847,6 @@ diesel::table! {
} }
} }
diesel::table! {
person_comment_mention (id) {
id -> Int4,
recipient_id -> Int4,
comment_id -> Int4,
read -> Bool,
published_at -> Timestamptz,
}
}
diesel::table! { diesel::table! {
person_content_combined (id) { person_content_combined (id) {
id -> Int4, id -> Int4,
@ -868,16 +867,6 @@ diesel::table! {
} }
} }
diesel::table! {
person_post_mention (id) {
id -> Int4,
recipient_id -> Int4,
post_id -> Int4,
read -> Bool,
published_at -> Timestamptz,
}
}
diesel::table! { diesel::table! {
person_saved_combined (id) { person_saved_combined (id) {
id -> Int4, id -> Int4,
@ -934,6 +923,9 @@ diesel::table! {
} }
diesel::table! { diesel::table! {
use diesel::sql_types::*;
use super::sql_types::PostNotificationsModeEnum;
post_actions (person_id, post_id) { post_actions (person_id, post_id) {
post_id -> Int4, post_id -> Int4,
person_id -> Int4, person_id -> Int4,
@ -944,6 +936,7 @@ diesel::table! {
liked_at -> Nullable<Timestamptz>, liked_at -> Nullable<Timestamptz>,
like_score -> Nullable<Int2>, like_score -> Nullable<Int2>,
hidden_at -> Nullable<Timestamptz>, hidden_at -> Nullable<Timestamptz>,
notifications -> Nullable<PostNotificationsModeEnum>,
} }
} }
@ -980,7 +973,6 @@ diesel::table! {
recipient_id -> Int4, recipient_id -> Int4,
content -> Text, content -> Text,
deleted -> Bool, deleted -> Bool,
read -> Bool,
published_at -> Timestamptz, published_at -> Timestamptz,
updated_at -> Nullable<Timestamptz>, updated_at -> Nullable<Timestamptz>,
#[max_length = 255] #[max_length = 255]
@ -1145,8 +1137,6 @@ diesel::joinable!(comment -> person (creator_id));
diesel::joinable!(comment -> post (post_id)); diesel::joinable!(comment -> post (post_id));
diesel::joinable!(comment_actions -> comment (comment_id)); diesel::joinable!(comment_actions -> comment (comment_id));
diesel::joinable!(comment_actions -> person (person_id)); diesel::joinable!(comment_actions -> person (person_id));
diesel::joinable!(comment_reply -> comment (comment_id));
diesel::joinable!(comment_reply -> person (recipient_id));
diesel::joinable!(comment_report -> comment (comment_id)); diesel::joinable!(comment_report -> comment (comment_id));
diesel::joinable!(community -> instance (instance_id)); diesel::joinable!(community -> instance (instance_id));
diesel::joinable!(community_actions -> community (community_id)); diesel::joinable!(community_actions -> community (community_id));
@ -1158,10 +1148,6 @@ diesel::joinable!(email_verification -> local_user (local_user_id));
diesel::joinable!(federation_allowlist -> instance (instance_id)); diesel::joinable!(federation_allowlist -> instance (instance_id));
diesel::joinable!(federation_blocklist -> instance (instance_id)); diesel::joinable!(federation_blocklist -> instance (instance_id));
diesel::joinable!(federation_queue_state -> 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 -> instance (instance_id));
diesel::joinable!(instance_actions -> person (person_id)); diesel::joinable!(instance_actions -> person (person_id));
diesel::joinable!(local_image -> person (person_id)); diesel::joinable!(local_image -> person (person_id));
@ -1214,19 +1200,19 @@ diesel::joinable!(multi_community_entry -> community (community_id));
diesel::joinable!(multi_community_entry -> multi_community (multi_community_id)); diesel::joinable!(multi_community_entry -> multi_community (multi_community_id));
diesel::joinable!(multi_community_follow -> multi_community (multi_community_id)); diesel::joinable!(multi_community_follow -> multi_community (multi_community_id));
diesel::joinable!(multi_community_follow -> person (person_id)); diesel::joinable!(multi_community_follow -> person (person_id));
diesel::joinable!(notification -> comment (comment_id));
diesel::joinable!(notification -> person (recipient_id));
diesel::joinable!(notification -> post (post_id));
diesel::joinable!(notification -> private_message (private_message_id));
diesel::joinable!(oauth_account -> local_user (local_user_id)); diesel::joinable!(oauth_account -> local_user (local_user_id));
diesel::joinable!(oauth_account -> oauth_provider (oauth_provider_id)); diesel::joinable!(oauth_account -> oauth_provider (oauth_provider_id));
diesel::joinable!(password_reset_request -> local_user (local_user_id)); diesel::joinable!(password_reset_request -> local_user (local_user_id));
diesel::joinable!(person -> instance (instance_id)); diesel::joinable!(person -> instance (instance_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 -> comment (comment_id));
diesel::joinable!(person_content_combined -> post (post_id)); diesel::joinable!(person_content_combined -> post (post_id));
diesel::joinable!(person_liked_combined -> comment (comment_id)); diesel::joinable!(person_liked_combined -> comment (comment_id));
diesel::joinable!(person_liked_combined -> person (person_id)); diesel::joinable!(person_liked_combined -> person (person_id));
diesel::joinable!(person_liked_combined -> post (post_id)); diesel::joinable!(person_liked_combined -> post (post_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 -> comment (comment_id));
diesel::joinable!(person_saved_combined -> person (person_id)); diesel::joinable!(person_saved_combined -> person (person_id));
diesel::joinable!(person_saved_combined -> post (post_id)); diesel::joinable!(person_saved_combined -> post (post_id));
@ -1265,7 +1251,6 @@ diesel::allow_tables_to_appear_in_same_query!(
captcha_answer, captcha_answer,
comment, comment,
comment_actions, comment_actions,
comment_reply,
comment_report, comment_report,
community, community,
community_actions, community_actions,
@ -1278,7 +1263,6 @@ diesel::allow_tables_to_appear_in_same_query!(
federation_blocklist, federation_blocklist,
federation_queue_state, federation_queue_state,
image_details, image_details,
inbox_combined,
instance, instance,
instance_actions, instance_actions,
language, language,
@ -1305,15 +1289,14 @@ diesel::allow_tables_to_appear_in_same_query!(
multi_community, multi_community,
multi_community_entry, multi_community_entry,
multi_community_follow, multi_community_follow,
notification,
oauth_account, oauth_account,
oauth_provider, oauth_provider,
password_reset_request, password_reset_request,
person, person,
person_actions, person_actions,
person_comment_mention,
person_content_combined, person_content_combined,
person_liked_combined, person_liked_combined,
person_post_mention,
person_saved_combined, person_saved_combined,
post, post,
post_actions, post_actions,

View file

@ -613,33 +613,6 @@ CALL r.create_modlog_combined_trigger ('mod_remove_comment');
CALL r.create_modlog_combined_trigger ('mod_remove_community'); CALL r.create_modlog_combined_trigger ('mod_remove_community');
CALL r.create_modlog_combined_trigger ('mod_remove_post'); CALL r.create_modlog_combined_trigger ('mod_remove_post');
CALL r.create_modlog_combined_trigger ('mod_transfer_community'); 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_at, thing_id)
VALUES (NEW.published_at, 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');
-- Prevent using delete instead of uplete on action tables -- Prevent using delete instead of uplete on action tables
CREATE FUNCTION r.require_uplete () CREATE FUNCTION r.require_uplete ()
RETURNS TRIGGER RETURNS TRIGGER

View file

@ -519,7 +519,7 @@ mod tests {
} }
fn check_test_data(conn: &mut PgConnection) -> LemmyResult<()> { fn check_test_data(conn: &mut PgConnection) -> LemmyResult<()> {
use lemmy_db_schema_file::schema::{comment, comment_reply, community, person, post}; use lemmy_db_schema_file::schema::{comment, community, notification, person, post};
// Check users // Check users
let users: Vec<(i32, String, Option<String>, String, String)> = person::table let users: Vec<(i32, String, Option<String>, String, String)> = person::table
@ -622,16 +622,16 @@ mod tests {
assert_eq!(comments[1].6, 0); // Zero upvotes assert_eq!(comments[1].6, 0); // Zero upvotes
// Check comment replies // Check comment replies
let replies: Vec<(i32, i32)> = comment_reply::table let replies: Vec<(Option<i32>, i32)> = notification::table
.select((comment_reply::comment_id, comment_reply::recipient_id)) .select((notification::comment_id, notification::recipient_id))
.order_by(comment_reply::comment_id) .order_by(notification::comment_id)
.load(conn) .load(conn)
.map_err(|e| anyhow!("Failed to read comment replies: {}", e))?; .map_err(|e| anyhow!("Failed to read comment replies: {}", e))?;
assert_eq!(replies.len(), 2); assert_eq!(replies.len(), 2);
assert_eq!(replies[0].0, TEST_COMMENT_ID_1); assert_eq!(replies[0].0, Some(TEST_COMMENT_ID_1));
assert_eq!(replies[0].1, TEST_USER_ID_1); assert_eq!(replies[0].1, TEST_USER_ID_1);
assert_eq!(replies[1].0, TEST_COMMENT_ID_2); assert_eq!(replies[1].0, Some(TEST_COMMENT_ID_2));
assert_eq!(replies[1].1, TEST_USER_ID_2); assert_eq!(replies[1].1, TEST_USER_ID_2);
Ok(()) Ok(())

View file

@ -1,12 +1,5 @@
use crate::{CommentSlimView, CommentView}; use crate::{CommentSlimView, CommentView};
use lemmy_db_schema::newtypes::{ use lemmy_db_schema::newtypes::{CommentId, CommunityId, LanguageId, PaginationCursor, PostId};
CommentId,
CommunityId,
LanguageId,
LocalUserId,
PaginationCursor,
PostId,
};
use lemmy_db_schema_file::enums::{CommentSortType, ListingType}; use lemmy_db_schema_file::enums::{CommentSortType, ListingType};
use lemmy_db_views_vote::VoteView; use lemmy_db_views_vote::VoteView;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -19,7 +12,6 @@ use serde_with::skip_serializing_none;
/// A comment response. /// A comment response.
pub struct CommentResponse { pub struct CommentResponse {
pub comment_view: CommentView, pub comment_view: CommentView,
pub recipient_ids: Vec<LocalUserId>,
} }
#[skip_serializing_none] #[skip_serializing_none]

View file

@ -4,7 +4,7 @@ use lemmy_db_schema::{
source::site::Site, source::site::Site,
CommunitySortType, CommunitySortType,
}; };
use lemmy_db_schema_file::enums::{CommunityVisibility, ListingType}; use lemmy_db_schema_file::enums::{CommunityNotificationsMode, CommunityVisibility, ListingType};
use lemmy_db_views_community_moderator::CommunityModeratorView; use lemmy_db_views_community_moderator::CommunityModeratorView;
use lemmy_db_views_person::PersonView; use lemmy_db_views_person::PersonView;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -364,3 +364,12 @@ pub struct FollowMultiCommunity {
pub multi_community_id: MultiCommunityId, pub multi_community_id: MultiCommunityId,
pub follow: bool, pub follow: bool,
} }
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// Change notification settings for a community
pub struct UpdateCommunityNotifications {
pub community_id: CommunityId,
pub mode: CommunityNotificationsMode,
}

View file

@ -1,901 +0,0 @@
use crate::{
CommentReplyView,
InboxCombinedView,
InboxCombinedViewInternal,
PersonCommentMentionView,
PersonPostMentionView,
};
use diesel::{
dsl::not,
BoolExpressionMethods,
ExpressionMethods,
JoinOnDsl,
NullableExpressionMethods,
QueryDsl,
SelectableHelper,
};
use diesel_async::RunQueryDsl;
use i_love_jesus::SortDirection;
use lemmy_db_schema::{
aliases::{self},
newtypes::{InstanceId, PaginationCursor, PersonId},
source::combined::inbox::{inbox_combined_keys as key, InboxCombined},
traits::{InternalToCombinedView, PaginationCursorBuilder},
utils::{
get_conn,
limit_fetch,
paginate,
queries::{
community_join,
creator_community_actions_join,
creator_home_instance_actions_join,
creator_local_instance_actions_join,
creator_local_user_admin_join,
image_details_join,
my_comment_actions_join,
my_community_actions_join,
my_instance_actions_person_join,
my_local_user_admin_join,
my_person_actions_join,
my_post_actions_join,
},
DbPool,
},
InboxDataType,
};
use lemmy_db_schema_file::schema::{
comment,
comment_reply,
inbox_combined,
instance_actions,
person,
person_actions,
person_comment_mention,
person_post_mention,
post,
private_message,
};
use lemmy_db_views_private_message::PrivateMessageView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
impl InboxCombinedViewInternal {
#[diesel::dsl::auto_type(no_type_alias)]
fn joins(my_person_id: PersonId, local_instance_id: InstanceId) -> _ {
let item_creator = person::id;
let recipient_person = aliases::person1.field(person::id);
let item_creator_join = person::table.on(
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 = aliases::person1.on(
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::table.on(
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 = post::table.on(
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 = private_message::table.on(
inbox_combined::private_message_id
.eq(private_message::id.nullable())
.and(not(private_message::deleted))
.and(not(private_message::removed)),
);
let my_community_actions_join: my_community_actions_join =
my_community_actions_join(Some(my_person_id));
let my_post_actions_join: my_post_actions_join = my_post_actions_join(Some(my_person_id));
let my_comment_actions_join: my_comment_actions_join =
my_comment_actions_join(Some(my_person_id));
let my_local_user_admin_join: my_local_user_admin_join =
my_local_user_admin_join(Some(my_person_id));
let my_instance_actions_person_join: my_instance_actions_person_join =
my_instance_actions_person_join(Some(my_person_id));
let my_person_actions_join: my_person_actions_join = my_person_actions_join(Some(my_person_id));
let creator_local_instance_actions_join: creator_local_instance_actions_join =
creator_local_instance_actions_join(local_instance_id);
inbox_combined::table
.left_join(comment_reply::table)
.left_join(person_comment_mention::table)
.left_join(person_post_mention::table)
.left_join(private_message_join)
.left_join(comment_join)
.left_join(post_join)
.left_join(community_join())
.inner_join(item_creator_join)
.inner_join(recipient_join)
.left_join(image_details_join())
.left_join(creator_community_actions_join())
.left_join(my_local_user_admin_join)
.left_join(creator_local_user_admin_join())
.left_join(my_community_actions_join)
.left_join(my_instance_actions_person_join)
.left_join(creator_home_instance_actions_join())
.left_join(creator_local_instance_actions_join)
.left_join(my_post_actions_join)
.left_join(my_person_actions_join)
.left_join(my_comment_actions_join)
}
/// Gets the number of unread mentions
pub async fn get_unread_count(
pool: &mut DbPool<'_>,
my_person_id: PersonId,
local_instance_id: InstanceId,
show_bot_accounts: bool,
) -> LemmyResult<i64> {
use diesel::dsl::count;
let conn = &mut get_conn(pool).await?;
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 mut query = Self::joins(my_person_id, local_instance_id)
// 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_at.is_null())
.filter(instance_actions::blocked_at.is_null())
.select(count(inbox_combined::id))
.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
.first::<i64>(conn)
.await
.with_lemmy_type(LemmyErrorType::NotFound)
}
}
impl PaginationCursorBuilder for InboxCombinedView {
type CursorData = InboxCombined;
fn to_cursor(&self) -> PaginationCursor {
let (prefix, id) = match &self {
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),
};
PaginationCursor::new_single(prefix, id)
}
async fn from_cursor(
cursor: &PaginationCursor,
pool: &mut DbPool<'_>,
) -> LemmyResult<Self::CursorData> {
let conn = &mut get_conn(pool).await?;
let pids = cursor.prefixes_and_ids();
let (prefix, id) = pids
.as_slice()
.first()
.ok_or(LemmyErrorType::CouldntParsePaginationToken)?;
let mut query = inbox_combined::table
.select(Self::CursorData::as_select())
.into_boxed();
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(LemmyErrorType::CouldntParsePaginationToken.into()),
};
let token = query.first(conn).await?;
Ok(token)
}
}
#[derive(Default)]
pub struct InboxCombinedQuery {
pub type_: Option<InboxDataType>,
pub unread_only: Option<bool>,
pub show_bot_accounts: Option<bool>,
pub cursor_data: Option<InboxCombined>,
pub page_back: Option<bool>,
pub limit: Option<i64>,
pub no_limit: Option<bool>,
}
impl InboxCombinedQuery {
pub async fn list(
self,
pool: &mut DbPool<'_>,
my_person_id: PersonId,
local_instance_id: InstanceId,
) -> LemmyResult<Vec<InboxCombinedView>> {
let conn = &mut get_conn(pool).await?;
let item_creator = person::id;
let recipient_person = aliases::person1.field(person::id);
let mut query = InboxCombinedViewInternal::joins(my_person_id, local_instance_id)
.select(InboxCombinedViewInternal::as_select())
.into_boxed();
if !self.no_limit.unwrap_or_default() {
let limit = limit_fetch(self.limit)?;
query = query.limit(limit);
}
// 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_at.is_null())
.filter(instance_actions::blocked_at.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())
}
}
}
// Sorting by published
let paginated_query = paginate(
query,
SortDirection::Desc,
self.cursor_data,
None,
self.page_back,
)
.then_order_by(key::published_at)
// Tie breaker
.then_order_by(key::id);
let res = paginated_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(post), Some(community)) = (
v.comment_reply,
v.comment.clone(),
v.post.clone(),
v.community.clone(),
) {
Some(InboxCombinedView::CommentReply(CommentReplyView {
comment_reply,
comment,
recipient: v.item_recipient,
post,
community,
creator: v.item_creator,
community_actions: v.community_actions,
comment_actions: v.comment_actions,
person_actions: v.person_actions,
instance_actions: v.instance_actions,
creator_is_admin: v.item_creator_is_admin,
post_tags: v.post_tags,
can_mod: v.can_mod,
creator_banned: v.creator_banned,
creator_banned_from_community: v.creator_banned_from_community,
creator_is_moderator: v.creator_is_moderator,
}))
} else if let (Some(person_comment_mention), Some(comment), Some(post), Some(community)) = (
v.person_comment_mention,
v.comment,
v.post.clone(),
v.community.clone(),
) {
Some(InboxCombinedView::CommentMention(
PersonCommentMentionView {
person_comment_mention,
comment,
recipient: v.item_recipient,
post,
community,
creator: v.item_creator,
community_actions: v.community_actions,
comment_actions: v.comment_actions,
person_actions: v.person_actions,
instance_actions: v.instance_actions,
creator_is_admin: v.item_creator_is_admin,
can_mod: v.can_mod,
creator_banned: v.creator_banned,
creator_banned_from_community: v.creator_banned_from_community,
creator_is_moderator: v.creator_is_moderator,
},
))
} else if let (Some(person_post_mention), Some(post), Some(community)) =
(v.person_post_mention, v.post, v.community)
{
Some(InboxCombinedView::PostMention(PersonPostMentionView {
person_post_mention,
post,
community,
creator: v.item_creator,
recipient: v.item_recipient,
community_actions: v.community_actions,
person_actions: v.person_actions,
instance_actions: v.instance_actions,
post_actions: v.post_actions,
image_details: v.image_details,
creator_is_admin: v.item_creator_is_admin,
post_tags: v.post_tags,
can_mod: v.can_mod,
creator_banned: v.creator_banned,
creator_banned_from_community: v.creator_banned_from_community,
creator_is_moderator: v.creator_is_moderator,
}))
} 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::{impls::InboxCombinedQuery, InboxCombinedView, InboxCombinedViewInternal};
use lemmy_db_schema::{
assert_length,
source::{
comment::{Comment, CommentInsertForm},
comment_reply::{CommentReply, CommentReplyInsertForm, CommentReplyUpdateForm},
community::{Community, CommunityInsertForm},
instance::{Instance, InstanceActions, InstanceBlockForm},
person::{Person, PersonActions, PersonBlockForm, PersonInsertForm, PersonUpdateForm},
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_db_views_private_message::PrivateMessageView;
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, data.instance.id, true)
.await?;
assert_eq!(1, timmy_unread_replies);
let timmy_inbox = InboxCombinedQuery::default()
.list(pool, data.timmy.id, data.instance.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, data.instance.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, data.instance.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, data.instance.id, true)
.await?;
assert_eq!(2, sara_unread_mentions);
let sara_inbox = InboxCombinedQuery::default()
.list(pool, data.sara.id, data.instance.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::new(data.sara.id, data.timmy.id);
PersonActions::block(pool, &sara_blocks_timmy_form).await?;
let sara_unread_mentions_after_block =
InboxCombinedViewInternal::get_unread_count(pool, data.sara.id, data.instance.id, true)
.await?;
assert_eq!(1, sara_unread_mentions_after_block);
let sara_inbox_after_block = InboxCombinedQuery::default()
.list(pool, data.sara.id, data.instance.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
PersonActions::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, data.instance.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, data.instance.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, data.instance.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, data.instance.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, data.instance.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, data.instance.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, data.instance.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, data.instance.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::new(data.timmy.id, data.sara.id);
let inserted_block = PersonActions::block(pool, &timmy_blocks_sara_form).await?;
assert_eq!(
(data.timmy.id, data.sara.id, true),
(
inserted_block.person_id,
inserted_block.target_id,
inserted_block.blocked_at.is_some()
)
);
let timmy_messages = map_to_pm(
&InboxCombinedQuery {
unread_only: Some(true),
..Default::default()
}
.list(pool, data.timmy.id, data.instance.id)
.await?,
);
assert_length!(1, &timmy_messages);
let timmy_unread =
InboxCombinedViewInternal::get_unread_count(pool, data.timmy.id, data.instance.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::new(data.timmy.id, data.sara.instance_id);
let inserted_instance_block = InstanceActions::block(pool, &timmy_blocks_instance_form).await?;
assert_eq!(
(data.timmy.id, data.sara.instance_id, true),
(
inserted_instance_block.person_id,
inserted_instance_block.instance_id,
inserted_instance_block.blocked_at.is_some()
)
);
let timmy_messages = map_to_pm(
&InboxCombinedQuery {
unread_only: Some(true),
..Default::default()
}
.list(pool, data.timmy.id, data.instance.id)
.await?,
);
assert_length!(0, &timmy_messages);
let timmy_unread =
InboxCombinedViewInternal::get_unread_count(pool, data.timmy.id, data.instance.id, false)
.await?;
assert_eq!(0, timmy_unread);
cleanup(data, pool).await?;
Ok(())
}
}

View file

@ -1,233 +0,0 @@
use lemmy_db_schema::{
newtypes::PaginationCursor,
source::{
combined::inbox::InboxCombined,
comment::{Comment, CommentActions},
comment_reply::CommentReply,
community::{Community, CommunityActions},
images::ImageDetails,
instance::InstanceActions,
person::{Person, PersonActions},
person_comment_mention::PersonCommentMention,
person_post_mention::PersonPostMention,
post::{Post, PostActions},
private_message::PrivateMessage,
tag::TagsView,
},
InboxDataType,
};
use lemmy_db_views_private_message::PrivateMessageView;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
use {
diesel::{Queryable, Selectable},
lemmy_db_schema::{
utils::queries::{
creator_banned,
creator_is_admin,
local_user_can_mod,
person1_select,
post_tags_fragment,
},
utils::queries::{creator_banned_from_community, creator_is_moderator},
Person1AliasAllColumnsTuple,
},
};
pub mod api;
#[cfg(feature = "full")]
pub mod impls;
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(Queryable, Selectable))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
/// A combined inbox view
pub struct InboxCombinedViewInternal {
#[cfg_attr(feature = "full", diesel(embed))]
pub inbox_combined: InboxCombined,
#[cfg_attr(feature = "full", diesel(embed))]
pub comment_reply: Option<CommentReply>,
#[cfg_attr(feature = "full", diesel(embed))]
pub person_comment_mention: Option<PersonCommentMention>,
#[cfg_attr(feature = "full", diesel(embed))]
pub person_post_mention: Option<PersonPostMention>,
#[cfg_attr(feature = "full", diesel(embed))]
pub private_message: Option<PrivateMessage>,
#[cfg_attr(feature = "full", diesel(embed))]
pub comment: Option<Comment>,
#[cfg_attr(feature = "full", diesel(embed))]
pub post: Option<Post>,
#[cfg_attr(feature = "full", diesel(embed))]
pub community: Option<Community>,
#[cfg_attr(feature = "full", diesel(embed))]
pub item_creator: Person,
#[cfg_attr(feature = "full",
diesel(
select_expression_type = Person1AliasAllColumnsTuple,
select_expression = person1_select()
)
)]
pub item_recipient: Person,
#[cfg_attr(feature = "full", diesel(embed))]
pub image_details: Option<ImageDetails>,
#[cfg_attr(feature = "full", diesel(embed))]
pub community_actions: Option<CommunityActions>,
#[cfg_attr(feature = "full", diesel(embed))]
pub instance_actions: Option<InstanceActions>,
#[cfg_attr(feature = "full", diesel(embed))]
pub post_actions: Option<PostActions>,
#[cfg_attr(feature = "full", diesel(embed))]
pub person_actions: Option<PersonActions>,
#[cfg_attr(feature = "full", diesel(embed))]
pub comment_actions: Option<CommentActions>,
#[cfg_attr(feature = "full",
diesel(
select_expression = creator_is_admin()
)
)]
pub item_creator_is_admin: bool,
#[cfg_attr(feature = "full",
diesel(
select_expression = post_tags_fragment()
)
)]
pub post_tags: TagsView,
#[cfg_attr(feature = "full",
diesel(
select_expression = local_user_can_mod()
)
)]
pub can_mod: bool,
#[cfg_attr(feature = "full",
diesel(
select_expression = creator_banned()
)
)]
pub creator_banned: bool,
#[cfg_attr(feature = "full",
diesel(
select_expression = creator_is_moderator()
)
)]
pub creator_is_moderator: bool,
#[cfg_attr(feature = "full",
diesel(
select_expression = creator_banned_from_community()
)
)]
pub creator_banned_from_community: bool,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", 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),
}
#[skip_serializing_none]
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(Queryable))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// A person comment mention view.
pub struct PersonCommentMentionView {
pub person_comment_mention: PersonCommentMention,
pub recipient: Person,
pub comment: Comment,
pub creator: Person,
pub post: Post,
pub community: Community,
pub community_actions: Option<CommunityActions>,
pub comment_actions: Option<CommentActions>,
pub person_actions: Option<PersonActions>,
pub instance_actions: Option<InstanceActions>,
pub creator_is_admin: bool,
pub can_mod: bool,
pub creator_banned: bool,
pub creator_is_moderator: bool,
pub creator_banned_from_community: bool,
}
#[skip_serializing_none]
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(Queryable))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// A person post mention view.
pub struct PersonPostMentionView {
pub person_post_mention: PersonPostMention,
pub recipient: Person,
pub post: Post,
pub creator: Person,
pub community: Community,
pub image_details: Option<ImageDetails>,
pub community_actions: Option<CommunityActions>,
pub person_actions: Option<PersonActions>,
pub post_actions: Option<PostActions>,
pub instance_actions: Option<InstanceActions>,
pub post_tags: TagsView,
pub creator_is_admin: bool,
pub can_mod: bool,
pub creator_banned: bool,
pub creator_is_moderator: bool,
pub creator_banned_from_community: bool,
}
#[skip_serializing_none]
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(Queryable))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// A comment reply view.
pub struct CommentReplyView {
pub comment_reply: CommentReply,
pub recipient: Person,
pub comment: Comment,
pub creator: Person,
pub post: Post,
pub community: Community,
pub community_actions: Option<CommunityActions>,
pub comment_actions: Option<CommentActions>,
pub person_actions: Option<PersonActions>,
#[cfg_attr(feature = "full", diesel(embed))]
pub instance_actions: Option<InstanceActions>,
pub creator_is_admin: bool,
pub post_tags: TagsView,
pub can_mod: bool,
pub creator_banned: bool,
pub creator_is_moderator: bool,
pub creator_banned_from_community: bool,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// Get your inbox (replies, comment mentions, post mentions, and messages)
pub struct ListInbox {
pub type_: Option<InboxDataType>,
pub unread_only: Option<bool>,
pub page_cursor: Option<PaginationCursor>,
pub page_back: Option<bool>,
pub limit: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// Get your inbox (replies, comment mentions, post mentions, and messages)
pub struct ListInboxResponse {
pub inbox: Vec<InboxCombinedView>,
/// the pagination cursor to use to fetch the next page
pub next_page: Option<PaginationCursor>,
pub prev_page: Option<PaginationCursor>,
}

View file

@ -1,5 +1,5 @@
[package] [package]
name = "lemmy_db_views_inbox_combined" name = "lemmy_db_views_notification"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
description.workspace = true description.workspace = true
@ -23,19 +23,16 @@ full = [
"i-love-jesus", "i-love-jesus",
"lemmy_db_schema/full", "lemmy_db_schema/full",
"lemmy_db_schema_file/full", "lemmy_db_schema_file/full",
"lemmy_db_views_private_message/full",
]
ts-rs = [
"dep:ts-rs",
"lemmy_db_schema/ts-rs",
"lemmy_db_views_private_message/ts-rs",
] ]
ts-rs = ["dep:ts-rs", "lemmy_db_schema/ts-rs"]
[dependencies] [dependencies]
lemmy_db_views_private_message = { workspace = true }
lemmy_db_schema = { workspace = true } lemmy_db_schema = { workspace = true }
lemmy_utils = { workspace = true, optional = true } lemmy_utils = { workspace = true, optional = true }
lemmy_db_schema_file = { workspace = true } lemmy_db_schema_file = { workspace = true }
lemmy_db_views_private_message = { workspace = true }
lemmy_db_views_post = { workspace = true }
lemmy_db_views_comment = { workspace = true }
diesel = { workspace = true, optional = true } diesel = { workspace = true, optional = true }
diesel-async = { workspace = true, optional = true } diesel-async = { workspace = true, optional = true }
serde = { workspace = true } serde = { workspace = true }
@ -44,6 +41,3 @@ i-love-jesus = { workspace = true, optional = true }
serde_with = { workspace = true } serde_with = { workspace = true }
[dev-dependencies] [dev-dependencies]
pretty_assertions = { workspace = true }
serial_test = { workspace = true }
tokio = { workspace = true }

View file

@ -1,9 +1,4 @@
use lemmy_db_schema::newtypes::{ use lemmy_db_schema::newtypes::{NotificationId, PrivateMessageId};
CommentReplyId,
PersonCommentMentionId,
PersonPostMentionId,
PrivateMessageId,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@ -26,26 +21,8 @@ pub struct GetUnreadRegistrationApplicationCountResponse {
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] #[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))] #[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// Mark a comment reply as read. /// Mark a comment reply as read.
pub struct MarkCommentReplyAsRead { pub struct MarkNotificationAsRead {
pub comment_reply_id: CommentReplyId, pub notification_id: NotificationId,
pub read: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// Mark a person mention as read.
pub struct MarkPersonCommentMentionAsRead {
pub person_comment_mention_id: PersonCommentMentionId,
pub read: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// Mark a person mention as read.
pub struct MarkPersonPostMentionAsRead {
pub person_post_mention_id: PersonPostMentionId,
pub read: bool, pub read: bool,
} }

View file

@ -0,0 +1,337 @@
use crate::{CommentView, NotificationData, NotificationView, NotificationViewInternal};
use diesel::{
dsl::not,
BoolExpressionMethods,
ExpressionMethods,
JoinOnDsl,
NullableExpressionMethods,
QueryDsl,
SelectableHelper,
};
use diesel_async::RunQueryDsl;
use i_love_jesus::SortDirection;
use lemmy_db_schema::{
aliases,
newtypes::PaginationCursor,
source::{
notification::{notification_keys, Notification},
person::Person,
},
traits::PaginationCursorBuilder,
utils::{
get_conn,
limit_fetch,
paginate,
queries::{
community_join,
creator_community_actions_join,
creator_home_instance_actions_join,
creator_local_instance_actions_join,
creator_local_user_admin_join,
image_details_join,
my_comment_actions_join,
my_community_actions_join,
my_instance_actions_person_join,
my_local_user_admin_join,
my_person_actions_join,
my_post_actions_join,
},
DbPool,
},
NotificationDataType,
};
use lemmy_db_schema_file::{
enums::NotificationTypes,
schema::{
comment,
instance_actions,
notification,
person,
person_actions,
post,
private_message,
},
};
use lemmy_db_views_post::PostView;
use lemmy_db_views_private_message::PrivateMessageView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
impl NotificationView {
#[diesel::dsl::auto_type(no_type_alias)]
fn joins(my_person: &Person) -> _ {
let item_creator = person::id;
let recipient_person = aliases::person1.field(person::id);
let item_creator_join = person::table.on(
comment::creator_id
.eq(item_creator)
.or(
notification::post_id
.is_not_null()
.and(post::creator_id.eq(item_creator)),
)
.or(private_message::creator_id.eq(item_creator)),
);
let recipient_join = aliases::person1.on(notification::recipient_id.eq(recipient_person));
let comment_join = comment::table.on(
notification::comment_id
.eq(comment::id.nullable())
// Filter out the deleted / removed
.and(not(comment::deleted))
.and(not(comment::removed)),
);
let post_join = post::table.on(
notification::post_id
.eq(post::id.nullable())
.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 = private_message::table.on(
notification::private_message_id
.eq(private_message::id.nullable())
.and(not(private_message::deleted))
.and(not(private_message::removed)),
);
let my_community_actions_join: my_community_actions_join =
my_community_actions_join(Some(my_person.id));
let my_post_actions_join: my_post_actions_join = my_post_actions_join(Some(my_person.id));
let my_comment_actions_join: my_comment_actions_join =
my_comment_actions_join(Some(my_person.id));
let my_local_user_admin_join: my_local_user_admin_join =
my_local_user_admin_join(Some(my_person.id));
let my_instance_actions_person_join: my_instance_actions_person_join =
my_instance_actions_person_join(Some(my_person.id));
let my_person_actions_join: my_person_actions_join = my_person_actions_join(Some(my_person.id));
let creator_local_instance_actions_join: creator_local_instance_actions_join =
creator_local_instance_actions_join(my_person.instance_id);
notification::table
.left_join(private_message_join)
.left_join(comment_join)
.left_join(post_join)
.left_join(community_join())
.inner_join(item_creator_join)
.inner_join(recipient_join)
.left_join(image_details_join())
.left_join(creator_community_actions_join())
.left_join(my_local_user_admin_join)
.left_join(creator_local_user_admin_join())
.left_join(my_community_actions_join)
.left_join(my_instance_actions_person_join)
.left_join(creator_home_instance_actions_join())
.left_join(creator_local_instance_actions_join)
.left_join(my_post_actions_join)
.left_join(my_person_actions_join)
.left_join(my_comment_actions_join)
}
/// Gets the number of unread mentions
pub async fn get_unread_count(
pool: &mut DbPool<'_>,
my_person: &Person,
show_bot_accounts: bool,
) -> LemmyResult<i64> {
use diesel::dsl::count;
let conn = &mut get_conn(pool).await?;
let unread_filter = notification::read.eq(false);
let mut query = Self::joins(my_person)
// Filter for your user
.filter(notification::recipient_id.eq(my_person.id))
// Filter unreads
.filter(unread_filter)
// Don't count replies from blocked users
.filter(person_actions::blocked_at.is_null())
.filter(instance_actions::blocked_at.is_null())
.select(count(notification::id))
.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
.first::<i64>(conn)
.await
.with_lemmy_type(LemmyErrorType::NotFound)
}
}
impl PaginationCursorBuilder for NotificationView {
type CursorData = Notification;
fn to_cursor(&self) -> PaginationCursor {
PaginationCursor(self.notification.id.0.to_string())
}
async fn from_cursor(
cursor: &PaginationCursor,
pool: &mut DbPool<'_>,
) -> LemmyResult<Self::CursorData> {
let conn = &mut get_conn(pool).await?;
let id: i32 = cursor.0.parse()?;
let query = notification::table
.select(Self::CursorData::as_select())
.filter(notification::id.eq(id));
let token = query.first(conn).await?;
Ok(token)
}
}
#[derive(Default)]
pub struct NotificationQuery {
pub type_: Option<NotificationDataType>,
pub unread_only: Option<bool>,
pub show_bot_accounts: Option<bool>,
pub cursor_data: Option<Notification>,
pub page_back: Option<bool>,
pub limit: Option<i64>,
pub no_limit: Option<bool>,
}
impl NotificationQuery {
pub async fn list(
self,
pool: &mut DbPool<'_>,
my_person: &Person,
) -> LemmyResult<Vec<NotificationView>> {
let conn = &mut get_conn(pool).await?;
let mut query = NotificationView::joins(my_person)
.select(NotificationViewInternal::as_select())
.into_boxed();
if !self.no_limit.unwrap_or_default() {
let limit = limit_fetch(self.limit)?;
query = query.limit(limit);
}
// Filters
if self.unread_only.unwrap_or_default() {
query = query
// The recipient filter (IE only show replies to you)
.filter(notification::recipient_id.eq(my_person.id))
.filter(notification::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(
notification::recipient_id.eq(my_person.id).or(
notification::private_message_id.is_not_null().and(
notification::recipient_id
.eq(my_person.id)
.or(person::id.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_at.is_null())
.filter(instance_actions::blocked_at.is_null());
if let Some(type_) = self.type_ {
query = match type_ {
NotificationDataType::All => query,
NotificationDataType::Reply => {
query.filter(notification::kind.eq(NotificationTypes::Reply))
}
NotificationDataType::Mention => {
query.filter(notification::kind.eq(NotificationTypes::Mention))
}
NotificationDataType::PrivateMessage => {
query.filter(notification::kind.eq(NotificationTypes::PrivateMessage))
}
NotificationDataType::Subscribed => {
query.filter(notification::kind.eq(NotificationTypes::Subscribed))
}
}
}
// Sorting by published
let paginated_query = paginate(
query,
SortDirection::Desc,
self.cursor_data,
None,
self.page_back,
)
.then_order_by(notification_keys::published_at)
// Tie breaker
.then_order_by(notification_keys::id);
let res = paginated_query
.load::<NotificationViewInternal>(conn)
.await?;
Ok(res.into_iter().filter_map(map_to_enum).collect())
}
}
fn map_to_enum(v: NotificationViewInternal) -> Option<NotificationView> {
let data = if let (Some(comment), Some(post), Some(community)) =
(v.comment, v.post.clone(), v.community.clone())
{
NotificationData::Comment(CommentView {
comment,
post,
community,
creator: v.creator,
community_actions: v.community_actions,
instance_actions: v.instance_actions,
person_actions: v.person_actions,
comment_actions: v.comment_actions,
creator_is_admin: v.creator_is_admin,
post_tags: v.post_tags,
can_mod: v.can_mod,
creator_banned: v.creator_banned,
creator_is_moderator: v.creator_is_moderator,
creator_banned_from_community: v.creator_banned_from_community,
})
} else if let (Some(post), Some(community)) = (v.post, v.community) {
NotificationData::Post(PostView {
post,
community,
creator: v.creator,
image_details: v.image_details,
community_actions: v.community_actions,
instance_actions: v.instance_actions,
post_actions: v.post_actions,
person_actions: v.person_actions,
creator_is_admin: v.creator_is_admin,
tags: v.post_tags,
can_mod: v.can_mod,
creator_banned: v.creator_banned,
creator_is_moderator: v.creator_is_moderator,
creator_banned_from_community: v.creator_banned_from_community,
})
} else if let Some(private_message) = v.private_message {
NotificationData::PrivateMessage(PrivateMessageView {
private_message,
creator: v.creator,
recipient: v.recipient,
})
} else {
return None;
};
Some(NotificationView {
notification: v.notification,
data,
})
}

View file

@ -0,0 +1,149 @@
use lemmy_db_schema::{
newtypes::PaginationCursor,
source::{
comment::{Comment, CommentActions},
community::{Community, CommunityActions},
images::ImageDetails,
instance::InstanceActions,
notification::Notification,
person::{Person, PersonActions},
post::{Post, PostActions},
private_message::PrivateMessage,
tag::TagsView,
},
NotificationDataType,
};
use lemmy_db_views_comment::CommentView;
use lemmy_db_views_post::PostView;
use lemmy_db_views_private_message::PrivateMessageView;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
use {
diesel::{Queryable, Selectable},
lemmy_db_schema::{
utils::queries::person1_select,
utils::queries::{creator_banned, creator_is_admin, local_user_can_mod, post_tags_fragment},
utils::queries::{creator_banned_from_community, creator_is_moderator},
Person1AliasAllColumnsTuple,
},
};
pub mod api;
#[cfg(feature = "full")]
pub mod impls;
#[derive(Clone)]
#[cfg_attr(feature = "full", derive(Queryable, Selectable))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
struct NotificationViewInternal {
#[cfg_attr(feature = "full", diesel(embed))]
notification: Notification,
#[cfg_attr(feature = "full", diesel(embed))]
private_message: Option<PrivateMessage>,
#[cfg_attr(feature = "full", diesel(embed))]
comment: Option<Comment>,
#[cfg_attr(feature = "full", diesel(embed))]
post: Option<Post>,
#[cfg_attr(feature = "full", diesel(embed))]
community: Option<Community>,
#[cfg_attr(feature = "full", diesel(embed))]
creator: Person,
#[cfg_attr(feature = "full",
diesel(
select_expression_type = Person1AliasAllColumnsTuple,
select_expression = person1_select()
)
)]
recipient: Person,
#[cfg_attr(feature = "full", diesel(embed))]
image_details: Option<ImageDetails>,
#[cfg_attr(feature = "full", diesel(embed))]
community_actions: Option<CommunityActions>,
#[cfg_attr(feature = "full", diesel(embed))]
instance_actions: Option<InstanceActions>,
#[cfg_attr(feature = "full", diesel(embed))]
post_actions: Option<PostActions>,
#[cfg_attr(feature = "full", diesel(embed))]
person_actions: Option<PersonActions>,
#[cfg_attr(feature = "full", diesel(embed))]
comment_actions: Option<CommentActions>,
#[cfg_attr(feature = "full",
diesel(
select_expression = creator_is_admin()
)
)]
creator_is_admin: bool,
#[cfg_attr(feature = "full",
diesel(
select_expression = post_tags_fragment()
)
)]
post_tags: TagsView,
#[cfg_attr(feature = "full",
diesel(
select_expression = local_user_can_mod()
)
)]
can_mod: bool,
#[cfg_attr(feature = "full",
diesel(
select_expression = creator_banned()
)
)]
creator_banned: bool,
#[cfg_attr(feature = "full",
diesel(
select_expression = creator_is_moderator()
)
)]
creator_is_moderator: bool,
#[cfg_attr(feature = "full",
diesel(
select_expression = creator_banned_from_community()
)
)]
creator_banned_from_community: bool,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub struct NotificationView {
pub notification: Notification,
pub data: NotificationData,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export))]
#[serde(tag = "type_")]
pub enum NotificationData {
Comment(CommentView),
Post(PostView),
PrivateMessage(PrivateMessageView),
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// Get your inbox (replies, comment mentions, post mentions, and messages)
pub struct ListNotifications {
pub type_: Option<NotificationDataType>,
pub unread_only: Option<bool>,
pub page_cursor: Option<PaginationCursor>,
pub page_back: Option<bool>,
pub limit: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// Get your inbox (replies, comment mentions, post mentions, and messages)
pub struct ListNotificationsResponse {
pub notifications: Vec<NotificationView>,
/// the pagination cursor to use to fetch the next page
pub next_page: Option<PaginationCursor>,
pub prev_page: Option<PaginationCursor>,
}

View file

@ -12,7 +12,7 @@ use lemmy_db_schema::{
}, },
PostFeatureType, PostFeatureType,
}; };
use lemmy_db_schema_file::enums::{ListingType, PostSortType}; use lemmy_db_schema_file::enums::{ListingType, PostNotificationsMode, PostSortType};
use lemmy_db_views_community::CommunityView; use lemmy_db_views_community::CommunityView;
use lemmy_db_views_vote::VoteView; use lemmy_db_views_vote::VoteView;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -93,6 +93,15 @@ pub struct FeaturePost {
pub feature_type: PostFeatureType, pub feature_type: PostFeatureType,
} }
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// Change notification settings for a post
pub struct UpdatePostNotifications {
pub post_id: PostId,
pub mode: PostNotificationsMode,
}
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] #[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]

View file

@ -745,7 +745,7 @@ pub struct UserSettingsBackup {
#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))] #[cfg_attr(feature = "ts-rs", ts(optional_fields, export))]
/// Your exported data. /// Your exported data.
pub struct ExportDataResponse { pub struct ExportDataResponse {
pub inbox: Vec<PostOrCommentOrPrivateMessage>, pub notifications: Vec<PostOrCommentOrPrivateMessage>,
pub content: Vec<PostOrCommentOrPrivateMessage>, pub content: Vec<PostOrCommentOrPrivateMessage>,
pub read_posts: Vec<Url>, pub read_posts: Vec<Url>,
pub liked: Vec<Url>, pub liked: Vec<Url>,

View file

@ -1,7 +1,7 @@
use crate::{inbox_link, send_email, user_language}; use crate::{inbox_link, send_email, user_language};
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::DbUrl, newtypes::DbUrl,
source::{comment::Comment, person::Person, post::Post}, source::{comment::Comment, community::Community, person::Person, post::Post},
}; };
use lemmy_db_views_local_user::LocalUserView; use lemmy_db_views_local_user::LocalUserView;
use lemmy_utils::{ use lemmy_utils::{
@ -30,69 +30,96 @@ pub async fn send_mention_email(
.await .await
} }
pub async fn send_comment_reply_email( pub async fn send_post_subscribed_email(
parent_user_view: &LocalUserView, user_view: &LocalUserView,
comment: &Comment,
person: &Person,
parent_comment: &Comment,
post: &Post, post: &Post,
comment: &Comment,
link: DbUrl,
settings: &Settings, settings: &Settings,
) -> LemmyResult<()> { ) {
let inbox_link = inbox_link(settings); let inbox_link = inbox_link(settings);
let lang = user_language(parent_user_view); let lang = user_language(user_view);
let content = markdown_to_html(&comment.content); let content = markdown_to_html(&comment.content);
send_email_to_user( send_email_to_user(
parent_user_view, user_view,
&lang.notification_comment_reply_subject(&person.name), &lang.notification_post_subscribed_subject(&post.name),
&lang.notification_comment_reply_body( &lang.notification_post_subscribed_body(&content, &link, inbox_link),
comment.local_url(settings)?,
&content,
&inbox_link,
&parent_comment.content,
&post.name,
&person.name,
),
settings, settings,
) )
.await; .await
Ok(())
} }
pub async fn send_post_reply_email( pub async fn send_community_subscribed_email(
user_view: &LocalUserView,
post: &Post,
community: &Community,
link: DbUrl,
settings: &Settings,
) {
let inbox_link = inbox_link(settings);
let lang = user_language(user_view);
let content = post
.body
.as_ref()
.map(|b| markdown_to_html(b))
.unwrap_or_default();
send_email_to_user(
user_view,
&lang.notification_community_subscribed_subject(&post.name, &community.title),
&lang.notification_community_subscribed_body(&content, &link, inbox_link),
settings,
)
.await
}
pub async fn send_reply_email(
parent_user_view: &LocalUserView, parent_user_view: &LocalUserView,
comment: &Comment, comment: &Comment,
person: &Person, person: &Person,
parent_comment: &Option<Comment>,
post: &Post, post: &Post,
settings: &Settings, settings: &Settings,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
let inbox_link = inbox_link(settings); let inbox_link = inbox_link(settings);
let lang = user_language(parent_user_view); let lang = user_language(parent_user_view);
let content = markdown_to_html(&comment.content); let content = markdown_to_html(&comment.content);
send_email_to_user( let (subject, body) = if let Some(parent_comment) = parent_comment {
parent_user_view, (
&lang.notification_post_reply_subject(&person.name), lang.notification_comment_reply_subject(&person.name),
&lang.notification_post_reply_body( lang.notification_comment_reply_body(
comment.local_url(settings)?, comment.local_url(settings)?,
&content, &content,
&inbox_link, &inbox_link,
&post.name, &parent_comment.content,
&person.name, &post.name,
), &person.name,
settings, ),
) )
.await; } else {
(
lang.notification_post_reply_subject(&person.name),
lang.notification_post_reply_body(
comment.local_url(settings)?,
&content,
&inbox_link,
&post.name,
&person.name,
),
)
};
send_email_to_user(parent_user_view, &subject, &body, settings).await;
Ok(()) Ok(())
} }
pub async fn send_private_message_email( pub async fn send_private_message_email(
sender: &LocalUserView, sender: &Person,
local_recipient: &LocalUserView, local_recipient: &LocalUserView,
content: &str, content: &str,
settings: &Settings, settings: &Settings,
) { ) {
let inbox_link = inbox_link(settings); let inbox_link = inbox_link(settings);
let lang = user_language(local_recipient); let lang = user_language(local_recipient);
let sender_name = &sender.person.name; let sender_name = &sender.name;
let content = markdown_to_html(content); let content = markdown_to_html(content);
send_email_to_user( send_email_to_user(
local_recipient, local_recipient,

@ -1 +1 @@
Subproject commit 7debe41492de3f04403c9c78ced9697be199e394 Subproject commit 72c9cc342b339779cd6d61a8e3349aeff5cad2ff

View file

@ -25,7 +25,7 @@ lemmy_db_views_community = { workspace = true, features = ["full"] }
lemmy_db_views_post = { workspace = true, features = ["full"] } lemmy_db_views_post = { workspace = true, features = ["full"] }
lemmy_db_views_local_image = { workspace = true, features = ["full"] } lemmy_db_views_local_image = { workspace = true, features = ["full"] }
lemmy_db_views_local_user = { workspace = true, features = ["full"] } lemmy_db_views_local_user = { workspace = true, features = ["full"] }
lemmy_db_views_inbox_combined = { workspace = true, features = ["full"] } lemmy_db_views_notification = { workspace = true, features = ["full"] }
lemmy_db_views_modlog_combined = { workspace = true, features = ["full"] } lemmy_db_views_modlog_combined = { workspace = true, features = ["full"] }
lemmy_db_views_person_content_combined = { workspace = true, features = [ lemmy_db_views_person_content_combined = { workspace = true, features = [
"full", "full",

View file

@ -11,8 +11,8 @@ use lemmy_db_schema::{
PersonContentType, PersonContentType,
}; };
use lemmy_db_schema_file::enums::{ListingType, PostSortType}; use lemmy_db_schema_file::enums::{ListingType, PostSortType};
use lemmy_db_views_inbox_combined::{impls::InboxCombinedQuery, InboxCombinedView};
use lemmy_db_views_modlog_combined::{impls::ModlogCombinedQuery, ModlogCombinedView}; use lemmy_db_views_modlog_combined::{impls::ModlogCombinedQuery, ModlogCombinedView};
use lemmy_db_views_notification::{impls::NotificationQuery, NotificationData, NotificationView};
use lemmy_db_views_person_content_combined::impls::PersonContentCombinedQuery; use lemmy_db_views_person_content_combined::impls::PersonContentCombinedQuery;
use lemmy_db_views_post::{impls::PostQuery, PostView}; use lemmy_db_views_post::{impls::PostQuery, PostView};
use lemmy_db_views_site::SiteView; use lemmy_db_views_site::SiteView;
@ -321,22 +321,20 @@ async fn get_feed_front(
async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> LemmyResult<Channel> { async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> LemmyResult<Channel> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool()).await?;
let local_instance_id = site_view.site.instance_id;
let local_user = local_user_view_from_jwt(jwt, context).await?; let local_user = local_user_view_from_jwt(jwt, context).await?;
let my_person_id = local_user.person.id;
let show_bot_accounts = Some(local_user.local_user.show_bot_accounts); let show_bot_accounts = Some(local_user.local_user.show_bot_accounts);
check_private_instance(&Some(local_user.clone()), &site_view.local_site)?; check_private_instance(&Some(local_user.clone()), &site_view.local_site)?;
let inbox = InboxCombinedQuery { let notifications = NotificationQuery {
show_bot_accounts, show_bot_accounts,
..Default::default() ..Default::default()
} }
.list(&mut context.pool(), my_person_id, local_instance_id) .list(&mut context.pool(), &local_user.person)
.await?; .await?;
let protocol_and_hostname = context.settings().get_protocol_and_hostname(); let protocol_and_hostname = context.settings().get_protocol_and_hostname();
let items = create_reply_and_mention_items(inbox, context)?; let items = create_reply_and_mention_items(notifications, context)?;
let mut channel = Channel { let mut channel = Channel {
namespaces: RSS_NAMESPACE.clone(), namespaces: RSS_NAMESPACE.clone(),
@ -387,49 +385,39 @@ async fn get_feed_modlog(context: &LemmyContext, jwt: &str) -> LemmyResult<Chann
} }
fn create_reply_and_mention_items( fn create_reply_and_mention_items(
inbox: Vec<InboxCombinedView>, inbox: Vec<NotificationView>,
context: &LemmyContext, context: &LemmyContext,
) -> LemmyResult<Vec<Item>> { ) -> LemmyResult<Vec<Item>> {
let reply_items: Vec<Item> = inbox let reply_items: Vec<Item> = inbox
.iter() .iter()
.map(|r| match r { .map(|v| match &v.data {
InboxCombinedView::CommentReply(v) => { NotificationData::Post(post) => {
let reply_url = v.comment.local_url(context.settings())?; let mention_url = post.post.local_url(context.settings())?;
build_item( build_item(
&v.creator, &post.creator,
&v.comment.published_at, &post.post.published_at,
mention_url.as_str(),
&post.post.body.clone().unwrap_or_default(),
context.settings(),
)
}
NotificationData::Comment(comment) => {
let reply_url = comment.comment.local_url(context.settings())?;
build_item(
&comment.creator,
&comment.comment.published_at,
reply_url.as_str(), reply_url.as_str(),
&v.comment.content, &comment.comment.content,
context.settings(), context.settings(),
) )
} }
InboxCombinedView::CommentMention(v) => { NotificationData::PrivateMessage(pm) => {
let mention_url = v.comment.local_url(context.settings())?;
build_item(
&v.creator,
&v.comment.published_at,
mention_url.as_str(),
&v.comment.content,
context.settings(),
)
}
InboxCombinedView::PostMention(v) => {
let mention_url = v.post.local_url(context.settings())?;
build_item(
&v.creator,
&v.post.published_at,
mention_url.as_str(),
&v.post.body.clone().unwrap_or_default(),
context.settings(),
)
}
InboxCombinedView::PrivateMessage(v) => {
let inbox_url = format!("{}/inbox", context.settings().get_protocol_and_hostname()); let inbox_url = format!("{}/inbox", context.settings().get_protocol_and_hostname());
build_item( build_item(
&v.creator, &pm.creator,
&v.private_message.published_at, &pm.private_message.published_at,
&inbox_url, &inbox_url,
&v.private_message.content, &pm.private_message.content,
context.settings(), context.settings(),
) )
} }

View file

@ -106,10 +106,8 @@ pub enum LemmyErrorType {
CouldntUpdateReadComments, CouldntUpdateReadComments,
CouldntHidePost, CouldntHidePost,
CouldntUpdateCommunity, CouldntUpdateCommunity,
CouldntCreatePersonCommentMention, CouldntCreateNotification,
CouldntUpdatePersonCommentMention, CouldntUpdateNotification,
CouldntCreatePersonPostMention,
CouldntUpdatePersonPostMention,
CouldntCreatePost, CouldntCreatePost,
CouldntCreatePrivateMessage, CouldntCreatePrivateMessage,
CouldntUpdatePrivateMessage, CouldntUpdatePrivateMessage,
@ -160,9 +158,6 @@ pub enum LemmyErrorType {
CouldntParsePaginationToken, CouldntParsePaginationToken,
PluginError(String), PluginError(String),
InvalidFetchLimit, InvalidFetchLimit,
CouldntCreateCommentReply,
CouldntUpdateCommentReply,
CouldntMarkCommentReplyAsRead,
CouldntCreateEmoji, CouldntCreateEmoji,
CouldntUpdateEmoji, CouldntUpdateEmoji,
CouldntCreatePerson, CouldntCreatePerson,

View file

@ -0,0 +1,140 @@
CREATE TABLE person_post_mention (
id serial PRIMARY KEY,
recipient_id int REFERENCES person (id) ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
post_id int REFERENCES post (id) ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
read bool NOT NULL DEFAULT FALSE,
published_at timestamptz DEFAULT now() NOT NULL
);
CREATE TABLE person_mention (
id serial PRIMARY KEY,
recipient_id int REFERENCES person (id) ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
comment_id int REFERENCES comment (id) ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
read bool NOT NULL DEFAULT FALSE,
published_at timestamptz DEFAULT now() NOT NULL,
UNIQUE (recipient_id, comment_id)
);
ALTER TABLE person_mention RENAME TO person_comment_mention;
CREATE TABLE comment_reply (
id serial PRIMARY KEY,
recipient_id int REFERENCES person (id) ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
comment_id int REFERENCES comment (id) ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
read bool NOT NULL DEFAULT FALSE,
published_at timestamptz DEFAULT now() NOT NULL,
UNIQUE (recipient_id, comment_id)
);
CREATE TABLE inbox_combined (
id serial PRIMARY KEY,
comment_reply_id int REFERENCES comment_reply (id) ON UPDATE CASCADE ON DELETE CASCADE UNIQUE,
person_comment_mention_id int REFERENCES person_comment_mention (id) ON UPDATE CASCADE ON DELETE CASCADE UNIQUE,
person_post_mention_id int REFERENCES person_post_mention (id) ON UPDATE CASCADE ON DELETE CASCADE UNIQUE,
private_message_id int REFERENCES private_message (id) ON UPDATE CASCADE ON DELETE CASCADE UNIQUE,
published_at timestamptz NOT NULL
);
ALTER TABLE private_message
ADD COLUMN read bool DEFAULT FALSE NOT NULL;
-- copy back data to person_post_mention table
INSERT INTO person_post_mention (recipient_id, post_id, read, published_at)
SELECT
recipient_id,
post_id,
read,
published_at
FROM
notification
WHERE
kind = 'Mention'
AND post_id IS NOT NULL;
INSERT INTO inbox_combined (person_post_mention_id, published_at)
SELECT
id,
published_at
FROM
person_post_mention;
-- copy back data to person_comment_mention table
INSERT INTO person_comment_mention (recipient_id, comment_id, read, published_at)
SELECT
recipient_id,
comment_id,
read,
published_at
FROM
notification
WHERE
kind = 'Mention'
AND comment_id IS NOT NULL;
-- copy back data to person_comment_mention table
UPDATE
private_message p
SET
read = n.read
FROM
notification n
WHERE
p.id = n.private_message_id;
INSERT INTO inbox_combined (person_comment_mention_id, published_at)
SELECT
id,
published_at
FROM
person_comment_mention;
-- copy back data to comment_reply table
INSERT INTO comment_reply (recipient_id, comment_id, read, published_at)
SELECT
recipient_id,
comment_id,
read,
published_at
FROM
notification
WHERE
kind = 'Reply'
AND comment_id IS NOT NULL;
INSERT INTO inbox_combined (comment_reply_id, published_at)
SELECT
id,
published_at
FROM
comment_reply;
ALTER TABLE ONLY person_post_mention
ADD CONSTRAINT person_post_mention_unique UNIQUE (recipient_id, post_id);
ALTER TABLE inbox_combined
ADD CONSTRAINT inbox_combined_check CHECK (num_nonnulls (comment_reply_id, person_comment_mention_id, person_post_mention_id, private_message_id) = 1);
CREATE INDEX idx_comment_reply_comment ON comment_reply USING btree (comment_id);
CREATE INDEX idx_comment_reply_recipient ON comment_reply USING btree (recipient_id);
CREATE INDEX idx_comment_reply_published ON comment_reply USING btree (published_at DESC);
CREATE INDEX idx_inbox_combined_published_asc ON inbox_combined USING btree (reverse_timestamp_sort (published_at) DESC, id DESC);
CREATE INDEX idx_inbox_combined_published ON inbox_combined USING btree (published_at DESC, id DESC);
DROP TABLE notification;
DROP TYPE notification_type_enum;
ALTER TABLE community_actions
DROP COLUMN notifications;
DROP TYPE community_notifications_mode_enum;
ALTER TABLE post_actions
DROP COLUMN notifications;
DROP TYPE post_notifications_mode_enum;

View file

@ -0,0 +1,104 @@
-- create new data types
CREATE TYPE notification_type_enum AS enum (
'Mention',
'Reply',
'Subscribed',
'PrivateMessage'
);
-- create notification table by renaming comment_reply, to avoid copying lots of data around
ALTER TABLE comment_reply RENAME TO notification;
ALTER INDEX idx_comment_reply_comment RENAME TO idx_notification_comment;
ALTER INDEX idx_comment_reply_recipient RENAME TO idx_notification_recipient;
ALTER INDEX idx_comment_reply_published RENAME TO idx_notification_published;
ALTER SEQUENCE comment_reply_id_seq
RENAME TO notification_id_seq;
ALTER TABLE notification RENAME CONSTRAINT comment_reply_comment_id_fkey TO notification_comment_id_fkey;
ALTER TABLE notification RENAME CONSTRAINT comment_reply_pkey TO notification_pkey;
ALTER TABLE notification
DROP CONSTRAINT comment_reply_recipient_id_comment_id_key;
ALTER TABLE notification RENAME CONSTRAINT comment_reply_recipient_id_fkey TO notification_recipient_id_fkey;
ALTER TABLE notification
ADD COLUMN kind notification_type_enum NOT NULL DEFAULT 'Reply',
ALTER COLUMN comment_id DROP NOT NULL,
ADD COLUMN post_id int REFERENCES post (id) ON UPDATE CASCADE ON DELETE CASCADE,
ADD COLUMN private_message_id int REFERENCES private_message (id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE notification
ALTER COLUMN kind DROP DEFAULT;
-- copy data from person_post_mention table
INSERT INTO notification (post_id, recipient_id, kind, read, published_at)
SELECT
post_id,
recipient_id,
'Mention',
read,
published_at
FROM
person_post_mention;
-- copy data from person_comment_mention table
INSERT INTO notification (comment_id, recipient_id, kind, read, published_at)
SELECT
comment_id,
recipient_id,
'Mention',
read,
published_at
FROM
person_comment_mention;
-- copy data from private_message table
INSERT INTO notification (private_message_id, recipient_id, kind, read, published_at)
SELECT
id,
recipient_id,
'PrivateMessage',
read,
published_at
FROM
private_message;
ALTER TABLE private_message
DROP COLUMN read;
ALTER TABLE notification
ADD CONSTRAINT notification_check CHECK (num_nonnulls (post_id, comment_id, private_message_id) = 1);
CREATE INDEX idx_notification_recipient_published ON notification (recipient_id, published_at);
CREATE INDEX idx_notification_post ON notification (post_id);
CREATE INDEX idx_notification_private_message ON notification (private_message_id);
DROP TABLE inbox_combined, person_post_mention, person_comment_mention;
CREATE TYPE post_notifications_mode_enum AS enum (
'AllComments',
'RepliesAndMentions',
'Mute'
);
ALTER TABLE post_actions
ADD COLUMN notifications post_notifications_mode_enum;
CREATE TYPE community_notifications_mode_enum AS enum (
'AllPostsAndComments',
'AllPosts',
'RepliesAndMentions',
'Mute'
);
ALTER TABLE community_actions
ADD COLUMN notifications community_notifications_mode_enum;

View file

@ -20,6 +20,7 @@ use lemmy_api::{
random::get_random_community, random::get_random_community,
tag::{create_community_tag, delete_community_tag, update_community_tag}, tag::{create_community_tag, delete_community_tag, update_community_tag},
transfer::transfer_community, transfer::transfer_community,
update_notifications::update_community_notifications,
}, },
local_user::{ local_user::{
add_admin::add_admin, add_admin::add_admin,
@ -41,11 +42,9 @@ use lemmy_api::{
logout::logout, logout::logout,
note_person::user_note_person, note_person::user_note_person,
notifications::{ notifications::{
list_inbox::list_inbox, list::list_notifications,
mark_all_read::mark_all_notifications_read, mark_all_read::mark_all_notifications_read,
mark_comment_mention_read::mark_comment_mention_as_read, mark_notification_read::mark_notification_as_read,
mark_post_mention_read::mark_post_mention_as_read,
mark_reply_read::mark_reply_as_read,
unread_count::unread_count, unread_count::unread_count,
}, },
report_count::report_count, report_count::report_count,
@ -67,8 +66,8 @@ use lemmy_api::{
mark_many_read::mark_posts_as_read, mark_many_read::mark_posts_as_read,
mark_read::mark_post_as_read, mark_read::mark_post_as_read,
save::save_post, save::save_post,
update_notifications::update_post_notifications,
}, },
private_message::mark_read::mark_pm_as_read,
reports::{ reports::{
comment_report::{create::create_comment_report, resolve::resolve_comment_report}, comment_report::{create::create_comment_report, resolve::resolve_comment_report},
community_report::{create::create_community_report, resolve::resolve_community_report}, community_report::{create::create_community_report, resolve::resolve_community_report},
@ -247,6 +246,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimit) {
.route("/tag", post().to(create_community_tag)) .route("/tag", post().to(create_community_tag))
.route("/tag", put().to(update_community_tag)) .route("/tag", put().to(update_community_tag))
.route("/tag", delete().to(delete_community_tag)) .route("/tag", delete().to(delete_community_tag))
.route("/notifications", post().to(update_community_notifications))
.service( .service(
scope("/pending_follows") scope("/pending_follows")
.route("/count", get().to(get_pending_follows_count)) .route("/count", get().to(get_pending_follows_count))
@ -294,7 +294,8 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimit) {
.route("/like/list", get().to(list_post_likes)) .route("/like/list", get().to(list_post_likes))
.route("/save", put().to(save_post)) .route("/save", put().to(save_post))
.route("/report", post().to(create_post_report)) .route("/report", post().to(create_post_report))
.route("/report/resolve", put().to(resolve_post_report)), .route("/report/resolve", put().to(resolve_post_report))
.route("/notifications", post().to(update_post_notifications)),
) )
// Comment // Comment
.service( .service(
@ -310,7 +311,6 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimit) {
.route("", put().to(update_comment)) .route("", put().to(update_comment))
.route("/delete", post().to(delete_comment)) .route("/delete", post().to(delete_comment))
.route("/remove", post().to(remove_comment)) .route("/remove", post().to(remove_comment))
.route("/mark_as_read", post().to(mark_reply_as_read))
.route("/distinguish", post().to(distinguish_comment)) .route("/distinguish", post().to(distinguish_comment))
.route("/like", post().to(like_comment)) .route("/like", post().to(like_comment))
.route("/like/list", get().to(list_comment_likes)) .route("/like/list", get().to(list_comment_likes))
@ -326,7 +326,6 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimit) {
.route("", post().to(create_private_message)) .route("", post().to(create_private_message))
.route("", put().to(update_private_message)) .route("", put().to(update_private_message))
.route("/delete", post().to(delete_private_message)) .route("/delete", post().to(delete_private_message))
.route("/mark_as_read", post().to(mark_pm_as_read))
.route("/report", post().to(create_pm_report)) .route("/report", post().to(create_pm_report))
.route("/report/resolve", put().to(resolve_pm_report)), .route("/report/resolve", put().to(resolve_pm_report)),
) )
@ -364,17 +363,10 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimit) {
.route("", delete().to(delete_image)) .route("", delete().to(delete_image))
.route("/list", get().to(list_media)), .route("/list", get().to(list_media)),
) )
.route("/inbox", get().to(list_inbox)) .route("/notifications", get().to(list_notifications))
.route("/delete", post().to(delete_account)) .route("/delete", post().to(delete_account))
.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("/mark_as_read/all", post().to(mark_all_notifications_read))
.route("/mark_as_read", post().to(mark_notification_as_read))
.route("/report_count", get().to(report_count)) .route("/report_count", get().to(report_count))
.route("/unread_count", get().to(unread_count)) .route("/unread_count", get().to(unread_count))
.route("/list_logins", get().to(list_logins)) .route("/list_logins", get().to(list_logins))