Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 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
2 changes: 2 additions & 0 deletions cmd/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,8 @@ func runWeb(ctx *cli.Context) error {
})
// ***** END: Repository *****

m.Get("/notifications", reqSignIn, user.Notifications)

m.Group("/api", func() {
apiv1.RegisterRoutes(m)
}, ignSignIn)
Expand Down
8 changes: 8 additions & 0 deletions conf/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ version = Version
page = Page
template = Template
language = Language
notifications = Notifications
create_new = Create...
user_profile_and_more = User profile and more
signed_in_as = Signed in as
Expand Down Expand Up @@ -1207,3 +1208,10 @@ default_message = Drop files here or click to upload.
invalid_input_type = You can't upload files of this type.
file_too_big = File size ({{filesize}} MB) exceeds maximum size ({{maxFilesize}} MB).
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.
7 changes: 7 additions & 0 deletions conf/locale/locale_pt-BR.ini
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ version=Versão
page=Página
template=Template
language=Idioma
notifications = Notificações
create_new=Criar...
user_profile_and_more=Perfil do usuário e configurações
signed_in_as=Logado como
Expand Down Expand Up @@ -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).
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.
43 changes: 34 additions & 9 deletions models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,40 @@ var (

func init() {
tables = append(tables,
new(User), new(PublicKey), new(AccessToken),
new(Repository), new(DeployKey), new(Collaboration), new(Access), new(Upload),
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(User),
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks! This has been messing with my OCD for a while 😆

Copy link
Member

Choose a reason for hiding this comment

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

How about reorder them?

Copy link
Contributor

Choose a reason for hiding this comment

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

@lunny in alphabetical order?

Copy link
Member

Choose a reason for hiding this comment

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

If order doesn't need to match the order in which fields are defined in the structure, then it makes sense to me to sort them alphabetically.

new(PublicKey),
new(AccessToken),
new(Repository),
new(DeployKey),
new(Collaboration),
new(Access),
new(Upload),
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"}
for _, name := range gonicNames {
Expand Down
184 changes: 184 additions & 0 deletions models/notification.go
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

gofmt

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above

}
}

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
}
36 changes: 36 additions & 0 deletions modules/notification/notification.go
Original file line number Diff line number Diff line change
@@ -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
}
18 changes: 18 additions & 0 deletions public/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -2691,6 +2691,24 @@ footer .ui.language .menu {
.user.followers .follow .ui.button {
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 {
padding-top: 15px;
padding-bottom: 80px;
Expand Down
21 changes: 21 additions & 0 deletions public/less/_user.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
5 changes: 5 additions & 0 deletions routers/repo/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markdown"
"code.gitea.io/gitea/modules/notification"
"code.gitea.io/gitea/modules/setting"
)

Expand Down Expand Up @@ -453,6 +454,8 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) {
return
}

notification.Service.NotifyIssue(issue)
Copy link
Member

Choose a reason for hiding this comment

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

Should this be put in background too ?
is: go notification.Service.NotifyIssue(issues)

If I read the code correctly the NotifyIssue writes into a channel, is that operation immediate or blocks until someone reads from the channel ? (lame gopher here)

Copy link
Contributor Author

@andreynering andreynering Dec 11, 2016

Choose a reason for hiding this comment

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

You have a good point. Channels act like a queue, so I think it will block until it's consumed by the service.

We can use buffered channels [1], but I think we will have a panic if we exceed the buffer size. I will make some tests.

[1] https://gobyexample.com/channel-buffering

Edit: buffered channels will do the trick: https://tour.golang.org/concurrency/3


log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
}
Expand Down Expand Up @@ -898,6 +901,8 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) {
return
}

notification.Service.NotifyIssue(issue)

log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
}

Expand Down
Loading