mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +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-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"
 | 
			
		||||
 | 
			
		||||
@@ -18,6 +19,7 @@ import (
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.Data["NotificationUnreadCount"] = func() int64 {
 | 
			
		||||
		count, err := models.GetNotificationCount(c.User, models.NotificationStatusUnread)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.ServerError("GetNotificationCount", err)
 | 
			
		||||
		return
 | 
			
		||||
			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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !c.QueryBool("noredirect") {
 | 
			
		||||
		url := fmt.Sprintf("%s/notifications?page=%s", setting.AppSubURL, c.Query("page"))
 | 
			
		||||
	c.Redirect(url, 303)
 | 
			
		||||
		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}}
 | 
			
		||||
					{{$notificationUnreadCount := 0}}
 | 
			
		||||
					{{if .NotificationUnreadCount}}{{$notificationUnreadCount = call .NotificationUnreadCount}}{{end}}
 | 
			
		||||
					<span class="ui red label {{if not $notificationUnreadCount}}hidden{{end}} notification_count">
 | 
			
		||||
						{{$notificationUnreadCount}}
 | 
			
		||||
					</span>
 | 
			
		||||
					{{end}}
 | 
			
		||||
				</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) {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user