summaryrefslogtreecommitdiffstats
path: root/models
diff options
context:
space:
mode:
authorLauris BH <lauris@nix.lv>2017-12-04 01:14:26 +0200
committerGitHub <noreply@github.com>2017-12-04 01:14:26 +0200
commit5dc37b187c8b839a15ff73758799f218ddeb3bc9 (patch)
treeb63e5ca72c7b9e72c79408ace82dfcba992b5793 /models
parente59adcde655aac0e8afd3249407c9a0a2b1b1d6b (diff)
downloadgitea-5dc37b187c8b839a15ff73758799f218ddeb3bc9.tar.gz
gitea-5dc37b187c8b839a15ff73758799f218ddeb3bc9.zip
Add reactions to issues/PR and comments (#2856)
Diffstat (limited to 'models')
-rw-r--r--models/fixtures/reaction.yml1
-rw-r--r--models/helper.go8
-rw-r--r--models/issue.go36
-rw-r--r--models/issue_comment.go24
-rw-r--r--models/issue_reaction.go255
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v50.go28
-rw-r--r--models/models.go1
-rw-r--r--models/user.go1
9 files changed, 354 insertions, 2 deletions
diff --git a/models/fixtures/reaction.yml b/models/fixtures/reaction.yml
new file mode 100644
index 0000000000..ca780a73aa
--- /dev/null
+++ b/models/fixtures/reaction.yml
@@ -0,0 +1 @@
+[] # empty
diff --git a/models/helper.go b/models/helper.go
index 6e8580524a..a284424bb5 100644
--- a/models/helper.go
+++ b/models/helper.go
@@ -19,3 +19,11 @@ func valuesRepository(m map[int64]*Repository) []*Repository {
}
return values
}
+
+func valuesUser(m map[int64]*User) []*User {
+ var values = make([]*User, 0, len(m))
+ for _, v := range m {
+ values = append(values, v)
+ }
+ return values
+}
diff --git a/models/issue.go b/models/issue.go
index 5f576be4a9..2119dcefd9 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -54,6 +54,7 @@ type Issue struct {
Attachments []*Attachment `xorm:"-"`
Comments []*Comment `xorm:"-"`
+ Reactions ReactionList `xorm:"-"`
}
// BeforeUpdate is invoked from XORM before updating this object.
@@ -155,6 +156,37 @@ func (issue *Issue) loadComments(e Engine) (err error) {
return err
}
+func (issue *Issue) loadReactions(e Engine) (err error) {
+ if issue.Reactions != nil {
+ return nil
+ }
+ reactions, err := findReactions(e, FindReactionsOptions{
+ IssueID: issue.ID,
+ })
+ if err != nil {
+ return err
+ }
+ // Load reaction user data
+ if _, err := ReactionList(reactions).LoadUsers(); err != nil {
+ return err
+ }
+
+ // Cache comments to map
+ comments := make(map[int64]*Comment)
+ for _, comment := range issue.Comments {
+ comments[comment.ID] = comment
+ }
+ // Add reactions either to issue or comment
+ for _, react := range reactions {
+ if react.CommentID == 0 {
+ issue.Reactions = append(issue.Reactions, react)
+ } else if comment, ok := comments[react.CommentID]; ok {
+ comment.Reactions = append(comment.Reactions, react)
+ }
+ }
+ return nil
+}
+
func (issue *Issue) loadAttributes(e Engine) (err error) {
if err = issue.loadRepo(e); err != nil {
return
@@ -192,10 +224,10 @@ func (issue *Issue) loadAttributes(e Engine) (err error) {
}
if err = issue.loadComments(e); err != nil {
- return
+ return err
}
- return nil
+ return issue.loadReactions(e)
}
// LoadAttributes loads the attribute of this issue.
diff --git a/models/issue_comment.go b/models/issue_comment.go
index 34c0ecdce5..aabeb9c8d4 100644
--- a/models/issue_comment.go
+++ b/models/issue_comment.go
@@ -107,6 +107,7 @@ type Comment struct {
CommitSHA string `xorm:"VARCHAR(40)"`
Attachments []*Attachment `xorm:"-"`
+ Reactions ReactionList `xorm:"-"`
// For view issue page.
ShowTag CommentTag `xorm:"-"`
@@ -287,6 +288,29 @@ func (c *Comment) MailParticipants(e Engine, opType ActionType, issue *Issue) (e
return nil
}
+func (c *Comment) loadReactions(e Engine) (err error) {
+ if c.Reactions != nil {
+ return nil
+ }
+ c.Reactions, err = findReactions(e, FindReactionsOptions{
+ IssueID: c.IssueID,
+ CommentID: c.ID,
+ })
+ if err != nil {
+ return err
+ }
+ // Load reaction user data
+ if _, err := c.Reactions.LoadUsers(); err != nil {
+ return err
+ }
+ return nil
+}
+
+// LoadReactions loads comment reactions
+func (c *Comment) LoadReactions() error {
+ return c.loadReactions(x)
+}
+
func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
var LabelID int64
if opts.Label != nil {
diff --git a/models/issue_reaction.go b/models/issue_reaction.go
new file mode 100644
index 0000000000..358e0701b3
--- /dev/null
+++ b/models/issue_reaction.go
@@ -0,0 +1,255 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import (
+ "bytes"
+ "fmt"
+ "time"
+
+ "github.com/go-xorm/builder"
+ "github.com/go-xorm/xorm"
+
+ "code.gitea.io/gitea/modules/setting"
+)
+
+// Reaction represents a reactions on issues and comments.
+type Reaction struct {
+ ID int64 `xorm:"pk autoincr"`
+ Type string `xorm:"INDEX UNIQUE(s) NOT NULL"`
+ IssueID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
+ CommentID int64 `xorm:"INDEX UNIQUE(s)"`
+ UserID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
+ User *User `xorm:"-"`
+ Created time.Time `xorm:"-"`
+ CreatedUnix int64 `xorm:"INDEX created"`
+}
+
+// AfterLoad is invoked from XORM after setting the values of all fields of this object.
+func (s *Reaction) AfterLoad() {
+ s.Created = time.Unix(s.CreatedUnix, 0).Local()
+}
+
+// FindReactionsOptions describes the conditions to Find reactions
+type FindReactionsOptions struct {
+ IssueID int64
+ CommentID int64
+}
+
+func (opts *FindReactionsOptions) toConds() builder.Cond {
+ var cond = builder.NewCond()
+ if opts.IssueID > 0 {
+ cond = cond.And(builder.Eq{"reaction.issue_id": opts.IssueID})
+ }
+ if opts.CommentID > 0 {
+ cond = cond.And(builder.Eq{"reaction.comment_id": opts.CommentID})
+ }
+ return cond
+}
+
+func findReactions(e Engine, opts FindReactionsOptions) ([]*Reaction, error) {
+ reactions := make([]*Reaction, 0, 10)
+ sess := e.Where(opts.toConds())
+ return reactions, sess.
+ Asc("reaction.issue_id", "reaction.comment_id", "reaction.created_unix", "reaction.id").
+ Find(&reactions)
+}
+
+func createReaction(e *xorm.Session, opts *ReactionOptions) (*Reaction, error) {
+ reaction := &Reaction{
+ Type: opts.Type,
+ UserID: opts.Doer.ID,
+ IssueID: opts.Issue.ID,
+ }
+ if opts.Comment != nil {
+ reaction.CommentID = opts.Comment.ID
+ }
+ if _, err := e.Insert(reaction); err != nil {
+ return nil, err
+ }
+
+ return reaction, nil
+}
+
+// ReactionOptions defines options for creating or deleting reactions
+type ReactionOptions struct {
+ Type string
+ Doer *User
+ Issue *Issue
+ Comment *Comment
+}
+
+// CreateReaction creates reaction for issue or comment.
+func CreateReaction(opts *ReactionOptions) (reaction *Reaction, err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return nil, err
+ }
+
+ reaction, err = createReaction(sess, opts)
+ if err != nil {
+ return nil, err
+ }
+
+ if err = sess.Commit(); err != nil {
+ return nil, err
+ }
+ return reaction, nil
+}
+
+// CreateIssueReaction creates a reaction on issue.
+func CreateIssueReaction(doer *User, issue *Issue, content string) (*Reaction, error) {
+ return CreateReaction(&ReactionOptions{
+ Type: content,
+ Doer: doer,
+ Issue: issue,
+ })
+}
+
+// CreateCommentReaction creates a reaction on comment.
+func CreateCommentReaction(doer *User, issue *Issue, comment *Comment, content string) (*Reaction, error) {
+ return CreateReaction(&ReactionOptions{
+ Type: content,
+ Doer: doer,
+ Issue: issue,
+ Comment: comment,
+ })
+}
+
+func deleteReaction(e *xorm.Session, opts *ReactionOptions) error {
+ reaction := &Reaction{
+ Type: opts.Type,
+ UserID: opts.Doer.ID,
+ IssueID: opts.Issue.ID,
+ }
+ if opts.Comment != nil {
+ reaction.CommentID = opts.Comment.ID
+ }
+ _, err := e.Delete(reaction)
+ return err
+}
+
+// DeleteReaction deletes reaction for issue or comment.
+func DeleteReaction(opts *ReactionOptions) error {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err := sess.Begin(); err != nil {
+ return err
+ }
+
+ if err := deleteReaction(sess, opts); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+// DeleteIssueReaction deletes a reaction on issue.
+func DeleteIssueReaction(doer *User, issue *Issue, content string) error {
+ return DeleteReaction(&ReactionOptions{
+ Type: content,
+ Doer: doer,
+ Issue: issue,
+ })
+}
+
+// DeleteCommentReaction deletes a reaction on comment.
+func DeleteCommentReaction(doer *User, issue *Issue, comment *Comment, content string) error {
+ return DeleteReaction(&ReactionOptions{
+ Type: content,
+ Doer: doer,
+ Issue: issue,
+ Comment: comment,
+ })
+}
+
+// ReactionList represents list of reactions
+type ReactionList []*Reaction
+
+// HasUser check if user has reacted
+func (list ReactionList) HasUser(userID int64) bool {
+ if userID == 0 {
+ return false
+ }
+ for _, reaction := range list {
+ if reaction.UserID == userID {
+ return true
+ }
+ }
+ return false
+}
+
+// GroupByType returns reactions grouped by type
+func (list ReactionList) GroupByType() map[string]ReactionList {
+ var reactions = make(map[string]ReactionList)
+ for _, reaction := range list {
+ reactions[reaction.Type] = append(reactions[reaction.Type], reaction)
+ }
+ return reactions
+}
+
+func (list ReactionList) getUserIDs() []int64 {
+ userIDs := make(map[int64]struct{}, len(list))
+ for _, reaction := range list {
+ if _, ok := userIDs[reaction.UserID]; !ok {
+ userIDs[reaction.UserID] = struct{}{}
+ }
+ }
+ return keysInt64(userIDs)
+}
+
+func (list ReactionList) loadUsers(e Engine) ([]*User, error) {
+ if len(list) == 0 {
+ return nil, nil
+ }
+
+ userIDs := list.getUserIDs()
+ userMaps := make(map[int64]*User, len(userIDs))
+ err := e.
+ In("id", userIDs).
+ Find(&userMaps)
+ if err != nil {
+ return nil, fmt.Errorf("find user: %v", err)
+ }
+
+ for _, reaction := range list {
+ if user, ok := userMaps[reaction.UserID]; ok {
+ reaction.User = user
+ } else {
+ reaction.User = NewGhostUser()
+ }
+ }
+ return valuesUser(userMaps), nil
+}
+
+// LoadUsers loads reactions' all users
+func (list ReactionList) LoadUsers() ([]*User, error) {
+ return list.loadUsers(x)
+}
+
+// GetFirstUsers returns first reacted user display names separated by comma
+func (list ReactionList) GetFirstUsers() string {
+ var buffer bytes.Buffer
+ var rem = setting.UI.ReactionMaxUserNum
+ for _, reaction := range list {
+ if buffer.Len() > 0 {
+ buffer.WriteString(", ")
+ }
+ buffer.WriteString(reaction.User.DisplayName())
+ if rem--; rem == 0 {
+ break
+ }
+ }
+ return buffer.String()
+}
+
+// GetMoreUserCount returns count of not shown users in reaction tooltip
+func (list ReactionList) GetMoreUserCount() int {
+ if len(list) <= setting.UI.ReactionMaxUserNum {
+ return 0
+ }
+ return len(list) - setting.UI.ReactionMaxUserNum
+}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index bccf6e6924..deded2f755 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -148,6 +148,8 @@ var migrations = []Migration{
NewMigration("add repo indexer status", addRepoIndexerStatus),
// v49 -> v50
NewMigration("add lfs lock table", addLFSLock),
+ // v50 -> v51
+ NewMigration("add reactions", addReactions),
}
// Migrate database to current version
diff --git a/models/migrations/v50.go b/models/migrations/v50.go
new file mode 100644
index 0000000000..7437cace25
--- /dev/null
+++ b/models/migrations/v50.go
@@ -0,0 +1,28 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package migrations
+
+import (
+ "fmt"
+
+ "github.com/go-xorm/xorm"
+)
+
+func addReactions(x *xorm.Engine) error {
+ // Reaction see models/issue_reaction.go
+ type Reaction struct {
+ ID int64 `xorm:"pk autoincr"`
+ Type string `xorm:"INDEX UNIQUE(s) NOT NULL"`
+ IssueID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
+ CommentID int64 `xorm:"INDEX UNIQUE(s)"`
+ UserID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
+ CreatedUnix int64 `xorm:"INDEX created"`
+ }
+
+ if err := x.Sync2(new(Reaction)); err != nil {
+ return fmt.Errorf("Sync2: %v", err)
+ }
+ return nil
+}
diff --git a/models/models.go b/models/models.go
index 8a3850b6ff..21bbdb7373 100644
--- a/models/models.go
+++ b/models/models.go
@@ -118,6 +118,7 @@ func init() {
new(DeletedBranch),
new(RepoIndexerStatus),
new(LFSLock),
+ new(Reaction),
)
gonicNames := []string{"SSL", "UID"}
diff --git a/models/user.go b/models/user.go
index 31af3747c0..61c2ac47a1 100644
--- a/models/user.go
+++ b/models/user.go
@@ -980,6 +980,7 @@ func deleteUser(e *xorm.Session, u *User) error {
&IssueUser{UID: u.ID},
&EmailAddress{UID: u.ID},
&UserOpenID{UID: u.ID},
+ &Reaction{UserID: u.ID},
); err != nil {
return fmt.Errorf("deleteBeans: %v", err)
}