diff --git a/.eslintrc b/.eslintrc
index 8fd53d54a..a8f7f1ae2 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -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]
diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample
index 691a65cc5..fdf974d11 100644
--- a/custom/conf/app.ini.sample
+++ b/custom/conf/app.ini.sample
@@ -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
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index fd32bfd16..9d9d2755e 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -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
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 069a3556d..bf2ed6111 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -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
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index b5b498742..8112880f4 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -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 {
diff --git a/routers/user/notification.go b/routers/user/notification.go
index 74803f149..9724c8108 100644
--- a/routers/user/notification.go
+++ b/routers/user/notification.go
@@ -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)
}
diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
index e0765d59d..2d7d737a0 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -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}},
+ },
};
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index de02bca1f..cedf29e2e 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -46,12 +46,11 @@
{{svg "octicon-bell" 16}}
{{.i18n.Tr "notifications"}}
-
- {{if .NotificationUnreadCount}}
-
- {{.NotificationUnreadCount}}
-
- {{end}}
+ {{$notificationUnreadCount := 0}}
+ {{if .NotificationUnreadCount}}{{$notificationUnreadCount = call .NotificationUnreadCount}}{{end}}
+
+ {{$notificationUnreadCount}}
+
diff --git a/templates/user/notification/notification.tmpl b/templates/user/notification/notification.tmpl
index c4f744a29..b483c15e9 100644
--- a/templates/user/notification/notification.tmpl
+++ b/templates/user/notification/notification.tmpl
@@ -1,119 +1,3 @@
{{template "base/head" .}}
-
-
-
-
-
-
-
- {{if eq (len .Notifications) 0}}
- {{if eq .Status 1}}
- {{.i18n.Tr "notification.no_unread"}}
- {{else}}
- {{.i18n.Tr "notification.no_read"}}
- {{end}}
- {{else}}
-
-
- {{range $notification := .Notifications}}
- {{$issue := $notification.Issue}}
- {{$repo := $notification.Repository}}
- {{$repoOwner := $repo.MustOwner}}
-
-
-
- {{if eq $notification.Status 3}}
- {{svg "octicon-pin" 16}}
- {{else if $issue.IsPull}}
- {{if $issue.IsClosed}}
- {{if $issue.GetPullRequest.HasMerged}}
- {{svg "octicon-git-merge" 16}}
- {{else}}
- {{svg "octicon-git-pull-request" 16}}
- {{end}}
- {{else}}
- {{svg "octicon-git-pull-request" 16}}
- {{end}}
- {{else}}
- {{if $issue.IsClosed}}
- {{svg "octicon-issue-closed" 16}}
- {{else}}
- {{svg "octicon-issue-opened" 16}}
- {{end}}
- {{end}}
- |
-
-
- #{{$issue.Index}} - {{$issue.Title}}
-
- |
-
-
- {{$repoOwner.Name}}/{{$repo.Name}}
-
- |
-
- {{if ne $notification.Status 3}}
-
- {{end}}
- |
-
- {{if or (eq $notification.Status 1) (eq $notification.Status 3)}}
-
- {{else if eq $notification.Status 2}}
-
- {{end}}
- |
-
- {{end}}
-
-
- {{end}}
-
-
- {{template "base/paginate" .}}
-
-
-
+{{template "user/notification/notification_div" .}}
{{template "base/footer" .}}
diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl
new file mode 100644
index 000000000..18054c479
--- /dev/null
+++ b/templates/user/notification/notification_div.tmpl
@@ -0,0 +1,128 @@
+
+
+
+
+
+ {{if eq (len .Notifications) 0}}
+ {{if eq .Status 1}}
+ {{.i18n.Tr "notification.no_unread"}}
+ {{else}}
+ {{.i18n.Tr "notification.no_read"}}
+ {{end}}
+ {{else}}
+
+
+ {{range $notification := .Notifications}}
+ {{$issue := .Issue}}
+ {{$repo := .Repository}}
+ {{$repoOwner := $repo.MustOwner}}
+
+
+ {{if eq .Status 3}}
+ {{svg "octicon-pin" 16}}
+ {{else if $issue.IsPull}}
+ {{if $issue.IsClosed}}
+ {{if $issue.GetPullRequest.HasMerged}}
+ {{svg "octicon-git-merge" 16}}
+ {{else}}
+ {{svg "octicon-git-pull-request" 16}}
+ {{end}}
+ {{else}}
+ {{svg "octicon-git-pull-request" 16}}
+ {{end}}
+ {{else}}
+ {{if $issue.IsClosed}}
+ {{svg "octicon-issue-closed" 16}}
+ {{else}}
+ {{svg "octicon-issue-opened" 16}}
+ {{end}}
+ {{end}}
+ |
+
+
+ #{{$issue.Index}} - {{$issue.Title}}
+
+ |
+
+
+ {{$repoOwner.Name}}/{{$repo.Name}}
+
+ |
+
+ {{if ne .Status 3}}
+
+ {{end}}
+ |
+
+ {{if or (eq .Status 1) (eq .Status 3)}}
+
+ {{else if eq .Status 2}}
+
+ {{end}}
+ |
+
+ {{end}}
+
+
+ {{end}}
+
+ {{template "base/paginate" .}}
+
+
diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js
new file mode 100644
index 000000000..3f2af4de9
--- /dev/null
+++ b/web_src/js/features/notification.js
@@ -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,
+ },
+ });
+}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index ed747765a..9e699c1a2 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -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 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) {
|