User Forums: Sort options and pagination

* The Explore tab can now sort forums by their:
  * Most recently updated thread
  * Topics, Posts or Users (counts)
* Show owner information in forum cards
* Passive pagination support for the "My List" on forum home page.
  * Only visible when there are >20 favorited Forums.
This commit is contained in:
Noah Petherbridge 2024-08-26 20:47:14 -07:00
parent 3921691319
commit 56a6190ce9
5 changed files with 99 additions and 9 deletions

View File

@ -24,6 +24,7 @@ var (
PageSizeInboxThread = 10 // conversation view PageSizeInboxThread = 10 // conversation view
PageSizeBrowseForums = 20 PageSizeBrowseForums = 20
PageSizeForums = 100 // TODO: for main category index view PageSizeForums = 100 // TODO: for main category index view
PageSizeMyListForums = 20 // "My List" pager on forum home (categories) page.
PageSizeThreadList = 20 // 20 threads per board, 20 posts per thread PageSizeThreadList = 20 // 20 threads per board, 20 posts per thread
PageSizeForumAdmin = 20 PageSizeForumAdmin = 20
PageSizeDashboardNotifications = 50 PageSizeDashboardNotifications = 50

View File

@ -20,6 +20,13 @@ func Explore() http.HandlerFunc {
"created_at asc", "created_at asc",
"title asc", "title asc",
"title desc", "title desc",
// Special sort handlers.
// See PaginateForums for expanded handlers for these.
"by_latest",
"by_threads",
"by_posts",
"by_users",
} }
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

View File

