Thread Moderator Buttons: Pin and Lock

* The bottoms of threads have moderator buttons now, to easily Pin or
  Unpin the thread (for Owners + Admins) or to Lock/Unlock the thread
  (all moderators).
This commit is contained in:
Noah Petherbridge 2024-08-23 22:24:38 -07:00
parent 90d0d10ee5
commit 85fd6ac5a2
5 changed files with 144 additions and 1 deletions

View File

@ -170,6 +170,11 @@ func AddEdit() http.HandlerFunc {
forum.Private, forum.Private,
)) ))
// If this is a Community forum, subscribe the owner to it immediately.
if forum.Category == "" {
models.CreateForumMembership(currentUser, forum)
}
return return
} else { } else {
session.FlashError(w, r, "Error creating the forum: %s", err) session.FlashError(w, r, "Error creating the forum: %s", err)

View File

@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/log" "code.nonshy.com/nonshy/website/pkg/log"
"code.nonshy.com/nonshy/website/pkg/models" "code.nonshy.com/nonshy/website/pkg/models"
"code.nonshy.com/nonshy/website/pkg/session" "code.nonshy.com/nonshy/website/pkg/session"
@ -138,3 +139,91 @@ func ManageModerators() http.HandlerFunc {
} }
}) })
} }
// ModerateThread endpoint - perform a mod action like pinning or locking a thread.
func ModerateThread() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Query params.
var (
threadID, err = strconv.Atoi(r.PathValue("id"))
intent = r.PostFormValue("intent")
nextURL = fmt.Sprintf("/forum/thread/%d", threadID)
)
if err != nil {
session.FlashError(w, r, "Invalid thread ID.")
templates.Redirect(w, nextURL)
return
}
// Get the current user.
currentUser, err := session.CurrentUser(r)
if err != nil {
session.FlashError(w, r, "Couldn't get current user: %s", err)
templates.Redirect(w, "/")
return
}
// Get this thread.
thread, err := models.GetThread(uint64(threadID))
if err != nil {
templates.NotFoundPage(w, r)
return
}
// Get its forum.
forum, err := models.GetForum(thread.ForumID)
if err != nil {
templates.NotFoundPage(w, r)
return
}
// User must at least be able to moderate.
if !forum.CanBeModeratedBy(currentUser) {
templates.ForbiddenPage(w, r)
return
}
// Does the user have Ownership level access (including privileged admins)
var isOwner = forum.OwnerID == currentUser.ID || currentUser.HasAdminScope(config.ScopeForumAdmin)
/****
* Moderator level permissions.
***/
switch intent {
case "lock":
thread.NoReply = true
session.Flash(w, r, "This thread has been locked and will not be accepting any new replies.")
case "unlock":
thread.NoReply = false
session.Flash(w, r, "This thread has been unlocked and can accept new replies again.")
default:
if !isOwner {
// End of the road.
templates.ForbiddenPage(w, r)
return
}
}
/****
* Owner + Admin level permissions.
***/
switch intent {
case "pin":
thread.Pinned = true
session.Flash(w, r, "This thread is now pinned to the top of the forum.")
case "unpin":
thread.Pinned = false
session.Flash(w, r, "This thread will no longer be pinned to the top of the forum.")
default:
session.FlashError(w, r, "Unknown moderator action.")
}
// Save changes to the thread.
if err := thread.Save(); err != nil {
session.FlashError(w, r, "Error saving thread: %s", err)
}
templates.Redirect(w, nextURL)
})
}

View File

@ -87,6 +87,7 @@ func New() http.Handler {
mux.Handle("GET /forum", middleware.CertRequired(forum.Landing())) mux.Handle("GET /forum", middleware.CertRequired(forum.Landing()))
mux.Handle("/forum/post", middleware.CertRequired(forum.NewPost())) mux.Handle("/forum/post", middleware.CertRequired(forum.NewPost()))
mux.Handle("GET /forum/thread/{id}", middleware.CertRequired(forum.Thread())) mux.Handle("GET /forum/thread/{id}", middleware.CertRequired(forum.Thread()))
mux.Handle("POST /forum/thread/{id}/moderate", middleware.CertRequired(forum.ModerateThread()))
mux.Handle("GET /forum/explore", middleware.CertRequired(forum.Explore())) mux.Handle("GET /forum/explore", middleware.CertRequired(forum.Explore()))
mux.Handle("GET /forum/newest", middleware.CertRequired(forum.Newest())) mux.Handle("GET /forum/newest", middleware.CertRequired(forum.Newest()))
mux.Handle("GET /forum/search", middleware.CertRequired(forum.Search())) mux.Handle("GET /forum/search", middleware.CertRequired(forum.Search()))

View File

@ -17,7 +17,7 @@
{{else}} {{else}}
<link rel="stylesheet" type="text/css" href="/static/css/nonshy-prefers-dark.css?build={{.BuildHash}}"> <link rel="stylesheet" type="text/css" href="/static/css/nonshy-prefers-dark.css?build={{.BuildHash}}">
{{end}} {{end}}
<link rel="stylesheet" href="/static/fontawesome-free-6.1.2-web/css/all.css"> <link rel="stylesheet" href="/static/fontawesome-free-6.6.0-web/css/all.css">
<link rel="stylesheet" href="/static/css/theme.css?build={{.BuildHash}}"> <link rel="stylesheet" href="/static/css/theme.css?build={{.BuildHash}}">
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json">
<title>{{template "title" .}} - {{ .Title }}</title> <title>{{template "title" .}} - {{ .Title }}</title>

View File

@ -368,6 +368,54 @@
{{SimplePager .Pager}} {{SimplePager .Pager}}
</div> </div>
<!-- Moderator controls -->
{{if .CanModerate}}
<div class="block p-2">
<form method="POST" action="/forum/thread/{{.Thread.ID}}/moderate">
{{InputCSRF}}
<div class="field has-addons">
<p class="control">
<button type="button" class="button is-small has-text-info"
onclick="alert('Note: These are your moderator controls for this forum thread.')">
<span class="icon is-small">
<i class="fa fa-user-tie"></i>
</span>
</button>
</p>
<!-- Pin/Unpin -->
{{if or (eq .Forum.OwnerID .CurrentUser.ID) (.CurrentUser.HasAdminScope "admin.forum.manage")}}
<p class="control">
<button type="submit" class="button is-small has-text-success"
name="intent"
value="{{if .Thread.Pinned}}un{{end}}pin"
onclick="return confirm('Are you sure you want to {{if .Thread.Pinned}}un{{end}}pin this thread to the top of the forum?')">
<span class="icon is-small">
<i class="fas fa-thumbtack{{if .Thread.Pinned}}-slash{{end}}"></i>
</span>
<span>{{if .Thread.Pinned}}Unp{{else}}P{{end}}in thread</span>
</button>
</p>
{{end}}
<!-- Lock/Unlock -->
<p class="control">
<button type="submit" class="button is-small has-text-warning"
name="intent"
value="{{if .Thread.NoReply}}un{{end}}lock"
onclick="return confirm('Do you want to {{if .Thread.NoReply}}UN{{end}}LOCK this thread?\n\nA locked thread will not accept any new replies.')">
<span class="icon is-small">
<i class="fa fa-ban"></i>
</span>
<span>{{if .Thread.NoReply}}Unl{{else}}L{{end}}ock replies</span>
</button>
</p>
</div>
</form>
</div>
{{end}}
{{if .Thread.NoReply}} {{if .Thread.NoReply}}
<div class="block notification is-warning is-light"> <div class="block notification is-warning is-light">
<i class="fa fa-ban pr-2"></i> <i class="fa fa-ban pr-2"></i>