Use `||` instead of `??` so empty strings also fall back to email. The nullish coalescing operator (`??`) only catches null/undefined, so users with an empty name string would appear nameless in emails.
134 lines
3.9 KiB
TypeScript
134 lines
3.9 KiB
TypeScript
import { env } from "next-runtime-env";
|
|
|
|
import type { dbClient } from "@kan/db/client";
|
|
import * as cardRepo from "@kan/db/repository/card.repo";
|
|
import * as memberRepo from "@kan/db/repository/member.repo";
|
|
import * as notificationRepo from "@kan/db/repository/notification.repo";
|
|
import * as userRepo from "@kan/db/repository/user.repo";
|
|
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
|
import { sendEmail } from "@kan/email";
|
|
import { parseMentionsFromHTML } from "@kan/shared/utils";
|
|
|
|
/**
|
|
* Sends mention notification emails to mentioned members
|
|
* Only sends emails for new mentions (checks notification table to avoid duplicates)
|
|
*/
|
|
export async function sendMentionEmails({
|
|
db,
|
|
cardPublicId,
|
|
commentHtml,
|
|
commenterUserId,
|
|
commentId,
|
|
}: {
|
|
db: dbClient;
|
|
cardPublicId: string;
|
|
commentHtml: string;
|
|
commenterUserId: string;
|
|
commentId?: number;
|
|
}) {
|
|
try {
|
|
// Parse mentions from HTML
|
|
const mentionPublicIds = parseMentionsFromHTML(commentHtml);
|
|
if (mentionPublicIds.length === 0) return;
|
|
|
|
// Get card with board information
|
|
const card = await cardRepo.getWithListAndMembersByPublicId(db, cardPublicId);
|
|
if (!card?.list.board) return;
|
|
|
|
const board = card.list.board;
|
|
const boardName = board.name;
|
|
const cardTitle = card.title;
|
|
const cardId = card.id;
|
|
|
|
// Get workspace ID from workspace publicId
|
|
const workspace = await workspaceRepo.getByPublicId(
|
|
db,
|
|
board.workspace.publicId,
|
|
);
|
|
if (!workspace?.id) return;
|
|
|
|
const workspaceId = workspace.id;
|
|
|
|
// Get commenter information
|
|
const commenter = await userRepo.getById(db, commenterUserId);
|
|
if (!commenter) return;
|
|
|
|
const commenterName = commenter.name?.trim() || commenter.email;
|
|
|
|
// Get mentioned members with full details (filtered by workspace)
|
|
const membersWithDetails = await memberRepo.getByPublicIdsWithUsers(
|
|
db,
|
|
mentionPublicIds,
|
|
workspaceId,
|
|
);
|
|
|
|
// Filter out the commenter
|
|
const membersToNotify = membersWithDetails.filter(
|
|
(member) => member.user?.id !== commenterUserId,
|
|
);
|
|
|
|
if (membersToNotify.length === 0) return;
|
|
|
|
const baseUrl = env("NEXT_PUBLIC_BASE_URL");
|
|
const cardUrl = `${baseUrl}/cards/${cardPublicId}`;
|
|
|
|
// Send emails to all mentioned members (only if notification doesn't exist)
|
|
await Promise.all(
|
|
membersToNotify.map(async (member) => {
|
|
const userId = member.user?.id;
|
|
const email = member.user?.email ?? member.email;
|
|
|
|
// Skip pending members (no userId) - they can be mentioned but won't receive emails
|
|
if (!userId || !email) return;
|
|
|
|
try {
|
|
// Check if notification already exists for this mention
|
|
const notificationExists = await notificationRepo.exists(db, {
|
|
userId,
|
|
cardId,
|
|
type: "mention",
|
|
});
|
|
|
|
// If notification already exists, skip sending email
|
|
if (notificationExists) {
|
|
return;
|
|
}
|
|
|
|
// Create notification record
|
|
await notificationRepo.create(db, {
|
|
type: "mention",
|
|
userId,
|
|
cardId,
|
|
commentId,
|
|
});
|
|
|
|
// Send email
|
|
await sendEmail(
|
|
email,
|
|
`${commenterName} mentioned you in a comment on ${cardTitle}`,
|
|
"MENTION",
|
|
{
|
|
commenterName,
|
|
boardName,
|
|
cardTitle,
|
|
cardUrl,
|
|
},
|
|
);
|
|
} catch (error) {
|
|
console.error("Failed to send mention email:", {
|
|
email,
|
|
cardPublicId,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
}),
|
|
);
|
|
} catch (error) {
|
|
console.error("Error sending mention emails:", {
|
|
cardPublicId,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
}
|
|
|