Skip to content

Commit db28ecc

Browse files
authored
Merge pull request #1214 from getfider/reactions
Github-style emoji reactions to comments.
2 parents 04d94e2 + bb44060 commit db28ecc

File tree

20 files changed

+569
-35
lines changed

20 files changed

+569
-35
lines changed

app/actions/post.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,38 @@ func (action *UpdatePost) Validate(ctx context.Context, user *entity.User) *vali
133133
return result
134134
}
135135

136+
type ToggleCommentReaction struct {
137+
Number int `route:"number"`
138+
Comment int `route:"id"`
139+
Reaction string `route:"reaction"`
140+
}
141+
142+
// IsAuthorized returns true if current user is authorized to perform this action
143+
func (action *ToggleCommentReaction) IsAuthorized(ctx context.Context, user *entity.User) bool {
144+
return user != nil
145+
}
146+
147+
// Validate if current model is valid
148+
func (action *ToggleCommentReaction) Validate(ctx context.Context, user *entity.User) *validate.Result {
149+
150+
result := validate.Success()
151+
152+
allowedEmojis := []string{"👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"}
153+
isAllowed := false
154+
for _, emoji := range allowedEmojis {
155+
if action.Reaction == emoji {
156+
isAllowed = true
157+
break
158+
}
159+
}
160+
161+
if !isAllowed {
162+
result.AddFieldFailure("reaction", i18n.T(ctx, "validation.custom.invalidemoji"))
163+
}
164+
165+
return result
166+
}
167+
136168
// AddNewComment represents a new comment to be added
137169
type AddNewComment struct {
138170
Number int `route:"number"`

app/cmd/routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ func routes(r *web.Engine) *web.Engine {
197197

198198
membersApi.Post("/api/v1/posts", apiv1.CreatePost())
199199
membersApi.Put("/api/v1/posts/:number", apiv1.UpdatePost())
200+
membersApi.Post("/api/v1/posts/:number/comments/:id/reactions/:reaction", apiv1.ToggleReaction())
200201
membersApi.Post("/api/v1/posts/:number/comments", apiv1.PostComment())
201202
membersApi.Put("/api/v1/posts/:number/comments/:id", apiv1.UpdateComment())
202203
membersApi.Delete("/api/v1/posts/:number/comments/:id", apiv1.DeleteComment())

app/handlers/apiv1/post.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,34 @@ func GetComment() web.HandlerFunc {
218218
}
219219
}
220220

221+
// ToggleReaction adds or removes a reaction on a comment
222+
func ToggleReaction() web.HandlerFunc {
223+
return func(c *web.Context) error {
224+
action := new(actions.ToggleCommentReaction)
225+
if result := c.BindTo(action); !result.Ok {
226+
return c.HandleValidation(result)
227+
}
228+
229+
getComment := &query.GetCommentByID{CommentID: action.Comment}
230+
if err := bus.Dispatch(c, getComment); err != nil {
231+
return c.Failure(err)
232+
}
233+
234+
toggleReaction := &cmd.ToggleCommentReaction{
235+
Comment: getComment.Result,
236+
Emoji: action.Reaction,
237+
User: c.User(),
238+
}
239+
if err := bus.Dispatch(c, toggleReaction); err != nil {
240+
return c.Failure(err)
241+
}
242+
243+
return c.Ok(web.Map{
244+
"added": toggleReaction.Result,
245+
})
246+
}
247+
}
248+
221249
// PostComment creates a new comment on given post
222250
func PostComment() web.HandlerFunc {
223251
return func(c *web.Context) error {

app/handlers/apiv1/post_test.go

Lines changed: 126 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,11 @@ func TestUpdatePostHandler_NonAuthorized(t *testing.T) {
124124
RegisterT(t)
125125

126126
post := &entity.Post{
127-
ID: 5,
128-
Number: 5,
129-
Title: "My First Post",
127+
ID: 5,
128+
Number: 5,
129+
Title: "My First Post",
130130
Description: "Such an amazing description",
131-
User: mock.JonSnow,
131+
User: mock.JonSnow,
132132
}
133133
bus.AddHandler(func(ctx context.Context, q *query.GetPostByNumber) error {
134134
if q.Number == post.Number {
@@ -151,12 +151,12 @@ func TestUpdatePostHandler_IsOwner_AfterGracePeriod(t *testing.T) {
151151
RegisterT(t)
152152

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

178-
179178
func TestUpdatePostHandler_IsOwner_WithinGracePeriod(t *testing.T) {
180179
RegisterT(t)
181180

182181
post := &entity.Post{
183-
ID: 5,
184-
Number: 5,
185-
Title: "My First Post",
182+
ID: 5,
183+
Number: 5,
184+
Title: "My First Post",
186185
Description: "Such an amazing description",
187-
User: mock.AryaStark,
188-
CreatedAt: time.Now().UTC(),
186+
User: mock.AryaStark,
187+
CreatedAt: time.Now().UTC(),
189188
}
190189
bus.AddHandler(func(ctx context.Context, q *query.GetPostByNumber) error {
191190
if q.Number == post.Number {
@@ -654,3 +653,115 @@ func TestListCommentHandler(t *testing.T) {
654653
Expect(query.IsArray()).IsTrue()
655654
Expect(query.ArrayLength()).Equals(2)
656655
}
656+
657+
func TestCommentReactionToggleHandler(t *testing.T) {
658+
RegisterT(t)
659+
660+
comment := &entity.Comment{ID: 5, Content: "Old comment text", User: mock.AryaStark}
661+
662+
bus.AddHandler(func(ctx context.Context, q *query.GetCommentByID) error {
663+
q.Result = comment
664+
return nil
665+
})
666+
667+
testCases := []struct {
668+
name string
669+
user *entity.User
670+
reaction string
671+
}{
672+
{"JonSnow reacts with like", mock.JonSnow, "👍"},
673+
{"AryaStark reacts with smile", mock.AryaStark, "👍"},
674+
}
675+
676+
for _, tc := range testCases {
677+
t.Run(tc.name, func(t *testing.T) {
678+
var toggleReaction *cmd.ToggleCommentReaction
679+
bus.AddHandler(func(ctx context.Context, c *cmd.ToggleCommentReaction) error {
680+
toggleReaction = c
681+
return nil
682+
})
683+
684+
code, _ := mock.NewServer().
685+
OnTenant(mock.DemoTenant).
686+
AsUser(tc.user).
687+
AddParam("number", 1).
688+
AddParam("id", comment.ID).
689+
AddParam("reaction", tc.reaction).
690+
ExecutePost(apiv1.ToggleReaction(), ``)
691+
692+
Expect(code).Equals(http.StatusOK)
693+
Expect(toggleReaction.Emoji).Equals(tc.reaction)
694+
Expect(toggleReaction.Comment).Equals(comment)
695+
Expect(toggleReaction.User).Equals(tc.user)
696+
})
697+
}
698+
}
699+
700+
func TestCommentReactionToggleHandler_InvalidEmoji(t *testing.T) {
701+
RegisterT(t)
702+
703+
comment := &entity.Comment{ID: 5, Content: "Old comment text", User: mock.AryaStark}
704+
bus.AddHandler(func(ctx context.Context, q *query.GetCommentByID) error {
705+
q.Result = comment
706+
return nil
707+
})
708+
709+
bus.AddHandler(func(ctx context.Context, c *cmd.ToggleCommentReaction) error {
710+
return nil
711+
})
712+
713+
code, _ := mock.NewServer().
714+
OnTenant(mock.DemoTenant).
715+
AsUser(mock.AryaStark).
716+
AddParam("number", 1).
717+
AddParam("id", comment.ID).
718+
AddParam("reaction", "like").
719+
ExecutePost(apiv1.ToggleReaction(), ``)
720+
721+
Expect(code).Equals(http.StatusBadRequest)
722+
}
723+
724+
func TestCommentReactionToggleHandler_UnAuthorised(t *testing.T) {
725+
RegisterT(t)
726+
727+
comment := &entity.Comment{ID: 5, Content: "Old comment text", User: mock.AryaStark}
728+
bus.AddHandler(func(ctx context.Context, q *query.GetCommentByID) error {
729+
q.Result = comment
730+
return nil
731+
})
732+
733+
bus.AddHandler(func(ctx context.Context, c *cmd.ToggleCommentReaction) error {
734+
return nil
735+
})
736+
737+
code, _ := mock.NewServer().
738+
OnTenant(mock.DemoTenant).
739+
AddParam("number", 1).
740+
AddParam("id", comment.ID).
741+
AddParam("reaction", "👍").
742+
ExecutePost(apiv1.ToggleReaction(), ``)
743+
744+
Expect(code).Equals(http.StatusForbidden)
745+
}
746+
747+
func TestCommentReactionToggleHandler_MismatchingTenantAndComment(t *testing.T) {
748+
RegisterT(t)
749+
750+
bus.AddHandler(func(ctx context.Context, q *query.GetCommentByID) error {
751+
return app.ErrNotFound
752+
})
753+
754+
bus.AddHandler(func(ctx context.Context, c *cmd.ToggleCommentReaction) error {
755+
return nil
756+
})
757+
758+
code, _ := mock.NewServer().
759+
OnTenant(mock.DemoTenant).
760+
AsUser(mock.JonSnow).
761+
AddParam("number", 1).
762+
AddParam("id", 1).
763+
AddParam("reaction", "👍").
764+
ExecutePost(apiv1.ToggleReaction(), ``)
765+
766+
Expect(code).Equals(http.StatusNotFound)
767+
}

app/models/cmd/reaction.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package cmd
2+
3+
import "github.com/getfider/fider/app/models/entity"
4+
5+
type ToggleCommentReaction struct {
6+
Comment *entity.Comment
7+
Emoji string
8+
User *entity.User
9+
Result bool
10+
}

app/models/entity/comment.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@ package entity
22

33
import "time"
44

5-
//Comment represents an user comment on an post
5+
type ReactionCounts struct {
6+
Emoji string `json:"emoji"`
7+
Count int `json:"count"`
8+
IncludesMe bool `json:"includesMe"`
9+
}
10+
11+
// Comment represents an user comment on an post
612
type Comment struct {
7-
ID int `json:"id"`
8-
Content string `json:"content"`
9-
CreatedAt time.Time `json:"createdAt"`
10-
User *User `json:"user"`
11-
Attachments []string `json:"attachments,omitempty"`
12-
EditedAt *time.Time `json:"editedAt,omitempty"`
13-
EditedBy *User `json:"editedBy,omitempty"`
13+
ID int `json:"id"`
14+
Content string `json:"content"`
15+
CreatedAt time.Time `json:"createdAt"`
16+
User *User `json:"user"`
17+
Attachments []string `json:"attachments,omitempty"`
18+
EditedAt *time.Time `json:"editedAt,omitempty"`
19+
EditedBy *User `json:"editedBy,omitempty"`
20+
ReactionCounts []ReactionCounts `json:"reactionCounts,omitempty"`
1421
}

app/models/entity/reaction.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package entity
2+
3+
import "time"
4+
5+
// Reaction represents a user's emoji reaction to a comment
6+
type Reaction struct {
7+
ID int `json:"id"`
8+
Emoji string `json:"emoji"`
9+
Comment *Comment `json:"-"`
10+
User *User `json:"user"`
11+
CreatedAt time.Time `json:"createdAt"`
12+
}

0 commit comments

Comments
 (0)