* Use AJAX for notifications table Signed-off-by: Andrew Thornton <art27@cantab.net> * move to separate js Signed-off-by: Andrew Thornton <art27@cantab.net> * placate golangci-lint Signed-off-by: Andrew Thornton <art27@cantab.net> * Add autoupdating notification count Signed-off-by: Andrew Thornton <art27@cantab.net> * Fix wipeall Signed-off-by: Andrew Thornton <art27@cantab.net> * placate tests Signed-off-by: Andrew Thornton <art27@cantab.net> * Try hidden Signed-off-by: Andrew Thornton <art27@cantab.net> * Try hide and hidden Signed-off-by: Andrew Thornton <art27@cantab.net> * More auto-update improvements Only run checker on pages that have a count Change starting checker to 10s with a back-off to 60s if there is no change Signed-off-by: Andrew Thornton <art27@cantab.net> * string comparison! Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @silverwind Signed-off-by: Andrew Thornton <art27@cantab.net> * add configurability as per @6543 Signed-off-by: Andrew Thornton <art27@cantab.net> * Add documentation as per @6543 Signed-off-by: Andrew Thornton <art27@cantab.net> * Use CSRF header not query Signed-off-by: Andrew Thornton <art27@cantab.net> * Further JS improvements Fix @etzelia update notification table request Fix @silverwind comments Co-Authored-By: silverwind <me@silverwind.io> Signed-off-by: Andrew Thornton <art27@cantab.net> * Simplify the notification count fns Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: silverwind <me@silverwind.io>tags/v1.21.12.1
| @@ -55,6 +55,7 @@ rules: | |||||
| no-param-reassign: [0] | no-param-reassign: [0] | ||||
| no-plusplus: [0] | no-plusplus: [0] | ||||
| no-restricted-syntax: [0] | no-restricted-syntax: [0] | ||||
| no-return-await: [0] | |||||
| no-shadow: [0] | no-shadow: [0] | ||||
| no-unused-vars: [2, {args: all, argsIgnorePattern: ^_, varsIgnorePattern: ^_, ignoreRestSiblings: true}] | no-unused-vars: [2, {args: all, argsIgnorePattern: ^_, varsIgnorePattern: ^_, ignoreRestSiblings: true}] | ||||
| no-use-before-define: [0] | no-use-before-define: [0] | ||||
| @@ -200,6 +200,14 @@ AUTHOR = Gitea - Git with a cup of tea | |||||
| DESCRIPTION = Gitea (Git with a cup of tea) is a painless self-hosted Git service written in Go | DESCRIPTION = Gitea (Git with a cup of tea) is a painless self-hosted Git service written in Go | ||||
| KEYWORDS = go,git,self-hosted,gitea | KEYWORDS = go,git,self-hosted,gitea | ||||
| [ui.notification] | |||||
| ; Control how often notification is queried to update the notification | |||||
| ; The timeout will increase to MAX_TIMEOUT in TIMEOUT_STEPs if the notification count is unchanged | |||||
| ; Set MIN_TIMEOUT to 0 to turn off | |||||
| MIN_TIMEOUT = 10s | |||||
| MAX_TIMEOUT = 60s | |||||
| TIMEOUT_STEP = 10s | |||||
| [markdown] | [markdown] | ||||
| ; Render soft line breaks as hard line breaks, which means a single newline character between | ; Render soft line breaks as hard line breaks, which means a single newline character between | ||||
| ; paragraphs will cause a line break and adding trailing whitespace to paragraphs is not | ; paragraphs will cause a line break and adding trailing whitespace to paragraphs is not | ||||
| @@ -140,6 +140,13 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. | |||||
| - `NOTICE_PAGING_NUM`: **25**: Number of notices that are shown in one page. | - `NOTICE_PAGING_NUM`: **25**: Number of notices that are shown in one page. | ||||
| - `ORG_PAGING_NUM`: **50**: Number of organizations that are shown in one page. | - `ORG_PAGING_NUM`: **50**: Number of organizations that are shown in one page. | ||||
| ### UI - Notification (`ui.notification`) | |||||
| - `MIN_TIMEOUT`: **10s**: These options control how often notification is queried to update the notification count. On page load the notification count will be checked after `MIN_TIMEOUT`. The timeout will increase to `MAX_TIMEOUT` by `TIMEOUT_STEP` if the notification count is unchanged. Set MIN_TIMEOUT to 0 to turn off. | |||||
| - `MAX_TIMEOUT`: **60s**. | |||||
| - `TIMEOUT_STEP`: **10s**. | |||||
| ## Markdown (`markdown`) | ## Markdown (`markdown`) | ||||
| - `ENABLE_HARD_LINE_BREAK`: **true**: Render soft line breaks as hard line breaks, which | - `ENABLE_HARD_LINE_BREAK`: **true**: Render soft line breaks as hard line breaks, which | ||||
| @@ -181,6 +181,12 @@ var ( | |||||
| SearchRepoDescription bool | SearchRepoDescription bool | ||||
| UseServiceWorker bool | UseServiceWorker bool | ||||
| Notification struct { | |||||
| MinTimeout time.Duration | |||||
| TimeoutStep time.Duration | |||||
| MaxTimeout time.Duration | |||||
| } `ini:"ui.notification"` | |||||
| Admin struct { | Admin struct { | ||||
| UserPagingNum int | UserPagingNum int | ||||
| RepoPagingNum int | RepoPagingNum int | ||||
| @@ -209,6 +215,15 @@ var ( | |||||
| DefaultTheme: `gitea`, | DefaultTheme: `gitea`, | ||||
| Themes: []string{`gitea`, `arc-green`}, | Themes: []string{`gitea`, `arc-green`}, | ||||
| Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, | Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, | ||||
| Notification: struct { | |||||
| MinTimeout time.Duration | |||||
| TimeoutStep time.Duration | |||||
| MaxTimeout time.Duration | |||||
| }{ | |||||
| MinTimeout: 10 * time.Second, | |||||
| TimeoutStep: 10 * time.Second, | |||||
| MaxTimeout: 60 * time.Second, | |||||
| }, | |||||
| Admin: struct { | Admin: struct { | ||||
| UserPagingNum int | UserPagingNum int | ||||
| RepoPagingNum int | RepoPagingNum int | ||||
| @@ -278,6 +278,13 @@ func NewFuncMap() []template.FuncMap { | |||||
| return "" | return "" | ||||
| } | } | ||||
| }, | }, | ||||
| "NotificationSettings": func() map[string]int { | |||||
| return map[string]int{ | |||||
| "MinTimeout": int(setting.UI.Notification.MinTimeout / time.Millisecond), | |||||
| "TimeoutStep": int(setting.UI.Notification.TimeoutStep / time.Millisecond), | |||||
| "MaxTimeout": int(setting.UI.Notification.MaxTimeout / time.Millisecond), | |||||
| } | |||||
| }, | |||||
| "contain": func(s []int64, id int64) bool { | "contain": func(s []int64, id int64) bool { | ||||
| for i := 0; i < len(s); i++ { | for i := 0; i < len(s); i++ { | ||||
| if s[i] == id { | if s[i] == id { | ||||
| @@ -7,6 +7,7 @@ package user | |||||
| import ( | import ( | ||||
| "errors" | "errors" | ||||
| "fmt" | "fmt" | ||||
| "net/http" | |||||
| "strconv" | "strconv" | ||||
| "strings" | "strings" | ||||
| @@ -17,7 +18,8 @@ import ( | |||||
| ) | ) | ||||
| const ( | const ( | ||||
| tplNotification base.TplName = "user/notification/notification" | |||||
| tplNotification base.TplName = "user/notification/notification" | |||||
| tplNotificationDiv base.TplName = "user/notification/notification_div" | |||||
| ) | ) | ||||
| // GetNotificationCount is the middleware that sets the notification count in the context | // GetNotificationCount is the middleware that sets the notification count in the context | ||||
| @@ -30,17 +32,31 @@ func GetNotificationCount(c *context.Context) { | |||||
| return | return | ||||
| } | } | ||||
| count, err := models.GetNotificationCount(c.User, models.NotificationStatusUnread) | |||||
| if err != nil { | |||||
| c.ServerError("GetNotificationCount", err) | |||||
| return | |||||
| } | |||||
| c.Data["NotificationUnreadCount"] = func() int64 { | |||||
| count, err := models.GetNotificationCount(c.User, models.NotificationStatusUnread) | |||||
| if err != nil { | |||||
| c.ServerError("GetNotificationCount", err) | |||||
| return -1 | |||||
| } | |||||
| c.Data["NotificationUnreadCount"] = count | |||||
| return count | |||||
| } | |||||
| } | } | ||||
| // Notifications is the notifications page | // Notifications is the notifications page | ||||
| func Notifications(c *context.Context) { | func Notifications(c *context.Context) { | ||||
| getNotifications(c) | |||||
| if c.Written() { | |||||
| return | |||||
| } | |||||
| if c.QueryBool("div-only") { | |||||
| c.HTML(http.StatusOK, tplNotificationDiv) | |||||
| return | |||||
| } | |||||
| c.HTML(http.StatusOK, tplNotification) | |||||
| } | |||||
| func getNotifications(c *context.Context) { | |||||
| var ( | var ( | ||||
| keyword = strings.Trim(c.Query("q"), " ") | keyword = strings.Trim(c.Query("q"), " ") | ||||
| status models.NotificationStatus | status models.NotificationStatus | ||||
| @@ -115,19 +131,13 @@ func Notifications(c *context.Context) { | |||||
| c.Flash.Error(fmt.Sprintf("ERROR: %d notifications were removed due to missing parts - check the logs", failCount)) | c.Flash.Error(fmt.Sprintf("ERROR: %d notifications were removed due to missing parts - check the logs", failCount)) | ||||
| } | } | ||||
| title := c.Tr("notifications") | |||||
| if status == models.NotificationStatusUnread && total > 0 { | |||||
| title = fmt.Sprintf("(%d) %s", total, title) | |||||
| } | |||||
| c.Data["Title"] = title | |||||
| c.Data["Title"] = c.Tr("notifications") | |||||
| c.Data["Keyword"] = keyword | c.Data["Keyword"] = keyword | ||||
| c.Data["Status"] = status | c.Data["Status"] = status | ||||
| c.Data["Notifications"] = notifications | c.Data["Notifications"] = notifications | ||||
| pager.SetDefaultParams(c) | pager.SetDefaultParams(c) | ||||
| c.Data["Page"] = pager | c.Data["Page"] = pager | ||||
| c.HTML(200, tplNotification) | |||||
| } | } | ||||
| // NotificationStatusPost is a route for changing the status of a notification | // NotificationStatusPost is a route for changing the status of a notification | ||||
| @@ -155,8 +165,17 @@ func NotificationStatusPost(c *context.Context) { | |||||
| return | return | ||||
| } | } | ||||
| url := fmt.Sprintf("%s/notifications?page=%s", setting.AppSubURL, c.Query("page")) | |||||
| c.Redirect(url, 303) | |||||
| if !c.QueryBool("noredirect") { | |||||
| url := fmt.Sprintf("%s/notifications?page=%s", setting.AppSubURL, c.Query("page")) | |||||
| c.Redirect(url, http.StatusSeeOther) | |||||
| } | |||||
| getNotifications(c) | |||||
| if c.Written() { | |||||
| return | |||||
| } | |||||
| c.HTML(http.StatusOK, tplNotificationDiv) | |||||
| } | } | ||||
| // NotificationPurgePost is a route for 'purging' the list of notifications - marking all unread as read | // NotificationPurgePost is a route for 'purging' the list of notifications - marking all unread as read | ||||
| @@ -168,5 +187,5 @@ func NotificationPurgePost(c *context.Context) { | |||||
| } | } | ||||
| url := fmt.Sprintf("%s/notifications", setting.AppSubURL) | url := fmt.Sprintf("%s/notifications", setting.AppSubURL) | ||||
| c.Redirect(url, 303) | |||||
| c.Redirect(url, http.StatusSeeOther) | |||||
| } | } | ||||
| @@ -94,6 +94,11 @@ | |||||
| U2F: {{if .RequireU2F}}true{{else}}false{{end}}, | U2F: {{if .RequireU2F}}true{{else}}false{{end}}, | ||||
| Heatmap: {{if .EnableHeatmap}}true{{else}}false{{end}}, | Heatmap: {{if .EnableHeatmap}}true{{else}}false{{end}}, | ||||
| heatmapUser: {{if .HeatmapUser}}'{{.HeatmapUser}}'{{else}}null{{end}}, | heatmapUser: {{if .HeatmapUser}}'{{.HeatmapUser}}'{{else}}null{{end}}, | ||||
| NotificationSettings: { | |||||
| MinTimeout: {{NotificationSettings.MinTimeout}}, | |||||
| TimeoutStep: {{NotificationSettings.TimeoutStep}}, | |||||
| MaxTimeout: {{NotificationSettings.MaxTimeout}}, | |||||
| }, | |||||
| }; | }; | ||||
| </script> | </script> | ||||
| <link rel="shortcut icon" href="{{StaticUrlPrefix}}/img/favicon.png"> | <link rel="shortcut icon" href="{{StaticUrlPrefix}}/img/favicon.png"> | ||||
| @@ -46,12 +46,11 @@ | |||||
| <span class="text"> | <span class="text"> | ||||
| <span class="fitted">{{svg "octicon-bell" 16}}</span> | <span class="fitted">{{svg "octicon-bell" 16}}</span> | ||||
| <span class="sr-mobile-only">{{.i18n.Tr "notifications"}}</span> | <span class="sr-mobile-only">{{.i18n.Tr "notifications"}}</span> | ||||
| {{if .NotificationUnreadCount}} | |||||
| <span class="ui red label"> | |||||
| {{.NotificationUnreadCount}} | |||||
| </span> | |||||
| {{end}} | |||||
| {{$notificationUnreadCount := 0}} | |||||
| {{if .NotificationUnreadCount}}{{$notificationUnreadCount = call .NotificationUnreadCount}}{{end}} | |||||
| <span class="ui red label {{if not $notificationUnreadCount}}hidden{{end}} notification_count"> | |||||
| {{$notificationUnreadCount}} | |||||
| </span> | |||||
| </span> | </span> | ||||
| </a> | </a> | ||||
| @@ -1,119 +1,3 @@ | |||||
| {{template "base/head" .}} | {{template "base/head" .}} | ||||
| <div class="user notification"> | |||||
| <div class="ui container"> | |||||
| <h1 class="ui dividing header">{{.i18n.Tr "notification.notifications"}}</h1> | |||||
| <div class="ui top attached tabular menu"> | |||||
| <a href="{{AppSubUrl}}/notifications?q=unread" class="{{if eq .Status 1}}active{{end}} item"> | |||||
| {{.i18n.Tr "notification.unread"}} | |||||
| {{if .NotificationUnreadCount}} | |||||
| <div class="ui label">{{.NotificationUnreadCount}}</div> | |||||
| {{end}} | |||||
| </a> | |||||
| <a href="{{AppSubUrl}}/notifications?q=read" class="{{if eq .Status 2}}active{{end}} item"> | |||||
| {{.i18n.Tr "notification.read"}} | |||||
| </a> | |||||
| {{if and (eq .Status 1) (.NotificationUnreadCount)}} | |||||
| <form action="{{AppSubUrl}}/notifications/purge" method="POST" style="margin-left: auto;"> | |||||
| {{$.CsrfTokenHtml}} | |||||
| <button class="ui mini button primary" title='{{$.i18n.Tr "notification.mark_all_as_read"}}'> | |||||
| {{svg "octicon-checklist" 16}} | |||||
| </button> | |||||
| </form> | |||||
| {{end}} | |||||
| </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}} | |||||
| <table class="ui unstackable striped very compact small selectable table"> | |||||
| <tbody> | |||||
| {{range $notification := .Notifications}} | |||||
| {{$issue := $notification.Issue}} | |||||
| {{$repo := $notification.Repository}} | |||||
| {{$repoOwner := $repo.MustOwner}} | |||||
| <tr data-href="{{$notification.HTMLURL}}"> | |||||
| <td class="collapsing"> | |||||
| {{if eq $notification.Status 3}} | |||||
| <span class="blue">{{svg "octicon-pin" 16}}</span> | |||||
| {{else if $issue.IsPull}} | |||||
| {{if $issue.IsClosed}} | |||||
| {{if $issue.GetPullRequest.HasMerged}} | |||||
| <span class="purple">{{svg "octicon-git-merge" 16}}</span> | |||||
| {{else}} | |||||
| <span class="red">{{svg "octicon-git-pull-request" 16}}</span> | |||||
| {{end}} | |||||
| {{else}} | |||||
| <span class="green">{{svg "octicon-git-pull-request" 16}}</span> | |||||
| {{end}} | |||||
| {{else}} | |||||
| {{if $issue.IsClosed}} | |||||
| <span class="red">{{svg "octicon-issue-closed" 16}}</span> | |||||
| {{else}} | |||||
| <span class="green">{{svg "octicon-issue-opened" 16}}</span> | |||||
| {{end}} | |||||
| {{end}} | |||||
| </td> | |||||
| <td class="eleven wide"> | |||||
| <a class="item" href="{{$notification.HTMLURL}}"> | |||||
| #{{$issue.Index}} - {{$issue.Title}} | |||||
| </a> | |||||
| </td> | |||||
| <td> | |||||
| <a class="item" href="{{AppSubUrl}}/{{$repoOwner.Name}}/{{$repo.Name}}"> | |||||
| {{$repoOwner.Name}}/{{$repo.Name}} | |||||
| </a> | |||||
| </td> | |||||
| <td class="collapsing"> | |||||
| {{if ne $notification.Status 3}} | |||||
| <form action="{{AppSubUrl}}/notifications/status" method="POST"> | |||||
| {{$.CsrfTokenHtml}} | |||||
| <input type="hidden" name="notification_id" value="{{$notification.ID}}" /> | |||||
| <input type="hidden" name="status" value="pinned" /> | |||||
| <button class="ui mini button" title='{{$.i18n.Tr "notification.pin"}}'> | |||||
| {{svg "octicon-pin" 16}} | |||||
| </button> | |||||
| </form> | |||||
| {{end}} | |||||
| </td> | |||||
| <td class="collapsing"> | |||||
| {{if or (eq $notification.Status 1) (eq $notification.Status 3)}} | |||||
| <form action="{{AppSubUrl}}/notifications/status" method="POST"> | |||||
| {{$.CsrfTokenHtml}} | |||||
| <input type="hidden" name="notification_id" value="{{$notification.ID}}" /> | |||||
| <input type="hidden" name="status" value="read" /> | |||||
| <input type="hidden" name="page" value="{{$.Page.Paginater.Current}}" /> | |||||
| <button class="ui mini button" title='{{$.i18n.Tr "notification.mark_as_read"}}'> | |||||
| {{svg "octicon-check" 16}} | |||||
| </button> | |||||
| </form> | |||||
| {{else if eq $notification.Status 2}} | |||||
| <form action="{{AppSubUrl}}/notifications/status" method="POST"> | |||||
| {{$.CsrfTokenHtml}} | |||||
| <input type="hidden" name="notification_id" value="{{$notification.ID}}" /> | |||||
| <input type="hidden" name="status" value="unread" /> | |||||
| <input type="hidden" name="page" value="{{$.Page.Paginater.Current}}" /> | |||||
| <button class="ui mini button" title='{{$.i18n.Tr "notification.mark_as_unread"}}'> | |||||
| {{svg "octicon-bell" 16}} | |||||
| </button> | |||||
| </form> | |||||
| {{end}} | |||||
| </td> | |||||
| </tr> | |||||
| {{end}} | |||||
| </tbody> | |||||
| </table> | |||||
| {{end}} | |||||
| </div> | |||||
| {{template "base/paginate" .}} | |||||
| </div> | |||||
| </div> | |||||
| {{template "user/notification/notification_div" .}} | |||||
| {{template "base/footer" .}} | {{template "base/footer" .}} | ||||
| @@ -0,0 +1,128 @@ | |||||
| <div class="user notification" id="notification_div" data-params="{{.Page.GetParams}}"> | |||||
| <div class="ui container"> | |||||
| <h1 class="ui dividing header">{{.i18n.Tr "notification.notifications"}}</h1> | |||||
| <div class="ui top attached tabular menu"> | |||||
| {{ $notificationUnreadCount := call .NotificationUnreadCount}} | |||||
| <a href="{{AppSubUrl}}/notifications?q=unread" class="{{if eq .Status 1}}active{{end}} item"> | |||||
| {{.i18n.Tr "notification.unread"}} | |||||
| <div class="ui label {{if not $notificationUnreadCount}}hidden{{end}}">{{$notificationUnreadCount}}</div> | |||||
| </a> | |||||
| <a href="{{AppSubUrl}}/notifications?q=read" class="{{if eq .Status 2}}active{{end}} item"> | |||||
| {{.i18n.Tr "notification.read"}} | |||||
| </a> | |||||
| {{if and (eq .Status 1)}} | |||||
| <form action="{{AppSubUrl}}/notifications/purge" method="POST" style="margin-left: auto;"> | |||||
| {{$.CsrfTokenHtml}} | |||||
| <div class="{{if not $notificationUnreadCount}}hide{{end}}"> | |||||
| <button class="ui mini button primary" title='{{$.i18n.Tr "notification.mark_all_as_read"}}'> | |||||
| {{svg "octicon-checklist" 16}} | |||||
| </button> | |||||
| </div> | |||||
| </form> | |||||
| {{end}} | |||||
| </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}} | |||||
| <table class="ui unstackable striped very compact small selectable table" id="notification_table"> | |||||
| <tbody> | |||||
| {{range $notification := .Notifications}} | |||||
| {{$issue := .Issue}} | |||||
| {{$repo := .Repository}} | |||||
| {{$repoOwner := $repo.MustOwner}} | |||||
| <tr id="notification_{{.ID}}"> | |||||
| <td class="collapsing" data-href="{{.HTMLURL}}"> | |||||
| {{if eq .Status 3}} | |||||
| <span class="blue">{{svg "octicon-pin" 16}}</span> | |||||
| {{else if $issue.IsPull}} | |||||
| {{if $issue.IsClosed}} | |||||
| {{if $issue.GetPullRequest.HasMerged}} | |||||
| <span class="purple">{{svg "octicon-git-merge" 16}}</span> | |||||
| {{else}} | |||||
| <span class="red">{{svg "octicon-git-pull-request" 16}}</span> | |||||
| {{end}} | |||||
| {{else}} | |||||
| <span class="green">{{svg "octicon-git-pull-request" 16}}</span> | |||||
| {{end}} | |||||
| {{else}} | |||||
| {{if $issue.IsClosed}} | |||||
| <span class="red">{{svg "octicon-issue-closed" 16}}</span> | |||||
| {{else}} | |||||
| <span class="green">{{svg "octicon-issue-opened" 16}}</span> | |||||
| {{end}} | |||||
| {{end}} | |||||
| </td> | |||||
| <td class="eleven wide" data-href="{{.HTMLURL}}"> | |||||
| <a class="item" href="{{.HTMLURL}}"> | |||||
| #{{$issue.Index}} - {{$issue.Title}} | |||||
| </a> | |||||
| </td> | |||||
| <td data-href="{{AppSubUrl}}/{{$repoOwner.Name}}/{{$repo.Name}}"> | |||||
| <a class="item" href="{{AppSubUrl}}/{{$repoOwner.Name}}/{{$repo.Name}}"> | |||||
| {{$repoOwner.Name}}/{{$repo.Name}} | |||||
| </a> | |||||
| </td> | |||||
| <td class="collapsing"> | |||||
| {{if ne .Status 3}} | |||||
| <form action="{{AppSubUrl}}/notifications/status" method="POST"> | |||||
| {{$.CsrfTokenHtml}} | |||||
| <input type="hidden" name="notification_id" value="{{.ID}}" /> | |||||
| <input type="hidden" name="status" value="pinned" /> | |||||
| <button class="ui mini button" title='{{$.i18n.Tr "notification.pin"}}' | |||||
| data-url="{{AppSubUrl}}/notifications/status" | |||||
| data-status="pinned" | |||||
| data-page="{{$.Page.Paginater.Current}}" | |||||
| data-notification-id="{{.ID}}" | |||||
| data-q="{{$.Keyword}}"> | |||||
| {{svg "octicon-pin" 16}} | |||||
| </button> | |||||
| </form> | |||||
| {{end}} | |||||
| </td> | |||||
| <td class="collapsing"> | |||||
| {{if or (eq .Status 1) (eq .Status 3)}} | |||||
| <form action="{{AppSubUrl}}/notifications/status" method="POST"> | |||||
| {{$.CsrfTokenHtml}} | |||||
| <input type="hidden" name="notification_id" value="{{.ID}}" /> | |||||
| <input type="hidden" name="status" value="read" /> | |||||
| <input type="hidden" name="page" value="{{$.Page.Paginater.Current}}" /> | |||||
| <button class="ui mini button" title='{{$.i18n.Tr "notification.mark_as_read"}}' | |||||
| data-url="{{AppSubUrl}}/notifications/status" | |||||
| data-status="read" | |||||
| data-page="{{$.Page.Paginater.Current}}" | |||||
| data-notification-id="{{.ID}}" | |||||
| data-q="{{$.Keyword}}"> | |||||
| {{svg "octicon-check" 16}} | |||||
| </button> | |||||
| </form> | |||||
| {{else if eq .Status 2}} | |||||
| <form action="{{AppSubUrl}}/notifications/status" method="POST"> | |||||
| {{$.CsrfTokenHtml}} | |||||
| <input type="hidden" name="notification_id" value="{{.ID}}" /> | |||||
| <input type="hidden" name="status" value="unread" /> | |||||
| <input type="hidden" name="page" value="{{$.Page.Paginater.Current}}" /> | |||||
| <button class="ui mini button" title='{{$.i18n.Tr "notification.mark_as_unread"}}' | |||||
| data-url="{{AppSubUrl}}/notifications/status" | |||||
| data-status="unread" | |||||
| data-page="{{$.Page.Paginater.Current}}" | |||||
| data-notification-id="{{.ID}}" | |||||
| data-q="{{$.Keyword}}"> | |||||
| {{svg "octicon-bell" 16}} | |||||
| </button> | |||||
| </form> | |||||
| {{end}} | |||||
| </td> | |||||
| </tr> | |||||
| {{end}} | |||||
| </tbody> | |||||
| </table> | |||||
| {{end}} | |||||
| </div> | |||||
| {{template "base/paginate" .}} | |||||
| </div> | |||||
| </div> | |||||
| @@ -0,0 +1,110 @@ | |||||
| const {AppSubUrl, csrf, NotificationSettings} = window.config; | |||||
| export function initNotificationsTable() { | |||||
| $('#notification_table .button').on('click', async function () { | |||||
| const data = await updateNotification( | |||||
| $(this).data('url'), | |||||
| $(this).data('status'), | |||||
| $(this).data('page'), | |||||
| $(this).data('q'), | |||||
| $(this).data('notification-id'), | |||||
| ); | |||||
| $('#notification_div').replaceWith(data); | |||||
| initNotificationsTable(); | |||||
| await updateNotificationCount(); | |||||
| return false; | |||||
| }); | |||||
| } | |||||
| export function initNotificationCount() { | |||||
| if (NotificationSettings.MinTimeout <= 0) { | |||||
| return; | |||||
| } | |||||
| const notificationCount = $('.notification_count'); | |||||
| if (notificationCount.length > 0) { | |||||
| const fn = (timeout, lastCount) => { | |||||
| setTimeout(async () => { | |||||
| await updateNotificationCountWithCallback(fn, timeout, lastCount); | |||||
| }, timeout); | |||||
| }; | |||||
| fn(NotificationSettings.MinTimeout, notificationCount.text()); | |||||
| } | |||||
| } | |||||
| async function updateNotificationCountWithCallback(callback, timeout, lastCount) { | |||||
| const currentCount = $('.notification_count').text(); | |||||
| if (lastCount !== currentCount) { | |||||
| callback(NotificationSettings.MinTimeout, currentCount); | |||||
| return; | |||||
| } | |||||
| const newCount = await updateNotificationCount(); | |||||
| let needsUpdate = false; | |||||
| if (lastCount !== newCount) { | |||||
| needsUpdate = true; | |||||
| timeout = NotificationSettings.MinTimeout; | |||||
| } else if (timeout < NotificationSettings.MaxTimeout) { | |||||
| timeout += NotificationSettings.TimeoutStep; | |||||
| } | |||||
| callback(timeout, newCount); | |||||
| const notificationDiv = $('#notification_div'); | |||||
| if (notificationDiv.length > 0 && needsUpdate) { | |||||
| const data = await $.ajax({ | |||||
| type: 'GET', | |||||
| url: `${AppSubUrl}/notifications?${notificationDiv.data('params')}`, | |||||
| data: { | |||||
| 'div-only': true, | |||||
| } | |||||
| }); | |||||
| notificationDiv.replaceWith(data); | |||||
| initNotificationsTable(); | |||||
| } | |||||
| } | |||||
| async function updateNotificationCount() { | |||||
| const data = await $.ajax({ | |||||
| type: 'GET', | |||||
| url: `${AppSubUrl}/api/v1/notifications/new`, | |||||
| headers: { | |||||
| 'X-Csrf-Token': csrf, | |||||
| }, | |||||
| }); | |||||
| const notificationCount = $('.notification_count'); | |||||
| if (data.new === 0) { | |||||
| notificationCount.addClass('hidden'); | |||||
| } else { | |||||
| notificationCount.removeClass('hidden'); | |||||
| } | |||||
| notificationCount.text(`${data.new}`); | |||||
| return `${data.new}`; | |||||
| } | |||||
| async function updateNotification(url, status, page, q, notificationID) { | |||||
| if (status !== 'pinned') { | |||||
| $(`#notification_${notificationID}`).remove(); | |||||
| } | |||||
| return $.ajax({ | |||||
| type: 'POST', | |||||
| url, | |||||
| data: { | |||||
| _csrf: csrf, | |||||
| notification_id: notificationID, | |||||
| status, | |||||
| page, | |||||
| q, | |||||
| noredirect: true, | |||||
| }, | |||||
| }); | |||||
| } | |||||
| @@ -18,6 +18,7 @@ import initDateTimePicker from './features/datetimepicker.js'; | |||||
| import createDropzone from './features/dropzone.js'; | import createDropzone from './features/dropzone.js'; | ||||
| import highlight from './features/highlight.js'; | import highlight from './features/highlight.js'; | ||||
| import ActivityTopAuthors from './components/ActivityTopAuthors.vue'; | import ActivityTopAuthors from './components/ActivityTopAuthors.vue'; | ||||
| import {initNotificationsTable, initNotificationCount} from './features/notification.js'; | |||||
| const {AppSubUrl, StaticUrlPrefix, csrf} = window.config; | const {AppSubUrl, StaticUrlPrefix, csrf} = window.config; | ||||
| @@ -2431,6 +2432,11 @@ $(document).ready(async () => { | |||||
| window.location = $(this).data('href'); | window.location = $(this).data('href'); | ||||
| }); | }); | ||||
| // make table <td> element clickable like a link | |||||
| $('td[data-href]').click(function () { | |||||
| window.location = $(this).data('href'); | |||||
| }); | |||||
| // Dropzone | // Dropzone | ||||
| const $dropzone = $('#dropzone'); | const $dropzone = $('#dropzone'); | ||||
| if ($dropzone.length > 0) { | if ($dropzone.length > 0) { | ||||
| @@ -2606,6 +2612,8 @@ $(document).ready(async () => { | |||||
| initRepoStatusChecker(); | initRepoStatusChecker(); | ||||
| initTemplateSearch(); | initTemplateSearch(); | ||||
| initContextPopups(); | initContextPopups(); | ||||
| initNotificationsTable(); | |||||
| initNotificationCount(); | |||||
| // Repo clone url. | // Repo clone url. | ||||
| if ($('#repo-clone-url').length > 0) { | if ($('#repo-clone-url').length > 0) { | ||||