DM Privacy + Settings Page Tabs

* Refactor the Settings page into a tabbed UI to reduce confusion with
  all the different forms and save buttons
* Add a DM Privacy setting to your page
* Update the About page
This commit is contained in:
Noah Petherbridge 2023-06-23 22:18:09 -07:00
parent 0f6b627156
commit 45cb4d260e
8 changed files with 701 additions and 352 deletions

View File

@ -64,6 +64,9 @@ var (
"interests", "interests",
"music_movies", "music_movies",
"hide_age", "hide_age",
// Site prefs
"dm_privacy",
} }
// Choices for the Contact Us subject // Choices for the Contact Us subject

View File

@ -46,12 +46,16 @@ func Settings() http.HandlerFunc {
return return
} }
// URL hashtag to redirect to
var hashtag string
// Are we POSTing? // Are we POSTing?
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
intent := r.PostFormValue("intent") intent := r.PostFormValue("intent")
switch intent { switch intent {
case "profile": case "profile":
// Setting profile values. // Setting profile values.
hashtag = "#profile"
var ( var (
displayName = r.PostFormValue("display_name") displayName = r.PostFormValue("display_name")
dob = r.PostFormValue("dob") dob = r.PostFormValue("dob")
@ -95,6 +99,7 @@ func Settings() http.HandlerFunc {
session.Flash(w, r, "Profile settings updated!") session.Flash(w, r, "Profile settings updated!")
case "preferences": case "preferences":
hashtag = "#prefs"
var ( var (
explicit = r.PostFormValue("explicit") == "true" explicit = r.PostFormValue("explicit") == "true"
visibility = models.UserVisibility(r.PostFormValue("visibility")) visibility = models.UserVisibility(r.PostFormValue("visibility"))
@ -109,12 +114,16 @@ func Settings() http.HandlerFunc {
} }
} }
// Set profile field prefs.
user.SetProfileField("dm_privacy", r.PostFormValue("dm_privacy"))
if err := user.Save(); err != nil { if err := user.Save(); err != nil {
session.FlashError(w, r, "Failed to save user to database: %s", err) session.FlashError(w, r, "Failed to save user to database: %s", err)
} }
session.Flash(w, r, "Website preferences updated!") session.Flash(w, r, "Website preferences updated!")
case "settings": case "settings":
hashtag = "#account"
var ( var (
oldPassword = r.PostFormValue("old_password") oldPassword = r.PostFormValue("old_password")
changeEmail = strings.TrimSpace(strings.ToLower(r.PostFormValue("change_email"))) changeEmail = strings.TrimSpace(strings.ToLower(r.PostFormValue("change_email")))
@ -197,7 +206,7 @@ func Settings() http.HandlerFunc {
session.FlashError(w, r, "Unknown POST intent value. Please try again.") session.FlashError(w, r, "Unknown POST intent value. Please try again.")
} }
templates.Redirect(w, r.URL.Path) templates.Redirect(w, r.URL.Path+hashtag+".")
return return
} }

View File

@ -74,7 +74,8 @@ func Compose() http.HandlerFunc {
var ( var (
imShy = currentUser.IsShy() imShy = currentUser.IsShy()
theyreShy = user.IsShy() theyreShy = user.IsShy()
isShyFrom = currentUser.IsShyFrom(user) || (imShy && !models.AreFriends(currentUser.ID, user.ID)) areFriends = models.AreFriends(currentUser.ID, user.ID)
isShyFrom = currentUser.IsShyFrom(user) || (imShy && !areFriends)
) )
if imShy && isShyFrom && !theyreShy && !user.IsAdmin { if imShy && isShyFrom && !theyreShy && !user.IsAdmin {
session.FlashError(w, r, "You have a Shy Account and can not initiate Direct Messages with a non-shy member.") session.FlashError(w, r, "You have a Shy Account and can not initiate Direct Messages with a non-shy member.")
@ -82,6 +83,22 @@ func Compose() http.HandlerFunc {
return return
} }
// Does the recipient have a privacy control on their DMs?
switch user.GetProfileField("dm_privacy") {
case "friends":
if !areFriends && !currentUser.IsAdmin {
session.FlashError(w, r, "This user only wants to receive new DMs from their friends.")
templates.Redirect(w, "/u/"+user.Username)
return
}
case "nobody":
if !currentUser.IsAdmin {
session.FlashError(w, r, "This user's DMs are closed and they do not want any new conversations.")
templates.Redirect(w, "/u/"+user.Username)
return
}
}
var vars = map[string]interface{}{ var vars = map[string]interface{}{
"User": user, "User": user,
} }

View File

@ -389,6 +389,7 @@ func (u *User) SetProfileField(name, value string) {
return return
} }
log.Debug("User(%s): append ProfileField %s", u.Username, name)
u.ProfileField = append(u.ProfileField, ProfileField{ u.ProfileField = append(u.ProfileField, ProfileField{
Name: name, Name: name,
Value: value, Value: value,

View File

@ -80,6 +80,155 @@
</li> </li>
</ul> </ul>
<h2>What features does this site have?</h2>
<p>
We have so far:
</p>
<ul>
<li>
A <a href="#webcam-chat-room"><strong>Webcam Chat Room</strong></a> where you can chat, share images,
and go on camera with other website members. 'Explicit' cameras are allowed, just mark
your camera feed as such so other members might know what they're getting into!
Read more about the <a href="#webcam-chat-room">chat room</a>, below.
</li>
<li>
<strong>Forums</strong> where you can meet and converse with members of the
site around a variety of themes and topics.
</li>
<li>
A <strong>Site Photo Gallery</strong> where members may opt-in their profile gallery
photos to be seen by the whole site (or at least to their friends when they look at
the Site Gallery).
</li>
<li>
<strong>Friend Requests</strong> so you can make some new connections through this site.
You can also tag some of your photos as "Friends only" and make sure only your approved
friends can see those!
</li>
<li>
<strong>Profile pages &amp; Photo Galleries</strong>: members may upload up to 100 photos
on their page and fill out a profile with the usual details.
</li>
<li>
A <strong>Member Directory</strong> to browse and search for people on the site.
</li>
</ul>
<p>
We also have some <strong>privacy controls</strong> that members on any stage of their
nudist journey may enjoy to have some control over what they share and with whom:
</p>
<ul>
<li>
By default, <strong>only logged-in members</strong> can see your profile page or
photos at all. (You may opt to have a "slightly public" profile page that you could
link to from your Twitter, which reveals little information beyond your username
and profile pic).
</li>
<li>
When you upload a photo you may mark it as for <strong>"Friends only"</strong> or <strong>"Private."</strong> This way
you can limit the audience of certain pictures to only your approved friendships, or
in the case of Private photos, to specific members to whom you grant access. You may
also revoke your private photos from one or all members at once.
</li>
<li>
If you are concerned about unsolicited messages by randoms, you may limit <strong>who is allowed
to slide into your DMs</strong> to "Friends only" or even to "Nobody." People may always reply to
messages that you send them first, but if they haven't contacted you before, you can prevent
them from doing so. "Nobody" is like having your DMs closed but you can still continue conversations
you had open already.
</li>
<li>
You may mark your whole profile page as "private" and then only your Friends may see its
contents. But, this will make you a "shy account" and see the next point.
</li>
<li>
Members who are <em>very</em> shy (they're Certified so the admin has seen their face,
but they keep all their photos or profile on Private so they look to others like a 'blank
profile') are considered by the site to be <a href="/faq#shy-faqs">"Shy Accounts"</a> and
they can <em>not</em> slide into your DMs if you are sharing photos on public but they are
not. They also won't see <em>any</em> of your photos (unless you are friends). See the
<a href="/faq#shy-faqs">FAQs</a> for more details.
</li>
</ul>
<h4 id="webcam-chat-room">A Webcam Chat Room</h4>
<p>
The chat room is a completely custom-built app for {{PrettyTitle}} (and it's
<a href="#open-source">open source</a> too!)
and here are some of its features in detail:
</p>
<ul>
<li>
It's a classic chat room featuring multiple public channels, Private Messages/DMs,
a list of online chatters and links to see their profile or open their webcam
if they're sharing.
</li>
<li>
You may broadcast your webcam and microphone, and other users in the
room may tune in and watch your camera.
</li>
<li>
You are permitted to get "sexual" on camera if you want. Just mark your camera
as <span class="has-text-danger">'Explicit'</span> so other chatters may know what
they're getting into before they click in.
</li>
<li>
You could have "infinite" cameras open with other chatters: as long as your
screen size and network can support it (there is no enforced limit, as it makes
no difference to my chat server how many cams you open -- the connections are
usually peer-to-peer!)
</li>
<li>
You can see who all is watching your camera, and you can "boot" somebody
off if you don't want them to watch. When booted, they will not be able to
watch again, and your camera will appear 'offline' to them so they can't
be sure you didn't simply turn your camera off.
</li>
<li>
You can 'mute' people if you no longer want to see their messages in chat
or receive any further DMs from them (and this also prevents them from
seeing your camera - like a 'boot' they won't see if your camera is even
broadcasting!)
</li>
<li>
You can upload photos (and GIFs up to 8 MB!) to share with the chat room
or over Direct Messages.
</li>
<li>
All chatters are guaranteed to be <a href="/faq#certification-faqs">"Certified"</a>
real human beings and have at least one 'public' photo on their page that you
could see. Certification is <strong>required</strong> for <em>all</em> site
features (see <a href="/faq#uncertified">what non-certified members can even do</a>)
but additionally the "certified, <a href="/faq#shy-faqs">but shy</a>" members are
not allowed in the chat room <strong>at all.</strong>
</li>
</ul>
<p>
A unique feature of the chat room are the <strong>mutual webcam options</strong>.
Some nudists feel weirded out if they are on camera and they are being watched
by some silent lurker who isn't sharing their own camera in return. On the
{{PrettyTitle}} chat room, you can opt-in to "mutual webcam" options if you like:
</p>
<ul>
<li>
You can require your webcam viewers to first be sharing <em>their own</em>
webcam in return, before they can open yours.
</li>
<li>
You may also automatically open the viewer's webcam (if they are broadcasting)
when they click in to see your camera. This can save you a click of needing to
open the camera of the person who just opened yours.
</li>
</ul>
<h2>Who is this site for?</h2> <h2>Who is this site for?</h2>
<p> <p>
@ -128,12 +277,14 @@
</div> </div>
</div> </div>
<div class="content block pt-1"> <div class="content block pt-1" id="open-source">
<h3><i class="fa fa-code"></i> This website is open source!</h3> <h3><i class="fa fa-code"></i> This website is open source!</h3>
<p> <p>
If you would like to see the source code or contribute bug fixes or new features, the If you would like to see the source code or contribute bug fixes or new features, the
source code behind this website is available at source code behind this website is available at
<a href="https://code.nonshy.com/nonshy/website" target="_blank">code.nonshy.com</a>. <a href="https://code.nonshy.com/nonshy/website" target="_blank">code.nonshy.com</a>.
The chat room is an independent open source app that you may use with <em>your</em>
website too and it's called <a href="https://git.kirsle.net/apps/BareRTC">BareRTC</a>.
</p> </p>
</div> </div>

View File

@ -155,6 +155,12 @@
</form> </form>
</div> </div>
<!-- DM button -->
{{if and (eq (.User.GetProfileField "dm_privacy") "friends") (not (eq .IsFriend "approved")) (not .CurrentUser.IsAdmin)}}
<!-- Only friends can send them a DM -->
{{else if and (eq (.User.GetProfileField "dm_privacy") "nobody") (not .CurrentUser.IsAdmin)}}
<!-- They set "Nobody" can send them a DM -->
{{else}}
<div class="column is-narrow has-text-centered"> <div class="column is-narrow has-text-centered">
<a href="/messages/compose?to={{.User.Username}}" class="button is-fullwidth"> <a href="/messages/compose?to={{.User.Username}}" class="button is-fullwidth">
<span class="icon-text"> <span class="icon-text">
@ -165,6 +171,7 @@
</span> </span>
</a> </a>
</div> </div>
{{end}}
<!-- Like button --> <!-- Like button -->
{{if not .IsPrivate}} {{if not .IsPrivate}}

View File

@ -12,19 +12,27 @@
{{ $User := .CurrentUser }} {{ $User := .CurrentUser }}
<div class="block p-4"> <div class="block p-4">
<div class="columns"> <div class="tabs is-boxed">
<ul>
<li class="is-active">
<a href="/settings#profile" class="nonshy-tab-button">
Profile
</a>
</li>
<li>
<a href="/settings#prefs" class="nonshy-tab-button">
Preferences
</a>
</li>
<div class="column is-hidden-tablet p-4"> <li>
<label class="label">Jump to section:</label> <a href="/settings#account" class="nonshy-tab-button">
<ul class="menu-list"> Account
<li><a href="#profile">My Profile</a></li> </a>
<li><a href="#verification">Verification Photo</a></li> </li>
<li><a href="#prefs">Website Preferences</a></li>
<li><a href="#account">Account Settings <small class="has-text-grey ml-2">Email &amp; password</small></a></li>
</ul> </ul>
</div> </div>
<div class="column">
<div class="card" id="profile"> <div class="card" id="profile">
<header class="card-header has-background-link"> <header class="card-header has-background-link">
<p class="card-header-title has-text-light"> <p class="card-header-title has-text-light">
@ -215,9 +223,6 @@
</div> </div>
</form> </form>
</div> </div>
</div>
<div class="column">
<!-- Website Preferences --> <!-- Website Preferences -->
<form method="POST" action="/settings"> <form method="POST" action="/settings">
@ -241,6 +246,7 @@
value="public" value="public"
{{if eq .CurrentUser.Visibility "public"}}checked{{end}}> {{if eq .CurrentUser.Visibility "public"}}checked{{end}}>
Public + Login Required Public + Login Required
<i class="fa fa-eye ml-2 has-text-info"></i>
</label> </label>
<p class="help"> <p class="help">
The default is that users must be logged-in to even look at your profile The default is that users must be logged-in to even look at your profile
@ -254,6 +260,7 @@
value="external" value="external"
{{if eq .CurrentUser.Visibility "external"}}checked{{end}}> {{if eq .CurrentUser.Visibility "external"}}checked{{end}}>
Public + Limited Logged-out View Public + Limited Logged-out View
<i class="fa fa-eye ml-2 has-text-danger"></i>
</label> </label>
<p class="help"> <p class="help">
Your profile is fully visible to logged-in users, but if a logged-out browser Your profile is fully visible to logged-in users, but if a logged-out browser
@ -268,6 +275,7 @@
value="private" value="private"
{{if eq .CurrentUser.Visibility "private"}}checked{{end}}> {{if eq .CurrentUser.Visibility "private"}}checked{{end}}>
Mark my profile page as "private" Mark my profile page as "private"
<i class="fa fa-lock ml-2 has-text-private"></i>
</label> </label>
<p class="help"> <p class="help">
If you check this box then only friends who you have approved are able to If you check this box then only friends who you have approved are able to
@ -277,6 +285,8 @@
</p> </p>
</div> </div>
<hr>
<div class="field"> <div class="field">
<label class="label">Explicit Content Filter</label> <label class="label">Explicit Content Filter</label>
<label class="checkbox"> <label class="checkbox">
@ -284,7 +294,7 @@
name="explicit" name="explicit"
value="true" value="true"
{{if .CurrentUser.Explicit}}checked{{end}}> {{if .CurrentUser.Explicit}}checked{{end}}>
Show explicit content Show explicit content <i class="fa fa-fire has-text-danger ml-1"></i>
</label> </label>
<p class="help"> <p class="help">
Check this box if you are OK seeing explicit content on this site, which may Check this box if you are OK seeing explicit content on this site, which may
@ -293,6 +303,90 @@
</p> </p>
</div> </div>
<hr>
<div class="field">
<label class="label mb-0">Who can send me the first <i class="fa fa-envelope"></i> Message?</label>
<div class="has-text-warning ml-4">
<small><em>
Note: this refers to Direct Messages on the main website
(not inside the chat room).
</em></small>
{{.CurrentUser.GetProfileField "dm_privacy"}}
</div>
<label class="checkbox">
<input type="radio"
name="dm_privacy"
value=""
{{if eq (.CurrentUser.GetProfileField "dm_privacy") ""}}checked{{end}}>
Anybody on the site
</label>
<p class="help">
Almost any member of the site may send you a Direct Message from your profile
page (except for maybe <a href="/faq#shy-faqs" target="_blank">Shy Accounts</a>).
</p>
<label class="checkbox">
<input type="radio"
name="dm_privacy"
value="friends"
{{if eq (.CurrentUser.GetProfileField "dm_privacy") "friends"}}checked{{end}}>
Only people on my Friends list
</label>
<p class="help">
Nobody can slide into your DMs except for friends (and admins if needed). Anybody
may <em>reply</em> to messages that you send to them.
</p>
<label class="checkbox">
<input type="radio"
name="dm_privacy"
value="nobody"
{{if eq (.CurrentUser.GetProfileField "dm_privacy") "nobody"}}checked{{end}}>
Nobody (close my DMs)
</label>
<p class="help">
Nobody can start a Direct Message conversation with you on the main website
(except an admin if necessary). Anybody may <em>reply</em> to messages that you
sent to them first.
</p>
</div>
<!-- TODO: manually opt-in dark mode is hairy, look at
those media queries in bulma-prefers-dark.js!
<div class="field">
<label class="label">Website Theme</label>
<label class="checkbox">
<input type="radio"
name="theme"
value=""
{{if eq ($User.GetProfileField "theme") "" }}checked{{end}}>
Automatic
</label>
<p class="help">
Automatically chooses a theme based on your device settings.
</p>
<label class="checkbox">
<input type="radio"
name="theme"
value="light"
{{if eq ($User.GetProfileField "theme") "light" }}checked{{end}}>
Light
</label>
<label class="checkbox">
<input type="radio"
name="theme"
value="dark"
{{if eq ($User.GetProfileField "theme") "dark" }}checked{{end}}>
Dark
</label>
</div>
-->
<div class="field"> <div class="field">
<button type="submit" class="button is-primary"> <button type="submit" class="button is-primary">
Save Website Preferences Save Website Preferences
@ -303,11 +397,12 @@
</form> </form>
<!-- Account Settings --> <!-- Account Settings -->
<div id="account">
<form method="POST" action="/settings"> <form method="POST" action="/settings">
<input type="hidden" name="intent" value="settings"> <input type="hidden" name="intent" value="settings">
{{InputCSRF}} {{InputCSRF}}
<div class="card mb-5" id="account"> <div class="card mb-5">
<header class="card-header has-background-warning"> <header class="card-header has-background-warning">
<p class="card-header-title has-text-dark-dark"> <p class="card-header-title has-text-dark-dark">
<i class="fa fa-gear pr-2"></i> <i class="fa fa-gear pr-2"></i>
@ -359,7 +454,7 @@
</form> </form>
<!-- Delete Account --> <!-- Delete Account -->
<div class="card mb-5" id="account"> <div class="card mb-5">
<header class="card-header has-background-danger"> <header class="card-header has-background-danger">
<p class="card-header-title has-text-light"> <p class="card-header-title has-text-light">
<i class="fa fa-gear pr-2"></i> <i class="fa fa-gear pr-2"></i>
@ -381,7 +476,73 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>
</div> </div>
{{end}} {{end}}
{{define "scripts"}}
<script>
window.addEventListener("DOMContentLoaded", (event) => {
// The tabs
const $profile = document.querySelector("#profile"),
$prefs = document.querySelector("#prefs"),
$account = document.querySelector("#account")
buttons = Array.from(document.getElementsByClassName("nonshy-tab-button"));
// Hide all by default.
$profile.style.display = 'none';
$prefs.style.display = 'none';
$account.style.display = 'none';
// Current tab to select by default.
let $activeTab = $profile;
// Global function to toggle the active tab.
const showTab = (name) => {
name = name.replace(/\.$/, '');
if (!name) name = "profile";
$activeTab.style.display = 'none';
switch (name) {
case "prefs":
$activeTab = $prefs;
break
case "account":
$activeTab = $account;
break
default:
$activeTab = $profile;
}
// Update the is-active classes on all the tabs.
buttons.forEach(tab_ => {
let name_ = tab_.href.split("#").pop(),
parent = tab_.parentElement;
if (name !== name_) {
console.log("button: remove is-active", tab_);
parent.classList.remove("is-active");
} else {
console.log("button %s: ADD is-active", tab_);
parent.classList.add("is-active");
}
});
$activeTab.style.display = 'block';
history.replaceState(undefined, undefined, '#'+name);
};
// Wire the tab buttons up.
buttons.forEach(el => {
let name = el.href.split("#").pop();
el.addEventListener("click", (e) => {
showTab(name);
e.preventDefault();
});
})
showTab(window.location.hash.replace(/^#/, ''));
});
</script>
{{end}}