mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-11-01 17:09:52 +00:00
723c0e84ba
Opening a conversation used to mark it read even when the agent never scrolled to the new messages or the tab was in the background. Reply notifications in the bell also stayed unread after the agent read the reply. Now the browser tells the server which message the agent saw. It only does this when the end of the thread is on screen and the tab has focus. The server moves the agent's last seen time forward to that message and never back. A reply notification counts as read when the agent's last seen time is at or after the reply. "Mark as unread" marks the old reply notifications read first, so the bell stays the same. Reply emails and pushes wait a little before sending. They are skipped if the agent already read the message or can no longer see the conversation.
174 lines
5.8 KiB
Go
174 lines
5.8 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/json"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/volatiletech/null/v9"
|
|
)
|
|
|
|
// NotificationType represents the type of user notification.
|
|
type NotificationType string
|
|
|
|
// NotificationChannel represents the delivery channel of a notification.
|
|
type NotificationChannel string
|
|
|
|
const (
|
|
NotificationTypeMention NotificationType = "mention"
|
|
NotificationTypeAssignment NotificationType = "assignment"
|
|
NotificationTypeNewReply NotificationType = "new_reply"
|
|
NotificationTypeNewReplyParticipating NotificationType = "new_reply_participating"
|
|
NotificationTypeConversationReopened NotificationType = "conversation_reopened"
|
|
NotificationTypeAutomation NotificationType = "automation"
|
|
NotificationTypeSLAFirstResponseWarn NotificationType = "sla_first_response_warning"
|
|
NotificationTypeSLAFirstResponseBreach NotificationType = "sla_first_response_breach"
|
|
NotificationTypeSLANextResponseWarn NotificationType = "sla_next_response_warning"
|
|
NotificationTypeSLANextResponseBreach NotificationType = "sla_next_response_breach"
|
|
NotificationTypeSLAResolutionWarn NotificationType = "sla_resolution_warning"
|
|
NotificationTypeSLAResolutionBreach NotificationType = "sla_resolution_breach"
|
|
|
|
NotificationTypeSLAWarning NotificationType = "sla_warning"
|
|
NotificationTypeSLABreach NotificationType = "sla_breach"
|
|
|
|
NotificationChannelInApp NotificationChannel = "in_app"
|
|
NotificationChannelEmail NotificationChannel = "email"
|
|
NotificationChannelPush NotificationChannel = "push"
|
|
)
|
|
|
|
// AgentNotificationTypes are the notification types agents can set preferences for.
|
|
var AgentNotificationTypes = []NotificationType{
|
|
NotificationTypeAssignment,
|
|
NotificationTypeMention,
|
|
NotificationTypeConversationReopened,
|
|
NotificationTypeNewReply,
|
|
NotificationTypeNewReplyParticipating,
|
|
NotificationTypeSLAFirstResponseWarn,
|
|
NotificationTypeSLAFirstResponseBreach,
|
|
NotificationTypeSLANextResponseWarn,
|
|
NotificationTypeSLANextResponseBreach,
|
|
NotificationTypeSLAResolutionWarn,
|
|
NotificationTypeSLAResolutionBreach,
|
|
}
|
|
|
|
// NotificationChannels are the delivery channels a preference can target.
|
|
var NotificationChannels = []NotificationChannel{
|
|
NotificationChannelInApp,
|
|
NotificationChannelEmail,
|
|
NotificationChannelPush,
|
|
}
|
|
|
|
var ReplyTypes = []NotificationType{
|
|
NotificationTypeNewReply,
|
|
NotificationTypeNewReplyParticipating,
|
|
NotificationTypeConversationReopened,
|
|
}
|
|
|
|
// defaultDisabledTypes are off until an agent opts in. Every other type defaults to on.
|
|
var defaultDisabledTypes = []NotificationType{
|
|
NotificationTypeNewReply,
|
|
NotificationTypeNewReplyParticipating,
|
|
}
|
|
|
|
// NotificationPreference is a per-channel toggle for a notification type.
|
|
type NotificationPreference struct {
|
|
NotificationType NotificationType `db:"notification_type" json:"notification_type"`
|
|
Channel NotificationChannel `db:"channel" json:"channel"`
|
|
Enabled bool `db:"enabled" json:"enabled"`
|
|
}
|
|
|
|
type Recipient struct {
|
|
UserID int
|
|
Email *EmailNotification
|
|
}
|
|
|
|
type Notification struct {
|
|
Type NotificationType
|
|
Recipients []Recipient
|
|
Title string
|
|
Body null.String
|
|
ConversationID null.Int
|
|
MessageID null.Int
|
|
ActorID null.Int
|
|
Meta json.RawMessage
|
|
ConversationUUID string
|
|
MessageUUID string
|
|
ActorFirstName string
|
|
ActorLastName string
|
|
}
|
|
|
|
type EmailNotification struct {
|
|
Recipient string
|
|
Subject string
|
|
Content string
|
|
Delay time.Duration
|
|
}
|
|
|
|
type NotificationReference struct {
|
|
Type NotificationType
|
|
ConversationID null.Int
|
|
MessageID null.Int
|
|
NotificationID null.Int
|
|
}
|
|
|
|
type Email struct {
|
|
UserID int
|
|
NotificationID null.Int
|
|
Type NotificationType
|
|
ConversationID null.Int
|
|
MessageID null.Int
|
|
Recipient string
|
|
Subject string
|
|
Content string
|
|
}
|
|
|
|
type PushPayload struct {
|
|
Title string `json:"title"`
|
|
Body string `json:"body,omitempty"`
|
|
Tag string `json:"tag,omitempty"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
type DeliveryResult struct {
|
|
RecipientID int
|
|
Channels []NotificationChannel
|
|
}
|
|
|
|
// DefaultEnabled reports the preference for a type when the agent has no stored row for it.
|
|
func DefaultEnabled(nType NotificationType) bool {
|
|
return !slices.Contains(defaultDisabledTypes, nType)
|
|
}
|
|
|
|
// UserNotification represents an in-app notification for a user.
|
|
type UserNotification struct {
|
|
ID int `db:"id" json:"id"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
UserID int `db:"user_id" json:"user_id"`
|
|
NotificationType NotificationType `db:"notification_type" json:"notification_type"`
|
|
Title string `db:"title" json:"title"`
|
|
Body null.String `db:"body" json:"body"`
|
|
IsRead bool `db:"is_read" json:"is_read"`
|
|
ConversationID null.Int `db:"conversation_id" json:"conversation_id"`
|
|
MessageID null.Int `db:"message_id" json:"message_id"`
|
|
ActorID null.Int `db:"actor_id" json:"actor_id"`
|
|
Meta json.RawMessage `db:"meta" json:"meta"`
|
|
|
|
// Joined fields from related tables
|
|
ActorFirstName null.String `db:"actor_first_name" json:"actor_first_name"`
|
|
ActorLastName null.String `db:"actor_last_name" json:"actor_last_name"`
|
|
ActorAvatarURL null.String `db:"actor_avatar_url" json:"actor_avatar_url"`
|
|
ConversationUUID null.String `db:"conversation_uuid" json:"conversation_uuid"`
|
|
MessageUUID null.String `db:"message_uuid" json:"message_uuid"`
|
|
}
|
|
|
|
// NotificationStats holds notification statistics for a user.
|
|
type NotificationStats struct {
|
|
UnreadCount int `db:"unread_count" json:"unread_count"`
|
|
TotalCount int `db:"total_count" json:"total_count"`
|
|
}
|
|
|
|
func IsReply(nType NotificationType) bool {
|
|
return slices.Contains(ReplyTypes, nType)
|
|
}
|