mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-21 12:44:49 -05:00
Add reactions to issues/PR and comments (#2856)
This commit is contained in:
parent
e59adcde65
commit
5dc37b187c
24 changed files with 677 additions and 8 deletions
|
@ -182,6 +182,7 @@ The goal of this project is to make the easiest, fastest, and most painless way
|
||||||
- Labels
|
- Labels
|
||||||
- Assign issues
|
- Assign issues
|
||||||
- Track time
|
- Track time
|
||||||
|
- Reactions
|
||||||
- Filter
|
- Filter
|
||||||
- Open
|
- Open
|
||||||
- Closed
|
- Closed
|
||||||
|
|
1
models/fixtures/reaction.yml
Normal file
1
models/fixtures/reaction.yml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[] # empty
|
|
@ -19,3 +19,11 @@ func valuesRepository(m map[int64]*Repository) []*Repository {
|
||||||
}
|
}
|
||||||
return values
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@ type Issue struct {
|
||||||
|
|
||||||
Attachments []*Attachment `xorm:"-"`
|
Attachments []*Attachment `xorm:"-"`
|
||||||
Comments []*Comment `xorm:"-"`
|
Comments []*Comment `xorm:"-"`
|
||||||
|
Reactions ReactionList `xorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeUpdate is invoked from XORM before updating this object.
|
// BeforeUpdate is invoked from XORM before updating this object.
|
||||||
|
@ -155,6 +156,37 @@ func (issue *Issue) loadComments(e Engine) (err error) {
|
||||||
return err
|
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) {
|
func (issue *Issue) loadAttributes(e Engine) (err error) {
|
||||||
if err = issue.loadRepo(e); err != nil {
|
if err = issue.loadRepo(e); err != nil {
|
||||||
return
|
return
|
||||||
|
@ -192,10 +224,10 @@ func (issue *Issue) loadAttributes(e Engine) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = issue.loadComments(e); err != nil {
|
if err = issue.loadComments(e); err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return issue.loadReactions(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadAttributes loads the attribute of this issue.
|
// LoadAttributes loads the attribute of this issue.
|
||||||
|
|
|
@ -107,6 +107,7 @@ type Comment struct {
|
||||||
CommitSHA string `xorm:"VARCHAR(40)"`
|
CommitSHA string `xorm:"VARCHAR(40)"`
|
||||||
|
|
||||||
Attachments []*Attachment `xorm:"-"`
|
Attachments []*Attachment `xorm:"-"`
|
||||||
|
Reactions ReactionList `xorm:"-"`
|
||||||
|
|
||||||
// For view issue page.
|
// For view issue page.
|
||||||
ShowTag CommentTag `xorm:"-"`
|
ShowTag CommentTag `xorm:"-"`
|
||||||
|
@ -287,6 +288,29 @@ func (c *Comment) MailParticipants(e Engine, opType ActionType, issue *Issue) (e
|
||||||
return nil
|
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) {
|
func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
|
||||||
var LabelID int64
|
var LabelID int64
|
||||||
if opts.Label != nil {
|
if opts.Label != nil {
|
||||||
|
|
255
models/issue_reaction.go
Normal file
255
models/issue_reaction.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -148,6 +148,8 @@ var migrations = []Migration{
|
||||||
NewMigration("add repo indexer status", addRepoIndexerStatus),
|
NewMigration("add repo indexer status", addRepoIndexerStatus),
|
||||||
// v49 -> v50
|
// v49 -> v50
|
||||||
NewMigration("add lfs lock table", addLFSLock),
|
NewMigration("add lfs lock table", addLFSLock),
|
||||||
|
// v50 -> v51
|
||||||
|
NewMigration("add reactions", addReactions),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate database to current version
|
// Migrate database to current version
|
||||||
|
|
28
models/migrations/v50.go
Normal file
28
models/migrations/v50.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -118,6 +118,7 @@ func init() {
|
||||||
new(DeletedBranch),
|
new(DeletedBranch),
|
||||||
new(RepoIndexerStatus),
|
new(RepoIndexerStatus),
|
||||||
new(LFSLock),
|
new(LFSLock),
|
||||||
|
new(Reaction),
|
||||||
)
|
)
|
||||||
|
|
||||||
gonicNames := []string{"SSL", "UID"}
|
gonicNames := []string{"SSL", "UID"}
|
||||||
|
|
|
@ -980,6 +980,7 @@ func deleteUser(e *xorm.Session, u *User) error {
|
||||||
&IssueUser{UID: u.ID},
|
&IssueUser{UID: u.ID},
|
||||||
&EmailAddress{UID: u.ID},
|
&EmailAddress{UID: u.ID},
|
||||||
&UserOpenID{UID: u.ID},
|
&UserOpenID{UID: u.ID},
|
||||||
|
&Reaction{UserID: u.ID},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("deleteBeans: %v", err)
|
return fmt.Errorf("deleteBeans: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -268,6 +268,16 @@ func (f *CreateCommentForm) Validate(ctx *macaron.Context, errs binding.Errors)
|
||||||
return validate(errs, ctx.Data, f, ctx.Locale)
|
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReactionForm form for adding and removing reaction
|
||||||
|
type ReactionForm struct {
|
||||||
|
Content string `binding:"Required;In(+1,-1,laugh,confused,heart,hooray)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the fields
|
||||||
|
func (f *ReactionForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
||||||
|
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||||
|
}
|
||||||
|
|
||||||
// _____ .__.__ __
|
// _____ .__.__ __
|
||||||
// / \ |__| | ____ _______/ |_ ____ ____ ____
|
// / \ |__| | ____ _______/ |_ ____ ____ ____
|
||||||
// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
|
// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
|
||||||
|
|
|
@ -211,7 +211,7 @@ func Contexter() macaron.Handler {
|
||||||
ctx.Data["SignedUserName"] = ctx.User.Name
|
ctx.Data["SignedUserName"] = ctx.User.Name
|
||||||
ctx.Data["IsAdmin"] = ctx.User.IsAdmin
|
ctx.Data["IsAdmin"] = ctx.User.IsAdmin
|
||||||
} else {
|
} else {
|
||||||
ctx.Data["SignedUserID"] = 0
|
ctx.Data["SignedUserID"] = int64(0)
|
||||||
ctx.Data["SignedUserName"] = ""
|
ctx.Data["SignedUserName"] = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -256,6 +256,7 @@ var (
|
||||||
IssuePagingNum int
|
IssuePagingNum int
|
||||||
RepoSearchPagingNum int
|
RepoSearchPagingNum int
|
||||||
FeedMaxCommitNum int
|
FeedMaxCommitNum int
|
||||||
|
ReactionMaxUserNum int
|
||||||
ThemeColorMetaTag string
|
ThemeColorMetaTag string
|
||||||
MaxDisplayFileSize int64
|
MaxDisplayFileSize int64
|
||||||
ShowUserEmail bool
|
ShowUserEmail bool
|
||||||
|
@ -279,6 +280,7 @@ var (
|
||||||
IssuePagingNum: 10,
|
IssuePagingNum: 10,
|
||||||
RepoSearchPagingNum: 10,
|
RepoSearchPagingNum: 10,
|
||||||
FeedMaxCommitNum: 5,
|
FeedMaxCommitNum: 5,
|
||||||
|
ReactionMaxUserNum: 10,
|
||||||
ThemeColorMetaTag: `#6cc644`,
|
ThemeColorMetaTag: `#6cc644`,
|
||||||
MaxDisplayFileSize: 8388608,
|
MaxDisplayFileSize: 8388608,
|
||||||
Admin: struct {
|
Admin: struct {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"container/list"
|
"container/list"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"mime"
|
"mime"
|
||||||
|
@ -162,6 +163,21 @@ func NewFuncMap() []template.FuncMap {
|
||||||
return setting.DisableGitHooks
|
return setting.DisableGitHooks
|
||||||
},
|
},
|
||||||
"TrN": TrN,
|
"TrN": TrN,
|
||||||
|
"Dict": func(values ...interface{}) (map[string]interface{}, error) {
|
||||||
|
if len(values)%2 != 0 {
|
||||||
|
return nil, errors.New("invalid dict call")
|
||||||
|
}
|
||||||
|
dict := make(map[string]interface{}, len(values)/2)
|
||||||
|
for i := 0; i < len(values); i += 2 {
|
||||||
|
key, ok := values[i].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("dict keys must be strings")
|
||||||
|
}
|
||||||
|
dict[key] = values[i+1]
|
||||||
|
}
|
||||||
|
return dict, nil
|
||||||
|
},
|
||||||
|
"Printf": fmt.Sprintf,
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -489,6 +489,8 @@ mirror_last_synced = Last Synced
|
||||||
watchers = Watchers
|
watchers = Watchers
|
||||||
stargazers = Stargazers
|
stargazers = Stargazers
|
||||||
forks = Forks
|
forks = Forks
|
||||||
|
pick_reaction = Pick your reaction
|
||||||
|
reactions_more = and %d more
|
||||||
|
|
||||||
form.reach_limit_of_creation = You have already reached your limit of %d repositories.
|
form.reach_limit_of_creation = You have already reached your limit of %d repositories.
|
||||||
form.name_reserved = The repository name '%s' is reserved.
|
form.name_reserved = The repository name '%s' is reserved.
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -117,6 +117,54 @@ function updateIssuesMeta(url, action, issueIds, elementId, afterSuccess) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initReactionSelector(parent) {
|
||||||
|
var reactions = '';
|
||||||
|
if (!parent) {
|
||||||
|
parent = $(document);
|
||||||
|
reactions = '.reactions > ';
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.find(reactions + 'a.label').popup({'position': 'bottom left', 'metadata': {'content': 'title', 'title': 'none'}});
|
||||||
|
|
||||||
|
parent.find('.select-reaction > .menu > .item, ' + reactions + 'a.label').on('click', function(e){
|
||||||
|
var vm = this;
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if ($(this).hasClass('disabled')) return;
|
||||||
|
|
||||||
|
var actionURL = $(this).hasClass('item') ?
|
||||||
|
$(this).closest('.select-reaction').data('action-url') :
|
||||||
|
$(this).data('action-url');
|
||||||
|
var url = actionURL + '/' + ($(this).hasClass('blue') ? 'unreact' : 'react');
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: url,
|
||||||
|
data: {
|
||||||
|
'_csrf': csrf,
|
||||||
|
'content': $(this).data('content')
|
||||||
|
}
|
||||||
|
}).done(function(resp) {
|
||||||
|
if (resp && (resp.html || resp.empty)) {
|
||||||
|
var content = $(vm).closest('.content');
|
||||||
|
var react = content.find('.segment.reactions');
|
||||||
|
if (react.length > 0) {
|
||||||
|
react.remove();
|
||||||
|
}
|
||||||
|
if (!resp.empty) {
|
||||||
|
react = $('<div class="ui attached segment reactions"></div>').appendTo(content);
|
||||||
|
react.html(resp.html);
|
||||||
|
var hasEmoji = react.find('.has-emoji');
|
||||||
|
for (var i = 0; i < hasEmoji.length; i++) {
|
||||||
|
emojify.run(hasEmoji.get(i));
|
||||||
|
}
|
||||||
|
react.find('.dropdown').dropdown();
|
||||||
|
initReactionSelector(react);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function initCommentForm() {
|
function initCommentForm() {
|
||||||
if ($('.comment.form').length == 0) {
|
if ($('.comment.form').length == 0) {
|
||||||
return
|
return
|
||||||
|
@ -594,6 +642,7 @@ function initRepository() {
|
||||||
$('#status').val($statusButton.data('status-val'));
|
$('#status').val($statusButton.data('status-val'));
|
||||||
$('#comment-form').submit();
|
$('#comment-form').submit();
|
||||||
});
|
});
|
||||||
|
initReactionSelector();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diff
|
// Diff
|
||||||
|
|
|
@ -548,7 +548,7 @@
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
margin-left: 4em;
|
margin-left: 4em;
|
||||||
.header {
|
> .header {
|
||||||
#avatar-arrow;
|
#avatar-arrow;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
padding: auto 15px;
|
padding: auto 15px;
|
||||||
|
@ -1350,6 +1350,43 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.segment.reactions, .select-reaction {
|
||||||
|
&.dropdown .menu {
|
||||||
|
right: 0!important;
|
||||||
|
left: auto!important;
|
||||||
|
> .header {
|
||||||
|
margin: 0.75rem 0 .5rem;
|
||||||
|
}
|
||||||
|
> .item {
|
||||||
|
float: left;
|
||||||
|
padding: .5rem .5rem !important;
|
||||||
|
img.emoji {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.segment.reactions {
|
||||||
|
padding: .3em 1em;
|
||||||
|
.ui.label {
|
||||||
|
padding: .4em;
|
||||||
|
&.disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
> img {
|
||||||
|
height: 1.5em !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.select-reaction {
|
||||||
|
float: none;
|
||||||
|
&:not(.active) a {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover .select-reaction a {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// End of .repository
|
// End of .repository
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,8 @@ const (
|
||||||
tplMilestoneNew base.TplName = "repo/issue/milestone_new"
|
tplMilestoneNew base.TplName = "repo/issue/milestone_new"
|
||||||
tplMilestoneEdit base.TplName = "repo/issue/milestone_edit"
|
tplMilestoneEdit base.TplName = "repo/issue/milestone_edit"
|
||||||
|
|
||||||
|
tplReactions base.TplName = "repo/issue/view_content/reactions"
|
||||||
|
|
||||||
issueTemplateKey = "IssueTemplate"
|
issueTemplateKey = "IssueTemplate"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -726,9 +728,8 @@ func GetActionIssue(ctx *context.Context) *models.Issue {
|
||||||
ctx.NotFoundOrServerError("GetIssueByIndex", models.IsErrIssueNotExist, err)
|
ctx.NotFoundOrServerError("GetIssueByIndex", models.IsErrIssueNotExist, err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if issue.IsPull && !ctx.Repo.Repository.UnitEnabled(models.UnitTypePullRequests) ||
|
checkIssueRights(ctx, issue)
|
||||||
!issue.IsPull && !ctx.Repo.Repository.UnitEnabled(models.UnitTypeIssues) {
|
if ctx.Written() {
|
||||||
ctx.Handle(404, "IssueOrPullRequestUnitNotAllowed", nil)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err = issue.LoadAttributes(); err != nil {
|
if err = issue.LoadAttributes(); err != nil {
|
||||||
|
@ -738,6 +739,13 @@ func GetActionIssue(ctx *context.Context) *models.Issue {
|
||||||
return issue
|
return issue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkIssueRights(ctx *context.Context, issue *models.Issue) {
|
||||||
|
if issue.IsPull && !ctx.Repo.Repository.UnitEnabled(models.UnitTypePullRequests) ||
|
||||||
|
!issue.IsPull && !ctx.Repo.Repository.UnitEnabled(models.UnitTypeIssues) {
|
||||||
|
ctx.Handle(404, "IssueOrPullRequestUnitNotAllowed", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getActionIssues(ctx *context.Context) []*models.Issue {
|
func getActionIssues(ctx *context.Context) []*models.Issue {
|
||||||
commaSeparatedIssueIDs := ctx.Query("issue_ids")
|
commaSeparatedIssueIDs := ctx.Query("issue_ids")
|
||||||
if len(commaSeparatedIssueIDs) == 0 {
|
if len(commaSeparatedIssueIDs) == 0 {
|
||||||
|
@ -1259,3 +1267,146 @@ func DeleteMilestone(ctx *context.Context) {
|
||||||
"redirect": ctx.Repo.RepoLink + "/milestones",
|
"redirect": ctx.Repo.RepoLink + "/milestones",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChangeIssueReaction create a reaction for issue
|
||||||
|
func ChangeIssueReaction(ctx *context.Context, form auth.ReactionForm) {
|
||||||
|
issue := GetActionIssue(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.HasError() {
|
||||||
|
ctx.Handle(500, "ChangeIssueReaction", errors.New(ctx.GetErrMsg()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ctx.Params(":action") {
|
||||||
|
case "react":
|
||||||
|
reaction, err := models.CreateIssueReaction(ctx.User, issue, form.Content)
|
||||||
|
if err != nil {
|
||||||
|
log.Info("CreateIssueReaction: %s", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Reload new reactions
|
||||||
|
issue.Reactions = nil
|
||||||
|
if err = issue.LoadAttributes(); err != nil {
|
||||||
|
log.Info("issue.LoadAttributes: %s", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace("Reaction for issue created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, reaction.ID)
|
||||||
|
case "unreact":
|
||||||
|
if err := models.DeleteIssueReaction(ctx.User, issue, form.Content); err != nil {
|
||||||
|
ctx.Handle(500, "DeleteIssueReaction", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload new reactions
|
||||||
|
issue.Reactions = nil
|
||||||
|
if err := issue.LoadAttributes(); err != nil {
|
||||||
|
log.Info("issue.LoadAttributes: %s", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace("Reaction for issue removed: %d/%d", ctx.Repo.Repository.ID, issue.ID)
|
||||||
|
default:
|
||||||
|
ctx.Handle(404, fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(issue.Reactions) == 0 {
|
||||||
|
ctx.JSON(200, map[string]interface{}{
|
||||||
|
"empty": true,
|
||||||
|
"html": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
html, err := ctx.HTMLString(string(tplReactions), map[string]interface{}{
|
||||||
|
"ctx": ctx.Data,
|
||||||
|
"ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
|
||||||
|
"Reactions": issue.Reactions.GroupByType(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.Handle(500, "ChangeIssueReaction.HTMLString", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(200, map[string]interface{}{
|
||||||
|
"html": html,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeCommentReaction create a reaction for comment
|
||||||
|
func ChangeCommentReaction(ctx *context.Context, form auth.ReactionForm) {
|
||||||
|
comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := models.GetIssueByID(comment.IssueID)
|
||||||
|
checkIssueRights(ctx, issue)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.HasError() {
|
||||||
|
ctx.Handle(500, "ChangeCommentReaction", errors.New(ctx.GetErrMsg()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ctx.Params(":action") {
|
||||||
|
case "react":
|
||||||
|
reaction, err := models.CreateCommentReaction(ctx.User, issue, comment, form.Content)
|
||||||
|
if err != nil {
|
||||||
|
log.Info("CreateCommentReaction: %s", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Reload new reactions
|
||||||
|
comment.Reactions = nil
|
||||||
|
if err = comment.LoadReactions(); err != nil {
|
||||||
|
log.Info("comment.LoadReactions: %s", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace("Reaction for comment created: %d/%d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID, reaction.ID)
|
||||||
|
case "unreact":
|
||||||
|
if err := models.DeleteCommentReaction(ctx.User, issue, comment, form.Content); err != nil {
|
||||||
|
ctx.Handle(500, "DeleteCommentReaction", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload new reactions
|
||||||
|
comment.Reactions = nil
|
||||||
|
if err = comment.LoadReactions(); err != nil {
|
||||||
|
log.Info("comment.LoadReactions: %s", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace("Reaction for comment removed: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
|
||||||
|
default:
|
||||||
|
ctx.Handle(404, fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(comment.Reactions) == 0 {
|
||||||
|
ctx.JSON(200, map[string]interface{}{
|
||||||
|
"empty": true,
|
||||||
|
"html": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
html, err := ctx.HTMLString(string(tplReactions), map[string]interface{}{
|
||||||
|
"ctx": ctx.Data,
|
||||||
|
"ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
|
||||||
|
"Reactions": comment.Reactions.GroupByType(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.Handle(500, "ChangeCommentReaction.HTMLString", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(200, map[string]interface{}{
|
||||||
|
"html": html,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -495,6 +495,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
m.Post("/cancel", repo.CancelStopwatch)
|
m.Post("/cancel", repo.CancelStopwatch)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
m.Post("/reactions/:action", bindIgnErr(auth.ReactionForm{}), repo.ChangeIssueReaction)
|
||||||
})
|
})
|
||||||
|
|
||||||
m.Post("/labels", reqRepoWriter, repo.UpdateIssueLabel)
|
m.Post("/labels", reqRepoWriter, repo.UpdateIssueLabel)
|
||||||
|
@ -505,6 +506,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
m.Group("/comments/:id", func() {
|
m.Group("/comments/:id", func() {
|
||||||
m.Post("", repo.UpdateCommentContent)
|
m.Post("", repo.UpdateCommentContent)
|
||||||
m.Post("/delete", repo.DeleteComment)
|
m.Post("/delete", repo.DeleteComment)
|
||||||
|
m.Post("/reactions/:action", bindIgnErr(auth.ReactionForm{}), repo.ChangeCommentReaction)
|
||||||
}, context.CheckAnyUnit(models.UnitTypeIssues, models.UnitTypePullRequests))
|
}, context.CheckAnyUnit(models.UnitTypeIssues, models.UnitTypePullRequests))
|
||||||
m.Group("/labels", func() {
|
m.Group("/labels", func() {
|
||||||
m.Post("/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel)
|
m.Post("/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel)
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
<div class="ui top attached header">
|
<div class="ui top attached header">
|
||||||
<span class="text grey"><a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.Name}}</a> {{.i18n.Tr "repo.issues.commented_at" .Issue.HashTag $createdStr | Safe}}</span>
|
<span class="text grey"><a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.Name}}</a> {{.i18n.Tr "repo.issues.commented_at" .Issue.HashTag $createdStr | Safe}}</span>
|
||||||
<div class="ui right actions">
|
<div class="ui right actions">
|
||||||
|
{{template "repo/issue/view_content/add_reaction" Dict "ctx" $ "ActionURL" (Printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index) }}
|
||||||
{{if .IsIssueOwner}}
|
{{if .IsIssueOwner}}
|
||||||
<div class="item action">
|
<div class="item action">
|
||||||
<a class="edit-content" href="#"><i class="octicon octicon-pencil"></i></a>
|
<a class="edit-content" href="#"><i class="octicon octicon-pencil"></i></a>
|
||||||
|
@ -37,6 +38,12 @@
|
||||||
<div class="raw-content hide">{{.Issue.Content}}</div>
|
<div class="raw-content hide">{{.Issue.Content}}</div>
|
||||||
<div class="edit-content-zone hide" data-write="issue-{{.Issue.ID}}-write" data-preview="issue-{{.Issue.ID}}-preview" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/content" data-context="{{.RepoLink}}"></div>
|
<div class="edit-content-zone hide" data-write="issue-{{.Issue.ID}}-write" data-preview="issue-{{.Issue.ID}}-preview" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/content" data-context="{{.RepoLink}}"></div>
|
||||||
</div>
|
</div>
|
||||||
|
{{$reactions := .Issue.Reactions.GroupByType}}
|
||||||
|
{{if $reactions}}
|
||||||
|
<div class="ui attached segment reactions">
|
||||||
|
{{template "repo/issue/view_content/reactions" Dict "ctx" $ "ActionURL" (Printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index) "Reactions" $reactions }}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
{{if .Issue.Attachments}}
|
{{if .Issue.Attachments}}
|
||||||
<div class="ui bottom attached segment">
|
<div class="ui bottom attached segment">
|
||||||
<div class="ui small images">
|
<div class="ui small images">
|
||||||
|
|
18
templates/repo/issue/view_content/add_reaction.tmpl
Normal file
18
templates/repo/issue/view_content/add_reaction.tmpl
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{{if .ctx.IsSigned}}
|
||||||
|
<div class="item action ui pointing top right select-reaction dropdown" data-action-url="{{ .ActionURL }}">
|
||||||
|
<a class="add-reaction">
|
||||||
|
<i class="octicon octicon-plus-small" style="width: 10px"></i>
|
||||||
|
<i class="octicon octicon-smiley"></i>
|
||||||
|
</a>
|
||||||
|
<div class="menu has-emoji">
|
||||||
|
<div class="header">{{ .ctx.i18n.Tr "repo.pick_reaction"}}</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="item" data-content="+1">:+1:</div>
|
||||||
|
<div class="item" data-content="-1">:-1:</div>
|
||||||
|
<div class="item" data-content="laugh">:laughing:</div>
|
||||||
|
<div class="item" data-content="confused">:confused:</div>
|
||||||
|
<div class="item" data-content="heart">:heart:</div>
|
||||||
|
<div class="item" data-content="hooray">:tada:</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
|
@ -22,6 +22,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{template "repo/issue/view_content/add_reaction" Dict "ctx" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID) }}
|
||||||
{{if or $.IsRepositoryAdmin (eq .Poster.ID $.SignedUserID)}}
|
{{if or $.IsRepositoryAdmin (eq .Poster.ID $.SignedUserID)}}
|
||||||
<div class="item action">
|
<div class="item action">
|
||||||
<a class="edit-content" href="#"><i class="octicon octicon-pencil"></i></a>
|
<a class="edit-content" href="#"><i class="octicon octicon-pencil"></i></a>
|
||||||
|
@ -41,6 +42,12 @@
|
||||||
<div class="raw-content hide">{{.Content}}</div>
|
<div class="raw-content hide">{{.Content}}</div>
|
||||||
<div class="edit-content-zone hide" data-write="issuecomment-{{.ID}}-write" data-preview="issuecomment-{{.ID}}-preview" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}"></div>
|
<div class="edit-content-zone hide" data-write="issuecomment-{{.ID}}-write" data-preview="issuecomment-{{.ID}}-preview" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}"></div>
|
||||||
</div>
|
</div>
|
||||||
|
{{$reactions := .Reactions.GroupByType}}
|
||||||
|
{{if $reactions}}
|
||||||
|
<div class="ui attached segment reactions">
|
||||||
|
{{template "repo/issue/view_content/reactions" Dict "ctx" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions }}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
{{if .Attachments}}
|
{{if .Attachments}}
|
||||||
<div class="ui bottom attached segment">
|
<div class="ui bottom attached segment">
|
||||||
<div class="ui small images">
|
<div class="ui small images">
|
||||||
|
|
15
templates/repo/issue/view_content/reactions.tmpl
Normal file
15
templates/repo/issue/view_content/reactions.tmpl
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{{range $key, $value := .Reactions}}
|
||||||
|
<a class="ui label basic{{if $value.HasUser $.ctx.SignedUserID}} blue{{end}}{{if not $.ctx.IsSigned}} disabled{{end}} has-emoji" data-title="{{$value.GetFirstUsers}}{{if gt ($value.GetMoreUserCount) 0}} {{ $.ctx.i18n.Tr "repo.reactions_more" $value.GetMoreUserCount}}{{end}}" data-content="{{ $key }}" data-action-url="{{ $.ActionURL }}">
|
||||||
|
{{if eq $key "hooray"}}
|
||||||
|
:tada:
|
||||||
|
{{else}}
|
||||||
|
{{if eq $key "laugh"}}
|
||||||
|
:laughing:
|
||||||
|
{{else}}
|
||||||
|
:{{$key}}:
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{len $value}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
{{template "repo/issue/view_content/add_reaction" Dict "ctx" $.ctx "ActionURL" .ActionURL }}
|
Loading…
Reference in a new issue