diff --git a/pkg/config/page_sizes.go b/pkg/config/page_sizes.go index a9a614e..236f17f 100644 --- a/pkg/config/page_sizes.go +++ b/pkg/config/page_sizes.go @@ -12,6 +12,7 @@ var ( PageSizeMemberSearch = 60 PageSizeFriends = 12 PageSizeBlockList = 12 + PageSizePrivatePhotoGrantees = 12 PageSizeAdminCertification = 20 PageSizeAdminFeedback = 20 PageSizeSiteGallery = 16 diff --git a/pkg/controller/photo/private.go b/pkg/controller/photo/private.go new file mode 100644 index 0000000..8c69845 --- /dev/null +++ b/pkg/controller/photo/private.go @@ -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 + } + }) +} diff --git a/pkg/controller/photo/site_gallery.go b/pkg/controller/photo/site_gallery.go index a42ce80..359e076 100644 --- a/pkg/controller/photo/site_gallery.go +++ b/pkg/controller/photo/site_gallery.go @@ -12,11 +12,37 @@ import ( // SiteGallery controller (/photo/gallery) to view all members' public gallery pics. func SiteGallery() http.HandlerFunc { 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) { // Query params. var ( 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" { viewStyle = "cards" } @@ -31,10 +57,10 @@ func SiteGallery() http.HandlerFunc { pager := &models.Pagination{ Page: 1, PerPage: config.PageSizeSiteGallery, - Sort: "created_at desc", + Sort: sort, } 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. var userIDs = []uint64{} @@ -62,6 +88,11 @@ func SiteGallery() http.HandlerFunc { "CommentMap": commentMap, "Pager": pager, "ViewStyle": viewStyle, + + // Search filters + "Sort": sort, + "FilterExplicit": filterExplicit, + "FilterVisibility": filterVisibility, } if err := tmpl.Execute(w, r, vars); err != nil { diff --git a/pkg/controller/photo/user_gallery.go b/pkg/controller/photo/user_gallery.go index 47306cb..0ad749e 100644 --- a/pkg/controller/photo/user_gallery.go +++ b/pkg/controller/photo/user_gallery.go @@ -62,9 +62,15 @@ func UserPhotos() http.HandlerFunc { 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? visibility := []models.PhotoVisibility{models.PhotoPublic} - if isOwnPhotos || currentUser.IsAdmin { + if isOwnPhotos || isGrantee || currentUser.IsAdmin { visibility = append(visibility, models.PhotoFriends, models.PhotoPrivate) } else if models.AreFriends(user.ID, currentUser.ID) { visibility = append(visibility, models.PhotoFriends) @@ -100,15 +106,16 @@ func UserPhotos() http.HandlerFunc { commentMap := models.MapCommentCounts("photos", photoIDs) var vars = map[string]interface{}{ - "IsOwnPhotos": currentUser.ID == user.ID, - "User": user, - "Photos": photos, - "PhotoCount": models.CountPhotos(user.ID), - "Pager": pager, - "LikeMap": likeMap, - "CommentMap": commentMap, - "ViewStyle": viewStyle, - "ExplicitCount": explicitCount, + "IsOwnPhotos": currentUser.ID == user.ID, + "IsMyPrivateUnlockedFor": isGranted, // have WE granted THIS USER to see our private pics? + "User": user, + "Photos": photos, + "PhotoCount": models.CountPhotos(user.ID), + "Pager": pager, + "LikeMap": likeMap, + "CommentMap": commentMap, + "ViewStyle": viewStyle, + "ExplicitCount": explicitCount, } if err := tmpl.Execute(w, r, vars); err != nil { diff --git a/pkg/controller/photo/view.go b/pkg/controller/photo/view.go index 7e6db8a..01fbdbc 100644 --- a/pkg/controller/photo/view.go +++ b/pkg/controller/photo/view.go @@ -66,6 +66,13 @@ func View() http.HandlerFunc { 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. likeMap := models.MapLikes(currentUser, "photos", []uint64{photo.ID}) commentMap := models.MapCommentCounts("photos", []uint64{photo.ID}) diff --git a/pkg/models/models.go b/pkg/models/models.go index b41447b..2dd56cd 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -11,6 +11,7 @@ func AutoMigrate() { DB.AutoMigrate(&User{}) DB.AutoMigrate(&ProfileField{}) DB.AutoMigrate(&Photo{}) + DB.AutoMigrate(&PrivatePhoto{}) DB.AutoMigrate(&CertificationPhoto{}) DB.AutoMigrate(&Message{}) DB.AutoMigrate(&Friend{}) diff --git a/pkg/models/notification.go b/pkg/models/notification.go index 2e9fd7a..05186f7 100644 --- a/pkg/models/notification.go +++ b/pkg/models/notification.go @@ -38,6 +38,7 @@ const ( NotificationAlsoPosted = "also_posted" // forum replies NotificationCertRejected = "cert_rejected" NotificationCertApproved = "cert_approved" + NotificationPrivatePhoto = "private_photo" NotificationCustom = "custom" // custom message pushed ) @@ -62,6 +63,16 @@ func RemoveNotification(tableName string, tableID uint64) 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. func MarkNotificationsRead(user *User) error { return DB.Model(&Notification{}).Where( diff --git a/pkg/models/photo.go b/pkg/models/photo.go index 280ce90..cfbd33b 100644 --- a/pkg/models/photo.go +++ b/pkg/models/photo.go @@ -149,16 +149,26 @@ func CountExplicitPhotos(userID uint64, visibility []PhotoVisibility) (int64, er 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. -func PaginateGalleryPhotos(userID uint64, adminView bool, explicitOK bool, pager *Pagination) ([]*Photo, error) { +/* +PaginateGalleryPhotos gets a page of all public user photos for the site gallery. + +Admin view returns ALL photos regardless of Gallery status. +*/ +func PaginateGalleryPhotos(user *User, filterExplicit, filterVisibility string, pager *Pagination) ([]*Photo, error) { var ( - p = []*Photo{} - query *gorm.DB - blocklist = BlockedUserIDs(userID) - friendIDs = FriendIDs(userID) - wheres = []string{} - placeholders = []interface{}{} + p = []*Photo{} + query *gorm.DB + + // Get the user ID and their preferences. + userID = user.ID + 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. @@ -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. 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, - friendIDs, PhotoVisibilityFriends, friendIDs, PhotoPublic, + friendIDs, PhotoVisibilityFriends, + privateUserIDs, PhotoVisibilityAll, + friendIDs, PhotoPublic, ) // Gallery photos only. @@ -182,12 +196,21 @@ func PaginateGalleryPhotos(userID uint64, adminView bool, explicitOK bool, pager placeholders = append(placeholders, blocklist) } - // Non-explicit pics unless the user opted in. - if !explicitOK { + // Non-explicit pics unless the user opted in. Allow explicit filter setting to override. + if filterExplicit != "" { + wheres = append(wheres, "explicit = ?") + placeholders = append(placeholders, filterExplicit == "true") + } else if !explicitOK { wheres = append(wheres, "explicit = ?") 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. wheres = append(wheres, "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. if adminView { query = DB + + // Admin may filter too. + if filterVisibility != "" { + query = query.Where("visibility = ?", filterVisibility) + } + if filterExplicit != "" { + query = query.Where("explicit = ?", filterExplicit == "true") + } } else { query = DB.Where( strings.Join(wheres, " AND "), diff --git a/pkg/models/private_photo.go b/pkg/models/private_photo.go new file mode 100644 index 0000000..8fc80c2 --- /dev/null +++ b/pkg/models/private_photo.go @@ -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 +} diff --git a/pkg/router/router.go b/pkg/router/router.go index bf2d684..56d5a11 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -46,6 +46,8 @@ func New() http.Handler { mux.Handle("/photo/edit", middleware.LoginRequired(photo.Edit())) mux.Handle("/photo/delete", middleware.LoginRequired(photo.Delete())) 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/read/", middleware.LoginRequired(inbox.Inbox())) mux.Handle("/messages/compose", middleware.LoginRequired(inbox.Compose())) diff --git a/web/static/css/theme.css b/web/static/css/theme.css index c3113c2..de31639 100644 --- a/web/static/css/theme.css +++ b/web/static/css/theme.css @@ -39,6 +39,12 @@ .has-text-private-light { 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 */ .nonshy-mobile-notification { @@ -56,4 +62,9 @@ /* Bulma hack: full-width columns in photo card headers */ .nonshy-fullwidth { width: 100%; +} + +/* Collapsible cards for mobile (e.g. filter cards) */ +.card.nonshy-collapsible-mobile { + cursor: pointer; } \ No newline at end of file diff --git a/web/static/js/bulma.js b/web/static/js/bulma.js index f31e3e3..842626a 100644 --- a/web/static/js/bulma.js +++ b/web/static/js/bulma.js @@ -89,4 +89,41 @@ document.addEventListener('DOMContentLoaded', () => { 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); + } + } + }); + }) }); \ No newline at end of file diff --git a/web/templates/account/dashboard.html b/web/templates/account/dashboard.html index c223235..38f39ca 100644 --- a/web/templates/account/dashboard.html +++ b/web/templates/account/dashboard.html @@ -93,6 +93,13 @@ Upload Photos +
  • + + + Manage Private Photos + NEW! + +
  • @@ -250,6 +257,13 @@ {{.AboutUser.Username}} accepted your friend request! + {{else if eq .Type "private_photo"}} + + + {{.AboutUser.Username}} + has granted you access to see their + private photos! + {{else if eq .Type "cert_approved"}} diff --git a/web/templates/account/search.html b/web/templates/account/search.html index 499aa63..e3eeec7 100644 --- a/web/templates/account/search.html +++ b/web/templates/account/search.html @@ -35,11 +35,16 @@
    -
    -
    +
    +
    diff --git a/web/templates/base.html b/web/templates/base.html index 0b6478f..5cc21ef 100644 --- a/web/templates/base.html +++ b/web/templates/base.html @@ -157,6 +157,10 @@ Upload Photo + + + Private Photos + Settings diff --git a/web/templates/photo/gallery.html b/web/templates/photo/gallery.html index dd41367..2aa9e78 100644 --- a/web/templates/photo/gallery.html +++ b/web/templates/photo/gallery.html @@ -166,7 +166,7 @@
    -
    +
    @@ -194,6 +194,102 @@
    +
    + + {{if .IsOwnPhotos}} + + {{else if and (not .IsSiteGallery) (not .IsMyPrivateUnlockedFor)}} + + {{else if and (not .IsSiteGallery) .IsMyPrivateUnlockedFor}} +
    + + You had granted {{.User.Username}} access to see your private photos. + Manage that here. + NEW +
    + {{end}} + {{template "pager" .}} diff --git a/web/templates/photo/private.html b/web/templates/photo/private.html new file mode 100644 index 0000000..d038513 --- /dev/null +++ b/web/templates/photo/private.html @@ -0,0 +1,157 @@ +{{define "title"}}Private Photos{{end}} +{{define "content"}} +
    + {{$Root := .}} +
    +
    +
    +

    + + Private Photos +

    +
    +
    +
    + + + +
    + +
    + {{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}}). +
    + + {{if not .IsGrantee}} + + {{end}} + +
    + +
    + +
    + + {{range .Users}} +
    + +
    + {{InputCSRF}} + + +
    +
    +
    + +
    +

    + {{.NameOrUsername}} +

    +

    + + {{.Username}} + {{if not .Certified}} + + + Not Certified! + + {{end}} + + {{if .IsAdmin}} + + + Admin + + {{end}} +

    +
    +
    +
    + {{if not $Root.IsGrantee}} +
    + +
    + {{end}} +
    +
    + +
    + {{end}} +
    + +
    +
    +{{end}} \ No newline at end of file diff --git a/web/templates/photo/share.html b/web/templates/photo/share.html new file mode 100644 index 0000000..0b0597e --- /dev/null +++ b/web/templates/photo/share.html @@ -0,0 +1,117 @@ +{{define "title"}}Share Private Photos{{end}} +{{define "content"}} +
    +
    +
    +
    +

    + Share Private Photos +

    +
    +
    +
    + +
    +
    +
    + +
    + +
    + + + {{if not .User}} +
    + You may use this page to grant access to your + + + Private Photos + + to another member on this site by entering their + username below. +
    + {{else}} +
    + Confirm that you wish to grant {{.User.Username}} + access to view your + + + Private Photos + + by clicking the button below. +
    +
    +
    +
    + {{if .User.ProfilePhoto.ID}} + + {{else}} + + {{end}} +
    +
    +
    +

    {{.NameOrUsername}}

    +

    + + {{.User.Username}} +

    +
    +
    + {{end}} + + +
    + {{InputCSRF}} + + {{if .User}} + + {{else}} +
    + + +
    + {{end}} + +
    + +
    +
    + +
    +
    + +
    +
    +
    + +
    + + +{{end}} \ No newline at end of file diff --git a/web/templates/photo/upload.html b/web/templates/photo/upload.html index c3f741a..c6949b0 100644 --- a/web/templates/photo/upload.html +++ b/web/templates/photo/upload.html @@ -214,7 +214,7 @@ name="visibility" value="public" {{if or (not .EditPhoto) (eq .EditPhoto.Visibility "public")}}checked{{end}}> - + Public @@ -231,7 +231,7 @@ name="visibility" value="friends" {{if eq .EditPhoto.Visibility "friends"}}checked{{end}}> - + Friends only @@ -248,7 +248,7 @@ name="visibility" value="private" {{if eq .EditPhoto.Visibility "private"}}checked{{end}}> - + Private @@ -258,6 +258,15 @@ granted access (the latter feature is coming soon!)

    + +
    + +
    + Notice: 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 can limit the full-size photo's visibility; but the square cropped + thumbnail is always seen to logged-in members.