website/web/templates/forum/thread.html
Noah 47f898561c Forum Photo Attachments
* Add support to upload a picture to forum posts and replies, in forums that
  have the PermitPhotos setting enabled.
* New DB table: CommentPhoto holds the association between a photo and a
  forum ID. Photos can be uploaded at preview time (before a CommentID is
  available) and get associated to the CommentID on save.
* Cron endpoint /v1/comment-photos/remove-orphaned can clean up orphaned
  photos without a CommentID older than 24 hours.
* Add "Photo Boards" as a default forum category for new boards.
2022-10-20 21:02:30 -07:00

428 lines
17 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">
<h1 class="title">
<a href="/f/{{.Forum.Fragment}}" class="has-text-light">
<span class="icon mr-4"><i class="fa fa-comments"></i></span>
<span>{{.Forum.Title}}</span>
</a>
</h1>
</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 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">
<nav class="pagination" role="navigation" aria-label="pagination">
<a class="pagination-previous{{if not .Pager.HasPrevious}} is-disabled{{end}}" title="Previous"
href="{{.Request.URL.Path}}?{{QueryPlus "page" .Pager.Previous}}">Previous</a>
<a class="pagination-next{{if not .Pager.HasNext}} is-disabled{{end}}" title="Next"
href="{{.Request.URL.Path}}?{{QueryPlus "page" .Pager.Next}}">Next page</a>
<ul class="pagination-list">
{{$Root := .}}
{{range .Pager.Iter}}
<li>
<a class="pagination-link{{if .IsCurrent}} is-current{{end}}"
aria-label="Page {{.Page}}"
href="{{$Root.Request.URL.Path}}?{{QueryPlus "page" .Page}}">
{{.Page}}
</a>
</li>
{{end}}
</ul>
</nav>
</div>
{{$Root := .}}
<div class="block p-2">
{{range .Comments}}
<div class="box has-background-link-light">
<div class="columns">
<div class="column is-2 has-text-centered">
<div>
<a href="/u/{{.User.Username}}">
{{template "avatar-96x96" .User}}
</a>
</div>
<a href="/u/{{.User.Username}}">{{.User.Username}}</a>
{{if .User.IsAdmin}}
<div class="is-size-7 mt-1">
<span class="tag is-danger is-light">
<span class="icon"><i class="fa fa-gavel"></i></span>
<span>Admin</span>
</span>
</div>
{{end}}
</div>
<div class="column content">
{{ToMarkdown .Message}}
{{if .IsEdited}}
<div class="mt-4">
<em title="{{.UpdatedAt.Format "2006-01-02 15:04:05"}}">
<small>Edited {{SincePrettyCoarse .UpdatedAt}} ago</small>
</em>
</div>
{{end}}
<!-- Photo attachments? -->
{{$Photos := $Root.PhotoMap.Get .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}}
<div class="image mt-4">
<img src="{{PhotoURL .Filename}}">
</div>
{{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">
<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}}&quote={{.ID}}"
class="has-text-dark nonshy-quote-button" data-quote-body="{{.Message}}">
<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.IsAdmin (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>
<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}}
</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>
{{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">
Markdown formatting 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}}
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
const $message = document.querySelector("#message");
// Enhance the in-post Quote and Reply buttons to activate the reply field
// at the page footer instead of going to the dedicated comment page.
(document.querySelectorAll(".nonshy-quote-button") || []).forEach(node => {
const message = node.dataset.quoteBody;
node.addEventListener("click", (e) => {
e.preventDefault();
// Prepare the quoted message.
var lines = [];
for (let line of message.split("\n")) {
lines.push("> " + line);
}
$message.value += lines.join("\n") + "\n\n";
$message.scrollIntoView();
$message.focus();
});
});
(document.querySelectorAll(".nonshy-reply-button") || []).forEach(node => {
const replyTo = node.dataset.replyTo;
node.addEventListener("click", (e) => {
e.preventDefault();
$message.value += "@" + replyTo + "\n\n";
$message.scrollIntoView();
$message.focus();
});
});
});
</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}}