Skip to content

Show Release Notification in the UI #27058

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 156 additions & 2 deletions models/activities/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ const (
NotificationSourceCommit
// NotificationSourceRepository is a notification for a repository
NotificationSourceRepository
// NotificationSourceRepository is a notification for a release
NotificationSourceRelease
)

// Notification represents a notification
Expand All @@ -64,13 +66,15 @@ type Notification struct {
IssueID int64 `xorm:"INDEX NOT NULL"`
CommitID string `xorm:"INDEX"`
CommentID int64
ReleaseID int64 `xorm:"INDEX"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a migration

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Migration will be added after the Code is reviewed.


UpdatedBy int64 `xorm:"INDEX NOT NULL"`

Issue *issues_model.Issue `xorm:"-"`
Repository *repo_model.Repository `xorm:"-"`
Comment *issues_model.Comment `xorm:"-"`
User *user_model.User `xorm:"-"`
Release *repo_model.Release `xorm:"-"`

CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated INDEX NOT NULL"`
Expand Down Expand Up @@ -260,7 +264,7 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n
continue
}

if notificationExists(notifications, issue.ID, userID) {
if issueNotificationExists(notifications, issue.ID, userID) {
if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil {
return err
}
Expand All @@ -280,7 +284,7 @@ func getNotificationsByIssueID(ctx context.Context, issueID int64) (notification
return notifications, err
}

func notificationExists(notifications []*Notification, issueID, userID int64) bool {
func issueNotificationExists(notifications []*Notification, issueID, userID int64) bool {
for _, notification := range notifications {
if notification.IssueID == issueID && notification.UserID == userID {
return true
Expand Down Expand Up @@ -341,6 +345,91 @@ func GetIssueNotification(ctx context.Context, userID, issueID int64) (*Notifica
return notification, err
}

// CreateOrUpdateIssueNotifications creates an release notification
// for each watcher, or updates it if already exists
// this function called from a queue, so we can't pass a context
func CreateOrUpdateReleaseNotifications(releaseID, receiverID int64) error {
ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()

release, err := repo_model.GetReleaseByID(ctx, releaseID)
if err != nil {
return err
}

notifications, err := getNotificationsByReleaseID(ctx, release.ID)
if err != nil {
return err
}

if releaseNotificationExists(notifications, release.ID, receiverID) {
err = updateReleaseNotification(ctx, receiverID, release)
} else {
err = createReleaseNotification(ctx, receiverID, release)
}
if err != nil {
return err
}

return committer.Commit()
}

func getNotificationsByReleaseID(ctx context.Context, releaseID int64) (notifications []*Notification, err error) {
err = db.GetEngine(ctx).
Where("release_id = ?", releaseID).
Find(&notifications)
return notifications, err
}

func releaseNotificationExists(notifications []*Notification, releaseID, userID int64) bool {
for _, notification := range notifications {
if notification.ReleaseID == releaseID && notification.UserID == userID {
return true
}
}

return false
}

func createReleaseNotification(ctx context.Context, userID int64, release *repo_model.Release) error {
notification := &Notification{
UserID: userID,
RepoID: release.RepoID,
Status: NotificationStatusUnread,
ReleaseID: release.ID,
Source: NotificationSourceRelease,
UpdatedBy: release.PublisherID,
}

return db.Insert(ctx, notification)
}

func updateReleaseNotification(ctx context.Context, userID int64, release *repo_model.Release) error {
notification, err := GetReleaseNotification(ctx, userID, release.ID)
if err != nil {
return err
}

notification.Status = NotificationStatusUnread
notification.UpdatedBy = release.PublisherID

_, err = db.GetEngine(ctx).ID(notification.ID).Cols([]string{"status", "update_by"}...).Update(notification)
return err
}

// GetIssueNotification return the notification about an release
func GetReleaseNotification(ctx context.Context, userID, releaseID int64) (*Notification, error) {
notification := new(Notification)
_, err := db.GetEngine(ctx).
Where("user_id = ?", userID).
And("release_id = ?", releaseID).
Get(notification)
return notification, err
}

// NotificationsForUser returns notifications for a given user and status
func NotificationsForUser(ctx context.Context, user *user_model.User, statuses []NotificationStatus, page, perPage int) (notifications NotificationList, err error) {
if len(statuses) == 0 {
Expand Down Expand Up @@ -384,6 +473,9 @@ func (n *Notification) LoadAttributes(ctx context.Context) (err error) {
if err = n.loadComment(ctx); err != nil {
return err
}
if err = n.loadRelease(ctx); err != nil {
return err
}
return err
}

Expand Down Expand Up @@ -434,6 +526,24 @@ func (n *Notification) loadUser(ctx context.Context) (err error) {
return nil
}

func (n *Notification) loadRelease(ctx context.Context) (err error) {
if n.Source != NotificationSourceRelease || n.Release != nil {
return nil
}

n.Release, err = repo_model.GetReleaseByID(ctx, n.ReleaseID)
if err != nil {
return err
}

err = n.Release.LoadAttributes(ctx)
if err != nil {
return err
}

return nil
}

// GetRepo returns the repo of the notification
func (n *Notification) GetRepo() (*repo_model.Repository, error) {
return n.Repository, n.loadRepo(db.DefaultContext)
Expand Down Expand Up @@ -472,6 +582,8 @@ func (n *Notification) Link() string {
return n.Repository.Link() + "/commit/" + url.PathEscape(n.CommitID)
case NotificationSourceRepository:
return n.Repository.Link()
case NotificationSourceRelease:
return n.Release.Link()
}
return ""
}
Expand Down Expand Up @@ -717,6 +829,31 @@ func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) {
return failures, nil
}

func (nl NotificationList) LoadReleases(ctx context.Context) ([]int, error) {
if len(nl) == 0 {
return []int{}, nil
}

failures := []int{}
for i, notification := range nl {
if notification.Source != NotificationSourceRelease {
continue
}

release, err := repo_model.GetReleaseByID(ctx, notification.ReleaseID)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's low efficiency. We need to id in () to fix the possible performance problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetReleaseByID does not more than doing a Database query with the ID. It does not load other things if it's that what you mean.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can find many examples to a batch loading. Use a for loop to get all release id and then get all releases use id in () and then assign release to notification.

if err != nil {
if repo_model.IsErrReleaseNotExist(err) {
failures = append(failures, i)
} else {
return nil, err
}
}
release.Repo = notification.Repository
notification.Release = release
}
return failures, nil
}

// GetNotificationCount returns the notification count for user
func GetNotificationCount(ctx context.Context, user *user_model.User, status NotificationStatus) (count int64, err error) {
count, err = db.GetEngine(ctx).
Expand Down Expand Up @@ -767,6 +904,23 @@ func setIssueNotificationStatusReadIfUnread(ctx context.Context, userID, issueID
return err
}

// SetReleaseReadBy sets release to be read by given user.
func SetReleaseReadBy(ctx context.Context, releaseID, userID int64) error {
notification, err := GetReleaseNotification(ctx, userID, releaseID)
if err != nil {
return err
}

if notification.Status != NotificationStatusUnread {
return nil
}

notification.Status = NotificationStatusRead

_, err = db.GetEngine(ctx).ID(notification.ID).Cols("status").Update(notification)
return err
}

// SetRepoReadBy sets repo to be visited by given user.
func SetRepoReadBy(ctx context.Context, userID, repoID int64) error {
_, err := db.GetEngine(ctx).Where(builder.Eq{
Expand Down
2 changes: 2 additions & 0 deletions modules/structs/notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,6 @@ const (
NotifySubjectCommit NotifySubjectType = "Commit"
// NotifySubjectRepository an repository is subject of an notification
NotifySubjectRepository NotifySubjectType = "Repository"
// NotifySubjectRelease an release is subject of an notification
NotifySubjectRelease NotifySubjectType = "Release"
)
2 changes: 2 additions & 0 deletions routers/api/v1/notify/notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ func subjectToSource(value []string) (result []activities_model.NotificationSour
result = append(result, activities_model.NotificationSourceCommit)
case "repository":
result = append(result, activities_model.NotificationSourceRepository)
case "release":
result = append(result, activities_model.NotificationSourceRelease)
}
}
return result
Expand Down
2 changes: 1 addition & 1 deletion routers/api/v1/notify/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func ListRepoNotifications(ctx *context.APIContext) {
// collectionFormat: multi
// items:
// type: string
// enum: [issue,pull,commit,repository]
// enum: [issue,pull,commit,repository,release]
// - name: since
// in: query
// description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
Expand Down
2 changes: 1 addition & 1 deletion routers/api/v1/notify/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func ListNotifications(ctx *context.APIContext) {
// collectionFormat: multi
// items:
// type: string
// enum: [issue,pull,commit,repository]
// enum: [issue,pull,commit,repository,release]
// - name: since
// in: query
// description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
Expand Down
9 changes: 9 additions & 0 deletions routers/web/repo/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"

"code.gitea.io/gitea/models"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
Expand Down Expand Up @@ -291,6 +292,14 @@ func SingleRelease(ctx *context.Context) {
return
}

if ctx.IsSigned && !release.IsTag {
err = activities_model.SetReleaseReadBy(ctx, release.ID, ctx.Doer.ID)
if err != nil {
ctx.ServerError("SetReleaseReadBy", err)
return
}
}

ctx.Data["Releases"] = []*repo_model.Release{release}
ctx.HTML(http.StatusOK, tplReleasesList)
}
Expand Down
8 changes: 8 additions & 0 deletions routers/web/user/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ func getNotifications(ctx *context.Context) {
notifications = notifications.Without(failures)
failCount += len(failures)

failures, err = notifications.LoadReleases(ctx)
if err != nil {
ctx.ServerError("LoadReleases", err)
return
}
notifications = notifications.Without(failures)
failCount += len(failures)

if failCount > 0 {
ctx.Flash.Error(fmt.Sprintf("ERROR: %d notifications were removed due to missing parts - check the logs", failCount))
}
Expand Down
7 changes: 7 additions & 0 deletions services/convert/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ func ToNotificationThread(n *activities_model.Notification) *api.NotificationThr
URL: n.Repository.Link(),
HTMLURL: n.Repository.HTMLURL(),
}
case activities_model.NotificationSourceRelease:
result.Subject = &api.NotificationSubject{Type: api.NotifySubjectRelease}
if n.Release != nil {
result.Subject.Title = n.Release.Title
result.Subject.URL = n.Release.APIURL()
result.Subject.HTMLURL = n.Release.HTMLURL()
}
}

return result
Expand Down
Loading