Compare commits

...

10 Commits

Author SHA1 Message Date
Andrey Nering 7da3f899a6 First version of page to see notifications 2016-12-10 17:29:20 -02:00
Andrey Nering a9844aeb8d Register notification router 2016-12-10 14:08:59 -02:00
Andrey Nering fedf445f6a Notification item on menu 2016-12-10 14:08:59 -02:00
Andrey Nering f692adea69 First version of notification service 2016-12-10 14:08:59 -02:00
Andrey Nering 679d91afdf Fix SQLs 2016-12-10 14:08:59 -02:00
Andrey Nering 8a3c856b1c Also create notification while commenting on issue 2016-12-10 14:08:59 -02:00
Andrey Nering c3810c6d43 Lint 2016-12-10 14:08:59 -02:00
Andrey Nering 75c2752ee6 Use own type (smallint on DB) and iota instead of VARCHAR(1) 2016-12-10 14:08:59 -02:00
Andrey Nering b19ec55338 Creating notifications on new issue 2016-12-10 14:08:59 -02:00
Andrey Nering 0f1b484e9a Create notification model/table 2016-12-10 14:08:59 -02:00
13 changed files with 428 additions and 10 deletions

View File

@ -562,6 +562,8 @@ func runWeb(ctx *cli.Context) error {
}) })
// ***** END: Repository ***** // ***** END: Repository *****
m.Get("/notifications", reqSignIn, user.Notifications)
m.Group("/api", func() { m.Group("/api", func() {
apiv1.RegisterRoutes(m) apiv1.RegisterRoutes(m)
}, ignSignIn) }, ignSignIn)

View File

@ -13,6 +13,7 @@ version = Version
page = Page page = Page
template = Template template = Template
language = Language language = Language
notifications = Notifications
create_new = Create... create_new = Create...
user_profile_and_more = User profile and more user_profile_and_more = User profile and more
signed_in_as = Signed in as signed_in_as = Signed in as
@ -1207,3 +1208,10 @@ default_message = Drop files here or click to upload.
invalid_input_type = You can't upload files of this type. invalid_input_type = You can't upload files of this type.
file_too_big = File size ({{filesize}} MB) exceeds maximum size ({{maxFilesize}} MB). file_too_big = File size ({{filesize}} MB) exceeds maximum size ({{maxFilesize}} MB).
remove_file = Remove file remove_file = Remove file
[notification]
notifications = Notifications
unread = Unread
read = Read
no_unread = You have no unread notifications.
no_read = You have no read notifications.

View File

@ -13,6 +13,7 @@ version=Versão
page=Página page=Página
template=Template template=Template
language=Idioma language=Idioma
notifications = Notificações
create_new=Criar... create_new=Criar...
user_profile_and_more=Perfil do usuário e configurações user_profile_and_more=Perfil do usuário e configurações
signed_in_as=Logado como signed_in_as=Logado como
@ -1198,3 +1199,9 @@ invalid_input_type=Você não pode enviar arquivos deste tipo.
file_too_big=O tamanho do arquivo ({{filesize}} MB) excede o limite máximo ({{maxFilesize}} MB). file_too_big=O tamanho do arquivo ({{filesize}} MB) excede o limite máximo ({{maxFilesize}} MB).
remove_file=Remover remove_file=Remover
[notification]
notifications = Notificações
unread = Não lidas
read = Lidas
no_unread = Você não possui notificações não lidas.
no_read = Você não possui notificações lidas.

View File

