From 147a9162ba6800235eb400817449568a2d427043 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Wed, 7 Aug 2024 23:05:23 -0700 Subject: [PATCH] Mute specific friends new photo upload notifications --- pkg/controller/comment/subscription.go | 48 ++++++++++++++++++++------ pkg/controller/photo/user_gallery.go | 11 ++++++ pkg/models/comment.go | 10 ++++++ pkg/models/notification.go | 21 ++++++++++- pkg/models/subscription.go | 27 +++++++++++++-- web/templates/account/dashboard.html | 11 +++++- web/templates/photo/gallery.html | 19 +++++++++- 7 files changed, 132 insertions(+), 15 deletions(-) diff --git a/pkg/controller/comment/subscription.go b/pkg/controller/comment/subscription.go index 38d1ae2..ddb048c 100644 --- a/pkg/controller/comment/subscription.go +++ b/pkg/controller/comment/subscription.go @@ -28,12 +28,26 @@ func Subscription() http.HandlerFunc { templates.Redirect(w, "/") return } else { - if idInt, err := strconv.Atoi(idStr); err != nil { - session.FlashError(w, r, "Comment table ID invalid.") - templates.Redirect(w, "/") - return - } else { - tableID = uint64(idInt) + // Is the table_id expected to be a username? + switch tableName { + case "friend.photos": + // Special "Friend uploaded a new photo" opt-out. + if user, err := models.FindUser(idStr); err != nil { + session.FlashError(w, r, "Username not found!") + templates.Redirect(w, "/") + return + } else { + tableID = user.ID + } + default: + // Integer IDs in all other cases. + if idInt, err := strconv.Atoi(idStr); err != nil { + session.FlashError(w, r, "Comment table ID invalid.") + templates.Redirect(w, "/") + return + } else { + tableID = uint64(idInt) + } } } @@ -47,7 +61,7 @@ func Subscription() http.HandlerFunc { } // Validate everything else. - if _, ok := models.CommentableTables[tableName]; !ok { + if _, ok := models.SubscribableTables[tableName]; !ok { session.FlashError(w, r, "You can not comment on that.") templates.Redirect(w, "/") return @@ -61,6 +75,12 @@ func Subscription() http.HandlerFunc { return } + // Language to use in the flash messages. + var kind = "comments" + if tableName == "friend.photos" { + kind = "new photo uploads" + } + // Get their subscription. sub, err := models.GetSubscription(currentUser, tableName, tableID) if err != nil { @@ -69,7 +89,15 @@ func Subscription() http.HandlerFunc { if _, err := models.SubscribeTo(currentUser, tableName, tableID); err != nil { session.FlashError(w, r, "Couldn't create subscription: %s", err) } else { - session.Flash(w, r, "You will now be notified about comments on this page.") + session.Flash(w, r, "You will now be notified about %s on this page.", kind) + } + } else { + // An explicit subscribe=false, may be a preemptive opt-out as in + // friend new photo notifications. + if _, err := models.UnsubscribeTo(currentUser, tableName, tableID); err != nil { + session.FlashError(w, r, "Couldn't create subscription: %s", err) + } else { + session.Flash(w, r, "You will no longer be notified about %s on this page.", kind) } } } else { @@ -79,9 +107,9 @@ func Subscription() http.HandlerFunc { session.FlashError(w, r, "Couldn't save your subscription settings: %s", err) } else { if subscribe { - session.Flash(w, r, "You will now be notified about comments on this page.") + session.Flash(w, r, "You will now be notified about %s on this page.", kind) } else { - session.Flash(w, r, "You will no longer be notified about new comments on that thread.") + session.Flash(w, r, "You will no longer be notified about new %s on this page.", kind) } } } diff --git a/pkg/controller/photo/user_gallery.go b/pkg/controller/photo/user_gallery.go index c380838..113adbb 100644 --- a/pkg/controller/photo/user_gallery.go +++ b/pkg/controller/photo/user_gallery.go @@ -195,6 +195,16 @@ func UserPhotos() http.HandlerFunc { profilePictureHidden = visibility } + // Friend Photos Notification Opt-out: + // If your friend posts too many photos and you want to mute them. + // NOTE: notifications are "on by default" and only an explicit "false" + // stored in the database indicates an opt-out. + // New photo upload notification subscription status. + var areNotificationsMuted bool + if exists, v := models.IsSubscribed(currentUser, "friend.photos", user.ID); exists { + areNotificationsMuted = !v + } + var vars = map[string]interface{}{ "IsOwnPhotos": currentUser.ID == user.ID, "IsShyUser": isShy, @@ -202,6 +212,7 @@ func UserPhotos() http.HandlerFunc { "IsMyPrivateUnlockedFor": isGranted, // have WE granted THIS USER to see our private pics? "AreWeGrantedPrivate": isGrantee, // have THEY granted US private photo access. "AreFriends": areFriends, + "AreNotificationsMuted": areNotificationsMuted, "ProfilePictureHiddenVisibility": profilePictureHidden, "User": user, "Photos": photos, diff --git a/pkg/models/comment.go b/pkg/models/comment.go index 8136a18..f4be430 100644 --- a/pkg/models/comment.go +++ b/pkg/models/comment.go @@ -29,6 +29,16 @@ var CommentableTables = map[string]interface{}{ "threads": nil, } +// SubscribableTables are the set of table names that allow notification subscriptions. +var SubscribableTables = map[string]interface{}{ + "photos": nil, + "threads": nil, + + // Special case: new photo uploads from your friends. You can't comment on this, + // but you can (un)subscribe from it all the same. + "friend.photos": nil, +} + // Preload related tables for the forum (classmethod). func (c *Comment) Preload() *gorm.DB { return DB.Preload("User.ProfilePhoto") diff --git a/pkg/models/notification.go b/pkg/models/notification.go index 83901a3..bad7213 100644 --- a/pkg/models/notification.go +++ b/pkg/models/notification.go @@ -288,8 +288,10 @@ func FilterPhotoUploadNotificationUserIDs(photo *Photo, userIDs []uint64) []uint result = []uint64{} // Collect notification opt-out profile fields and map them by user ID for easy lookup. - prefs = []*ProfileField{} + prefs = []*ProfileField{} // Global Notification preferences + mutes = []*Subscription{} // Individual "friend.photos" notification mutes mapPrefs = map[uint64]map[string]bool{} + mapMutes = map[uint64]bool{} ) if len(userIDs) == 0 { return userIDs @@ -308,6 +310,15 @@ func FilterPhotoUploadNotificationUserIDs(photo *Photo, userIDs []uint64) []uint log.Error("FilterPhotoUploadNotificationUserIDs: couldn't collect user preferences: %s", r.Error) } + // Collect any muted notification threads, e.g. the user doesn't want your new photo notifications. + r = DB.Model(&Subscription{}).Where( + "table_name = 'friend.photos' AND table_id = ? AND subscribed IS FALSE", + photo.UserID, + ).Find(&mutes) + if r.Error != nil { + log.Error("FilterPhotoUploadNotificationUserIDs: couldn't collect user notification mutes: %s", r.Error) + } + // Map the preferences by user ID. for _, row := range prefs { if _, ok := mapPrefs[row.UserID]; !ok { @@ -315,6 +326,9 @@ func FilterPhotoUploadNotificationUserIDs(photo *Photo, userIDs []uint64) []uint } mapPrefs[row.UserID][row.Name] = row.Value == "true" } + for _, row := range mutes { + mapMutes[row.UserID] = true + } // Narrow the notification recipients based on photo property and their preferences. for _, userID := range userIDs { @@ -333,6 +347,11 @@ func FilterPhotoUploadNotificationUserIDs(photo *Photo, userIDs []uint64) []uint continue } + // They muted your friend "new photo" notifications? + if mapMutes[userID] { + continue + } + // They get the notification. result = append(result, userID) } diff --git a/pkg/models/subscription.go b/pkg/models/subscription.go index 1773cdc..27167a4 100644 --- a/pkg/models/subscription.go +++ b/pkg/models/subscription.go @@ -12,8 +12,8 @@ type Subscription struct { ID uint64 `gorm:"primaryKey"` UserID uint64 `gorm:"index"` // who it belongs to Subscribed bool `gorm:"index"` - TableName string // on which of your tables (photos, comments, ...) - TableID uint64 + TableName string `gorm:"index"` // on which of your tables (photos, comments, ...) + TableID uint64 `gorm:"index"` CreatedAt time.Time UpdatedAt time.Time } @@ -81,6 +81,9 @@ func IsSubscribed(user *User, tableName string, tableID uint64) (exists bool, no } // SubscribeTo creates a subscription to a thing (comment thread) to be notified of future activity on. +// +// If a Subscription row already exists, it is NOT modified. So if a user has expressly opted out of a +// comment thread, they do not get re-subscribed when they comment on it again. func SubscribeTo(user *User, tableName string, tableID uint64) (*Subscription, error) { // Is there already a subscription row? if sub, err := GetSubscription(user, tableName, tableID); err == nil { @@ -98,6 +101,26 @@ func SubscribeTo(user *User, tableName string, tableID uint64) (*Subscription, e return sub, result.Error } +// UnsubscribeTo will create an explicit opt-out Subscription only if no subscription exists. +// +// It is the inverse of SubscribeTo. +func UnsubscribeTo(user *User, tableName string, tableID uint64) (*Subscription, error) { + // Is there already a subscription row? + if sub, err := GetSubscription(user, tableName, tableID); err == nil { + return sub, err + } + + // Create the default subscription. + sub := &Subscription{ + UserID: user.ID, + Subscribed: false, + TableName: tableName, + TableID: tableID, + } + result := DB.Create(sub) + return sub, result.Error +} + // Save a subscription. func (n *Subscription) Save() error { return DB.Save(n).Error diff --git a/web/templates/account/dashboard.html b/web/templates/account/dashboard.html index 99d3167..84ffee1 100644 --- a/web/templates/account/dashboard.html +++ b/web/templates/account/dashboard.html @@ -669,10 +669,19 @@ + onclick="return confirm('Do you want to TURN OFF notifications about this comment thread?')"> Mute this thread + {{else if eq .Type "new_photo"}} + + + Mute these notifications + + {{end}} diff --git a/web/templates/photo/gallery.html b/web/templates/photo/gallery.html index 35353ed..4d78f5f 100644 --- a/web/templates/photo/gallery.html +++ b/web/templates/photo/gallery.html @@ -255,7 +255,7 @@ {{end}}
-
+
{{if .Pager.Total}} @@ -290,6 +290,23 @@
+ + {{if .AreFriends}} +

+ + + + {{if .AreNotificationsMuted}} + Enable notifications about {{.User.Username}}'s new photos + {{else}} + Mute notifications about {{.User.Username}}'s new photos + {{end}} + + +

+ {{end}} + {{if and (not .IsSiteGallery) (eq .CurrentUser.ProfilePhoto.ID 0) (eq .CurrentUser.ID .User.ID)}}