Signed-off-by: oppiliappan me@oppi.li
+18
-13
appview/db/notifications.go
+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
+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
+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
+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 }}