mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 19:17:48 +00:00 
			
		
		
		
	feat: add activity message for priority change (#6933)
* feat: add priority const * feat: add toggle priority method * feat: update controller route and specs * refactor: status change method * refactor: abstract label change and mute activity * feat: add priority change_activity * fix: interpolation for previous_changes * refactor: reduce cognitive complexity of priority_change_activity * refactor: move priority activity message handler to a separate module * refactor: move typing logic to a service * refactor: tests to reduce complexity * fix: typo * fix: constants * fix: priority conditions * fix: add a response * fix: argument destructuring in I18n.t --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
		| @@ -64,13 +64,14 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro | |||||||
|     assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent? |     assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent? | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def toggle_typing_status |   def toggle_priority | ||||||
|     case params[:typing_status] |     @conversation.toggle_priority(params[:priority]) | ||||||
|     when 'on' |     head :ok | ||||||
|       trigger_typing_event(CONVERSATION_TYPING_ON, params[:is_private]) |  | ||||||
|     when 'off' |  | ||||||
|       trigger_typing_event(CONVERSATION_TYPING_OFF, params[:is_private]) |  | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   def toggle_typing_status | ||||||
|  |     typing_status_manager = ::Conversations::TypingStatusManager.new(@conversation, current_user, params) | ||||||
|  |     typing_status_manager.toggle_typing_status | ||||||
|     head :ok |     head :ok | ||||||
|   end |   end | ||||||
|  |  | ||||||
| @@ -111,11 +112,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro | |||||||
|     @conversation.update_assignee(@agent) |     @conversation.update_assignee(@agent) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def trigger_typing_event(event, is_private) |  | ||||||
|     user = current_user.presence || @resource |  | ||||||
|     Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user, is_private: is_private) |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def conversation |   def conversation | ||||||
|     @conversation ||= Current.account.conversations.find_by!(display_id: params[:id]) |     @conversation ||= Current.account.conversations.find_by!(display_id: params[:id]) | ||||||
|     authorize @conversation.inbox, :show? |     authorize @conversation.inbox, :show? | ||||||
|   | |||||||
| @@ -52,6 +52,12 @@ class ConversationApi extends ApiClient { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   togglePriority({ conversationId, priority }) { | ||||||
|  |     return axios.post(`${this.url}/${conversationId}/toggle_priority`, { | ||||||
|  |       priority, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   assignAgent({ conversationId, agentId }) { |   assignAgent({ conversationId, agentId }) { | ||||||
|     return axios.post( |     return axios.post( | ||||||
|       `${this.url}/${conversationId}/assignments?assignee_id=${agentId}`, |       `${this.url}/${conversationId}/assignments?assignee_id=${agentId}`, | ||||||
|   | |||||||
| @@ -19,6 +19,14 @@ export const CONVERSATION_STATUS = { | |||||||
|   PENDING: 'pending', |   PENDING: 'pending', | ||||||
|   SNOOZED: 'snoozed', |   SNOOZED: 'snoozed', | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const CONVERSATION_PRIORITY = { | ||||||
|  |   URGENT: 'urgent', | ||||||
|  |   HIGH: 'high', | ||||||
|  |   LOW: 'low', | ||||||
|  |   MEDIUM: 'medium', | ||||||
|  | }; | ||||||
|  |  | ||||||
| // Size in mega bytes | // Size in mega bytes | ||||||
| export const MAXIMUM_FILE_UPLOAD_SIZE = 40; | export const MAXIMUM_FILE_UPLOAD_SIZE = 40; | ||||||
| export const MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL = 5; | export const MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL = 5; | ||||||
|   | |||||||
| @@ -1,80 +1,77 @@ | |||||||
| module ActivityMessageHandler | module ActivityMessageHandler | ||||||
|   extend ActiveSupport::Concern |   extend ActiveSupport::Concern | ||||||
|  |  | ||||||
|  |   include PriorityActivityMessageHandler | ||||||
|  |  | ||||||
|   private |   private | ||||||
|  |  | ||||||
|   def create_activity |   def create_activity | ||||||
|     user_name = Current.user.name if Current.user.present? |     user_name = Current.user.name if Current.user.present? | ||||||
|     status_change_activity(user_name) if saved_change_to_status? |     status_change_activity(user_name) if saved_change_to_status? | ||||||
|  |     priority_change_activity(user_name) if saved_change_to_priority? | ||||||
|     create_label_change(activity_message_ownner(user_name)) if saved_change_to_label_list? |     create_label_change(activity_message_ownner(user_name)) if saved_change_to_label_list? | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def status_change_activity(user_name) |   def status_change_activity(user_name) | ||||||
|     return send_automation_activity if Current.executed_by.present? |     content = if Current.executed_by.present? | ||||||
|  |                 automation_status_change_activity_content | ||||||
|     create_status_change_message(user_name) |               else | ||||||
|  |                 user_status_change_activity_content(user_name) | ||||||
|               end |               end | ||||||
|  |  | ||||||
|   def activity_message_params(content) |     ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content | ||||||
|     { account_id: account_id, inbox_id: inbox_id, message_type: :activity, content: content } |  | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def create_status_change_message(user_name) |   def user_status_change_activity_content(user_name) | ||||||
|     content = if user_name |     if user_name | ||||||
|       I18n.t("conversations.activity.status.#{status}", user_name: user_name) |       I18n.t("conversations.activity.status.#{status}", user_name: user_name) | ||||||
|     elsif Current.contact.present? && resolved? |     elsif Current.contact.present? && resolved? | ||||||
|       I18n.t('conversations.activity.status.contact_resolved', contact_name: Current.contact.name.capitalize) |       I18n.t('conversations.activity.status.contact_resolved', contact_name: Current.contact.name.capitalize) | ||||||
|     elsif resolved? |     elsif resolved? | ||||||
|       I18n.t('conversations.activity.status.auto_resolved', duration: auto_resolve_duration) |       I18n.t('conversations.activity.status.auto_resolved', duration: auto_resolve_duration) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content |  | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def send_automation_activity |   def automation_status_change_activity_content | ||||||
|     content = if Current.executed_by.instance_of?(AutomationRule) |     if Current.executed_by.instance_of?(AutomationRule) | ||||||
|       I18n.t("conversations.activity.status.#{status}", user_name: 'Automation System') |       I18n.t("conversations.activity.status.#{status}", user_name: 'Automation System') | ||||||
|     elsif Current.executed_by.instance_of?(Contact) |     elsif Current.executed_by.instance_of?(Contact) | ||||||
|       Current.executed_by = nil |       Current.executed_by = nil | ||||||
|       I18n.t('conversations.activity.status.system_auto_open') |       I18n.t('conversations.activity.status.system_auto_open') | ||||||
|     end |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|     ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content |   def activity_message_params(content) | ||||||
|  |     { account_id: account_id, inbox_id: inbox_id, message_type: :activity, content: content } | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def create_label_added(user_name, labels = []) |   def create_label_added(user_name, labels = []) | ||||||
|     return unless labels.size.positive? |     create_label_change_activity('added', user_name, labels) | ||||||
|  |  | ||||||
|     params = { user_name: user_name, labels: labels.join(', ') } |  | ||||||
|     content = I18n.t('conversations.activity.labels.added', **params) |  | ||||||
|  |  | ||||||
|     ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content |  | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def create_label_removed(user_name, labels = []) |   def create_label_removed(user_name, labels = []) | ||||||
|  |     create_label_change_activity('removed', user_name, labels) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def create_label_change_activity(change_type, user_name, labels = []) | ||||||
|     return unless labels.size.positive? |     return unless labels.size.positive? | ||||||
|  |  | ||||||
|     params = { user_name: user_name, labels: labels.join(', ') } |     content = I18n.t("conversations.activity.labels.#{change_type}", user_name: user_name, labels: labels.join(', ')) | ||||||
|     content = I18n.t('conversations.activity.labels.removed', **params) |  | ||||||
|  |  | ||||||
|     ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content |     ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def create_muted_message |   def create_muted_message | ||||||
|     return unless Current.user |     create_mute_change_activity('muted') | ||||||
|  |  | ||||||
|     params = { user_name: Current.user.name } |  | ||||||
|     content = I18n.t('conversations.activity.muted', **params) |  | ||||||
|  |  | ||||||
|     ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content |  | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def create_unmuted_message |   def create_unmuted_message | ||||||
|  |     create_mute_change_activity('unmuted') | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def create_mute_change_activity(change_type) | ||||||
|     return unless Current.user |     return unless Current.user | ||||||
|  |  | ||||||
|     params = { user_name: Current.user.name } |     content = I18n.t("conversations.activity.#{change_type}", user_name: Current.user.name) | ||||||
|     content = I18n.t('conversations.activity.unmuted', **params) |  | ||||||
|  |  | ||||||
|     ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content |     ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										33
									
								
								app/models/concerns/priority_activity_message_handler.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								app/models/concerns/priority_activity_message_handler.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | module PriorityActivityMessageHandler | ||||||
|  |   extend ActiveSupport::Concern | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def priority_change_activity(user_name) | ||||||
|  |     old_priority, new_priority = previous_changes.values_at('priority')[0] | ||||||
|  |     return unless priority_change?(old_priority, new_priority) | ||||||
|  |  | ||||||
|  |     user = Current.executed_by.instance_of?(AutomationRule) ? 'Automation System' : user_name | ||||||
|  |     content = build_priority_change_content(user, old_priority, new_priority) | ||||||
|  |  | ||||||
|  |     ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def priority_change?(old_priority, new_priority) | ||||||
|  |     old_priority.present? || new_priority.present? | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def build_priority_change_content(user_name, old_priority = nil, new_priority = nil) | ||||||
|  |     change_type = get_priority_change_type(old_priority, new_priority) | ||||||
|  |  | ||||||
|  |     I18n.t("conversations.activity.priority.#{change_type}", user_name: user_name, new_priority: new_priority, old_priority: old_priority) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def get_priority_change_type(old_priority, new_priority) | ||||||
|  |     case [old_priority.present?, new_priority.present?] | ||||||
|  |     when [true, true] then 'updated' | ||||||
|  |     when [false, true] then 'added' | ||||||
|  |     when [true, false] then 'removed' | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -149,6 +149,11 @@ class Conversation < ApplicationRecord | |||||||
|     save |     save | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   def toggle_priority(priority = nil) | ||||||
|  |     self.priority = priority.presence | ||||||
|  |     save | ||||||
|  |   end | ||||||
|  |  | ||||||
|   def bot_handoff! |   def bot_handoff! | ||||||
|     open! |     open! | ||||||
|     dispatcher_dispatch(CONVERSATION_BOT_HANDOFF) |     dispatcher_dispatch(CONVERSATION_BOT_HANDOFF) | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								app/services/conversations/typing_status_manager.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								app/services/conversations/typing_status_manager.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | class Conversations::TypingStatusManager | ||||||
|  |   include Events::Types | ||||||
|  |  | ||||||
|  |   attr_reader :conversation, :user, :params | ||||||
|  |  | ||||||
|  |   def initialize(conversation, user, params) | ||||||
|  |     @conversation = conversation | ||||||
|  |     @user = user | ||||||
|  |     @params = params | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def trigger_typing_event(event, is_private) | ||||||
|  |     user = @user.presence || @resource | ||||||
|  |     Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user, is_private: is_private) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def toggle_typing_status | ||||||
|  |     case params[:typing_status] | ||||||
|  |     when 'on' | ||||||
|  |       trigger_typing_event(CONVERSATION_TYPING_ON, params[:is_private]) | ||||||
|  |     when 'off' | ||||||
|  |       trigger_typing_event(CONVERSATION_TYPING_OFF, params[:is_private]) | ||||||
|  |     end | ||||||
|  |     # Return the head :ok response from the controller | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -129,6 +129,10 @@ en: | |||||||
|         snoozed: "Conversation was snoozed by %{user_name}" |         snoozed: "Conversation was snoozed by %{user_name}" | ||||||
|         auto_resolved: "Conversation was marked resolved by system due to %{duration} days of inactivity" |         auto_resolved: "Conversation was marked resolved by system due to %{duration} days of inactivity" | ||||||
|         system_auto_open: System reopened the conversation due to a new incoming message. |         system_auto_open: System reopened the conversation due to a new incoming message. | ||||||
|  |       priority: | ||||||
|  |         added: '%{user_name} set the priority to %{new_priority}' | ||||||
|  |         updated: '%{user_name} changed the priority from %{old_priority} to %{new_priority}' | ||||||
|  |         removed: '%{user_name} removed the priority' | ||||||
|       assignee: |       assignee: | ||||||
|         self_assigned: "%{user_name} self-assigned this conversation" |         self_assigned: "%{user_name} self-assigned this conversation" | ||||||
|         assigned: "Assigned to %{assignee_name} by %{user_name}" |         assigned: "Assigned to %{assignee_name} by %{user_name}" | ||||||
|   | |||||||
| @@ -91,6 +91,7 @@ Rails.application.routes.draw do | |||||||
|               post :unmute |               post :unmute | ||||||
|               post :transcript |               post :transcript | ||||||
|               post :toggle_status |               post :toggle_status | ||||||
|  |               post :toggle_priority | ||||||
|               post :toggle_typing_status |               post :toggle_typing_status | ||||||
|               post :update_last_seen |               post :update_last_seen | ||||||
|               post :unread |               post :unread | ||||||
|   | |||||||
| @@ -434,6 +434,52 @@ RSpec.describe 'Conversations API', type: :request do | |||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   describe 'POST /api/v1/accounts/{account.id}/conversations/:id/toggle_priority' do | ||||||
|  |     let(:conversation) { create(:conversation, account: account) } | ||||||
|  |  | ||||||
|  |     context 'when it is an unauthenticated user' do | ||||||
|  |       it 'returns unauthorized' do | ||||||
|  |         post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_priority" | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:unauthorized) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when it is an authenticated user' do | ||||||
|  |       let(:agent) { create(:user, account: account, role: :agent) } | ||||||
|  |       let(:administrator) { create(:user, account: account, role: :administrator) } | ||||||
|  |  | ||||||
|  |       before do | ||||||
|  |         create(:inbox_member, user: agent, inbox: conversation.inbox) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'toggles the conversation priority to nil if no value is passed' do | ||||||
|  |         expect(conversation.priority).to be_nil | ||||||
|  |  | ||||||
|  |         post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_priority", | ||||||
|  |              headers: agent.create_new_auth_token, | ||||||
|  |              params: { priority: 'low' }, | ||||||
|  |              as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:success) | ||||||
|  |         expect(conversation.reload.priority).to eq('low') | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'toggles the conversation priority' do | ||||||
|  |         conversation.priority = 'low' | ||||||
|  |         conversation.save! | ||||||
|  |         expect(conversation.reload.priority).to eq('low') | ||||||
|  |  | ||||||
|  |         post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_priority", | ||||||
|  |              headers: agent.create_new_auth_token, | ||||||
|  |              as: :json | ||||||
|  |  | ||||||
|  |         expect(response).to have_http_status(:success) | ||||||
|  |         expect(conversation.reload.priority).to be_nil | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|   describe 'POST /api/v1/accounts/{account.id}/conversations/:id/toggle_typing_status' do |   describe 'POST /api/v1/accounts/{account.id}/conversations/:id/toggle_typing_status' do | ||||||
|     let(:conversation) { create(:conversation, account: account) } |     let(:conversation) { create(:conversation, account: account) } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -292,6 +292,47 @@ RSpec.describe Conversation, type: :model do | |||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   describe '#toggle_priority' do | ||||||
|  |     it 'defaults priority to nil when created' do | ||||||
|  |       conversation = create(:conversation, status: 'open') | ||||||
|  |       expect(conversation.priority).to be_nil | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     it 'toggles the priority to nil if nothing is passed' do | ||||||
|  |       conversation = create(:conversation, status: 'open', priority: 'high') | ||||||
|  |       expect(conversation.toggle_priority).to be(true) | ||||||
|  |       expect(conversation.reload.priority).to be_nil | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     it 'sets the priority to low' do | ||||||
|  |       conversation = create(:conversation, status: 'open') | ||||||
|  |  | ||||||
|  |       expect(conversation.toggle_priority('low')).to be(true) | ||||||
|  |       expect(conversation.reload.priority).to eq('low') | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     it 'sets the priority to medium' do | ||||||
|  |       conversation = create(:conversation, status: 'open') | ||||||
|  |  | ||||||
|  |       expect(conversation.toggle_priority('medium')).to be(true) | ||||||
|  |       expect(conversation.reload.priority).to eq('medium') | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     it 'sets the priority to high' do | ||||||
|  |       conversation = create(:conversation, status: 'open') | ||||||
|  |  | ||||||
|  |       expect(conversation.toggle_priority('high')).to be(true) | ||||||
|  |       expect(conversation.reload.priority).to eq('high') | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     it 'sets the priority to urgent' do | ||||||
|  |       conversation = create(:conversation, status: 'open') | ||||||
|  |  | ||||||
|  |       expect(conversation.toggle_priority('urgent')).to be(true) | ||||||
|  |       expect(conversation.reload.priority).to eq('urgent') | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|   describe '#ensure_snooze_until_reset' do |   describe '#ensure_snooze_until_reset' do | ||||||
|     it 'resets the snoozed_until when status is toggled' do |     it 'resets the snoozed_until when status is toggled' do | ||||||
|       conversation = create(:conversation, status: 'snoozed', snoozed_until: 2.days.from_now) |       conversation = create(:conversation, status: 'snoozed', snoozed_until: 2.days.from_now) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Shivam Mishra
					Shivam Mishra