feat: add inline mentions in comments

This commit is contained in:
Krispire Admin
2026-06-13 13:10:05 +05:30
parent 05567ad438
commit 2135ec334c
2 changed files with 67 additions and 1 deletions
+37
View File
@@ -4,12 +4,41 @@ import (
"context"
"errors"
"fmt"
"regexp"
"strings"
"github.com/prabhupadasays/spsays-bff/internal/model"
"github.com/prabhupadasays/spsays-bff/internal/repository"
)
// mentionHandleRe matches @handle tokens. The leading boundary (line start or
// a non-handle character that isn't itself "@") keeps it from firing inside
// emails (foo@bar) or "@@".
var mentionHandleRe = regexp.MustCompile(`(^|[^A-Za-z0-9_@])@([A-Za-z0-9_]{1,30})`)
// mentionHandles pulls unique @handles out of a comment body (capped, so a
// pathological body can't fan out unboundedly).
func mentionHandles(body string) []string {
matches := mentionHandleRe.FindAllStringSubmatch(body, -1)
if matches == nil {
return nil
}
seen := map[string]bool{}
out := make([]string, 0, len(matches))
for _, m := range matches {
key := strings.ToLower(m[2])
if seen[key] {
continue
}
seen[key] = true
out = append(out, m[2])
if len(out) >= 20 {
break
}
}
return out
}
// maxCommentLen caps a comment/reply body.
const maxCommentLen = 2000
@@ -87,6 +116,14 @@ func (s *CommentService) Add(ctx context.Context, viewerID, postID string, paren
go s.notif.NotifyPostComment(context.Background(), postID, notifyUserID, viewerID)
}
}
// Ping anyone @mentioned in the body — skipping the actor and whoever was
// already notified above (post/parent author) so they aren't double-pinged.
if s.notif != nil {
if handles := mentionHandles(body); len(handles) > 0 {
go s.notif.NotifyCommentMentions(context.Background(), postID, viewerID, handles, notifyUserID)
}
}
return c, nil
}
+30 -1
View File
@@ -37,6 +37,7 @@ const (
NotifPostMention = "post_mention"
NotifPostComment = "post_comment"
NotifCommentReply = "comment_reply"
NotifCommentMention = "comment_mention"
// Reminder kinds are delivered live (email + push + a websocket toast)
// but NOT persisted — they're ephemeral nudges, not history.
@@ -394,8 +395,11 @@ func (s *NotificationService) notifyComment(ctx context.Context, kind, postID, r
url = s.frontendURL + "/u/" + h + "/posts#post-" + postID
}
title, body := "New comment", who+" commented on your post"
if kind == NotifCommentReply {
switch kind {
case NotifCommentReply:
title, body = "New reply", who+" replied to your comment"
case NotifCommentMention:
title, body = "You were mentioned", who+" mentioned you in a comment"
}
s.push.SendToUser(ctx, recipientID, PushPayload{
Title: title,
@@ -406,6 +410,31 @@ func (s *NotificationService) notifyComment(ctx context.Context, kind, postID, r
})
}
// NotifyCommentMentions tells each user named by `handles` that `actorID`
// mentioned them in a comment on `postID`. Handles are resolved to users,
// deduped, and the actor plus any `skip` ids (e.g. the post/parent author who
// already got a comment/reply notification) are excluded so nobody is
// double-pinged. Reuses the comment notification path (row + realtime + push).
func (s *NotificationService) NotifyCommentMentions(ctx context.Context, postID, actorID string, handles []string, skip ...string) {
if actorID == "" || len(handles) == 0 {
return
}
skipSet := map[string]bool{actorID: true}
for _, id := range skip {
if id != "" {
skipSet[id] = true
}
}
for _, h := range handles {
u, err := s.users.FindByHandle(ctx, h)
if err != nil || u == nil || skipSet[u.ID] {
continue
}
skipSet[u.ID] = true // dedupe repeats of the same mention
s.notifyComment(ctx, NotifCommentMention, postID, u.ID, actorID)
}
}
// likeMessage renders the aggregated like text: "X liked your post" or
// "X and N others liked your post".
func likeMessage(who string, count int) string {