A social RSS reader built on the AT Protocol. glean.at
glean atproto atmosphere rss feed social app
14
fork

Configure Feed

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

Fix pagination: replace load more by actual pages

+208 -103
+25 -13
internal/server/annotations_handler.go
··· 14 14 func (s *Server) handleLibrary(w http.ResponseWriter, r *http.Request) { 15 15 user := currentUser(r) 16 16 17 - likedOffset, _ := strconv.Atoi(r.URL.Query().Get("liked_offset")) 18 - if likedOffset < 0 { 19 - likedOffset = 0 17 + limit := 20 18 + 19 + likedPageNum, _ := strconv.Atoi(r.URL.Query().Get("liked_page")) 20 + if likedPageNum < 1 { 21 + likedPageNum = 1 20 22 } 21 - annotOffset, _ := strconv.Atoi(r.URL.Query().Get("annot_offset")) 22 - if annotOffset < 0 { 23 - annotOffset = 0 23 + annotPageNum, _ := strconv.Atoi(r.URL.Query().Get("annot_page")) 24 + if annotPageNum < 1 { 25 + annotPageNum = 1 24 26 } 25 - limit := 20 27 + 28 + likedPage := Pagination{Page: likedPageNum, PageSize: limit} 29 + annotPage := Pagination{Page: annotPageNum, PageSize: limit} 26 30 27 - articles, _ := s.db.ListLikedArticles(r.Context(), user.DID, limit+1, likedOffset) 31 + articles, _ := s.db.ListLikedArticles(r.Context(), user.DID, limit+1, likedPage.Offset()) 28 32 likedHasMore := len(articles) > limit 29 33 if likedHasMore { 30 34 articles = articles[:limit] 31 35 } 36 + likedPage = likedPage.Paginate(len(articles)) 37 + if likedHasMore { 38 + likedPage.HasNext = true 39 + likedPage.NextPage = likedPage.Page + 1 40 + } 32 41 33 - annotations, _ := s.db.ListAnnotations(r.Context(), "", "", user.DID, limit+1, annotOffset) 42 + annotations, _ := s.db.ListAnnotations(r.Context(), "", "", user.DID, limit+1, annotPage.Offset()) 34 43 annotHasMore := len(annotations) > limit 35 44 if annotHasMore { 36 45 annotations = annotations[:limit] 37 46 } 47 + annotPage = annotPage.Paginate(len(annotations)) 48 + if annotHasMore { 49 + annotPage.HasNext = true 50 + annotPage.NextPage = annotPage.Page + 1 51 + } 38 52 39 53 s.render(w, r, "library.html", map[string]any{ 40 54 "User": user, 41 55 "Articles": articles, 42 56 "Annotations": annotations, 43 - "LikedHasMore": likedHasMore, 44 - "AnnotHasMore": annotHasMore, 45 - "NextLiked": likedOffset + limit, 46 - "NextAnnot": annotOffset + limit, 57 + "LikedPage": likedPage, 58 + "AnnotPage": annotPage, 47 59 }) 48 60 } 49 61
+11 -8
internal/server/articles_handler.go
··· 38 38 feedURL := r.URL.Query().Get("feed") 39 39 40 40 page := pageFromRequest(r, 50) 41 - articles, err := s.db.ListArticles(r.Context(), user.DID, feedURL, page.FetchLimit(), page.Offset) 41 + articles, err := s.db.ListArticles(r.Context(), user.DID, feedURL, page.Limit()+1, page.Offset()) 42 42 if err != nil { 43 43 s.logger.Error("failed to list articles", "error", err) 44 44 http.Error(w, err.Error(), http.StatusInternalServerError) 45 45 return 46 46 } 47 47 48 - page = page.Paginate(len(articles)) 49 - if page.HasMore { 50 - articles = articles[:page.Limit] 48 + totalFetched := len(articles) 49 + page = page.Paginate(totalFetched) 50 + if page.HasNext { 51 + articles = articles[:page.PageSize] 51 52 } 52 53 53 54 s.render(w, r, "articles.html", map[string]any{ 54 - "User": user, 55 - "Articles": articles, 56 - "FeedURL": feedURL, 57 - "Page": page, 55 + "User": user, 56 + "Articles": articles, 57 + "FeedURL": feedURL, 58 + "Page": page, 59 + "BaseURL": "/articles", 60 + "QueryParams": buildQueryParams(map[string]string{"feed": feedURL}), 58 61 }) 59 62 } 60 63
+7 -4
internal/server/dashboard_handler.go
··· 12 12 subCount, _ := s.db.GetSubscriptionCount(r.Context(), user.DID) 13 13 14 14 page := pageFromRequest(r, 25) 15 - articles, _ := s.db.ListUnreadArticles(r.Context(), user.DID, "", page.FetchLimit(), page.Offset) 16 - page = page.Paginate(len(articles)) 17 - if page.HasMore { 18 - articles = articles[:page.Limit] 15 + articles, _ := s.db.ListUnreadArticles(r.Context(), user.DID, "", page.Limit()+1, page.Offset()) 16 + totalFetched := len(articles) 17 + page = page.Paginate(totalFetched) 18 + if page.HasNext { 19 + articles = articles[:page.PageSize] 19 20 } 20 21 21 22 articleRecs, _ := s.db.GetArticleRecommendations(r.Context(), user.DID, 5) ··· 35 36 "PeopleRecommendations": peopleRecs, 36 37 "PersonalTrending": personalTrending, 37 38 "Page": page, 39 + "BaseURL": "/dashboard", 40 + "QueryParams": map[string]string{}, 38 41 }) 39 42 }
+7 -4
internal/server/feeds_handler.go
··· 19 19 category := r.URL.Query().Get("category") 20 20 21 21 page := pageFromRequest(r, 50) 22 - subs, _ := s.db.ListSubscriptions(r.Context(), user.DID, category, page.FetchLimit(), page.Offset) 23 - page = page.Paginate(len(subs)) 24 - if page.HasMore { 25 - subs = subs[:page.Limit] 22 + subs, _ := s.db.ListSubscriptions(r.Context(), user.DID, category, page.Limit()+1, page.Offset()) 23 + totalFetched := len(subs) 24 + page = page.Paginate(totalFetched) 25 + if page.HasNext { 26 + subs = subs[:page.PageSize] 26 27 } 27 28 28 29 allSubs, _ := s.db.ListSubscriptions(r.Context(), user.DID, "", 1000, 0) ··· 49 50 "PeopleRecommendations": peopleRecs, 50 51 "DeadFeeds": deadFeeds, 51 52 "Page": page, 53 + "BaseURL": "/feeds", 54 + "QueryParams": buildQueryParams(map[string]string{"category": category}), 52 55 }) 53 56 } 54 57
+38 -16
internal/server/pagination.go
··· 8 8 const defaultPageSize = 25 9 9 10 10 type Pagination struct { 11 - Offset int 12 - Limit int 13 - HasMore bool 14 - NextOffset int 11 + Page int 12 + PageSize int 13 + HasPrev bool 14 + HasNext bool 15 + PrevPage int 16 + NextPage int 15 17 } 16 18 17 - func pageFromRequest(r *http.Request, limit int) Pagination { 18 - if limit <= 0 { 19 - limit = defaultPageSize 19 + func pageFromRequest(r *http.Request, pageSize int) Pagination { 20 + if pageSize <= 0 { 21 + pageSize = defaultPageSize 20 22 } 21 - offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) 22 - if offset < 0 { 23 - offset = 0 23 + page, _ := strconv.Atoi(r.URL.Query().Get("page")) 24 + if page < 1 { 25 + page = 1 24 26 } 25 - return Pagination{Offset: offset, Limit: limit} 27 + return Pagination{Page: page, PageSize: pageSize} 26 28 } 27 29 28 - func (p Pagination) Paginate(count int) Pagination { 29 - p.HasMore = count > p.Limit 30 - p.NextOffset = p.Offset + p.Limit 30 + func (p Pagination) Paginate(totalFetched int) Pagination { 31 + if totalFetched > p.PageSize { 32 + p.HasNext = true 33 + p.NextPage = p.Page + 1 34 + } 35 + if p.Page > 1 { 36 + p.HasPrev = true 37 + p.PrevPage = p.Page - 1 38 + } 31 39 return p 32 40 } 33 41 34 - func (p Pagination) FetchLimit() int { 35 - return p.Limit + 1 42 + func (p Pagination) Offset() int { 43 + return (p.Page - 1) * p.PageSize 44 + } 45 + 46 + func (p Pagination) Limit() int { 47 + return p.PageSize 48 + } 49 + 50 + func buildQueryParams(params map[string]string) map[string]string { 51 + result := make(map[string]string) 52 + for k, v := range params { 53 + if v != "" { 54 + result[k] = v 55 + } 56 + } 57 + return result 36 58 }
+15
internal/server/server.go
··· 7 7 "html/template" 8 8 "log/slog" 9 9 "net/http" 10 + "net/url" 10 11 "path/filepath" 11 12 "strings" 12 13 "time" ··· 207 208 return "" 208 209 } 209 210 return template.HTML(`<input type="hidden" name="csrf_token" value="` + s + `">`) 211 + }, 212 + "paginationURL": func(baseURL string, page int, queryParams map[string]string) string { 213 + u, _ := url.Parse(baseURL) 214 + q := u.Query() 215 + for k, v := range queryParams { 216 + q.Set(k, v) 217 + } 218 + if page > 1 { 219 + q.Set("page", fmt.Sprintf("%d", page)) 220 + } else { 221 + q.Del("page") 222 + } 223 + u.RawQuery = q.Encode() 224 + return u.String() 210 225 }, 211 226 } 212 227
+12 -9
internal/server/trending_handler.go
··· 20 20 21 21 var trending []*db.TrendingItem 22 22 if scope == "for-me" { 23 - trending, _ = s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, page.FetchLimit(), page.Offset) 23 + trending, _ = s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, page.Limit()+1, page.Offset()) 24 24 } else { 25 - trending, _ = s.db.ListTrendingArticles(r.Context(), since, page.FetchLimit(), page.Offset) 25 + trending, _ = s.db.ListTrendingArticles(r.Context(), since, page.Limit()+1, page.Offset()) 26 26 } 27 27 28 - page = page.Paginate(len(trending)) 29 - if page.HasMore { 30 - trending = trending[:page.Limit] 28 + totalFetched := len(trending) 29 + page = page.Paginate(totalFetched) 30 + if page.HasNext { 31 + trending = trending[:page.PageSize] 31 32 } 32 33 33 34 s.render(w, r, "trending.html", map[string]any{ 34 - "User": user, 35 - "Trending": trending, 36 - "Scope": scope, 37 - "Page": page, 35 + "User": user, 36 + "Trending": trending, 37 + "Scope": scope, 38 + "Page": page, 39 + "BaseURL": "/trending", 40 + "QueryParams": buildQueryParams(map[string]string{"scope": scope}), 38 41 }) 39 42 }
+15 -8
internal/tmpl/articles.html
··· 17 17 {{end}} 18 18 </div> 19 19 20 - {{if .Page.HasMore}} 21 - <div class="text-center py-4"> 22 - <button hx-get="/articles?offset={{.Page.NextOffset}}{{if .FeedURL}}&feed={{.FeedURL}}{{end}}" 23 - hx-target="#article-list" hx-swap="beforeend" 24 - hx-select="article" 25 - class="text-sm text-spot-secondary hover:text-spot-green transition"> 26 - Load more 27 - </button> 20 + {{if or .Page.HasPrev .Page.HasNext}} 21 + <div class="flex items-center justify-center gap-3 py-6"> 22 + {{if .Page.HasPrev}} 23 + <a href="{{paginationURL .BaseURL .Page.PrevPage .QueryParams}}" class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-text hover:bg-spot-hover-50 transition">&laquo; Prev</a> 24 + {{else}} 25 + <span class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-muted cursor-not-allowed">&laquo; Prev</span> 26 + {{end}} 27 + 28 + <span class="text-sm text-spot-secondary">Page {{.Page.Page}}</span> 29 + 30 + {{if .Page.HasNext}} 31 + <a href="{{paginationURL .BaseURL .Page.NextPage .QueryParams}}" class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-text hover:bg-spot-hover-50 transition">Next &raquo;</a> 32 + {{else}} 33 + <span class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-muted cursor-not-allowed">Next &raquo;</span> 34 + {{end}} 28 35 </div> 29 36 {{end}} 30 37
+15 -8
internal/tmpl/dashboard.html
··· 38 38 {{end}} 39 39 </div> 40 40 41 - {{if .Page.HasMore}} 42 - <div class="text-center py-4"> 43 - <button hx-get="/dashboard?offset={{.Page.NextOffset}}" 44 - hx-target="#article-list" hx-swap="beforeend" 45 - hx-select="article" 46 - class="text-sm text-spot-secondary hover:text-spot-green transition"> 47 - Load more 48 - </button> 41 + {{if or .Page.HasPrev .Page.HasNext}} 42 + <div class="flex items-center justify-center gap-3 py-6"> 43 + {{if .Page.HasPrev}} 44 + <a href="{{paginationURL .BaseURL .Page.PrevPage .QueryParams}}" class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-text hover:bg-spot-hover-50 transition">&laquo; Prev</a> 45 + {{else}} 46 + <span class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-muted cursor-not-allowed">&laquo; Prev</span> 47 + {{end}} 48 + 49 + <span class="text-sm text-spot-secondary">Page {{.Page.Page}}</span> 50 + 51 + {{if .Page.HasNext}} 52 + <a href="{{paginationURL .BaseURL .Page.NextPage .QueryParams}}" class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-text hover:bg-spot-hover-50 transition">Next &raquo;</a> 53 + {{else}} 54 + <span class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-muted cursor-not-allowed">Next &raquo;</span> 55 + {{end}} 49 56 </div> 50 57 {{end}} 51 58 </div>
+15 -8
internal/tmpl/feeds.html
··· 92 92 {{end}} 93 93 </div> 94 94 95 - {{if .Page.HasMore}} 96 - <div class="text-center py-4"> 97 - <button hx-get="/feeds?offset={{.Page.NextOffset}}{{if .Category}}&category={{.Category}}{{end}}" 98 - hx-target="#feed-list" hx-swap="beforeend" 99 - hx-select=".feed-item" 100 - class="text-sm text-spot-secondary hover:text-spot-green transition"> 101 - Load more 102 - </button> 95 + {{if or .Page.HasPrev .Page.HasNext}} 96 + <div class="flex items-center justify-center gap-3 py-6"> 97 + {{if .Page.HasPrev}} 98 + <a href="{{paginationURL .BaseURL .Page.PrevPage .QueryParams}}" class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-text hover:bg-spot-hover-50 transition">&laquo; Prev</a> 99 + {{else}} 100 + <span class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-muted cursor-not-allowed">&laquo; Prev</span> 101 + {{end}} 102 + 103 + <span class="text-sm text-spot-secondary">Page {{.Page.Page}}</span> 104 + 105 + {{if .Page.HasNext}} 106 + <a href="{{paginationURL .BaseURL .Page.NextPage .QueryParams}}" class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-text hover:bg-spot-hover-50 transition">Next &raquo;</a> 107 + {{else}} 108 + <span class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-muted cursor-not-allowed">Next &raquo;</span> 109 + {{end}} 103 110 </div> 104 111 {{end}} 105 112 </div>
+30 -16
internal/tmpl/library.html
··· 19 19 {{end}} 20 20 </div> 21 21 22 - {{if .LikedHasMore}} 23 - <div class="text-center py-4"> 24 - <button hx-get="/library?liked_offset={{.NextLiked}}" 25 - hx-target="#liked-list" hx-swap="beforeend" 26 - hx-select="article" 27 - class="text-sm text-spot-secondary hover:text-spot-green transition"> 28 - Load more 29 - </button> 22 + {{if or .LikedPage.HasPrev .LikedPage.HasNext}} 23 + <div class="flex items-center justify-center gap-3 py-6"> 24 + {{if .LikedPage.HasPrev}} 25 + <a href="/library?liked_page={{.LikedPage.PrevPage}}{{if gt .AnnotPage.Page 1}}&annot_page={{.AnnotPage.Page}}{{end}}" class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-text hover:bg-spot-hover-50 transition">&laquo; Prev</a> 26 + {{else}} 27 + <span class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-muted cursor-not-allowed">&laquo; Prev</span> 28 + {{end}} 29 + 30 + <span class="text-sm text-spot-secondary">Page {{.LikedPage.Page}}</span> 31 + 32 + {{if .LikedPage.HasNext}} 33 + <a href="/library?liked_page={{.LikedPage.NextPage}}{{if gt .AnnotPage.Page 1}}&annot_page={{.AnnotPage.Page}}{{end}}" class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-text hover:bg-spot-hover-50 transition">Next &raquo;</a> 34 + {{else}} 35 + <span class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-muted cursor-not-allowed">Next &raquo;</span> 36 + {{end}} 30 37 </div> 31 38 {{end}} 32 39 </div> ··· 51 58 {{end}} 52 59 </div> 53 60 54 - {{if .AnnotHasMore}} 55 - <div class="text-center py-4"> 56 - <button hx-get="/library?annot_offset={{.NextAnnot}}" 57 - hx-target="#annotations-list" hx-swap="beforeend" 58 - hx-select=".bg-spot-surface" 59 - class="text-sm text-spot-secondary hover:text-spot-green transition"> 60 - Load more 61 - </button> 61 + {{if or .AnnotPage.HasPrev .AnnotPage.HasNext}} 62 + <div class="flex items-center justify-center gap-3 py-6"> 63 + {{if .AnnotPage.HasPrev}} 64 + <a href="/library?annot_page={{.AnnotPage.PrevPage}}{{if gt .LikedPage.Page 1}}&liked_page={{.LikedPage.Page}}{{end}}" class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-text hover:bg-spot-hover-50 transition">&laquo; Prev</a> 65 + {{else}} 66 + <span class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-muted cursor-not-allowed">&laquo; Prev</span> 67 + {{end}} 68 + 69 + <span class="text-sm text-spot-secondary">Page {{.AnnotPage.Page}}</span> 70 + 71 + {{if .AnnotPage.HasNext}} 72 + <a href="/library?annot_page={{.AnnotPage.NextPage}}{{if gt .LikedPage.Page 1}}&liked_page={{.LikedPage.Page}}{{end}}" class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-text hover:bg-spot-hover-50 transition">Next &raquo;</a> 73 + {{else}} 74 + <span class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-muted cursor-not-allowed">Next &raquo;</span> 75 + {{end}} 62 76 </div> 63 77 {{end}} 64 78 </div>
+15 -8
internal/tmpl/trending.html
··· 37 37 {{end}} 38 38 </div> 39 39 40 - {{if .Page.HasMore}} 41 - <div class="text-center py-4"> 42 - <button hx-get="/trending?scope={{.Scope}}&offset={{.Page.NextOffset}}" 43 - hx-target="#trending-list" hx-swap="beforeend" 44 - hx-select="article" 45 - class="text-sm text-spot-secondary hover:text-spot-green transition"> 46 - Load more 47 - </button> 40 + {{if or .Page.HasPrev .Page.HasNext}} 41 + <div class="flex items-center justify-center gap-3 py-6"> 42 + {{if .Page.HasPrev}} 43 + <a href="{{paginationURL .BaseURL .Page.PrevPage .QueryParams}}" class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-text hover:bg-spot-hover-50 transition">&laquo; Prev</a> 44 + {{else}} 45 + <span class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-muted cursor-not-allowed">&laquo; Prev</span> 46 + {{end}} 47 + 48 + <span class="text-sm text-spot-secondary">Page {{.Page.Page}}</span> 49 + 50 + {{if .Page.HasNext}} 51 + <a href="{{paginationURL .BaseURL .Page.NextPage .QueryParams}}" class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-text hover:bg-spot-hover-50 transition">Next &raquo;</a> 52 + {{else}} 53 + <span class="px-3 py-1.5 rounded-lg text-sm bg-spot-hover text-spot-muted cursor-not-allowed">Next &raquo;</span> 54 + {{end}} 48 55 </div> 49 56 {{end}} 50 57 {{end}}
+3 -1
readme.md
··· 1 - # [Glean.at](https://glean.at) 1 + # Glean.at 2 + 3 + [See live](https://glean.at)