Compare commits
10 Commits
main
...
notificati
Author | SHA1 | Date |
---|---|---|
Andrey Nering | 7da3f899a6 | |
Andrey Nering | a9844aeb8d | |
Andrey Nering | fedf445f6a | |
Andrey Nering | f692adea69 | |
Andrey Nering | 679d91afdf | |
Andrey Nering | 8a3c856b1c | |
Andrey Nering | c3810c6d43 | |
Andrey Nering | 75c2752ee6 | |
Andrey Nering | b19ec55338 | |
Andrey Nering | 0f1b484e9a |
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
@ -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.
|
||||
|
|
|
@ -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),
|
||||
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 {
|
||||
|
|
|
@ -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(¬ifications)
|
||||
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
|
||||
}
|
|
@ -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 = ¬ificationService{
|
||||
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
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
@ -453,6 +454,8 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) {
|
|||
return
|
||||
}
|
||||
|
||||
notification.Service.NotifyIssue(issue)
|
||||
|
||||
log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -29,7 +29,6 @@ const (
|
|||
tplSettingsSocial base.TplName = "user/settings/social"
|
||||
tplSettingsApplications base.TplName = "user/settings/applications"
|
||||
tplSettingsDelete base.TplName = "user/settings/delete"
|
||||
tplNotification base.TplName = "user/notification"
|
||||
tplSecurity base.TplName = "user/security"
|
||||
)
|
||||
|
||||
|
|
|
@ -75,6 +75,12 @@
|
|||
|
||||
{{if .IsSigned}}
|
||||
<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">
|
||||
<span class="text">
|
||||
<i class="octicon octicon-plus"><span class="sr-only">{{.i18n.Tr "create_new"}}</span></i>
|
||||
|
|
|
@ -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" .}}
|
Loading…
Reference in New Issue