+173
appview/notifications/notifications.go
+173
appview/notifications/notifications.go
···
1
+
package notifications
2
+
3
+
import (
4
+
"log"
5
+
"net/http"
6
+
"strconv"
7
+
8
+
"github.com/go-chi/chi/v5"
9
+
"tangled.org/core/appview/db"
10
+
"tangled.org/core/appview/middleware"
11
+
"tangled.org/core/appview/oauth"
12
+
"tangled.org/core/appview/pages"
13
+
)
14
+
15
+
type Notifications struct {
16
+
db *db.DB
17
+
oauth *oauth.OAuth
18
+
pages *pages.Pages
19
+
}
20
+
21
+
func New(database *db.DB, oauthHandler *oauth.OAuth, pagesHandler *pages.Pages) *Notifications {
22
+
return &Notifications{
23
+
db: database,
24
+
oauth: oauthHandler,
25
+
pages: pagesHandler,
26
+
}
27
+
}
28
+
29
+
func (n *Notifications) Router(mw *middleware.Middleware) http.Handler {
30
+
r := chi.NewRouter()
31
+
32
+
r.Use(middleware.AuthMiddleware(n.oauth))
33
+
34
+
r.Get("/", n.notificationsPage)
35
+
36
+
r.Get("/count", n.getUnreadCount)
37
+
r.Post("/{id}/read", n.markRead)
38
+
r.Post("/read-all", n.markAllRead)
39
+
r.Delete("/{id}", n.deleteNotification)
40
+
41
+
return r
42
+
}
43
+
44
+
func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) {
45
+
userDid := n.oauth.GetDid(r)
46
+
47
+
limitStr := r.URL.Query().Get("limit")
48
+
offsetStr := r.URL.Query().Get("offset")
49
+
50
+
limit := 20 // default
51
+
if limitStr != "" {
52
+
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
53
+
limit = l
54
+
}
55
+
}
56
+
57
+
offset := 0 // default
58
+
if offsetStr != "" {
59
+
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
60
+
offset = o
61
+
}
62
+
}
63
+
64
+
notifications, err := n.db.GetNotificationsWithEntities(r.Context(), userDid, limit+1, offset)
65
+
if err != nil {
66
+
log.Println("failed to get notifications:", err)
67
+
n.pages.Error500(w)
68
+
return
69
+
}
70
+
71
+
hasMore := len(notifications) > limit
72
+
if hasMore {
73
+
notifications = notifications[:limit]
74
+
}
75
+
76
+
err = n.db.MarkAllNotificationsRead(r.Context(), userDid)
77
+
if err != nil {
78
+
log.Println("failed to mark notifications as read:", err)
79
+
}
80
+
81
+
unreadCount := 0
82
+
83
+
user := n.oauth.GetUser(r)
84
+
if user == nil {
85
+
http.Error(w, "Failed to get user", http.StatusInternalServerError)
86
+
return
87
+
}
88
+
89
+
params := pages.NotificationsParams{
90
+
LoggedInUser: user,
91
+
Notifications: notifications,
92
+
UnreadCount: unreadCount,
93
+
HasMore: hasMore,
94
+
NextOffset: offset + limit,
95
+
Limit: limit,
96
+
}
97
+
98
+
err = n.pages.Notifications(w, params)
99
+
if err != nil {
100
+
log.Println("failed to load notifs:", err)
101
+
n.pages.Error500(w)
102
+
return
103
+
}
104
+
}
105
+
106
+
func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) {
107
+
userDid := n.oauth.GetDid(r)
108
+
109
+
count, err := n.db.GetUnreadNotificationCount(r.Context(), userDid)
110
+
if err != nil {
111
+
http.Error(w, "Failed to get unread count", http.StatusInternalServerError)
112
+
return
113
+
}
114
+
115
+
params := pages.NotificationCountParams{
116
+
Count: count,
117
+
}
118
+
err = n.pages.NotificationCount(w, params)
119
+
if err != nil {
120
+
http.Error(w, "Failed to render count", http.StatusInternalServerError)
121
+
return
122
+
}
123
+
}
124
+
125
+
func (n *Notifications) markRead(w http.ResponseWriter, r *http.Request) {
126
+
userDid := n.oauth.GetDid(r)
127
+
128
+
idStr := chi.URLParam(r, "id")
129
+
notificationID, err := strconv.ParseInt(idStr, 10, 64)
130
+
if err != nil {
131
+
http.Error(w, "Invalid notification ID", http.StatusBadRequest)
132
+
return
133
+
}
134
+
135
+
err = n.db.MarkNotificationRead(r.Context(), notificationID, userDid)
136
+
if err != nil {
137
+
http.Error(w, "Failed to mark notification as read", http.StatusInternalServerError)
138
+
return
139
+
}
140
+
141
+
w.WriteHeader(http.StatusNoContent)
142
+
}
143
+
144
+
func (n *Notifications) markAllRead(w http.ResponseWriter, r *http.Request) {
145
+
userDid := n.oauth.GetDid(r)
146
+
147
+
err := n.db.MarkAllNotificationsRead(r.Context(), userDid)
148
+
if err != nil {
149
+
http.Error(w, "Failed to mark all notifications as read", http.StatusInternalServerError)
150
+
return
151
+
}
152
+
153
+
http.Redirect(w, r, "/notifications", http.StatusSeeOther)
154
+
}
155
+
156
+
func (n *Notifications) deleteNotification(w http.ResponseWriter, r *http.Request) {
157
+
userDid := n.oauth.GetDid(r)
158
+
159
+
idStr := chi.URLParam(r, "id")
160
+
notificationID, err := strconv.ParseInt(idStr, 10, 64)
161
+
if err != nil {
162
+
http.Error(w, "Invalid notification ID", http.StatusBadRequest)
163
+
return
164
+
}
165
+
166
+
err = n.db.DeleteNotification(r.Context(), notificationID, userDid)
167
+
if err != nil {
168
+
http.Error(w, "Failed to delete notification", http.StatusInternalServerError)
169
+
return
170
+
}
171
+
172
+
w.WriteHeader(http.StatusOK)
173
+
}
+51
appview/settings/settings.go
+51
appview/settings/settings.go
···
41
41
{"Name": "profile", "Icon": "user"},
42
42
{"Name": "keys", "Icon": "key"},
43
43
{"Name": "emails", "Icon": "mail"},
44
+
{"Name": "notifications", "Icon": "bell"},
44
45
}
45
46
)
46
47
···
68
69
r.Post("/primary", s.emailsPrimary)
69
70
})
70
71
72
+
r.Route("/notifications", func(r chi.Router) {
73
+
r.Get("/", s.notificationsSettings)
74
+
r.Put("/", s.updateNotificationPreferences)
75
+
})
76
+
71
77
return r
72
78
}
73
79
···
79
85
Tabs: settingsTabs,
80
86
Tab: "profile",
81
87
})
88
+
}
89
+
90
+
func (s *Settings) notificationsSettings(w http.ResponseWriter, r *http.Request) {
91
+
user := s.OAuth.GetUser(r)
92
+
did := s.OAuth.GetDid(r)
93
+
94
+
prefs, err := s.Db.GetNotificationPreferences(r.Context(), did)
95
+
if err != nil {
96
+
log.Printf("failed to get notification preferences: %s", err)
97
+
s.Pages.Notice(w, "settings-notifications-error", "Unable to load notification preferences.")
98
+
return
99
+
}
100
+
101
+
s.Pages.UserNotificationSettings(w, pages.UserNotificationSettingsParams{
102
+
LoggedInUser: user,
103
+
Preferences: prefs,
104
+
Tabs: settingsTabs,
105
+
Tab: "notifications",
106
+
})
107
+
}
108
+
109
+
func (s *Settings) updateNotificationPreferences(w http.ResponseWriter, r *http.Request) {
110
+
did := s.OAuth.GetDid(r)
111
+
112
+
prefs := &models.NotificationPreferences{
113
+
UserDid: did,
114
+
RepoStarred: r.FormValue("repo_starred") == "on",
115
+
IssueCreated: r.FormValue("issue_created") == "on",
116
+
IssueCommented: r.FormValue("issue_commented") == "on",
117
+
IssueClosed: r.FormValue("issue_closed") == "on",
118
+
PullCreated: r.FormValue("pull_created") == "on",
119
+
PullCommented: r.FormValue("pull_commented") == "on",
120
+
PullMerged: r.FormValue("pull_merged") == "on",
121
+
Followed: r.FormValue("followed") == "on",
122
+
EmailNotifications: r.FormValue("email_notifications") == "on",
123
+
}
124
+
125
+
err := s.Db.UpdateNotificationPreferences(r.Context(), prefs)
126
+
if err != nil {
127
+
log.Printf("failed to update notification preferences: %s", err)
128
+
s.Pages.Notice(w, "settings-notifications-error", "Unable to save notification preferences.")
129
+
return
130
+
}
131
+
132
+
s.Pages.Notice(w, "settings-notifications-success", "Notification preferences saved successfully.")
82
133
}
83
134
84
135
func (s *Settings) keysSettings(w http.ResponseWriter, r *http.Request) {
+6
appview/state/router.go
+6
appview/state/router.go
···
10
10
"tangled.org/core/appview/knots"
11
11
"tangled.org/core/appview/labels"
12
12
"tangled.org/core/appview/middleware"
13
+
"tangled.org/core/appview/notifications"
13
14
oauthhandler "tangled.org/core/appview/oauth/handler"
14
15
"tangled.org/core/appview/pipelines"
15
16
"tangled.org/core/appview/pulls"
···
274
275
func (s *State) LabelsRouter(mw *middleware.Middleware) http.Handler {
275
276
ls := labels.New(s.oauth, s.pages, s.db, s.validator)
276
277
return ls.Router(mw)
278
+
}
279
+
280
+
func (s *State) NotificationsRouter(mw *middleware.Middleware) http.Handler {
281
+
notifs := notifications.New(s.db, s.oauth, s.pages)
282
+
return notifs.Router(mw)
277
283
}
278
284
279
285
func (s *State) SignupRouter() http.Handler {