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:
authored and GitHub committed 2026-10-20 15:04:10 +02:00
1 parent e645afbb3f
commit edd0c0e2ce
42 files changed
+508 -313

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package release
import (
"context"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
)
func AllowedReleaseReactions() []string {
return setting.UI.ReleaseReactions
}
func CreateReleaseReaction(ctx context.Context, doer *user_model.User, rel *repo_model.Release, content string) (*issues_model.Reaction, error) {
if err := rel.LoadRepo(ctx); err != nil {
return nil, err
}
if user_model.IsUserBlockedBy(ctx, doer, rel.PublisherID, rel.Repo.OwnerID) {
return nil, user_model.ErrBlockedUser
}
return issues_model.CreateReaction(ctx, setting.UI.ReleaseReactionsLookup, &issues_model.ReactionOptions{
Type: content,
DoerID: doer.ID,
ReleaseID: rel.ID,
})
}
+5
View File
@@ -11,6 +11,7 @@ import (
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
@@ -421,6 +422,10 @@ func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *re
return fmt.Errorf("DeleteAttachments: %w", err)
}
if _, err := db.DeleteByBean(ctx, &issues_model.Reaction{ReleaseID: rel.ID}); err != nil {
return fmt.Errorf("DeleteReleaseByID: %w", err)
}
for i := range rel.Attachments {
attachment := rel.Attachments[i]
if err := storage.Attachments.Delete(attachment.RelativePath()); err != nil {
+21
View File
@@ -8,6 +8,8 @@ import (
"testing"
"time"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
@@ -407,6 +409,25 @@ func TestCreateNewTag(t *testing.T) {
"v2.0 is released \n\n BUGFIX: .... \n\n 123"))
}
func TestCreateReleaseReaction(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 1})
assert.Equal(t, []string{"+1", "laugh", "hooray", "heart", "rocket", "eyes"}, AllowedReleaseReactions())
_, err := CreateReleaseReaction(t.Context(), user, release, "heart")
assert.NoError(t, err)
assert.NoError(t, DeleteReleaseByID(t.Context(), repo, release, user, false))
unittest.AssertNotExistsBean(t, &issues_model.Reaction{ReleaseID: release.ID})
assert.NoError(t, db.Insert(t.Context(), &user_model.Blocking{BlockerID: repo.OwnerID, BlockeeID: user.ID}))
_, err = CreateReleaseReaction(t.Context(), user, release, "heart")
assert.ErrorIs(t, err, user_model.ErrBlockedUser)
}
func TestRelease_DatedByTargetCommit(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})