Skip to content

Commit

Permalink
chore: Add controllers for conversation participants (#6462)
Browse files Browse the repository at this point in the history
Co-authored-by: Aswin Dev P.S <aswindevps@gmail.com>
Co-authored-by: Sojan Jose <sojan@chatwoot.com>
  • Loading branch information
3 people committed Feb 16, 2023
1 parent 949ddf6 commit 7044eda
Show file tree
Hide file tree
Showing 34 changed files with 546 additions and 63 deletions.
2 changes: 0 additions & 2 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ Metrics/ClassLength:
- 'app/builders/messages/facebook/message_builder.rb'
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
- 'app/listeners/action_cable_listener.rb'
- 'app/models/conversation.rb'
RSpec/ExampleLength:
Max: 25
Style/Documentation:
Expand Down Expand Up @@ -188,4 +187,3 @@ AllCops:
- db/migrate/20200927135222_add_last_activity_at_to_conversation.rb
- db/migrate/20210306170117_add_last_activity_at_to_contacts.rb
- db/migrate/20220809104508_revert_cascading_indexes.rb

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
class Api::V1::Accounts::Conversations::ParticipantsController < Api::V1::Accounts::Conversations::BaseController
def show
@participants = @conversation.conversation_participants
end

def create
ActiveRecord::Base.transaction do
@participants = participants_to_be_added_ids.map { |user_id| @conversation.conversation_participants.find_or_create_by(user_id: user_id) }
end
end

def update
ActiveRecord::Base.transaction do
participants_to_be_added_ids.each { |user_id| @conversation.conversation_participants.find_or_create_by(user_id: user_id) }
participants_to_be_removed_ids.each { |user_id| @conversation.conversation_participants.find_by(user_id: user_id)&.destroy }
end
@participants = @conversation.conversation_participants
render action: 'show'
end

def destroy
ActiveRecord::Base.transaction do
params[:user_ids].map { |user_id| @conversation.conversation_participants.find_by(user_id: user_id)&.destroy }
end
head :ok
end

private

def participants_to_be_added_ids
params[:user_ids] - current_participant_ids
end

def participants_to_be_removed_ids
current_participant_ids - params[:user_ids]
end

def current_participant_ids
@current_participant_ids ||= @conversation.conversation_participants.pluck(:user_id)
end
end
2 changes: 2 additions & 0 deletions app/finders/conversation_finder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ def filter_by_conversation_type
when 'mention'
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
@conversations = @conversations.where(id: conversation_ids)
when 'participating'
@conversations = current_user.participating_conversations.where(account_id: current_account.id)
when 'unattended'
@conversations = @conversations.where(first_reply_created_at: nil)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ def assigned_conversation_new_message(message, agent)
send_mail_with_liquid(to: @agent.email, subject: subject) and return
end

def participating_conversation_new_message(message, agent)
return unless smtp_config_set_or_development?
# Don't spam with email notifications if agent is online
return if ::OnlineStatusTracker.get_presence(message.account_id, 'User', agent.id)

@agent = agent
@conversation = message.conversation
subject = "#{@agent.available_name}, New message in your participating conversation [ID - #{@conversation.display_id}]."
@action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
send_mail_with_liquid(to: @agent.email, subject: subject) and return
end

private

def liquid_droppables
Expand Down
13 changes: 12 additions & 1 deletion app/models/concerns/assignment_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module AssignmentHandler

included do
before_save :ensure_assignee_is_from_team
after_commit :notify_assignment_change, :process_assignment_activities
after_commit :notify_assignment_change, :process_assignment_changes
end

private
Expand Down Expand Up @@ -36,6 +36,11 @@ def notify_assignment_change
end
end

def process_assignment_changes
process_assignment_activities
process_participant_assignment
end

def process_assignment_activities
user_name = Current.user.name if Current.user.present?
if saved_change_to_team_id?
Expand All @@ -44,4 +49,10 @@ def process_assignment_activities
create_assignee_change_activity(user_name)
end
end

def process_participant_assignment
return unless saved_change_to_assignee_id? && assignee_id.present?

conversation_participants.find_or_create_by!(user_id: assignee_id)
end
end
53 changes: 53 additions & 0 deletions app/models/concerns/user_attribute_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
module UserAttributeHelpers
extend ActiveSupport::Concern

def available_name
self[:display_name].presence || name
end

def availability_status
current_account_user&.availability_status
end

def auto_offline
current_account_user&.auto_offline
end

def inviter
current_account_user&.inviter
end

def active_account_user
account_users.order(active_at: :desc)&.first
end

def current_account_user
# We want to avoid subsequent queries in case where the association is preloaded.
# using where here will trigger n+1 queries.
account_users.find { |ac_usr| ac_usr.account_id == Current.account.id } if Current.account
end

def account
current_account_user&.account
end

def administrator?
current_account_user&.administrator?
end

def agent?
current_account_user&.agent?
end

def role
current_account_user&.role
end

# Used internally for Chatwoot in Chatwoot
def hmac_identifier
hmac_key = GlobalConfig.get('CHATWOOT_INBOX_HMAC_KEY')['CHATWOOT_INBOX_HMAC_KEY']
return OpenSSL::HMAC.hexdigest('sha256', hmac_key, email) if hmac_key.present?

''
end
end
1 change: 1 addition & 0 deletions app/models/conversation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ class Conversation < ApplicationRecord
has_many :mentions, dependent: :destroy_async
has_many :messages, dependent: :destroy_async, autosave: true
has_one :csat_survey_response, dependent: :destroy_async
has_many :conversation_participants, dependent: :destroy_async
has_many :notifications, as: :primary_actor, dependent: :destroy_async

before_save :ensure_snooze_until_reset
Expand Down
41 changes: 41 additions & 0 deletions app/models/conversation_participant.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# == Schema Information
#
# Table name: conversation_participants
#
# id :bigint not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# conversation_id :bigint not null
# user_id :bigint not null
#
# Indexes
#
# index_conversation_participants_on_account_id (account_id)
# index_conversation_participants_on_conversation_id (conversation_id)
# index_conversation_participants_on_user_id (user_id)
# index_conversation_participants_on_user_id_and_conversation_id (user_id,conversation_id) UNIQUE
#
class ConversationParticipant < ApplicationRecord
validates :account_id, presence: true
validates :conversation_id, presence: true
validates :user_id, presence: true
validates :user_id, uniqueness: { scope: [:conversation_id] }
validate :ensure_inbox_access

belongs_to :account
belongs_to :conversation
belongs_to :user

before_validation :ensure_account_id

private

def ensure_account_id
self.account_id = conversation&.account_id
end

def ensure_inbox_access
errors.add(:user, 'must have inbox access') if conversation && conversation.inbox.assignable_agents.exclude?(user)
end
end
11 changes: 8 additions & 3 deletions app/models/notification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class Notification < ApplicationRecord
conversation_creation: 1,
conversation_assignment: 2,
assigned_conversation_new_message: 3,
conversation_mention: 4
conversation_mention: 4,
participating_conversation_new_message: 5
}.freeze

