-
-
Notifications
You must be signed in to change notification settings - Fork 5.9k
Partially refresh notifications list #35010
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8a63806
65071d5
a9c914f
2e4add4
138cc99
cb0c106
c3247a5
45f1d57
a646b32
11443d8
b0c1110
1712c11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,10 +4,8 @@ | |
package user | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
|
||
activities_model "code.gitea.io/gitea/models/activities" | ||
|
@@ -34,58 +32,42 @@ const ( | |
tplNotificationSubscriptions templates.TplName = "user/notification/notification_subscriptions" | ||
) | ||
|
||
// Notifications is the notifications page | ||
// Notifications is the notification list page | ||
func Notifications(ctx *context.Context) { | ||
getNotifications(ctx) | ||
prepareUserNotificationsData(ctx) | ||
if ctx.Written() { | ||
return | ||
} | ||
if ctx.FormBool("div-only") { | ||
ctx.Data["SequenceNumber"] = ctx.FormString("sequence-number") | ||
ctx.HTML(http.StatusOK, tplNotificationDiv) | ||
return | ||
} | ||
ctx.HTML(http.StatusOK, tplNotification) | ||
} | ||
|
||
func getNotifications(ctx *context.Context) { | ||
var ( | ||
keyword = ctx.FormTrim("q") | ||
status activities_model.NotificationStatus | ||
page = ctx.FormInt("page") | ||
perPage = ctx.FormInt("perPage") | ||
) | ||
if page < 1 { | ||
page = 1 | ||
} | ||
if perPage < 1 { | ||
perPage = 20 | ||
} | ||
|
||
switch keyword { | ||
case "read": | ||
status = activities_model.NotificationStatusRead | ||
default: | ||
status = activities_model.NotificationStatusUnread | ||
} | ||
func prepareUserNotificationsData(ctx *context.Context) { | ||
pageType := ctx.FormString("type", ctx.FormString("q")) // "q" is the legacy query parameter for "page type" | ||
page := max(1, ctx.FormInt("page")) | ||
perPage := util.IfZero(ctx.FormInt("perPage"), 20) // this value is never used or exposed .... | ||
queryStatus := util.Iif(pageType == "read", activities_model.NotificationStatusRead, activities_model.NotificationStatusUnread) | ||
|
||
total, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{ | ||
UserID: ctx.Doer.ID, | ||
Status: []activities_model.NotificationStatus{status}, | ||
Status: []activities_model.NotificationStatus{queryStatus}, | ||
}) | ||
if err != nil { | ||
ctx.ServerError("ErrGetNotificationCount", err) | ||
return | ||
} | ||
|
||
// redirect to last page if request page is more than total pages | ||
pager := context.NewPagination(int(total), perPage, page, 5) | ||
if pager.Paginater.Current() < page { | ||
ctx.Redirect(fmt.Sprintf("%s/notifications?q=%s&page=%d", setting.AppSubURL, url.QueryEscape(ctx.FormString("q")), pager.Paginater.Current())) | ||
return | ||
// use the last page if the requested page is more than total pages | ||
page = pager.Paginater.Current() | ||
pager = context.NewPagination(int(total), perPage, page, 5) | ||
} | ||
|
||
statuses := []activities_model.NotificationStatus{status, activities_model.NotificationStatusPinned} | ||
statuses := []activities_model.NotificationStatus{queryStatus, activities_model.NotificationStatusPinned} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The pinned status would still appear in both lists which is somehow strange IMO. No idea what the best way to improve would be as I am in general not 100% sure how ppl use this. Would it make sense to put pinned entries to the top? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leave the problem to the future? |
||
nls, err := db.Find[activities_model.Notification](ctx, activities_model.FindNotificationOptions{ | ||
ListOptions: db.ListOptions{ | ||
PageSize: perPage, | ||
|
@@ -142,51 +124,37 @@ func getNotifications(ctx *context.Context) { | |
} | ||
|
||
ctx.Data["Title"] = ctx.Tr("notifications") | ||
ctx.Data["Keyword"] = keyword | ||
ctx.Data["Status"] = status | ||
ctx.Data["PageType"] = pageType | ||
ctx.Data["Notifications"] = notifications | ||
|
||
ctx.Data["Link"] = setting.AppSubURL + "/notifications" | ||
ctx.Data["SequenceNumber"] = ctx.FormString("sequence-number") | ||
pager.AddParamFromRequest(ctx.Req) | ||
ctx.Data["Page"] = pager | ||
} | ||
|
||
// NotificationStatusPost is a route for changing the status of a notification | ||
func NotificationStatusPost(ctx *context.Context) { | ||
var ( | ||
notificationID = ctx.FormInt64("notification_id") | ||
statusStr = ctx.FormString("status") | ||
status activities_model.NotificationStatus | ||
) | ||
|
||
switch statusStr { | ||
case "read": | ||
status = activities_model.NotificationStatusRead | ||
case "unread": | ||
status = activities_model.NotificationStatusUnread | ||
case "pinned": | ||
status = activities_model.NotificationStatusPinned | ||
notificationID := ctx.FormInt64("notification_id") | ||
var newStatus activities_model.NotificationStatus | ||
switch ctx.FormString("notification_action") { | ||
case "mark_as_read": | ||
newStatus = activities_model.NotificationStatusRead | ||
case "mark_as_unread": | ||
newStatus = activities_model.NotificationStatusUnread | ||
case "pin": | ||
newStatus = activities_model.NotificationStatusPinned | ||
default: | ||
ctx.ServerError("InvalidNotificationStatus", errors.New("Invalid notification status")) | ||
return | ||
return // ignore user's invalid input | ||
} | ||
|
||
if _, err := activities_model.SetNotificationStatus(ctx, notificationID, ctx.Doer, status); err != nil { | ||
if _, err := activities_model.SetNotificationStatus(ctx, notificationID, ctx.Doer, newStatus); err != nil { | ||
ctx.ServerError("SetNotificationStatus", err) | ||
return | ||
} | ||
|
||
if !ctx.FormBool("noredirect") { | ||
url := fmt.Sprintf("%s/notifications?page=%s", setting.AppSubURL, url.QueryEscape(ctx.FormString("page"))) | ||
ctx.Redirect(url, http.StatusSeeOther) | ||
} | ||
|
||
getNotifications(ctx) | ||
prepareUserNotificationsData(ctx) | ||
if ctx.Written() { | ||
return | ||
} | ||
ctx.Data["Link"] = setting.AppSubURL + "/notifications" | ||
ctx.Data["SequenceNumber"] = ctx.Req.PostFormValue("sequence-number") | ||
|
||
ctx.HTML(http.StatusOK, tplNotificationDiv) | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,124 +1,96 @@ | ||
<div role="main" aria-label="{{.Title}}" class="page-content user notification" id="notification_div" data-sequence-number="{{.SequenceNumber}}"> | ||
<div class="ui container"> | ||
{{$statusUnread := 1}}{{$statusRead := 2}}{{$statusPinned := 3}} | ||
{{$notificationUnreadCount := call .PageGlobalData.GetNotificationUnreadCount}} | ||
<div class="tw-flex tw-items-center tw-justify-between tw-mb-[--page-spacing]"> | ||
{{$pageTypeIsRead := eq $.PageType "read"}} | ||
<div class="flex-text-block tw-justify-between tw-mb-[--page-spacing]"> | ||
<div class="small-menu-items ui compact tiny menu"> | ||
<a class="{{if eq .Status 1}}active {{end}}item" href="{{AppSubUrl}}/notifications?q=unread"> | ||
<a class="{{if not $pageTypeIsRead}}active{{end}} item" href="{{AppSubUrl}}/notifications?type=unread"> | ||
{{ctx.Locale.Tr "notification.unread"}} | ||
<div class="notifications-unread-count ui label {{if not $notificationUnreadCount}}tw-hidden{{end}}">{{$notificationUnreadCount}}</div> | ||
</a> | ||
<a class="{{if eq .Status 2}}active {{end}}item" href="{{AppSubUrl}}/notifications?q=read"> | ||
<a class="{{if $pageTypeIsRead}}active{{end}} item" href="{{AppSubUrl}}/notifications?type=read"> | ||
{{ctx.Locale.Tr "notification.read"}} | ||
</a> | ||
</div> | ||
{{if and (eq .Status 1)}} | ||
{{if and (not $pageTypeIsRead) $notificationUnreadCount}} | ||
<form action="{{AppSubUrl}}/notifications/purge" method="post"> | ||
{{$.CsrfTokenHtml}} | ||
<div class="{{if not $notificationUnreadCount}}tw-hidden{{end}}"> | ||
<button class="ui mini button primary tw-mr-0" title="{{ctx.Locale.Tr "notification.mark_all_as_read"}}"> | ||
{{svg "octicon-checklist"}} | ||
</button> | ||
</div> | ||
<button class="ui mini button primary tw-mr-0" title="{{ctx.Locale.Tr "notification.mark_all_as_read"}}"> | ||
{{svg "octicon-checklist"}} | ||
</button> | ||
</form> | ||
{{end}} | ||
</div> | ||
<div class="tw-p-0"> | ||
<div id="notification_table"> | ||
{{if not .Notifications}} | ||
<div class="tw-flex tw-items-center tw-flex-col tw-p-4"> | ||
{{svg "octicon-inbox" 56 "tw-mb-4"}} | ||
{{if eq .Status 1}} | ||
{{ctx.Locale.Tr "notification.no_unread"}} | ||
<div id="notification_table"> | ||
{{range $one := .Notifications}} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not keeping There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using
I think |
||
<div class="notifications-item" id="notification_{{$one.ID}}" data-status="{{$one.Status}}"> | ||
<div class="tw-self-start tw-mt-[2px]"> | ||
{{if $one.Issue}} | ||
{{template "shared/issueicon" $one.Issue}} | ||
{{else}} | ||
{{ctx.Locale.Tr "notification.no_read"}} | ||
{{svg "octicon-repo" 16 "text grey"}} | ||
{{end}} | ||
</div> | ||
{{else}} | ||
{{range $notification := .Notifications}} | ||
<div class="notifications-item tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-p-2" id="notification_{{.ID}}" data-status="{{.Status}}"> | ||
<div class="notifications-icon tw-ml-2 tw-mr-1 tw-self-start tw-mt-1"> | ||
{{if .Issue}} | ||
{{template "shared/issueicon" .Issue}} | ||
{{else}} | ||
{{svg "octicon-repo" 16 "text grey"}} | ||
{{end}} | ||
</div> | ||
<a class="notifications-link tw-flex tw-flex-1 tw-flex-col silenced" href="{{.Link ctx}}"> | ||
<div class="notifications-top-row tw-text-13 tw-break-anywhere"> | ||
{{.Repository.FullName}} {{if .Issue}}<span class="text light-3">#{{.Issue.Index}}</span>{{end}} | ||
{{if eq .Status 3}} | ||
{{svg "octicon-pin" 13 "text blue tw-mt-0.5 tw-ml-1"}} | ||
{{end}} | ||
</div> | ||
<div class="notifications-bottom-row tw-text-16 tw-py-0.5"> | ||
<span class="issue-title tw-break-anywhere"> | ||
{{if .Issue}} | ||
{{.Issue.Title | ctx.RenderUtils.RenderIssueSimpleTitle}} | ||
{{else}} | ||
{{.Repository.FullName}} | ||
{{end}} | ||
</span> | ||
</div> | ||
</a> | ||
<div class="notifications-updated tw-items-center tw-mr-2"> | ||
{{if .Issue}} | ||
{{DateUtils.TimeSince .Issue.UpdatedUnix}} | ||
{{else}} | ||
{{DateUtils.TimeSince .UpdatedUnix}} | ||
{{end}} | ||
</div> | ||
<div class="notifications-buttons tw-items-center tw-justify-end tw-gap-1 tw-px-1"> | ||
{{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="btn interact-bg tw-p-2" title="{{ctx.Locale.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"}} | ||
</button> | ||
</form> | ||
{{end}} | ||
{{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="btn interact-bg tw-p-2" title="{{ctx.Locale.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"}} | ||
</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="btn interact-bg tw-p-2" title="{{ctx.Locale.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"}} | ||
</button> | ||
</form> | ||
{{end}} | ||
</div> | ||
<a class="notifications-link silenced tw-flex-1" href="{{$one.Link ctx}}"> | ||
<div class="flex-text-block tw-text-[0.95em]"> | ||
{{$one.Repository.FullName}} {{if $one.Issue}}<span class="text light-3">#{{$one.Issue.Index}}</span>{{end}} | ||
{{if eq $one.Status $statusPinned}} | ||
{{svg "octicon-pin" 13 "text blue"}} | ||
{{end}} | ||
</div> | ||
<div class="tw-text-16 tw-py-0.5"> | ||
{{if $one.Issue}} | ||
{{$one.Issue.Title | ctx.RenderUtils.RenderIssueSimpleTitle}} | ||
{{else}} | ||
{{$one.Repository.FullName}} | ||
{{end}} | ||
</div> | ||
</a> | ||
<div class="notifications-updated flex-text-inline"> | ||
{{if $one.Issue}} | ||
{{DateUtils.TimeSince $one.Issue.UpdatedUnix}} | ||
{{else}} | ||
{{DateUtils.TimeSince $one.UpdatedUnix}} | ||
{{end}} | ||
</div> | ||
<form class="notifications-buttons" action="{{AppSubUrl}}/notifications/status?type={{$.PageType}}&page={{$.Page.Paginater.Current}}&perPage={{$.Page.Paginater.PagingNum}}" method="post" | ||
hx-boost="true" hx-target="#notification_div" hx-swap="outerHTML" | ||
> | ||
{{$.CsrfTokenHtml}} | ||
<input type="hidden" name="notification_id" value="{{$one.ID}}"> | ||
{{if ne $one.Status $statusPinned}} | ||
<button class="btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "notification.pin"}}" | ||
name="notification_action" value="pin" | ||
> | ||
{{svg "octicon-pin"}} | ||
</button> | ||
{{end}} | ||
{{if or (eq $one.Status $statusUnread) (eq $one.Status $statusPinned)}} | ||
<button class="btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "notification.mark_as_read"}}" | ||
name="notification_action" value="mark_as_read" | ||
> | ||
{{svg "octicon-check"}} | ||
</button> | ||
{{else if eq $one.Status $statusRead}} | ||
<button class="btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "notification.mark_as_unread"}}" | ||
name="notification_action" value="mark_as_unread" | ||
> | ||
{{svg "octicon-bell"}} | ||
</button> | ||
{{end}} | ||
</form> | ||
</div> | ||
{{else}} | ||
<div class="empty-placeholder"> | ||
{{svg "octicon-inbox" 56 "tw-mb-4"}} | ||
{{if $pageTypeIsRead}} | ||
{{ctx.Locale.Tr "notification.no_read"}} | ||
{{else}} | ||
{{ctx.Locale.Tr "notification.no_unread"}} | ||
{{end}} | ||
{{end}} | ||
</div> | ||
</div> | ||
{{end}} | ||
</div> | ||
{{template "base/paginate" .}} | ||
</div> | ||
|
Uh oh!
There was an error while loading. Please reload this page.