* 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-plusplus: [0] | |||
| no-restricted-syntax: [0] | |||
| no-return-await: [0] | |||
| no-shadow: [0] | |||
| no-unused-vars: [2, {args: all, argsIgnorePattern: ^_, varsIgnorePattern: ^_, ignoreRestSiblings: true}] | |||
| 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 | |||
| 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] | |||
| ; 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 | |||
| @@ -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. | |||
| - `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`) | |||
| - `ENABLE_HARD_LINE_BREAK`: **true**: Render soft line breaks as hard line breaks, which | |||
| @@ -181,6 +181,12 @@ var ( | |||
| SearchRepoDescription bool | |||
| UseServiceWorker bool | |||
| Notification struct { | |||
| MinTimeout time.Duration | |||
| TimeoutStep time.Duration | |||
| MaxTimeout time.Duration | |||
| } `ini:"ui.notification"` | |||
| Admin struct { | |||
| UserPagingNum int | |||
| RepoPagingNum int | |||
| @@ -209,6 +215,15 @@ var ( | |||
| DefaultTheme: `gitea`, | |||
| Themes: []string{`gitea`, `arc-green`}, | |||
| 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 { | |||
| UserPagingNum int | |||
| RepoPagingNum int | |||
| @@ -278,6 +278,13 @@ func NewFuncMap() []template.FuncMap { | |||
| 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 { | |||
| for i := 0; i < len(s); i++ { | |||
| if s[i] == id { | |||
| @@ -7,6 +7,7 @@ package user | |||
| import ( | |||
| "errors" | |||
| "fmt" | |||
| "net/http" | |||
| "strconv" | |||
| "strings" | |||
| @@ -17,7 +18,8 @@ import ( | |||
| ) | |||
| 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 | |||
| @@ -30,17 +32,31 @@ func GetNotificationCount(c *context.Context) { | |||
| 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 | |||
| 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 ( | |||
| keyword = strings.Trim(c.Query("q"), " ") | |||
| 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)) | |||
| } | |||
| 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["Status"] = status | |||
| c.Data["Notifications"] = notifications | |||
| pager.SetDefaultParams(c) | |||
| c.Data["Page"] = pager | |||
| c.HTML(200, tplNotification) | |||
| } | |||
| // NotificationStatusPost is a route for changing the status of a notification | |||
| @@ -155,8 +165,17 @@ func NotificationStatusPost(c *context.Context) { | |||
| 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 | |||
| @@ -168,5 +187,5 @@ func NotificationPurgePost(c *context.Context) { | |||
| } | |||
| 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}}, | |||
| Heatmap: {{if .EnableHeatmap}}true{{else}}false{{end}}, | |||
| heatmapUser: {{if .HeatmapUser}}'{{.HeatmapUser}}'{{else}}null{{end}}, | |||
| NotificationSettings: { | |||
| MinTimeout: {{NotificationSettings.MinTimeout}}, | |||
| TimeoutStep: {{NotificationSettings.TimeoutStep}}, | |||
| MaxTimeout: {{NotificationSettings.MaxTimeout}}, | |||
| }, | |||
| }; | |||
| </script> | |||
| <link rel="shortcut icon" href="{{StaticUrlPrefix}}/img/favicon.png"> | |||
| @@ -46,12 +46,11 @@ | |||
| <span class="text"> | |||
| <span class="fitted">{{svg "octicon-bell" 16}}</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> | |||
| </a> | |||
| @@ -1,119 +1,3 @@ | |||
| {{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" .}} | |||
| @@ -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 highlight from './features/highlight.js'; | |||
| import ActivityTopAuthors from './components/ActivityTopAuthors.vue'; | |||
| import {initNotificationsTable, initNotificationCount} from './features/notification.js'; | |||
| const {AppSubUrl, StaticUrlPrefix, csrf} = window.config; | |||
| @@ -2431,6 +2432,11 @@ $(document).ready(async () => { | |||
| window.location = $(this).data('href'); | |||
| }); | |||
| // make table <td> element clickable like a link | |||
| $('td[data-href]').click(function () { | |||
| window.location = $(this).data('href'); | |||
| }); | |||
| // Dropzone | |||
| const $dropzone = $('#dropzone'); | |||
| if ($dropzone.length > 0) { | |||
| @@ -2606,6 +2612,8 @@ $(document).ready(async () => { | |||
| initRepoStatusChecker(); | |||
| initTemplateSearch(); | |||
| initContextPopups(); | |||
| initNotificationsTable(); | |||
| initNotificationCount(); | |||
| // Repo clone url. | |||
| if ($('#repo-clone-url').length > 0) { | |||