From 6c91c67c9740d40982e770fee9e7af56ee1bb169 Mon Sep 17 00:00:00 2001 From: Noah Date: Thu, 8 Sep 2022 21:42:20 -0700 Subject: [PATCH] More Private User Avatars * Users who set their Profile Picture to "friends only" or "private" can have their avatar be private all over the website to users who are not their friends or not granted access. * Users who are not your friends see a yellow placeholder avatar, and users not granted access to a private Profile Pic sees a purple avatar. * Admin users see these same placeholder avatars most places too (on search, forums, comments, etc.) if the user did not friend or grant the admin. But admins ALWAYS see it on their Profile Page directly, for ability to moderate. * Fix marking Notifications as read: clicking the link in an unread notification now will wait on the ajax request to finish before allowing the redirect. * Update the FAQ --- pkg/controller/account/profile.go | 10 ++ pkg/controller/account/search.go | 2 +- pkg/controller/admin/feedback.go | 2 +- pkg/controller/block/block.go | 2 +- pkg/controller/friend/friends.go | 5 +- pkg/controller/inbox/inbox.go | 2 +- pkg/controller/photo/certification.go | 8 +- pkg/controller/photo/private.go | 2 +- pkg/controller/photo/site_gallery.go | 2 +- pkg/models/blocklist.go | 6 +- pkg/models/comment.go | 4 + pkg/models/forum_recent.go | 12 ++ pkg/models/friend.go | 10 +- pkg/models/private_photo.go | 8 +- pkg/models/thread.go | 4 + pkg/models/user.go | 22 +++- pkg/models/user_relationship.go | 87 +++++++++++++++ pkg/templates/templates.go | 3 +- web/static/img/shy-friends.png | Bin 0 -> 10108 bytes web/static/img/shy-private.png | Bin 0 -> 9857 bytes web/templates/account/block_list.html | 8 +- web/templates/account/dashboard.html | 82 ++++++++------ web/templates/account/profile.html | 10 +- web/templates/account/search.html | 10 +- web/templates/admin/user_actions.html | 8 +- web/templates/faq.html | 140 ++++++++++++++++++++---- web/templates/forum/board_index.html | 8 +- web/templates/forum/newest.html | 16 +-- web/templates/forum/thread.html | 8 +- web/templates/friend/friends.html | 10 +- web/templates/inbox/compose.html | 8 +- web/templates/inbox/inbox.html | 8 +- web/templates/partials/user_avatar.html | 77 +++++++++++++ web/templates/photo/private.html | 10 +- web/templates/photo/share.html | 10 +- web/templates/photo/upload.html | 8 -- 36 files changed, 429 insertions(+), 183 deletions(-) create mode 100644 pkg/models/user_relationship.go create mode 100644 web/static/img/shy-friends.png create mode 100644 web/static/img/shy-private.png create mode 100644 web/templates/partials/user_avatar.html diff --git a/pkg/controller/account/profile.go b/pkg/controller/account/profile.go index ff7a12c..7dfa172 100644 --- a/pkg/controller/account/profile.go +++ b/pkg/controller/account/profile.go @@ -65,6 +65,16 @@ func Profile() http.HandlerFunc { return } + // Inject relationship booleans for profile picture display. + models.SetUserRelationships(currentUser, []*models.User{user}) + + // Admin user can always see the profile pic - but only on this page. Other avatar displays + // will show the yellow or pink shy.png if the admin is not friends or not granted. + if currentUser.IsAdmin { + user.UserRelationship.IsFriend = true + user.UserRelationship.IsPrivateGranted = true + } + var isSelf = currentUser.ID == user.ID // Banned or disabled? Only admin can view then. diff --git a/pkg/controller/account/search.go b/pkg/controller/account/search.go index 2ea64f3..e743540 100644 --- a/pkg/controller/account/search.go +++ b/pkg/controller/account/search.go @@ -72,7 +72,7 @@ func Search() http.HandlerFunc { } pager.ParsePage(r) - users, err := models.SearchUsers(currentUser.ID, &models.UserSearch{ + users, err := models.SearchUsers(currentUser, &models.UserSearch{ EmailOrUsername: username, Gender: gender, Orientation: orientation, diff --git a/pkg/controller/admin/feedback.go b/pkg/controller/admin/feedback.go index ddd54f3..a429ca4 100644 --- a/pkg/controller/admin/feedback.go +++ b/pkg/controller/admin/feedback.go @@ -156,7 +156,7 @@ func Feedback() http.HandlerFunc { userIDs = append(userIDs, p.UserID) } } - userMap, err := models.MapUsers(userIDs) + userMap, err := models.MapUsers(currentUser, userIDs) if err != nil { session.FlashError(w, r, "Couldn't map user IDs: %s", err) } diff --git a/pkg/controller/block/block.go b/pkg/controller/block/block.go index c126348..297affc 100644 --- a/pkg/controller/block/block.go +++ b/pkg/controller/block/block.go @@ -27,7 +27,7 @@ func Blocked() http.HandlerFunc { Sort: "updated_at desc", } pager.ParsePage(r) - blocked, err := models.PaginateBlockList(currentUser.ID, pager) + blocked, err := models.PaginateBlockList(currentUser, pager) if err != nil { session.FlashError(w, r, "Couldn't paginate block list: %s", err) templates.Redirect(w, "/") diff --git a/pkg/controller/friend/friends.go b/pkg/controller/friend/friends.go index 50f723b..4d2897c 100644 --- a/pkg/controller/friend/friends.go +++ b/pkg/controller/friend/friends.go @@ -32,13 +32,16 @@ func Friends() http.HandlerFunc { Sort: "updated_at desc", } pager.ParsePage(r) - friends, err := models.PaginateFriends(currentUser.ID, isRequests, isPending, pager) + friends, err := models.PaginateFriends(currentUser, isRequests, isPending, pager) if err != nil { session.FlashError(w, r, "Couldn't paginate friends: %s", err) templates.Redirect(w, "/") return } + // Inject relationship booleans. + models.SetUserRelationships(currentUser, friends) + var vars = map[string]interface{}{ "IsRequests": isRequests, "IsPending": isPending, diff --git a/pkg/controller/inbox/inbox.go b/pkg/controller/inbox/inbox.go index 0e1762f..bb55186 100644 --- a/pkg/controller/inbox/inbox.go +++ b/pkg/controller/inbox/inbox.go @@ -116,7 +116,7 @@ func Inbox() http.HandlerFunc { userIDs = append(userIDs, m.SourceUserID, m.TargetUserID) } } - userMap, err := models.MapUsers(userIDs) + userMap, err := models.MapUsers(currentUser, userIDs) if err != nil { session.FlashError(w, r, "Couldn't map users: %s", err) } diff --git a/pkg/controller/photo/certification.go b/pkg/controller/photo/certification.go index e81e842..7d2eeee 100644 --- a/pkg/controller/photo/certification.go +++ b/pkg/controller/photo/certification.go @@ -174,6 +174,12 @@ func AdminCertification() http.HandlerFunc { view = "pending" } + // Get the current user. + currentUser, err := session.CurrentUser(r) + if err != nil { + session.FlashError(w, r, "Couldn't get CurrentUser: %s", err) + } + // Short circuit the GET view for username/email search (exact match) if username := r.FormValue("username"); username != "" { user, err := models.FindUser(username) @@ -341,7 +347,7 @@ func AdminCertification() http.HandlerFunc { for _, p := range photos { userIDs = append(userIDs, p.UserID) } - userMap, err := models.MapUsers(userIDs) + userMap, err := models.MapUsers(currentUser, userIDs) if err != nil { session.FlashError(w, r, "Couldn't map user IDs: %s", err) } diff --git a/pkg/controller/photo/private.go b/pkg/controller/photo/private.go index 23ae2df..ac0c728 100644 --- a/pkg/controller/photo/private.go +++ b/pkg/controller/photo/private.go @@ -35,7 +35,7 @@ func Private() http.HandlerFunc { Sort: "updated_at desc", } pager.ParsePage(r) - users, err := models.PaginatePrivatePhotoList(currentUser.ID, isGrantee, pager) + users, err := models.PaginatePrivatePhotoList(currentUser, isGrantee, pager) if err != nil { session.FlashError(w, r, "Couldn't paginate users: %s", err) templates.Redirect(w, "/") diff --git a/pkg/controller/photo/site_gallery.go b/pkg/controller/photo/site_gallery.go index 359e076..681c8df 100644 --- a/pkg/controller/photo/site_gallery.go +++ b/pkg/controller/photo/site_gallery.go @@ -67,7 +67,7 @@ func SiteGallery() http.HandlerFunc { for _, photo := range photos { userIDs = append(userIDs, photo.UserID) } - userMap, err := models.MapUsers(userIDs) + userMap, err := models.MapUsers(currentUser, userIDs) if err != nil { session.FlashError(w, r, "Failed to MapUsers: %s", err) } diff --git a/pkg/models/blocklist.go b/pkg/models/blocklist.go index 80801be..144611c 100644 --- a/pkg/models/blocklist.go +++ b/pkg/models/blocklist.go @@ -63,7 +63,7 @@ func IsBlocked(sourceUserID, targetUserID uint64) bool { } // PaginateBlockList views a user's blocklist. -func PaginateBlockList(userID uint64, pager *Pagination) ([]*User, error) { +func PaginateBlockList(user *User, pager *Pagination) ([]*User, error) { // We paginate over the Block table. var ( bs = []*Block{} @@ -73,7 +73,7 @@ func PaginateBlockList(userID uint64, pager *Pagination) ([]*User, error) { query = DB.Where( "source_user_id = ?", - userID, + user.ID, ) query = query.Order(pager.Sort) @@ -88,7 +88,7 @@ func PaginateBlockList(userID uint64, pager *Pagination) ([]*User, error) { userIDs = append(userIDs, b.TargetUserID) } - return GetUsers(userIDs) + return GetUsers(user, userIDs) } // BlockedUserIDs returns all user IDs blocked by the user. diff --git a/pkg/models/comment.go b/pkg/models/comment.go index d28dd70..2fd6dfd 100644 --- a/pkg/models/comment.go +++ b/pkg/models/comment.go @@ -79,6 +79,10 @@ func PaginateComments(user *User, tableName string, tableID uint64, pager *Pagin query.Model(&Comment{}).Count(&pager.Total) result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&cs) + + // Inject user relationships into these comments' authors. + SetUserRelationshipsInComments(user, cs) + return cs, result.Error } diff --git a/pkg/models/forum_recent.go b/pkg/models/forum_recent.go index f131934..2ecb2f9 100644 --- a/pkg/models/forum_recent.go +++ b/pkg/models/forum_recent.go @@ -131,14 +131,22 @@ func PaginateRecentPosts(user *User, categories []string, pager *Pagination) ([] forums, _ = GetForums(forumIDs) } + // Collect comments so we can inject UserRelationships in efficiently. + var ( + coms = []*Comment{} + thrs = []*Thread{} + ) + // Merge all the objects back in. for _, rc := range result { if com, ok := comments[rc.CommentID]; ok { rc.Comment = com + coms = append(coms, com) } if thr, ok := threads[rc.ThreadID]; ok { rc.Thread = thr + thrs = append(thrs, thr) } else { log.Error("RecentPosts: didn't find thread ID %d in map!") } @@ -148,5 +156,9 @@ func PaginateRecentPosts(user *User, categories []string, pager *Pagination) ([] } } + // Inject user relationships into all comment users now. + SetUserRelationshipsInComments(user, coms) + SetUserRelationshipsInThreads(user, thrs) + return result, nil } diff --git a/pkg/models/friend.go b/pkg/models/friend.go index 01fb470..dd7b954 100644 --- a/pkg/models/friend.go +++ b/pkg/models/friend.go @@ -123,7 +123,7 @@ The `requests` and `sent` bools are mutually exclusive (use only one, or neither asks for unanswered friend requests to you, and `sent` returns the friend requests that you have sent and have not been answered. */ -func PaginateFriends(userID uint64, requests bool, sent bool, pager *Pagination) ([]*User, error) { +func PaginateFriends(user *User, requests bool, sent bool, pager *Pagination) ([]*User, error) { // We paginate over the Friend table. var ( fs = []*Friend{} @@ -138,17 +138,17 @@ func PaginateFriends(userID uint64, requests bool, sent bool, pager *Pagination) if requests { query = DB.Where( "target_user_id = ? AND approved = ?", - userID, false, + user.ID, false, ) } else if sent { query = DB.Where( "source_user_id = ? AND approved = ?", - userID, false, + user.ID, false, ) } else { query = DB.Where( "source_user_id = ? AND approved = ?", - userID, true, + user.ID, true, ) } @@ -168,7 +168,7 @@ func PaginateFriends(userID uint64, requests bool, sent bool, pager *Pagination) } } - return GetUsers(userIDs) + return GetUsers(user, userIDs) } // GetFriendRequests returns all pending friend requests for a user. diff --git a/pkg/models/private_photo.go b/pkg/models/private_photo.go index 8fc80c2..66ee1cb 100644 --- a/pkg/models/private_photo.go +++ b/pkg/models/private_photo.go @@ -96,7 +96,7 @@ If grantee is true, it returns the list of users who have granted YOU access to private photos. If grantee is false, it returns the users that YOU have granted access to see YOUR OWN private photos. */ -func PaginatePrivatePhotoList(userID uint64, grantee bool, pager *Pagination) ([]*User, error) { +func PaginatePrivatePhotoList(user *User, grantee bool, pager *Pagination) ([]*User, error) { var ( pbs = []*PrivatePhoto{} userIDs = []uint64{} @@ -109,11 +109,11 @@ func PaginatePrivatePhotoList(userID uint64, grantee bool, pager *Pagination) ([ if grantee { // Return the private photo grants for whom YOU are the recipient. wheres = append(wheres, "target_user_id = ?") - placeholders = append(placeholders, userID) + placeholders = append(placeholders, user.ID) } else { // Return the users that YOU have granted access to YOUR private pictures. wheres = append(wheres, "source_user_id = ?") - placeholders = append(placeholders, userID) + placeholders = append(placeholders, user.ID) } query = DB.Where( @@ -137,7 +137,7 @@ func PaginatePrivatePhotoList(userID uint64, grantee bool, pager *Pagination) ([ } } - return GetUsers(userIDs) + return GetUsers(user, userIDs) } // Save photo. diff --git a/pkg/models/thread.go b/pkg/models/thread.go index 22f5f3b..8a57bc6 100644 --- a/pkg/models/thread.go +++ b/pkg/models/thread.go @@ -151,6 +151,10 @@ func PaginateThreads(user *User, forum *Forum, pager *Pagination) ([]*Thread, er query.Model(&Thread{}).Count(&pager.Total) result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&ts) + + // Inject user relationships into these threads' comments' users. + SetUserRelationshipsInThreads(user, ts) + return ts, result.Error } diff --git a/pkg/models/user.go b/pkg/models/user.go index de32bb6..53dfe50 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -34,6 +34,9 @@ type User struct { ProfileField []ProfileField ProfilePhotoID *uint64 ProfilePhoto Photo `gorm:"foreignKey:profile_photo_id"` + + // Current user's relationship to this user -- not stored in DB. + UserRelationship UserRelationship `gorm:"-"` } type UserVisibility string @@ -97,8 +100,8 @@ func GetUser(userId uint64) (*User, error) { } // GetUsers queries for multiple user IDs and returns users in the same order. -func GetUsers(userIDs []uint64) ([]*User, error) { - userMap, err := MapUsers(userIDs) +func GetUsers(currentUser *User, userIDs []uint64) ([]*User, error) { + userMap, err := MapUsers(currentUser, userIDs) if err != nil { return nil, err } @@ -141,7 +144,7 @@ type UserSearch struct { } // SearchUsers from the perspective of a given user. -func SearchUsers(userID uint64, search *UserSearch, pager *Pagination) ([]*User, error) { +func SearchUsers(user *User, search *UserSearch, pager *Pagination) ([]*User, error) { if search == nil { search = &UserSearch{} } @@ -151,7 +154,7 @@ func SearchUsers(userID uint64, search *UserSearch, pager *Pagination) ([]*User, query *gorm.DB wheres = []string{} placeholders = []interface{}{} - blockedUserIDs = BlockedUserIDs(userID) + blockedUserIDs = BlockedUserIDs(user.ID) ) if len(blockedUserIDs) > 0 { @@ -218,6 +221,10 @@ func SearchUsers(userID uint64, search *UserSearch, pager *Pagination) ([]*User, ).Order(pager.Sort) query.Model(&User{}).Count(&pager.Total) result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&users) + + // Inject relationship booleans. + SetUserRelationships(user, users) + return users, result.Error } @@ -227,7 +234,7 @@ type UserMap map[uint64]*User // MapUsers looks up a set of user IDs in bulk and returns a UserMap suitable for templates. // Useful to avoid circular reference issues with Photos especially; the Site Gallery queries // photos of ALL users and MapUsers helps stitch them together for the frontend. -func MapUsers(userIDs []uint64) (UserMap, error) { +func MapUsers(user *User, userIDs []uint64) (UserMap, error) { var ( usermap = UserMap{} set = map[uint64]interface{}{} @@ -248,6 +255,11 @@ func MapUsers(userIDs []uint64) (UserMap, error) { result = (&User{}).Preload().Where("id IN ?", distinct).Find(&users) ) + // Inject user relationships. + if user != nil { + SetUserRelationships(user, users) + } + if result.Error == nil { for _, row := range users { usermap[row.ID] = row diff --git a/pkg/models/user_relationship.go b/pkg/models/user_relationship.go new file mode 100644 index 0000000..359974c --- /dev/null +++ b/pkg/models/user_relationship.go @@ -0,0 +1,87 @@ +package models + +// 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. +// The zero-values should fail safely: in case the UserRelationship isn't populated correctly, +// private profile pics show as private by default. +type UserRelationship struct { + IsFriend bool // if true, a friends-only profile pic can show + IsPrivateGranted bool // if true, a private profile pic can show +} + +// SetUserRelationships updates a set of User objects to populate their UserRelationships in +// relationship to the current user who is looking. +func SetUserRelationships(user *User, users []*User) error { + // Collect the current user's Friendships and Private Grants. + var ( + friendIDs = FriendIDs(user.ID) + privateGrants = PrivateGrantedUserIDs(user.ID) + ) + + // Map them for easier lookup. + var ( + friendMap = map[uint64]interface{}{} + privateMap = map[uint64]interface{}{} + ) + + for _, id := range friendIDs { + friendMap[id] = nil + } + + for _, id := range privateGrants { + privateMap[id] = nil + } + + // Inject the UserRelationships. + for _, u := range users { + if u.ID == user.ID { + // Current user - set both bools to true - you can always see your own profile pic. + u.UserRelationship.IsFriend = true + u.UserRelationship.IsPrivateGranted = true + continue + } + + if _, ok := friendMap[u.ID]; ok { + u.UserRelationship.IsFriend = true + } + + if _, ok := privateMap[u.ID]; ok { + u.UserRelationship.IsPrivateGranted = true + } + } + return nil +} + +// SetUserRelationshipsInComments takes a set of Comments and sets relationship booleans on their Users. +func SetUserRelationshipsInComments(user *User, comments []*Comment) { + // Gather and map the users. + var ( + users = []*User{} + userMap = map[uint64]*User{} + ) + + for _, c := range comments { + users = append(users, &c.User) + userMap[c.User.ID] = &c.User + } + + // Inject relationships. + SetUserRelationships(user, users) +} + +// SetUserRelationshipsInThreads takes a set of Threads and sets relationship booleans on their Users. +func SetUserRelationshipsInThreads(user *User, threads []*Thread) { + // Gather and map the thread parent comments. + var ( + comments = []*Comment{} + comMap = map[uint64]*Comment{} + ) + + for _, c := range threads { + comments = append(comments, &c.Comment) + comMap[c.Comment.ID] = &c.Comment + } + + // Inject relationships into those comments' users. + SetUserRelationshipsInComments(user, comments) +} diff --git a/pkg/templates/templates.go b/pkg/templates/templates.go index 575a7dc..3a244af 100644 --- a/pkg/templates/templates.go +++ b/pkg/templates/templates.go @@ -30,7 +30,7 @@ func LoadTemplate(filename string) (*Template, error) { filepath := config.TemplatePath + "/" + filename stat, err := os.Stat(filepath) if err != nil { - return nil, fmt.Errorf("LoadTemplate(%s): %s", err) + return nil, fmt.Errorf("LoadTemplate(%s): %s", filename, err) } files := templates(config.TemplatePath + "/" + filename) @@ -118,6 +118,7 @@ func (t *Template) Reload() error { // Base template layout. var baseTemplates = []string{ config.TemplatePath + "/base.html", + config.TemplatePath + "/partials/user_avatar.html", } // templates returns a template chain with the base templates preceding yours. diff --git a/web/static/img/shy-friends.png b/web/static/img/shy-friends.png new file mode 100644 index 0000000000000000000000000000000000000000..34af734a9495d4f58682cd6eddc94b2648fea86c GIT binary patch literal 10108 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 literal 0 HcmV?d00001 diff --git a/web/templates/account/block_list.html b/web/templates/account/block_list.html index 2675369..8555009 100644 --- a/web/templates/account/block_list.html +++ b/web/templates/account/block_list.html @@ -51,13 +51,7 @@
-
- {{if .ProfilePhoto.ID}} - - {{else}} - - {{end}} -
+ {{template "avatar-64x64" .}}

