Mute specific friends new photo upload notifications

This commit is contained in:
Noah Petherbridge 2024-08-07 23:05:23 -07:00
parent 01c38c5c21
commit 147a9162ba
7 changed files with 132 additions and 15 deletions

View File

@ -28,12 +28,26 @@ func Subscription() http.HandlerFunc {
templates.Redirect(w, "/") templates.Redirect(w, "/")
return return
} else { } else {
if idInt, err := strconv.Atoi(idStr); err != nil { // Is the table_id expected to be a username?
session.FlashError(w, r, "Comment table ID invalid.") switch tableName {
templates.Redirect(w, "/") case "friend.photos":
return // Special "Friend uploaded a new photo" opt-out.
} else { if user, err := models.FindUser(idStr); err != nil {
tableID = uint64(idInt) 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. // 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.") session.FlashError(w, r, "You can not comment on that.")
templates.Redirect(w, "/") templates.Redirect(w, "/")
return return
@ -61,6 +75,12 @@ func Subscription() http.HandlerFunc {
return return
} }
// Language to use in the flash messages.
var kind = "comments"
if tableName == "friend.photos" {
kind = "new photo uploads"
}
// Get their subscription. // Get their subscription.
sub, err := models.GetSubscription(currentUser, tableName, tableID) sub, err := models.GetSubscription(currentUser, tableName, tableID)
if err != nil { if err != nil {
@ -69,7 +89,15 @@ func Subscription() http.HandlerFunc {
if _, err := models.SubscribeTo(currentUser, tableName, tableID); err != nil { if _, err := models.SubscribeTo(currentUser, tableName, tableID); err != nil {
session.FlashError(w, r, "Couldn't create subscription: %s", err) session.FlashError(w, r, "Couldn't create subscription: %s", err)
} else { } 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 { } else {
@ -79,9 +107,9 @@ func Subscription() http.HandlerFunc {
session.FlashError(w, r, "Couldn't save your subscription settings: %s", err) session.FlashError(w, r, "Couldn't save your subscription settings: %s", err)
} else { } else {
if subscribe { 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 { } 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)
} }
} }
} }

View File

@ -195,6 +195,16 @@ func UserPhotos() http.HandlerFunc {
profilePictureHidden = visibility 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{}{ var vars = map[string]interface{}{
"IsOwnPhotos": currentUser.ID == user.ID, "IsOwnPhotos": currentUser.ID == user.ID,
"IsShyUser": isShy, "IsShyUser": isShy,
@ -202,6 +212,7 @@ func UserPhotos() http.HandlerFunc {
"IsMyPrivateUnlockedFor": isGranted, // have WE granted THIS USER to see our private pics? "IsMyPrivateUnlockedFor": isGranted, // have WE granted THIS USER to see our private pics?
"AreWeGrantedPrivate": isGrantee, // have THEY granted US private photo access. "AreWeGrantedPrivate": isGrantee, // have THEY granted US private photo access.
"AreFriends": areFriends, "AreFriends": areFriends,
"AreNotificationsMuted": areNotificationsMuted,
"ProfilePictureHiddenVisibility": profilePictureHidden, "ProfilePictureHiddenVisibility": profilePictureHidden,
"User": user, "User": user,
"Photos": photos, "Photos": photos,

View File

@ -29,6 +29,16 @@ var CommentableTables = map[string]interface{}{
"threads": nil, "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). // Preload related tables for the forum (classmethod).
func (c *Comment) Preload() *gorm.DB { func (c *Comment) Preload() *gorm.DB {
return DB.Preload("User.ProfilePhoto") return DB.Preload("User.ProfilePhoto")

View File

@ -288,8 +288,10 @@ func FilterPhotoUploadNotificationUserIDs(photo *Photo, userIDs []uint64) []uint
result = []uint64{} result = []uint64{}
// Collect notification opt-out profile fields and map them by user ID for easy lookup. // 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{} mapPrefs = map[uint64]map[string]bool{}
mapMutes = map[uint64]bool{}
) )
if len(userIDs) == 0 { if len(userIDs) == 0 {
return userIDs return userIDs
@ -308,6 +310,15 @@ func FilterPhotoUploadNotificationUserIDs(photo *Photo, userIDs []uint64) []uint
log.Error("FilterPhotoUploadNotificationUserIDs: couldn't collect user preferences: %s", r.Error) 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. // Map the preferences by user ID.
for _, row := range prefs { for _, row := range prefs {
if _, ok := mapPrefs[row.UserID]; !ok { 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" 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. // Narrow the notification recipients based on photo property and their preferences.
for _, userID := range userIDs { for _, userID := range userIDs {
@ -333,6 +347,11 @@ func FilterPhotoUploadNotificationUserIDs(photo *Photo, userIDs []uint64) []uint
continue continue
} }
// They muted your friend "new photo" notifications?
if mapMutes[userID] {
continue
}
// They get the notification. // They get the notification.
result = append(result, userID) result = append(result, userID)
} }

View File

@ -12,8 +12,8 @@ type Subscription struct {
ID uint64 `gorm:"primaryKey"` ID uint64 `gorm:"primaryKey"`
UserID uint64 `gorm:"index"` // who it belongs to UserID uint64 `gorm:"index"` // who it belongs to
Subscribed bool `gorm:"index"` Subscribed bool `gorm:"index"`
TableName string // on which of your tables (photos, comments, ...) TableName string `gorm:"index"` // on which of your tables (photos, comments, ...)
TableID uint64 TableID uint64 `gorm:"index"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt 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. // 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) { func SubscribeTo(user *User, tableName string, tableID uint64) (*Subscription, error) {
// Is there already a subscription row? // Is there already a subscription row?
if sub, err := GetSubscription(user, tableName, tableID); err == nil { 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 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. // Save a subscription.
func (n *Subscription) Save() error { func (n *Subscription) Save() error {
return DB.Save(n).Error return DB.Save(n).Error

View File

@ -669,10 +669,19 @@
<a href="/comments/subscription?table_name={{.TableName}}&table_id={{.TableID}}&next={{UrlEncode $Root.Request.URL.String}}&subscribe=false" <a href="/comments/subscription?table_name={{.TableName}}&table_id={{.TableID}}&next={{UrlEncode $Root.Request.URL.String}}&subscribe=false"
class="has-text-warning is-small" class="has-text-warning is-small"
title="Turn off notifications about this thread" title="Turn off notifications about this thread"
onclick="return confirm('Do you want to turn off notifications about this comment thread?')"> onclick="return confirm('Do you want to TURN OFF notifications about this comment thread?')">
<i class="fa fa-microphone-slash mr-1"></i> Mute this thread <i class="fa fa-microphone-slash mr-1"></i> Mute this thread
</a> </a>
</small> </small>
{{else if eq .Type "new_photo"}}
<small>
<a href="/comments/subscription?table_name=friend.photos&table_id={{.AboutUser.Username}}&next={{UrlEncode $Root.Request.URL.String}}&subscribe=false"
class="has-text-warning is-small"
title="Turn off notifications about @{{.AboutUser.Username}}'s new photo uploads"
onclick="return confirm('Do you want to TURN OFF notifications about @{{.AboutUser.Username}}\'s new photo uploads?\n\nNote: to re-subscribe to their new photo notifications, see the link at the top of @{{.AboutUser.Username}}\'s Photo Gallery page.')">
<i class="fa fa-microphone-slash mr-1"></i> Mute these notifications
</a>
</small>
{{end}} {{end}}
</small> </small>
</div> </div>

View File

@ -255,7 +255,7 @@
{{end}} {{end}}
<div class="block"> <div class="block">
<div class="level"> <div class="level mb-2">
<div class="level-left"> <div class="level-left">
<div class="level-item"> <div class="level-item">
{{if .Pager.Total}} {{if .Pager.Total}}
@ -290,6 +290,23 @@
</div> </div>
</div> </div>
<!-- Show an "Unsubscribe to this user's new photo notifications" if you are Friends. -->
{{if .AreFriends}}
<p class="block">
<a href="/comments/subscription?table_name=friend.photos&table_id={{.User.Username}}&next={{UrlEncode .Request.URL.String}}&subscribe={{if .AreNotificationsMuted}}true{{else}}false{{end}}"
class="{{if .AreNotificationsMuted}}has-text-success{{else}}{{end}}">
<span class="icon"><i class="fa fa-bell{{if not .AreNotificationsMuted}}-slash{{end}}"></i></span>
<span>
{{if .AreNotificationsMuted}}
Enable notifications about <strong>{{.User.Username}}</strong>'s new photos
{{else}}
Mute notifications about <strong>{{.User.Username}}</strong>'s new photos
{{end}}
</span>
</a>
</p>
{{end}}
<!-- If viewing our own profile, and we don't have a profile picture set, offer advice. --> <!-- If viewing our own profile, and we don't have a profile picture set, offer advice. -->
{{if and (not .IsSiteGallery) (eq .CurrentUser.ProfilePhoto.ID 0) (eq .CurrentUser.ID .User.ID)}} {{if and (not .IsSiteGallery) (eq .CurrentUser.ProfilePhoto.ID 0) (eq .CurrentUser.ID .User.ID)}}
<div class="notification is-success is-light content"> <div class="notification is-success is-light content">