feat: add post like

This commit is contained in:
Krispire Admin
2026-06-09 16:28:40 +05:30
parent da2bbc323d
commit e9299051f5
12 changed files with 658 additions and 8 deletions
+72
View File
@@ -15,6 +15,7 @@ package service
import (
"bytes"
"context"
"fmt"
"html/template"
"strings"
@@ -29,6 +30,7 @@ import (
const (
NotifFollowRequest = "follow_request"
NotifFollowAccepted = "follow_accepted"
NotifPostLike = "post_like"
// Reminder kinds are delivered live (email + push + a websocket toast)
// but NOT persisted — they're ephemeral nudges, not history.
@@ -96,6 +98,76 @@ func (s *NotificationService) NotifyFollowAccepted(ctx context.Context, recipien
"%s accepted your follow request — Prabhupāda Says")
}
// NotifyPostLike tells the post author that `likerID` liked their post. Sent
// only when the liker isn't the author (callers also guard). Unlike follows,
// likes are high-volume, so this delivers a persisted row + realtime nudge +
// Web Push but NO email. The push is tagged per-post so repeated likes on the
// same post collapse into one notification rather than stacking.
func (s *NotificationService) NotifyPostLike(ctx context.Context, postID, authorID, likerID string) {
if authorID == "" || likerID == "" || authorID == likerID {
return
}
// Aggregate into the single (author, post) like-notification row, getting
// back how many likers it now represents.
count, err := s.notifs.UpsertLike(ctx, authorID, likerID, postID)
if err != nil {
s.logger.Warn("aggregate like notification failed",
zap.String("recipient", authorID), zap.Error(err))
count = 1
}
actorUser, _ := s.users.FindByID(ctx, likerID)
who := "Someone"
if actorUser != nil {
who = actorDisplay(actorUser)
}
body := likeMessage(who, count)
// Realtime nudge — carry actor_count so the toast can read "and N others".
if s.rt != nil {
payload := map[string]any{"kind": NotifPostLike, "actor_count": count}
if actorUser != nil {
payload["actor_name"] = actorDisplay(actorUser)
payload["actor_handle"] = actorUser.Handle
payload["actor_avatar_url"] = actorUser.AvatarURL
}
if err := s.rt.Trigger(ctx, "notifications."+authorID, "notification", payload); err != nil {
s.logger.Warn("realtime like publish failed", zap.String("recipient", authorID), zap.Error(err))
}
}
// Web Push — tapping opens the author's own feed anchored to the post.
// Tagged per-post so repeated likes collapse into one notification.
if s.push == nil {
return
}
url := s.frontendURL + "/notifications"
if author, err := s.users.FindByID(ctx, authorID); err == nil && author != nil {
url = s.frontendURL + "/u/" + author.Handle + "/posts#post-" + postID
}
s.push.SendToUser(ctx, authorID, PushPayload{
Title: "New Like",
Body: body,
URL: url,
Icon: s.iconURL(),
Tag: NotifPostLike + ":" + postID,
})
}
// 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 {
if count <= 1 {
return who + " liked your post"
}
others := count - 1
noun := "others"
if others == 1 {
noun = "other"
}
return fmt.Sprintf("%s and %d %s liked your post", who, others, noun)
}
// dispatch persists the notification and sends the matching email. All
// failures are logged, never propagated.
func (s *NotificationService) dispatch(ctx context.Context, recipientID, actorID, kind, subjectFmt string) {