Monorepo for Tangled
Something went wrong. Try again.
9.2 kB · 316 lines
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317package notifications
import ( "log/slog" "net/http" "strconv"
"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" "tangled.org/core/orm")
type Notifications struct { db *db.DB oauth *oauth.OAuth pages *pages.Pages logger *slog.Logger}
func New(database *db.DB, oauthHandler *oauth.OAuth, pagesHandler *pages.Pages, logger *slog.Logger) *Notifications { return &Notifications{ db: database, oauth: oauthHandler, pages: pagesHandler, logger: logger, }}
func (n *Notifications) Router(mw *middleware.Middleware) http.Handler { r := chi.NewRouter()
r.Get("/count", n.getUnreadCount)
r.Group(func(r chi.Router) { r.Use(middleware.AuthMiddleware(n.oauth)) r.With(middleware.Paginate).Get("/", n.notificationsPage) r.Get("/preview", n.previewHandler) r.Post("/{id}/read", n.markRead) r.Post("/{id}/unread", n.markUnread) r.Post("/read-all", n.markAllRead) r.Delete("/{id}", n.deleteNotification) })
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)
// 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, filters...) if err != nil { l.Error("failed to get notifications", "err", err) n.pages.Error500(w) return }
// 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 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)))
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) }
focusCount, err := db.CountFocusNotifs(n.db, user.Did) if err != nil { l.Error("failed to count focus notifs", "err", err) }
err = n.pages.Notifications(w, pages.NotificationsParams{ BaseParams: pages.BaseParamsFromContext(r.Context()), MobileGroups: pages.GroupNotificationsByDate(notifications), WorkGroups: pages.GroupNotificationsByDate(workNotifications), SocialGroups: pages.GroupNotificationsByDate(socialNotifications), WorkUnreadCount: workUnreadCount, SocialUnreadCount: socialUnreadCount, Page: page, Total: total, ReadFilter: readFilter, CategoryFilter: categoryFilter, CanFocus: focusCount > 1, }) 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}, filters..., ) if err != nil { l.Error("failed to get notifications", "err", err) n.pages.Error500(w) return }
focusCount, err := db.CountFocusNotifs(n.db, user.Did) if err != nil { l.Error("failed to count focus notifs", "err", err) }
err = n.pages.NotificationPreview(w, pages.NotificationPreviewParams{ BaseParams: pages.BaseParamsFromContext(r.Context()), Notifications: notifications, ReadFilter: readFilter, CategoryFilter: categoryFilter, CanFocus: focusCount > 1, }) if err != nil { l.Error("failed to render notification preview", "err", err) }}
func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) { user := n.oauth.GetMultiAccountUser(r) if user == nil { http.Error(w, "Forbidden", http.StatusUnauthorized) return }
count, err := db.CountNotifications( n.db, orm.FilterEq("recipient_did", user.Did), orm.FilterEq("read", 0), ) 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) { n.toggleRead(w, r, true)}
func (n *Notifications) markUnread(w http.ResponseWriter, r *http.Request) { n.toggleRead(w, r, false)}
func (n *Notifications) toggleRead(w http.ResponseWriter, r *http.Request, read bool) { l := n.logger.With("handler", "toggleRead") 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 }
if read { err = db.MarkNotificationRead(n.db, notificationID, userDid) } else { err = db.MarkNotificationUnread(n.db, notificationID, userDid) } if err != nil { n.logger.Error("failed to toggle notification read state", "err", err) http.Error(w, "Failed to update notification", http.StatusInternalServerError) return }
// if called via HTMX (has HX-Request header), return the updated item fragment if r.Header.Get("HX-Request") == "true" { notif, err := db.GetNotificationWithEntity(n.db, notificationID, userDid) if err != nil { l.Error("failed to fetch notification after toggle", "err", err) http.Error(w, "Failed to fetch notification", http.StatusInternalServerError) return } if err := n.pages.NotificationItem(w, notif); err != nil { l.Error("failed to render notification item", "err", err) } return }
w.WriteHeader(http.StatusNoContent)}
func (n *Notifications) markAllRead(w http.ResponseWriter, r *http.Request) { userDid := n.oauth.GetDid(r)
err := db.MarkAllNotificationsRead(n.db, userDid) if err != nil { n.logger.Error("failed to mark all notifications as read", "err", err) http.Error(w, "Failed to mark all notifications as read", http.StatusInternalServerError) return }
w.WriteHeader(http.StatusOK)}
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 = db.DeleteNotification(n.db, notificationID, userDid) if err != nil { http.Error(w, "Failed to delete notification", http.StatusInternalServerError) return }
w.WriteHeader(http.StatusOK)}