diff options
author | Lauris BH <lauris@nix.lv> | 2017-12-04 01:14:26 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-12-04 01:14:26 +0200 |
commit | 5dc37b187c8b839a15ff73758799f218ddeb3bc9 (patch) | |
tree | b63e5ca72c7b9e72c79408ace82dfcba992b5793 /models | |
parent | e59adcde655aac0e8afd3249407c9a0a2b1b1d6b (diff) | |
download | gitea-5dc37b187c8b839a15ff73758799f218ddeb3bc9.tar.gz gitea-5dc37b187c8b839a15ff73758799f218ddeb3bc9.zip |
Add reactions to issues/PR and comments (#2856)
Diffstat (limited to 'models')
-rw-r--r-- | models/fixtures/reaction.yml | 1 | ||||
-rw-r--r-- | models/helper.go | 8 | ||||
-rw-r--r-- | models/issue.go | 36 | ||||
-rw-r--r-- | models/issue_comment.go | 24 | ||||
-rw-r--r-- | models/issue_reaction.go | 255 | ||||
-rw-r--r-- | models/migrations/migrations.go | 2 | ||||
-rw-r--r-- | models/migrations/v50.go | 28 | ||||
-rw-r--r-- | models/models.go | 1 | ||||
-rw-r--r-- | models/user.go | 1 |
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) } |