Public Avatar Consent Page

The nonshy website is changing the policy on profile pictures. From August 30,
the square cropped avatar images will need to be publicly viewable to everyone.

This implements the first pass of the rollout:

* Add the Public Avatar Consent Page which explains the change to users and
  asks for their acknowledgement. The link is available from their User Settings
  page, near their Certification Photo link.
* When users (with non-public avatars) accept the change: their square cropped
  avatar will become visible to everybody, instead of showing a placeholder
  avatar.
* Users can change their mind and opt back out, which will again show the
  placeholder avatar.
* The Certification Required middleware will automatically enforce the consent
  page once the scheduled go-live date arrives.

Next steps are:

1. Post an announcement on the forum about the upcoming change and link users
   to the consent form if they want to check it out early.
2. Update the nonshy site to add banners to places like the User Dashboard for
   users who will be affected by the change, to link them to the forum post
   and the consent page.
This commit is contained in:
Noah Petherbridge 2024-06-29 16:44:18 -07:00
parent a82e04b2f8
commit 4f04323d5a
14 changed files with 493 additions and 29 deletions

View File

@ -118,6 +118,15 @@ const (
SiteGalleryRateLimitInterval = 24 * time.Hour SiteGalleryRateLimitInterval = 24 * time.Hour
) )
// Profile photo "public square cropped version" migration controls.
// Previously: a Private or Friends-only profile picture would show a placeholder avatar to users
// who couldn't see the full size version. In migration, these square cropped images will move to
// be always public (the full-size versions can remain private).
// NOTE: search code base for PublicAvatar or "public avatar" for related feature code.
var (
PublicAvatarEnforcementDate = time.Date(2024, time.August, 30, 12, 0, 0, 0, time.UTC)
)
// Forum settings // Forum settings
const ( const (
// Only ++ the Views count per user per thread within a small // Only ++ the Views count per user per thread within a small

View File

@ -78,7 +78,8 @@ var (
SitePreferenceFields = []string{ SitePreferenceFields = []string{
"dm_privacy", "dm_privacy",
"blur_explicit", "blur_explicit",
"site_gallery_default", // default view on site gallery (friends-only or all certified?) "site_gallery_default", // default view on site gallery (friends-only or all certified?)
"public_avatar_consent", // Public Avatars consent agreement
} }
// Choices for the Contact Us subject // Choices for the Contact Us subject

View File

@ -0,0 +1,55 @@
package account
import (
"net/http"
"strings"
"code.nonshy.com/nonshy/website/pkg/session"
"code.nonshy.com/nonshy/website/pkg/templates"
)
// Public avatar consent page (/settings/public-avatar-consent)
func PublicAvatarConsent() http.HandlerFunc {
tmpl := templates.Must("account/public_avatar_consent.html")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var next = r.FormValue("next")
if !strings.HasPrefix(next, "/") {
next = "/me"
}
vars := map[string]interface{}{
"NextURL": next,
}
// Load the current user in case of updates.
currentUser, err := session.CurrentUser(r)
if err != nil {
session.FlashError(w, r, "Couldn't get CurrentUser: %s", err)
templates.Redirect(w, r.URL.Path)
return
}
// Are we POSTing?
if r.Method == http.MethodPost {
var (
accept = r.PostFormValue("accept")
)
currentUser.SetProfileField("public_avatar_consent", accept)
if accept == "true" {
session.Flash(w, r, "Thank you for agreeing to the Public Avatar policy! You may revisit this option from your Account Settings page in the future.")
templates.Redirect(w, next)
return
}
session.Flash(w, r, "We have noted your non-consent to this change. Your avatar image (if non-public) will show as a placeholder avatar to people who don't have permission to see the full-size photo.")
templates.Redirect(w, r.URL.Path)
return
}
if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
}

View File

@ -99,13 +99,15 @@ func Landing() http.HandlerFunc {
// Avatar URL - masked if non-public. // Avatar URL - masked if non-public.
avatar := photo.URLPath(currentUser.ProfilePhoto.CroppedFilename) avatar := photo.URLPath(currentUser.ProfilePhoto.CroppedFilename)
switch currentUser.ProfilePhoto.Visibility { if currentUser.GetProfileField("public_avatar_consent") != "true" {
case models.PhotoPrivate: switch currentUser.ProfilePhoto.Visibility {
avatar = "/static/img/shy-private.png" case models.PhotoPrivate:
case models.PhotoFriends: avatar = "/static/img/shy-private.png"
avatar = "/static/img/shy-friends.png" case models.PhotoFriends:
case models.PhotoInnerCircle: avatar = "/static/img/shy-friends.png"
avatar = "/static/img/shy-secret.png" case models.PhotoInnerCircle:
avatar = "/static/img/shy-secret.png"
}
} }
// Country flag emoji. // Country flag emoji.

View File

@ -157,6 +157,11 @@ func CertRequired(handler http.Handler) http.Handler {
return return
} }
// Public Avatar consent enforcement?
if PublicAvatarConsent(currentUser, w, r) {
return
}
handler.ServeHTTP(w, r) handler.ServeHTTP(w, r)
}) })
} }

