mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +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? | ||||
|   end | ||||
|  | ||||
|   def toggle_priority | ||||
|     @conversation.toggle_priority(params[:priority]) | ||||
|     head :ok | ||||
|   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 | ||||
|     typing_status_manager = ::Conversations::TypingStatusManager.new(@conversation, current_user, params) | ||||
|     typing_status_manager.toggle_typing_status | ||||
|     head :ok | ||||
|   end | ||||
|  | ||||
| @@ -111,11 +112,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro | ||||
|     @conversation.update_assignee(@agent) | ||||
|   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 | ||||
|     @conversation ||= Current.account.conversations.find_by!(display_id: params[:id]) | ||||
|     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 }) { | ||||
|     return axios.post( | ||||
|       `${this.url}/${conversationId}/assignments?assignee_id=${agentId}`, | ||||
|   | ||||
| @@ -19,6 +19,14 @@ export const CONVERSATION_STATUS = { | ||||
|   PENDING: 'pending', | ||||
|   SNOOZED: 'snoozed', | ||||
| }; | ||||
|  | ||||
| export const CONVERSATION_PRIORITY = { | ||||
|   URGENT: 'urgent', | ||||
|   HIGH: 'high', | ||||
|   LOW: 'low', | ||||
|   MEDIUM: 'medium', | ||||
| }; | ||||
|  | ||||
| // Size in mega bytes | ||||
| export const MAXIMUM_FILE_UPLOAD_SIZE = 40; | ||||
| export const MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL = 5; | ||||
|   | ||||
| @@ -1,80 +1,77 @@ | ||||
| module ActivityMessageHandler | ||||
|   extend ActiveSupport::Concern | ||||
|  | ||||
|   include PriorityActivityMessageHandler | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def create_activity | ||||
|     user_name = Current.user.name if Current.user.present? | ||||
|     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? | ||||
|   end | ||||
|  | ||||
|   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 | ||||
|               else | ||||
|                 user_status_change_activity_content(user_name) | ||||
|               end | ||||
|  | ||||
|     create_status_change_message(user_name) | ||||
|     ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content | ||||
|   end | ||||
|  | ||||
|   def user_status_change_activity_content(user_name) | ||||
|     if user_name | ||||
|       I18n.t("conversations.activity.status.#{status}", user_name: user_name) | ||||
|     elsif Current.contact.present? && resolved? | ||||
|       I18n.t('conversations.activity.status.contact_resolved', contact_name: Current.contact.name.capitalize) | ||||
|     elsif resolved? | ||||
|       I18n.t('conversations.activity.status.auto_resolved', duration: auto_resolve_duration) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def automation_status_change_activity_content | ||||
|     if Current.executed_by.instance_of?(AutomationRule) | ||||
|       I18n.t("conversations.activity.status.#{status}", user_name: 'Automation System') | ||||
|     elsif Current.executed_by.instance_of?(Contact) | ||||
|       Current.executed_by = nil | ||||
|       I18n.t('conversations.activity.status.system_auto_open') | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def activity_message_params(content) | ||||
|     { account_id: account_id, inbox_id: inbox_id, message_type: :activity, content: content } | ||||
|   end | ||||
|  | ||||
|   def create_status_change_message(user_name) | ||||
|     content = if user_name | ||||
|                 I18n.t("conversations.activity.status.#{status}", user_name: user_name) | ||||
|               elsif Current.contact.present? && resolved? | ||||
|                 I18n.t('conversations.activity.status.contact_resolved', contact_name: Current.contact.name.capitalize) | ||||
|               elsif resolved? | ||||
|                 I18n.t('conversations.activity.status.auto_resolved', duration: auto_resolve_duration) | ||||
|               end | ||||
|  | ||||
|     ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content | ||||
|   end | ||||
|  | ||||
|   def send_automation_activity | ||||
|     content = if Current.executed_by.instance_of?(AutomationRule) | ||||
|                 I18n.t("conversations.activity.status.#{status}", user_name: 'Automation System') | ||||
|               elsif Current.executed_by.instance_of?(Contact) | ||||
|                 Current.executed_by = nil | ||||
|                 I18n.t('conversations.activity.status.system_auto_open') | ||||
|               end | ||||
|  | ||||
|     ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content | ||||
|   end | ||||
|  | ||||
|   def create_label_added(user_name, labels = []) | ||||
|     return unless labels.size.positive? | ||||
|  | ||||
|     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 | ||||
|     create_label_change_activity('added', user_name, labels) | ||||
|   end | ||||
|  | ||||
|   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? | ||||
|  | ||||
|     params = { user_name: user_name, labels: labels.join(', ') } | ||||
|     content = I18n.t('conversations.activity.labels.removed', **params) | ||||
|  | ||||
|     content = I18n.t("conversations.activity.labels.#{change_type}", user_name: user_name, labels: labels.join(', ')) | ||||
|     ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content | ||||
|   end | ||||
|  | ||||
|   def create_muted_message | ||||
|     return unless Current.user | ||||
|  | ||||
|     params = { user_name: Current.user.name } | ||||
|     content = I18n.t('conversations.activity.muted', **params) | ||||
|  | ||||
|     ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content | ||||
|     create_mute_change_activity('muted') | ||||
|   end | ||||
|  | ||||
|   def create_unmuted_message | ||||
|     create_mute_change_activity('unmuted') | ||||
|   end | ||||
|  | ||||
|   def create_mute_change_activity(change_type) | ||||
|     return unless Current.user | ||||
|  | ||||
|     params = { user_name: Current.user.name } | ||||
|     content = I18n.t('conversations.activity.unmuted', **params) | ||||
|  | ||||
|     content = I18n.t("conversations.activity.#{change_type}", user_name: Current.user.name) | ||||
|     ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content | ||||
|   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 | ||||
|   end | ||||
|  | ||||
|   def toggle_priority(priority = nil) | ||||
|     self.priority = priority.presence | ||||
|     save | ||||
|   end | ||||
|  | ||||
|   def bot_handoff! | ||||
|     open! | ||||
|     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}" | ||||
|         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. | ||||
|       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: | ||||
|         self_assigned: "%{user_name} self-assigned this conversation" | ||||
|         assigned: "Assigned to %{assignee_name} by %{user_name}" | ||||
|   | ||||
| @@ -91,6 +91,7 @@ Rails.application.routes.draw do | ||||
|               post :unmute | ||||
|               post :transcript | ||||
|               post :toggle_status | ||||
|               post :toggle_priority | ||||
|               post :toggle_typing_status | ||||
|               post :update_last_seen | ||||
|               post :unread | ||||
|   | ||||
| @@ -434,6 +434,52 @@ RSpec.describe 'Conversations API', type: :request do | ||||
|     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 | ||||
|     let(:conversation) { create(:conversation, account: account) } | ||||
|  | ||||
|   | ||||
| @@ -292,6 +292,47 @@ RSpec.describe Conversation, type: :model do | ||||
|     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 | ||||
|     it 'resets the snoozed_until when status is toggled' do | ||||
|       conversation = create(:conversation, status: 'snoozed', snoozed_until: 2.days.from_now) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Shivam Mishra
					Shivam Mishra