Private Photo Sharing
* Add ability to unlock your private photos for others. * Link to unlock is on another user's Photos page. * You can also access your "Manage Private Photos" page in the User Menu or Dashboard and add a user by username. * See who you have granted access, and see users who have granted you access to their private photos. * Private photos of users who unlocked them for you may appear on the Site Gallery if the photo allows. * Add new filters to the Site Gallery: you can choose to filter by Explicit, limit to a specific Visibility, and order by Newest or Oldest. Non-explicit users do not get a filter to see explicit content - they must opt-in in their settings. * Bugfix: Site Gallery was not correctly filtering Explicit photos away from users who did not opt-in for explicit!
This commit is contained in:
parent
31712eba4a
commit
de9ba94dd9
|
@ -12,6 +12,7 @@ var (
|
||||||
PageSizeMemberSearch = 60
|
PageSizeMemberSearch = 60
|
||||||
PageSizeFriends = 12
|
PageSizeFriends = 12
|
||||||
PageSizeBlockList = 12
|
PageSizeBlockList = 12
|
||||||
|
PageSizePrivatePhotoGrantees = 12
|
||||||
PageSizeAdminCertification = 20
|
PageSizeAdminCertification = 20
|
||||||
PageSizeAdminFeedback = 20
|
PageSizeAdminFeedback = 20
|
||||||
PageSizeSiteGallery = 16
|
PageSizeSiteGallery = 16
|
||||||
|
|
157
pkg/controller/photo/private.go
Normal file
157
pkg/controller/photo/private.go
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
package photo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/config"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/log"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/models"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/session"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Private controller (/photo/private) to see and modify your Private Photo grants.
|
||||||
|
func Private() http.HandlerFunc {
|
||||||
|
// Reuse the upload page but with an EditPhoto variable.
|
||||||
|
tmpl := templates.Must("photo/private.html")
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
view = r.FormValue("view")
|
||||||
|
isGrantee = view == "grantee"
|
||||||
|
)
|
||||||
|
|
||||||
|
currentUser, err := session.CurrentUser(r)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Unexpected error: could not get currentUser.")
|
||||||
|
templates.Redirect(w, "/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the users.
|
||||||
|
pager := &models.Pagination{
|
||||||
|
PerPage: config.PageSizePrivatePhotoGrantees,
|
||||||
|
Sort: "updated_at desc",
|
||||||
|
}
|
||||||
|
pager.ParsePage(r)
|
||||||
|
users, err := models.PaginatePrivatePhotoList(currentUser.ID, isGrantee, pager)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't paginate users: %s", err)
|
||||||
|
templates.Redirect(w, "/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var vars = map[string]interface{}{
|
||||||
|
"IsGrantee": isGrantee,
|
||||||
|
"CountGrantee": models.CountPrivateGrantee(currentUser.ID),
|
||||||
|
"Users": users,
|
||||||
|
"Pager": pager,
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Share your private photos with a new user.
|
||||||
|
func Share() http.HandlerFunc {
|
||||||
|
tmpl := templates.Must("photo/share.html")
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// To whom?
|
||||||
|
var (
|
||||||
|
user *models.User
|
||||||
|
username = r.FormValue("to")
|
||||||
|
isRevokeAll = r.FormValue("intent") == "revoke-all"
|
||||||
|
)
|
||||||
|
|
||||||
|
if username != "" {
|
||||||
|
if u, err := models.FindUser(username); err != nil {
|
||||||
|
session.FlashError(w, r, "That username was not found, please try again.")
|
||||||
|
templates.Redirect(w, r.URL.Path)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
user = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser, err := session.CurrentUser(r)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Unexpected error: could not get currentUser.")
|
||||||
|
templates.Redirect(w, "/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Are we revoking our privates from ALL USERS?
|
||||||
|
if isRevokeAll {
|
||||||
|
models.RevokePrivatePhotosAll(currentUser.ID)
|
||||||
|
session.Flash(w, r, "Your private photos have been locked from ALL users.")
|
||||||
|
templates.Redirect(w, "/photo/private")
|
||||||
|
|
||||||
|
// Remove ALL notifications sent to ALL users who had access before.
|
||||||
|
models.RemoveNotification("__private_photos", currentUser.ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if user != nil && currentUser.ID == user.ID {
|
||||||
|
session.FlashError(w, r, "You cannot share your private photos with yourself.")
|
||||||
|
templates.Redirect(w, r.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any blocking?
|
||||||
|
if user != nil && models.IsBlocking(currentUser.ID, user.ID) && !currentUser.IsAdmin {
|
||||||
|
session.FlashError(w, r, "You are blocked from contacting this user.")
|
||||||
|
templates.Redirect(w, r.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// POSTing?
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
var (
|
||||||
|
intent = r.PostFormValue("intent")
|
||||||
|
)
|
||||||
|
|
||||||
|
// If submitting, do it and redirect.
|
||||||
|
if intent == "submit" {
|
||||||
|
models.UnlockPrivatePhotos(currentUser.ID, user.ID)
|
||||||
|
session.Flash(w, r, "Your private photos have been unlocked for %s.", user.Username)
|
||||||
|
templates.Redirect(w, "/photo/private")
|
||||||
|
|
||||||
|
// Create a notification for this.
|
||||||
|
notif := &models.Notification{
|
||||||
|
UserID: user.ID,
|
||||||
|
AboutUser: *currentUser,
|
||||||
|
Type: models.NotificationPrivatePhoto,
|
||||||
|
TableName: "__private_photos",
|
||||||
|
TableID: currentUser.ID,
|
||||||
|
Link: fmt.Sprintf("/photo/u/%s", currentUser.Username),
|
||||||
|
}
|
||||||
|
if err := models.CreateNotification(notif); err != nil {
|
||||||
|
log.Error("Couldn't create PrivatePhoto notification: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
} else if intent == "revoke" {
|
||||||
|
models.RevokePrivatePhotos(currentUser.ID, user.ID)
|
||||||
|
session.Flash(w, r, "You have revoked access to your private photos for %s.", user.Username)
|
||||||
|
templates.Redirect(w, "/photo/private")
|
||||||
|
|
||||||
|
// Remove any notification we created when the grant was given.
|
||||||
|
models.RemoveSpecificNotification(user.ID, models.NotificationPrivatePhoto, "__private_photos", currentUser.ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other intent is "preview" so the user gets the confirmation
|
||||||
|
// screen before they continue, which shows the selected user info.
|
||||||
|
}
|
||||||
|
|
||||||
|
var vars = map[string]interface{}{
|
||||||
|
"User": user,
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -12,11 +12,37 @@ import (
|
||||||
// SiteGallery controller (/photo/gallery) to view all members' public gallery pics.
|
// SiteGallery controller (/photo/gallery) to view all members' public gallery pics.
|
||||||
func SiteGallery() http.HandlerFunc {
|
func SiteGallery() http.HandlerFunc {
|
||||||
tmpl := templates.Must("photo/gallery.html")
|
tmpl := templates.Must("photo/gallery.html")
|
||||||
|
|
||||||
|
// Whitelist for ordering options.
|
||||||
|
var sortWhitelist = []string{
|
||||||
|
"created_at desc",
|
||||||
|
"created_at asc",
|
||||||
|
}
|
||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Query params.
|
// Query params.
|
||||||
var (
|
var (
|
||||||
viewStyle = r.FormValue("view") // cards (default), full
|
viewStyle = r.FormValue("view") // cards (default), full
|
||||||
|
|
||||||
|
// Search filters.
|
||||||
|
filterExplicit = r.FormValue("explicit")
|
||||||
|
filterVisibility = r.FormValue("visibility")
|
||||||
|
sort = r.FormValue("sort")
|
||||||
|
sortOK bool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Sort options.
|
||||||
|
for _, v := range sortWhitelist {
|
||||||
|
if sort == v {
|
||||||
|
sortOK = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !sortOK {
|
||||||
|
sort = "created_at desc"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults.
|
||||||
if viewStyle != "full" {
|
if viewStyle != "full" {
|
||||||
viewStyle = "cards"
|
viewStyle = "cards"
|
||||||
}
|
}
|
||||||
|
@ -31,10 +57,10 @@ func SiteGallery() http.HandlerFunc {
|
||||||
pager := &models.Pagination{
|
pager := &models.Pagination{
|
||||||
Page: 1,
|
Page: 1,
|
||||||
PerPage: config.PageSizeSiteGallery,
|
PerPage: config.PageSizeSiteGallery,
|
||||||
Sort: "created_at desc",
|
Sort: sort,
|
||||||
}
|
}
|
||||||
pager.ParsePage(r)
|
pager.ParsePage(r)
|
||||||
photos, err := models.PaginateGalleryPhotos(currentUser.ID, currentUser.IsAdmin, currentUser.Explicit, pager)
|
photos, err := models.PaginateGalleryPhotos(currentUser, filterExplicit, filterVisibility, pager)
|
||||||
|
|
||||||
// Bulk load the users associated with these photos.
|
// Bulk load the users associated with these photos.
|
||||||
var userIDs = []uint64{}
|
var userIDs = []uint64{}
|
||||||
|
@ -62,6 +88,11 @@ func SiteGallery() http.HandlerFunc {
|
||||||
"CommentMap": commentMap,
|
"CommentMap": commentMap,
|
||||||
"Pager": pager,
|
"Pager": pager,
|
||||||
"ViewStyle": viewStyle,
|
"ViewStyle": viewStyle,
|
||||||
|
|
||||||
|
// Search filters
|
||||||
|
"Sort": sort,
|
||||||
|
"FilterExplicit": filterExplicit,
|
||||||
|
"FilterVisibility": filterVisibility,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
|
|
@ -62,9 +62,15 @@ func UserPhotos() http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Has this user granted access to see their privates?
|
||||||
|
var (
|
||||||
|
isGrantee = models.IsPrivateUnlocked(user.ID, currentUser.ID) // THEY have granted US access
|
||||||
|
isGranted = models.IsPrivateUnlocked(currentUser.ID, user.ID) // WE have granted THEM access
|
||||||
|
)
|
||||||
|
|
||||||
// What set of visibilities to query?
|
// What set of visibilities to query?
|
||||||
visibility := []models.PhotoVisibility{models.PhotoPublic}
|
visibility := []models.PhotoVisibility{models.PhotoPublic}
|
||||||
if isOwnPhotos || currentUser.IsAdmin {
|
if isOwnPhotos || isGrantee || currentUser.IsAdmin {
|
||||||
visibility = append(visibility, models.PhotoFriends, models.PhotoPrivate)
|
visibility = append(visibility, models.PhotoFriends, models.PhotoPrivate)
|
||||||
} else if models.AreFriends(user.ID, currentUser.ID) {
|
} else if models.AreFriends(user.ID, currentUser.ID) {
|
||||||
visibility = append(visibility, models.PhotoFriends)
|
visibility = append(visibility, models.PhotoFriends)
|
||||||
|
@ -100,15 +106,16 @@ func UserPhotos() http.HandlerFunc {
|
||||||
commentMap := models.MapCommentCounts("photos", photoIDs)
|
commentMap := models.MapCommentCounts("photos", photoIDs)
|
||||||
|
|
||||||
var vars = map[string]interface{}{
|
var vars = map[string]interface{}{
|
||||||
"IsOwnPhotos": currentUser.ID == user.ID,
|
"IsOwnPhotos": currentUser.ID == user.ID,
|
||||||
"User": user,
|
"IsMyPrivateUnlockedFor": isGranted, // have WE granted THIS USER to see our private pics?
|
||||||
"Photos": photos,
|
"User": user,
|
||||||
"PhotoCount": models.CountPhotos(user.ID),
|
"Photos": photos,
|
||||||
"Pager": pager,
|
"PhotoCount": models.CountPhotos(user.ID),
|
||||||
"LikeMap": likeMap,
|
"Pager": pager,
|
||||||
"CommentMap": commentMap,
|
"LikeMap": likeMap,
|
||||||
"ViewStyle": viewStyle,
|
"CommentMap": commentMap,
|
||||||
"ExplicitCount": explicitCount,
|
"ViewStyle": viewStyle,
|
||||||
|
"ExplicitCount": explicitCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
|
|
@ -66,6 +66,13 @@ func View() http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Is this a private photo and are we allowed to see?
|
||||||
|
isGranted := models.IsPrivateUnlocked(user.ID, currentUser.ID)
|
||||||
|
if !isGranted && !currentUser.IsAdmin {
|
||||||
|
templates.NotFoundPage(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Get Likes information about these photos.
|
// Get Likes information about these photos.
|
||||||
likeMap := models.MapLikes(currentUser, "photos", []uint64{photo.ID})
|
likeMap := models.MapLikes(currentUser, "photos", []uint64{photo.ID})
|
||||||
commentMap := models.MapCommentCounts("photos", []uint64{photo.ID})
|
commentMap := models.MapCommentCounts("photos", []uint64{photo.ID})
|
||||||
|
|
|
@ -11,6 +11,7 @@ func AutoMigrate() {
|
||||||
DB.AutoMigrate(&User{})
|
DB.AutoMigrate(&User{})
|
||||||
DB.AutoMigrate(&ProfileField{})
|
DB.AutoMigrate(&ProfileField{})
|
||||||
DB.AutoMigrate(&Photo{})
|
DB.AutoMigrate(&Photo{})
|
||||||
|
DB.AutoMigrate(&PrivatePhoto{})
|
||||||
DB.AutoMigrate(&CertificationPhoto{})
|
DB.AutoMigrate(&CertificationPhoto{})
|
||||||
DB.AutoMigrate(&Message{})
|
DB.AutoMigrate(&Message{})
|
||||||
DB.AutoMigrate(&Friend{})
|
DB.AutoMigrate(&Friend{})
|
||||||
|
|
|
@ -38,6 +38,7 @@ const (
|
||||||
NotificationAlsoPosted = "also_posted" // forum replies
|
NotificationAlsoPosted = "also_posted" // forum replies
|
||||||
NotificationCertRejected = "cert_rejected"
|
NotificationCertRejected = "cert_rejected"
|
||||||
NotificationCertApproved = "cert_approved"
|
NotificationCertApproved = "cert_approved"
|
||||||
|
NotificationPrivatePhoto = "private_photo"
|
||||||
NotificationCustom = "custom" // custom message pushed
|
NotificationCustom = "custom" // custom message pushed
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -62,6 +63,16 @@ func RemoveNotification(tableName string, tableID uint64) error {
|
||||||
return result.Error
|
return result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveSpecificNotification to remove more specialized notifications where just removing by
|
||||||
|
// table name+ID is not adequate, e.g. for Private Photo Unlocks.
|
||||||
|
func RemoveSpecificNotification(userID uint64, t NotificationType, tableName string, tableID uint64) error {
|
||||||
|
result := DB.Where(
|
||||||
|
"user_id = ? AND type = ? AND table_name = ? AND table_id = ?",
|
||||||
|
userID, t, tableName, tableID,
|
||||||
|
).Delete(&Notification{})
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
// MarkNotificationsRead sets all a user's notifications to read.
|
// MarkNotificationsRead sets all a user's notifications to read.
|
||||||
func MarkNotificationsRead(user *User) error {
|
func MarkNotificationsRead(user *User) error {
|
||||||
return DB.Model(&Notification{}).Where(
|
return DB.Model(&Notification{}).Where(
|
||||||
|
|
|
@ -149,16 +149,26 @@ func CountExplicitPhotos(userID uint64, visibility []PhotoVisibility) (int64, er
|
||||||
return count, result.Error
|
return count, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// PaginateGalleryPhotos gets a page of all public user photos for the site gallery. Admin view
|
/*
|
||||||
// returns ALL photos regardless of Gallery status.
|
PaginateGalleryPhotos gets a page of all public user photos for the site gallery.
|
||||||
func PaginateGalleryPhotos(userID uint64, adminView bool, explicitOK bool, pager *Pagination) ([]*Photo, error) {
|
|
||||||
|
Admin view returns ALL photos regardless of Gallery status.
|
||||||
|
*/
|
||||||
|
func PaginateGalleryPhotos(user *User, filterExplicit, filterVisibility string, pager *Pagination) ([]*Photo, error) {
|
||||||
var (
|
var (
|
||||||
p = []*Photo{}
|
p = []*Photo{}
|
||||||
query *gorm.DB
|
query *gorm.DB
|
||||||
blocklist = BlockedUserIDs(userID)
|
|
||||||
friendIDs = FriendIDs(userID)
|
// Get the user ID and their preferences.
|
||||||
wheres = []string{}
|
userID = user.ID
|
||||||
placeholders = []interface{}{}
|
adminView = user.IsAdmin // Admins see everything on the site.
|
||||||
|
explicitOK = user.Explicit // User opted-in for explicit content
|
||||||
|
|
||||||
|
blocklist = BlockedUserIDs(userID)
|
||||||
|
friendIDs = FriendIDs(userID)
|
||||||
|
privateUserIDs = PrivateGrantedUserIDs(userID)
|
||||||
|
wheres = []string{}
|
||||||
|
placeholders = []interface{}{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Include ourself in our friend IDs.
|
// Include ourself in our friend IDs.
|
||||||
|
@ -166,10 +176,14 @@ func PaginateGalleryPhotos(userID uint64, adminView bool, explicitOK bool, pager
|
||||||
|
|
||||||
// You can see friends' Friend photos but only public for non-friends.
|
// You can see friends' Friend photos but only public for non-friends.
|
||||||
wheres = append(wheres,
|
wheres = append(wheres,
|
||||||
"(user_id IN ? AND visibility IN ?) OR (user_id NOT IN ? AND visibility = ?)",
|
"((user_id IN ? AND visibility IN ?) OR "+
|
||||||
|
"(user_id IN ? AND visibility IN ?) OR "+
|
||||||
|
"(user_id NOT IN ? AND visibility = ?))",
|
||||||
)
|
)
|
||||||
placeholders = append(placeholders,
|
placeholders = append(placeholders,
|
||||||
friendIDs, PhotoVisibilityFriends, friendIDs, PhotoPublic,
|
friendIDs, PhotoVisibilityFriends,
|
||||||
|
privateUserIDs, PhotoVisibilityAll,
|
||||||
|
friendIDs, PhotoPublic,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Gallery photos only.
|
// Gallery photos only.
|
||||||
|
@ -182,12 +196,21 @@ func PaginateGalleryPhotos(userID uint64, adminView bool, explicitOK bool, pager
|
||||||
placeholders = append(placeholders, blocklist)
|
placeholders = append(placeholders, blocklist)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-explicit pics unless the user opted in.
|
// Non-explicit pics unless the user opted in. Allow explicit filter setting to override.
|
||||||
if !explicitOK {
|
if filterExplicit != "" {
|
||||||
|
wheres = append(wheres, "explicit = ?")
|
||||||
|
placeholders = append(placeholders, filterExplicit == "true")
|
||||||
|
} else if !explicitOK {
|
||||||
wheres = append(wheres, "explicit = ?")
|
wheres = append(wheres, "explicit = ?")
|
||||||
placeholders = append(placeholders, false)
|
placeholders = append(placeholders, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Is the user furthermore clamping the visibility filter?
|
||||||
|
if filterVisibility != "" {
|
||||||
|
wheres = append(wheres, "visibility = ?")
|
||||||
|
placeholders = append(placeholders, filterVisibility)
|
||||||
|
}
|
||||||
|
|
||||||
// Only certified user photos.
|
// Only certified user photos.
|
||||||
wheres = append(wheres,
|
wheres = append(wheres,
|
||||||
"EXISTS (SELECT 1 FROM users WHERE id = photos.user_id AND certified = true)",
|
"EXISTS (SELECT 1 FROM users WHERE id = photos.user_id AND certified = true)",
|
||||||
|
@ -201,6 +224,14 @@ func PaginateGalleryPhotos(userID uint64, adminView bool, explicitOK bool, pager
|
||||||
// Admin view: get ALL PHOTOS on the site, period.
|
// Admin view: get ALL PHOTOS on the site, period.
|
||||||
if adminView {
|
if adminView {
|
||||||
query = DB
|
query = DB
|
||||||
|
|
||||||
|
// Admin may filter too.
|
||||||
|
if filterVisibility != "" {
|
||||||
|
query = query.Where("visibility = ?", filterVisibility)
|
||||||
|
}
|
||||||
|
if filterExplicit != "" {
|
||||||
|
query = query.Where("explicit = ?", filterExplicit == "true")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
query = DB.Where(
|
query = DB.Where(
|
||||||
strings.Join(wheres, " AND "),
|
strings.Join(wheres, " AND "),
|
||||||
|
|
147
pkg/models/private_photo.go
Normal file
147
pkg/models/private_photo.go
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrivatePhoto table to track who you have unlocked your private photos for.
|
||||||
|
type PrivatePhoto struct {
|
||||||
|
ID uint64 `gorm:"primaryKey"`
|
||||||
|
SourceUserID uint64 `gorm:"index"` // the owner of a photo
|
||||||
|
TargetUserID uint64 `gorm:"index"` // the receiver
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnlockPrivatePhotos is sourceUserId allowing targetUserId to see their private photos.
|
||||||
|
func UnlockPrivatePhotos(sourceUserID, targetUserID uint64) error {
|
||||||
|
// Did we already allow this user?
|
||||||
|
var pb *PrivatePhoto
|
||||||
|
exist := DB.Where(
|
||||||
|
"source_user_id = ? AND target_user_id = ?",
|
||||||
|
sourceUserID, targetUserID,
|
||||||
|
).First(&pb).Error
|
||||||
|
|
||||||
|
// Update existing.
|
||||||
|
if exist == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the PrivatePhoto.
|
||||||
|
pb = &PrivatePhoto{
|
||||||
|
SourceUserID: sourceUserID,
|
||||||
|
TargetUserID: targetUserID,
|
||||||
|
}
|
||||||
|
return DB.Create(pb).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokePrivatePhotos is sourceUserId revoking targetUserId to see their private photos.
|
||||||
|
func RevokePrivatePhotos(sourceUserID, targetUserID uint64) error {
|
||||||
|
result := DB.Where(
|
||||||
|
"source_user_id = ? AND target_user_id = ?",
|
||||||
|
sourceUserID, targetUserID,
|
||||||
|
).Delete(&PrivatePhoto{})
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokePrivatePhotosAll is sourceUserId revoking ALL USERS from their private photos.
|
||||||
|
func RevokePrivatePhotosAll(sourceUserID uint64) error {
|
||||||
|
result := DB.Where(
|
||||||
|
"source_user_id = ?",
|
||||||
|
sourceUserID,
|
||||||
|
).Delete(&PrivatePhoto{})
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPrivateUnlocked quickly sees if sourceUserID has unlocked private photos for targetUserID to see.
|
||||||
|
func IsPrivateUnlocked(sourceUserID, targetUserID uint64) bool {
|
||||||
|
pb := &PrivatePhoto{}
|
||||||
|
result := DB.Where(
|
||||||
|
"source_user_id = ? AND target_user_id = ?",
|
||||||
|
sourceUserID, targetUserID,
|
||||||
|
).First(&pb)
|
||||||
|
return result.Error == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountPrivateGrantee returns how many users have granted you access to their private photos.
|
||||||
|
func CountPrivateGrantee(userID uint64) int64 {
|
||||||
|
var count int64
|
||||||
|
DB.Model(&PrivatePhoto{}).Where(
|
||||||
|
"target_user_id = ?",
|
||||||
|
userID,
|
||||||
|
).Count(&count)
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrivateGrantedUserIDs returns all user IDs who have granted access for userId to see their private photos.
|
||||||
|
func PrivateGrantedUserIDs(userId uint64) []uint64 {
|
||||||
|
var (
|
||||||
|
ps = []*PrivatePhoto{}
|
||||||
|
userIDs = []uint64{userId}
|
||||||
|
)
|
||||||
|
DB.Where("target_user_id = ?", userId).Find(&ps)
|
||||||
|
for _, row := range ps {
|
||||||
|
userIDs = append(userIDs, row.SourceUserID)
|
||||||
|
}
|
||||||
|
return userIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
PaginatePrivatePhotoList views a user's list of private photo grants.
|
||||||
|
|
||||||
|
If grantee is true, it returns the list of users who have granted YOU access to see THEIR
|
||||||
|
private photos. If grantee is false, it returns the users that YOU have granted access to
|
||||||
|
see YOUR OWN private photos.
|
||||||
|
*/
|
||||||
|
func PaginatePrivatePhotoList(userID uint64, grantee bool, pager *Pagination) ([]*User, error) {
|
||||||
|
var (
|
||||||
|
pbs = []*PrivatePhoto{}
|
||||||
|
userIDs = []uint64{}
|
||||||
|
query *gorm.DB
|
||||||
|
wheres = []string{}
|
||||||
|
placeholders = []interface{}{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Which direction are we going?
|
||||||
|
if grantee {
|
||||||
|
// Return the private photo grants for whom YOU are the recipient.
|
||||||
|
wheres = append(wheres, "target_user_id = ?")
|
||||||
|
placeholders = append(placeholders, userID)
|
||||||
|
} else {
|
||||||
|
// Return the users that YOU have granted access to YOUR private pictures.
|
||||||
|
wheres = append(wheres, "source_user_id = ?")
|
||||||
|
placeholders = append(placeholders, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = DB.Where(
|
||||||
|
strings.Join(wheres, " AND "),
|
||||||
|
placeholders...,
|
||||||
|
)
|
||||||
|
|
||||||
|
query = query.Order(pager.Sort)
|
||||||
|
query.Model(&PrivatePhoto{}).Count(&pager.Total)
|
||||||
|
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&pbs)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now of these user IDs get their User objects.
|
||||||
|
for _, b := range pbs {
|
||||||
|
if grantee {
|
||||||
|
userIDs = append(userIDs, b.SourceUserID)
|
||||||
|
} else {
|
||||||
|
userIDs = append(userIDs, b.TargetUserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetUsers(userIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save photo.
|
||||||
|
func (pb *PrivatePhoto) Save() error {
|
||||||
|
result := DB.Save(pb)
|
||||||
|
return result.Error
|
||||||
|
}
|
|
@ -46,6 +46,8 @@ func New() http.Handler {
|
||||||
mux.Handle("/photo/edit", middleware.LoginRequired(photo.Edit()))
|
mux.Handle("/photo/edit", middleware.LoginRequired(photo.Edit()))
|
||||||
mux.Handle("/photo/delete", middleware.LoginRequired(photo.Delete()))
|
mux.Handle("/photo/delete", middleware.LoginRequired(photo.Delete()))
|
||||||
mux.Handle("/photo/certification", middleware.LoginRequired(photo.Certification()))
|
mux.Handle("/photo/certification", middleware.LoginRequired(photo.Certification()))
|
||||||
|
mux.Handle("/photo/private", middleware.LoginRequired(photo.Private()))
|
||||||
|
mux.Handle("/photo/private/share", middleware.LoginRequired(photo.Share()))
|
||||||
mux.Handle("/messages", middleware.LoginRequired(inbox.Inbox()))
|
mux.Handle("/messages", middleware.LoginRequired(inbox.Inbox()))
|
||||||
mux.Handle("/messages/read/", middleware.LoginRequired(inbox.Inbox()))
|
mux.Handle("/messages/read/", middleware.LoginRequired(inbox.Inbox()))
|
||||||
mux.Handle("/messages/compose", middleware.LoginRequired(inbox.Compose()))
|
mux.Handle("/messages/compose", middleware.LoginRequired(inbox.Compose()))
|
||||||
|
|
|
@ -39,6 +39,12 @@
|
||||||
.has-text-private-light {
|
.has-text-private-light {
|
||||||
color: #FF99FF;
|
color: #FF99FF;
|
||||||
}
|
}
|
||||||
|
.hero.is-private {
|
||||||
|
background-color: #b748c7;
|
||||||
|
}
|
||||||
|
.hero.is-private.is-bold {
|
||||||
|
background-image: linear-gradient(141deg,#b329b1 0,#9948c7 71%,#7156d2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile: notification badge near the hamburger menu */
|
/* Mobile: notification badge near the hamburger menu */
|
||||||
.nonshy-mobile-notification {
|
.nonshy-mobile-notification {
|
||||||
|
@ -56,4 +62,9 @@
|
||||||
/* Bulma hack: full-width columns in photo card headers */
|
/* Bulma hack: full-width columns in photo card headers */
|
||||||
.nonshy-fullwidth {
|
.nonshy-fullwidth {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapsible cards for mobile (e.g. filter cards) */
|
||||||
|
.card.nonshy-collapsible-mobile {
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
|
@ -89,4 +89,41 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
target.classList.remove("is-active");
|
target.classList.remove("is-active");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Collapsible cards for mobile view (e.g. People Search filters box)
|
||||||
|
(document.querySelectorAll(".card.nonshy-collapsible-mobile") || []).forEach(node => {
|
||||||
|
const header = node.querySelector(".card-header"),
|
||||||
|
body = node.querySelector(".card-content"),
|
||||||
|
icon = header.querySelector("button.card-header-icon > .icon > i");
|
||||||
|
|
||||||
|
// Icon classes.
|
||||||
|
const iconExpanded = "fa-angle-up",
|
||||||
|
iconContracted = "fa-angle-down";
|
||||||
|
|
||||||
|
// If we are already on mobile, hide the body now.
|
||||||
|
if (screen.width <= 768) {
|
||||||
|
body.style.display = "none";
|
||||||
|
if (icon !== null) {
|
||||||
|
icon.classList.remove(iconExpanded);
|
||||||
|
icon.classList.add(iconContracted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add click toggle handler to the header.
|
||||||
|
header.addEventListener("click", () => {
|
||||||
|
if (body.style.display === "none") {
|
||||||
|
body.style.display = "block";
|
||||||
|
if (icon !== null) {
|
||||||
|
icon.classList.remove(iconContracted);
|
||||||
|
icon.classList.add(iconExpanded);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body.style.display = "none";
|
||||||
|
if (icon !== null) {
|
||||||
|
icon.classList.remove(iconExpanded);
|
||||||
|
icon.classList.add(iconContracted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
});
|
});
|
|
@ -93,6 +93,13 @@
|
||||||
Upload Photos
|
Upload Photos
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/photo/private">
|
||||||
|
<span class="icon"><i class="fa fa-eye"></i></span>
|
||||||
|
Manage Private Photos
|
||||||
|
<span class="tag is-success ml-1">NEW!</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/settings">
|
<a href="/settings">
|
||||||
<span class="icon"><i class="fa fa-edit"></i></span>
|
<span class="icon"><i class="fa fa-edit"></i></span>
|
||||||
|
@ -250,6 +257,13 @@
|
||||||
<a href="/u/{{.AboutUser.Username}}"><strong>{{.AboutUser.Username}}</strong></a>
|
<a href="/u/{{.AboutUser.Username}}"><strong>{{.AboutUser.Username}}</strong></a>
|
||||||
accepted your friend request!
|
accepted your friend request!
|
||||||
</span>
|
</span>
|
||||||
|
{{else if eq .Type "private_photo"}}
|
||||||
|
<span class="icon"><i class="fa fa-unlock has-text-private"></i></span>
|
||||||
|
<span>
|
||||||
|
<a href="/u/{{.AboutUser.Username}}"><strong>{{.AboutUser.Username}}</strong></a>
|
||||||
|
has granted you access to see their
|
||||||
|
<a href="{{.Link}}" class="has-text-private">private photos</a>!
|
||||||
|
</span>
|
||||||
{{else if eq .Type "cert_approved"}}
|
{{else if eq .Type "cert_approved"}}
|
||||||
<span class="icon"><i class="fa fa-certificate has-text-success"></i></span>
|
<span class="icon"><i class="fa fa-certificate has-text-success"></i></span>
|
||||||
<span>
|
<span>
|
||||||
|
|
|
@ -35,11 +35,16 @@
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
|
|
||||||
<div class="card">
|
<div class="card nonshy-collapsible-mobile">
|
||||||
<header class="card-header">
|
<header class="card-header has-background-link-light">
|
||||||
<p class="card-header-title">
|
<p class="card-header-title">
|
||||||
Search Filters
|
Search Filters
|
||||||
</p>
|
</p>
|
||||||
|
<button class="card-header-icon" type="button">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-angle-up"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="columns is-multiline">
|
<div class="columns is-multiline">
|
||||||
|
|
|
@ -157,6 +157,10 @@
|
||||||
<span class="icon"><i class="fa fa-upload"></i></span>
|
<span class="icon"><i class="fa fa-upload"></i></span>
|
||||||
<span>Upload Photo</span>
|
<span>Upload Photo</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a class="navbar-item" href="/photo/private">
|
||||||
|
<span class="icon"><i class="fa fa-eye"></i></span>
|
||||||
|
<span>Private Photos</span>
|
||||||
|
</a>
|
||||||
<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>
|
||||||
|
|
|
@ -166,7 +166,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div class="level">
|
<div class="level{{if .IsOwnPhotos}}mb-0{{end}}">
|
||||||
<div class="level-left">
|
<div class="level-left">
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
<span>
|
<span>
|
||||||
|
@ -194,6 +194,102 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<form action="/photo/gallery" method="GET">
|
||||||
|
|
||||||
|
<div class="card nonshy-collapsible-mobile">
|
||||||
|
<header class="card-header has-background-link-light">
|
||||||
|
<p class="card-header-title">
|
||||||
|
Search Filters <span class="tag is-success ml-2">NEW</span>
|
||||||
|
</p>
|
||||||
|
<button class="card-header-icon" type="button">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-angle-up"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="columns is-multiline mb-0">
|
||||||
|
|
||||||
|
{{if .CurrentUser.Explicit}}
|
||||||
|
<div class="column">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="explicit">Explicit:</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select id="explicit" name="explicit">
|
||||||
|
<option value="">Show all</option>
|
||||||
|
<option value="true"{{if eq .FilterExplicit "true"}} selected{{end}}>Only explicit</option>
|
||||||
|
<option value="false"{{if eq .FilterExplicit "false"}} selected{{end}}>Hide explicit</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="visibility">Visibility:</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select id="visibility" name="visibility">
|
||||||
|
<option value="">All photos</option>
|
||||||
|
<option value="public"{{if eq .FilterVisibility "public"}} selected{{end}}>Public 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>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="sort">Sort by:</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select id="sort" name="sort">
|
||||||
|
<option value="created_at desc"{{if eq .Sort "created_at desc"}} selected{{end}}>Most recent</option>
|
||||||
|
<option value="created_at asc"{{if eq .Sort "created_at asc"}} selected{{end}}>Oldest first</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="has-text-centered">
|
||||||
|
<a href="/photo/gallery" class="button">Reset</a>
|
||||||
|
<button type="submit" class="button is-success">
|
||||||
|
Apply Filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if .IsOwnPhotos}}
|
||||||
|
<div class="block">
|
||||||
|
<a href="/photo/private" class="has-text-private">
|
||||||
|
<span class="icon"><i class="fa fa-lock"></i></span>
|
||||||
|
<span>Manage who can see <strong>my</strong> private photos</span>
|
||||||
|
<span class="tag is-success">NEW</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{else if and (not .IsSiteGallery) (not .IsMyPrivateUnlockedFor)}}
|
||||||
|
<div class="block">
|
||||||
|
<a href="/photo/private/share?to={{.User.Username}}" class="has-text-private">
|
||||||
|
<span class="icon"><i class="fa fa-unlock"></i></span>
|
||||||
|
<span>Grant <strong>{{.User.Username}}</strong> access to see <strong>my</strong> private photos</span>
|
||||||
|
<span class="tag is-success">NEW</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{else if and (not .IsSiteGallery) .IsMyPrivateUnlockedFor}}
|
||||||
|
<div class="block">
|
||||||
|
<span class="icon"><i class="fa fa-unlock has-text-private"></i></span>
|
||||||
|
<span>You had granted <strong>{{.User.Username}}</strong> access to see <strong>your</strong> private photos.</span>
|
||||||
|
<a href="/photo/private">Manage that here.</a>
|
||||||
|
<span class="tag is-success">NEW</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{template "pager" .}}
|
{{template "pager" .}}
|
||||||
|
|
||||||
<!-- "Full" view style? (blog style) -->
|
<!-- "Full" view style? (blog style) -->
|
||||||
|
|
157
web/templates/photo/private.html
Normal file
157
web/templates/photo/private.html
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
{{define "title"}}Private Photos{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<div class="container">
|
||||||
|
{{$Root := .}}
|
||||||
|
<section class="hero is-private is-bold">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title has-text-light">
|
||||||
|
<i class="fa fa-eye mr-2"></i>
|
||||||
|
Private Photos
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="p-4 is-text-centered">
|
||||||
|
<div class="level">
|
||||||
|
<div class="level-item">
|
||||||
|
<div class="tabs is-toggle">
|
||||||
|
<ul>
|
||||||
|
<li{{if not .IsGrantee}} class="is-active"{{end}}>
|
||||||
|
<a href="/photo/private">My Shares</a>
|
||||||
|
</li>
|
||||||
|
<li{{if .IsGrantee}} class="is-active"{{end}}>
|
||||||
|
<a href="/photo/private?view=grantee">
|
||||||
|
Shared With Me
|
||||||
|
{{if .CountGrantee}}
|
||||||
|
<span class="tag is-grey ml-2">{{.CountGrantee}}</span>
|
||||||
|
{{end}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4">
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
{{if .IsGrantee}}
|
||||||
|
{{.Pager.Total}} member{{Pluralize64 .Pager.Total}}
|
||||||
|
{{Pluralize64 .Pager.Total "has" "have"}}
|
||||||
|
granted you access to see their private photos
|
||||||
|
{{else}}
|
||||||
|
You have granted access to {{.Pager.Total}} member{{Pluralize64 .Pager.Total}}
|
||||||
|
to see your private photos
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
(page {{.Pager.Page}} of {{.Pager.Pages}}).
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if not .IsGrantee}}
|
||||||
|
<div class="columns is-gapless is-centered">
|
||||||
|
<div class="column is-narrow mx-1 my-2">
|
||||||
|
<a href="/photo/private/share" class="button is-primary is-outlined is-fullwidth">
|
||||||
|
<span class="icon"><i class="fa fa-plus"></i></span>
|
||||||
|
<span>Add new share</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow mx-1 my-2">
|
||||||
|
<a href="/photo/private/share?intent=revoke-all" class="button is-danger is-outlined is-fullwidth"
|
||||||
|
onclick="return confirm('Are you sure you want to lock your Private Photos from ALL users?')">
|
||||||
|
<span class="icon"><i class="fa fa-lock"></i></span>
|
||||||
|
<span>Revoke ALL Shares</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<nav class="pagination" role="navigation" aria-label="pagination">
|
||||||
|
<a class="pagination-previous{{if not .Pager.HasPrevious}} is-disabled{{end}}" title="Previous"
|
||||||
|
href="{{.Request.URL.Path}}?{{if .IsGrantee}}view=grantee&{{end}}page={{.Pager.Previous}}">Previous</a>
|
||||||
|
<a class="pagination-next{{if not .Pager.HasNext}} is-disabled{{end}}" title="Next"
|
||||||
|
href="{{.Request.URL.Path}}?{{if .IsGrantee}}view=grantee&{{end}}page={{.Pager.Next}}">Next page</a>
|
||||||
|
<ul class="pagination-list">
|
||||||
|
{{$Root := .}}
|
||||||
|
{{range .Pager.Iter}}
|
||||||
|
<li>
|
||||||
|
<a class="pagination-link{{if .IsCurrent}} is-current{{end}}"
|
||||||
|
aria-label="Page {{.Page}}"
|
||||||
|
href="{{$Root.Request.URL.Path}}?{{if $Root.IsGrantee}}view=grantee&{{end}}page={{.Page}}">
|
||||||
|
{{.Page}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
|
||||||
|
{{range .Users}}
|
||||||
|
<div class="column is-half-tablet is-one-third-desktop">
|
||||||
|
|
||||||
|
<form action="/photo/private/share" method="POST">
|
||||||
|
{{InputCSRF}}
|
||||||
|
<input type="hidden" name="to" value="{{.Username}}">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="media block">
|
||||||
|
<div class="media-left">
|
||||||
|
<figure class="image is-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 class="media-content">
|
||||||
|
<p class="title is-4">
|
||||||
|
<a href="/u/{{.Username}}" class="has-text-dark">{{.NameOrUsername}}</a>
|
||||||
|
</p>
|
||||||
|
<p class="subtitle is-6">
|
||||||
|
<span class="icon"><i class="fa fa-user"></i></span>
|
||||||
|
<a href="/u/{{.Username}}">{{.Username}}</a>
|
||||||
|
{{if not .Certified}}
|
||||||
|
<span class="has-text-danger">
|
||||||
|
<span class="icon"><i class="fa fa-certificate"></i></span>
|
||||||
|
<span>Not Certified!</span>
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .IsAdmin}}
|
||||||
|
<span class="has-text-danger">
|
||||||
|
<span class="icon"><i class="fa fa-gavel"></i></span>
|
||||||
|
<span>Admin</span>
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{if not $Root.IsGrantee}}
|
||||||
|
<footer class="card-footer">
|
||||||
|
<button type="submit" name="intent" value="revoke" class="card-footer-item button is-danger is-outlined"
|
||||||
|
onclick="return confirm('Are you sure you want to revoke private photo access to this user?')">
|
||||||
|
<span class="icon"><i class="fa fa-xmark"></i></span>
|
||||||
|
<span>Revoke Access</span>
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{{end}}<!-- range .Friends -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
117
web/templates/photo/share.html
Normal file
117
web/templates/photo/share.html
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
{{define "title"}}Share Private Photos{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<div class="container">
|
||||||
|
<section class="hero is-info is-bold">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">
|
||||||
|
Share Private Photos
|
||||||
|
</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>Share Private Photos</span>
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
|
||||||
|
<!-- Show the introduction or selected user profile -->
|
||||||
|
{{if not .User}}
|
||||||
|
<div class="block">
|
||||||
|
You may use this page to grant access to your
|
||||||
|
<span class="has-text-private">
|
||||||
|
<i class="fa fa-eye"></i>
|
||||||
|
Private Photos
|
||||||
|
</span>
|
||||||
|
to another member on this site by entering their
|
||||||
|
username below.
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="block">
|
||||||
|
Confirm that you wish to grant <strong>{{.User.Username}}</strong>
|
||||||
|
access to view your
|
||||||
|
<span class="has-text-private">
|
||||||
|
<i class="fa fa-eye"></i>
|
||||||
|
Private Photos
|
||||||
|
</span>
|
||||||
|
by clicking the button below.
|
||||||
|
</div>
|
||||||
|
<div class="media block">
|
||||||
|
<div class="media-left">
|
||||||
|
<figure class="image is-64x64">
|
||||||
|
{{if .User.ProfilePhoto.ID}}
|
||||||
|
<img src="{{PhotoURL .User.ProfilePhoto.CroppedFilename}}">
|
||||||
|
{{else}}
|
||||||
|
<img src="/static/img/shy.png">
|
||||||
|
{{end}}
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<p class="title is-4">{{.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>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- Show the relevant form -->
|
||||||
|
<form action="/photo/private/share" method="POST">
|
||||||
|
{{InputCSRF}}
|
||||||
|
|
||||||
|
{{if .User}}
|
||||||
|
<input type="hidden" name="to" value="{{.User.Username}}">
|
||||||
|
{{else}}
|
||||||
|
<div class="field block">
|
||||||
|
<label for="to" class="label">Share with Username:</label>
|
||||||
|
<input type="text" class="input"
|
||||||
|
name="to" id="to"
|
||||||
|
placeholder="username">
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="field has-text-centered">
|
||||||
|
<button type="submit" class="button is-success"
|
||||||
|
name="intent"
|
||||||
|
value="{{if .User}}submit{{else}}preview{{end}}">
|
||||||
|
{{if .User}}
|
||||||
|
<span class="icon"><i class="fa fa-unlock"></i></span>
|
||||||
|
<span>Confirm Share</span>
|
||||||
|
{{else}}
|
||||||
|
Continue
|
||||||
|
{{end}}
|
||||||
|
</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}}
|
|
@ -214,7 +214,7 @@
|
||||||
name="visibility"
|
name="visibility"
|
||||||
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">
|
<strong class="has-text-link ml-1">
|
||||||
<span>Public</span>
|
<span>Public</span>
|
||||||
<span class="icon"><i class="fa fa-eye"></i></span>
|
<span class="icon"><i class="fa fa-eye"></i></span>
|
||||||
</strong>
|
</strong>
|
||||||
|
@ -231,7 +231,7 @@
|
||||||
name="visibility"
|
name="visibility"
|
||||||
value="friends"
|
value="friends"
|
||||||
{{if eq .EditPhoto.Visibility "friends"}}checked{{end}}>
|
{{if eq .EditPhoto.Visibility "friends"}}checked{{end}}>
|
||||||
<strong class="has-text-warning-dark">
|
<strong class="has-text-warning-dark ml-1">
|
||||||
<span>Friends only</span>
|
<span>Friends only</span>
|
||||||
<span class="icon"><i class="fa fa-user-group"></i></span>
|
<span class="icon"><i class="fa fa-user-group"></i></span>
|
||||||
</strong>
|
</strong>
|
||||||
|
@ -248,7 +248,7 @@
|
||||||
name="visibility"
|
name="visibility"
|
||||||
value="private"
|
value="private"
|
||||||
{{if eq .EditPhoto.Visibility "private"}}checked{{end}}>
|
{{if eq .EditPhoto.Visibility "private"}}checked{{end}}>
|
||||||
<strong class="has-text-private">
|
<strong class="has-text-private ml-1">
|
||||||
<span>Private</span>
|
<span>Private</span>
|
||||||
<span class="icon"><i class="fa fa-lock"></i></span>
|
<span class="icon"><i class="fa fa-lock"></i></span>
|
||||||
</strong>
|
</strong>
|
||||||
|
@ -258,6 +258,15 @@
|
||||||
granted access (the latter feature is coming soon!)
|
granted access (the latter feature is coming soon!)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="field mb-5">
|
<div class="field mb-5">
|
||||||
|
|
Loading…
Reference in New Issue
Block a user