feat(blog): add comment reactions and improve UI
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Unit Tests (push) Successful in 3m9s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m19s
Build and Release / Lint (push) Successful in 5m27s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m1s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m37s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Unit Tests (push) Successful in 3m9s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m19s
Build and Release / Lint (push) Successful in 5m27s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m1s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m37s
Build and Release / Build Binary (linux/arm64) (push) Has been cancelled
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been cancelled
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been cancelled
Implements thumbs up/down reactions for blog comments with toggle functionality. Adds batch loading of reaction counts and user reactions for performance. Updates standalone view and repo blog view to display comment reactions. Improves explore blogs UI with better card layout and navigation. Includes guest IP tracking for anonymous reactions.
This commit is contained in:
168
models/blog/blog_comment_reaction.go
Normal file
168
models/blog/blog_comment_reaction.go
Normal file
@@ -0,0 +1,168 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package blog
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitcaddy.com/server/v3/models/db"
|
||||
"code.gitcaddy.com/server/v3/modules/timeutil"
|
||||
)
|
||||
|
||||
// BlogCommentReaction represents a thumbs up/down reaction on a blog comment.
|
||||
type BlogCommentReaction struct { //revive:disable-line:exported
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
CommentID int64 `xorm:"INDEX NOT NULL"`
|
||||
UserID int64 `xorm:"INDEX NOT NULL DEFAULT 0"`
|
||||
GuestIP string `xorm:"VARCHAR(45) NOT NULL DEFAULT ''"`
|
||||
IsLike bool `xorm:"NOT NULL DEFAULT true"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(BlogCommentReaction))
|
||||
}
|
||||
|
||||
// BlogCommentReactionCounts holds aggregated reaction counts for a comment.
|
||||
type BlogCommentReactionCounts struct { //revive:disable-line:exported
|
||||
Likes int64
|
||||
Dislikes int64
|
||||
}
|
||||
|
||||
// ToggleBlogCommentReaction creates, updates, or removes a reaction on a comment.
|
||||
func ToggleBlogCommentReaction(ctx context.Context, commentID, userID int64, guestIP string, isLike bool) (reacted bool, err error) {
|
||||
existing, err := GetUserBlogCommentReaction(ctx, commentID, userID, guestIP)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if existing != nil {
|
||||
if existing.IsLike == isLike {
|
||||
// Same type — toggle off (remove)
|
||||
_, err = db.GetEngine(ctx).ID(existing.ID).Delete(new(BlogCommentReaction))
|
||||
return false, err
|
||||
}
|
||||
// Different type — switch
|
||||
existing.IsLike = isLike
|
||||
_, err = db.GetEngine(ctx).ID(existing.ID).Cols("is_like").Update(existing)
|
||||
return true, err
|
||||
}
|
||||
|
||||
// No existing reaction — create
|
||||
_, err = db.GetEngine(ctx).Insert(&BlogCommentReaction{
|
||||
CommentID: commentID,
|
||||
UserID: userID,
|
||||
GuestIP: guestIP,
|
||||
IsLike: isLike,
|
||||
})
|
||||
return true, err
|
||||
}
|
||||
|
||||
// GetBlogCommentReactionCounts returns aggregated like/dislike counts for a comment.
|
||||
func GetBlogCommentReactionCounts(ctx context.Context, commentID int64) (*BlogCommentReactionCounts, error) {
|
||||
likes, err := db.GetEngine(ctx).Where("comment_id = ? AND is_like = ?", commentID, true).Count(new(BlogCommentReaction))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dislikes, err := db.GetEngine(ctx).Where("comment_id = ? AND is_like = ?", commentID, false).Count(new(BlogCommentReaction))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &BlogCommentReactionCounts{Likes: likes, Dislikes: dislikes}, nil
|
||||
}
|
||||
|
||||
// GetBlogCommentReactionCountsBatch returns reaction counts for multiple comments.
|
||||
func GetBlogCommentReactionCountsBatch(ctx context.Context, commentIDs []int64) (map[int64]*BlogCommentReactionCounts, error) {
|
||||
result := make(map[int64]*BlogCommentReactionCounts, len(commentIDs))
|
||||
for _, id := range commentIDs {
|
||||
result[id] = &BlogCommentReactionCounts{}
|
||||
}
|
||||
if len(commentIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type countRow struct {
|
||||
CommentID int64 `xorm:"comment_id"`
|
||||
IsLike bool `xorm:"is_like"`
|
||||
Cnt int64 `xorm:"cnt"`
|
||||
}
|
||||
var rows []countRow
|
||||
err := db.GetEngine(ctx).Table("blog_comment_reaction").
|
||||
Select("comment_id, is_like, COUNT(*) AS cnt").
|
||||
In("comment_id", commentIDs).
|
||||
GroupBy("comment_id, is_like").
|
||||
Find(&rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range rows {
|
||||
counts, ok := result[r.CommentID]
|
||||
if !ok {
|
||||
counts = &BlogCommentReactionCounts{}
|
||||
result[r.CommentID] = counts
|
||||
}
|
||||
if r.IsLike {
|
||||
counts.Likes = r.Cnt
|
||||
} else {
|
||||
counts.Dislikes = r.Cnt
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetUserBlogCommentReaction returns the existing reaction for a user/guest on a comment, or nil.
|
||||
func GetUserBlogCommentReaction(ctx context.Context, commentID, userID int64, guestIP string) (*BlogCommentReaction, error) {
|
||||
r := &BlogCommentReaction{}
|
||||
var has bool
|
||||
var err error
|
||||
if userID > 0 {
|
||||
has, err = db.GetEngine(ctx).Where("comment_id = ? AND user_id = ?", commentID, userID).Get(r)
|
||||
} else {
|
||||
has, err = db.GetEngine(ctx).Where("comment_id = ? AND user_id = 0 AND guest_ip = ?", commentID, guestIP).Get(r)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// GetUserBlogCommentReactionsBatch returns the user's reactions for multiple comments.
|
||||
func GetUserBlogCommentReactionsBatch(ctx context.Context, commentIDs []int64, userID int64, guestIP string) (map[int64]*BlogCommentReaction, error) {
|
||||
result := make(map[int64]*BlogCommentReaction, len(commentIDs))
|
||||
if len(commentIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var reactions []*BlogCommentReaction
|
||||
sess := db.GetEngine(ctx).In("comment_id", commentIDs)
|
||||
if userID > 0 {
|
||||
sess = sess.Where("user_id = ?", userID)
|
||||
} else {
|
||||
sess = sess.Where("user_id = 0 AND guest_ip = ?", guestIP)
|
||||
}
|
||||
if err := sess.Find(&reactions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range reactions {
|
||||
result[r.CommentID] = r
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DeleteBlogCommentReactionsByCommentID removes all reactions for a comment.
|
||||
func DeleteBlogCommentReactionsByCommentID(ctx context.Context, commentID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("comment_id = ?", commentID).Delete(new(BlogCommentReaction))
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteBlogCommentReactionsByPostID removes all comment reactions for a blog post's comments.
|
||||
func DeleteBlogCommentReactionsByPostID(ctx context.Context, blogPostID int64) error {
|
||||
_, err := db.GetEngine(ctx).
|
||||
Where("comment_id IN (SELECT id FROM blog_comment WHERE blog_post_id = ?)", blogPostID).
|
||||
Delete(new(BlogCommentReaction))
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user