enum notification_type: NOTIFICATION_TYPES
Expand Down Expand Up @@ -94,7 +95,7 @@ def push_message_title
I18n.t('notifications.notification_title.conversation_creation', display_id: primary_actor.display_id, inbox_name: primary_actor.inbox.name)
when 'conversation_assignment'
I18n.t('notifications.notification_title.conversation_assignment', display_id: primary_actor.display_id)
when 'assigned_conversation_new_message'
when 'assigned_conversation_new_message', 'participating_conversation_new_message'
I18n.t(
'notifications.notification_title.assigned_conversation_new_message',
display_id: conversation.display_id,
Expand All @@ -109,7 +110,11 @@ def push_message_title
# rubocop:enable Metrics/CyclomaticComplexity

def conversation
return primary_actor.conversation if %w[assigned_conversation_new_message conversation_mention].include? notification_type
return primary_actor.conversation if %w[
assigned_conversation_new_message
participating_conversation_new_message
conversation_mention
].include? notification_type

primary_actor
end
Expand Down
53 changes: 3 additions & 50 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class User < ApplicationRecord
include Rails.application.routes.url_helpers
include Reportable
include SsoAuthenticatable
include UserAttributeHelpers

devise :database_authenticatable,
:registerable,
Expand Down Expand Up @@ -76,6 +77,8 @@ class User < ApplicationRecord
has_many :assigned_conversations, foreign_key: 'assignee_id', class_name: 'Conversation', dependent: :nullify
alias_attribute :conversations, :assigned_conversations
has_many :csat_survey_responses, foreign_key: 'assigned_agent_id', dependent: :nullify
has_many :conversation_participants, dependent: :destroy_async
has_many :participating_conversations, through: :conversation_participants, source: :conversation

has_many :inbox_members, dependent: :destroy_async
has_many :inboxes, through: :inbox_members, source: :inbox
Expand Down Expand Up @@ -110,60 +113,10 @@ def set_password_and_uid
self.uid = email
end

def active_account_user
account_users.order(active_at: :desc)&.first
end

def current_account_user
# We want to avoid subsequent queries in case where the association is preloaded.
# using where here will trigger n+1 queries.
account_users.find { |ac_usr| ac_usr.account_id == Current.account.id } if Current.account
end

def available_name
self[:display_name].presence || name
end

# Used internally for Chatwoot in Chatwoot
def hmac_identifier
hmac_key = GlobalConfig.get('CHATWOOT_INBOX_HMAC_KEY')['CHATWOOT_INBOX_HMAC_KEY']
return OpenSSL::HMAC.hexdigest('sha256', hmac_key, email) if hmac_key.present?

''
end

def account
current_account_user&.account
end

def assigned_inboxes
administrator? ? Current.account.inboxes : inboxes.where(account_id: Current.account.id)
end

def administrator?
current_account_user&.administrator?
end

def agent?
current_account_user&.agent?
end

def role
current_account_user&.role
end

def availability_status
current_account_user&.availability_status
end

def auto_offline
current_account_user&.auto_offline
end

def inviter
current_account_user&.inviter
end

def serializable_hash(options = nil)
super(options).merge(confirmed: confirmed?)
end
Expand Down
7 changes: 7 additions & 0 deletions app/services/messages/mention_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ def perform

Conversations::UserMentionJob.perform_later(validated_mentioned_ids, message.conversation.id, message.account.id)
generate_notifications_for_mentions(validated_mentioned_ids)
add_mentioned_users_as_participants(validated_mentioned_ids)
end

private
Expand Down Expand Up @@ -38,4 +39,10 @@ def generate_notifications_for_mentions(validated_mentioned_ids)
).perform
end
end

