nandi/gleanpublic⑂ Fork 0
⑂ ef3d40a
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Add new articles count badge with pollingUnverified

Julien Robert committed 2026-04-21T12:58:30+02:00 Browse files
ef3d40a parent: dd385c8
modified internal/db/article.go +12 -0
@@ -3,6 +3,7 @@ package db
33 import (
44 "context"
55 "database/sql"
6+ "time"
67 )
78
89 type Article struct {
@@ -281,3 +282,14 @@ func (db *DB) GetArticleByURL(ctx context.Context, url string) (*Article, error)
281282 }
282283 return a, nil
283284 }
285+
286+func (db *DB) CountNewArticles(ctx context.Context, userDID string, since time.Time) (int, error) {
287+ var count int
288+ err := db.QueryRowContext(ctx, `
289+ SELECT COUNT(*)
290+ FROM articles a
291+ JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ?
292+ WHERE a.fetched_at > ?
293+ `, userDID, since).Scan(&count)
294+ return count, err
295+}
@@ -3,6 +3,7 @@ package db
3 import (3 import (
4 "context"4 "context"
5 "database/sql"5 "database/sql"
6+ "time"
6 )7 )
7 8
8 type Article struct {9 type Article struct {
@@ -281,3 +282,14 @@ func (db *DB) GetArticleByURL(ctx context.Context, url string) (*Article, error)
281 }282 }
282 return a, nil283 return a, nil
283 }284 }
285+
286+func (db *DB) CountNewArticles(ctx context.Context, userDID string, since time.Time) (int, error) {
287+ var count int
288+ err := db.QueryRowContext(ctx, `
289+ SELECT COUNT(*)
290+ FROM articles a
291+ JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ?
292+ WHERE a.fetched_at > ?
293+ `, userDID, since).Scan(&count)
294+ return count, err
295+}
modified internal/server/articles_handler.go +31 -0
@@ -65,9 +65,40 @@ func (s *Server) handleArticles(w http.ResponseWriter, r *http.Request) {
6565 "Page": page,
6666 "BaseURL": "/articles",
6767 "QueryParams": buildQueryParams(map[string]string{"feed": feedURL, "status": status}),
68+ "Now": time.Now(),
6869 })
6970 }
7071
72+func (s *Server) handleNewArticleCount(w http.ResponseWriter, r *http.Request) {
73+ user := currentUser(r)
74+ sinceUnix, err := strconv.ParseInt(r.URL.Query().Get("since"), 10, 64)
75+ if err != nil {
76+ w.WriteHeader(http.StatusBadRequest)
77+ return
78+ }
79+ since := time.Unix(sinceUnix, 0)
80+
81+ count, err := s.db.CountNewArticles(r.Context(), user.DID, since)
82+ if err != nil {
83+ w.WriteHeader(http.StatusInternalServerError)
84+ return
85+ }
86+
87+ w.Header().Set("Content-Type", "text/html")
88+ if count == 0 {
89+ w.Write([]byte(""))
90+ return
91+ }
92+ fmt.Fprintf(w, `<div id="new-articles-banner" class="bg-spot-green rounded-xl px-5 py-3 flex items-center justify-between mb-4"><span class="text-sm text-white font-medium">%d new article%s available.</span><a href="%s" class="text-sm font-bold text-white uppercase tracking-button hover:underline transition">Refresh</a></div>`, count, pluralS(count), r.URL.Query().Get("return"))
93+}
94+
95+func pluralS(n int) string {
96+ if n != 1 {
97+ return "s"
98+ }
99+ return ""
100+}
101+
71102 func (s *Server) handleArticleDetail(w http.ResponseWriter, r *http.Request) {
72103 user := currentUser(r)
73104 id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
@@ -65,9 +65,40 @@ func (s *Server) handleArticles(w http.ResponseWriter, r *http.Request) {
65 "Page": page,65 "Page": page,
66 "BaseURL": "/articles",66 "BaseURL": "/articles",
67 "QueryParams": buildQueryParams(map[string]string{"feed": feedURL, "status": status}),67 "QueryParams": buildQueryParams(map[string]string{"feed": feedURL, "status": status}),
68+ "Now": time.Now(),
68 })69 })
69 }70 }
70 71
72+func (s *Server) handleNewArticleCount(w http.ResponseWriter, r *http.Request) {
73+ user := currentUser(r)
74+ sinceUnix, err := strconv.ParseInt(r.URL.Query().Get("since"), 10, 64)
75+ if err != nil {
76+ w.WriteHeader(http.StatusBadRequest)
77+ return
78+ }
79+ since := time.Unix(sinceUnix, 0)
80+
81+ count, err := s.db.CountNewArticles(r.Context(), user.DID, since)
82+ if err != nil {
83+ w.WriteHeader(http.StatusInternalServerError)
84+ return
85+ }
86+
87+ w.Header().Set("Content-Type", "text/html")
88+ if count == 0 {
89+ w.Write([]byte(""))
90+ return
91+ }
92+ fmt.Fprintf(w, `<div id="new-articles-banner" class="bg-spot-green rounded-xl px-5 py-3 flex items-center justify-between mb-4"><span class="text-sm text-white font-medium">%d new article%s available.</span><a href="%s" class="text-sm font-bold text-white uppercase tracking-button hover:underline transition">Refresh</a></div>`, count, pluralS(count), r.URL.Query().Get("return"))
93+}
94+
95+func pluralS(n int) string {
96+ if n != 1 {
97+ return "s"
98+ }
99+ return ""
100+}
101+
71 func (s *Server) handleArticleDetail(w http.ResponseWriter, r *http.Request) {102 func (s *Server) handleArticleDetail(w http.ResponseWriter, r *http.Request) {
72 user := currentUser(r)103 user := currentUser(r)
73 id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)104 id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
modified internal/server/dashboard_handler.go +1 -0
@@ -38,5 +38,6 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
3838 "Page": page,
3939 "BaseURL": "/dashboard",
4040 "QueryParams": map[string]string{},
41+ "Now": time.Now(),
4142 })
4243 }
@@ -38,5 +38,6 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
38 "Page": page,38 "Page": page,
39 "BaseURL": "/dashboard",39 "BaseURL": "/dashboard",
40 "QueryParams": map[string]string{},40 "QueryParams": map[string]string{},
41+ "Now": time.Now(),
41 })42 })
42 }43 }
modified internal/server/server.go +1 -0
@@ -163,6 +163,7 @@ func (s *Server) setupRoutes() {
163163 s.router.Route("/articles", func(r chi.Router) {
164164 r.Use(s.requireAuth)
165165 r.Get("/", s.handleArticles)
166+ r.Get("/new-count", s.handleNewArticleCount)
166167 r.Get("/{id}", s.handleArticleDetail)
167168 r.Post("/{id}/read", s.handleMarkRead)
168169 r.Post("/{id}/unread", s.handleMarkUnread)
@@ -163,6 +163,7 @@ func (s *Server) setupRoutes() {
163 s.router.Route("/articles", func(r chi.Router) {163 s.router.Route("/articles", func(r chi.Router) {
164 r.Use(s.requireAuth)164 r.Use(s.requireAuth)
165 r.Get("/", s.handleArticles)165 r.Get("/", s.handleArticles)
166+ r.Get("/new-count", s.handleNewArticleCount)
166 r.Get("/{id}", s.handleArticleDetail)167 r.Get("/{id}", s.handleArticleDetail)
167 r.Post("/{id}/read", s.handleMarkRead)168 r.Post("/{id}/read", s.handleMarkRead)
168 r.Post("/{id}/unread", s.handleMarkUnread)169 r.Post("/{id}/unread", s.handleMarkUnread)
modified internal/tmpl/articles.html +1 -0
@@ -1,4 +1,5 @@
11 {{define "articles.html"}}
2+ <div hx-get="/articles/new-count?since={{.Now.Unix}}&return=/articles" hx-trigger="every 30s" hx-swap="beforebegin" hx-target="#article-list"></div>
23 <div class="flex items-center justify-between mb-2">
34 <h1 class="text-2xl font-bold text-spot-text">Articles</h1>
45 <form hx-post="/articles/mark-all-read" hx-confirm="Mark all articles as read?">
@@ -1,4 +1,5 @@
1 {{define "articles.html"}}1 {{define "articles.html"}}
2+ <div hx-get="/articles/new-count?since={{.Now.Unix}}&return=/articles" hx-trigger="every 30s" hx-swap="beforebegin" hx-target="#article-list"></div>
2 <div class="flex items-center justify-between mb-2">3 <div class="flex items-center justify-between mb-2">
3 <h1 class="text-2xl font-bold text-spot-text">Articles</h1>4 <h1 class="text-2xl font-bold text-spot-text">Articles</h1>
4 <form hx-post="/articles/mark-all-read" hx-confirm="Mark all articles as read?">5 <form hx-post="/articles/mark-all-read" hx-confirm="Mark all articles as read?">
modified internal/tmpl/dashboard.html +2 -0
@@ -21,6 +21,8 @@
2121 {{else}}
2222 <p class="text-sm text-spot-secondary mb-6">Your personalized feed, based on your social graph.</p>
2323
24+<div hx-get="/articles/new-count?since={{.Now.Unix}}&return=/dashboard" hx-trigger="every 30s" hx-swap="beforebegin" hx-target="#article-list"></div>
25+
2426 <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
2527 <div class="lg:col-span-2">
2628 {{if .ArticleRecommendations}}
@@ -21,6 +21,8 @@
21 {{else}}21 {{else}}
22 <p class="text-sm text-spot-secondary mb-6">Your personalized feed, based on your social graph.</p>22 <p class="text-sm text-spot-secondary mb-6">Your personalized feed, based on your social graph.</p>
23 23
24+<div hx-get="/articles/new-count?since={{.Now.Unix}}&return=/dashboard" hx-trigger="every 30s" hx-swap="beforebegin" hx-target="#article-list"></div>
25+
24 <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">26 <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
25 <div class="lg:col-span-2">27 <div class="lg:col-span-2">
26 {{if .ArticleRecommendations}}28 {{if .ArticleRecommendations}}