Forums - Spit & polish

* On Forums landing page, show who was the most recent commenter on each
  board's most recently updated post.
* Show photo count on Profile Pages on the "Photos" tab.
* Revise the mobile and tablet top nav bar:
    * Always show small badge icons linking to the Site Gallery & Forum
    * Always show Friends & Messages badges. If no new notifications, they
      display as grey instead of yellow w/ a number.
* Put icons next to most nav bar items, especially the User Menu
* Tighten the sprawling page layouts in the Forums to be more compact
  for mobile screens.
* Fix bug where some pages scrolled horizontally on mobile: the root cause
  was divs with class="content p-2", needs minimum p-3 (but p-4 is used) to
  provide enough padding to overcome column margins which were pushing the
  page too wide on mobile.
This commit is contained in:
Noah 2022-08-25 19:58:43 -07:00
parent bb08ec56ce
commit 82f3914ae6
15 changed files with 304 additions and 102 deletions

View File

@ -61,10 +61,11 @@ func Profile() http.HandlerFunc {
likeMap := models.MapLikes(currentUser, "users", []uint64{user.ID})
vars := map[string]interface{}{
"User": user,
"LikeMap": likeMap,
"IsFriend": isFriend,
"IsPrivate": isPrivate,
"User": user,
"LikeMap": likeMap,
"IsFriend": isFriend,
"IsPrivate": isPrivate,
"PhotoCount": models.CountPhotos(user.ID),
}
if err := tmpl.Execute(w, r, vars); err != nil {

View File

@ -102,6 +102,7 @@ func UserPhotos() http.HandlerFunc {
"IsOwnPhotos": currentUser.ID == user.ID,
"User": user,
"Photos": photos,
"PhotoCount": models.CountPhotos(user.ID),
"Pager": pager,
"LikeMap": likeMap,
"ViewStyle": viewStyle,

View File

@ -30,6 +30,21 @@ func GetComment(id uint64) (*Comment, error) {
return c, result.Error
}
// GetComments queries a set of comment IDs and returns them mapped.
func GetComments(IDs []uint64) (map[uint64]*Comment, error) {
var (
mt = map[uint64]*Comment{}
ts = []*Comment{}
)
result := (&Comment{}).Preload().Where("id IN ?", IDs).Find(&ts)
for _, row := range ts {
mt[row.ID] = row
}
return mt, result.Error
}
// AddComment about anything.
func AddComment(user *User, tableName string, tableID uint64, message string) (*Comment, error) {
c := &Comment{

View File

@ -5,6 +5,7 @@ import "git.kirsle.net/apps/gosocial/pkg/log"
// ForumStatistics queries for forum-level statistics.
type ForumStatistics struct {
RecentThread *Thread
RecentPost *Comment // latest post on the recent thread
Threads uint64
Posts uint64
Users uint64
@ -30,6 +31,7 @@ func MapForumStatistics(forums []*Forum) ForumStatsMap {
result.generatePostCount(IDs)
result.generateUserCount(IDs)
result.generateRecentThreads(IDs)
result.generateRecentPosts(IDs)
return result
}
@ -201,3 +203,65 @@ func (ts ForumStatsMap) generateRecentThreads(IDs []uint64) {
}
}
}
// Compute the recent post on each recent thread of each forum.
func (ts ForumStatsMap) generateRecentPosts(IDs []uint64) {
// We already have the RecentThread of each forum - map these Thread IDs to recent comments.
var (
threadIDs = []uint64{}
threadStatsMap = map[uint64]*ForumStatistics{}
)
for _, stats := range ts {
if stats.RecentThread != nil {
threadIDs = append(threadIDs, stats.RecentThread.ID)
threadStatsMap[stats.RecentThread.ID] = stats
}
}
// The newest posts in these threads.
type scanner struct {
ThreadID uint64
CommentID uint64
}
var scan []scanner
err := DB.Table(
"comments",
).Select(
"table_id AS thread_id, id AS comment_id",
// "forum_id, id AS thread_id, updated_at",
).Where(
`table_name='threads' AND table_id IN ?
AND updated_at = (SELECT MAX(updated_at)
FROM comments c2
WHERE c2.table_name=comments.table_name
AND c2.table_id=comments.table_id
)`,
threadIDs,
).Order(
"updated_at desc",
).Scan(&scan)
if err.Error != nil {
log.Error("Getting most recent post IDs: %s", err.Error)
}
// Gather the ThreadID:CommentID map.
var (
commentIDs = []uint64{}
commentStatsMap = map[uint64]*ForumStatistics{}
)
for _, row := range scan {
if stats, ok := threadStatsMap[row.ThreadID]; ok {
commentStatsMap[row.CommentID] = stats
commentIDs = append(commentIDs, row.CommentID)
}
}
// Select all these comments and map them in.
if commentMap, err := GetComments(commentIDs); err == nil {
for commentId, comment := range commentMap {
if stats, ok := commentStatsMap[commentId]; ok {
stats.RecentPost = comment
}
}
}
}

View File

@ -5,6 +5,7 @@ import (
"strings"
"time"
"git.kirsle.net/apps/gosocial/pkg/log"
"gorm.io/gorm"
)
@ -122,13 +123,16 @@ func PaginateUserPhotos(userID uint64, visibility []PhotoVisibility, explicitOK
}
// CountPhotos returns the total number of photos on a user's account.
func CountPhotos(userID uint64) (int64, error) {
func CountPhotos(userID uint64) int64 {
var count int64
result := DB.Where(
"user_id = ?",
userID,
).Model(&Photo{}).Count(&count)
return count, result.Error
if result.Error != nil {
log.Error("CountPhotos(%d): %s", userID, result.Error)
}
return count
}
// CountExplicitPhotos returns the number of explicit photos a user has (so non-explicit viewers can see some do exist)

View File

@ -8,7 +8,7 @@ import (
// QuoteForUser returns the current photo usage quota for a given user.
func QuotaForUser(u *models.User) (current, allowed int) {
// Count their photos.
count, _ := models.CountPhotos(u.ID)
count := models.CountPhotos(u.ID)
// What is their quota at?
var quota int

View File

@ -43,7 +43,7 @@
/* Mobile: notification badge near the hamburger menu */
.nonshy-mobile-notification {
position: absolute;
top: 12px;
top: 10px;
right: 50px;
z-index: 1000;
}

View File

@ -42,6 +42,11 @@ document.addEventListener('DOMContentLoaded', () => {
// Touching the user drop-down button toggles it.
userMenu.addEventListener("touchstart", (e) => {
// On mobile/tablet screens they had to hamburger menu their way here anyway, let it thru.
if (screen.width < 1024) {
return;
}
e.preventDefault();
if (userMenu.classList.contains(activeClass)) {
userMenu.classList.remove(activeClass);

View File

@ -194,7 +194,10 @@
<span class="icon is-small">
<i class="fa fa-image"></i>
</span>
<span>Photos</span>
<span>
Photos
{{if .PhotoCount}}<span class="tag is-link is-light ml-1">{{.PhotoCount}}</span>{{end}}
</span>
</a>
</li>
</ul>

View File

@ -47,16 +47,16 @@
<span>Home</span>
</a>
<a class="navbar-item" href="/photo/gallery">
<span class="icon"><i class="fa fa-image"></i></span>
<span>Gallery</span>
</a>
<a class="navbar-item" href="/forum">
<span class="icon"><i class="fa fa-comments"></i></span>
<span>Forum</span>
</a>
<a class="navbar-item" href="/photo/gallery">
<span class="icon"><i class="fa fa-image"></i></span>
<span>Gallery</span>
</a>
<a class="navbar-item" href="/friends{{if gt .NavFriendRequests 0}}?view=requests{{end}}">
<span class="icon"><i class="fa fa-user-group"></i></span>
<span>Friends</span>
@ -83,23 +83,29 @@
</a>
{{end}}
<a class="navbar-item" href="/about">
About
<span class="icon"><i class="fa fa-circle-info"></i></span>
<span>About {{PrettyTitle}}</span>
</a>
<a class="navbar-item" href="/faq">
FAQ
<span class="icon"><i class="fa fa-circle-question"></i></span>
<span>FAQ</span>
</a>
<a class="navbar-item" href="/tos">
Terms of Service
<span class="icon"><i class="fa fa-list"></i></span>
<span>Terms of Service</span>
</a>
<a class="navbar-item" href="/privacy">
Privacy Policy
<span class="icon"><i class="fa fa-file-shield"></i></span>
<span>Privacy Policy</span>
</a>
<a class="navbar-item" href="/contact">
Contact
<span class="icon"><i class="fa fa-message"></i></span>
<span>Contact</span>
</a>
<hr class="navbar-divider">
<a class="navbar-item" href="/contact?intent=report">
Report an issue
<span class="icon"><i class="fa fa-triangle-exclamation"></i></span>
<span>Report an issue</span>
</a>
</div>
</div>
@ -129,7 +135,8 @@
<div class="navbar-dropdown is-right is-hoverable">
<a class="navbar-item" href="/me{{if .NavUnreadNotifications}}#notifications{{end}}">
Dashboard
<span class="icon"><i class="fa fa-home-user"></i></span>
<span>Dashboard</span>
{{if .NavUnreadNotifications}}
<span class="tag is-warning ml-1">
<span class="icon"><i class="fa fa-bell"></i></span>
@ -137,13 +144,26 @@
</span>
{{end}}
</a>
<a class="navbar-item" href="/u/{{.CurrentUser.Username}}">My Profile</a>
<a class="navbar-item" href="/photo/u/{{.CurrentUser.Username}}">My Photos</a>
<a class="navbar-item" href="/photo/upload">Upload Photo</a>
<a class="navbar-item" href="/settings">Settings</a>
<a class="navbar-item" href="/u/{{.CurrentUser.Username}}">
<span class="icon"><i class="fa fa-user"></i></span>
<span>My Profile</span>
</a>
<a class="navbar-item" href="/photo/u/{{.CurrentUser.Username}}">
<span class="icon"><i class="fa fa-image"></i></span>
<span>My Photos</span>
</a>
<a class="navbar-item" href="/photo/upload">
<span class="icon"><i class="fa fa-upload"></i></span>
<span>Upload Photo</span>
</a>
<a class="navbar-item" href="/settings">
<span class="icon"><i class="fa fa-gear"></i></span>
<span>Settings</span>
</a>
{{if .CurrentUser.IsAdmin}}
<a class="navbar-item has-text-danger" href="/admin">
Admin
<span class="icon"><i class="fa fa-gavel"></i></span>
<span>Admin</span>
{{if .NavAdminNotifications}}<span class="tag is-danger ml-1">{{.NavAdminNotifications}}</span>{{end}}
</a>
{{end}}
@ -153,7 +173,10 @@
<span>Unimpersonate</span>
</a>
{{end}}
<a class="navbar-item" href="/logout">Log out</a>
<a class="navbar-item" href="/logout">
<span class="icon"><i class="fa fa-arrow-right-from-bracket"></i></span>
<span>Log out</span>
</a>
</div>
</div>
{{ else }}
@ -173,33 +196,45 @@
</nav>
<!-- Mobile: notifications badge next to hamburger menu -->
{{if gt .NavTotalNotifications 0}}
{{if .LoggedIn}}
<div class="mobile nonshy-mobile-notification">
{{if gt .NavFriendRequests 0}}
<a class="tag is-warning" href="/friends?view=requests">
<span class="icon"><i class="fa fa-user-group"></i></span>
<span>{{.NavFriendRequests}}</span>
</a>
{{end}}
<a class="tag is-grey py-4"
href="/forum">
<span class="icon"><i class="fa fa-comments"></i></span>
</a>
{{if gt .NavUnreadMessages 0}}
<a class="tag is-warning" href="/messages">
<span class="icon"><i class="fa fa-envelope"></i></span>
<span>{{.NavUnreadMessages}}</span>
</a>
{{end}}
<a class="tag is-grey py-4"
href="/photo/gallery">
<span class="icon"><i class="fa fa-image"></i></span>
</a>
<a class="tag {{if gt .NavFriendRequests 0}}is-warning{{else}}is-grey{{end}} py-4"
href="/friends{{if gt .NavFriendRequests 0}}?view=requests{{end}}">
<span class="icon"><i class="fa fa-user-group"></i></span>
{{if gt .NavFriendRequests 0}}
<small>{{.NavFriendRequests}}</small>
{{end}}
</a>
<a class="tag {{if gt .NavUnreadMessages 0}}is-warning{{else}}is-grey{{end}} py-4" href="/messages">
<span class="icon"><i class="fa fa-envelope"></i></span>
{{if gt .NavUnreadMessages 0}}
<small>{{.NavUnreadMessages}}</small>
{{end}}
</a>
{{if gt .NavUnreadNotifications 0}}
<a class="tag is-warning" href="/me#notifications">
<a class="tag is-warning py-4" href="/me#notifications">
<span class="icon"><i class="fa fa-bell"></i></span>
<span>{{.NavUnreadNotifications}}</span>
<small>{{.NavUnreadNotifications}}</small>
</a>
{{end}}
{{if gt .NavAdminNotifications 0}}
<a class="tag is-danger" href="/admin">
<a class="tag is-danger py-4" href="/admin">
<span class="icon"><i class="fa fa-gavel"></i></span>
<span>{{.NavAdminNotifications}}</span>
<small>{{.NavAdminNotifications}}</small>
</a>
{{end}}
</div>
@ -228,7 +263,7 @@
{{template "content" .}}
<div class="block has-text-centered has-text-grey">
<div class="block p-4 has-text-centered has-text-grey">
&copy; {{.YYYY}} {{.Title}}
<div class="columns">
<div class="column">

View File

@ -118,6 +118,77 @@
content from other users -- by default this site is "normal nudes" friendly!
</p>
<h1>Forum FAQs</h1>
<h3>What do the various badges on the forum mean?</h3>
<p>
You may see some of these badges on the forums or their posts. These are their meanings:
</p>
<ul>
<li>
<span class="tag is-danger is-light">
<span class="icon"><i class="fa fa-fire"></i></span>
<span>Explicit</span>
</span> -
on a forum it means the entire forum is "<abbr title="Not Safe For Work">NSFW</abbr>";
but individual topics within an otherwise non-explicit forum may also opt in to the
Explicit tag if its content is border-line. You will not see any Explicit forums or
posts unless you opt-in to see explicit content in your <a href="/settings">settings</a>.
</li>
<li>
<span class="tag is-warning is-light">
<span class="icon"><i class="fa fa-gavel"></i></span>
<span>Privileged</span>
</span> -
only a forum's moderators can create new topics in a Privileged forum (such as the
forum for Site Announcements). Moderators include the site admins, the creator of
the forum, and any additional moderators appointed by the forum creator.
</li>
<li>
<span class="tag is-success is-light">
<span class="icon"><i class="fa fa-thumbtack"></i></span>
<span>Pinned</span>
</span> -
these forum posts are pinned to the top of a forum, appearing above regular posts
on the first page of the forum.
</li>
<li>
<span class="tag is-warning is-light">
<span class="icon"><i class="fa fa-ban"></i></span>
<span>No Reply</span>
</span> -
topics with this badge can not accept any new replies. Some types of announcement
posts may start with this badge from the beginning; other threads that are locked
by a moderator may gain this badge if the conversation was going off the rails.
</li>
</ul>
<h3>Can I create my own forums?</h3>
<p>
This feature is coming soon! Users will be allowed to create their own forums and
act as moderator within their own board. The forum admin pages need a bit more
spit &amp; polish before it's ready!
</p>
<p>
Some related features with managing your own forums will include:
</p>
<ul>
<li>
You'll be able to make your forum "invite-only" if you want, where only approved
members can see and reply to threads.
</li>
<li>
You'll be able to choose other users to help you moderate your forum. As the forum
owner, you'll retain admin control of your forum unless you assign ownership away
to another member.
</li>
</ul>
<h1>Technical FAQs</h1>
<h3>Why did you build a custom website?</h3>

View File

@ -17,7 +17,7 @@
{{$Root := .}}
<div class="block px-4">
<div class="block p-4">
<div class="columns">
<div class="column">
<nav class="breadcrumb" aria-label="breadcrumbs">
@ -77,12 +77,12 @@
<div class="block p-2">
{{range .Threads}}
{{$Stats := $Root.ThreadMap.Get .ID}}
<div class="box has-background-link-light">
<div class="box has-background-success-light">
<div class="columns">
<div class="column is-2 has-text-centered">
<div class="column is-2 has-text-centered pt-0 pb-1">
<div>
<a href="/u/{{.Comment.User.Username}}">
<figure class="image is-96x96 is-inline-block">
<figure class="image is-64x64 is-inline-block">
{{if .Comment.User.ProfilePhoto.ID}}
<img src="{{PhotoURL .Comment.User.ProfilePhoto.CroppedFilename}}">
{{else}}
@ -93,16 +93,16 @@
</div>
<a href="/u/{{.Comment.User.Username}}">{{.Comment.User.Username}}</a>
</div>
<div class="column content">
<div class="column content pt-1 pb-0">
<a href="/forum/thread/{{.ID}}" class="has-text-dark">
<h1 class="title pt-0">
{{if .Pinned}}<sup class="fa fa-thumbtack has-text-success mr-2 is-size-5" title="Pinned"></sup>{{end}}
<h2 class="is-size-4 pt-0">
{{if .Pinned}}<sup class="fa fa-thumbtack has-text-success mr-2 is-size-6" title="Pinned"></sup>{{end}}
{{or .Title "Untitled"}}
</h1>
</h2>
{{TrimEllipses .Comment.Message 256}}
</a>
<hr class="mb-1">
<hr class="has-background-success my-2">
<div>
{{if .Pinned}}
<span class="tag is-success is-light mr-2">
@ -128,10 +128,10 @@
<em title="{{.UpdatedAt.Format "2006-01-02 15:04:05"}}">Updated {{SincePrettyCoarse .UpdatedAt}} ago</em>
</div>
</div>
<div class="column is-narrow">
<div class="column is-narrow pb-1 pt-0 px-1">
<div class="columns is-mobile">
<div class="column has-text-centered">
<div class="box">
<div class="column has-text-centered pr-1">
<div class="box p-2">
<p class="is-size-7">Replies</p>
{{if $Stats}}
{{$Stats.Replies}}
@ -140,8 +140,8 @@
{{end}}
</div>
</div>
<div class="column has-text-centered">
<div class="box">
<div class="column has-text-centered pl-1">
<div class="box p-2">
<p class="is-size-7">Views</p>
{{if $Stats}}
{{$Stats.Views}}

View File

@ -4,32 +4,32 @@
<section class="hero is-light is-success">
<div class="hero-body">
<div class="container">
<h1 class="title">
<span class="icon mr-4"><i class="fa fa-comments"></i></span>
<span>Forums</span>
</h1>
<div class="level">
<div class="level-left">
<h1 class="title">
<span class="icon mr-4"><i class="fa fa-comments"></i></span>
<span>Forums</span>
</h1>
</div>
{{if .CurrentUser.IsAdmin}}
<div class="level-right">
<div>
<a href="/forum/admin" class="button is-small has-text-danger">
<span class="icon"><i class="fa fa-gavel"></i></span>
<span>Manage Forums</span>
</a>
</div>
</div>
{{end}}
</div>
</div>
</div>
</section>
</div>
<div class="block p-2">
<div class="columns">
<div class="column">To Do</div>
{{if .CurrentUser.IsAdmin}}
<div class="column is-narrow">
<a href="/forum/admin" class="button is-small has-text-danger">
<span class="icon"><i class="fa fa-gavel"></i></span>
<span>Manage Forums</span>
</a>
</div>
{{end}}
</div>
</div>
{{$Root := .}}
{{range .Categories}}
<div class="block p-2">
<div class="block p-4">
<h1 class="title">{{.Category}}</h1>
{{if eq (len .Forums) 0}}
@ -41,20 +41,15 @@
{{range .Forums}}
{{$Stats := $Root.ForumMap.Get .ID}}
<div class="card block has-background-primary-light">
<!-- <header class="card-header has-background-success">
<p class="card-header-title has-text-light">
{{.Title}}
</p>
</header> -->
<div class="card-content">
<div class="columns">
<div class="column">
<div class="column is-3 pt-0 pb-1">
<h1 class="title">
<a href="/f/{{.Fragment}}">{{.Title}}</a>
</h1>
<h2 class="is-size-4">
<strong><a href="/f/{{.Fragment}}">{{.Title}}</a></strong>
</h2>
<div class="content">
<div class="content mb-1">
{{if .Description}}
{{ToMarkdown .Description}}
{{else}}
@ -79,26 +74,31 @@
</div>
</div>
<div class="column">
<div class="column py-1">
<div class="box has-background-success-light">
<h2 class="subtitle mb-0">Latest Post</h2>
<h2 class="subtitle mb-1">Latest Post</h2>
{{if $Stats.RecentThread}}
<a href="/forum/thread/{{$Stats.RecentThread.ID}}">
<strong>{{$Stats.RecentThread.Title}}</strong>
</a>
<em>by {{$Stats.RecentThread.Comment.User.Username}}</em>
<div>
<small title="{{$Stats.RecentThread.UpdatedAt.Format "2006-01-02 15:04:05"}}">Last updated {{SincePrettyCoarse $Stats.RecentThread.UpdatedAt}} ago</small>
<em>
{{if and $Stats.RecentPost (not (eq $Stats.RecentPost.ID $Stats.RecentThread.CommentID))}}
<small>Last comment by {{$Stats.RecentPost.User.Username}}</small>
{{end}}
<small title="{{$Stats.RecentThread.UpdatedAt.Format "2006-01-02 15:04:05"}}">{{SincePrettyCoarse $Stats.RecentThread.UpdatedAt}} ago</small>
</em>
</div>
{{else}}
<em>No posts found.</em>
{{end}}
</div>
</div>
<div class="column is-3">
<div class="columns is-mobile">
<div class="column has-text-centered">
<div class="box has-background-warning-light">
<div class="column is-3 py-1">
<div class="columns is-mobile is-gapless">
<div class="column has-text-centered mr-1">
<div class="box has-background-warning-light p-2">
<p class="is-size-7">Topics</p>
{{if $Stats}}
{{$Stats.Threads}}
@ -107,8 +107,8 @@
{{end}}
</div>
</div>
<div class="column has-text-centered">
<div class="box has-background-warning-light">
<div class="column has-text-centered mx-1">
<div class="box has-background-warning-light p-2">
<p class="is-size-7">Posts</p>
{{if $Stats}}
{{$Stats.Posts}}
@ -117,8 +117,8 @@
{{end}}
</div>
</div>
<div class="column has-text-centered">
<div class="box has-background-warning-light">
<div class="column has-text-centered ml-1">
<div class="box has-background-warning-light p-2">
<p class="is-size-7">Users</p>
{{if $Stats}}
{{$Stats.Users}}

View File

@ -11,7 +11,7 @@
</section>
</div>
<div class="block">
<div class="block p-4">
<div class="columns">
<div class="column content is-three-quarters p-4">
<p>

View File

@ -144,7 +144,10 @@
<span class="icon is-small">
<i class="fa fa-image"></i>
</span>
<span>Photos</span>
<span>
Photos
{{if .PhotoCount}}<span class="tag is-link is-light ml-1">{{.PhotoCount}}</span>{{end}}
</span>
</a>
</li>
</ul>
@ -177,7 +180,7 @@
<div class="level-right">
<div class="level-item">
<div class="tabs is-toggle is-small">
<div class="tabs is-toggle is-small is-hidden-mobile">
<ul>
<li{{if eq .ViewStyle "cards"}} class="is-active"{{end}}>
<a href="{{.Request.URL.Path}}?view=cards">Cards</a>