website/pkg/models/notification.go
Noah aa8d719fc4 Comment Thread Subscriptions
* Add ability to (un)subscribe from comment threads on Forums and Photos.
* Creating a forum post, replying to a post or adding a comment to a photo
  automatically subscribes you to be notified when somebody else adds a
  comment to the thing later.
* At the top of each comment thread is a link to disable or re-enable your
  subscription. You can join a subscription without even needing to comment.
  If you click to disable notifications, they stay disabled even if you
  add another comment later.
2022-08-27 11:42:48 -07:00

237 lines
6.2 KiB
Go

package models
import (
"time"
"code.nonshy.com/nonshy/website/pkg/log"
"gorm.io/gorm"
)
// Notification table.
type Notification struct {
ID uint64 `gorm:"primaryKey"`
UserID uint64 `gorm:"index"` // who it belongs to
AboutUserID *uint64 `form:"index"` // the other party of this notification
AboutUser User `gorm:"foreignKey:about_user_id"`
Type NotificationType // like, comment, ...
Read bool `gorm:"index"`
TableName string // on which of your tables (photos, comments, ...)
TableID uint64
Message string // text associated, e.g. copy of comment added
Link string // associated URL, e.g. for comments
CreatedAt time.Time
UpdatedAt time.Time
}
// Preload related tables for the forum (classmethod).
func (n *Notification) Preload() *gorm.DB {
return DB.Preload("AboutUser.ProfilePhoto")
}
type NotificationType string
const (
NotificationLike NotificationType = "like"
NotificationFriendApproved = "friendship_approved"
NotificationComment = "comment"
NotificationAlsoCommented = "also_comment"
NotificationAlsoPosted = "also_posted" // forum replies
NotificationCertRejected = "cert_rejected"
NotificationCertApproved = "cert_approved"
NotificationCustom = "custom" // custom message pushed
)
// CreateNotification
func CreateNotification(n *Notification) error {
return DB.Create(n).Error
}
// GetNotification by ID.
func GetNotification(id uint64) (*Notification, error) {
var n *Notification
result := DB.Model(n).First(&n, id)
return n, result.Error
}
// RemoveNotification about a table ID, e.g. when removing a like.
func RemoveNotification(tableName string, tableID uint64) error {
result := DB.Where(
"table_name = ? AND table_id = ?",
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(
"user_id = ? AND read IS NOT TRUE",
user.ID,
).Update("read", true).Error
}
// CountUnreadNotifications gets the count of unread Notifications for a user.
func CountUnreadNotifications(userID uint64) (int64, error) {
query := DB.Where(
"user_id = ? AND read = ?",
userID,
false,
)
var count int64
result := query.Model(&Notification{}).Count(&count)
return count, result.Error
}
// PaginateNotifications returns the user's notifications.
func PaginateNotifications(user *User, pager *Pagination) ([]*Notification, error) {
var ns = []*Notification{}
query := (&Notification{}).Preload().Where(
"user_id = ?",
user.ID,
).Order(
pager.Sort,
)
query.Model(&Notification{}).Count(&pager.Total)
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&ns)
return ns, result.Error
}
// Save a notification.
func (n *Notification) Save() error {
return DB.Save(n).Error
}
// NotificationBody can store remote tables mapped.
type NotificationBody struct {
PhotoID uint64
ThreadID uint64
Photo *Photo
Thread *Thread
}
type NotificationMap map[uint64]*NotificationBody
// Get a notification's body from the map.
func (m NotificationMap) Get(id uint64) *NotificationBody {
if body, ok := m[id]; ok {
return body
}
return &NotificationBody{}
}
// MapNotifications loads associated assets, like Photos, mapped to their notification ID.
func MapNotifications(ns []*Notification) NotificationMap {
var (
IDs = []uint64{}
result = NotificationMap{}
)
// Collect notification IDs.
for _, row := range ns {
IDs = append(IDs, row.ID)
result[row.ID] = &NotificationBody{}
}
result.mapNotificationPhotos(IDs)
result.mapNotificationThreads(IDs)
return result
}
// Helper function of MapNotifications to eager load Photo attachments.
func (nm NotificationMap) mapNotificationPhotos(IDs []uint64) {
type scanner struct {
PhotoID uint64
NotificationID uint64
}
var scan []scanner
// Load all of these that have photos.
err := DB.Table(
"notifications",
).Joins(
"JOIN photos ON (notifications.table_name='photos' AND notifications.table_id=photos.id)",
).Select(
"photos.id AS photo_id",
"notifications.id AS notification_id",
).Where(
"notifications.id IN ?",
IDs,
).Scan(&scan)
if err.Error != nil {
log.Error("Couldn't select photo IDs for notifications: %s", err.Error)
}
// Collect and load all the photos by ID.
var photoIDs = []uint64{}
for _, row := range scan {
// Store the photo ID in the result now.
nm[row.NotificationID].PhotoID = row.PhotoID
photoIDs = append(photoIDs, row.PhotoID)
}
// Load the photos.
if len(photoIDs) > 0 {
if photos, err := GetPhotos(photoIDs); err != nil {
log.Error("Couldn't load photo IDs for notifications: %s", err)
} else {
// Marry them to their notification IDs.
for _, body := range nm {
if photo, ok := photos[body.PhotoID]; ok {
body.Photo = photo
}
}
}
}
}
// Helper function of MapNotifications to eager load Thread attachments.
func (nm NotificationMap) mapNotificationThreads(IDs []uint64) {
type scanner struct {
ThreadID uint64
NotificationID uint64
}
var scan []scanner
// Load all of these that have threads.
err := DB.Table(
"notifications",
).Joins(
"JOIN threads ON (notifications.table_name='threads' AND notifications.table_id=threads.id)",
).Select(
"threads.id AS thread_id",
"notifications.id AS notification_id",
).Where(
"notifications.id IN ?",
IDs,
).Scan(&scan)
if err.Error != nil {
log.Error("Couldn't select thread IDs for notifications: %s", err.Error)
}
// Collect and load all the threads by ID.
var threadIDs = []uint64{}
for _, row := range scan {
// Store the thread ID in the result now.
nm[row.NotificationID].ThreadID = row.ThreadID
threadIDs = append(threadIDs, row.ThreadID)
}
// Load the threads.
if len(threadIDs) > 0 {
if threads, err := GetThreads(threadIDs); err != nil {
log.Error("Couldn't load thread IDs for notifications: %s", err)
} else {
// Marry them to their notification IDs.
for _, body := range nm {
if thread, ok := threads[body.ThreadID]; ok {
body.Thread = thread
}
}
}
}
}