]> source.dussan.org Git - gitea.git/commitdiff
Add notification interface and refactor UI notifications (#5085)
authorLunny Xiao <xiaolunwen@gmail.com>
Thu, 18 Oct 2018 11:23:05 +0000 (19:23 +0800)
committerGitHub <noreply@github.com>
Thu, 18 Oct 2018 11:23:05 +0000 (19:23 +0800)
* add notification interface and refactor UI notifications

* add missing methods on notification interface and notifiy only issue status really changed

* implement NotifyPullRequestReview for ui notification

models/issue.go
models/review.go
modules/notification/base/base.go [new file with mode: 0644]
modules/notification/notification.go
modules/notification/ui/ui.go [new file with mode: 0644]
routers/api/v1/repo/issue.go
routers/api/v1/repo/issue_comment.go
routers/api/v1/repo/pull.go
routers/repo/issue.go
routers/repo/pull.go
routers/repo/pull_review.go

index a3274104358dfbb26cb1eee9762b6561564c4259..8dc0466752dd3a50a106eec859839988b6e1c58a 100644 (file)
@@ -112,6 +112,10 @@ func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) {
        }
 
        pr, err = getPullRequestByIssueID(x, issue.ID)
+       if err != nil {
+               return nil, err
+       }
+       pr.Issue = issue
        return
 }
 
index 3326ea0549d2ffb227e2cb72e0489948b6bfc9e0..dd8743586d43cd05c7c02e0eb4f0dfb0a4e2893a 100644 (file)
@@ -239,6 +239,8 @@ func getCurrentReview(e Engine, reviewer *User, issue *Issue) (*Review, error) {
        if len(reviews) == 0 {
                return nil, ErrReviewNotExist{}
        }
+       reviews[0].Reviewer = reviewer
+       reviews[0].Issue = issue
        return reviews[0], nil
 }
 
diff --git a/modules/notification/base/base.go b/modules/notification/base/base.go
new file mode 100644 (file)
index 0000000..bac90f5
--- /dev/null
@@ -0,0 +1,43 @@
+// Copyright 2018 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 base
+
+import (
+       "code.gitea.io/git"
+       "code.gitea.io/gitea/models"
+)
+
+// Notifier defines an interface to notify receiver
+type Notifier interface {
+       Run()
+
+       NotifyCreateRepository(doer *models.User, u *models.User, repo *models.Repository)
+       NotifyMigrateRepository(doer *models.User, u *models.User, repo *models.Repository)
+       NotifyDeleteRepository(doer *models.User, repo *models.Repository)
+       NotifyForkRepository(doer *models.User, oldRepo, repo *models.Repository)
+
+       NotifyNewIssue(*models.Issue)
+       NotifyIssueChangeStatus(*models.User, *models.Issue, bool)
+       NotifyIssueChangeMilestone(doer *models.User, issue *models.Issue)
+       NotifyIssueChangeAssignee(doer *models.User, issue *models.Issue, removed bool)
+       NotifyIssueChangeContent(doer *models.User, issue *models.Issue, oldContent string)
+       NotifyIssueClearLabels(doer *models.User, issue *models.Issue)
+       NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string)
+       NotifyIssueChangeLabels(doer *models.User, issue *models.Issue,
+               addedLabels []*models.Label, removedLabels []*models.Label)
+
+       NotifyNewPullRequest(*models.PullRequest)
+       NotifyMergePullRequest(*models.PullRequest, *models.User, *git.Repository)
+       NotifyPullRequestReview(*models.PullRequest, *models.Review, *models.Comment)
+
+       NotifyCreateIssueComment(*models.User, *models.Repository,
+               *models.Issue, *models.Comment)
+       NotifyUpdateComment(*models.User, *models.Comment, string)
+       NotifyDeleteComment(*models.User, *models.Comment)
+
+       NotifyNewRelease(rel *models.Release)
+       NotifyUpdateRelease(doer *models.User, rel *models.Release)
+       NotifyDeleteRelease(doer *models.User, rel *models.Release)
+}
index ffe885240bcd740b72de4b0dce7d15795fce9bf3..3f3579394eb4b728b6b424b9ecac2b2b64d7099b 100644 (file)
-// Copyright 2016 The Gitea Authors. All rights reserved.
+// Copyright 2018 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 notification
 
 import (
+       "code.gitea.io/git"
        "code.gitea.io/gitea/models"
-       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/notification/base"
+       "code.gitea.io/gitea/modules/notification/ui"
 )
 
