Feature: User Notification Objects (#752)
Co-authored-by: Pranav Raj S <pranavrajs@gmail.com>
This commit is contained in:
32
app/builders/notification_builder.rb
Normal file
32
app/builders/notification_builder.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
class NotificationBuilder
|
||||
pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!]
|
||||
|
||||
def perform
|
||||
return unless user_subscribed_to_notification?
|
||||
|
||||
build_notification
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def secondary_actor
|
||||
Current.user
|
||||
end
|
||||
|
||||
def user_subscribed_to_notification?
|
||||
notification_setting = user.notification_settings.find_by(account_id: account.id)
|
||||
return true if notification_setting.public_send("email_#{notification_type}?")
|
||||
return true if notification_setting.public_send("push_#{notification_type}?")
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def build_notification
|
||||
user.notifications.create!(
|
||||
notification_type: notification_type,
|
||||
account: account,
|
||||
primary_actor: primary_actor,
|
||||
secondary_actor: secondary_actor
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -4,11 +4,6 @@ class Api::V1::Accounts::ContactsController < Api::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :fetch_contact, only: [:show, :update]
|
||||
|
||||
skip_before_action :authenticate_user!, only: [:create]
|
||||
skip_before_action :set_current_user, only: [:create]
|
||||
skip_before_action :check_subscription, only: [:create]
|
||||
skip_around_action :handle_with_exception, only: [:create]
|
||||
|
||||
def index
|
||||
@contacts = current_account.contacts
|
||||
end
|
||||
|
||||
21
app/controllers/api/v1/accounts/notifications_controller.rb
Normal file
21
app/controllers/api/v1/accounts/notifications_controller.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
class Api::V1::Accounts::NotificationsController < Api::BaseController
|
||||
protect_from_forgery with: :null_session
|
||||
|
||||
before_action :fetch_notification, only: [:update]
|
||||
|
||||
def index
|
||||
@notifications = current_user.notifications.where(account_id: current_account.id)
|
||||
render json: @notifications
|
||||
end
|
||||
|
||||
def update
|
||||
@notification.update(read_at: DateTime.now.utc)
|
||||
render json: @notification
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_notification
|
||||
@notification = current_user.notifications.find(params[:id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
class Api::V1::NotificationSubscriptionsController < Api::BaseController
|
||||
before_action :set_user
|
||||
|
||||
def create
|
||||
notification_subscription = @user.notification_subscriptions.create(notification_subscription_params)
|
||||
render json: notification_subscription
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = current_user
|
||||
end
|
||||
|
||||
def notification_subscription_params
|
||||
params.require(:notification_subscription).permit(:subscription_type, subscription_attributes: {})
|
||||
end
|
||||
end
|
||||
@@ -9,8 +9,7 @@ class AsyncDispatcher < BaseDispatcher
|
||||
end
|
||||
|
||||
def listeners
|
||||
listeners = [EmailNotificationListener.instance, ReportingListener.instance, WebhookListener.instance]
|
||||
listeners << EventListener.instance
|
||||
listeners = [EventListener.instance, ReportingListener.instance, WebhookListener.instance]
|
||||
listeners << SubscriptionListener.instance if ENV['BILLING_ENABLED']
|
||||
listeners
|
||||
end
|
||||
|
||||
@@ -5,6 +5,6 @@ class SyncDispatcher < BaseDispatcher
|
||||
end
|
||||
|
||||
def listeners
|
||||
[ActionCableListener.instance, AgentBotListener.instance]
|
||||
[ActionCableListener.instance, AgentBotListener.instance, NotificationListener.instance]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
v-model="selectedNotifications"
|
||||
class="email-notification--checkbox"
|
||||
type="checkbox"
|
||||
value="conversation_creation"
|
||||
value="email_conversation_creation"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<label for="conversation_creation">
|
||||
@@ -29,7 +29,7 @@
|
||||
v-model="selectedNotifications"
|
||||
class="email-notification--checkbox"
|
||||
type="checkbox"
|
||||
value="conversation_assignment"
|
||||
value="email_conversation_assignment"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<label for="conversation_assignment">
|
||||
|
||||
@@ -4,86 +4,88 @@ class ActionCableListener < BaseListener
|
||||
def conversation_created(event)
|
||||
conversation, account, timestamp = extract_conversation_and_account(event)
|
||||
|
||||
send_to_agents(account, conversation.inbox.members, CONVERSATION_CREATED, conversation.push_event_data)
|
||||
broadcast(user_tokens(account, conversation.inbox.members), CONVERSATION_CREATED, conversation.push_event_data)
|
||||
end
|
||||
|
||||
def conversation_read(event)
|
||||
conversation, account, timestamp = extract_conversation_and_account(event)
|
||||
|
||||
send_to_agents(account, conversation.inbox.members, CONVERSATION_READ, conversation.push_event_data)
|
||||
broadcast(user_tokens(account, conversation.inbox.members), CONVERSATION_READ, conversation.push_event_data)
|
||||
end
|
||||
|
||||
def message_created(event)
|
||||
message, account, timestamp = extract_message_and_account(event)
|
||||
conversation = message.conversation
|
||||
tokens = user_tokens(account, conversation.inbox.members) + contact_token(conversation.contact, message)
|
||||
|
||||
send_to_agents(account, conversation.inbox.members, MESSAGE_CREATED, message.push_event_data)
|
||||
send_to_contact(conversation.contact, MESSAGE_CREATED, message)
|
||||
broadcast(tokens, MESSAGE_CREATED, message.push_event_data)
|
||||
end
|
||||
|
||||
def message_updated(event)
|
||||
message, account, timestamp = extract_message_and_account(event)
|
||||
conversation = message.conversation
|
||||
contact = conversation.contact
|
||||
tokens = user_tokens(account, conversation.inbox.members) + contact_token(conversation.contact, message)
|
||||
|
||||
send_to_agents(account, conversation.inbox.members, MESSAGE_UPDATED, message.push_event_data)
|
||||
send_to_contact(contact, MESSAGE_UPDATED, message)
|
||||
broadcast(tokens, MESSAGE_UPDATED, message.push_event_data)
|
||||
end
|
||||
|
||||
def conversation_resolved(event)
|
||||
conversation, account, timestamp = extract_conversation_and_account(event)
|
||||
|
||||
send_to_agents(account, conversation.inbox.members, CONVERSATION_RESOLVED, conversation.push_event_data)
|
||||
broadcast(user_tokens(account, conversation.inbox.members), CONVERSATION_RESOLVED, conversation.push_event_data)
|
||||
end
|
||||
|
||||
def conversation_opened(event)
|
||||
conversation, account, timestamp = extract_conversation_and_account(event)
|
||||
|
||||
send_to_agents(account, conversation.inbox.members, CONVERSATION_OPENED, conversation.push_event_data)
|
||||
broadcast(user_tokens(account, conversation.inbox.members), CONVERSATION_OPENED, conversation.push_event_data)
|
||||
end
|
||||
|
||||
def conversation_lock_toggle(event)
|
||||
conversation, account, timestamp = extract_conversation_and_account(event)
|
||||
|
||||
send_to_agents(account, conversation.inbox.members, CONVERSATION_LOCK_TOGGLE, conversation.lock_event_data)
|
||||
broadcast(user_tokens(account, conversation.inbox.members), CONVERSATION_LOCK_TOGGLE, conversation.lock_event_data)
|
||||
end
|
||||
|
||||
def assignee_changed(event)
|
||||
conversation, account, timestamp = extract_conversation_and_account(event)
|
||||
|
||||
send_to_agents(account, conversation.inbox.members, ASSIGNEE_CHANGED, conversation.push_event_data)
|
||||
broadcast(user_tokens(account, conversation.inbox.members), ASSIGNEE_CHANGED, conversation.push_event_data)
|
||||
end
|
||||
|
||||
def contact_created(event)
|
||||
contact, account, timestamp = extract_contact_and_account(event)
|
||||
|
||||
send_to_agents(account, account.agents, CONTACT_CREATED, contact.push_event_data)
|
||||
broadcast(user_tokens(account, account.agents), CONTACT_CREATED, contact.push_event_data)
|
||||
end
|
||||
|
||||
def contact_updated(event)
|
||||
contact, account, timestamp = extract_contact_and_account(event)
|
||||
|
||||
send_to_agents(account, account.agents, CONTACT_UPDATED, contact.push_event_data)
|
||||
broadcast(user_tokens(account, account.agents), CONTACT_UPDATED, contact.push_event_data)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_to_agents(account, agents, event_name, data)
|
||||
def user_tokens(account, agents)
|
||||
agent_tokens = agents.pluck(:pubsub_token)
|
||||
admin_tokens = account.administrators.pluck(:pubsub_token)
|
||||
|
||||
pubsub_tokens = (agent_tokens + admin_tokens).uniq
|
||||
|
||||
return if pubsub_tokens.blank?
|
||||
|
||||
::ActionCableBroadcastJob.perform_later(pubsub_tokens, event_name, data)
|
||||
pubsub_tokens
|
||||
end
|
||||
|
||||
def send_to_contact(contact, event_name, message)
|
||||
return if message.private?
|
||||
return if message.activity?
|
||||
return if contact.nil?
|
||||
def contact_token(contact, message)
|
||||
return [] if message.private?
|
||||
return [] if message.activity?
|
||||
return [] if contact.nil?
|
||||
|
||||
::ActionCableBroadcastJob.perform_later([contact.pubsub_token], event_name, message.push_event_data)
|
||||
[contact.pubsub_token]
|
||||
end
|
||||
|
||||
def broadcast(tokens, event_name, data)
|
||||
return if tokens.blank?
|
||||
|
||||
::ActionCableBroadcastJob.perform_later(tokens.uniq, event_name, data)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
class EmailNotificationListener < BaseListener
|
||||
def conversation_created(event)
|
||||
conversation, _account, _timestamp = extract_conversation_and_account(event)
|
||||
return if conversation.bot?
|
||||
|
||||
conversation.inbox.members.each do |agent|
|
||||
next unless agent.notification_settings.find_by(account_id: conversation.account_id).conversation_creation?
|
||||
|
||||
AgentNotifications::ConversationNotificationsMailer.conversation_created(conversation, agent).deliver_later
|
||||
end
|
||||
end
|
||||
end
|
||||
29
app/listeners/notification_listener.rb
Normal file
29
app/listeners/notification_listener.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
class NotificationListener < BaseListener
|
||||
def conversation_created(event)
|
||||
conversation, account, _timestamp = extract_conversation_and_account(event)
|
||||
return if conversation.bot?
|
||||
|
||||
conversation.inbox.members.each do |agent|
|
||||
NotificationBuilder.new(
|
||||
notification_type: 'conversation_creation',
|
||||
user: agent,
|
||||
account: account,
|
||||
primary_actor: conversation
|
||||
).perform
|
||||
end
|
||||
end
|
||||
|
||||
def assignee_changed(event)
|
||||
conversation, account, _timestamp = extract_conversation_and_account(event)
|
||||
assignee = conversation.assignee
|
||||
return unless conversation.notifiable_assignee_change?
|
||||
return if conversation.bot?
|
||||
|
||||
NotificationBuilder.new(
|
||||
notification_type: 'conversation_assignment',
|
||||
user: assignee,
|
||||
account: account,
|
||||
primary_actor: conversation
|
||||
).perform
|
||||
end
|
||||
end
|
||||
@@ -2,7 +2,7 @@ class AgentNotifications::ConversationNotificationsMailer < ApplicationMailer
|
||||
default from: ENV.fetch('MAILER_SENDER_EMAIL', 'accounts@chatwoot.com')
|
||||
layout 'mailer'
|
||||
|
||||
def conversation_created(conversation, agent)
|
||||
def conversation_creation(conversation, agent)
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
@agent = agent
|
||||
@@ -11,7 +11,7 @@ class AgentNotifications::ConversationNotificationsMailer < ApplicationMailer
|
||||
mail(to: @agent.email, subject: subject)
|
||||
end
|
||||
|
||||
def conversation_assigned(conversation, agent)
|
||||
def conversation_assignment(conversation, agent)
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
@agent = agent
|
||||
|
||||
@@ -37,7 +37,8 @@ class AccountUser < ApplicationRecord
|
||||
|
||||
def create_notification_setting
|
||||
setting = user.notification_settings.new(account_id: account.id)
|
||||
setting.selected_email_flags = [:conversation_assignment]
|
||||
setting.selected_email_flags = [:email_conversation_assignment]
|
||||
setting.selected_push_flags = [:push_conversation_assignment]
|
||||
setting.save!
|
||||
end
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ class Conversation < ApplicationRecord
|
||||
|
||||
before_create :set_bot_conversation
|
||||
|
||||
after_update :notify_status_change, :create_activity, :send_email_notification_to_assignee
|
||||
after_update :notify_status_change, :create_activity
|
||||
|
||||
after_create :notify_conversation_creation, :run_round_robin
|
||||
|
||||
@@ -105,16 +105,6 @@ class Conversation < ApplicationRecord
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_bot_conversation
|
||||
self.status = :bot if inbox.agent_bot_inbox&.active?
|
||||
end
|
||||
|
||||
def notify_conversation_creation
|
||||
dispatcher_dispatch(CONVERSATION_CREATED)
|
||||
end
|
||||
|
||||
def notifiable_assignee_change?
|
||||
return false if self_assign?(assignee_id)
|
||||
return false unless saved_change_to_assignee_id?
|
||||
@@ -123,12 +113,14 @@ class Conversation < ApplicationRecord
|
||||
true
|
||||
end
|
||||
|
||||
def send_email_notification_to_assignee
|
||||
return unless notifiable_assignee_change?
|
||||
return if assignee.notification_settings.find_by(account_id: account_id).not_conversation_assignment?
|
||||
return if bot?
|
||||
private
|
||||
|
||||
AgentNotifications::ConversationNotificationsMailer.conversation_assigned(self, assignee).deliver_later
|
||||
def set_bot_conversation
|
||||
self.status = :bot if inbox.agent_bot_inbox&.active?
|
||||
end
|
||||
|
||||
def notify_conversation_creation
|
||||
dispatcher_dispatch(CONVERSATION_CREATED)
|
||||
end
|
||||
|
||||
def self_assign?(assignee_id)
|
||||
|
||||
@@ -75,7 +75,7 @@ class Message < ApplicationRecord
|
||||
:notify_via_mail
|
||||
|
||||
# we need to wait for the active storage attachments to be available
|
||||
after_create_commit :dispatch_event, :send_reply
|
||||
after_create_commit :dispatch_create_events, :send_reply
|
||||
|
||||
after_update :dispatch_update_event
|
||||
|
||||
@@ -117,7 +117,7 @@ class Message < ApplicationRecord
|
||||
|
||||
private
|
||||
|
||||
def dispatch_event
|
||||
def dispatch_create_events
|
||||
Rails.configuration.dispatcher.dispatch(MESSAGE_CREATED, Time.zone.now, message: self)
|
||||
|
||||
if outgoing? && conversation.messages.outgoing.count == 1
|
||||
|
||||
47
app/models/notification.rb
Normal file
47
app/models/notification.rb
Normal file
@@ -0,0 +1,47 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: notifications
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# notification_type :integer not null
|
||||
# primary_actor_type :string not null
|
||||
# read_at :datetime
|
||||
# secondary_actor_type :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# primary_actor_id :bigint not null
|
||||
# secondary_actor_id :bigint
|
||||
# user_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_notifications_on_account_id (account_id)
|
||||
# index_notifications_on_user_id (user_id)
|
||||
# uniq_primary_actor_per_account_notifications (primary_actor_type,primary_actor_id)
|
||||
# uniq_secondary_actor_per_account_notifications (secondary_actor_type,secondary_actor_id)
|
||||
#
|
||||
|
||||
class Notification < ApplicationRecord
|
||||
belongs_to :account
|
||||
belongs_to :user
|
||||
|
||||
belongs_to :primary_actor, polymorphic: true
|
||||
belongs_to :secondary_actor, polymorphic: true, optional: true
|
||||
|
||||
NOTIFICATION_TYPES = {
|
||||
conversation_creation: 1,
|
||||
conversation_assignment: 2
|
||||
}.freeze
|
||||
|
||||
enum notification_type: NOTIFICATION_TYPES
|
||||
|
||||
after_create_commit :process_notification_delivery
|
||||
|
||||
private
|
||||
|
||||
def process_notification_delivery
|
||||
Notification::EmailNotificationService.new(notification: self).perform
|
||||
# Notification::PushNotificationService.new(notification: self).perform
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,7 @@
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# email_flags :integer default(0), not null
|
||||
# push_flags :integer default(0), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :integer
|
||||
@@ -25,10 +26,9 @@ class NotificationSetting < ApplicationRecord
|
||||
flag_query_mode: :bit_operator
|
||||
}.freeze
|
||||
|
||||
EMAIL_NOTIFICATION_FLAGS = {
|
||||
1 => :conversation_creation,
|
||||
2 => :conversation_assignment
|
||||
}.freeze
|
||||
EMAIL_NOTIFICATION_FLAGS = ::Notification::NOTIFICATION_TYPES.transform_keys { |key| "email_#{key}".to_sym }.invert.freeze
|
||||
PUSH_NOTIFICATION_FLAGS = ::Notification::NOTIFICATION_TYPES.transform_keys { |key| "push_#{key}".to_sym }.invert.freeze
|
||||
|
||||
has_flags EMAIL_NOTIFICATION_FLAGS.merge(column: 'email_flags').merge(DEFAULT_QUERY_SETTING)
|
||||
has_flags PUSH_NOTIFICATION_FLAGS.merge(column: 'push_flags').merge(DEFAULT_QUERY_SETTING)
|
||||
end
|
||||
|
||||
26
app/models/notification_subscription.rb
Normal file
26
app/models/notification_subscription.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: notification_subscriptions
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# subscription_attributes :jsonb not null
|
||||
# subscription_type :integer not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# user_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_notification_subscriptions_on_user_id (user_id)
|
||||
#
|
||||
|
||||
class NotificationSubscription < ApplicationRecord
|
||||
belongs_to :user
|
||||
|
||||
SUBSCRIPTION_TYPES = {
|
||||
browser_push: 1,
|
||||
gcm: 2
|
||||
}.freeze
|
||||
|
||||
enum subscription_type: SUBSCRIPTION_TYPES
|
||||
end
|
||||
@@ -67,7 +67,10 @@ class User < ApplicationRecord
|
||||
has_many :assigned_inboxes, through: :inbox_members, source: :inbox
|
||||
has_many :messages
|
||||
has_many :invitees, through: :account_users, class_name: 'User', foreign_key: 'inviter_id', dependent: :nullify
|
||||
|
||||
has_many :notifications, dependent: :destroy
|
||||
has_many :notification_settings, dependent: :destroy
|
||||
has_many :notification_subscriptions, dependent: :destroy
|
||||
|
||||
before_validation :set_password_and_uid, on: :create
|
||||
|
||||
@@ -119,12 +122,6 @@ class User < ApplicationRecord
|
||||
Rails.configuration.dispatcher.dispatch(AGENT_ADDED, Time.zone.now, account: account)
|
||||
end
|
||||
|
||||
def create_notification_setting
|
||||
setting = notification_settings.new(account_id: account.id)
|
||||
setting.selected_email_flags = [:conversation_assignment]
|
||||
setting.save!
|
||||
end
|
||||
|
||||
def notify_deletion
|
||||
Rails.configuration.dispatcher.dispatch(AGENT_REMOVED, Time.zone.now, account: account)
|
||||
end
|
||||
|
||||
20
app/services/notification/email_notification_service.rb
Normal file
20
app/services/notification/email_notification_service.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
class Notification::EmailNotificationService
|
||||
pattr_initialize [:notification!]
|
||||
|
||||
def perform
|
||||
return unless user_subscribed_to_notification?
|
||||
|
||||
# TODO : Clean up whatever happening over here
|
||||
AgentNotifications::ConversationNotificationsMailer.public_send(notification
|
||||
.notification_type.to_s, notification.primary_actor, notification.user).deliver_later
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_subscribed_to_notification?
|
||||
notification_setting = notification.user.notification_settings.find_by(account_id: notification.account.id)
|
||||
return true if notification_setting.public_send("email_#{notification.notification_type}?")
|
||||
|
||||
false
|
||||
end
|
||||
end
|
||||
17
app/services/notification/push_notification_service.rb
Normal file
17
app/services/notification/push_notification_service.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
class Notification::PushNotificationService
|
||||
pattr_initialize [:notification!]
|
||||
|
||||
def perform
|
||||
return unless user_subscribed_to_notification?
|
||||
# TODO: implement the push delivery logic here
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_subscribed_to_notification?
|
||||
notification_setting = notification.user.notification_settings.find_by(account_id: notification.account.id)
|
||||
return true if notification_setting.public_send("push_#{notification.notification_type}?")
|
||||
|
||||
false
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user