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:
		
							parent
							
								
									e74c4e1be9
								
							
						
					
					
						commit
						b10c416f9e
					
				
					 12 changed files with 331 additions and 140 deletions
				
			
		|  | @ -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" .}} | ||||
|  |  | |||
							
								
								
									
										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 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) { | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue