Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Github-style emoji reactions to comments. #1214

Merged
merged 9 commits into from
Oct 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions app/actions/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,38 @@ func (action *UpdatePost) Validate(ctx context.Context, user *entity.User) *vali
return result
}

type ToggleCommentReaction struct {
Number int `route:"number"`
Comment int `route:"id"`
Reaction string `route:"reaction"`
}

// IsAuthorized returns true if current user is authorized to perform this action
func (action *ToggleCommentReaction) IsAuthorized(ctx context.Context, user *entity.User) bool {
return user != nil
}

// Validate if current model is valid
func (action *ToggleCommentReaction) Validate(ctx context.Context, user *entity.User) *validate.Result {

result := validate.Success()

allowedEmojis := []string{"👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"}
isAllowed := false
for _, emoji := range allowedEmojis {
if action.Reaction == emoji {
isAllowed = true
break
}
}

if !isAllowed {
result.AddFieldFailure("reaction", i18n.T(ctx, "validation.custom.invalidemoji"))
}

return result
}

// AddNewComment represents a new comment to be added
type AddNewComment struct {
Number int `route:"number"`
Expand Down
1 change: 1 addition & 0 deletions app/cmd/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ func routes(r *web.Engine) *web.Engine {

membersApi.Post("/api/v1/posts", apiv1.CreatePost())
membersApi.Put("/api/v1/posts/:number", apiv1.UpdatePost())
membersApi.Post("/api/v1/posts/:number/comments/:id/reactions/:reaction", apiv1.ToggleReaction())
membersApi.Post("/api/v1/posts/:number/comments", apiv1.PostComment())
membersApi.Put("/api/v1/posts/:number/comments/:id", apiv1.UpdateComment())
membersApi.Delete("/api/v1/posts/:number/comments/:id", apiv1.DeleteComment())
Expand Down
28 changes: 28 additions & 0 deletions app/handlers/apiv1/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,34 @@ func GetComment() web.HandlerFunc {
}
}

// ToggleReaction adds or removes a reaction on a comment
func ToggleReaction() web.HandlerFunc {
return func(c *web.Context) error {
action := new(actions.ToggleCommentReaction)
if result := c.BindTo(action); !result.Ok {
return c.HandleValidation(result)
}

getComment := &query.GetCommentByID{CommentID: action.Comment}
if err := bus.Dispatch(c, getComment); err != nil {
return c.Failure(err)
}

toggleReaction := &cmd.ToggleCommentReaction{
Comment: getComment.Result,
Emoji: action.Reaction,
User: c.User(),
}
if err := bus.Dispatch(c, toggleReaction); err != nil {
return c.Failure(err)
}

return c.Ok(web.Map{
"added": toggleReaction.Result,
})
}
}

// PostComment creates a new comment on given post
func PostComment() web.HandlerFunc {
return func(c *web.Context) error {
Expand Down
141 changes: 126 additions & 15 deletions app/handlers/apiv1/post_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,11 @@ func TestUpdatePostHandler_NonAuthorized(t *testing.T) {
RegisterT(t)

post := &entity.Post{
ID: 5,
Number: 5,
Title: "My First Post",
ID: 5,
Number: 5,
Title: "My First Post",
Description: "Such an amazing description",
User: mock.JonSnow,
User: mock.JonSnow,
}
bus.AddHandler(func(ctx context.Context, q *query.GetPostByNumber) error {
if q.Number == post.Number {
Expand All @@ -151,12 +151,12 @@ func TestUpdatePostHandler_IsOwner_AfterGracePeriod(t *testing.T) {
RegisterT(t)

post := &entity.Post{
ID: 5,
Number: 5,
Title: "My First Post",
ID: 5,
Number: 5,
Title: "My First Post",
Description: "Such an amazing description",
User: mock.AryaStark,
CreatedAt: time.Now().UTC().Add(-2 * time.Hour),
User: mock.AryaStark,
CreatedAt: time.Now().UTC().Add(-2 * time.Hour),
}
bus.AddHandler(func(ctx context.Context, q *query.GetPostByNumber) error {
if q.Number == post.Number {
Expand All @@ -175,17 +175,16 @@ func TestUpdatePostHandler_IsOwner_AfterGracePeriod(t *testing.T) {
Expect(code).Equals(http.StatusForbidden)
}


func TestUpdatePostHandler_IsOwner_WithinGracePeriod(t *testing.T) {
RegisterT(t)

post := &entity.Post{
ID: 5,
Number: 5,
Title: "My First Post",
ID: 5,
Number: 5,
Title: "My First Post",
Description: "Such an amazing description",
User: mock.AryaStark,
CreatedAt: time.Now().UTC(),
User: mock.AryaStark,
CreatedAt: time.Now().UTC(),
}
bus.AddHandler(func(ctx context.Context, q *query.GetPostByNumber) error {
if q.Number == post.Number {
Expand Down Expand Up @@ -654,3 +653,115 @@ func TestListCommentHandler(t *testing.T) {
Expect(query.IsArray()).IsTrue()
Expect(query.ArrayLength()).Equals(2)
}

func TestCommentReactionToggleHandler(t *testing.T) {
RegisterT(t)

comment := &entity.Comment{ID: 5, Content: "Old comment text", User: mock.AryaStark}

bus.AddHandler(func(ctx context.Context, q *query.GetCommentByID) error {
q.Result = comment
return nil
})

testCases := []struct {
name string
user *entity.User
reaction string
}{
{"JonSnow reacts with like", mock.JonSnow, "👍"},
{"AryaStark reacts with smile", mock.AryaStark, "👍"},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var toggleReaction *cmd.ToggleCommentReaction
bus.AddHandler(func(ctx context.Context, c *cmd.ToggleCommentReaction) error {
toggleReaction = c
return nil
})

code, _ := mock.NewServer().
OnTenant(mock.DemoTenant).
AsUser(tc.user).
AddParam("number", 1).
AddParam("id", comment.ID).
AddParam("reaction", tc.reaction).
ExecutePost(apiv1.ToggleReaction(), ``)

Expect(code).Equals(http.StatusOK)
Expect(toggleReaction.Emoji).Equals(tc.reaction)
Expect(toggleReaction.Comment).Equals(comment)
Expect(toggleReaction.User).Equals(tc.user)
})
}
}

func TestCommentReactionToggleHandler_InvalidEmoji(t *testing.T) {
RegisterT(t)

comment := &entity.Comment{ID: 5, Content: "Old comment text", User: mock.AryaStark}
bus.AddHandler(func(ctx context.Context, q *query.GetCommentByID) error {
q.Result = comment
return nil
})

bus.AddHandler(func(ctx context.Context, c *cmd.ToggleCommentReaction) error {
return nil
})

code, _ := mock.NewServer().
OnTenant(mock.DemoTenant).
AsUser(mock.AryaStark).
AddParam("number", 1).
AddParam("id", comment.ID).
AddParam("reaction", "like").
ExecutePost(apiv1.ToggleReaction(), ``)

Expect(code).Equals(http.StatusBadRequest)
}

func TestCommentReactionToggleHandler_UnAuthorised(t *testing.T) {
RegisterT(t)

comment := &entity.Comment{ID: 5, Content: "Old comment text", User: mock.AryaStark}
bus.AddHandler(func(ctx context.Context, q *query.GetCommentByID) error {
q.Result = comment
return nil
})

bus.AddHandler(func(ctx context.Context, c *cmd.ToggleCommentReaction) error {
return nil
})

code, _ := mock.NewServer().
OnTenant(mock.DemoTenant).
AddParam("number", 1).
AddParam("id", comment.ID).
AddParam("reaction", "👍").
ExecutePost(apiv1.ToggleReaction(), ``)

Expect(code).Equals(http.StatusForbidden)
}

func TestCommentReactionToggleHandler_MismatchingTenantAndComment(t *testing.T) {
RegisterT(t)

bus.AddHandler(func(ctx context.Context, q *query.GetCommentByID) error {
return app.ErrNotFound
})

bus.AddHandler(func(ctx context.Context, c *cmd.ToggleCommentReaction) error {
return nil
})

code, _ := mock.NewServer().
OnTenant(mock.DemoTenant).
AsUser(mock.JonSnow).
AddParam("number", 1).
AddParam("id", 1).
AddParam("reaction", "👍").
ExecutePost(apiv1.ToggleReaction(), ``)

Expect(code).Equals(http.StatusNotFound)
}
10 changes: 10 additions & 0 deletions app/models/cmd/reaction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package cmd

import "github.com/getfider/fider/app/models/entity"

type ToggleCommentReaction struct {
Comment *entity.Comment
Emoji string
User *entity.User
Result bool
}
23 changes: 15 additions & 8 deletions app/models/entity/comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@ package entity

import "time"

//Comment represents an user comment on an post
type ReactionCounts struct {
Emoji string `json:"emoji"`
Count int `json:"count"`
IncludesMe bool `json:"includesMe"`
}

// Comment represents an user comment on an post
type Comment struct {
ID int `json:"id"`
Content string `json:"content"`
CreatedAt time.Time `json:"createdAt"`
User *User `json:"user"`
Attachments []string `json:"attachments,omitempty"`
EditedAt *time.Time `json:"editedAt,omitempty"`
EditedBy *User `json:"editedBy,omitempty"`
ID int `json:"id"`
Content string `json:"content"`
CreatedAt time.Time `json:"createdAt"`
User *User `json:"user"`
Attachments []string `json:"attachments,omitempty"`
EditedAt *time.Time `json:"editedAt,omitempty"`
EditedBy *User `json:"editedBy,omitempty"`
ReactionCounts []ReactionCounts `json:"reactionCounts,omitempty"`
}
12 changes: 12 additions & 0 deletions app/models/entity/reaction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package entity

import "time"

// Reaction represents a user's emoji reaction to a comment
type Reaction struct {
ID int `json:"id"`
Emoji string `json:"emoji"`
Comment *Comment `json:"-"`
User *User `json:"user"`
CreatedAt time.Time `json:"createdAt"`
}
Loading
Loading