feat: add post mention

This commit is contained in:
Krispire Admin
2026-06-10 16:28:57 +05:30
parent d86d876ac1
commit cb62adf2de
11 changed files with 472 additions and 34 deletions
+99 -8
View File
@@ -31,14 +31,15 @@ var (
// ---------------------------------------------------------------------------
type PostService struct {
posts *repository.PostRepo
r2 *repository.R2
likes *repository.LikeRepo
notif *NotificationService // optional; set via SetNotifier
posts *repository.PostRepo
r2 *repository.R2
likes *repository.LikeRepo
mentions *repository.MentionRepo
notif *NotificationService // optional; set via SetNotifier
}
func NewPostService(posts *repository.PostRepo, r2 *repository.R2, likes *repository.LikeRepo) *PostService {
return &PostService{posts: posts, r2: r2, likes: likes}
func NewPostService(posts *repository.PostRepo, r2 *repository.R2, likes *repository.LikeRepo, mentions *repository.MentionRepo) *PostService {
return &PostService{posts: posts, r2: r2, likes: likes, mentions: mentions}
}
// SetNotifier wires the notification seam after construction (the
@@ -49,7 +50,7 @@ func (s *PostService) SetNotifier(n *NotificationService) { s.notif = n }
// the frontend to send the exact text the user selected. Source URL must be
// a same-origin path (e.g. /books/bg/1/1?hl=…) — we strip the host if one
// was provided so links can't be hijacked.
func (s *PostService) Create(ctx context.Context, userID, caption, sourceURL, sourceTitle string, imageURLs []string) (*model.Post, error) {
func (s *PostService) Create(ctx context.Context, userID, caption, sourceURL, sourceTitle string, imageURLs, mentionUserIDs []string) (*model.Post, error) {
caption = strings.TrimSpace(caption)
if caption == "" {
return nil, errors.New("caption is required")
@@ -64,7 +65,19 @@ func (s *PostService) Create(ctx context.Context, userID, caption, sourceURL, so
if len(imageURLs) > 5 {
return nil, errors.New("a post can hold up to 5 images")
}
return s.posts.Create(ctx, userID, caption, sourceURL, sourceTitle, imageURLs)
p, err := s.posts.Create(ctx, userID, caption, sourceURL, sourceTitle, imageURLs)
if err != nil {
return nil, err
}
// Tag mentioned users (best-effort, additive — a bad id never fails the
// post itself). Reflect the count on the returned post, then notify the
// tagged people (fire-and-forget on a detached context).
if len(mentionUserIDs) > 0 {
_ = s.mentions.AddMany(ctx, p.ID, mentionUserIDs)
_ = s.mentions.HydrateCounts(ctx, []*model.Post{p})
go s.notifyMentions(context.Background(), p.ID, userID, mentionUserIDs)
}
return p, nil
}
// Get returns one post (public). `viewerID` is "" for anonymous callers;
@@ -81,6 +94,9 @@ func (s *PostService) Get(ctx context.Context, id, viewerID string) (*model.Post
if err := s.likes.Hydrate(ctx, viewerID, []*model.Post{p}); err != nil {
return nil, err
}
if err := s.mentions.HydrateCounts(ctx, []*model.Post{p}); err != nil {
return nil, err
}
return p, nil
}
@@ -183,6 +199,9 @@ func (s *PostService) ListByHandle(ctx context.Context, handle, viewerID string,
if err := s.hydrateLikesSlice(ctx, viewerID, posts); err != nil {
return nil, 0, err
}
if err := s.hydrateMentionsSlice(ctx, posts); err != nil {
return nil, 0, err
}
return posts, total, nil
}
@@ -198,6 +217,75 @@ func (s *PostService) hydrateLikesSlice(ctx context.Context, viewerID string, po
return s.likes.Hydrate(ctx, viewerID, ptrs)
}
// hydrateMentionsSlice fills MentionsCount on a value slice.
func (s *PostService) hydrateMentionsSlice(ctx context.Context, posts []model.Post) error {
if len(posts) == 0 {
return nil
}
ptrs := make([]*model.Post, len(posts))
for i := range posts {
ptrs[i] = &posts[i]
}
return s.mentions.HydrateCounts(ctx, ptrs)
}
// SuggestMentions returns prioritised mention suggestions for the composer.
func (s *PostService) SuggestMentions(ctx context.Context, viewerID, q string, limit, offset int) ([]model.UserSummary, error) {
return s.mentions.Suggest(ctx, viewerID, q, limit, offset)
}
// Mentions lists the users tagged on a post.
func (s *PostService) Mentions(ctx context.Context, postID string) ([]model.UserSummary, error) {
return s.mentions.ListForPost(ctx, postID)
}
// AddMention / RemoveMention edit a post's tags — author only.
func (s *PostService) AddMention(ctx context.Context, viewerID, postID, targetUserID string) error {
if err := s.requireAuthor(ctx, viewerID, postID); err != nil {
return err
}
if err := s.mentions.Add(ctx, postID, targetUserID); err != nil {
return err
}
if s.notif != nil {
go s.notif.NotifyPostMention(context.Background(), targetUserID, viewerID, postID)
}
return nil
}
// notifyMentions notifies each freshly-tagged user (deduped, never the author).
func (s *PostService) notifyMentions(ctx context.Context, postID, authorID string, userIDs []string) {
if s.notif == nil {
return
}
seen := make(map[string]bool, len(userIDs))
for _, uid := range userIDs {
if uid == "" || uid == authorID || seen[uid] {
continue
}
seen[uid] = true
s.notif.NotifyPostMention(ctx, uid, authorID, postID)
}
}
func (s *PostService) RemoveMention(ctx context.Context, viewerID, postID, targetUserID string) error {
if err := s.requireAuthor(ctx, viewerID, postID); err != nil {
return err
}
return s.mentions.Remove(ctx, postID, targetUserID)
}
func (s *PostService) requireAuthor(ctx context.Context, viewerID, postID string) error {
p, err := s.posts.FindByID(ctx, postID)
if err != nil {
return err
}
if p.UserID != viewerID {
return ErrForbidden
}
return nil
}
// Like / Unlike toggle the caller's like on a post and return the effective
// total (likes_count + pending Redis delta).
func (s *PostService) Like(ctx context.Context, userID, postID string) (int, error) {
@@ -256,6 +344,9 @@ func (s *PostService) ListMine(ctx context.Context, userID string, limit, offset
if err := s.hydrateLikesSlice(ctx, userID, posts); err != nil {
return nil, 0, err
}
if err := s.hydrateMentionsSlice(ctx, posts); err != nil {
return nil, 0, err
}
return posts, total, nil
}