ed4a9f8c89
* 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.
520 lines
22 KiB
HTML
520 lines
22 KiB
HTML
{{define "title"}}{{.Thread.Title}} - {{.Forum.Title}}{{end}}
|
|
{{define "content"}}
|
|
<div class="block">
|
|
<section class="hero is-light is-success">
|
|
<div class="hero-body">
|
|
<div class="container">
|
|
<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>
|
|
</div>
|
|
|
|
{{$Root := .}}
|
|
|
|
<div class="block px-4">
|
|
<div class="columns">
|
|
<div class="column">
|
|
<nav class="breadcrumb" aria-label="breadcrumbs">
|
|
<ul>
|
|
<li><a href="/forum">Forums</a></li>
|
|
<li><a href="/f/{{.Forum.Fragment}}">{{.Forum.Title}}</a></Li>
|
|
<li class="is-active">
|
|
<a href="{{.Request.URL.Path}}" aria-current="page">{{or .Thread.Title "Untitled Thread"}}</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
<div class="column is-narrow">
|
|
{{if not .Thread.NoReply}}
|
|
<a href="/forum/post?to={{.Forum.Fragment}}&thread={{.Thread.ID}}" class="button is-link">
|
|
<span class="icon"><i class="fa fa-reply"></i></span>
|
|
<span>Add Reply</span>
|
|
</a>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h1 class="title px-4">
|
|
{{if .Thread.Pinned}}<sup class="fa fa-thumbtack has-text-success mr-2 is-size-5" title="Pinned"></sup>{{end}}
|
|
{{or .Thread.Title "Untitled Thread"}}
|
|
</h1>
|
|
|
|
<div class="px-4">
|
|
{{if .Thread.Pinned}}
|
|
<span class="tag is-success is-light mr-2">
|
|
<span class="icon"><i class="fa fa-thumbtack"></i></span>
|
|
<span>Pinned</span>
|
|
</span>
|
|
{{end}}
|
|
|
|
{{if .Thread.Explicit}}
|
|
<span class="tag is-danger is-light mr-2">
|
|
<span class="icon"><i class="fa fa-fire"></i></span>
|
|
<span>NSFW</span>
|
|
</span>
|
|
{{end}}
|
|
|
|
{{if .Thread.NoReply}}
|
|
<span class="tag is-warning is-light mr-2" title="This thread can not be replied to.">
|
|
<span class="icon"><i class="fa fa-ban"></i></span>
|
|
<span>No Reply</span>
|
|
</span>
|
|
{{end}}
|
|
|
|
<span class="tag is-grey is-light has-text-dark mr-2" title="This thread can not be replied to.">
|
|
<span class="icon"><i class="fa fa-eye"></i></span>
|
|
<span>{{.Thread.Views}} View{{PluralizeU64 .Thread.Views}}</span>
|
|
</span>
|
|
|
|
<em title="{{.Thread.UpdatedAt.Format "2006-01-02 15:04:05"}}">
|
|
Updated {{SincePrettyCoarse .Thread.UpdatedAt}} ago
|
|
</em>
|
|
</div>
|
|
|
|
<p class="block p-4 mb-0">
|
|
Found <strong>{{.Pager.Total}}</strong> post{{Pluralize64 .Pager.Total}} on this thread
|
|
(page {{.Pager.Page}} of {{.Pager.Pages}}).
|
|
</p>
|
|
|
|
<p class="block px-4 mb-4">
|
|
<a href="/comments/subscription?table_name=threads&table_id={{.Thread.ID}}&next={{UrlEncode .Request.URL.String}}&subscribe={{if not .IsSubscribed}}true{{else}}false{{end}}">
|
|
<span class="icon"><i class="fa fa-bell{{if not .IsSubscribed}}-slash{{end}}"></i></span>
|
|
<span>
|
|
{{if .IsSubscribed}}
|
|
Disable notifications about this thread
|
|
{{else}}
|
|
Enable notifications about this thread
|
|
{{end}}
|
|
</span>
|
|
</a>
|
|
</p>
|
|
|
|
<div class="block p-2">
|
|
{{SimplePager .Pager}}
|
|
</div>
|
|
|
|
{{$Root := .}}
|
|
<div class="block p-2">
|
|
{{range $i, $c := .Comments}}
|
|
<div class="box has-background-link-light has-text-dark" id="p{{.ID}}">
|
|
<div class="columns">
|
|
<div class="column is-2 has-text-centered">
|
|
<div>
|
|
<a href="/u/{{$c.User.Username}}">
|
|
{{template "avatar-96x96" $c.User}}
|
|
</a>
|
|
</div>
|
|
<a href="/u/{{$c.User.Username}}">{{$c.User.NameOrUsername}}</a>
|
|
{{if $c.User.IsAdmin}}
|
|
<div class="is-size-7 mt-1">
|
|
<span class="tag is-danger is-light">
|
|
<span class="icon"><i class="fa fa-peace"></i></span>
|
|
<span>Admin</span>
|
|
</span>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
<div class="column content">
|
|
{{ToMarkdown $c.Message}}
|
|
|
|
{{if $c.IsEdited}}
|
|
<div class="mt-4">
|
|
<em title="{{$c.UpdatedAt.Format "2006-01-02 15:04:05"}}">
|
|
<small>Edited {{SincePrettyCoarse $c.UpdatedAt}} ago</small>
|
|
</em>
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Poll attachment? -->
|
|
{{if and (eq $Root.Pager.Page 1) (eq $i 0) $Root.Thread.PollID}}
|
|
<h2>Poll</h2>
|
|
|
|
<!-- Get the results -->
|
|
{{$Poll := $Root.Thread.Poll}}
|
|
{{$PollResult := $Poll.Result $Root.CurrentUser}}
|
|
|
|
<form name="ballot" action="/poll/vote" method="POST">
|
|
{{InputCSRF}}
|
|
<input type="hidden" name="poll_id" value="{{$Root.Thread.PollID}}">
|
|
<input type="hidden" name="from_thread_id" value="{{$Root.Thread.ID}}">
|
|
|
|
<!-- Poll is open? -->
|
|
{{if $PollResult.AcceptingVotes}}
|
|
{{range $Poll.Options}}
|
|
<div class="control">
|
|
<label class="{{$Poll.InputType}} box nonshy-fullwidth p-3 mb-3">
|
|
<input type="{{$Poll.InputType}}"
|
|
name="answer"
|
|
value="{{.}}">
|
|
{{.}}
|
|
</label>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if $Poll.MultipleChoice}}
|
|
<div class="mt-2 mb-4">
|
|
<strong>Multiple choice:</strong> select all the answers you want before casting your vote!
|
|
</div>
|
|
{{end}}
|
|
|
|
<div class="mb-4">
|
|
{{if and ($Poll.Expires) (not $Poll.IsExpired)}}
|
|
Poll expires in about <span title="{{$Poll.ExpiresAt.Format "2006-01-02 15:04:05"}}">{{SincePrettyCoarse $Root.Thread.Poll.ExpiresAt}}</span>.
|
|
Vote or wait to see the responses.
|
|
{{else}}
|
|
Poll doesn't expire. Vote to see the responses.
|
|
{{end}}
|
|
</div>
|
|
|
|
<button type="submit"
|
|
class="button is-primary is-outline">
|
|
Submit response
|
|
</button>
|
|
{{else}}
|
|
{{range $Poll.Options}}
|
|
<div class="columns mb-0">
|
|
<div class="column is-one-quarter">
|
|
{{.}}
|
|
</div>
|
|
<div class="column">
|
|
<progress class="{{$PollResult.GetClass .}}" value="{{$PollResult.GetPercent .}}" max="100">{{$PollResult.GetPercent .}}%</progress>
|
|
</div>
|
|
<div class="column is-1">
|
|
{{$PollResult.GetPercent .}}%
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
<em>
|
|
{{$PollResult.TotalVotes}} vote{{Pluralize $PollResult.TotalVotes}}.
|
|
{{if $Poll.IsExpired}}
|
|
Poll ended <span title="{{$Poll.ExpiresAt.Format "2006-01-02 15:04:05"}}">{{SincePrettyCoarse $Root.Thread.Poll.ExpiresAt}} ago</span>.
|
|
{{end}}
|
|
</em>
|
|
{{end}}
|
|
|
|
|
|
</form>
|
|
{{end}}
|
|
|
|
<!-- Photo attachments? -->
|
|
{{$Photos := $Root.PhotoMap.Get $c.ID}}
|
|
{{if $Photos}}
|
|
{{range $Photos}}
|
|
{{if not .ExpiredAt.IsZero}}
|
|
<div class="mt-4">
|
|
<span class="tag is-dark">photo expired on {{.ExpiredAt.Format "2006-01-02"}}</span>
|
|
</div>
|
|
{{else}}
|
|
<!-- GIF video? -->
|
|
{{if HasSuffix .Filename ".mp4"}}
|
|
<video autoplay loop controls controlsList="nodownload" playsinline>
|
|
<source src="{{PhotoURL .Filename}}" type="video/mp4">
|
|
</video>
|
|
{{else}}
|
|
<div class="mt-4 is-clipped">
|
|
<img src="{{PhotoURL .Filename}}" loading="lazy"{{if and (or $Root.Forum.Explicit $Root.Thread.Explicit) (eq ($Root.CurrentUser.GetProfileField "blur_explicit") "true")}} class="blurred-explicit"{{end}}>
|
|
</div>
|
|
{{end}}
|
|
{{end}}
|
|
{{end}}
|
|
{{end}}
|
|
|
|
<hr class="has-background-grey mb-2">
|
|
|
|
<div class="columns is-mobile is-multiline is-size-7 mb-0">
|
|
<div class="column is-narrow">
|
|
<span title="{{.CreatedAt.Format "2006-01-02 15:04:05"}}">
|
|
{{SincePrettyCoarse .CreatedAt}} ago
|
|
</span>
|
|
</div>
|
|
|
|
<div class="column is-narrow">
|
|
{{$Like := $Root.LikeMap.Get .ID}}
|
|
<a href="#" class="has-text-dark nonshy-like-button"
|
|
data-table-name="comments" data-table-id="{{.ID}}"
|
|
title="Like">
|
|
<span class="icon{{if $Like.UserLikes}} has-text-danger{{end}}"><i class="fa fa-heart"></i></span>
|
|
<span class="nonshy-likes">
|
|
Like
|
|
{{if gt $Like.Count 0}}
|
|
({{$Like.Count}})
|
|
{{end}}
|
|
</span>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="column is-narrow">
|
|
<!-- Button to inspect the likes -->
|
|
<a href="#" class="has-text-dark"
|
|
onclick="ShowLikeModal('comments', {{.ID}}); return false">
|
|
<span class="icon"><i class="fa fa-eye"></i></span>
|
|
<span>Likes</span>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="column is-narrow">
|
|
<a href="/contact?intent=report&subject=report.comment&id={{.ID}}" class="has-text-dark">
|
|
<span class="icon"><i class="fa fa-flag"></i></span>
|
|
<span>Report</span>
|
|
</a>
|
|
</div>
|
|
|
|
{{if not $Root.Thread.NoReply}}
|
|
<div class="column is-narrow">
|
|
<a href="/forum/post?to={{$Root.Forum.Fragment}}&thread={{$Root.Thread.ID}}"e={{.ID}}"
|
|
class="has-text-dark nonshy-quote-button" data-quote-body="{{.Message}}" data-reply-to="{{.User.Username}}">
|
|
<span class="icon"><i class="fa fa-quote-right"></i></span>
|
|
<span>Quote</span>
|
|
</a>
|
|
</div>
|
|
<div class="column is-narrow">
|
|
<a href="/forum/post?to={{$Root.Forum.Fragment}}&thread={{$Root.Thread.ID}}"
|
|
class="has-text-dark nonshy-reply-button" data-reply-to="{{.User.Username}}">
|
|
<span class="icon"><i class="fa fa-reply"></i></span>
|
|
<span>Reply</span>
|
|
</a>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if or ($Root.CurrentUser.HasAdminScope "social.moderator.forum") (eq $Root.CurrentUser.ID .User.ID)}}
|
|
<div class="column is-narrow">
|
|
<a href="/forum/post?to={{$Root.Forum.Fragment}}&thread={{$Root.Thread.ID}}&edit={{.ID}}" class="has-text-dark">
|
|
<span class="icon"><i class="fa fa-edit"></i></span>
|
|
<span>Edit</span>
|
|
</a>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if or $Root.CanModerate ($Root.CurrentUser.HasAdminScope "social.moderator.forum") (eq $Root.CurrentUser.ID .User.ID)}}
|
|
<div class="column is-narrow">
|
|
<a href="/forum/post?to={{$Root.Forum.Fragment}}&thread={{$Root.Thread.ID}}&edit={{.ID}}&delete=true" onclick="return confirm('Are you sure you want to delete this comment?')" class="has-text-dark">
|
|
<span class="icon"><i class="fa fa-trash"></i></span>
|
|
<span>Delete</span>
|
|
</a>
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Copy link to clipboard -->
|
|
<div class="column is-narrow">
|
|
<a href="/go/comment?id={{.ID}}" onclick="navigator.clipboard.writeText(this.href); window.location='#p{{.ID}}'; return false"
|
|
class="has-text-dark"
|
|
title="Copy link to clipboard">
|
|
<span class="icon"><i class="fa fa-paragraph"></i></span>
|
|
<span>Link</span>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Admin: history -->
|
|
{{if $Root.CurrentUser.HasAdminScope "admin.changelog"}}
|
|
<div class="column is-narrow">
|
|
<a href="/admin/changelog?table_name=comments&table_id={{.ID}}" class="has-text-warning">
|
|
<span class="icon"><i class="fa fa-clipboard-list"></i></span>
|
|
<span>Change log</span>
|
|
</a>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
{{if $Root.CurrentUser.IsAdmin}}
|
|
<div>
|
|
<span class="tag is-primary is-light">
|
|
<span class="icon"><i class="fa fa-database"></i></span>
|
|
<span>ID: {{.ID}}</span>
|
|
</span>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
<div class="block p-2">
|
|
{{SimplePager .Pager}}
|
|
</div>
|
|
|
|
{{if .Thread.NoReply}}
|
|
<div class="block notification is-warning is-light">
|
|
<i class="fa fa-ban pr-2"></i>
|
|
This thread is not accepting any new replies.
|
|
</div>
|
|
{{else}}
|
|
<div class="block p-2" id="reply">
|
|
<div class="card">
|
|
<header class="card-header has-background-link">
|
|
<p class="card-header-title has-text-light">
|
|
<i class="fa fa-comment mr-2"></i>
|
|
Reply to Thread
|
|
</p>
|
|
</header>
|
|
|
|
<div class="card-content">
|
|
<form action="/forum/post?to={{.Forum.Fragment}}&thread={{.Thread.ID}}" method="POST" enctype="multipart/form-data">
|
|
{{InputCSRF}}
|
|
|
|
<div class="field block">
|
|
<label for="message" class="label">Message</label>
|
|
<textarea class="textarea" cols="80" rows="6"
|
|
name="message"
|
|
id="message"
|
|
{{if not .Forum.PermitPhotos}}required{{end}}
|
|
placeholder="Message"></textarea>
|
|
<p class="help">
|
|
<a href="/markdown" target="_blank">Markdown formatting</a> supported.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Photo board that allows attachments? -->
|
|
{{if .Forum.PermitPhotos}}
|
|
<div class="field block mb-4">
|
|
<input type="hidden" name="photo_intent" id="photoIntent">
|
|
|
|
<!-- Drag/Drop Modal -->
|
|
<div class="modal" id="drop-modal">
|
|
<div class="modal-background"></div>
|
|
<div class="modal-content">
|
|
<div class="box content has-text-centered">
|
|
<h1><i class="fa fa-upload mr-2"></i> Drop image to select it for upload</h1>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<label class="label">Photo Attachment</label>
|
|
<div class="file has-name is-fullwidth">
|
|
<label class="file-label">
|
|
<input class="file-input" type="file"
|
|
name="file"
|
|
id="file"
|
|
accept=".jpg,.jpeg,.jpe,.png">
|
|
<span class="file-cta">
|
|
<span class="file-icon">
|
|
<i class="fas fa-upload"></i>
|
|
</span>
|
|
<span class="file-label">
|
|
Choose a file…
|
|
</span>
|
|
</span>
|
|
<span class="file-name" id="fileName">
|
|
Select a file
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
<div class="field has-text-centered">
|
|
<button type="submit"
|
|
name="intent"
|
|
value="preview"
|
|
class="button is-link">
|
|
Preview
|
|
</button>
|
|
<button type="submit"
|
|
name="intent"
|
|
value="submit"
|
|
class="button is-success">
|
|
Post Message
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Enhance inline Quote and Reply buttons to activate the on-page comment textarea. -->
|
|
<script src="/static/js/inline-replies.js?build={{.BuildHash}}"></script>
|
|
|
|
<!-- Script for photo upload on photo boards -->
|
|
{{if .Forum.PermitPhotos}}
|
|
<script type="text/javascript">
|
|
window.addEventListener("DOMContentLoaded", (event) => {
|
|
let $file = document.querySelector("#file"),
|
|
$fileName = document.querySelector("#fileName"),
|
|
$dropArea = document.querySelector("#drop-modal"),
|
|
$photoIntent = document.querySelector("#photoIntent"),
|
|
$body = document.querySelector("body");
|
|
|
|
// Common handler for file selection (file input or drag/drop)
|
|
let onFile = (file) => {
|
|
$photoIntent.value = "upload";
|
|
$fileName.innerHTML = file.name;
|
|
};
|
|
|
|
// Set up drag/drop file upload events.
|
|
$body.addEventListener("dragenter", function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
$dropArea.classList.add("is-active");
|
|
});
|
|
$body.addEventListener("dragover", function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
$dropArea.classList.add("is-active");
|
|
});
|
|
$body.addEventListener("dragleave", function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
$dropArea.classList.remove("is-active");
|
|
});
|
|
$body.addEventListener("drop", function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
$dropArea.classList.remove("is-active");
|
|
|
|
// Grab the file.
|
|
let dt = e.dataTransfer;
|
|
let file = dt.files[0];
|
|
|
|
// Set the file on the input field too.
|
|
$file.files = dt.files;
|
|
|
|
onFile(file);
|
|
});
|
|
|
|
// File input handler.
|
|
$file.addEventListener("change", function() {
|
|
let file = this.files[0];
|
|
onFile(file);
|
|
});
|
|
});
|
|
</script>
|
|
{{end}}<!-- .Forum.PermitPhotos -->
|
|
|
|
{{end}}
|