WIP: Deduplicate threads on Newest forum tab

This commit is contained in:
Noah Petherbridge 2024-02-14 21:38:20 -08:00
parent 7ceb14053b
commit 3c0473c633
4 changed files with 84 additions and 18 deletions

View File

@ -14,6 +14,11 @@ import (
func Newest() http.HandlerFunc { func Newest() http.HandlerFunc {
tmpl := templates.Must("forum/newest.html") tmpl := templates.Must("forum/newest.html")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Query parameters.
var (
allComments = r.FormValue("all") == "true"
)
// Get the current user. // Get the current user.
currentUser, err := session.CurrentUser(r) currentUser, err := session.CurrentUser(r)
if err != nil { if err != nil {
@ -29,7 +34,7 @@ func Newest() http.HandlerFunc {
} }
pager.ParsePage(r) pager.ParsePage(r)
posts, err := models.PaginateRecentPosts(currentUser, config.ForumCategories, pager) posts, err := models.PaginateRecentPosts(currentUser, config.ForumCategories, allComments, 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, "/")
@ -50,6 +55,7 @@ func Newest() http.HandlerFunc {
"Pager": pager, "Pager": pager,
"RecentPosts": posts, "RecentPosts": posts,
"PhotoMap": photos, "PhotoMap": photos,
"AllComments": allComments,
} }
if err := tmpl.Execute(w, r, vars); err != nil { if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -1,9 +1,11 @@
package models package models
import ( import (
"fmt"
"strings" "strings"
"time" "time"
"code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/log" "code.nonshy.com/nonshy/website/pkg/log"
) )
@ -20,7 +22,7 @@ type RecentPost struct {
} }
// PaginateRecentPosts returns all of the comments on a forum paginated. // PaginateRecentPosts returns all of the comments on a forum paginated.
func PaginateRecentPosts(user *User, categories []string, pager *Pagination) ([]*RecentPost, error) { func PaginateRecentPosts(user *User, categories []string, allComments bool, pager *Pagination) ([]*RecentPost, error) {
var ( var (
result = []*RecentPost{} result = []*RecentPost{}
query = (&Comment{}).Preload() query = (&Comment{}).Preload()
@ -65,12 +67,53 @@ func PaginateRecentPosts(user *User, categories []string, pager *Pagination) ([]
CommentID uint64 CommentID uint64
ThreadID *uint64 ThreadID *uint64
ForumID *uint64 ForumID *uint64
UpdatedAt time.Time
} }
var scan []scanner var scan []scanner
// Deduplicate forum threads: if one thread is BLOWING UP with replies, we should only
// mention the thread once and show the newest comment so it doesn't spam the whole page.
if config.Current.Database.IsPostgres && !allComments {
// Deduplicating is supported in Postgres but not SQLite. We also need a custom
// total count query which is 99% the same as the select query except for the
// SELECT and ORDER BY bookends.
subquery := fmt.Sprintf(`
FROM (
SELECT DISTINCT ON (threads.id)
comments.id AS comment_id,
threads.id AS thread_id,
forums.id AS forum_id,
comments.updated_at AS updated_at
FROM comments
LEFT OUTER JOIN threads ON (table_name='threads' AND table_id=threads.id)
LEFT OUTER JOIN forums ON (threads.forum_id=forums.id)
WHERE %s
ORDER BY threads.id
) AS subquery
`, strings.Join(wheres, " AND "))
query = DB.Raw(fmt.Sprintf(`
SELECT comment_id, thread_id, forum_id, updated_at
%s
ORDER BY subquery.updated_at DESC
OFFSET %d LIMIT %d
`, subquery, pager.GetOffset(), pager.PerPage), placeholders...)
// Get a count of records.
DB.Raw(fmt.Sprintf("SELECT count(*) %s", subquery), placeholders...).Count(&pager.Total)
query = query.Find(&scan)
if query.Error != nil {
return nil, query.Error
}
} else {
// SQLite/non-Postgres doesn't support DISTINCT ON, this is the old query which
// shows objectively all comments and a popular thread may dominate the page.
query = DB.Table("comments").Select( query = DB.Table("comments").Select(
`comments.id AS comment_id, `comments.id AS comment_id,
threads.id AS thread_id, threads.id AS thread_id,
forums.id AS forum_id`, forums.id AS forum_id,
comments.updated_at AS updated_at`,
).Joins( ).Joins(
"LEFT OUTER JOIN threads ON (table_name = 'threads' AND table_id = threads.id)", "LEFT OUTER JOIN threads ON (table_name = 'threads' AND table_id = threads.id)",
).Joins( ).Joins(
@ -79,10 +122,18 @@ func PaginateRecentPosts(user *User, categories []string, pager *Pagination) ([]
strings.Join(wheres, " AND "), strings.Join(wheres, " AND "),
placeholders..., placeholders...,
).Order("comments.updated_at desc") ).Order("comments.updated_at desc")
query.Model(&Comment{}).Count(&pager.Total)
// Execute the query.
query = query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&scan)
if query.Error != nil {
return nil, query.Error
}
}
// Get the total for the pager and scan the page of ID sets. // Get the total for the pager and scan the page of ID sets.
query.Model(&Comment{}).Count(&pager.Total) // query.Model(&Comment{}).Count(&pager.Total)
query = query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&scan) // query = query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&scan)
if query.Error != nil { if query.Error != nil {
return nil, query.Error return nil, query.Error
} }

View File

@ -99,7 +99,7 @@
</li> </li>
<li> <li>
On the On the
<a href="/members"><strong><i class="fa fa-comments mr-1"></i> Forums</strong></a> <a href="/f/circle"><strong><i class="fa fa-comments mr-1"></i> Forums</strong></a>
you can access exclusive inner circle-only boards. you can access exclusive inner circle-only boards.
</li> </li>
<li> <li>

View File

@ -39,7 +39,16 @@
</div> </div>
<div class="p-4"> <div class="p-4">
Found {{FormatNumberCommas .Pager.Total}} posts (page {{.Pager.Page}} of {{.Pager.Pages}}) Found {{FormatNumberCommas .Pager.Total}} {{if .AllComments}}posts{{else}}threads{{end}} (page {{.Pager.Page}} of {{.Pager.Pages}})
<div class="mt-2">
{{if not .AllComments}}
<!-- Default view is to deduplicate and show only threads and their newest comment -->
Showing only the latest comment per thread. <a href="?{{QueryPlus "all" "true"}}">Show all comments instead?</a>
{{else}}
Showing <strong>all</strong> forum posts by most recent. <a href="{{.Request.URL.Path}}">Deduplicate by thread?</a>
{{end}}
</div>
</div> </div>
<div class="p-4"> <div class="p-4">