- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 6.2k
 
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 1 commit
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,7 +4,6 @@ | |
| package user | ||
| 
     | 
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "net/http" | ||
| "net/url" | ||
| 
          
            
          
           | 
    @@ -34,58 +33,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 := min(1, ctx.FormInt("page")) | ||
| perPage := util.IfZero(ctx.FormInt("perPage"), 20) | ||
| 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 | ||
| // redirect to the 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 | ||
| } | ||
| 
     | 
||
| 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 +125,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": | ||
| notificationID := ctx.FormInt64("notification_id") | ||
| var status activities_model.NotificationStatus | ||
| switch ctx.FormString("notification_action") { | ||
| case "mark_as_read": | ||
| status = activities_model.NotificationStatusRead | ||
| case "unread": | ||
| case "mark_as_unread": | ||
| status = activities_model.NotificationStatusUnread | ||
| case "pinned": | ||
| case "pin": | ||
| status = 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 { | ||
| 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,108 +1,97 @@ | ||
| <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}} | ||
| <button type="button" class="btn interact-bg tw-p-2" title="{{ctx.Locale.Tr "notification.pin"}}" | ||
| hx-post="{{AppSubUrl}}/notifications/status?q={{ $.Keyword }}" | ||
| hx-target="#notification_div" | ||
| hx-swap="outerHTML" | ||
| hx-vals='{"notification_id": "{{.ID}}", "status": "pinned", "page": "{{$.Page.Paginater.Current}}", "_csrf": "{{$.CsrfToken}}", "noredirect": "true" }' | ||
| {{if eq .Status 3}}disabled{{end}} | ||
| > | ||
| {{svg "octicon-pin"}} | ||
| </button> | ||
| {{end}} | ||
| {{if or (eq .Status 1) (eq .Status 3)}} | ||
| <button type="button" class="btn interact-bg tw-p-2" title="{{ctx.Locale.Tr "notification.mark_as_read"}}" | ||
| hx-post="{{AppSubUrl}}/notifications/status?q={{ $.Keyword }}" | ||
| hx-target="#notification_div" | ||
| hx-swap="outerHTML" | ||
| hx-vals='{"notification_id": "{{.ID}}", "status": "read", "page": "{{$.Page.Paginater.Current}}", "_csrf": "{{$.CsrfToken}}", "noredirect": "true"}' | ||
| > | ||
| {{svg "octicon-check"}} | ||
| </button> | ||
| {{else if eq .Status 2}} | ||
| <button type="button" class="btn interact-bg tw-p-2" title="{{ctx.Locale.Tr "notification.mark_as_unread"}}" | ||
| hx-post="{{AppSubUrl}}/notifications/status?q={{ $.Keyword }}" | ||
| hx-target="#notification_div" | ||
| hx-swap="outerHTML" | ||
| hx-vals='{"notification_id": "{{.ID}}", "status": "unread", "page": "{{$.Page.Paginater.Current}}", "_csrf": "{{$.CsrfToken}}", "noredirect": "true"}' | ||
| > | ||
| {{svg "octicon-bell"}} | ||
| </button> | ||
| {{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}}" method="post" | ||
| hx-boost="true" hx-target="#notification_div" hx-swap="outerHTML" | ||
| > | ||
| {{$.CsrfTokenHtml}} | ||
| <input type="hidden" name="notification_id" value="{{$one.ID}}"> | ||
| <input type="hidden" name="page" value="{{$.Page.Paginater.Current}}"> | ||
| {{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 eq .Status 1}} | ||
| {{ctx.Locale.Tr "notification.no_unread"}} | ||
| {{else}} | ||
| {{ctx.Locale.Tr "notification.no_read"}} | ||
| {{end}} | ||
| {{end}} | ||
| </div> | ||
| </div> | ||
| {{end}} | ||
| </div> | ||
| {{template "base/paginate" .}} | ||
| </div> | ||
| 
          
            
          
           | 
    ||
Uh oh!
There was an error while loading. Please reload this page.