-type (
-       notificationService struct {
-               issueQueue chan issueNotificationOpts
+var (
+       notifiers []base.Notifier
+)
+
+// RegisterNotifier providers method to receive notify messages
+func RegisterNotifier(notifier base.Notifier) {
+       go notifier.Run()
+       notifiers = append(notifiers, notifier)
+}
+
+func init() {
+       RegisterNotifier(ui.NewNotifier())
+}
+
+// NotifyCreateIssueComment notifies issue comment related message to notifiers
+func NotifyCreateIssueComment(doer *models.User, repo *models.Repository,
+       issue *models.Issue, comment *models.Comment) {
+       for _, notifier := range notifiers {
+               notifier.NotifyCreateIssueComment(doer, repo, issue, comment)
        }
+}
 
-       issueNotificationOpts struct {
-               issue                *models.Issue
-               notificationAuthorID int64
+// NotifyNewIssue notifies new issue to notifiers
+func NotifyNewIssue(issue *models.Issue) {
+       for _, notifier := range notifiers {
+               notifier.NotifyNewIssue(issue)
        }
-)
+}
 
-var (
-       // Service is the notification service
-       Service = &notificationService{
-               issueQueue: make(chan issueNotificationOpts, 100),
+// NotifyIssueChangeStatus notifies close or reopen issue to notifiers
+func NotifyIssueChangeStatus(doer *models.User, issue *models.Issue, closeOrReopen bool) {
+       for _, notifier := range notifiers {
+               notifier.NotifyIssueChangeStatus(doer, issue, closeOrReopen)
        }
-)
+}
 
-func init() {
-       go Service.Run()
+// NotifyMergePullRequest notifies merge pull request to notifiers
+func NotifyMergePullRequest(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repository) {
+       for _, notifier := range notifiers {
+               notifier.NotifyMergePullRequest(pr, doer, baseGitRepo)
+       }
+}
+
+// NotifyNewPullRequest notifies new pull request to notifiers
+func NotifyNewPullRequest(pr *models.PullRequest) {
+       for _, notifier := range notifiers {
+               notifier.NotifyNewPullRequest(pr)
+       }
+}
+
+// NotifyPullRequestReview notifies new pull request review
+func NotifyPullRequestReview(pr *models.PullRequest, review *models.Review, comment *models.Comment) {
+       for _, notifier := range notifiers {
+               notifier.NotifyPullRequestReview(pr, review, comment)
+       }
+}
+
+// NotifyUpdateComment notifies update comment to notifiers
+func NotifyUpdateComment(doer *models.User, c *models.Comment, oldContent string) {
+       for _, notifier := range notifiers {
+               notifier.NotifyUpdateComment(doer, c, oldContent)
+       }
+}
+
+// NotifyDeleteComment notifies delete comment to notifiers
+func NotifyDeleteComment(doer *models.User, c *models.Comment) {
+       for _, notifier := range notifiers {
+               notifier.NotifyDeleteComment(doer, c)
+       }
+}
+
+// NotifyDeleteRepository notifies delete repository to notifiers
+func NotifyDeleteRepository(doer *models.User, repo *models.Repository) {
+       for _, notifier := range notifiers {
+               notifier.NotifyDeleteRepository(doer, repo)
+       }
+}
+
+// NotifyForkRepository notifies fork repository to notifiers
+func NotifyForkRepository(doer *models.User, oldRepo, repo *models.Repository) {
+       for _, notifier := range notifiers {
+               notifier.NotifyForkRepository(doer, oldRepo, repo)
+       }
+}
+
+// NotifyNewRelease notifies new release to notifiers
+func NotifyNewRelease(rel *models.Release) {
+       for _, notifier := range notifiers {
+               notifier.NotifyNewRelease(rel)
+       }
+}
+
+// NotifyUpdateRelease notifies update release to notifiers
+func NotifyUpdateRelease(doer *models.User, rel *models.Release) {
+       for _, notifier := range notifiers {
+               notifier.NotifyUpdateRelease(doer, rel)
+       }
+}
+
+// NotifyDeleteRelease notifies delete release to notifiers
+func NotifyDeleteRelease(doer *models.User, rel *models.Release) {
+       for _, notifier := range notifiers {
+               notifier.NotifyDeleteRelease(doer, rel)
+       }
+}
+
+// NotifyIssueChangeMilestone notifies change milestone to notifiers
+func NotifyIssueChangeMilestone(doer *models.User, issue *models.Issue) {
+       for _, notifier := range notifiers {
+               notifier.NotifyIssueChangeMilestone(doer, issue)
+       }
+}
+
+// NotifyIssueChangeContent notifies change content to notifiers
+func NotifyIssueChangeContent(doer *models.User, issue *models.Issue, oldContent string) {
+       for _, notifier := range notifiers {
+               notifier.NotifyIssueChangeContent(doer, issue, oldContent)
+       }
+}
+
+// NotifyIssueChangeAssignee notifies change content to notifiers
+func NotifyIssueChangeAssignee(doer *models.User, issue *models.Issue, removed bool) {
+       for _, notifier := range notifiers {
+               notifier.NotifyIssueChangeAssignee(doer, issue, removed)
+       }
+}
+
+// NotifyIssueClearLabels notifies clear labels to notifiers
+func NotifyIssueClearLabels(doer *models.User, issue *models.Issue) {
+       for _, notifier := range notifiers {
+               notifier.NotifyIssueClearLabels(doer, issue)
+       }
+}
+
+// NotifyIssueChangeTitle notifies change title to notifiers
+func NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string) {
+       for _, notifier := range notifiers {
+               notifier.NotifyIssueChangeTitle(doer, issue, oldTitle)
+       }
+}
+
+// NotifyIssueChangeLabels notifies change labels to notifiers
+func NotifyIssueChangeLabels(doer *models.User, issue *models.Issue,
+       addedLabels []*models.Label, removedLabels []*models.Label) {
+       for _, notifier := range notifiers {
+               notifier.NotifyIssueChangeLabels(doer, issue, addedLabels, removedLabels)
+       }
 }
 
-func (ns *notificationService) Run() {
-       for {
-               select {
-               case opts := <-ns.issueQueue:
-                       if err := models.CreateOrUpdateIssueNotifications(opts.issue, opts.notificationAuthorID); err != nil {
-                               log.Error(4, "Was unable to create issue notification: %v", err)
-                       }
-               }
+// NotifyCreateRepository notifies create repository to notifiers
+func NotifyCreateRepository(doer *models.User, u *models.User, repo *models.Repository) {
+       for _, notifier := range notifiers {
+               notifier.NotifyCreateRepository(doer, u, repo)
        }
 }
 
-func (ns *notificationService) NotifyIssue(issue *models.Issue, notificationAuthorID int64) {
-       ns.issueQueue <- issueNotificationOpts{
-               issue,
-               notificationAuthorID,
+// NotifyMigrateRepository notifies create repository to notifiers
+func NotifyMigrateRepository(doer *models.User, u *models.User, repo *models.Repository) {
+       for _, notifier := range notifiers {
+               notifier.NotifyMigrateRepository(doer, u, repo)
        }
 }
diff --git a/modules/notification/ui/ui.go b/modules/notification/ui/ui.go
new file mode 100644 (file)
index 0000000..f0d708e
--- /dev/null
@@ -0,0 +1,134 @@
+// Copyright 2018 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 ui
+
+import (
+       "code.gitea.io/git"
+       "code.gitea.io/gitea/models"
+       "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/notification/base"
+)
+
+type (
+       notificationService struct {
+               issueQueue chan issueNotificationOpts
+       }
+
+       issueNotificationOpts struct {
+               issue                *models.Issue
+               notificationAuthorID int64
+       }
+)
+
+var (
+       _ base.Notifier = &notificationService{}
+)
+
+// NewNotifier create a new notificationService notifier
+func NewNotifier() base.Notifier {
+       return &notificationService{
+               issueQueue: make(chan issueNotificationOpts, 100),
+       }
+}
+
+func (ns *notificationService) Run() {
+       for {
+               select {
+               case opts := <-ns.issueQueue:
+                       if err := models.CreateOrUpdateIssueNotifications(opts.issue, opts.notificationAuthorID); err != nil {
+                               log.Error(4, "Was unable to create issue notification: %v", err)
+                       }
+               }
+       }
+}
+
+func (ns *notificationService) NotifyCreateIssueComment(doer *models.User, repo *models.Repository,
+       issue *models.Issue, comment *models.Comment) {
+       ns.issueQueue <- issueNotificationOpts{
+               issue,
+               doer.ID,
+       }
+}
+
+func (ns *notificationService) NotifyNewIssue(issue *models.Issue) {
+       ns.issueQueue <- issueNotificationOpts{
+               issue,
+               issue.Poster.ID,
+       }
+}
+
+func (ns *notificationService) NotifyIssueChangeStatus(doer *models.User, issue *models.Issue, isClosed bool) {
+       ns.issueQueue <- issueNotificationOpts{
+               issue,
+               doer.ID,
+       }
+}
+
+func (ns *notificationService) NotifyMergePullRequest(pr *models.PullRequest, doer *models.User, gitRepo *git.Repository) {
+       ns.issueQueue <- issueNotificationOpts{
+               pr.Issue,
+               doer.ID,
+       }
+}
+
+func (ns *notificationService) NotifyNewPullRequest(pr *models.PullRequest) {
+       ns.issueQueue <- issueNotificationOpts{
+               pr.Issue,
+               pr.Issue.PosterID,
+       }
+}
+
+func (ns *notificationService) NotifyPullRequestReview(pr *models.PullRequest, r *models.Review, c *models.Comment) {
+       ns.issueQueue <- issueNotificationOpts{
+               pr.Issue,
+               r.Reviewer.ID,
+       }
+}
+
+func (ns *notificationService) NotifyUpdateComment(doer *models.User, c *models.Comment, oldContent string) {
+}
+
+func (ns *notificationService) NotifyDeleteComment(doer *models.User, c *models.Comment) {
+}
+
+func (ns *notificationService) NotifyDeleteRepository(doer *models.User, repo *models.Repository) {
+}
+
+func (ns *notificationService) NotifyForkRepository(doer *models.User, oldRepo, repo *models.Repository) {
+}
+
+func (ns *notificationService) NotifyNewRelease(rel *models.Release) {
+}
+
+func (ns *notificationService) NotifyUpdateRelease(doer *models.User, rel *models.Release) {
+}
+
+func (ns *notificationService) NotifyDeleteRelease(doer *models.User, rel *models.Release) {
+}
+
+func (ns *notificationService) NotifyIssueChangeMilestone(doer *models.User, issue *models.Issue) {
+}
+
+func (ns *notificationService) NotifyIssueChangeContent(doer *models.User, issue *models.Issue, oldContent string) {
+}
+
+func (ns *notificationService) NotifyIssueChangeAssignee(doer *models.User, issue *models.Issue, removed bool) {
+}
+
+func (ns *notificationService) NotifyIssueClearLabels(doer *models.User, issue *models.Issue) {
+}
+
+func (ns *notificationService) NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string) {
+}
+
+func (ns *notificationService) NotifyIssueChangeLabels(doer *models.User, issue *models.Issue,
+       addedLabels []*models.Label, removedLabels []*models.Label) {
+}
+
+func (ns *notificationService) NotifyCreateRepository(doer *models.User, u *models.User, repo *models.Repository) {
+}
+
+func (ns *notificationService) NotifyMigrateRepository(doer *models.User, u *models.User, repo *models.Repository) {
+}
index f8ef0fe3d9c05abe9611936e8cf6c51ef285097d..4b634c9ca602c6e839981a915b9fe75f036cc9fb 100644 (file)
@@ -13,6 +13,7 @@ import (
        "code.gitea.io/gitea/models"
        "code.gitea.io/gitea/modules/context"
        "code.gitea.io/gitea/modules/indexer"
+       "code.gitea.io/gitea/modules/notification"
        "code.gitea.io/gitea/modules/setting"
        "code.gitea.io/gitea/modules/util"
 
@@ -207,6 +208,8 @@ func CreateIssue(ctx *context.APIContext, form api.CreateIssueOption) {
                return
        }
 
+       notification.NotifyNewIssue(issue)
+
        if form.Closed {
                if err := issue.ChangeStatus(ctx.User, ctx.Repo.Repository, true); err != nil {
                        if models.IsErrDependenciesLeft(err) {
@@ -337,6 +340,8 @@ func EditIssue(ctx *context.APIContext, form api.EditIssueOption) {
                        ctx.Error(500, "ChangeStatus", err)
                        return
                }
+
+               notification.NotifyIssueChangeStatus(ctx.User, issue, api.StateClosed == api.StateType(*form.State))
        }
 
        // Refetch from database to assign some automatic values
index ba627bb8a2677883e39fa055e96b565c55a36785..f958922914f7a15bbeea5fd37a1be479a371ec34 100644 (file)
@@ -9,6 +9,7 @@ import (
 
        "code.gitea.io/gitea/models"
        "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/notification"
 
        api "code.gitea.io/sdk/gitea"
 )
@@ -163,6 +164,8 @@ func CreateIssueComment(ctx *context.APIContext, form api.CreateIssueCommentOpti
                return
        }
 
+       notification.NotifyCreateIssueComment(ctx.User, ctx.Repo.Repository, issue, comment)
+
        ctx.JSON(201, comment.APIFormat())
 }
 
index 1527b8e8c988f7de4add8d7baa0cc8aaf37efef3..0ec2d368711f2914cbeac1d447a76fc300bfe28e 100644 (file)
@@ -14,6 +14,7 @@ import (
        "code.gitea.io/gitea/modules/auth"
        "code.gitea.io/gitea/modules/context"
        "code.gitea.io/gitea/modules/log"
+       "code.gitea.io/gitea/modules/notification"
        "code.gitea.io/gitea/modules/util"
 
        api "code.gitea.io/sdk/gitea"
@@ -270,6 +271,8 @@ func CreatePullRequest(ctx *context.APIContext, form api.CreatePullRequestOption
                return
        }
 
+       notification.NotifyNewPullRequest(pr)
+
        log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
        ctx.JSON(201, pr.APIFormat())
 }
@@ -386,6 +389,8 @@ func EditPullRequest(ctx *context.APIContext, form api.EditPullRequestOption) {
                        ctx.Error(500, "ChangeStatus", err)
                        return
                }
+
+               notification.NotifyIssueChangeStatus(ctx.User, issue, api.StateClosed == api.StateType(*form.State))
        }
 
        // Refetch from database
index 3cce483062fd2a25b9576e8d69ab76b35f1e1bd9..3bcfdf1b04be287fd9fac6bb8184f49c56ab4d5d 100644 (file)
@@ -490,7 +490,7 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) {
                return
        }
 
-       notification.Service.NotifyIssue(issue, ctx.User.ID)
+       notification.NotifyNewIssue(issue)
 
        log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
        ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
@@ -1004,15 +1004,19 @@ func UpdateIssueStatus(ctx *context.Context) {
                return
        }
        for _, issue := range issues {
-               if err := issue.ChangeStatus(ctx.User, issue.Repo, isClosed); err != nil {
-                       if models.IsErrDependenciesLeft(err) {
-                               ctx.JSON(http.StatusPreconditionFailed, map[string]interface{}{
-                                       "error": "cannot close this issue because it still has open dependencies",
-                               })
+               if issue.IsClosed != isClosed {
+                       if err := issue.ChangeStatus(ctx.User, issue.Repo, isClosed); err != nil {
+                               if models.IsErrDependenciesLeft(err) {
+                                       ctx.JSON(http.StatusPreconditionFailed, map[string]interface{}{
+                                               "error": "cannot close this issue because it still has open dependencies",
+                                       })
+                                       return
+                               }
+                               ctx.ServerError("ChangeStatus", err)
                                return
                        }
-                       ctx.ServerError("ChangeStatus", err)
-                       return
+
+                       notification.NotifyIssueChangeStatus(ctx.User, issue, isClosed)
                }
        }
        ctx.JSON(200, map[string]interface{}{
@@ -1072,7 +1076,8 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) {
                        if pr != nil {
                                ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
                        } else {
-                               if err := issue.ChangeStatus(ctx.User, ctx.Repo.Repository, form.Status == "close"); err != nil {
+                               isClosed := form.Status == "close"
+                               if err := issue.ChangeStatus(ctx.User, ctx.Repo.Repository, isClosed); err != nil {
                                        log.Error(4, "ChangeStatus: %v", err)
 
                                        if models.IsErrDependenciesLeft(err) {
@@ -1088,7 +1093,7 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) {
                                } else {
                                        log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
 
-                                       notification.Service.NotifyIssue(issue, ctx.User.ID)
+                                       notification.NotifyIssueChangeStatus(ctx.User, issue, isClosed)
                                }
                        }
                }
@@ -1116,7 +1121,7 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) {
                return
        }
 
-       notification.Service.NotifyIssue(issue, ctx.User.ID)
+       notification.NotifyCreateIssueComment(ctx.User, ctx.Repo.Repository, issue, comment)
 
        log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
 }
index 57fe33f85547b7ace1a2b48f6139c00d328c3dd8..4ec1c27cea0550f61a8d71a92841e1100d27fb29 100644 (file)
@@ -580,7 +580,7 @@ func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
                return
        }
 
-       notification.Service.NotifyIssue(pr.Issue, ctx.User.ID)
+       notification.NotifyMergePullRequest(pr, ctx.User, ctx.Repo.GitRepo)
 
        log.Trace("Pull request merged: %d", pr.ID)
        ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
@@ -888,7 +888,7 @@ func CompareAndPullRequestPost(ctx *context.Context, form auth.CreateIssueForm)
                return
        }
 
-       notification.Service.NotifyIssue(pullIssue, ctx.User.ID)
+       notification.NotifyNewPullRequest(pullRequest)
 
        log.Trace("Pull request created: %d/%d", repo.ID, pullIssue.ID)
        ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pullIssue.Index))
index 9d1db3ff4e415ce924ac9a40f95a6953711ea0cc..91257fea33958859c76646b1777d076e2a54d8b8 100644 (file)
@@ -79,7 +79,7 @@ func CreateCodeComment(ctx *context.Context, form auth.CodeCommentForm) {
        }
        // Send no notification if comment is pending
        if !form.IsReview {
-               notification.Service.NotifyIssue(issue, ctx.User.ID)
+               notification.NotifyCreateIssueComment(ctx.User, issue.Repo, issue, comment)
        }
 
        log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
@@ -184,5 +184,13 @@ func SubmitReview(ctx *context.Context, form auth.SubmitReviewForm) {
                ctx.ServerError("Publish", err)
                return
        }
+
+       pr, err := issue.GetPullRequest()
+       if err != nil {
+               ctx.ServerError("GetPullRequest", err)
+               return
+       }
+       notification.NotifyPullRequestReview(pr, review, comm)
+
        ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, issue.Index, comm.HashTag()))
 }