View File

@ -0,0 +1,31 @@
package middleware
import (
"net/http"
"net/url"
"time"
"code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/models"
"code.nonshy.com/nonshy/website/pkg/templates"
)
// PublicAvatarConsent: part of CertificationRequired that enforces the consent page for affected users.
func PublicAvatarConsent(user *models.User, w http.ResponseWriter, r *http.Request) (handled bool) {
// Is the enforcement date live?
if time.Now().Before(config.PublicAvatarEnforcementDate) {
return
}
// If the current user has a non-public avatar and has not consented.
if user.ProfilePhoto.ID > 0 && user.ProfilePhoto.Visibility != models.PhotoPublic {
if user.GetProfileField("public_avatar_consent") == "true" {
return
}
templates.Redirect(w, "/settings/public-avatar-consent?next="+url.QueryEscape(r.URL.String()))
return true
}
return
}

View File

@ -1,6 +1,6 @@
package models package models
// Supplementary functions to do with Shy Accounts. // Supplementary functions to do with Shy Accounts and PublicAvatar consent.
import "code.nonshy.com/nonshy/website/pkg/log" import "code.nonshy.com/nonshy/website/pkg/log"
@ -96,3 +96,38 @@ func MapShyAccounts(users []*User) ShyMap {
func (um ShyMap) Get(id uint64) bool { func (um ShyMap) Get(id uint64) bool {
return um[id] return um[id]
} }
// MapPublicAvatarConsent checks a set of users if they have answered the public avatar consent form.
func MapPublicAvatarConsent(users []*User) (map[uint64]bool, error) {
var (
userIDs = []uint64{}
result = map[uint64]bool{}
)
for _, user := range users {
userIDs = append(userIDs, user.ID)
result[user.ID] = false
}
type record struct {
UserID uint64
Value string
}
var rows = []record{}
if res := DB.Table(
"profile_fields",
).Select(
"user_id, value",
).Where(
"user_id IN ? AND name='public_avatar_consent'",
userIDs,
).Scan(&rows); res.Error != nil {
return nil, res.Error
}
for _, row := range rows {
result[row.UserID] = row.Value == "true"
}
return result, nil
}

View File

@ -637,8 +637,12 @@ func (u *User) NameOrUsername() string {
// //
// Expects that UserRelationships are available on the user. // Expects that UserRelationships are available on the user.
func (u *User) VisibleAvatarURL(currentUser *User) string { func (u *User) VisibleAvatarURL(currentUser *User) string {
// Can the viewer see the picture based on its visibility setting?
canSee, visibility := u.CanSeeProfilePicture(currentUser) canSee, visibility := u.CanSeeProfilePicture(currentUser)
if canSee {
// Or has the owner consented on the Public Avatar policy?
consent := u.GetProfileField("public_avatar_consent") == "true"
if canSee || consent {
return config.PhotoWebPath + "/" + u.ProfilePhoto.CroppedFilename return config.PhotoWebPath + "/" + u.ProfilePhoto.CroppedFilename
} }

View File

@ -1,14 +1,17 @@
package models package models
import "code.nonshy.com/nonshy/website/pkg/log"
// UserRelationship fields - how a target User relates to the CurrentUser, especially // UserRelationship fields - how a target User relates to the CurrentUser, especially
// with regards to whether the User's friends-only or private profile picture should show. // with regards to whether the User's friends-only or private profile picture should show.
// The zero-values should fail safely: in case the UserRelationship isn't populated correctly, // The zero-values should fail safely: in case the UserRelationship isn't populated correctly,
// private profile pics show as private by default. // private profile pics show as private by default.
type UserRelationship struct { type UserRelationship struct {
Computed bool // check whether the SetUserRelationships function has been run Computed bool // check whether the SetUserRelationships function has been run
IsFriend bool // if true, a friends-only profile pic can show IsFriend bool // if true, a friends-only profile pic can show
IsPrivateGranted bool // if true, a private profile pic can show IsPrivateGranted bool // if true, a private profile pic can show
IsInnerCirclePeer bool // if true, the current user is in the inner circle so may see circle-only profile picture IsInnerCirclePeer bool // if true, the current user is in the inner circle so may see circle-only profile picture
PublicAvatarConsent bool // if true, this user answered the Public Avatar consent form
} }
// SetUserRelationships updates a set of User objects to populate their UserRelationships in // SetUserRelationships updates a set of User objects to populate their UserRelationships in
@ -35,6 +38,12 @@ func SetUserRelationships(currentUser *User, users []*User) error {
privateMap[id] = nil privateMap[id] = nil
} }
// Get user PublicAvatar consent maps.
consent, err := MapPublicAvatarConsent(users)
if err != nil {
log.Error("MapPublicAvatarConsent: %s", err)
}
// Inject the UserRelationships. // Inject the UserRelationships.
for _, u := range users { for _, u := range users {
u.UserRelationship.IsInnerCirclePeer = isInnerCircle u.UserRelationship.IsInnerCirclePeer = isInnerCircle
@ -53,6 +62,10 @@ func SetUserRelationships(currentUser *User, users []*User) error {
if _, ok := privateMap[u.ID]; ok { if _, ok := privateMap[u.ID]; ok {
u.UserRelationship.IsPrivateGranted = true u.UserRelationship.IsPrivateGranted = true
} }
if v, ok := consent[u.ID]; ok {
u.UserRelationship.PublicAvatarConsent = v
}
} }
return nil return nil
} }

View File

@ -48,6 +48,7 @@ func New() http.Handler {
mux.Handle("/me", middleware.LoginRequired(account.Dashboard())) mux.Handle("/me", middleware.LoginRequired(account.Dashboard()))
mux.Handle("/settings", middleware.LoginRequired(account.Settings())) mux.Handle("/settings", middleware.LoginRequired(account.Settings()))
mux.Handle("/settings/age-gate", middleware.LoginRequired(account.AgeGate())) mux.Handle("/settings/age-gate", middleware.LoginRequired(account.AgeGate()))
mux.Handle("/settings/public-avatar-consent", middleware.LoginRequired(account.PublicAvatarConsent()))
mux.Handle("/account/two-factor/setup", middleware.LoginRequired(account.Setup2FA())) mux.Handle("/account/two-factor/setup", middleware.LoginRequired(account.Setup2FA()))
mux.Handle("/account/delete", middleware.LoginRequired(account.Delete())) mux.Handle("/account/delete", middleware.LoginRequired(account.Delete()))
mux.Handle("/account/deactivate", middleware.LoginRequired(account.Deactivate())) mux.Handle("/account/deactivate", middleware.LoginRequired(account.Deactivate()))

View File

@ -23,6 +23,9 @@ func MergeVars(r *http.Request, m map[string]interface{}) {
// Integrations // Integrations
m["TurnstileCAPTCHA"] = config.Current.Turnstile m["TurnstileCAPTCHA"] = config.Current.Turnstile
// Temporary? variables for migration efforts on PublicAvatar consent.
m["PublicAvatarEnforcementDate"] = config.PublicAvatarEnforcementDate
if r == nil { if r == nil {
return return
} }

View File

@ -0,0 +1,296 @@
{{define "title"}}An update about your Default Profile Picture{{end}}
{{define "content"}}
<div class="container">
<section class="hero is-info is-bold">
<div class="hero-body">
<div class="container">
<h1 class="title">
An update about your Default Profile Picture
</h1>
<h2 class="subtitle">
Please review the upcoming website change about our Default Profile Picture policy
</h2>
</div>
</div>
</section>
<div class="block p-4">
<div class="level">
<div class="level-item">
<div class="card" style="width: 100%; max-width: 800px">
<header class="card-header has-background-link">
<p class="card-header-title has-text-light">
<i class="fa fa-image mr-2"></i>
We are updating our Default Profile Picture policy
</p>
</header>
<div class="card-content">
<div class="content">
<p>
This page serves as a notification about an upcoming change to the {{PrettyTitle}} site
policy around the visibility of your <strong>square cropped Default Profile Picture</strong>
(which we will refer to as your <strong>"Avatar"</strong> for the purpose of this page).
</p>
<p>
Beginning on <strong>{{.PublicAvatarEnforcementDate.Format "January 2, 2006"}}</strong>,
all {{PrettyTitle}} members will be required to make their Avatar (the square cropped,
default profile picture which includes your face) visible to all members of the website.
After {{.PublicAvatarEnforcementDate.Format "January 2"}}, you will not be permitted to
use the chat room, forums, site gallery or other 'certification required' pages until you
have responded to this consent form by answering the prompt at the bottom of this page.
</p>
<ul>
<li>
<a href="#summary">Summary of the change</a>
</li>
<li>
<a href="#what">What is being changed about our Avatars?</a>
</li>
<li>
<a href="#fullsize">Your full-size profile picture can still remain private</a>
</li>
<li>
<a href="#consent">Your consent is requested for this change</a>
</li>
<li>
<a href="#disagree">What if I disagree with this change?</a>
</li>
</ul>
<hr>
<h3 id="summary">Summary of the change</h3>
<ul>
<li>
On the {{PrettyTitle}} website, all <strong>square cropped default profile pictures</strong>
will become visible to everybody on the site, instead of showing as a yellow or purple
placeholder image when your full-size profile picture was set to "Friends-only" or "Private"
visibility.
</li>
<li>
The <strong>full size profile photo</strong> on your gallery can remain as "Friends-only" or
"Private" if you want; but the square Avatar shown next to your name on most pages of the
website will be visible to all logged-in members.
</li>
<li>
The website policy that <strong>your Avatar must show your face</strong> will also be more
strictly enforced along with this change. We want to know that <em>everybody</em> on {{PrettyTitle}}
has a face that we can all see.
</li>
<li>
<strong class="has-text-success">Your consent is required</strong> for this change. If your
profile picture is <em>currently</em> non-public, we <strong>will not</strong> begin showing it
to others on the website until you acknowledge and agree to this change in site policy.
</li>
</ul>
<hr>
<h3 id="what">What is being changed about our Avatars?</h3>
<p>
Previously, it was possible to set your Default Profile Picture on your gallery page as
"Private" or "Friends-only" visibility, and it would cause your <strong>square cropped Avatar</strong>
icon to show as a placeholder image to people who were not allowed to see the full-size copy
on your gallery.
</p>
<p>
For example, if your profile picture was set to "Friends-only" then a
<img src="/static/img/shy-friends.png" width="16" height="16" alt=""> yellow placeholder avatar
would be shown in its place to people who are not on your Friends list, or a
<img src="/static/img/shy-private.png" width="16" height="16" alt=""> purple placeholder avatar
would stand in for your "Private" profile picture to people who you did not grant access.
</p>
<p>
Going forward, the square cropped Avatar images will be moved to become
<strong>visible to everybody</strong> who is logged-in to the website. Here
is an example of how your profile card may look to people before, and after
this change:
</p>
</div>
<div class="columns">
<div class="column">
<em class="is-size-4">
<strong class="has-text-info">Before:</strong>
</em>
<div class="media block mt-2">
<div class="media-left">
<figure class="image is-48x48 is-inline-block">
{{if eq .CurrentUser.ProfilePhoto.Visibility "friends"}}
<img src="/static/img/shy-friends.png">
{{else if eq .CurrentUser.ProfilePhoto.Visibility "private"}}
<img src="/static/img/shy-private.png">
{{else if .CurrentUser.ProfilePhoto.ID}}
<img src="{{PhotoURL .CurrentUser.ProfilePhoto.CroppedFilename}}">
{{else}}
<img src="/static/img/shy.png">
{{end}}
</figure>
</div>
<div class="media-content">
<p class="title is-5">
{{.CurrentUser.NameOrUsername}}
</p>
<p class="subtitle is-6">
<span class="icon"><i class="fa fa-user"></i></span>
{{.CurrentUser.Username}}
</p>
</div>
</div>
</div>
<div class="column">
<em class="is-size-4">
<strong class="has-text-success">After:</strong>
</em>
<div class="media block mt-2">
<div class="media-left">
<figure class="image is-48x48 is-inline-block">
{{if .CurrentUser.ProfilePhoto.ID}}
<img src="{{PhotoURL .CurrentUser.ProfilePhoto.CroppedFilename}}">
{{else}}
<img src="/static/img/shy.png">
{{end}}
</figure>
</div>
<div class="media-content">
<p class="title is-5">
{{.CurrentUser.NameOrUsername}}
</p>
<p class="subtitle is-6">
<span class="icon"><i class="fa fa-user"></i></span>
{{.CurrentUser.Username}}
</p>
</div>
</div>
</div>
</div>
<!-- Already a public picture -->
{{if and (.CurrentUser.ProfilePhoto.ID) (eq .CurrentUser.ProfilePhoto.Visibility "public")}}
<div class="notification is-success is-light p-4">
<strong>Notice:</strong> your default profile picture is already set to 'Public' visibility,
so this change in site policy will not affect you.
</div>
{{end}}
<div class="content">
<hr>
<h3 id="fullsize">Your full-size profile picture can still remain private</h3>
<p>
This change in website policy <strong>only</strong> applies to the
<strong>square cropped Avatar image</strong> of your profile picture -- the
image shown on the top of your profile page and next to your name on the chat
room, forums, and other places around the {{PrettyTitle}} website.
</p>
<p>
Your <strong>full size default profile photo</strong> can still be kept on "Friends-only"
or "Private" visibility. So, for example: if your default profile photo is a full-body nude
picture that includes your face, you could crop your Avatar to show only your face (visible
to everybody) but you can leave the full-size photo on Private if you don't want everybody
to see it.
</p>
<hr>
<h3 id="consent" class="has-text-success">Your consent is requested for this change</h3>
<p>
If your profile picture is currently "Private" or "Friends-only," we <strong>will not</strong>
automatically reveal your Avatar to everybody without your consent. We know that for some of
you, the ability to hide your face picture from other members on the site is very important.
</p>
<p>
While the new policy will go into effect on {{.PublicAvatarEnforcementDate.Format "January 2, 2006"}},
you may accept the updated policy early by clicking on the checkbox below.
<strong>After {{.PublicAvatarEnforcementDate.Format "January 2"}}</strong>, if you have not yet
accepted the new policy, you will be shown this page any time you access the "Certification Required"
areas of the website (such as the chat room, forums, gallery and member directory). All members who
have a non-public profile picture will need to accept this change after that date to continue using
the website.
</p>
</div>
<form method="POST" action="{{ .Request.URL.Path }}">
<input type="hidden" name="next" value="{{.NextURL}}">
{{InputCSRF}}
<div class="field">
<label for="accept" class="label">Acknowledge and accept the new Public Avatar policy</label>
<label class="checkbox">
<input type="checkbox"
name="accept" id="accept"
value="true"
{{if eq (.CurrentUser.GetProfileField "public_avatar_consent") "true"}}checked{{end}}>
I accept the {{PrettyTitle}} Public Avatar policy
</label>
<p class="help">
After accepting this change: if your default profile picture is currently non-public, the
square cropped avatar image will become visible for everybody.
</p>
</div>
<div class="block has-text-centered">
<button type="submit" class="button is-primary">Save my Public Avatar Consent setting</button>
</div>
</form>
<div class="content">
<hr>
<h3 id="disagree" class="has-text-warning">What if I disagree with this change?</h3>
<p>
After <strong>{{.PublicAvatarEnforcementDate.Format "January 2, 2006"}}</strong>, when this change
in website policy goes live, members who have a non-public default profile picture and have
<strong>not</strong> consented to the above update will no longer have access to the "Certification
Required" areas of the website.
</p>
<p>
That is: the Chat Room, Forums, Site Gallery, and Member Directory will no longer be accessible to
you from that time. When visiting any of those areas of the website, you will be redirected to
this consent page which you will need to agree to before you can continue using the website.
</p>
<p>
You will still have access to your Friends list, Messages, profile pages and your account settings
so that you can make arrangements with your friends to connect on a different platform.
If this change in site policy is simply incompatible with you, then we would ask that you
<a href="/settings#deactivate">delete your {{PrettyTitle}} account</a>.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
window.addEventListener("DOMContentLoaded", (event) => {
let $file = document.querySelector("#file"),
$fileName = document.querySelector("#fileName");
$file.addEventListener("change", function() {
let file = this.files[0];
$fileName.innerHTML = file.name;
});
});
</script>
{{end}}

View File

@ -113,6 +113,15 @@
</a> </a>
</li> </li>
<li>
<a href="/settings/public-avatar-consent">
<strong><i class="fa fa-image-portrait mr-1"></i> Public Avatar Consent</strong>
<p class="help">
Review and acknowledge the change in default profile picture policy.
</p>
</a>
</li>
<li> <li>
<a href="/photo/private"> <a href="/photo/private">
<strong><i class="fa fa-eye mr-1"></i> Private Photos</strong> <strong><i class="fa fa-eye mr-1"></i> Private Photos</strong>

View File

@ -5,11 +5,11 @@
<figure class="image is-24x24 is-inline-block"> <figure class="image is-24x24 is-inline-block">
<a href="/u/{{.Username}}" class="has-text-dark"> <a href="/u/{{.Username}}" class="has-text-dark">
{{if .ProfilePhoto.ID}} {{if .ProfilePhoto.ID}}
{{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted)}} {{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted) (not .UserRelationship.PublicAvatarConsent)}}
<img src="/static/img/shy-private.png" class="is-rounded"> <img src="/static/img/shy-private.png" class="is-rounded">
{{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend)}} {{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend) (not .UserRelationship.PublicAvatarConsent)}}
<img src="/static/img/shy-friends.png" class="is-rounded"> <img src="/static/img/shy-friends.png" class="is-rounded">
{{else if and (eq .ProfilePhoto.Visibility "circle") (not .UserRelationship.IsInnerCirclePeer)}} {{else if and (eq .ProfilePhoto.Visibility "circle") (not .UserRelationship.IsInnerCirclePeer) (not .UserRelationship.PublicAvatarConsent)}}
<img src="/static/img/shy-secret.png"> <img src="/static/img/shy-secret.png">
{{else}} {{else}}
<img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}" class="is-rounded"> <img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}" class="is-rounded">
@ -26,11 +26,11 @@
<figure class="image is-48x48 is-inline-block"> <figure class="image is-48x48 is-inline-block">
<a href="/u/{{.Username}}" class="has-text-dark"> <a href="/u/{{.Username}}" class="has-text-dark">
{{if .ProfilePhoto.ID}} {{if .ProfilePhoto.ID}}
{{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted)}} {{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted) (not .UserRelationship.PublicAvatarConsent)}}
<img src="/static/img/shy-private.png"> <img src="/static/img/shy-private.png">
{{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend)}} {{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend) (not .UserRelationship.PublicAvatarConsent)}}
<img src="/static/img/shy-friends.png"> <img src="/static/img/shy-friends.png">
{{else if and (eq .ProfilePhoto.Visibility "circle") (not .UserRelationship.IsInnerCirclePeer)}} {{else if and (eq .ProfilePhoto.Visibility "circle") (not .UserRelationship.IsInnerCirclePeer) (not .UserRelationship.PublicAvatarConsent)}}
<img src="/static/img/shy-secret.png"> <img src="/static/img/shy-secret.png">
{{else}} {{else}}
<img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}"> <img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}">
@ -47,11 +47,11 @@
<figure class="image is-64x64 is-inline-block"> <figure class="image is-64x64 is-inline-block">
<a href="/u/{{.Username}}" class="has-text-dark"> <a href="/u/{{.Username}}" class="has-text-dark">
{{if .ProfilePhoto.ID}} {{if .ProfilePhoto.ID}}
{{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted)}} {{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted) (not .UserRelationship.PublicAvatarConsent)}}
<img src="/static/img/shy-private.png"> <img src="/static/img/shy-private.png">
{{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend)}} {{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend) (not .UserRelationship.PublicAvatarConsent)}}
<img src="/static/img/shy-friends.png"> <img src="/static/img/shy-friends.png">
{{else if and (eq .ProfilePhoto.Visibility "circle") (not .UserRelationship.IsInnerCirclePeer)}} {{else if and (eq .ProfilePhoto.Visibility "circle") (not .UserRelationship.IsInnerCirclePeer) (not .UserRelationship.PublicAvatarConsent)}}
<img src="/static/img/shy-secret.png"> <img src="/static/img/shy-secret.png">
{{else}} {{else}}
<img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}"> <img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}">
@ -68,11 +68,11 @@
<figure class="image is-96x96 is-inline-block"> <figure class="image is-96x96 is-inline-block">
<a href="/u/{{.Username}}" class="has-text-dark"> <a href="/u/{{.Username}}" class="has-text-dark">
{{if .ProfilePhoto.ID}} {{if .ProfilePhoto.ID}}
{{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted)}} {{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted) (not .UserRelationship.PublicAvatarConsent)}}
<img src="/static/img/shy-private.png"> <img src="/static/img/shy-private.png">
{{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend)}} {{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend) (not .UserRelationship.PublicAvatarConsent)}}
<img src="/static/img/shy-friends.png"> <img src="/static/img/shy-friends.png">
{{else if and (eq .ProfilePhoto.Visibility "circle") (not .UserRelationship.IsInnerCirclePeer)}} {{else if and (eq .ProfilePhoto.Visibility "circle") (not .UserRelationship.IsInnerCirclePeer) (not .UserRelationship.PublicAvatarConsent)}}
<img src="/static/img/shy-secret.png"> <img src="/static/img/shy-secret.png">
{{else}} {{else}}
<img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}"> <img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}">
@ -89,11 +89,11 @@
<figure class="image is-32x32 is-inline-block"> <figure class="image is-32x32 is-inline-block">
<a href="/u/{{.Username}}" class="has-text-dark"> <a href="/u/{{.Username}}" class="has-text-dark">
{{if .ProfilePhoto.ID}} {{if .ProfilePhoto.ID}}
{{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted)}} {{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted) (not .UserRelationship.PublicAvatarConsent)}}
<img class="is-rounded" src="/static/img/shy-private.png"> <img class="is-rounded" src="/static/img/shy-private.png">
{{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend)}} {{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend) (not .UserRelationship.PublicAvatarConsent)}}
<img class="is-rounded" src="/static/img/shy-friends.png"> <img class="is-rounded" src="/static/img/shy-friends.png">
{{else if and (eq .ProfilePhoto.Visibility "circle") (not .UserRelationship.IsInnerCirclePeer)}} {{else if and (eq .ProfilePhoto.Visibility "circle") (not .UserRelationship.IsInnerCirclePeer) (not .UserRelationship.PublicAvatarConsent)}}
<img src="/static/img/shy-secret.png"> <img src="/static/img/shy-secret.png">
{{else}} {{else}}
<img class="is-rounded" src="{{PhotoURL .ProfilePhoto.CroppedFilename}}"> <img class="is-rounded" src="{{PhotoURL .ProfilePhoto.CroppedFilename}}">