diff --git a/app/builders/messages/facebook/message_builder.rb b/app/builders/messages/facebook/message_builder.rb index 2c55922f6..1f59deadb 100644 --- a/app/builders/messages/facebook/message_builder.rb +++ b/app/builders/messages/facebook/message_builder.rb @@ -105,15 +105,19 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder end def message_params + content_attributes = { + in_reply_to_external_id: response.in_reply_to_external_id + } + content_attributes[:external_echo] = true if @outgoing_echo + { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: @message_type, + status: @outgoing_echo ? :delivered : :sent, content: response.content, source_id: response.identifier, - content_attributes: { - in_reply_to_external_id: response.in_reply_to_external_id - }, + content_attributes: content_attributes, sender: @outgoing_echo ? nil : @contact_inbox.contact } end diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index 7821bee49..fa437327d 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -11,6 +11,7 @@ class ConversationFinder 'priority_desc' => %w[sort_on_priority desc], 'waiting_since_asc' => %w[sort_on_waiting_since asc], 'waiting_since_desc' => %w[sort_on_waiting_since desc], + 'priority_desc_created_at_asc' => %w[sort_on_priority_created_at desc], # To be removed in v3.5.0 'latest' => %w[sort_on_last_activity_at desc], diff --git a/app/javascript/dashboard/api/contacts.js b/app/javascript/dashboard/api/contacts.js index 1e76ac987..bae5623a7 100644 --- a/app/javascript/dashboard/api/contacts.js +++ b/app/javascript/dashboard/api/contacts.js @@ -57,14 +57,14 @@ class ContactAPI extends ApiClient { return axios.post(`${this.url}/${contactId}/labels`, { labels }); } - search(search = '', page = 1, sortAttr = 'name', label = '') { + search(search = '', page = 1, sortAttr = 'name', label = '', options = {}) { let requestURL = `${this.url}/search?${buildContactParams( page, sortAttr, label, search )}`; - return axios.get(requestURL); + return axios.get(requestURL, { signal: options.signal }); } active(page = 1, sortAttr = 'name') { diff --git a/app/javascript/dashboard/api/specs/contacts.spec.js b/app/javascript/dashboard/api/specs/contacts.spec.js index 0059518b0..b21aeb102 100644 --- a/app/javascript/dashboard/api/specs/contacts.spec.js +++ b/app/javascript/dashboard/api/specs/contacts.spec.js @@ -68,7 +68,19 @@ describe('#ContactsAPI', () => { it('#search', () => { contactAPI.search('leads', 1, 'date', 'customer-support'); expect(axiosMock.get).toHaveBeenCalledWith( - '/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support' + '/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support', + { signal: undefined } + ); + }); + + it('#search with signal', () => { + const controller = new AbortController(); + contactAPI.search('leads', 1, 'date', 'customer-support', { + signal: controller.signal, + }); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support', + { signal: controller.signal } ); }); diff --git a/app/javascript/dashboard/components-next/Editor/Editor.vue b/app/javascript/dashboard/components-next/Editor/Editor.vue index c2cde6d17..847bbd600 100644 --- a/app/javascript/dashboard/components-next/Editor/Editor.vue +++ b/app/javascript/dashboard/components-next/Editor/Editor.vue @@ -28,7 +28,7 @@ const props = defineProps({ medium: { type: String, default: '' }, }); -const emit = defineEmits(['update:modelValue']); +const emit = defineEmits(['update:modelValue', 'executeCopilotAction']); const slots = useSlots(); @@ -113,6 +113,9 @@ watch( @input="handleInput" @focus="handleFocus" @blur="handleBlur" + @execute-copilot-action=" + (...args) => emit('executeCopilotAction', ...args) + " />
{ contact = rest; } selectedContact.value = contact; + contacts.value = []; if (contact?.id) { isFetchingInboxes.value = true; try { diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue b/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue index 5ba97b779..455abf996 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue @@ -15,6 +15,9 @@ import { prepareWhatsAppMessagePayload, } from 'dashboard/components-next/NewConversation/helpers/composeConversationHelper.js'; +import { useCopilotReply } from 'dashboard/composables/useCopilotReply'; +import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents'; + import ContactSelector from './ContactSelector.vue'; import InboxSelector from './InboxSelector.vue'; import EmailOptions from './EmailOptions.vue'; @@ -22,6 +25,7 @@ import MessageEditor from './MessageEditor.vue'; import ActionButtons from './ActionButtons.vue'; import InboxEmptyState from './InboxEmptyState.vue'; import AttachmentPreviews from './AttachmentPreviews.vue'; +import CopilotReplyBottomPanel from 'dashboard/components/widgets/WootWriter/CopilotReplyBottomPanel.vue'; const props = defineProps({ contacts: { type: Array, default: () => [] }, @@ -42,6 +46,7 @@ const props = defineProps({ const emit = defineEmits([ 'searchContacts', + 'resetContactSearch', 'discard', 'updateSelectedContact', 'updateTargetInbox', @@ -51,6 +56,8 @@ const emit = defineEmits([ const DEFAULT_FORMATTING = 'Context::Default'; +const copilot = useCopilotReply(); + const showContactsDropdown = ref(false); const showInboxesDropdown = ref(false); const showCcEmailsDropdown = ref(false); @@ -157,22 +164,8 @@ const isAnyDropdownActive = computed(() => { }); const handleContactSearch = value => { - showContactsDropdown.value = true; - const query = typeof value === 'string' ? value.trim() : ''; - const hasAlphabet = Array.from(query).some(char => { - const lower = char.toLowerCase(); - const upper = char.toUpperCase(); - return lower !== upper; - }); - const isEmailLike = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(query); - - const keys = ['email', 'phone_number', 'name'].filter(key => { - if (key === 'phone_number' && hasAlphabet) return false; - if (key === 'name' && isEmailLike) return false; - return true; - }); - - emit('searchContacts', { keys, query: value }); + showContactsDropdown.value = value.trim().length > 1; + emit('searchContacts', value); }; const handleDropdownUpdate = (type, value) => { @@ -186,13 +179,17 @@ const handleDropdownUpdate = (type, value) => { }; const searchCcEmails = value => { - showCcEmailsDropdown.value = true; - emit('searchContacts', { keys: ['email'], query: value }); + showBccEmailsDropdown.value = false; + emit('resetContactSearch'); + showCcEmailsDropdown.value = value.trim().length >= 2; + emit('searchContacts', value); }; const searchBccEmails = value => { - showBccEmailsDropdown.value = true; - emit('searchContacts', { keys: ['email'], query: value }); + showCcEmailsDropdown.value = false; + emit('resetContactSearch'); + showBccEmailsDropdown.value = value.trim().length >= 2; + emit('searchContacts', value); }; const setSelectedContact = async ({ value, action, ...rest }) => { @@ -210,6 +207,7 @@ const stripMessageFormatting = channelType => { const handleInboxAction = ({ value, action, channelType, medium, ...rest }) => { v$.value.$reset(); + copilot.reset(false); // Strip unsupported formatting when changing the target inbox if (channelType) { @@ -236,6 +234,7 @@ const removeSignatureFromMessage = () => { const removeTargetInbox = value => { v$.value.$reset(); + copilot.reset(false); removeSignatureFromMessage(); stripMessageFormatting(DEFAULT_FORMATTING); @@ -245,6 +244,7 @@ const removeTargetInbox = value => { }; const clearSelectedContact = () => { + copilot.reset(false); removeSignatureFromMessage(); emit('clearSelectedContact'); state.message = ''; @@ -276,6 +276,7 @@ const handleAttachFile = files => { }; const clearForm = () => { + copilot.reset(false); Object.assign(state, { message: '', subject: '', @@ -338,6 +339,24 @@ const shouldShowMessageEditor = computed(() => { !inboxTypes.value.isTwilioWhatsapp ); }); + +const isCopilotActive = computed(() => copilot.isActive?.value ?? false); + +const onSubmitCopilotReply = () => { + const acceptedMessage = copilot.accept(); + state.message = acceptedMessage; +}; + +useKeyboardEvents({ + '$mod+Enter': { + action: () => { + if (isCopilotActive.value && !copilot.isButtonDisabled.value) { + onSubmitCopilotReply(); + } + }, + allowOnFocusedInput: true, + }, +});