Skip to content

Commit ea809a5

Browse files
anbratenAnton Brackewxiaoguang
authored
Partially refresh notifications list (#35010)
This PR prevents full reloads for the notifications list when changing a notifications status (read, unread, pinned). --------- Co-authored-by: Anton Bracke <anton.bracke@fastleansmart.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
1 parent b6d6402 commit ea809a5

File tree

5 files changed

+120
-196
lines changed

5 files changed

+120
-196
lines changed

routers/web/user/notification.go

Lines changed: 27 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44
package user
55

66
import (
7-
"errors"
87
"fmt"
98
"net/http"
10-
"net/url"
119
"strings"
1210

1311
activities_model "code.gitea.io/gitea/models/activities"
@@ -34,58 +32,42 @@ const (
3432
tplNotificationSubscriptions templates.TplName = "user/notification/notification_subscriptions"
3533
)
3634

37-
// Notifications is the notifications page
35+
// Notifications is the notification list page
3836
func Notifications(ctx *context.Context) {
39-
getNotifications(ctx)
37+
prepareUserNotificationsData(ctx)
4038
if ctx.Written() {
4139
return
4240
}
4341
if ctx.FormBool("div-only") {
44-
ctx.Data["SequenceNumber"] = ctx.FormString("sequence-number")
4542
ctx.HTML(http.StatusOK, tplNotificationDiv)
4643
return
4744
}
4845
ctx.HTML(http.StatusOK, tplNotification)
4946
}
5047

51-
func getNotifications(ctx *context.Context) {
52-
var (
53-
keyword = ctx.FormTrim("q")
54-
status activities_model.NotificationStatus
55-
page = ctx.FormInt("page")
56-
perPage = ctx.FormInt("perPage")
57-
)
58-
if page < 1 {
59-
page = 1
60-
}
61-
if perPage < 1 {
62-
perPage = 20
63-
}
64-
65-
switch keyword {
66-
case "read":
67-
status = activities_model.NotificationStatusRead
68-
default:
69-
status = activities_model.NotificationStatusUnread
70-
}
48+
func prepareUserNotificationsData(ctx *context.Context) {
49+
pageType := ctx.FormString("type", ctx.FormString("q")) // "q" is the legacy query parameter for "page type"
50+
page := max(1, ctx.FormInt("page"))
51+
perPage := util.IfZero(ctx.FormInt("perPage"), 20) // this value is never used or exposed ....
52+
queryStatus := util.Iif(pageType == "read", activities_model.NotificationStatusRead, activities_model.NotificationStatusUnread)
7153

7254
total, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
7355
UserID: ctx.Doer.ID,
74-
Status: []activities_model.NotificationStatus{status},
56+
Status: []activities_model.NotificationStatus{queryStatus},
7557
})
7658
if err != nil {
7759
ctx.ServerError("ErrGetNotificationCount", err)
7860
return
7961
}
8062

81-
// redirect to last page if request page is more than total pages
8263
pager := context.NewPagination(int(total), perPage, page, 5)
8364
if pager.Paginater.Current() < page {
84-
ctx.Redirect(fmt.Sprintf("%s/notifications?q=%s&page=%d", setting.AppSubURL, url.QueryEscape(ctx.FormString("q")), pager.Paginater.Current()))
85-
return
65+
// use the last page if the requested page is more than total pages
66+
page = pager.Paginater.Current()
67+
pager = context.NewPagination(int(total), perPage, page, 5)
8668
}
8769

88-
statuses := []activities_model.NotificationStatus{status, activities_model.NotificationStatusPinned}
70+
statuses := []activities_model.NotificationStatus{queryStatus, activities_model.NotificationStatusPinned}
8971
nls, err := db.Find[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
9072
ListOptions: db.ListOptions{
9173
PageSize: perPage,
@@ -142,51 +124,37 @@ func getNotifications(ctx *context.Context) {
142124
}
143125

144126
ctx.Data["Title"] = ctx.Tr("notifications")
145-
ctx.Data["Keyword"] = keyword
146-
ctx.Data["Status"] = status
127+
ctx.Data["PageType"] = pageType
147128
ctx.Data["Notifications"] = notifications
148-
129+
ctx.Data["Link"] = setting.AppSubURL + "/notifications"
130+
ctx.Data["SequenceNumber"] = ctx.FormString("sequence-number")
149131
pager.AddParamFromRequest(ctx.Req)
150132
ctx.Data["Page"] = pager
151133
}
152134

153135
// NotificationStatusPost is a route for changing the status of a notification
154136
func NotificationStatusPost(ctx *context.Context) {
155-
var (
156-
notificationID = ctx.FormInt64("notification_id")
157-
statusStr = ctx.FormString("status")
158-
status activities_model.NotificationStatus
159-
)
160-
161-
switch statusStr {
162-
case "read":
163-
status = activities_model.NotificationStatusRead
164-
case "unread":
165-
status = activities_model.NotificationStatusUnread
166-
case "pinned":
167-
status = activities_model.NotificationStatusPinned
137+
notificationID := ctx.FormInt64("notification_id")
138+
var newStatus activities_model.NotificationStatus
139+
switch ctx.FormString("notification_action") {
140+
case "mark_as_read":
141+
newStatus = activities_model.NotificationStatusRead
142+
case "mark_as_unread":
143+
newStatus = activities_model.NotificationStatusUnread
144+
case "pin":
145+
newStatus = activities_model.NotificationStatusPinned
168146
default:
169-
ctx.ServerError("InvalidNotificationStatus", errors.New("Invalid notification status"))
170-
return
147+
return // ignore user's invalid input
171148
}
172-
173-
if _, err := activities_model.SetNotificationStatus(ctx, notificationID, ctx.Doer, status); err != nil {
149+
if _, err := activities_model.SetNotificationStatus(ctx, notificationID, ctx.Doer, newStatus); err != nil {
174150
ctx.ServerError("SetNotificationStatus", err)
175151
return
176152
}
177153

178-
if !ctx.FormBool("noredirect") {
179-
url := fmt.Sprintf("%s/notifications?page=%s", setting.AppSubURL, url.QueryEscape(ctx.FormString("page")))
180-
ctx.Redirect(url, http.StatusSeeOther)
181-
}
182-
183-
getNotifications(ctx)
154+
prepareUserNotificationsData(ctx)
184155
if ctx.Written() {
185156
return
186157
}
187-
ctx.Data["Link"] = setting.AppSubURL + "/notifications"
188-
ctx.Data["SequenceNumber"] = ctx.Req.PostFormValue("sequence-number")
189-
190158
ctx.HTML(http.StatusOK, tplNotificationDiv)
191159
}
192160

templates/user/notification/notification_div.tmpl

Lines changed: 73 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,96 @@
11
<div role="main" aria-label="{{.Title}}" class="page-content user notification" id="notification_div" data-sequence-number="{{.SequenceNumber}}">
22
<div class="ui container">
3+
{{$statusUnread := 1}}{{$statusRead := 2}}{{$statusPinned := 3}}
34
{{$notificationUnreadCount := call .PageGlobalData.GetNotificationUnreadCount}}
4-
<div class="tw-flex tw-items-center tw-justify-between tw-mb-[--page-spacing]">
5+
{{$pageTypeIsRead := eq $.PageType "read"}}
6+
<div class="flex-text-block tw-justify-between tw-mb-[--page-spacing]">
57
<div class="small-menu-items ui compact tiny menu">
6-
<a class="{{if eq .Status 1}}active {{end}}item" href="{{AppSubUrl}}/notifications?q=unread">
8+
<a class="{{if not $pageTypeIsRead}}active{{end}} item" href="{{AppSubUrl}}/notifications?type=unread">
79
{{ctx.Locale.Tr "notification.unread"}}
810
<div class="notifications-unread-count ui label {{if not $notificationUnreadCount}}tw-hidden{{end}}">{{$notificationUnreadCount}}</div>
911
</a>
10-
<a class="{{if eq .Status 2}}active {{end}}item" href="{{AppSubUrl}}/notifications?q=read">
12+
<a class="{{if $pageTypeIsRead}}active{{end}} item" href="{{AppSubUrl}}/notifications?type=read">
1113
{{ctx.Locale.Tr "notification.read"}}
1214
</a>
1315
</div>
14-
{{if and (eq .Status 1)}}
16+
{{if and (not $pageTypeIsRead) $notificationUnreadCount}}
1517
<form action="{{AppSubUrl}}/notifications/purge" method="post">
1618
{{$.CsrfTokenHtml}}
17-
<div class="{{if not $notificationUnreadCount}}tw-hidden{{end}}">
18-
<button class="ui mini button primary tw-mr-0" title="{{ctx.Locale.Tr "notification.mark_all_as_read"}}">
19-
{{svg "octicon-checklist"}}
20-
</button>
21-
</div>
19+
<button class="ui mini button primary tw-mr-0" title="{{ctx.Locale.Tr "notification.mark_all_as_read"}}">
20+
{{svg "octicon-checklist"}}
21+
</button>
2222
</form>
2323
{{end}}
2424
</div>
25-
<div class="tw-p-0">
26-
<div id="notification_table">
27-
{{if not .Notifications}}
28-
<div class="tw-flex tw-items-center tw-flex-col tw-p-4">
29-
{{svg "octicon-inbox" 56 "tw-mb-4"}}
30-
{{if eq .Status 1}}
31-
{{ctx.Locale.Tr "notification.no_unread"}}
25+
<div id="notification_table">
26+
{{range $one := .Notifications}}
27+
<div class="notifications-item" id="notification_{{$one.ID}}" data-status="{{$one.Status}}">
28+
<div class="tw-self-start tw-mt-[2px]">
29+
{{if $one.Issue}}
30+
{{template "shared/issueicon" $one.Issue}}
3231
{{else}}
33-
{{ctx.Locale.Tr "notification.no_read"}}
32+
{{svg "octicon-repo" 16 "text grey"}}
3433
{{end}}
3534
</div>
36-
{{else}}
37-
{{range $notification := .Notifications}}
38-
<div class="notifications-item tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-p-2" id="notification_{{.ID}}" data-status="{{.Status}}">
39-
<div class="notifications-icon tw-ml-2 tw-mr-1 tw-self-start tw-mt-1">
40-
{{if .Issue}}
41-
{{template "shared/issueicon" .Issue}}
42-
{{else}}
43-
{{svg "octicon-repo" 16 "text grey"}}
44-
{{end}}
45-
</div>
46-
<a class="notifications-link tw-flex tw-flex-1 tw-flex-col silenced" href="{{.Link ctx}}">
47-
<div class="notifications-top-row tw-text-13 tw-break-anywhere">
48-
{{.Repository.FullName}} {{if .Issue}}<span class="text light-3">#{{.Issue.Index}}</span>{{end}}
49-
{{if eq .Status 3}}
50-
{{svg "octicon-pin" 13 "text blue tw-mt-0.5 tw-ml-1"}}
51-
{{end}}
52-
</div>
53-
<div class="notifications-bottom-row tw-text-16 tw-py-0.5">
54-
<span class="issue-title tw-break-anywhere">
55-
{{if .Issue}}
56-
{{.Issue.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}
57-
{{else}}
58-
{{.Repository.FullName}}
59-
{{end}}
60-
</span>
61-
</div>
62-
</a>
63-
<div class="notifications-updated tw-items-center tw-mr-2">
64-
{{if .Issue}}
65-
{{DateUtils.TimeSince .Issue.UpdatedUnix}}
66-
{{else}}
67-
{{DateUtils.TimeSince .UpdatedUnix}}
68-
{{end}}
69-
</div>
70-
<div class="notifications-buttons tw-items-center tw-justify-end tw-gap-1 tw-px-1">
71-
{{if ne .Status 3}}
72-
<form action="{{AppSubUrl}}/notifications/status" method="post">
73-
{{$.CsrfTokenHtml}}
74-
<input type="hidden" name="notification_id" value="{{.ID}}">
75-
<input type="hidden" name="status" value="pinned">
76-
<button class="btn interact-bg tw-p-2" title="{{ctx.Locale.Tr "notification.pin"}}"
77-
data-url="{{AppSubUrl}}/notifications/status"
78-
data-status="pinned"
79-
data-page="{{$.Page.Paginater.Current}}"
80-
data-notification-id="{{.ID}}"
81-
data-q="{{$.Keyword}}">
82-
{{svg "octicon-pin"}}
83-
</button>
84-
</form>
85-
{{end}}
86-
{{if or (eq .Status 1) (eq .Status 3)}}
87-
<form action="{{AppSubUrl}}/notifications/status" method="post">
88-
{{$.CsrfTokenHtml}}
89-
<input type="hidden" name="notification_id" value="{{.ID}}">
90-
<input type="hidden" name="status" value="read">
91-
<input type="hidden" name="page" value="{{$.Page.Paginater.Current}}">
92-
<button class="btn interact-bg tw-p-2" title="{{ctx.Locale.Tr "notification.mark_as_read"}}"
93-
data-url="{{AppSubUrl}}/notifications/status"
94-
data-status="read"
95-
data-page="{{$.Page.Paginater.Current}}"
96-
data-notification-id="{{.ID}}"
97-
data-q="{{$.Keyword}}">
98-
{{svg "octicon-check"}}
99-
</button>
100-
</form>
101-
{{else if eq .Status 2}}
102-
<form action="{{AppSubUrl}}/notifications/status" method="post">
103-
{{$.CsrfTokenHtml}}
104-
<input type="hidden" name="notification_id" value="{{.ID}}">
105-
<input type="hidden" name="status" value="unread">
106-
<input type="hidden" name="page" value="{{$.Page.Paginater.Current}}">
107-
<button class="btn interact-bg tw-p-2" title="{{ctx.Locale.Tr "notification.mark_as_unread"}}"
108-
data-url="{{AppSubUrl}}/notifications/status"
109-
data-status="unread"
110-
data-page="{{$.Page.Paginater.Current}}"
111-
data-notification-id="{{.ID}}"
112-
data-q="{{$.Keyword}}">
113-
{{svg "octicon-bell"}}
114-
</button>
115-
</form>
116-
{{end}}
117-
</div>
35+
<a class="notifications-link silenced tw-flex-1" href="{{$one.Link ctx}}">
36+
<div class="flex-text-block tw-text-[0.95em]">
37+
{{$one.Repository.FullName}} {{if $one.Issue}}<span class="text light-3">#{{$one.Issue.Index}}</span>{{end}}
38+
{{if eq $one.Status $statusPinned}}
39+
{{svg "octicon-pin" 13 "text blue"}}
40+
{{end}}
41+
</div>
42+
<div class="tw-text-16 tw-py-0.5">
43+
{{if $one.Issue}}
44+
{{$one.Issue.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}
45+
{{else}}
46+
{{$one.Repository.FullName}}
47+
{{end}}
11848
</div>
49+
</a>
50+
<div class="notifications-updated flex-text-inline">
51+
{{if $one.Issue}}
52+
{{DateUtils.TimeSince $one.Issue.UpdatedUnix}}
53+
{{else}}
54+
{{DateUtils.TimeSince $one.UpdatedUnix}}
55+
{{end}}
56+
</div>
57+
<form class="notifications-buttons" action="{{AppSubUrl}}/notifications/status?type={{$.PageType}}&page={{$.Page.Paginater.Current}}&perPage={{$.Page.Paginater.PagingNum}}" method="post"
58+
hx-boost="true" hx-target="#notification_div" hx-swap="outerHTML"
59+
>
60+
{{$.CsrfTokenHtml}}
61+
<input type="hidden" name="notification_id" value="{{$one.ID}}">
62+
{{if ne $one.Status $statusPinned}}
63+
<button class="btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "notification.pin"}}"
64+
name="notification_action" value="pin"
65+
>
66+
{{svg "octicon-pin"}}
67+
</button>
68+
{{end}}
69+
{{if or (eq $one.Status $statusUnread) (eq $one.Status $statusPinned)}}
70+
<button class="btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "notification.mark_as_read"}}"
71+
name="notification_action" value="mark_as_read"
72+
>
73+
{{svg "octicon-check"}}
74+
</button>
75+
{{else if eq $one.Status $statusRead}}
76+
<button class="btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "notification.mark_as_unread"}}"
77+
name="notification_action" value="mark_as_unread"
78+
>
79+
{{svg "octicon-bell"}}
80+
</button>
81+
{{end}}
82+
</form>
83+
</div>
84+
{{else}}
85+
<div class="empty-placeholder">
86+
{{svg "octicon-inbox" 56 "tw-mb-4"}}
87+
{{if $pageTypeIsRead}}
88+
{{ctx.Locale.Tr "notification.no_read"}}
89+
{{else}}
90+
{{ctx.Locale.Tr "notification.no_unread"}}
11991
{{end}}
120-
{{end}}
121-
</div>
92+
</div>
93+
{{end}}
12294
</div>
12395
{{template "base/paginate" .}}
12496
</div>

web_src/css/user.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@
114114
border-radius: var(--border-radius);
115115
}
116116

117+
.notifications-item {
118+
display: flex;
119+
align-items: center;
120+
flex-wrap: wrap;
121+
gap: 0.5em;
122+
padding: 0.5em 1em;
123+
}
124+
117125
.notifications-item:hover {
118126
background: var(--color-hover);
119127
}
@@ -129,6 +137,9 @@
129137

130138
.notifications-item:hover .notifications-buttons {
131139
display: flex;
140+
align-items: center;
141+
justify-content: end;
142+
gap: 0.25em;
132143
}
133144

134145
.notifications-item:hover .notifications-updated {

0 commit comments

Comments
 (0)