appview/notifications: fix pagination #620

merged
opened by oppi.li targeting master from push-spvnpqlqqpkw
Changed files
+99 -69
appview
db
notifications
pages
templates
notifications
pagination
+18 -13
appview/db/notifications.go
··· 3 import ( 4 "context" 5 "database/sql" 6 "fmt" 7 "time" 8 9 "tangled.org/core/appview/models" ··· 248 return GetNotificationsPaginated(e, pagination.FirstPage(), filters...) 249 } 250 251 - func (d *DB) GetUnreadNotificationCount(ctx context.Context, userDID string) (int, error) { 252 - recipientFilter := FilterEq("recipient_did", userDID) 253 - readFilter := FilterEq("read", 0) 254 255 - query := fmt.Sprintf(` 256 - SELECT COUNT(*) 257 - FROM notifications 258 - WHERE %s AND %s 259 - `, recipientFilter.Condition(), readFilter.Condition()) 260 261 - args := append(recipientFilter.Arg(), readFilter.Arg()...) 262 263 - var count int 264 - err := d.DB.QueryRowContext(ctx, query, args...).Scan(&count) 265 - if err != nil { 266 - return 0, fmt.Errorf("failed to get unread count: %w", err) 267 } 268 269 return count, nil
··· 3 import ( 4 "context" 5 "database/sql" 6 + "errors" 7 "fmt" 8 + "strings" 9 "time" 10 11 "tangled.org/core/appview/models" ··· 250 return GetNotificationsPaginated(e, pagination.FirstPage(), filters...) 251 } 252 253 + func CountNotifications(e Execer, filters ...filter) (int64, error) { 254 + var conditions []string 255 + var args []any 256 + for _, filter := range filters { 257 + conditions = append(conditions, filter.Condition()) 258 + args = append(args, filter.Arg()...) 259 + } 260 261 + whereClause := "" 262 + if conditions != nil { 263 + whereClause = " where " + strings.Join(conditions, " and ") 264 + } 265 266 + query := fmt.Sprintf(`select count(1) from notifications %s`, whereClause) 267 + var count int64 268 + err := e.QueryRow(query, args...).Scan(&count) 269 270 + if !errors.Is(err, sql.ErrNoRows) && err != nil { 271 + return 0, err 272 } 273 274 return count, nil
+29 -36
appview/notifications/notifications.go
··· 1 package notifications 2 3 import ( 4 "log" 5 "net/http" 6 "strconv" ··· 32 33 r.Use(middleware.AuthMiddleware(n.oauth)) 34 35 - r.Get("/", n.notificationsPage) 36 37 r.Get("/count", n.getUnreadCount) 38 r.Post("/{id}/read", n.markRead) ··· 45 func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) { 46 userDid := n.oauth.GetDid(r) 47 48 - limitStr := r.URL.Query().Get("limit") 49 - offsetStr := r.URL.Query().Get("offset") 50 - 51 - limit := 20 // default 52 - if limitStr != "" { 53 - if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 { 54 - limit = l 55 - } 56 } 57 58 - offset := 0 // default 59 - if offsetStr != "" { 60 - if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 { 61 - offset = o 62 - } 63 } 64 65 - page := pagination.Page{Limit: limit + 1, Offset: offset} 66 - notifications, err := db.GetNotificationsWithEntities(n.db, page, db.FilterEq("recipient_did", userDid)) 67 if err != nil { 68 log.Println("failed to get notifications:", err) 69 n.pages.Error500(w) 70 return 71 } 72 73 - hasMore := len(notifications) > limit 74 - if hasMore { 75 - notifications = notifications[:limit] 76 - } 77 - 78 err = n.db.MarkAllNotificationsRead(r.Context(), userDid) 79 if err != nil { 80 log.Println("failed to mark notifications as read:", err) ··· 88 return 89 } 90 91 - params := pages.NotificationsParams{ 92 LoggedInUser: user, 93 Notifications: notifications, 94 UnreadCount: unreadCount, 95 - HasMore: hasMore, 96 - NextOffset: offset + limit, 97 - Limit: limit, 98 - } 99 - 100 - err = n.pages.Notifications(w, params) 101 - if err != nil { 102 - log.Println("failed to load notifs:", err) 103 - n.pages.Error500(w) 104 - return 105 - } 106 } 107 108 func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) { 109 - userDid := n.oauth.GetDid(r) 110 - 111 - count, err := n.db.GetUnreadNotificationCount(r.Context(), userDid) 112 if err != nil { 113 http.Error(w, "Failed to get unread count", http.StatusInternalServerError) 114 return
··· 1 package notifications 2 3 import ( 4 + "fmt" 5 "log" 6 "net/http" 7 "strconv" ··· 33 34 r.Use(middleware.AuthMiddleware(n.oauth)) 35 36 + r.With(middleware.Paginate).Get("/", n.notificationsPage) 37 38 r.Get("/count", n.getUnreadCount) 39 r.Post("/{id}/read", n.markRead) ··· 46 func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) { 47 userDid := n.oauth.GetDid(r) 48 49 + page, ok := r.Context().Value("page").(pagination.Page) 50 + if !ok { 51 + log.Println("failed to get page") 52 + page = pagination.FirstPage() 53 } 54 55 + total, err := db.CountNotifications( 56 + n.db, 57 + db.FilterEq("recipient_did", userDid), 58 + ) 59 + if err != nil { 60 + log.Println("failed to get total notifications:", err) 61 + n.pages.Error500(w) 62 + return 63 } 64 65 + notifications, err := db.GetNotificationsWithEntities( 66 + n.db, 67 + page, 68 + db.FilterEq("recipient_did", userDid), 69 + ) 70 if err != nil { 71 log.Println("failed to get notifications:", err) 72 n.pages.Error500(w) 73 return 74 } 75 76 err = n.db.MarkAllNotificationsRead(r.Context(), userDid) 77 if err != nil { 78 log.Println("failed to mark notifications as read:", err) ··· 86 return 87 } 88 89 + fmt.Println(n.pages.Notifications(w, pages.NotificationsParams{ 90 LoggedInUser: user, 91 Notifications: notifications, 92 UnreadCount: unreadCount, 93 + Page: page, 94 + Total: total, 95 + })) 96 } 97 98 func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) { 99 + user := n.oauth.GetUser(r) 100 + count, err := db.CountNotifications( 101 + n.db, 102 + db.FilterEq("recipient_did", user.Did), 103 + db.FilterEq("read", 0), 104 + ) 105 if err != nil { 106 http.Error(w, "Failed to get unread count", http.StatusInternalServerError) 107 return
+3 -4
appview/pages/pages.go
··· 326 LoggedInUser *oauth.User 327 Notifications []*models.NotificationWithEntity 328 UnreadCount int 329 - HasMore bool 330 - NextOffset int 331 - Limit int 332 } 333 334 func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error { ··· 344 } 345 346 type NotificationCountParams struct { 347 - Count int 348 } 349 350 func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) error {
··· 326 LoggedInUser *oauth.User 327 Notifications []*models.NotificationWithEntity 328 UnreadCount int 329 + Page pagination.Page 330 + Total int64 331 } 332 333 func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error { ··· 343 } 344 345 type NotificationCountParams struct { 346 + Count int64 347 } 348 349 func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) error {
+48 -15
appview/pages/templates/notifications/list.html
··· 11 </div> 12 </div> 13 14 - {{if .Notifications}} 15 - <div class="flex flex-col gap-2" id="notifications-list"> 16 - {{range .Notifications}} 17 - {{template "notifications/fragments/item" .}} 18 - {{end}} 19 - </div> 20 21 - {{else}} 22 - <div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white"> 23 - <div class="text-center py-12"> 24 - <div class="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600"> 25 - {{ i "bell-off" "w-16 h-16" }} 26 - </div> 27 - <h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">No notifications</h3> 28 - <p class="text-gray-600 dark:text-gray-400">When you receive notifications, they'll appear here.</p> 29 </div> 30 </div> 31 - {{end}} 32 {{ end }}
··· 11 </div> 12 </div> 13 14 + {{if .Notifications}} 15 + <div class="flex flex-col gap-2" id="notifications-list"> 16 + {{range .Notifications}} 17 + {{template "notifications/fragments/item" .}} 18 + {{end}} 19 + </div> 20 21 + {{else}} 22 + <div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white"> 23 + <div class="text-center py-12"> 24 + <div class="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600"> 25 + {{ i "bell-off" "w-16 h-16" }} 26 </div> 27 + <h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">No notifications</h3> 28 + <p class="text-gray-600 dark:text-gray-400">When you receive notifications, they'll appear here.</p> 29 </div> 30 + </div> 31 + {{end}} 32 + 33 + {{ template "pagination" . }} 34 + {{ end }} 35 + 36 + {{ define "pagination" }} 37 + <div class="flex justify-end mt-4 gap-2"> 38 + {{ if gt .Page.Offset 0 }} 39 + {{ $prev := .Page.Previous }} 40 + <a 41 + class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 42 + hx-boost="true" 43 + href = "/notifications?offset={{ $prev.Offset }}&limit={{ $prev.Limit }}" 44 + > 45 + {{ i "chevron-left" "w-4 h-4" }} 46 + previous 47 + </a> 48 + {{ else }} 49 + <div></div> 50 + {{ end }} 51 + 52 + {{ $next := .Page.Next }} 53 + {{ if lt $next.Offset .Total }} 54 + {{ $next := .Page.Next }} 55 + <a 56 + class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 57 + hx-boost="true" 58 + href = "/notifications?offset={{ $next.Offset }}&limit={{ $next.Limit }}" 59 + > 60 + next 61 + {{ i "chevron-right" "w-4 h-4" }} 62 + </a> 63 + {{ end }} 64 + </div> 65 {{ end }}
+1 -1
appview/pagination/page.go
··· 8 func FirstPage() Page { 9 return Page{ 10 Offset: 0, 11 - Limit: 10, 12 } 13 } 14
··· 8 func FirstPage() Page { 9 return Page{ 10 Offset: 0, 11 + Limit: 30, 12 } 13 } 14