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
+63 -2
View File
@@ -33,12 +33,18 @@ var (
type PostService struct {
posts *repository.PostRepo
r2 *repository.R2
likes *repository.LikeRepo
notif *NotificationService // optional; set via SetNotifier
}
func NewPostService(posts *repository.PostRepo, r2 *repository.R2) *PostService {
return &PostService{posts: posts, r2: r2}
func NewPostService(posts *repository.PostRepo, r2 *repository.R2, likes *repository.LikeRepo) *PostService {
return &PostService{posts: posts, r2: r2, likes: likes}
}
// SetNotifier wires the notification seam after construction (the
// NotificationService is built later in server wiring than PostService).
func (s *PostService) SetNotifier(n *NotificationService) { s.notif = n }
// Create persists a new post. The caption is treated as opaque — we trust
// 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
@@ -72,6 +78,9 @@ func (s *PostService) Get(ctx context.Context, id, viewerID string) (*model.Post
if err := s.hydrateReshareStates(ctx, viewerID, []*model.Post{p}); err != nil {
return nil, err
}
if err := s.likes.Hydrate(ctx, viewerID, []*model.Post{p}); err != nil {
return nil, err
}
return p, nil
}
@@ -171,9 +180,58 @@ func (s *PostService) ListByHandle(ctx context.Context, handle, viewerID string,
if err := s.hydrateReshareStatesSlice(ctx, viewerID, posts); err != nil {
return nil, 0, err
}
if err := s.hydrateLikesSlice(ctx, viewerID, posts); err != nil {
return nil, 0, err
}
return posts, total, nil
}
// hydrateLikesSlice fills LikesCount + ViewerHasLiked on a value slice.
func (s *PostService) hydrateLikesSlice(ctx context.Context, viewerID string, 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.likes.Hydrate(ctx, viewerID, ptrs)
}
// 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) {
changed, err := s.likes.Like(ctx, postID, userID)
if err != nil {
return 0, err
}
count, err := s.likes.EffectiveCount(ctx, postID)
if err != nil {
return 0, err
}
// Notify the author on a genuinely new like by someone else. Fire-and-
// forget on a detached context so it isn't cancelled when the request
// returns; NotifyPostLike re-checks self-like internally too.
if changed && s.notif != nil {
if authorID, aerr := s.likes.PostAuthor(ctx, postID); aerr == nil && authorID != userID {
go s.notif.NotifyPostLike(context.Background(), postID, authorID, userID)
}
}
return count, nil
}
func (s *PostService) Unlike(ctx context.Context, userID, postID string) (int, error) {
if _, err := s.likes.Unlike(ctx, postID, userID); err != nil {
return 0, err
}
return s.likes.EffectiveCount(ctx, postID)
}
// Likers returns one page of users who liked the post + the total count.
func (s *PostService) Likers(ctx context.Context, postID, q string, limit, offset int) ([]model.UserSummary, int, error) {
return s.likes.ListLikers(ctx, postID, q, limit, offset)
}
// ListMine is the owner's view of their own profile feed. Every post
// they own — authored or reshared — comes from posts.user_id, so the
// viewer is by construction the owner. ViewerHasReshared is true for
@@ -195,6 +253,9 @@ func (s *PostService) ListMine(ctx context.Context, userID string, limit, offset
posts[i].ViewerHasReshared = true
}
}
if err := s.hydrateLikesSlice(ctx, userID, posts); err != nil {
return nil, 0, err
}
return posts, total, nil
}