The inner circle

This commit is contained in:
Noah Petherbridge 2023-05-23 20:04:17 -07:00
parent 17d9760b61
commit 9788ea6a33
33 changed files with 743 additions and 14 deletions

View File

@ -90,6 +90,7 @@ var (
// Default forum categories for forum landing page. // Default forum categories for forum landing page.
ForumCategories = []string{ ForumCategories = []string{
"Rules and Announcements", "Rules and Announcements",
"The Inner Circle",
"Nudists", "Nudists",
"Exhibitionists", "Exhibitionists",
"Photo Boards", "Photo Boards",

View File

@ -0,0 +1,137 @@
package account
import (
"net/http"
"code.nonshy.com/nonshy/website/pkg/models"
"code.nonshy.com/nonshy/website/pkg/session"
"code.nonshy.com/nonshy/website/pkg/templates"
)
// InnerCircle is the landing page for inner circle members only.
func InnerCircle() http.HandlerFunc {
tmpl := templates.Must("account/inner_circle.html")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
currentUser, err := session.CurrentUser(r)
if err != nil || !currentUser.IsInnerCircle() {
templates.NotFoundPage(w, r)
return
}
if err := tmpl.Execute(w, r, nil); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
}
// InviteCircle is the landing page to invite a user into the circle.
func InviteCircle() http.HandlerFunc {
tmpl := templates.Must("account/invite_circle.html")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
currentUser, err := session.CurrentUser(r)
if err != nil || !currentUser.IsInnerCircle() {
templates.NotFoundPage(w, r)
return
}
// Invite whom?
username := r.FormValue("to")
user, err := models.FindUser(username)
if err != nil {
templates.NotFoundPage(w, r)
return
}
if currentUser.ID == user.ID && currentUser.InnerCircle {
session.FlashError(w, r, "You are already part of the inner circle.")
templates.Redirect(w, "/inner-circle")
return
}
// Any blocking?
if models.IsBlocking(currentUser.ID, user.ID) && !currentUser.IsAdmin {
session.FlashError(w, r, "You are blocked from inviting this user to the circle.")
templates.Redirect(w, "/inner-circle")
return
}
// POSTing?
if r.Method == http.MethodPost {
var (
confirm = r.FormValue("intent") == "confirm"
)
if !confirm {
templates.Redirect(w, "/u/"+username)
return
}
// Add them!
if err := models.AddToInnerCircle(user); err != nil {
session.FlashError(w, r, "Couldn't add to the inner circle: %s", err)
}
session.Flash(w, r, "%s has been added to the inner circle!", user.Username)
templates.Redirect(w, "/photo/u/"+user.Username)
return
}
var vars = map[string]interface{}{
"User": user,
}
if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
}
// RemoveCircle is the admin-only page to remove a member from the circle.
func RemoveCircle() http.HandlerFunc {
tmpl := templates.Must("account/remove_circle.html")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
currentUser, err := session.CurrentUser(r)
if err != nil || !currentUser.IsInnerCircle() {
templates.NotFoundPage(w, r)
return
}
// Remove whom?
username := r.FormValue("to")
user, err := models.FindUser(username)
if err != nil {
templates.NotFoundPage(w, r)
return
}
// POSTing?
if r.Method == http.MethodPost {
var (
confirm = r.FormValue("intent") == "confirm"
)
if !confirm {
templates.Redirect(w, "/u/"+username)
return
}
// Add them!
if err := models.RemoveFromInnerCircle(user); err != nil {
session.FlashError(w, r, "Couldn't remove from the inner circle: %s", err)
}
session.Flash(w, r, "%s has been removed from the inner circle!", user.Username)
templates.Redirect(w, "/u/"+user.Username)
return
}
var vars = map[string]interface{}{
"User": user,
}
if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
}

View File

@ -77,7 +77,8 @@ func Search() http.HandlerFunc {
Gender: gender, Gender: gender,
Orientation: orientation, Orientation: orientation,
MaritalStatus: maritalStatus, MaritalStatus: maritalStatus,
Certified: isCertified == "true", Certified: isCertified != "false",
InnerCircle: isCertified == "circle",
AgeMin: ageMin, AgeMin: ageMin,
AgeMax: ageMax, AgeMax: ageMax,
}, pager) }, pager)

View File

