Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Paginate the whole activity feed

c8cdb6accc47af06a3df2b8b5023c21134fdb812 · Keenan Nemetz · 9d ago · parent 4d938fe8

Paginate the whole activity feed

Both activity pages stopped at a fixed number of events: the repository
feed was truncated at 100, and the instance feed merged 100 per
repository and then took 200 before its pager. Anything older was
unreachable.

Now that issues, changes, comments and state changes are index rows, the
whole feed is assembled from the database and paginated:

- the repository feed and the instance feed keep the whole sorted,
  filtered list, and both show 50 events per page;
- the repository feed gains a pager (it had none), and the instance
  pager now reaches all history;
- the kind/actor/assignee filters travel through the page links, so
  paging never widens the feed;
- the repository pager swaps the `#activity-feed` fragment over HTMX like
  the chips and the filter form, and the page links are plain hrefs when
  JavaScript is not there;
- the feed reads comment summaries (parent, id, author, created) rather
  than every comment body it will not render.

The home rail still asks for a bounded feed, so it keeps its handful of
events. The feed is assembled in memory per request, so a page costs
O(history) instead of O(page); that is the accepted trade for now, and a
SQL event stream is the follow-up if a large history makes it visible.

server/web/activity.go +37−8

@@ -9,8 +9,10 @@ package web
9 9
10 10 import (
11 11 "net/http"
12 "net/url"
12 13 "slices"
13 14 "sort"
15 "strconv"
14 16 "strings"
15 17 "time"
16 18
@@ -20,7 +22,10 @@ import (
20 22 "github.com/rodent-software/vole/metadata"
21 23 )
22 24
23 const activityLimit = 100
25 // activityPage is how many events one page of the repository feed shows. The
26 // whole feed is paginated, so this is a reading size rather than a bound on how
27 // far back someone can look.
28 const activityPage = 50
24 29
25 30 type activityEvent struct {
26 31 Kind string
@@ -65,6 +70,7 @@ type activityView struct {
65 70 Actors []activityActor
66 71 Assignees []string
67 72 Events []activityEvent
73 Pager pager
68 74 }
69 75
70 76 func normalizeActivityKind(kind string) string {
@@ -79,7 +85,7 @@ func normalizeActivityKind(kind string) string {
79 85 return ""
80 86 }
81 87
82 func (w *Web) repoActivity(memo *requestMemo, m *repo.Meta, kind, actor, assignee string) activityFeed {
88 func (w *Web) repoActivity(memo *requestMemo, m *repo.Meta, kind, actor, assignee string, limit int) activityFeed {
83 89 g := m.Git()
84 90 var events []activityEvent
85 91 var lastCommit *git.Commit
@@ -227,8 +233,10 @@ func (w *Web) repoActivity(memo *requestMemo, m *repo.Meta, kind, actor, assigne
227 233 }
228 234 events = filtered
229 235 }
230 if len(events) > activityLimit {
231 events = events[:activityLimit]
236 // The activity pages want the whole feed (they paginate it); the home page
237 // asks for what fits in its rail.
238 if limit > 0 && len(events) > limit {
239 events = events[:limit]
232 240 }
233 241 return activityFeed{
234 242 Events: events, Assignees: assignees,
@@ -274,7 +282,7 @@ func (w *Web) activityIssues(memo *requestMemo, m *repo.Meta) []activityIssue {
274 282 if err := w.ensureIndex(memo, m); err == nil {
275 283 rows, err := w.Index.Issues(m.ID, index.Filter{})
276 284 if err == nil {
277 comments, err := w.Index.CommentsForRepo(m.ID, index.KindIssueComment)
285 comments, err := w.Index.CommentSummaries(m.ID, index.KindIssueComment)
278 286 if err == nil {
279 287 byParent := map[string][]activityComment{}
280 288 for _, c := range comments {
@@ -318,7 +326,7 @@ func (w *Web) activityRequests(memo *requestMemo, m *repo.Meta) []activityReques
318 326 if err := w.ensureIndex(memo, m); err == nil {
319 327 rows, err := w.Index.Requests(m.ID, index.RequestFilter{})
320 328 if err == nil {
321 comments, err := w.Index.CommentsForRepo(m.ID, index.KindRequestComment)
329 comments, err := w.Index.CommentSummaries(m.ID, index.KindRequestComment)
322 330 if err == nil {
323 331 approvals, err := w.Index.AllApprovals(m.ID)
324 332 if err == nil {
@@ -442,11 +450,32 @@ func (w *Web) Activity(rw http.ResponseWriter, r *http.Request) {
442 450 kind := normalizeActivityKind(r.URL.Query().Get("kind"))
443 451 actor := strings.TrimSpace(r.URL.Query().Get("actor"))
444 452 assignee := strings.TrimSpace(r.URL.Query().Get("assignee"))
445 feed := w.repoActivity(memoOf(r), m, kind, actor, assignee)
453 feed := w.repoActivity(memoOf(r), m, kind, actor, assignee, 0)
454 number, _ := strconv.Atoi(r.URL.Query().Get("page"))
455 events, pages := paginate(feed.Events, number, activityPage, func(n int) string {
456 v := url.Values{}
457 if kind != "" {
458 v.Set("kind", kind)
459 }
460 if actor != "" {
461 v.Set("actor", actor)
462 }
463 if assignee != "" {
464 v.Set("assignee", assignee)
465 }
466 if n > 1 {
467 v.Set("page", strconv.Itoa(n))
468 }
469 if q := v.Encode(); q != "" {
470 return "/r/" + m.Name + "/activity?" + q
471 }
472 return "/r/" + m.Name + "/activity"
473 }, "Activity pages")
474 pages.Target = "#activity-feed"
446 475 view := activityView{
447 476 Page: w.page(r, m.Name+" · activity"), Name: m.Name, Kind: kind,
448 477 Actor: actor, Assignee: assignee,
449 Actors: w.activityActors(), Assignees: feed.Assignees, Events: feed.Events,
478 Actors: w.activityActors(), Assignees: feed.Assignees, Events: events, Pager: pages,
450 479 }
451 480 if htmxRequest(r) {
452 481 w.render(rw, "activity_feed", view, 0)