Monorepo for Tangled tangled.org
5

Configure Feed

Select the types of activity you want to include in your feed.

at icy/lqyotq 9.0 kB View raw
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("/{id}/unread", n.markUnread) 45 r.Post("/read-all", n.markAllRead) 46 r.Delete("/{id}", n.deleteNotification) 47 }) 48 49 return r 50} 51 52func notificationFilters(r *http.Request, userDid string) (filters []orm.Filter, readFilter, categoryFilter string) { 53 filters = []orm.Filter{orm.FilterEq("recipient_did", userDid)} 54 55 readFilter = r.URL.Query().Get("read") 56 if readFilter != "unread" { 57 readFilter = "inbox" 58 } 59 if readFilter == "unread" { 60 filters = append(filters, orm.FilterEq("read", 0)) 61 } 62 63 categoryFilter = r.URL.Query().Get("category") 64 switch categoryFilter { 65 case "social": 66 filters = append(filters, orm.FilterIn("type", models.SocialNotificationTypes)) 67 case "work": 68 filters = append(filters, orm.FilterIn("type", models.WorkNotificationTypes)) 69 default: 70 categoryFilter = "all" 71 } 72 73 return filters, readFilter, categoryFilter 74} 75 76func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) { 77 l := n.logger.With("handler", "notificationsPage") 78 user := n.oauth.GetMultiAccountUser(r) 79 80 page := pagination.FromContext(r.Context()) 81 filters, readFilter, categoryFilter := notificationFilters(r, user.Did) 82 83 // mobile: respects category filter 84 mobileTotal, err := db.CountNotifications(n.db, filters...) 85 if err != nil { 86 l.Error("failed to get total notifications", "err", err) 87 n.pages.Error500(w) 88 return 89 } 90 notifications, err := db.GetNotificationsWithEntities(n.db, page, filters...) 91 if err != nil { 92 l.Error("failed to get notifications", "err", err) 93 n.pages.Error500(w) 94 return 95 } 96 97 // desktop columns: category is fixed, only read filter applies 98 readFilters := []orm.Filter{orm.FilterEq("recipient_did", user.Did)} 99 if readFilter == "unread" { 100 readFilters = append(readFilters, orm.FilterEq("read", 0)) 101 } 102 workTotal, err := db.CountNotifications(n.db, 103 append(readFilters, orm.FilterIn("type", models.WorkNotificationTypes))..., 104 ) 105 if err != nil { 106 l.Error("failed to count work notifications", "err", err) 107 n.pages.Error500(w) 108 return 109 } 110 workNotifications, err := db.GetNotificationsWithEntities(n.db, page, 111 append(readFilters, orm.FilterIn("type", models.WorkNotificationTypes))..., 112 ) 113 if err != nil { 114 l.Error("failed to get work notifications", "err", err) 115 n.pages.Error500(w) 116 return 117 } 118 socialTotal, err := db.CountNotifications(n.db, 119 append(readFilters, orm.FilterIn("type", models.SocialNotificationTypes))..., 120 ) 121 if err != nil { 122 l.Error("failed to count social notifications", "err", err) 123 n.pages.Error500(w) 124 return 125 } 126 socialNotifications, err := db.GetNotificationsWithEntities(n.db, page, 127 append(readFilters, orm.FilterIn("type", models.SocialNotificationTypes))..., 128 ) 129 if err != nil { 130 l.Error("failed to get social notifications", "err", err) 131 n.pages.Error500(w) 132 return 133 } 134 135 // shared pagination total: max of all relevant counts 136 total := int(max(socialTotal, max(workTotal, mobileTotal))) 137 138 unreadBase := []orm.Filter{ 139 orm.FilterEq("recipient_did", user.Did), 140 orm.FilterEq("read", 0), 141 } 142 workUnreadCount, err := db.CountNotifications(n.db, 143 append(unreadBase, orm.FilterIn("type", models.WorkNotificationTypes))..., 144 ) 145 if err != nil { 146 l.Error("failed to count work unread", "err", err) 147 } 148 socialUnreadCount, err := db.CountNotifications(n.db, 149 append(unreadBase, orm.FilterIn("type", models.SocialNotificationTypes))..., 150 ) 151 if err != nil { 152 l.Error("failed to count social unread", "err", err) 153 } 154 155 focusCount, err := db.CountFocusNotifs(n.db, user.Did) 156 if err != nil { 157 l.Error("failed to count focus notifs", "err", err) 158 } 159 160 err = n.pages.Notifications(w, pages.NotificationsParams{ 161 BaseParams: pages.BaseParamsFromContext(r.Context()), 162 MobileGroups: pages.GroupNotificationsByDate(notifications), 163 WorkGroups: pages.GroupNotificationsByDate(workNotifications), 164 SocialGroups: pages.GroupNotificationsByDate(socialNotifications), 165 WorkUnreadCount: workUnreadCount, 166 SocialUnreadCount: socialUnreadCount, 167 Page: page, 168 Total: total, 169 ReadFilter: readFilter, 170 CategoryFilter: categoryFilter, 171 CanFocus: focusCount > 1, 172 }) 173 if err != nil { 174 l.Error("failed to render page", "err", err) 175 } 176} 177 178func (n *Notifications) previewHandler(w http.ResponseWriter, r *http.Request) { 179 l := n.logger.With("handler", "previewHandler") 180 user := n.oauth.GetMultiAccountUser(r) 181 182 filters, readFilter, categoryFilter := notificationFilters(r, user.Did) 183 184 notifications, err := db.GetNotificationsWithEntities( 185 n.db, 186 pagination.Page{Limit: 5, Offset: 0}, 187 filters..., 188 ) 189 if err != nil { 190 l.Error("failed to get notifications", "err", err) 191 n.pages.Error500(w) 192 return 193 } 194 195 focusCount, err := db.CountFocusNotifs(n.db, user.Did) 196 if err != nil { 197 l.Error("failed to count focus notifs", "err", err) 198 } 199 200 err = n.pages.NotificationPreview(w, pages.NotificationPreviewParams{ 201 BaseParams: pages.BaseParamsFromContext(r.Context()), 202 Notifications: notifications, 203 ReadFilter: readFilter, 204 CategoryFilter: categoryFilter, 205 CanFocus: focusCount > 1, 206 }) 207 if err != nil { 208 l.Error("failed to render notification preview", "err", err) 209 } 210} 211 212func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) { 213 user := n.oauth.GetMultiAccountUser(r) 214 if user == nil { 215 http.Error(w, "Forbidden", http.StatusUnauthorized) 216 return 217 } 218 219 count, err := db.CountNotifications( 220 n.db, 221 orm.FilterEq("recipient_did", user.Did), 222 orm.FilterEq("read", 0), 223 ) 224 if err != nil { 225 http.Error(w, "Failed to get unread count", http.StatusInternalServerError) 226 return 227 } 228 229 params := pages.NotificationCountParams{ 230 Count: count, 231 } 232 err = n.pages.NotificationCount(w, params) 233 if err != nil { 234 http.Error(w, "Failed to render count", http.StatusInternalServerError) 235 return 236 } 237} 238 239func (n *Notifications) markRead(w http.ResponseWriter, r *http.Request) { 240 n.toggleRead(w, r, true) 241} 242 243func (n *Notifications) markUnread(w http.ResponseWriter, r *http.Request) { 244 n.toggleRead(w, r, false) 245} 246 247func (n *Notifications) toggleRead(w http.ResponseWriter, r *http.Request, read bool) { 248 l := n.logger.With("handler", "toggleRead") 249 userDid := n.oauth.GetDid(r) 250 251 idStr := chi.URLParam(r, "id") 252 notificationID, err := strconv.ParseInt(idStr, 10, 64) 253 if err != nil { 254 http.Error(w, "Invalid notification ID", http.StatusBadRequest) 255 return 256 } 257 258 if read { 259 err = db.MarkNotificationRead(n.db, notificationID, userDid) 260 } else { 261 err = db.MarkNotificationUnread(n.db, notificationID, userDid) 262 } 263 if err != nil { 264 http.Error(w, "Failed to update notification", http.StatusInternalServerError) 265 return 266 } 267 268 // if called via HTMX (has HX-Request header), return the updated item fragment 269 if r.Header.Get("HX-Request") == "true" { 270 notif, err := db.GetNotificationWithEntity(n.db, notificationID, userDid) 271 if err != nil { 272 l.Error("failed to fetch notification after toggle", "err", err) 273 http.Error(w, "Failed to fetch notification", http.StatusInternalServerError) 274 return 275 } 276 if err := n.pages.NotificationItem(w, notif); err != nil { 277 l.Error("failed to render notification item", "err", err) 278 } 279 return 280 } 281 282 w.WriteHeader(http.StatusNoContent) 283} 284 285func (n *Notifications) markAllRead(w http.ResponseWriter, r *http.Request) { 286 userDid := n.oauth.GetDid(r) 287 288 err := db.MarkAllNotificationsRead(n.db, userDid) 289 if err != nil { 290 http.Error(w, "Failed to mark all notifications as read", http.StatusInternalServerError) 291 return 292 } 293 294 w.WriteHeader(http.StatusOK) 295} 296 297func (n *Notifications) deleteNotification(w http.ResponseWriter, r *http.Request) { 298 userDid := n.oauth.GetDid(r) 299 300 idStr := chi.URLParam(r, "id") 301 notificationID, err := strconv.ParseInt(idStr, 10, 64) 302 if err != nil { 303 http.Error(w, "Invalid notification ID", http.StatusBadRequest) 304 return 305 } 306 307 err = db.DeleteNotification(n.db, notificationID, userDid) 308 if err != nil { 309 http.Error(w, "Failed to delete notification", http.StatusInternalServerError) 310 return 311 } 312 313 w.WriteHeader(http.StatusOK) 314}