Comments on Photos
* Add permalink URL for photos to view their comment threads. * Commenters can Edit or Delete their own comments. * Photo owners can Delete any comment on it. * Update Privacy Policy
This commit is contained in:
parent
0690a9a5b0
commit
c1268ae9b1
192
pkg/controller/comment/post_comment.go
Normal file
192
pkg/controller/comment/post_comment.go
Normal file
|
@ -0,0 +1,192 @@
|
|||
package comment
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// PostComment view - for previewing or submitting your comment.
|
||||
func PostComment() http.HandlerFunc {
|
||||
tmpl := templates.Must("comment/post_comment.html")
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Query params.
|
||||
var (
|
||||
tableName = r.FormValue("table_name")
|
||||
tableID uint64
|
||||
editCommentID = r.FormValue("edit") // edit your comment
|
||||
isDelete = r.FormValue("delete") == "true"
|
||||
intent = r.FormValue("intent") // preview or submit
|
||||
message = r.PostFormValue("message") // comment body
|
||||
comment *models.Comment // if editing a comment
|
||||
fromURL = r.FormValue("next") // what page to send back to
|
||||
)
|
||||
|
||||
// Parse the table ID param.
|
||||
if idStr := r.FormValue("table_id"); idStr == "" {
|
||||
session.FlashError(w, r, "Comment table ID required.")
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
} else {
|
||||
if idInt, err := strconv.Atoi(idStr); err != nil {
|
||||
session.FlashError(w, r, "Comment table ID invalid.")
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
} else {
|
||||
tableID = uint64(idInt)
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect URL must be relative.
|
||||
if !strings.HasPrefix(fromURL, "/") {
|
||||
// Maybe it's URL encoded?
|
||||
fromURL, _ = url.QueryUnescape(fromURL)
|
||||
if !strings.HasPrefix(fromURL, "/") {
|
||||
fromURL = "/"
|
||||
}
|
||||
}
|
||||
|
||||
// Validate everything else.
|
||||
if _, ok := models.CommentableTables[tableName]; !ok {
|
||||
session.FlashError(w, r, "You can not comment on that.")
|
||||
templates.Redirect(w, "/")
|
||||
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
|
||||
}
|
||||
|
||||
// Who will we notify about this comment? e.g. if commenting on a photo,
|
||||
// this is the user who owns the photo.
|
||||
var notifyUser *models.User
|
||||
switch tableName {
|
||||
case "photos":
|
||||
if photo, err := models.GetPhoto(tableID); err == nil {
|
||||
if user, err := models.GetUser(photo.UserID); err == nil {
|
||||
notifyUser = user
|
||||
} else {
|
||||
log.Error("Comments: couldn't get NotifyUser for photo ID %d (user ID %d): %s",
|
||||
tableID, photo.UserID, err,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
log.Error("Comments: couldn't get NotifyUser for photo ID %d: %s", tableID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Are we editing or deleting our comment?
|
||||
if len(editCommentID) > 0 {
|
||||
if i, err := strconv.Atoi(editCommentID); err == nil {
|
||||
if found, err := models.GetComment(uint64(i)); err == nil {
|
||||
comment = found
|
||||
|
||||
// Verify that it is indeed OUR comment to manage:
|
||||
// - If the current user posted it
|
||||
// - If we are an admin
|
||||
// - If we are the notifyUser for this comment (they can delete, not edit).
|
||||
if currentUser.ID != comment.UserID && !currentUser.IsAdmin &&
|
||||
!(notifyUser != nil && currentUser.ID == notifyUser.ID && isDelete) {
|
||||
templates.ForbiddenPage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize the form w/ the content of this message.
|
||||
if r.Method == http.MethodGet {
|
||||
message = comment.Message
|
||||
}
|
||||
|
||||
// Are we DELETING this comment?
|
||||
if isDelete {
|
||||
if err := comment.Delete(); err != nil {
|
||||
session.FlashError(w, r, "Error deleting your commenting: %s", err)
|
||||
} else {
|
||||
session.Flash(w, r, "Your comment has been deleted.")
|
||||
}
|
||||
templates.Redirect(w, fromURL)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Comment not found - show the Forbidden page anyway.
|
||||
templates.ForbiddenPage(w, r)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
templates.NotFoundPage(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Submitting the form.
|
||||
if r.Method == http.MethodPost {
|
||||
// Default intent is preview unless told to submit.
|
||||
if intent == "submit" {
|
||||
// Are we modifying an existing comment?
|
||||
if comment != nil {
|
||||
comment.Message = message
|
||||
|
||||
if err := comment.Save(); err != nil {
|
||||
session.FlashError(w, r, "Couldn't save comment: %s", err)
|
||||
} else {
|
||||
session.Flash(w, r, "Comment updated!")
|
||||
}
|
||||
templates.Redirect(w, fromURL)
|
||||
return
|
||||
}
|
||||
|
||||
// Create the comment.
|
||||
if comment, err := models.AddComment(
|
||||
currentUser,
|
||||
tableName,
|
||||
tableID,
|
||||
message,
|
||||
); err != nil {
|
||||
session.FlashError(w, r, "Couldn't create comment: %s", err)
|
||||
} else {
|
||||
session.Flash(w, r, "Comment added!")
|
||||
templates.Redirect(w, fromURL)
|
||||
|
||||
// Notify the recipient of the comment.
|
||||
if notifyUser != nil && notifyUser.ID != currentUser.ID {
|
||||
notif := &models.Notification{
|
||||
UserID: notifyUser.ID,
|
||||
AboutUser: *currentUser,
|
||||
Type: models.NotificationComment,
|
||||
TableName: comment.TableName,
|
||||
TableID: comment.TableID,
|
||||
Message: message,
|
||||
Link: fromURL,
|
||||
}
|
||||
if err := models.CreateNotification(notif); err != nil {
|
||||
log.Error("Couldn't create Comment notification: %s", err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"Intent": intent,
|
||||
"EditCommentID": editCommentID,
|
||||
"Message": message,
|
||||
"TableName": tableName,
|
||||
"TableID": tableID,
|
||||
"Next": fromURL,
|
||||
}
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
|
@ -52,12 +52,14 @@ func SiteGallery() http.HandlerFunc {
|
|||
photoIDs = append(photoIDs, p.ID)
|
||||
}
|
||||
likeMap := models.MapLikes(currentUser, "photos", photoIDs)
|
||||
commentMap := models.MapCommentCounts("photos", photoIDs)
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"IsSiteGallery": true,
|
||||
"Photos": photos,
|
||||
"UserMap": userMap,
|
||||
"LikeMap": likeMap,
|
||||
"CommentMap": commentMap,
|
||||
"Pager": pager,
|
||||
"ViewStyle": viewStyle,
|
||||
}
|
||||
|
|
|
@ -97,6 +97,7 @@ func UserPhotos() http.HandlerFunc {
|
|||
photoIDs = append(photoIDs, p.ID)
|
||||
}
|
||||
likeMap := models.MapLikes(currentUser, "photos", photoIDs)
|
||||
commentMap := models.MapCommentCounts("photos", photoIDs)
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"IsOwnPhotos": currentUser.ID == user.ID,
|
||||
|
@ -105,6 +106,7 @@ func UserPhotos() http.HandlerFunc {
|
|||
"PhotoCount": models.CountPhotos(user.ID),
|
||||
"Pager": pager,
|
||||
"LikeMap": likeMap,
|
||||
"CommentMap": commentMap,
|
||||
"ViewStyle": viewStyle,
|
||||
"ExplicitCount": explicitCount,
|
||||
}
|
||||
|
|
92
pkg/controller/photo/view.go
Normal file
92
pkg/controller/photo/view.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package photo
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
// View photo controller to see the comment thread.
|
||||
func View() http.HandlerFunc {
|
||||
tmpl := templates.Must("photo/permalink.html")
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Required query param: the photo ID.
|
||||
var photo *models.Photo
|
||||
if idStr := r.FormValue("id"); idStr == "" {
|
||||
session.FlashError(w, r, "Missing photo ID parameter.")
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
} else {
|
||||
if idInt, err := strconv.Atoi(idStr); err != nil {
|
||||
session.FlashError(w, r, "Invalid ID parameter.")
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
} else {
|
||||
if found, err := models.GetPhoto(uint64(idInt)); err != nil {
|
||||
templates.NotFoundPage(w, r)
|
||||
return
|
||||
} else {
|
||||
photo = found
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find the photo's owner.
|
||||
user, err := models.GetUser(photo.UserID)
|
||||
if err != nil {
|
||||
templates.NotFoundPage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Load the current user in case they are viewing their own page.
|
||||
currentUser, err := session.CurrentUser(r)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Unexpected error: couldn't get CurrentUser")
|
||||
}
|
||||
var isOwnPhoto = currentUser.ID == user.ID
|
||||
|
||||
// Is either one blocking?
|
||||
if models.IsBlocking(currentUser.ID, user.ID) && !currentUser.IsAdmin {
|
||||
templates.NotFoundPage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Is this user private and we're not friends?
|
||||
var (
|
||||
areFriends = models.AreFriends(user.ID, currentUser.ID)
|
||||
isPrivate = user.Visibility == models.UserVisibilityPrivate && !areFriends
|
||||
)
|
||||
if isPrivate && !currentUser.IsAdmin && !isOwnPhoto {
|
||||
session.FlashError(w, r, "This user's profile page and photo gallery are private.")
|
||||
templates.Redirect(w, "/u/"+user.Username)
|
||||
return
|
||||
}
|
||||
|
||||
// Get Likes information about these photos.
|
||||
likeMap := models.MapLikes(currentUser, "photos", []uint64{photo.ID})
|
||||
commentMap := models.MapCommentCounts("photos", []uint64{photo.ID})
|
||||
|
||||
// Get all the comments.
|
||||
comments, err := models.ListComments("photos", photo.ID)
|
||||
if err != nil {
|
||||
log.Error("Couldn't list comments for photo %d: %s", photo.ID, err)
|
||||
}
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"IsOwnPhoto": currentUser.ID == user.ID,
|
||||
"User": user,
|
||||
"Photo": photo,
|
||||
"LikeMap": likeMap,
|
||||
"CommentMap": commentMap,
|
||||
"Comments": comments,
|
||||
}
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
|
@ -3,6 +3,7 @@ package models
|
|||
import (
|
||||
"time"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
|
@ -18,6 +19,12 @@ type Comment struct {
|
|||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// CommentableTables are the set of table names that allow comments (via the
|
||||
// generic "/comments" URI which accepts a table_name param)
|
||||
var CommentableTables = map[string]interface{}{
|
||||
"photos": nil,
|
||||
}
|
||||
|
||||
// Preload related tables for the forum (classmethod).
|
||||
func (c *Comment) Preload() *gorm.DB {
|
||||
return DB.Preload("User.ProfilePhoto")
|
||||
|
@ -74,6 +81,16 @@ func PaginateComments(user *User, tableName string, tableID uint64, pager *Pagin
|
|||
return cs, result.Error
|
||||
}
|
||||
|
||||
// ListComments returns a complete set of comments without paging.
|
||||
func ListComments(tableName string, tableID uint64) ([]*Comment, error) {
|
||||
var cs []*Comment
|
||||
result := (&Comment{}).Preload().Where(
|
||||
"table_name = ? AND table_id = ?",
|
||||
tableName, tableID,
|
||||
).Order("created_at asc").Find(&cs)
|
||||
return cs, result.Error
|
||||
}
|
||||
|
||||
// Save a comment.
|
||||
func (c *Comment) Save() error {
|
||||
return DB.Save(c).Error
|
||||
|
@ -83,3 +100,50 @@ func (c *Comment) Save() error {
|
|||
func (c *Comment) Delete() error {
|
||||
return DB.Delete(c).Error
|
||||
}
|
||||
|
||||
type CommentCountMap map[uint64]int64
|
||||
|
||||
// MapCommentCounts collects total numbers of comments over a set of table IDs. Returns a
|
||||
// map of table ID (uint64) to comment counts for each (int64).
|
||||
func MapCommentCounts(tableName string, tableIDs []uint64) CommentCountMap {
|
||||
var result = CommentCountMap{}
|
||||
|
||||
// Initialize the result set.
|
||||
for _, id := range tableIDs {
|
||||
result[id] = 0
|
||||
}
|
||||
|
||||
// Hold the result of the grouped count query.
|
||||
type group struct {
|
||||
ID uint64
|
||||
Comments int64
|
||||
}
|
||||
var groups = []group{}
|
||||
|
||||
// Map the counts of comments to each of these IDs.
|
||||
if res := DB.Table(
|
||||
"comments",
|
||||
).Select(
|
||||
"table_id AS id, count(id) AS comments",
|
||||
).Where(
|
||||
"table_name = ? AND table_id IN ?",
|
||||
tableName, tableIDs,
|
||||
).Group("table_id").Scan(&groups); res.Error != nil {
|
||||
log.Error("MapCommentCounts: count query: %s", res.Error)
|
||||
}
|
||||
|
||||
// Map the counts back in.
|
||||
for _, row := range groups {
|
||||
result[row.ID] = row.Comments
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Get a comment count for the given table ID from the map.
|
||||
func (cc CommentCountMap) Get(id uint64) int64 {
|
||||
if value, ok := cc[id]; ok {
|
||||
return value
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ type Notification struct {
|
|||
TableName string // on which of your tables (photos, comments, ...)
|
||||
TableID uint64
|
||||
Message string // text associated, e.g. copy of comment added
|
||||
Link string // associated URL, e.g. for comments
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
@ -148,8 +149,8 @@ func MapNotifications(ns []*Notification) NotificationMap {
|
|||
"notifications.id IN ?",
|
||||
IDs,
|
||||
).Scan(&scan)
|
||||
if err != nil {
|
||||
log.Error("Couldn't select photo IDs for notifications: %s", err)
|
||||
if err.Error != nil {
|
||||
log.Error("Couldn't select photo IDs for notifications: %s", err.Error)
|
||||
}
|
||||
|
||||
// Collect and load all the photos by ID.
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"code.nonshy.com/nonshy/website/pkg/controller/admin"
|
||||
"code.nonshy.com/nonshy/website/pkg/controller/api"
|
||||
"code.nonshy.com/nonshy/website/pkg/controller/block"
|
||||
"code.nonshy.com/nonshy/website/pkg/controller/comment"
|
||||
"code.nonshy.com/nonshy/website/pkg/controller/forum"
|
||||
"code.nonshy.com/nonshy/website/pkg/controller/friend"
|
||||
"code.nonshy.com/nonshy/website/pkg/controller/inbox"
|
||||
|
@ -41,6 +42,7 @@ func New() http.Handler {
|
|||
mux.Handle("/u/", middleware.LoginRequired(account.Profile()))
|
||||
mux.Handle("/photo/upload", middleware.LoginRequired(photo.Upload()))
|
||||
mux.Handle("/photo/u/", middleware.LoginRequired(photo.UserPhotos()))
|
||||
mux.Handle("/photo/view", middleware.LoginRequired(photo.View()))
|
||||
mux.Handle("/photo/edit", middleware.LoginRequired(photo.Edit()))
|
||||
mux.Handle("/photo/delete", middleware.LoginRequired(photo.Delete()))
|
||||
mux.Handle("/photo/certification", middleware.LoginRequired(photo.Certification()))
|
||||
|
@ -51,6 +53,7 @@ func New() http.Handler {
|
|||
mux.Handle("/friends/add", middleware.LoginRequired(friend.AddFriend()))
|
||||
mux.Handle("/users/block", middleware.LoginRequired(block.BlockUser()))
|
||||
mux.Handle("/users/blocked", middleware.LoginRequired(block.Blocked()))
|
||||
mux.Handle("/comments", middleware.LoginRequired(comment.PostComment()))
|
||||
mux.Handle("/admin/unimpersonate", middleware.LoginRequired(admin.Unimpersonate()))
|
||||
|
||||
// Certification Required. Pages that only full (verified) members can access.
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -74,6 +75,13 @@ func TemplateFuncs(r *http.Request) template.FuncMap {
|
|||
"SubtractInt": func(a, b int) int {
|
||||
return a - b
|
||||
},
|
||||
"UrlEncode": func(values ...interface{}) string {
|
||||
var result string
|
||||
for _, value := range values {
|
||||
result += url.QueryEscape(fmt.Sprintf("%v", value))
|
||||
}
|
||||
return result
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -194,15 +194,32 @@
|
|||
<a href="/u/{{.AboutUser.Username}}"><strong>{{.AboutUser.Username}}</strong></a>
|
||||
liked your
|
||||
{{if eq .TableName "photos"}}
|
||||
photo.
|
||||
{{if $Body.Photo}}
|
||||
<a href="/photo/view?id={{$Body.Photo.ID}}">photo</a>.
|
||||
{{else}}
|
||||
photo.
|
||||
{{end}}
|
||||
{{else if eq .TableName "users"}}
|
||||
profile page.
|
||||
{{else}}
|
||||
{{.TableName}}.
|
||||
{{end}}
|
||||
</span>
|
||||
{{else if eq .Type "comment"}}
|
||||
<span class="icon"><i class="fa fa-comment has-text-success"></i></span>
|
||||
<span>
|
||||
<a href="/u/{{.AboutUser.Username}}"><strong>{{.AboutUser.Username}}</strong></a>
|
||||
commented on your
|
||||
<a href="{{.Link}}">
|
||||
{{if eq .TableName "photos"}}
|
||||
photo:
|
||||
{{else}}
|
||||
{{.TableName}}:
|
||||
{{end}}
|
||||
</a>
|
||||
</span>
|
||||
{{else if eq .Type "friendship_approved"}}
|
||||
<span class="icon"><i class="fa fa-user-group"></i></span>
|
||||
<span class="icon"><i class="fa fa-user-group has-text-success"></i></span>
|
||||
<span>
|
||||
<a href="/u/{{.AboutUser.Username}}"><strong>{{.AboutUser.Username}}</strong></a>
|
||||
accepted your friend request!
|
||||
|
@ -224,7 +241,7 @@
|
|||
|
||||
<!-- Attached message? -->
|
||||
{{if .Message}}
|
||||
<div class="block content">
|
||||
<div class="block content mb-1">
|
||||
{{ToMarkdown .Message}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
@ -232,7 +249,15 @@
|
|||
<!-- Photo caption? -->
|
||||
{{if $Body.Photo}}
|
||||
<div class="block">
|
||||
<em>{{or $Body.Photo.Caption "No caption."}}</em>
|
||||
<!-- If it's a comment, have a link to view it -->
|
||||
{{if eq .Type "comment"}}
|
||||
<div class="is-size-7 pt-1">
|
||||
<span class="icon"><i class="fa fa-arrow-right"></i></span>
|
||||
<a href="{{.Link}}">See all comments</a>
|
||||
</div>
|
||||
{{else}}
|
||||
<em>{{or $Body.Photo.Caption "No caption."}}</em>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
@ -245,7 +270,9 @@
|
|||
<!-- Attached photo? -->
|
||||
{{if $Body.PhotoID}}
|
||||
<div class="column is-one-quarter">
|
||||
<img src="{{PhotoURL $Body.Photo.Filename}}">
|
||||
<a href="/photo/view?id={{$Body.Photo.ID}}">
|
||||
<img src="{{PhotoURL $Body.Photo.Filename}}">
|
||||
</a>
|
||||
|
||||
{{if $Body.Photo.Caption}}
|
||||
<small>{{$Body.Photo.Caption}}</small>
|
||||
|
|
91
web/templates/comment/post_comment.html
Normal file
91
web/templates/comment/post_comment.html
Normal file
|
@ -0,0 +1,91 @@
|
|||
{{define "title"}}
|
||||
{{if .EditCommentID}}
|
||||
Edit Comment
|
||||
{{else}}
|
||||
New Comment
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{define "content"}}
|
||||
<div class="container">
|
||||
<section class="hero is-info is-bold">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
{{if .EditCommentID}}
|
||||
Edit Comment
|
||||
{{else}}
|
||||
Add Comment
|
||||
{{end}}
|
||||
</h1>
|
||||
</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 has-background-link">
|
||||
<p class="card-header-title has-text-light">
|
||||
<span class="icon"><i class="fa fa-message"></i></span>
|
||||
{{if .EditCommentID}}
|
||||
Edit Comment
|
||||
{{else}}
|
||||
New Comment
|
||||
{{end}}
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
|
||||
{{if and (eq .Request.Method "POST") (ne .Message "")}}
|
||||
<label class="label">Preview:</label>
|
||||
<div class="box content has-background-warning-light">
|
||||
{{ToMarkdown .Message}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<form action="{{.Request.URL.Path}}" method="POST">
|
||||
{{InputCSRF}}
|
||||
<input type="hidden" name="table_name" value="{{.TableName}}">
|
||||
<input type="hidden" name="table_id" value="{{.TableID}}">
|
||||
<input type="hidden" name="next" value="{{.Next}}">
|
||||
<input type="hidden" name="edit" value="{{.EditCommentID}}">
|
||||
|
||||
<div class="field block">
|
||||
<label for="message" class="label">Message</label>
|
||||
<textarea class="textarea" cols="80" rows="8"
|
||||
name="message"
|
||||
id="message"
|
||||
required
|
||||
placeholder="Message">{{.Message}}</textarea>
|
||||
<p class="help">
|
||||
Markdown formatting supported.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
|
@ -127,6 +127,14 @@
|
|||
<div class="column content">
|
||||
{{ToMarkdown .Message}}
|
||||
|
||||
{{if .UpdatedAt.After .CreatedAt}}
|
||||
<div class="mt-4">
|
||||
<em title="{{.UpdatedAt.Format "2006-01-02 15:04:05"}}">
|
||||
<small>Edited {{SincePrettyCoarse .UpdatedAt}} ago</small>
|
||||
</em>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<hr class="has-background-grey mb-2">
|
||||
|
||||
<div class="columns is-mobile is-multiline is-size-7 mb-0">
|
||||
|
|
|
@ -263,20 +263,29 @@
|
|||
|
||||
{{template "card-body" .}}
|
||||
|
||||
<!-- Like button -->
|
||||
<div class="mt-4">
|
||||
{{$Like := $Root.LikeMap.Get .ID}}
|
||||
<button type="button" class="button is-small nonshy-like-button"
|
||||
data-table-name="photos" 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>
|
||||
</button>
|
||||
<!-- Like & Comments buttons -->
|
||||
<div class="mt-4 columns is-centered is-mobile is-gapless">
|
||||
<div class="column is-narrow mr-1">
|
||||
{{$Like := $Root.LikeMap.Get .ID}}
|
||||
<button type="button" class="button is-small nonshy-like-button"
|
||||
data-table-name="photos" 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>
|
||||
</button>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
{{$Comments := $Root.CommentMap.Get .ID}}
|
||||
<a href="/photo/view?id={{.ID}}#comments" class="button is-small">
|
||||
<span class="icon"><i class="fa fa-comment"></i></span>
|
||||
<span>{{$Comments}} Comment{{Pluralize64 $Comments}}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -360,20 +369,29 @@
|
|||
|
||||
{{template "card-body" .}}
|
||||
|
||||
<!-- Like button -->
|
||||
<div class="mt-4">
|
||||
{{$Like := $Root.LikeMap.Get .ID}}
|
||||
<button type="button" class="button is-small nonshy-like-button"
|
||||
data-table-name="photos" 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>
|
||||
</button>
|
||||
<!-- Like & Comments buttons -->
|
||||
<div class="mt-4 columns is-centered is-mobile is-gapless">
|
||||
<div class="column is-narrow mr-1">
|
||||
{{$Like := $Root.LikeMap.Get .ID}}
|
||||
<button type="button" class="button is-small nonshy-like-button"
|
||||
data-table-name="photos" 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>
|
||||
</button>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
{{$Comments := $Root.CommentMap.Get .ID}}
|
||||
<a href="/photo/view?id={{.ID}}#comments" class="button is-small">
|
||||
<span class="icon"><i class="fa fa-comment"></i></span>
|
||||
<span>{{$Comments}} Comment{{Pluralize64 $Comments}}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -385,7 +403,7 @@
|
|||
{{if not $Root.IsOwnPhotos}}
|
||||
<a class="card-footer-item has-text-danger" href="/contact?intent=report&subject=report.photo&id={{.ID}}">
|
||||
<span class="icon"><i class="fa fa-flag"></i></span>
|
||||
<span>Report</span>
|
||||
<span class="is-hidden-desktop">Report</span>
|
||||
</a>
|
||||
{{end}}
|
||||
</footer>
|
||||
|
|
264
web/templates/photo/permalink.html
Normal file
264
web/templates/photo/permalink.html
Normal file
|
@ -0,0 +1,264 @@
|
|||
{{define "title"}}Upload a Photo{{end}}
|
||||
{{define "content"}}
|
||||
<div class="container">
|
||||
<section class="hero is-info is-bold">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
<span class="icon mr-4">
|
||||
<i class="fa fa-image"></i>
|
||||
</span>
|
||||
<span>{{or .Photo.Caption "Photo"}}</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{ $Root := . }}
|
||||
{{ $User := .CurrentUser }}
|
||||
{{ $Comments := .CommentMap.Get .Photo.ID }}
|
||||
|
||||
<div class="block p-4">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/u/{{.User.Username}}">
|
||||
<span class="icon"><i class="fa fa-user"></i></span>
|
||||
<span>{{.User.Username}}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/photo/u/{{.User.Username}}">Photos</a>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<a href="{{.Request.URL.Path}}" aria-current="page">Comments</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="block p-4">
|
||||
|
||||
<!-- Photo Card -->
|
||||
<div class="card block">
|
||||
<header class="card-header {{if .Photo.Explicit}}has-background-danger{{else}}has-background-link{{end}}">
|
||||
<div class="card-header-title has-text-light">
|
||||
<div class="columns is-mobile is-gapless nonshy-fullwidth">
|
||||
<div class="column is-narrow">
|
||||
<figure class="image is-24x24 mr-2">
|
||||
{{if gt .User.ProfilePhoto.ID 0}}
|
||||
<img src="{{PhotoURL .User.ProfilePhoto.CroppedFilename}}" class="is-rounded">
|
||||
{{else}}
|
||||
<img src="/static/img/shy.png" class="is-rounded has-background-warning">
|
||||
{{end}}
|
||||
</figure>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="/u/{{.User.Username}}" class="has-text-light">
|
||||
{{.User.Username}}
|
||||
<i class="fa fa-external-link ml-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<span class="icon">
|
||||
{{if eq .Photo.Visibility "friends"}}
|
||||
<i class="fa fa-user-group has-text-warning" title="Friends"></i>
|
||||
{{else if eq .Photo.Visibility "private"}}
|
||||
<i class="fa fa-lock has-text-private-light" title="Private"></i>
|
||||
{{else}}
|
||||
<i class="fa fa-eye has-text-link-light" title="Public"></i>
|
||||
{{end}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="card-image">
|
||||
<figure class="image">
|
||||
<img src="{{PhotoURL .Photo.Filename}}">
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
{{if .Photo.Caption}}
|
||||
{{.Photo.Caption}}
|
||||
{{else}}<em>No caption</em>{{end}}
|
||||
|
||||
<!-- Like & Comments buttons -->
|
||||
<div class="mt-4 mb-2 columns is-centered is-mobile is-gapless">
|
||||
<div class="column is-narrow mr-2">
|
||||
{{$Like := .LikeMap.Get .Photo.ID}}
|
||||
<button type="button" class="button is-small nonshy-like-button"
|
||||
data-table-name="photos" data-table-id="{{.Photo.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>
|
||||
</button>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<a href="/photo/view?id={{.Photo.ID}}#comments" class="button is-small">
|
||||
<span class="icon"><i class="fa fa-comment"></i></span>
|
||||
<span>{{$Comments}} Comment{{Pluralize64 $Comments}}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Photo controls buttons (edit/delete/report) -->
|
||||
<div class="my-0 columns is-centered is-mobile is-gapless">
|
||||
<!-- Owned photo: have edit/delete buttons too -->
|
||||
{{if or .IsOwnPhoto .CurrentUser.IsAdmin}}
|
||||
<div class="column is-narrow">
|
||||
<a href="/photo/edit?id={{.Photo.ID}}" class="button is-small">
|
||||
<span class="icon"><i class="fa fa-edit"></i></span>
|
||||
<span>Edit</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="column is-narrow ml-2">
|
||||
<a href="/photo/delete?id={{.Photo.ID}}" class="button is-small has-text-danger">
|
||||
<span class="icon"><i class="fa fa-trash"></i></span>
|
||||
<span>Delete</span>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Report button except on your own pic -->
|
||||
{{if not .IsOwnPhoto}}
|
||||
<div class="column is-narrow ml-2">
|
||||
<a href="/contact?intent=report&subject=report.photo&id={{.Photo.ID}}" class="button is-small has-text-danger">
|
||||
<span class="icon"><i class="fa fa-flag"></i></span>
|
||||
<span>Report</span>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /photo card -->
|
||||
|
||||
<!-- Comments Card -->
|
||||
<div class="card" id="comments">
|
||||
<header class="card-header has-background-success">
|
||||
<p class="card-header-title has-text-light">
|
||||
<span class="icon mr-2"><i class="fa fa-comment"></i></span>
|
||||
<span>{{$Comments}} Comment{{Pluralize64 $Comments}}</span>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="card-content">
|
||||
<form action="/comments" method="POST">
|
||||
{{InputCSRF}}
|
||||
<input type="hidden" name="table_name" value="photos">
|
||||
<input type="hidden" name="table_id" value="{{.Photo.ID}}">
|
||||
<input type="hidden" name="next" value="{{.Request.URL.Path}}?id={{.Photo.ID}}">
|
||||
|
||||
<div class="field">
|
||||
<label for="message">Add your comment</label>
|
||||
<textarea class="textarea" cols="80" rows="4"
|
||||
name="message" id="message"
|
||||
placeholder="Add your comment"></textarea>
|
||||
<p class="help">
|
||||
Markdown formatting supported.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field has-text-centered">
|
||||
<button type="submit" class="button is-link"
|
||||
name="intent" value="preview">
|
||||
Preview
|
||||
</button>
|
||||
|
||||
<button type="submit" class="button is-success"
|
||||
name="intent" value="submit">
|
||||
Post Comment
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="is-dark">
|
||||
|
||||
{{if eq $Comments 0}}
|
||||
<p>
|
||||
<em>There are no comments yet.</em>
|
||||
</p>
|
||||
{{else}}
|
||||
{{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}}">
|
||||
<figure class="image is-96x96 is-inline-block">
|
||||
{{if .User.ProfilePhoto.ID}}
|
||||
<img src="{{PhotoURL .User.ProfilePhoto.CroppedFilename}}">
|
||||
{{else}}
|
||||
<img src="/static/img/shy.png">
|
||||
{{end}}
|
||||
</figure>
|
||||
</a>
|
||||
</div>
|
||||
<a href="/u/{{.User.Username}}">{{.User.Username}}</a>
|
||||
</div>
|
||||
|
||||
<div class="column content">
|
||||
{{ToMarkdown .Message}}
|
||||
|
||||
{{if .UpdatedAt.After .CreatedAt}}
|
||||
<div class="mt-4">
|
||||
<em title="{{.UpdatedAt.Format "2006-01-02 15:04:05"}}">
|
||||
<small>Edited {{SincePrettyCoarse .UpdatedAt}} ago</small>
|
||||
</em>
|
||||
</div>
|
||||
{{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">
|
||||
<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 or $Root.CurrentUser.IsAdmin (eq $Root.CurrentUser.ID .User.ID)}}
|
||||
<div class="column is-narrow">
|
||||
<a href="/comments?table_name=photos&table_id={{$Root.Photo.ID}}&edit={{.ID}}&next={{UrlEncode $Root.Request.URL.Path "?id=" $Root.Photo.ID}}" class="has-text-dark">
|
||||
<span class="icon"><i class="fa fa-edit"></i></span>
|
||||
<span>Edit</span>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- The poster, the photo owner, and the admin can delete the comment -->
|
||||
{{if or $Root.CurrentUser.IsAdmin (eq $Root.CurrentUser.ID .User.ID) $Root.IsOwnPhoto}}
|
||||
<div class="column is-narrow">
|
||||
<a href="/comments?table_name=photos&table_id={{$Root.Photo.ID}}&edit={{.ID}}&delete=true&next={{UrlEncode $Root.Request.URL.Path "?id=" $Root.Photo.ID}}" 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
|
@ -25,7 +25,7 @@
|
|||
</p>
|
||||
|
||||
<p>
|
||||
This page was last updated on <strong>August 15, 2022.</strong>
|
||||
This page was last updated on <strong>August 26, 2022.</strong>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
@ -43,6 +43,21 @@
|
|||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
You may mark your entire profile as "Private" which limits some of the contact you
|
||||
may receive:
|
||||
<ul>
|
||||
<li>
|
||||
Only users you have approved as a friend can see your profile and your
|
||||
photo gallery.
|
||||
</li>
|
||||
<li>
|
||||
Your photos will <strong>never</strong> appear on the Site Gallery - not
|
||||
even to your friends. They will only see your photos by visiting your
|
||||
profile page directly.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
Profile photos have visibility settings including Public, Friends-only or Private:
|
||||
<ul>
|
||||
|
@ -73,10 +88,12 @@
|
|||
|
||||
<p>
|
||||
When you are uploading or editing a photo, there is a checkbox labeled "Gallery" where you
|
||||
can opt your photo in (or out) of the Site Gallery. Only <strong>public</strong> photos will
|
||||
ever appear on the Site Gallery (never private or friends-only photos). You are also able to
|
||||
<em>exclude</em> a public photo from the Site Gallery by unchecking the "Gallery" box on that
|
||||
photo.
|
||||
can opt your photo in (or out) of the Site Gallery. Only your <strong>public</strong> photos
|
||||
will appear on the Site Gallery by default; your <strong>friends-only</strong> photos may
|
||||
appear there for people you approved as a friend, or your private photos to people for whom
|
||||
you have granted access. You are also able to <em>exclude</em> a photo from the Site Gallery
|
||||
by unchecking the "Gallery" box on that photo -- then it will only be viewable on your own
|
||||
profile page, given its other permissions (friends/private).
|
||||
</p>
|
||||
|
||||
<h3>Deletion of User Data</h3>
|
||||
|
|
Loading…
Reference in New Issue
Block a user