Yōten: A social tracker for your language learning journey built on the atproto.

feat: add notification route handlers

brookjeynes.dev 63e15018 9c8f8954

verified
+127
+84
internal/server/handlers/notification.go
··· 1 + package handlers 2 + 3 + import ( 4 + "log" 5 + "net/http" 6 + "strconv" 7 + 8 + "yoten.app/internal/clients/bsky" 9 + "yoten.app/internal/db" 10 + "yoten.app/internal/server/htmx" 11 + "yoten.app/internal/server/views/partials" 12 + ) 13 + 14 + func (h *Handler) HandleNotificationFeed(w http.ResponseWriter, r *http.Request) { 15 + user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 16 + if err != nil { 17 + log.Println("failed to get logged-in user:", err) 18 + htmx.HxRedirect(w, "/login") 19 + return 20 + } 21 + 22 + pageStr := r.URL.Query().Get("page") 23 + if pageStr == "" { 24 + pageStr = "1" 25 + } 26 + page, err := strconv.ParseInt(pageStr, 10, 64) 27 + if err != nil { 28 + log.Println("failed to parse page value:", err) 29 + page = 1 30 + } 31 + if page == 0 { 32 + page = 1 33 + } 34 + 35 + const pageSize = 10 36 + offset := (page - 1) * pageSize 37 + 38 + switch r.Method { 39 + case http.MethodGet: 40 + notifications, err := db.GetNotificationsByDid(h.Db, user.Did, pageSize+1, int(offset)) 41 + if err != nil { 42 + log.Println("failed to retrieve notifications:", err) 43 + htmx.HxError(w, http.StatusInternalServerError, "Failed to get notifications, try again later.") 44 + return 45 + } 46 + 47 + hydratedNotifications, err := h.getBskyProfileHydratedNotificationFeed(notifications) 48 + if err != nil { 49 + log.Println("failed to hydrate notifications with bsky profile:", err) 50 + htmx.HxError(w, http.StatusInternalServerError, "Failed to get notifications, try again later.") 51 + return 52 + } 53 + 54 + nextPage := 0 55 + if len(hydratedNotifications) > pageSize { 56 + nextPage = int(page + 1) 57 + hydratedNotifications = hydratedNotifications[:pageSize] 58 + } 59 + 60 + partials.NotificationFeed(partials.NotificationFeedProps{ 61 + User: user, 62 + Feed: hydratedNotifications, 63 + NextPage: nextPage, 64 + }).Render(r.Context(), w) 65 + } 66 + } 67 + 68 + func (h *Handler) HandleNotificationMarkAllRead(w http.ResponseWriter, r *http.Request) { 69 + user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 70 + if err != nil { 71 + log.Println("failed to get logged-in user:", err) 72 + htmx.HxRedirect(w, "/login") 73 + return 74 + } 75 + 76 + err = db.MarkAllNotificationsAsRead(h.Db, user.Did) 77 + if err != nil { 78 + log.Println("failed to mark all notifications:", err) 79 + htmx.HxError(w, http.StatusInternalServerError, "Failed to mark all notifications as read, try again later.") 80 + return 81 + } 82 + 83 + htmx.HxRedirect(w, "/profile/notifications") 84 + }
+36
internal/server/handlers/profile.go
··· 584 584 }).Render(r.Context(), w) 585 585 } 586 586 } 587 + 588 + func (h *Handler) HandleNotificationsPage(w http.ResponseWriter, r *http.Request) { 589 + user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 590 + if err != nil { 591 + log.Println("failed to get logged-in user:", err) 592 + htmx.HxRedirect(w, "/login") 593 + return 594 + } 595 + 596 + views.NotificationsPage(views.NotificationsPageParams{ 597 + User: user, 598 + }).Render(r.Context(), w) 599 + } 600 + 601 + func (h *Handler) getBskyProfileHydratedNotificationFeed(feed []db.Notification) ([]db.NotificationWithBskyHandle, error) { 602 + hydratedFeed := []db.NotificationWithBskyHandle{} 603 + 604 + profileDids := utils.Map(feed, func(item db.Notification) string { 605 + return item.ActorDid 606 + }) 607 + bskyProfiles, err := bsky.GetBskyProfiles(profileDids) 608 + if err != nil { 609 + return hydratedFeed, err 610 + } 611 + 612 + for i := range feed { 613 + if profile, ok := bskyProfiles[feed[i].ActorDid]; ok { 614 + hydratedFeed = append(hydratedFeed, db.NotificationWithBskyHandle{ 615 + Notification: feed[i], 616 + ActorBskyHandle: profile.Handle, 617 + }) 618 + } 619 + } 620 + 621 + return hydratedFeed, nil 622 + }
+7
internal/server/handlers/router.go
··· 62 62 r.Post("/edit", h.HandleEditProfilePage) 63 63 r.Get("/activities", h.HandleActivitiesPage) 64 64 r.Get("/resources", h.HandleResourcesPage) 65 + r.Get("/notifications", h.HandleNotificationsPage) 65 66 }) 66 67 67 68 r.Route("/follow", func(r chi.Router) { 68 69 r.Use(middleware.AuthMiddleware(h.Oauth)) 69 70 r.Post("/", h.HandleFollow) 70 71 r.Delete("/", h.HandleFollow) 72 + }) 73 + 74 + r.Route("/notification", func(r chi.Router) { 75 + r.Use(middleware.AuthMiddleware(h.Oauth)) 76 + r.Get("/feed", h.HandleNotificationFeed) 77 + r.Post("/mark-all-read", h.HandleNotificationMarkAllRead) 71 78 }) 72 79 73 80 r.Route("/resource", func(r chi.Router) {