Monorepo for Tangled
tangled.org
1package notifications
2
3import (
4 "log/slog"
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/models"
12 "tangled.org/core/appview/oauth"
13 "tangled.org/core/appview/pages"
14 "tangled.org/core/appview/pagination"
15 "tangled.org/core/orm"
16)
17
18type Notifications struct {
19 db *db.DB
20 oauth *oauth.OAuth
21 pages *pages.Pages
22 logger *slog.Logger
23}
24
25func New(database *db.DB, oauthHandler *oauth.OAuth, pagesHandler *pages.Pages, logger *slog.Logger) *Notifications {
26 return &Notifications{
27 db: database,
28 oauth: oauthHandler,
29 pages: pagesHandler,
30 logger: logger,
31 }
32}
33
34func (n *Notifications) Router(mw *middleware.Middleware) http.Handler {
35 r := chi.NewRouter()
36
37 r.Get("/count", n.getUnreadCount)
38
39 r.Group(func(r chi.Router) {
40 r.Use(middleware.AuthMiddleware(n.oauth))
41 r.With(middleware.Paginate).Get("/", n.notificationsPage)
42 r.Get("/preview", n.previewHandler)
43 r.Post("/{id}/read", n.markRead)
44 r.Post("/read-all", n.markAllRead)
45 r.Delete("/{id}", n.deleteNotification)
46 })
47
48 return r
49}
50
51func notificationFilters(r *http.Request, userDid string) (filters []orm.Filter, readFilter, categoryFilter string) {
52 filters = []orm.Filter{orm.FilterEq("recipient_did", userDid)}
53
54 readFilter = r.URL.Query().Get("read")
55 if readFilter != "unread" {
56 readFilter = "inbox"
57 }
58 if readFilter == "unread" {
59 filters = append(filters, orm.FilterEq("read", 0))
60 }
61
62 categoryFilter = r.URL.Query().Get("category")
63 switch categoryFilter {
64 case "social":
65 filters = append(filters, orm.FilterIn("type", models.SocialNotificationTypes))
66 case "work":
67 filters = append(filters, orm.FilterIn("type", models.WorkNotificationTypes))
68 default:
69 categoryFilter = "all"
70 }
71
72 return filters, readFilter, categoryFilter
73}
74
75func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) {
76 l := n.logger.With("handler", "notificationsPage")
77 user := n.oauth.GetMultiAccountUser(r)
78
79 page := pagination.FromContext(r.Context())
80 filters, readFilter, categoryFilter := notificationFilters(r, user.Did)
81
82 // mobile: respects category filter
83 mobileTotal, err := db.CountNotifications(n.db, filters...)
84 if err != nil {
85 l.Error("failed to get total notifications", "err", err)
86 n.pages.Error500(w)
87 return
88 }
89 notifications, err := db.GetNotificationsWithEntities(n.db, page, filters...)
90 if err != nil {
91 l.Error("failed to get notifications", "err", err)
92 n.pages.Error500(w)
93 return
94 }
95
96 // desktop columns: category is fixed, only read filter applies
97 readFilters := []orm.Filter{orm.FilterEq("recipient_did", user.Did)}
98 if readFilter == "unread" {
99 readFilters = append(readFilters, orm.FilterEq("read", 0))
100 }
101 workTotal, err := db.CountNotifications(n.db,
102 append(readFilters, orm.FilterIn("type", models.WorkNotificationTypes))...,
103 )
104 if err != nil {
105 l.Error("failed to count work notifications", "err", err)
106 n.pages.Error500(w)
107 return
108 }
109 workNotifications, err := db.GetNotificationsWithEntities(n.db, page,
110 append(readFilters, orm.FilterIn("type", models.WorkNotificationTypes))...,
111 )
112 if err != nil {
113 l.Error("failed to get work notifications", "err", err)
114 n.pages.Error500(w)
115 return
116 }
117 socialTotal, err := db.CountNotifications(n.db,
118 append(readFilters, orm.FilterIn("type", models.SocialNotificationTypes))...,
119 )
120 if err != nil {
121 l.Error("failed to count social notifications", "err", err)
122 n.pages.Error500(w)
123 return
124 }
125 socialNotifications, err := db.GetNotificationsWithEntities(n.db, page,
126 append(readFilters, orm.FilterIn("type", models.SocialNotificationTypes))...,
127 )
128 if err != nil {
129 l.Error("failed to get social notifications", "err", err)
130 n.pages.Error500(w)
131 return
132 }
133
134 // shared pagination total: max of all relevant counts
135 total := int(max(socialTotal, max(workTotal, mobileTotal)))
136
137 unreadBase := []orm.Filter{
138 orm.FilterEq("recipient_did", user.Did),
139 orm.FilterEq("read", 0),
140 }
141 workUnreadCount, err := db.CountNotifications(n.db,
142 append(unreadBase, orm.FilterIn("type", models.WorkNotificationTypes))...,
143 )
144 if err != nil {
145 l.Error("failed to count work unread", "err", err)
146 }
147 socialUnreadCount, err := db.CountNotifications(n.db,
148 append(unreadBase, orm.FilterIn("type", models.SocialNotificationTypes))...,
149 )
150 if err != nil {
151 l.Error("failed to count social unread", "err", err)
152 }
153
154 err = n.pages.Notifications(w, pages.NotificationsParams{
155 LoggedInUser: user,
156 MobileGroups: pages.GroupNotificationsByDate(notifications),
157 WorkGroups: pages.GroupNotificationsByDate(workNotifications),
158 SocialGroups: pages.GroupNotificationsByDate(socialNotifications),
159 WorkUnreadCount: workUnreadCount,
160 SocialUnreadCount: socialUnreadCount,
161 Page: page,
162 Total: total,
163 ReadFilter: readFilter,
164 CategoryFilter: categoryFilter,
165 })
166 if err != nil {
167 l.Error("failed to render page", "err", err)
168 }
169}
170
171func (n *Notifications) previewHandler(w http.ResponseWriter, r *http.Request) {
172 l := n.logger.With("handler", "previewHandler")
173 user := n.oauth.GetMultiAccountUser(r)
174
175 filters, readFilter, categoryFilter := notificationFilters(r, user.Did)
176
177 notifications, err := db.GetNotificationsWithEntities(
178 n.db,
179 pagination.Page{Limit: 5, Offset: 0},
180 filters...,
181 )
182 if err != nil {
183 l.Error("failed to get notifications", "err", err)
184 n.pages.Error500(w)
185 return
186 }
187
188 err = n.pages.NotificationPreview(w, pages.NotificationPreviewParams{
189 LoggedInUser: user,
190 Notifications: notifications,
191 ReadFilter: readFilter,
192 CategoryFilter: categoryFilter,
193 })
194 if err != nil {
195 l.Error("failed to render notification preview", "err", err)
196 }
197}
198
199func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) {
200 user := n.oauth.GetMultiAccountUser(r)
201 if user == nil {
202 http.Error(w, "Forbidden", http.StatusUnauthorized)
203 return
204 }
205
206 count, err := db.CountNotifications(
207 n.db,
208 orm.FilterEq("recipient_did", user.Did),
209 orm.FilterEq("read", 0),
210 )
211 if err != nil {
212 http.Error(w, "Failed to get unread count", http.StatusInternalServerError)
213 return
214 }
215
216 params := pages.NotificationCountParams{
217 Count: count,
218 }
219 err = n.pages.NotificationCount(w, params)
220 if err != nil {
221 http.Error(w, "Failed to render count", http.StatusInternalServerError)
222 return
223 }
224}
225
226func (n *Notifications) markRead(w http.ResponseWriter, r *http.Request) {
227 userDid := n.oauth.GetDid(r)
228
229 idStr := chi.URLParam(r, "id")
230 notificationID, err := strconv.ParseInt(idStr, 10, 64)
231 if err != nil {
232 http.Error(w, "Invalid notification ID", http.StatusBadRequest)
233 return
234 }
235
236 err = db.MarkNotificationRead(n.db, notificationID, userDid)
237 if err != nil {
238 http.Error(w, "Failed to mark notification as read", http.StatusInternalServerError)
239 return
240 }
241
242 w.WriteHeader(http.StatusNoContent)
243}
244
245func (n *Notifications) markAllRead(w http.ResponseWriter, r *http.Request) {
246 userDid := n.oauth.GetDid(r)
247
248 err := db.MarkAllNotificationsRead(n.db, userDid)
249 if err != nil {
250 http.Error(w, "Failed to mark all notifications as read", http.StatusInternalServerError)
251 return
252 }
253
254 http.Redirect(w, r, "/notifications", http.StatusSeeOther)
255}
256
257func (n *Notifications) deleteNotification(w http.ResponseWriter, r *http.Request) {
258 userDid := n.oauth.GetDid(r)
259
260 idStr := chi.URLParam(r, "id")
261 notificationID, err := strconv.ParseInt(idStr, 10, 64)
262 if err != nil {
263 http.Error(w, "Invalid notification ID", http.StatusBadRequest)
264 return
265 }
266
267 err = db.DeleteNotification(n.db, notificationID, userDid)
268 if err != nil {
269 http.Error(w, "Failed to delete notification", http.StatusInternalServerError)
270 return
271 }
272
273 w.WriteHeader(http.StatusOK)
274}