Privacy Improvements and Notification Fixes
* On user profile pages and gallery: the total photo count for the user will only include photos that the viewer can actually see (taking into account friendship and private grants), so that users won't harass each other to see the additional photos that aren't visible to them. * On the member directory search: the photo counts will only show public photos on their page for now, and may be fewer than the number of photos the current user could actually see. * Blocklist: you can now manually add a user by username to your block list. So if somebody blocked you on the site and you want to block them back, there is a way to do this. * Friends: you can now directly unfriend someone from their profile page by clicking on the "Friends" button. You get a confirmation popup before the remove friend action goes through. * Bugfix: when viewing a user's gallery, you were able to see their Friends-only photos if they granted you their Private photo access, even if you were not their friend. * Bugfix: when uploading a new private photo, instead of notifying everybody you granted access to your privates it will only notify if they are also on your friend list.
This commit is contained in:
parent
47859ee204
commit
666d3105b7
|
@ -104,7 +104,7 @@ func Profile() http.HandlerFunc {
|
|||
"LikeMap": likeMap,
|
||||
"IsFriend": isFriend,
|
||||
"IsPrivate": isPrivate,
|
||||
"PhotoCount": models.CountPhotos(user.ID),
|
||||
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
|
|
|
@ -47,6 +47,17 @@ func Blocked() http.HandlerFunc {
|
|||
})
|
||||
}
|
||||
|
||||
// AddUser to manually add someone to your block list.
|
||||
func AddUser() http.HandlerFunc {
|
||||
tmpl := templates.Must("account/block_list_add.html")
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := tmpl.Execute(w, r, nil); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BlockUser controller.
|
||||
func BlockUser() http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -75,7 +86,7 @@ func BlockUser() http.HandlerFunc {
|
|||
user, err := models.FindUser(username)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "User Not Found")
|
||||
templates.Redirect(w, "/")
|
||||
templates.Redirect(w, "/users/blocklist/add")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -167,40 +167,48 @@ func Upload() http.HandlerFunc {
|
|||
// Create a notification for all our Friends about a new photo.
|
||||
// Run in a background goroutine in case it takes a while.
|
||||
func notifyFriendsNewPhoto(photo *models.Photo, currentUser *models.User) {
|
||||
var friendIDs []uint64
|
||||
var (
|
||||
friendIDs []uint64
|
||||
notifyUserIDs []uint64
|
||||
)
|
||||
|
||||
// Who to notify?
|
||||
// Get the user's literal list of friends (with explicit opt-ins).
|
||||
if photo.Explicit {
|
||||
friendIDs = models.FriendIDsAreExplicit(currentUser.ID)
|
||||
} else {
|
||||
friendIDs = models.FriendIDs(currentUser.ID)
|
||||
}
|
||||
|
||||
// Who to notify about this upload?
|
||||
if photo.Visibility == models.PhotoPrivate {
|
||||
// Private grantees
|
||||
if photo.Explicit {
|
||||
friendIDs = models.PrivateGranteeAreExplicitUserIDs(currentUser.ID)
|
||||
log.Info("Notify %d EXPLICIT private grantees about the new photo by %s", len(friendIDs), currentUser.Username)
|
||||
notifyUserIDs = models.PrivateGranteeAreExplicitUserIDs(currentUser.ID)
|
||||
log.Info("Notify %d EXPLICIT private grantees about the new photo by %s", len(notifyUserIDs), currentUser.Username)
|
||||
} else {
|
||||
friendIDs = models.PrivateGranteeUserIDs(currentUser.ID)
|
||||
log.Info("Notify %d private grantees about the new photo by %s", len(friendIDs), currentUser.Username)
|
||||
notifyUserIDs = models.PrivateGranteeUserIDs(currentUser.ID)
|
||||
log.Info("Notify %d private grantees about the new photo by %s", len(notifyUserIDs), currentUser.Username)
|
||||
}
|
||||
} else if photo.Visibility == models.PhotoInnerCircle {
|
||||
// Inner circle members. If the pic is also Explicit, further narrow to explicit friend IDs.
|
||||
if photo.Explicit {
|
||||
friendIDs = models.FriendIDsInCircleAreExplicit(currentUser.ID)
|
||||
log.Info("Notify %d EXPLICIT circle friends about the new photo by %s", len(friendIDs), currentUser.Username)
|
||||
notifyUserIDs = models.FriendIDsInCircleAreExplicit(currentUser.ID)
|
||||
log.Info("Notify %d EXPLICIT circle friends about the new photo by %s", len(notifyUserIDs), currentUser.Username)
|
||||
} else {
|
||||
friendIDs = models.FriendIDsInCircle(currentUser.ID)
|
||||
log.Info("Notify %d circle friends about the new photo by %s", len(friendIDs), currentUser.Username)
|
||||
notifyUserIDs = models.FriendIDsInCircle(currentUser.ID)
|
||||
log.Info("Notify %d circle friends about the new photo by %s", len(notifyUserIDs), currentUser.Username)
|
||||
}
|
||||
} else {
|
||||
// Get all our friend IDs. If this photo is Explicit, only select
|
||||
// the friends who've opted-in for Explicit photo visibility.
|
||||
if photo.Explicit {
|
||||
friendIDs = models.FriendIDsAreExplicit(currentUser.ID)
|
||||
log.Info("Notify %d EXPLICIT friends about the new photo by %s", len(friendIDs), currentUser.Username)
|
||||
} else {
|
||||
friendIDs = models.FriendIDs(currentUser.ID)
|
||||
log.Info("Notify %d friends about the new photo by %s", len(friendIDs), currentUser.Username)
|
||||
}
|
||||
// Friends only: we will notify exactly the friends we selected above.
|
||||
notifyUserIDs = friendIDs
|
||||
}
|
||||
|
||||
for _, fid := range friendIDs {
|
||||
// Filter down the notifyUserIDs to only include the user's friends.
|
||||
// Example: someone unlocked private photos for you, but you are not their friend.
|
||||
// You should not get notified about their new private photos.
|
||||
notifyUserIDs = models.FilterFriendIDs(notifyUserIDs, friendIDs)
|
||||
|
||||
for _, fid := range notifyUserIDs {
|
||||
notif := &models.Notification{
|
||||
UserID: fid,
|
||||
AboutUser: *currentUser,
|
||||
|
|
|
@ -96,8 +96,9 @@ func UserPhotos() http.HandlerFunc {
|
|||
// What set of visibilities to query?
|
||||
visibility := []models.PhotoVisibility{models.PhotoPublic}
|
||||
if isOwnPhotos || isGrantee || currentUser.HasAdminScope(config.ScopePhotoModerator) {
|
||||
visibility = append(visibility, models.PhotoFriends, models.PhotoPrivate)
|
||||
} else if models.AreFriends(user.ID, currentUser.ID) {
|
||||
visibility = append(visibility, models.PhotoPrivate)
|
||||
}
|
||||
if models.AreFriends(user.ID, currentUser.ID) {
|
||||
visibility = append(visibility, models.PhotoFriends)
|
||||
}
|
||||
|
||||
|
@ -146,7 +147,7 @@ func UserPhotos() http.HandlerFunc {
|
|||
"AreWeGrantedPrivate": isGrantee, // have THEY granted US private photo access.
|
||||
"User": user,
|
||||
"Photos": photos,
|
||||
"PhotoCount": models.CountPhotos(user.ID),
|
||||
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
||||
"PublicPhotoCount": models.CountPublicPhotos(user.ID),
|
||||
"InnerCircleMinimumPublicPhotos": config.InnerCircleMinimumPublicPhotos,
|
||||
"Pager": pager,
|
||||
|
|
|
@ -105,6 +105,28 @@ func FriendIDs(userId uint64) []uint64 {
|
|||
return userIDs
|
||||
}
|
||||
|
||||
// FilterFriendIDs can filter down a listing of user IDs and return only the ones who are your friends.
|
||||
func FilterFriendIDs(userIDs []uint64, friendIDs []uint64) []uint64 {
|
||||
var (
|
||||
seen = map[uint64]interface{}{}
|
||||
filtered = []uint64{}
|
||||
)
|
||||
|
||||
// Map the friend IDs out.
|
||||
for _, friendID := range friendIDs {
|
||||
seen[friendID] = nil
|
||||
}
|
||||
|
||||
// Filter the userIDs.
|
||||
for _, userID := range userIDs {
|
||||
if _, ok := seen[userID]; ok {
|
||||
filtered = append(filtered, userID)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
// FriendIDsAreExplicit returns friend IDs who have opted-in for Explicit content,
|
||||
// e.g. to notify only them when you uploaded a new Explicit photo so that non-explicit
|
||||
// users don't need to see that notification.
|
||||
|
|
|
@ -146,6 +146,40 @@ func CountPhotos(userID uint64) int64 {
|
|||
return count
|
||||
}
|
||||
|
||||
// CountPhotosICanSee returns the number of photos on an account which can be seen by the given viewer.
|
||||
func CountPhotosICanSee(user *User, viewer *User) int64 {
|
||||
// Visibility filters to query by.
|
||||
var visibilities = []PhotoVisibility{
|
||||
PhotoPublic,
|
||||
}
|
||||
|
||||
// Is the viewer in the inner circle?
|
||||
if viewer.IsInnerCircle() {
|
||||
visibilities = append(visibilities, PhotoInnerCircle)
|
||||
}
|
||||
|
||||
// Is the viewer friends with the target?
|
||||
if AreFriends(user.ID, viewer.ID) {
|
||||
visibilities = append(visibilities, PhotoFriends)
|
||||
}
|
||||
|
||||
// Is the viewer granted private access?
|
||||
if IsPrivateUnlocked(user.ID, viewer.ID) {
|
||||
visibilities = append(visibilities, PhotoPrivate)
|
||||
}
|
||||
|
||||
// Get the photo count now.
|
||||
var count int64
|
||||
result := DB.Where(
|
||||
"user_id = ? AND visibility IN ?",
|
||||
user.ID, visibilities,
|
||||
).Model(&Photo{}).Count(&count)
|
||||
if result.Error != nil {
|
||||
log.Error("CountPhotosICanSee(%d, %d): %s", user.ID, viewer.ID, result.Error)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// MapPhotoCounts returns a mapping of user ID to the CountPhotos()-equivalent result for each.
|
||||
// It's used on the member directory to show photo counts on each user card.
|
||||
func MapPhotoCounts(users []*User) PhotoCountMap {
|
||||
|
@ -169,7 +203,7 @@ func MapPhotoCounts(users []*User) PhotoCountMap {
|
|||
).Select(
|
||||
"user_id, count(id) AS photo_count",
|
||||
).Where(
|
||||
"user_id IN ?", userIDs,
|
||||
"user_id IN ? AND visibility = ?", userIDs, PhotoPublic,
|
||||
).Group("user_id").Scan(&groups); res.Error != nil {
|
||||
log.Error("CountPhotosForUsers: %s", res.Error)
|
||||
}
|
||||
|
@ -182,6 +216,91 @@ func MapPhotoCounts(users []*User) PhotoCountMap {
|
|||
return result
|
||||
}
|
||||
|
||||
// MapPhotoCounts returns a mapping of user ID to the CountPhotosICanSee()-equivalent result for each.
|
||||
// It's used on the member directory to show photo counts on each user card.
|
||||
/* TODO: under construction..
|
||||
func MapPhotoCounts(users []*User, viewer *User) PhotoCountMap {
|
||||
var (
|
||||
userIDs = []uint64{}
|
||||
result = PhotoCountMap{}
|
||||
|
||||
wheres = []string{}
|
||||
placeholders = []interface{}{}
|
||||
|
||||
// User ID filters for the viewer's context.
|
||||
myFriendIDs = FriendIDs(viewer.ID)
|
||||
myPrivateGrantedIDs = PrivateGrantedUserIDs(viewer.ID)
|
||||
isInnerCircle = viewer.IsInnerCircle()
|
||||
)
|
||||
|
||||
// Define "all photos visibilities"
|
||||
var (
|
||||
photosPublic = []PhotoVisibility{
|
||||
PhotoPublic,
|
||||
}
|
||||
photosFriends = []PhotoVisibility{
|
||||
PhotoPublic,
|
||||
PhotoFriends,
|
||||
}
|
||||
photosPrivate = []PhotoVisibility{
|
||||
PhotoPublic,
|
||||
PhotoPrivate,
|
||||
}
|
||||
)
|
||||
if isInnerCircle {
|
||||
photosPublic = append(photosPublic, PhotoInnerCircle)
|
||||
photosFriends = append(photosFriends, PhotoInnerCircle)
|
||||
}
|
||||
|
||||
// Flatten the userIDs of all passed in users.
|
||||
for _, user := range users {
|
||||
userIDs = append(userIDs, user.ID)
|
||||
}
|
||||
|
||||
// Build the where clause.
|
||||
wheres = append(wheres, "user_id IN ?")
|
||||
placeholders = append(placeholders, userIDs)
|
||||
|
||||
log.Error("FRIEND IDS: %+v", myFriendIDs)
|
||||
|
||||
// Filter by which photos are visible to us.
|
||||
wheres = append(wheres,
|
||||
"((user_id IN ? AND visibility IN ?) OR "+
|
||||
"(user_id IN ? AND visibility IN ?) OR "+
|
||||
"(user_id NOT IN ? AND visibility IN ?))",
|
||||
)
|
||||
placeholders = append(placeholders,
|
||||
myFriendIDs, photosFriends,
|
||||
myPrivateGrantedIDs, photosPrivate,
|
||||
myFriendIDs, photosPublic,
|
||||
)
|
||||
|
||||
type group struct {
|
||||
UserID uint64
|
||||
PhotoCount int64
|
||||
}
|
||||
var groups = []group{}
|
||||
|
||||
if res := DB.Table(
|
||||
"photos",
|
||||
).Select(
|
||||
"user_id, count(id) AS photo_count",
|
||||
).Where(
|
||||
strings.Join(wheres, " AND "),
|
||||
placeholders...,
|
||||
).Group("user_id").Scan(&groups); res.Error != nil {
|
||||
log.Error("CountPhotosForUsers: %s", res.Error)
|
||||
}
|
||||
|
||||
// Map the results in.
|
||||
for _, row := range groups {
|
||||
result[row.UserID] = row.PhotoCount
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
*/
|
||||
|
||||
type PhotoCountMap map[uint64]int64
|
||||
|
||||
// Get a photo count for the given user ID from the map.
|
||||
|
|
|
@ -63,6 +63,7 @@ func New() http.Handler {
|
|||
mux.Handle("/friends/add", middleware.LoginRequired(friend.AddFriend()))
|
||||
mux.Handle("/users/block", middleware.LoginRequired(block.BlockUser()))
|
||||
mux.Handle("/users/blocked", middleware.LoginRequired(block.Blocked()))
|
||||
mux.Handle("/users/blocklist/add", middleware.LoginRequired(block.AddUser()))
|
||||
mux.Handle("/comments", middleware.LoginRequired(comment.PostComment()))
|
||||
mux.Handle("/comments/subscription", middleware.LoginRequired(comment.Subscription()))
|
||||
mux.Handle("/admin/unimpersonate", middleware.LoginRequired(admin.Unimpersonate()))
|
||||
|
|
|
@ -17,6 +17,13 @@
|
|||
(page {{.Pager.Page}} of {{.Pager.Pages}}).
|
||||
</div>
|
||||
|
||||
<div class="has-text-centered block">
|
||||
<a href="/users/blocklist/add" class="button is-primary is-outlined">
|
||||
<span class="icon"><i class="fa fa-plus"></i></span>
|
||||
<span>Add someone to my block list</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
{{SimplePager .Pager}}
|
||||
</div>
|
||||
|
|
65
web/templates/account/block_list_add.html
Normal file
65
web/templates/account/block_list_add.html
Normal file
|
@ -0,0 +1,65 @@
|
|||
{{define "title"}}Add to Block List{{end}}
|
||||
{{define "content"}}
|
||||
<div class="container">
|
||||
<section class="hero is-info is-bold">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
Add to Block List
|
||||
</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 mr-2"><i class="fa fa-eye"></i></span>
|
||||
<span>Add to Block List</span>
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
|
||||
<form action="/users/block" method="POST">
|
||||
{{InputCSRF}}
|
||||
|
||||
<div class="field block">
|
||||
<label for="username" class="label">Block Username:</label>
|
||||
<input type="text" class="input"
|
||||
name="username" id="username"
|
||||
placeholder="username"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="field has-text-centered">
|
||||
<button type="submit" class="button is-success">
|
||||
Continue
|
||||
</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}}
|
|
@ -151,8 +151,14 @@
|
|||
{{InputCSRF}}
|
||||
<input type="hidden" name="username" value="{{.User.Username}}">
|
||||
|
||||
<!-- If you are already friends, the button will remove them -->
|
||||
{{if eq .IsFriend "approved"}}
|
||||
<input type="hidden" name="verdict" value="remove">
|
||||
{{end}}
|
||||
|
||||
<button type="submit" class="button is-fullwidth"
|
||||
{{if not (eq .IsFriend "none")}}title="Friendship {{.IsFriend}}"{{end}}>
|
||||
{{if not (eq .IsFriend "none")}}title="Friendship {{.IsFriend}}"{{end}}
|
||||
{{if eq .IsFriend "approved"}}onclick="return confirm('Do you want to remove this friendship?')"{{end}}>
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
{{if eq .IsFriend "approved"}}
|
||||
|
|
Loading…
Reference in New Issue
Block a user