diff --git a/Gemfile b/Gemfile index 6ba360289..a6679f069 100644 --- a/Gemfile +++ b/Gemfile @@ -113,9 +113,9 @@ gem 'sentry-ruby', require: false gem 'sentry-sidekiq', '>= 5.11.0', require: false ##-- background job processing --## -gem 'sidekiq' +gem 'sidekiq', '>= 7.1.3' # We want cron jobs -gem 'sidekiq-cron' +gem 'sidekiq-cron', '>= 1.10.1' ##-- Push notification service --## gem 'fcm' diff --git a/Gemfile.lock b/Gemfile.lock index d2fdbe457..bf109b551 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -708,12 +708,12 @@ GEM sexp_processor (4.17.0) shoulda-matchers (5.3.0) activesupport (>= 5.2.0) - sidekiq (7.1.2) + sidekiq (7.1.3) concurrent-ruby (< 2) connection_pool (>= 2.3.0) rack (>= 2.2.4) redis-client (>= 0.14.0) - sidekiq-cron (1.10.0) + sidekiq-cron (1.10.1) fugit (~> 1.8) globalid (>= 1.0.1) sidekiq (>= 6) @@ -926,8 +926,8 @@ DEPENDENCIES sentry-ruby sentry-sidekiq (>= 5.11.0) shoulda-matchers - sidekiq - sidekiq-cron + sidekiq (>= 7.1.3) + sidekiq-cron (>= 1.10.1) simplecov (= 0.17.1) slack-ruby-client (~> 2.0.0) spring diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss index e538299aa..f6fae6737 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss @@ -152,10 +152,6 @@ &.is-image { @apply rounded-lg; - - .message__mail-head { - @apply px-4 py-2; - } } } diff --git a/app/javascript/dashboard/assets/scss/widgets/_modal.scss b/app/javascript/dashboard/assets/scss/widgets/_modal.scss index 1f0641c6e..f20ba72b1 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_modal.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_modal.scss @@ -4,6 +4,10 @@ @apply flex items-center justify-center bg-modal dark:bg-modal z-[9990] h-full left-0 fixed top-0 w-full; } +.modal--close { + @apply absolute right-2 rtl:right-[unset] rtl:left-2 top-2; +} + .page-top-bar { @apply px-8 pt-9 pb-0; diff --git a/app/javascript/dashboard/components/Modal.vue b/app/javascript/dashboard/components/Modal.vue index 09d449271..b42ee5526 100644 --- a/app/javascript/dashboard/components/Modal.vue +++ b/app/javascript/dashboard/components/Modal.vue @@ -12,7 +12,7 @@ color-scheme="secondary" icon="dismiss" variant="clear" - class="absolute ltr:right-2 rtl:left-2 top-2 z-10" + class="modal--close" @click="close" /> diff --git a/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue b/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue index 0a1c80fe6..586a9c1f2 100644 --- a/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue +++ b/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue @@ -1,9 +1,11 @@ @@ -233,11 +169,6 @@ import { INBOX_TYPES } from 'shared/mixins/inboxMixin'; import { ExceptionWithMessage } from 'shared/helpers/CustomErrors'; import { getInboxSource } from 'dashboard/helper/inbox'; import { required, requiredIf } from 'vuelidate/lib/validators'; -import inboxMixin from 'shared/mixins/inboxMixin'; -import FileUpload from 'vue-upload-component'; -import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview'; -import { ALLOWED_FILE_TYPES } from 'shared/constants/messages'; -import fileUploadMixin from 'dashboard/mixins/fileUploadMixin'; export default { components: { @@ -247,10 +178,8 @@ export default { CannedResponse, WhatsappTemplates, InboxDropdownItem, - FileUpload, - AttachmentPreview, }, - mixins: [alertMixin, inboxMixin, fileUploadMixin], + mixins: [alertMixin], props: { contact: { type: Object, @@ -272,7 +201,6 @@ export default { ccEmails: '', targetInbox: {}, whatsappTemplateSelected: false, - attachedFiles: [], }; }, validations: { @@ -291,9 +219,8 @@ export default { uiFlags: 'contacts/getUIFlags', conversationsUiFlags: 'contactConversations/getUIFlags', currentUser: 'getCurrentUser', - globalConfig: 'globalConfig/get', }), - newMessagePayload() { + emailMessagePayload() { const payload = { inboxId: this.targetInbox.id, sourceId: this.targetInbox.sourceId, @@ -302,12 +229,6 @@ export default { mailSubject: this.subject, assigneeId: this.currentUser.id, }; - - if (this.attachedFiles && this.attachedFiles.length) { - payload.files = []; - this.setAttachmentPayload(payload); - } - if (this.ccEmails) { payload.message.cc_emails = this.ccEmails; } @@ -363,15 +284,6 @@ export default { hasWhatsappTemplates() { return !!this.selectedInbox.inbox?.message_templates; }, - hasAttachments() { - return this.attachedFiles.length; - }, - inbox() { - return this.targetInbox; - }, - allowedFileTypes() { - return ALLOWED_FILE_TYPES; - }, }, watch: { message(value) { @@ -388,33 +300,6 @@ export default { }, }, methods: { - setAttachmentPayload(payload) { - this.attachedFiles.forEach(attachment => { - if (this.globalConfig.directUploadsEnabled) { - payload.files.push(attachment.blobSignedId); - } else { - payload.files.push(attachment.resource.file); - } - }); - }, - attachFile({ blob, file }) { - const reader = new FileReader(); - reader.readAsDataURL(file.file); - reader.onloadend = () => { - this.attachedFiles.push({ - currentChatId: this.contact.id, - resource: blob || file, - isPrivate: this.isPrivate, - thumb: reader.result, - blobSignedId: blob ? blob.signed_id : undefined, - }); - }; - }, - removeAttachment(itemIndex) { - this.attachedFiles = this.attachedFiles.filter( - (item, index) => itemIndex !== index - ); - }, onCancel() { this.$emit('cancel'); }, @@ -435,10 +320,6 @@ export default { message: { content, template_params: templateParams }, assigneeId: this.currentUser.id, }; - if (this.attachedFiles && this.attachedFiles.length) { - payload.files = []; - this.setAttachmentPayload(payload); - } return payload; }, onFormSubmit() { @@ -446,7 +327,7 @@ export default { if (this.$v.$invalid) { return; } - this.createConversation(this.newMessagePayload); + this.createConversation(this.emailMessagePayload); }, async createConversation(payload) { try { @@ -508,18 +389,6 @@ export default { } } -.file-uploads { - @apply text-start; -} - -.multiselect-wrap--small.has-multi-select-error { - ::v-deep { - .multiselect__tags { - @apply border-red-500; - } - } -} - ::v-deep { .mention--box { @apply left-0 m-auto right-0 top-auto h-fit; diff --git a/app/javascript/dashboard/store/modules/contactConversations.js b/app/javascript/dashboard/store/modules/contactConversations.js index 94158735e..a696a3e75 100644 --- a/app/javascript/dashboard/store/modules/contactConversations.js +++ b/app/javascript/dashboard/store/modules/contactConversations.js @@ -3,34 +3,6 @@ import * as types from '../mutation-types'; import ContactAPI from '../../api/contacts'; import ConversationApi from '../../api/conversations'; -export const createMessagePayload = (payload, message) => { - const { content, cc_emails, bcc_emails } = message; - payload.append('message[content]', content); - if (cc_emails) payload.append('message[cc_emails]', cc_emails); - if (bcc_emails) payload.append('message[bcc_emails]', bcc_emails); -}; - -export const createConversationPayload = ({ params, contactId, files }) => { - const { inboxId, message, sourceId, mailSubject, assigneeId } = params; - const payload = new FormData(); - - if (message) { - createMessagePayload(payload, message); - } - - if (files && files.length > 0) { - files.forEach(file => payload.append('message[attachments][]', file)); - } - - payload.append('inbox_id', inboxId); - payload.append('contact_id', contactId); - payload.append('source_id', sourceId); - payload.append('additional_attributes[mail_subject]', mailSubject); - payload.append('assignee_id', assigneeId); - - return payload; -}; - const state = { records: {}, uiFlags: { @@ -52,17 +24,29 @@ export const actions = { commit(types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, { isCreating: true, }); - const { contactId, files } = params; - + const { + inboxId, + message, + contactId, + sourceId, + mailSubject, + assigneeId, + } = params; try { - const payload = createConversationPayload({ params, contactId, files }); - - const { data } = await ConversationApi.create(payload); + const { data } = await ConversationApi.create({ + inbox_id: inboxId, + contact_id: contactId, + source_id: sourceId, + additional_attributes: { + mail_subject: mailSubject, + }, + message, + assignee_id: assigneeId, + }); commit(types.default.ADD_CONTACT_CONVERSATION, { id: contactId, data, }); - return data; } catch (error) { throw new Error(error); diff --git a/app/javascript/dashboard/store/modules/specs/contactConversations/actions.spec.js b/app/javascript/dashboard/store/modules/specs/contactConversations/actions.spec.js index 8524f7bce..0de32311b 100644 --- a/app/javascript/dashboard/store/modules/specs/contactConversations/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/contactConversations/actions.spec.js @@ -1,9 +1,5 @@ import axios from 'axios'; -import { - actions, - createMessagePayload, - createConversationPayload, -} from '../../contactConversations'; +import { actions } from '../../contactConversations'; import * as types from '../../../mutation-types'; import conversationList from './fixtures'; @@ -53,35 +49,6 @@ describe('#actions', () => { contactId: 4, sourceId: 5, mailSubject: 'Mail Subject', - assigneeId: 6, - files: [], - } - ); - expect(commit.mock.calls).toEqual([ - [types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, { isCreating: true }], - - [ - types.default.ADD_CONTACT_CONVERSATION, - { id: 4, data: conversationList[0] }, - ], - [ - types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, - { isCreating: false }, - ], - ]); - }); - it('sends correct actions with files if API is success', async () => { - axios.post.mockResolvedValue({ data: conversationList[0] }); - await actions.create( - { commit }, - { - inboxId: 1, - message: { content: 'hi' }, - contactId: 4, - sourceId: 5, - assigneeId: 6, - mailSubject: 'Mail Subject', - files: [new File([], 'file1')], } ); expect(commit.mock.calls).toEqual([ @@ -107,7 +74,6 @@ describe('#actions', () => { inboxId: 1, message: { content: 'hi' }, contactId: 4, - assigneeId: 6, sourceId: 5, mailSubject: 'Mail Subject', } @@ -121,121 +87,5 @@ describe('#actions', () => { ], ]); }); - it('sends correct actions with files if API is error', async () => { - axios.post.mockRejectedValue({ message: 'Incorrect header' }); - - await expect( - actions.create( - { commit }, - { - inboxId: 1, - message: { content: 'hi' }, - contactId: 4, - assigneeId: 6, - sourceId: 5, - mailSubject: 'Mail Subject', - files: [new File([], 'file1')], - } - ) - ).rejects.toThrow(Error); - expect(commit.mock.calls).toEqual([ - [types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, { isCreating: true }], - [ - types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, - { isCreating: false }, - ], - ]); - }); - }); -}); - -describe('createMessagePayload', () => { - it('creates message payload with cc and bcc emails', () => { - const payload = new FormData(); - const message = { - content: 'Test message content', - cc_emails: 'cc@example.com', - bcc_emails: 'bcc@example.com', - }; - - createMessagePayload(payload, message); - - expect(payload.get('message[content]')).toBe(message.content); - expect(payload.get('message[cc_emails]')).toBe(message.cc_emails); - expect(payload.get('message[bcc_emails]')).toBe(message.bcc_emails); - }); - - it('creates message payload without cc and bcc emails', () => { - const payload = new FormData(); - const message = { - content: 'Test message content', - }; - - createMessagePayload(payload, message); - - expect(payload.get('message[content]')).toBe(message.content); - expect(payload.get('message[cc_emails]')).toBeNull(); - expect(payload.get('message[bcc_emails]')).toBeNull(); - }); -}); - -describe('createConversationPayload', () => { - it('creates conversation payload with message and attachments', () => { - const options = { - params: { - inboxId: '1', - message: { - content: 'Test message content', - }, - sourceId: '12', - mailSubject: 'Test Subject', - assigneeId: '123', - }, - contactId: '23', - files: ['file1.pdf', 'file2.jpg'], - }; - - const payload = createConversationPayload(options); - - expect(payload.get('message[content]')).toBe( - options.params.message.content - ); - expect(payload.get('inbox_id')).toBe(options.params.inboxId); - expect(payload.get('contact_id')).toBe(options.contactId); - expect(payload.get('source_id')).toBe(options.params.sourceId); - expect(payload.get('additional_attributes[mail_subject]')).toBe( - options.params.mailSubject - ); - expect(payload.get('assignee_id')).toBe(options.params.assigneeId); - expect(payload.getAll('message[attachments][]')).toEqual(options.files); - }); - - it('creates conversation payload with message and without attachments', () => { - const options = { - params: { - inboxId: '1', - message: { - content: 'Test message content', - }, - sourceId: '12', - mailSubject: 'Test Subject', - assigneeId: '123', - }, - contactId: '23', - }; - - const payload = createConversationPayload(options); - - expect(payload.get('message[content]')).toBe( - options.params.message.content - ); - expect(payload.get('inbox_id')).toBe(options.params.inboxId); - expect(payload.get('contact_id')).toBe(options.contactId); - expect(payload.get('source_id')).toBe(options.params.sourceId); - expect(payload.get('additional_attributes[mail_subject]')).toBe( - options.params.mailSubject - ); - expect(payload.get('assignee_id')).toBe(options.params.assigneeId); - expect(payload.getAll('message[attachments][]')).toEqual([]); }); }); diff --git a/app/javascript/shared/constants/busEvents.js b/app/javascript/shared/constants/busEvents.js index 6edf96fcd..6c9a21072 100644 --- a/app/javascript/shared/constants/busEvents.js +++ b/app/javascript/shared/constants/busEvents.js @@ -10,5 +10,4 @@ export const BUS_EVENTS = { ON_MESSAGE_LIST_SCROLL: 'ON_MESSAGE_LIST_SCROLL', WEBSOCKET_DISCONNECT: 'WEBSOCKET_DISCONNECT', SHOW_TOAST: 'newToastMessage', - NEW_CONVERSATION_MODAL: 'newConversationModal', }; diff --git a/app/javascript/widget/i18n/locale/es.json b/app/javascript/widget/i18n/locale/es.json index 133f940b1..9a4dbf984 100644 --- a/app/javascript/widget/i18n/locale/es.json +++ b/app/javascript/widget/i18n/locale/es.json @@ -107,7 +107,7 @@ } }, "PORTAL": { - "POPULAR_ARTICLES": "Popular Articles", + "POPULAR_ARTICLES": "Artículos populares", "VIEW_ALL_ARTICLES": "Ver todos los artículos" } } diff --git a/app/javascript/widget/i18n/locale/nl.json b/app/javascript/widget/i18n/locale/nl.json index 7f5a66a97..1d69938e9 100644 --- a/app/javascript/widget/i18n/locale/nl.json +++ b/app/javascript/widget/i18n/locale/nl.json @@ -20,16 +20,16 @@ "IN_A_FEW_MINUTES": "Reageert meestal binnen een paar minuten", "IN_A_FEW_HOURS": "Reageert meestal binnen een paar uur", "IN_A_DAY": "Reageert meestal binnen een dag", - "BACK_IN": "We will be back online" + "BACK_IN": "We zullen weer online zijn" }, "DAY_NAMES": [ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday" + "zondag", + "maandag", + "dinsdag", + "woensdag", + "donderdag", + "vrijdag", + "zaterdag" ], "START_CONVERSATION": "Start Chat", "END_CONVERSATION": "Gesprek beëindigen", @@ -66,8 +66,8 @@ "LABEL": "Telefoonnummer", "PLACEHOLDER": "Vul je telefoonnummer in", "REQUIRED_ERROR": "Telefoon nummer is vereist", - "DIAL_CODE_VALID_ERROR": "Please select a country code", - "VALID_ERROR": "Please enter a valid phone number", + "DIAL_CODE_VALID_ERROR": "Selecteer a.u.b. een landcode", + "VALID_ERROR": "Voer een geldig telefoonnummer in", "DROPDOWN_EMPTY": "Geen resultaten gevonden" }, "MESSAGE": { @@ -78,7 +78,7 @@ }, "CAMPAIGN_HEADER": "Geef uw naam en e-mail voordat u het gesprek begint", "IS_REQUIRED": "is vereist", - "REQUIRED": "Required" + "REQUIRED": "Vereist" }, "FILE_SIZE_LIMIT": "Bestand overschrijdt de bijlagelimiet van {MAXIMUM_FILE_UPLOAD_SIZE}", "CHAT_FORM": { @@ -102,12 +102,12 @@ }, "INTEGRATIONS": { "DYTE": { - "CLICK_HERE_TO_JOIN": "Click here to join", - "LEAVE_THE_ROOM": "Leave the call" + "CLICK_HERE_TO_JOIN": "Klik hier om deel te nemen", + "LEAVE_THE_ROOM": "Gesprek verlaten" } }, "PORTAL": { - "POPULAR_ARTICLES": "Popular Articles", - "VIEW_ALL_ARTICLES": "View all articles" + "POPULAR_ARTICLES": "Populaire artikelen", + "VIEW_ALL_ARTICLES": "Bekijk alle artikelen" } } diff --git a/app/javascript/widget/i18n/locale/zh_CN.json b/app/javascript/widget/i18n/locale/zh_CN.json index 340240e14..654d83360 100644 --- a/app/javascript/widget/i18n/locale/zh_CN.json +++ b/app/javascript/widget/i18n/locale/zh_CN.json @@ -87,8 +87,8 @@ } }, "EMOJI": { - "PLACEHOLDER": "Search emojis", - "NOT_FOUND": "No emoji match your search" + "PLACEHOLDER": "搜索表情符号", + "NOT_FOUND": "没有适合你的搜索结果" }, "CSAT": { "TITLE": "评价您的对话", diff --git a/app/services/twilio/incoming_message_service.rb b/app/services/twilio/incoming_message_service.rb index 4473131df..097274d44 100644 --- a/app/services/twilio/incoming_message_service.rb +++ b/app/services/twilio/incoming_message_service.rb @@ -9,7 +9,7 @@ class Twilio::IncomingMessageService set_contact set_conversation @message = @conversation.messages.create!( - content: params[:Body], + content: message_body, account_id: @inbox.account_id, inbox_id: @inbox.id, message_type: :incoming, @@ -46,6 +46,10 @@ class Twilio::IncomingMessageService TelephoneNumber.parse(phone_number).international_number end + def message_body + params[:Body].delete("\u0000") + end + def set_contact contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: params[:From], diff --git a/config/locales/es.yml b/config/locales/es.yml index 5c9455951..141471d45 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -50,7 +50,7 @@ es: dyte: invalid_message_type: "Tipo de mensaje inválido. Acción no permitida" slack: - invalid_channel_id: "Invalid slack channel. Please try again" + invalid_channel_id: "Canal de slack inválido. Por favor, inténtalo de nuevo" inboxes: imap: socket_error: Verifique la conexión de red, la dirección IMAP y vuelva a intentarlo. diff --git a/lib/integrations/slack/channel_builder.rb b/lib/integrations/slack/channel_builder.rb index 062452a4b..1edacf8f7 100644 --- a/lib/integrations/slack/channel_builder.rb +++ b/lib/integrations/slack/channel_builder.rb @@ -24,7 +24,7 @@ class Integrations::Slack::ChannelBuilder end def channels - conversations_list = slack_client.conversations_list(types: 'public_channel, private_channel') + conversations_list = slack_client.conversations_list(types: 'public_channel, private_channel', exclude_archived: true) channel_list = conversations_list.channels while conversations_list.response_metadata.next_cursor.present? conversations_list = slack_client.conversations_list(cursor: conversations_list.response_metadata.next_cursor) diff --git a/spec/services/twilio/incoming_message_service_spec.rb b/spec/services/twilio/incoming_message_service_spec.rb index 62e368ca0..cf9d42874 100644 --- a/spec/services/twilio/incoming_message_service_spec.rb +++ b/spec/services/twilio/incoming_message_service_spec.rb @@ -24,6 +24,19 @@ describe Twilio::IncomingMessageService do expect(conversation.reload.messages.last.content).to eq('testing3') end + it 'removes null bytes' do + params = { + SmsSid: 'SMxx', + From: '+12345', + AccountSid: 'ACxxx', + MessagingServiceSid: twilio_channel.messaging_service_sid, + Body: "remove\u0000 null bytes\u0000" + } + + described_class.new(params: params).perform + expect(conversation.reload.messages.last.content).to eq('remove null bytes') + end + it 'creates a new conversation' do params = { SmsSid: 'SMxx',