Likes on Comments, and other minor improvements

* Add "Like" buttons to comments and forum posts.
* Make "private" profiles more private (logged-in users see only their profile
  pic, display name, and can friend request or message, if they are not approved
  friends of the private user)
* Add "logged-out view" visibility setting to profiles: to share a link to your
  page on other sites. Opt-in setting - default is login required to view your
  public profile page.
* CSRF cookie fix.
* Updated FAQ & Privacy pages.
pull/12/head
Noah 2022-08-29 20:00:15 -07:00
parent d2700490cc
commit 8419958b25
20 changed files with 250 additions and 74 deletions

View File

@ -29,7 +29,7 @@ const (
const (
BcryptCost = 14
SessionCookieName = "session_id"
CSRFCookieName = "csrf_token"
CSRFCookieName = "xsrf_token"
CSRFInputName = "_csrf" // html input name
SessionCookieMaxAge = 60 * 60 * 24 * 30
SessionRedisKeyFormat = "session/%s"

View File

@ -30,12 +30,26 @@ func Profile() http.HandlerFunc {
return
}
// Get the current user (if logged in).
// Forcing an external view? (preview of logged-out profile view for visibility=external accounts)
if r.FormValue("view") == "external" {
vars := map[string]interface{}{
"User": user,
"IsPrivate": true,
"IsExternalView": true,
}
if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
return
}
// Get the current user (if logged in). If not, check for external view.
currentUser, err := session.CurrentUser(r)
if err != nil {
// The viewer is not logged in, bail now with the basic profile page. If this
// user is private, redirect to login.
if user.Visibility == models.UserVisibilityPrivate {
// user doesn't allow external viewers, redirect to login page.
if user.Visibility != models.UserVisibilityExternal {
session.FlashError(w, r, "You must be signed in to view this page.")
templates.Redirect(w, "/login?next="+url.QueryEscape(r.URL.String()))
return

View File

@ -92,15 +92,17 @@ func Settings() http.HandlerFunc {
session.Flash(w, r, "Profile settings updated!")
case "preferences":
var (
explicit = r.PostFormValue("explicit") == "true"
private = r.PostFormValue("private") == "true"
explicit = r.PostFormValue("explicit") == "true"
visibility = models.UserVisibility(r.PostFormValue("visibility"))
)
user.Explicit = explicit
if private {
user.Visibility = models.UserVisibilityPrivate
} else {
user.Visibility = models.UserVisibilityPublic
user.Visibility = models.UserVisibilityPublic
for _, cmp := range models.UserVisibilityOptions {
if visibility == cmp {
user.Visibility = visibility
}
}
if err := user.Save(); err != nil {

View File

@ -5,8 +5,6 @@ import (
"errors"
"io/ioutil"
"net/http"
"code.nonshy.com/nonshy/website/pkg/log"
)
// Envelope is the standard JSON response envelope.
@ -27,8 +25,6 @@ func ParseJSON(r *http.Request, v interface{}) error {
return err
}
log.Error("body: %+v", body)
// Parse params from JSON.
if err := json.Unmarshal(body, v); err != nil {
return err

View File

@ -16,6 +16,7 @@ func Likes() http.HandlerFunc {
TableName string `json:"name"`
TableID uint64 `json:"id"`
Unlike bool `json:"unlike,omitempty"`
Referrer string `json:"page"`
}
// Response JSON schema.
@ -51,8 +52,18 @@ func Likes() http.HandlerFunc {
return
}
// Sanity check things. The page= param (Referrer) must be a relative URL, the path
// is useful for "liked your comment" notifications to supply the Link URL for the
// notification.
if len(req.Referrer) > 0 && req.Referrer[0] != '/' {
req.Referrer = ""
}
// Who do we notify about this like?
var targetUser *models.User
var (
targetUser *models.User
notificationMessage string
)
switch req.TableName {
case "photos":
if photo, err := models.GetPhoto(req.TableID); err == nil {
@ -70,6 +81,15 @@ func Likes() http.HandlerFunc {
} else {
log.Error("For like on users table: didn't find user %d: %s", req.TableID, err)
}
case "comments":
log.Error("subject is users, find %d", req.TableID)
if comment, err := models.GetComment(req.TableID); err == nil {
targetUser = &comment.User
notificationMessage = comment.Message
log.Warn("found user %s", targetUser.Username)
} else {
log.Error("For like on users table: didn't find user %d: %s", req.TableID, err)
}
}
// Is the table likeable?
@ -108,6 +128,8 @@ func Likes() http.HandlerFunc {
Type: models.NotificationLike,
TableName: req.TableName,
TableID: req.TableID,
Message: notificationMessage,
Link: req.Referrer,
}
if err := models.CreateNotification(notif); err != nil {
log.Error("Couldn't create Likes notification: %s", err)

View File

@ -14,7 +14,7 @@ import (
var ThreadPathRegexp = regexp.MustCompile(`^/forum/thread/(\d+)$`)
// Thread view for a specific board index.
// Thread view for the comment thread body of a forum post.
func Thread() http.HandlerFunc {
tmpl := templates.Must("forum/thread.html")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -74,6 +74,13 @@ func Thread() http.HandlerFunc {
return
}
// Get the like map for these comments.
commentIDs := []uint64{}
for _, com := range comments {
commentIDs = append(commentIDs, com.ID)
}
commentLikeMap := models.MapLikes(currentUser, "comments", commentIDs)
// Is the current user subscribed to notifications on this thread?
_, isSubscribed := models.IsSubscribed(currentUser, "threads", thread.ID)
@ -81,6 +88,7 @@ func Thread() http.HandlerFunc {
"Forum": forum,
"Thread": thread,
"Comments": comments,
"LikeMap": commentLikeMap,
"Pager": pager,
"IsSubscribed": isSubscribed,
}

View File

@ -76,17 +76,25 @@ func View() http.HandlerFunc {
log.Error("Couldn't list comments for photo %d: %s", photo.ID, err)
}
// Get the like map for these comments.
commentIDs := []uint64{}
for _, com := range comments {
commentIDs = append(commentIDs, com.ID)
}
commentLikeMap := models.MapLikes(currentUser, "comments", commentIDs)
// Is the current user subscribed to notifications on this thread?
_, isSubscribed := models.IsSubscribed(currentUser, "photos", photo.ID)
var vars = map[string]interface{}{
"IsOwnPhoto": currentUser.ID == user.ID,
"User": user,
"Photo": photo,
"LikeMap": likeMap,
"CommentMap": commentMap,
"Comments": comments,
"IsSubscribed": isSubscribed,
"IsOwnPhoto": currentUser.ID == user.ID,
"User": user,
"Photo": photo,
"LikeMap": likeMap,
"CommentMap": commentMap,
"Comments": comments,
"CommentLikeMap": commentLikeMap,
"IsSubscribed": isSubscribed,
}
if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -58,6 +58,7 @@ func MakeCSRFCookie(r *http.Request, w http.ResponseWriter) string {
Name: config.CSRFCookieName,
Value: token,
HttpOnly: true,
Path: "/",
}
// log.Debug("MakeCSRFCookie: giving cookie value %s to user", token)
http.SetCookie(w, cookie)

View File

@ -18,8 +18,9 @@ type Like struct {
// LikeableTables are the set of table names that allow likes (used by the JSON API).
var LikeableTables = map[string]interface{}{
"photos": nil,
"users": nil,
"photos": nil,
"users": nil,
"comments": nil,
}
// AddLike to something.

View File

@ -39,10 +39,18 @@ type User struct {
type UserVisibility string
const (
UserVisibilityPublic UserVisibility = "public"
UserVisibilityPrivate = "private"
UserVisibilityPublic UserVisibility = "public"
UserVisibilityExternal = "external"
UserVisibilityPrivate = "private"
)
// All visibility options.
var UserVisibilityOptions = []UserVisibility{
UserVisibilityPublic,
UserVisibilityExternal,
UserVisibilityPrivate,
}
// Preload related tables for the user (classmethod).
func (u *User) Preload() *gorm.DB {
return DB.Preload("ProfileField").Preload("ProfilePhoto")

View File

@ -51,19 +51,21 @@ document.addEventListener('DOMContentLoaded', () => {
});
// Touching the user drop-down button toggles it.
userMenu.addEventListener("touchstart", (e) => {
// On mobile/tablet screens they had to hamburger menu their way here anyway, let it thru.
if (screen.width < 1024) {
return;
}
if (userMenu !== null) {
userMenu.addEventListener("touchstart", (e) => {
// On mobile/tablet screens they had to hamburger menu their way here anyway, let it thru.
if (screen.width < 1024) {
return;
}
e.preventDefault();
if (userMenu.classList.contains(activeClass)) {
userMenu.classList.remove(activeClass);
} else {
userMenu.classList.add(activeClass);
}
});
e.preventDefault();
if (userMenu.classList.contains(activeClass)) {
userMenu.classList.remove(activeClass);
} else {
userMenu.classList.add(activeClass);
}
});
}
// Touching a link from the user menu lets it click thru.
(document.querySelectorAll(".navbar-dropdown") || []).forEach(node => {

View File

@ -6,6 +6,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Bind to the like buttons.
(document.querySelectorAll(".nonshy-like-button") || []).forEach(node => {
node.addEventListener("click", (e) => {
e.preventDefault();
if (busy) return;
let $icon = node.querySelector(".icon"),
@ -36,6 +37,7 @@ document.addEventListener('DOMContentLoaded', () => {
"name": tableName, // TODO
"id": parseInt(tableID),
"unlike": !liking,
"page": window.location.pathname + window.location.search + window.location.hash,
}),
})
.then((response) => response.json())

View File

@ -201,6 +201,12 @@
{{end}}
{{else if eq .TableName "users"}}
profile page.
{{else if eq .TableName "comments"}}
{{if .Link}}
<a href="{{.Link}}">comment</a>:
{{else}}
comment.
{{end}}
{{else}}
{{.TableName}}.
{{end}}

View File

@ -1,7 +1,7 @@
{{define "title"}}{{.User.Username}}{{end}}
{{define "content"}}
<div class="container">
<section class="hero {{if .LoggedIn}}is-info{{else}}is-light is-bold{{end}}">
<section class="hero {{if and .LoggedIn (not .IsPrivate)}}is-info{{else}}is-light is-bold{{end}}">
<div class="hero-body">
<div class="container">
<div class="columns">
@ -14,7 +14,7 @@
{{end}}
<!-- CurrentUser can upload a new profile pic -->
{{if and .LoggedIn (eq .CurrentUser.ID .User.ID)}}
{{if and .LoggedIn (eq .CurrentUser.ID .User.ID) (not .IsPrivate)}}
<span class="corner">
<button class="button is-small p-1 is-success">
<a href="/photo/upload?intent=profile_pic"
@ -36,7 +36,7 @@
({{.User.Status}})
</h2>
{{end}}
{{if not .LoggedIn}}
{{if or (not .LoggedIn) .IsPrivate}}
<h2 class="subtitle">is on {{PrettyTitle}}, a social network for nudists &amp; exhibitionists.</h2>
<p>
{{PrettyTitle}} is a new social network for <strong>real</strong> nudists and exhibionists.
@ -47,7 +47,7 @@
{{end}}
</div>
{{if .LoggedIn}}
{{if and .LoggedIn (not .IsPrivate)}}
<div class="column is-narrow">
<div class="box">
<div>
@ -97,7 +97,7 @@
{{end}}<!-- if .LoggedIn -->
</div>
{{if .LoggedIn}}
{{if and .LoggedIn (not .IsExternalView)}}
<div class="columns is-centered is-gapless">
<div class="column is-narrow has-text-centered">
<form action="/friends/add" method="POST">
@ -134,20 +134,22 @@
</div>
<!-- Like button -->
{{$Like := .LikeMap.Get .User.ID}}
<div class="column is-narrow has-text-centered">
<button type="button" class="button is-fullwidth nonshy-like-button"
data-table-name="users" data-table-id="{{.User.ID}}"
title="Like this profile">
<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>
{{if not .IsPrivate}}
{{$Like := .LikeMap.Get .User.ID}}
<div class="column is-narrow has-text-centered">
<button type="button" class="button is-fullwidth nonshy-like-button"
data-table-name="users" data-table-id="{{.User.ID}}"
title="Like this profile">
<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>
{{end}}
<div class="column is-narrow has-text-centered">
<form action="/users/block" method="POST">
@ -183,7 +185,7 @@
</div>
</section>
{{if not .LoggedIn}}
{{if or (not .LoggedIn) .IsPrivate}}
<div class="py-6"></div>
{{else if .IsPrivate}}
<div class="block p-4">

View File

@ -220,18 +220,46 @@
<div class="card-content">
<div class="field">
<label class="label">Private Profile</label>
<label class="label">Profile Visibility</label>
<label class="checkbox">
<input type="checkbox"
name="private"
value="true"
<input type="radio"
name="visibility"
value="public"
{{if eq .CurrentUser.Visibility "public"}}checked{{end}}>
Public + Login Required
</label>
<p class="help">
The default is that users must be logged-in to even look at your profile
page. If your profile URL (/u/{{.CurrentUser.Username}}) is visited by a
logged-out browser, they are prompted to log in.
</p>
<label class="checkbox mt-2">
<input type="radio"
name="visibility"
value="external"
{{if eq .CurrentUser.Visibility "external"}}checked{{end}}>
Public + Limited Logged-out View
</label>
<p class="help">
Your profile is fully visible to logged-in users, but if a logged-out browser
visits your page they will see a very minimal view: only your profile picture
and display name are shown.
<a href="/u/{{.CurrentUser.Username}}?view=external" target="_blank">Preview <i class="fa fa-external-link"></i></a>
</p>
<label class="checkbox mt-2">
<input type="radio"
name="visibility"
value="private"
{{if eq .CurrentUser.Visibility "private"}}checked{{end}}>
Mark my profile page as "private"
</label>
<p class="help">
If you check this box then only friends who you have approved are able to
see your profile page and gallery. Your gallery photos also will NOT appear
on the Site Gallery page.
on the Site Gallery page. If your profile page is visited by a logged-out
viewer, they are prompted to log in.
</p>
</div>

View File

@ -42,6 +42,37 @@
until your profile has been certified.
</p>
<h3>What are the visibility options for my profile page?</h3>
<p>
There are currently three different choices for your profile visibility on your
<a href="/settings">Settings</a> page:
</p>
<ul>
<li>
The <strong>default</strong> visibility is <strong class="has-text-success-dark">"Public + Login Required."</strong> Users must be
logged-in to an account in order to see anything about your profile page - if an
external (logged out) browser visits your profile URL, they will be redirected to
log in to an account first.
</li>
<li>
You may optionally go <em>more</em> public with a <strong class="has-text-warning-dark">"Limited Logged-out View."</strong>
This enables your profile URL (e.g.,
{{if .LoggedIn}}<a href="/u/{{.CurrentUser.Username}}?view=external">/u/{{.CurrentUser.Username}}</a>{{else}}/u/username{{end}})
to show a <em>basic</em> page (with your square profile picture and display name) to
logged-out browsers. This may be useful if you wish to link to your page from an external
site (e.g. your Twitter page) and present new users with a better experience than just
a redirect to login page.
</li>
<li>
You may <strong class="has-text-private">"Mark my profile as 'private'"</strong> to
be private even from other logged-in members who are not on your Friends
list. Logged-in users will see only your square profile picture and display
name, and be able only to send you a friend request or a message.
</li>
</ul>
<h1>Photo FAQs</h1>
<h3>Do I have to post my nudes here?</h3>

View File

@ -157,6 +157,21 @@
</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>

View File

@ -21,15 +21,9 @@
<p>
This website was designed by a life-long nudist, exhibitionist and software engineer to create
a safe space for like-minded individuals online, especially in the modern online political
climate and after Tumblr, Pornhub and other social networks had begun clamping down and kicking
off all the nudists from their platforms.
</p>
<p>
This website is open to <em>all</em> nudists and exhibitionists, but I understand that not all
nudists want to see any sexual content, so this site provides some controls to support
both camps:
a safe space for like-minded individuals online. This website is open to <em>all</em> nudists
and exhibitionists (18+), but I understand that not all nudists want to see any sexual content, so
this site provides some controls to support both camps:
</p>
<ul>
@ -44,6 +38,11 @@
</li>
</ul>
<p>
You can read more <a href="/about">about this website</a> and check out the <a href="/faq">FAQ</a>
page for more information.
</p>
<h1>Site Rules</h1>
<ul>

View File

@ -241,6 +241,21 @@
</span>
</div>
<div class="column is-narrow">
{{$Like := $Root.CommentLikeMap.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>

View File

@ -25,7 +25,7 @@
</p>
<p>
This page was last updated on <strong>August 26, 2022.</strong>
This page was last updated on <strong>August 29, 2022.</strong>
</p>
<p>
@ -43,6 +43,10 @@
</p>
<ul>
<li>
By default, your profile page on {{PrettyTitle}} may <strong>only</strong> be seen
by logged-in members of the website.
</li>
<li>
You may mark your entire profile as "Private" which limits some of the contact you
may receive:
@ -58,6 +62,12 @@
</li>
</ul>
</li>
<li>
Optionally, you may mark your Public profile to allow a limited "logged out" view which
shows only your square profile picture and display name. This may be useful to link to
your profile from external sites (like Twitter) so the visitor isn't just redirected to a
"login required" page.
</li>
<li>
Profile photos have visibility settings including Public, Friends-only or Private:
<ul>
@ -77,6 +87,12 @@
</li>
</ul>
</li>
<li>
<strong>Notice:</strong> the square default profile picture that appears on your page
will always be visible to all logged-in users. The full size version on your Gallery
page may be restricted to friends or private, but the square cropped version that appears
next to your username on many parts of the website is always seen by logged-in users.
</li>
</ul>
<h3>Site-Wide Photo Gallery</h3>