feat: add email notification for post like

This commit is contained in:
Krispire Admin
2026-06-09 16:57:12 +05:30
parent e9299051f5
commit d86d876ac1
6 changed files with 165 additions and 30 deletions
+72 -17
View File
@@ -18,6 +18,7 @@ import (
"fmt"
"html/template"
"strings"
"time"
"github.com/prabhupadasays/spsays-bff/internal/model"
"github.com/prabhupadasays/spsays-bff/internal/repository"
@@ -83,6 +84,16 @@ func (s *NotificationService) MarkAllRead(ctx context.Context, userID string) er
return s.notifs.MarkAllRead(ctx, userID)
}
// LikeEmailPref / SetLikeEmailPref back the Settings → Notifications toggle
// for aggregated like emails.
func (s *NotificationService) LikeEmailPref(ctx context.Context, userID string) (bool, error) {
return s.notifs.LikeEmailsEnabled(ctx, userID)
}
func (s *NotificationService) SetLikeEmailPref(ctx context.Context, userID string, enabled bool) error {
return s.notifs.SetLikeEmailsEnabled(ctx, userID, enabled)
}
// NotifyFollowRequest tells `recipientID` that `actorID` asked to follow
// them. Persists the row and emails the recipient (if they have an email
// on file).
@@ -107,13 +118,13 @@ func (s *NotificationService) NotifyPostLike(ctx context.Context, postID, author
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)
// Aggregate into the single (author, post) like-notification row. Returns
// the new liker count and whether an aggregated email is due (throttled).
count, emailDue, err := s.notifs.UpsertLike(ctx, authorID, likerID, postID, likeEmailThrottle)
if err != nil {
s.logger.Warn("aggregate like notification failed",
zap.String("recipient", authorID), zap.Error(err))
count = 1
count, emailDue = 1, false
}
actorUser, _ := s.users.FindByID(ctx, likerID)
@@ -136,24 +147,51 @@ func (s *NotificationService) NotifyPostLike(ctx context.Context, postID, author
}
}
// 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
}
// Tapping a notification opens the author's own feed anchored to the post.
author, _ := s.users.FindByID(ctx, authorID)
url := s.frontendURL + "/notifications"
if author, err := s.users.FindByID(ctx, authorID); err == nil && author != nil {
if 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,
})
// Web Push — tagged per-post so repeated likes collapse into one.
if s.push != nil {
s.push.SendToUser(ctx, authorID, PushPayload{
Title: "New Like",
Body: body,
URL: url,
Icon: s.iconURL(),
Tag: NotifPostLike + ":" + postID,
})
}
// Aggregated email — only when the throttle says one is due (so a burst of
// likes yields a single email), the author has an address, and they
// haven't opted out. `body` already aggregates ("X and N others …").
if !emailDue || author == nil || author.Email == nil || strings.TrimSpace(*author.Email) == "" {
return
}
if enabled, perr := s.notifs.LikeEmailsEnabled(ctx, authorID); perr != nil || !enabled {
return
}
var buf bytes.Buffer
if err := likeEmailTpl.Execute(&buf, map[string]any{
"RecipientName": author.Name,
"Message": body,
"URL": url,
}); err != nil {
s.logger.Warn("render like email failed", zap.Error(err))
return
}
if err := s.mail.Send(strings.TrimSpace(*author.Email), "New likes on your post — Prabhupāda Says", buf.String()); err != nil {
s.logger.Warn("like email send failed", zap.String("to", *author.Email), zap.Error(err))
}
}
// likeEmailThrottle is the minimum gap between aggregated like emails for a
// single post — a burst of likes within the window produces one email.
const likeEmailThrottle = 6 * time.Hour
// 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 {
@@ -402,3 +440,20 @@ var reminderEmailTpl = template.Must(template.New("reminder").Parse(`<div style=
<p style="margin-top:1em">Your servants,<br>
<strong>The Prabhupāda Says team</strong></p>
</div>`))
// Aggregated like email. Message is "X and N others liked your post"; the CTA
// opens the author's post.
var likeEmailTpl = template.Must(template.New("postLike").Parse(`<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#2a2118;max-width:560px;margin:0 auto;font-size:17px;line-height:1.7">
<p style="font-size:18px;color:#9a6b2f;letter-spacing:0.04em;margin-bottom:1em">Hare Kṛṣṇa{{if .RecipientName}}, {{.RecipientName}}{{end}} 🙏</p>
<p>{{.Message}}.</p>
<p style="margin:1.25em 0">
<a href="{{.URL}}" style="display:inline-block;background:#9a6b2f;color:#fff;text-decoration:none;padding:0.6em 1.3em;border-radius:8px;font-weight:600">View your post</a>
</p>
<p style="margin-top:1.5em;font-size:14px;color:#7a6a55">You can turn off like emails any time in Settings → Notifications.</p>
<p style="margin-top:1em">Your servants,<br>
<strong>The Prabhupāda Says team</strong></p>
</div>`))