@ -63,6 +63,7 @@ func AddEdit() http.HandlerFunc {
isExplicit = r.PostFormValue("explicit") == "true" isExplicit = r.PostFormValue("explicit") == "true"
isPrivileged = r.PostFormValue("privileged") == "true" isPrivileged = r.PostFormValue("privileged") == "true"
isPermitPhotos = r.PostFormValue("permit_photos") == "true" isPermitPhotos = r.PostFormValue("permit_photos") == "true"
isInnerCircle = r.PostFormValue("inner_circle") == "true"
) )
// Sanity check admin-only settings. // Sanity check admin-only settings.
@ -79,6 +80,7 @@ func AddEdit() http.HandlerFunc {
forum.Explicit = isExplicit forum.Explicit = isExplicit
forum.Privileged = isPrivileged forum.Privileged = isPrivileged
forum.PermitPhotos = isPermitPhotos forum.PermitPhotos = isPermitPhotos
forum.InnerCircle = isInnerCircle
// Save it. // Save it.
if err := forum.Save(); err == nil { if err := forum.Save(); err == nil {
@ -111,6 +113,7 @@ func AddEdit() http.HandlerFunc {
Explicit: isExplicit, Explicit: isExplicit,
Privileged: isPrivileged, Privileged: isPrivileged,
PermitPhotos: isPermitPhotos, PermitPhotos: isPermitPhotos,
InnerCircle: isInnerCircle,
} }
if err := models.CreateForum(forum); err == nil { if err := models.CreateForum(forum); err == nil {

View File

@ -41,6 +41,12 @@ func Forum() http.HandlerFunc {
return return
} }
// Is it an inner circle forum?
if forum.InnerCircle && !currentUser.IsInnerCircle() {
templates.NotFoundPage(w, r)
return
}
// Get the pinned threads. // Get the pinned threads.
pinned, err := models.PinnedThreads(forum) pinned, err := models.PinnedThreads(forum)
if err != nil { if err != nil {

View File

@ -54,6 +54,12 @@ func Thread() http.HandlerFunc {
return return
} }
// Is it an inner circle forum?
if forum.InnerCircle && !currentUser.IsInnerCircle() {
templates.NotFoundPage(w, r)
return
}
// Ping the view count on this thread. // Ping the view count on this thread.
if err := thread.View(currentUser.ID); err != nil { if err := thread.View(currentUser.ID); err != nil {
log.Error("Couldn't ping view count on thread %d: %s", thread.ID, err) log.Error("Couldn't ping view count on thread %d: %s", thread.ID, err)

View File

@ -156,6 +156,9 @@ func notifyFriendsNewPhoto(photo *models.Photo, currentUser *models.User) {
// Private grantees // Private grantees
friendIDs = models.PrivateGranteeUserIDs(currentUser.ID) friendIDs = models.PrivateGranteeUserIDs(currentUser.ID)
log.Info("Notify %d private grantees about the new photo by %s", len(friendIDs), currentUser.Username) log.Info("Notify %d private grantees about the new photo by %s", len(friendIDs), currentUser.Username)
} else if photo.Visibility == models.PhotoInnerCircle {
friendIDs = models.FriendIDsInCircle(currentUser.ID)
log.Info("Notify %d circle friends about the new photo by %s", len(friendIDs), currentUser.Username)
} else { } else {
// Get all our friend IDs. If this photo is Explicit, only select // Get all our friend IDs. If this photo is Explicit, only select
// the friends who've opted-in for Explicit photo visibility. // the friends who've opted-in for Explicit photo visibility.

View File

@ -101,6 +101,11 @@ func UserPhotos() http.HandlerFunc {
visibility = append(visibility, models.PhotoFriends) visibility = append(visibility, models.PhotoFriends)
} }
// Inner circle photos.
if currentUser.IsInnerCircle() {
visibility = append(visibility, models.PhotoInnerCircle)
}
// Explicit photo filter? // Explicit photo filter?
explicit := currentUser.Explicit explicit := currentUser.Explicit
if isOwnPhotos { if isOwnPhotos {

View File

@ -20,6 +20,7 @@ type Forum struct {
Explicit bool `gorm:"index"` Explicit bool `gorm:"index"`
Privileged bool Privileged bool
PermitPhotos bool PermitPhotos bool
InnerCircle bool
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
} }
@ -95,6 +96,11 @@ func PaginateForums(user *User, categories []string, pager *Pagination) ([]*Foru
wheres = append(wheres, "explicit = false") wheres = append(wheres, "explicit = false")
} }
// Hide circle forums if the user isn't in the circle.
if !user.IsInnerCircle() {
wheres = append(wheres, "inner_circle is not true")
}
// Filters? // Filters?
if len(wheres) > 0 { if len(wheres) > 0 {
query = query.Where( query = query.Where(
@ -172,5 +178,14 @@ func CategorizeForums(fs []*Forum, categories []string) []*CategorizedForum {
result[idx].Forums = append(result[idx].Forums, forum) result[idx].Forums = append(result[idx].Forums, forum)
} }
return result // Remove any blank categories with no boards.
var filtered = []*CategorizedForum{}
for _, forum := range result {
if len(forum.Forums) == 0 {
continue
}
filtered = append(filtered, forum)
}
return filtered
} }

View File

@ -131,6 +131,30 @@ func FriendIDsAreExplicit(userId uint64) []uint64 {
return userIDs return userIDs
} }
// FriendIDsInCircle returns friend IDs who are part of the inner circle.
func FriendIDsInCircle(userId uint64) []uint64 {
var (
userIDs = []uint64{}
)
err := DB.Table(
"friends",
).Joins(
"JOIN users ON (users.id = friends.target_user_id)",
).Select(
"friends.target_user_id AS friend_id",
).Where(
"friends.source_user_id = ? AND friends.approved = ? AND (users.inner_circle = ? OR users.is_admin = ?)",
userId, true, true, true,
).Scan(&userIDs)
if err.Error != nil {
log.Error("SQL error collecting circle FriendIDs for %d: %s", userId, err)
}
return userIDs
}
// CountFriendRequests gets a count of pending requests for the user. // CountFriendRequests gets a count of pending requests for the user.
func CountFriendRequests(userID uint64) (int64, error) { func CountFriendRequests(userID uint64) (int64, error) {
var count int64 var count int64

View File

@ -40,6 +40,7 @@ const (
NotificationCertApproved = "cert_approved" NotificationCertApproved = "cert_approved"
NotificationPrivatePhoto = "private_photo" NotificationPrivatePhoto = "private_photo"
NotificationNewPhoto = "new_photo" NotificationNewPhoto = "new_photo"
NotificationInnerCircle = "inner_circle"
NotificationCustom = "custom" // custom message pushed NotificationCustom = "custom" // custom message pushed
) )

View File

@ -32,6 +32,7 @@ const (
PhotoPublic PhotoVisibility = "public" // on profile page and/or public gallery PhotoPublic PhotoVisibility = "public" // on profile page and/or public gallery
PhotoFriends = "friends" // only friends can see it PhotoFriends = "friends" // only friends can see it
PhotoPrivate = "private" // private PhotoPrivate = "private" // private
PhotoInnerCircle = "circle" // inner circle
) )
// PhotoVisibility preset settings. // PhotoVisibility preset settings.
@ -42,6 +43,14 @@ var (
PhotoPrivate, PhotoPrivate,
} }
// "All" but also for Inner Circle members.
PhotoVisibilityCircle = []PhotoVisibility{
PhotoPublic,
PhotoFriends,
PhotoPrivate,
PhotoInnerCircle,
}
// Site Gallery visibility for when your friends show up in the gallery. // Site Gallery visibility for when your friends show up in the gallery.
// Or: "Friends + Gallery" photos can appear to your friends in the Site Gallery. // Or: "Friends + Gallery" photos can appear to your friends in the Site Gallery.
PhotoVisibilityFriends = []string{ PhotoVisibilityFriends = []string{
@ -213,6 +222,12 @@ func PaginateGalleryPhotos(user *User, conf Gallery, pager *Pagination) ([]*Phot
placeholders = []interface{}{} placeholders = []interface{}{}
) )
// Define "all photos visibilities"
var photosAll = PhotoVisibilityAll
if user.IsInnerCircle() {
photosAll = PhotoVisibilityCircle
}
// Admins see everything on the site (only an admin user can get an admin view). // Admins see everything on the site (only an admin user can get an admin view).
adminView = user.IsAdmin && adminView adminView = user.IsAdmin && adminView
@ -229,7 +244,7 @@ func PaginateGalleryPhotos(user *User, conf Gallery, pager *Pagination) ([]*Phot
) )
placeholders = append(placeholders, placeholders = append(placeholders,
friendIDs, PhotoVisibilityFriends, friendIDs, PhotoVisibilityFriends,
privateUserIDs, PhotoVisibilityAll, privateUserIDs, photosAll,
) )
} else { } else {
// You can see friends' Friend photos but only public for non-friends. // You can see friends' Friend photos but only public for non-friends.
@ -240,7 +255,7 @@ func PaginateGalleryPhotos(user *User, conf Gallery, pager *Pagination) ([]*Phot
) )
placeholders = append(placeholders, placeholders = append(placeholders,
friendIDs, PhotoVisibilityFriends, friendIDs, PhotoVisibilityFriends,
privateUserIDs, PhotoVisibilityAll, privateUserIDs, photosAll,
friendIDs, PhotoPublic, friendIDs, PhotoPublic,
) )
} }

View File

@ -25,7 +25,8 @@ type User struct {
Name *string Name *string
Birthdate time.Time Birthdate time.Time
Certified bool Certified bool
Explicit bool // user has opted-in to see explicit content Explicit bool `gorm:"index"` // user has opted-in to see explicit content
InnerCircle bool `gorm:"index"` // user is in the inner circle
CreatedAt time.Time `gorm:"index"` CreatedAt time.Time `gorm:"index"`
UpdatedAt time.Time `gorm:"index"` UpdatedAt time.Time `gorm:"index"`
LastLoginAt time.Time `gorm:"index"` LastLoginAt time.Time `gorm:"index"`
@ -185,6 +186,7 @@ type UserSearch struct {
Orientation string Orientation string
MaritalStatus string MaritalStatus string
Certified bool Certified bool
InnerCircle bool
AgeMin int AgeMin int
AgeMax int AgeMax int
} }
@ -249,6 +251,11 @@ func SearchUsers(user *User, search *UserSearch, pager *Pagination) ([]*User, er
placeholders = append(placeholders, search.Certified, UserStatusActive) placeholders = append(placeholders, search.Certified, UserStatusActive)
} }
if search.InnerCircle {
wheres = append(wheres, "inner_circle = ? OR is_admin = ?")
placeholders = append(placeholders, true, true)
}
if search.AgeMin > 0 { if search.AgeMin > 0 {
date := time.Now().AddDate(-search.AgeMin, 0, 0) date := time.Now().AddDate(-search.AgeMin, 0, 0)
wheres = append(wheres, "birthdate <= ?") wheres = append(wheres, "birthdate <= ?")

View File

@ -0,0 +1,67 @@
package models
import (
"errors"
"code.nonshy.com/nonshy/website/pkg/log"
)
// Helper functions relating to the inner circle.
// IsInnerCircle returns whether the user is in the inner circle (including if the user is an admin, who is always in the inner circle).
func (u *User) IsInnerCircle() bool {
return u.InnerCircle || u.IsAdmin
}
// AddToInnerCircle adds a user to the circle, sending them a notification in the process.
func AddToInnerCircle(u *User) error {
if u.InnerCircle {
return errors.New("already a part of the inner circle")
}
u.InnerCircle = true
if err := u.Save(); err != nil {
return err
}
// Send them a notification.
notif := &Notification{
UserID: u.ID,
AboutUserID: &u.ID,
Type: NotificationInnerCircle,
Link: "/inner-circle",
TableName: "__inner_circle",
TableID: u.ID,
}
if err := CreateNotification(notif); err != nil {
log.Error("AddToInnerCircle: couldn't create notification: %s", err)
}
return nil
}
// RemoveFromInnerCircle kicks a user from the inner circle. Any photo they
// had that was marked circle-only is updated to public.
func RemoveFromInnerCircle(u *User) error {
if !u.InnerCircle {
return errors.New("is not a part of the inner circle")
}
u.InnerCircle = false
if err := u.Save(); err != nil {
return err
}
// Update their circle-only photos to public.
if err := DB.Model(&Photo{}).Where(
"user_id = ? AND visibility = ?",
u.ID, PhotoInnerCircle,
).Update("visibility", PhotoPublic); err != nil {
log.Error("RemoveFromInnerCircle: couldn't update photo visibility: %s", err)
}
// Revoke any historic notification about the circle.
RemoveNotification("__inner_circle", u.ID)
return nil
}

View File

@ -62,6 +62,8 @@ func New() http.Handler {
mux.Handle("/comments", middleware.LoginRequired(comment.PostComment())) mux.Handle("/comments", middleware.LoginRequired(comment.PostComment()))
mux.Handle("/comments/subscription", middleware.LoginRequired(comment.Subscription())) mux.Handle("/comments/subscription", middleware.LoginRequired(comment.Subscription()))
mux.Handle("/admin/unimpersonate", middleware.LoginRequired(admin.Unimpersonate())) mux.Handle("/admin/unimpersonate", middleware.LoginRequired(admin.Unimpersonate()))
mux.Handle("/inner-circle", middleware.LoginRequired(account.InnerCircle()))
mux.Handle("/inner-circle/invite", middleware.LoginRequired(account.InviteCircle()))
// Certification Required. Pages that only full (verified) members can access. // Certification Required. Pages that only full (verified) members can access.
mux.Handle("/photo/gallery", middleware.CertRequired(photo.SiteGallery())) mux.Handle("/photo/gallery", middleware.CertRequired(photo.SiteGallery()))
@ -81,6 +83,7 @@ func New() http.Handler {
mux.Handle("/admin/user-action", middleware.AdminRequired(admin.UserActions())) mux.Handle("/admin/user-action", middleware.AdminRequired(admin.UserActions()))
mux.Handle("/forum/admin", middleware.AdminRequired(forum.Manage())) mux.Handle("/forum/admin", middleware.AdminRequired(forum.Manage()))
mux.Handle("/forum/admin/edit", middleware.AdminRequired(forum.AddEdit())) mux.Handle("/forum/admin/edit", middleware.AdminRequired(forum.AddEdit()))
mux.Handle("/inner-circle/remove", middleware.AdminRequired(account.RemoveCircle()))
// JSON API endpoints. // JSON API endpoints.
mux.HandleFunc("/v1/version", api.Version()) mux.HandleFunc("/v1/version", api.Version())

View File

@ -39,10 +39,14 @@ func TemplateFuncs(r *http.Request) template.FuncMap {
)) ))
}, },
"PrettyTitleShort": func() template.HTML { "PrettyTitleShort": func() template.HTML {
return template.HTML(fmt.Sprintf( return template.HTML(`<strong style="color: #0077FF">n</strong>` +
`<strong style="color: #0077FF">n</strong>` +
`<strong style="color: #FF77FF">s</strong>`, `<strong style="color: #FF77FF">s</strong>`,
)) )
},
"PrettyCircle": func() template.HTML {
return template.HTML(
`<span style="color: #0077ff">I</span><span style="color: #1c77ff">n</span><span style="color: #3877ff">n</span><span style="color: #5477ff">e</span><span style="color: #7077ff">r</span><span style="color: #8c77ff"> </span><span style="color: #aa77ff">c</span><span style="color: #b877ff">i</span><span style="color: #c677ff">r</span><span style="color: #d477ff">c</span><span style="color: #e277ff">l</span><span style="color: #f077ff">e</span>`,
)
}, },
"Pluralize": Pluralize[int], "Pluralize": Pluralize[int],
"Pluralize64": Pluralize[int64], "Pluralize64": Pluralize[int64],

