diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index af312bf2e..0ec73edc3 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -9,11 +9,11 @@ class Messages::MessageBuilder @user = user @message_type = params[:message_type] || 'outgoing' @attachments = params[:attachments] - @automation_rule = @params&.dig(:content_attributes, :automation_rule_id) + @automation_rule = content_attributes&.dig(:automation_rule_id) return unless params.instance_of?(ActionController::Parameters) - @in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to) - @items = params.to_unsafe_h&.dig(:content_attributes, :items) + @in_reply_to = content_attributes&.dig(:in_reply_to) + @items = content_attributes&.dig(:items) end def perform @@ -26,6 +26,38 @@ class Messages::MessageBuilder private + # Extracts content attributes from the given params. + # - Converts ActionController::Parameters to a regular hash if needed. + # - Attempts to parse a JSON string if content is a string. + # - Returns an empty hash if content is not present, if there's a parsing error, or if it's an unexpected type. + def content_attributes + params = convert_to_hash(@params) + content_attributes = params.fetch(:content_attributes, {}) + + return parse_json(content_attributes) if content_attributes.is_a?(String) + return content_attributes if content_attributes.is_a?(Hash) + + {} + end + + # Converts the given object to a hash. + # If it's an instance of ActionController::Parameters, converts it to an unsafe hash. + # Otherwise, returns the object as-is. + def convert_to_hash(obj) + return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters) + + obj + end + + # Attempts to parse a string as JSON. + # If successful, returns the parsed hash with symbolized names. + # If unsuccessful, returns nil. + def parse_json(content) + JSON.parse(content, symbolize_names: true) + rescue JSON::ParserError + {} + end + def process_attachments return if @attachments.blank? diff --git a/app/javascript/dashboard/api/inbox/message.js b/app/javascript/dashboard/api/inbox/message.js index fc77b24f5..6cb885e3d 100644 --- a/app/javascript/dashboard/api/inbox/message.js +++ b/app/javascript/dashboard/api/inbox/message.js @@ -26,9 +26,13 @@ export const buildCreatePayload = ({ payload.append('echo_id', echoId); payload.append('cc_emails', ccEmails); payload.append('bcc_emails', bccEmails); + if (toEmails) { payload.append('to_emails', toEmails); } + if (contentAttributes) { + payload.append('content_attributes', JSON.stringify(contentAttributes)); + } } else { payload = { content: message, diff --git a/app/javascript/dashboard/api/specs/inbox/message.spec.js b/app/javascript/dashboard/api/specs/inbox/message.spec.js index e75bf34b0..0d45b2157 100644 --- a/app/javascript/dashboard/api/specs/inbox/message.spec.js +++ b/app/javascript/dashboard/api/specs/inbox/message.spec.js @@ -50,6 +50,7 @@ describe('#ConversationAPI', () => { message: 'test content', echoId: 12, isPrivate: true, + contentAttributes: { in_reply_to: 12 }, files: [new Blob(['test-content'], { type: 'application/pdf' })], }); expect(formPayload).toBeInstanceOf(FormData); @@ -57,6 +58,10 @@ describe('#ConversationAPI', () => { expect(formPayload.get('echo_id')).toEqual('12'); expect(formPayload.get('private')).toEqual('true'); expect(formPayload.get('cc_emails')).toEqual(''); + expect(formPayload.get('bcc_emails')).toEqual(''); + expect(formPayload.get('content_attributes')).toEqual( + '{"in_reply_to":12}' + ); }); it('builds object payload if file is not available', () => { diff --git a/app/javascript/dashboard/components/widgets/conversation/Message.vue b/app/javascript/dashboard/components/widgets/conversation/Message.vue index 920d115b5..363003982 100644 --- a/app/javascript/dashboard/components/widgets/conversation/Message.vue +++ b/app/javascript/dashboard/components/widgets/conversation/Message.vue @@ -38,6 +38,7 @@ v-if="inReplyToMessageId && inboxSupportsReplyTo" :message="inReplyTo" :message-type="data.message_type" + :parent-has-attachments="hasAttachments" /> reply_to } if reply_to + + {} + end + def message_params? params[:message].present? end diff --git a/spec/builders/messages/message_builder_spec.rb b/spec/builders/messages/message_builder_spec.rb index 728cb8cf1..891f8eb02 100644 --- a/spec/builders/messages/message_builder_spec.rb +++ b/spec/builders/messages/message_builder_spec.rb @@ -8,6 +8,7 @@ describe Messages::MessageBuilder do let(:inbox) { create(:inbox, account: account) } let(:inbox_member) { create(:inbox_member, inbox: inbox, account: account) } let(:conversation) { create(:conversation, inbox: inbox, account: account) } + let(:message_for_reply) { create(:message, conversation: conversation) } let(:params) do ActionController::Parameters.new({ content: 'test' @@ -21,6 +22,75 @@ describe Messages::MessageBuilder do end end + describe '#content_attributes' do + context 'when content_attributes is a JSON string' do + let(:params) do + ActionController::Parameters.new({ + content: 'test', + content_attributes: "{\"in_reply_to\":#{message_for_reply.id}}" + }) + end + + it 'parses content_attributes from JSON string' do + message = described_class.new(user, conversation, params).perform + expect(message.content_attributes).to include(in_reply_to: message_for_reply.id) + end + end + + context 'when content_attributes is a hash' do + let(:params) do + ActionController::Parameters.new({ + content: 'test', + content_attributes: { in_reply_to: message_for_reply.id } + }) + end + + it 'uses content_attributes as provided' do + message = described_class.new(user, conversation, params).perform + expect(message.content_attributes).to include(in_reply_to: message_for_reply.id) + end + end + + context 'when content_attributes is absent' do + let(:params) do + ActionController::Parameters.new({ content: 'test' }) + end + + it 'defaults to an empty hash' do + message = message_builder + expect(message.content_attributes).to eq({}) + end + end + + context 'when content_attributes is nil' do + let(:params) do + ActionController::Parameters.new({ + content: 'test', + content_attributes: nil + }) + end + + it 'defaults to an empty hash' do + message = message_builder + expect(message.content_attributes).to eq({}) + end + end + + context 'when content_attributes is an invalid JSON string' do + let(:params) do + ActionController::Parameters.new({ + content: 'test', + content_attributes: 'invalid_json' + }) + end + + it 'defaults to an empty hash' do + message = message_builder + expect(message.content_attributes).to eq({}) + end + end + end + describe '#perform when message_type is incoming' do context 'when channel is not api' do let(:params) do diff --git a/spec/models/channel/telegram_spec.rb b/spec/models/channel/telegram_spec.rb index 4fe49f844..0fa5515fb 100644 --- a/spec/models/channel/telegram_spec.rb +++ b/spec/models/channel/telegram_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Channel::Telegram do stub_request(:post, "https://api.telegram.org/bot#{telegram_channel.bot_token}/sendMessage") .with( - body: 'chat_id=123&text=test&reply_markup=' + body: 'chat_id=123&text=test&reply_markup=&reply_to_message_id=' ) .to_return( status: 200, @@ -32,7 +32,7 @@ RSpec.describe Channel::Telegram do .with( body: 'chat_id=123&text=test' \ '&reply_markup=%7B%22one_time_keyboard%22%3Atrue%2C%22inline_keyboard%22%3A%5B%5B%7B%22text%22%3A%22test%22%2C%22' \ - 'callback_data%22%3A%22test%22%7D%5D%5D%7D' + 'callback_data%22%3A%22test%22%7D%5D%5D%7D&reply_to_message_id=' ) .to_return( status: 200,