feat: add inline mentions in comments
This commit is contained in:
@@ -4,12 +4,41 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/prabhupadasays/spsays-bff/internal/model"
|
"github.com/prabhupadasays/spsays-bff/internal/model"
|
||||||
"github.com/prabhupadasays/spsays-bff/internal/repository"
|
"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.
|
// maxCommentLen caps a comment/reply body.
|
||||||
const maxCommentLen = 2000
|
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)
|
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
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const (
|
|||||||
NotifPostMention = "post_mention"
|
NotifPostMention = "post_mention"
|
||||||
NotifPostComment = "post_comment"
|
NotifPostComment = "post_comment"
|
||||||
NotifCommentReply = "comment_reply"
|
NotifCommentReply = "comment_reply"
|
||||||
|
NotifCommentMention = "comment_mention"
|
||||||
|
|
||||||
// Reminder kinds are delivered live (email + push + a websocket toast)
|
// Reminder kinds are delivered live (email + push + a websocket toast)
|
||||||
// but NOT persisted — they're ephemeral nudges, not history.
|
// 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
|
url = s.frontendURL + "/u/" + h + "/posts#post-" + postID
|
||||||
}
|
}
|
||||||
title, body := "New comment", who+" commented on your post"
|
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"
|
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{
|
s.push.SendToUser(ctx, recipientID, PushPayload{
|
||||||
Title: title,
|
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
|
// likeMessage renders the aggregated like text: "X liked your post" or
|
||||||
// "X and N others liked your post".
|
// "X and N others liked your post".
|
||||||
func likeMessage(who string, count int) string {
|
func likeMessage(who string, count int) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user