Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).

appview: repo/issues: paginate issues

loading up issues on @tangled.sh/core should be much faster because we
only have to resolve ~10 DIDs at a time. with a saturated did-handle
cache, this should be near-instant page load times.

authored by oppi.li and committed by

Tangled 8a571743 3646ff6d

+153 -28
+35 -20
appview/db/issues.go
··· 5 5 "time" 6 6 7 7 "github.com/bluesky-social/indigo/atproto/syntax" 8 + "tangled.sh/tangled.sh/core/appview/pagination" 8 9 ) 9 10 10 11 type Issue struct { ··· 103 102 return ownerDid, err 104 103 } 105 104 106 - func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool) ([]Issue, error) { 105 + func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) { 107 106 var issues []Issue 108 107 openValue := 0 109 108 if isOpen { ··· 111 110 } 112 111 113 112 rows, err := e.Query( 114 - `select 115 - i.owner_did, 116 - i.issue_id, 117 - i.created, 118 - i.title, 119 - i.body, 120 - i.open, 121 - count(c.id) 122 - from 123 - issues i 124 - left join 125 - comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id 126 - where 127 - i.repo_at = ? and i.open = ? 128 - group by 129 - i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open 130 - order by 131 - i.created desc`, 132 - repoAt, openValue) 113 + ` 114 + with numbered_issue as ( 115 + select 116 + i.owner_did, 117 + i.issue_id, 118 + i.created, 119 + i.title, 120 + i.body, 121 + i.open, 122 + count(c.id) as comment_count, 123 + row_number() over (order by i.created desc) as row_num 124 + from 125 + issues i 126 + left join 127 + comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id 128 + where 129 + i.repo_at = ? and i.open = ? 130 + group by 131 + i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open 132 + ) 133 + select 134 + owner_did, 135 + issue_id, 136 + created, 137 + title, 138 + body, 139 + open, 140 + comment_count 141 + from 142 + numbered_issue 143 + where 144 + row_num between ? and ?`, 145 + repoAt, openValue, page.Offset+1, page.Offset+page.Limit) 133 146 if err != nil { 134 147 return nil, err 135 148 }
+32
appview/middleware/middleware.go
··· 1 1 package middleware 2 2 3 3 import ( 4 + "context" 4 5 "log" 5 6 "net/http" 7 + "strconv" 6 8 "time" 7 9 8 10 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 11 "github.com/bluesky-social/indigo/xrpc" 10 12 "tangled.sh/tangled.sh/core/appview" 11 13 "tangled.sh/tangled.sh/core/appview/auth" 14 + "tangled.sh/tangled.sh/core/appview/pagination" 12 15 ) 13 16 14 17 type Middleware func(http.Handler) http.Handler ··· 94 91 next.ServeHTTP(w, r) 95 92 }) 96 93 } 94 + } 95 + 96 + func Paginate(next http.Handler) http.Handler { 97 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 98 + page := pagination.FirstPage() 99 + 100 + offsetVal := r.URL.Query().Get("offset") 101 + if offsetVal != "" { 102 + offset, err := strconv.Atoi(offsetVal) 103 + if err != nil { 104 + log.Println("invalid offset") 105 + } else { 106 + page.Offset = offset 107 + } 108 + } 109 + 110 + limitVal := r.URL.Query().Get("limit") 111 + if limitVal != "" { 112 + limit, err := strconv.Atoi(limitVal) 113 + if err != nil { 114 + log.Println("invalid limit") 115 + } else { 116 + page.Limit = limit 117 + } 118 + } 119 + 120 + ctx := context.WithValue(r.Context(), "page", page) 121 + next.ServeHTTP(w, r.WithContext(ctx)) 122 + }) 97 123 }
+7 -6
appview/pages/pages.go
··· 20 20 "tangled.sh/tangled.sh/core/appview/auth" 21 21 "tangled.sh/tangled.sh/core/appview/db" 22 22 "tangled.sh/tangled.sh/core/appview/pages/markup" 23 + "tangled.sh/tangled.sh/core/appview/pagination" 23 24 "tangled.sh/tangled.sh/core/appview/state/userutil" 24 25 "tangled.sh/tangled.sh/core/patchutil" 25 26 "tangled.sh/tangled.sh/core/types" ··· 652 651 } 653 652 654 653 type RepoIssuesParams struct { 655 - LoggedInUser *auth.User 656 - RepoInfo RepoInfo 657 - Active string 658 - Issues []db.Issue 659 - DidHandleMap map[string]string 660 - 654 + LoggedInUser *auth.User 655 + RepoInfo RepoInfo 656 + Active string 657 + Issues []db.Issue 658 + DidHandleMap map[string]string 659 + Page pagination.Page 661 660 FilteringByOpen bool 662 661 } 663 662
+38
appview/pages/templates/repo/issues/issues.html
··· 70 70 </div> 71 71 {{ end }} 72 72 </div> 73 + 74 + {{ block "pagination" . }} {{ end }} 75 + 76 + {{ end }} 77 + 78 + {{ define "pagination" }} 79 + <div class="flex justify-end mt-4 gap-2"> 80 + {{ $currentState := "closed" }} 81 + {{ if .FilteringByOpen }} 82 + {{ $currentState = "open" }} 83 + {{ end }} 84 + 85 + {{ if gt .Page.Offset 0 }} 86 + {{ $prev := .Page.Previous }} 87 + <a 88 + class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 89 + hx-boost="true" 90 + href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&offset={{ $prev.Offset }}&limit={{ $prev.Limit }}" 91 + > 92 + {{ i "chevron-left" "w-4 h-4" }} 93 + previous 94 + </a> 95 + {{ else }} 96 + <div></div> 97 + {{ end }} 98 + 99 + {{ if eq (len .Issues) .Page.Limit }} 100 + {{ $next := .Page.Next }} 101 + <a 102 + class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 103 + hx-boost="true" 104 + href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&offset={{ $next.Offset }}&limit={{ $next.Limit }}" 105 + > 106 + next 107 + {{ i "chevron-right" "w-4 h-4" }} 108 + </a> 109 + {{ end }} 110 + </div> 73 111 {{ end }}
+31
appview/pagination/page.go
··· 1 + package pagination 2 + 3 + type Page struct { 4 + Offset int // where to start from 5 + Limit int // number of items in a page 6 + } 7 + 8 + func FirstPage() Page { 9 + return Page{ 10 + Offset: 0, 11 + Limit: 10, 12 + } 13 + } 14 + 15 + func (p Page) Previous() Page { 16 + if p.Offset-p.Limit < 0 { 17 + return FirstPage() 18 + } else { 19 + return Page{ 20 + Offset: p.Offset - p.Limit, 21 + Limit: p.Limit, 22 + } 23 + } 24 + } 25 + 26 + func (p Page) Next() Page { 27 + return Page{ 28 + Offset: p.Offset + p.Limit, 29 + Limit: p.Limit, 30 + } 31 + }
+9 -1
appview/state/repo.go
··· 28 28 "tangled.sh/tangled.sh/core/appview/db" 29 29 "tangled.sh/tangled.sh/core/appview/pages" 30 30 "tangled.sh/tangled.sh/core/appview/pages/markup" 31 + "tangled.sh/tangled.sh/core/appview/pagination" 31 32 "tangled.sh/tangled.sh/core/types" 32 33 33 34 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 1560 1559 isOpen = true 1561 1560 } 1562 1561 1562 + page, ok := r.Context().Value("page").(pagination.Page) 1563 + if !ok { 1564 + log.Println("failed to get page") 1565 + page = pagination.FirstPage() 1566 + } 1567 + 1563 1568 user := s.auth.GetUser(r) 1564 1569 f, err := fullyResolvedRepo(r) 1565 1570 if err != nil { ··· 1573 1566 return 1574 1567 } 1575 1568 1576 - issues, err := db.GetIssues(s.db, f.RepoAt, isOpen) 1569 + issues, err := db.GetIssues(s.db, f.RepoAt, isOpen, page) 1577 1570 if err != nil { 1578 1571 log.Println("failed to get issues", err) 1579 1572 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.") ··· 1600 1593 Issues: issues, 1601 1594 DidHandleMap: didHandleMap, 1602 1595 FilteringByOpen: isOpen, 1596 + Page: page, 1603 1597 }) 1604 1598 return 1605 1599 }
+1 -1
appview/state/router.go
··· 68 68 r.Get("/blob/{ref}/raw/*", s.RepoBlobRaw) 69 69 70 70 r.Route("/issues", func(r chi.Router) { 71 - r.Get("/", s.RepoIssues) 71 + r.With(middleware.Paginate).Get("/", s.RepoIssues) 72 72 r.Get("/{issue}", s.RepoSingleIssue) 73 73 74 74 r.Group(func(r chi.Router) {