mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-23 21:04:55 +02:00
feat: add release reactions and a GitHub-like reaction bar (#38415)
Adds emoji reactions to releases and moves the reaction picker of issues, comments and releases into a GitHub-like reaction bar. - Releases omit 👎 or 😕 - Drafts and tags can't be reacted to - Moved menu to tippy - Added keyboard navigation Fixes: https://github.com/go-gitea/gitea/issues/34268 Fixes: https://github.com/go-gitea/gitea/issues/16676 Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
42 files changed
+508
-313
No files matched your search
@@ -73,7 +73,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
reactions, _, err := issues_model.FindCommentReactions(ctx, comment.IssueID, comment.ID)
|
||||
reactions, _, err := issues_model.FindCommentReactions(ctx, comment.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
|
||||
@@ -669,7 +669,6 @@ func (cpi *comparePageInfoType) prepareCreatePullRequestPage(ctx *context.Contex
|
||||
}
|
||||
|
||||
ctx.Data["IsProjectsEnabled"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
|
||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
||||
upload.AddUploadContext(ctx, "comment")
|
||||
|
||||
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypePullRequests)
|
||||
|
||||
+10
-14
@@ -25,6 +25,7 @@ import (
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/markup/markdown"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
@@ -45,7 +46,7 @@ const (
|
||||
tplIssueView templates.TplName = "repo/issue/view"
|
||||
|
||||
tplPullMergeBox templates.TplName = "repo/issue/view_content/pull_merge_box"
|
||||
tplReactions templates.TplName = "repo/issue/view_content/reactions"
|
||||
tplReactions templates.TplName = "shared/reactions"
|
||||
|
||||
issueTemplateKey = "IssueTemplate"
|
||||
issueTemplateTitleKey = "IssueTemplateTitle"
|
||||
@@ -536,25 +537,20 @@ func ChangeIssueReaction(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(issue.Reactions) == 0 {
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"empty": true,
|
||||
"html": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
renderReactions(ctx, fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index), issue.Reactions, setting.UI.Reactions)
|
||||
}
|
||||
|
||||
func renderReactions(ctx *context.Context, actionURL string, reactions issues_model.ReactionList, allowedReactions []string) {
|
||||
html, err := ctx.RenderToHTML(tplReactions, map[string]any{
|
||||
"ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
|
||||
"Reactions": issue.Reactions.GroupByType(),
|
||||
"ActionURL": actionURL,
|
||||
"Reactions": reactions.GroupByType(),
|
||||
"AllowedReactions": allowedReactions,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("ChangeIssueReaction.HTMLString", err)
|
||||
ctx.ServerError("RenderToHTML", err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"html": html,
|
||||
})
|
||||
ctx.JSON(http.StatusOK, map[string]any{"html": html})
|
||||
}
|
||||
|
||||
// GetIssueAttachments returns attachments for the issue
|
||||
|
||||
@@ -300,25 +300,7 @@ func ChangeCommentReaction(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(comment.Reactions) == 0 {
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"empty": true,
|
||||
"html": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
html, err := ctx.RenderToHTML(tplReactions, map[string]any{
|
||||
"ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
|
||||
"Reactions": comment.Reactions.GroupByType(),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("ChangeCommentReaction.HTMLString", err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"html": html,
|
||||
})
|
||||
renderReactions(ctx, fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID), comment.Reactions, setting.UI.Reactions)
|
||||
}
|
||||
|
||||
// GetCommentAttachments returns attachments for the comment
|
||||
|
||||
@@ -111,7 +111,6 @@ func NewIssue(ctx *context.Context) {
|
||||
|
||||
isProjectsEnabled := ctx.Repo.Permission.CanRead(unit.TypeProjects)
|
||||
ctx.Data["IsProjectsEnabled"] = isProjectsEnabled
|
||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
||||
upload.AddUploadContext(ctx, "comment")
|
||||
|
||||
pageMetaData := retrieveRepoIssueMetaDataWithPrefill(ctx, ctx.Repo.Repository, false)
|
||||
|
||||
@@ -362,7 +362,7 @@ func ViewIssue(ctx *context.Context) {
|
||||
}
|
||||
|
||||
ctx.Data["IsProjectsEnabled"] = ctx.Repo.Permission.CanRead(unit.TypeProjects)
|
||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
||||
ctx.Data["AllowedIssueReactions"] = setting.UI.Reactions
|
||||
upload.AddUploadContext(ctx, "comment")
|
||||
|
||||
if err := issue.LoadAttributes(ctx); err != nil {
|
||||
|
||||
@@ -908,12 +908,13 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
|
||||
ctx.Data["IsIssuePoster"] = ctx.Doer != nil && issue.IsPoster(ctx.Doer.ID)
|
||||
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
|
||||
|
||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
||||
// For files changed page
|
||||
PrepareBranchList(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["AllowedIssueReactions"] = setting.UI.Reactions
|
||||
upload.AddUploadContext(ctx, "comment")
|
||||
|
||||
ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
|
||||
|
||||
@@ -55,7 +55,6 @@ func RenderNewCodeCommentForm(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
ctx.Data["AfterCommitID"] = pullHeadCommitID
|
||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
||||
upload.AddUploadContext(ctx, "comment")
|
||||
ctx.HTML(http.StatusOK, tplNewComment)
|
||||
}
|
||||
@@ -186,7 +185,7 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
||||
ctx.Data["AllowedIssueReactions"] = setting.UI.Reactions
|
||||
upload.AddUploadContext(ctx, "comment")
|
||||
|
||||
ctx.Data["comments"] = comments
|
||||
|
||||
+85
-13
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"gitea.dev/models/db"
|
||||
git_model "gitea.dev/models/git"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/models/renderhelper"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
@@ -73,6 +74,7 @@ type ReleaseInfo struct {
|
||||
Release *repo_model.Release
|
||||
CommitStatus *git_model.CommitStatus
|
||||
CommitStatuses []*git_model.CommitStatus
|
||||
Reactions issues_model.ReactionList
|
||||
}
|
||||
|
||||
func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) ([]*ReleaseInfo, error) {
|
||||
@@ -81,17 +83,31 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
releaseIDs := make([]int64, 0, len(releases))
|
||||
for _, release := range releases {
|
||||
release.Repo = ctx.Repo.Repository
|
||||
releaseIDs = append(releaseIDs, release.ID)
|
||||
}
|
||||
|
||||
if err = repo_model.GetReleaseAttachments(ctx, releases...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reactions, err := issues_model.FindReleaseReactions(ctx, releaseIDs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cacheUsers, err := reactions.LoadUsers(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reactionsByRelease := make(map[int64]issues_model.ReactionList)
|
||||
for _, reaction := range reactions {
|
||||
reactionsByRelease[reaction.ReleaseID] = append(reactionsByRelease[reaction.ReleaseID], reaction)
|
||||
}
|
||||
|
||||
// Temporary cache commits count of used branches to speed up.
|
||||
countCache := make(map[string]int64)
|
||||
cacheUsers := make(map[int64]*user_model.User)
|
||||
if ctx.Doer != nil {
|
||||
cacheUsers[ctx.Doer.ID] = ctx.Doer
|
||||
}
|
||||
@@ -138,7 +154,8 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
|
||||
}
|
||||
|
||||
info := &ReleaseInfo{
|
||||
Release: r,
|
||||
Release: r,
|
||||
Reactions: reactionsByRelease[r.ID],
|
||||
}
|
||||
|
||||
if canReadCode {
|
||||
@@ -154,10 +171,22 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
|
||||
return releaseInfos, nil
|
||||
}
|
||||
|
||||
// Releases render releases list page
|
||||
func Releases(ctx *context.Context) {
|
||||
func prepareReleasePageCommon(ctx *context.Context) (ret struct {
|
||||
writeAccess bool
|
||||
},
|
||||
) {
|
||||
ctx.Data["PageIsReleaseList"] = true
|
||||
ctx.Data["Title"] = ctx.Tr("repo.release.releases")
|
||||
ctx.Data["AllowedReleaseReactions"] = release_service.AllowedReleaseReactions()
|
||||
|
||||
ret.writeAccess = ctx.Repo.Permission.CanWrite(unit.TypeReleases)
|
||||
ctx.Data["CanCreateRelease"] = ret.writeAccess && !ctx.Repo.Repository.IsArchived
|
||||
return ret
|
||||
}
|
||||
|
||||
// Releases render releases list page
|
||||
func Releases(ctx *context.Context) {
|
||||
preparedData := prepareReleasePageCommon(ctx)
|
||||
|
||||
listOptions := db.ListOptions{
|
||||
Page: ctx.FormInt("page"),
|
||||
@@ -170,13 +199,10 @@ func Releases(ctx *context.Context) {
|
||||
listOptions.PageSize = setting.API.MaxResponseItems
|
||||
}
|
||||
|
||||
writeAccess := ctx.Repo.Permission.CanWrite(unit.TypeReleases)
|
||||
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
|
||||
|
||||
releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{
|
||||
ListOptions: listOptions,
|
||||
// only show draft releases for users who can write, read-only users shouldn't see draft releases.
|
||||
IncludeDrafts: writeAccess,
|
||||
IncludeDrafts: preparedData.writeAccess,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -275,17 +301,14 @@ func releasesOrTagsFeed(ctx *context.Context, isReleasesOnly bool, formatType st
|
||||
|
||||
// SingleRelease renders a single release's page
|
||||
func SingleRelease(ctx *context.Context) {
|
||||
ctx.Data["PageIsReleaseList"] = true
|
||||
|
||||
writeAccess := ctx.Repo.Permission.CanWrite(unit.TypeReleases)
|
||||
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
|
||||
preparedData := prepareReleasePageCommon(ctx)
|
||||
|
||||
releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{
|
||||
ListOptions: db.ListOptions{Page: 1, PageSize: 1},
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
TagNames: []string{ctx.PathParam("*")},
|
||||
// only show draft releases for users who can write, read-only users shouldn't see draft releases.
|
||||
IncludeDrafts: writeAccess,
|
||||
IncludeDrafts: preparedData.writeAccess,
|
||||
IncludeTags: true,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -681,3 +704,52 @@ func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
|
||||
|
||||
redirect()
|
||||
}
|
||||
|
||||
func ChangeReleaseReaction(ctx *context.Context) {
|
||||
form := context.GetFetchActionForm[*forms.ReactionForm](ctx)
|
||||
if form == nil {
|
||||
return
|
||||
}
|
||||
|
||||
rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
ctx.ServerError("GetReleaseForRepoByID", err)
|
||||
return
|
||||
}
|
||||
if rel.IsTag || rel.IsDraft {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
rel.Repo = ctx.Repo.Repository
|
||||
|
||||
switch ctx.PathParam("action") {
|
||||
case "react":
|
||||
_, err = release_service.CreateReleaseReaction(ctx, ctx.Doer, rel, form.Content)
|
||||
if errors.Is(err, util.ErrPermissionDenied) {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
} else if err != nil && !errors.Is(err, util.ErrAlreadyExist) {
|
||||
ctx.ServerError("CreateReleaseReaction", err)
|
||||
return
|
||||
}
|
||||
case "unreact":
|
||||
if err = issues_model.DeleteReleaseReaction(ctx, ctx.Doer.ID, rel.ID, form.Content); err != nil {
|
||||
ctx.ServerError("DeleteReleaseReaction", err)
|
||||
return
|
||||
}
|
||||
default:
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
reactions, err := issues_model.FindReleaseReactions(ctx, rel.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindReleaseReactions", err)
|
||||
return
|
||||
}
|
||||
if _, err = reactions.LoadUsers(ctx, ctx.Repo.Repository); err != nil {
|
||||
ctx.ServerError("LoadUsers", err)
|
||||
return
|
||||
}
|
||||
renderReactions(ctx, fmt.Sprintf("%s/releases/%d/reactions", ctx.Repo.RepoLink, rel.ID), reactions, release_service.AllowedReleaseReactions())
|
||||
}
|
||||
+3
-2
@@ -224,13 +224,13 @@ func verifyAuthWithOptionsWeb(options *common.VerifyOptions) func(ctx *context.C
|
||||
}
|
||||
|
||||
if options.SignInRequired && !ctx.IsSigned {
|
||||
ctx.Redirect(middleware.RedirectLinkUserLogin(ctx.Req))
|
||||
ctx.RedirectForLogin(middleware.RedirectLinkUserLogin(ctx.Req))
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect to log in page if auto-signin info is provided and has not signed in.
|
||||
if !options.SignOutRequired && !ctx.IsSigned && ctx.GetSiteCookie(setting.CookieRememberName) != "" {
|
||||
ctx.Redirect(middleware.RedirectLinkUserLogin(ctx.Req))
|
||||
ctx.RedirectForLogin(middleware.RedirectLinkUserLogin(ctx.Req))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1503,6 +1503,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
}, ctxDataSet(reqctx.ContextData{"EnableFeed": setting.Other.EnableFeed}))
|
||||
m.Get("/releases/attachments/{uuid}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment)
|
||||
m.Get("/releases/download/{vTag}/{fileName}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.RedirectDownload)
|
||||
m.Post("/releases/{id}/reactions/{action}", reqSignIn, context.RepoMustNotBeArchived(), repo.ChangeReleaseReaction)
|
||||
m.Group("/releases", func() {
|
||||
m.Get("/new", repo.NewRelease)
|
||||
m.Post("/new", web.Bind[*forms.NewReleaseForm](), repo.NewReleasePost)
|
||||
|
||||
Reference in new issue
Block a user