From d6808b3adbef982dbfc49f812130f49bcea84fba Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Mon, 15 Sep 2025 16:58:32 +0300 Subject: [PATCH] appview/notifications: handlers for notifs Change-Id: lmsyxqmrlllnlnykkwpvxzzpyurstnsv Signed-off-by: Anirudh Oppiliappan --- appview/notifications/notifications.go | 173 +++++++++++++++++++++++++ appview/settings/settings.go | 51 ++++++++ appview/state/router.go | 6 + 3 files changed, 230 insertions(+) create mode 100644 appview/notifications/notifications.go diff --git a/appview/notifications/notifications.go b/appview/notifications/notifications.go new file mode 100644 index 00000000..8ba1c8c7 --- /dev/null +++ b/appview/notifications/notifications.go @@ -0,0 +1,173 @@ +package notifications + +import ( + "log" + "net/http" + "strconv" + + "github.com/go-chi/chi/v5" + "tangled.org/core/appview/db" + "tangled.org/core/appview/middleware" + "tangled.org/core/appview/oauth" + "tangled.org/core/appview/pages" +) + +type Notifications struct { + db *db.DB + oauth *oauth.OAuth + pages *pages.Pages +} + +func New(database *db.DB, oauthHandler *oauth.OAuth, pagesHandler *pages.Pages) *Notifications { + return &Notifications{ + db: database, + oauth: oauthHandler, + pages: pagesHandler, + } +} + +func (n *Notifications) Router(mw *middleware.Middleware) http.Handler { + r := chi.NewRouter() + + r.Use(middleware.AuthMiddleware(n.oauth)) + + r.Get("/", n.notificationsPage) + + r.Get("/count", n.getUnreadCount) + r.Post("/{id}/read", n.markRead) + r.Post("/read-all", n.markAllRead) + r.Delete("/{id}", n.deleteNotification) + + return r +} + +func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) { + userDid := n.oauth.GetDid(r) + + limitStr := r.URL.Query().Get("limit") + offsetStr := r.URL.Query().Get("offset") + + limit := 20 // default + if limitStr != "" { + if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 { + limit = l + } + } + + offset := 0 // default + if offsetStr != "" { + if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 { + offset = o + } + } + + notifications, err := n.db.GetNotificationsWithEntities(r.Context(), userDid, limit+1, offset) + if err != nil { + log.Println("failed to get notifications:", err) + n.pages.Error500(w) + return + } + + hasMore := len(notifications) > limit + if hasMore { + notifications = notifications[:limit] + } + + err = n.db.MarkAllNotificationsRead(r.Context(), userDid) + if err != nil { + log.Println("failed to mark notifications as read:", err) + } + + unreadCount := 0 + + user := n.oauth.GetUser(r) + if user == nil { + http.Error(w, "Failed to get user", http.StatusInternalServerError) + return + } + + params := pages.NotificationsParams{ + LoggedInUser: user, + Notifications: notifications, + UnreadCount: unreadCount, + HasMore: hasMore, + NextOffset: offset + limit, + Limit: limit, + } + + err = n.pages.Notifications(w, params) + if err != nil { + log.Println("failed to load notifs:", err) + n.pages.Error500(w) + return + } +} + +func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) { + userDid := n.oauth.GetDid(r) + + count, err := n.db.GetUnreadNotificationCount(r.Context(), userDid) + if err != nil { + http.Error(w, "Failed to get unread count", http.StatusInternalServerError) + return + } + + params := pages.NotificationCountParams{ + Count: count, + } + err = n.pages.NotificationCount(w, params) + if err != nil { + http.Error(w, "Failed to render count", http.StatusInternalServerError) + return + } +} + +func (n *Notifications) markRead(w http.ResponseWriter, r *http.Request) { + userDid := n.oauth.GetDid(r) + + idStr := chi.URLParam(r, "id") + notificationID, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + http.Error(w, "Invalid notification ID", http.StatusBadRequest) + return + } + + err = n.db.MarkNotificationRead(r.Context(), notificationID, userDid) + if err != nil { + http.Error(w, "Failed to mark notification as read", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func (n *Notifications) markAllRead(w http.ResponseWriter, r *http.Request) { + userDid := n.oauth.GetDid(r) + + err := n.db.MarkAllNotificationsRead(r.Context(), userDid) + if err != nil { + http.Error(w, "Failed to mark all notifications as read", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/notifications", http.StatusSeeOther) +} + +func (n *Notifications) deleteNotification(w http.ResponseWriter, r *http.Request) { + userDid := n.oauth.GetDid(r) + + idStr := chi.URLParam(r, "id") + notificationID, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + http.Error(w, "Invalid notification ID", http.StatusBadRequest) + return + } + + err = n.db.DeleteNotification(r.Context(), notificationID, userDid) + if err != nil { + http.Error(w, "Failed to delete notification", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/appview/settings/settings.go b/appview/settings/settings.go index 3a9684ec..00b6bda4 100644 --- a/appview/settings/settings.go +++ b/appview/settings/settings.go @@ -41,6 +41,7 @@ var ( {"Name": "profile", "Icon": "user"}, {"Name": "keys", "Icon": "key"}, {"Name": "emails", "Icon": "mail"}, + {"Name": "notifications", "Icon": "bell"}, } ) @@ -68,6 +69,11 @@ func (s *Settings) Router() http.Handler { r.Post("/primary", s.emailsPrimary) }) + r.Route("/notifications", func(r chi.Router) { + r.Get("/", s.notificationsSettings) + r.Put("/", s.updateNotificationPreferences) + }) + return r } @@ -81,6 +87,51 @@ func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) { }) } +func (s *Settings) notificationsSettings(w http.ResponseWriter, r *http.Request) { + user := s.OAuth.GetUser(r) + did := s.OAuth.GetDid(r) + + prefs, err := s.Db.GetNotificationPreferences(r.Context(), did) + if err != nil { + log.Printf("failed to get notification preferences: %s", err) + s.Pages.Notice(w, "settings-notifications-error", "Unable to load notification preferences.") + return + } + + s.Pages.UserNotificationSettings(w, pages.UserNotificationSettingsParams{ + LoggedInUser: user, + Preferences: prefs, + Tabs: settingsTabs, + Tab: "notifications", + }) +} + +func (s *Settings) updateNotificationPreferences(w http.ResponseWriter, r *http.Request) { + did := s.OAuth.GetDid(r) + + prefs := &models.NotificationPreferences{ + UserDid: did, + RepoStarred: r.FormValue("repo_starred") == "on", + IssueCreated: r.FormValue("issue_created") == "on", + IssueCommented: r.FormValue("issue_commented") == "on", + IssueClosed: r.FormValue("issue_closed") == "on", + PullCreated: r.FormValue("pull_created") == "on", + PullCommented: r.FormValue("pull_commented") == "on", + PullMerged: r.FormValue("pull_merged") == "on", + Followed: r.FormValue("followed") == "on", + EmailNotifications: r.FormValue("email_notifications") == "on", + } + + err := s.Db.UpdateNotificationPreferences(r.Context(), prefs) + if err != nil { + log.Printf("failed to update notification preferences: %s", err) + s.Pages.Notice(w, "settings-notifications-error", "Unable to save notification preferences.") + return + } + + s.Pages.Notice(w, "settings-notifications-success", "Notification preferences saved successfully.") +} + func (s *Settings) keysSettings(w http.ResponseWriter, r *http.Request) { user := s.OAuth.GetUser(r) pubKeys, err := db.GetPublicKeysForDid(s.Db, user.Did) diff --git a/appview/state/router.go b/appview/state/router.go index d8a5de06..aa57720c 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -10,6 +10,7 @@ import ( "tangled.org/core/appview/knots" "tangled.org/core/appview/labels" "tangled.org/core/appview/middleware" + "tangled.org/core/appview/notifications" oauthhandler "tangled.org/core/appview/oauth/handler" "tangled.org/core/appview/pipelines" "tangled.org/core/appview/pulls" @@ -276,6 +277,11 @@ func (s *State) LabelsRouter(mw *middleware.Middleware) http.Handler { return ls.Router(mw) } +func (s *State) NotificationsRouter(mw *middleware.Middleware) http.Handler { + notifs := notifications.New(s.db, s.oauth, s.pages) + return notifs.Router(mw) +} + func (s *State) SignupRouter() http.Handler { logger := log.New("signup") -- 2.43.0