Spit and Polish
* Friends: can now see your sent requests awaiting approval too. * Site Gallery: you may see Friends-only photos on the Gallery if you are friends with the owner and the pic is opted-in for the Gallery. * Site Gallery: show color coded visibility icons in card headers. * Improve appearance of Upload Photo page. * Update FAQ
This commit is contained in:
parent
13aa5a8544
commit
e06bf2f9c4
|
@ -13,7 +13,11 @@ import (
|
|||
func Friends() http.HandlerFunc {
|
||||
tmpl := templates.Must("friend/friends.html")
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
isRequests := r.FormValue("view") == "requests"
|
||||
var (
|
||||
view = r.FormValue("view")
|
||||
isRequests = view == "requests"
|
||||
isPending = view == "pending"
|
||||
)
|
||||
|
||||
currentUser, err := session.CurrentUser(r)
|
||||
if err != nil {
|
||||
|
@ -28,7 +32,7 @@ func Friends() http.HandlerFunc {
|
|||
Sort: "updated_at desc",
|
||||
}
|
||||
pager.ParsePage(r)
|
||||
friends, err := models.PaginateFriends(currentUser.ID, isRequests, pager)
|
||||
friends, err := models.PaginateFriends(currentUser.ID, isRequests, isPending, pager)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't paginate friends: %s", err)
|
||||
templates.Redirect(w, "/")
|
||||
|
@ -37,6 +41,7 @@ func Friends() http.HandlerFunc {
|
|||
|
||||
var vars = map[string]interface{}{
|
||||
"IsRequests": isRequests,
|
||||
"IsPending": isPending,
|
||||
"Friends": friends,
|
||||
"Pager": pager,
|
||||
}
|
||||
|
|
|
@ -91,6 +91,19 @@ func FriendStatus(sourceUserID, targetUserID uint64) string {
|
|||
return "none"
|
||||
}
|
||||
|
||||
// FriendIDs returns all user IDs with approved friendship to the user.
|
||||
func FriendIDs(userId uint64) []uint64 {
|
||||
var (
|
||||
fs = []*Friend{}
|
||||
userIDs = []uint64{}
|
||||
)
|
||||
DB.Where("source_user_id = ? AND approved = ?", userId, true).Find(&fs)
|
||||
for _, row := range fs {
|
||||
userIDs = append(userIDs, row.TargetUserID)
|
||||
}
|
||||
return userIDs
|
||||
}
|
||||
|
||||
// CountFriendRequests gets a count of pending requests for the user.
|
||||
func CountFriendRequests(userID uint64) (int64, error) {
|
||||
var count int64
|
||||
|
@ -102,9 +115,15 @@ func CountFriendRequests(userID uint64) (int64, error) {
|
|||
return count, result.Error
|
||||
}
|
||||
|
||||
// PaginateFriends gets a page of friends (or pending friend requests) as User objects ordered
|
||||
// by friendship date.
|
||||
func PaginateFriends(userID uint64, requests bool, pager *Pagination) ([]*User, error) {
|
||||
/*
|
||||
PaginateFriends gets a page of friends (or pending friend requests) as User objects ordered
|
||||
by friendship date.
|
||||
|
||||
The `requests` and `sent` bools are mutually exclusive (use only one, or neither). `requests`
|
||||
asks for unanswered friend requests to you, and `sent` returns the friend requests that you
|
||||
have sent and have not been answered.
|
||||
*/
|
||||
func PaginateFriends(userID uint64, requests bool, sent bool, pager *Pagination) ([]*User, error) {
|
||||
// We paginate over the Friend table.
|
||||
var (
|
||||
fs = []*Friend{}
|
||||
|
@ -112,17 +131,24 @@ func PaginateFriends(userID uint64, requests bool, pager *Pagination) ([]*User,
|
|||
query *gorm.DB
|
||||
)
|
||||
|
||||
if requests && sent {
|
||||
return nil, errors.New("requests and sent are mutually exclusive options, use one or neither")
|
||||
}
|
||||
|
||||
if requests {
|
||||
query = DB.Where(
|
||||
"target_user_id = ? AND approved = ?",
|
||||
userID,
|
||||
false,
|
||||
userID, false,
|
||||
)
|
||||
} else if sent {
|
||||
query = DB.Where(
|
||||
"source_user_id = ? AND approved = ?",
|
||||
userID, false,
|
||||
)
|
||||
} else {
|
||||
query = DB.Where(
|
||||
"source_user_id = ? AND approved = ?",
|
||||
userID,
|
||||
true,
|
||||
userID, true,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -33,12 +33,22 @@ const (
|
|||
PhotoPrivate = "private" // private
|
||||
)
|
||||
|
||||
var PhotoVisibilityAll = []PhotoVisibility{
|
||||
// PhotoVisibility preset settings.
|
||||
var (
|
||||
PhotoVisibilityAll = []PhotoVisibility{
|
||||
PhotoPublic,
|
||||
PhotoFriends,
|
||||
PhotoPrivate,
|
||||
}
|
||||
|
||||
// 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.
|
||||
PhotoVisibilityFriends = []string{
|
||||
string(PhotoPublic),
|
||||
string(PhotoFriends),
|
||||
}
|
||||
)
|
||||
|
||||
// CreatePhoto with most of the settings you want (not ID or timestamps) in the database.
|
||||
func CreatePhoto(tmpl Photo) (*Photo, error) {
|
||||
if tmpl.UserID == 0 {
|
||||
|
@ -127,13 +137,25 @@ func PaginateGalleryPhotos(userID uint64, adminView bool, explicitOK bool, pager
|
|||
p = []*Photo{}
|
||||
query *gorm.DB
|
||||
blocklist = BlockedUserIDs(userID)
|
||||
friendIDs = FriendIDs(userID)
|
||||
wheres = []string{}
|
||||
placeholders = []interface{}{}
|
||||
)
|
||||
|
||||
// Universal filters: public + gallery photos only.
|
||||
wheres = append(wheres, "visibility = ?", "gallery = ?")
|
||||
placeholders = append(placeholders, PhotoPublic, true)
|
||||
// Include ourself in our friend IDs.
|
||||
friendIDs = append(friendIDs, userID)
|
||||
|
||||
// You can see friends' Friend photos but only public for non-friends.
|
||||
wheres = append(wheres,
|
||||
"(user_id IN ? AND visibility IN ?) OR (user_id NOT IN ? AND visibility = ?)",
|
||||
)
|
||||
placeholders = append(placeholders,
|
||||
friendIDs, PhotoVisibilityFriends, friendIDs, PhotoPublic,
|
||||
)
|
||||
|
||||
// Gallery photos only.
|
||||
wheres = append(wheres, "gallery = ?")
|
||||
placeholders = append(placeholders, true)
|
||||
|
||||
// Filter blocked users.
|
||||
if len(blocklist) > 0 {
|
||||
|
|
|
@ -33,6 +33,13 @@
|
|||
background-color: #FFEEFF;
|
||||
}
|
||||
|
||||
.has-text-private {
|
||||
color: #CC00CC;
|
||||
}
|
||||
.has-text-private-light {
|
||||
color: #FF99FF;
|
||||
}
|
||||
|
||||
/* Mobile: notification badge near the hamburger menu */
|
||||
.nonshy-mobile-notification {
|
||||
position: absolute;
|
||||
|
@ -45,3 +52,8 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Bulma hack: full-width columns in photo card headers */
|
||||
.nonshy-fullwidth {
|
||||
width: 100%;
|
||||
}
|
|
@ -69,6 +69,27 @@
|
|||
want to see just dick pics everywhere. And don't set those as your default profile pic!
|
||||
</p>
|
||||
|
||||
<h3>What appears on the Site Gallery?</h3>
|
||||
|
||||
<p>
|
||||
The "<strong><i class="fa fa-image"></i> Gallery</strong>" link on the site nav bar goes to the Site-wide
|
||||
Photo Gallery page. Here is shown all of the <strong>public</strong> photos uploaded by
|
||||
all (certified) users, if those pictures are also opted-in to appear on the Gallery in
|
||||
their settings.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you have friends on here, you may also see their "Friends-only" photos on the Site
|
||||
Gallery. This way, you don't miss any updates if your friends add a new picture (so
|
||||
long as they allow their picture to appear on the Gallery).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
When you upload a picture you may opt it in or out of the Gallery by checking a box on
|
||||
its settings page. For example, you can upload a Public photo but opt it <em>out</em> of
|
||||
the Gallery -- it will then only appear on your profile page.
|
||||
</p>
|
||||
|
||||
<h3>What is considered "explicit" in photos?</h3>
|
||||
|
||||
<p>
|
||||
|
@ -96,6 +117,48 @@
|
|||
You can enable a setting on your profile if you are comfortable with seeing explicit
|
||||
content from other users -- by default this site is "normal nudes" friendly!
|
||||
</p>
|
||||
|
||||
<h1>Technical FAQs</h1>
|
||||
|
||||
<h3>Why did you build a custom website?</h3>
|
||||
|
||||
<p>
|
||||
Other variants on this question might be: why not just run a
|
||||
<a href="https://joinmastodon.org" target="_blank">Mastodon</a> instance? Or why
|
||||
this website and not a Discord server or MeWe group or <em>insert off-the-shelf
|
||||
free software or hosted web service here</em>?
|
||||
</p>
|
||||
|
||||
<p>
|
||||
It certainly would've been simpler to just use an off-the-shelf open source app
|
||||
such as Mastodon (a decentralized, Twitter-like app) or similar. These apps though
|
||||
have a scalability problem: users with their infinitely long timelines will upload
|
||||
infinite photos until your server runs out of disk space and not enough of them may
|
||||
donate to cover the costs. And the Fediverse feature (Mastodon is like e-mail and
|
||||
users from all servers can like, follow and comment on one another across the entire
|
||||
network) is a double edged sword too: all my members would need to tag even their
|
||||
"normal nudes" as NSFW or else other servers would ban ours (meaning we have to follow
|
||||
rules imposed by the wider Internet community), and conversely it is difficult to
|
||||
moderate incoming content from other servers showing up on my users' timelines.
|
||||
It's not a good fit for the vision I had in mind.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
And on just using a service like Discord or MeWe to host my community: that's still
|
||||
putting us in the hands of a corporation which can one day decide to ban all NSFW
|
||||
users. Many people run nudist Discords and MeWe groups, but I needed something whose
|
||||
fate is kept in my own hands.
|
||||
</p>
|
||||
|
||||
<h3>Is this website open source?</h3>
|
||||
|
||||
<p>
|
||||
Yes! The source code is currently hosted on the author's personal Git server. It
|
||||
will eventually have a GitHub mirror and accept pull requests from the community.
|
||||
In the mean time, contact the site owner to get a link to the public git repo.
|
||||
This site is programmed in the Go language and released under the GNU General Public
|
||||
License.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
|
@ -15,7 +15,7 @@
|
|||
<div class="level-item">
|
||||
<div class="tabs is-toggle">
|
||||
<ul>
|
||||
<li{{if not .IsRequests}} class="is-active"{{end}}>
|
||||
<li{{if and (not .IsRequests) (not .IsPending)}} class="is-active"{{end}}>
|
||||
<a href="/friends">My Friends</a>
|
||||
</li>
|
||||
<li{{if .IsRequests}} class="is-active"{{end}}>
|
||||
|
@ -24,6 +24,11 @@
|
|||
{{if .NavFriendRequests}}<span class="tag is-warning ml-2">{{.NavFriendRequests}}</span>{{end}}
|
||||
</a>
|
||||
</li>
|
||||
<li{{if .IsPending}} class="is-active"{{end}}>
|
||||
<a href="/friends?view=pending">
|
||||
Sent
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -33,8 +38,13 @@
|
|||
<div class="p-4">
|
||||
|
||||
<div class="block">
|
||||
{{if .IsPending}}
|
||||
You have sent {{.Pager.Total}} friend request{{Pluralize64 .Pager.Total}} which
|
||||
{{Pluralize64 .Pager.Total "has" "have"}} not been approved yet.
|
||||
{{else}}
|
||||
You have {{.Pager.Total}} friend{{if .IsRequests}} request{{end}}{{Pluralize64 .Pager.Total}}
|
||||
(page {{.Pager.Page}} of {{.Pager.Pages}}).
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
|
@ -121,7 +131,7 @@
|
|||
<button type="submit" name="verdict" value="remove" class="card-footer-item button is-danger"
|
||||
onclick="return confirm('Are you sure you want to remove this friendship?')">
|
||||
<span class="icon"><i class="fa fa-xmark"></i></span>
|
||||
<span>Remove</span>
|
||||
<span>{{if $Root.IsPending}}Cancel{{else}}Remove{{end}}</span>
|
||||
</button>
|
||||
</footer>
|
||||
{{end}}
|
||||
|
|
|
@ -35,14 +35,14 @@
|
|||
</span>
|
||||
{{else if eq .Visibility "friends"}}
|
||||
<span class="tag is-warning is-light">
|
||||
<span class="icon"><i class="fa fa-eye"></i></span>
|
||||
<span class="icon"><i class="fa fa-user-group"></i></span>
|
||||
<span>
|
||||
Friends
|
||||
</span>
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="tag is-private is-light">
|
||||
<span class="icon"><i class="fa fa-eye"></i></span>
|
||||
<span class="icon"><i class="fa fa-lock"></i></span>
|
||||
<span>
|
||||
Private
|
||||
</span>
|
||||
|
@ -203,7 +203,7 @@
|
|||
<div class="card-header-title has-text-light">
|
||||
{{if $Root.UserMap.Has .UserID}}
|
||||
{{$Owner := $Root.UserMap.Get .UserID}}
|
||||
<div class="columns is-mobile is-gapless">
|
||||
<div class="columns is-mobile is-gapless nonshy-fullwidth">
|
||||
<div class="column is-narrow">
|
||||
<figure class="image is-24x24 mr-2">
|
||||
{{if gt $Owner.ProfilePhoto.ID 0}}
|
||||
|
@ -219,6 +219,17 @@
|
|||
<i class="fa fa-external-link ml-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<span class="icon">
|
||||
{{if eq .Visibility "friends"}}
|
||||
<i class="fa fa-user-group has-text-warning" title="Friends"></i>
|
||||
{{else if eq .Visibility "private"}}
|
||||
<i class="fa fa-lock has-text-private-light" title="Private"></i>
|
||||
{{else}}
|
||||
<i class="fa fa-eye has-text-link-light" title="Public"></i>
|
||||
{{end}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<span class="fa fa-user mr-2"></span>
|
||||
|
@ -278,7 +289,7 @@
|
|||
<div class="card-header-title has-text-light">
|
||||
{{if $Root.UserMap.Has .UserID}}
|
||||
{{$Owner := $Root.UserMap.Get .UserID}}
|
||||
<div class="columns is-mobile is-gapless">
|
||||
<div class="columns is-mobile is-gapless nonshy-fullwidth">
|
||||
<div class="column is-narrow">
|
||||
<figure class="image is-24x24 mr-2">
|
||||
{{if gt $Owner.ProfilePhoto.ID 0}}
|
||||
|
@ -294,6 +305,17 @@
|
|||
<i class="fa fa-external-link ml-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<span class="icon">
|
||||
{{if eq .Visibility "friends"}}
|
||||
<i class="fa fa-user-group has-text-warning" title="Friends"></i>
|
||||
{{else if eq .Visibility "private"}}
|
||||
<i class="fa fa-lock has-text-private-light" title="Private"></i>
|
||||
{{else}}
|
||||
<i class="fa fa-eye has-text-link-light" title="Public"></i>
|
||||
{{end}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<span class="fa fa-user mr-2"></span>
|
||||
|
|
|
@ -197,7 +197,7 @@
|
|||
|
||||
<div class="card-content">
|
||||
|
||||
<div class="field">
|
||||
<div class="field mb-5">
|
||||
<label class="label" for="caption">Caption</label>
|
||||
<input type="text" class="input"
|
||||
name="caption"
|
||||
|
@ -206,8 +206,89 @@
|
|||
value="{{.EditPhoto.Caption}}">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Explicit Content</label>
|
||||
<div class="field mb-5">
|
||||
<label class="label">Photo Visibility</label>
|
||||
<div>
|
||||
<label class="radio">
|
||||
<input type="radio"
|
||||
name="visibility"
|
||||
value="public"
|
||||
{{if or (not .EditPhoto) (eq .EditPhoto.Visibility "public")}}checked{{end}}>
|
||||
<strong class="has-text-link">
|
||||
<span>Public</span>
|
||||
<span class="icon"><i class="fa fa-eye"></i></span>
|
||||
</strong>
|
||||
</label>
|
||||
<p class="help">
|
||||
This photo will appear on your profile page and can be seen by any
|
||||
logged-in user account. It may also appear on the site-wide Photo
|
||||
Gallery if that option is enabled, below.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="radio">
|
||||
<input type="radio"
|
||||
name="visibility"
|
||||
value="friends"
|
||||
{{if eq .EditPhoto.Visibility "friends"}}checked{{end}}>
|
||||
<strong class="has-text-warning-dark">
|
||||
<span>Friends only</span>
|
||||
<span class="icon"><i class="fa fa-user-group"></i></span>
|
||||
</strong>
|
||||
</label>
|
||||
<p class="help">
|
||||
Only users you have accepted as a friend can see this photo on your
|
||||
profile page and on the site-wide Photo Gallery if that option is
|
||||
enabled, below.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="radio">
|
||||
<input type="radio"
|
||||
name="visibility"
|
||||
value="private"
|
||||
{{if eq .EditPhoto.Visibility "private"}}checked{{end}}>
|
||||
<strong class="has-text-private">
|
||||
<span>Private</span>
|
||||
<span class="icon"><i class="fa fa-lock"></i></span>
|
||||
</strong>
|
||||
</label>
|
||||
<p class="help">
|
||||
This photo is visible only to you and to users for whom you have
|
||||
granted access (the latter feature is coming soon!)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field mb-5">
|
||||
<label class="label">
|
||||
<span>Site Photo Gallery</span>
|
||||
<span class="icon"><i class="fa fa-image"></i></span>
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox"
|
||||
name="gallery"
|
||||
value="true"
|
||||
checked
|
||||
{{if .EditPhoto.Gallery}}checked{{end}}>
|
||||
Show this photo in the site-wide Photo Gallery
|
||||
</label>
|
||||
<p class="help">
|
||||
Leave this box checked and your photo can appear in the site's Photo Gallery
|
||||
page. Mainly your <strong class="has-text-link">Public</strong> photos will appear
|
||||
on the Gallery, and your approved friends may see your
|
||||
<strong class="has-text-warning-dark">Friends-only</strong> photos there as well.
|
||||
<strong class="has-text-private">Private</strong> photos may appear in
|
||||
the gallery to users whom you have granted access. If this is undesirable,
|
||||
un-check the Gallery box to skip the Site Gallery.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field mb-5">
|
||||
<label class="label has-text-danger">
|
||||
<span>Explicit Content</span>
|
||||
<span class="icon"><i class="fa fa-fire"></i></span>
|
||||
</label>
|
||||
{{if eq .Intent "profile_pic"}}
|
||||
<span class="has-text-danger">
|
||||
Your default profile picture should
|
||||
|
@ -238,59 +319,6 @@
|
|||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Photo Visibility</label>
|
||||
<div>
|
||||
<label class="radio">
|
||||
<input type="radio"
|
||||
name="visibility"
|
||||
value="public"
|
||||
{{if or (not .EditPhoto) (eq .EditPhoto.Visibility "public")}}checked{{end}}>
|
||||
<strong>Public:</strong> this photo will appear on your profile page
|
||||
and can be seen by any logged-in user account. It may also appear
|
||||
on the site-wide Photo Gallery if that option is enabled, below.
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="radio">
|
||||
<input type="radio"
|
||||
name="visibility"
|
||||
value="friends"
|
||||
{{if eq .EditPhoto.Visibility "friends"}}checked{{end}}>
|
||||
<strong>Friends only:</strong> only users you have added as a friend
|
||||
can see this photo on your profile page.
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="radio">
|
||||
<input type="radio"
|
||||
name="visibility"
|
||||
value="private"
|
||||
{{if eq .EditPhoto.Visibility "private"}}checked{{end}}>
|
||||
<strong>Private:</strong> this photo is not visible to anybody except
|
||||
for people whom you allow to see your private pictures (not implemented yet!)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Site Photo Gallery</label>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox"
|
||||
name="gallery"
|
||||
value="true"
|
||||
checked
|
||||
{{if .EditPhoto.Gallery}}checked{{end}}>
|
||||
Show this photo in the site-wide Photo Gallery (public photos only)
|
||||
</label>
|
||||
<p class="help">
|
||||
Leave this box checked and your (public only) photo can appear in the site's
|
||||
Photo Gallery page. If you uncheck this box, your (public) photo will still
|
||||
appear on your profile page but not on the site photo gallery. Friends-only
|
||||
or private photos never appear in the gallery even if this box is checked.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{if not .EditPhoto}}
|
||||
<div class="field">
|
||||
<label class="label">Confirm Upload</label>
|
||||
|
|
Reference in New Issue
Block a user