def add_mentioned_users_as_participants(validated_mentioned_ids)
validated_mentioned_ids.each do |user_id|
message.conversation.conversation_participants.find_or_create_by!(user_id: user_id)
end
end
end
25 changes: 25 additions & 0 deletions app/services/messages/new_message_notification_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,31 @@ class Messages::NewMessageNotificationService
def perform
return unless message.notifiable?

notify_participating_users
notify_conversation_assignee
end

private

delegate :conversation, :sender, :account, to: :message

def notify_participating_users
participating_users = conversation.conversation_participants.map(&:user)
participating_users -= [sender] if sender.is_a?(User)

participating_users.uniq.each do |participant|
NotificationBuilder.new(
notification_type: 'participating_conversation_new_message',
user: participant,
account: account,
primary_actor: message
).perform
end
end

def notify_conversation_assignee
return if conversation.assignee.blank?
return if assignee_already_notified_via_participation?
return if conversation.assignee == sender

NotificationBuilder.new(
Expand All @@ -22,4 +38,13 @@ def notify_conversation_assignee
primary_actor: message
).perform
end

def assignee_already_notified_via_participation?
return unless conversation.conversation_participants.map(&:user).include?(conversation.assignee)

# check whether participation notifcation is disabled for assignee
notification_setting = conversation.assignee.notification_settings.find_by(account_id: account.id)
notification_setting.public_send(:email_participating_conversation_new_message?) || notification_setting
.public_send(:push_participating_conversation_new_message?)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
json.array! @participants do |participant|
json.partial! 'api/v1/models/agent', format: :json, resource: participant.user
end
Loading

0 comments on commit 7044eda

Please sign in to comment.