Spit and polish

* Show follower counts on forums
* Sort by popularity (follow count)
This commit is contained in:
Noah Petherbridge 2024-08-26 21:36:48 -07:00
parent 56a6190ce9
commit 242333d8b7
8 changed files with 128 additions and 27 deletions

View File

@ -124,7 +124,7 @@ const (
ThreadViewDebounceCooldown = 1 * time.Hour ThreadViewDebounceCooldown = 1 * time.Hour
// Enable user-owned forums (feature flag) // Enable user-owned forums (feature flag)
UserForumsEnabled = false UserForumsEnabled = true
) )
// User-Owned Forums: Quota settings for how many forums a user can own. // User-Owned Forums: Quota settings for how many forums a user can own.

View File

@ -23,6 +23,7 @@ func Explore() http.HandlerFunc {
// Special sort handlers. // Special sort handlers.
// See PaginateForums for expanded handlers for these. // See PaginateForums for expanded handlers for these.
"by_followers",
"by_latest", "by_latest",
"by_threads", "by_threads",
"by_posts", "by_posts",
@ -99,6 +100,7 @@ func Explore() http.HandlerFunc {
"Categories": categorized, "Categories": categorized,
"ForumMap": forumMap, "ForumMap": forumMap,
"FollowMap": followMap, "FollowMap": followMap,
"FollowersMap": models.MapForumFollowers(forums),
// Search filters // Search filters
"SearchTerm": searchTerm, "SearchTerm": searchTerm,

View File

@ -81,12 +81,13 @@ func Forum() http.HandlerFunc {
} }
var vars = map[string]interface{}{ var vars = map[string]interface{}{
"Forum": forum, "Forum": forum,
"ForumModerators": mods, "ForumModerators": mods,
"IsForumSubscribed": models.IsForumSubscribed(currentUser, forum), "ForumSubscriberCount": models.CountForumMemberships(forum),
"Threads": threads, "IsForumSubscribed": models.IsForumSubscribed(currentUser, forum),
"ThreadMap": threadMap, "Threads": threads,
"Pager": pager, "ThreadMap": threadMap,
"Pager": pager,
} }
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

@ -76,10 +76,11 @@ func Landing() http.HandlerFunc {
followMap := models.MapForumMemberships(currentUser, forums) followMap := models.MapForumMemberships(currentUser, forums)
var vars = map[string]interface{}{ var vars = map[string]interface{}{
"Pager": pager, "Pager": pager,
"Categories": categorized, "Categories": categorized,
"ForumMap": forumMap, "ForumMap": forumMap,
"FollowMap": followMap, "FollowMap": followMap,
"FollowersMap": models.MapForumFollowers(forums),
// Current viewer's forum quota. // Current viewer's forum quota.
"ForumQuota": models.ComputeForumQuota(currentUser), "ForumQuota": models.ComputeForumQuota(currentUser),

View File

@ -168,6 +168,12 @@ func PaginateForums(user *User, categories []string, search *Search, subscribed
// Custom SORT parameters. // Custom SORT parameters.
switch pager.Sort { switch pager.Sort {
case "by_followers":
pager.Sort = `(
SELECT count(forum_memberships.id)
FROM forum_memberships
WHERE forum_memberships.forum_id = forums.id
) DESC`
case "by_latest": case "by_latest":
pager.Sort = `( pager.Sort = `(
SELECT MAX(threads.updated_at) SELECT MAX(threads.updated_at)

View File

@ -166,6 +166,16 @@ func (u *User) HasForumSubscriptions() bool {
return count > 0 return count > 0
} }
// CountForumMemberships counts how many subscribers a forum has.
func CountForumMemberships(forum *Forum) int64 {
var count int64
DB.Model(&ForumMembership{}).Where(
"forum_id = ?",
forum.ID,
).Count(&count)
return count
}
// Save a forum membership. // Save a forum membership.
func (f *ForumMembership) Save() error { func (f *ForumMembership) Save() error {
return DB.Save(f).Error return DB.Save(f).Error
@ -233,3 +243,49 @@ func MapForumMemberships(user *User, forums []*Forum) ForumMembershipMap {
return result return result
} }
// ForumFollowerMap maps table IDs to counts of memberships.
type ForumFollowerMap map[uint64]int64
// Get like stats from the map.
func (fm ForumFollowerMap) Get(id uint64) int64 {
return fm[id]
}
// MapForumFollowers maps out the count of followers for a set of forums.
func MapForumFollowers(forums []*Forum) ForumFollowerMap {
var (
result = ForumFollowerMap{}
forumIDs = []uint64{}
)
// Initialize the result set.
for _, forum := range forums {
forumIDs = append(forumIDs, forum.ID)
}
// Hold the result of the grouped count query.
type group struct {
ID uint64
Followers int64
}
var groups = []group{}
// Map the counts of likes to each of these IDs.
if res := DB.Model(
&ForumMembership{},
).Select(
"forum_id AS id, count(id) AS followers",
).Where(
"forum_id IN ?",
forumIDs,
).Group("forum_id").Scan(&groups); res.Error != nil {
log.Error("MapLikes: count query: %s", res.Error)
}
for _, row := range groups {
result[row.ID] = row.Followers
}
return result
}

View File

@ -200,6 +200,35 @@
<label class="label"><i class="fa fa-info-circle"></i> Forum Info</label> <label class="label"><i class="fa fa-info-circle"></i> Forum Info</label>
<div class="mb-4"> <div class="mb-4">
Created on: <span title="{{.Forum.CreatedAt}}">{{.Forum.CreatedAt.Format "Jan _2 2006"}}</span> Created on: <span title="{{.Forum.CreatedAt}}">{{.Forum.CreatedAt.Format "Jan _2 2006"}}</span>
{{if .ForumSubscriberCount}}
<div class="has-text-info mt-2">
<i class="fa fa-book-bookmark mr-1"></i>
{{.ForumSubscriberCount}} {{if eq .ForumSubscriberCount 1}}person follows{{else}}people people{{end}} this forum.
<!-- Follow/Unfollow This Forum -->
<form action="/forum/subscribe" method="POST" class="is-inline">
{{InputCSRF}}
<input type="hidden" name="fragment" value="{{.Forum.Fragment}}">
{{if .IsForumSubscribed}}
<button type="submit" class="button is-small ml-2"
name="intent" value="unfollow"
onclick="return confirm('Do you want to remove this forum from your list?')">
<span class="icon"><i class="fa fa-bookmark"></i></span>
<span>Unfollow</span>
</button>
{{else}}
<button type="submit" class="button is-small ml-2"
name="intent" value="follow">
<span class="icon"><i class="fa-regular fa-bookmark has-text-success"></i></span>
<span>Follow it too?</span>
</button>
{{end}}
</form>
</div>
{{end}}
<div class="mt-2"> <div class="mt-2">
{{if .Forum.Explicit}} {{if .Forum.Explicit}}
<span class="tag is-danger is-light"> <span class="tag is-danger is-light">

View File

@ -91,12 +91,13 @@
<option value="title asc"{{if eq .Sort "title asc"}} selected{{end}}>Title (A-Z)</option> <option value="title asc"{{if eq .Sort "title asc"}} selected{{end}}>Title (A-Z)</option>
<option value="title desc"{{if eq .Sort "title desc"}} selected{{end}}>Title (Z-A)</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> <option value="created_at desc"{{if eq .Sort "created_at desc"}} selected{{end}}>Recently created</option>
<option value="by_followers"{{if eq .Sort "by_followers"}} selected{{end}}>Popularity (follower count)</option>
</optgroup> </optgroup>
<optgroup label="Contents"> <optgroup label="Contents">
<option value="by_latest"{{if eq .Sort "by_latest"}} selected{{end}}>Latest post</option> <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_threads"{{if eq .Sort "by_threads"}} selected{{end}}>Topics (count of threads)</option>
<option value="by_posts"{{if eq .Sort "by_posts"}} selected{{end}}>Post count</option> <option value="by_posts"{{if eq .Sort "by_posts"}} selected{{end}}>Posts (count of threads and replies)</option>
<option value="by_users"{{if eq .Sort "by_users"}} selected{{end}}>User count</option> <option value="by_users"{{if eq .Sort "by_users"}} selected{{end}}>Users (distinct members who have posted)</option>
</optgroup> </optgroup>
</select> </select>
</div> </div>
@ -204,23 +205,28 @@
</div> </div>
<!-- Owner line --> <!-- Owner line -->
{{if .Category}}
<div class="mt-2 has-text-grey" style="font-size: smaller"> <div class="mt-2 has-text-grey" style="font-size: smaller">
by <a href="/f/{{.Fragment}}">{{PrettyTitle}}</a> {{if .Category}}
</div> by <a href="/f/{{.Fragment}}">{{PrettyTitle}}</a>
{{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}} {{else}}
[unavailable] 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}}
{{end}}
{{$FollowerCount := $Root.FollowersMap.Get .ID}}
{{if $FollowerCount}}
<span class="has-text-success ml-2" title="This forum is followed by {{$FollowerCount}} member{{Pluralize64 $FollowerCount}}.">
<i class="fa fa-book-bookmark mr-1"></i> {{$FollowerCount}}
</span>
{{end}} {{end}}
</div> </div>
{{end}}
</div> </div>
<div class="column py-1"> <div class="column py-1">