diff --git a/app/helpers/portal_helper.rb b/app/helpers/portal_helper.rb index 342bb62aa..2b1ab7b21 100644 --- a/app/helpers/portal_helper.rb +++ b/app/helpers/portal_helper.rb @@ -5,8 +5,7 @@ module PortalHelper end def generate_portal_bg(portal_color, theme) - bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg' - "url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}" + generate_portal_bg_color(portal_color, theme) end def generate_gradient_to_bottom(theme) diff --git a/app/javascript/dashboard/components-next/message/bubbles/BaseAttachment.vue b/app/javascript/dashboard/components-next/message/bubbles/BaseAttachment.vue index 832bcd3bc..082f39ec8 100644 --- a/app/javascript/dashboard/components-next/message/bubbles/BaseAttachment.vue +++ b/app/javascript/dashboard/components-next/message/bubbles/BaseAttachment.vue @@ -10,6 +10,7 @@ defineProps({ iconBgColor: { type: String, default: 'bg-n-alpha-3' }, senderTranslationKey: { type: String, required: true }, content: { type: String, required: true }, + title: { type: String, default: '' }, // Title can be any name, description, etc action: { type: Object, required: true, @@ -48,6 +49,9 @@ const senderName = computed(() => { }} +
+ {{ title }} +
{{ content }}
diff --git a/app/javascript/dashboard/components-next/message/bubbles/Contact.vue b/app/javascript/dashboard/components-next/message/bubbles/Contact.vue index 7230a7e05..dda249559 100644 --- a/app/javascript/dashboard/components-next/message/bubbles/Contact.vue +++ b/app/javascript/dashboard/components-next/message/bubbles/Contact.vue @@ -11,7 +11,7 @@ import { ExceptionWithMessage, } from 'shared/helpers/CustomErrors'; -const { content, attachments } = useMessageContext(); +const { attachments } = useMessageContext(); const $store = useStore(); const { t } = useI18n(); @@ -24,6 +24,12 @@ const phoneNumber = computed(() => { return attachment.value.fallbackTitle; }); +const contactName = computed(() => { + const { meta } = attachment.value ?? {}; + const { firstName, lastName } = meta ?? {}; + return `${firstName ?? ''} ${lastName ?? ''}`.trim(); +}); + const formattedPhoneNumber = computed(() => { return phoneNumber.value.replace(/\s|-|[A-Za-z]/g, ''); }); @@ -32,13 +38,9 @@ const rawPhoneNumber = computed(() => { return phoneNumber.value.replace(/\D/g, ''); }); -const name = computed(() => { - return content.value; -}); - function getContactObject() { const contactItem = { - name: name.value, + name: contactName.value, phone_number: `+${rawPhoneNumber.value}`, }; return contactItem; @@ -99,6 +101,7 @@ const action = computed(() => ({ icon="i-teenyicons-user-circle-solid" icon-bg-color="bg-[#D6409F]" sender-translation-key="CONVERSATION.SHARED_ATTACHMENT.CONTACT" + :title="contactName" :content="phoneNumber" :action="formattedPhoneNumber ? action : null" /> diff --git a/app/javascript/dashboard/components/Modal.vue b/app/javascript/dashboard/components/Modal.vue index a1f7ba5ed..653e6cde0 100644 --- a/app/javascript/dashboard/components/Modal.vue +++ b/app/javascript/dashboard/components/Modal.vue @@ -57,7 +57,7 @@ useEventListener(document.body, 'mouseup', onMouseUp); useEventListener(document, 'keydown', onKeydown); onMounted(() => { - if (onClose && typeof onClose === 'function') { + if (import.meta.env.DEV && onClose && typeof onClose === 'function') { // eslint-disable-next-line no-console console.warn( "[DEPRECATED] The 'onClose' prop is deprecated. Please use the 'close' event instead." diff --git a/app/javascript/dashboard/components/buttons/Button.vue b/app/javascript/dashboard/components/buttons/Button.vue index 1ec445890..64b5ecb9a 100644 --- a/app/javascript/dashboard/components/buttons/Button.vue +++ b/app/javascript/dashboard/components/buttons/Button.vue @@ -28,10 +28,12 @@ export default { }, }, created() { - // eslint-disable-next-line - console.warn( - '[DEPRECATED] This component has been deprecated and will be removed soon. Please use v3/components/Form/Button.vue instead' - ); + if (import.meta.env.DEV) { + // eslint-disable-next-line + console.warn( + '[DEPRECATED] This component has been deprecated and will be removed soon. Please use v3/components/Form/Button.vue instead' + ); + } }, }; diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index d3675dbac..7a876aaa6 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -30,7 +30,7 @@ import { import WhatsappTemplates from './WhatsappTemplates/Modal.vue'; import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper'; import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin'; -import { trimContent, debounce } from '@chatwoot/utils'; +import { trimContent, debounce, getRecipients } from '@chatwoot/utils'; import wootConstants from 'dashboard/constants/globals'; import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events'; import fileUploadMixin from 'dashboard/mixins/fileUploadMixin'; @@ -388,7 +388,6 @@ export default { watch: { currentChat(conversation) { const { can_reply: canReply } = conversation; - this.setCCAndToEmailsFromLastChat(); if (this.isOnPrivateNote) { @@ -403,6 +402,19 @@ export default { this.fetchAndSetReplyTo(); }, + // When moving from one conversation to another, the store may not have the + // list of all the messages. A fetch is subsequently made to get the messages. + // However, this update does not trigger the `currentChat` watcher. + // We can add a deep watcher to it, but then, that would be too broad of a net to cast + // And would impact performance too. So we watch the messages directly. + // The watcher here is `deep` too, because the messages array is mutated and + // not replaced. So, a shallow watcher would not catch the change. + 'currentChat.messages': { + handler() { + this.setCCAndToEmailsFromLastChat(); + }, + deep: true, + }, conversationIdByRoute(conversationId, oldConversationId) { if (conversationId !== oldConversationId) { this.setToDraft(oldConversationId, this.replyType); @@ -989,45 +1001,20 @@ export default { this.ccEmails = value.ccEmails; }, setCCAndToEmailsFromLastChat() { - if (!this.lastEmail) return; - - const { - content_attributes: { email: emailAttributes = {} }, - } = this.lastEmail; - - // Retrieve the email of the current conversation's sender const conversationContact = this.currentChat?.meta?.sender?.email || ''; - let cc = emailAttributes.cc ? [...emailAttributes.cc] : []; - let to = []; + const { email: inboxEmail, forward_to_email: forwardToEmail } = + this.inbox; - // there might be a situation where the current conversation will include a message from a third person, - // and the current conversation contact is in CC. - // This is an edge-case, reported here: CW-1511 [ONLY FOR INTERNAL REFERENCE] - // So we remove the current conversation contact's email from the CC list if present - if (cc.includes(conversationContact)) { - cc = cc.filter(email => email !== conversationContact); - } - - // If the last incoming message sender is different from the conversation contact, add them to the "to" - // and add the conversation contact to the CC - if (!emailAttributes.from.includes(conversationContact)) { - to.push(...emailAttributes.from); - cc.push(conversationContact); - } - - // Remove the conversation contact's email from the BCC list if present - let bcc = (emailAttributes.bcc || []).filter( - email => email !== conversationContact + const { cc, bcc, to } = getRecipients( + this.lastEmail, + conversationContact, + inboxEmail, + forwardToEmail ); - // Ensure only unique email addresses are in the CC list - bcc = [...new Set(bcc)]; - cc = [...new Set(cc)]; - to = [...new Set(to)]; - + this.toEmails = to.join(', '); this.ccEmails = cc.join(', '); this.bccEmails = bcc.join(', '); - this.toEmails = to.join(', '); }, fetchAndSetReplyTo() { const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO; diff --git a/app/javascript/dashboard/components/widgets/forms/Input.vue b/app/javascript/dashboard/components/widgets/forms/Input.vue index b34d81259..bd6d49470 100644 --- a/app/javascript/dashboard/components/widgets/forms/Input.vue +++ b/app/javascript/dashboard/components/widgets/forms/Input.vue @@ -40,10 +40,12 @@ export default { }, emits: ['update:modelValue', 'input', 'blur'], mounted() { - // eslint-disable-next-line - console.warn( - '[DEPRECATED] has be deprecated and will be removed soon. Please use v3/components/Form/Input.vue instead' - ); + if (import.meta.env.DEV) { + // eslint-disable-next-line no-console + console.warn( + '[DEPRECATED] has be deprecated and will be removed soon. Please use v3/components/Form/Input.vue instead' + ); + } }, methods: { onChange(e) { diff --git a/app/javascript/dashboard/helper/AudioAlerts/DashboardAudioNotificationHelper.js b/app/javascript/dashboard/helper/AudioAlerts/DashboardAudioNotificationHelper.js index 21cb0fc9f..9e79bb3a2 100644 --- a/app/javascript/dashboard/helper/AudioAlerts/DashboardAudioNotificationHelper.js +++ b/app/javascript/dashboard/helper/AudioAlerts/DashboardAudioNotificationHelper.js @@ -152,7 +152,10 @@ export class DashboardAudioNotificationHelper { const shouldPlayAudio = []; - if (audioAlertType.includes(EVENT_TYPES.ASSIGNED)) { + if ( + audioAlertType.includes(EVENT_TYPES.ASSIGNED) || + audioAlertType.includes('mine') + ) { shouldPlayAudio.push(assignedToMe); } if (audioAlertType.includes(EVENT_TYPES.UNASSIGNED)) { diff --git a/app/javascript/dashboard/store/modules/conversations/getters.js b/app/javascript/dashboard/store/modules/conversations/getters.js index 085766bfd..3695de6a1 100644 --- a/app/javascript/dashboard/store/modules/conversations/getters.js +++ b/app/javascript/dashboard/store/modules/conversations/getters.js @@ -27,18 +27,12 @@ const getters = { const selectedChat = _getters.getSelectedChat; const { messages = [] } = selectedChat; const lastEmail = [...messages].reverse().find(message => { - const { - content_attributes: contentAttributes = {}, - message_type: messageType, - } = message; - const { email = {} } = contentAttributes; - const isIncomingOrOutgoing = - messageType === MESSAGE_TYPE.OUTGOING || - messageType === MESSAGE_TYPE.INCOMING; - if (email.from && isIncomingOrOutgoing) { - return true; - } - return false; + const { message_type: messageType } = message; + if (message.private) return false; + + return [MESSAGE_TYPE.OUTGOING, MESSAGE_TYPE.INCOMING].includes( + messageType + ); }); return lastEmail; diff --git a/app/javascript/v3/components/Button/SubmitButton.vue b/app/javascript/v3/components/Button/SubmitButton.vue index 24c04acbb..88dad0799 100644 --- a/app/javascript/v3/components/Button/SubmitButton.vue +++ b/app/javascript/v3/components/Button/SubmitButton.vue @@ -40,10 +40,12 @@ export default { }, }, created() { - // eslint-disable-next-line - console.warn( - '[DEPRECATED] This component has been deprecated and will be removed soon. Please use v3/components/Form/Button.vue instead' - ); + if (import.meta.env.DEV) { + // eslint-disable-next-line + console.warn( + '[DEPRECATED] This component has been deprecated and will be removed soon. Please use v3/components/Form/Button.vue instead' + ); + } }, }; diff --git a/app/jobs/action_cable_broadcast_job.rb b/app/jobs/action_cable_broadcast_job.rb index b1208a97c..74dab618d 100644 --- a/app/jobs/action_cable_broadcast_job.rb +++ b/app/jobs/action_cable_broadcast_job.rb @@ -1,9 +1,44 @@ class ActionCableBroadcastJob < ApplicationJob queue_as :critical + include Events::Types + + CONVERSATION_UPDATE_EVENTS = [ + CONVERSATION_READ, + CONVERSATION_UPDATED, + TEAM_CHANGED, + ASSIGNEE_CHANGED, + CONVERSATION_STATUS_CHANGED + ].freeze def perform(members, event_name, data) + return if members.blank? + + broadcast_data = prepare_broadcast_data(event_name, data) + broadcast_to_members(members, event_name, broadcast_data) + end + + private + + # Ensures that only the latest available data is sent to prevent UI issues + # caused by out-of-order events during high-traffic periods. This prevents + # the conversation job from processing outdated data. + def prepare_broadcast_data(event_name, data) + return data unless CONVERSATION_UPDATE_EVENTS.include?(event_name) + + account = Account.find(data[:account_id]) + conversation = account.conversations.find_by!(display_id: data[:id]) + conversation.push_event_data.merge(account_id: data[:account_id]) + end + + def broadcast_to_members(members, event_name, broadcast_data) members.each do |member| - ActionCable.server.broadcast(member, { event: event_name, data: data }) + ActionCable.server.broadcast( + member, + { + event: event_name, + data: broadcast_data + } + ) end end end diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 65e56a21f..0bfd9a978 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -9,6 +9,7 @@ # external_url :string # fallback_title :string # file_type :integer default("image") +# meta :jsonb # created_at :datetime not null # updated_at :datetime not null # account_id :integer not null @@ -116,7 +117,8 @@ class Attachment < ApplicationRecord def contact_metadata { - fallback_title: fallback_title + fallback_title: fallback_title, + meta: meta || {} } end diff --git a/app/presenters/conversations/event_data_presenter.rb b/app/presenters/conversations/event_data_presenter.rb index 1ba188de8..2ef69080d 100644 --- a/app/presenters/conversations/event_data_presenter.rb +++ b/app/presenters/conversations/event_data_presenter.rb @@ -43,7 +43,7 @@ class Conversations::EventDataPresenter < SimpleDelegator last_activity_at: last_activity_at.to_i, timestamp: last_activity_at.to_i, created_at: created_at.to_i, - updated_at: updated_at.to_i + updated_at: updated_at.to_f } end end diff --git a/app/services/telegram/incoming_message_service.rb b/app/services/telegram/incoming_message_service.rb index d39005030..e337a03f6 100644 --- a/app/services/telegram/incoming_message_service.rb +++ b/app/services/telegram/incoming_message_service.rb @@ -143,7 +143,11 @@ class Telegram::IncomingMessageService @message.attachments.new( account_id: @message.account_id, file_type: :contact, - fallback_title: contact_card['phone_number'].to_s + fallback_title: contact_card['phone_number'].to_s, + meta: { + first_name: contact_card['first_name'], + last_name: contact_card['last_name'] + } ) end diff --git a/app/views/api/v1/conversations/partials/_conversation.json.jbuilder b/app/views/api/v1/conversations/partials/_conversation.json.jbuilder index 2e6474953..8867ba695 100644 --- a/app/views/api/v1/conversations/partials/_conversation.json.jbuilder +++ b/app/views/api/v1/conversations/partials/_conversation.json.jbuilder @@ -44,7 +44,7 @@ json.muted conversation.muted? json.snoozed_until conversation.snoozed_until json.status conversation.status json.created_at conversation.created_at.to_i -json.updated_at conversation.updated_at.to_i +json.updated_at conversation.updated_at.to_f json.timestamp conversation.last_activity_at.to_i json.first_reply_created_at conversation.first_reply_created_at.to_i json.unread_count conversation.unread_incoming_messages.count diff --git a/db/migrate/20250207040150_add_meta_to_attachment.rb b/db/migrate/20250207040150_add_meta_to_attachment.rb new file mode 100644 index 000000000..32188b4d7 --- /dev/null +++ b/db/migrate/20250207040150_add_meta_to_attachment.rb @@ -0,0 +1,5 @@ +class AddMetaToAttachment < ActiveRecord::Migration[7.0] + def change + add_column :attachments, :meta, :jsonb, default: {} + end +end diff --git a/db/schema.rb b/db/schema.rb index 17e5c46da..6bea44c8b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2025_01_16_061033) do +ActiveRecord::Schema[7.0].define(version: 2025_02_07_040150) do # These extensions should be enabled to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -174,6 +174,7 @@ ActiveRecord::Schema[7.0].define(version: 2025_01_16_061033) do t.datetime "updated_at", precision: nil, null: false t.string "fallback_title" t.string "extension" + t.jsonb "meta", default: {} t.index ["account_id"], name: "index_attachments_on_account_id" t.index ["message_id"], name: "index_attachments_on_message_id" end diff --git a/package.json b/package.json index b3fbda8f0..aefa5439d 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@breezystack/lamejs": "^1.2.7", "@chatwoot/ninja-keys": "1.2.3", "@chatwoot/prosemirror-schema": "1.1.1-next", - "@chatwoot/utils": "^0.0.35", + "@chatwoot/utils": "^0.0.38", "@formkit/core": "^1.6.7", "@formkit/vue": "^1.6.7", "@hcaptcha/vue3-hcaptcha": "^1.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20142653e..6bbf22593 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,8 +23,8 @@ importers: specifier: 1.1.1-next version: 1.1.1-next '@chatwoot/utils': - specifier: ^0.0.35 - version: 0.0.35 + specifier: ^0.0.38 + version: 0.0.38 '@formkit/core': specifier: ^1.6.7 version: 1.6.7 @@ -376,6 +376,10 @@ packages: resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.26.7': + resolution: {integrity: sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==} + engines: {node: '>=6.9.0'} + '@babel/types@7.26.0': resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} engines: {node: '>=6.9.0'} @@ -393,8 +397,8 @@ packages: '@chatwoot/prosemirror-schema@1.1.1-next': resolution: {integrity: sha512-/M2qZ+ZF7GlQNt1riwVP499fvp3hxSqd5iy8hxyF9pkj9qQ+OKYn5JK+v3qwwqQY3IxhmNOn1Lp6tm7vstrd9Q==} - '@chatwoot/utils@0.0.35': - resolution: {integrity: sha512-uSRbd3pFp+IcEhsRtK1XGcFGFJc+X/YIwQnQrVDXsvsX3Mm7HEANj+Yz6J2clfHotajniwJwH2u5/y48+JrTyA==} + '@chatwoot/utils@0.0.38': + resolution: {integrity: sha512-6CTvuueBQLZJcm++pI2ZBY8Pp7OP3WzPCYyXoCagl8ZLpOfpjyVkLx9fc81falOoaVa/r+7EZ85Cv7vkT0ZyQw==} engines: {node: '>=10'} '@codemirror/commands@6.7.0': @@ -2372,8 +2376,8 @@ packages: resolution: {integrity: sha512-m1WR0xGiC6j6jNFAyW4Nvh4WxAi4JF4w9jRJwSI8nBmNcyZXPcP9VUQG+6gHQXAmqaGEKDKhOqAtENDC941UkA==} engines: {node: '>=0.11'} - date-fns@2.29.3: - resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==} + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} date-format-parse@0.2.7: @@ -5092,6 +5096,10 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 + '@babel/runtime@7.26.7': + dependencies: + regenerator-runtime: 0.14.1 + '@babel/types@7.26.0': dependencies: '@babel/helper-string-parser': 7.25.9 @@ -5125,9 +5133,9 @@ snapshots: prosemirror-utils: 1.2.2(prosemirror-model@1.22.3)(prosemirror-state@1.4.3) prosemirror-view: 1.34.1 - '@chatwoot/utils@0.0.35': + '@chatwoot/utils@0.0.38': dependencies: - date-fns: 2.29.3 + date-fns: 2.30.0 '@codemirror/commands@6.7.0': dependencies: @@ -7380,7 +7388,9 @@ snapshots: date-fns@2.21.1: {} - date-fns@2.29.3: {} + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.26.7 date-format-parse@0.2.7: {} diff --git a/spec/helpers/portal_helper_spec.rb b/spec/helpers/portal_helper_spec.rb index 84ed4ba72..dfd37138a 100644 --- a/spec/helpers/portal_helper_spec.rb +++ b/spec/helpers/portal_helper_spec.rb @@ -33,26 +33,26 @@ describe PortalHelper do describe '#generate_portal_bg' do context 'when theme is dark' do it 'returns the correct background with dark grid image and color mix with black' do - expected_bg = 'url(/assets/images/hc/hexagon-dark.svg) color-mix(in srgb, #ff0000 20%, black)' + expected_bg = 'color-mix(in srgb, #ff0000 20%, black)' expect(helper.generate_portal_bg('#ff0000', 'dark')).to eq(expected_bg) end end context 'when theme is not dark' do it 'returns the correct background with light grid image and color mix with white' do - expected_bg = 'url(/assets/images/hc/hexagon-light.svg) color-mix(in srgb, #ff0000 20%, white)' + expected_bg = 'color-mix(in srgb, #ff0000 20%, white)' expect(helper.generate_portal_bg('#ff0000', 'light')).to eq(expected_bg) end end context 'when provided with various colors' do it 'adjusts the background appropriately for dark theme' do - expected_bg = 'url(/assets/images/hc/hexagon-dark.svg) color-mix(in srgb, #00ff00 20%, black)' + expected_bg = 'color-mix(in srgb, #00ff00 20%, black)' expect(helper.generate_portal_bg('#00ff00', 'dark')).to eq(expected_bg) end it 'adjusts the background appropriately for light theme' do - expected_bg = 'url(/assets/images/hc/hexagon-light.svg) color-mix(in srgb, #0000ff 20%, white)' + expected_bg = 'color-mix(in srgb, #0000ff 20%, white)' expect(helper.generate_portal_bg('#0000ff', 'light')).to eq(expected_bg) end end diff --git a/spec/models/attachment_spec.rb b/spec/models/attachment_spec.rb index b1d19bd0e..241125538 100644 --- a/spec/models/attachment_spec.rb +++ b/spec/models/attachment_spec.rb @@ -67,4 +67,65 @@ RSpec.describe Attachment do expect(message.attachments.first.push_event_data[:data_url]).not_to eq message.attachments.first.external_url end end + + describe 'meta data handling' do + let(:message) { create(:message) } + + context 'when attachment is a contact type' do + let(:contact_attachment) do + message.attachments.create!( + account_id: message.account_id, + file_type: :contact, + fallback_title: '+1234567890', + meta: { + first_name: 'John', + last_name: 'Doe' + } + ) + end + + it 'stores and retrieves meta data correctly' do + expect(contact_attachment.meta['first_name']).to eq('John') + expect(contact_attachment.meta['last_name']).to eq('Doe') + end + + it 'includes meta data in push_event_data' do + event_data = contact_attachment.push_event_data + expect(event_data[:meta]).to eq({ + 'first_name' => 'John', + 'last_name' => 'Doe' + }) + end + + it 'returns empty hash for meta if not set' do + attachment = message.attachments.create!( + account_id: message.account_id, + file_type: :contact, + fallback_title: '+1234567890' + ) + expect(attachment.push_event_data[:meta]).to eq({}) + end + end + + context 'when meta is used with other file types' do + let(:image_attachment) do + attachment = message.attachments.new( + account_id: message.account_id, + file_type: :image, + meta: { description: 'Test image' } + ) + attachment.file.attach( + io: Rails.root.join('spec/assets/avatar.png').open, + filename: 'avatar.png', + content_type: 'image/png' + ) + attachment.save! + attachment + end + + it 'preserves meta data with file attachments' do + expect(image_attachment.meta['description']).to eq('Test image') + end + end + end end diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index 0c8f9de50..1938fb204 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -538,7 +538,7 @@ RSpec.describe Conversation do contact_last_seen_at: conversation.contact_last_seen_at.to_i, agent_last_seen_at: conversation.agent_last_seen_at.to_i, created_at: conversation.created_at.to_i, - updated_at: conversation.updated_at.to_i, + updated_at: conversation.updated_at.to_f, waiting_since: conversation.waiting_since.to_i, priority: nil, unread_count: 0 diff --git a/spec/presenters/conversations/event_data_presenter_spec.rb b/spec/presenters/conversations/event_data_presenter_spec.rb index 6f2534efb..a645caf1d 100644 --- a/spec/presenters/conversations/event_data_presenter_spec.rb +++ b/spec/presenters/conversations/event_data_presenter_spec.rb @@ -31,7 +31,7 @@ RSpec.describe Conversations::EventDataPresenter do contact_last_seen_at: conversation.contact_last_seen_at.to_i, agent_last_seen_at: conversation.agent_last_seen_at.to_i, created_at: conversation.created_at.to_i, - updated_at: conversation.updated_at.to_i, + updated_at: conversation.updated_at.to_f, waiting_since: conversation.waiting_since.to_i, priority: nil, unread_count: 0