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
This commit is contained in:
parent
0fe538fd87
commit
6c91c67c97
|
@ -65,6 +65,16 @@ func Profile() http.HandlerFunc {
|
||||||
return
|
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
|
var isSelf = currentUser.ID == user.ID
|
||||||
|
|
||||||
// Banned or disabled? Only admin can view then.
|
// Banned or disabled? Only admin can view then.
|
||||||
|
|
|
@ -72,7 +72,7 @@ func Search() http.HandlerFunc {
|
||||||
}
|
}
|
||||||
pager.ParsePage(r)
|
pager.ParsePage(r)
|
||||||
|
|
||||||
users, err := models.SearchUsers(currentUser.ID, &models.UserSearch{
|
users, err := models.SearchUsers(currentUser, &models.UserSearch{
|
||||||
EmailOrUsername: username,
|
EmailOrUsername: username,
|
||||||
Gender: gender,
|
Gender: gender,
|
||||||
Orientation: orientation,
|
Orientation: orientation,
|
||||||
|
|
|
@ -156,7 +156,7 @@ func Feedback() http.HandlerFunc {
|
||||||
userIDs = append(userIDs, p.UserID)
|
userIDs = append(userIDs, p.UserID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
userMap, err := models.MapUsers(userIDs)
|
userMap, err := models.MapUsers(currentUser, userIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
session.FlashError(w, r, "Couldn't map user IDs: %s", err)
|
session.FlashError(w, r, "Couldn't map user IDs: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ func Blocked() http.HandlerFunc {
|
||||||
Sort: "updated_at desc",
|
Sort: "updated_at desc",
|
||||||
}
|
}
|
||||||
pager.ParsePage(r)
|
pager.ParsePage(r)
|
||||||
blocked, err := models.PaginateBlockList(currentUser.ID, pager)
|
blocked, err := models.PaginateBlockList(currentUser, pager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
session.FlashError(w, r, "Couldn't paginate block list: %s", err)
|
session.FlashError(w, r, "Couldn't paginate block list: %s", err)
|
||||||
templates.Redirect(w, "/")
|
templates.Redirect(w, "/")
|
||||||
|
|
|
@ -32,13 +32,16 @@ func Friends() http.HandlerFunc {
|
||||||
Sort: "updated_at desc",
|
Sort: "updated_at desc",
|
||||||
}
|
}
|
||||||
pager.ParsePage(r)
|
pager.ParsePage(r)
|
||||||
friends, err := models.PaginateFriends(currentUser.ID, isRequests, isPending, pager)
|
friends, err := models.PaginateFriends(currentUser, isRequests, isPending, pager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
session.FlashError(w, r, "Couldn't paginate friends: %s", err)
|
session.FlashError(w, r, "Couldn't paginate friends: %s", err)
|
||||||
templates.Redirect(w, "/")
|
templates.Redirect(w, "/")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject relationship booleans.
|
||||||
|
models.SetUserRelationships(currentUser, friends)
|
||||||
|
|
||||||
var vars = map[string]interface{}{
|
var vars = map[string]interface{}{
|
||||||
"IsRequests": isRequests,
|
"IsRequests": isRequests,
|
||||||
"IsPending": isPending,
|
"IsPending": isPending,
|
||||||
|
|
|
@ -116,7 +116,7 @@ func Inbox() http.HandlerFunc {
|
||||||
userIDs = append(userIDs, m.SourceUserID, m.TargetUserID)
|
userIDs = append(userIDs, m.SourceUserID, m.TargetUserID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
userMap, err := models.MapUsers(userIDs)
|
userMap, err := models.MapUsers(currentUser, userIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
session.FlashError(w, r, "Couldn't map users: %s", err)
|
session.FlashError(w, r, "Couldn't map users: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -174,6 +174,12 @@ func AdminCertification() http.HandlerFunc {
|
||||||
view = "pending"
|
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)
|
// Short circuit the GET view for username/email search (exact match)
|
||||||
if username := r.FormValue("username"); username != "" {
|
if username := r.FormValue("username"); username != "" {
|
||||||
user, err := models.FindUser(username)
|
user, err := models.FindUser(username)
|
||||||
|
@ -341,7 +347,7 @@ func AdminCertification() http.HandlerFunc {
|
||||||
for _, p := range photos {
|
for _, p := range photos {
|
||||||
userIDs = append(userIDs, p.UserID)
|
userIDs = append(userIDs, p.UserID)
|
||||||
}
|
}
|
||||||
userMap, err := models.MapUsers(userIDs)
|
userMap, err := models.MapUsers(currentUser, userIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
session.FlashError(w, r, "Couldn't map user IDs: %s", err)
|
session.FlashError(w, r, "Couldn't map user IDs: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ func Private() http.HandlerFunc {
|
||||||
Sort: "updated_at desc",
|
Sort: "updated_at desc",
|
||||||
}
|
}
|
||||||
pager.ParsePage(r)
|
pager.ParsePage(r)
|
||||||
users, err := models.PaginatePrivatePhotoList(currentUser.ID, isGrantee, pager)
|
users, err := models.PaginatePrivatePhotoList(currentUser, isGrantee, pager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
session.FlashError(w, r, "Couldn't paginate users: %s", err)
|
session.FlashError(w, r, "Couldn't paginate users: %s", err)
|
||||||
templates.Redirect(w, "/")
|
templates.Redirect(w, "/")
|
||||||
|
|
|
@ -67,7 +67,7 @@ func SiteGallery() http.HandlerFunc {
|
||||||
for _, photo := range photos {
|
for _, photo := range photos {
|
||||||
userIDs = append(userIDs, photo.UserID)
|
userIDs = append(userIDs, photo.UserID)
|
||||||
}
|
}
|
||||||
userMap, err := models.MapUsers(userIDs)
|
userMap, err := models.MapUsers(currentUser, userIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
session.FlashError(w, r, "Failed to MapUsers: %s", err)
|
session.FlashError(w, r, "Failed to MapUsers: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ func IsBlocked(sourceUserID, targetUserID uint64) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// PaginateBlockList views a user's blocklist.
|
// 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.
|
// We paginate over the Block table.
|
||||||
var (
|
var (
|
||||||
bs = []*Block{}
|
bs = []*Block{}
|
||||||
|
@ -73,7 +73,7 @@ func PaginateBlockList(userID uint64, pager *Pagination) ([]*User, error) {
|
||||||
|
|
||||||
query = DB.Where(
|
query = DB.Where(
|
||||||
"source_user_id = ?",
|
"source_user_id = ?",
|
||||||
userID,
|
user.ID,
|
||||||
)
|
)
|
||||||
|
|
||||||
query = query.Order(pager.Sort)
|
query = query.Order(pager.Sort)
|
||||||
|
@ -88,7 +88,7 @@ func PaginateBlockList(userID uint64, pager *Pagination) ([]*User, error) {
|
||||||
userIDs = append(userIDs, b.TargetUserID)
|
userIDs = append(userIDs, b.TargetUserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetUsers(userIDs)
|
return GetUsers(user, userIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlockedUserIDs returns all user IDs blocked by the user.
|
// BlockedUserIDs returns all user IDs blocked by the user.
|
||||||
|
|
|
@ -79,6 +79,10 @@ func PaginateComments(user *User, tableName string, tableID uint64, pager *Pagin
|
||||||
|
|
||||||
query.Model(&Comment{}).Count(&pager.Total)
|
query.Model(&Comment{}).Count(&pager.Total)
|
||||||
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&cs)
|
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&cs)
|
||||||
|
|
||||||
|
// Inject user relationships into these comments' authors.
|
||||||
|
SetUserRelationshipsInComments(user, cs)
|
||||||
|
|
||||||
return cs, result.Error
|
return cs, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -131,14 +131,22 @@ func PaginateRecentPosts(user *User, categories []string, pager *Pagination) ([]
|
||||||
forums, _ = GetForums(forumIDs)
|
forums, _ = GetForums(forumIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collect comments so we can inject UserRelationships in efficiently.
|
||||||
|
var (
|
||||||
|
coms = []*Comment{}
|
||||||
|
thrs = []*Thread{}
|
||||||
|
)
|
||||||
|
|
||||||
// Merge all the objects back in.
|
// Merge all the objects back in.
|
||||||
for _, rc := range result {
|
for _, rc := range result {
|
||||||
if com, ok := comments[rc.CommentID]; ok {
|
if com, ok := comments[rc.CommentID]; ok {
|
||||||
rc.Comment = com
|
rc.Comment = com
|
||||||
|
coms = append(coms, com)
|
||||||
}
|
}
|
||||||
|
|
||||||
if thr, ok := threads[rc.ThreadID]; ok {
|
if thr, ok := threads[rc.ThreadID]; ok {
|
||||||
rc.Thread = thr
|
rc.Thread = thr
|
||||||
|
thrs = append(thrs, thr)
|
||||||
} else {
|
} else {
|
||||||
log.Error("RecentPosts: didn't find thread ID %d in map!")
|
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
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
asks for unanswered friend requests to you, and `sent` returns the friend requests that you
|
||||||
have sent and have not been answered.
|
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.
|
// We paginate over the Friend table.
|
||||||
var (
|
var (
|
||||||
fs = []*Friend{}
|
fs = []*Friend{}
|
||||||
|
@ -138,17 +138,17 @@ func PaginateFriends(userID uint64, requests bool, sent bool, pager *Pagination)
|
||||||
if requests {
|
if requests {
|
||||||
query = DB.Where(
|
query = DB.Where(
|
||||||
"target_user_id = ? AND approved = ?",
|
"target_user_id = ? AND approved = ?",
|
||||||
userID, false,
|
user.ID, false,
|
||||||
)
|
)
|
||||||
} else if sent {
|
} else if sent {
|
||||||
query = DB.Where(
|
query = DB.Where(
|
||||||
"source_user_id = ? AND approved = ?",
|
"source_user_id = ? AND approved = ?",
|
||||||
userID, false,
|
user.ID, false,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
query = DB.Where(
|
query = DB.Where(
|
||||||
"source_user_id = ? AND approved = ?",
|
"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.
|
// GetFriendRequests returns all pending friend requests for a user.
|
||||||
|
|
|
@ -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
|
private photos. If grantee is false, it returns the users that YOU have granted access to
|
||||||
see YOUR OWN private photos.
|
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 (
|
var (
|
||||||
pbs = []*PrivatePhoto{}
|
pbs = []*PrivatePhoto{}
|
||||||
userIDs = []uint64{}
|
userIDs = []uint64{}
|
||||||
|
@ -109,11 +109,11 @@ func PaginatePrivatePhotoList(userID uint64, grantee bool, pager *Pagination) ([
|
||||||
if grantee {
|
if grantee {
|
||||||
// Return the private photo grants for whom YOU are the recipient.
|
// Return the private photo grants for whom YOU are the recipient.
|
||||||
wheres = append(wheres, "target_user_id = ?")
|
wheres = append(wheres, "target_user_id = ?")
|
||||||
placeholders = append(placeholders, userID)
|
placeholders = append(placeholders, user.ID)
|
||||||
} else {
|
} else {
|
||||||
// Return the users that YOU have granted access to YOUR private pictures.
|
// Return the users that YOU have granted access to YOUR private pictures.
|
||||||
wheres = append(wheres, "source_user_id = ?")
|
wheres = append(wheres, "source_user_id = ?")
|
||||||
placeholders = append(placeholders, userID)
|
placeholders = append(placeholders, user.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
query = DB.Where(
|
query = DB.Where(
|
||||||
|
@ -137,7 +137,7 @@ func PaginatePrivatePhotoList(userID uint64, grantee bool, pager *Pagination) ([
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetUsers(userIDs)
|
return GetUsers(user, userIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save photo.
|
// Save photo.
|
||||||
|
|
|
@ -151,6 +151,10 @@ func PaginateThreads(user *User, forum *Forum, pager *Pagination) ([]*Thread, er
|
||||||
|
|
||||||
query.Model(&Thread{}).Count(&pager.Total)
|
query.Model(&Thread{}).Count(&pager.Total)
|
||||||
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&ts)
|
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
|
return ts, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,9 @@ type User struct {
|
||||||
ProfileField []ProfileField
|
ProfileField []ProfileField
|
||||||
ProfilePhotoID *uint64
|
ProfilePhotoID *uint64
|
||||||
ProfilePhoto Photo `gorm:"foreignKey:profile_photo_id"`
|
ProfilePhoto Photo `gorm:"foreignKey:profile_photo_id"`
|
||||||
|
|
||||||
|
// Current user's relationship to this user -- not stored in DB.
|
||||||
|
UserRelationship UserRelationship `gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserVisibility string
|
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.
|
// GetUsers queries for multiple user IDs and returns users in the same order.
|
||||||
func GetUsers(userIDs []uint64) ([]*User, error) {
|
func GetUsers(currentUser *User, userIDs []uint64) ([]*User, error) {
|
||||||
userMap, err := MapUsers(userIDs)
|
userMap, err := MapUsers(currentUser, userIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -141,7 +144,7 @@ type UserSearch struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchUsers from the perspective of a given user.
|
// 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 {
|
if search == nil {
|
||||||
search = &UserSearch{}
|
search = &UserSearch{}
|
||||||
}
|
}
|
||||||
|
@ -151,7 +154,7 @@ func SearchUsers(userID uint64, search *UserSearch, pager *Pagination) ([]*User,
|
||||||
query *gorm.DB
|
query *gorm.DB
|
||||||
wheres = []string{}
|
wheres = []string{}
|
||||||
placeholders = []interface{}{}
|
placeholders = []interface{}{}
|
||||||
blockedUserIDs = BlockedUserIDs(userID)
|
blockedUserIDs = BlockedUserIDs(user.ID)
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(blockedUserIDs) > 0 {
|
if len(blockedUserIDs) > 0 {
|
||||||
|
@ -218,6 +221,10 @@ func SearchUsers(userID uint64, search *UserSearch, pager *Pagination) ([]*User,
|
||||||
).Order(pager.Sort)
|
).Order(pager.Sort)
|
||||||
query.Model(&User{}).Count(&pager.Total)
|
query.Model(&User{}).Count(&pager.Total)
|
||||||
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&users)
|
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&users)
|
||||||
|
|
||||||
|
// Inject relationship booleans.
|
||||||
|
SetUserRelationships(user, users)
|
||||||
|
|
||||||
return users, result.Error
|
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.
|
// 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
|
// 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.
|
// 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 (
|
var (
|
||||||
usermap = UserMap{}
|
usermap = UserMap{}
|
||||||
set = map[uint64]interface{}{}
|
set = map[uint64]interface{}{}
|
||||||
|
@ -248,6 +255,11 @@ func MapUsers(userIDs []uint64) (UserMap, error) {
|
||||||
result = (&User{}).Preload().Where("id IN ?", distinct).Find(&users)
|
result = (&User{}).Preload().Where("id IN ?", distinct).Find(&users)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Inject user relationships.
|
||||||
|
if user != nil {
|
||||||
|
SetUserRelationships(user, users)
|
||||||
|
}
|
||||||
|
|
||||||
if result.Error == nil {
|
if result.Error == nil {
|
||||||
for _, row := range users {
|
for _, row := range users {
|
||||||
usermap[row.ID] = row
|
usermap[row.ID] = row
|
||||||
|
|
87
pkg/models/user_relationship.go
Normal file
87
pkg/models/user_relationship.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ func LoadTemplate(filename string) (*Template, error) {
|
||||||
filepath := config.TemplatePath + "/" + filename
|
filepath := config.TemplatePath + "/" + filename
|
||||||
stat, err := os.Stat(filepath)
|
stat, err := os.Stat(filepath)
|
||||||
if err != nil {
|
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)
|
files := templates(config.TemplatePath + "/" + filename)
|
||||||
|
@ -118,6 +118,7 @@ func (t *Template) Reload() error {
|
||||||
// Base template layout.
|
// Base template layout.
|
||||||
var baseTemplates = []string{
|
var baseTemplates = []string{
|
||||||
config.TemplatePath + "/base.html",
|
config.TemplatePath + "/base.html",
|
||||||
|
config.TemplatePath + "/partials/user_avatar.html",
|
||||||
}
|
}
|
||||||
|
|
||||||
// templates returns a template chain with the base templates preceding yours.
|
// templates returns a template chain with the base templates preceding yours.
|
||||||
|
|
BIN
web/static/img/shy-friends.png
Normal file
BIN
web/static/img/shy-friends.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
BIN
web/static/img/shy-private.png
Normal file
BIN
web/static/img/shy-private.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.6 KiB |
|
@ -51,13 +51,7 @@
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="media block">
|
<div class="media block">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image is-64x64">
|
{{template "avatar-64x64" .}}
|
||||||
{{if .ProfilePhoto.ID}}
|
|
||||||
<img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}">
|
|
||||||
{{else}}
|
|
||||||
<img src="/static/img/shy.png">
|
|
||||||
{{end}}
|
|
||||||
</figure>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<p class="title is-4">{{.NameOrUsername}}</p>
|
<p class="title is-4">{{.NameOrUsername}}</p>
|
||||||
|
|
|
@ -184,13 +184,7 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<a href="/u/{{.AboutUser.Username}}">
|
<a href="/u/{{.AboutUser.Username}}">
|
||||||
<figure class="image is-48x48 is-inline-block">
|
{{template "avatar-48x48" .AboutUser}}
|
||||||
{{if .AboutUser.ProfilePhoto.ID}}
|
|
||||||
<img src="{{PhotoURL .AboutUser.ProfilePhoto.CroppedFilename}}">
|
|
||||||
{{else}}
|
|
||||||
<img src="/static/img/shy.png">
|
|
||||||
{{end}}
|
|
||||||
</figure>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
|
@ -355,13 +349,33 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
// Bind to the notification table rows.
|
// Bind to the notification table rows.
|
||||||
(document.querySelectorAll(".nonshy-notification-row") || []).forEach(node => {
|
(document.querySelectorAll(".nonshy-notification-row") || []).forEach(node => {
|
||||||
node.addEventListener("click", (e) => {
|
|
||||||
if (busy) return;
|
|
||||||
|
|
||||||
let $newBadge = node.querySelector(".nonshy-notification-new"),
|
let $newBadge = node.querySelector(".nonshy-notification-new"),
|
||||||
ID = node.dataset.notificationId;
|
ID = node.dataset.notificationId;
|
||||||
|
|
||||||
|
// If the notification doesn't have a "NEW!" badge, no action needed.
|
||||||
|
if ($newBadge === null) return;
|
||||||
|
|
||||||
|
// 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";
|
$newBadge.style.display = "none";
|
||||||
|
|
||||||
|
busy = true;
|
||||||
return fetch("/v1/notifications/read", {
|
return fetch("/v1/notifications/read", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
mode: "same-origin",
|
mode: "same-origin",
|
||||||
|
@ -381,8 +395,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
window.alert(resp);
|
window.alert(resp);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
busy = false;
|
busy = false;
|
||||||
})
|
if (href !== undefined) {
|
||||||
|
window.location.href = href;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -8,9 +8,15 @@
|
||||||
<div class="column is-narrow has-text-centered">
|
<div class="column is-narrow has-text-centered">
|
||||||
<figure class="profile-photo is-inline-block">
|
<figure class="profile-photo is-inline-block">
|
||||||
{{if .User.ProfilePhoto.ID}}
|
{{if .User.ProfilePhoto.ID}}
|
||||||
<img src="/static/photos/{{.User.ProfilePhoto.CroppedFilename}}" data-photo-id="{{.User.ProfilePhoto.ID}}">
|
{{if and (eq .User.ProfilePhoto.Visibility "private") (not .User.UserRelationship.IsPrivateGranted)}}
|
||||||
|
<img src="/static/img/shy-private.png" data-photo-id="{{.User.ProfilePhoto.ID}}">
|
||||||
|
{{else if and (eq .User.ProfilePhoto.Visibility "friends") (not .User.UserRelationship.IsFriend)}}
|
||||||
|
<img src="/static/img/shy-friends.png" data-photo-id="{{.User.ProfilePhoto.ID}}">
|
||||||
{{else}}
|
{{else}}
|
||||||
<img class="is-rounded" src="/static/img/shy.png">
|
<img src="{{PhotoURL .User.ProfilePhoto.CroppedFilename}}" data-photo-id="{{.User.ProfilePhoto.ID}}">
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<img src="/static/img/shy.png">
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<!-- CurrentUser can upload a new profile pic -->
|
<!-- CurrentUser can upload a new profile pic -->
|
||||||
|
|
|
@ -176,15 +176,7 @@
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="media block">
|
<div class="media block">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image is-64x64">
|
{{template "avatar-64x64" .}}
|
||||||
<a href="/u/{{.Username}}" class="has-text-dark">
|
|
||||||
{{if .ProfilePhoto.ID}}
|
|
||||||
<img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}">
|
|
||||||
{{else}}
|
|
||||||
<img src="/static/img/shy.png">
|
|
||||||
{{end}}
|
|
||||||
</a>
|
|
||||||
</figure>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<p class="title is-4">
|
<p class="title is-4">
|
||||||
|
|
|
@ -38,13 +38,7 @@
|
||||||
|
|
||||||
<div class="media block">
|
<div class="media block">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image is-64x64">
|
{{template "avatar-64x64" .}}
|
||||||
{{if .User.ProfilePhoto.ID}}
|
|
||||||
<img src="{{PhotoURL .User.ProfilePhoto.CroppedFilename}}">
|
|
||||||
{{else}}
|
|
||||||
<img src="/static/img/shy.png">
|
|
||||||
{{end}}
|
|
||||||
</figure>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<p class="title is-4">{{.NameOrUsername}}</p>
|
<p class="title is-4">{{.NameOrUsername}}</p>
|
||||||
|
|
|
@ -13,9 +13,48 @@
|
||||||
|
|
||||||
<div class="block p-4">
|
<div class="block p-4">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h1>General FAQs</h1>
|
<!-- Table of Contents -->
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="#certification-faqs">Certification FAQs</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#certification">What does <strong>certification</strong> mean, and what is a <strong>"verification selfie"</strong>?</a></li>
|
||||||
|
<li><a href="#need-certification">Do I <strong>need</strong> to send a "verification selfie"?</a></li>
|
||||||
|
<li><a href="#cannot-certify">Are there <strong>alternative options</strong> to becoming Certified?</a> <strong><span class="tag is-success is-light">NEW Sept. 8 2022</span></strong></li>
|
||||||
|
<li><a href="#private-avatar">Can my <strong>Profile Picture be kept private?</strong></a> <strong><span class="tag is-success is-light">NEW Sept. 8 2022</span></strong></li>
|
||||||
|
<li><a href="#uncertified">What can non-certified members do?</a></li>
|
||||||
|
<li><a href="#profile-visibility">What are the <strong>visibility options</strong> for my profile page?</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#photo-faqs">Photo FAQs</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#nudes-required">Do I have to post my nudes here?</a></li>
|
||||||
|
<li><a href="#face-in-nudes">Do I have to include my face in my nudes?</a></li>
|
||||||
|
<li><a href="#site-gallery">What appears on the Site Gallery?</a></li>
|
||||||
|
<li><a href="#define-explicit">What is considered "explicit" in photos?</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#forum-faqs">Forum FAQs</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#forum-badges">What do the various badges on the forum mean?</a></li>
|
||||||
|
<li><a href="#create-forums">Can I create my own forums?</a></li>
|
||||||
|
|
||||||
<h3>What does certification mean, and what is a "verification selfie"?</h3>
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#technical-faqs">Technical FAQs</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#why">Why did you build a custom website?</a></li>
|
||||||
|
<li><a href="#open-source">Is this website open source?</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h1 id="certification-faqs">Certification FAQs</h1>
|
||||||
|
|
||||||
|
<h3 id="certification">What does certification mean, and what is a "verification selfie"?</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
This website requires all members to be "certified" or proven to be real human beings
|
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.
|
spam robots that plague other similar sites.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3>Do I need to send a "verification selfie"?</h3>
|
<h3 id="need-certification">Do I need to send a "verification selfie"?</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Yes.
|
Yes.
|
||||||
|
@ -42,7 +81,67 @@
|
||||||
until your profile has been certified.
|
until your profile has been certified.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3>What can non-certified members do?</h3>
|
<p>
|
||||||
|
Your certification photo is <em>only</em> seen by site administrators and does not appear
|
||||||
|
on your profile page.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 id="cannot-certify">Are there alternative options to becoming Certified?</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong><span class="tag is-success is-light">NEW Sept. 8 2022</span></strong>
|
||||||
|
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 <em>anywhere at all</em> 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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
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
|
||||||
|
<a href="/messages/compose?to=introvertnudist">u/introvertnudist</a>
|
||||||
|
to inquire about alternative verification methods, which may include just hopping on a
|
||||||
|
quick video call with a site admin.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Note, though: this is a social site for {{PrettyTitle}} nudists so you <strong>are</strong>
|
||||||
|
expected to post at least <em>some</em> 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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 id="private-avatar">Can my Profile Picture be kept private?</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong><span class="tag is-success is-light">NEW Sept. 8 2022</span></strong>
|
||||||
|
You <em>may</em> set your Profile Picture to be "Friends only" or "Private" visibility
|
||||||
|
if you wish to be more discreet about your face pictures.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong class="has-text-warning-dark">Friends only</strong>
|
||||||
|
<i class="fa fa-users has-text-warning-dark"></i>:
|
||||||
|
your profile pic displays as a yellow
|
||||||
|
<img src="/static/img/shy-friends.png" width="16" height="16">
|
||||||
|
placeholder image for people who are not on your <a href="/friends">Friends</a> list.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong class="has-text-private">Private</strong>
|
||||||
|
<i class="fa fa-lock has-text-private"></i>:
|
||||||
|
your profile pic displays as a purple
|
||||||
|
<img src="/static/img/shy-private.png" width="16" height="16">
|
||||||
|
placeholder image for everybody except for people that you had
|
||||||
|
<a href="/photo/private">granted access</a> to see your
|
||||||
|
private photos.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3 id="uncertified">What can non-certified members do?</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Before you have an approved certification photo, you can mainly only access and edit your
|
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.
|
this is intentional to help guard against spam bots and creepy people.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3>What are the visibility options for my profile page?</h3>
|
<h3 id="profile-visibility">What are the visibility options for my profile page?</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
There are currently three different choices for your profile visibility on your
|
There are currently three different choices for your profile visibility on your
|
||||||
|
@ -93,9 +192,9 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h1>Photo FAQs</h1>
|
<h1 id="photo-faqs">Photo FAQs</h1>
|
||||||
|
|
||||||
<h3>Do I have to post my nudes here?</h3>
|
<h3 id="nudes-required">Do I have to post my nudes here?</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
You must be comfortable with doing so, yes. On some other nudist social websites, many
|
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.
|
feel more comfortable if you post some of your own nudes as well.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3>Do I have to include my face in my nudes?</h3>
|
<h3 id="face-in-nudes">Do I have to include my face in my nudes?</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
You don't have to! I know many nudists are not comfortable with their face appearing
|
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!
|
want to see just dick pics everywhere. And don't set those as your default profile pic!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3>What appears on the Site Gallery?</h3>
|
<h3 id="site-gallery">What appears on the Site Gallery?</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
The "<strong><i class="fa fa-image"></i> Gallery</strong>" link on the site nav bar goes to the Site-wide
|
The "<strong><i class="fa fa-image"></i> Gallery</strong>" 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.
|
the Gallery -- it will then only appear on your profile page.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3>What is considered "explicit" in photos?</h3>
|
<h3 id="define-explicit">What is considered "explicit" in photos?</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
On this website, I make a fairly common distinction between what's a "normal nude" and
|
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!
|
content from other users -- by default this site is "normal nudes" friendly!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3>Does this site prevent people from downloading my pictures?</h3>
|
<h3 id="downloading">Does this site prevent people from downloading my pictures?</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
This website does not go out of its way to prevent people from downloading pictures, and
|
This website does not go out of its way to prevent people from downloading pictures, and
|
||||||
|
@ -214,16 +313,9 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Additionally, your Profile Page altogether can <em>only</em> be seen by logged-in members
|
Additionally, your Profile Page altogether can <em>only</em> be seen by logged-in members
|
||||||
of this site by default. You <em>may</em> tighten it further and mark your entire profile
|
of this site by default. You <em>may</em> tighten it even further and mark your entire profile
|
||||||
as Private (and only approved friends can see your profile or <em>any</em> of your photos).
|
as Private (and only approved friends can see your profile or <em>any</em> of your photos).
|
||||||
Note that your square cropped profile picture is still visible even so.
|
Note that your square cropped profile picture is still visible even so.
|
||||||
</p>
|
</p>
|
||||||
|
@ -237,7 +329,7 @@
|
||||||
|
|
||||||
<h1>Forum FAQs</h1>
|
<h1>Forum FAQs</h1>
|
||||||
|
|
||||||
<h3>What do the various badges on the forum mean?</h3>
|
<h3 id="forum-badges">What do the various badges on the forum mean?</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
You may see some of these badges on the forums or their posts. These are their meanings:
|
You may see some of these badges on the forums or their posts. These are their meanings:
|
||||||
|
@ -282,7 +374,7 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3>Can I create my own forums?</h3>
|
<h3 id="create-forums">Can I create my own forums?</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
This feature is coming soon! Users will be allowed to create their own forums and
|
This feature is coming soon! Users will be allowed to create their own forums and
|
||||||
|
@ -306,9 +398,9 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h1>Technical FAQs</h1>
|
<h1 id="technical-faqs">Technical FAQs</h1>
|
||||||
|
|
||||||
<h3>Why did you build a custom website?</h3>
|
<h3 id="why">Why did you build a custom website?</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Other variants on this question might be: why not just run a
|
Other variants on this question might be: why not just run a
|
||||||
|
@ -338,7 +430,7 @@
|
||||||
fate is kept in my own hands.
|
fate is kept in my own hands.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3>Is this website open source?</h3>
|
<h3 id="open-source">Is this website open source?</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Yes! The source code for this website is released as free software under the GNU
|
Yes! The source code for this website is released as free software under the GNU
|
||||||
|
|
|
@ -82,13 +82,7 @@
|
||||||
<div class="column is-2 has-text-centered pt-0 pb-1">
|
<div class="column is-2 has-text-centered pt-0 pb-1">
|
||||||
<div>
|
<div>
|
||||||
<a href="/u/{{.Comment.User.Username}}">
|
<a href="/u/{{.Comment.User.Username}}">
|
||||||
<figure class="image is-64x64 is-inline-block">
|
{{template "avatar-64x64" .Comment.User}}
|
||||||
{{if .Comment.User.ProfilePhoto.ID}}
|
|
||||||
<img src="{{PhotoURL .Comment.User.ProfilePhoto.CroppedFilename}}">
|
|
||||||
{{else}}
|
|
||||||
<img src="/static/img/shy.png">
|
|
||||||
{{end}}
|
|
||||||
</figure>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<a href="/u/{{.Comment.User.Username}}">{{.Comment.User.Username}}</a>
|
<a href="/u/{{.Comment.User.Username}}">{{.Comment.User.Username}}</a>
|
||||||
|
|
|
@ -66,13 +66,7 @@
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-narrow has-text-centered pt-0 pb-1">
|
<div class="column is-narrow has-text-centered pt-0 pb-1">
|
||||||
<a href="/u/{{$User.Username}}">
|
<a href="/u/{{$User.Username}}">
|
||||||
<figure class="image is-96x96 is-inline-block">
|
{{template "avatar-96x96" $User}}
|
||||||
{{if $User.ProfilePhoto.ID}}
|
|
||||||
<img src="{{PhotoURL $User.ProfilePhoto.CroppedFilename}}">
|
|
||||||
{{else}}
|
|
||||||
<img src="/static/img/shy.png">
|
|
||||||
{{end}}
|
|
||||||
</figure>
|
|
||||||
<div>
|
<div>
|
||||||
<a href="/u/{{$User.Username}}" class="is-size-7">{{$User.Username}}</a>
|
<a href="/u/{{$User.Username}}" class="is-size-7">{{$User.Username}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -136,13 +130,7 @@
|
||||||
<div class="columns is-gapless is-mobile">
|
<div class="columns is-gapless is-mobile">
|
||||||
<div class="column is-narrow mx-2">
|
<div class="column is-narrow mx-2">
|
||||||
<a href="/u/{{.Comment.User.Username}}">
|
<a href="/u/{{.Comment.User.Username}}">
|
||||||
<figure class="image is-32x32 is-inline-block">
|
{{template "avatar-32x32" .Comment.User}}
|
||||||
{{if .Comment.User.ProfilePhoto.ID}}
|
|
||||||
<img src="{{PhotoURL .Comment.User.ProfilePhoto.CroppedFilename}}" class="is-rounded">
|
|
||||||
{{else}}
|
|
||||||
<img src="/static/photos/shy.png" class="is-rounded">
|
|
||||||
{{end}}
|
|
||||||
</figure>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
|
|
|
@ -125,13 +125,7 @@
|
||||||
<div class="column is-2 has-text-centered">
|
<div class="column is-2 has-text-centered">
|
||||||
<div>
|
<div>
|
||||||
<a href="/u/{{.User.Username}}">
|
<a href="/u/{{.User.Username}}">
|
||||||
<figure class="image is-96x96 is-inline-block">
|
{{template "avatar-96x96" .User}}
|
||||||
{{if .User.ProfilePhoto.ID}}
|
|
||||||
<img src="{{PhotoURL .User.ProfilePhoto.CroppedFilename}}">
|
|
||||||
{{else}}
|
|
||||||
<img src="/static/img/shy.png">
|
|
||||||
{{end}}
|
|
||||||
</figure>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<a href="/u/{{.User.Username}}">{{.User.Username}}</a>
|
<a href="/u/{{.User.Username}}">{{.User.Username}}</a>
|
||||||
|
|
|
@ -81,15 +81,7 @@
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="media block">
|
<div class="media block">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image is-64x64">
|
{{template "avatar-64x64" .}}
|
||||||
<a href="/u/{{.Username}}">
|
|
||||||
{{if .ProfilePhoto.ID}}
|
|
||||||
<img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}">
|
|
||||||
{{else}}
|
|
||||||
<img src="/static/img/shy.png">
|
|
||||||
{{end}}
|
|
||||||
</a>
|
|
||||||
</figure>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<p class="title is-4">
|
<p class="title is-4">
|
||||||
|
|
|
@ -26,13 +26,7 @@
|
||||||
|
|
||||||
<div class="media block">
|
<div class="media block">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image is-64x64">
|
{{template "avatar-64x64" .}}
|
||||||
{{if .User.ProfilePhoto.ID}}
|
|
||||||
<img src="{{PhotoURL .User.ProfilePhoto.CroppedFilename}}">
|
|
||||||
{{else}}
|
|
||||||
<img src="/static/img/shy.png">
|
|
||||||
{{end}}
|
|
||||||
</figure>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<p class="title is-4">{{.NameOrUsername}}</p>
|
<p class="title is-4">{{.NameOrUsername}}</p>
|
||||||
|
|
|
@ -55,13 +55,7 @@
|
||||||
<div class="media block">
|
<div class="media block">
|
||||||
{{$SourceUser := $UserMap.Get .SourceUserID}}
|
{{$SourceUser := $UserMap.Get .SourceUserID}}
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image is-64x64">
|
{{template "avatar-64x64" $SourceUser}}
|
||||||
{{if $SourceUser.ProfilePhoto.ID}}
|
|
||||||
<img src="{{PhotoURL $SourceUser.ProfilePhoto.CroppedFilename}}">
|
|
||||||
{{else}}
|
|
||||||
<img src="/static/img/shy.png">
|
|
||||||
{{end}}
|
|
||||||
</figure>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<p class="title is-4">{{$SourceUser.NameOrUsername}}</p>
|
<p class="title is-4">{{$SourceUser.NameOrUsername}}</p>
|
||||||
|
|
77
web/templates/partials/user_avatar.html
Normal file
77
web/templates/partials/user_avatar.html
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<!-- User avatar widgets -->
|
||||||
|
|
||||||
|
<!-- Parameter: .User -->
|
||||||
|
{{define "avatar-48x48"}}
|
||||||
|
<figure class="image is-48x48 is-inline-block">
|
||||||
|
<a href="/u/{{.Username}}" class="has-text-dark">
|
||||||
|
{{if .ProfilePhoto.ID}}
|
||||||
|
{{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted)}}
|
||||||
|
<img src="/static/img/shy-private.png">
|
||||||
|
{{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend)}}
|
||||||
|
<img src="/static/img/shy-friends.png">
|
||||||
|
{{else}}
|
||||||
|
<img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}">
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<img src="/static/img/shy.png">
|
||||||
|
{{end}}
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- Parameter: .User -->
|
||||||
|
{{define "avatar-64x64"}}
|
||||||
|
<figure class="image is-64x64 is-inline-block">
|
||||||
|
<a href="/u/{{.Username}}" class="has-text-dark">
|
||||||
|
{{if .ProfilePhoto.ID}}
|
||||||
|
{{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted)}}
|
||||||
|
<img src="/static/img/shy-private.png">
|
||||||
|
{{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend)}}
|
||||||
|
<img src="/static/img/shy-friends.png">
|
||||||
|
{{else}}
|
||||||
|
<img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}">
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<img src="/static/img/shy.png">
|
||||||
|
{{end}}
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- Parameter: .User -->
|
||||||
|
{{define "avatar-96x96"}}
|
||||||
|
<figure class="image is-96x96 is-inline-block">
|
||||||
|
<a href="/u/{{.Username}}" class="has-text-dark">
|
||||||
|
{{if .ProfilePhoto.ID}}
|
||||||
|
{{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted)}}
|
||||||
|
<img src="/static/img/shy-private.png">
|
||||||
|
{{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend)}}
|
||||||
|
<img src="/static/img/shy-friends.png">
|
||||||
|
{{else}}
|
||||||
|
<img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}">
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<img src="/static/img/shy.png">
|
||||||
|
{{end}}
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- Parameter: .User -->
|
||||||
|
{{define "avatar-32x32"}}
|
||||||
|
<figure class="image is-32x32 is-inline-block">
|
||||||
|
<a href="/u/{{.Username}}" class="has-text-dark">
|
||||||
|
{{if .ProfilePhoto.ID}}
|
||||||
|
{{if and (eq .ProfilePhoto.Visibility "private") (not .UserRelationship.IsPrivateGranted)}}
|
||||||
|
<img class="is-rounded" src="/static/img/shy-private.png">
|
||||||
|
{{else if and (eq .ProfilePhoto.Visibility "friends") (not .UserRelationship.IsFriend)}}
|
||||||
|
<img class="is-rounded" src="/static/img/shy-friends.png">
|
||||||
|
{{else}}
|
||||||
|
<img class="is-rounded" src="{{PhotoURL .ProfilePhoto.CroppedFilename}}">
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<img class="is-rounded" src="/static/img/shy.png">
|
||||||
|
{{end}}
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
|
{{end}}
|
|
@ -102,15 +102,7 @@
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="media block">
|
<div class="media block">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image is-64x64">
|
{{template "avatar-64x64" .}}
|
||||||
<a href="/u/{{.Username}}">
|
|
||||||
{{if .ProfilePhoto.ID}}
|
|
||||||
<img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}">
|
|
||||||
{{else}}
|
|
||||||
<img src="/static/img/shy.png">
|
|
||||||
{{end}}
|
|
||||||
</a>
|
|
||||||
</figure>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<p class="title is-4">
|
<p class="title is-4">
|
||||||
|
|
|
@ -47,16 +47,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="media block">
|
<div class="media block">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image is-64x64">
|
{{template "avatar-64x64" .User}}
|
||||||
{{if .User.ProfilePhoto.ID}}
|
|
||||||
<img src="{{PhotoURL .User.ProfilePhoto.CroppedFilename}}">
|
|
||||||
{{else}}
|
|
||||||
<img src="/static/img/shy.png">
|
|
||||||
{{end}}
|
|
||||||
</figure>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<p class="title is-4">{{.NameOrUsername}}</p>
|
<p class="title is-4">{{.User.NameOrUsername}}</p>
|
||||||
<p class="subtitle is-6">
|
<p class="subtitle is-6">
|
||||||
<span class="icon"><i class="fa fa-user"></i></span>
|
<span class="icon"><i class="fa fa-user"></i></span>
|
||||||
<a href="/u/{{.User.Username}}" target="_blank">{{.User.Username}}</a>
|
<a href="/u/{{.User.Username}}" target="_blank">{{.User.Username}}</a>
|
||||||
|
|
|
@ -262,14 +262,6 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="notification is-warning is-light p-2 is-size-7">
|
|
||||||
<i class="fa fa-warning"></i> <strong>Notice:</strong> 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 <em>can</em> limit the full-size photo's visibility; but the square cropped
|
|
||||||
thumbnail is always seen to logged-in members.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field mb-5">
|
<div class="field mb-5">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span>Site Photo Gallery</span>
|
<span>Site Photo Gallery</span>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user