Monorepo for Tangled tangled.org
2

Configure Feed

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

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.

author
oppiliappan
committer
Lewis
date (May 29, 2026, 2:50 PM +0300) commit 60d5173e parent 6c83413a change-id roqzumzl
+161 -29
+18
appview/models/notifications.go
··· 23 23 NotificationTypeUserMentioned NotificationType = "user_mentioned" 24 24 ) 25 25 26 + var SocialNotificationTypes = []NotificationType{ 27 + NotificationTypeRepoStarred, 28 + NotificationTypeFollowed, 29 + } 30 + 31 + var WorkNotificationTypes = []NotificationType{ 32 + NotificationTypeIssueCreated, 33 + NotificationTypeIssueCommented, 34 + NotificationTypeIssueClosed, 35 + NotificationTypeIssueReopen, 36 + NotificationTypePullCreated, 37 + NotificationTypePullCommented, 38 + NotificationTypePullMerged, 39 + NotificationTypePullClosed, 40 + NotificationTypePullReopen, 41 + NotificationTypeUserMentioned, 42 + } 43 + 26 44 type Notification struct { 27 45 ID int64 28 46 RecipientDid string
+104 -22
appview/notifications/notifications.go
··· 8 8 "github.com/go-chi/chi/v5" 9 9 "tangled.org/core/appview/db" 10 10 "tangled.org/core/appview/middleware" 11 + "tangled.org/core/appview/models" 11 12 "tangled.org/core/appview/oauth" 12 13 "tangled.org/core/appview/pages" 13 14 "tangled.org/core/appview/pagination" ··· 47 48 return r 48 49 } 49 50 51 + func 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 + 50 75 func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) { 51 76 l := n.logger.With("handler", "notificationsPage") 52 77 user := n.oauth.GetMultiAccountUser(r) 53 78 54 79 page := pagination.FromContext(r.Context()) 80 + filters, readFilter, categoryFilter := notificationFilters(r, user.Did) 55 81 56 - total, err := db.CountNotifications( 57 - n.db, 58 - orm.FilterEq("recipient_did", user.Did), 59 - ) 82 + // mobile: respects category filter 83 + mobileTotal, err := db.CountNotifications(n.db, filters...) 60 84 if err != nil { 61 85 l.Error("failed to get total notifications", "err", err) 62 86 n.pages.Error500(w) 63 87 return 64 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 + } 65 95 66 - notifications, err := db.GetNotificationsWithEntities( 67 - n.db, 68 - page, 69 - orm.FilterEq("recipient_did", user.Did), 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))..., 70 103 ) 71 104 if err != nil { 72 - l.Error("failed to get notifications", "err", err) 105 + l.Error("failed to count work notifications", "err", err) 73 106 n.pages.Error500(w) 74 107 return 75 108 } 76 - 77 - err = db.MarkAllNotificationsRead(n.db, user.Did) 109 + workNotifications, err := db.GetNotificationsWithEntities(n.db, page, 110 + append(readFilters, orm.FilterIn("type", models.WorkNotificationTypes))..., 111 + ) 78 112 if err != nil { 79 - l.Error("failed to mark notifications as read", "err", err) 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 80 132 } 81 133 82 - unreadCount := 0 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 + } 83 153 84 - n.pages.Notifications(w, pages.NotificationsParams{ 85 - LoggedInUser: user, 86 - Notifications: notifications, 87 - UnreadCount: unreadCount, 88 - Page: page, 89 - Total: total, 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, 90 165 }) 166 + if err != nil { 167 + l.Error("failed to render page", "err", err) 168 + } 91 169 } 92 170 93 171 func (n *Notifications) previewHandler(w http.ResponseWriter, r *http.Request) { 94 172 l := n.logger.With("handler", "previewHandler") 95 173 user := n.oauth.GetMultiAccountUser(r) 96 174 175 + filters, readFilter, categoryFilter := notificationFilters(r, user.Did) 176 + 97 177 notifications, err := db.GetNotificationsWithEntities( 98 178 n.db, 99 179 pagination.Page{Limit: 5, Offset: 0}, 100 - orm.FilterEq("recipient_did", user.Did), 180 + filters..., 101 181 ) 102 182 if err != nil { 103 183 l.Error("failed to get notifications", "err", err) ··· 106 186 } 107 187 108 188 err = n.pages.NotificationPreview(w, pages.NotificationPreviewParams{ 109 - LoggedInUser: user, 110 - Notifications: notifications, 189 + LoggedInUser: user, 190 + Notifications: notifications, 191 + ReadFilter: readFilter, 192 + CategoryFilter: categoryFilter, 111 193 }) 112 194 if err != nil { 113 195 l.Error("failed to render notification preview", "err", err)
+39 -7
appview/pages/pages.go
··· 445 445 return p.execute("user/settings/profile", w, params) 446 446 } 447 447 448 + type GroupedNotifications struct { 449 + Today []*models.NotificationWithEntity 450 + ThisWeek []*models.NotificationWithEntity 451 + Older []*models.NotificationWithEntity 452 + } 453 + 454 + func GroupNotificationsByDate(notifs []*models.NotificationWithEntity) GroupedNotifications { 455 + now := time.Now() 456 + todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) 457 + weekStart := todayStart.AddDate(0, 0, -6) 458 + 459 + var g GroupedNotifications 460 + for _, n := range notifs { 461 + switch { 462 + case !n.Created.Before(todayStart): 463 + g.Today = append(g.Today, n) 464 + case !n.Created.Before(weekStart): 465 + g.ThisWeek = append(g.ThisWeek, n) 466 + default: 467 + g.Older = append(g.Older, n) 468 + } 469 + } 470 + return g 471 + } 472 + 448 473 type NotificationsParams struct { 449 - LoggedInUser *oauth.MultiAccountUser 450 - Notifications []*models.NotificationWithEntity 451 - UnreadCount int 452 - Page pagination.Page 453 - Total int64 474 + LoggedInUser *oauth.MultiAccountUser 475 + WorkGroups GroupedNotifications 476 + SocialGroups GroupedNotifications 477 + MobileGroups GroupedNotifications 478 + WorkUnreadCount int64 479 + SocialUnreadCount int64 480 + Page pagination.Page 481 + Total int 482 + ReadFilter string // "inbox" or "unread" 483 + CategoryFilter string // "all", "work", "social" 454 484 } 455 485 456 486 func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error { ··· 474 504 } 475 505 476 506 type NotificationPreviewParams struct { 477 - LoggedInUser *oauth.MultiAccountUser 478 - Notifications []*models.NotificationWithEntity 507 + LoggedInUser *oauth.MultiAccountUser 508 + Notifications []*models.NotificationWithEntity 509 + ReadFilter string 510 + CategoryFilter string 479 511 } 480 512 481 513 func (p *Pages) NotificationPreview(w io.Writer, params NotificationPreviewParams) error {