View File

@ -55,6 +55,10 @@ abbr {
background-image: linear-gradient(141deg, #b329b1 0, #9948c7 71%, #7156d2 100%); background-image: linear-gradient(141deg, #b329b1 0, #9948c7 71%, #7156d2 100%);
} }
.hero.is-inner-circle {
background-image: linear-gradient(141deg, #294eb3 0, #9948c7 71%, #d256d2 100%)
}
/* Mobile: notification badge near the hamburger menu */ /* Mobile: notification badge near the hamburger menu */
.nonshy-mobile-notification { .nonshy-mobile-notification {
position: absolute; position: absolute;

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -326,6 +326,8 @@
<span class="icon"> <span class="icon">
{{if and $Body.Photo (eq $Body.Photo.Visibility "private")}} {{if and $Body.Photo (eq $Body.Photo.Visibility "private")}}
<i class="fa fa-eye has-text-private"></i> <i class="fa fa-eye has-text-private"></i>
{{else if and $Body.Photo (eq $Body.Photo.Visibility "circle")}}
<img src="/static/img/circle-16.png">
{{else}} {{else}}
<i class="fa fa-image has-text-link"></i> <i class="fa fa-image has-text-link"></i>
{{end}} {{end}}
@ -349,6 +351,15 @@
<span> <span>
Your <strong>certification photo</strong> was rejected! Your <strong>certification photo</strong> was rejected!
</span> </span>
{{else if eq .Type "inner_circle"}}
<span class="icon"><img src="/static/img/circle-16.png"></span>
<span>
You have been added to the {{PrettyCircle}} of nonshy.
</span>
<div class="block content mt-2">
<a href="/inner-circle">Click to learn more</a> about the inner circle.
</div>
{{else}} {{else}}
{{.AboutUser.Username}} {{.Type}} {{.TableName}} {{.TableID}} {{.AboutUser.Username}} {{.Type}} {{.TableName}} {{.TableID}}
{{end}} {{end}}

View File

@ -0,0 +1,118 @@
{{define "title"}}The inner circle{{end}}
{{define "content"}}
<div class="block">
<section class="hero is-inner-circle is-bold">
<div class="hero-body">
<div class="container">
<h1 class="title">
<img src="/static/img/circle-24.png" class="mr-1">
The inner circle
</h1>
</div>
</div>
</section>
</div>
<div class="block p-4">
<div class="content">
<p>
Congratulations! You have been added to the <strong>{{PrettyCircle}}</strong> because you
exemplify what it truly means to be a {{PrettyTitle}} nudist.
</p>
<h2>What is the inner circle?</h2>
<p>
The inner circle is for {{PrettyTitle}} members who <em>actually</em> share a lot of nude pictures,
<strong>with face</strong>, of themselves on their profile page. It is "the party inside the party"
designed only for members who truly embrace the spirit of the {{PrettyTitle}} website by boldly
sharing nude pics with face for other nonshy nudists to see.
</p>
<h2>What can I do for being in the inner circle?</h2>
<p>
As a part of the inner circle, you have access to the following new features:
</p>
<ul>
<li>
When
<a href="/photo/upload"><strong><i class="fa fa-upload mr-1"></i> Uploading a photo</strong></a> you
have a new Visibility option for "<strong>{{PrettyCircle}}</strong> <img src="/static/img/circle-16.png">"
so that only members of the inner circle can see those pictures.
</li>
<li>
On the
<a href="/photo/gallery"><strong><i class="fa fa-image mr-1"></i> Site Gallery</strong></a>
you can filter for <a href="/photo/gallery?visibility=circle">Inner Circle-only photos</a> shared
by other members of the circle.
</li>
<li>
On the
<a href="/members"><strong><i class="fa fa-people-group mr-1"></i> Member Directory</strong></a>
you can see who else is <a href="/members?certified=circle">in the inner circle.</a>
</li>
<li>
On the
<a href="/members"><strong><i class="fa fa-comments mr-1"></i> Forums</strong></a>
you can access exclusive inner circle-only boards.
</li>
<li>
On your <a href="/u/{{.CurrentUser.Username}}">profile page</a> you get an "Inner circle" badge near your
Certified status. This badge is <strong>only</strong> visible to members of the inner circle.
</li>
<li>
You may <strong>invite</strong> other members to join the inner circle.
</li>
</ul>
<h2>How do I invite others to join the inner circle?</h2>
<p>
When you are viewing a <strong>member's photo gallery</strong> page, look for the prompt at the top of the
page.
</p>
<p>
If a member posts several nude pics <strong>including face</strong> you should invite them to join
the inner circle. All members of the circle are allowed to invite new members to join. We trust your
judgment: please only invite like-minded nudists who <em>actually</em> share nudes <em>with face</em>
to join the inner circle.
</p>
<h2>Please keep the existence of the inner circle a secret</h2>
<p>
The inner circle is not publicly advertised on the site. This is to help ensure that "bad actors" won't
try and game the system (e.g., by begging somebody to invite them into the circle, or uploading a bare
minimum of nude pics for somebody to invite them only for them to delete their nudes and try and stay
in the inner circle). Plus, it adds an air of exclusivity to keep the existence of the circle on the
down low.
</p>
<h2>Still continue to share at least <em>some</em> nudes on "public"</h2>
<p>
With the new Photo visibility option for "inner circle only" you may tag your best nudes for only members
of the inner circle to see. However, you should still continue to share at least <em>some</em> photos on
"Public" as you were doing previously. This is for a couple of reasons:
</p>
<ul>
<li>
Members who are <em>not</em> in the circle won't see your circle-only photos. If for example you
placed <em>all</em> of your nudes on circle-only you would appear to look the same as someone who
uploaded no nudes at all.
</li>
<li>
The "<a href="/faq#shy-faqs">Shy Account</a>" system of the main website still applies: if you have
not one public photo on your page you may be marked as a Shy Account and be limited from some site
features such as the Chat Room.
</li>
</ul>
</div>
</div>
{{end}}

View File

@ -0,0 +1,104 @@
{{define "title"}}Invite to the inner circle{{end}}
{{define "content"}}
<div class="container">
<section class="hero is-inner-circle is-bold">
<div class="hero-body">
<div class="container">
<h1 class="title">
<img src="/static/img/circle-24.png">
Invite to the inner circle
</h1>
</div>
</div>
</section>
<div class="block p-4">
<div class="columns is-centered">
<div class="column is-half">
<div class="card" style="width: 100%; max-width: 640px">
<header class="card-header has-background-link">
<p class="card-header-title has-text-light">
<span class="icon"><img src="/static/img/circle-16.png"></span>
Invite to the inner circle
</p>
</header>
<div class="card-content">
<div class="media block">
<div class="media-left">
{{template "avatar-64x64" .User}}
</div>
<div class="media-content">
<p class="title is-4">{{.User.NameOrUsername}}</p>
<p class="subtitle is-6">
<span class="icon"><i class="fa fa-user"></i></span>
<a href="/u/{{.User.Username}}" target="_blank">{{.User.Username}}</a>
</p>
</div>
</div>
<form action="/inner-circle/invite" method="POST">
{{InputCSRF}}
<input type="hidden" name="to" value="{{.User.Username}}">
<div class="content">
<p>
Do you want to invite <strong>{{.User.Username}}</strong> to join the
{{PrettyCircle}}? Please review the following notes:
</p>
<ul>
<li>
The inner circle is designed for {{PrettyTitle}} members who <em>actually</em> post
several nude photos <strong>with face</strong> on their profile page.
If {{.User.Username}} only has clothed selfies on their page, or keeps
their face hidden or cropped out of their nudes, please <strong>do not</strong>
invite them to join the inner circle.
</li>
<li>
All members of the inner circle are allowed to invite others to join the
circle. We trust your judgment -- help ensure that the inner circle is only
made up of truly non-shy nudists who show their whole body on their profile
page.
</li>
</ul>
<p>
Note: while we use the word "invite" they will actually be added to the inner circle
immediately and receive a notification that they had been added. They won't know that
it was <em>you</em> who invited them to join the circle.
</p>
</div>
<div class="field has-text-centered">
<button type="submit" name="intent" value="confirm" class="button is-success">
Add to the inner circle
</button>
<button type="submit" name="intent" value="cancel" class="button is-warning ml-1">
Cancel
</button>
</div>
</form>
</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

@ -104,6 +104,17 @@
</div> </div>
{{end}} {{end}}
{{if and .CurrentUser.IsInnerCircle .User.IsInnerCircle}}
<div class="pt-1">
<div class="icon-text has-text-danger">
<span class="icon">
<img src="/static/img/circle-16.png">
</span>
<strong>{{PrettyCircle}}</strong>
</div>
</div>
{{end}}
{{if .User.IsAdmin}} {{if .User.IsAdmin}}
<div class="pt-1"> <div class="pt-1">
<div class="icon-text has-text-danger"> <div class="icon-text has-text-danger">

View File

@ -0,0 +1,94 @@
{{define "title"}}Remove from the inner circle{{end}}
{{define "content"}}
<div class="container">
<section class="hero is-inner-circle is-bold">
<div class="hero-body">
<div class="container">
<h1 class="title">
<img src="/static/img/circle-24.png">
Remove from the inner circle
</h1>
</div>
</div>
</section>
<div class="block p-4">
<div class="columns is-centered">
<div class="column is-half">
<div class="card" style="width: 100%; max-width: 640px">
<header class="card-header has-background-warning">
<p class="card-header-title has-text-black">
<span class="icon"><img src="/static/img/circle-16.png"></span>
Remove from the inner circle
</p>
</header>
<div class="card-content">
<div class="media block">
<div class="media-left">
{{template "avatar-64x64" .User}}
</div>
<div class="media-content">
<p class="title is-4">{{.User.NameOrUsername}}</p>
<p class="subtitle is-6">
<span class="icon"><i class="fa fa-user"></i></span>
<a href="/u/{{.User.Username}}" target="_blank">{{.User.Username}}</a>
</p>
</div>
</div>
<form action="/inner-circle/remove" method="POST">
{{InputCSRF}}
<input type="hidden" name="to" value="{{.User.Username}}">
<div class="content">
<p>
Do you want to <strong class="has-text-danger">remove</strong> {{.User.Username}} from
the {{PrettyCircle}}? Doing so will:
</p>
<ul>
<li>
Unset their inner circle flag, removing them from all inner circle features.
</li>
<li>
Set any photo they had for "inner circle only" to be "public" instead.
</li>
<li>
Clean up any notification they once received about being invited to the inner circle.
</li>
</ul>
</div>
<div class="field has-text-centered">
<button type="submit" name="intent" value="confirm" class="button is-danger">
Remove from the inner circle
</button>
<button type="submit" name="intent" value="cancel" class="button is-warning ml-1">
Cancel
</button>
</div>
</form>
</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

@ -54,10 +54,13 @@
<div class="column"> <div class="column">
<div class="field"> <div class="field">
<label class="label">Certified:</label> <label class="label">Status:</label>
<div class="select is-fullwidth"> <div class="select is-fullwidth">
<select id="certified" name="certified"> <select id="certified" name="certified">
<option value="true">Only certified users</option> <option value="true">Only certified users</option>
{{if .CurrentUser.IsInnerCircle}}
<option value="circle"{{if eq $Root.Certified "circle"}} selected{{end}}>Inner circle only</option>
{{end}}
<option value="false"{{if eq $Root.Certified "false"}} selected{{end}}>Show all users</option> <option value="false"{{if eq $Root.Certified "false"}} selected{{end}}>Show all users</option>
</select> </select>
</div> </div>
@ -189,6 +192,9 @@
{{else}} {{else}}
{{.NameOrUsername}} {{.NameOrUsername}}
{{end}} {{end}}
{{if .InnerCircle}}
<img src="/static/img/circle-16.png">
{{end}}
</a> </a>
{{if eq .Visibility "private"}} {{if eq .Visibility "private"}}
<sup class="fa fa-mask is-size-7" title="Private Profile"></sup> <sup class="fa fa-mask is-size-7" title="Private Profile"></sup>

View File

@ -167,6 +167,12 @@
<span class="icon"><i class="fa fa-eye"></i></span> <span class="icon"><i class="fa fa-eye"></i></span>
<span>Private Photos</span> <span>Private Photos</span>
</a> </a>
{{if .CurrentUser.IsInnerCircle}}
<a class="navbar-item" href="/inner-circle">
<span class="icon"><img src="/static/img/circle-16.png"></span>
<span>Inner circle</span>
</a>
{{end}}
<a class="navbar-item" href="/settings"> <a class="navbar-item" href="/settings">
<span class="icon"><i class="fa fa-gear"></i></span> <span class="icon"><i class="fa fa-gear"></i></span>
<span>Settings</span> <span>Settings</span>

View File

@ -127,6 +127,19 @@
Check this box if the forum allows photos to be uploaded (not implemented) Check this box if the forum allows photos to be uploaded (not implemented)
</p> </p>
{{end}} {{end}}
{{if .CurrentUser.IsAdmin}}
<label class="checkbox mt-3">
<input type="checkbox"
name="inner_circle"
value="true"
{{if and .EditForum .EditForum.InnerCircle}}checked{{end}}>
Inner circle <img src="/static/img/circle-16.png" class="ml-1">
</label>
<p class="help">
This forum is only available to inner circle members.
</p>
{{end}}
</div> </div>
<div class="field"> <div class="field">

View File

@ -76,6 +76,13 @@
</div> </div>
<div> <div>
{{if .InnerCircle}}
<div class="tag is-info is-light">
<span class="icon"><img src="/static/img/circle-10.png" width="9" height="9"></span>
InnerCircle
</div>
{{end}}
{{if .Explicit}} {{if .Explicit}}
<div class="tag is-danger is-light"> <div class="tag is-danger is-light">
<span class="icon"><i class="fa fa-fire"></i></span> <span class="icon"><i class="fa fa-fire"></i></span>

View File

@ -64,6 +64,7 @@
<div class="column is-3 pt-0 pb-1"> <div class="column is-3 pt-0 pb-1">
<h2 class="is-size-4"> <h2 class="is-size-4">
{{if .InnerCircle}}<img src="/static/img/circle-24.png" width="20" height="20" class="mr-1">{{end}}
<strong><a href="/f/{{.Fragment}}">{{.Title}}</a></strong> <strong><a href="/f/{{.Fragment}}">{{.Title}}</a></strong>
</h2> </h2>

View File

@ -40,6 +40,15 @@
Friends Friends
</span> </span>
</span> </span>
{{else if eq .Visibility "circle"}}
<span class="tag is-info is-light">
<span class="icon">
<img src="/static/img/circle-10.png">
</span>
<span>
{{PrettyCircle}}
</span>
</span>
{{else}} {{else}}
<span class="tag is-private is-light"> <span class="tag is-private is-light">
<span class="icon"><i class="fa fa-lock"></i></span> <span class="icon"><i class="fa fa-lock"></i></span>
@ -251,6 +260,9 @@
<select id="visibility" name="visibility"> <select id="visibility" name="visibility">
<option value="">All photos</option> <option value="">All photos</option>
<option value="public"{{if eq .FilterVisibility "public"}} selected{{end}}>Public only</option> <option value="public"{{if eq .FilterVisibility "public"}} selected{{end}}>Public only</option>
{{if .CurrentUser.IsInnerCircle}}
<option value="circle"{{if eq .FilterVisibility "circle"}} selected{{end}}>Inner circle only</option>
{{end}}
<option value="friends"{{if eq .FilterVisibility "friends"}} selected{{end}}>Friends only</option> <option value="friends"{{if eq .FilterVisibility "friends"}} selected{{end}}>Friends only</option>
<option value="private"{{if eq .FilterVisibility "private"}} selected{{end}}>Private only</option> <option value="private"{{if eq .FilterVisibility "private"}} selected{{end}}>Private only</option>
</select> </select>
@ -328,6 +340,25 @@
</div> </div>
{{end}} {{end}}
<!-- Inner circle invitation -->
{{if and (.CurrentUser.IsInnerCircle) (not .User.InnerCircle) (ne .CurrentUser.Username .User.Username)}}
<div class="block mt-0">
<span class="icon"><img src="/static/img/circle-16.png"></span>
Does <strong>{{.User.Username}}</strong> show a lot of nudity? Consider
<a href="/inner-circle/invite?to={{.User.Username}}">inviting them to join the {{PrettyCircle}}</a>.
</div>
{{else if (and .CurrentUser.IsInnerCircle .User.IsInnerCircle)}}
<div class="block mt-0">
<span class="icon"><img src="/static/img/circle-16.png"></span>
<strong>{{.User.Username}}</strong> is a part of the {{PrettyCircle}}.
{{if .CurrentUser.IsAdmin}}
<a href="/inner-circle/remove?to={{.User.Username}}" class="has-text-danger ml-2">
<i class="fa fa-gavel"></i> Remove from circle?
</a>
{{end}}
</div>
{{end}}
{{template "pager" .}} {{template "pager" .}}
<!-- "Full" view style? (blog style) --> <!-- "Full" view style? (blog style) -->
@ -356,6 +387,8 @@
<i class="fa fa-user-group has-text-warning" title="Friends"></i> <i class="fa fa-user-group has-text-warning" title="Friends"></i>
{{else if eq .Visibility "private"}} {{else if eq .Visibility "private"}}
<i class="fa fa-lock has-text-private-light" title="Private"></i> <i class="fa fa-lock has-text-private-light" title="Private"></i>
{{else if eq .Visibility "circle"}}
<img src="/static/img/circle-16.png">
{{else}} {{else}}
<i class="fa fa-eye has-text-link-light" title="Public"></i> <i class="fa fa-eye has-text-link-light" title="Public"></i>
{{end}} {{end}}
@ -463,6 +496,8 @@
<i class="fa fa-user-group has-text-warning" title="Friends"></i> <i class="fa fa-user-group has-text-warning" title="Friends"></i>
{{else if eq .Visibility "private"}} {{else if eq .Visibility "private"}}
<i class="fa fa-lock has-text-private-light" title="Private"></i> <i class="fa fa-lock has-text-private-light" title="Private"></i>
{{else if eq .Visibility "circle"}}
<img src="/static/img/circle-16.png">
{{else}} {{else}}
<i class="fa fa-eye has-text-link-light" title="Public"></i> <i class="fa fa-eye has-text-link-light" title="Public"></i>
{{end}} {{end}}

View File

@ -215,7 +215,7 @@
value="public" value="public"
{{if or (not .EditPhoto) (eq .EditPhoto.Visibility "public")}}checked{{end}}> {{if or (not .EditPhoto) (eq .EditPhoto.Visibility "public")}}checked{{end}}>
<strong class="has-text-link ml-1"> <strong class="has-text-link ml-1">
<span>Public</span> <span>Public <small>(members only)</small></span>
<span class="icon"><i class="fa fa-eye"></i></span> <span class="icon"><i class="fa fa-eye"></i></span>
</strong> </strong>
</label> </label>
@ -225,6 +225,27 @@
Gallery if that option is enabled, below. Gallery if that option is enabled, below.
</p> </p>
</div> </div>
{{if .CurrentUser.IsInnerCircle}}
<div>
<label class="radio">
<input type="radio"
name="visibility"
value="circle"
{{if eq .EditPhoto.Visibility "circle"}}checked{{end}}>
<strong class="has-text-link ml-1">
<span>{{PrettyCircle}}</span>
<span class="icon">
<img src="/static/img/circle-16.png">
</span>
</strong>
</label>
<p class="help">
Only members of the <a href="/inner-circle">inner circle</a> will see this photo. This is
like the "Public" visibility except only people in the inner circle will see it on your
profile page or on the Site Gallery (if that option is enabled, below).
</p>
</div>
{{end}}
<div> <div>
<label class="radio"> <label class="radio">
<input type="radio" <input type="radio"