diff --git a/packages/api/src/routers/card.ts b/packages/api/src/routers/card.ts
index b39256c5..949804d1 100644
--- a/packages/api/src/routers/card.ts
+++ b/packages/api/src/routers/card.ts
@@ -10,6 +10,7 @@ import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import { mergeActivities } from "../utils/activities";
+import { sendMentionEmails } from "../utils/notifications";
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
import { generateAttachmentUrl, generateAvatarUrl } from "@kan/shared/utils";
@@ -153,6 +154,17 @@ export const cardRouter = createTRPCRouter({
await cardActivityRepo.bulkCreate(ctx.db, cardActivitesInsert);
}
+ if (input.description) {
+ sendMentionEmails({
+ db: ctx.db,
+ cardPublicId: newCard.publicId,
+ commentHtml: input.description,
+ commenterUserId: userId,
+ }).catch((error) => {
+ console.error("Failed to send mention emails:", error);
+ });
+ }
+
return newCard;
}),
addComment: protectedProcedure
@@ -215,6 +227,16 @@ export const cardRouter = createTRPCRouter({
createdBy: userId,
});
+ sendMentionEmails({
+ db: ctx.db,
+ cardPublicId: input.cardPublicId,
+ commentHtml: input.comment,
+ commenterUserId: userId,
+ commentId: newComment.id,
+ }).catch((error) => {
+ console.error("Failed to send mention emails:", error);
+ });
+
return newComment;
}),
updateComment: protectedProcedure
@@ -295,6 +317,16 @@ export const cardRouter = createTRPCRouter({
createdBy: userId,
});
+ sendMentionEmails({
+ db: ctx.db,
+ cardPublicId: input.cardPublicId,
+ commentHtml: input.comment,
+ commenterUserId: userId,
+ commentId: updatedComment.id,
+ }).catch((error) => {
+ console.error("Failed to send mention emails:", error);
+ });
+
return updatedComment;
}),
deleteComment: protectedProcedure
@@ -924,6 +956,15 @@ export const cardRouter = createTRPCRouter({
fromDescription: existingCard.description ?? undefined,
toDescription: input.description,
});
+
+ sendMentionEmails({
+ db: ctx.db,
+ cardPublicId: input.cardPublicId,
+ commentHtml: input.description,
+ commenterUserId: userId,
+ }).catch((error) => {
+ console.error("Failed to send mention emails:", error);
+ });
}
if (
diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts
index 0e6ab7d1..28c63eb6 100644
--- a/packages/shared/src/utils/index.ts
+++ b/packages/shared/src/utils/index.ts
@@ -4,3 +4,4 @@ export * from "./subscriptions";
export * from "./email";
export * from "./dueDateFilters";
export * from "./s3";
+export * from "./mentions";
diff --git a/packages/shared/src/utils/mentions.ts b/packages/shared/src/utils/mentions.ts
new file mode 100644
index 00000000..42f16088
--- /dev/null
+++ b/packages/shared/src/utils/mentions.ts
@@ -0,0 +1,22 @@
+/**
+ * Parses mention data-id attributes from HTML content
+ * Mentions are stored as: @label
+ * @param htmlContent - The HTML content to parse
+ * @returns Array of unique mention public IDs
+ */
+export function parseMentionsFromHTML(htmlContent: string): string[] {
+ if (!htmlContent) return [];
+
+ // Match all mention spans with data-id attributes
+ const mentionRegex = /]*data-type="mention"[^>]*data-id="([^"]+)"[^>]*>/gi;
+ const matches = Array.from(htmlContent.matchAll(mentionRegex));
+
+ // Extract unique mention IDs
+ const mentionIds = matches
+ .map((match) => match[1])
+ .filter((id): id is string => !!id && id.length >= 12);
+
+ // Return unique IDs
+ return Array.from(new Set(mentionIds));
+}
+