@ -33,14 +33,13 @@ func Landing() http.HandlerFunc {
// Get all the categorized index forums. // Get all the categorized index forums.
// XXX: we get a large page size to get ALL official forums // XXX: we get a large page size to get ALL official forums
var pager = &models.Pagination{ // This pager is hardcoded and doesn't parse from ?page= params.
var indexPager = &models.Pagination{
Page: 1, Page: 1,
PerPage: config.PageSizeForums, PerPage: config.PageSizeForums,
Sort: "title asc", Sort: "title asc",
} }
pager.ParsePage(r) forums, err := models.PaginateForums(currentUser, config.ForumCategories, nil, false, indexPager)
forums, err := models.PaginateForums(currentUser, config.ForumCategories, nil, false, pager)
if err != nil { if err != nil {
session.FlashError(w, r, "Couldn't paginate forums: %s", err) session.FlashError(w, r, "Couldn't paginate forums: %s", err)
templates.Redirect(w, "/") templates.Redirect(w, "/")
@ -51,6 +50,12 @@ func Landing() http.HandlerFunc {
categorized := models.CategorizeForums(forums, config.ForumCategories) categorized := models.CategorizeForums(forums, config.ForumCategories)
// Inject the "My List" Category if the user subscribes to forums. // Inject the "My List" Category if the user subscribes to forums.
var pager = &models.Pagination{
Page: 1,
PerPage: config.PageSizeMyListForums,
Sort: "by_latest",
}
pager.ParsePage(r)
if config.UserForumsEnabled { if config.UserForumsEnabled {
myList, err := models.PaginateForums(currentUser, nil, nil, true, pager) myList, err := models.PaginateForums(currentUser, nil, nil, true, pager)
if err != nil { if err != nil {

View File

@ -85,6 +85,13 @@ Parameters:
- userID: of who is looking - userID: of who is looking
- categories: optional, filter within categories - categories: optional, filter within categories
- pager - pager
The pager Sort accepts a couple of custom values for more advanced sorting:
- by_latest: recently updated posts
- by_threads: thread count
- by_posts: post count
- by_users: user count
*/ */
func PaginateForums(user *User, categories []string, search *Search, subscribed bool, pager *Pagination) ([]*Forum, error) { func PaginateForums(user *User, categories []string, search *Search, subscribed bool, pager *Pagination) ([]*Forum, error) {
var ( var (
@ -159,6 +166,37 @@ func PaginateForums(user *User, categories []string, search *Search, subscribed
) )
} }
// Custom SORT parameters.
switch pager.Sort {
case "by_latest":
pager.Sort = `(
SELECT MAX(threads.updated_at)
FROM threads
WHERE threads.forum_id = forums.id
) DESC NULLS LAST`
case "by_threads":
pager.Sort = `(
SELECT count(threads.id)
FROM threads
WHERE threads.forum_id = forums.id
) DESC`
case "by_posts":
pager.Sort = `(
SELECT count(comments.id)
FROM threads
JOIN comments ON comments.table_name='threads' AND comments.table_id=threads.id
WHERE threads.forum_id = forums.id
) DESC`
case "by_users":
pager.Sort = `(
SELECT count(distinct(users.id))
FROM threads
JOIN comments ON comments.table_name='threads' AND comments.table_id=threads.id
JOIN users ON comments.user_id=users.id
WHERE threads.forum_id = forums.id
) DESC`
}
query = query.Order(pager.Sort) query = query.Order(pager.Sort)
query.Model(&Forum{}).Count(&pager.Total) query.Model(&Forum{}).Count(&pager.Total)
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&fs) result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&fs)

View File

@ -87,9 +87,17 @@
<label class="label" for="sort">Sort by:</label> <label class="label" for="sort">Sort by:</label>
<div class="select is-fullwidth"> <div class="select is-fullwidth">
<select id="sort" name="sort"> <select id="sort" name="sort">
<option value="title asc"{{if eq .Sort "title asc"}} selected{{end}}>Title (A-Z)</option> <optgroup label="Forum">
<option value="title desc"{{if eq .Sort "title desc"}} selected{{end}}>Title (Z-A)</option> <option value="title asc"{{if eq .Sort "title asc"}} selected{{end}}>Title (A-Z)</option>
<option value="created_at desc"{{if eq .Sort "created_at desc"}} selected{{end}}>Recently created</option> <option value="title desc"{{if eq .Sort "title desc"}} selected{{end}}>Title (Z-A)</option>
<option value="created_at desc"{{if eq .Sort "created_at desc"}} selected{{end}}>Recently created</option>
</optgroup>
<optgroup label="Contents">
<option value="by_latest"{{if eq .Sort "by_latest"}} selected{{end}}>Latest post</option>
<option value="by_threads"{{if eq .Sort "by_threads"}} selected{{end}}>Topic count</option>
<option value="by_posts"{{if eq .Sort "by_posts"}} selected{{end}}>Post count</option>
<option value="by_users"{{if eq .Sort "by_users"}} selected{{end}}>User count</option>
</optgroup>
</select> </select>
</div> </div>
</div> </div>
@ -121,16 +129,22 @@
{{end}} {{end}}
{{range .Categories}} {{range .Categories}}
{{$IsMyList := eq .Category "My List"}}
<div class="block p-4"> <div class="block p-4">
{{if .Category}} {{if .Category}}
<h1 class="title"> <h1 class="title">
{{.Category}} {{.Category}}
{{if eq .Category "My List"}} {{if $IsMyList}}
<i class="fa fa-book-bookmark ml-2"></i> <i class="fa fa-book-bookmark ml-2"></i>
{{end}} {{end}}
</h1> </h1>
{{end}} {{end}}
<!-- My List: show pagers if needed -->
{{if and $IsMyList (gt $Root.Pager.Pages 1)}}
{{SimplePager $Root.Pager}}
{{end}}
{{if eq (len .Forums) 0}} {{if eq (len .Forums) 0}}
<em> <em>
There are no forums under this category. There are no forums under this category.
@ -159,7 +173,7 @@
{{end}} {{end}}
</div> </div>
<div> <div class="mb-3">
{{if .Explicit}} {{if .Explicit}}
<span class="tag is-danger is-light"> <span class="tag is-danger is-light">
<span class="icon"><i class="fa fa-fire"></i></span> <span class="icon"><i class="fa fa-fire"></i></span>
@ -189,6 +203,25 @@
{{end}} {{end}}
</div> </div>
<!-- Owner line -->
{{if .Category}}
<div class="mt-2 has-text-grey" style="font-size: smaller">
by <a href="/f/{{.Fragment}}">{{PrettyTitle}}</a>
</div>
{{else}}
<div class="mt-2 has-text-grey" style="font-size: smaller">
by
{{template "avatar-16x16" .Owner}}
{{if .Owner.Username}}
<a href="/u/{{.Owner.Username}}" class="has-text-grey">
<strong>{{or .Owner.Username "[unavailable]"}}</strong>
</a>
{{else}}
[unavailable]
{{end}}
</div>
{{end}}
</div> </div>
<div class="column py-1"> <div class="column py-1">
<div class="box has-background-success-light has-text-dark"> <div class="box has-background-success-light has-text-dark">
@ -251,6 +284,12 @@
</div> </div>
{{end}} {{end}}
{{end}} {{end}}
<!-- My List: show pagers -->
{{if and $IsMyList (gt $Root.Pager.Pages 1)}}
{{SimplePager $Root.Pager}}
<hr class="mb-0">
{{end}}
</div> </div>
{{end}}<!-- range .Categories --> {{end}}<!-- range .Categories -->