diff --git a/apps/web/app/api/v1/recipes/[id]/comments/route.ts b/apps/web/app/api/v1/recipes/[id]/comments/route.ts index e109edb..0b11d60 100644 --- a/apps/web/app/api/v1/recipes/[id]/comments/route.ts +++ b/apps/web/app/api/v1/recipes/[id]/comments/route.ts @@ -1,12 +1,13 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { db, recipes, comments, users, userBlocks, eq, and } from "@epicure/db"; +import { db, recipes, comments, users, userBlocks, eq, and, inArray } from "@epicure/db"; import { requireSession } from "@/lib/api-auth"; import { applyRateLimit } from "@/lib/rate-limit"; import { dispatchWebhook } from "@/lib/webhooks"; import { sendPushNotification } from "@/lib/push"; import { createNotification } from "@/lib/notifications"; import { isBlockedEitherWay } from "@/lib/blocks"; +import { extractMentionedUsernames } from "@/lib/mentions"; const Schema = z.object({ content: z.string().min(1).max(5000), @@ -107,5 +108,18 @@ export async function POST(req: NextRequest, { params }: Params) { void createNotification({ userId: recipe.authorId, type: "comment", actorId: session!.user.id, recipeId: id, commentId }); } + const mentionedUsernames = extractMentionedUsernames(parsed.data.content); + if (mentionedUsernames.length > 0) { + const alreadyNotified = new Set([recipe.authorId, parent?.userId].filter(Boolean)); + const mentionedUsers = await db + .select({ id: users.id, username: users.username }) + .from(users) + .where(inArray(users.username, mentionedUsernames)); + for (const mentioned of mentionedUsers) { + if (alreadyNotified.has(mentioned.id)) continue; + void createNotification({ userId: mentioned.id, type: "mention", actorId: session!.user.id, recipeId: id, commentId }); + } + } + return NextResponse.json({ id: commentId }, { status: 201 }); } diff --git a/apps/web/components/social/comments-section.tsx b/apps/web/components/social/comments-section.tsx index bd17a07..f2d69d1 100644 --- a/apps/web/components/social/comments-section.tsx +++ b/apps/web/components/social/comments-section.tsx @@ -1,6 +1,7 @@ "use client"; -import { useState, useEffect, useCallback, useMemo } from "react"; +import { useState, useEffect, useCallback, useMemo, Fragment } from "react"; +import Link from "next/link"; import { MessageCircle, Reply, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { useTranslations } from "next-intl"; @@ -24,6 +25,25 @@ type Comment = { }; const MAX_VISUAL_INDENT = 4; +const MENTION_REGEX = /@([a-z0-9_-]{3,30})/gi; + +function renderContentWithMentions(content: string) { + const parts = content.split(MENTION_REGEX); + // split() with a capturing group interleaves [text, username, text, username, ...text] + return parts.map((part, i) => + i % 2 === 1 ? ( + + @{part} + + ) : ( + {part} + ) + ); +} function timeAgo(dateStr: string) { const diff = Date.now() - new Date(dateStr).getTime(); @@ -126,7 +146,7 @@ function CommentItem({ {comment.userName} {timeAgo(comment.createdAt)} -

{comment.content}

+

{renderContentWithMentions(comment.content)}

{currentUserId && ( diff --git a/apps/web/lib/mentions.ts b/apps/web/lib/mentions.ts new file mode 100644 index 0000000..b9a00d6 --- /dev/null +++ b/apps/web/lib/mentions.ts @@ -0,0 +1,11 @@ +const MENTION_REGEX = /@([a-z0-9_-]{3,30})/gi; + +export function extractMentionedUsernames(content: string): string[] { + const matches = content.matchAll(MENTION_REGEX); + const usernames = new Set(); + for (const m of matches) { + const username = m[1]; + if (username) usernames.add(username.toLowerCase()); + } + return [...usernames]; +} diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 92c9686..cae5c70 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -25,7 +25,8 @@ "comment": "{name} commented on your recipe", "reply": "{name} replied to your comment", "reaction": "{name} reacted to your comment", - "rating": "{name} rated your recipe" + "rating": "{name} rated your recipe", + "mention": "{name} mentioned you in a comment" }, "recipe": { "share": "Share", diff --git a/apps/web/messages/fr.json b/apps/web/messages/fr.json index 1bbdc35..4d9a656 100644 --- a/apps/web/messages/fr.json +++ b/apps/web/messages/fr.json @@ -25,7 +25,8 @@ "comment": "{name} a commenté votre recette", "reply": "{name} a répondu à votre commentaire", "reaction": "{name} a réagi à votre commentaire", - "rating": "{name} a noté votre recette" + "rating": "{name} a noté votre recette", + "mention": "{name} vous a mentionné dans un commentaire" }, "recipe": { "share": "Partager",