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" NotificationPrivatePhoto = "private_photo" NotificationNewPhoto = "new_photo" NotificationCustom = "custom" // custom message pushed ) // CreateNotification func CreateNotification(n *Notification) error { // Insert via raw SQL query, reasoning: // the AboutUser relationship has gorm do way too much work: // - Upsert the user profile photo // - Upsert the user profile fields // - Upsert the user row itself // .. and if we notify all your friends, all these wasteful queries ran // for every single notification created! if n.AboutUserID == nil && n.AboutUser.ID > 0 { n.AboutUserID = &n.AboutUser.ID } return DB.Exec( ` INSERT INTO notifications (user_id, about_user_id, type, read, table_name, table_id, message, link, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, n.UserID, n.AboutUserID, n.Type, false, n.TableName, n.TableID, n.Message, n.Link, time.Now(), time.Now(), ).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 } // 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( "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 } } } } }