From 60d5173ed50168f13b602516362a1a2f958a5aa5 Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Mon, 25 May 2026 15:54:51 +0100 Subject: [PATCH] appview/notifications: sort notifs by work/social and group by timeframe notifs are categorized into work and social notifs, and grouped by three time frames: today, this week and older. this changeset includes all the backend noodling to achieve this. --- appview/models/notifications.go | 18 ++++ appview/notifications/notifications.go | 126 ++++++++++++++++++++----- appview/pages/pages.go | 46 +++++++-- 3 files changed, 161 insertions(+), 29 deletions(-) diff --git a/appview/models/notifications.go b/appview/models/notifications.go index 57b2e747..e5da8f87 100644 --- a/appview/models/notifications.go +++ b/appview/models/notifications.go @@ -23,6 +23,24 @@ const ( NotificationTypeUserMentioned NotificationType = "user_mentioned" ) +var SocialNotificationTypes = []NotificationType{ + NotificationTypeRepoStarred, + NotificationTypeFollowed, +} + +var WorkNotificationTypes = []NotificationType{ + NotificationTypeIssueCreated, + NotificationTypeIssueCommented, + NotificationTypeIssueClosed, + NotificationTypeIssueReopen, + NotificationTypePullCreated, + NotificationTypePullCommented, + NotificationTypePullMerged, + NotificationTypePullClosed, + NotificationTypePullReopen, + NotificationTypeUserMentioned, +} + type Notification struct { ID int64 RecipientDid string diff --git a/appview/notifications/notifications.go b/appview/notifications/notifications.go index 6ddc0a94..7ea04482 100644 --- a/appview/notifications/notifications.go +++ b/appview/notifications/notifications.go @@ -8,6 +8,7 @@ import ( "github.com/go-chi/chi/v5" "tangled.org/core/appview/db" "tangled.org/core/appview/middleware" + "tangled.org/core/appview/models" "tangled.org/core/appview/oauth" "tangled.org/core/appview/pages" "tangled.org/core/appview/pagination" @@ -47,57 +48,136 @@ func (n *Notifications) Router(mw *middleware.Middleware) http.Handler { return r } +func notificationFilters(r *http.Request, userDid string) (filters []orm.Filter, readFilter, categoryFilter string) { + filters = []orm.Filter{orm.FilterEq("recipient_did", userDid)} + + readFilter = r.URL.Query().Get("read") + if readFilter != "unread" { + readFilter = "inbox" + } + if readFilter == "unread" { + filters = append(filters, orm.FilterEq("read", 0)) + } + + categoryFilter = r.URL.Query().Get("category") + switch categoryFilter { + case "social": + filters = append(filters, orm.FilterIn("type", models.SocialNotificationTypes)) + case "work": + filters = append(filters, orm.FilterIn("type", models.WorkNotificationTypes)) + default: + categoryFilter = "all" + } + + return filters, readFilter, categoryFilter +} + func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) { l := n.logger.With("handler", "notificationsPage") user := n.oauth.GetMultiAccountUser(r) page := pagination.FromContext(r.Context()) + filters, readFilter, categoryFilter := notificationFilters(r, user.Did) - total, err := db.CountNotifications( - n.db, - orm.FilterEq("recipient_did", user.Did), - ) + // mobile: respects category filter + mobileTotal, err := db.CountNotifications(n.db, filters...) if err != nil { l.Error("failed to get total notifications", "err", err) n.pages.Error500(w) return } - - notifications, err := db.GetNotificationsWithEntities( - n.db, - page, - orm.FilterEq("recipient_did", user.Did), - ) + notifications, err := db.GetNotificationsWithEntities(n.db, page, filters...) if err != nil { l.Error("failed to get notifications", "err", err) n.pages.Error500(w) return } - err = db.MarkAllNotificationsRead(n.db, user.Did) + // desktop columns: category is fixed, only read filter applies + readFilters := []orm.Filter{orm.FilterEq("recipient_did", user.Did)} + if readFilter == "unread" { + readFilters = append(readFilters, orm.FilterEq("read", 0)) + } + workTotal, err := db.CountNotifications(n.db, + append(readFilters, orm.FilterIn("type", models.WorkNotificationTypes))..., + ) + if err != nil { + l.Error("failed to count work notifications", "err", err) + n.pages.Error500(w) + return + } + workNotifications, err := db.GetNotificationsWithEntities(n.db, page, + append(readFilters, orm.FilterIn("type", models.WorkNotificationTypes))..., + ) if err != nil { - l.Error("failed to mark notifications as read", "err", err) + l.Error("failed to get work notifications", "err", err) + n.pages.Error500(w) + return } + socialTotal, err := db.CountNotifications(n.db, + append(readFilters, orm.FilterIn("type", models.SocialNotificationTypes))..., + ) + if err != nil { + l.Error("failed to count social notifications", "err", err) + n.pages.Error500(w) + return + } + socialNotifications, err := db.GetNotificationsWithEntities(n.db, page, + append(readFilters, orm.FilterIn("type", models.SocialNotificationTypes))..., + ) + if err != nil { + l.Error("failed to get social notifications", "err", err) + n.pages.Error500(w) + return + } + + // shared pagination total: max of all relevant counts + total := int(max(socialTotal, max(workTotal, mobileTotal))) - unreadCount := 0 + unreadBase := []orm.Filter{ + orm.FilterEq("recipient_did", user.Did), + orm.FilterEq("read", 0), + } + workUnreadCount, err := db.CountNotifications(n.db, + append(unreadBase, orm.FilterIn("type", models.WorkNotificationTypes))..., + ) + if err != nil { + l.Error("failed to count work unread", "err", err) + } + socialUnreadCount, err := db.CountNotifications(n.db, + append(unreadBase, orm.FilterIn("type", models.SocialNotificationTypes))..., + ) + if err != nil { + l.Error("failed to count social unread", "err", err) + } - n.pages.Notifications(w, pages.NotificationsParams{ - LoggedInUser: user, - Notifications: notifications, - UnreadCount: unreadCount, - Page: page, - Total: total, + err = n.pages.Notifications(w, pages.NotificationsParams{ + LoggedInUser: user, + MobileGroups: pages.GroupNotificationsByDate(notifications), + WorkGroups: pages.GroupNotificationsByDate(workNotifications), + SocialGroups: pages.GroupNotificationsByDate(socialNotifications), + WorkUnreadCount: workUnreadCount, + SocialUnreadCount: socialUnreadCount, + Page: page, + Total: total, + ReadFilter: readFilter, + CategoryFilter: categoryFilter, }) + if err != nil { + l.Error("failed to render page", "err", err) + } } func (n *Notifications) previewHandler(w http.ResponseWriter, r *http.Request) { l := n.logger.With("handler", "previewHandler") user := n.oauth.GetMultiAccountUser(r) + filters, readFilter, categoryFilter := notificationFilters(r, user.Did) + notifications, err := db.GetNotificationsWithEntities( n.db, pagination.Page{Limit: 5, Offset: 0}, - orm.FilterEq("recipient_did", user.Did), + filters..., ) if err != nil { l.Error("failed to get notifications", "err", err) @@ -106,8 +186,10 @@ func (n *Notifications) previewHandler(w http.ResponseWriter, r *http.Request) { } err = n.pages.NotificationPreview(w, pages.NotificationPreviewParams{ - LoggedInUser: user, - Notifications: notifications, + LoggedInUser: user, + Notifications: notifications, + ReadFilter: readFilter, + CategoryFilter: categoryFilter, }) if err != nil { l.Error("failed to render notification preview", "err", err) diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 35967655..2edcc664 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -445,12 +445,42 @@ func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParam return p.execute("user/settings/profile", w, params) } +type GroupedNotifications struct { + Today []*models.NotificationWithEntity + ThisWeek []*models.NotificationWithEntity + Older []*models.NotificationWithEntity +} + +func GroupNotificationsByDate(notifs []*models.NotificationWithEntity) GroupedNotifications { + now := time.Now() + todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + weekStart := todayStart.AddDate(0, 0, -6) + + var g GroupedNotifications + for _, n := range notifs { + switch { + case !n.Created.Before(todayStart): + g.Today = append(g.Today, n) + case !n.Created.Before(weekStart): + g.ThisWeek = append(g.ThisWeek, n) + default: + g.Older = append(g.Older, n) + } + } + return g +} + type NotificationsParams struct { - LoggedInUser *oauth.MultiAccountUser - Notifications []*models.NotificationWithEntity - UnreadCount int - Page pagination.Page - Total int64 + LoggedInUser *oauth.MultiAccountUser + WorkGroups GroupedNotifications + SocialGroups GroupedNotifications + MobileGroups GroupedNotifications + WorkUnreadCount int64 + SocialUnreadCount int64 + Page pagination.Page + Total int + ReadFilter string // "inbox" or "unread" + CategoryFilter string // "all", "work", "social" } func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error { @@ -474,8 +504,10 @@ func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) e } type NotificationPreviewParams struct { - LoggedInUser *oauth.MultiAccountUser - Notifications []*models.NotificationWithEntity + LoggedInUser *oauth.MultiAccountUser + Notifications []*models.NotificationWithEntity + ReadFilter string + CategoryFilter string } func (p *Pages) NotificationPreview(w io.Writer, params NotificationPreviewParams) error { -- 2.51.2