mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	Use AJAX for notifications table (#10961)
* 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>
This commit is contained in:
		@@ -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)
 | 
						c.Data["NotificationUnreadCount"] = func() int64 {
 | 
				
			||||||
	if err != nil {
 | 
							count, err := models.GetNotificationCount(c.User, models.NotificationStatusUnread)
 | 
				
			||||||
		c.ServerError("GetNotificationCount", err)
 | 
							if err != nil {
 | 
				
			||||||
		return
 | 
								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")
 | 
						c.Data["Title"] = c.Tr("notifications")
 | 
				
			||||||
	if status == models.NotificationStatusUnread && total > 0 {
 | 
					 | 
				
			||||||
		title = fmt.Sprintf("(%d) %s", total, title)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	c.Data["Title"] = title
 | 
					 | 
				
			||||||
	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"))
 | 
						if !c.QueryBool("noredirect") {
 | 
				
			||||||
	c.Redirect(url, 303)
 | 
							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>
 | 
				
			||||||
 | 
										{{$notificationUnreadCount := 0}}
 | 
				
			||||||
					{{if .NotificationUnreadCount}}
 | 
										{{if .NotificationUnreadCount}}{{$notificationUnreadCount = call .NotificationUnreadCount}}{{end}}
 | 
				
			||||||
						<span class="ui red label">
 | 
										<span class="ui red label {{if not $notificationUnreadCount}}hidden{{end}} notification_count">
 | 
				
			||||||
							{{.NotificationUnreadCount}}
 | 
											{{$notificationUnreadCount}}
 | 
				
			||||||
						</span>
 | 
										</span>
 | 
				
			||||||
					{{end}}
 | 
					 | 
				
			||||||
				</span>
 | 
									</span>
 | 
				
			||||||
			</a>
 | 
								</a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,119 +1,3 @@
 | 
				
			|||||||
{{template "base/head" .}}
 | 
					{{template "base/head" .}}
 | 
				
			||||||
 | 
					{{template "user/notification/notification_div" .}}
 | 
				
			||||||
<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 "base/footer" .}}
 | 
					{{template "base/footer" .}}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										128
									
								
								templates/user/notification/notification_div.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								templates/user/notification/notification_div.tmpl
									
									
									
									
									
										Normal file
									
								
							@@ -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>
 | 
				
			||||||
							
								
								
									
										110
									
								
								web_src/js/features/notification.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								web_src/js/features/notification.js
									
									
									
									
									
										Normal file
									
								
							@@ -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) {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user