Monorepo for Tangled tangled.org
6

Configure Feed

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

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}