{{.NameOrUsername}}

diff --git a/web/templates/account/dashboard.html b/web/templates/account/dashboard.html index 38f39ca..732ab06 100644 --- a/web/templates/account/dashboard.html +++ b/web/templates/account/dashboard.html @@ -184,13 +184,7 @@
{{end}} -
- {{if .AboutUser.ProfilePhoto.ID}} - - {{else}} - - {{end}} -
+ {{template "avatar-48x48" .AboutUser}}
@@ -355,34 +349,58 @@ document.addEventListener('DOMContentLoaded', () => { // Bind to the notification table rows. (document.querySelectorAll(".nonshy-notification-row") || []).forEach(node => { - node.addEventListener("click", (e) => { - if (busy) return; + let $newBadge = node.querySelector(".nonshy-notification-new"), + ID = node.dataset.notificationId; - let $newBadge = node.querySelector(".nonshy-notification-new"), - ID = node.dataset.notificationId; - $newBadge.style.display = "none"; + // If the notification doesn't have a "NEW!" badge, no action needed. + if ($newBadge === null) return; - return fetch("/v1/notifications/read", { - method: "POST", - mode: "same-origin", - cache: "no-cache", - credentials: "same-origin", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - "id": parseInt(ID), - }), + // Collect any hyperlinks in this row. + let links = Array.from(node.querySelectorAll("a")); + links.push(node); + + // Apply a "click" handler to the notification row as a whole, and to all of the hyperlinks in it. + // For the hyperlinks: prevent the browser following the link UNTIL the successful ajax request to + // mark the notification "read" has run. + links.forEach(link => { + link.addEventListener("click", (e) => { + if (busy) return; + + // In case it's a hyperlink, grab the href. + let href = link.attributes.href; + if (href !== undefined) { + e.preventDefault(); + href = href.textContent; + } + + $newBadge.style.display = "none"; + + busy = true; + return fetch("/v1/notifications/read", { + method: "POST", + mode: "same-origin", + cache: "no-cache", + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + "id": parseInt(ID), + }), + }) + .then((response) => response.json()) + .then((data) => { + console.log(data); + }).catch(resp => { + window.alert(resp); + }).finally(() => { + busy = false; + if (href !== undefined) { + window.location.href = href; + } + }); }) - .then((response) => response.json()) - .then((data) => { - console.log(data); - }).catch(resp => { - window.alert(resp); - }).finally(() => { - busy = false; - }) - }); + }) }); }); diff --git a/web/templates/account/profile.html b/web/templates/account/profile.html index 19a5dbc..4df9e8b 100644 --- a/web/templates/account/profile.html +++ b/web/templates/account/profile.html @@ -8,9 +8,15 @@
{{if .User.ProfilePhoto.ID}} - + {{if and (eq .User.ProfilePhoto.Visibility "private") (not .User.UserRelationship.IsPrivateGranted)}} + + {{else if and (eq .User.ProfilePhoto.Visibility "friends") (not .User.UserRelationship.IsFriend)}} + + {{else}} + + {{end}} {{else}} - + {{end}} diff --git a/web/templates/account/search.html b/web/templates/account/search.html index e3eeec7..91ce193 100644 --- a/web/templates/account/search.html +++ b/web/templates/account/search.html @@ -176,15 +176,7 @@
-
- - {{if .ProfilePhoto.ID}} - - {{else}} - - {{end}} - -
+ {{template "avatar-64x64" .}}

diff --git a/web/templates/admin/user_actions.html b/web/templates/admin/user_actions.html index d22b2db..8ed64bb 100644 --- a/web/templates/admin/user_actions.html +++ b/web/templates/admin/user_actions.html @@ -38,13 +38,7 @@

-
- {{if .User.ProfilePhoto.ID}} - - {{else}} - - {{end}} -
+ {{template "avatar-64x64" .}}

{{.NameOrUsername}}

diff --git a/web/templates/faq.html b/web/templates/faq.html index 0573668..6e9a768 100644 --- a/web/templates/faq.html +++ b/web/templates/faq.html @@ -13,9 +13,48 @@
-

General FAQs

+ + + +

Certification FAQs

+ +

What does certification mean, and what is a "verification selfie"?

This website requires all members to be "certified" or proven to be real human beings @@ -29,7 +68,7 @@ spam robots that plague other similar sites.

-

Do I need to send a "verification selfie"?

+

Do I need to send a "verification selfie"?

Yes. @@ -42,7 +81,67 @@ until your profile has been certified.

-

What can non-certified members do?

+

+ Your certification photo is only seen by site administrators and does not appear + on your profile page. +

+ +

Are there alternative options to becoming Certified?

+ +

+ NEW Sept. 8 2022 + I understand that some nudists need to exercise a degree of discretion and will + not want to take a face pic with {{PrettyTitle}}'s name and upload that anywhere at all onto the Internet. For + example if you are a teacher or work in law enforcement or the clergy and you need to keep + careful control of your image online because the world isn't quite enlightened enough yet + when it comes to nudity and sexuality. +

+ +

+ These are very valid concerns and can be handled on a case-by-case basis. The + main thing I'm looking for in certification is to prove that you're a real person, + you look like your pictures here and that you're not a minor. Send a DM to + u/introvertnudist + to inquire about alternative verification methods, which may include just hopping on a + quick video call with a site admin. +

+ +

+ Note, though: this is a social site for {{PrettyTitle}} nudists so you are + expected to post at least some pictures of yourself on your profile page. You + could post a face pic wearing a hat and sunglasses; or you could crop out half your face + showing only from the chin down; or you could post full body nudes with your face censored + out with an emoji character. +

+ +

Can my Profile Picture be kept private?

+ +

+ NEW Sept. 8 2022 + You may set your Profile Picture to be "Friends only" or "Private" visibility + if you wish to be more discreet about your face pictures. +

+ +
    +
  • + Friends only + : + your profile pic displays as a yellow + + placeholder image for people who are not on your Friends list. +
  • +
  • + Private + : + your profile pic displays as a purple + + placeholder image for everybody except for people that you had + granted access to see your + private photos. +
  • +
+ +

What can non-certified members do?

Before you have an approved certification photo, you can mainly only access and edit your @@ -62,7 +161,7 @@ this is intentional to help guard against spam bots and creepy people.

-

What are the visibility options for my profile page?

+

What are the visibility options for my profile page?

There are currently three different choices for your profile visibility on your @@ -93,9 +192,9 @@ -

Photo FAQs

+

Photo FAQs

-

Do I have to post my nudes here?

+

Do I have to post my nudes here?

You must be comfortable with doing so, yes. On some other nudist social websites, many @@ -105,7 +204,7 @@ feel more comfortable if you post some of your own nudes as well.

-

Do I have to include my face in my nudes?

+

Do I have to include my face in my nudes?

You don't have to! I know many nudists are not comfortable with their face appearing @@ -120,7 +219,7 @@ want to see just dick pics everywhere. And don't set those as your default profile pic!

-

What appears on the Site Gallery?

+

The " Gallery" link on the site nav bar goes to the Site-wide @@ -141,7 +240,7 @@ the Gallery -- it will then only appear on your profile page.

-

What is considered "explicit" in photos?

+

What is considered "explicit" in photos?

On this website, I make a fairly common distinction between what's a "normal nude" and @@ -169,7 +268,7 @@ content from other users -- by default this site is "normal nudes" friendly!

-

Does this site prevent people from downloading my pictures?

+

Does this site prevent people from downloading my pictures?

This website does not go out of its way to prevent people from downloading pictures, and @@ -214,16 +313,9 @@ -

- Note that your square cropped Default Profile Picture that appears next to your username - on various parts of this website is always visible to logged-in user accounts. The "full size" - version may be Friends-only or Private, but the square crop that you chose for your Profile - Picture is currently displayed to all logged-in users. -

-

Additionally, your Profile Page altogether can only be seen by logged-in members - of this site by default. You may tighten it further and mark your entire profile + of this site by default. You may tighten it even further and mark your entire profile as Private (and only approved friends can see your profile or any of your photos). Note that your square cropped profile picture is still visible even so.

@@ -237,7 +329,7 @@

Forum FAQs

-

What do the various badges on the forum mean?

+

What do the various badges on the forum mean?

You may see some of these badges on the forums or their posts. These are their meanings: @@ -282,7 +374,7 @@ -

Can I create my own forums?

+

Can I create my own forums?

This feature is coming soon! Users will be allowed to create their own forums and @@ -306,9 +398,9 @@ -

Technical FAQs

+

Technical FAQs

-

Why did you build a custom website?

+

Why did you build a custom website?

Other variants on this question might be: why not just run a @@ -338,7 +430,7 @@ fate is kept in my own hands.

-

Is this website open source?

+

Is this website open source?

Yes! The source code for this website is released as free software under the GNU diff --git a/web/templates/forum/board_index.html b/web/templates/forum/board_index.html index d8cc395..e279343 100644 --- a/web/templates/forum/board_index.html +++ b/web/templates/forum/board_index.html @@ -82,13 +82,7 @@

{{.Comment.User.Username}} diff --git a/web/templates/forum/newest.html b/web/templates/forum/newest.html index 2aac5c7..3eabdcb 100644 --- a/web/templates/forum/newest.html +++ b/web/templates/forum/newest.html @@ -66,13 +66,7 @@
-
- {{if $User.ProfilePhoto.ID}} - - {{else}} - - {{end}} -
+ {{template "avatar-96x96" $User}}
@@ -136,13 +130,7 @@
diff --git a/web/templates/forum/thread.html b/web/templates/forum/thread.html index 999e239..2f0c521 100644 --- a/web/templates/forum/thread.html +++ b/web/templates/forum/thread.html @@ -125,13 +125,7 @@
{{.User.Username}} diff --git a/web/templates/friend/friends.html b/web/templates/friend/friends.html index ff7057f..4448fe3 100644 --- a/web/templates/friend/friends.html +++ b/web/templates/friend/friends.html @@ -81,15 +81,7 @@
-
- - {{if .ProfilePhoto.ID}} - - {{else}} - - {{end}} - -
+ {{template "avatar-64x64" .}}

diff --git a/web/templates/inbox/compose.html b/web/templates/inbox/compose.html index 38e64b4..c684bfe 100644 --- a/web/templates/inbox/compose.html +++ b/web/templates/inbox/compose.html @@ -26,13 +26,7 @@

-
- {{if .User.ProfilePhoto.ID}} - - {{else}} - - {{end}} -
+ {{template "avatar-64x64" .}}

{{.NameOrUsername}}

diff --git a/web/templates/inbox/inbox.html b/web/templates/inbox/inbox.html index 5cc8d41..7aaa823 100644 --- a/web/templates/inbox/inbox.html +++ b/web/templates/inbox/inbox.html @@ -55,13 +55,7 @@
{{$SourceUser := $UserMap.Get .SourceUserID}}
-
- {{if $SourceUser.ProfilePhoto.ID}} - - {{else}} - - {{end}} -
+ {{template "avatar-64x64" $SourceUser}}

{{$SourceUser.NameOrUsername}}

diff --git a/web/templates/partials/user_avatar.html b/web/templates/partials/user_avatar.html new file mode 100644 index 0000000..df33ede --- /dev/null +++ b/web/templates/partials/user_avatar.html @@ -0,0 +1,77 @@ + + + +{{define "avatar-48x48"}} +
+ + {{if .ProfilePhoto.ID}} + {{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted)}} + + {{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend)}} + + {{else}} + + {{end}} + {{else}} + + {{end}} + +
+{{end}} + + +{{define "avatar-64x64"}} +
+ + {{if .ProfilePhoto.ID}} + {{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted)}} + + {{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend)}} + + {{else}} + + {{end}} + {{else}} + + {{end}} + +
+{{end}} + + +{{define "avatar-96x96"}} +
+ + {{if .ProfilePhoto.ID}} + {{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted)}} + + {{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend)}} + + {{else}} + + {{end}} + {{else}} + + {{end}} + +
+{{end}} + + +{{define "avatar-32x32"}} +
+ + {{if .ProfilePhoto.ID}} + {{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted)}} + + {{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend)}} + + {{else}} + + {{end}} + {{else}} + + {{end}} + +
+{{end}} \ No newline at end of file diff --git a/web/templates/photo/private.html b/web/templates/photo/private.html index d038513..8169cf9 100644 --- a/web/templates/photo/private.html +++ b/web/templates/photo/private.html @@ -102,15 +102,7 @@
-
- - {{if .ProfilePhoto.ID}} - - {{else}} - - {{end}} - -
+ {{template "avatar-64x64" .}}

diff --git a/web/templates/photo/share.html b/web/templates/photo/share.html index 0b0597e..ab6e684 100644 --- a/web/templates/photo/share.html +++ b/web/templates/photo/share.html @@ -47,16 +47,10 @@

-
- {{if .User.ProfilePhoto.ID}} - - {{else}} - - {{end}} -
+ {{template "avatar-64x64" .User}}
-

{{.NameOrUsername}}

+

{{.User.NameOrUsername}}

{{.User.Username}} diff --git a/web/templates/photo/upload.html b/web/templates/photo/upload.html index 934ca0e..33ce691 100644 --- a/web/templates/photo/upload.html +++ b/web/templates/photo/upload.html @@ -262,14 +262,6 @@

-
- Notice: the square cropped - thumbnail of your Default Profile Picture will always be visible on your - profile and displayed alongside your username elsewhere on the site. The above Visibility - setting can limit the full-size photo's visibility; but the square cropped - thumbnail is always seen to logged-in members. -
-