Skip to content

Commit

Permalink
Merge pull request #1214 from getfider/reactions
Browse files Browse the repository at this point in the history
Github-style emoji reactions to comments.
  • Loading branch information
mattwoberts authored Oct 20, 2024
2 parents 04d94e2 + bb44060 commit db28ecc
Show file tree
Hide file tree
Showing 20 changed files with 569 additions and 35 deletions.
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

0 comments on commit db28ecc

Please sign in to comment.