forked from tangled.org/core
Monorepo for Tangled

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.

Changed files
+153 -28
appview
db
middleware
pages
templates
repo
issues
pagination
state
+35 -20
appview/db/issues.go
··· 5 "time" 6 7 "github.com/bluesky-social/indigo/atproto/syntax" 8 ) 9 10 type Issue struct { ··· 102 return ownerDid, err 103 } 104 105 - func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool) ([]Issue, error) { 106 var issues []Issue 107 openValue := 0 108 if isOpen { ··· 110 } 111 112 rows, err := e.Query( 113 - `select 114 - i.owner_did, 115 - i.issue_id, 116 - i.created, 117 - i.title, 118 - i.body, 119 - i.open, 120 - count(c.id) 121 - from 122 - issues i 123 - left join 124 - comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id 125 - where 126 - i.repo_at = ? and i.open = ? 127 - group by 128 - i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open 129 - order by 130 - i.created desc`, 131 - repoAt, openValue) 132 if err != nil { 133 return nil, err 134 }
··· 5 "time" 6 7 "github.com/bluesky-social/indigo/atproto/syntax" 8 + "tangled.sh/tangled.sh/core/appview/pagination" 9 ) 10 11 type Issue struct { ··· 103 return ownerDid, err 104 } 105 106 + func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) { 107 var issues []Issue 108 openValue := 0 109 if isOpen { ··· 111 } 112 113 rows, err := e.Query( 114 + ` 115 + with numbered_issue as ( 116 + select 117 + i.owner_did, 118 + i.issue_id, 119 + i.created, 120 + i.title, 121 + i.body, 122 + i.open, 123 + count(c.id) as comment_count, 124 + row_number() over (order by i.created desc) as row_num 125 + from 126 + issues i 127 + left join 128 + comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id 129 + where 130 + i.repo_at = ? and i.open = ? 131 + group by 132 + i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open 133 + ) 134 + select 135 + owner_did, 136 + issue_id, 137 + created, 138 + title, 139 + body, 140 + open, 141 + comment_count 142 + from 143 + numbered_issue 144 + where 145 + row_num between ? and ?`, 146 + repoAt, openValue, page.Offset+1, page.Offset+page.Limit) 147 if err != nil { 148 return nil, err 149 }
+32
appview/middleware/middleware.go
··· 1 package middleware 2 3 import ( 4 "log" 5 "net/http" 6 "time" 7 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 "github.com/bluesky-social/indigo/xrpc" 10 "tangled.sh/tangled.sh/core/appview" 11 "tangled.sh/tangled.sh/core/appview/auth" 12 ) 13 14 type Middleware func(http.Handler) http.Handler ··· 92 }) 93 } 94 }
··· 1 package middleware 2 3 import ( 4 + "context" 5 "log" 6 "net/http" 7 + "strconv" 8 "time" 9 10 comatproto "github.com/bluesky-social/indigo/api/atproto" 11 "github.com/bluesky-social/indigo/xrpc" 12 "tangled.sh/tangled.sh/core/appview" 13 "tangled.sh/tangled.sh/core/appview/auth" 14 + "tangled.sh/tangled.sh/core/appview/pagination" 15 ) 16 17 type Middleware func(http.Handler) http.Handler ··· 95 }) 96 } 97 } 98 + 99 + func Paginate(next http.Handler) http.Handler { 100 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 101 + page := pagination.FirstPage() 102 + 103 + offsetVal := r.URL.Query().Get("offset") 104 + if offsetVal != "" { 105 + offset, err := strconv.Atoi(offsetVal) 106 + if err != nil { 107 + log.Println("invalid offset") 108 + } else { 109 + page.Offset = offset 110 + } 111 + } 112 + 113 + limitVal := r.URL.Query().Get("limit") 114 + if limitVal != "" { 115 + limit, err := strconv.Atoi(limitVal) 116 + if err != nil { 117 + log.Println("invalid limit") 118 + } else { 119 + page.Limit = limit 120 + } 121 + } 122 + 123 + ctx := context.WithValue(r.Context(), "page", page) 124 + next.ServeHTTP(w, r.WithContext(ctx)) 125 + }) 126 + }
+7 -6
appview/pages/pages.go
··· 19 "tangled.sh/tangled.sh/core/appview/auth" 20 "tangled.sh/tangled.sh/core/appview/db" 21 "tangled.sh/tangled.sh/core/appview/pages/markup" 22 "tangled.sh/tangled.sh/core/appview/state/userutil" 23 "tangled.sh/tangled.sh/core/patchutil" 24 "tangled.sh/tangled.sh/core/types" ··· 564 } 565 566 type RepoIssuesParams struct { 567 - LoggedInUser *auth.User 568 - RepoInfo RepoInfo 569 - Active string 570 - Issues []db.Issue 571 - DidHandleMap map[string]string 572 - 573 FilteringByOpen bool 574 } 575
··· 19 "tangled.sh/tangled.sh/core/appview/auth" 20 "tangled.sh/tangled.sh/core/appview/db" 21 "tangled.sh/tangled.sh/core/appview/pages/markup" 22 + "tangled.sh/tangled.sh/core/appview/pagination" 23 "tangled.sh/tangled.sh/core/appview/state/userutil" 24 "tangled.sh/tangled.sh/core/patchutil" 25 "tangled.sh/tangled.sh/core/types" ··· 565 } 566 567 type RepoIssuesParams struct { 568 + LoggedInUser *auth.User 569 + RepoInfo RepoInfo 570 + Active string 571 + Issues []db.Issue 572 + DidHandleMap map[string]string 573 + Page pagination.Page 574 FilteringByOpen bool 575 } 576
+38
appview/pages/templates/repo/issues/issues.html
··· 70 </div> 71 {{ end }} 72 </div> 73 {{ end }}
··· 70 </div> 71 {{ end }} 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> 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 "tangled.sh/tangled.sh/core/appview/db" 29 "tangled.sh/tangled.sh/core/appview/pages" 30 "tangled.sh/tangled.sh/core/appview/pages/markup" 31 "tangled.sh/tangled.sh/core/types" 32 33 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 1559 isOpen = true 1560 } 1561 1562 user := s.auth.GetUser(r) 1563 f, err := fullyResolvedRepo(r) 1564 if err != nil { ··· 1566 return 1567 } 1568 1569 - issues, err := db.GetIssues(s.db, f.RepoAt, isOpen) 1570 if err != nil { 1571 log.Println("failed to get issues", err) 1572 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.") ··· 1593 Issues: issues, 1594 DidHandleMap: didHandleMap, 1595 FilteringByOpen: isOpen, 1596 }) 1597 return 1598 }
··· 28 "tangled.sh/tangled.sh/core/appview/db" 29 "tangled.sh/tangled.sh/core/appview/pages" 30 "tangled.sh/tangled.sh/core/appview/pages/markup" 31 + "tangled.sh/tangled.sh/core/appview/pagination" 32 "tangled.sh/tangled.sh/core/types" 33 34 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 1560 isOpen = true 1561 } 1562 1563 + page, ok := r.Context().Value("page").(pagination.Page) 1564 + if !ok { 1565 + log.Println("failed to get page") 1566 + page = pagination.FirstPage() 1567 + } 1568 + 1569 user := s.auth.GetUser(r) 1570 f, err := fullyResolvedRepo(r) 1571 if err != nil { ··· 1573 return 1574 } 1575 1576 + issues, err := db.GetIssues(s.db, f.RepoAt, isOpen, page) 1577 if err != nil { 1578 log.Println("failed to get issues", err) 1579 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.") ··· 1600 Issues: issues, 1601 DidHandleMap: didHandleMap, 1602 FilteringByOpen: isOpen, 1603 + Page: page, 1604 }) 1605 return 1606 }
+1 -1
appview/state/router.go
··· 68 r.Get("/blob/{ref}/raw/*", s.RepoBlobRaw) 69 70 r.Route("/issues", func(r chi.Router) { 71 - r.Get("/", s.RepoIssues) 72 r.Get("/{issue}", s.RepoSingleIssue) 73 74 r.Group(func(r chi.Router) {
··· 68 r.Get("/blob/{ref}/raw/*", s.RepoBlobRaw) 69 70 r.Route("/issues", func(r chi.Router) { 71 + r.With(middleware.Paginate).Get("/", s.RepoIssues) 72 r.Get("/{issue}", s.RepoSingleIssue) 73 74 r.Group(func(r chi.Router) {