diff --git a/api/api/v1/accounts/update_credentials/index.ts b/api/api/v1/accounts/update_credentials/index.ts index 2c3fa4ba..a9ed466d 100644 --- a/api/api/v1/accounts/update_credentials/index.ts +++ b/api/api/v1/accounts/update_credentials/index.ts @@ -1,4 +1,5 @@ import { apiRoute, applyConfig, auth, jsonOrForm } from "@/api"; +import { mergeAndDeduplicate } from "@/lib"; import { sanitizedHtmlStrip } from "@/sanitization"; import { createRoute } from "@hono/zod-openapi"; import { Attachment, Emoji, User } from "@versia/kit/db"; @@ -335,14 +336,10 @@ export default apiRoute((app) => await Emoji.parseFromText(sanitizedDisplayName); const noteEmojis = await Emoji.parseFromText(self.note); - const emojis = [ - ...displaynameEmojis, - ...noteEmojis, - ...fieldEmojis, - ].filter( - // Deduplicate emojis - (emoji, index, self) => - self.findIndex((e) => e.id === emoji.id) === index, + const emojis = mergeAndDeduplicate( + displaynameEmojis, + noteEmojis, + fieldEmojis, ); // Connect emojis, if any diff --git a/api/api/v1/statuses/:id/reblog.ts b/api/api/v1/statuses/:id/reblog.ts index 8a09feea..09ec4a9c 100644 --- a/api/api/v1/statuses/:id/reblog.ts +++ b/api/api/v1/statuses/:id/reblog.ts @@ -1,6 +1,6 @@ import { apiRoute, applyConfig, auth, jsonOrForm } from "@/api"; import { createRoute } from "@hono/zod-openapi"; -import { Note, Notification } from "@versia/kit/db"; +import { Note } from "@versia/kit/db"; import { Notes, RolePermissions } from "@versia/kit/tables"; import { and, eq } from "drizzle-orm"; import { z } from "zod"; @@ -138,12 +138,7 @@ export default apiRoute((app) => } if (note.author.isLocal() && user.isLocal()) { - await Notification.insert({ - accountId: user.id, - notifiedId: note.author.id, - type: "reblog", - noteId: newReblog.data.reblogId, - }); + await note.author.createNotification("reblog", user, newReblog); } return context.json(await finalNewReblog.toApi(user), 201); diff --git a/classes/database/note.ts b/classes/database/note.ts index 9778471e..894ba029 100644 --- a/classes/database/note.ts +++ b/classes/database/note.ts @@ -1,5 +1,6 @@ import { idValidator } from "@/api"; import { localObjectUri } from "@/constants"; +import { mergeAndDeduplicate } from "@/lib.ts"; import { sanitizedHtmlStrip } from "@/sanitization"; import { sentry } from "@/sentry"; import { getLogger } from "@logtape/logtape"; @@ -13,7 +14,7 @@ import type { Delete as VersiaDelete, Note as VersiaNote, } from "@versia/federation/types"; -import { Instance, Notification, db } from "@versia/kit/db"; +import { Instance, db } from "@versia/kit/db"; import { Attachments, EmojiToNote, @@ -364,14 +365,12 @@ export class Note extends BaseInterface { }, ); - const fusedUsers = [...mentionedUsers, ...usersThatCanSeePost]; - - const deduplicatedUsersById = fusedUsers.filter( - (user, index, self) => - index === self.findIndex((t) => t.id === user.id), + const fusedUsers = mergeAndDeduplicate( + mentionedUsers, + usersThatCanSeePost, ); - return deduplicatedUsersById; + return fusedUsers; } public get author(): User { @@ -452,22 +451,13 @@ export class Note extends BaseInterface { data.content["text/plain"]?.content ?? Object.entries(data.content)[0][1].content; - const parsedMentions = [ - ...(data.mentions ?? []), - ...(await parseTextMentions(plaintextContent, data.author)), - // Deduplicate by .id - ].filter( - (mention, index, self) => - index === self.findIndex((t) => t.id === mention.id), + const parsedMentions = mergeAndDeduplicate( + data.mentions ?? [], + await parseTextMentions(plaintextContent, data.author), ); - - const parsedEmojis = [ - ...(data.emojis ?? []), - ...(await Emoji.parseFromText(plaintextContent)), - // Deduplicate by .id - ].filter( - (emoji, index, self) => - index === self.findIndex((t) => t.id === emoji.id), + const parsedEmojis = mergeAndDeduplicate( + data.emojis ?? [], + await Emoji.parseFromText(plaintextContent), ); const htmlContent = await contentToHtml(data.content, parsedMentions); @@ -500,14 +490,13 @@ export class Note extends BaseInterface { await newNote.updateAttachments(data.mediaAttachments ?? []); // Send notifications for mentioned local users - for (const mention of parsedMentions ?? []) { + for (const mention of parsedMentions) { if (mention.isLocal()) { - await Notification.insert({ - accountId: data.author.id, - notifiedId: mention.id, - type: "mention", - noteId: newNote.id, - }); + await mention.createNotification( + "mention", + data.author, + newNote, + ); } } @@ -540,26 +529,15 @@ export class Note extends BaseInterface { Object.entries(data.content)[0][1].content) : undefined; - const parsedMentions = [ - ...(data.mentions ?? []), - ...(plaintextContent + const parsedMentions = mergeAndDeduplicate( + data.mentions ?? [], + plaintextContent ? await parseTextMentions(plaintextContent, data.author) - : []), - // Deduplicate by .id - ].filter( - (mention, index, self) => - index === self.findIndex((t) => t.id === mention.id), + : [], ); - - const parsedEmojis = [ - ...(data.emojis ?? []), - ...(plaintextContent - ? await Emoji.parseFromText(plaintextContent) - : []), - // Deduplicate by .id - ].filter( - (emoji, index, self) => - index === self.findIndex((t) => t.id === emoji.id), + const parsedEmojis = mergeAndDeduplicate( + data.emojis ?? [], + plaintextContent ? await Emoji.parseFromText(plaintextContent) : [], ); const htmlContent = data.content @@ -608,18 +586,12 @@ export class Note extends BaseInterface { return; } - // Fuse and deduplicate - const fusedEmojis = emojis.filter( - (emoji, index, self) => - index === self.findIndex((t) => t.id === emoji.id), - ); - // Connect emojis await db .delete(EmojiToNote) .where(eq(EmojiToNote.noteId, this.data.id)); await db.insert(EmojiToNote).values( - fusedEmojis.map((emoji) => ({ + emojis.map((emoji) => ({ emojiId: emoji.id, noteId: this.data.id, })), diff --git a/classes/database/user.ts b/classes/database/user.ts index 0226f235..15bf6f21 100644 --- a/classes/database/user.ts +++ b/classes/database/user.ts @@ -275,11 +275,10 @@ export class User extends BaseInterface { senderId: this.id, }); } else { - await Notification.insert({ - accountId: this.id, - type: otherUser.data.isLocked ? "follow_request" : "follow", - notifiedId: otherUser.id, - }); + await otherUser.createNotification( + otherUser.data.isLocked ? "follow_request" : "follow", + this, + ); } return foundRelationship; @@ -295,20 +294,6 @@ export class User extends BaseInterface { recipientId: followee.id, senderId: this.id, }); - } else if (!this.data.isLocked) { - if (relationship.data.following) { - await Notification.insert({ - accountId: followee.id, - type: "unfollow", - notifiedId: this.id, - }); - } else { - await Notification.insert({ - accountId: followee.id, - type: "cancel-follow", - notifiedId: this.id, - }); - } } await relationship.update({ @@ -526,12 +511,7 @@ export class User extends BaseInterface { if (this.isLocal() && note.author.isLocal()) { // Notify the user that their post has been favourited - await Notification.insert({ - accountId: this.id, - type: "favourite", - notifiedId: note.author.id, - noteId: note.id, - }); + await note.author.createNotification("favourite", this, note); } else if (this.isLocal() && note.author.isRemote()) { // Federate the like this.federateToFollowers(newLike.toVersia()); @@ -567,6 +547,19 @@ export class User extends BaseInterface { } } + public async createNotification( + type: "mention" | "follow_request" | "follow" | "favourite" | "reblog", + relatedUser: User, + note?: Note, + ): Promise { + await Notification.insert({ + accountId: relatedUser.id, + type, + notifiedId: this.id, + noteId: note?.id ?? null, + }); + } + public async clearAllNotifications(): Promise { await db .update(Notifications) diff --git a/classes/inbox/processor.ts b/classes/inbox/processor.ts index eb570eb4..86e27737 100644 --- a/classes/inbox/processor.ts +++ b/classes/inbox/processor.ts @@ -15,14 +15,7 @@ import type { Note as VersiaNote, User as VersiaUser, } from "@versia/federation/types"; -import { - Instance, - Like, - Note, - Notification, - Relationship, - User, -} from "@versia/kit/db"; +import { Instance, Like, Note, Relationship, User } from "@versia/kit/db"; import { Likes, Notes } from "@versia/kit/tables"; import type { SocketAddress } from "bun"; import chalk from "chalk"; @@ -340,11 +333,10 @@ export class InboxProcessor { languages: [], }); - await Notification.insert({ - accountId: author.id, - type: followee.data.isLocked ? "follow_request" : "follow", - notifiedId: followee.id, - }); + await followee.createNotification( + followee.data.isLocked ? "follow_request" : "follow", + author, + ); if (!followee.data.isLocked) { await followee.sendFollowAccept(author); diff --git a/utils/lib.ts b/utils/lib.ts new file mode 100644 index 00000000..a5cb0dac --- /dev/null +++ b/utils/lib.ts @@ -0,0 +1,11 @@ +type ElementWithId = { id: string }; + +export const mergeAndDeduplicate = ( + ...elements: T[][] +): T[] => + elements + .reduce((acc, val) => acc.concat(val), []) + .filter( + (element, index, self) => + index === self.findIndex((t) => t.id === element.id), + );