Monorepo for Tangled tangled.org

appview/notifications: handlers for notifs

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

authored by anirudh.fi and committed by anirudh.fi d6808b3a b2857c26

Changed files
+230
appview
notifications
settings
state
+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
··· 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
··· 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 {