@ -68,15 +68,40 @@ var (
func init() { func init() {
tables = append(tables, tables = append(tables,
new(User), new(PublicKey), new(AccessToken), new(User),
new(Repository), new(DeployKey), new(Collaboration), new(Access), new(Upload), new(PublicKey),
new(Watch), new(Star), new(Follow), new(Action), new(AccessToken),
new(Issue), new(PullRequest), new(Comment), new(Attachment), new(IssueUser), new(Repository),
new(Label), new(IssueLabel), new(Milestone), new(DeployKey),
new(Mirror), new(Release), new(LoginSource), new(Webhook), new(Collaboration),
new(UpdateTask), new(HookTask), new(Access),
new(Team), new(OrgUser), new(TeamUser), new(TeamRepo), new(Upload),
new(Notice), new(EmailAddress)) new(Watch),
new(Star),
new(Follow),
new(Action),
new(Issue),
new(PullRequest),
new(Comment),
new(Attachment),
new(IssueUser),
new(Label),
new(IssueLabel),
new(Milestone),
new(Mirror),
new(Release),
new(LoginSource),
new(Webhook),
new(UpdateTask),
new(HookTask),
new(Team),
new(OrgUser),
new(TeamUser),
new(TeamRepo),
new(Notice),
new(EmailAddress),
new(Notification),
)
gonicNames := []string{"SSL", "UID"} gonicNames := []string{"SSL", "UID"}
for _, name := range gonicNames { for _, name := range gonicNames {

184
models/notification.go Normal file
View File

@ -0,0 +1,184 @@
package models
import (
"time"
)
type (
// NotificationStatus is the status of the notification (read or unread)
NotificationStatus uint8
// NotificationSource is the source of the notification (issue, PR, commit, etc)
NotificationSource uint8
)
const (
// NotificationStatusUnread represents an unread notification
NotificationStatusUnread NotificationStatus = iota + 1
// NotificationStatusRead represents a read notification
NotificationStatusRead
)
const (
// NotificationSourceIssue is a notification of an issue
NotificationSourceIssue NotificationSource = iota + 1
// NotificationSourcePullRequest is a notification of a pull request
NotificationSourcePullRequest
// NotificationSourceCommit is a notification of a commit
NotificationSourceCommit
)
// Notification represents a notification
type Notification struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"INDEX NOT NULL"`
RepoID int64 `xorm:"INDEX NOT NULL"`
Status NotificationStatus `xorm:"SMALLINT INDEX NOT NULL"`
Source NotificationSource `xorm:"SMALLINT INDEX NOT NULL"`
IssueID int64 `xorm:"INDEX NOT NULL"`
PullID int64 `xorm:"INDEX"`
CommitID string `xorm:"INDEX"`
Issue *Issue `xorm:"-"`
PullRequest *PullRequest `xorm:"-"`
Created time.Time `xorm:"-"`
CreatedUnix int64 `xorm:"INDEX NOT NULL"`
Updated time.Time `xorm:"-"`
UpdatedUnix int64 `xorm:"INDEX NOT NULL"`
}
// BeforeInsert runs while inserting a record
func (n *Notification) BeforeInsert() {
var (
now = time.Now()
nowUnix = now.Unix()
)
n.Created = now
n.CreatedUnix = nowUnix
n.Updated = now
n.UpdatedUnix = nowUnix
}
// BeforeUpdate runs while updateing a record
func (n *Notification) BeforeUpdate() {
var (
now = time.Now()
nowUnix = now.Unix()
)
n.Updated = now
n.UpdatedUnix = nowUnix
}
// CreateOrUpdateIssueNotifications creates an issue notification
// for each watcher, or updates it if already exists
func CreateOrUpdateIssueNotifications(issue *Issue) error {
watches, err := getWatchers(x, issue.RepoID)
if err != nil {
return err
}
sess := x.NewSession()
if err := sess.Begin(); err != nil {
return err
}
defer sess.Close()
for _, watch := range watches {
exists, err := issueNotificationExists(sess, watch.UserID, issue.ID)
if err != nil {
return err
}
if exists {
err = updateIssueNotification(sess, watch.UserID, issue.ID)
} else {
err = createIssueNotification(sess, watch.UserID, issue)
}
if err != nil {
return err
}
}
return sess.Commit()
}
func issueNotificationExists(e Engine, userID, issueID int64) (bool, error) {
count, err := e.
Where("user_id = ?", userID).
And("issue_id = ?", issueID).
Count(Notification{})
return count > 0, err
}
func createIssueNotification(e Engine, userID int64, issue *Issue) error {
notification := &Notification{
UserID: userID,
RepoID: issue.RepoID,
Status: NotificationStatusUnread,
IssueID: issue.ID,
}
if issue.IsPull {
notification.Source = NotificationSourcePullRequest
} else {
notification.Source = NotificationSourceIssue
}
_, err := e.Insert(notification)
return err
}
func updateIssueNotification(e Engine, userID, issueID int64) error {
notification, err := getIssueNotification(e, userID, issueID)
if err != nil {
return err
}
notification.Status = NotificationStatusUnread
_, err = e.Id(notification.ID).Update(notification)
return err
}
func getIssueNotification(e Engine, userID, issueID int64) (*Notification, error) {
notification := new(Notification)
_, err := e.
Where("user_id = ?", userID).
And("issue_id = ?", issueID).
Get(notification)
return notification, err
}
// NotificationsForUser returns notifications for a given user and status
func NotificationsForUser(user *User, status NotificationStatus) ([]*Notification, error) {
return notificationsForUser(x, user, status)
}
func notificationsForUser(e Engine, user *User, status NotificationStatus) (notifications []*Notification, err error) {
err = e.
Where("user_id = ?", user.ID).
And("status = ?", status).
OrderBy("updated_unix DESC").
Find(&notifications)
return
}
// GetRepo returns the repo of the notification
func (n *Notification) GetRepo() (repo *Repository, err error) {
repo = new(Repository)
_, err = x.
Where("id = ?", n.RepoID).
Get(repo)
return
}
// GetIssue returns the issue of the notification
func (n *Notification) GetIssue() (issue *Issue, err error) {
issue = new(Issue)
_, err = x.
Where("id = ?", n.IssueID).
Get(issue)
return
}

View File

@ -0,0 +1,36 @@
package notification
import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
)
type notificationService struct {
issueQueue chan *models.Issue
}
var (
// Service is the notification service
Service = &notificationService{
issueQueue: make(chan *models.Issue),
}
)
func init() {
go Service.Run()
}
func (ns *notificationService) Run() {
for {
select {
case issue := <-ns.issueQueue:
if err := models.CreateOrUpdateIssueNotifications(issue); err != nil {
log.Error(4, "Was unable to create issue notification: %v", err)
}
}
}
}
func (ns *notificationService) NotifyIssue(issue *models.Issue) {
ns.issueQueue <- issue
}

View File

@ -2691,6 +2691,24 @@ footer .ui.language .menu {
.user.followers .follow .ui.button { .user.followers .follow .ui.button {
padding: 8px 15px; padding: 8px 15px;
} }
.user.notification .octicon {
float: left;
font-size: 2em;
}
.user.notification .content {
float: left;
margin-left: 7px;
}
.user.notification .octicon-issue-opened,
.user.notification .octicon-git-pull-request {
color: green;
}
.user.notification .octicon-issue-closed {
color: red;
}
.user.notification .octicon-git-merge {
color: purple;
}
.dashboard { .dashboard {
padding-top: 15px; padding-top: 15px;
padding-bottom: 80px; padding-bottom: 80px;

View File

@ -74,4 +74,25 @@
} }
} }
} }
&.notification {
.octicon {
float: left;
font-size: 2em;
}
.content {
float: left;
margin-left: 7px;
}
.octicon-issue-opened, .octicon-git-pull-request {
color: green;
}
.octicon-issue-closed {
color: red;
}
.octicon-git-merge {
color: purple;
}
}
} }

View File

@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markdown" "code.gitea.io/gitea/modules/markdown"
"code.gitea.io/gitea/modules/notification"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
) )
@ -453,6 +454,8 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) {
return return
} }
notification.Service.NotifyIssue(issue)
log.Trace("Issue created: %d/%d", repo.ID, issue.ID) log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index)) ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
} }
@ -898,6 +901,8 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) {
return return
} }
notification.Service.NotifyIssue(issue)
log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID) log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
} }

View File

@ -0,0 +1,39 @@
package user
import (
"fmt"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
)
const (
tplNotification base.TplName = "user/notification/notification"
)
// Notifications is the notifications page
func Notifications(c *context.Context) {
var status models.NotificationStatus
switch c.Query("status") {
case "read":
status = models.NotificationStatusRead
default:
status = models.NotificationStatusUnread
}
notifications, err := models.NotificationsForUser(c.User, status)
if err != nil {
c.Handle(500, "ErrNotificationsForUser", err)
return
}
title := "Notifications"
if count := len(notifications); count > 0 {
title = fmt.Sprintf("(%d) %s", count, title)
}
c.Data["Title"] = title
c.Data["Status"] = status
c.Data["Notifications"] = notifications
c.HTML(200, tplNotification)
}

View File

@ -29,7 +29,6 @@ const (
tplSettingsSocial base.TplName = "user/settings/social" tplSettingsSocial base.TplName = "user/settings/social"
tplSettingsApplications base.TplName = "user/settings/applications" tplSettingsApplications base.TplName = "user/settings/applications"
tplSettingsDelete base.TplName = "user/settings/delete" tplSettingsDelete base.TplName = "user/settings/delete"
tplNotification base.TplName = "user/notification"
tplSecurity base.TplName = "user/security" tplSecurity base.TplName = "user/security"
) )

View File

@ -75,6 +75,12 @@
{{if .IsSigned}} {{if .IsSigned}}
<div class="right menu"> <div class="right menu">
<a href="/notifications" class="ui head link jump item poping up" data-content='{{.i18n.Tr "notifications"}}' data-variation="tiny inverted">
<span class="text">
<i class="octicon octicon-inbox"><span class="sr-only">{{.i18n.Tr "notifications"}}</span></i>
</span>
</a>
<div class="ui dropdown head link jump item poping up" data-content="{{.i18n.Tr "create_new"}}" data-variation="tiny inverted"> <div class="ui dropdown head link jump item poping up" data-content="{{.i18n.Tr "create_new"}}" data-variation="tiny inverted">
<span class="text"> <span class="text">
<i class="octicon octicon-plus"><span class="sr-only">{{.i18n.Tr "create_new"}}</span></i> <i class="octicon octicon-plus"><span class="sr-only">{{.i18n.Tr "create_new"}}</span></i>

View File

@ -0,0 +1,68 @@
{{template "base/head" .}}
<div class="user notification">
<div class="ui container">
<h1 class="ui header">{{.i18n.Tr "notification.notifications"}}</h1>
<div class="ui top attached tabular menu">
<a href="/notifications?status=unread">
<div class="{{if eq .Status 1}}active{{end}} item">
{{.i18n.Tr "notification.unread"}}
{{if eq .Status 1}}
<div class="ui label">{{len .Notifications}}</div>
{{end}}
</div>
</a>
<a href="/notifications?status=read">
<div class="{{if eq .Status 2}}active{{end}} item">
{{.i18n.Tr "notification.read"}}
{{if eq .Status 2}}
<div class="ui label">{{len .Notifications}}</div>
{{end}}
</div>
</a>
</div>
<div class="ui bottom attached active tab segment">
{{if eq (len .Notifications) 0}}
{{if eq .Status 1}}
{{.i18n.Tr "notification.no_unread"}}
{{else}}
{{.i18n.Tr "notification.no_read"}}
{{end}}
{{else}}
<div class="ui relaxed divided list">
{{range $notification := .Notifications}}
{{$issue := $notification.GetIssue}}
{{$repo := $notification.GetRepo}}
{{$repoOwner := $repo.MustOwner}}
<div class="item">
<a href="{{$.AppSubUrl}}/{{$repoOwner.Name}}/{{$repo.Name}}/issues/{{$issue.Index}}">
{{if and $issue.IsPull}}
{{if $issue.IsClosed}}
<i class="octicon octicon-git-merge"></i>
{{else}}
<i class="octicon octicon-git-pull-request"></i>
{{end}}
{{else}}
{{if $issue.IsClosed}}
<i class="octicon octicon-issue-closed"></i>
{{else}}
<i class="octicon octicon-issue-opened"></i>
{{end}}
{{end}}
<div class="content">
<div class="header">{{$repoOwner.Name}}/{{$repo.Name}}</div>
<div class="description">{{$issue.Title}}</div>
</div>
</a>
</div>
{{end}}
</div>
{{end}}
</div>
</div>
</div>
{{template "base/footer" .}}