User Forums: Newest Tab, Moderators
* The "Newest" tab of the forum is updated with new filter options. * Which forums: All, Official, Community, My List * Show: By threads, All posts * The option for "Which forums" is saved in the user's preferences and set as their default on future visits, similar to the Site Gallery "Whose photos" option. * So users can subscribe to their favorite forums and always get their latest posts easily while filtering out the rest. * Forum Moderators * Add the ability to add and remove moderators for your forum. * Users are notified when they are added as a moderator. * Moderators can opt themselves out by unfollowing the forum. * ForumMembership: add unique constraint on user_id,forum_id.
This commit is contained in:
parent
eb85b2e090
commit
ed4a9f8c89
|
@ -45,7 +45,7 @@ func AddEdit() http.HandlerFunc {
|
|||
return
|
||||
} else {
|
||||
// Do we have permission?
|
||||
if found.OwnerID != currentUser.ID && !currentUser.IsAdmin {
|
||||
if !found.CanEdit(currentUser) {
|
||||
templates.ForbiddenPage(w, r)
|
||||
return
|
||||
}
|
||||
|
@ -67,8 +67,8 @@ func AddEdit() http.HandlerFunc {
|
|||
isPrivate = r.PostFormValue("private") == "true"
|
||||
)
|
||||
|
||||
// Sanity check admin-only settings.
|
||||
if !currentUser.IsAdmin {
|
||||
// Sanity check admin-only settings -> default these to OFF.
|
||||
if !currentUser.HasAdminScope(config.ScopeForumAdmin) {
|
||||
isPrivileged = false
|
||||
isPermitPhotos = false
|
||||
isPrivate = false
|
||||
|
@ -81,18 +81,25 @@ func AddEdit() http.HandlerFunc {
|
|||
models.NewFieldDiff("Description", forum.Description, description),
|
||||
models.NewFieldDiff("Category", forum.Category, category),
|
||||
models.NewFieldDiff("Explicit", forum.Explicit, isExplicit),
|
||||
models.NewFieldDiff("Privileged", forum.Privileged, isPrivileged),
|
||||
models.NewFieldDiff("PermitPhotos", forum.PermitPhotos, isPermitPhotos),
|
||||
models.NewFieldDiff("Private", forum.Private, isPrivate),
|
||||
}
|
||||
|
||||
forum.Title = title
|
||||
forum.Description = description
|
||||
forum.Category = category
|
||||
forum.Explicit = isExplicit
|
||||
forum.Privileged = isPrivileged
|
||||
forum.PermitPhotos = isPermitPhotos
|
||||
forum.Private = isPrivate
|
||||
|
||||
// Forum Admin-only options: if the current viewer is not a forum admin, do not change these settings.
|
||||
// e.g.: the front-end checkboxes are hidden and don't want to accidentally unset these!
|
||||
if currentUser.HasAdminScope(config.ScopeForumAdmin) {
|
||||
diffs = append(diffs,
|
||||
models.NewFieldDiff("Privileged", forum.Privileged, isPrivileged),
|
||||
models.NewFieldDiff("PermitPhotos", forum.PermitPhotos, isPermitPhotos),
|
||||
models.NewFieldDiff("Private", forum.Private, isPrivate),
|
||||
)
|
||||
forum.Privileged = isPrivileged
|
||||
forum.PermitPhotos = isPermitPhotos
|
||||
forum.Private = isPrivate
|
||||
}
|
||||
|
||||
// Save it.
|
||||
if err := forum.Save(); err == nil {
|
||||
|
@ -164,12 +171,20 @@ func AddEdit() http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
_ = editID
|
||||
// Get the list of moderators.
|
||||
var mods []*models.User
|
||||
if forum != nil {
|
||||
mods, err = forum.GetModerators()
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Error getting moderators list: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"EditID": editID,
|
||||
"EditForum": forum,
|
||||
"Categories": config.ForumCategories,
|
||||
"Moderators": mods,
|
||||
}
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
|
|
@ -9,8 +9,8 @@ import (
|
|||
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||
)
|
||||
|
||||
// Browse all existing forums.
|
||||
func Browse() http.HandlerFunc {
|
||||
// Explore all existing forums.
|
||||
func Explore() http.HandlerFunc {
|
||||
// This page shares a template with the board index (Categories) page.
|
||||
tmpl := templates.Must("forum/index.html")
|
||||
|
||||
|
@ -84,8 +84,8 @@ func Browse() http.HandlerFunc {
|
|||
followMap := models.MapForumMemberships(currentUser, forums)
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"CurrentForumTab": "browse",
|
||||
"IsBrowseTab": true,
|
||||
"CurrentForumTab": "explore",
|
||||
"IsExploreTab": true,
|
||||
"Pager": pager,
|
||||
"Categories": categorized,
|
||||
"ForumMap": forumMap,
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/config"
|
||||
"code.nonshy.com/nonshy/website/pkg/log"
|
||||
"code.nonshy.com/nonshy/website/pkg/models"
|
||||
"code.nonshy.com/nonshy/website/pkg/session"
|
||||
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||
|
@ -71,8 +72,15 @@ func Forum() http.HandlerFunc {
|
|||
// Map the statistics (replies, views) of these threads.
|
||||
threadMap := models.MapThreadStatistics(threads)
|
||||
|
||||
// Load the forum's moderators.
|
||||
mods, err := forum.GetModerators()
|
||||
if err != nil {
|
||||
log.Error("Getting forum moderators: %s", err)
|
||||
}
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"Forum": forum,
|
||||
"ForumModerators": mods,
|
||||
"IsForumSubscribed": models.IsForumSubscribed(currentUser, forum),
|
||||
"Threads": threads,
|
||||
"ThreadMap": threadMap,
|
||||
|
|
140
pkg/controller/forum/moderators.go
Normal file
140
pkg/controller/forum/moderators.go
Normal file
|
@ -0,0 +1,140 @@
|
|||
package forum
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/log"
|
||||
"code.nonshy.com/nonshy/website/pkg/models"
|
||||
"code.nonshy.com/nonshy/website/pkg/session"
|
||||
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||
)
|
||||
|
||||
// ManageModerators controller (/forum/admin/moderators) to appoint moderators to your (user) forum.
|
||||
func ManageModerators() http.HandlerFunc {
|
||||
// Reuse the upload page but with an EditPhoto variable.
|
||||
tmpl := templates.Must("forum/moderators.html")
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
intent = r.FormValue("intent")
|
||||
stringID = r.FormValue("forum_id")
|
||||
)
|
||||
|
||||
// Parse forum_id query parameter.
|
||||
var forumID uint64
|
||||
if stringID != "" {
|
||||
if i, err := strconv.Atoi(stringID); err == nil {
|
||||
forumID = uint64(i)
|
||||
} else {
|
||||
session.FlashError(w, r, "Edit parameter: forum_id was not an integer")
|
||||
templates.Redirect(w, "/forum/admin")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect URLs
|
||||
var (
|
||||
next = fmt.Sprintf("%s?forum_id=%d", r.URL.Path, forumID)
|
||||
nextFinished = fmt.Sprintf("/forum/admin/edit?id=%d", forumID)
|
||||
)
|
||||
|
||||
// Load the current user.
|
||||
currentUser, err := session.CurrentUser(r)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Unexpected error: could not get currentUser.")
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
}
|
||||
|
||||
// Are we adding/removing a user as moderator?
|
||||
var (
|
||||
username = r.FormValue("to")
|
||||
user *models.User
|
||||
)
|
||||
if username != "" {
|
||||
if found, err := models.FindUser(username); err != nil {
|
||||
templates.NotFoundPage(w, r)
|
||||
return
|
||||
} else {
|
||||
user = found
|
||||
}
|
||||
}
|
||||
|
||||
// Look up the forum by its fragment.
|
||||
forum, err := models.GetForum(forumID)
|
||||
if err != nil {
|
||||
templates.NotFoundPage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// User must be the owner of this forum, or a privileged admin.
|
||||
if !forum.CanEdit(currentUser) {
|
||||
templates.ForbiddenPage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// The forum owner can not add themself.
|
||||
if user != nil && forum.OwnerID == user.ID {
|
||||
session.FlashError(w, r, "You can not add the forum owner to its moderators list.")
|
||||
templates.Redirect(w, next)
|
||||
return
|
||||
}
|
||||
|
||||
// POSTing?
|
||||
if r.Method == http.MethodPost {
|
||||
switch intent {
|
||||
case "submit":
|
||||
// Confirmed adding a moderator.
|
||||
if _, err := forum.AddModerator(user); err != nil {
|
||||
session.FlashError(w, r, "Error adding the moderator: %s", err)
|
||||
templates.Redirect(w, next)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a notification for this.
|
||||
notif := &models.Notification{
|
||||
UserID: user.ID,
|
||||
AboutUser: *currentUser,
|
||||
Type: models.NotificationForumModerator,
|
||||
TableName: "forums",
|
||||
TableID: forum.ID,
|
||||
Link: fmt.Sprintf("/f/%s", forum.Fragment),
|
||||
}
|
||||
if err := models.CreateNotification(notif); err != nil {
|
||||
log.Error("Couldn't create PrivatePhoto notification: %s", err)
|
||||
}
|
||||
|
||||
session.Flash(w, r, "%s has been added to the moderators list!", user.Username)
|
||||
templates.Redirect(w, nextFinished)
|
||||
return
|
||||
case "confirm-remove":
|
||||
// Confirm removing a moderator.
|
||||
if _, err := forum.RemoveModerator(user); err != nil {
|
||||
session.FlashError(w, r, "Error removing the moderator: %s", err)
|
||||
templates.Redirect(w, next)
|
||||
return
|
||||
}
|
||||
|
||||
// Revoke any past notifications they had about being added as moderator.
|
||||
if err := models.RemoveSpecificNotification(user.ID, models.NotificationForumModerator, "forums", forum.ID); err != nil {
|
||||
log.Error("Couldn't revoke the forum moderator notification: %s", err)
|
||||
}
|
||||
|
||||
session.Flash(w, r, "%s has been removed from the moderators list.", user.Username)
|
||||
templates.Redirect(w, nextFinished)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"Forum": forum,
|
||||
"User": user,
|
||||
"IsRemoving": intent == "remove",
|
||||
}
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
|
@ -17,6 +17,9 @@ func Newest() http.HandlerFunc {
|
|||
// Query parameters.
|
||||
var (
|
||||
allComments = r.FormValue("all") == "true"
|
||||
whichForums = r.FormValue("which")
|
||||
categories = []string{}
|
||||
subscribed bool
|
||||
)
|
||||
|
||||
// Get the current user.
|
||||
|
@ -27,6 +30,29 @@ func Newest() http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
// Recall the user's default "Which forum:" answer if not selected.
|
||||
if whichForums == "" {
|
||||
whichForums = currentUser.GetProfileField("forum_newest_default")
|
||||
if whichForums == "" {
|
||||
whichForums = "official"
|
||||
}
|
||||
}
|
||||
|
||||
// Narrow down to which set of forums?
|
||||
switch whichForums {
|
||||
case "official":
|
||||
categories = config.ForumCategories
|
||||
case "community":
|
||||
categories = []string{""}
|
||||
case "followed":
|
||||
subscribed = true
|
||||
default:
|
||||
whichForums = "all"
|
||||
}
|
||||
|
||||
// Store their "Which forums" filter to be their new default view.
|
||||
currentUser.SetProfileField("forum_newest_default", whichForums)
|
||||
|
||||
// Get all the categorized index forums.
|
||||
var pager = &models.Pagination{
|
||||
Page: 1,
|
||||
|
@ -34,7 +60,7 @@ func Newest() http.HandlerFunc {
|
|||
}
|
||||
pager.ParsePage(r)
|
||||
|
||||
posts, err := models.PaginateRecentPosts(currentUser, config.ForumCategories, allComments, pager)
|
||||
posts, err := models.PaginateRecentPosts(currentUser, categories, subscribed, allComments, pager)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't paginate forums: %s", err)
|
||||
templates.Redirect(w, "/")
|
||||
|
@ -56,7 +82,10 @@ func Newest() http.HandlerFunc {
|
|||
"Pager": pager,
|
||||
"RecentPosts": posts,
|
||||
"PhotoMap": photos,
|
||||
"AllComments": allComments,
|
||||
|
||||
// Filter options.
|
||||
"WhichForums": whichForums,
|
||||
"AllComments": allComments,
|
||||
}
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
|
|
@ -3,6 +3,7 @@ package forum
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/log"
|
||||
"code.nonshy.com/nonshy/website/pkg/models"
|
||||
"code.nonshy.com/nonshy/website/pkg/session"
|
||||
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||
|
@ -51,6 +52,13 @@ func Subscribe() http.HandlerFunc {
|
|||
case "unfollow":
|
||||
fm, err := models.GetForumMembership(currentUser, forum)
|
||||
if err == nil {
|
||||
// Were we a moderator previously? If so, revoke the notification about it.
|
||||
if fm.IsModerator {
|
||||
if err := models.RemoveSpecificNotification(currentUser.ID, models.NotificationForumModerator, "forums", forum.ID); err != nil {
|
||||
log.Error("User unsubscribed from forum and couldn't remove their moderator notification: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = fm.Delete()
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't delete your forum membership: %s", err)
|
||||
|
|
|
@ -101,14 +101,15 @@ func Thread() http.HandlerFunc {
|
|||
_, isSubscribed := models.IsSubscribed(currentUser, "threads", thread.ID)
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"Forum": forum,
|
||||
"Thread": thread,
|
||||
"Comments": comments,
|
||||
"LikeMap": commentLikeMap,
|
||||
"PhotoMap": photos,
|
||||
"Pager": pager,
|
||||
"CanModerate": canModerate,
|
||||
"IsSubscribed": isSubscribed,
|
||||
"Forum": forum,
|
||||
"Thread": thread,
|
||||
"Comments": comments,
|
||||
"LikeMap": commentLikeMap,
|
||||
"PhotoMap": photos,
|
||||
"Pager": pager,
|
||||
"CanModerate": canModerate,
|
||||
"IsSubscribed": isSubscribed,
|
||||
"IsForumSubscribed": models.IsForumSubscribed(currentUser, forum),
|
||||
}
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/config"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
|
@ -27,7 +28,7 @@ type Forum struct {
|
|||
|
||||
// Preload related tables for the forum (classmethod).
|
||||
func (f *Forum) Preload() *gorm.DB {
|
||||
return DB.Preload("Owner")
|
||||
return DB.Preload("Owner").Preload("Owner.ProfilePhoto")
|
||||
}
|
||||
|
||||
// GetForum by ID.
|
||||
|
@ -69,6 +70,13 @@ func ForumByFragment(fragment string) (*Forum, error) {
|
|||
return f, result.Error
|
||||
}
|
||||
|
||||
// CanEdit checks if the user has edit rights over this forum.
|
||||
//
|
||||
// That is, they are its Owner or they are an admin with Manage Forums permission.
|
||||
func (f *Forum) CanEdit(user *User) bool {
|
||||
return user.HasAdminScope(config.ScopeForumAdmin) || f.OwnerID == user.ID
|
||||
}
|
||||
|
||||
/*
|
||||
PaginateForums scans over the available forums for a user.
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -9,11 +10,13 @@ import (
|
|||
)
|
||||
|
||||
// ForumMembership table.
|
||||
//
|
||||
// Unique key constraint pairs user_id and forum_id.
|
||||
type ForumMembership struct {
|
||||
ID uint64 `gorm:"primaryKey"`
|
||||
UserID uint64 `gorm:"index"`
|
||||
UserID uint64 `gorm:"uniqueIndex:idx_forum_membership"`
|
||||
User User `gorm:"foreignKey:user_id"`
|
||||
ForumID uint64 `gorm:"index"`
|
||||
ForumID uint64 `gorm:"uniqueIndex:idx_forum_membership"`
|
||||
Forum Forum `gorm:"foreignKey:forum_id"`
|
||||
Approved bool `gorm:"index"`
|
||||
IsModerator bool `gorm:"index"`
|
||||
|
@ -39,7 +42,7 @@ func CreateForumMembership(user *User, forum *Forum) (*ForumMembership, error) {
|
|||
return f, result.Error
|
||||
}
|
||||
|
||||
// GetForumMembership looks up a forum membership.
|
||||
// GetForumMembership looks up a forum membership, returning an error if one is not found.
|
||||
func GetForumMembership(user *User, forum *Forum) (*ForumMembership, error) {
|
||||
var (
|
||||
f = &ForumMembership{}
|
||||
|
@ -51,12 +54,88 @@ func GetForumMembership(user *User, forum *Forum) (*ForumMembership, error) {
|
|||
return f, result.Error
|
||||
}
|
||||
|
||||
// AddModerator appoints a moderator to the forum, returning that user's ForumMembership.
|
||||
//
|
||||
// If the target is not following the forum, a ForumMembership is created, marked as a moderator and returned.
|
||||
func (f *Forum) AddModerator(user *User) (*ForumMembership, error) {
|
||||
var fm *ForumMembership
|
||||
if found, err := GetForumMembership(user, f); err != nil {
|
||||
fm = &ForumMembership{
|
||||
User: *user,
|
||||
Forum: *f,
|
||||
Approved: true,
|
||||
}
|
||||
} else {
|
||||
fm = found
|
||||
}
|
||||
|
||||
// They are already a moderator?
|
||||
if fm.IsModerator {
|
||||
return fm, errors.New("they are already a moderator of this forum")
|
||||
}
|
||||
|
||||
fm.IsModerator = true
|
||||
err := fm.Save()
|
||||
return fm, err
|
||||
}
|
||||
|
||||
// RemoveModerator will unset a user's moderator flag on this forum.
|
||||
func (f *Forum) RemoveModerator(user *User) (*ForumMembership, error) {
|
||||
fm, err := GetForumMembership(user, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fm.IsModerator = false
|
||||
err = fm.Save()
|
||||
return fm, err
|
||||
}
|
||||
|
||||
// GetModerators loads all of the moderators of a forum, ordered alphabetically by username.
|
||||
func (f *Forum) GetModerators() ([]*User, error) {
|
||||
// Find all forum memberships that moderate us.
|
||||
var (
|
||||
fm = []*ForumMembership{}
|
||||
result = (&ForumMembership{}).Preload().Where(
|
||||
"forum_id = ? AND is_moderator IS TRUE",
|
||||
f.ID,
|
||||
).Find(&fm)
|
||||
)
|
||||
if result.Error != nil {
|
||||
log.Error("Forum(%d).GetModerators(): %s", f.ID, result.Error)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Load these users.
|
||||
var userIDs = []uint64{}
|
||||
for _, row := range fm {
|
||||
userIDs = append(userIDs, row.UserID)
|
||||
}
|
||||
|
||||
return GetUsersAlphabetically(userIDs)
|
||||
}
|
||||
|
||||
// IsForumSubscribed checks if the current user subscribes to this forum.
|
||||
func IsForumSubscribed(user *User, forum *Forum) bool {
|
||||
f, _ := GetForumMembership(user, forum)
|
||||
return f.UserID == user.ID
|
||||
}
|
||||
|
||||
// HasForumSubscriptions returns if the current user has at least one forum membership.
|
||||
func (u *User) HasForumSubscriptions() bool {
|
||||
var count int64
|
||||
DB.Model(&ForumMembership{}).Where(
|
||||
"user_id = ?",
|
||||
u.ID,
|
||||
).Count(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
// Save a forum membership.
|
||||
func (f *ForumMembership) Save() error {
|
||||
return DB.Save(f).Error
|
||||
}
|
||||
|
||||
// Delete a forum membership.
|
||||
func (f *ForumMembership) Delete() error {
|
||||
return DB.Delete(f).Error
|
||||
|
|
|
@ -22,7 +22,7 @@ type RecentPost struct {
|
|||
}
|
||||
|
||||
// PaginateRecentPosts returns all of the comments on a forum paginated.
|
||||
func PaginateRecentPosts(user *User, categories []string, allComments bool, pager *Pagination) ([]*RecentPost, error) {
|
||||
func PaginateRecentPosts(user *User, categories []string, subscribed, allComments bool, pager *Pagination) ([]*RecentPost, error) {
|
||||
var (
|
||||
result = []*RecentPost{}
|
||||
blockedUserIDs = BlockedUserIDs(user)
|
||||
|
@ -52,6 +52,19 @@ func PaginateRecentPosts(user *User, categories []string, allComments bool, page
|
|||
wheres = append(wheres, "forums.private is not true")
|
||||
}
|
||||
|
||||
// Forums I follow?
|
||||
if subscribed {
|
||||
wheres = append(wheres, `
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM forum_memberships
|
||||
WHERE user_id = ?
|
||||
AND forum_id = forums.id
|
||||
)
|
||||
`)
|
||||
placeholders = append(placeholders, user.ID)
|
||||
}
|
||||
|
||||
// Blocked users?
|
||||
if len(blockedUserIDs) > 0 {
|
||||
comment_wheres = append(comment_wheres, "comments.user_id NOT IN ?")
|
||||
|
|
|
@ -45,7 +45,8 @@ const (
|
|||
NotificationCertApproved NotificationType = "cert_approved"
|
||||
NotificationPrivatePhoto NotificationType = "private_photo" // private photo grants
|
||||
NotificationNewPhoto NotificationType = "new_photo"
|
||||
NotificationCustom NotificationType = "custom" // custom message pushed
|
||||
NotificationForumModerator NotificationType = "forum_moderator" // appointed as a forum moderator
|
||||
NotificationCustom NotificationType = "custom" // custom message pushed
|
||||
)
|
||||
|
||||
// CreateNotification inserts a new notification into the database.
|
||||
|
@ -372,9 +373,11 @@ func (n *Notification) Delete() error {
|
|||
type NotificationBody struct {
|
||||
PhotoID uint64
|
||||
ThreadID uint64
|
||||
ForumID uint64
|
||||
CommentID uint64
|
||||
Photo *Photo
|
||||
Thread *Thread
|
||||
Forum *Forum
|
||||
Comment *Comment
|
||||
}
|
||||
|
||||
|
@ -403,6 +406,7 @@ func MapNotifications(ns []*Notification) NotificationMap {
|
|||
|
||||
result.mapNotificationPhotos(IDs)
|
||||
result.mapNotificationThreads(IDs)
|
||||
result.mapNotificationForums(IDs)
|
||||
|
||||
// NOTE: comment loading is not used - was added when trying to add "Like" buttons inside
|
||||
// your Comment notifications. But when a photo is commented on, the notification table_name=photos,
|
||||
|
@ -507,6 +511,53 @@ func (nm NotificationMap) mapNotificationThreads(IDs []uint64) {
|
|||
}
|
||||
}
|
||||
|
||||
// Helper function of MapNotifications to eager load Forum attachments.
|
||||
func (nm NotificationMap) mapNotificationForums(IDs []uint64) {
|
||||
type scanner struct {
|
||||
ForumID uint64
|
||||
NotificationID uint64
|
||||
}
|
||||
var scan []scanner
|
||||
|
||||
// Load all of these that have forums.
|
||||
err := DB.Table(
|
||||
"notifications",
|
||||
).Joins(
|
||||
"JOIN forums ON (notifications.table_name='forums' AND notifications.table_id=forums.id)",
|
||||
).Select(
|
||||
"forums.id AS forum_id",
|
||||
"notifications.id AS notification_id",
|
||||
).Where(
|
||||
"notifications.id IN ?",
|
||||
IDs,
|
||||
).Scan(&scan)
|
||||
if err.Error != nil {
|
||||
log.Error("Couldn't select forum IDs for notifications: %s", err.Error)
|
||||
}
|
||||
|
||||
// Collect and load all the forums by ID.
|
||||
var forumIDs = []uint64{}
|
||||
for _, row := range scan {
|
||||
// Store the forum ID in the result now.
|
||||
nm[row.NotificationID].ForumID = row.ForumID
|
||||
forumIDs = append(forumIDs, row.ForumID)
|
||||
}
|
||||
|
||||
// Load the forums.
|
||||
if len(forumIDs) > 0 {
|
||||
if forums, err := GetForums(forumIDs); err != nil {
|
||||
log.Error("Couldn't load forum IDs for notifications: %s", err)
|
||||
} else {
|
||||
// Marry them to their notification IDs.
|
||||
for _, body := range nm {
|
||||
if forum, ok := forums[body.ForumID]; ok {
|
||||
body.Forum = forum
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function of MapNotifications to eager load Comment attachments.
|
||||
func (nm NotificationMap) mapNotificationComments(IDs []uint64) {
|
||||
type scanner struct {
|
||||
|
|
|
@ -125,6 +125,25 @@ func GetUsers(currentUser *User, userIDs []uint64) ([]*User, error) {
|
|||
return users, nil
|
||||
}
|
||||
|
||||
// GetUsersAlphabetically queries for multiple user IDs and returns them sorted by username.
|
||||
//
|
||||
// Note: it doesn't respect blocked lists or a viewer context. Used for things like the forum moderators lists.
|
||||
func GetUsersAlphabetically(userIDs []uint64) ([]*User, error) {
|
||||
var (
|
||||
users = []*User{}
|
||||
result = (&User{}).Preload().Where(
|
||||
"id IN ?", userIDs,
|
||||
).Order("username asc").Find(&users)
|
||||
)
|
||||
|
||||
// Inject user relationships.
|
||||
for _, user := range users {
|
||||
SetUserRelationships(user, users)
|
||||
}
|
||||
|
||||
return users, result.Error
|
||||
}
|
||||
|
||||
// GetUsersByUsernames queries for multiple usernames and returns users in the same order.
|
||||
func GetUsersByUsernames(currentUser *User, usernames []string) ([]*User, error) {
|
||||
// Map the usernames.
|
||||
|
|
|
@ -87,12 +87,15 @@ func New() http.Handler {
|
|||
mux.Handle("GET /forum", middleware.CertRequired(forum.Landing()))
|
||||
mux.Handle("/forum/post", middleware.CertRequired(forum.NewPost()))
|
||||
mux.Handle("GET /forum/thread/{id}", middleware.CertRequired(forum.Thread()))
|
||||
mux.Handle("GET /forum/browse", middleware.CertRequired(forum.Browse()))
|
||||
mux.Handle("GET /forum/explore", middleware.CertRequired(forum.Explore()))
|
||||
mux.Handle("GET /forum/newest", middleware.CertRequired(forum.Newest()))
|
||||
mux.Handle("GET /forum/search", middleware.CertRequired(forum.Search()))
|
||||
mux.Handle("POST /forum/subscribe", middleware.CertRequired(forum.Subscribe()))
|
||||
mux.Handle("GET /f/{fragment}", middleware.CertRequired(forum.Forum()))
|
||||
mux.Handle("POST /poll/vote", middleware.CertRequired(poll.Vote()))
|
||||
mux.Handle("/forum/admin", middleware.CertRequired(forum.Manage()))
|
||||
mux.Handle("/forum/admin/edit", middleware.CertRequired(forum.AddEdit()))
|
||||
mux.Handle("/forum/admin/moderator", middleware.CertRequired(forum.ManageModerators()))
|
||||
|
||||
// Admin endpoints.
|
||||
mux.Handle("GET /admin", middleware.AdminRequired("", admin.Dashboard()))
|
||||
|
@ -102,8 +105,6 @@ func New() http.Handler {
|
|||
mux.Handle("/admin/user-action", middleware.AdminRequired("", admin.UserActions()))
|
||||
mux.Handle("/admin/maintenance", middleware.AdminRequired(config.ScopeMaintenance, admin.Maintenance()))
|
||||
mux.Handle("/admin/add-user", middleware.AdminRequired(config.ScopeUserCreate, admin.AddUser()))
|
||||
mux.Handle("/forum/admin", middleware.CertRequired(forum.Manage()))
|
||||
mux.Handle("/forum/admin/edit", middleware.CertRequired(forum.AddEdit()))
|
||||
mux.Handle("/admin/photo/mark-explicit", middleware.AdminRequired("", admin.MarkPhotoExplicit()))
|
||||
mux.Handle("GET /admin/changelog", middleware.AdminRequired(config.ScopeChangeLog, admin.ChangeLog()))
|
||||
|
||||
|
|
|
@ -492,7 +492,7 @@
|
|||
</a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="mb-1">
|
||||
<div class="mb-1 pr-4">
|
||||
{{if eq .Type "like"}}
|
||||
<span class="icon"><i class="fa fa-heart has-text-danger"></i></span>
|
||||
<span>
|
||||
|
@ -594,6 +594,12 @@
|
|||
<span>
|
||||
Your <strong>certification photo</strong> was rejected!
|
||||
</span>
|
||||
{{else if eq .Type "forum_moderator"}}
|
||||
<span class="icon"><i class="fa fa-peace has-text-success"></i></span>
|
||||
<span>
|
||||
You have been appointed as a <strong class="has-text-success">moderator</strong>
|
||||
for the forum <a href="/f/{{$Body.Forum.Fragment}}">{{$Body.Forum.Title}}</a>!
|
||||
</span>
|
||||
{{else}}
|
||||
{{.AboutUser.Username}} {{.Type}} {{.TableName}} {{.TableID}}
|
||||
{{end}}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{{define "title"}}Forums{{end}}
|
||||
{{define "content"}}
|
||||
<div class="block">
|
||||
<section class="hero is-light is-danger">
|
||||
<section class="hero is-bold is-primary">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
|
@ -14,12 +14,30 @@
|
|||
|
||||
{{$Root := .}}
|
||||
|
||||
<div class="block p-4">
|
||||
<div class="columns is-multiline is-mobile">
|
||||
<div class="column is-narrow">
|
||||
<a href="/forum/admin">
|
||||
<i class="fa fa-book"></i> My Forums
|
||||
</a>
|
||||
</div>
|
||||
{{if .EditForum}}
|
||||
<div class="column is-narrow">
|
||||
<a href="/f/{{.EditForum.Fragment}}">
|
||||
<i class="fa fa-comments"></i>
|
||||
Go to Forum
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block p-4">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-two-thirds">
|
||||
<div class="card">
|
||||
<header class="card-header has-background-info">
|
||||
<p class="card-header-title has-text-light">Forum Properties</p>
|
||||
<p class="card-header-title">Forum Properties</p>
|
||||
</header>
|
||||
|
||||
<div class="card-content">
|
||||
|
@ -96,11 +114,11 @@
|
|||
Explicit <i class="fa fa-fire has-text-danger ml-1"></i>
|
||||
</label>
|
||||
<p class="help">
|
||||
Check this box if the forum is intended for explicit content. Users must
|
||||
opt-in to see explicit content.
|
||||
Check this box if the forum is intended for explicit content. Only members who have opted-in
|
||||
to see explicit content can find this forum.
|
||||
</p>
|
||||
|
||||
{{if .CurrentUser.IsAdmin}}
|
||||
{{if .CurrentUser.HasAdminScope "admin.forum.manage"}}
|
||||
<label class="checkbox mt-3">
|
||||
<input type="checkbox"
|
||||
name="privileged"
|
||||
|
@ -115,26 +133,26 @@
|
|||
</p>
|
||||
{{end}}
|
||||
|
||||
{{if .CurrentUser.IsAdmin}}
|
||||
{{if .CurrentUser.HasAdminScope "admin.forum.manage"}}
|
||||
<label class="checkbox mt-3">
|
||||
<input type="checkbox"
|
||||
name="permit_photos"
|
||||
value="true"
|
||||
{{if and .EditForum .EditForum.PermitPhotos}}checked{{end}}>
|
||||
Permit Photos <i class="fa fa-camera ml-1"></i>
|
||||
Permit Photos <i class="fa fa-camera ml-1"></i> <i class="fa fa-peace has-text-danger ml-1"></i>
|
||||
</label>
|
||||
<p class="help">
|
||||
Check this box if the forum allows photos to be uploaded (not implemented)
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
{{if .CurrentUser.IsAdmin}}
|
||||
{{if .CurrentUser.HasAdminScope "admin.forum.manage"}}
|
||||
<label class="checkbox mt-3">
|
||||
<input type="checkbox"
|
||||
name="private"
|
||||
value="true"
|
||||
{{if and .EditForum .EditForum.Private}}checked{{end}}>
|
||||
Private forum
|
||||
Private forum <i class="fa fa-peace has-text-danger ml-1"></i>
|
||||
</label>
|
||||
<p class="help">
|
||||
This forum is only visible to admins or approved subscribers.
|
||||
|
@ -142,6 +160,45 @@
|
|||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- On "Edit" of existing forum: show and allow adding Moderators. -->
|
||||
{{if .EditForum}}
|
||||
<hr>
|
||||
<div class="field">
|
||||
<label class="label">Forum Moderators</label>
|
||||
|
||||
{{if .Moderators}}
|
||||
<table class="table">
|
||||
<tbody>
|
||||
{{range .Moderators}}
|
||||
<tr>
|
||||
<td>
|
||||
{{template "avatar-16x16" .}}
|
||||
<a href="/u/{{.Username}}">{{.Username}}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/forum/admin/moderator?forum_id={{$Root.EditForum.ID}}&intent=remove&to={{.Username}}"
|
||||
class="has-text-danger"
|
||||
aria-label="Remove">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
|
||||
<a href="/forum/admin/moderator?forum_id={{.EditForum.ID}}">
|
||||
<i class="fa fa-plus"></i> Appoint a moderator
|
||||
</a>
|
||||
<p class="help">
|
||||
You may appoint other members from the {{PrettyTitle}} community to help you moderate your forum.
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="field">
|
||||
<button type="submit" class="button is-success">
|
||||
{{if .EditForum}}Save Forum{{else}}Create Forum{{end}}
|
||||
|
|
|
@ -57,6 +57,13 @@
|
|||
</nav>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<!-- Edit button for forum's settings -->
|
||||
{{if or (eq .Forum.OwnerID .CurrentUser.ID) (.CurrentUser.HasAdminScope "admin.forum.manage")}}
|
||||
<a href="/forum/admin/edit?id={{.Forum.ID}}" class="button mr-2">
|
||||
<span class="icon"><i class="fa fa-gear"></i></span>
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if or .CurrentUser.IsAdmin (not .Forum.Privileged) (eq .Forum.OwnerID .CurrentUser.ID)}}
|
||||
<a href="/forum/post?to={{.Forum.Fragment}}" class="button is-info">
|
||||
<span class="icon"><i class="fa fa-plus"></i></span>
|
||||
|
@ -83,6 +90,20 @@
|
|||
{{SimplePager .Pager}}
|
||||
</div>
|
||||
|
||||
<!-- If no posts at all? -->
|
||||
{{if not .Threads}}
|
||||
<div class="block p-2">
|
||||
<div class="notification is-info is-light content">
|
||||
<p>
|
||||
There are no posts yet on this forum.
|
||||
{{if not .Forum.Privileged}}
|
||||
Why not <a href="/forum/post?to={{.Forum.Fragment}}">start the first thread?</a>
|
||||
{{end}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{$Root := .}}
|
||||
<div class="block p-2">
|
||||
{{range .Threads}}
|
||||
|
@ -172,4 +193,63 @@
|
|||
{{SimplePager .Pager}}
|
||||
</div>
|
||||
|
||||
<!-- Info box: owner, moderators -->
|
||||
<div class="block p-2">
|
||||
<div class="box">
|
||||
|
||||
<label class="label">Forum Info</label>
|
||||
<div class="mb-4">
|
||||
Created: <span title="{{.Forum.CreatedAt}}">{{.Forum.CreatedAt.Format "Jan _2 2006"}}</span>
|
||||
<div class="mt-2">
|
||||
{{if .Forum.Explicit}}
|
||||
<span class="tag is-danger is-light">
|
||||
<span class="icon"><i class="fa fa-fire"></i></span>
|
||||
<span>Explicit</span>
|
||||
</span>
|
||||
{{end}}
|
||||
|
||||
{{if .Forum.Privileged}}
|
||||
<span class="tag is-warning is-light">
|
||||
<span class="icon"><i class="fa fa-peace"></i></span>
|
||||
<span>Privileged</span>
|
||||
</span>
|
||||
{{end}}
|
||||
|
||||
{{if .Forum.PermitPhotos}}
|
||||
<span class="tag is-grey">
|
||||
<span class="icon"><i class="fa fa-camera"></i></span>
|
||||
<span>Photos</span>
|
||||
</span>
|
||||
{{end}}
|
||||
|
||||
{{if .Forum.Private}}
|
||||
<span class="tag is-private is-light">
|
||||
<span class="icon"><i class="fa fa-lock"></i></span>
|
||||
<span>Private</span>
|
||||
</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="label">Forum Moderators</label>
|
||||
|
||||
<!-- The owner first -->
|
||||
{{template "avatar-16x16" .Forum.Owner}}
|
||||
<a href="/u/{{.Forum.Owner.Username}}">
|
||||
{{.Forum.Owner.Username}}
|
||||
</a>
|
||||
<small class="has-text-grey">(owner)</small>
|
||||
|
||||
<!-- Additional moderators -->
|
||||
{{range .ForumModerators}}
|
||||
<span class="pl-2">
|
||||
{{template "avatar-16x16" .}}
|
||||
<a href="/u/{{.Username}}">
|
||||
{{.Username}}
|
||||
</a>
|
||||
</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{end}}
|
||||
|
|
|
@ -32,8 +32,8 @@
|
|||
<!-- Tab bar -->
|
||||
{{template "ForumTabs" .}}
|
||||
|
||||
<!-- Filters for the Browse tab -->
|
||||
{{if .IsBrowseTab}}
|
||||
<!-- Filters for the Explore tab -->
|
||||
{{if .IsExploreTab}}
|
||||
<div class="block mb-0 p-4">
|
||||
<form action="{{.Request.URL.Path}}" method="GET">
|
||||
|
||||
|
@ -251,8 +251,8 @@
|
|||
</div>
|
||||
{{end}}<!-- range .Categories -->
|
||||
|
||||
<!-- Pager footer for Browse tab -->
|
||||
{{if .IsBrowseTab}}
|
||||
<!-- Pager footer for Explore tab -->
|
||||
{{if .IsExploreTab}}
|
||||
<div class="block p-4">
|
||||
{{SimplePager .Pager}}
|
||||
</div>
|
||||
|
|
138
web/templates/forum/moderators.html
Normal file
138
web/templates/forum/moderators.html
Normal file
|
@ -0,0 +1,138 @@
|
|||
{{define "title"}}Manage Forum Moderators{{end}}
|
||||
{{define "content"}}
|
||||
<div class="container">
|
||||
<section class="hero is-warning is-bold">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
Manage Forum Moderators
|
||||
</h1>
|
||||
<h2 class="subtitle">
|
||||
For {{.Forum.Title}} <small class="ml-2">/f/{{.Forum.Fragment}}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="block p-4">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-half">
|
||||
|
||||
<div class="card" style="width: 100%; max-width: 640px">
|
||||
<header class="card-header {{if .IsRemoving}}has-background-danger{{else}}has-background-link{{end}}">
|
||||
<p class="card-header-title has-text-light">
|
||||
<span class="icon mr-2"><i class="fa fa-user-tie"></i></span>
|
||||
<span>
|
||||
{{if .IsRemoving}}
|
||||
Remove a Moderator
|
||||
{{else}}
|
||||
Appoint a Moderator
|
||||
{{end}}
|
||||
</span>
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
|
||||
<!-- Show the introduction or selected user profile -->
|
||||
{{if not .User}}
|
||||
<div class="block">
|
||||
You may use this page to <strong>appoint a moderator</strong> to help you
|
||||
manage your forum. This can be <em>anybody</em> from the {{PrettyTitle}} community.
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
Moderators will be able to delete threads and replies on your forum.
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
As the owner of the forum, you retain full control over its settings and you alone
|
||||
can add or remove its moderators.
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="block">
|
||||
{{if .IsRemoving}}
|
||||
Confirm that you wish to <strong>remove</strong> moderator rights for
|
||||
{{.User.Username}} on your forum.
|
||||
{{else}}
|
||||
Confirm that you wish to grant <strong>{{.User.Username}}</strong>
|
||||
access to moderate your forum by clicking the button below.
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="media block">
|
||||
<div class="media-left">
|
||||
{{template "avatar-64x64" .User}}
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="title is-4">{{.User.NameOrUsername}}</p>
|
||||
<p class="subtitle is-6">
|
||||
<span class="icon"><i class="fa fa-user"></i></span>
|
||||
<a href="/u/{{.User.Username}}" target="_blank">{{.User.Username}}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Show the relevant form -->
|
||||
<form action="/forum/admin/moderator" method="POST">
|
||||
{{InputCSRF}}
|
||||
<input type="hidden" name="forum_id" value="{{.Forum.ID}}">
|
||||
|
||||
{{if .User}}
|
||||
<input type="hidden" name="to" value="{{.User.Username}}">
|
||||
{{else}}
|
||||
<div class="field block">
|
||||
<label for="to" class="label">Add a moderator by their username:</label>
|
||||
<input type="text" class="input"
|
||||
name="to" id="to"
|
||||
placeholder="username">
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="field has-text-centered">
|
||||
{{if .IsRemoving}}
|
||||
<button type="submit" class="button is-danger"
|
||||
name="intent"
|
||||
value="{{if .User}}confirm-remove{{else}}preview{{end}}">
|
||||
{{if .User}}
|
||||
<span class="icon"><i class="fa fa-user-xmark"></i></span>
|
||||
<span>Remove from moderators</span>
|
||||
{{else}}
|
||||
Continue
|
||||
{{end}}
|
||||
</button>
|
||||
{{else}}
|
||||
<button type="submit" class="button is-success"
|
||||
name="intent"
|
||||
value="{{if .User}}submit{{else}}preview{{end}}">
|
||||
{{if .User}}
|
||||
<span class="icon"><i class="fa fa-user-tie"></i></span>
|
||||
<span>Add to moderators</span>
|
||||
{{else}}
|
||||
Continue
|
||||
{{end}}
|
||||
</button>
|
||||
{{end}}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.addEventListener("DOMContentLoaded", (event) => {
|
||||
let $file = document.querySelector("#file"),
|
||||
$fileName = document.querySelector("#fileName");
|
||||
|
||||
$file.addEventListener("change", function() {
|
||||
let file = this.files[0];
|
||||
$fileName.innerHTML = file.name;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
|
@ -19,19 +19,71 @@
|
|||
<!-- Tab bar -->
|
||||
{{template "ForumTabs" .}}
|
||||
|
||||
<div class="p-4">
|
||||
Found {{FormatNumberCommas .Pager.Total}} {{if .AllComments}}posts{{else}}threads{{end}} (page {{.Pager.Page}} of {{.Pager.Pages}})
|
||||
<!-- Display Options -->
|
||||
<div class="px-4">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
Found {{FormatNumberCommas .Pager.Total}} {{if .AllComments}}posts{{else}}threads{{end}} (page {{.Pager.Page}} of {{.Pager.Pages}})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 class="level-right">
|
||||
<div class="level-item">
|
||||
<div class="columns align-right">
|
||||
|
||||
{{if .FeatureUserForumsEnabled}}
|
||||
<div class="column is-narrow">
|
||||
<label class="label">Which forums:</label>
|
||||
<div class="tabs is-toggle is-small">
|
||||
<ul>
|
||||
<li {{if eq .WhichForums "all"}}class="is-active"{{end}}>
|
||||
<a href="{{.Request.URL.Path}}?{{QueryPlus "which" "all"}}">All</a>
|
||||
</li>
|
||||
<li {{if eq .WhichForums "official"}}class="is-active"{{end}}>
|
||||
<a href="{{.Request.URL.Path}}?{{QueryPlus "which" "official"}}">Official</a>
|
||||
</li>
|
||||
<li {{if eq .WhichForums "community"}}class="is-active"{{end}}>
|
||||
<a href="{{.Request.URL.Path}}?{{QueryPlus "which" "community"}}">Community</a>
|
||||
</li>
|
||||
{{if .CurrentUser.HasForumSubscriptions}}
|
||||
<li {{if eq .WhichForums "followed"}}class="is-active"{{end}}>
|
||||
<a href="{{.Request.URL.Path}}?{{QueryPlus "which" "followed"}}">My List</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="column is-narrow">
|
||||
<label class="label">Show:</label>
|
||||
<div class="tabs is-toggle is-small">
|
||||
<ul>
|
||||
<li {{if not .AllComments}}class="is-active"{{end}}>
|
||||
<a href="{{.Request.URL.Path}}?{{QueryPlus "all" ""}}">By thread</a>
|
||||
</li>
|
||||
<li {{if .AllComments}}class="is-active"{{end}}>
|
||||
<a href="{{.Request.URL.Path}}?{{QueryPlus "all" "true"}}">All posts</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
{{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}}?{{QueryPlus "all" ""}}">Deduplicate by thread?</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
{{SimplePager .Pager}}
|
||||
</div>
|
||||
|
|
|
@ -4,10 +4,39 @@
|
|||
<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>{{.Forum.Title}}</span>
|
||||
</h1>
|
||||
<div class="level">
|
||||
<div class="level-left mb-4">
|
||||
<h1 class="title">
|
||||
<span class="icon mr-4"><i class="fa fa-comments"></i></span>
|
||||
<span>{{.Forum.Title}}</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{{if .FeatureUserForumsEnabled}}
|
||||
<div class="level-right">
|
||||
<!-- 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"
|
||||
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>Followed</span>
|
||||
</button>
|
||||
{{else}}
|
||||
<button type="submit" class="button"
|
||||
name="intent" value="follow">
|
||||
<span class="icon"><i class="fa-regular fa-bookmark"></i></span>
|
||||
<span>Follow</span>
|
||||
</button>
|
||||
{{end}}
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -16,9 +16,9 @@ Variables that your template should set:
|
|||
Categories
|
||||
</a>
|
||||
</li>
|
||||
<li {{if eq .CurrentForumTab "browse" }}class="is-active"{{end}}>
|
||||
<a href="/forum/browse">
|
||||
Browse
|
||||
<li {{if eq .CurrentForumTab "explore" }}class="is-active"{{end}}>
|
||||
<a href="/forum/explore">
|
||||
Explore
|
||||
</a>
|
||||
</li>
|
||||
<li {{if eq .CurrentForumTab "newest" }}class="is-active"{{end}}>
|
||||
|
|
|
@ -1,5 +1,24 @@
|
|||
<!-- User avatar widgets -->
|
||||
|
||||
<!-- Parameter: .User -->
|
||||
{{define "avatar-16x16"}}
|
||||
<figure class="image is-16x16 is-inline-block">
|
||||
<a href="/u/{{.Username}}" class="has-text-dark">
|
||||
{{if .ProfilePhoto.ID}}
|
||||
{{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted)}}
|
||||
<img src="/static/img/shy-private.png" class="is-rounded">
|
||||
{{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend)}}
|
||||
<img src="/static/img/shy-friends.png" class="is-rounded">
|
||||
{{else}}
|
||||
<img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}" class="is-rounded">
|
||||
{{end}}
|
||||
{{else}}
|
||||
<img src="/static/img/shy.png" class="is-rounded">
|
||||
{{end}}
|
||||
</a>
|
||||
</figure>
|
||||
{{end}}
|
||||
|
||||
<!-- Parameter: .User -->
|
||||
{{define "avatar-24x24"}}
|
||||
<figure class="image is-24x24 is-inline-block">
|
||||
|
|
Loading…
Reference in New Issue
Block a user